From 6598927af2c830047acc63c07b748c032cfe1881 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 29 May 2026 10:56:27 -0700 Subject: [PATCH 01/14] feat(tables): pinned columns (#4770) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(tables): freeze columns * fix(tables): sticky meta-header row for frozen workflow groups, remove dead handleChangeType * fix(tables): scope frozenOffsets dep to frozen column widths only * fix(tables): restore frozenColumns on delete-column undo/redo * fix(tables): restore useMemo for isAllRowsSelected (O(n) computation) * fix(tables): use current frozenColumns on delete-column redo, not stale snapshot * fix(tables): clean up frozenColumns on create-column undo * fix(tables): merge frozen state on delete-column undo instead of overwriting * fix(tables): add previousFrozenColumns to test fixture for delete-column action * fix(tables): skip frozen state update on delete-column redo when column was not frozen * refactor(tables): rename frozen columns to pinned, fix sticky-zone UX - rename frozenColumns → pinnedColumns across types, contract, undo actions, grid state/refs/props, and dropdown labels - add Pin / PinOff emcn icons; use them in the column menu in place of Lock / Unlock - pinned body cells render at z-[6], above the cell selection border (z-[5]), so the blue selection border can't draw on top of the sticky-left zone - restrict column drag-reorder to within the pinned or unpinned zone in both handleColumnDragOver and handleScrollDragOver; cross-zone drop indicators are suppressed - on unpin, slide the column to the first unpinned slot so the sticky zone stays contiguous; consolidates pin and unpin into one branch that always re-enforces pinned-at-front Co-Authored-By: Claude Opus 4.7 (1M context) * fix(tables): biome formatting + tighten pinned-zone comments - collapse two onPinToggle JSX props that biome wanted on a single line - drop a WHAT comment in handleScrollDragOver; tighten the why-comments in handlePinToggle, handleColumnDragOver, and handleColumnDragEnd so they describe the invariant being protected instead of narrating the recent change Co-Authored-By: Claude Opus 4.7 (1M context) * fix(tables): re-sort reorder-columns undo to keep pinned-at-front If the user reordered, then pinned a column, then undid the reorder, the restored snapshot could leave a currently-pinned column in the middle of columnOrder. pinnedOffsets walks displayColumns left→right and assigns sticky `left` from checkboxColWidth — a pinned column in the middle gets a sticky offset as if it were at the front, causing it to jump over its left neighbors on horizontal scroll. Re-sort the restored order with pinned entries pulled to the front before applying. Mirrors the belt-and-suspenders re-sort in handleColumnDragEnd. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Theodore Li Co-authored-by: Claude Opus 4.7 (1M context) --- .../components/table-grid/data-row.tsx | 22 +- .../table-grid/headers/column-header-menu.tsx | 26 +- .../headers/workflow-group-meta-cell.tsx | 191 ++++---- .../components/table-grid/table-grid.tsx | 425 ++++++++++++------ apps/sim/components/emcn/icons/index.ts | 2 + apps/sim/components/emcn/icons/pin-off.tsx | 28 ++ apps/sim/components/emcn/icons/pin.tsx | 26 ++ apps/sim/hooks/use-table-undo.test.ts | 1 + apps/sim/hooks/use-table-undo.ts | 65 ++- apps/sim/lib/api/contracts/tables.ts | 1 + apps/sim/lib/table/types.ts | 6 +- apps/sim/stores/table/types.ts | 1 + 12 files changed, 568 insertions(+), 226 deletions(-) create mode 100644 apps/sim/components/emcn/icons/pin-off.tsx create mode 100644 apps/sim/components/emcn/icons/pin.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx index e228edba84d..3bc1d465774 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx @@ -57,6 +57,10 @@ export interface DataRowProps { * queued indicators across page refresh during long Run-all dispatches. */ activeDispatches: ActiveDispatch[] | undefined + /** Pixel `left` value for each pinned column key; absent keys are not pinned. */ + pinnedOffsets?: Map + /** Key of the rightmost pinned column, used to render a separator shadow. */ + lastPinnedColKey?: string | null } function cellRangeRowChanged( @@ -113,7 +117,9 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean { prev.onStopRow !== next.onStopRow || prev.onRunRow !== next.onRunRow || prev.workflowGroups !== next.workflowGroups || - prev.activeDispatches !== next.activeDispatches + prev.activeDispatches !== next.activeDispatches || + prev.pinnedOffsets !== next.pinnedOffsets || + prev.lastPinnedColKey !== next.lastPinnedColKey ) { return false } @@ -157,6 +163,8 @@ export const DataRow = React.memo(function DataRow({ onRunRow, workflowGroups, activeDispatches, + pinnedOffsets, + lastPinnedColKey, }: DataRowProps) { const sel = normalizedSelection /** @@ -264,13 +272,23 @@ export const DataRow = React.memo(function DataRow({ const isLeftEdge = inRange ? colIndex === sel!.startCol : colIndex === 0 const isRightEdge = inRange ? colIndex === sel!.endCol : colIndex === columns.length - 1 + const pinnedLeft = pinnedOffsets?.get(column.key) + const isPinnedCell = pinnedLeft !== undefined + const isPinnedSeparator = column.key === lastPinnedColKey + return ( { if (e.button !== 0 || isEditing) return onCellMouseDown(rowIndex, colIndex, e.shiftKey) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx index 13010ad3179..7a76d6ee9be 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx @@ -1,9 +1,9 @@ 'use client' import React, { useCallback, useEffect, useRef, useState } from 'react' -import { ChevronDown } from 'lucide-react' +import { ChevronDown } from '@/components/emcn/icons' import { cn } from '@/lib/core/utils/cn' -import type { ColumnDefinition, WorkflowGroup } from '@/lib/table' +import type { WorkflowGroup } from '@/lib/table' import type { WorkflowMetadata } from '@/stores/workflows/registry/types' import { COL_WIDTH, SELECTION_TINT_BG } from '../constants' import type { ColumnSourceInfo, DisplayColumn } from '../types' @@ -21,7 +21,6 @@ interface ColumnHeaderMenuProps { onRenameSubmit: () => void onRenameCancel: () => void onColumnSelect: (colIndex: number, shiftKey: boolean) => void - onChangeType: (columnName: string, newType: ColumnDefinition['type']) => void onInsertLeft: (columnName: string) => void onInsertRight: (columnName: string) => void onDeleteColumn: (columnName: string) => void @@ -42,6 +41,14 @@ interface ColumnHeaderMenuProps { /** Opens a popup preview of the column's underlying workflow. Surfaced in * the chevron menu for workflow-output columns. */ onViewWorkflow?: (workflowId: string) => void + /** Whether this column is currently pinned to the left. */ + isPinned?: boolean + /** Toggle the pinned state for this column. */ + onPinToggle?: (columnName: string) => void + /** Left offset in pixels when pinned (drives `position: sticky`). */ + stickyLeft?: number + /** Whether this is the rightmost pinned column (renders a separator shadow). */ + isLastPinned?: boolean } /** @@ -76,6 +83,10 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({ sourceInfo, onOpenConfig, onViewWorkflow, + isPinned, + onPinToggle, + stickyLeft, + isLastPinned, }: ColumnHeaderMenuProps) { const renameInputRef = useRef(null) const didDragRef = useRef(false) @@ -228,7 +239,12 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({ return ( onViewWorkflow(ownGroup.workflowId) : undefined } + isPinned={isPinned} + onPinToggle={onPinToggle} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx index 211c3e0a55a..56468fb1f61 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx @@ -1,7 +1,7 @@ 'use client' import type React from 'react' -import { useCallback, useRef, useState } from 'react' +import { useRef, useState } from 'react' import { DropdownMenu, DropdownMenuContent, @@ -18,6 +18,8 @@ import { Eye, EyeOff, Pencil, + Pin, + PinOff, PlayOutline, Trash, } from '@/components/emcn/icons' @@ -67,6 +69,10 @@ interface ColumnOptionsMenuProps { /** When set, the menu surfaces a "View workflow" item that opens a popup * preview of the configured workflow. */ onViewWorkflow?: () => void + /** Whether this column is currently pinned to the left. */ + isPinned?: boolean + /** Toggle the pinned state of this column. */ + onPinToggle?: (columnName: string) => void } /** @@ -93,6 +99,8 @@ export function ColumnOptionsMenu({ onRunColumnSelected, selectedRowCount = 0, onViewWorkflow, + isPinned, + onPinToggle, }: ColumnOptionsMenuProps) { const showRunActions = Boolean(onRunColumnAll && onRunColumnIncomplete) const showRunSelected = Boolean(onRunColumnSelected) && selectedRowCount > 0 @@ -159,6 +167,12 @@ export function ColumnOptionsMenu({ Edit column + {onPinToggle && ( + onPinToggle(column.name)}> + {isPinned ? : } + {isPinned ? 'Unpin column' : 'Pin column'} + + )} onInsertLeft(column.name)}> @@ -219,6 +233,14 @@ interface WorkflowGroupMetaCellProps { onDragEnd?: () => void onDragLeave?: () => void readOnly?: boolean + /** Left offset in pixels when pinned (drives `position: sticky`). */ + stickyLeft?: number + /** Whether this is the rightmost pinned column group (renders a separator shadow). */ + isLastPinned?: boolean + /** Whether this column group is currently pinned to the left. */ + isPinned?: boolean + /** Toggle the pinned state for this column group. */ + onPinToggle?: (columnName: string) => void } /** @@ -252,6 +274,10 @@ export function WorkflowGroupMetaCell({ onDragEnd, onDragLeave, readOnly, + stickyLeft, + isLastPinned, + isPinned, + onPinToggle, }: WorkflowGroupMetaCellProps) { const isEnrichment = groupType === 'enrichment' const enrichment = isEnrichment ? getEnrichment(enrichmentId) : undefined @@ -269,112 +295,94 @@ export function WorkflowGroupMetaCell({ const selectedCount = selectedRowIds?.length ?? 0 - const handleRunAll = useCallback(() => { + function handleRunAll() { if (groupId) onRunColumn?.(groupId, 'all') - }, [groupId, onRunColumn]) + } - const handleRunIncomplete = useCallback(() => { + function handleRunIncomplete() { if (groupId) onRunColumn?.(groupId, 'incomplete') - }, [groupId, onRunColumn]) + } - const handleRunSelected = useCallback(() => { + function handleRunSelected() { if (groupId && selectedRowIds && selectedRowIds.length > 0) { onRunColumn?.(groupId, 'all', selectedRowIds) } - }, [groupId, onRunColumn, selectedRowIds]) + } - const handleRunLimited = useCallback( - (max: number) => { - if (groupId) onRunColumn?.(groupId, 'incomplete', undefined, { type: 'rows', max }) - }, - [groupId, onRunColumn] - ) + function handleRunLimited(max: number) { + if (groupId) onRunColumn?.(groupId, 'incomplete', undefined, { type: 'rows', max }) + } - const handleContextMenu = useCallback( - (e: React.MouseEvent) => { - if (!column) return - e.preventDefault() - e.stopPropagation() - setOptionsMenuPosition({ x: e.clientX, y: e.clientY }) - setOptionsMenuOpen(true) - }, - [column] - ) + function handleContextMenu(e: React.MouseEvent) { + if (!column) return + e.preventDefault() + e.stopPropagation() + setOptionsMenuPosition({ x: e.clientX, y: e.clientY }) + setOptionsMenuOpen(true) + } - const selectGroupAndOpenConfig = useCallback( - (e: React.MouseEvent) => { - // Ignore clicks that landed on an interactive child (badge, play button, - // dropdown items rendered via portal). Only the bare meta-cell area - // should select the group + open the config sidebar. - const target = e.target as HTMLElement - if (target.closest('button, [role="menuitem"], [role="menu"]')) return - // Drag-vs-click guard: when a drag just ended on this cell, swallow the - // synthetic click so we don't accidentally pop open the sidebar. - if (didDragRef.current) { - didDragRef.current = false - return - } - onSelectGroup(startColIndex, size) - if (columnName) onOpenConfig(columnName) - }, - [columnName, onOpenConfig, onSelectGroup, size, startColIndex] - ) + function selectGroupAndOpenConfig(e: React.MouseEvent) { + // Ignore clicks that landed on an interactive child (badge, play button, + // dropdown items rendered via portal). Only the bare meta-cell area + // should select the group + open the config sidebar. + const target = e.target as HTMLElement + if (target.closest('button, [role="menuitem"], [role="menu"]')) return + // Drag-vs-click guard: when a drag just ended on this cell, swallow the + // synthetic click so we don't accidentally pop open the sidebar. + if (didDragRef.current) { + didDragRef.current = false + return + } + onSelectGroup(startColIndex, size) + if (columnName) onOpenConfig(columnName) + } - const handleDragStart = useCallback( - (e: React.DragEvent) => { - if (readOnly || !onDragStart || !columnName) { - e.preventDefault() - return - } - didDragRef.current = true - e.dataTransfer.effectAllowed = 'move' - e.dataTransfer.setData('text/plain', columnName) + function handleDragStart(e: React.DragEvent) { + if (readOnly || !onDragStart || !columnName) { + e.preventDefault() + return + } + didDragRef.current = true + e.dataTransfer.effectAllowed = 'move' + e.dataTransfer.setData('text/plain', columnName) - const ghost = document.createElement('div') - ghost.textContent = name - ghost.style.cssText = - 'position:absolute;top:-9999px;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:13px;font-weight:500;white-space:nowrap;color:var(--text-primary)' - document.body.appendChild(ghost) - e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2) - requestAnimationFrame(() => ghost.parentNode?.removeChild(ghost)) + const ghost = document.createElement('div') + ghost.textContent = name + ghost.style.cssText = + 'position:absolute;top:-9999px;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;font-size:13px;font-weight:500;white-space:nowrap;color:var(--text-primary)' + document.body.appendChild(ghost) + e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2) + requestAnimationFrame(() => ghost.parentNode?.removeChild(ghost)) - onDragStart(columnName) - }, - [columnName, name, onDragStart, readOnly] - ) + onDragStart(columnName) + } - const handleDragOver = useCallback( - (e: React.DragEvent) => { - if (!onDragOver || !columnName) return - e.preventDefault() - e.dataTransfer.dropEffect = 'move' - const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() - const midX = rect.left + rect.width / 2 - const side = e.clientX < midX ? 'left' : 'right' - onDragOver(columnName, side) - }, - [columnName, onDragOver] - ) + function handleDragOver(e: React.DragEvent) { + if (!onDragOver || !columnName) return + e.preventDefault() + e.dataTransfer.dropEffect = 'move' + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() + const midX = rect.left + rect.width / 2 + const side = e.clientX < midX ? 'left' : 'right' + onDragOver(columnName, side) + } - const handleDragEnd = useCallback(() => { + function handleDragEnd() { didDragRef.current = false onDragEnd?.() - }, [onDragEnd]) + } - const handleDragLeave = useCallback( - (e: React.DragEvent) => { - const th = e.currentTarget as HTMLElement - const related = e.relatedTarget as Node | null - if (related && th.contains(related)) return - if (related && related instanceof Element && related.closest('th')) return - onDragLeave?.() - }, - [onDragLeave] - ) + function handleDragLeave(e: React.DragEvent) { + const th = e.currentTarget as HTMLElement + const related = e.relatedTarget as Node | null + if (related && th.contains(related)) return + if (related && related instanceof Element && related.closest('th')) return + onDragLeave?.() + } - const handleDrop = useCallback((e: React.DragEvent) => { + function handleDrop(e: React.DragEvent) { e.preventDefault() - }, []) + } const isDraggable = !readOnly && Boolean(onDragStart) @@ -389,7 +397,12 @@ export function WorkflowGroupMetaCell({ onDragEnd={isDraggable ? handleDragEnd : undefined} onDragLeave={isDraggable ? handleDragLeave : undefined} onDrop={isDraggable ? handleDrop : undefined} - className='group relative cursor-pointer border-[var(--border)] border-r border-b bg-[var(--bg)] px-2 py-[5px] text-left align-middle before:pointer-events-none before:absolute before:top-0 before:bottom-0 before:left-[-1px] before:w-px before:bg-[var(--border)] before:content-[""]' + className={cn( + 'group relative cursor-pointer border-[var(--border)] border-r border-b bg-[var(--bg)] px-2 py-[5px] text-left align-middle before:pointer-events-none before:absolute before:top-0 before:bottom-0 before:left-[-1px] before:w-px before:bg-[var(--border)] before:content-[""]', + stickyLeft !== undefined && 'z-[11]', + isLastPinned && '[box-shadow:2px_0_0_0_var(--border)]' + )} + style={stickyLeft !== undefined ? { position: 'sticky', left: stickyLeft } : undefined} >
0 ? handleRunSelected : undefined} selectedRowCount={selectedCount} onViewWorkflow={onViewWorkflow ? () => onViewWorkflow(workflowId) : undefined} + isPinned={isPinned} + onPinToggle={onPinToggle} /> )} 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 d75b63c9ebb..b060d9f2734 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 @@ -302,6 +302,9 @@ export function TableGrid({ const [dropSide, setDropSide] = useState<'left' | 'right'>('left') const dropSideRef = useRef(dropSide) dropSideRef.current = dropSide + const [pinnedColumns, setPinnedColumns] = useState([]) + const pinnedColumnsRef = useRef(pinnedColumns) + pinnedColumnsRef.current = pinnedColumns const metadataSeededRef = useRef(false) const containerRef = useRef(null) const scrollRef = useRef(null) @@ -466,9 +469,13 @@ export function TableGrid({ } const updatedOrder = columnOrderRef.current?.map((n) => (n === oldName ? newName : n)) if (updatedOrder) setColumnOrder(updatedOrder) + const updatedPinned = pinnedColumnsRef.current.map((n) => (n === oldName ? newName : n)) + const pinnedChanged = updatedPinned.some((n, i) => n !== pinnedColumnsRef.current[i]) + if (pinnedChanged) setPinnedColumns(updatedPinned) updateMetadataRef.current({ columnWidths: updatedWidths, ...(updatedOrder ? { columnOrder: updatedOrder } : {}), + ...(pinnedChanged ? { pinnedColumns: updatedPinned } : {}), }) } // Populate the wrapper's sink so its sidebars can fire renames back into @@ -483,12 +490,61 @@ export function TableGrid({ setColumnWidths(widths) } + function handlePinnedColumnsChange(pinned: string[]) { + setPinnedColumns(pinned) + pinnedColumnsRef.current = pinned + } + + function getPinnedColumns() { + return pinnedColumnsRef.current + } + + const handlePinToggle = useCallback((columnName: string) => { + const col = columnsRef.current.find((c) => c.name === columnName) + const siblings: string[] = col?.workflowGroupId + ? columnsRef.current + .filter((c) => c.workflowGroupId === col.workflowGroupId) + .map((c) => c.name) + : [columnName] + + const current = pinnedColumnsRef.current + const newPinned = current.includes(columnName) + ? current.filter((n) => !siblings.includes(n)) + : [...current, ...siblings.filter((n) => !current.includes(n))] + setPinnedColumns(newPinned) + pinnedColumnsRef.current = newPinned + + // Pinned-at-front is an invariant the rest of the grid relies on (sticky + // offsets walk displayColumns left→right and stop at the first unpinned + // entry). On unpin we must re-sort so the unpinned column doesn't stay + // sandwiched between still-pinned siblings, which would render the sticky + // zone with a gap. + const currentOrder = columnOrderRef.current ?? schemaColumnsRef.current.map((c) => c.name) + const pinnedSet = new Set(newPinned) + const newOrder = [ + ...currentOrder.filter((n) => pinnedSet.has(n)), + ...currentOrder.filter((n) => !pinnedSet.has(n)), + ] + const orderChanged = newOrder.some((n, i) => n !== currentOrder[i]) + if (orderChanged) { + setColumnOrder(newOrder) + columnOrderRef.current = newOrder + } + updateMetadataRef.current({ + pinnedColumns: newPinned, + ...(orderChanged ? { columnOrder: newOrder } : {}), + columnWidths: columnWidthsRef.current, + }) + }, []) + const { pushUndo, undo, redo } = useTableUndo({ workspaceId, tableId, onColumnOrderChange: handleColumnOrderChange, onColumnRename: handleColumnRename, onColumnWidthsChange: handleColumnWidthsChange, + onPinnedColumnsChange: handlePinnedColumnsChange, + getPinnedColumns, getColumnWidths, }) const undoRef = useRef(undo) @@ -530,6 +586,49 @@ export function TableGrid({ hasWorkflowColumns ) + const pinnedColumnSet = useMemo(() => new Set(pinnedColumns), [pinnedColumns]) + + // Stable fingerprint of pinned-column widths only. Changes when a pinned + // column is resized; stays the same when an unpinned column is resized. + // Used as the sole dep that ties pinnedOffsets to column-width changes so + // that unpinned resizes don't recreate the Map and re-render all DataRows. + const pinnedWidthsKey = displayColumns + .filter((c) => pinnedColumnSet.has(c.name)) + .map((c) => columnWidths[c.key] ?? COL_WIDTH) + .join(',') + + /** Pinned column key → sticky `left` px offset. */ + const pinnedOffsets = useMemo>(() => { + const offsets = new Map() + let left = checkboxColWidth + const widths = columnWidthsRef.current + for (const col of displayColumns) { + if (pinnedColumnSet.has(col.name)) { + offsets.set(col.key, left) + left += widths[col.key] ?? COL_WIDTH + } + } + return offsets + }, [displayColumns, pinnedColumnSet, checkboxColWidth, pinnedWidthsKey]) + + const lastPinnedColKey = useMemo(() => { + let last: string | null = null + for (const col of displayColumns) { + if (pinnedColumnSet.has(col.name)) last = col.key + } + return last + }, [displayColumns, pinnedColumnSet]) + + /** Right edge of the pinned sticky zone; used as the left inset for scroll-to-reveal. */ + const pinnedStickyLeftEdge = useMemo(() => { + let edge = checkboxColWidth + const widths = columnWidthsRef.current + for (const [key, left] of pinnedOffsets) { + edge = Math.max(edge, left + (widths[key] ?? COL_WIDTH)) + } + return edge + }, [pinnedOffsets, checkboxColWidth]) + const headerGroups = useMemo( () => buildHeaderGroups(displayColumns, tableWorkflowGroups), [displayColumns, tableWorkflowGroups] @@ -1127,6 +1226,16 @@ export function TableGrid({ } } + // Reorder is restricted to within a single zone so a cross-zone drop + // indicator never appears for an insertion the grid would refuse. + if (dragged) { + const pinned = pinnedColumnsRef.current + if (pinned.includes(dragged) !== pinned.includes(columnName)) { + if (dropTargetColumnNameRef.current !== null) setDropTargetColumnName(null) + return + } + } + // Workflow groups: skip per-`` writes and let `handleScrollDragOver` // do the bookkeeping. The scroll handler computes side from the group's // full bounds, so it stays stable across sibling cursor moves; the per-th @@ -1248,17 +1357,29 @@ export function TableGrid({ ...remaining.slice(insertIndex), ] - const orderChanged = newOrder.some((name, i) => currentOrder[i] !== name) + // Belt-and-suspenders re-sort: dragover already blocks cross-zone drops, + // but if anything ever slips through, the pinned-at-front invariant gets + // restored here (relative order within each zone is preserved). + let finalOrder = newOrder + const currentPinned = pinnedColumnsRef.current + if (currentPinned.length > 0) { + const pinnedSet = new Set(currentPinned) + const pinnedInNew = newOrder.filter((n) => pinnedSet.has(n)) + const unpinnedInNew = newOrder.filter((n) => !pinnedSet.has(n)) + finalOrder = [...pinnedInNew, ...unpinnedInNew] + } + + const orderChanged = finalOrder.some((name, i) => currentOrder[i] !== name) if (orderChanged) { pushUndoRef.current({ type: 'reorder-columns', previousOrder: currentOrder, - newOrder, + newOrder: finalOrder, }) - setColumnOrder(newOrder) + setColumnOrder(finalOrder) updateMetadataRef.current({ columnWidths: columnWidthsRef.current, - columnOrder: newOrder, + columnOrder: finalOrder, }) } } @@ -1302,6 +1423,12 @@ export function TableGrid({ if (dropTargetColumnNameRef.current !== null) setDropTargetColumnName(null) return } + const pinned = pinnedColumnsRef.current + const draggedName = dragColumnNameRef.current + if (draggedName && pinned.includes(draggedName) !== pinned.includes(col.name)) { + if (dropTargetColumnNameRef.current !== null) setDropTargetColumnName(null) + return + } const midX = left + groupWidth / 2 const side = cursorX < midX ? 'left' : 'right' if (col.name !== dropTargetColumnNameRef.current || side !== dropSideRef.current) { @@ -1345,8 +1472,13 @@ export function TableGrid({ useEffect(() => { if (!tableData?.metadata) return - if (!tableData.metadata.columnWidths && !tableData.metadata.columnOrder) return - // First load: seed both from the server and remember we've seeded. + if ( + !tableData.metadata.columnWidths && + !tableData.metadata.columnOrder && + !tableData.metadata.pinnedColumns + ) + return + // First load: seed all from the server and remember we've seeded. if (!metadataSeededRef.current) { metadataSeededRef.current = true if (tableData.metadata.columnWidths) { @@ -1355,6 +1487,9 @@ export function TableGrid({ if (tableData.metadata.columnOrder) { setColumnOrder(tableData.metadata.columnOrder) } + if (tableData.metadata.pinnedColumns) { + setPinnedColumns(tableData.metadata.pinnedColumns) + } return } // After first load: only re-seed `columnOrder` when the *set of columns* @@ -1528,7 +1663,7 @@ export function TableGrid({ const selector = `[data-table-scroll] [data-row="${rowIndex}"][data-col="${colIndex}"]` // `scrollIntoView` ignores the sticky `` and sticky gutter, so a cell // scrolled to the edge lands behind them. Scroll manually with insets equal - // to the sticky header height (top) and the row-number column width (left). + // to the sticky header height (top) and the full pinned left edge (left). const revealCell = (cell: HTMLElement) => { const scrollEl = scrollRef.current if (!scrollEl) return @@ -1540,10 +1675,14 @@ export function TableGrid({ } else if (rect.bottom > view.bottom) { scrollEl.scrollTop += rect.bottom - view.bottom } - if (rect.left < view.left + checkboxColWidth) { - scrollEl.scrollLeft -= view.left + checkboxColWidth - rect.left - } else if (rect.right > view.right) { - scrollEl.scrollLeft += rect.right - view.right + const targetColName = columnsRef.current[colIndex]?.name + const targetIsPinned = targetColName ? pinnedColumnSet.has(targetColName) : false + if (!targetIsPinned) { + if (rect.left < view.left + pinnedStickyLeftEdge) { + scrollEl.scrollLeft -= view.left + pinnedStickyLeftEdge - rect.left + } else if (rect.right > view.right) { + scrollEl.scrollLeft += rect.right - view.right + } } } let secondRaf = 0 @@ -1565,7 +1704,14 @@ export function TableGrid({ cancelAnimationFrame(rafId) if (secondRaf) cancelAnimationFrame(secondRaf) } - }, [selectionAnchor, selectionFocus, isColumnSelection, rowVirtualizer, checkboxColWidth]) + }, [ + selectionAnchor, + selectionFocus, + isColumnSelection, + rowVirtualizer, + pinnedStickyLeftEdge, + pinnedColumnSet, + ]) const handleCellClick = useCallback( (rowId: string, columnName: string, options?: { toggleBoolean?: boolean }) => { @@ -2497,26 +2643,6 @@ export function TableGrid({ [] ) - const handleChangeType = useCallback((columnName: string, newType: ColumnDefinition['type']) => { - const column = columnsRef.current.find((c) => c.name === columnName) - const previousType = column?.type - updateColumnMutation.mutate( - { columnName, updates: { type: newType } }, - { - onSuccess: () => { - if (previousType) { - pushUndoRef.current({ - type: 'update-column-type', - columnName, - previousType, - newType, - }) - } - }, - } - ) - }, []) - const insertColumnInOrder = useCallback( (anchorColumn: string, newColumn: string, side: 'left' | 'right') => { const order = columnOrderRef.current ?? schemaColumnsRef.current.map((c) => c.name) @@ -2725,6 +2851,7 @@ export function TableGrid({ .map((r) => ({ rowId: r.id, value: r.data[columnToDelete] })) const previousWidth = columnWidthsRef.current[columnToDelete] ?? null const orderSnapshot = currentOrder ? [...currentOrder] : null + const pinnedSnapshot = [...pinnedColumnsRef.current] const onDeleted = () => { deletedOriginalPositions.push(entry.position) @@ -2738,21 +2865,32 @@ export function TableGrid({ cellData, previousOrder: orderSnapshot, previousWidth, + previousPinnedColumns: pinnedSnapshot, }) const { [columnToDelete]: _removedWidth, ...cleanedWidths } = columnWidthsRef.current setColumnWidths(cleanedWidths) columnWidthsRef.current = cleanedWidths + const updatedPinned = pinnedColumnsRef.current.filter((n) => n !== columnToDelete) + if (updatedPinned.length !== pinnedColumnsRef.current.length) { + setPinnedColumns(updatedPinned) + pinnedColumnsRef.current = updatedPinned + } + if (currentOrder) { currentOrder = currentOrder.filter((n) => n !== columnToDelete) setColumnOrder(currentOrder) updateMetadataRef.current({ columnWidths: cleanedWidths, columnOrder: currentOrder, + pinnedColumns: pinnedColumnsRef.current, }) } else { - updateMetadataRef.current({ columnWidths: cleanedWidths }) + updateMetadataRef.current({ + columnWidths: cleanedWidths, + pinnedColumns: pinnedColumnsRef.current, + }) } deleteNext(index + 1) @@ -3173,66 +3311,88 @@ export function TableGrid({ {hasWorkflowGroup && ( - {headerGroups.map((g) => - g.kind === 'workflow' ? ( - = g.startColIndex + g.size - 1 - } - groupId={g.groupId} - groupType={workflowGroupById.get(g.groupId)?.type} - enrichmentId={workflowGroupById.get(g.groupId)?.enrichmentId} - groupName={workflowGroupById.get(g.groupId)?.name} - onSelectGroup={handleGroupSelect} - onOpenConfig={() => handleConfigureWorkflowGroup(g.groupId)} - onRunColumn={userPermissions.canEdit ? handleRunColumn : undefined} - selectedRowIds={selectedRowIds} - onInsertLeft={ - userPermissions.canEdit ? handleInsertColumnLeft : undefined - } - onInsertRight={ - userPermissions.canEdit ? handleInsertColumnRight : undefined - } - onDeleteColumn={ - userPermissions.canEdit ? handleDeleteColumn : undefined - } - onDeleteGroup={ - userPermissions.canEdit ? handleDeleteWorkflowGroup : undefined - } - onViewWorkflow={ - workflowGroupById.get(g.groupId)?.type === 'enrichment' - ? undefined - : handleViewWorkflow - } - readOnly={!userPermissions.canEdit} - onDragStart={ - userPermissions.canEdit ? handleColumnDragStart : undefined - } - onDragOver={ - userPermissions.canEdit ? handleColumnDragOver : undefined - } - onDragEnd={userPermissions.canEdit ? handleColumnDragEnd : undefined} - onDragLeave={ - userPermissions.canEdit ? handleColumnDragLeave : undefined - } - /> - ) : ( + {headerGroups.map((g) => { + const firstCol = displayColumns[g.startColIndex] + const stickyLeft = firstCol ? pinnedOffsets.get(firstCol.key) : undefined + if (g.kind === 'workflow') { + const lastCol = displayColumns[g.startColIndex + g.size - 1] + return ( + = g.startColIndex + g.size - 1 + } + groupId={g.groupId} + groupType={workflowGroupById.get(g.groupId)?.type} + enrichmentId={workflowGroupById.get(g.groupId)?.enrichmentId} + groupName={workflowGroupById.get(g.groupId)?.name} + onSelectGroup={handleGroupSelect} + onOpenConfig={() => handleConfigureWorkflowGroup(g.groupId)} + onRunColumn={userPermissions.canEdit ? handleRunColumn : undefined} + selectedRowIds={selectedRowIds} + onInsertLeft={ + userPermissions.canEdit ? handleInsertColumnLeft : undefined + } + onInsertRight={ + userPermissions.canEdit ? handleInsertColumnRight : undefined + } + onDeleteColumn={ + userPermissions.canEdit ? handleDeleteColumn : undefined + } + onDeleteGroup={ + userPermissions.canEdit ? handleDeleteWorkflowGroup : undefined + } + onViewWorkflow={ + workflowGroupById.get(g.groupId)?.type === 'enrichment' + ? undefined + : handleViewWorkflow + } + readOnly={!userPermissions.canEdit} + onDragStart={ + userPermissions.canEdit ? handleColumnDragStart : undefined + } + onDragOver={ + userPermissions.canEdit ? handleColumnDragOver : undefined + } + onDragEnd={ + userPermissions.canEdit ? handleColumnDragEnd : undefined + } + onDragLeave={ + userPermissions.canEdit ? handleColumnDragLeave : undefined + } + isPinned={firstCol ? pinnedColumnSet.has(firstCol.name) : false} + onPinToggle={userPermissions.canEdit ? handlePinToggle : undefined} + stickyLeft={stickyLeft} + isLastPinned={lastCol?.key === lastPinnedColKey} + /> + ) + } + const isLastFrz = firstCol?.key === lastPinnedColKey + return ( ) - )} + })} {userPermissions.canEdit && ( )} @@ -3243,45 +3403,52 @@ export function TableGrid({ checked={isAllRowsSelected} onCheckedChange={handleSelectAllToggle} /> - {displayColumns.map((column, idx) => ( - = normalizedSelection.startCol && - idx <= normalizedSelection.endCol - } - renameValue={ - columnRename.editingId === column.name ? columnRename.editValue : '' - } - onRenameValueChange={columnRename.setEditValue} - onRenameSubmit={columnRename.submitRename} - onRenameCancel={columnRename.cancelRename} - onColumnSelect={handleColumnSelect} - onChangeType={handleChangeType} - onInsertLeft={handleInsertColumnLeft} - onInsertRight={handleInsertColumnRight} - onDeleteColumn={handleDeleteColumn} - onResizeStart={handleColumnResizeStart} - onResize={handleColumnResize} - onResizeEnd={handleColumnResizeEnd} - onAutoResize={handleColumnAutoResize} - onDragStart={handleColumnDragStart} - onDragOver={handleColumnDragOver} - onDragEnd={handleColumnDragEnd} - onDragLeave={handleColumnDragLeave} - workflows={workflows} - workflowGroups={tableWorkflowGroups} - sourceInfo={columnSourceInfo.get(column.name)} - onOpenConfig={handleConfigureColumn} - onViewWorkflow={handleViewWorkflow} - /> - ))} + {displayColumns.map((column, idx) => { + const colIsPinned = pinnedColumnSet.has(column.name) + const colStickyLeft = pinnedOffsets.get(column.key) + return ( + = normalizedSelection.startCol && + idx <= normalizedSelection.endCol + } + renameValue={ + columnRename.editingId === column.name ? columnRename.editValue : '' + } + onRenameValueChange={columnRename.setEditValue} + onRenameSubmit={columnRename.submitRename} + onRenameCancel={columnRename.cancelRename} + onColumnSelect={handleColumnSelect} + onInsertLeft={handleInsertColumnLeft} + onInsertRight={handleInsertColumnRight} + onDeleteColumn={handleDeleteColumn} + onResizeStart={handleColumnResizeStart} + onResize={handleColumnResize} + onResizeEnd={handleColumnResizeEnd} + onAutoResize={handleColumnAutoResize} + onDragStart={handleColumnDragStart} + onDragOver={handleColumnDragOver} + onDragEnd={handleColumnDragEnd} + onDragLeave={handleColumnDragLeave} + workflows={workflows} + workflowGroups={tableWorkflowGroups} + sourceInfo={columnSourceInfo.get(column.name)} + onOpenConfig={handleConfigureColumn} + onViewWorkflow={handleViewWorkflow} + isPinned={colIsPinned} + onPinToggle={userPermissions.canEdit ? handlePinToggle : undefined} + stickyLeft={colStickyLeft} + isLastPinned={column.key === lastPinnedColKey} + /> + ) + })} {userPermissions.canEdit && ( 0 ? pinnedOffsets : undefined} + lastPinnedColKey={lastPinnedColKey} /> ) })} diff --git a/apps/sim/components/emcn/icons/index.ts b/apps/sim/components/emcn/icons/index.ts index 83c087a599e..3d1cb1fdaf6 100644 --- a/apps/sim/components/emcn/icons/index.ts +++ b/apps/sim/components/emcn/icons/index.ts @@ -60,6 +60,8 @@ export { PanelLeft } from './panel-left' export { Pause } from './pause' export { Pencil } from './pencil' export { PillsRing } from './pills-ring' +export { Pin } from './pin' +export { PinOff } from './pin-off' export { Play, PlayOutline } from './play' export { Plus } from './plus' export { Redo } from './redo' diff --git a/apps/sim/components/emcn/icons/pin-off.tsx b/apps/sim/components/emcn/icons/pin-off.tsx new file mode 100644 index 00000000000..0f1bf606275 --- /dev/null +++ b/apps/sim/components/emcn/icons/pin-off.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * PinOff icon component - thumbtack pin with diagonal strike-through + * @param props - SVG properties including className, fill, etc. + */ +export function PinOff(props: SVGProps) { + return ( + + ) +} diff --git a/apps/sim/components/emcn/icons/pin.tsx b/apps/sim/components/emcn/icons/pin.tsx new file mode 100644 index 00000000000..0e9fbfec2a0 --- /dev/null +++ b/apps/sim/components/emcn/icons/pin.tsx @@ -0,0 +1,26 @@ +import type { SVGProps } from 'react' + +/** + * Pin icon component - thumbtack pin + * @param props - SVG properties including className, fill, etc. + */ +export function Pin(props: SVGProps) { + return ( + + ) +} diff --git a/apps/sim/hooks/use-table-undo.test.ts b/apps/sim/hooks/use-table-undo.test.ts index 7a5e2db347d..3caee9dcca7 100644 --- a/apps/sim/hooks/use-table-undo.test.ts +++ b/apps/sim/hooks/use-table-undo.test.ts @@ -189,6 +189,7 @@ describe('useTableUndo – delete-column undo cell restore chunking', () => { cellData: [], previousOrder: null, previousWidth: null, + previousPinnedColumns: null, } it('does not call mutateAsync when cellData is empty', async () => { diff --git a/apps/sim/hooks/use-table-undo.ts b/apps/sim/hooks/use-table-undo.ts index 8a364d54691..289fcc01c70 100644 --- a/apps/sim/hooks/use-table-undo.ts +++ b/apps/sim/hooks/use-table-undo.ts @@ -31,6 +31,8 @@ interface UseTableUndoProps { onColumnOrderChange?: (order: string[]) => void onColumnRename?: (oldName: string, newName: string) => void onColumnWidthsChange?: (widths: Record) => void + onPinnedColumnsChange?: (pinned: string[]) => void + getPinnedColumns?: () => string[] getColumnWidths?: () => Record } @@ -40,6 +42,8 @@ export function useTableUndo({ onColumnOrderChange, onColumnRename, onColumnWidthsChange, + onPinnedColumnsChange, + getPinnedColumns, getColumnWidths, }: UseTableUndoProps) { const push = useTableUndoStore((s) => s.push) @@ -69,6 +73,10 @@ export function useTableUndo({ onColumnRenameRef.current = onColumnRename const onColumnWidthsChangeRef = useRef(onColumnWidthsChange) onColumnWidthsChangeRef.current = onColumnWidthsChange + const onPinnedColumnsChangeRef = useRef(onPinnedColumnsChange) + onPinnedColumnsChangeRef.current = onPinnedColumnsChange + const getPinnedColumnsRef = useRef(getPinnedColumns) + getPinnedColumnsRef.current = getPinnedColumns const getColumnWidthsRef = useRef(getColumnWidths) getColumnWidthsRef.current = getColumnWidths @@ -206,11 +214,21 @@ export function useTableUndo({ if (direction === 'undo') { deleteColumnMutation.mutate(action.columnName, { onSuccess: () => { + const metadata: Record = {} const currentWidths = getColumnWidthsRef.current?.() ?? {} if (action.columnName in currentWidths) { const { [action.columnName]: _, ...rest } = currentWidths onColumnWidthsChangeRef.current?.(rest) - updateMetadataMutation.mutate({ columnWidths: rest }) + metadata.columnWidths = rest + } + const currentPinned = getPinnedColumnsRef.current?.() ?? [] + if (currentPinned.includes(action.columnName)) { + const newPinned = currentPinned.filter((n) => n !== action.columnName) + onPinnedColumnsChangeRef.current?.(newPinned) + metadata.pinnedColumns = newPinned + } + if (Object.keys(metadata).length > 0) { + updateMetadataMutation.mutate(metadata) } }, }) @@ -273,6 +291,27 @@ export function useTableUndo({ metadata.columnWidths = merged onColumnWidthsChangeRef.current?.(merged) } + if (action.previousPinnedColumns !== null) { + const wasColumnPinned = action.previousPinnedColumns.includes( + action.columnName + ) + if (wasColumnPinned) { + const currentPinned = getPinnedColumnsRef.current?.() ?? [] + if (!currentPinned.includes(action.columnName)) { + const insertIndex = action.previousPinnedColumns.indexOf( + action.columnName + ) + const restoredPinned = [...currentPinned] + restoredPinned.splice( + Math.min(insertIndex, restoredPinned.length), + 0, + action.columnName + ) + onPinnedColumnsChangeRef.current?.(restoredPinned) + metadata.pinnedColumns = restoredPinned + } + } + } if (Object.keys(metadata).length > 0) { updateMetadataMutation.mutate(metadata) } @@ -294,6 +333,14 @@ export function useTableUndo({ metadata.columnWidths = rest onColumnWidthsChangeRef.current?.(rest) } + if (action.previousPinnedColumns !== null) { + const currentPinned = getPinnedColumnsRef.current?.() ?? [] + if (currentPinned.includes(action.columnName)) { + const newPinned = currentPinned.filter((n) => n !== action.columnName) + onPinnedColumnsChangeRef.current?.(newPinned) + metadata.pinnedColumns = newPinned + } + } if (Object.keys(metadata).length > 0) { updateMetadataMutation.mutate(metadata) } @@ -339,7 +386,21 @@ export function useTableUndo({ } case 'reorder-columns': { - const order = direction === 'undo' ? action.previousOrder : action.newOrder + const restored = direction === 'undo' ? action.previousOrder : action.newOrder + // The user may have pinned/unpinned since the original reorder; + // restoring the raw snapshot can leave a currently-pinned column + // in the middle, which breaks the sticky-offset walk in + // pinnedOffsets and causes the column to jump over its left + // neighbors on scroll. + const pinned = getPinnedColumnsRef.current?.() ?? [] + let order = restored + if (pinned.length > 0) { + const pinnedSet = new Set(pinned) + order = [ + ...restored.filter((n) => pinnedSet.has(n)), + ...restored.filter((n) => !pinnedSet.has(n)), + ] + } onColumnOrderChangeRef.current?.(order) updateMetadataMutation.mutate({ columnOrder: order }) break diff --git a/apps/sim/lib/api/contracts/tables.ts b/apps/sim/lib/api/contracts/tables.ts index aadb38f1352..f56c22a1222 100644 --- a/apps/sim/lib/api/contracts/tables.ts +++ b/apps/sim/lib/api/contracts/tables.ts @@ -135,6 +135,7 @@ export const deleteTableColumnBodySchema = z.object({ 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(), }) satisfies z.ZodType export const updateTableMetadataBodySchema = z.object({ diff --git a/apps/sim/lib/table/types.ts b/apps/sim/lib/table/types.ts index 2df30b8f9b4..bef5b8abbde 100644 --- a/apps/sim/lib/table/types.ts +++ b/apps/sim/lib/table/types.ts @@ -142,12 +142,14 @@ export interface TableSchema { /** * Table-level metadata stored alongside the table definition. UI state only - * (column widths, column order) — workflow-group concurrency is enforced at - * the trigger.dev queue layer, not via metadata. + * (column widths, column order, pinned columns) — workflow-group concurrency + * is enforced at the trigger.dev queue layer, not via metadata. */ export interface TableMetadata { columnWidths?: Record columnOrder?: string[] + /** Logical column names that are pinned to the left while scrolling horizontally. */ + pinnedColumns?: string[] } export interface TableDefinition { diff --git a/apps/sim/stores/table/types.ts b/apps/sim/stores/table/types.ts index 13f9f999c43..68496d3cc81 100644 --- a/apps/sim/stores/table/types.ts +++ b/apps/sim/stores/table/types.ts @@ -44,6 +44,7 @@ export type TableUndoAction = cellData: Array<{ rowId: string; value: unknown }> previousOrder: string[] | null previousWidth: number | null + previousPinnedColumns: string[] | null } | { type: 'rename-column'; oldName: string; newName: string } | { From dc6073eda42a17fc2b26f6ed3afbda6595a5150f Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 29 May 2026 11:50:21 -0700 Subject: [PATCH 02/14] fix(auth): block signup spam by denylisting shared MX backends (#4790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(auth): block signup spam by denylisting shared MX backends Signup-spam bots rotate throwaway domains rapidly but funnel them through a small number of shared catch-all mail providers. Across the current wave, 85% of bot domains resolved to just two MX backends (smtp.215.im, email.gravityengine.cc), while every domain differed — so the resolved MX host is a far more durable signal than the domain itself. Add a server-only MX validator (validateSignupEmailMx) that resolves the domain's MX records during /sign-up/email and rejects: - domains with no MX record (no_mx) - domains whose MX backend is on the denylist (blocked_mx_backend) Seeded with the two observed backends; extend at runtime via BLOCKED_EMAIL_MX_HOSTS. Fail-open on DNS timeout/transient error so legitimate users are never blocked by a resolver blip; kill switch via DISABLE_SIGNUP_MX_VALIDATION. Returns a clean 403 (APIError), not a 500. * refactor(auth): make MX signup validation opt-in (SIGNUP_MX_VALIDATION_ENABLED) Aligns with the sibling feature SIGNUP_EMAIL_VALIDATION_ENABLED (disposable blocking via harmony), which is also opt-in. Default-off avoids adding a DNS dependency to the signup path and prevents surprise signup blocking on self-hosted deployments with non-standard mail setups (internal domains, or a too-broad MX entry catching legit shared infra like Cloudflare Email Routing). Enable on hosted/abuse-targeted deployments via SIGNUP_MX_VALIDATION_ENABLED; the flag doubles as the kill switch, so the separate DISABLE_ flag is removed. * fix(auth): clear MX-lookup timeout to avoid dangling timer on success * refactor(auth): remove hardcoded MX denylist defaults The MX-backend denylist is now entirely operator-supplied via BLOCKED_EMAIL_MX_HOSTS. Sim is open source, so no specific mail backends are named in the repo, the env example, or the tests — deployments configure their own list out of band (e.g. via secrets). The no-MX hygiene check is unchanged; with an empty denylist no backend is blocked. --- apps/sim/lib/auth/auth.ts | 11 +++ apps/sim/lib/core/config/env.ts | 2 + apps/sim/lib/core/config/feature-flags.ts | 7 ++ .../messaging/email/validation.server.test.ts | 90 +++++++++++++++++ .../lib/messaging/email/validation.server.ts | 98 +++++++++++++++++++ 5 files changed, 208 insertions(+) create mode 100644 apps/sim/lib/messaging/email/validation.server.test.ts create mode 100644 apps/sim/lib/messaging/email/validation.server.ts diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index faffc248668..e84123de557 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -78,6 +78,7 @@ import { isOrganizationsEnabled, isRegistrationDisabled, isSignupEmailValidationEnabled, + isSignupMxValidationEnabled, } from '@/lib/core/config/feature-flags' import { PlatformEvents } from '@/lib/core/telemetry' import { getBaseUrl, isLocalhostUrl, parseOriginList } from '@/lib/core/utils/urls' @@ -85,6 +86,7 @@ import { processCredentialDraft } from '@/lib/credentials/draft-processor' import { sendEmail } from '@/lib/messaging/email/mailer' import { getFromEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils' import { quickValidateEmail } from '@/lib/messaging/email/validation' +import { validateSignupEmailMx } from '@/lib/messaging/email/validation.server' import { scheduleLifecycleEmail } from '@/lib/messaging/lifecycle' import { captureServerEvent, getPostHogClient } from '@/lib/posthog/server' import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server' @@ -843,6 +845,15 @@ export const auth = betterAuth({ }) } + if (isSignupMxValidationEnabled && ctx.path.startsWith('/sign-up/email') && ctx.body?.email) { + const mxCheck = await validateSignupEmailMx(ctx.body.email) + if (!mxCheck.allowed) { + throw new APIError('FORBIDDEN', { + message: 'Sign-ups from this email domain are not allowed.', + }) + } + } + if (ctx.path === '/oauth2/authorize' || ctx.path === '/oauth2/token') { const clientId = (ctx.query?.client_id ?? ctx.body?.client_id) as string | undefined if (clientId && isMetadataUrl(clientId)) { diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index a3189f679d0..3b871859295 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -27,6 +27,8 @@ export const env = createEnv({ ALLOWED_LOGIN_EMAILS: z.string().optional(), // Comma-separated list of allowed email addresses for login ALLOWED_LOGIN_DOMAINS: z.string().optional(), // Comma-separated list of allowed email domains for login BLOCKED_SIGNUP_DOMAINS: z.string().optional(), // Comma-separated list of email domains blocked from signing up (e.g., "gmail.com,yahoo.com") + SIGNUP_MX_VALIDATION_ENABLED: z.boolean().optional(), // Opt-in: validate the email's MX backend at signup (blocks no-MX domains and denylisted shared spam backends). Off by default; enable on hosted/abuse-targeted deployments. + BLOCKED_EMAIL_MX_HOSTS: z.string().optional(), // Comma-separated MX-host substrings blocked from signing up; matched against the domain's resolved MX backend to catch throwaway domains that share a mail backend. No defaults — operators supply their own list. Only used when SIGNUP_MX_VALIDATION_ENABLED is set. TRUSTED_ORIGINS: z.string().optional(), // Comma-separated additional origins to trust for auth (e.g., "https://app.example.com,https://www.example.com"). Merged into Better Auth trustedOrigins. TURNSTILE_SECRET_KEY: z.string().min(1).optional(), // Cloudflare Turnstile secret key for captcha verification SIGNUP_EMAIL_VALIDATION_ENABLED: z.boolean().optional(), // Enable disposable email blocking via better-auth-harmony (55K+ domains) diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index f1d6e959f36..bde9252d652 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -81,6 +81,13 @@ export const isEmailPasswordEnabled = !isFalsy(env.EMAIL_PASSWORD_SIGNUP_ENABLED */ export const isSignupEmailValidationEnabled = isTruthy(env.SIGNUP_EMAIL_VALIDATION_ENABLED) +/** + * Is MX-based signup validation enabled (blocks no-MX domains and denylisted shared spam + * mail backends). Opt-in to avoid adding a DNS dependency or blocking legitimate signups on + * self-hosted deployments with non-standard mail setups; enable on abuse-targeted deployments. + */ +export const isSignupMxValidationEnabled = isTruthy(env.SIGNUP_MX_VALIDATION_ENABLED) + /** * Is Trigger.dev enabled for async job processing */ diff --git a/apps/sim/lib/messaging/email/validation.server.test.ts b/apps/sim/lib/messaging/email/validation.server.test.ts new file mode 100644 index 00000000000..9fcfb4de6d4 --- /dev/null +++ b/apps/sim/lib/messaging/email/validation.server.test.ts @@ -0,0 +1,90 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockResolveMx, envRef } = vi.hoisted(() => ({ + mockResolveMx: vi.fn(), + envRef: { + BLOCKED_EMAIL_MX_HOSTS: undefined as string | undefined, + }, +})) + +vi.mock('dns/promises', () => ({ + default: { resolveMx: mockResolveMx }, +})) + +vi.mock('@/lib/core/config/env', () => ({ + get env() { + return envRef + }, +})) + +import { validateSignupEmailMx } from '@/lib/messaging/email/validation.server' + +const mx = (...hosts: string[]) => + hosts.map((exchange, i) => ({ exchange, priority: (i + 1) * 10 })) + +describe('validateSignupEmailMx', () => { + beforeEach(() => { + vi.clearAllMocks() + envRef.BLOCKED_EMAIL_MX_HOSTS = undefined + }) + + it('blocks a domain whose MX backend is on the configured denylist', async () => { + envRef.BLOCKED_EMAIL_MX_HOSTS = 'blocked-backend.example' + mockResolveMx.mockResolvedValue(mx('smtp.blocked-backend.example')) + const result = await validateSignupEmailMx('user@rotated-domain.test') + expect(result.allowed).toBe(false) + expect(result.reason).toBe('blocked_mx_backend') + }) + + it('matches the denylist as a case-insensitive substring of the MX exchange', async () => { + envRef.BLOCKED_EMAIL_MX_HOSTS = 'Blocked-Backend.Example' + mockResolveMx.mockResolvedValue(mx('mx1.blocked-backend.example')) + const result = await validateSignupEmailMx('user@another-domain.test') + expect(result.allowed).toBe(false) + expect(result.reason).toBe('blocked_mx_backend') + }) + + it('does not block any backend when the denylist is empty (no hardcoded defaults)', async () => { + envRef.BLOCKED_EMAIL_MX_HOSTS = undefined + mockResolveMx.mockResolvedValue(mx('smtp.blocked-backend.example')) + const result = await validateSignupEmailMx('user@rotated-domain.test') + expect(result.allowed).toBe(true) + }) + + it('allows a legitimate domain (gmail)', async () => { + mockResolveMx.mockResolvedValue( + mx('gmail-smtp-in.l.google.com', 'alt1.gmail-smtp-in.l.google.com') + ) + const result = await validateSignupEmailMx('real.person@gmail.com') + expect(result.allowed).toBe(true) + }) + + it('blocks a domain with no MX records (ENOTFOUND)', async () => { + mockResolveMx.mockRejectedValue(Object.assign(new Error('not found'), { code: 'ENOTFOUND' })) + const result = await validateSignupEmailMx('x@no-such-domain.invalid') + expect(result.allowed).toBe(false) + expect(result.reason).toBe('no_mx') + }) + + it('blocks a domain that resolves to an empty MX set', async () => { + mockResolveMx.mockResolvedValue([]) + const result = await validateSignupEmailMx('x@empty.example') + expect(result.allowed).toBe(false) + expect(result.reason).toBe('no_mx') + }) + + it('fails open on a transient DNS error (does not block legit users)', async () => { + mockResolveMx.mockRejectedValue(Object.assign(new Error('timeout'), { code: 'ETIMEOUT' })) + const result = await validateSignupEmailMx('user@some-real-domain.com') + expect(result.allowed).toBe(true) + }) + + it('allows when the email has no domain (defers to other validation)', async () => { + const result = await validateSignupEmailMx('not-an-email') + expect(result.allowed).toBe(true) + expect(mockResolveMx).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/messaging/email/validation.server.ts b/apps/sim/lib/messaging/email/validation.server.ts new file mode 100644 index 00000000000..2d1df5b3048 --- /dev/null +++ b/apps/sim/lib/messaging/email/validation.server.ts @@ -0,0 +1,98 @@ +import type { MxRecord } from 'dns' +import dns from 'dns/promises' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { env } from '@/lib/core/config/env' + +const logger = createLogger('EmailValidationServer') + +const MX_LOOKUP_TIMEOUT_MS = 3000 + +/** + * MX-host substrings to block, supplied at runtime via `BLOCKED_EMAIL_MX_HOSTS`. + * + * Signup-spam botnets rotate throwaway domains rapidly but funnel them through a + * small number of shared catch-all mail providers, so the resolved MX host is a + * far more stable signal than the domain itself. Each entry is matched as a + * case-insensitive substring against the domain's resolved MX exchanges. No + * hosts are hardcoded — operators configure their own denylist out of band. + */ +function getBlockedMxHosts(): string[] { + return ( + env.BLOCKED_EMAIL_MX_HOSTS?.split(',') + .map((h) => h.trim().toLowerCase()) + .filter(Boolean) ?? [] + ) +} + +export interface SignupEmailCheck { + /** Whether the email may proceed to signup. */ + allowed: boolean + /** Machine-readable block reason, present only when `allowed` is false. */ + reason?: 'no_mx' | 'blocked_mx_backend' +} + +/** + * Server-side signup email validation backed by an MX lookup. + * + * Rejects domains that resolve to no mail server (`no_mx`) or to a denylisted + * catch-all backend (`blocked_mx_backend`). Designed to be fail-open: any DNS + * timeout or transient resolver error allows the signup through so legitimate + * users are never blocked by an infrastructure blip. Only a definitive + * "domain has no MX" answer (`ENOTFOUND` / `ENODATA`) blocks. + * + * Server-only — imports `dns/promises`. Never import from client code. Gated by the caller + * behind `isSignupMxValidationEnabled`; this function performs the check unconditionally. + */ +export async function validateSignupEmailMx(email: string): Promise { + const domain = email.split('@')[1]?.toLowerCase() + if (!domain) return { allowed: true } + + let records: MxRecord[] + let timeoutHandle: ReturnType | undefined + try { + records = await Promise.race([ + dns.resolveMx(domain), + new Promise((_, reject) => { + timeoutHandle = setTimeout( + () => reject(new Error('mx_lookup_timeout')), + MX_LOOKUP_TIMEOUT_MS + ) + }), + ]) + } catch (error) { + const code = (error as NodeJS.ErrnoException).code + if (code === 'ENOTFOUND' || code === 'ENODATA') { + logger.info('Blocked signup: domain has no MX record', { domain }) + return { allowed: false, reason: 'no_mx' } + } + logger.warn('MX lookup failed; allowing signup (fail-open)', { + domain, + error: getErrorMessage(error), + }) + return { allowed: true } + } finally { + if (timeoutHandle) clearTimeout(timeoutHandle) + } + + if (!records || records.length === 0) { + logger.info('Blocked signup: domain has no MX record', { domain }) + return { allowed: false, reason: 'no_mx' } + } + + const blocked = getBlockedMxHosts() + const match = records.find((record) => { + const exchange = record.exchange.toLowerCase() + return blocked.some((host) => exchange.includes(host)) + }) + + if (match) { + logger.info('Blocked signup: denylisted MX backend', { + domain, + exchange: match.exchange, + }) + return { allowed: false, reason: 'blocked_mx_backend' } + } + + return { allowed: true } +} From fd77bb40695fa79fabf90e3d18efb30f0d40e80e Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 29 May 2026 11:50:44 -0700 Subject: [PATCH 03/14] feat(copilot): add seq ordinal to copilot_messages for order-preserving reads (#4791) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit copilot_messages had no column preserving message order: created_at (set from each message's timestamp) ties at millisecond granularity in 58% of chats, and some chats have out-of-order timestamps within their array. The only other tiebreaker, id, is a random UUID — so ORDER BY created_at, id renders same-timestamp user/assistant pairs swapped. This blocks the R+1 read cutover. Add an integer seq = the message's 0-based index within the chat's JSONB array (ground-truth order), backfilled inline in migration 0219 (no script for self-hosters or us). Reads will use ORDER BY seq NULLS LAST, created_at, id at cutover; reads still come from JSONB after this PR. Design: - seq is a tiebreaker, not the sole sort key (concurrent-append/NULL safety). - Nullable now; defer NOT NULL so rolling-deploy old pods don't fail inserts. - replace (update-messages snapshot) overwrites seq = array index (re-densifies after a mid-conversation delete); append preserves existing seq via COALESCE and assigns base+idx from a single MAX(seq) read (never MAX+i in SQL — multi-row batches would collide). The non-atomic read-then-insert window is documented and bounded by the read tiebreak + snapshot re-densify. - Dedupe message ids before insert (87 prod chats carry dup ids; a repeated id in one INSERT...ON CONFLICT would otherwise throw). - Backfill picks first-occurrence per (chat,id), gap-free via ROW_NUMBER; validated on staging data (0-based, contiguous, 0 bad ranges). Co-authored-by: Claude Opus 4.8 --- .../copilot/chat/messages-dual-write.test.ts | 73 +- .../lib/copilot/chat/messages-dual-write.ts | 50 +- packages/db/migrations/0219_amused_leo.sql | 19 + .../db/migrations/meta/0219_snapshot.json | 17519 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 4 + packages/testing/src/mocks/schema.mock.ts | 1 + 7 files changed, 17650 insertions(+), 23 deletions(-) create mode 100644 packages/db/migrations/0219_amused_leo.sql create mode 100644 packages/db/migrations/meta/0219_snapshot.json diff --git a/apps/sim/lib/copilot/chat/messages-dual-write.test.ts b/apps/sim/lib/copilot/chat/messages-dual-write.test.ts index f2d631e8dac..17d3e1666c5 100644 --- a/apps/sim/lib/copilot/chat/messages-dual-write.test.ts +++ b/apps/sim/lib/copilot/chat/messages-dual-write.test.ts @@ -26,6 +26,12 @@ const assistantMsg: PersistedMessage = { timestamp: '2026-01-01T00:00:01.000Z', } +/** The first arg passed to the most recent `.values(...)` call. */ +function lastValuesRows() { + const calls = dbChainMockFns.values.mock.calls + return calls[calls.length - 1][0] as Array> +} + describe('messages-dual-write', () => { beforeEach(() => { vi.clearAllMocks() @@ -43,7 +49,7 @@ describe('messages-dual-write', () => { expect(dbChainMockFns.insert).toHaveBeenCalledTimes(1) expect(dbChainMockFns.values).toHaveBeenCalledTimes(1) - const rows = dbChainMockFns.values.mock.calls[0][0] + const rows = lastValuesRows() expect(rows).toHaveLength(2) expect(rows[0]).toMatchObject({ @@ -54,8 +60,8 @@ describe('messages-dual-write', () => { model: null, streamId: null, }) - expect(rows[0].createdAt).toEqual(new Date(userMsg.timestamp)) - expect(rows[0].updatedAt).toEqual(new Date(userMsg.timestamp)) + expect(rows[0].createdAt as Date).toEqual(new Date(userMsg.timestamp)) + expect(rows[0].updatedAt as Date).toEqual(new Date(userMsg.timestamp)) expect(rows[1]).toMatchObject({ chatId: 'chat-1', @@ -63,13 +69,25 @@ describe('messages-dual-write', () => { role: 'assistant', content: assistantMsg, }) - expect(rows[1].createdAt).toEqual(new Date(assistantMsg.timestamp)) + expect(rows[1].createdAt as Date).toEqual(new Date(assistantMsg.timestamp)) }) - it('preserves per-message ordering via timestamp', async () => { + it('assigns seq as 0-based array index when the chat has no prior rows', async () => { + dbChainMockFns.where.mockResolvedValueOnce([{ maxSeq: null }]) + await appendCopilotChatMessages('chat-1', [userMsg, assistantMsg]) - const rows = dbChainMockFns.values.mock.calls[0][0] - expect(rows[0].createdAt.getTime()).toBeLessThan(rows[1].createdAt.getTime()) + const rows = lastValuesRows() + expect(rows[0].seq).toBe(0) + expect(rows[1].seq).toBe(1) + }) + + it('continues seq from MAX(seq)+1 when the chat already has rows', async () => { + dbChainMockFns.where.mockResolvedValueOnce([{ maxSeq: 4 }]) + + await appendCopilotChatMessages('chat-1', [userMsg, assistantMsg]) + const rows = lastValuesRows() + expect(rows[0].seq).toBe(5) + expect(rows[1].seq).toBe(6) }) it('passes chatModel and streamId options to every row', async () => { @@ -78,14 +96,14 @@ describe('messages-dual-write', () => { streamId: 'stream-xyz', }) - const rows = dbChainMockFns.values.mock.calls[0][0] + const rows = lastValuesRows() expect(rows[0].model).toBe('claude-sonnet-4-5') expect(rows[0].streamId).toBe('stream-xyz') expect(rows[1].model).toBe('claude-sonnet-4-5') expect(rows[1].streamId).toBe('stream-xyz') }) - it('uses ON CONFLICT DO UPDATE with chat_id + message_id target', async () => { + it('uses ON CONFLICT DO UPDATE that PRESERVES existing seq', async () => { await appendCopilotChatMessages('chat-1', [userMsg]) expect(dbChainMockFns.onConflictDoUpdate).toHaveBeenCalledTimes(1) @@ -96,6 +114,14 @@ describe('messages-dual-write', () => { expect(conflictArg.set).toHaveProperty('model') expect(conflictArg.set).toHaveProperty('streamId') expect(conflictArg.set).toHaveProperty('updatedAt') + expect(conflictArg.set.seq.strings.join('')).toContain('COALESCE(') + }) + + it('collapses duplicate message ids to a single row', async () => { + await appendCopilotChatMessages('chat-1', [userMsg, { ...userMsg, content: 'dupe' }]) + const rows = lastValuesRows() + expect(rows).toHaveLength(1) + expect(rows[0].messageId).toBe('msg-user-1') }) it('swallows DB errors so the legacy JSONB write stays canonical', async () => { @@ -120,12 +146,9 @@ describe('messages-dual-write', () => { expect(dbChainMockFns.delete).toHaveBeenCalledTimes(1) expect(dbChainMockFns.insert).toHaveBeenCalledTimes(1) - const rows = dbChainMockFns.values.mock.calls[0][0] + const rows = lastValuesRows() expect(rows).toHaveLength(2) - expect(rows.map((r: { messageId: string }) => r.messageId)).toEqual([ - 'msg-user-1', - 'msg-asst-1', - ]) + expect(rows.map((r) => r.messageId)).toEqual(['msg-user-1', 'msg-asst-1']) expect(dbChainMockFns.onConflictDoUpdate).toHaveBeenCalledTimes(1) const conflictArg = dbChainMockFns.onConflictDoUpdate.mock.calls[0][0] @@ -133,12 +156,32 @@ describe('messages-dual-write', () => { expect(conflictArg.set).toHaveProperty('model') }) + it('assigns seq as the snapshot array index (0-based)', async () => { + await replaceCopilotChatMessages('chat-1', [userMsg, assistantMsg]) + const rows = lastValuesRows() + expect(rows[0].seq).toBe(0) + expect(rows[1].seq).toBe(1) + }) + + it('OVERWRITES seq on conflict so positions re-densify after a delete', async () => { + await replaceCopilotChatMessages('chat-1', [userMsg]) + const conflictArg = dbChainMockFns.onConflictDoUpdate.mock.calls[0][0] + expect(conflictArg.set.seq.strings.join('')).toBe('excluded.seq') + }) + + it('collapses duplicate message ids to a single row', async () => { + await replaceCopilotChatMessages('chat-1', [userMsg, { ...userMsg, content: 'dupe' }]) + const rows = lastValuesRows() + expect(rows).toHaveLength(1) + expect(rows[0].seq).toBe(0) + }) + it('passes chatModel to every row in the snapshot', async () => { await replaceCopilotChatMessages('chat-1', [userMsg], { chatModel: 'gpt-4o-mini', }) - const rows = dbChainMockFns.values.mock.calls[0][0] + const rows = lastValuesRows() expect(rows[0].model).toBe('gpt-4o-mini') }) diff --git a/apps/sim/lib/copilot/chat/messages-dual-write.ts b/apps/sim/lib/copilot/chat/messages-dual-write.ts index 2a621d0b3c6..54e98afe3a0 100644 --- a/apps/sim/lib/copilot/chat/messages-dual-write.ts +++ b/apps/sim/lib/copilot/chat/messages-dual-write.ts @@ -7,9 +7,26 @@ import type { PersistedMessage } from '@/lib/copilot/chat/persisted-message' const logger = createLogger('CopilotMessagesDualWrite') +/** + * Keep the first occurrence of each message id. A single `INSERT ... ON + * CONFLICT` cannot touch the same conflict target twice, so a repeated id + * would otherwise throw. + */ +function dedupeById(messages: PersistedMessage[]): PersistedMessage[] { + const seen = new Set() + const out: PersistedMessage[] = [] + for (const m of messages) { + if (seen.has(m.id)) continue + seen.add(m.id) + out.push(m) + } + return out +} + function toRow( chatId: string, message: PersistedMessage, + seq: number, options?: { chatModel?: string | null; streamId?: string | null } ): typeof copilotMessages.$inferInsert { const ts = new Date(message.timestamp) @@ -18,6 +35,7 @@ function toRow( messageId: message.id, role: message.role, content: message, + seq, model: options?.chatModel ?? null, streamId: options?.streamId ?? null, createdAt: ts, @@ -27,8 +45,15 @@ function toRow( /** * Append messages to the new `copilot_messages` table. Best-effort — errors - * are logged but never thrown, since the legacy `copilot_chats.messages` - * JSONB column remains the source of truth during the dual-write rollout. + * are logged but never thrown; the legacy `copilot_chats.messages` JSONB + * column stays the source of truth during the dual-write rollout. + * + * `seq` is `MAX(seq) + index`, computed in JS (not in SQL, where every row of + * a multi-row INSERT would read the same pre-insert MAX and collide). The + * read-then-insert is non-atomic, so interleaved appends to one chat can tie + * `seq`; that window is bounded by the cutover read order (`seq, created_at, + * id`) and `replaceCopilotChatMessages`, which re-densifies `seq` from the + * authoritative JSONB order on the next snapshot save. */ export async function appendCopilotChatMessages( chatId: string, @@ -37,9 +62,15 @@ export async function appendCopilotChatMessages( ): Promise { if (messages.length === 0) return try { + const deduped = dedupeById(messages) + const [maxRow] = await db + .select({ maxSeq: sql`max(${copilotMessages.seq})` }) + .from(copilotMessages) + .where(eq(copilotMessages.chatId, chatId)) + const base = (maxRow?.maxSeq ?? -1) + 1 await db .insert(copilotMessages) - .values(messages.map((m) => toRow(chatId, m, options))) + .values(deduped.map((m, i) => toRow(chatId, m, base + i, options))) .onConflictDoUpdate({ target: [copilotMessages.chatId, copilotMessages.messageId], set: { @@ -47,6 +78,7 @@ export async function appendCopilotChatMessages( role: sql`excluded.role`, model: sql`COALESCE(excluded.model, ${copilotMessages.model})`, streamId: sql`COALESCE(excluded.stream_id, ${copilotMessages.streamId})`, + seq: sql`COALESCE(${copilotMessages.seq}, excluded.seq)`, updatedAt: sql`now()`, }, }) @@ -69,7 +101,8 @@ export async function replaceCopilotChatMessages( options?: { chatModel?: string | null } ): Promise { try { - const newMessageIds = messages.map((m) => m.id) + const deduped = dedupeById(messages) + const newMessageIds = deduped.map((m) => m.id) await db.transaction(async (tx) => { // Drop rows for messages not in the new snapshot. await tx @@ -82,12 +115,12 @@ export async function replaceCopilotChatMessages( ) : eq(copilotMessages.chatId, chatId) ) - if (messages.length === 0) return - // Upsert remaining rows. ON CONFLICT preserves existing stream_id / model - // so a snapshot save doesn't clobber metadata set during streaming. + if (deduped.length === 0) return + // Snapshot is authoritative on order, so seq = array index is overwritten + // on conflict; stream_id / model are preserved via COALESCE. await tx .insert(copilotMessages) - .values(messages.map((m) => toRow(chatId, m, options))) + .values(deduped.map((m, i) => toRow(chatId, m, i, options))) .onConflictDoUpdate({ target: [copilotMessages.chatId, copilotMessages.messageId], set: { @@ -95,6 +128,7 @@ export async function replaceCopilotChatMessages( role: sql`excluded.role`, model: sql`COALESCE(excluded.model, ${copilotMessages.model})`, streamId: sql`COALESCE(excluded.stream_id, ${copilotMessages.streamId})`, + seq: sql`excluded.seq`, updatedAt: sql`now()`, }, }) diff --git a/packages/db/migrations/0219_amused_leo.sql b/packages/db/migrations/0219_amused_leo.sql new file mode 100644 index 00000000000..c32c029caca --- /dev/null +++ b/packages/db/migrations/0219_amused_leo.sql @@ -0,0 +1,19 @@ +ALTER TABLE "copilot_messages" ADD COLUMN "seq" integer;--> statement-breakpoint +WITH ordered AS ( + SELECT c."id" AS chat_id, elem.value->>'id' AS message_id, elem.ord AS ord + FROM "copilot_chats" c + CROSS JOIN LATERAL jsonb_array_elements(c."messages") WITH ORDINALITY AS elem(value, ord) + WHERE jsonb_typeof(c."messages") = 'array' AND jsonb_array_length(c."messages") > 0 +), +first_occurrence AS ( + SELECT chat_id, message_id, MIN(ord) AS first_ord FROM ordered GROUP BY chat_id, message_id +), +ranked AS ( + SELECT chat_id, message_id, + (ROW_NUMBER() OVER (PARTITION BY chat_id ORDER BY first_ord) - 1) AS seq + FROM first_occurrence +) +UPDATE "copilot_messages" m SET "seq" = r.seq +FROM ranked r +WHERE m."chat_id" = r.chat_id AND m."message_id" = r.message_id;--> statement-breakpoint +CREATE INDEX "copilot_messages_chat_seq_idx" ON "copilot_messages" USING btree ("chat_id","seq") WHERE "copilot_messages"."deleted_at" IS NULL; \ No newline at end of file diff --git a/packages/db/migrations/meta/0219_snapshot.json b/packages/db/migrations/meta/0219_snapshot.json new file mode 100644 index 00000000000..0205b4d34e2 --- /dev/null +++ b/packages/db/migrations/meta/0219_snapshot.json @@ -0,0 +1,17519 @@ +{ + "id": "9c04ddff-9332-4201-be6f-98fbf006874a", + "prevId": "7718b87c-bd20-48e1-8a85-28bdd43a8bc4", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"form\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_archived_at_partial_idx": { + "name": "form_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"form\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_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.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_access_token_access_token_idx": { + "name": "oauth_access_token_access_token_idx", + "columns": [ + { + "expression": "access_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_token_idx": { + "name": "oauth_access_token_refresh_token_idx", + "columns": [ + { + "expression": "refresh_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_application_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_access_token_unique": { + "name": "oauth_access_token_access_token_unique", + "nullsNotDistinct": false, + "columns": ["access_token"] + }, + "oauth_access_token_refresh_token_unique": { + "name": "oauth_access_token_refresh_token_unique", + "nullsNotDistinct": false, + "columns": ["refresh_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_application": { + "name": "oauth_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_urls": { + "name": "redirect_urls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_application_client_id_idx": { + "name": "oauth_application_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_application_user_id_user_id_fk": { + "name": "oauth_application_user_id_user_id_fk", + "tableFrom": "oauth_application", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_application_client_id_unique": { + "name": "oauth_application_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "consent_given": { + "name": "consent_given", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_user_client_idx": { + "name": "oauth_consent_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_application_client_id_fk": { + "name": "oauth_consent_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_name_unique": { + "name": "permission_group_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_auto_add_unique": { + "name": "permission_group_workspace_auto_add_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_id_workspace_id_fk", + "tableFrom": "permission_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_workspace_user_unique": { + "name": "permission_group_member_workspace_user_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_workspace_id_workspace_id_fk": { + "name": "permission_group_member_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "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_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "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": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_table_id_idx": { + "name": "user_table_rows_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_data_gin_idx": { + "name": "user_table_rows_data_gin_idx", + "columns": [ + { + "expression": "data", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 32d0ae5f16d..9ac105fb2e4 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1527,6 +1527,13 @@ "when": 1779996122972, "tag": "0218_chubby_zzzax", "breakpoints": true + }, + { + "idx": 219, + "version": "7", + "when": 1780079220753, + "tag": "0219_amused_leo", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 0c767dcece9..32beb356dd5 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1985,6 +1985,7 @@ export const copilotMessages = pgTable( model: text('model'), tokensIn: integer('tokens_in'), tokensOut: integer('tokens_out'), + seq: integer('seq'), deletedAt: timestamp('deleted_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), @@ -1997,6 +1998,9 @@ export const copilotMessages = pgTable( chatCreatedAtIdx: index('copilot_messages_chat_created_at_idx') .on(table.chatId, table.createdAt, table.id) .where(sql`${table.deletedAt} IS NULL`), + chatSeqIdx: index('copilot_messages_chat_seq_idx') + .on(table.chatId, table.seq) + .where(sql`${table.deletedAt} IS NULL`), chatStreamIdx: index('copilot_messages_chat_stream_idx') .on(table.chatId, table.streamId) .where(sql`${table.streamId} IS NOT NULL`), diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index 08a10338af8..e148296ab24 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -749,6 +749,7 @@ export const schemaMock = { model: 'model', tokensIn: 'tokensIn', tokensOut: 'tokensOut', + seq: 'seq', deletedAt: 'deletedAt', createdAt: 'createdAt', updatedAt: 'updatedAt', From 8d68c8a0f3703fb3c28f79a5a72ac7f84848e9fe Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 29 May 2026 11:59:22 -0700 Subject: [PATCH 04/14] fix(tables): resource-cell icons, embedded filters, run-state + UI fixes (#4789) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(tables): resource-cell icons, embedded filters, run-count + queued fixes - table-grid: render in-workspace resource URLs (workflow/table/KB/file) as tagged-resource cells reusing ContextMentionIcon (colored square for workflows), matching @-mention chips; only the matching list is fetched. - table-grid: fix row-number sticky cell overflow — reserve the full run/stop button area (30px, not 16px) so wide row indices don't clip. - table-grid: show an infinite-scroll loading spinner while the next page loads instead of looking like the end of the table. - table: surface sort + filter (and run/stop via the options-bar extras slot) in the embedded mothership table resource view. - table-grid/utils: stop the dispatch overlay from optimistically painting autoRun=false cells Queued for auto-fire dispatches — the dispatcher skips those groups ('autoRun-off'); manual runs still show Queued (manual-bypass). - dispatcher: exclude orphan pre-stamps (pending + executionId null) from countRunningCells so the "X running" badge doesn't stick above zero. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(tables): single run/stop control, right-aligned row numbers, View-execution guard - table: de-duplicate the run/stop control in the embedded mothership view — drop TableGrid's own embedded run-status bar; it now lives only in the options bar (left-aligned next to Filter + Sort). Removes the orphaned RunStatusControl import + onStopAll/cancelRunsPending props. - data-row: right-align the row number within its box (hugs the right edge, no hover position jump) with a scaled right inset — 2px for ≤3-digit indices, 4px for 4+ so narrow columns don't look over-padded. - table-grid: require a real executionId in the action bar's canViewExecution flag so an error that never produced an execution (enqueue failure → status 'error', executionId null) doesn't offer "View execution". Co-Authored-By: Claude Opus 4.8 (1M context) * fix(tables): address review — drizzle operators for orphan filter, enabled flag for files query - dispatcher: replace the `not(and(...)) as SQL` cast in countRunningCells with `or(ne(status,'pending'), isNotNull(executionId))` — De Morgan equivalent, fully type-checked, no cast and no hand-written raw SQL. - workspace-files: add an `enabled` option to useWorkspaceFiles; sim-resource cell now passes the real workspaceId with `enabled` instead of '' so the query cache isn't polluted with an empty-key entry. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(tables): "X running" badge counts actual in-flight cells, not dispatch scope The badge derived from `runningCellCount` (the dispatch-scope estimate = rows-ahead × groupCount), which over-counts groups that already finished on rows still inside a dispatch's scope — a cascade where 3 of 4 workflow columns completed read "4 running" instead of "1". Derive `totalRunning` from the live `runningByRowId` map instead (the same per-row source the gutter and action-bar selection already sum), so it reflects cells actually in flight and updates per-cell via SSE rather than only on dispatch-window events. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../table-grid/cells/cell-content.tsx | 13 ++- .../table-grid/cells/cell-render.tsx | 72 ++++++++++++ .../table-grid/cells/sim-resource-cell.tsx | 104 ++++++++++++++++++ .../components/table-grid/data-row.tsx | 15 ++- .../components/table-grid/table-grid.tsx | 43 ++++---- .../[tableId]/components/table-grid/utils.ts | 11 +- .../[workspaceId]/tables/[tableId]/table.tsx | 70 ++++++------ apps/sim/hooks/queries/workspace-files.ts | 8 +- apps/sim/lib/table/dispatcher.ts | 16 ++- 9 files changed, 294 insertions(+), 58 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/sim-resource-cell.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx index 60c3cc05336..54a2c7f2dea 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx @@ -10,6 +10,9 @@ interface CellContentProps { value: unknown exec?: RowExecutionMetadata column: DisplayColumn + /** Current workspace id — lets string cells holding an in-workspace resource + * URL render as a tagged-resource chip instead of a plain external link. */ + workspaceId: string isEditing: boolean initialCharacter?: string | null onSave: (value: unknown, reason: SaveReason) => void @@ -34,6 +37,7 @@ export function CellContent({ value, exec, column, + workspaceId, isEditing, initialCharacter, onSave, @@ -41,7 +45,14 @@ export function CellContent({ waitingOnLabels, isEnrichmentOutput, }: CellContentProps) { - const kind = resolveCellRender({ value, exec, column, waitingOnLabels, isEnrichmentOutput }) + const kind = resolveCellRender({ + value, + exec, + column, + waitingOnLabels, + isEnrichmentOutput, + currentWorkspaceId: workspaceId, + }) return ( <> diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx index 557186b7668..91cabea7994 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx @@ -9,6 +9,7 @@ import type { RowExecutionMetadata } from '@/lib/table' import { StatusBadge } from '@/app/workspace/[workspaceId]/logs/utils' import { storageToDisplay } from '../../../utils' import type { DisplayColumn } from '../types' +import { SimResourceCell, type SimResourceType } from './sim-resource-cell' export type CellRenderKind = // Workflow-output cells @@ -26,6 +27,13 @@ export type CellRenderKind = | { kind: 'json'; text: string } | { kind: 'date'; text: string } | { kind: 'url'; text: string; href: string; domain: string } + | { + kind: 'sim-resource' + workspaceId: string + resourceType: SimResourceType + resourceId: string + href: string + } | { kind: 'text'; text: string } // Universal fallback | { kind: 'empty' } @@ -38,6 +46,9 @@ interface ResolveCellRenderInput { /** Column is an enrichment-group output — a completed-but-empty cell renders * "Not found" rather than a blank, since the enrichment ran and matched nothing. */ isEnrichmentOutput?: boolean + /** Current workspace id — a URL pointing to a resource in this workspace + * renders as a tagged-resource chip rather than a plain external link. */ + currentWorkspaceId?: string } export function resolveCellRender({ @@ -46,6 +57,7 @@ export function resolveCellRender({ column, waitingOnLabels, isEnrichmentOutput, + currentWorkspaceId, }: ResolveCellRenderInput): CellRenderKind { const isNull = value === null || value === undefined const isEmpty = isNull || value === '' @@ -97,6 +109,18 @@ export function resolveCellRender({ if (column.type === 'date') return { kind: 'date', text: String(value) } if (column.type === 'string') { const text = stringifyValue(value) + if (currentWorkspaceId) { + const resource = extractSimResourceInfo(text) + if (resource && resource.workspaceId === currentWorkspaceId) { + return { + kind: 'sim-resource', + workspaceId: resource.workspaceId, + resourceType: resource.resourceType, + resourceId: resource.resourceId, + href: resource.href, + } + } + } const urlInfo = extractUrlInfo(text) if (urlInfo) return { kind: 'url', text, href: urlInfo.href, domain: urlInfo.domain } return { kind: 'text', text } @@ -131,6 +155,43 @@ function extractUrlInfo(text: string): { href: string; domain: string } | null { return null } +/** Maps a workspace route section to the sim resource kind it addresses. */ +const SIM_RESOURCE_SECTIONS: Record = { + w: 'workflow', + tables: 'table', + knowledge: 'knowledge', + files: 'file', +} + +/** + * Recognizes a `/workspace/{id}/{section}/{resourceId}` URL (absolute or + * relative) pointing to a sim resource and returns its descriptor. The href is + * the pathname so the link stays within the current deployment. Returns null + * for anything that isn't a single-segment resource route. + */ +function extractSimResourceInfo( + text: string +): { workspaceId: string; resourceType: SimResourceType; resourceId: string; href: string } | null { + const trimmed = text.trim() + if (!trimmed) return null + let pathname: string + if (/^https?:\/\//i.test(trimmed)) { + try { + pathname = new URL(trimmed).pathname + } catch { + return null + } + } else if (trimmed.startsWith('/')) { + pathname = trimmed.split(/[?#]/)[0] + } else { + return null + } + const match = pathname.match(/^\/workspace\/([^/]+)\/(w|tables|knowledge|files)\/([^/]+)\/?$/) + if (!match) return null + const [, workspaceId, section, resourceId] = match + return { workspaceId, resourceType: SIM_RESOURCE_SECTIONS[section], resourceId, href: pathname } +} + interface CellRenderProps { kind: CellRenderKind isEditing: boolean @@ -270,6 +331,17 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle ) + case 'sim-resource': + return ( + + ) + case 'text': return ( = { + workflow: 'Workflow', + table: 'Table', + knowledge: 'Knowledge base', + file: 'File', +} + +interface SimResourceCellProps { + /** Always the current workspace — the resolver only emits this kind for same-workspace URLs. */ + workspaceId: string + resourceType: SimResourceType + resourceId: string + /** In-app pathname the resource link navigates to. */ + href: string + isEditing: boolean +} + +/** + * Renders a cell whose value is a URL pointing to a sim resource in the current + * workspace as a tagged-resource chip — the same icon (and per-workflow colored + * square) used for @-style resource mentions, plus the resource's name as a link. + * Only the list matching `resourceType` is fetched; the other queries stay + * disabled so a sim-resource cell subscribes to a single shared list. + */ +export function SimResourceCell({ + workspaceId, + resourceType, + resourceId, + href, + isEditing, +}: SimResourceCellProps) { + const { data: workflows = [] } = useWorkflows( + resourceType === 'workflow' ? workspaceId : undefined + ) + const { data: tables = [] } = useTablesList(resourceType === 'table' ? workspaceId : undefined) + const { data: knowledgeBases = [] } = useKnowledgeBasesQuery(workspaceId, { + enabled: resourceType === 'knowledge', + }) + const { data: files = [] } = useWorkspaceFiles(workspaceId, 'active', { + enabled: resourceType === 'file', + }) + + const workflow = + resourceType === 'workflow' ? workflows.find((w) => w.id === resourceId) : undefined + + const name = useMemo(() => { + switch (resourceType) { + case 'workflow': + return workflow?.name + case 'table': + return tables.find((t) => t.id === resourceId)?.name + case 'knowledge': + return knowledgeBases.find((kb) => kb.id === resourceId)?.name + case 'file': + return files.find((f) => f.id === resourceId)?.name + } + }, [resourceType, resourceId, workflow, tables, knowledgeBases, files]) + + const label = name ?? FALLBACK_LABEL[resourceType] + + const context: ChatMessageContext = + resourceType === 'workflow' + ? { kind: 'workflow', label, workflowId: resourceId } + : resourceType === 'table' + ? { kind: 'table', label, tableId: resourceId } + : resourceType === 'knowledge' + ? { kind: 'knowledge', label, knowledgeId: resourceId } + : { kind: 'file', label, fileId: resourceId } + + return ( + + + e.stopPropagation()} + onDoubleClick={(e) => e.stopPropagation()} + > + {label} + + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx index 3bc1d465774..219a3376e78 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx @@ -23,6 +23,9 @@ import { type NormalizedSelection, resolveCellExec } from './utils' export interface DataRowProps { row: TableRowType columns: DisplayColumn[] + /** Current workspace id — forwarded to cells so in-workspace resource URLs + * render as tagged-resource chips. */ + workspaceId: string rowIndex: number isFirstRow: boolean editingColumnName: string | null @@ -98,6 +101,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean { if ( prev.row !== next.row || prev.columns !== next.columns || + prev.workspaceId !== next.workspaceId || prev.rowIndex !== next.rowIndex || prev.isFirstRow !== next.isFirstRow || prev.editingColumnName !== next.editingColumnName || @@ -141,6 +145,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean { export const DataRow = React.memo(function DataRow({ row, columns, + workspaceId, rowIndex, isFirstRow, editingColumnName, @@ -204,7 +209,12 @@ export const DataRow = React.memo(function DataRow({ tabIndex={0} aria-checked={isRowSelected} aria-label={`Select row ${rowIndex + 1}`} - className='group/checkbox flex h-[20px] shrink-0 items-center justify-center' + className={cn( + 'group/checkbox flex h-[20px] shrink-0 items-center justify-end', + // Lighter right inset for narrow indices (≤3 digits → numDivWidth ≤ 28); + // full 4px once the column widens (4+ digits, numDivWidth ≥ 36). + numDivWidth >= 36 ? 'pr-1' : 'pr-0.5' + )} style={{ width: numDivWidth }} onMouseDown={(e) => { if (e.button !== 0) return @@ -216,7 +226,7 @@ export const DataRow = React.memo(function DataRow({ > @@ -328,6 +338,7 @@ export const DataRow = React.memo(function DataRow({ )}
void /** Single-row stop for the per-row gutter button. */ onStopRow: (rowId: string) => void - /** Wholesale cancel — page-header "Stop all". */ - onStopAll: () => void - /** Whether `useCancelTableRuns` is currently in flight. */ - cancelRunsPending: boolean /** * Fired whenever the action-bar selection or running-count derivations * change. Wrapper uses this to render . @@ -258,8 +253,6 @@ export function TableGrid({ onRunRows, onStopRows, onStopRow, - onStopAll, - cancelRunsPending, onSelectionChange, queryOptions, columnRenameSinkRef, @@ -333,8 +326,13 @@ export function TableGrid({ const { data: tableRunState } = useTableRunState(tableId) const activeDispatches = tableRunState?.dispatches - const totalRunning = tableRunState?.runningCellCount ?? 0 const runningByRowId = tableRunState?.runningByRowId ?? EMPTY_RUNNING_BY_ROW + // Actual in-flight cell count = sum of the live per-row map (kept current by + // applyCell's SSE deltas, and the same source the per-row gutter uses). The + // dispatch-scope `runningCellCount` over-counts already-completed groups on + // rows still inside a dispatch's scope — e.g. a cascade where 3 of 4 columns + // finished would read "4 running" instead of "1". + const totalRunning = Object.values(runningByRowId).reduce((sum, n) => sum + n, 0) const tableRowCountRef = useRef(tableData?.rowCount ?? 0) tableRowCountRef.current = tableData?.rowCount ?? 0 @@ -3087,8 +3085,12 @@ export function TableGrid({ rowId: row.id, groupId, executionId: exec?.executionId ?? null, + // Requires a real executionId: an error that never produced an execution + // (e.g. enqueue failure → status 'error' with executionId null) has no + // trace to open, so "View execution" must not offer it. canViewExecution: !isEnrichmentGroup && + Boolean(exec?.executionId) && (status === 'completed' || status === 'error' || status === 'running' || isPaused), } }, [normalizedSelection, rows, displayColumns, workflowGroupById]) @@ -3235,16 +3237,6 @@ export function TableGrid({ return (
- {embedded && totalRunning > 0 && ( -
- -
- )} -
)} + {isFetchingNextPage && ( + + +
+ +
+ + + )} ) })() diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts index d66182b70f4..6a7f53b2185 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts @@ -49,7 +49,10 @@ export function checkboxColLayout( ): { colWidth: number; numDivWidth: number } { const digits = maxRows > 0 ? Math.floor(Math.log10(maxRows)) + 1 : 1 const numDivWidth = Math.max(20, digits * 8 + 4) - const colWidth = Math.max(32, numDivWidth + 8) + (hasWorkflowCols ? 16 : 0) + // When workflow columns are present a 20px run/stop button sits to the right of + // the number, separated by a 6px gap and a 4px right pad — 30px total. Reserving + // only the button width clipped the number on tables with many (wide) row indices. + const colWidth = Math.max(32, numDivWidth + 8) + (hasWorkflowCols ? 30 : 0) return { colWidth, numDivWidth } } @@ -196,6 +199,12 @@ export function resolveCellExec( // cell SSE) cover the actual rows instead. if (d.limit) continue if (!d.scope.groupIds.includes(group.id)) continue + // Auto-fire dispatches (row writes / schema changes) scope every group but + // the dispatcher honors `autoRun: false` per-cell ('autoRun-off'), so those + // cells never actually run — don't optimistically paint them Queued. Manual + // runs (Run all / Run column) bypass autoRun and DO run them, so keep the + // overlay's Queued there. + if (!d.isManualRun && group.autoRun === false) continue if (d.scope.rowIds && !d.scope.rowIds.includes(row.id)) continue if (row.position <= d.cursor) continue return { diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 64fa3c1af0d..466835d041b 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -462,36 +462,46 @@ export function Table({ return (
{!embedded && ( - <> - 0 ? ( - - ) : null - } - /> - setFilterOpen((prev) => !prev)} - filterActive={filterOpen || !!queryOptions.filter} - /> - {filterOpen && ( - setFilterOpen(false)} + 0 ? ( + + ) : null + } + /> + )} + {/* Sort + filter render in both modes. In embedded (mothership) mode there's + no ResourceHeader, so the run/stop control rides in the options bar's + `extras` slot — keeping the bar populated whether or not a run is live. */} + setFilterOpen((prev) => !prev)} + filterActive={filterOpen || !!queryOptions.filter} + extras={ + embedded && selection.totalRunning > 0 ? ( + - )} - + ) : undefined + } + /> + {filterOpen && ( + setFilterOpen(false)} + /> )} fetchWorkspaceFiles(workspaceId, scope, signal), - enabled: !!workspaceId, + enabled: !!workspaceId && (options?.enabled ?? true), staleTime: 30 * 1000, // 30 seconds - files can change frequently placeholderData: keepPreviousData, // Show cached data immediately }) diff --git a/apps/sim/lib/table/dispatcher.ts b/apps/sim/lib/table/dispatcher.ts index 885df978bb0..441abda9330 100644 --- a/apps/sim/lib/table/dispatcher.ts +++ b/apps/sim/lib/table/dispatcher.ts @@ -3,7 +3,7 @@ import { tableRowExecutions, tableRunDispatches, userTableRows } from '@sim/db/s import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, asc, eq, gt, inArray, type SQL, sql } from 'drizzle-orm' +import { and, asc, eq, gt, inArray, isNotNull, ne, or, type SQL, sql } from 'drizzle-orm' import { getJobQueue } from '@/lib/core/async-jobs/config' import { writeWorkflowGroupState } from '@/lib/table/cell-write' import { appendTableEvent } from '@/lib/table/events' @@ -185,6 +185,15 @@ export async function insertDispatch(input: { * gutter Run/Stop button. All three statuses are user-cancellable, so the * gutter must surface Stop whenever any of them are present (else clicking * Play during the queued window would re-run an already-queued cell). + * + * Excludes orphan pre-stamps — `pending` rows with no `executionId` — which + * are dead placeholders left when a dispatcher loop wrote the stamp but no + * cell-task ever picked it up (lock contention, queue failure, crash). The + * cell already shows its prior value and `classifyEligibility` treats these as + * claimable, so counting them stuck the "X running" badge above zero forever + * even though nothing was running. Same `executionId == null` test used by + * {@link classifyEligibility} / {@link pickNextEligibleGroupForRow}. + * * Hits the `(table_id, status)` partial index on table_row_executions. */ export async function countRunningCells( tableId: string @@ -198,7 +207,10 @@ export async function countRunningCells( .where( and( eq(tableRowExecutions.tableId, tableId), - inArray(tableRowExecutions.status, ['queued', 'running', 'pending']) + inArray(tableRowExecutions.status, ['queued', 'running', 'pending']), + // Exclude orphan pre-stamps (`pending` + null executionId). De Morgan of + // NOT(pending AND null) — `status` is NOT NULL so `ne` is well-defined. + or(ne(tableRowExecutions.status, 'pending'), isNotNull(tableRowExecutions.executionId)) ) ) .groupBy(tableRowExecutions.rowId) From 8daca91e52617ff2585ea6e475be21d6ae313e1e Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 29 May 2026 12:22:21 -0700 Subject: [PATCH 05/14] feat(slack): request channels:manage and groups:write for conversation ops (#4792) --- apps/sim/lib/oauth/oauth.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 21d12053ebd..8ba80675de2 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -696,8 +696,10 @@ export const OAUTH_PROVIDERS: Record = { scopes: [ 'channels:read', 'channels:history', + 'channels:manage', 'groups:read', 'groups:history', + 'groups:write', 'chat:write', 'chat:write.public', 'im:write', From c95aa879f7f08de05c3813c17bade3ea3bf8bf20 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 29 May 2026 13:08:09 -0700 Subject: [PATCH 06/14] feat(access-control): add per-model denylist to permission groups (#4794) * feat(access-control): add per-model denylist to permission groups * fix(access-control): default deniedModels in response schema, hide blocked badge on disabled rows, trim comments * chore(access-control): reuse canonical DYNAMIC_MODEL_PROVIDERS from providers/models --- apps/sim/app/api/guardrails/validate/route.ts | 3 +- apps/sim/app/api/providers/route.ts | 7 +- .../components/combobox/combobox.tsx | 17 +- .../components/access-control.tsx | 294 ++++++++++++++++-- .../utils/permission-check.test.ts | 51 +++ .../access-control/utils/permission-check.ts | 73 +++-- apps/sim/hooks/use-permission-config.ts | 11 + .../lib/api/contracts/permission-groups.ts | 1 + apps/sim/lib/permission-groups/types.ts | 10 + 9 files changed, 411 insertions(+), 56 deletions(-) diff --git a/apps/sim/app/api/guardrails/validate/route.ts b/apps/sim/app/api/guardrails/validate/route.ts index e9d19853c04..03bf505a4df 100644 --- a/apps/sim/app/api/guardrails/validate/route.ts +++ b/apps/sim/app/api/guardrails/validate/route.ts @@ -12,6 +12,7 @@ import { validatePII } from '@/lib/guardrails/validate_pii' import { validateRegex } from '@/lib/guardrails/validate_regex' import { assertPermissionsAllowed, + ModelNotAllowedError, ProviderNotAllowedError, } from '@/ee/access-control/utils/permission-check' @@ -161,7 +162,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { model, }) } catch (err) { - if (err instanceof ProviderNotAllowedError) { + if (err instanceof ProviderNotAllowedError || err instanceof ModelNotAllowedError) { return NextResponse.json({ success: true, output: { diff --git a/apps/sim/app/api/providers/route.ts b/apps/sim/app/api/providers/route.ts index f0bfc2b4a45..bc5e344772b 100644 --- a/apps/sim/app/api/providers/route.ts +++ b/apps/sim/app/api/providers/route.ts @@ -19,6 +19,7 @@ import { import { assertPermissionsAllowed, IntegrationNotAllowedError, + ModelNotAllowedError, ProviderNotAllowedError, } from '@/ee/access-control/utils/permission-check' import type { StreamingExecution } from '@/executor/types' @@ -132,7 +133,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => { model, }) } catch (err) { - if (err instanceof ProviderNotAllowedError || err instanceof IntegrationNotAllowedError) { + if ( + err instanceof ProviderNotAllowedError || + err instanceof ModelNotAllowedError || + err instanceof IntegrationNotAllowedError + ) { return NextResponse.json({ error: err.message }, { status: 403 }) } throw err diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx index e802667aa4b..d5437cf025b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx @@ -151,7 +151,11 @@ export const ComboBox = memo(function ComboBox({ const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue // Permission-based filtering for model dropdowns - const { isProviderAllowed, isLoading: isPermissionLoading } = usePermissionConfig() + const { + isProviderAllowed, + isModelAllowed, + isLoading: isPermissionLoading, + } = usePermissionConfig() // Evaluate static options if provided as a function const staticOptions = useMemo(() => { @@ -160,9 +164,9 @@ export const ComboBox = memo(function ComboBox({ if (subBlockId === 'model') { return opts.filter((opt) => { const modelId = typeof opt === 'string' ? opt : opt.id + if (!isModelAllowed(modelId)) return false try { - const providerId = getProviderFromModel(modelId) - return isProviderAllowed(providerId) + return isProviderAllowed(getProviderFromModel(modelId)) } catch { return true } @@ -170,7 +174,7 @@ export const ComboBox = memo(function ComboBox({ } return opts - }, [options, subBlockId, isProviderAllowed]) + }, [options, subBlockId, isProviderAllowed, isModelAllowed]) // Normalize fetched options to match ComboBoxOption format const normalizedFetchedOptions = useMemo((): ComboBoxOption[] => { @@ -185,9 +189,9 @@ export const ComboBox = memo(function ComboBox({ if (subBlockId === 'model' && fetchOptions && normalizedFetchedOptions.length > 0) { opts = opts.filter((opt) => { const modelId = typeof opt === 'string' ? opt : opt.id + if (!isModelAllowed(modelId)) return false try { - const providerId = getProviderFromModel(modelId) - return isProviderAllowed(providerId) + return isProviderAllowed(getProviderFromModel(modelId)) } catch { return true } @@ -212,6 +216,7 @@ export const ComboBox = memo(function ComboBox({ hydratedOption, subBlockId, isProviderAllowed, + isModelAllowed, ]) // Convert options to Combobox format diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 930c9da176b..30f744225d6 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' -import { Plus, Search } from 'lucide-react' +import { ChevronDown, Plus, Search } from 'lucide-react' import { useParams } from 'next/navigation' import { Avatar, @@ -27,6 +27,7 @@ import { } from '@/components/emcn' import { Input as BaseInput } from '@/components/ui' import { getEnv, isTruthy } from '@/lib/core/config/env' +import { cn } from '@/lib/core/utils/cn' import type { PermissionGroupConfig } from '@/lib/permission-groups/types' import { getUserColor } from '@/lib/workspaces/colors' import { getAllBlocks } from '@/blocks' @@ -42,9 +43,16 @@ import { useUserPermissionConfig, } from '@/ee/access-control/hooks/permission-groups' import { useBlacklistedProviders } from '@/hooks/queries/allowed-providers' +import { useProviderModels } from '@/hooks/queries/providers' import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace' -import { PROVIDER_DEFINITIONS } from '@/providers/models' -import { getAllProviderIds } from '@/providers/utils' +import { + DYNAMIC_MODEL_PROVIDERS, + getProviderModels, + PROVIDER_DEFINITIONS, +} from '@/providers/models' +import type { ProviderId } from '@/providers/types' +import { getAllProviderIds, getProviderFromModel } from '@/providers/utils' +import type { ProviderName } from '@/stores/providers' const logger = createLogger('AccessControl') @@ -252,6 +260,188 @@ function AccessControlSkeleton() { ) } +interface ModelDenylistControls { + isModelAllowed: (model: string) => boolean + onToggleModel: (model: string) => void + onSetModelsDenied: (models: string[], denied: boolean) => void +} + +interface ModelCheckboxGridProps extends ModelDenylistControls { + models: string[] + isLoading: boolean +} + +function ModelCheckboxGrid({ + models, + isLoading, + isModelAllowed, + onToggleModel, + onSetModelsDenied, +}: ModelCheckboxGridProps) { + const [search, setSearch] = useState('') + + const sortedModels = useMemo(() => [...models].sort((a, b) => a.localeCompare(b)), [models]) + + const filteredModels = useMemo(() => { + if (!search.trim()) return sortedModels + const query = search.toLowerCase() + return sortedModels.filter((model) => model.toLowerCase().includes(query)) + }, [sortedModels, search]) + + if (isLoading) { + return
Loading models…
+ } + + if (models.length === 0) { + return ( +
+ No models available for this provider. +
+ ) + } + + const allFilteredAllowed = filteredModels.every((model) => isModelAllowed(model)) + + return ( +
+
+
+ + setSearch(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-sm leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + /> +
+ +
+
+ {filteredModels.map((model) => { + const checkboxId = `model-${model}` + return ( + + ) + })} +
+
+ ) +} + +interface DynamicProviderModelsProps extends ModelDenylistControls { + provider: ProviderName + workspaceId?: string +} + +function DynamicProviderModels({ provider, workspaceId, ...controls }: DynamicProviderModelsProps) { + const { data, isPending } = useProviderModels(provider, workspaceId) + return +} + +interface StaticProviderModelsProps extends ModelDenylistControls { + providerId: ProviderId +} + +function StaticProviderModels({ providerId, ...controls }: StaticProviderModelsProps) { + const models = useMemo(() => getProviderModels(providerId), [providerId]) + return +} + +interface ProviderRowProps extends ModelDenylistControls { + providerId: ProviderId + isProviderAllowed: boolean + onToggleProvider: () => void + deniedCount: number + workspaceId?: string +} + +function ProviderRow({ + providerId, + isProviderAllowed, + onToggleProvider, + deniedCount, + workspaceId, + ...controls +}: ProviderRowProps) { + const [expanded, setExpanded] = useState(false) + + const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon + const providerName = + PROVIDER_DEFINITIONS[providerId]?.name || + providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + const isDynamic = (DYNAMIC_MODEL_PROVIDERS as readonly string[]).includes(providerId) + const checkboxId = `provider-${providerId}` + + return ( +
+
+ onToggleProvider()} + /> +
+ {ProviderIcon && } +
+ +
+ {expanded && isProviderAllowed && ( +
+ {isDynamic ? ( + + ) : ( + + )} +
+ )} +
+ ) +} + export function AccessControl() { const params = useParams() const workspaceId = typeof params?.workspaceId === 'string' ? params.workspaceId : undefined @@ -748,6 +938,65 @@ export function AccessControl() { [editingConfig] ) + const isModelAllowed = useCallback( + (model: string) => { + if (!editingConfig) return true + const normalized = model.toLowerCase() + return !editingConfig.deniedModels.some((denied) => denied.toLowerCase() === normalized) + }, + [editingConfig] + ) + + const toggleModel = useCallback( + (model: string) => { + if (!editingConfig) return + const normalized = model.toLowerCase() + const isDenied = editingConfig.deniedModels.some( + (denied) => denied.toLowerCase() === normalized + ) + const deniedModels = isDenied + ? editingConfig.deniedModels.filter((denied) => denied.toLowerCase() !== normalized) + : [...editingConfig.deniedModels, model] + setEditingConfig({ ...editingConfig, deniedModels }) + }, + [editingConfig] + ) + + const setModelsDenied = useCallback( + (models: string[], denied: boolean) => { + if (!editingConfig) return + if (denied) { + const existing = new Set(editingConfig.deniedModels.map((m) => m.toLowerCase())) + const additions = models.filter((m) => !existing.has(m.toLowerCase())) + if (additions.length === 0) return + setEditingConfig({ + ...editingConfig, + deniedModels: [...editingConfig.deniedModels, ...additions], + }) + } else { + const toRemove = new Set(models.map((m) => m.toLowerCase())) + setEditingConfig({ + ...editingConfig, + deniedModels: editingConfig.deniedModels.filter((m) => !toRemove.has(m.toLowerCase())), + }) + } + }, + [editingConfig] + ) + + const deniedCountByProvider = useMemo(() => { + const counts: Record = {} + for (const model of editingConfig?.deniedModels ?? []) { + try { + const providerId = getProviderFromModel(model) + counts[providerId] = (counts[providerId] ?? 0) + 1 + } catch { + // Unknown/blacklisted provider — omit from counts. + } + } + return counts + }, [editingConfig?.deniedModels]) + const availableMembersToAdd = useMemo(() => { const existingMemberUserIds = new Set(members.map((m) => m.userId)) return workspaceMembers.filter((m) => !existingMemberUserIds.has(m.userId)) @@ -945,31 +1194,20 @@ export function AccessControl() { : 'Select All'}
-
- {filteredProviders.map((providerId) => { - const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon - const providerName = - PROVIDER_DEFINITIONS[providerId]?.name || - providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) - const checkboxId = `provider-${providerId}` - return ( - - ) - })} +
+ {filteredProviders.map((providerId) => ( + toggleProvider(providerId)} + deniedCount={deniedCountByProvider[providerId] ?? 0} + workspaceId={workspaceId} + isModelAllowed={isModelAllowed} + onToggleModel={toggleModel} + onSetModelsDenied={setModelsDenied} + /> + ))}
diff --git a/apps/sim/ee/access-control/utils/permission-check.test.ts b/apps/sim/ee/access-control/utils/permission-check.test.ts index 0c0a3934399..5a2cf46bced 100644 --- a/apps/sim/ee/access-control/utils/permission-check.test.ts +++ b/apps/sim/ee/access-control/utils/permission-check.test.ts @@ -13,6 +13,7 @@ const { DEFAULT_PERMISSION_GROUP_CONFIG: { allowedIntegrations: null, allowedModelProviders: null, + deniedModels: [], hideTraceSpans: false, hideKnowledgeBaseTab: false, hideTablesTab: false, @@ -94,6 +95,7 @@ import { getUserPermissionConfig, IntegrationNotAllowedError, McpToolsNotAllowedError, + ModelNotAllowedError, ProviderNotAllowedError, SkillsNotAllowedError, validateBlockType, @@ -237,6 +239,42 @@ describe('validateModelProvider', () => { await validateModelProvider('user-123', 'workspace-1', 'gpt-4') }) + + it('throws ModelNotAllowedError when the model is on the denylist', async () => { + mockDbGroupMembership.value = [{ config: { deniedModels: ['gpt-4'] } }] + mockGetProviderFromModel.mockReturnValue('openai') + + await expect(validateModelProvider('user-123', 'workspace-1', 'gpt-4')).rejects.toBeInstanceOf( + ModelNotAllowedError + ) + }) + + it('denylist match is case-insensitive', async () => { + mockDbGroupMembership.value = [{ config: { deniedModels: ['Ollama/Llama3'] } }] + mockGetProviderFromModel.mockReturnValue('ollama') + + await expect( + validateModelProvider('user-123', 'workspace-1', 'ollama/llama3') + ).rejects.toBeInstanceOf(ModelNotAllowedError) + }) + + it('enforces the denylist even when no provider allowlist is set', async () => { + mockDbGroupMembership.value = [ + { config: { allowedModelProviders: null, deniedModels: ['gpt-4'] } }, + ] + mockGetProviderFromModel.mockReturnValue('openai') + + await expect(validateModelProvider('user-123', 'workspace-1', 'gpt-4')).rejects.toBeInstanceOf( + ModelNotAllowedError + ) + }) + + it('allows a model that is not on the denylist', async () => { + mockDbGroupMembership.value = [{ config: { deniedModels: ['gpt-4'] } }] + mockGetProviderFromModel.mockReturnValue('openai') + + await validateModelProvider('user-123', 'workspace-1', 'gpt-4o') + }) }) describe('validateMcpToolsAllowed', () => { @@ -281,6 +319,19 @@ describe('assertPermissionsAllowed', () => { ).rejects.toBeInstanceOf(ProviderNotAllowedError) }) + it('throws ModelNotAllowedError when the model is on the denylist', async () => { + mockDbGroupMembership.value = [{ config: { deniedModels: ['gpt-4'] } }] + mockGetProviderFromModel.mockReturnValue('openai') + + await expect( + assertPermissionsAllowed({ + userId: 'user-123', + workspaceId: 'workspace-1', + model: 'gpt-4', + }) + ).rejects.toBeInstanceOf(ModelNotAllowedError) + }) + it('throws IntegrationNotAllowedError when block type is blocked', async () => { mockDbGroupMembership.value = [{ config: { allowedIntegrations: ['slack'] } }] diff --git a/apps/sim/ee/access-control/utils/permission-check.ts b/apps/sim/ee/access-control/utils/permission-check.ts index e871cf45740..400682953fe 100644 --- a/apps/sim/ee/access-control/utils/permission-check.ts +++ b/apps/sim/ee/access-control/utils/permission-check.ts @@ -29,6 +29,13 @@ export class ProviderNotAllowedError extends Error { } } +export class ModelNotAllowedError extends Error { + constructor(model: string) { + super(`Model "${model}" is not allowed based on your permission group settings`) + this.name = 'ModelNotAllowedError' + } +} + export class IntegrationNotAllowedError extends Error { constructor(blockType: string, reason?: string) { super( @@ -168,6 +175,18 @@ async function getPermissionConfig( return getUserPermissionConfig(userId, workspaceId) } +/** + * Returns true when `model` appears in the group's model denylist. Comparison is + * case-insensitive to match the normalization applied by `getProviderFromModel`. + */ +function isModelDenied(config: PermissionGroupConfig, model: string): boolean { + if (!config.deniedModels || config.deniedModels.length === 0) { + return false + } + const normalized = model.toLowerCase() + return config.deniedModels.some((denied) => denied.toLowerCase() === normalized) +} + export async function validateModelProvider( userId: string | undefined, workspaceId: string | undefined, @@ -180,20 +199,27 @@ export async function validateModelProvider( const config = await getPermissionConfig(userId, workspaceId, ctx) - if (!config || config.allowedModelProviders === null) { + if (!config) { return } - const providerId = getProviderFromModel(model) + if (config.allowedModelProviders !== null) { + const providerId = getProviderFromModel(model) - if (!config.allowedModelProviders.includes(providerId)) { - logger.warn('Model provider blocked by permission group', { - userId, - workspaceId, - model, - providerId, - }) - throw new ProviderNotAllowedError(providerId, model) + if (!config.allowedModelProviders.includes(providerId)) { + logger.warn('Model provider blocked by permission group', { + userId, + workspaceId, + model, + providerId, + }) + throw new ProviderNotAllowedError(providerId, model) + } + } + + if (isModelDenied(config, model)) { + logger.warn('Model blocked by permission group', { userId, workspaceId, model }) + throw new ModelNotAllowedError(model) } } @@ -421,16 +447,23 @@ export async function assertPermissionsAllowed(req: PermissionAssertion): Promis ? await getPermissionConfig(userId, workspaceId, ctx) : mergeEnvAllowlist(null) - if (model && config && config.allowedModelProviders !== null) { - const providerId = getProviderFromModel(model) - if (!config.allowedModelProviders.includes(providerId)) { - logger.warn('Model provider blocked by permission group', { - userId, - workspaceId, - model, - providerId, - }) - throw new ProviderNotAllowedError(providerId, model) + if (model && config) { + if (config.allowedModelProviders !== null) { + const providerId = getProviderFromModel(model) + if (!config.allowedModelProviders.includes(providerId)) { + logger.warn('Model provider blocked by permission group', { + userId, + workspaceId, + model, + providerId, + }) + throw new ProviderNotAllowedError(providerId, model) + } + } + + if (isModelDenied(config, model)) { + logger.warn('Model blocked by permission group', { userId, workspaceId, model }) + throw new ModelNotAllowedError(model) } } diff --git a/apps/sim/hooks/use-permission-config.ts b/apps/sim/hooks/use-permission-config.ts index cc52003b49f..88ef67f7229 100644 --- a/apps/sim/hooks/use-permission-config.ts +++ b/apps/sim/hooks/use-permission-config.ts @@ -21,6 +21,7 @@ export interface PermissionConfigResult { filterProviders: (providerIds: string[]) => string[] isBlockAllowed: (blockType: string) => boolean isProviderAllowed: (providerId: string) => boolean + isModelAllowed: (model: string) => boolean isInvitationsDisabled: boolean isPublicApiDisabled: boolean } @@ -98,6 +99,14 @@ export function usePermissionConfig(): PermissionConfigResult { } }, [config.allowedModelProviders]) + const isModelAllowed = useMemo(() => { + return (model: string) => { + if (config.deniedModels.length === 0) return true + const normalized = model.toLowerCase() + return !config.deniedModels.some((denied) => denied.toLowerCase() === normalized) + } + }, [config.deniedModels]) + const filterBlocks = useMemo(() => { return (blocks: T[]): T[] => { if (mergedAllowedIntegrations === null) return blocks @@ -140,6 +149,7 @@ export function usePermissionConfig(): PermissionConfigResult { filterProviders, isBlockAllowed, isProviderAllowed, + isModelAllowed, isInvitationsDisabled, isPublicApiDisabled, }), @@ -151,6 +161,7 @@ export function usePermissionConfig(): PermissionConfigResult { filterProviders, isBlockAllowed, isProviderAllowed, + isModelAllowed, isInvitationsDisabled, isPublicApiDisabled, ] diff --git a/apps/sim/lib/api/contracts/permission-groups.ts b/apps/sim/lib/api/contracts/permission-groups.ts index 3b0bd1652e4..09bfe87176d 100644 --- a/apps/sim/lib/api/contracts/permission-groups.ts +++ b/apps/sim/lib/api/contracts/permission-groups.ts @@ -5,6 +5,7 @@ import { permissionGroupConfigSchema } from '@/lib/permission-groups/types' export const permissionGroupFullConfigSchema = z.object({ allowedIntegrations: z.array(z.string()).nullable(), allowedModelProviders: z.array(z.string()).nullable(), + deniedModels: z.array(z.string()).default([]), hideTraceSpans: z.boolean(), hideKnowledgeBaseTab: z.boolean(), hideTablesTab: z.boolean(), diff --git a/apps/sim/lib/permission-groups/types.ts b/apps/sim/lib/permission-groups/types.ts index 3630691217c..39320823081 100644 --- a/apps/sim/lib/permission-groups/types.ts +++ b/apps/sim/lib/permission-groups/types.ts @@ -8,6 +8,7 @@ export const PERMISSION_GROUP_MEMBER_CONSTRAINTS = { export const permissionGroupConfigSchema = z.object({ allowedIntegrations: z.array(z.string()).nullable().optional(), allowedModelProviders: z.array(z.string()).nullable().optional(), + deniedModels: z.array(z.string()).optional(), hideTraceSpans: z.boolean().optional(), hideKnowledgeBaseTab: z.boolean().optional(), hideTablesTab: z.boolean().optional(), @@ -32,6 +33,11 @@ export const permissionGroupConfigSchema = z.object({ export interface PermissionGroupConfig { allowedIntegrations: string[] | null allowedModelProviders: string[] | null + /** + * Fully-qualified model IDs (e.g. `ollama/llama3`, `gpt-4o`) blocked for this + * group, checked after `allowedModelProviders`. Empty means nothing is blocked. + */ + deniedModels: string[] hideTraceSpans: boolean hideKnowledgeBaseTab: boolean hideTablesTab: boolean @@ -56,6 +62,7 @@ export interface PermissionGroupConfig { export const DEFAULT_PERMISSION_GROUP_CONFIG: PermissionGroupConfig = { allowedIntegrations: null, allowedModelProviders: null, + deniedModels: [], hideTraceSpans: false, hideKnowledgeBaseTab: false, hideTablesTab: false, @@ -87,6 +94,9 @@ export function parsePermissionGroupConfig(config: unknown): PermissionGroupConf return { allowedIntegrations: Array.isArray(c.allowedIntegrations) ? c.allowedIntegrations : null, allowedModelProviders: Array.isArray(c.allowedModelProviders) ? c.allowedModelProviders : null, + deniedModels: Array.isArray(c.deniedModels) + ? c.deniedModels.filter((m): m is string => typeof m === 'string') + : [], hideTraceSpans: typeof c.hideTraceSpans === 'boolean' ? c.hideTraceSpans : false, hideKnowledgeBaseTab: typeof c.hideKnowledgeBaseTab === 'boolean' ? c.hideKnowledgeBaseTab : false, From 49673055beb6a92a538cba2edf9eafa152f84c8c Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 29 May 2026 13:47:32 -0700 Subject: [PATCH 07/14] improvement(logs): object storage backed tracespans (#4787) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improvement(logs): obj storage backed tracespans * fix storage write context * fix tests * address comments * address comments * chore(db): remove migration 0219 to regenerate after staging merge Drops the 0219_robust_shard SQL, its snapshot, and the journal entry so the trace-spans/cost schema migration can be regenerated on top of the latest staging migration chain (avoids a number collision with staging's migrations). Co-authored-by: Cursor * improvement(billing): accurate per-member usage via shared ledger helper Per-member/per-user usage in the org-member routes now adds the usage_log ledger to the currentPeriodCost baseline (which is no longer incremented), via a shared getOrgMemberLedgerByUser helper to avoid repeating the subscription→period→ledger lookup across the admin and member-facing routes. Co-authored-by: Cursor * regen migrations * update migration * address comments * more code cleanup * incorrect type cast --------- Co-authored-by: Cursor --- apps/sim/app/api/billing/route.ts | 1 - apps/sim/app/api/knowledge/utils.test.ts | 7 +- .../api/logs/execution/[executionId]/route.ts | 15 +- apps/sim/app/api/logs/export/route.ts | 41 +- apps/sim/app/api/logs/route.ts | 12 +- .../[id]/members/[memberId]/route.ts | 16 + .../api/organizations/[id]/members/route.ts | 14 + .../[id]/members/[memberId]/route.ts | 11 +- .../admin/organizations/[id]/members/route.ts | 12 +- .../admin/organizations/[id]/seats/route.ts | 10 - apps/sim/app/api/v1/admin/types.ts | 26 +- .../api/v1/admin/users/[id]/billing/route.ts | 21 +- apps/sim/app/api/v1/logs/[id]/route.ts | 14 +- .../v1/logs/executions/[executionId]/route.ts | 4 +- apps/sim/app/api/v1/logs/filters.ts | 11 +- apps/sim/app/api/v1/logs/route.ts | 56 +- .../[id]/executions/[executionId]/route.ts | 21 +- .../components/trace-view/trace-view.tsx | 17 +- .../components/log-details/log-details.tsx | 139 +- .../logs/components/log-details/utils.ts | 7 +- .../workspace-notification-delivery.ts | 40 +- apps/sim/lib/api/contracts/logs.ts | 31 +- apps/sim/lib/api/contracts/subscription.ts | 1 - .../api/contracts/v1/admin/organizations.ts | 13 - apps/sim/lib/api/contracts/v1/admin/users.ts | 13 - apps/sim/lib/api/contracts/v1/logs.ts | 9 +- apps/sim/lib/billing/core/billing.ts | 47 +- apps/sim/lib/billing/core/organization.ts | 62 +- apps/sim/lib/billing/core/usage-log.ts | 154 +- apps/sim/lib/billing/core/usage.ts | 45 - .../lib/billing/credits/conversion.test.ts | 65 + apps/sim/lib/billing/credits/conversion.ts | 77 + apps/sim/lib/billing/index.ts | 1 - apps/sim/lib/billing/types/index.ts | 2 - .../lib/billing/validation/seat-management.ts | 28 +- apps/sim/lib/billing/webhooks/invoices.ts | 48 +- apps/sim/lib/copilot/chat/process-contents.ts | 18 +- .../server/workflow/get-execution-summary.ts | 54 +- .../server/workflow/get-workflow-logs.ts | 98 +- apps/sim/lib/core/utils/concurrency.ts | 35 + .../lib/data-drains/sources/workflow-logs.ts | 21 +- apps/sim/lib/execution/payloads/store.ts | 38 +- apps/sim/lib/logs/execution/logger.test.ts | 350 + apps/sim/lib/logs/execution/logger.ts | 547 +- .../logs/execution/logging-factory.test.ts | 97 +- .../sim/lib/logs/execution/logging-factory.ts | 78 +- .../logs/execution/logging-session.test.ts | 79 +- .../sim/lib/logs/execution/logging-session.ts | 232 +- apps/sim/lib/logs/execution/trace-store.ts | 179 + apps/sim/lib/logs/fetch-log-detail.ts | 76 +- apps/sim/lib/logs/filters.ts | 3 +- apps/sim/lib/table/service.ts | 16 +- apps/sim/lib/workspaces/organization/types.ts | 1 - apps/sim/providers/utils.ts | 8 +- apps/sim/scripts/backfill-trace-spans.ts | 188 + bun.lock | 1 - packages/db/migrations/0220_early_hellion.sql | 36 + .../db/migrations/meta/0220_snapshot.json | 17582 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 87 +- packages/db/scripts/migrate.ts | 29 + 61 files changed, 20011 insertions(+), 940 deletions(-) create mode 100644 apps/sim/lib/billing/credits/conversion.test.ts create mode 100644 apps/sim/lib/core/utils/concurrency.ts create mode 100644 apps/sim/lib/logs/execution/trace-store.ts create mode 100644 apps/sim/scripts/backfill-trace-spans.ts create mode 100644 packages/db/migrations/0220_early_hellion.sql create mode 100644 packages/db/migrations/meta/0220_snapshot.json diff --git a/apps/sim/app/api/billing/route.ts b/apps/sim/app/api/billing/route.ts index 1b9411647e8..2cc338abd48 100644 --- a/apps/sim/app/api/billing/route.ts +++ b/apps/sim/app/api/billing/route.ts @@ -140,7 +140,6 @@ export const GET = withRouteHandler(async (request: NextRequest) => { members: rawBillingData.members.map((m) => ({ ...m, joinedAt: m.joinedAt.toISOString(), - lastActive: m.lastActive?.toISOString() || null, })), } diff --git a/apps/sim/app/api/knowledge/utils.test.ts b/apps/sim/app/api/knowledge/utils.test.ts index 326bbee660f..b8297e4cf65 100644 --- a/apps/sim/app/api/knowledge/utils.test.ts +++ b/apps/sim/app/api/knowledge/utils.test.ts @@ -259,7 +259,12 @@ describe('Knowledge Utils', () => { {} ) - expect(dbOps.order).toEqual(['insert', 'updateDoc']) + // Embeddings are inserted first, then the document counter update. A + // usage_log billing insert (recordUsage) may trail after updateDoc and is + // irrelevant to this ordering invariant, so assert position rather than + // exact array equality. + expect(dbOps.order[0]).toBe('insert') + expect(dbOps.order.indexOf('updateDoc')).toBeGreaterThan(0) expect(dbOps.updatePayloads[0]).toMatchObject({ processingStatus: 'completed', diff --git a/apps/sim/app/api/logs/execution/[executionId]/route.ts b/apps/sim/app/api/logs/execution/[executionId]/route.ts index 71deb54267d..adab287bf99 100644 --- a/apps/sim/app/api/logs/execution/[executionId]/route.ts +++ b/apps/sim/app/api/logs/execution/[executionId]/route.ts @@ -13,6 +13,7 @@ import { executionIdParamsSchema } from '@/lib/api/contracts/logs' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types' const logger = createLogger('LogsByExecutionIdAPI') @@ -39,13 +40,14 @@ export const GET = withRouteHandler( .select({ id: workflowExecutionLogs.id, workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, executionId: workflowExecutionLogs.executionId, stateSnapshotId: workflowExecutionLogs.stateSnapshotId, trigger: workflowExecutionLogs.trigger, startedAt: workflowExecutionLogs.startedAt, endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, executionData: workflowExecutionLogs.executionData, }) .from(workflowExecutionLogs) @@ -119,7 +121,14 @@ export const GET = withRouteHandler( return NextResponse.json({ error: 'Workflow state snapshot not found' }, { status: 404 }) } - const executionData = workflowLog.executionData as WorkflowExecutionLog['executionData'] + const executionData = (await materializeExecutionData( + workflowLog.executionData as Record | null, + { + workspaceId: workflowLog.workspaceId, + workflowId: workflowLog.workflowId, + executionId: workflowLog.executionId, + } + )) as WorkflowExecutionLog['executionData'] const traceSpans = (executionData?.traceSpans as TraceSpan[]) || [] const childSnapshotIds = new Set() const collectSnapshotIds = (spans: TraceSpan[]) => { @@ -163,7 +172,7 @@ export const GET = withRouteHandler( startedAt: workflowLog.startedAt.toISOString(), endedAt: workflowLog.endedAt?.toISOString(), totalDurationMs: workflowLog.totalDurationMs, - cost: workflowLog.cost || null, + cost: workflowLog.costTotal != null ? { total: Number(workflowLog.costTotal) } : null, }, } diff --git a/apps/sim/app/api/logs/export/route.ts b/apps/sim/app/api/logs/export/route.ts index 2c817411b68..766435eadd4 100644 --- a/apps/sim/app/api/logs/export/route.ts +++ b/apps/sim/app/api/logs/export/route.ts @@ -4,7 +4,9 @@ import { createLogger } from '@sim/logger' import { and, desc, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' +import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { buildFilterConditions, LogFilterParamsSchema } from '@/lib/logs/filters' import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' @@ -41,7 +43,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { startedAt: workflowExecutionLogs.startedAt, endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, executionData: workflowExecutionLogs.executionData, workflowName: sql`COALESCE(${workflow.name}, 'Deleted Workflow')`, } @@ -96,11 +98,29 @@ export const GET = withRouteHandler(async (request: NextRequest) => { if (!rows.length) break - for (const r of rows as any[]) { + // Heavy execution data may live in object storage; materialize per + // row with bounded concurrency so a 1000-row page doesn't fan out + // into 1000 simultaneous reads. + const materialized = await mapWithConcurrency( + rows as any[], + MATERIALIZE_CONCURRENCY, + (r) => + materializeExecutionData(r.executionData as Record | null, { + workspaceId: params.workspaceId, + workflowId: r.workflowId, + executionId: r.executionId, + }) + ) + + for (let j = 0; j < rows.length; j++) { + const r = rows[j] as any + const ed = materialized[j] as Record + // A single malformed/unserializable row must not abort the whole CSV + // stream — derive the message/trace columns defensively and fall back + // to empty on error so the row's metadata still exports. let message = '' - let traces: any = null + let tracesJson = '' try { - const ed = (r as any).executionData if (ed) { if (ed.finalOutput) message = @@ -108,20 +128,25 @@ export const GET = withRouteHandler(async (request: NextRequest) => { ? ed.finalOutput : JSON.stringify(ed.finalOutput) if (ed.message) message = ed.message - if (ed.traceSpans) traces = ed.traceSpans + if (ed.traceSpans) tracesJson = JSON.stringify(ed.traceSpans) } - } catch {} + } catch (rowError) { + logger.warn('Skipping unserializable execution data for export row', { + executionId: r.executionId, + error: rowError instanceof Error ? rowError.message : String(rowError), + }) + } const line = [ escapeCsv(r.startedAt?.toISOString?.() || r.startedAt), escapeCsv(r.level), escapeCsv(r.workflowName), escapeCsv(r.trigger), escapeCsv(r.totalDurationMs ?? ''), - escapeCsv(r.cost?.total ?? r.cost?.value?.total ?? ''), + escapeCsv(r.costTotal ?? ''), escapeCsv(r.workflowId ?? ''), escapeCsv(r.executionId ?? ''), escapeCsv(message), - escapeCsv(traces ? JSON.stringify(traces) : ''), + escapeCsv(tracesJson), ].join(',') controller.enqueue(encoder.encode(`${line}\n`)) } diff --git a/apps/sim/app/api/logs/route.ts b/apps/sim/app/api/logs/route.ts index 89f52048b72..548383c1035 100644 --- a/apps/sim/app/api/logs/route.ts +++ b/apps/sim/app/api/logs/route.ts @@ -31,6 +31,7 @@ import { listLogsContract, type WorkflowLogSummary } from '@/lib/api/contracts/l import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { jobCostTotal } from '@/lib/logs/fetch-log-detail' import { buildFilterConditions } from '@/lib/logs/filters' import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' @@ -81,7 +82,8 @@ export const GET = withRouteHandler(async (request: NextRequest) => { case 'duration': return sql`${workflowExecutionLogs.totalDurationMs}` case 'cost': - return sql`(${workflowExecutionLogs.cost}->>'total')::numeric` + // Indexed projection of the usage_log ledger (dollars); no live aggregation. + return sql`${workflowExecutionLogs.costTotal}` case 'status': return sql`${workflowExecutionLogs.status}` default: @@ -201,7 +203,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { startedAt: workflowExecutionLogs.startedAt, endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, createdAt: workflowExecutionLogs.createdAt, workflowName: workflow.name, workflowDescription: workflow.description, @@ -379,7 +381,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => { } : null, jobTitle: null, - cost: (log.cost as WorkflowLogSummary['cost']) ?? null, + // List cost is the cost_total projection (faithful ledger sum). Null until + // completion (running) or until the one-time legacy backfill populates it. + cost: log.costTotal != null ? { total: Number(log.costTotal) } : null, pauseSummary: { status: log.pausedStatus ?? null, total: totalPauseCount, @@ -405,7 +409,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { createdAt: log.startedAt.toISOString(), workflow: null, jobTitle: log.jobTitle ?? null, - cost: (log.cost as WorkflowLogSummary['cost']) ?? null, + cost: jobCostTotal(log.cost), pauseSummary: { status: null, total: 0, resumed: 0 }, hasPendingPause: false, } diff --git a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts index 89d2cc81cb7..ed6bcf4d5b8 100644 --- a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts @@ -11,6 +11,7 @@ import { import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { setActiveOrganizationForCurrentSession } from '@/lib/auth/active-organization' +import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' import { getUserUsageData } from '@/lib/billing/core/usage' import { removeExternalUserFromOrganizationWorkspaces, @@ -101,10 +102,25 @@ export const GET = withRouteHandler( const computed = await getUserUsageData(memberId) if (usageData.length > 0) { + // currentPeriodCost is only a baseline; add this member's attributed + // usage_log for the period. (getUserUsageData returns the org POOL for + // org-scoped members, so it can't supply the per-member figure.) + const memberLedger = + ( + await getOrgMemberLedgerByUser( + organizationId, + computed.billingPeriodStart && computed.billingPeriodEnd + ? { start: computed.billingPeriodStart, end: computed.billingPeriodEnd } + : null + ) + ).get(memberId) ?? 0 memberData = { ...memberData, usage: { ...usageData[0], + currentPeriodCost: ( + Number(usageData[0].currentPeriodCost ?? 0) + memberLedger + ).toString(), billingPeriodStart: computed.billingPeriodStart, billingPeriodEnd: computed.billingPeriodEnd, }, diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts index 48a356215cf..e412a7e635f 100644 --- a/apps/sim/app/api/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/route.ts @@ -17,6 +17,7 @@ import { } from '@/lib/api/contracts/organization' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' +import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' import { validateSeatAvailability } from '@/lib/billing/validation/seat-management' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -139,8 +140,21 @@ export const GET = withRouteHandler( const billingPeriodStart = orgSub?.periodStart ?? null const billingPeriodEnd = orgSub?.periodEnd ?? null + // currentPeriodCost is only a baseline; add each member's attributed + // usage_log for the period (batched, one query) so the roster shows real + // usage rather than the frozen baseline. + const usageByUser = await getOrgMemberLedgerByUser( + organizationId, + billingPeriodStart && billingPeriodEnd + ? { start: billingPeriodStart, end: billingPeriodEnd } + : null + ) + const membersWithUsage = base.map((row) => ({ ...row, + currentPeriodCost: ( + Number(row.currentPeriodCost ?? 0) + (usageByUser.get(row.userId) ?? 0) + ).toString(), billingPeriodStart, billingPeriodEnd, })) diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts index 4dadf2cf93e..7a33e113133 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts @@ -35,6 +35,7 @@ import { adminV1UpdateOrganizationMemberContract, } from '@/lib/api/contracts/v1/admin' import { parseRequest } from '@/lib/api/server' +import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' import { removeUserFromOrganization } from '@/lib/billing/organizations/membership' import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -86,7 +87,6 @@ export const GET = withRouteHandler( userEmail: user.email, currentPeriodCost: userStats.currentPeriodCost, currentUsageLimit: userStats.currentUsageLimit, - lastActive: userStats.lastActive, billingBlocked: userStats.billingBlocked, }) .from(member) @@ -99,6 +99,10 @@ export const GET = withRouteHandler( return notFoundResponse('Member') } + // currentPeriodCost is only a baseline; add this member's attributed + // usage_log for the org's period so admin shows real current usage. + const ledgerByUser = await getOrgMemberLedgerByUser(organizationId) + const data: AdminMemberDetail = { id: memberData.id, userId: memberData.userId, @@ -107,9 +111,10 @@ export const GET = withRouteHandler( createdAt: memberData.createdAt.toISOString(), userName: memberData.userName, userEmail: memberData.userEmail, - currentPeriodCost: memberData.currentPeriodCost ?? '0', + currentPeriodCost: ( + Number(memberData.currentPeriodCost ?? 0) + (ledgerByUser.get(memberData.userId) ?? 0) + ).toString(), currentUsageLimit: memberData.currentUsageLimit, - lastActive: memberData.lastActive?.toISOString() ?? null, billingBlocked: memberData.billingBlocked ?? false, } diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts index 0c5b50fefd6..5c4466d182d 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts @@ -37,6 +37,7 @@ import { adminV1ListOrganizationMembersContract, } from '@/lib/api/contracts/v1/admin' import { parseRequest } from '@/lib/api/server' +import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' import { addUserToOrganization } from '@/lib/billing/organizations/membership' import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -96,7 +97,6 @@ export const GET = withRouteHandler( userEmail: user.email, currentPeriodCost: userStats.currentPeriodCost, currentUsageLimit: userStats.currentUsageLimit, - lastActive: userStats.lastActive, billingBlocked: userStats.billingBlocked, }) .from(member) @@ -109,6 +109,11 @@ export const GET = withRouteHandler( ]) const total = countResult[0].count + + // currentPeriodCost is only a baseline; add each member's attributed + // usage_log for the org's period so admin shows real current usage. + const usageByUser = await getOrgMemberLedgerByUser(organizationId) + const data: AdminMemberDetail[] = membersData.map((m) => ({ id: m.id, userId: m.userId, @@ -117,9 +122,10 @@ export const GET = withRouteHandler( createdAt: m.createdAt.toISOString(), userName: m.userName, userEmail: m.userEmail, - currentPeriodCost: m.currentPeriodCost ?? '0', + currentPeriodCost: ( + Number(m.currentPeriodCost ?? 0) + (usageByUser.get(m.userId) ?? 0) + ).toString(), currentUsageLimit: m.currentUsageLimit, - lastActive: m.lastActive?.toISOString() ?? null, billingBlocked: m.billingBlocked ?? false, })) diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/seats/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/seats/route.ts index 6c015190408..921131e2409 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/seats/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/seats/route.ts @@ -51,16 +51,6 @@ export const GET = withRouteHandler( subscriptionPlan: analytics.subscriptionPlan, canAddSeats: analytics.canAddSeats, utilizationRate: analytics.utilizationRate, - activeMembers: analytics.activeMembers, - inactiveMembers: analytics.inactiveMembers, - memberActivity: analytics.memberActivity.map((m) => ({ - userId: m.userId, - userName: m.userName, - userEmail: m.userEmail, - role: m.role, - joinedAt: m.joinedAt.toISOString(), - lastActive: m.lastActive?.toISOString() ?? null, - })), } logger.info(`Admin API: Retrieved seat analytics for organization ${organizationId}`) diff --git a/apps/sim/app/api/v1/admin/types.ts b/apps/sim/app/api/v1/admin/types.ts index 3cdbfc46d27..eb3ce167e0a 100644 --- a/apps/sim/app/api/v1/admin/types.ts +++ b/apps/sim/app/api/v1/admin/types.ts @@ -543,7 +543,6 @@ export interface AdminMemberDetail extends AdminMember { // Billing/usage info from userStats currentPeriodCost: string currentUsageLimit: string | null - lastActive: string | null billingBlocked: boolean } @@ -574,28 +573,15 @@ interface AdminUserBilling { userEmail: string stripeCustomerId: string | null // Usage stats - totalManualExecutions: number - totalApiCalls: number - totalWebhookTriggers: number - totalScheduledExecutions: number - totalChatExecutions: number - totalMcpExecutions: number - totalA2aExecutions: number - totalTokensUsed: number - totalCost: string currentUsageLimit: string | null currentPeriodCost: string lastPeriodCost: string | null billedOverageThisPeriod: string storageUsedBytes: number - lastActive: string | null billingBlocked: boolean - // Copilot usage - totalCopilotCost: string + // Copilot usage (active per-period baselines) currentPeriodCopilotCost: string lastPeriodCopilotCost: string | null - totalCopilotTokens: number - totalCopilotCalls: number } export interface AdminUserBillingWithSubscription extends AdminUserBilling { @@ -643,16 +629,6 @@ export interface AdminSeatAnalytics { subscriptionPlan: string canAddSeats: boolean utilizationRate: number - activeMembers: number - inactiveMembers: number - memberActivity: Array<{ - userId: string - userName: string - userEmail: string - role: string - joinedAt: string - lastActive: string | null - }> } export interface AdminDeploymentVersion { diff --git a/apps/sim/app/api/v1/admin/users/[id]/billing/route.ts b/apps/sim/app/api/v1/admin/users/[id]/billing/route.ts index 7c080a4e6e5..85a5f69b63a 100644 --- a/apps/sim/app/api/v1/admin/users/[id]/billing/route.ts +++ b/apps/sim/app/api/v1/admin/users/[id]/billing/route.ts @@ -29,6 +29,7 @@ import { } from '@/lib/api/contracts/v1/admin' import { parseRequest } from '@/lib/api/server' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' +import { getUserUsageData } from '@/lib/billing/core/usage' import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' @@ -78,6 +79,11 @@ export const GET = withRouteHandler( const [stats] = await db.select().from(userStats).where(eq(userStats.userId, userId)).limit(1) + // currentPeriodCost is now only a baseline; canonical current-period usage + // (baseline + attributed usage_log, refresh-adjusted) comes from the same + // helper users see, so admin reflects real usage instead of a stale 0. + const usage = await getUserUsageData(userId) + const memberOrgs = await db .select({ organizationId: member.organizationId, @@ -107,27 +113,14 @@ export const GET = withRouteHandler( userName: userData.name, userEmail: userData.email, stripeCustomerId: userData.stripeCustomerId, - totalManualExecutions: stats?.totalManualExecutions ?? 0, - totalApiCalls: stats?.totalApiCalls ?? 0, - totalWebhookTriggers: stats?.totalWebhookTriggers ?? 0, - totalScheduledExecutions: stats?.totalScheduledExecutions ?? 0, - totalChatExecutions: stats?.totalChatExecutions ?? 0, - totalMcpExecutions: stats?.totalMcpExecutions ?? 0, - totalA2aExecutions: stats?.totalA2aExecutions ?? 0, - totalTokensUsed: stats?.totalTokensUsed ?? 0, - totalCost: stats?.totalCost ?? '0', currentUsageLimit: stats?.currentUsageLimit ?? null, - currentPeriodCost: stats?.currentPeriodCost ?? '0', + currentPeriodCost: usage.currentUsage.toString(), lastPeriodCost: stats?.lastPeriodCost ?? null, billedOverageThisPeriod: stats?.billedOverageThisPeriod ?? '0', storageUsedBytes: stats?.storageUsedBytes ?? 0, - lastActive: stats?.lastActive?.toISOString() ?? null, billingBlocked: stats?.billingBlocked ?? false, - totalCopilotCost: stats?.totalCopilotCost ?? '0', currentPeriodCopilotCost: stats?.currentPeriodCopilotCost ?? '0', lastPeriodCopilotCost: stats?.lastPeriodCopilotCost ?? null, - totalCopilotTokens: stats?.totalCopilotTokens ?? 0, - totalCopilotCalls: stats?.totalCopilotCalls ?? 0, subscriptions: subscriptions.map(toAdminSubscription), organizationMemberships: memberOrgs.map((m) => ({ organizationId: m.organizationId, diff --git a/apps/sim/app/api/v1/logs/[id]/route.ts b/apps/sim/app/api/v1/logs/[id]/route.ts index c32acfd444c..9858a1b4e24 100644 --- a/apps/sim/app/api/v1/logs/[id]/route.ts +++ b/apps/sim/app/api/v1/logs/[id]/route.ts @@ -7,6 +7,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { v1GetLogContract } from '@/lib/api/contracts/v1/logs' import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' import { checkRateLimit, @@ -50,7 +51,7 @@ export const GET = withRouteHandler( endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, executionData: workflowExecutionLogs.executionData, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, files: workflowExecutionLogs.files, createdAt: workflowExecutionLogs.createdAt, workflowName: workflow.name, @@ -101,8 +102,15 @@ export const GET = withRouteHandler( totalDurationMs: log.totalDurationMs, files: log.files || undefined, workflow: workflowSummary, - executionData: log.executionData as any, - cost: log.cost as any, + executionData: (await materializeExecutionData( + log.executionData as Record | null, + { + workspaceId: log.workspaceId, + workflowId: log.workflowId, + executionId: log.executionId, + } + )) as any, + cost: log.costTotal != null ? { total: Number(log.costTotal) } : null, createdAt: log.createdAt.toISOString(), } diff --git a/apps/sim/app/api/v1/logs/executions/[executionId]/route.ts b/apps/sim/app/api/v1/logs/executions/[executionId]/route.ts index e7503ecb071..eefad39bb80 100644 --- a/apps/sim/app/api/v1/logs/executions/[executionId]/route.ts +++ b/apps/sim/app/api/v1/logs/executions/[executionId]/route.ts @@ -70,7 +70,9 @@ export const GET = withRouteHandler( startedAt: workflowLog.startedAt.toISOString(), endedAt: workflowLog.endedAt?.toISOString(), totalDurationMs: workflowLog.totalDurationMs, - cost: workflowLog.cost || null, + // Sourced from the cost_total projection of the usage_log ledger + // (the deprecated cost jsonb column was dropped). + cost: workflowLog.costTotal != null ? { total: Number(workflowLog.costTotal) } : null, }, } diff --git a/apps/sim/app/api/v1/logs/filters.ts b/apps/sim/app/api/v1/logs/filters.ts index 4f9bdbdfc6c..0e409e4d53f 100644 --- a/apps/sim/app/api/v1/logs/filters.ts +++ b/apps/sim/app/api/v1/logs/filters.ts @@ -84,18 +84,19 @@ export function buildLogFilters(filters: LogFilters): SQL { conditions.push(lte(workflowExecutionLogs.totalDurationMs, filters.maxDurationMs)) } - // Cost filters + // Cost filters — indexed projection of the usage_log ledger (dollars). if (filters.minCost !== undefined) { - conditions.push(sql`(${workflowExecutionLogs.cost}->>'total')::numeric >= ${filters.minCost}`) + conditions.push(sql`${workflowExecutionLogs.costTotal} >= ${filters.minCost}`) } if (filters.maxCost !== undefined) { - conditions.push(sql`(${workflowExecutionLogs.cost}->>'total')::numeric <= ${filters.maxCost}`) + conditions.push(sql`${workflowExecutionLogs.costTotal} <= ${filters.maxCost}`) } - // Model filter + // Model filter — uses the models_used projection (includes zero-cost/BYOK + // models, which the usage_log ledger drops), preserving prior behavior. if (filters.model) { - conditions.push(sql`${workflowExecutionLogs.cost}->>'models' ? ${filters.model}`) + conditions.push(sql`${workflowExecutionLogs.modelsUsed} @> ARRAY[${filters.model}]::text[]`) } // Combine all conditions with AND diff --git a/apps/sim/app/api/v1/logs/route.ts b/apps/sim/app/api/v1/logs/route.ts index 0f8f7b31b82..c85031e3cc4 100644 --- a/apps/sim/app/api/v1/logs/route.ts +++ b/apps/sim/app/api/v1/logs/route.ts @@ -6,7 +6,9 @@ import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { v1ListLogsContract } from '@/lib/api/contracts/v1/logs' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' +import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { buildLogFilters, getOrderBy } from '@/app/api/v1/logs/filters' import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' import { @@ -103,6 +105,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { .select({ id: workflowExecutionLogs.id, workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, executionId: workflowExecutionLogs.executionId, deploymentVersionId: workflowExecutionLogs.deploymentVersionId, level: workflowExecutionLogs.level, @@ -110,7 +113,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { startedAt: workflowExecutionLogs.startedAt, endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, files: workflowExecutionLogs.files, executionData: params.details === 'full' ? workflowExecutionLogs.executionData : sql`null`, workflowName: workflow.name, @@ -144,7 +147,12 @@ export const GET = withRouteHandler(async (request: NextRequest) => { }) } - const formattedLogs = data.map((log) => { + // Only materialize externalized execution data when the response actually + // needs it (details=full + finalOutput/traceSpans requested). + const needsMaterialize = + params.details === 'full' && (params.includeFinalOutput || params.includeTraceSpans) + + const buildBase = (log: (typeof data)[number]) => { const result: any = { id: log.id, workflowId: log.workflowId, @@ -155,7 +163,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { startedAt: log.startedAt.toISOString(), endedAt: log.endedAt?.toISOString() || null, totalDurationMs: log.totalDurationMs, - cost: log.cost ? { total: (log.cost as any).total } : null, + cost: log.costTotal != null ? { total: Number(log.costTotal) } : null, files: log.files || null, } @@ -166,24 +174,36 @@ export const GET = withRouteHandler(async (request: NextRequest) => { description: log.workflowDescription, deleted: !log.workflowName, } - - if (log.cost) { - result.cost = log.cost - } - - if (log.executionData) { - const execData = log.executionData as any - if (params.includeFinalOutput && execData.finalOutput) { - result.finalOutput = execData.finalOutput - } - if (params.includeTraceSpans && execData.traceSpans) { - result.traceSpans = execData.traceSpans - } - } } return result - }) + } + + // Only run the bounded-concurrency materialization when the response actually + // needs object-storage reads; otherwise a plain synchronous map avoids the + // per-row worker/promise overhead. + const formattedLogs = needsMaterialize + ? await mapWithConcurrency(data, MATERIALIZE_CONCURRENCY, async (log) => { + const result = buildBase(log) + if (log.executionData) { + const execData = (await materializeExecutionData( + log.executionData as Record | null, + { + workspaceId: log.workspaceId, + workflowId: log.workflowId, + executionId: log.executionId, + } + )) as any + if (params.includeFinalOutput && execData.finalOutput) { + result.finalOutput = execData.finalOutput + } + if (params.includeTraceSpans && execData.traceSpans) { + result.traceSpans = execData.traceSpans + } + } + return result + }) + : data.map(buildBase) const limits = await getUserLimits(userId) diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts index 6efff82a7cc..fbbdb0abb5f 100644 --- a/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/route.ts @@ -9,6 +9,7 @@ import { } from '@/lib/api/contracts/workflows' import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { validateWorkflowAccess } from '@/app/api/workflows/middleware' import type { PausePoint } from '@/executor/types' @@ -117,6 +118,7 @@ export const GET = withRouteHandler( .select({ executionId: workflowExecutionLogs.executionId, workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, status: workflowExecutionLogs.status, level: workflowExecutionLogs.level, trigger: workflowExecutionLogs.trigger, @@ -124,7 +126,7 @@ export const GET = withRouteHandler( endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, executionData: workflowExecutionLogs.executionData, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, }) .from(workflowExecutionLogs) .where( @@ -177,13 +179,20 @@ export const GET = withRouteHandler( } } - const cost = logRow.cost - ? { total: Number((logRow.cost as { total?: number }).total ?? 0) } - : null + const cost = logRow.costTotal != null ? { total: Number(logRow.costTotal) } : null - const error = status === 'failed' ? extractError(logRow.executionData) : null + // Heavy execution data may live in object storage; resolve the pointer + // before reading error / finalOutput / traceSpans (no-op for inline rows). + const executionData = (await materializeExecutionData( + logRow.executionData as Record | null, + { + workspaceId: logRow.workspaceId, + workflowId: logRow.workflowId, + executionId: logRow.executionId, + } + )) as ExecutionDataShape | undefined - const executionData = logRow.executionData as ExecutionDataShape | undefined + const error = status === 'failed' ? extractError(executionData) : null const finalOutput = includeOutput && status === 'completed' && executionData diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index 07a3cf78181..5a170bb9485 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -55,6 +55,13 @@ const MIN_BAR_PCT = 0.5 interface TraceViewProps { traceSpans: TraceSpan[] + /** + * Authoritative, multiplier-inclusive run cost (dollars) from the persisted + * execution log. When provided it drives the header credit chip so the Trace + * tab and the Overview cost breakdown can never show different totals. Falls + * back to the root span's own cost only when absent (e.g. live previews). + */ + runCostDollars?: number } interface FlatSpanEntry { @@ -708,8 +715,10 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa if (cacheRead) metaEntries.push({ label: 'Cache read', value: cacheRead }) if (cacheWrite) metaEntries.push({ label: 'Cache write', value: cacheWrite }) if (reasoning) metaEntries.push({ label: 'Reasoning tokens', value: reasoning }) - const costTotal = formatCostAmount(span.cost?.total) - if (costTotal) metaEntries.push({ label: 'Cost', value: costTotal }) + // Per-span cost is intentionally not shown: cost lives only in the usage_log + // ledger (the authoritative, multiplier-inclusive run total drives the header + // chip). Persisted spans are cost-stripped, so a per-span row would render on + // live runs but vanish on reload — show one consistent total instead. if (span.errorType) metaEntries.push({ label: 'Error type', value: span.errorType }) if (span.iterationIndex !== undefined) metaEntries.push({ label: 'Iteration', value: String(span.iterationIndex + 1) }) @@ -812,7 +821,7 @@ const TraceDetailPane = memo(function TraceDetailPane({ span }: { span: TraceSpa * in a way that mirrors the executor's internal structure so investigators can * follow block-by-block and segment-by-segment what happened and why. */ -export const TraceView = memo(function TraceView({ traceSpans }: TraceViewProps) { +export const TraceView = memo(function TraceView({ traceSpans, runCostDollars }: TraceViewProps) { const treeRef = useRef(null) const [searchQuery, setSearchQuery] = useState('') const [treePaneWidth, setTreePaneWidth] = useState(DEFAULT_TREE_PANE_WIDTH) @@ -1021,7 +1030,7 @@ export const TraceView = memo(function TraceView({ traceSpans }: TraceViewProps) {blockCount} {blockCount === 1 ? 'span' : 'spans'} {(() => { - const rootCost = formatCostAmount(normalizedSpans[0]?.cost?.total) + const rootCost = formatCostAmount(runCostDollars ?? normalizedSpans[0]?.cost?.total) return rootCost ? ( {rootCost} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index a4bf55ea765..615b885a0cb 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -24,6 +24,7 @@ import { } from '@/components/emcn' import type { WorkflowLogRow } from '@/lib/api/contracts/logs' import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants' +import { apportionCredits, dollarsToCredits } from '@/lib/billing/credits/conversion' import { cn } from '@/lib/core/utils/cn' import { handleKeyboardActivation } from '@/lib/core/utils/keyboard' import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-spans' @@ -49,6 +50,16 @@ import { formatCost } from '@/providers/utils' import { useLogDetailsUIStore } from '@/stores/logs/store' import { MAX_LOG_DETAILS_WIDTH_RATIO, MIN_LOG_DETAILS_WIDTH } from '@/stores/logs/utils' +/** + * Renders an already-apportioned integer credit value. `dollars` is only used + * to distinguish a genuine zero ("0 credits") from a sub-credit charge that + * rounded down to zero ("<1 credit"); the credit figure itself is authoritative. + */ +function creditLabel(credits: number, dollars: number): string { + if (credits <= 0) return dollars > 0 ? '<1 credit' : '0 credits' + return `${credits.toLocaleString()} ${credits === 1 ? 'credit' : 'credits'}` +} + export const WorkflowOutputSection = memo( function WorkflowOutputSection({ output }: { output: Record }) { const contentRef = useRef(null) @@ -326,6 +337,54 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP return { input: raw } as Record }, [log.executionData]) + // Cost breakdown, sourced solely from the usage_log ledger (single source of + // truth). Line items (Base Run / per-model / per-integration) get integer + // credits apportioned with a single round at the total so rows always + // reconcile (never round-then-sum, which drifts). Pre-ledger runs that only + // have the cost_total projection show the total alone — no itemization, no + // parallel jsonb reconstruction. + const costBreakdown = useMemo((): { + rows: Array<{ key: string; label: string; credits: number; dollars: number }> + totalCredits: number + totalDollars: number + tokens: { input: number; output: number } + } | null => { + const ledger = log.costLedger + if (ledger && ledger.items.length > 0) { + const credits = apportionCredits( + ledger.items.map((item, i) => ({ key: String(i), dollars: item.cost })) + ) + const rows = ledger.items.map((item, i) => ({ + key: String(i), + label: + item.category === 'fixed' && item.description === 'execution_fee' + ? 'Base Run' + : item.description, + credits: credits[String(i)] ?? 0, + dollars: item.cost, + })) + return { + rows, + totalCredits: dollarsToCredits(ledger.total), + totalDollars: ledger.total, + tokens: { + input: ledger.items.reduce((s, it) => s + (it.inputTokens ?? 0), 0), + output: ledger.items.reduce((s, it) => s + (it.outputTokens ?? 0), 0), + }, + } + } + + // Total-only (pre-ledger runs with just the cost_total projection). + const total = log.cost?.total + if (total == null) return null + return { + rows: [], + totalCredits: dollarsToCredits(total), + totalDollars: total, + tokens: { input: 0, output: 0 }, + } + }, [log.costLedger, log.cost]) + const formattedTimestamp = formatDate(log.createdAt) const logStatus = getDisplayStatus(log.status) @@ -529,67 +588,39 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP {log.files && log.files.length > 0 && } {/* Cost Breakdown */} - {hasCostInfo && ( + {hasCostInfo && costBreakdown && (
-
- - Base Run - - - {formatCost(BASE_EXECUTION_CHARGE)} - -
-
- - Model Input - - - {formatCost(log.cost?.input || 0)} - -
-
- - Model Output - - - {formatCost(log.cost?.output || 0)} - -
- {(() => { - const models = (log.cost as Record)?.models as - | Record - | undefined - const totalToolCost = models - ? Object.values(models).reduce((sum, m) => sum + (m?.toolCost || 0), 0) - : 0 - return totalToolCost > 0 ? ( -
- - Tool Usage - - - {formatCost(totalToolCost)} - -
- ) : null - })()} + {costBreakdown.rows.map((row) => ( +
+ + {row.label} + + + {creditLabel(row.credits, row.dollars)} + +
+ ))}
Total - {formatCost(log.cost?.total || 0)} - -
-
- - Tokens - - - {log.cost?.tokens?.input || log.cost?.tokens?.prompt || 0} in ·{' '} - {log.cost?.tokens?.output || log.cost?.tokens?.completion || 0} out + {creditLabel(costBreakdown.totalCredits, costBreakdown.totalDollars)}
+ {(costBreakdown.tokens.input > 0 || costBreakdown.tokens.output > 0) && ( +
+ + Tokens + + + {costBreakdown.tokens.input} in · {costBreakdown.tokens.output} out + +
+ )}

Total includes a {formatCost(BASE_EXECUTION_CHARGE)} base charge plus model and @@ -608,7 +639,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP className='mt-3 min-h-0 flex-1 overflow-hidden focus-visible:outline-none' > {traceSpans?.length ? ( - + ) : log.executionData ? (

diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts index 4f142b0c67d..09ead88ac8d 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/utils.ts @@ -1,6 +1,6 @@ import type React from 'react' import { AgentSkillsIcon, WorkflowIcon } from '@/components/icons' -import { dollarsToCredits } from '@/lib/billing/credits/conversion' +import { formatCreditCost } from '@/lib/billing/credits/conversion' import type { TraceSpan } from '@/lib/logs/types' import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config' import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config' @@ -117,10 +117,7 @@ export function getDisplayName(span: TraceSpan): string { } export function formatCostAmount(value: number | undefined): string | undefined { - if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined - const credits = dollarsToCredits(value) - if (credits <= 0) return '<1 credit' - return `${credits.toLocaleString('en-US')} ${credits === 1 ? 'credit' : 'credits'}` + return formatCreditCost(value, { emptyForZeroOrLess: true }) } export function formatTokensSummary(tokens: TraceSpan['tokens']): string | undefined { diff --git a/apps/sim/background/workspace-notification-delivery.ts b/apps/sim/background/workspace-notification-delivery.ts index 1baaa3b20b4..76c2333d9fd 100644 --- a/apps/sim/background/workspace-notification-delivery.ts +++ b/apps/sim/background/workspace-notification-delivery.ts @@ -26,6 +26,7 @@ import { RateLimiter } from '@/lib/core/rate-limiter' import { decryptSecret } from '@/lib/core/security/encryption' import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { getBaseUrl } from '@/lib/core/utils/urls' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types' import { sendEmail } from '@/lib/messaging/email/mailer' import type { AlertConfig } from '@/lib/notifications/alert-rules' @@ -507,27 +508,6 @@ function formatLogDate(value: Date | string | null | undefined, fallback = ''): return typeof value === 'string' ? value : fallback } -function normalizeLogCost(value: unknown): WorkflowExecutionLog['cost'] { - if (!isRecord(value)) { - return undefined - } - - const tokens = isRecord(value.tokens) - ? { - input: typeof value.tokens.input === 'number' ? value.tokens.input : undefined, - output: typeof value.tokens.output === 'number' ? value.tokens.output : undefined, - total: typeof value.tokens.total === 'number' ? value.tokens.total : undefined, - } - : undefined - - return { - input: typeof value.input === 'number' ? value.input : undefined, - output: typeof value.output === 'number' ? value.output : undefined, - total: typeof value.total === 'number' ? value.total : undefined, - tokens, - } -} - function normalizeLogFiles(value: unknown): WorkflowExecutionLog['files'] { if (!Array.isArray(value)) { return undefined @@ -545,11 +525,18 @@ function normalizeLogFiles(value: unknown): WorkflowExecutionLog['files'] { ) } -function normalizeWorkflowExecutionLog( +async function normalizeWorkflowExecutionLog( row: typeof workflowExecutionLogs.$inferSelect -): WorkflowExecutionLog { +): Promise { const startedAt = formatLogDate(row.startedAt) + // Heavy execution data may live in object storage; resolve the pointer so + // retry deliveries get finalOutput/traceSpans (no-op for inline rows). + const executionData = await materializeExecutionData( + isRecord(row.executionData) ? row.executionData : {}, + { workspaceId: row.workspaceId, workflowId: row.workflowId, executionId: row.executionId } + ) + return { id: row.id, workflowId: row.workflowId, @@ -561,8 +548,9 @@ function normalizeWorkflowExecutionLog( endedAt: formatLogDate(row.endedAt, startedAt), totalDurationMs: row.totalDurationMs ?? 0, files: normalizeLogFiles(row.files), - executionData: isRecord(row.executionData) ? row.executionData : {}, - cost: normalizeLogCost(row.cost), + executionData: executionData as WorkflowExecutionLog['executionData'], + // cost_total projection of the usage_log ledger (not the deprecated jsonb). + cost: row.costTotal != null ? { total: Number(row.costTotal) } : undefined, createdAt: formatLogDate(row.createdAt, startedAt), } } @@ -580,7 +568,7 @@ async function buildRetryLog(params: NotificationDeliveryParams): Promise +export type CostLedgerItem = z.output + const pauseSummarySchema = z.object({ status: z.string().nullable(), total: z.number(), @@ -218,7 +239,10 @@ export const workflowLogSummarySchema = z.object({ createdAt: z.string(), workflow: workflowSummarySchema.nullable(), jobTitle: z.string().nullable(), - cost: costSummarySchema.nullable(), + // Top-level run cost is the cost_total projection of the usage_log ledger, + // rendered as { total } (dollars). The itemized breakdown lives in costLedger + // (detail only); per-block costs use the richer costSummarySchema elsewhere. + cost: z.object({ total: z.number() }).nullable(), pauseSummary: pauseSummarySchema, hasPendingPause: z.boolean(), }) @@ -226,6 +250,9 @@ export const workflowLogSummarySchema = z.object({ export const workflowLogDetailSchema = workflowLogSummarySchema.extend({ executionData: executionDataDetailSchema, files: z.array(userFileSchema).nullable(), + // Itemized, ledger-sourced cost breakdown. Null for legacy/pre-ledger runs, + // where the UI falls back to the (reconciling) cost jsonb. + costLedger: costLedgerSchema.nullable().optional(), }) export type WorkflowLogSummary = z.output @@ -236,7 +263,7 @@ export type WorkflowLogDetail = z.output * UI surfaces that render the same log before and after its detail query resolves. */ export type WorkflowLogRow = WorkflowLogSummary & - Partial> + Partial> export const listLogsResponseSchema = z.object({ data: z.array(workflowLogSummarySchema), diff --git a/apps/sim/lib/api/contracts/subscription.ts b/apps/sim/lib/api/contracts/subscription.ts index c8b486deaf5..ee1498ddac1 100644 --- a/apps/sim/lib/api/contracts/subscription.ts +++ b/apps/sim/lib/api/contracts/subscription.ts @@ -103,7 +103,6 @@ export const organizationBillingMemberSchema = z userName: z.string().nullable().optional(), userEmail: z.string().nullable().optional(), joinedAt: z.string().nullable().optional(), - lastActive: z.string().nullable().optional(), }) .passthrough() diff --git a/apps/sim/lib/api/contracts/v1/admin/organizations.ts b/apps/sim/lib/api/contracts/v1/admin/organizations.ts index 9dad65f197f..1281b5e649d 100644 --- a/apps/sim/lib/api/contracts/v1/admin/organizations.ts +++ b/apps/sim/lib/api/contracts/v1/admin/organizations.ts @@ -43,7 +43,6 @@ export const adminV1MemberSchema = z.object({ export const adminV1MemberDetailSchema = adminV1MemberSchema.extend({ currentPeriodCost: z.string(), currentUsageLimit: z.string().nullable(), - lastActive: z.string().nullable(), billingBlocked: z.boolean(), }) @@ -75,18 +74,6 @@ export const adminV1SeatAnalyticsSchema = z.object({ subscriptionPlan: z.string(), canAddSeats: z.boolean(), utilizationRate: z.number(), - activeMembers: z.number(), - inactiveMembers: z.number(), - memberActivity: z.array( - z.object({ - userId: z.string(), - userName: z.string(), - userEmail: z.string(), - role: z.string(), - joinedAt: z.string(), - lastActive: z.string().nullable(), - }) - ), }) export const adminV1CreateOrganizationBodySchema = z.object({ diff --git a/apps/sim/lib/api/contracts/v1/admin/users.ts b/apps/sim/lib/api/contracts/v1/admin/users.ts index 6610134d93f..34bc00f5e22 100644 --- a/apps/sim/lib/api/contracts/v1/admin/users.ts +++ b/apps/sim/lib/api/contracts/v1/admin/users.ts @@ -23,27 +23,14 @@ export const adminV1UserBillingSchema = z.object({ userName: z.string(), userEmail: z.string(), stripeCustomerId: z.string().nullable(), - totalManualExecutions: z.number(), - totalApiCalls: z.number(), - totalWebhookTriggers: z.number(), - totalScheduledExecutions: z.number(), - totalChatExecutions: z.number(), - totalMcpExecutions: z.number(), - totalA2aExecutions: z.number(), - totalTokensUsed: z.number(), - totalCost: z.string(), currentUsageLimit: z.string().nullable(), currentPeriodCost: z.string(), lastPeriodCost: z.string().nullable(), billedOverageThisPeriod: z.string(), storageUsedBytes: z.number(), - lastActive: z.string().nullable(), billingBlocked: z.boolean(), - totalCopilotCost: z.string(), currentPeriodCopilotCost: z.string(), lastPeriodCopilotCost: z.string().nullable(), - totalCopilotTokens: z.number(), - totalCopilotCalls: z.number(), }) export const adminV1UserBillingWithSubscriptionSchema = adminV1UserBillingSchema.extend({ diff --git a/apps/sim/lib/api/contracts/v1/logs.ts b/apps/sim/lib/api/contracts/v1/logs.ts index 6659ccb99ad..52e48b096d2 100644 --- a/apps/sim/lib/api/contracts/v1/logs.ts +++ b/apps/sim/lib/api/contracts/v1/logs.ts @@ -27,7 +27,14 @@ export const v1ListLogsQuerySchema = z.object({ details: z.enum(['basic', 'full']).optional().default('basic'), includeTraceSpans: booleanQueryFlagSchema.optional().default(false), includeFinalOutput: booleanQueryFlagSchema.optional().default(false), - limit: z.coerce.number().optional().default(100), + // Clamp rather than reject: this limit was previously unbounded, so a hard + // .max() would 400 existing consumers passing a larger value. Page size is + // still capped to bound response size and materialization reads. + limit: z.coerce + .number() + .optional() + .default(100) + .transform((v) => Math.min(Math.max(1, Math.trunc(v)), 1000)), cursor: z.string().optional(), order: z.enum(['desc', 'asc']).optional().default('desc'), }) diff --git a/apps/sim/lib/billing/core/billing.ts b/apps/sim/lib/billing/core/billing.ts index 8595277c385..2db6b3ef7c7 100644 --- a/apps/sim/lib/billing/core/billing.ts +++ b/apps/sim/lib/billing/core/billing.ts @@ -7,7 +7,7 @@ import { type SubscriptionMetadata, } from '@/lib/billing/core/subscription' import { getOrgUsageLimit, getUserUsageData } from '@/lib/billing/core/usage' -import { getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' +import { COPILOT_USAGE_SOURCES, getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' import { getCreditBalance } from '@/lib/billing/credits/balance' import { computeDailyRefreshConsumed, @@ -128,6 +128,8 @@ async function aggregateOrgMemberStats(organizationId: string): Promise<{ .where(eq(member.organizationId, organizationId)) let currentPeriodCost = new Decimal(0) + // Copilot baseline (copilot source). All copilot-family usage (incl. MCP) lives + // in usage_log and is added via the copilot ledger by callers — not a baseline. let currentPeriodCopilotCost = new Decimal(0) let lastPeriodCopilotCost = new Decimal(0) const memberIds: string[] = [] @@ -454,20 +456,34 @@ export async function getSimplifiedBillingSummary( const pooled = await aggregateOrgMemberStats(organizationId) const rawCurrentUsage = pooled.currentPeriodCost - const totalCopilotCost = pooled.currentPeriodCopilotCost const totalLastPeriodCopilotCost = pooled.lastPeriodCopilotCost // Deduct daily-refresh credits against this specific org's pool. // `usageData` is derived from the caller's priority subscription // and may not match the requested org (multi-org admins, personal // priority sub, etc.), so it cannot be reused here. - const ledgerUsage = + const orgBillingPeriod = subscription.periodStart && subscription.periodEnd + ? { start: subscription.periodStart, end: subscription.periodEnd } + : null + const ledgerUsage = orgBillingPeriod + ? await getBillingPeriodUsageCost( + { type: 'organization', id: organizationId }, + orgBillingPeriod + ) + : 0 + // Copilot breakdown = member baselines (copilot + MCP) + the copilot-family + // ledger for the period (COPILOT_USAGE_SOURCES: copilot/workspace-chat/ + // mcp_copilot/mothership_block); the baseline columns are no longer incremented. + const totalCopilotCost = + pooled.currentPeriodCopilotCost + + (orgBillingPeriod ? await getBillingPeriodUsageCost( { type: 'organization', id: organizationId }, - { start: subscription.periodStart, end: subscription.periodEnd } + orgBillingPeriod, + COPILOT_USAGE_SOURCES ) - : 0 + : 0) let refreshDeduction = 0 if (isPaid(plan) && subscription.periodStart) { const planDollars = getPlanTierDollars(plan) @@ -561,6 +577,8 @@ export async function getSimplifiedBillingSummary( .where(eq(userStats.userId, userId)) .limit(1) + // Copilot baseline (copilot source). MCP copilot usage lives in usage_log and + // is added via the copilot ledger below, not a userStats baseline. const copilotCost = userStatsRows.length > 0 ? toNumber(toDecimal(userStatsRows[0].currentPeriodCopilotCost)) : 0 @@ -576,6 +594,25 @@ export async function getSimplifiedBillingSummary( totalLastPeriodCopilotCost = pooled.lastPeriodCopilotCost } + // Add the copilot-family ledger (COPILOT_USAGE_SOURCES: copilot/workspace-chat/ + // mcp_copilot/mothership_block) on top of the baseline; those columns are no + // longer incremented per usage. + const copilotBillingPeriod = + usageData.billingPeriodStart && usageData.billingPeriodEnd + ? { start: usageData.billingPeriodStart, end: usageData.billingPeriodEnd } + : null + if (copilotBillingPeriod) { + const copilotEntity = + orgScoped && subscription?.referenceId + ? ({ type: 'organization', id: subscription.referenceId } as const) + : ({ type: 'user', id: userId } as const) + totalCopilotCost += await getBillingPeriodUsageCost( + copilotEntity, + copilotBillingPeriod, + COPILOT_USAGE_SOURCES + ) + } + const percentUsed = usageData.limit > 0 ? (currentUsage / usageData.limit) * 100 : 0 const daysRemaining = usageData.billingPeriodEnd diff --git a/apps/sim/lib/billing/core/organization.ts b/apps/sim/lib/billing/core/organization.ts index eed717003a3..dd3389213f9 100644 --- a/apps/sim/lib/billing/core/organization.ts +++ b/apps/sim/lib/billing/core/organization.ts @@ -4,7 +4,10 @@ import { createLogger } from '@sim/logger' import { and, count, eq, gt, ne } from 'drizzle-orm' import { isOrganizationBillingBlocked } from '@/lib/billing/core/access' import { getOrganizationSubscription, getPlanPricing } from '@/lib/billing/core/billing' -import { getBillingPeriodUsageCost } from '@/lib/billing/core/usage-log' +import { + getBillingPeriodUsageCost, + getBillingPeriodUsageCostByUser, +} from '@/lib/billing/core/usage-log' import { computeDailyRefreshConsumed, getOrgMemberRefreshBounds, @@ -50,7 +53,32 @@ interface MemberUsageData { isOverLimit: boolean role: string joinedAt: Date - lastActive: Date | null +} + +/** + * Per-member usage_log cost for an org's current billing period, keyed by userId. + * `currentPeriodCost` is only a baseline (no longer incremented on the hot path), + * so callers add this ledger component to it for each member's real current-period + * usage. Pass `period` to reuse an already-fetched subscription window; omit it to + * look up the org's subscription here. Returns an empty map when there's no period. + */ +export async function getOrgMemberLedgerByUser( + organizationId: string, + period?: { start: Date; end: Date } | null +): Promise> { + let billingPeriod = period ?? null + if (period === undefined) { + const subscription = await getOrganizationSubscription(organizationId) + billingPeriod = + subscription?.periodStart && subscription?.periodEnd + ? { start: subscription.periodStart, end: subscription.periodEnd } + : null + } + if (!billingPeriod) return new Map() + return getBillingPeriodUsageCostByUser( + { type: 'organization', id: organizationId }, + billingPeriod + ) } /** @@ -93,16 +121,25 @@ export async function getOrganizationBillingData( // User stats fields currentPeriodCost: userStats.currentPeriodCost, currentUsageLimit: userStats.currentUsageLimit, - lastActive: userStats.lastActive, }) .from(member) .innerJoin(user, eq(member.userId, user.id)) .leftJoin(userStats, eq(member.userId, userStats.userId)) .where(eq(member.organizationId, organizationId)) + // Per-member current-period usage = userStats baseline + attributed usage_log + // rows. currentPeriodCost is no longer incremented on the hot path, so the + // baseline alone under-reports; add each member's ledger sum for the period. + const billingPeriod = + subscription.periodStart && subscription.periodEnd + ? { start: subscription.periodStart, end: subscription.periodEnd } + : null + const usageByUser = await getOrgMemberLedgerByUser(organizationId, billingPeriod) + // Process member data const members: MemberUsageData[] = membersWithUsage.map((memberRecord) => { - const currentUsage = Number(memberRecord.currentPeriodCost || 0) + const currentUsage = + Number(memberRecord.currentPeriodCost || 0) + (usageByUser.get(memberRecord.userId) ?? 0) const usageLimit = Number(memberRecord.currentUsageLimit || getFreeTierLimit()) const percentUsed = usageLimit > 0 ? (currentUsage / usageLimit) * 100 : 0 @@ -116,17 +153,22 @@ export async function getOrganizationBillingData( isOverLimit: currentUsage > usageLimit, role: memberRecord.role, joinedAt: memberRecord.joinedAt, - lastActive: memberRecord.lastActive, } }) - // Calculate aggregated statistics - let totalCurrentUsage = members.reduce((sum, m) => sum + m.currentUsage, 0) - - if (subscription.periodStart && subscription.periodEnd) { + // Authoritative org total = member baselines + the org's full usage_log for + // the period (also captures rows from members no longer present). Computed + // from raw baselines, NOT members[].currentUsage — the latter already folds + // in per-member usage_log for display, so summing it AND adding the org + // ledger would double-count. + let totalCurrentUsage = membersWithUsage.reduce( + (sum, m) => sum + Number(m.currentPeriodCost || 0), + 0 + ) + if (billingPeriod) { totalCurrentUsage += await getBillingPeriodUsageCost( { type: 'organization', id: subscription.referenceId }, - { start: subscription.periodStart, end: subscription.periodEnd } + billingPeriod ) } diff --git a/apps/sim/lib/billing/core/usage-log.ts b/apps/sim/lib/billing/core/usage-log.ts index 6232a1baf3a..ad1657a6db6 100644 --- a/apps/sim/lib/billing/core/usage-log.ts +++ b/apps/sim/lib/billing/core/usage-log.ts @@ -4,18 +4,18 @@ import { usageLog } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { and, desc, eq, gte, lte, sql } from 'drizzle-orm' +import { and, desc, eq, gte, inArray, lte, sql } from 'drizzle-orm' import { defaultBillingPeriod } from '@/lib/billing/core/billing-period' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import type { DbOrTx } from '@/lib/db/types' const logger = createLogger('UsageLog') /** * Usage log category types */ -export type UsageLogCategory = 'model' | 'fixed' +export type UsageLogCategory = 'model' | 'fixed' | 'tool' /** * Usage log source types @@ -31,6 +31,18 @@ export type UsageLogSource = | 'voice-input' | 'enrichment' +/** + * usage_log sources that make up the "copilot" cost breakdown shown in billing + * summaries: the copilot agent, mothership/workspace chat, MCP copilot, and + * mothership blocks. Mirrors the source set billed via /api/billing/update-cost. + */ +export const COPILOT_USAGE_SOURCES: UsageLogSource[] = [ + 'copilot', + 'workspace-chat', + 'mcp_copilot', + 'mothership_block', +] + /** * Metadata for 'model' category charges */ @@ -47,7 +59,7 @@ export type UsageLogMetadata = ModelUsageMetadata | Record | nu export type BillingEntityType = 'user' | 'organization' -interface BillingEntity { +export interface BillingEntity { type: BillingEntityType id: string } @@ -84,9 +96,16 @@ export interface RecordUsageParams { billingEntity?: BillingEntity /** Billing period bounds, resolved by caller when already known. */ billingPeriod?: { start: Date; end: Date } + /** + * Optional transaction to run the ledger INSERT in. Callers that reconcile a + * read-then-insert under a lock (e.g. the per-execution advisory lock in the + * workflow completion path) pass their tx so the insert participates in the + * same locked transaction. Defaults to the pooled db. + */ + tx?: DbOrTx } -function stableEventKey(parts: Record): string { +export function stableEventKey(parts: Record): string { const payload = Object.keys(parts) .sort() .map((key) => `${key}:${String(parts[key] ?? '')}`) @@ -94,32 +113,53 @@ function stableEventKey(parts: Record): string { return createHash('sha256').update(payload).digest('hex') } +type ResolvedSubscription = Awaited> + +export interface BillingContext { + billingEntity: BillingEntity + billingPeriod: { start: Date; end: Date } +} + +/** + * Derive the billing entity + period from an ALREADY-resolved subscription. + * Callers that already hold the subscription (e.g. the workflow completion path, + * which fetches it for usage-threshold emails) can derive the context once and + * pass it into recordUsage so resolveBillingContext skips a redundant lookup. + * This is the single source of the entity/period derivation — keep it the only + * place that maps a subscription to a billing context. + */ +export function deriveBillingContext( + userId: string, + subscription: ResolvedSubscription +): BillingContext { + const billingEntity: BillingEntity = + subscription && isOrgScopedSubscription(subscription, userId) + ? { type: 'organization', id: subscription.referenceId } + : { type: 'user', id: userId } + + const billingPeriod = + subscription?.periodStart && subscription.periodEnd + ? { start: subscription.periodStart, end: subscription.periodEnd } + : defaultBillingPeriod() + + return { billingEntity, billingPeriod } +} + async function resolveBillingContext( userId: string, billingEntity?: BillingEntity, billingPeriod?: { start: Date; end: Date } -): Promise<{ - billingEntity: BillingEntity - billingPeriod: { start: Date; end: Date } -}> { +): Promise { if (billingEntity && billingPeriod) { return { billingEntity, billingPeriod } } const subscription = await getHighestPrioritySubscription(userId) - const resolvedEntity = - billingEntity ?? - (subscription && isOrgScopedSubscription(subscription, userId) - ? { type: 'organization' as const, id: subscription.referenceId } - : { type: 'user' as const, id: userId }) - - const resolvedPeriod = - billingPeriod ?? - (subscription?.periodStart && subscription.periodEnd - ? { start: subscription.periodStart, end: subscription.periodEnd } - : defaultBillingPeriod()) - - return { billingEntity: resolvedEntity, billingPeriod: resolvedPeriod } + const derived = deriveBillingContext(userId, subscription) + return { + billingEntity: billingEntity ?? derived.billingEntity, + billingPeriod: billingPeriod ?? derived.billingPeriod, + } } /** @@ -128,43 +168,55 @@ async function resolveBillingContext( */ export async function getBillingPeriodUsageCost( billingEntity: BillingEntity, - billingPeriod: { start: Date; end: Date } + billingPeriod: { start: Date; end: Date }, + source?: UsageLogSource | UsageLogSource[] ): Promise { + const conditions = [ + eq(usageLog.billingEntityType, billingEntity.type), + eq(usageLog.billingEntityId, billingEntity.id), + eq(usageLog.billingPeriodStart, billingPeriod.start), + eq(usageLog.billingPeriodEnd, billingPeriod.end), + ] + if (source) { + conditions.push( + Array.isArray(source) ? inArray(usageLog.source, source) : eq(usageLog.source, source) + ) + } + const [row] = await db .select({ cost: sql`COALESCE(SUM(${usageLog.cost}), 0)`, }) .from(usageLog) - .where( - and( - eq(usageLog.billingEntityType, billingEntity.type), - eq(usageLog.billingEntityId, billingEntity.id), - eq(usageLog.billingPeriodStart, billingPeriod.start), - eq(usageLog.billingPeriodEnd, billingPeriod.end) - ) - ) + .where(and(...conditions)) return Number.parseFloat(row?.cost ?? '0') } export async function getBillingPeriodUsageCostByUser( billingEntity: BillingEntity, - billingPeriod: { start: Date; end: Date } + billingPeriod: { start: Date; end: Date }, + source?: UsageLogSource | UsageLogSource[] ): Promise> { + const conditions = [ + eq(usageLog.billingEntityType, billingEntity.type), + eq(usageLog.billingEntityId, billingEntity.id), + eq(usageLog.billingPeriodStart, billingPeriod.start), + eq(usageLog.billingPeriodEnd, billingPeriod.end), + ] + if (source) { + conditions.push( + Array.isArray(source) ? inArray(usageLog.source, source) : eq(usageLog.source, source) + ) + } + const rows = await db .select({ userId: usageLog.userId, cost: sql`COALESCE(SUM(${usageLog.cost}), 0)`, }) .from(usageLog) - .where( - and( - eq(usageLog.billingEntityType, billingEntity.type), - eq(usageLog.billingEntityId, billingEntity.id), - eq(usageLog.billingPeriodStart, billingPeriod.start), - eq(usageLog.billingPeriodEnd, billingPeriod.end) - ) - ) + .where(and(...conditions)) .groupBy(usageLog.userId) return new Map(rows.map((row) => [row.userId, Number.parseFloat(row.cost ?? '0')])) @@ -178,12 +230,20 @@ export async function getBillingPeriodUsageCostByUser( * but usage writes no longer contend on the user_stats row. */ export async function recordUsage(params: RecordUsageParams): Promise { - if (!isBillingEnabled) { - return - } - - const { userId, entries, workspaceId, workflowId, executionId, billingEntity, billingPeriod } = - params + // The usage ledger is written regardless of BILLING_ENABLED so it is the + // single, universal source of truth for cost (including self-hosted, where + // it powers the logs-page cost display). Billing *enforcement* (Stripe / + // overage) is gated separately by callers, not here. + const { + userId, + entries, + workspaceId, + workflowId, + executionId, + billingEntity, + billingPeriod, + tx, + } = params const validEntries = entries.filter((e) => e.cost > 0) @@ -193,7 +253,7 @@ export async function recordUsage(params: RecordUsageParams): Promise { const context = await resolveBillingContext(userId, billingEntity, billingPeriod) - const insertedRows = await db + const insertedRows = await (tx ?? db) .insert(usageLog) .values( validEntries.map((entry, index) => { diff --git a/apps/sim/lib/billing/core/usage.ts b/apps/sim/lib/billing/core/usage.ts index 06eba35faae..972a199b0fb 100644 --- a/apps/sim/lib/billing/core/usage.ts +++ b/apps/sim/lib/billing/core/usage.ts @@ -607,51 +607,6 @@ export async function syncUsageLimitsFromSubscription(userId: string): Promise -> { - try { - const teamMembers = await db - .select({ - userId: member.userId, - userName: user.name, - userEmail: user.email, - currentLimit: userStats.currentUsageLimit, - currentPeriodCost: userStats.currentPeriodCost, - totalCost: userStats.totalCost, - lastActive: userStats.lastActive, - }) - .from(member) - .innerJoin(user, eq(member.userId, user.id)) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .where(eq(member.organizationId, organizationId)) - - return teamMembers.map((memberData) => ({ - userId: memberData.userId, - userName: memberData.userName, - userEmail: memberData.userEmail, - currentLimit: toNumber(toDecimal(memberData.currentLimit || getFreeTierLimit().toString())), - currentUsage: toNumber(toDecimal(memberData.currentPeriodCost)), - totalCost: toNumber(toDecimal(memberData.totalCost)), - lastActive: memberData.lastActive, - })) - } catch (error) { - logger.error('Failed to get team usage limits', { organizationId, error }) - return [] - } -} - /** * Returns the effective current period usage cost for a user, with daily * refresh credits deducted. Org-scoped subs return the pooled sum across diff --git a/apps/sim/lib/billing/credits/conversion.test.ts b/apps/sim/lib/billing/credits/conversion.test.ts new file mode 100644 index 00000000000..7d4a45a3733 --- /dev/null +++ b/apps/sim/lib/billing/credits/conversion.test.ts @@ -0,0 +1,65 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + apportionCredits, + dollarsToCredits, + formatCreditCost, +} from '@/lib/billing/credits/conversion' + +describe('formatCreditCost', () => { + it('renders multiplier-inclusive dollars as a single-rounded credit label', () => { + expect(formatCreditCost(0.005)).toBe('1 credit') + expect(formatCreditCost(0.03141848)).toBe('6 credits') + expect(formatCreditCost(1.234)).toBe('247 credits') + }) + + it('distinguishes sub-credit charges from zero', () => { + expect(formatCreditCost(0.001)).toBe('<1 credit') + expect(formatCreditCost(0)).toBe('0 credits') + }) + + it('honors emptyForZeroOrLess for the trace view contract', () => { + expect(formatCreditCost(0, { emptyForZeroOrLess: true })).toBeUndefined() + expect(formatCreditCost(undefined, { emptyForZeroOrLess: true })).toBeUndefined() + expect(formatCreditCost(undefined)).toBe('—') + }) +}) + +describe('apportionCredits', () => { + it('keeps line items summing exactly to the rounded total (no round-then-sum drift)', () => { + // Real execution 43ef064d: base + 2x model, multiplier already applied. + // Round-then-sum would give 1 + 4 + 2 = 7; the true total is 6. + const credits = apportionCredits([ + { key: 'base', dollars: 0.005 }, + { key: 'input', dollars: 0.018798 }, + { key: 'output', dollars: 0.00762 }, + { key: 'tool', dollars: 0 }, + ]) + + const total = dollarsToCredits(0.005 + 0.018798 + 0.00762 + 0) + expect(total).toBe(6) + expect(credits.base + credits.input + credits.output + credits.tool).toBe(total) + expect(credits.base).toBe(1) + }) + + it('handles all-zero components', () => { + const credits = apportionCredits([ + { key: 'base', dollars: 0 }, + { key: 'model', dollars: 0 }, + ]) + expect(credits.base + credits.model).toBe(0) + }) + + it('ignores negative/non-finite components without throwing', () => { + const credits = apportionCredits([ + { key: 'base', dollars: 0.005 }, + { key: 'model', dollars: Number.NaN }, + { key: 'tool', dollars: -1 }, + ]) + expect(credits.base).toBe(1) + expect(credits.model).toBe(0) + expect(credits.tool).toBe(0) + }) +}) diff --git a/apps/sim/lib/billing/credits/conversion.ts b/apps/sim/lib/billing/credits/conversion.ts index a4f241f9954..74a77b8fdaa 100644 --- a/apps/sim/lib/billing/credits/conversion.ts +++ b/apps/sim/lib/billing/credits/conversion.ts @@ -12,6 +12,83 @@ export function dollarsToCredits(dollars: number): number { return Math.round(dollars * CREDIT_MULTIPLIER) } +/** + * Single source of truth for rendering a dollar cost as a credit label. + * + * Both the billing cost breakdown and the trace view derive their credit + * strings from here so the two surfaces can never diverge in rounding, + * thresholds, or pluralization. The dollar amount passed in is expected to + * already carry any cost multiplier (the value is converted with one — and + * only one — round via `dollarsToCredits`, i.e. multiply-then-round; never + * round per-line then multiply). + * + * `emptyForZeroOrLess` controls the zero/empty behavior so existing call sites + * keep their contracts: the breakdown wants a concrete "0 credits"/"—" string, + * the trace view wants `undefined` so it can hide the chip entirely. + */ +export function formatCreditCost( + dollars: number | null | undefined, + opts?: { emptyForZeroOrLess?: boolean } +): string | undefined { + if (dollars === undefined || dollars === null || !Number.isFinite(dollars)) { + return opts?.emptyForZeroOrLess ? undefined : '—' + } + + const credits = dollarsToCredits(dollars) + + if (credits <= 0) { + if (dollars > 0) return '<1 credit' + return opts?.emptyForZeroOrLess ? undefined : '0 credits' + } + + return `${credits.toLocaleString()} ${credits === 1 ? 'credit' : 'credits'}` +} + +/** + * Splits a set of cost components into integer credits that sum *exactly* to + * the credits of their combined total. + * + * This is the fix for the "line items don't add up to the total" class of bug: + * rounding each line independently (round-then-sum) drifts from the real charge + * (e.g. 1 + 2 + 1 = 4, or 1 + 4 + 2 = 7, when the true total is 6). Instead we + * convert the *summed* dollars to credits with a single round (multiply-then- + * round) and distribute that figure across the components via the largest- + * remainder method, so every component is multiplier-applied, internally + * consistent, and the rows always reconcile with the total. + * + * Each component's `dollars` is expected to already include any cost multiplier. + */ +export function apportionCredits( + components: { key: K; dollars: number }[] +): Record { + const result = {} as Record + + const sanitized = components.map((c) => ({ + key: c.key, + dollars: Number.isFinite(c.dollars) && c.dollars > 0 ? c.dollars : 0, + })) + + const totalDollars = sanitized.reduce((sum, c) => sum + c.dollars, 0) + const targetCredits = dollarsToCredits(totalDollars) + + const exact = sanitized.map((c) => ({ + key: c.key, + floor: Math.floor(c.dollars * CREDIT_MULTIPLIER), + frac: c.dollars * CREDIT_MULTIPLIER - Math.floor(c.dollars * CREDIT_MULTIPLIER), + })) + + for (const c of exact) result[c.key] = c.floor + + let remainder = targetCredits - exact.reduce((sum, c) => sum + c.floor, 0) + const byFraction = [...exact].sort((a, b) => b.frac - a.frac) + for (let i = 0; i < byFraction.length && remainder > 0; i++) { + result[byFraction[i].key] += 1 + remainder-- + } + + return result +} + /** * Format a dollar amount as a comma-separated credit string. * Values at or above the on-demand unlimited threshold display as ∞. diff --git a/apps/sim/lib/billing/index.ts b/apps/sim/lib/billing/index.ts index 8c01b7928c6..0df31e11d88 100644 --- a/apps/sim/lib/billing/index.ts +++ b/apps/sim/lib/billing/index.ts @@ -25,7 +25,6 @@ export { export * from '@/lib/billing/core/usage' export { checkUsageStatus, - getTeamUsageLimits, getUserUsageData as getUsageData, getUserUsageLimit as getUsageLimit, updateUserUsageLimit as updateUsageLimit, diff --git a/apps/sim/lib/billing/types/index.ts b/apps/sim/lib/billing/types/index.ts index db8b5adde3b..c90d0991c98 100644 --- a/apps/sim/lib/billing/types/index.ts +++ b/apps/sim/lib/billing/types/index.ts @@ -137,8 +137,6 @@ interface TeamUsageLimit { userEmail: string currentLimit: number currentUsage: number - totalCost: number - lastActive: Date | null limitSetBy: string | null limitUpdatedAt: Date | null } diff --git a/apps/sim/lib/billing/validation/seat-management.ts b/apps/sim/lib/billing/validation/seat-management.ts index df9a83d8e72..230ebee46aa 100644 --- a/apps/sim/lib/billing/validation/seat-management.ts +++ b/apps/sim/lib/billing/validation/seat-management.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { invitation, member, organization, subscription, user, userStats } from '@sim/db/schema' +import { invitation, member, organization, subscription, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, count, eq, gt, ne } from 'drizzle-orm' import { getOrganizationSubscription } from '@/lib/billing/core/billing' @@ -307,35 +307,15 @@ export async function getOrganizationSeatAnalytics(organizationId: string) { return null } - const memberActivity = await db - .select({ - userId: member.userId, - userName: user.name, - userEmail: user.email, - role: member.role, - joinedAt: member.createdAt, - lastActive: userStats.lastActive, - }) - .from(member) - .innerJoin(user, eq(member.userId, user.id)) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .where(eq(member.organizationId, organizationId)) - const utilizationRate = seatInfo.maxSeats > 0 ? (seatInfo.currentSeats / seatInfo.maxSeats) * 100 : 0 - const recentlyActive = memberActivity.filter((memberData) => { - if (!memberData.lastActive) return false - const daysSinceActive = (Date.now() - memberData.lastActive.getTime()) / (1000 * 60 * 60 * 24) - return daysSinceActive <= 30 // Active in last 30 days - }).length - + // Member activity analytics (active/inactive counts, memberActivity) were + // derived from userStats.lastActive, which is no longer written. Dropped + // rather than report frozen data; reintroduce with a real activity source. return { ...seatInfo, utilizationRate: Math.round(utilizationRate * 100) / 100, - activeMembers: recentlyActive, - inactiveMembers: seatInfo.currentSeats - recentlyActive, - memberActivity, } } catch (error) { logger.error('Failed to get organization seat analytics', { organizationId, error }) diff --git a/apps/sim/lib/billing/webhooks/invoices.ts b/apps/sim/lib/billing/webhooks/invoices.ts index 0a30d364f36..9460f521623 100644 --- a/apps/sim/lib/billing/webhooks/invoices.ts +++ b/apps/sim/lib/billing/webhooks/invoices.ts @@ -13,7 +13,10 @@ import type Stripe from 'stripe' import { getEmailSubject, PaymentFailedEmail, renderCreditPurchaseEmail } from '@/components/emails' import { BILLING_LOCK_TIMEOUT_MS } from '@/lib/billing/constants' import { calculateSubscriptionOverage, isSubscriptionOrgScoped } from '@/lib/billing/core/billing' -import { getBillingPeriodUsageCostByUser } from '@/lib/billing/core/usage-log' +import { + COPILOT_USAGE_SOURCES, + getBillingPeriodUsageCostByUser, +} from '@/lib/billing/core/usage-log' import { addCredits, getCreditBalanceForEntity } from '@/lib/billing/credits/balance' import { setUsageLimitForCredits } from '@/lib/billing/credits/purchase' import { blockOrgMembers, unblockOrgMembers } from '@/lib/billing/organizations/membership' @@ -424,6 +427,15 @@ export async function resetUsageForSubscription(sub: { billingPeriod ) : new Map() + // Copilot-family ledger per user, so last-period copilot mirrors last-period + // cost (baseline + usage_log) instead of capturing the baseline alone. + const copilotLedgerByUser = billingPeriod + ? await getBillingPeriodUsageCostByUser( + { type: 'organization', id: sub.referenceId }, + billingPeriod, + COPILOT_USAGE_SOURCES + ) + : new Map() await db.transaction(async (tx) => { await tx.execute(sql.raw(`SET LOCAL lock_timeout = '${BILLING_LOCK_TIMEOUT_MS}ms'`)) @@ -494,15 +506,27 @@ export async function resetUsageForSubscription(sub: { memberStatsRows.map((row) => sql`WHEN ${row.userId} THEN ${row.currentCopilot ?? '0'}`), sql` ` ) + // Last-period copilot = baseline copilot + copilot-family ledger, mirroring + // lastPeriodCost. (The reset below still subtracts only the baseline, since + // the ledger is period-scoped and rolls over on its own.) + const lastCopilotCostByUser = sql.join( + memberStatsRows.map((row) => { + const baselineCopilot = toNumber(toDecimal(row.currentCopilot)) + const copilotLedger = copilotLedgerByUser.get(row.userId) ?? 0 + return sql`WHEN ${row.userId} THEN ${(baselineCopilot + copilotLedger).toString()}` + }), + sql` ` + ) const capturedLastCost = sql`CASE ${userStats.userId} ${lastCostByUser} ELSE '0' END` const capturedCurrentCost = sql`CASE ${userStats.userId} ${currentCostByUser} ELSE '0' END` const capturedCurrentCopilotCost = sql`CASE ${userStats.userId} ${currentCopilotCostByUser} ELSE '0' END` + const capturedLastCopilotCost = sql`CASE ${userStats.userId} ${lastCopilotCostByUser} ELSE '0' END` await tx .update(userStats) .set({ lastPeriodCost: capturedLastCost, - lastPeriodCopilotCost: capturedCurrentCopilotCost, + lastPeriodCopilotCost: capturedLastCopilotCost, currentPeriodCost: sql`GREATEST(0, ${userStats.currentPeriodCost} - (${capturedCurrentCost})::decimal)`, currentPeriodCopilotCost: sql`GREATEST(0, ${userStats.currentPeriodCopilotCost} - (${capturedCurrentCopilotCost})::decimal)`, billedOverageThisPeriod: '0', @@ -536,6 +560,15 @@ export async function resetUsageForSubscription(sub: { ) : new Map() const userLedgerUsage = ledgerUsage.get(sub.referenceId) ?? 0 + const copilotLedgerUsage = billingPeriod + ? (( + await getBillingPeriodUsageCostByUser( + { type: 'user', id: sub.referenceId }, + billingPeriod, + COPILOT_USAGE_SOURCES + ) + ).get(sub.referenceId) ?? 0) + : 0 // Snapshot > 0: user joined a paid org mid-cycle. The pre-join // portion was billed on this invoice (snapshot); `currentPeriodCost` @@ -546,7 +579,12 @@ export async function resetUsageForSubscription(sub: { .update(userStats) .set({ lastPeriodCost: (snapshot + userLedgerUsage).toString(), - lastPeriodCopilotCost: '0', + // Pre-join personal copilot = the user-scoped copilot ledger only + // (post-join copilot usage is org-attributed, so this captures the + // pre-join portion). The copilot baseline stays with the org via the + // retained currentPeriodCopilotCost, so don't add it here (avoids a + // double count at the org's cycle-close). + lastPeriodCopilotCost: copilotLedgerUsage.toString(), proPeriodCostSnapshot: '0', proPeriodCostSnapshotAt: null, billedOverageThisPeriod: '0', @@ -563,7 +601,9 @@ export async function resetUsageForSubscription(sub: { .update(userStats) .set({ lastPeriodCost: totalLastPeriod, - lastPeriodCopilotCost: currentCopilot, + lastPeriodCopilotCost: ( + toNumber(toDecimal(currentCopilot)) + copilotLedgerUsage + ).toString(), currentPeriodCost: sql`GREATEST(0, ${userStats.currentPeriodCost} - ${current}::decimal)`, currentPeriodCopilotCost: sql`GREATEST(0, ${userStats.currentPeriodCopilotCost} - ${currentCopilot}::decimal)`, proPeriodCostSnapshot: '0', diff --git a/apps/sim/lib/copilot/chat/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index 21b6842cd6f..ea60844218a 100644 --- a/apps/sim/lib/copilot/chat/process-contents.ts +++ b/apps/sim/lib/copilot/chat/process-contents.ts @@ -639,6 +639,7 @@ async function processExecutionLogFromDb( .select({ id: workflowExecutionLogs.id, workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, executionId: workflowExecutionLogs.executionId, level: workflowExecutionLogs.level, trigger: workflowExecutionLogs.trigger, @@ -646,7 +647,7 @@ async function processExecutionLogFromDb( endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, executionData: workflowExecutionLogs.executionData, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, workflowName: workflow.name, }) .from(workflowExecutionLogs) @@ -671,6 +672,13 @@ async function processExecutionLogFromDb( } } + // Heavy execution data may live in object storage; resolve the pointer. + const { materializeExecutionData } = await import('@/lib/logs/execution/trace-store') + const executionData = (await materializeExecutionData( + log.executionData as Record | null, + { workspaceId: log.workspaceId, workflowId: log.workflowId, executionId: log.executionId } + )) as any + const summary = { id: log.id, workflowId: log.workflowId, @@ -681,13 +689,13 @@ async function processExecutionLogFromDb( endedAt: log.endedAt?.toISOString?.() || (log.endedAt ? String(log.endedAt) : null), totalDurationMs: log.totalDurationMs ?? null, workflowName: log.workflowName || '', - executionData: log.executionData + executionData: executionData ? { - traceSpans: (log.executionData as any).traceSpans || undefined, - errorDetails: (log.executionData as any).errorDetails || undefined, + traceSpans: executionData.traceSpans || undefined, + errorDetails: executionData.errorDetails || undefined, } : undefined, - cost: log.cost || undefined, + cost: log.costTotal != null ? { total: Number(log.costTotal) } : undefined, } const content = JSON.stringify(summary) diff --git a/apps/sim/lib/copilot/tools/server/workflow/get-execution-summary.ts b/apps/sim/lib/copilot/tools/server/workflow/get-execution-summary.ts index f561d7b6190..10f18a192b9 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/get-execution-summary.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/get-execution-summary.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, desc, eq, type SQL } from 'drizzle-orm' import { GetExecutionSummary } from '@/lib/copilot/generated/tool-catalog-v1' import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('GetExecutionSummaryServerTool') @@ -86,13 +87,14 @@ export const getExecutionSummaryServerTool: BaseServerTool< .select({ executionId: workflowExecutionLogs.executionId, workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, workflowName: workflow.name, status: workflowExecutionLogs.status, level: workflowExecutionLogs.level, trigger: workflowExecutionLogs.trigger, startedAt: workflowExecutionLogs.startedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, executionData: workflowExecutionLogs.executionData, }) .from(workflowExecutionLogs) @@ -101,26 +103,36 @@ export const getExecutionSummaryServerTool: BaseServerTool< .orderBy(desc(workflowExecutionLogs.startedAt)) .limit(clampedLimit) - const summaries: ExecutionSummary[] = rows.map((row) => { - const costData = row.cost as any - const errorMsg = row.level === 'error' ? extractErrorMessage(row.executionData) : null - - return { - executionId: row.executionId, - workflowId: row.workflowId, - workflowName: row.workflowName, - status: row.status, - trigger: row.trigger, - startedAt: row.startedAt.toISOString(), - durationMs: row.totalDurationMs ?? null, - cost: costData?.total ? Number(costData.total) : null, - error: errorMsg - ? typeof errorMsg === 'string' - ? errorMsg - : JSON.stringify(errorMsg) - : null, - } - }) + const summaries: ExecutionSummary[] = await Promise.all( + rows.map(async (row) => { + // Only externalized rows need a fetch; error fields live in the heavy data. + const executionData = + row.level === 'error' + ? await materializeExecutionData(row.executionData as Record | null, { + workspaceId: row.workspaceId, + workflowId: row.workflowId, + executionId: row.executionId, + }) + : row.executionData + const errorMsg = row.level === 'error' ? extractErrorMessage(executionData) : null + + return { + executionId: row.executionId, + workflowId: row.workflowId, + workflowName: row.workflowName, + status: row.status, + trigger: row.trigger, + startedAt: row.startedAt.toISOString(), + durationMs: row.totalDurationMs ?? null, + cost: row.costTotal != null ? Number(row.costTotal) : null, + error: errorMsg + ? typeof errorMsg === 'string' + ? errorMsg + : JSON.stringify(errorMsg) + : null, + } + }) + ) logger.info('Execution summary prepared', { count: summaries.length, diff --git a/apps/sim/lib/copilot/tools/server/workflow/get-workflow-logs.ts b/apps/sim/lib/copilot/tools/server/workflow/get-workflow-logs.ts index 0daf2aa07d0..471bc388057 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/get-workflow-logs.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/get-workflow-logs.ts @@ -5,6 +5,7 @@ import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, desc, eq } from 'drizzle-orm' import { GetWorkflowLogs } from '@/lib/copilot/generated/tool-catalog-v1' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import type { TraceSpan } from '@/lib/logs/types' const logger = createLogger('GetWorkflowLogsServerTool') @@ -145,6 +146,8 @@ export const getWorkflowLogsServerTool: BaseServerTool { - const executionData = log.executionData as ExecutionData - const traceSpans = executionData?.traceSpans ?? [] - const blockExecutions = includeDetails - ? extractBlockExecutionsFromTraceSpans(traceSpans) - : [] - - const simplifiedBlocks: SimplifiedBlock[] = blockExecutions.map((block) => ({ - id: block.blockId, - name: block.blockName, - startedAt: block.startedAt, - endedAt: block.endedAt, - durationMs: block.durationMs, - output: block.outputData, - error: block.status === 'error' ? block.errorMessage : undefined, - })) - - const rawError = - executionData?.errorDetails?.error || - executionData?.errorDetails?.message || - executionData?.finalOutput?.error || - executionData?.error || - null - const errorMessage = rawError - ? typeof rawError === 'string' - ? rawError - : JSON.stringify(rawError) - : undefined - - return { - id: log.id, - executionId: log.executionId, - status: log.status, - startedAt: log.startedAt.toISOString(), - endedAt: log.endedAt ? log.endedAt.toISOString() : null, - durationMs: log.totalDurationMs ?? null, - ...(errorMessage ? { error: errorMessage } : {}), - ...(simplifiedBlocks.length > 0 ? { blocks: simplifiedBlocks } : {}), - } - }) + // Enforce the documented hard limit (the materialization fans out per row). + .limit(executionId ? 1 : Math.min(Math.max(1, limit), 3)) + + const simplifiedExecutions: SimplifiedExecution[] = await Promise.all( + executionLogs.map(async (log) => { + const executionData = (await materializeExecutionData( + log.executionData as Record | null, + { + workspaceId: log.workspaceId, + workflowId: log.workflowId, + executionId: log.executionId, + } + )) as ExecutionData + const traceSpans = executionData?.traceSpans ?? [] + const blockExecutions = includeDetails + ? extractBlockExecutionsFromTraceSpans(traceSpans) + : [] + + const simplifiedBlocks: SimplifiedBlock[] = blockExecutions.map((block) => ({ + id: block.blockId, + name: block.blockName, + startedAt: block.startedAt, + endedAt: block.endedAt, + durationMs: block.durationMs, + output: block.outputData, + error: block.status === 'error' ? block.errorMessage : undefined, + })) + + const rawError = + executionData?.errorDetails?.error || + executionData?.errorDetails?.message || + executionData?.finalOutput?.error || + executionData?.error || + null + const errorMessage = rawError + ? typeof rawError === 'string' + ? rawError + : JSON.stringify(rawError) + : undefined + + return { + id: log.id, + executionId: log.executionId, + status: log.status, + startedAt: log.startedAt.toISOString(), + endedAt: log.endedAt ? log.endedAt.toISOString() : null, + durationMs: log.totalDurationMs ?? null, + ...(errorMessage ? { error: errorMessage } : {}), + ...(simplifiedBlocks.length > 0 ? { blocks: simplifiedBlocks } : {}), + } + }) + ) const resultSize = JSON.stringify(simplifiedExecutions).length logger.info('Workflow logs result prepared', { diff --git a/apps/sim/lib/core/utils/concurrency.ts b/apps/sim/lib/core/utils/concurrency.ts new file mode 100644 index 00000000000..9156c3c83d2 --- /dev/null +++ b/apps/sim/lib/core/utils/concurrency.ts @@ -0,0 +1,35 @@ +/** + * Maps over `items` with at most `limit` concurrent invocations of `fn`, + * preserving input order in the result. Use to bound fan-out (e.g. per-row + * object-storage reads) so a large batch doesn't issue every request at once. + * + * Contract: `fn` MUST NOT reject. Results are awaited via `Promise.all`, so a + * single rejection fails the entire batch (e.g. one bad row would break a whole + * logs export/list page). Callers that materialize per-row data pass a total + * mapper (one that catches and returns a degraded value rather than throwing); + * keep it that way. If a future caller needs per-item isolation, add an + * allSettled-style variant rather than letting a throwing mapper through here. + */ +export async function mapWithConcurrency( + items: readonly T[], + limit: number, + fn: (item: T, index: number) => Promise +): Promise { + const results = new Array(items.length) + const workerCount = Math.max(1, Math.min(limit, items.length)) + let cursor = 0 + + const worker = async (): Promise => { + while (true) { + const index = cursor++ + if (index >= items.length) return + results[index] = await fn(items[index], index) + } + } + + await Promise.all(Array.from({ length: workerCount }, worker)) + return results +} + +/** Default bound for per-row object-storage materialization fan-out. */ +export const MATERIALIZE_CONCURRENCY = 20 diff --git a/apps/sim/lib/data-drains/sources/workflow-logs.ts b/apps/sim/lib/data-drains/sources/workflow-logs.ts index 22487388f83..b64ce154791 100644 --- a/apps/sim/lib/data-drains/sources/workflow-logs.ts +++ b/apps/sim/lib/data-drains/sources/workflow-logs.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { workflowExecutionLogs } from '@sim/db/schema' import { and, inArray, isNotNull } from 'drizzle-orm' +import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency' import { decodeTimeCursor, encodeTimeCursor, @@ -9,6 +10,7 @@ import { } from '@/lib/data-drains/sources/cursor' import { getOrganizationWorkspaceIds } from '@/lib/data-drains/sources/helpers' import type { Cursor, DrainSource, SourcePageInput } from '@/lib/data-drains/types' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' type WorkflowLogRow = typeof workflowExecutionLogs.$inferSelect @@ -45,6 +47,22 @@ async function* pages(input: SourcePageInput): AsyncIterable { .limit(input.chunkSize) if (rows.length === 0) return + + // Heavy execution data may live in object storage; resolve pointers (bounded + // concurrency) so the drain exports full execution data, not the slim row. + // Use the order-preserving returned array (the util's documented contract) + // and write back, rather than mutating rows inside the mapper. + const materialized = await mapWithConcurrency(rows, MATERIALIZE_CONCURRENCY, (row) => + materializeExecutionData(row.executionData as Record | null, { + workspaceId: row.workspaceId, + workflowId: row.workflowId, + executionId: row.executionId, + }) + ) + for (let i = 0; i < rows.length; i++) { + rows[i].executionData = materialized[i] as WorkflowLogRow['executionData'] + } + yield rows const last = rows[rows.length - 1] cursor = { ts: last.endedAt!.toISOString(), id: last.id } @@ -71,7 +89,8 @@ export const workflowLogsSource: DrainSource = { endedAt: row.endedAt ? row.endedAt.toISOString() : null, totalDurationMs: row.totalDurationMs, executionData: row.executionData, - cost: row.cost, + // cost_total projection of the usage_log ledger (not the deprecated jsonb). + cost: row.costTotal != null ? { total: Number(row.costTotal) } : null, files: row.files, createdAt: row.createdAt.toISOString(), } diff --git a/apps/sim/lib/execution/payloads/store.ts b/apps/sim/lib/execution/payloads/store.ts index f13af025eb4..d662447aaa8 100644 --- a/apps/sim/lib/execution/payloads/store.ts +++ b/apps/sim/lib/execution/payloads/store.ts @@ -31,6 +31,13 @@ export interface LargeValueStoreContext { userId?: string requireDurable?: boolean maxBytes?: number + /** + * When false, materialization does not register an execution_log reference for + * the key. Read-only consumers (e.g. viewing/exporting a completed log) set + * this: the value is already owned + referenced by its own execution, so + * re-registering on every read is wasteful and a needless failure point. + */ + trackReference?: boolean } function getKind(value: unknown): LargeValueKind { @@ -163,6 +170,11 @@ export async function storeLargeValue( const id = `lv_${generateShortId(12)}` let key = await persistValue(id, json, context) if (key) { + // Only clean up the uploaded object when registration definitively did NOT + // record ownership (returns false). If registration THROWS, the metadata + // state is uncertain (a row may have partially committed), so we propagate + // without deleting — deleting could orphan a metadata row pointing at a + // now-missing object. const registered = await registerPersistedValueOwner(key, size, referencedKeys, context) if (!registered) { await deleteUntrackedPersistedValue(key) @@ -204,16 +216,22 @@ export async function materializeLargeValueRef( return materializeLargeValueRefSync(ref, context) } - const { addLargeValueReference } = await import('@/lib/execution/payloads/large-value-metadata') - await addLargeValueReference( - { - workspaceId: context.workspaceId, - workflowId: context.workflowId, - executionId: context.executionId, - source: 'execution_log', - }, - ref.key - ) + if (context.trackReference !== false) { + const { addLargeValueReference } = await import('@/lib/execution/payloads/large-value-metadata') + // Reference tracking is GC-critical: if it fails, fail the read rather than + // return a value whose reference was never recorded (it could later be + // garbage-collected out from under a live consumer). Read-only consumers + // that don't need a reference set trackReference: false to skip this. + await addLargeValueReference( + { + workspaceId: context.workspaceId, + workflowId: context.workflowId, + executionId: context.executionId, + source: 'execution_log', + }, + ref.key + ) + } try { const cached = materializeLargeValueRefSync(ref, context) diff --git a/apps/sim/lib/logs/execution/logger.test.ts b/apps/sim/lib/logs/execution/logger.test.ts index 1cca0faca74..cf245552cc6 100644 --- a/apps/sim/lib/logs/execution/logger.test.ts +++ b/apps/sim/lib/logs/execution/logger.test.ts @@ -1,7 +1,37 @@ import { featureFlagsMock } from '@sim/testing' import { beforeEach, describe, expect, test, vi } from 'vitest' +import { recordUsage } from '@/lib/billing/core/usage-log' import { ExecutionLogger } from '@/lib/logs/execution/logger' +const dbSelectMock = vi.hoisted(() => vi.fn()) +const dbExecuteMock = vi.hoisted(() => vi.fn()) +const txUpdateMock = vi.hoisted(() => + vi.fn(() => ({ set: () => ({ where: () => Promise.resolve() }) })) +) + +vi.mock('@sim/db', () => { + // The reconcile runs inside db.transaction with an advisory lock. The tx + // shares dbSelectMock so the existing call-order seeding (call 1 = workflow + // row via .limit, call 2 = already-billed via .groupBy) still applies; + // tx.execute (set_config + pg_advisory_xact_lock) is a no-op; tx.update backs + // the exact cost_total refine. + const tx = { + select: dbSelectMock, + insert: vi.fn(), + update: txUpdateMock, + execute: dbExecuteMock, + } + return { + db: { + select: dbSelectMock, + insert: vi.fn(), + update: vi.fn(), + execute: dbExecuteMock, + transaction: vi.fn(async (cb: (txArg: typeof tx) => Promise) => cb(tx)), + }, + } +}) + // Mock billing modules vi.mock('@/lib/billing/core/subscription', () => ({ getHighestPrioritySubscription: vi.fn(() => Promise.resolve(null)), @@ -19,6 +49,7 @@ vi.mock('@/lib/billing/core/usage', () => ({ vi.mock('@/lib/billing/core/usage-log', () => ({ recordUsage: vi.fn(() => Promise.resolve()), + stableEventKey: vi.fn((parts: Record) => JSON.stringify(parts)), })) vi.mock('@/lib/billing/threshold-billing', () => ({ @@ -439,3 +470,322 @@ describe('ExecutionLogger', () => { }) }) }) + +describe('recordExecutionUsage boundary-delta reconciliation', () => { + let logger: any + + beforeEach(() => { + logger = new ExecutionLogger() as any + vi.clearAllMocks() + }) + + const costSummary = (overrides: Record = {}) => ({ + totalCost: 0, + totalInputCost: 0, + totalOutputCost: 0, + totalTokens: 0, + totalPromptTokens: 0, + totalCompletionTokens: 0, + baseExecutionCharge: 0.005, + models: {}, + charges: {}, + ...overrides, + }) + + // db.select() is called twice in recordExecutionUsage: first the workflow row + // (terminated by .limit), then the already-billed usage_log rows (terminated + // by .groupBy). Return each in order. + const mockDb = (billedRows: Array>) => { + let call = 0 + dbSelectMock.mockImplementation(() => { + call += 1 + const rows = call === 1 ? [{ id: 'workflow-1', workspaceId: 'ws-1' }] : billedRows + const chain: any = { + from: () => chain, + where: () => chain, + limit: () => Promise.resolve(rows), + groupBy: () => Promise.resolve(rows), + } + return chain + }) + } + + const run = ( + summary: ReturnType, + billedRows: Array> + ) => { + mockDb(billedRows) + return logger.recordExecutionUsage('workflow-1', summary, 'api', 'exec-1', 'user-1') + } + + const lastEntries = () => vi.mocked(recordUsage).mock.calls[0][0].entries + + test('fresh completion records all targets (base fee + model) and returns the increment', async () => { + const recorded = await run( + costSummary({ + models: { + 'gpt-4o': { + total: 1, + input: 0.6, + output: 0.4, + tokens: { input: 10, output: 5, total: 15 }, + }, + }, + }), + [] + ) + + expect(recordUsage).toHaveBeenCalledTimes(1) + expect(lastEntries()).toEqual([ + expect.objectContaining({ category: 'fixed', description: 'execution_fee', cost: 0.005 }), + expect.objectContaining({ category: 'model', description: 'gpt-4o', cost: 1 }), + ]) + // Returns the amount recorded at this boundary (drives threshold-email math). + expect(recorded).toBeCloseTo(1.005, 8) + // cost_total is refined to the exact ledger sum inside the locked tx. + expect(txUpdateMock).toHaveBeenCalledTimes(1) + }) + + test('resume records only the increment over what is already billed', async () => { + await run( + costSummary({ + models: { + 'gpt-4o': { + total: 3, + input: 1.8, + output: 1.2, + tokens: { input: 30, output: 15, total: 45 }, + }, + }, + }), + [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + { category: 'model', description: 'gpt-4o', cost: '1' }, + ] + ) + + expect(recordUsage).toHaveBeenCalledTimes(1) + const entries = lastEntries() + expect(entries).toHaveLength(1) + expect(entries[0]).toEqual( + expect.objectContaining({ category: 'model', description: 'gpt-4o', cost: 2 }) + ) + }) + + test('returns only the post-resume increment (not the cumulative total)', async () => { + const recorded = await run( + costSummary({ + models: { + 'gpt-4o': { + total: 3, + input: 0, + output: 0, + tokens: { input: 0, output: 0, total: 0 }, + }, + }, + }), + [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + { category: 'model', description: 'gpt-4o', cost: '1' }, + ] + ) + // Cumulative is 3.005, but only the $2 increment was recorded here — the + // threshold-email math must not re-count the pre-pause $1.005. + expect(recorded).toBe(2) + }) + + test('forwards a pre-resolved billing context to recordUsage (skips re-lookup)', async () => { + mockDb([]) + const billingContext = { + billingEntity: { type: 'user' as const, id: 'user-1' }, + billingPeriod: { start: new Date('2026-01-01'), end: new Date('2026-02-01') }, + } + await (logger as any).recordExecutionUsage( + 'workflow-1', + costSummary({ + models: { + 'gpt-4o': { + total: 1, + input: 0, + output: 0, + tokens: { input: 0, output: 0, total: 0 }, + }, + }, + }), + 'api', + 'exec-1', + 'user-1', + billingContext + ) + expect(vi.mocked(recordUsage).mock.calls[0][0]).toMatchObject({ + billingEntity: { type: 'user', id: 'user-1' }, + billingPeriod: billingContext.billingPeriod, + }) + }) + + test('retry with everything already billed records nothing (idempotent)', async () => { + await run( + costSummary({ + models: { + 'gpt-4o': { + total: 1, + input: 0.6, + output: 0.4, + tokens: { input: 10, output: 5, total: 15 }, + }, + }, + }), + [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + { category: 'model', description: 'gpt-4o', cost: '1' }, + ] + ) + + expect(recordUsage).not.toHaveBeenCalled() + }) + + test('BYOK run records only the base fee (no zero-cost model rows)', async () => { + await run(costSummary({ models: {}, charges: {} }), []) + + expect(recordUsage).toHaveBeenCalledTimes(1) + expect(lastEntries()).toEqual([ + expect.objectContaining({ category: 'fixed', description: 'execution_fee', cost: 0.005 }), + ]) + }) + + test('standalone hosted-tool charge reconciles as a tool row', async () => { + await run(costSummary({ charges: { 'Exa Search': { total: 0.02 } } }), [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + ]) + + expect(lastEntries()).toEqual([ + expect.objectContaining({ category: 'tool', description: 'Exa Search', cost: 0.02 }), + ]) + }) + + test('two boundaries (pause then resume) bill the full run exactly once', async () => { + const model = (total: number) => ({ + 'gpt-4o': { + input: total * 0.6, + output: total * 0.4, + total, + tokens: { input: 10, output: 5, total: 15 }, + }, + }) + + // Boundary 1 (pause): nothing billed yet, partial cost. + await run(costSummary({ models: model(1) }), []) + const firstEntries = vi.mocked(recordUsage).mock.calls[0][0].entries + + // Feed boundary 1's rows back as already-billed for boundary 2. + const billedAfterFirst = firstEntries.map((e: any) => ({ + category: e.category, + description: e.description, + cost: String(e.cost), + })) + + // Boundary 2 (resume terminal): same model, higher cumulative cost. + await run(costSummary({ models: model(3) }), billedAfterFirst) + const secondEntries = vi.mocked(recordUsage).mock.calls[1][0].entries + + const ledgerTotal = [...firstEntries, ...secondEntries].reduce( + (sum: number, e: any) => sum + e.cost, + 0 + ) + expect(ledgerTotal).toBeCloseTo(3.005, 8) // base 0.005 once + gpt-4o 3 total + // Base fee billed once (boundary 1 only); model increment only at boundary 2. + expect(firstEntries.some((e: any) => e.category === 'fixed')).toBe(true) + expect(secondEntries.some((e: any) => e.category === 'fixed')).toBe(false) + expect(secondEntries).toEqual([ + expect.objectContaining({ category: 'model', description: 'gpt-4o', cost: 2 }), + ]) + }) + + test('eventKey is scoped by billedBefore so cross-boundary increments do not collide', async () => { + const model = (total: number) => ({ + 'gpt-4o': { input: 0, output: 0, total, tokens: { input: 0, output: 0, total: 0 } }, + }) + + await run(costSummary({ models: model(1) }), []) + const key0 = vi + .mocked(recordUsage) + .mock.calls[0][0].entries.find((e: any) => e.category === 'model')?.eventKey + + await run(costSummary({ models: model(3) }), [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + { category: 'model', description: 'gpt-4o', cost: '1' }, + ]) + const key1 = vi + .mocked(recordUsage) + .mock.calls[1][0].entries.find((e: any) => e.category === 'model')?.eventKey + + expect(key0).toContain('"billedBefore":"0.00000000"') + expect(key1).toContain('"billedBefore":"1.00000000"') + expect(key0).not.toEqual(key1) + }) + + test('a decreased cumulative cost (negative delta) records nothing for that line', async () => { + await run( + costSummary({ + models: { + 'gpt-4o': { input: 0, output: 0, total: 3, tokens: { input: 0, output: 0, total: 0 } }, + }, + }), + [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + { category: 'model', description: 'gpt-4o', cost: '5' }, + ] + ) + expect(recordUsage).not.toHaveBeenCalled() + }) + + test('a model introduced only post-resume is billed in full; the already-billed model is skipped', async () => { + await run( + costSummary({ + models: { + 'gpt-4o': { input: 0, output: 0, total: 1, tokens: { input: 0, output: 0, total: 0 } }, + 'claude-3': { input: 0, output: 0, total: 2, tokens: { input: 0, output: 0, total: 0 } }, + }, + }), + [ + { category: 'fixed', description: 'execution_fee', cost: '0.005' }, + { category: 'model', description: 'gpt-4o', cost: '1' }, + ] + ) + expect(lastEntries()).toEqual([ + expect.objectContaining({ category: 'model', description: 'claude-3', cost: 2 }), + ]) + }) + + test('zero-cost models and charges (BYOK) are filtered out, leaving only the base fee', async () => { + await run( + costSummary({ + models: { + 'gpt-4o': { input: 0, output: 0, total: 0, tokens: { input: 0, output: 0, total: 0 } }, + }, + charges: { Exa: { total: 0 } }, + }), + [] + ) + expect(lastEntries()).toEqual([ + expect.objectContaining({ category: 'fixed', description: 'execution_fee', cost: 0.005 }), + ]) + }) + + test('reconciles inside a transaction holding a per-execution advisory lock', async () => { + await run( + costSummary({ + models: { + 'gpt-4o': { input: 0, output: 0, total: 1, tokens: { input: 0, output: 0, total: 0 } }, + }, + }), + [] + ) + + // set_config('lock_timeout') + pg_advisory_xact_lock both run on the tx. + expect(dbExecuteMock).toHaveBeenCalledTimes(2) + expect(recordUsage).toHaveBeenCalledTimes(1) + // The ledger INSERT participates in the locked transaction. + expect(vi.mocked(recordUsage).mock.calls[0][0]).toHaveProperty('tx') + }) +}) diff --git a/apps/sim/lib/logs/execution/logger.ts b/apps/sim/lib/logs/execution/logger.ts index e52b92f4276..66abe6283a1 100644 --- a/apps/sim/lib/logs/execution/logger.ts +++ b/apps/sim/lib/logs/execution/logger.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { member, + usageLog, userStats, user as userTable, workflow, @@ -9,15 +10,20 @@ import { import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { eq, sql } from 'drizzle-orm' -import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants' +import { and, eq, sql } from 'drizzle-orm' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { checkUsageStatus, getOrgUsageLimit, maybeSendUsageThresholdEmail, } from '@/lib/billing/core/usage' -import { type ModelUsageMetadata, recordUsage } from '@/lib/billing/core/usage-log' +import { + type BillingContext, + deriveBillingContext, + type ModelUsageMetadata, + recordUsage, + stableEventKey, +} from '@/lib/billing/core/usage-log' import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing' import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { redactApiKeys } from '@/lib/core/security/redaction' @@ -28,6 +34,7 @@ import { } from '@/lib/execution/payloads/large-value-metadata' import { emitWorkflowExecutionCompleted } from '@/lib/logs/events' import { snapshotService } from '@/lib/logs/execution/snapshot/service' +import { externalizeExecutionData, stripSpanCosts } from '@/lib/logs/execution/trace-store' import type { BlockOutputData, ExecutionEnvironment, @@ -48,6 +55,9 @@ const MAX_WORKFLOW_VALUE_BYTES = 512 * 1024 const EXECUTION_LOG_STATEMENT_TIMEOUT_MS = 30_000 const EXECUTION_LOG_LOCK_TIMEOUT_MS = 3_000 const EXECUTION_LOG_IDLE_TIMEOUT_MS = 5_000 +// Bounds the wait for the per-execution usage-reconcile advisory lock. Generous +// (favor waiting over dropping a charge); only trips on a pathological lock hold. +const USAGE_RECONCILE_LOCK_TIMEOUT_MS = 10_000 type ExecutionData = WorkflowExecutionLog['executionData'] @@ -548,13 +558,6 @@ export class ExecutionLogger implements IExecutionLoggerService { hasTraceSpans: false, traceSpanCount: 0, }, - cost: { - total: BASE_EXECUTION_CHARGE, - input: 0, - output: 0, - tokens: { input: 0, output: 0, total: 0 }, - models: {}, - }, }) .returning() @@ -590,7 +593,6 @@ export class ExecutionLogger implements IExecutionLoggerService { totalPromptTokens: number totalCompletionTokens: number baseExecutionCharge: number - modelCost: number models: Record< string, { @@ -601,6 +603,7 @@ export class ExecutionLogger implements IExecutionLoggerService { tokens: { input: number; output: number; total: number } } > + charges?: Record } finalOutput: BlockOutputData traceSpans?: TraceSpan[] @@ -732,7 +735,26 @@ export class ExecutionLogger implements IExecutionLoggerService { }, executionId ) - const completedExecutionLargeValueKeys = collectLargeValueReferenceKeys(completedExecutionData) + + stripSpanCosts((completedExecutionData as Record).traceSpans) + + // Externalization requires the execution owner (workspace_files.user_id is + // NOT NULL). billingUserId comes from environment.userId and is effectively + // always present for a real run; if it's somehow absent, keep data inline. + let storedExecutionData = completedExecutionData as Record + if (billingUserId) { + storedExecutionData = await externalizeExecutionData(storedExecutionData, { + workspaceId: existingLog?.workspaceId ?? null, + workflowId: existingLog?.workflowId ?? null, + executionId, + userId: billingUserId, + }) + } else { + execLog.warn('Skipping execution-data externalization: missing owner userId', { + executionId, + }) + } + const completedExecutionLargeValueKeys = collectLargeValueReferenceKeys(storedExecutionData) const updatedLog = await db.transaction(async (tx) => { await setExecutionLogWriteTimeouts(tx) @@ -745,8 +767,17 @@ export class ExecutionLogger implements IExecutionLoggerService { endedAt: new Date(endedAt), totalDurationMs: totalDuration, files: executionFiles.length > 0 ? executionFiles : null, - executionData: completedExecutionData, - cost: executionCost, + executionData: storedExecutionData, + // Faithful projection of the usage_log ledger. Neither cost_total nor + // models_used may regress below a prior boundary: a paused run that + // resumes into an empty-span error/cancel/cost-only fallback produces a + // base-only summary. GREATEST keeps the higher cumulative cost_total, + // and models_used is overwritten only when this boundary actually has + // models — so both stay == SUM(usage_log) on every monotonic path. + costTotal: sql`GREATEST(COALESCE(${workflowExecutionLogs.costTotal}, 0), ${costSummary.totalCost.toString()}::numeric)`, + ...(Object.keys(costSummary.models).length > 0 + ? { modelsUsed: Object.keys(costSummary.models) } + : {}), }) .where(eq(workflowExecutionLogs.executionId, executionId)) .returning() @@ -770,116 +801,146 @@ export class ExecutionLogger implements IExecutionLoggerService { }) try { - // Skip workflow lookup if workflow was deleted + // Skip workflow lookup if workflow was deleted. const wf = updatedLog.workflowId ? (await db.select().from(workflow).where(eq(workflow.id, updatedLog.workflowId)))[0] : undefined - if (wf && billingUserId) { - const [usr] = await db - .select({ id: userTable.id, email: userTable.email, name: userTable.name }) - .from(userTable) - .where(eq(userTable.id, billingUserId)) - .limit(1) - - if (usr?.email) { - const sub = await getHighestPrioritySubscription(usr.id) - - const costDelta = costSummary.totalCost - - const { getDisplayPlanName } = await import('@/lib/billing/plan-helpers') - const { isOrgScopedSubscription } = await import('@/lib/billing/subscriptions/utils') - const planName = getDisplayPlanName(sub?.plan) - const scope: 'user' | 'organization' = isOrgScopedSubscription(sub, usr.id) - ? 'organization' - : 'user' - - if (scope === 'user') { - const before = await checkUsageStatus(usr.id) - - await this.updateUserStats( - updatedLog.workflowId, - costSummary, - updatedLog.trigger as ExecutionTrigger['type'], - executionId, - billingUserId - ) - - const limit = before.usageData.limit - const percentBefore = before.usageData.percentUsed - const percentAfter = - limit > 0 ? Math.min(100, percentBefore + (costDelta / limit) * 100) : percentBefore - const currentUsageAfter = before.usageData.currentUsage + costDelta - - await maybeSendUsageThresholdEmail({ - scope: 'user', - userId: usr.id, - userEmail: usr.email, - userName: usr.name || undefined, - planName, - percentBefore, - percentAfter, - currentUsageAfter, - limit, - }) - } else if (sub?.referenceId) { - // Get org usage limit using shared helper - const { limit: orgLimit } = await getOrgUsageLimit(sub.referenceId, sub.plan, sub.seats) - - const [{ sum: orgUsageBefore }] = await db - .select({ sum: sql`COALESCE(SUM(${userStats.currentPeriodCost}), 0)` }) - .from(member) - .leftJoin(userStats, eq(member.userId, userStats.userId)) - .where(eq(member.organizationId, sub.referenceId)) - .limit(1) - const orgUsageBeforeNum = Number.parseFloat(String(orgUsageBefore ?? '0')) - - await this.updateUserStats( - updatedLog.workflowId, - costSummary, - updatedLog.trigger as ExecutionTrigger['type'], - executionId, - billingUserId - ) - const percentBefore = - orgLimit > 0 ? Math.min(100, (orgUsageBeforeNum / orgLimit) * 100) : 0 - const percentAfter = - orgLimit > 0 - ? Math.min(100, percentBefore + (costDelta / orgLimit) * 100) - : percentBefore - const currentUsageAfter = orgUsageBeforeNum + costDelta - - await maybeSendUsageThresholdEmail({ - scope: 'organization', - organizationId: sub.referenceId, - planName, - percentBefore, - percentAfter, - currentUsageAfter, - limit: orgLimit, - }) + const usr = + wf && billingUserId + ? ( + await db + .select({ id: userTable.id, email: userTable.email, name: userTable.name }) + .from(userTable) + .where(eq(userTable.id, billingUserId)) + .limit(1) + )[0] + : undefined + + // Resolve the billing context + the pre-increment usage snapshot for the + // threshold email BEFORE recording, so currentUsageAfter = before + + // costDelta doesn't double-count this boundary's own increment. + type EmailContext = + | { + scope: 'user' + userId: string + userEmail: string + userName: string | null + planName: string + before: Awaited> + } + | { + scope: 'organization' + organizationId: string + planName: string + orgLimit: number + orgUsageBefore: number + } + let billingContext: BillingContext | undefined + let emailContext: EmailContext | undefined + + if (usr?.email) { + const sub = await getHighestPrioritySubscription(usr.id) + // Derive the billing context once from the subscription we just fetched + // and thread it into recordExecutionUsage so recordUsage doesn't + // re-resolve the subscription on the hot completion path. + billingContext = deriveBillingContext(usr.id, sub) + + const { getDisplayPlanName } = await import('@/lib/billing/plan-helpers') + const { isOrgScopedSubscription } = await import('@/lib/billing/subscriptions/utils') + const planName = getDisplayPlanName(sub?.plan) + + if (isOrgScopedSubscription(sub, usr.id) && sub?.referenceId) { + const { limit: orgLimit } = await getOrgUsageLimit(sub.referenceId, sub.plan, sub.seats) + const [{ sum: orgBaselineSum }] = await db + .select({ sum: sql`COALESCE(SUM(${userStats.currentPeriodCost}), 0)` }) + .from(member) + .leftJoin(userStats, eq(member.userId, userStats.userId)) + .where(eq(member.organizationId, sub.referenceId)) + .limit(1) + // currentPeriodCost is only a baseline; add the org's attributed + // usage_log for the period so the threshold email reflects real usage. + const { getBillingPeriodUsageCost } = await import('@/lib/billing/core/usage-log') + const orgLedger = + sub.periodStart && sub.periodEnd + ? await getBillingPeriodUsageCost( + { type: 'organization', id: sub.referenceId }, + { start: sub.periodStart, end: sub.periodEnd } + ) + : 0 + emailContext = { + scope: 'organization', + organizationId: sub.referenceId, + planName, + orgLimit, + orgUsageBefore: Number.parseFloat(String(orgBaselineSum ?? '0')) + orgLedger, } } else { - await this.updateUserStats( - updatedLog.workflowId, - costSummary, - updatedLog.trigger as ExecutionTrigger['type'], - executionId, - billingUserId - ) + emailContext = { + scope: 'user', + userId: usr.id, + userEmail: usr.email, + userName: usr.name, + planName, + before: await checkUsageStatus(usr.id), + } } - } else { - await this.updateUserStats( - updatedLog.workflowId, - costSummary, - updatedLog.trigger as ExecutionTrigger['type'], - executionId, - billingUserId - ) + } + + // Record usage exactly once for every path. costDelta is the amount + // actually recorded at this boundary (the increment), not the cumulative + // run total — so resumed runs don't double-count pre-pause cost below. + const costDelta = await this.recordExecutionUsage( + updatedLog.workflowId, + costSummary, + updatedLog.trigger as ExecutionTrigger['type'], + executionId, + billingUserId, + billingContext + ) + + // Best-effort usage-threshold email. + if (emailContext?.scope === 'user') { + const limit = emailContext.before.usageData.limit + const percentBefore = emailContext.before.usageData.percentUsed + const percentAfter = + limit > 0 ? Math.min(100, percentBefore + (costDelta / limit) * 100) : percentBefore + const currentUsageAfter = emailContext.before.usageData.currentUsage + costDelta + + await maybeSendUsageThresholdEmail({ + scope: 'user', + userId: emailContext.userId, + userEmail: emailContext.userEmail, + userName: emailContext.userName || undefined, + planName: emailContext.planName, + percentBefore, + percentAfter, + currentUsageAfter, + limit, + }) + } else if (emailContext?.scope === 'organization') { + const { orgLimit, orgUsageBefore } = emailContext + const percentBefore = orgLimit > 0 ? Math.min(100, (orgUsageBefore / orgLimit) * 100) : 0 + const percentAfter = + orgLimit > 0 ? Math.min(100, percentBefore + (costDelta / orgLimit) * 100) : percentBefore + const currentUsageAfter = orgUsageBefore + costDelta + + await maybeSendUsageThresholdEmail({ + scope: 'organization', + organizationId: emailContext.organizationId, + planName: emailContext.planName, + percentBefore, + percentAfter, + currentUsageAfter, + limit: orgLimit, + }) } } catch (e) { + // Safety net: if a step above threw BEFORE the single record call, ensure + // the run is still billed. Reconciliation is idempotent, so re-recording + // after a successful call is a no-op. try { - await this.updateUserStats( + await this.recordExecutionUsage( updatedLog.workflowId, costSummary, updatedLog.trigger as ExecutionTrigger['type'], @@ -902,8 +963,13 @@ export class ExecutionLogger implements IExecutionLoggerService { startedAt: updatedLog.startedAt.toISOString(), endedAt: updatedLog.endedAt?.toISOString() || endedAt, totalDurationMs: updatedLog.totalDurationMs || totalDurationMs, - executionData: updatedLog.executionData as WorkflowExecutionLog['executionData'], - cost: updatedLog.cost as WorkflowExecutionLog['cost'], + // Return the full in-memory execution data (cost-stripped, with traceSpans + // and finalOutput), not the slim externalized row — downstream consumers + // (notification delivery, events) need the complete payload without an + // extra storage round-trip. + executionData: completedExecutionData as WorkflowExecutionLog['executionData'], + // From the in-memory cost summary (not the deprecated cost jsonb column). + cost: executionCost as WorkflowExecutionLog['cost'], createdAt: updatedLog.createdAt.toISOString(), } @@ -934,7 +1000,10 @@ export class ExecutionLogger implements IExecutionLoggerService { endedAt: workflowLog.endedAt?.toISOString() || workflowLog.startedAt.toISOString(), totalDurationMs: workflowLog.totalDurationMs || 0, executionData: workflowLog.executionData as WorkflowExecutionLog['executionData'], - cost: workflowLog.cost as WorkflowExecutionLog['cost'], + // cost_total projection of the usage_log ledger (not the deprecated jsonb). + cost: (workflowLog.costTotal != null + ? { total: Number(workflowLog.costTotal) } + : null) as WorkflowExecutionLog['cost'], createdAt: workflowLog.createdAt.toISOString(), } } @@ -959,7 +1028,7 @@ export class ExecutionLogger implements IExecutionLoggerService { return trimmedUserId.length > 0 ? trimmedUserId : null } - private async updateUserStats( + private async recordExecutionUsage( workflowId: string | null, costSummary: { totalCost: number @@ -969,7 +1038,6 @@ export class ExecutionLogger implements IExecutionLoggerService { totalPromptTokens: number totalCompletionTokens: number baseExecutionCharge: number - modelCost: number models?: Record< string, { @@ -980,28 +1048,31 @@ export class ExecutionLogger implements IExecutionLoggerService { tokens: { input: number; output: number; total: number } } > + charges?: Record }, trigger: ExecutionTrigger['type'], executionId?: string, - billingUserId?: string | null - ): Promise { + billingUserId?: string | null, + // Pre-resolved billing context. The completion path already fetches the + // subscription for usage-threshold emails; passing the derived context here + // lets recordUsage skip a redundant subscription lookup per completion. + billingContext?: BillingContext + ): Promise { const statsLog = logger.withMetadata({ workflowId: workflowId ?? undefined, executionId }) - if (!isBillingEnabled) { - statsLog.debug('Billing is disabled, skipping user stats cost update') - return - } - - if (costSummary.totalCost <= 0) { - statsLog.debug('No cost to update in user stats') - return - } + // The usage ledger (recordUsage below) is written regardless of + // BILLING_ENABLED so cost is available everywhere (incl. self-hosted). + // Only enforcement (overage/Stripe) is gated on the flag. + // Returns the amount actually recorded at THIS boundary (the increment), so + // callers drive usage-threshold math off the delta rather than the + // cumulative run total (which would double-count pre-pause cost on resume). if (!workflowId) { - statsLog.debug('Workflow was deleted, skipping user stats update') - return + statsLog.debug('Workflow was deleted, skipping usage recording') + return 0 } + let recordedIncrement = 0 try { const [workflowRecord] = await db .select() @@ -1010,43 +1081,47 @@ export class ExecutionLogger implements IExecutionLoggerService { .limit(1) if (!workflowRecord) { - statsLog.error('Workflow not found for user stats update') - return + statsLog.error('Workflow not found for usage recording') + return 0 } const userId = billingUserId?.trim() || null if (!userId) { - statsLog.error('Missing billing actor in execution context; skipping stats update', { + statsLog.error('Missing billing actor in execution context; skipping usage recording', { trigger, }) - return + return 0 } - const entries: Array<{ - category: 'model' | 'fixed' - source: 'workflow' + // Build the run's *cumulative* target ledger lines from the cost summary. + // The usage_log is then reconciled to these targets: at each completion + // boundary (pause or terminal) we record only the increment versus what + // is already billed for this execution. This bills the full run exactly + // once across pause/resume without double-charging on resume, and keeps + // pre-pause work billed even if the run is later abandoned. + type TargetLine = { + category: 'model' | 'fixed' | 'tool' description: string - cost: number + target: number metadata?: ModelUsageMetadata | null - }> = [] + } + const targets: TargetLine[] = [] if (costSummary.baseExecutionCharge > 0) { - entries.push({ + targets.push({ category: 'fixed', - source: 'workflow', description: 'execution_fee', - cost: costSummary.baseExecutionCharge, + target: costSummary.baseExecutionCharge, }) } if (costSummary.models) { for (const [modelName, modelData] of Object.entries(costSummary.models)) { if (modelData.total > 0) { - entries.push({ + targets.push({ category: 'model', - source: 'workflow', description: modelName, - cost: modelData.total, + target: modelData.total, metadata: { inputTokens: modelData.tokens.input, outputTokens: modelData.tokens.output, @@ -1058,23 +1133,173 @@ export class ExecutionLogger implements IExecutionLoggerService { } } - await recordUsage({ - userId, - entries, - workspaceId: workflowRecord.workspaceId ?? undefined, - workflowId, - executionId, - }) + // Non-model billable charges (standalone hosted-key tool/integration + // blocks). These derive from already-gated span costs in + // calculateCostSummary — BYOK'd tools produce no cost upstream, so they + // never create a row here. Recording them closes the standalone-tool gap + // so the ledger fully reconciles with the run total (no double charge: + // agent-embedded tool cost stays folded into its model row). + if (costSummary.charges) { + for (const [description, charge] of Object.entries(costSummary.charges)) { + if (charge.total > 0) { + targets.push({ category: 'tool', description, target: charge.total }) + } + } + } + + if (targets.length === 0) { + statsLog.debug('No cost to record') + return 0 + } + + // Matches the billedBefore key resolution (toFixed(8)): a delta below this + // is finer than the idempotency key can distinguish across boundaries, so + // ignoring it keeps the key and the gate consistent. + const COST_EPSILON = 1e-8 + + // Build the positive-increment ledger entries for a given already-billed + // snapshot. The eventKey is scoped by the already-billed-so-far amount so + // increments across boundaries never collide, while a retried boundary + // (same already-billed) dedups via onConflictDoNothing. + // + // Reconciliation keys on `description` (model name / tool name), which is + // the billing identity — DO NOT normalize or relabel it. Correctness + // across pause/resume relies on the same usage carrying the same + // description at every boundary; the paused snapshot retains each block's + // original model label, so pre-pause cost stays under its original key + // (delta 0 at terminal) and only genuinely new usage is charged. A future + // change that relabels historical spans would break this invariant. + const buildDeltaEntries = (alreadyBilled: Map) => { + const entries: Array<{ + category: 'model' | 'fixed' | 'tool' + source: 'workflow' + description: string + cost: number + eventKey: string + metadata?: ModelUsageMetadata | null + }> = [] + for (const line of targets) { + const billed = alreadyBilled.get(`${line.category}::${line.description}`) ?? 0 + const delta = line.target - billed + if (delta <= COST_EPSILON) continue + entries.push({ + category: line.category, + source: 'workflow', + description: line.description, + cost: delta, + eventKey: stableEventKey({ + executionId: executionId ?? '', + category: line.category, + description: line.description, + billedBefore: billed.toFixed(8), + }), + ...(line.metadata !== undefined ? { metadata: line.metadata } : {}), + }) + } + return entries + } + + if (executionId) { + // Serialize concurrent completion boundaries for this execution so the + // read-then-insert reconciliation cannot race. pg_advisory_xact_lock is + // transaction-scoped (auto-released on commit/rollback, pool-safe) and + // bounded by lock_timeout. The critical section is one SELECT + one + // INSERT; the lock is uncontended in the normal (already-serialized) + // flow and only matters under a cross-process double-completion of the + // same execution, where it stops a stale already-billed read from + // dropping the larger delta. + await db.transaction(async (tx) => { + await tx.execute( + sql`select set_config('lock_timeout', ${`${USAGE_RECONCILE_LOCK_TIMEOUT_MS}ms`}, true)` + ) + await tx.execute(sql`select pg_advisory_xact_lock(hashtextextended(${executionId}, 0))`) + + // Already-billed for this execution, scoped to the rows this path owns + // (source='workflow') so a same-executionId row from another source + // can't suppress a charge. + const billedRows = await tx + .select({ + category: usageLog.category, + description: usageLog.description, + cost: sql`COALESCE(SUM(${usageLog.cost}), 0)`, + }) + .from(usageLog) + .where(and(eq(usageLog.executionId, executionId), eq(usageLog.source, 'workflow'))) + .groupBy(usageLog.category, usageLog.description) + + const alreadyBilled = new Map() + for (const row of billedRows) { + alreadyBilled.set( + `${row.category}::${row.description}`, + Number.parseFloat(row.cost ?? '0') + ) + } - // Check if user has hit overage threshold and bill incrementally - await checkAndBillOverageThreshold(userId) + const entries = buildDeltaEntries(alreadyBilled) + if (entries.length > 0) { + await recordUsage({ + userId, + entries, + workspaceId: workflowRecord.workspaceId ?? undefined, + workflowId, + executionId, + tx, + ...(billingContext ?? {}), + }) + recordedIncrement = entries.reduce((acc, e) => acc + e.cost, 0) + + // Refine cost_total to the EXACT post-reconciliation ledger sum, + // inside the same advisory-locked tx so it is atomic with the inserts + // and can't be clobbered by a concurrent boundary. Exact by + // construction: under the lock no delta collides, so the new sum is + // the prior workflow-source sum plus the deltas just inserted. This + // supersedes the main-transaction GREATEST baseline (which remains for + // early-return / no-executionId / failed-reconcile paths). + const ledgerSum = + [...alreadyBilled.values()].reduce((acc, v) => acc + v, 0) + recordedIncrement + await tx + .update(workflowExecutionLogs) + .set({ costTotal: ledgerSum.toString() }) + .where(eq(workflowExecutionLogs.executionId, executionId)) + } + }) + } else { + // No execution scope to reconcile/lock against (not expected at a + // workflow completion): record the full targets directly. + const entries = buildDeltaEntries(new Map()) + if (entries.length > 0) { + await recordUsage({ + userId, + entries, + workspaceId: workflowRecord.workspaceId ?? undefined, + workflowId, + ...(billingContext ?? {}), + }) + recordedIncrement = entries.reduce((acc, e) => acc + e.cost, 0) + } + } + + // Enforcement only when billing is enabled: the ledger above is always + // written, but overage/Stripe billing is gated on BILLING_ENABLED. + if (isBillingEnabled) { + await checkAndBillOverageThreshold(userId) + } } catch (error) { - statsLog.error('Error updating user stats with cost information', { - error, - costSummary, - }) - // Don't throw - we want execution to continue even if user stats update fails + // Swallowed so a billing-write failure never fails the execution. The + // reconciliation self-heals on a later boundary; a TERMINAL-boundary + // failure leaves the run under-billed (and cost_total may then exceed + // SUM(usage_log)), so log loudly enough to alert / reconcile out of band. + statsLog.error( + 'Failed to record execution usage to usage_log ledger; charge may be unbilled', + { + error, + billingUserId, + costSummary, + } + ) } + + return recordedIncrement } /** diff --git a/apps/sim/lib/logs/execution/logging-factory.test.ts b/apps/sim/lib/logs/execution/logging-factory.test.ts index 27194c4d913..62601650774 100644 --- a/apps/sim/lib/logs/execution/logging-factory.test.ts +++ b/apps/sim/lib/logs/execution/logging-factory.test.ts @@ -150,7 +150,6 @@ describe('calculateCostSummary', () => { expect(result.totalCost).toBe(BASE_EXECUTION_CHARGE) expect(result.baseExecutionCharge).toBe(BASE_EXECUTION_CHARGE) - expect(result.modelCost).toBe(0) expect(result.totalInputCost).toBe(0) expect(result.totalOutputCost).toBe(0) expect(result.totalTokens).toBe(0) @@ -188,7 +187,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) expect(result.totalCost).toBe(0.03 + BASE_EXECUTION_CHARGE) - expect(result.modelCost).toBe(0.03) expect(result.totalInputCost).toBe(0.01) expect(result.totalOutputCost).toBe(0.02) expect(result.totalTokens).toBe(300) @@ -221,7 +219,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) expect(result.totalCost).toBe(0.033 + BASE_EXECUTION_CHARGE) - expect(result.modelCost).toBe(0.033) expect(result.totalInputCost).toBe(0.011) expect(result.totalOutputCost).toBe(0.022) expect(result.totalTokens).toBe(450) @@ -280,7 +277,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0.03) expect(result.totalCost).toBe(0.03 + BASE_EXECUTION_CHARGE) expect(result.models['claude-3']).toBeDefined() expect(result.models['claude-3'].total).toBe(0.03) @@ -308,7 +304,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0.03) expect(result.models['gpt-4']).toBeDefined() }) @@ -345,7 +340,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0.03) expect(Object.keys(result.models)).toHaveLength(1) }) @@ -361,7 +355,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0.03) expect(result.totalCost).toBe(0.03 + BASE_EXECUTION_CHARGE) // Should not add to models if model is not specified expect(Object.keys(result.models)).toHaveLength(0) @@ -427,7 +420,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0) expect(result.totalCost).toBe(BASE_EXECUTION_CHARGE) // Model is still tracked for token-usage display, but cost must be zero. expect(result.models['claude-opus-4-6'].total).toBe(0) @@ -462,7 +454,6 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0.03) expect(result.totalCost).toBe(0.03 + BASE_EXECUTION_CHARGE) expect(result.models['gpt-4o'].total).toBe(0.03) }) @@ -494,9 +485,95 @@ describe('calculateCostSummary', () => { const result = calculateCostSummary(traceSpans) - expect(result.modelCost).toBe(0.045) expect(result.totalCost).toBe(0.045 + BASE_EXECUTION_CHARGE) expect(result.models['gpt-4o'].total).toBe(0.045) expect(result.models['gpt-4o'].toolCost).toBe(0.015) }) + + test('records a standalone non-model billable span as a charge (closes the tool gap)', () => { + const traceSpans = [ + { + id: 'exa-block', + name: 'Exa Search', + type: 'tool', + cost: { input: 0, output: 0, total: 0.01 }, + }, + ] + + const result = calculateCostSummary(traceSpans) + + expect(result.charges['Exa Search']).toBeDefined() + expect(result.charges['Exa Search'].total).toBe(0.01) + expect(Object.keys(result.models)).toHaveLength(0) + // Ledger partition reconciles with the run total. + const ledgerSum = + result.baseExecutionCharge + + Object.values(result.models).reduce((s, m) => s + m.total, 0) + + Object.values(result.charges).reduce((s, c) => s + c.total, 0) + expect(ledgerSum).toBeCloseTo(result.totalCost, 10) + }) + + test('does not double-count: agent-embedded tool stays in the model row, not charges', () => { + const traceSpans = [ + { + id: 'agent-span', + name: 'Agent', + type: 'agent', + model: 'gpt-4o', + cost: { input: 0.01, output: 0.02, total: 0.045, toolCost: 0.015 }, + tokens: { input: 1000, output: 2000, total: 3000 }, + }, + ] + + const result = calculateCostSummary(traceSpans) + + expect(Object.keys(result.charges)).toHaveLength(0) + expect(result.models['gpt-4o'].total).toBe(0.045) + expect(result.models['gpt-4o'].toolCost).toBe(0.015) + }) + + test('mixed model + standalone tool run reconciles to total', () => { + const traceSpans = [ + { + id: 'agent', + name: 'Agent', + type: 'agent', + model: 'gpt-4o', + cost: { input: 0.01, output: 0.02, total: 0.03 }, + tokens: { input: 100, output: 200, total: 300 }, + }, + { + id: 'exa', + name: 'Exa Search', + type: 'tool', + cost: { input: 0, output: 0, total: 0.01 }, + }, + ] + + const result = calculateCostSummary(traceSpans) + + expect(result.models['gpt-4o'].total).toBe(0.03) + expect(result.charges['Exa Search'].total).toBe(0.01) + const ledgerSum = + result.baseExecutionCharge + + Object.values(result.models).reduce((s, m) => s + m.total, 0) + + Object.values(result.charges).reduce((s, c) => s + c.total, 0) + expect(ledgerSum).toBeCloseTo(result.totalCost, 10) + }) + + test('BYOK tool (no cost generated upstream) produces no charge row', () => { + const traceSpans = [ + { + id: 'exa-byok', + name: 'Exa Search', + type: 'tool', + cost: { input: 0, output: 0, total: 0 }, + }, + ] + + const result = calculateCostSummary(traceSpans) + + expect(Object.keys(result.charges)).toHaveLength(0) + expect(result.totalCost).toBe(BASE_EXECUTION_CHARGE) + }) }) diff --git a/apps/sim/lib/logs/execution/logging-factory.ts b/apps/sim/lib/logs/execution/logging-factory.ts index 9011a39189e..33582faab01 100644 --- a/apps/sim/lib/logs/execution/logging-factory.ts +++ b/apps/sim/lib/logs/execution/logging-factory.ts @@ -87,20 +87,29 @@ export async function loadDeployedWorkflowStateForLogging( type CostTraceSpan = Pick & { type?: TraceSpan['type'] + name?: TraceSpan['name'] children?: CostTraceSpan[] } -type BillableTraceSpan = CostTraceSpan & { cost: NonNullable } - -function hasBillableCost(span: CostTraceSpan): span is BillableTraceSpan { - return span.cost !== undefined +export interface CostSummaryModel { + input: number + output: number + total: number + toolCost?: number + tokens: { input: number; output: number; total: number } } -function isModelBreakdownSpan(span: CostTraceSpan): boolean { - return span.type === 'model' +/** + * Non-model billable charge (e.g. a standalone hosted-key tool block such as + * Exa/Tavily/falai run outside an agent). These spans contribute to the run's + * total cost but carry no `model`, so they live here rather than in `models`. + * Summed per span name so the ledger has one row per integration. + */ +export interface CostSummaryCharge { + total: number } -export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { +export interface CostSummary { totalCost: number totalInputCost: number totalOutputCost: number @@ -108,18 +117,22 @@ export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { totalPromptTokens: number totalCompletionTokens: number baseExecutionCharge: number - modelCost: number - models: Record< - string, - { - input: number - output: number - total: number - toolCost?: number - tokens: { input: number; output: number; total: number } - } - > -} { + models: Record + /** Non-model billable charges keyed by span name (tool/integration costs). */ + charges: Record +} + +type BillableTraceSpan = CostTraceSpan & { cost: NonNullable } + +function hasBillableCost(span: CostTraceSpan): span is BillableTraceSpan { + return span.cost !== undefined +} + +function isModelBreakdownSpan(span: CostTraceSpan): boolean { + return span.type === 'model' +} + +export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): CostSummary { if (!traceSpans || traceSpans.length === 0) { return { totalCost: BASE_EXECUTION_CHARGE, @@ -129,8 +142,8 @@ export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { totalPromptTokens: 0, totalCompletionTokens: 0, baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost: 0, models: {}, + charges: {}, } } @@ -181,16 +194,8 @@ export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { let totalTokens = 0 let totalPromptTokens = 0 let totalCompletionTokens = 0 - const models: Record< - string, - { - input: number - output: number - total: number - toolCost?: number - tokens: { input: number; output: number; total: number } - } - > = {} + const models: Record = {} + const charges: Record = {} for (const span of costSpans) { totalCost += span.cost.total || 0 @@ -220,10 +225,19 @@ export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { if (span.cost.toolCost) { models[model].toolCost = (models[model].toolCost || 0) + span.cost.toolCost } + } else if ((span.cost.total || 0) > 0) { + // Non-model billable span (e.g. a standalone hosted-key tool block). + // These previously contributed to the run total but were never itemized + // in the ledger (the "standalone tool gap"). Key by span name so each + // integration gets a single, reconciling charge row. + const description = span.name || span.type || 'tool' + if (!charges[description]) { + charges[description] = { total: 0 } + } + charges[description].total += span.cost.total || 0 } } - const modelCost = totalCost totalCost += BASE_EXECUTION_CHARGE return { @@ -234,7 +248,7 @@ export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): { totalPromptTokens, totalCompletionTokens, baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost, models, + charges, } } diff --git a/apps/sim/lib/logs/execution/logging-session.test.ts b/apps/sim/lib/logs/execution/logging-session.test.ts index bdd338e1ff5..7ad138d0f93 100644 --- a/apps/sim/lib/logs/execution/logging-session.test.ts +++ b/apps/sim/lib/logs/execution/logging-session.test.ts @@ -75,7 +75,6 @@ vi.mock('@/lib/logs/execution/logging-factory', () => ({ totalPromptTokens: 0, totalCompletionTokens: 0, baseExecutionCharge: 0, - modelCost: 0, models: {}, }), createEnvironmentObject: vi.fn(), @@ -84,6 +83,7 @@ vi.mock('@/lib/logs/execution/logging-factory', () => ({ loadWorkflowStateForExecution: loadWorkflowStateForExecutionMock, })) +import { calculateCostSummary } from '@/lib/logs/execution/logging-factory' import { LoggingSession } from './logging-session' describe('LoggingSession start snapshots', () => { @@ -238,34 +238,54 @@ describe('LoggingSession completion retries', () => { ) }) - it('preserves accumulated cost during fallback completion', async () => { + it('derives fallback cost from trace spans when the primary completion fails', async () => { const session = new LoggingSession('workflow-1', 'execution-6', 'api', 'req-1') as any - session.accumulatedCost = { - total: 12, - input: 5, - output: 7, - tokens: { input: 11, output: 13, total: 24 }, - models: { - 'test-model': { - input: 5, - output: 7, - total: 12, - tokens: { input: 11, output: 13, total: 24 }, - }, - }, + // Resume-accumulation is retired: the cost-only fallback now derives its + // cost summary from the in-memory trace spans (billing itself reconciles + // from the usage_log ledger in recordExecutionUsage). The primary complete() + // path consumes one calculateCostSummary call before it fails, so queue the + // same value twice (primary attempt + fallback). + const spanCostSummary = { + totalCost: 12, + totalInputCost: 5, + totalOutputCost: 7, + totalTokens: 24, + totalPromptTokens: 11, + totalCompletionTokens: 13, + baseExecutionCharge: 0, + models: {}, + charges: {}, } - session.costFlushed = true + vi.mocked(calculateCostSummary) + .mockReturnValueOnce(spanCostSummary) + .mockReturnValueOnce(spanCostSummary) completeWorkflowExecutionMock .mockRejectedValueOnce(new Error('success finalize failed')) .mockResolvedValueOnce({}) - await expect(session.safeComplete({ finalOutput: { ok: true } })).resolves.toBeUndefined() + const traceSpans = [ + { + id: 'span-1', + name: 'Block A', + type: 'tool', + duration: 25, + startTime: '2026-03-13T10:00:00.000Z', + endTime: '2026-03-13T10:00:00.025Z', + status: 'success', + }, + ] as any + + await expect( + session.safeComplete({ finalOutput: { ok: true }, traceSpans }) + ).resolves.toBeUndefined() + expect(calculateCostSummary).toHaveBeenLastCalledWith(traceSpans) expect(completeWorkflowExecutionMock).toHaveBeenLastCalledWith( expect.objectContaining({ executionId: 'execution-6', + finalizationPath: 'fallback_completed', costSummary: expect.objectContaining({ totalCost: 12, totalInputCost: 5, @@ -440,24 +460,21 @@ describe('LoggingSession completion retries', () => { expect(session.complete).toHaveBeenCalledTimes(1) }) - it('drains fire-and-forget cost flushes before terminal completion', async () => { - let releaseFlush: (() => void) | undefined - const flushPromise = new Promise((resolve) => { - releaseFlush = resolve + it('drains fire-and-forget block-complete marker writes before terminal completion', async () => { + let releasePersist: (() => void) | undefined + const persistPromise = new Promise((resolve) => { + releasePersist = resolve }) const session = new LoggingSession('workflow-1', 'execution-1', 'api', 'req-1') as any - session.flushAccumulatedCost = vi.fn(() => flushPromise) + session.persistLastCompletedBlock = vi.fn(() => persistPromise) session.complete = vi.fn().mockResolvedValue(undefined) - await session.onBlockComplete('block-2', 'Transform', 'function', { + // onBlockComplete is now marker-only; its marker write is fire-and-forget + // but tracked, so terminal completion must drain it first. + void session.onBlockComplete('block-2', 'Transform', 'function', { endedAt: '2025-01-01T00:00:01.000Z', - output: { - value: true, - cost: { total: 1, input: 1, output: 0 }, - tokens: { input: 1, output: 0, total: 1 }, - model: 'test-model', - }, + output: { value: true }, }) const completionPromise = session.safeComplete({ finalOutput: { ok: true } }) @@ -466,11 +483,11 @@ describe('LoggingSession completion retries', () => { expect(session.complete).not.toHaveBeenCalled() - releaseFlush?.() + releasePersist?.() await completionPromise - expect(session.flushAccumulatedCost).toHaveBeenCalledTimes(1) + expect(session.persistLastCompletedBlock).toHaveBeenCalledTimes(1) expect(session.complete).toHaveBeenCalledTimes(1) }) diff --git a/apps/sim/lib/logs/execution/logging-session.ts b/apps/sim/lib/logs/execution/logging-session.ts index 56eb463e201..09bfd2348ca 100644 --- a/apps/sim/lib/logs/execution/logging-session.ts +++ b/apps/sim/lib/logs/execution/logging-session.ts @@ -3,7 +3,6 @@ import { workflowExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { and, eq, sql } from 'drizzle-orm' -import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants' import { executionLogger } from '@/lib/logs/execution/logger' import { calculateCostSummary, @@ -118,22 +117,6 @@ export interface SessionPausedParams { workflowInput?: any } -interface AccumulatedCost { - total: number - input: number - output: number - tokens: { input: number; output: number; total: number } - models: Record< - string, - { - input: number - output: number - total: number - tokens: { input: number; output: number; total: number } - } - > -} - export class LoggingSession { private workflowId: string private executionId: string @@ -151,15 +134,7 @@ export class LoggingSession { private completionPromise: Promise | null = null private completionAttempt: CompletionAttempt | null = null private completionAttemptFailed = false - private accumulatedCost: AccumulatedCost = { - total: BASE_EXECUTION_CHARGE, - input: 0, - output: 0, - tokens: { input: 0, output: 0, total: 0 }, - models: {}, - } private pendingProgressWrites = new Set>() - private costFlushed = false private postExecutionPromise: Promise | null = null constructor( @@ -249,7 +224,6 @@ export class LoggingSession { totalPromptTokens: number totalCompletionTokens: number baseExecutionCharge: number - modelCost: number models: Record< string, { @@ -259,6 +233,9 @@ export class LoggingSession { tokens: { input: number; output: number; total: number } } > + // Non-model billable charges (standalone tool/integration costs). Carried + // through so the partition can't be silently dropped at this boundary. + charges?: Record } finalOutput: Record traceSpans: TraceSpan[] @@ -292,46 +269,9 @@ export class LoggingSession { blockType: string, output: any ): Promise { - // Accumulate cost synchronously before any await so that fire-and-forget - // callers still capture the full cost even if DB writes are not awaited. - const blockOutput = output?.output - if ( - blockOutput?.cost && - typeof blockOutput.cost.total === 'number' && - blockOutput.cost.total > 0 - ) { - const { cost, tokens, model } = blockOutput - - this.accumulatedCost.total += cost.total || 0 - this.accumulatedCost.input += cost.input || 0 - this.accumulatedCost.output += cost.output || 0 - - if (tokens) { - this.accumulatedCost.tokens.input += tokens.input || 0 - this.accumulatedCost.tokens.output += tokens.output || 0 - this.accumulatedCost.tokens.total += tokens.total || 0 - } - - if (model) { - if (!this.accumulatedCost.models[model]) { - this.accumulatedCost.models[model] = { - input: 0, - output: 0, - total: 0, - tokens: { input: 0, output: 0, total: 0 }, - } - } - this.accumulatedCost.models[model].input += cost.input || 0 - this.accumulatedCost.models[model].output += cost.output || 0 - this.accumulatedCost.models[model].total += cost.total || 0 - if (tokens) { - this.accumulatedCost.models[model].tokens.input += tokens.input || 0 - this.accumulatedCost.models[model].tokens.output += tokens.output || 0 - this.accumulatedCost.models[model].tokens.total += tokens.total || 0 - } - } - } - + // Cost is recorded into the usage_log ledger and reconciled at completion + // boundaries (see recordExecutionUsage); onBlockComplete only persists the + // last-completed-block progress marker. await this.trackProgressWrite( this.persistLastCompletedBlock({ blockId, @@ -341,76 +281,6 @@ export class LoggingSession { success: !output?.output?.error, }) ) - - if ( - blockOutput?.cost && - typeof blockOutput.cost.total === 'number' && - blockOutput.cost.total > 0 - ) { - void this.trackProgressWrite(this.flushAccumulatedCost()) - } - } - - private async flushAccumulatedCost(): Promise { - try { - await db - .update(workflowExecutionLogs) - .set({ - cost: { - total: this.accumulatedCost.total, - input: this.accumulatedCost.input, - output: this.accumulatedCost.output, - tokens: this.accumulatedCost.tokens, - models: this.accumulatedCost.models, - }, - }) - .where( - and( - eq(workflowExecutionLogs.workflowId, this.workflowId), - eq(workflowExecutionLogs.executionId, this.executionId) - ) - ) - - this.costFlushed = true - } catch (error) { - logger.error(`Failed to flush accumulated cost for execution ${this.executionId}:`, { - error: toError(error).message, - }) - } - } - - private async loadExistingCost(): Promise { - try { - const [existing] = await db - .select({ cost: workflowExecutionLogs.cost }) - .from(workflowExecutionLogs) - .where( - and( - eq(workflowExecutionLogs.workflowId, this.workflowId), - eq(workflowExecutionLogs.executionId, this.executionId) - ) - ) - .limit(1) - - if (existing?.cost) { - const cost = existing.cost as AccumulatedCost - this.accumulatedCost = { - total: cost.total || BASE_EXECUTION_CHARGE, - input: cost.input || 0, - output: cost.output || 0, - tokens: { - input: cost.tokens?.input || 0, - output: cost.tokens?.output || 0, - total: cost.tokens?.total || 0, - }, - models: cost.models || {}, - } - } - } catch (error) { - logger.error(`Failed to load existing cost for execution ${this.executionId}:`, { - error: toError(error).message, - }) - } } async start(params: SessionStartParams): Promise { @@ -451,8 +321,9 @@ export class LoggingSession { deploymentVersionId, }) } else { + // Resume: no cost reload needed. Billing reconciles from the usage_log + // ledger (pre-pause rows already exist) plus the live cost summary. this.isResume = true - await this.loadExistingCost() } } catch (error) { if (this.requestId) { @@ -577,6 +448,8 @@ export class LoggingSession { const hasProvidedSpans = Array.isArray(traceSpans) && traceSpans.length > 0 + // calculateCostSummary([]) / (undefined) already returns the base-charge + // summary, so the no-spans branch needs no separate literal. const costSummary = skipCost ? { totalCost: 0, @@ -586,22 +459,10 @@ export class LoggingSession { totalPromptTokens: 0, totalCompletionTokens: 0, baseExecutionCharge: 0, - modelCost: 0, models: {}, + charges: {}, } - : hasProvidedSpans - ? calculateCostSummary(traceSpans) - : { - totalCost: BASE_EXECUTION_CHARGE, - totalInputCost: 0, - totalOutputCost: 0, - totalTokens: 0, - totalPromptTokens: 0, - totalCompletionTokens: 0, - baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost: 0, - models: {}, - } + : calculateCostSummary(traceSpans) const message = error?.message || 'Run failed before starting blocks' @@ -710,19 +571,9 @@ export class LoggingSession { return } - const costSummary = traceSpans?.length - ? calculateCostSummary(traceSpans) - : { - totalCost: BASE_EXECUTION_CHARGE, - totalInputCost: 0, - totalOutputCost: 0, - totalTokens: 0, - totalPromptTokens: 0, - totalCompletionTokens: 0, - baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost: 0, - models: {}, - } + // calculateCostSummary handles empty/undefined spans by returning the + // base-charge summary, so no separate no-spans literal is needed. + const costSummary = calculateCostSummary(traceSpans) await this.completeExecutionWithFinalization({ endedAt: endTime.toISOString(), @@ -814,19 +665,9 @@ export class LoggingSession { return } - const costSummary = traceSpans?.length - ? calculateCostSummary(traceSpans) - : { - totalCost: BASE_EXECUTION_CHARGE, - totalInputCost: 0, - totalOutputCost: 0, - totalTokens: 0, - totalPromptTokens: 0, - totalCompletionTokens: 0, - baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost: 0, - models: {}, - } + // calculateCostSummary handles empty/undefined spans by returning the + // base-charge summary, so no separate no-spans literal is needed. + const costSummary = calculateCostSummary(traceSpans) await this.completeExecutionWithFinalization({ endedAt: endTime.toISOString(), @@ -1185,37 +1026,12 @@ export class LoggingSession { ) try { - const hasAccumulatedCost = - this.costFlushed || - this.accumulatedCost.total > BASE_EXECUTION_CHARGE || - this.accumulatedCost.tokens.total > 0 || - Object.keys(this.accumulatedCost.models).length > 0 - - const costSummary = hasAccumulatedCost - ? { - totalCost: this.accumulatedCost.total, - totalInputCost: this.accumulatedCost.input, - totalOutputCost: this.accumulatedCost.output, - totalTokens: this.accumulatedCost.tokens.total, - totalPromptTokens: this.accumulatedCost.tokens.input, - totalCompletionTokens: this.accumulatedCost.tokens.output, - baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost: Math.max(0, this.accumulatedCost.total - BASE_EXECUTION_CHARGE), - models: this.accumulatedCost.models, - } - : params.traceSpans?.length - ? calculateCostSummary(params.traceSpans) - : { - totalCost: BASE_EXECUTION_CHARGE, - totalInputCost: 0, - totalOutputCost: 0, - totalTokens: 0, - totalPromptTokens: 0, - totalCompletionTokens: 0, - baseExecutionCharge: BASE_EXECUTION_CHARGE, - modelCost: 0, - models: {}, - } + // Billing is reconciled from the usage_log ledger in recordExecutionUsage; + // here we only need a cost summary to compute the run total. Derive it + // from the in-memory trace spans when available (this fallback fires when + // persisting spans failed, not when computing them did), else just the + // base execution charge. + const costSummary = calculateCostSummary(params.traceSpans) const finalOutput = params.finalOutput || { _fallback: true, error: params.errorMessage } diff --git a/apps/sim/lib/logs/execution/trace-store.ts b/apps/sim/lib/logs/execution/trace-store.ts new file mode 100644 index 00000000000..83312eb1ec2 --- /dev/null +++ b/apps/sim/lib/logs/execution/trace-store.ts @@ -0,0 +1,179 @@ +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import { isLargeValueRef } from '@/lib/execution/payloads/large-value-ref' +import { materializeLargeValueRef, storeLargeValue } from '@/lib/execution/payloads/store' + +const logger = createLogger('TraceStore') + +/** + * Key under which the externalized-execution-data pointer (a `__simLargeValueRef`) + * is stored on the slim `execution_data` row. + */ +export const TRACE_STORE_REF_KEY = 'traceStoreRef' + +/** + * The only metadata kept inline on the slim row (everything else lives in the + * externalized object). These two describe trace presence/count and uniquely + * survive object expiry — so a reader can still report "trace data expired (N + * spans)" after retention without an object fetch. All other fields + * (environment, trigger, tokens, models, truncation flags, and of course the + * heavy payloads) are in the stored object and recovered on materialize, so + * keeping them inline too would just be duplication. + */ +const INLINE_MARKER_KEYS = ['hasTraceSpans', 'traceSpanCount'] as const + +/** + * Read-path context. Resolves an externalized payload by storage key, authorized + * via the (already-authorized) workspace — no owner needed. + */ +interface TraceStoreReadContext { + workspaceId: string | null + workflowId: string | null + executionId: string +} + +/** + * Write-path context. Requires the execution owner's `userId`: the externalized + * object is tracked in `workspace_files`, whose `user_id` column is NOT NULL + * (FK -> user.id). Requiring it here makes "a write needs an owner" a + * compile-time invariant, so callers must resolve the owner before persisting. + */ +interface TraceStoreWriteContext extends TraceStoreReadContext { + userId: string +} + +/** + * Recovers the workflowId embedded in a large-value storage key + * (`execution/{workspaceId}/{workflowId}/{executionId}/`). Used when the + * log row's workflowId has been nulled by workflow deletion. + */ +function workflowIdFromStorageKey(key: string | undefined): string | undefined { + if (!key) return undefined + const parts = key.split('/') + return parts.length >= 5 && parts[0] === 'execution' ? parts[2] : undefined +} + +/** + * Recursively removes `cost` from trace spans before persistence. Cost lives in + * exactly one place — the usage_log ledger — so persisted spans carry only + * structure, timing, and tokens (KTD7). Must run AFTER `calculateCostSummary` + * has consumed span costs in memory. + */ +export function stripSpanCosts(spans: unknown): void { + if (!Array.isArray(spans)) return + for (const span of spans) { + if (!span || typeof span !== 'object') continue + const record = span as { cost?: unknown; children?: unknown } + if ('cost' in record) record.cost = undefined + if (Array.isArray(record.children)) stripSpanCosts(record.children) + } +} + +/** + * Externalizes heavy `execution_data` to object storage as a single large value + * (reusing the execution-context large-value store + its reference/dependency/GC + * machinery — KTD4/KTD8), returning a slim row payload that keeps inline markers + * plus the `__simLargeValueRef` pointer. + * + * On any failure (no scope, oversized, storage error) the original (already + * cost-stripped) execution data is returned unchanged so the log is never lost. + */ +export async function externalizeExecutionData( + executionData: Record, + context: TraceStoreWriteContext +): Promise> { + const { workspaceId, workflowId, executionId, userId } = context + // workspaceId/workflowId build the storage key and can be null for + // deleted-workflow rows. userId is type-guaranteed by TraceStoreWriteContext; + // the falsy check is a defensive guard against an empty string. If any are + // missing the durable write can't succeed, so keep the data inline. + if (!workspaceId || !workflowId || !userId) return executionData + + try { + const json = JSON.stringify(executionData) + const size = Buffer.byteLength(json, 'utf8') + + // storeLargeValue persists to the execution bucket with a conforming key and + // registers owner + dependency closure (trace -> nested span large values), + // so GC keeps nested children alive while this run's log row exists. + const ref = await storeLargeValue(executionData, json, size, { + workspaceId, + workflowId, + executionId, + userId, + requireDurable: true, + }) + + const { preview: _preview, ...slimRef } = ref + + const slim: Record = { [TRACE_STORE_REF_KEY]: slimRef } + for (const key of INLINE_MARKER_KEYS) { + if (key in executionData) slim[key] = executionData[key] + } + return slim + } catch (error) { + logger.warn('Failed to externalize execution data; keeping inline', { + executionId, + error: toError(error).message, + }) + return executionData + } +} + +/** + * Resolves an `execution_data` row into its full form for reads. When the row + * carries a trace-store pointer, the payload is materialized from storage and + * merged with the inline markers; otherwise the row is returned unchanged + * (inline / pre-externalization runs). One level only — nested span + * `__simLargeValueRef` stubs remain as previews, matching prior behavior. + * + * Returns metadata-only (the slim row minus the pointer) if the object is + * missing/unreadable (e.g. post-retention) so reads degrade rather than crash. + */ +export async function materializeExecutionData( + executionData: Record | null | undefined, + context: TraceStoreReadContext +): Promise> { + if (!executionData) return {} + + const ref = executionData[TRACE_STORE_REF_KEY] + if (!isLargeValueRef(ref)) return executionData + + const { [TRACE_STORE_REF_KEY]: _pointer, ...markers } = executionData + + if (!context.workspaceId) return markers + + // workflowId is `set null` on workflow delete, but the ref key embeds the + // original workflowId — recover it so deleted-workflow logs stay readable. + // Workspace authorization still comes from the (authorized) caller context. + const workflowId = context.workflowId ?? workflowIdFromStorageKey(ref.key) + if (!workflowId) return markers + + try { + const materialized = await materializeLargeValueRef(ref, { + workspaceId: context.workspaceId, + workflowId, + executionId: context.executionId, + maxBytes: ref.size, + // Read-only: the value is already referenced by its own execution; don't + // re-register (or fail) on every view/export. + trackReference: false, + }) + + if (!materialized || typeof materialized !== 'object') { + logger.warn('Trace store object unavailable; returning metadata only', { + executionId: context.executionId, + key: ref.key, + }) + return markers + } + + return { ...(materialized as Record), ...markers } + } catch (error) { + logger.warn('Failed to materialize execution data; returning metadata only', { + executionId: context.executionId, + error: toError(error).message, + }) + return markers + } +} diff --git a/apps/sim/lib/logs/fetch-log-detail.ts b/apps/sim/lib/logs/fetch-log-detail.ts index 1a5aea4dc26..e1cef8728d2 100644 --- a/apps/sim/lib/logs/fetch-log-detail.ts +++ b/apps/sim/lib/logs/fetch-log-detail.ts @@ -3,14 +3,69 @@ import { jobExecutionLogs, pausedExecutions, permissions, + usageLog, workflow, workflowDeploymentVersion, workflowExecutionLogs, } from '@sim/db/schema' import { and, eq, type SQL } from 'drizzle-orm' +import type { CostLedger } from '@/lib/api/contracts/logs' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' type LookupColumn = 'id' | 'executionId' +async function buildCostLedger(executionId: string): Promise { + const rows = await db + .select({ + category: usageLog.category, + description: usageLog.description, + cost: usageLog.cost, + metadata: usageLog.metadata, + }) + .from(usageLog) + .where(and(eq(usageLog.executionId, executionId), eq(usageLog.source, 'workflow'))) + + if (rows.length === 0) return null + + type LedgerItem = CostLedger['items'][number] + const byKey = new Map() + for (const row of rows) { + const metadata = (row.metadata ?? {}) as { inputTokens?: number; outputTokens?: number } + const category = row.category as LedgerItem['category'] + const key = `${category}::${row.description}` + const existing = byKey.get(key) + if (existing) { + existing.cost += Number(row.cost) + if (typeof metadata.inputTokens === 'number') { + existing.inputTokens = Math.max(existing.inputTokens ?? 0, metadata.inputTokens) + } + if (typeof metadata.outputTokens === 'number') { + existing.outputTokens = Math.max(existing.outputTokens ?? 0, metadata.outputTokens) + } + } else { + byKey.set(key, { + category, + description: row.description, + cost: Number(row.cost), + ...(typeof metadata.inputTokens === 'number' ? { inputTokens: metadata.inputTokens } : {}), + ...(typeof metadata.outputTokens === 'number' + ? { outputTokens: metadata.outputTokens } + : {}), + }) + } + } + + const items = [...byKey.values()] + const total = items.reduce((sum, item) => sum + item.cost, 0) + return { total, items } +} + +export function jobCostTotal(raw: unknown): { total: number } | null { + const total = (raw as { total?: unknown } | null | undefined)?.total + const n = total == null ? Number.NaN : Number(total) + return Number.isFinite(n) ? { total: n } : null +} + interface FetchLogDetailArgs { userId: string workspaceId: string @@ -47,7 +102,7 @@ export async function fetchLogDetail({ endedAt: workflowExecutionLogs.endedAt, totalDurationMs: workflowExecutionLogs.totalDurationMs, executionData: workflowExecutionLogs.executionData, - cost: workflowExecutionLogs.cost, + costTotal: workflowExecutionLogs.costTotal, files: workflowExecutionLogs.files, createdAt: workflowExecutionLogs.createdAt, workflowName: workflow.name, @@ -105,6 +160,18 @@ export async function fetchLogDetail({ (totalPauseCount > 0 && resumedCount < totalPauseCount) || (log.pausedStatus !== null && log.pausedStatus !== 'fully_resumed') + // Cost is sourced exclusively from the usage_log ledger (itemized breakdown) + // and its cost_total projection (run total). The cost jsonb is never read. + const costLedger = await buildCostLedger(log.executionId) + const totalDollars = costLedger?.total ?? (log.costTotal != null ? Number(log.costTotal) : null) + + // Trace spans / heavy execution data may live in object storage; resolve the + // pointer here (no-op for inline / pre-externalization rows). + const executionData = await materializeExecutionData( + log.executionData as Record | null, + { workspaceId, workflowId: log.workflowId, executionId: log.executionId } + ) + return { id: log.id, workflowId: log.workflowId, @@ -119,7 +186,8 @@ export async function fetchLogDetail({ createdAt: log.startedAt.toISOString(), workflow: workflowSummary, jobTitle: null, - cost: log.cost ?? null, + cost: totalDollars != null ? { total: totalDollars } : null, + costLedger, pauseSummary: { status: log.pausedStatus ?? null, total: totalPauseCount, @@ -128,7 +196,7 @@ export async function fetchLogDetail({ hasPendingPause, executionData: { totalDuration: log.totalDurationMs, - ...((log.executionData as Record | null) ?? {}), + ...executionData, enhanced: true as const, }, files: log.files ?? null, @@ -184,7 +252,7 @@ export async function fetchLogDetail({ createdAt: jobLog.startedAt.toISOString(), workflow: null, jobTitle: ((execData.trigger as Record | undefined)?.source as string) ?? null, - cost: jobLog.cost ?? null, + cost: jobCostTotal(jobLog.cost), pauseSummary: { status: null, total: 0, resumed: 0 }, hasPendingPause: false, executionData: { diff --git a/apps/sim/lib/logs/filters.ts b/apps/sim/lib/logs/filters.ts index cb2e1dccf29..0569e535a5c 100644 --- a/apps/sim/lib/logs/filters.ts +++ b/apps/sim/lib/logs/filters.ts @@ -195,7 +195,8 @@ function buildSearchConditions(params: { } function buildCostCondition(operator: ComparisonOperator, value: number): SQL { - const costField = sql`(${workflowExecutionLogs.cost}->>'total')::numeric` + // Indexed projection of the usage_log ledger (dollars); no live aggregation. + const costField = sql`${workflowExecutionLogs.costTotal}` switch (operator) { case '=': diff --git a/apps/sim/lib/table/service.ts b/apps/sim/lib/table/service.ts index 18f1fda418e..ae4bce8eb37 100644 --- a/apps/sim/lib/table/service.ts +++ b/apps/sim/lib/table/service.ts @@ -18,8 +18,10 @@ import { createLogger } from '@sim/logger' import { getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, count, eq, gt, gte, inArray, isNull, type SQL, sql } from 'drizzle-orm' +import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency' import { generateRestoreName } from '@/lib/core/utils/restore-name' import type { DbOrTx } from '@/lib/db/types' +import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { COLUMN_TYPES, NAME_PATTERN, TABLE_LIMITS, USER_TABLE_ROWS_SQL_NAME } from './constants' import { buildFilterClause, buildSortClause } from './sql' import { fireTableTrigger } from './trigger' @@ -3935,18 +3937,26 @@ async function backfillGroupOutputsFromLogs(opts: { const logs = await db .select({ executionId: workflowExecutionLogs.executionId, + workflowId: workflowExecutionLogs.workflowId, + workspaceId: workflowExecutionLogs.workspaceId, executionData: workflowExecutionLogs.executionData, }) .from(workflowExecutionLogs) .where(inArray(workflowExecutionLogs.executionId, executionIds)) const logByExecutionId = new Map() - for (const log of logs) { + // Heavy execution data may live in object storage; resolve pointers (bounded + // concurrency) so trace spans are available for table-column enrichment. + await mapWithConcurrency(logs, MATERIALIZE_CONCURRENCY, async (log) => { + const executionData = await materializeExecutionData( + log.executionData as Record | null, + { workspaceId: log.workspaceId, workflowId: log.workflowId, executionId: log.executionId } + ) logByExecutionId.set( log.executionId, - (log.executionData as { traceSpans?: BackfillTraceSpan[] }) ?? {} + (executionData as { traceSpans?: BackfillTraceSpan[] }) ?? {} ) - } + }) const updates: Array<{ rowId: string; data: RowData }> = [] for (const r of rowRecords) { diff --git a/apps/sim/lib/workspaces/organization/types.ts b/apps/sim/lib/workspaces/organization/types.ts index e3f07c50c64..fd1e770eb43 100644 --- a/apps/sim/lib/workspaces/organization/types.ts +++ b/apps/sim/lib/workspaces/organization/types.ts @@ -71,7 +71,6 @@ interface MemberUsageData { isOverLimit: boolean role: string joinedAt: string - lastActive: string | null } interface OrganizationBillingData { diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 205fb307873..5815f0aecfb 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -3,7 +3,7 @@ import { getErrorMessage } from '@sim/utils/errors' import type OpenAI from 'openai' import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { dollarsToCredits } from '@/lib/billing/credits/conversion' +import { formatCreditCost } from '@/lib/billing/credits/conversion' import { env } from '@/lib/core/config/env' import { getBlacklistedProvidersFromEnv, isHosted } from '@/lib/core/config/feature-flags' import { @@ -700,11 +700,7 @@ export function getModelPricing(modelId: string): any { * @returns Formatted credit string (e.g. "200 credits", "<1 credit", "0 credits") */ export function formatCost(cost: number): string { - if (cost === undefined || cost === null) return '—' - const credits = dollarsToCredits(cost) - if (credits <= 0 && cost > 0) return '<1 credit' - if (credits <= 0) return '0 credits' - return `${credits.toLocaleString()} credits` + return formatCreditCost(cost) ?? '—' } /** diff --git a/apps/sim/scripts/backfill-trace-spans.ts b/apps/sim/scripts/backfill-trace-spans.ts new file mode 100644 index 00000000000..1fb3a9fd8f3 --- /dev/null +++ b/apps/sim/scripts/backfill-trace-spans.ts @@ -0,0 +1,188 @@ +#!/usr/bin/env bun + +/** + * One-shot, idempotent, resumable backfill that externalizes inline heavy + * `execution_data` (traceSpans, finalOutput, workflowInput, ...) into the + * execution-context large-value store, matching the completion path (cost-stripped + * spans, trace pointer + markers, owner/dependency + execution_log reference + * registration). Skips running rows and rows already carrying the pointer. + * + * Requires object storage to be configured; self-hosted deployments without it + * keep `execution_data` inline (reads resolve inline transparently) and can skip + * this script entirely. + * + * NOTE: the companion `cost_total` / `models_used` backfill is done in SQL by + * migration 0220 (batched, idempotent), so it runs for everyone — including + * self-hosted — and is intentionally NOT part of this script. + * + * Usage: + * DATABASE_URL=... bun apps/sim/scripts/backfill-trace-spans.ts [--max-batches=] + */ + +import { db } from '@sim/db' +import { workflowExecutionLogs } from '@sim/db/schema' +import { toError } from '@sim/utils/errors' +import { and, asc, eq, gt, sql } from 'drizzle-orm' +import { + collectLargeValueReferenceKeys, + replaceLargeValueReferenceKeysWithClient, +} from '@/lib/execution/payloads/large-value-metadata' +import { + externalizeExecutionData, + stripSpanCosts, + TRACE_STORE_REF_KEY, +} from '@/lib/logs/execution/trace-store' + +const TRACE_BATCH_SIZE = 100 + +/** + * Recursively counts trace spans (matching the completion path). Legacy rows + * predate the inline hasTraceSpans/traceSpanCount markers, so we derive them + * before externalizing — otherwise a post-expiry degraded read can't report + * "trace data expired (N spans)". + */ +function countTraceSpans(spans: unknown): number { + if (!Array.isArray(spans)) return 0 + return spans.reduce( + (count: number, span) => + count + 1 + countTraceSpans((span as { children?: unknown } | null)?.children), + 0 + ) +} + +interface Options { + maxBatches: number +} + +function parseArgs(argv: string[]): Options { + const maxBatchesArg = argv.find((a) => a.startsWith('--max-batches=')) + const maxBatches = maxBatchesArg + ? Number.parseInt(maxBatchesArg.slice('--max-batches='.length), 10) + : Number.POSITIVE_INFINITY + + if (Number.isNaN(maxBatches) || maxBatches <= 0) { + throw new Error('--max-batches must be a positive integer') + } + + return { maxBatches } +} + +/** Externalize inline heavy execution_data into the large-value store. */ +async function backfillTraceStorage( + maxBatches: number +): Promise<{ migrated: number; failed: number }> { + let migrated = 0 + let failed = 0 + // Keyset cursor by id: every row is visited at most once per run, so rows that + // can't be externalized (storage error, oversized) aren't re-selected into an + // infinite loop. A fresh re-run (cursor reset) retries any that failed. + let lastId = '' + + for (let batch = 0; batch < maxBatches; batch++) { + const rows = await db + .select({ + id: workflowExecutionLogs.id, + workspaceId: workflowExecutionLogs.workspaceId, + workflowId: workflowExecutionLogs.workflowId, + executionId: workflowExecutionLogs.executionId, + executionData: workflowExecutionLogs.executionData, + }) + .from(workflowExecutionLogs) + .where( + and( + sql`${workflowExecutionLogs.endedAt} IS NOT NULL`, + // Skip deleted-workflow rows: externalization requires a workflowId. + sql`${workflowExecutionLogs.workflowId} IS NOT NULL`, + sql`${workflowExecutionLogs.executionData} ? 'traceSpans'`, + sql`NOT (${workflowExecutionLogs.executionData} ? ${TRACE_STORE_REF_KEY})`, + lastId ? gt(workflowExecutionLogs.id, lastId) : undefined + ) + ) + .orderBy(asc(workflowExecutionLogs.id)) + .limit(TRACE_BATCH_SIZE) + + if (rows.length === 0) break + + for (const row of rows) { + try { + const executionData = (row.executionData ?? {}) as Record + // Derive the inline markers legacy rows lack so externalizeExecutionData + // carries them onto the slim row (they survive object expiry). + const traceSpanCount = countTraceSpans(executionData.traceSpans) + executionData.hasTraceSpans = traceSpanCount > 0 + executionData.traceSpanCount = traceSpanCount + stripSpanCosts(executionData.traceSpans) + // workspace_files.user_id (NOT NULL) needs the execution owner; legacy + // rows carry it under executionData.environment.userId. Rows without an + // owner can't be externalized — count them as failed and skip. + const environment = executionData.environment as { userId?: string } | undefined + const ownerUserId = environment?.userId + if (!ownerUserId) { + failed++ + continue + } + const slim = await externalizeExecutionData(executionData, { + workspaceId: row.workspaceId, + workflowId: row.workflowId, + executionId: row.executionId, + userId: ownerUserId, + }) + + if (!(TRACE_STORE_REF_KEY in slim)) { + failed++ + continue + } + + await db.transaction(async (tx) => { + await tx + .update(workflowExecutionLogs) + .set({ executionData: slim }) + .where(eq(workflowExecutionLogs.id, row.id)) + + await replaceLargeValueReferenceKeysWithClient( + tx, + { + workspaceId: row.workspaceId, + workflowId: row.workflowId, + executionId: row.executionId, + source: 'execution_log', + }, + collectLargeValueReferenceKeys(slim) + ) + }) + + migrated++ + } catch (error) { + failed++ + console.error(` [trace] row ${row.id} failed: ${toError(error).message}`) + } + } + + // Advance the cursor past this batch so failed rows aren't re-selected. + lastId = rows[rows.length - 1].id + + console.log(` [trace] batch ${batch + 1}: migrated ${migrated}, failed ${failed}`) + } + + return { migrated, failed } +} + +async function main(): Promise { + const options = parseArgs(process.argv.slice(2)) + const startedAt = Date.now() + + console.log('Backfilling trace storage (externalizing execution_data)…') + const { migrated, failed } = await backfillTraceStorage(options.maxBatches) + console.log(`Trace storage done: ${migrated} migrated, ${failed} skipped/failed.`) + + console.log(`Backfill complete in ${((Date.now() - startedAt) / 1000).toFixed(1)}s.`) +} + +main() + .catch((err) => { + console.error('Backfill failed:', err) + process.exit(1) + }) + .finally(() => { + process.exit(0) + }) diff --git a/bun.lock b/bun.lock index fdee6edfc42..070992c9a4f 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "simstudio", diff --git a/packages/db/migrations/0220_early_hellion.sql b/packages/db/migrations/0220_early_hellion.sql new file mode 100644 index 00000000000..558f964c2d9 --- /dev/null +++ b/packages/db/migrations/0220_early_hellion.sql @@ -0,0 +1,36 @@ +ALTER TABLE "workflow_execution_logs" ADD COLUMN IF NOT EXISTS "cost_total" numeric;--> statement-breakpoint +ALTER TABLE "workflow_execution_logs" ADD COLUMN IF NOT EXISTS "models_used" text[];--> statement-breakpoint +COMMIT;--> statement-breakpoint +ALTER TYPE "public"."usage_log_category" ADD VALUE IF NOT EXISTS 'tool';--> statement-breakpoint +CREATE OR REPLACE PROCEDURE backfill_wel_cost_total_0220() LANGUAGE plpgsql AS $$ +DECLARE + updated integer; +BEGIN + LOOP + WITH candidates AS ( + SELECT id FROM workflow_execution_logs + WHERE cost_total IS NULL + AND cost ? 'total' + AND (cost->>'total') ~ '^-?[0-9]+(\.[0-9]+)?$' + LIMIT 5000 + ) + UPDATE workflow_execution_logs wel + SET cost_total = NULLIF(wel.cost->>'total', '')::numeric, + models_used = CASE + WHEN jsonb_typeof(wel.cost->'models') = 'object' + THEN ARRAY(SELECT jsonb_object_keys(wel.cost->'models')) + ELSE wel.models_used + END + FROM candidates + WHERE wel.id = candidates.id; + GET DIAGNOSTICS updated = ROW_COUNT; + EXIT WHEN updated = 0; + COMMIT; + END LOOP; +END; +$$;--> statement-breakpoint +CALL backfill_wel_cost_total_0220();--> statement-breakpoint +DROP PROCEDURE backfill_wel_cost_total_0220();--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "usage_log_execution_id_idx" ON "usage_log" USING btree ("execution_id");--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "workflow_execution_logs_workspace_cost_total_idx" ON "workflow_execution_logs" USING btree ("workspace_id","cost_total");--> statement-breakpoint +CREATE INDEX CONCURRENTLY IF NOT EXISTS "workflow_execution_logs_models_used_idx" ON "workflow_execution_logs" USING gin ("models_used"); diff --git a/packages/db/migrations/meta/0220_snapshot.json b/packages/db/migrations/meta/0220_snapshot.json new file mode 100644 index 00000000000..64c6073d85c --- /dev/null +++ b/packages/db/migrations/meta/0220_snapshot.json @@ -0,0 +1,17582 @@ +{ + "id": "10027d61-99fa-4082-99a8-27be35fc3d8a", + "prevId": "9c04ddff-9332-4201-be6f-98fbf006874a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": ["certificate_number"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"form\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_archived_at_partial_idx": { + "name": "form_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"form\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_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.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_access_token_access_token_idx": { + "name": "oauth_access_token_access_token_idx", + "columns": [ + { + "expression": "access_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_token_idx": { + "name": "oauth_access_token_refresh_token_idx", + "columns": [ + { + "expression": "refresh_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_application_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_access_token_unique": { + "name": "oauth_access_token_access_token_unique", + "nullsNotDistinct": false, + "columns": ["access_token"] + }, + "oauth_access_token_refresh_token_unique": { + "name": "oauth_access_token_refresh_token_unique", + "nullsNotDistinct": false, + "columns": ["refresh_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_application": { + "name": "oauth_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_urls": { + "name": "redirect_urls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_application_client_id_idx": { + "name": "oauth_application_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_application_user_id_user_id_fk": { + "name": "oauth_application_user_id_user_id_fk", + "tableFrom": "oauth_application", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_application_client_id_unique": { + "name": "oauth_application_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "consent_given": { + "name": "consent_given", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_user_client_idx": { + "name": "oauth_consent_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_application_client_id_fk": { + "name": "oauth_consent_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_name_unique": { + "name": "permission_group_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_auto_add_unique": { + "name": "permission_group_workspace_auto_add_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_id_workspace_id_fk", + "tableFrom": "permission_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_workspace_user_unique": { + "name": "permission_group_member_workspace_user_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_workspace_id_workspace_id_fk": { + "name": "permission_group_member_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "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_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_table_id_idx": { + "name": "user_table_rows_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_data_gin_idx": { + "name": "user_table_rows_data_gin_idx", + "columns": [ + { + "expression": "data", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 9ac105fb2e4..f348c087a60 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1534,6 +1534,13 @@ "when": 1780079220753, "tag": "0219_amused_leo", "breakpoints": true + }, + { + "idx": 220, + "version": "7", + "when": 1780081787541, + "tag": "0220_early_hellion", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 32beb356dd5..4e9b691a4ce 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -329,8 +329,25 @@ export const workflowExecutionLogs = pgTable( endedAt: timestamp('ended_at'), totalDurationMs: integer('total_duration_ms'), + /** + * Heavy trace data (traceSpans, finalOutput, workflowInput, executionState) + * is externalized to object storage; this column then holds a slim payload: + * a `traceStoreRef` (__simLargeValueRef) pointer to the stored object plus + * inline markers (hasTraceSpans, traceSpanCount, environment, trigger, + * truncation flags). It also still holds the FULL payload inline for legacy + * / not-yet-backfilled rows, for the storage-write-failure fallback, and for + * job_execution_logs. Required — not droppable. Read it via + * `materializeExecutionData`, which resolves the pointer. + */ executionData: jsonb('execution_data').notNull().default('{}'), + /** @deprecated Not written/read; cost lives in usage_log + the `cost_total` projection. Drop in a follow-up PR after the `cost_total` backfill. */ cost: jsonb('cost'), + // Faithful, write-once projection of the run's usage_log ledger sum (dollars). + // Backs list cost display/filter/sort without live aggregation; never an + // independently-computed value (cost_total == SUM(usage_log) for the run). + costTotal: decimal('cost_total'), + // Model names used by the run (incl. zero-cost/BYOK), for the v1 model filter. + modelsUsed: text('models_used').array(), files: jsonb('files'), // File metadata for execution files createdAt: timestamp('created_at').notNull().defaultNow(), }, @@ -356,6 +373,11 @@ export const workflowExecutionLogs = pgTable( table.workspaceId, table.startedAt ), + workspaceCostTotalIdx: index('workflow_execution_logs_workspace_cost_total_idx').on( + table.workspaceId, + table.costTotal + ), + modelsUsedIdx: index('workflow_execution_logs_models_used_idx').using('gin', table.modelsUsed), workspaceEndedAtIdIdx: index('workflow_execution_logs_workspace_ended_at_id_idx').on( table.workspaceId, sql`date_trunc('milliseconds', ${table.endedAt})`, @@ -881,39 +903,33 @@ export const userStats = pgTable('user_stats', { .notNull() .references(() => user.id, { onDelete: 'cascade' }) .unique(), // One record per user - /** - * Deprecated former usage hot-path counters. - * - * These used to be incremented from execution/API/trigger/chat/MCP/A2A - * billing paths on every usage event. New usage reporting must derive - * these dimensions from `usage_log` instead of writing this row. - */ + // Retired usage hot-path counters: no writers/readers; derive from usage_log. + // Drop via DROP COLUMN in a follow-up migration. + /** @deprecated Retired usage counter; derive from usage_log. */ totalManualExecutions: integer('total_manual_executions').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalApiCalls: integer('total_api_calls').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalWebhookTriggers: integer('total_webhook_triggers').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalScheduledExecutions: integer('total_scheduled_executions').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalChatExecutions: integer('total_chat_executions').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalMcpExecutions: integer('total_mcp_executions').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalA2aExecutions: integer('total_a2a_executions').notNull().default(0), + /** @deprecated Retired usage counter; derive from usage_log. */ totalTokensUsed: bigint('total_tokens_used', { mode: 'number' }).notNull().default(0), - /** - * Deprecated former usage hot-path cost aggregate. - * - * `recordUsage` now appends attributed rows to `usage_log`; this column is - * retained only for legacy/admin reporting until consumers move to ledger - * aggregations. - */ + /** @deprecated Not written (recordUsage appends to usage_log); legacy/admin reads only. Move readers to ledger aggregation. */ totalCost: decimal('total_cost').notNull().default('0'), currentUsageLimit: decimal('current_usage_limit').default(DEFAULT_FREE_CREDITS.toString()), // Default $5 (1,000 credits) for free plan, null for team/enterprise usageLimitUpdatedAt: timestamp('usage_limit_updated_at').defaultNow(), /** - * Deprecated former usage hot-path current-period aggregate. - * - * Keep only as the pre-shift baseline for the active billing period. - * Canonical current-period usage is `currentPeriodCost` baseline plus - * attributed `usage_log` rows for the same billing entity and period. + * Active per-period baseline (not a per-usage hot-path counter). Current usage + * = this baseline + attributed usage_log rows for the period; reset at rollover. */ - currentPeriodCost: decimal('current_period_cost').notNull().default('0'), // Usage in current billing period + currentPeriodCost: decimal('current_period_cost').notNull().default('0'), lastPeriodCost: decimal('last_period_cost').default('0'), // Usage from previous billing period /** * Threshold/final billing tracker. @@ -933,25 +949,21 @@ export const userStats = pgTable('user_stats', { * overage collection. It is not a per-usage aggregate counter. */ creditBalance: decimal('credit_balance').notNull().default('0'), - /** - * Deprecated former Copilot hot-path cost/counter aggregates. - * - * Copilot/MCP Copilot usage should be reported from `usage_log` going - * forward. Current/last period Copilot columns remain as legacy reset - * trackers until those consumers are migrated. - */ + /** @deprecated Not written; report Copilot cost from usage_log. Legacy/admin reads only. */ totalCopilotCost: decimal('total_copilot_cost').notNull().default('0'), + /** Active per-period Copilot baseline; reset at rollover (not a per-usage counter). */ currentPeriodCopilotCost: decimal('current_period_copilot_cost').notNull().default('0'), + /** Previous-period Copilot cost; set at rollover. */ lastPeriodCopilotCost: decimal('last_period_copilot_cost').default('0'), + /** @deprecated Not written; report Copilot tokens from usage_log. Legacy/admin reads only. */ totalCopilotTokens: bigint('total_copilot_tokens', { mode: 'number' }).notNull().default(0), + /** @deprecated Not written; report Copilot calls from usage_log. Legacy/admin reads only. */ totalCopilotCalls: integer('total_copilot_calls').notNull().default(0), - /** - * Deprecated former MCP Copilot hot-path aggregates. - * - * New MCP Copilot billing usage should be reported from `usage_log`. - */ + /** @deprecated Not written; report MCP Copilot calls from usage_log. Legacy/admin reads only. */ totalMcpCopilotCalls: integer('total_mcp_copilot_calls').notNull().default(0), + /** @deprecated Not written; report MCP Copilot cost from usage_log. Legacy/admin reads only. */ totalMcpCopilotCost: decimal('total_mcp_copilot_cost').notNull().default('0'), + /** @deprecated No writer (never incremented or reset). MCP copilot usage lives in usage_log (source 'mcp_copilot'); read it from there, not this column. */ currentPeriodMcpCopilotCost: decimal('current_period_mcp_copilot_cost').notNull().default('0'), /** * Storage upload/delete hot-path tracker for personal plans. @@ -960,13 +972,7 @@ export const userStats = pgTable('user_stats', { * org-scoped storage writes update `organization.storageUsedBytes`. */ storageUsedBytes: bigint('storage_used_bytes', { mode: 'number' }).notNull().default(0), - /** - * Deprecated former execution hot-path activity timestamp. - * - * Successful workflow execution no longer updates `user_stats`; this column - * is retained only for legacy/admin reporting until replaced by an activity - * source that does not contend on this row. - */ + /** @deprecated Not updated by execution (no user_stats write on completion); legacy/admin reads only. */ lastActive: timestamp('last_active').notNull().defaultNow(), billingBlocked: boolean('billing_blocked').notNull().default(false), billingBlockedReason: billingBlockedReasonEnum('billing_blocked_reason'), @@ -2791,7 +2797,7 @@ export const auditLog = pgTable( }) ) -export const usageLogCategoryEnum = pgEnum('usage_log_category', ['model', 'fixed']) +export const usageLogCategoryEnum = pgEnum('usage_log_category', ['model', 'fixed', 'tool']) export const usageLogSourceEnum = pgEnum('usage_log_source', [ 'workflow', 'wand', @@ -2861,6 +2867,7 @@ export const usageLog = pgTable( table.workspaceId, table.createdAt ), + executionIdIdx: index('usage_log_execution_id_idx').on(table.executionId), }) ) diff --git a/packages/db/scripts/migrate.ts b/packages/db/scripts/migrate.ts index d8449a5775a..9d967a9db7c 100644 --- a/packages/db/scripts/migrate.ts +++ b/packages/db/scripts/migrate.ts @@ -2,6 +2,33 @@ import { drizzle } from 'drizzle-orm/postgres-js' import { migrate } from 'drizzle-orm/postgres-js/migrator' import postgres from 'postgres' +/** + * Concurrent-index convention (avoid write-blocking index builds on large tables) + * -------------------------------------------------------------------------------- + * drizzle-kit emits plain `CREATE INDEX`, which takes a SHARE lock and blocks all + * writes for the build duration — on a big, write-hot table (e.g. + * workflow_execution_logs, usage_log) that stalls every in-flight workflow + * completion for minutes. drizzle wraps each migration in a transaction, and + * `CREATE INDEX CONCURRENTLY` cannot run inside a transaction block. + * + * So, after generating a migration that adds an index on a large/hot table, edit + * the generated SQL to end drizzle's transaction first, then build concurrently + * and idempotently: + * + * COMMIT;--> statement-breakpoint + * CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_name" ON "table" (...); + * + * Notes: + * - Put the `COMMIT` breakpoint AFTER all transactional DDL (ALTER TABLE/TYPE) + * in the file and only the concurrent CREATE INDEX statements below it. + * - Use `IF NOT EXISTS` (and make sibling DDL idempotent, e.g. + * `ADD COLUMN IF NOT EXISTS`, `ADD VALUE IF NOT EXISTS`) so a re-run after a + * failed CONCURRENTLY build is safe — fresh DBs and re-applies both work. + * - CONCURRENTLY only takes a SHARE UPDATE EXCLUSIVE lock (allows reads/writes). + * - Always validate on staging before prod; a failed CONCURRENTLY build can + * leave an INVALID index that must be dropped and rebuilt. + */ + const url = process.env.DATABASE_URL if (!url) { console.error('ERROR: Missing DATABASE_URL environment variable.') @@ -12,6 +39,8 @@ if (!url) { const client = postgres(url, { max: 1, connect_timeout: 10 }) try { + // statement_timeout=0: index builds (esp. CONCURRENTLY on large tables) can run + // far longer than the app default; a migration must never be killed mid-build. await client`SET statement_timeout = 0` await migrate(drizzle(client), { migrationsFolder: './migrations' }) console.log('Migrations applied successfully.') From e533f1b56eb1be1c9098ae59875d190c932ab6f7 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 29 May 2026 14:22:13 -0700 Subject: [PATCH 08/14] improvement(providers): harden OpenAI-compatible providers + add tests (#4796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improvement(providers): harden OpenAI-compatible providers + add tests * fix(vllm): let tool-loop errors propagate instead of returning silent partial success * fix(litellm): force tool_choice 'none' on final structured-output call The deferred final call used tool_choice 'auto', so the model could emit another tool_calls round instead of the structured answer, leaving content stale. Use 'none' (matching vLLM/Fireworks) on both the streaming and non-streaming final calls so the model must return the structured response. Co-Authored-By: Claude Opus 4.8 * fix(providers/ollama): drop tools from post-tool streaming call Ollama ignores tool_choice (not in its supported fields), so vLLM/Fireworks' tool_choice:'none' guard is a no-op here. Omit tools from the final streaming payload instead so the summarization turn can't emit dropped tool calls. Co-Authored-By: Claude Opus 4.8 * fix(litellm): spread payload into deferred final call so reasoning_effort carries over The non-streaming deferred finalPayload hand-picked fields and dropped reasoning_effort (and any future payload field), diverging from the streaming path which spreads ...payload. Spread payload here too for consistency. Co-Authored-By: Claude Opus 4.8 * chore(providers/ollama): restore enrichment TSDoc block Keeps parity with sibling Chat Completions providers (cerebras/mistral/xai). Co-Authored-By: Claude Opus 4.8 * docs(fireworks): restore TSDoc on utils helpers Restore the TSDoc blocks on supportsNativeStructuredOutputs, createReadableStreamFromOpenAIStream, and checkForForcedToolUsage — TSDoc is the codebase documentation standard and should not have been stripped. Co-Authored-By: Claude Opus 4.8 * chore(litellm): remove inline rationale comments (codebase uses TSDoc) Co-Authored-By: Claude Opus 4.8 * chore(providers/ollama): drop orphaned enrichment TSDoc The block documented a function that now lives in trace-enrichment.ts, so it documents nothing in this file. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- apps/sim/providers/fireworks/index.test.ts | 226 +++++++++++++ apps/sim/providers/fireworks/index.ts | 10 +- apps/sim/providers/litellm/index.test.ts | 290 ++++++++++++++++ apps/sim/providers/litellm/index.ts | 115 ++++++- apps/sim/providers/ollama/index.test.ts | 351 ++++++++++++++++++++ apps/sim/providers/ollama/index.ts | 50 ++- apps/sim/providers/openai/core.ts | 55 +-- apps/sim/providers/openrouter/index.test.ts | 345 +++++++++++++++++++ apps/sim/providers/openrouter/index.ts | 9 +- apps/sim/providers/openrouter/utils.ts | 14 - apps/sim/providers/utils.ts | 53 +++ apps/sim/providers/vllm/index.test.ts | 296 +++++++++++++++++ apps/sim/providers/vllm/index.ts | 54 ++- apps/sim/providers/vllm/utils.ts | 15 +- 14 files changed, 1737 insertions(+), 146 deletions(-) create mode 100644 apps/sim/providers/fireworks/index.test.ts create mode 100644 apps/sim/providers/litellm/index.test.ts create mode 100644 apps/sim/providers/ollama/index.test.ts create mode 100644 apps/sim/providers/openrouter/index.test.ts create mode 100644 apps/sim/providers/vllm/index.test.ts diff --git a/apps/sim/providers/fireworks/index.test.ts b/apps/sim/providers/fireworks/index.test.ts new file mode 100644 index 00000000000..68fba04c736 --- /dev/null +++ b/apps/sim/providers/fireworks/index.test.ts @@ -0,0 +1,226 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockCreate, + mockSupportsNativeStructuredOutputs, + mockPrepareToolsWithUsageControl, + mockExecuteTool, +} = vi.hoisted(() => ({ + mockCreate: vi.fn(), + mockSupportsNativeStructuredOutputs: vi.fn(), + mockPrepareToolsWithUsageControl: vi.fn(), + mockExecuteTool: vi.fn(), +})) + +vi.mock('openai', () => ({ + default: vi.fn().mockImplementation(() => ({ + chat: { completions: { create: mockCreate } }, + })), +})) + +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 5 })) + +vi.mock('@/providers/models', () => ({ + getProviderModels: vi.fn().mockReturnValue([]), + getProviderDefaultModel: vi.fn().mockReturnValue('llama-v3p1-70b-instruct'), +})) + +vi.mock('@/providers/attachments', () => ({ + formatMessagesForProvider: vi.fn((messages) => messages), +})) + +vi.mock('@/providers/fireworks/utils', () => ({ + supportsNativeStructuredOutputs: mockSupportsNativeStructuredOutputs, + createReadableStreamFromOpenAIStream: vi.fn(() => ({}) as ReadableStream), + checkForForcedToolUsage: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })), +})) + +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) + +vi.mock('@/providers/utils', () => ({ + calculateCost: vi.fn().mockReturnValue({ input: 0, output: 0, total: 0 }), + generateSchemaInstructions: vi.fn(() => 'SCHEMA_INSTRUCTIONS'), + prepareToolExecution: vi.fn(() => ({ toolParams: { x: 1 }, executionParams: { x: 1 } })), + prepareToolsWithUsageControl: mockPrepareToolsWithUsageControl, + sumToolCosts: vi.fn().mockReturnValue(0), +})) + +vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) + +import { fireworksProvider } from '@/providers/fireworks/index' +import { ProviderError } from '@/providers/types' + +const textResponse = (content: string) => ({ + choices: [{ message: { content, tool_calls: [] } }], + usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, +}) + +const toolCallResponse = () => ({ + choices: [ + { + message: { + content: null, + tool_calls: [ + { id: 'call_1', type: 'function', function: { name: 'my_tool', arguments: '{"x":1}' } }, + ], + }, + }, + ], + usage: { prompt_tokens: 8, completion_tokens: 4, total_tokens: 12 }, +}) + +const toolDef = { + id: 'my_tool', + name: 'my_tool', + description: '', + params: {}, + parameters: { type: 'object', properties: {}, required: [] }, +} + +const callBody = (index: number) => mockCreate.mock.calls[index][0] +const lastCallBody = () => mockCreate.mock.calls.at(-1)?.[0] + +describe('fireworksProvider', () => { + beforeEach(() => { + vi.clearAllMocks() + mockSupportsNativeStructuredOutputs.mockResolvedValue(true) + mockPrepareToolsWithUsageControl.mockImplementation((tools) => ({ + tools, + toolChoice: 'auto', + forcedTools: [], + })) + mockExecuteTool.mockResolvedValue({ success: true, output: { ok: true } }) + }) + + const baseRequest = { + model: 'fireworks/llama-v3p1-70b-instruct', + systemPrompt: 'You are helpful.', + messages: [{ role: 'user' as const, content: 'Hello' }], + apiKey: 'fw-test-key', + } + + it('throws when the API key is missing', async () => { + await expect( + fireworksProvider.executeRequest({ ...baseRequest, apiKey: undefined }) + ).rejects.toThrow('API key is required for Fireworks') + }) + + it('returns content and token usage for a simple request', async () => { + mockCreate.mockResolvedValueOnce(textResponse('hi there')) + + const result = await fireworksProvider.executeRequest(baseRequest) + + expect(result).toMatchObject({ + content: 'hi there', + model: 'llama-v3p1-70b-instruct', + tokens: { input: 10, output: 5, total: 15 }, + }) + }) + + it('wraps API errors in a ProviderError', async () => { + mockCreate.mockRejectedValueOnce(new Error('boom')) + + await expect(fireworksProvider.executeRequest(baseRequest)).rejects.toBeInstanceOf( + ProviderError + ) + }) + + it('streams directly when there are no tools', async () => { + mockCreate.mockResolvedValueOnce({}) + + const result = await fireworksProvider.executeRequest({ ...baseRequest, stream: true }) + + expect(lastCallBody()).toMatchObject({ stream: true, stream_options: { include_usage: true } }) + expect(result).toHaveProperty('stream') + expect(result).toHaveProperty('execution') + }) + + it('sends a json_schema response_format with no strict field', async () => { + mockCreate.mockResolvedValueOnce(textResponse('{}')) + + await fireworksProvider.executeRequest({ + ...baseRequest, + responseFormat: { name: 'my_schema', schema: { type: 'object' }, strict: true }, + }) + + expect(lastCallBody().response_format).toEqual({ + type: 'json_schema', + json_schema: { name: 'my_schema', schema: { type: 'object' } }, + }) + expect(lastCallBody().response_format.json_schema).not.toHaveProperty('strict') + }) + + it('falls back to json_object with prompt instructions when native is unsupported', async () => { + mockSupportsNativeStructuredOutputs.mockResolvedValue(false) + mockCreate.mockResolvedValueOnce(textResponse('{}')) + + await fireworksProvider.executeRequest({ + ...baseRequest, + responseFormat: { name: 'my_schema', schema: { type: 'object' } }, + }) + + expect(lastCallBody().response_format).toEqual({ type: 'json_object' }) + expect(lastCallBody().messages.at(-1)).toEqual({ + role: 'user', + content: 'SCHEMA_INSTRUCTIONS', + }) + }) + + it('defers response_format to a final call when tools are active', async () => { + mockCreate + .mockResolvedValueOnce(textResponse('intermediate')) + .mockResolvedValueOnce(textResponse('{"done":true}')) + + await fireworksProvider.executeRequest({ + ...baseRequest, + responseFormat: { name: 'my_schema', schema: { type: 'object' } }, + tools: [toolDef], + }) + + expect(mockCreate).toHaveBeenCalledTimes(2) + expect(callBody(0).response_format).toBeUndefined() + expect(callBody(0).tools).toBeDefined() + expect(callBody(1).response_format).toEqual({ + type: 'json_schema', + json_schema: { name: 'my_schema', schema: { type: 'object' } }, + }) + expect(callBody(1).tools).toBeUndefined() + }) + + it('runs the tool loop and threads tool results back into the conversation', async () => { + mockCreate + .mockResolvedValueOnce(toolCallResponse()) + .mockResolvedValueOnce(textResponse('final answer')) + + const result = await fireworksProvider.executeRequest({ ...baseRequest, tools: [toolDef] }) + + expect(mockExecuteTool).toHaveBeenCalledWith('my_tool', { x: 1 }, expect.anything()) + expect(result).toMatchObject({ content: 'final answer' }) + expect((result as { toolCalls?: unknown[] }).toolCalls).toHaveLength(1) + + const followUpMessages = callBody(1).messages + expect(followUpMessages).toContainEqual( + expect.objectContaining({ role: 'assistant', tool_calls: expect.any(Array) }) + ) + expect(followUpMessages).toContainEqual( + expect.objectContaining({ role: 'tool', tool_call_id: 'call_1' }) + ) + }) + + it("forces tool_choice 'none' on the final streaming call after tools run", async () => { + mockCreate + .mockResolvedValueOnce(toolCallResponse()) + .mockResolvedValueOnce(textResponse('done')) + .mockResolvedValueOnce({}) + + await fireworksProvider.executeRequest({ ...baseRequest, stream: true, tools: [toolDef] }) + + expect(mockCreate).toHaveBeenCalledTimes(3) + expect(lastCallBody()).toMatchObject({ tool_choice: 'none', stream: true }) + }) +}) diff --git a/apps/sim/providers/fireworks/index.ts b/apps/sim/providers/fireworks/index.ts index 794ee3f0805..cdf355d3451 100644 --- a/apps/sim/providers/fireworks/index.ts +++ b/apps/sim/providers/fireworks/index.ts @@ -34,7 +34,7 @@ const logger = createLogger('FireworksProvider') /** * Applies structured output configuration to a payload based on model capabilities. - * Uses json_schema with strict mode for supported models, falls back to json_object with prompt instructions. + * Uses native json_schema for supported models, falls back to json_object with prompt instructions. */ async function applyResponseFormat( targetPayload: any, @@ -51,7 +51,6 @@ async function applyResponseFormat( json_schema: { name: responseFormat.name || 'response_schema', schema: responseFormat.schema || responseFormat, - strict: responseFormat.strict !== false, }, } return messages @@ -469,7 +468,7 @@ export const fireworksProvider: ProviderConfig = { const streamingParams: ChatCompletionCreateParamsStreaming = { ...payload, messages: [...currentMessages], - tool_choice: 'auto', + tool_choice: 'none', stream: true, stream_options: { include_usage: true }, } @@ -652,8 +651,3 @@ export const fireworksProvider: ProviderConfig = { } }, } - -/** - * Enriches the last model segment with per-iteration content from a Chat - * Completions response: assistant text, tool calls, finish reason, token usage. - */ diff --git a/apps/sim/providers/litellm/index.test.ts b/apps/sim/providers/litellm/index.test.ts new file mode 100644 index 00000000000..5261f4e23d6 --- /dev/null +++ b/apps/sim/providers/litellm/index.test.ts @@ -0,0 +1,290 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockCreate, mockExecuteTool } = vi.hoisted(() => ({ + mockCreate: vi.fn(), + mockExecuteTool: vi.fn(), +})) + +vi.mock('openai', () => ({ + default: vi.fn().mockImplementation(() => ({ + chat: { completions: { create: mockCreate } }, + })), +})) + +vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) + +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 20 })) + +vi.mock('@/lib/core/config/env', () => ({ + env: { LITELLM_BASE_URL: 'http://litellm.test', LITELLM_API_KEY: '' }, +})) + +vi.mock('@/stores/providers', () => ({ + useProvidersStore: { getState: () => ({ setProviderModels: vi.fn() }) }, +})) + +vi.mock('@/providers/models', () => ({ + getProviderModels: () => [], + getProviderDefaultModel: () => '', +})) + +vi.mock('@/providers/attachments', () => ({ + formatMessagesForProvider: (messages: unknown) => messages, +})) + +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) + +vi.mock('@/providers/litellm/utils', () => ({ + createReadableStreamFromLiteLLMStream: vi.fn( + () => new ReadableStream({ start: (c) => c.close() }) + ), +})) + +vi.mock('@/providers/utils', () => ({ + calculateCost: vi.fn(() => ({ input: 0, output: 0, total: 0 })), + sumToolCosts: vi.fn(() => 0), + prepareToolExecution: vi.fn((_tool, toolArgs) => ({ + toolParams: toolArgs, + executionParams: toolArgs, + })), + prepareToolsWithUsageControl: vi.fn((tools) => ({ + tools, + toolChoice: 'auto', + forcedTools: [], + hasFilteredTools: false, + })), + trackForcedToolUsage: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })), + enforceStrictSchema: vi.fn((schema) => ({ ...schema, additionalProperties: false })), +})) + +import { litellmProvider } from '@/providers/litellm' +import { ProviderError } from '@/providers/types' + +interface ChatOptions { + content?: string | null + toolCalls?: Array<{ id: string; function: { name: string; arguments: string } }> + usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number } +} + +function chat({ content = null, toolCalls, usage }: ChatOptions = {}) { + return { + choices: [ + { + message: { content, tool_calls: toolCalls }, + finish_reason: toolCalls ? 'tool_calls' : 'stop', + }, + ], + usage: usage ?? { prompt_tokens: 5, completion_tokens: 3, total_tokens: 8 }, + } +} + +function tool(name: string) { + return { id: name, name, description: 'd', parameters: {} } +} + +function run(request: Record) { + return litellmProvider.executeRequest!({ + model: 'litellm/llama-3', + messages: [{ role: 'user', content: 'Hi' }], + ...request, + } as never) as Promise +} + +const firstPayload = () => mockCreate.mock.calls[0][0] +const lastPayload = () => mockCreate.mock.calls.at(-1)![0] + +describe('litellmProvider.executeRequest', () => { + beforeEach(() => { + vi.clearAllMocks() + mockCreate.mockResolvedValue(chat({ content: 'hello' })) + mockExecuteTool.mockResolvedValue({ success: true, output: { ok: true } }) + }) + + it('assembles messages, strips the model prefix, and maps params', async () => { + const result = await run({ + systemPrompt: 'You are helpful.', + context: 'Some context', + temperature: 0.5, + maxTokens: 256, + }) + + const payload = firstPayload() + expect(payload.model).toBe('llama-3') + expect(payload.messages).toEqual([ + { role: 'system', content: 'You are helpful.' }, + { role: 'user', content: 'Some context' }, + { role: 'user', content: 'Hi' }, + ]) + expect(payload.temperature).toBe(0.5) + expect(payload.max_completion_tokens).toBe(256) + expect(result.content).toBe('hello') + expect(result.tokens).toEqual({ input: 5, output: 3, total: 8 }) + }) + + it('forwards reasoning_effort only when set to a non-default value', async () => { + await run({ reasoningEffort: 'high' }) + expect(firstPayload().reasoning_effort).toBe('high') + + mockCreate.mockClear() + await run({ reasoningEffort: 'auto' }) + expect(firstPayload().reasoning_effort).toBeUndefined() + + mockCreate.mockClear() + await run({}) + expect(firstPayload().reasoning_effort).toBeUndefined() + }) + + it('sanitizes the schema for strict response_format and passes it through otherwise', async () => { + await run({ responseFormat: { name: 'r', schema: { type: 'object', properties: {} } } }) + let rf = firstPayload().response_format + expect(rf.type).toBe('json_schema') + expect(rf.json_schema.strict).toBe(true) + expect(rf.json_schema.schema.additionalProperties).toBe(false) + + mockCreate.mockClear() + await run({ + responseFormat: { name: 'r', schema: { type: 'object', properties: {} }, strict: false }, + }) + rf = firstPayload().response_format + expect(rf.json_schema.strict).toBe(false) + expect(rf.json_schema.schema.additionalProperties).toBeUndefined() + }) + + it('defers response_format past the tool loop and keeps tools on the final call', async () => { + mockCreate + .mockResolvedValueOnce( + chat({ toolCalls: [{ id: 'c1', function: { name: 'known', arguments: '{"q":1}' } }] }) + ) + .mockResolvedValueOnce(chat({ content: 'mid' })) + .mockResolvedValueOnce(chat({ content: '{"answer":1}' })) + + const result = await run({ + tools: [tool('known')], + reasoningEffort: 'high', + responseFormat: { name: 'r', schema: { type: 'object', properties: {} } }, + }) + + expect(firstPayload().response_format).toBeUndefined() + expect(firstPayload().tools).toBeDefined() + + const final = lastPayload() + expect(final.response_format.type).toBe('json_schema') + expect(final.tools).toBeDefined() + expect(final.tool_choice).toBe('none') + expect(final.parallel_tool_calls).toBe(false) + expect(final.reasoning_effort).toBe('high') + expect(result.content).toBe('{"answer":1}') + }) + + it('defers response_format into the final streaming call while keeping tools', async () => { + mockCreate + .mockResolvedValueOnce( + chat({ toolCalls: [{ id: 'c1', function: { name: 'known', arguments: '{}' } }] }) + ) + .mockResolvedValueOnce(chat({ content: 'mid' })) + + const result = await run({ + stream: true, + tools: [tool('known')], + responseFormat: { name: 'r', schema: { type: 'object', properties: {} } }, + }) + + const final = lastPayload() + expect(final.stream).toBe(true) + expect(final.response_format.type).toBe('json_schema') + expect(final.tools).toBeDefined() + expect(final.tool_choice).toBe('none') + expect(final.parallel_tool_calls).toBe(false) + expect(result.execution.isStreaming).toBe(true) + }) + + it('threads assistant tool_calls and a named tool response, and reports toolCalls', async () => { + mockCreate + .mockResolvedValueOnce( + chat({ toolCalls: [{ id: 'c1', function: { name: 'known', arguments: '{}' } }] }) + ) + .mockResolvedValueOnce(chat({ content: 'done' })) + mockExecuteTool.mockResolvedValue({ success: true, output: { temp: 72 } }) + + const result = await run({ tools: [tool('known')] }) + + const followupMessages = mockCreate.mock.calls[1][0].messages + expect(followupMessages).toContainEqual({ + role: 'assistant', + content: null, + tool_calls: [{ id: 'c1', type: 'function', function: { name: 'known', arguments: '{}' } }], + }) + expect(followupMessages).toContainEqual({ + role: 'tool', + tool_call_id: 'c1', + name: 'known', + content: JSON.stringify({ temp: 72 }), + }) + expect(result.toolCalls).toHaveLength(1) + expect(result.content).toBe('done') + }) + + it('emits a stub tool response for an unanswered tool_call_id', async () => { + mockCreate + .mockResolvedValueOnce( + chat({ toolCalls: [{ id: 'cX', function: { name: 'ghost', arguments: '{}' } }] }) + ) + .mockResolvedValueOnce(chat({ content: 'recovered' })) + + await run({ tools: [tool('known')] }) + + expect(mockExecuteTool).not.toHaveBeenCalled() + const followupMessages = mockCreate.mock.calls[1][0].messages + const toolMsg = followupMessages.find((m: any) => m.role === 'tool' && m.tool_call_id === 'cX') + expect(toolMsg).toBeDefined() + expect(toolMsg.content).toContain('not available') + }) + + it('executes a tool with empty arguments without failing', async () => { + mockCreate + .mockResolvedValueOnce( + chat({ toolCalls: [{ id: 'c1', function: { name: 'ping', arguments: '' } }] }) + ) + .mockResolvedValueOnce(chat({ content: 'pong' })) + + await run({ tools: [tool('ping')] }) + + expect(mockExecuteTool).toHaveBeenCalledTimes(1) + const toolMsg = mockCreate.mock.calls[1][0].messages.find((m: any) => m.role === 'tool') + expect(toolMsg.content).not.toContain('"error":true') + }) + + it('stops the tool loop at MAX_TOOL_ITERATIONS', async () => { + mockCreate.mockResolvedValue( + chat({ toolCalls: [{ id: 'c1', function: { name: 'known', arguments: '{}' } }] }) + ) + + await run({ tools: [tool('known')] }) + + expect(mockCreate).toHaveBeenCalledTimes(1 + 20) + expect(mockExecuteTool).toHaveBeenCalledTimes(20) + }) + + it('returns a streaming execution when streaming without active tools', async () => { + const result = await run({ stream: true }) + + expect(firstPayload().stream).toBe(true) + expect(firstPayload().stream_options).toEqual({ include_usage: true }) + expect(result.stream).toBeInstanceOf(ReadableStream) + expect(result.execution.isStreaming).toBe(true) + }) + + it('wraps API errors in a ProviderError using the error envelope message', async () => { + mockCreate.mockRejectedValue({ + error: { message: 'rate limited', type: 'rate_limit_error', code: '429' }, + }) + + await expect(run({})).rejects.toBeInstanceOf(ProviderError) + await expect(run({})).rejects.toThrow('rate limited') + }) +}) diff --git a/apps/sim/providers/litellm/index.ts b/apps/sim/providers/litellm/index.ts index 33e363f0509..53a5360d2c9 100644 --- a/apps/sim/providers/litellm/index.ts +++ b/apps/sim/providers/litellm/index.ts @@ -19,6 +19,7 @@ import type { import { ProviderError } from '@/providers/types' import { calculateCost, + enforceStrictSchema, prepareToolExecution, prepareToolsWithUsageControl, sumToolCosts, @@ -146,19 +147,27 @@ export const litellmProvider: ProviderConfig = { if (request.temperature !== undefined) payload.temperature = request.temperature if (request.maxTokens != null) payload.max_completion_tokens = request.maxTokens - if (request.responseFormat) { - payload.response_format = { - type: 'json_schema', - json_schema: { - name: request.responseFormat.name || 'response_schema', - schema: request.responseFormat.schema || request.responseFormat, - strict: request.responseFormat.strict !== false, - }, - } - - logger.info('Added JSON schema response format to LiteLLM request') + if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') { + payload.reasoning_effort = request.reasoningEffort } + const isStrictResponseFormat = request.responseFormat + ? request.responseFormat.strict !== false + : false + + const responseFormatPayload = request.responseFormat + ? { + type: 'json_schema' as const, + json_schema: { + name: request.responseFormat.name || 'response_schema', + schema: isStrictResponseFormat + ? enforceStrictSchema(request.responseFormat.schema || request.responseFormat) + : request.responseFormat.schema || request.responseFormat, + strict: isStrictResponseFormat, + }, + } + : undefined + let preparedTools: ReturnType | null = null let hasActiveTools = false @@ -184,6 +193,12 @@ export const litellmProvider: ProviderConfig = { } } + const deferResponseFormat = !!responseFormatPayload && hasActiveTools + if (responseFormatPayload && !deferResponseFormat) { + payload.response_format = responseFormatPayload + logger.info('Added JSON schema response format to LiteLLM request') + } + const providerStartTime = Date.now() const providerStartTimeISO = new Date(providerStartTime).toISOString() @@ -271,6 +286,7 @@ export const litellmProvider: ProviderConfig = { endTime: new Date().toISOString(), duration: Date.now() - providerStartTime, }, + isStreaming: true, }, } as StreamingExecution @@ -374,7 +390,9 @@ export const litellmProvider: ProviderConfig = { const toolName = toolCall.function.name try { - const toolArgs = JSON.parse(toolCall.function.arguments) + const toolArgs = toolCall.function.arguments + ? JSON.parse(toolCall.function.arguments) + : {} const tool = request.tools?.find((t) => t.id === toolName) if (!tool) return null @@ -429,6 +447,8 @@ export const litellmProvider: ProviderConfig = { })), }) + const respondedToolCallIds = new Set() + for (const settledResult of executionResults) { if (settledResult.status === 'rejected' || !settledResult.value) continue @@ -469,8 +489,24 @@ export const litellmProvider: ProviderConfig = { currentMessages.push({ role: 'tool', tool_call_id: toolCall.id, + name: toolName, content: JSON.stringify(resultContent), }) + respondedToolCallIds.add(toolCall.id) + } + + for (const tc of toolCallsInResponse) { + if (respondedToolCallIds.has(tc.id)) continue + currentMessages.push({ + role: 'tool', + tool_call_id: tc.id, + name: tc.function.name, + content: JSON.stringify({ + error: true, + message: `Tool "${tc.function.name}" is not available`, + tool: tc.function.name, + }), + }) } const thisToolsTime = Date.now() - toolsStartTime @@ -551,10 +587,14 @@ export const litellmProvider: ProviderConfig = { const streamingParams: ChatCompletionCreateParamsStreaming = { ...payload, messages: currentMessages, - tool_choice: 'auto', + tool_choice: 'none', stream: true, stream_options: { include_usage: true }, } + if (deferResponseFormat && responseFormatPayload) { + streamingParams.response_format = responseFormatPayload + streamingParams.parallel_tool_calls = false + } const streamResponse = await litellm.chat.completions.create( streamingParams, request.abortSignal ? { signal: request.abortSignal } : undefined @@ -626,12 +666,59 @@ export const litellmProvider: ProviderConfig = { endTime: new Date().toISOString(), duration: Date.now() - providerStartTime, }, + isStreaming: true, }, } as StreamingExecution return streamingResult as StreamingExecution } + if (deferResponseFormat && responseFormatPayload) { + logger.info('Applying deferred JSON schema response format after tool processing') + + const finalFormatStartTime = Date.now() + const finalPayload: any = { + ...payload, + messages: currentMessages, + response_format: responseFormatPayload, + tool_choice: 'none', + parallel_tool_calls: false, + } + + currentResponse = await litellm.chat.completions.create( + finalPayload, + request.abortSignal ? { signal: request.abortSignal } : undefined + ) + + const finalFormatEndTime = Date.now() + timeSegments.push({ + type: 'model', + name: request.model, + startTime: finalFormatStartTime, + endTime: finalFormatEndTime, + duration: finalFormatEndTime - finalFormatStartTime, + }) + modelTime += finalFormatEndTime - finalFormatStartTime + + const formattedContent = currentResponse.choices[0]?.message?.content + if (formattedContent) { + content = formattedContent.replace(/```json\n?|\n?```/g, '').trim() + } + + if (currentResponse.usage) { + tokens.input += currentResponse.usage.prompt_tokens || 0 + tokens.output += currentResponse.usage.completion_tokens || 0 + tokens.total += currentResponse.usage.total_tokens || 0 + } + + enrichLastModelSegmentFromChatCompletions( + timeSegments, + currentResponse, + currentResponse.choices[0]?.message?.tool_calls, + { model: request.model, provider: 'litellm' } + ) + } + const providerEndTime = Date.now() const providerEndTimeISO = new Date(providerEndTime).toISOString() const totalDuration = providerEndTime - providerStartTime @@ -660,7 +747,7 @@ export const litellmProvider: ProviderConfig = { let errorMessage = toError(error).message let errorType: string | undefined - let errorCode: number | undefined + let errorCode: string | number | undefined if (error && typeof error === 'object' && 'error' in error) { const litellmError = error.error as any diff --git a/apps/sim/providers/ollama/index.test.ts b/apps/sim/providers/ollama/index.test.ts new file mode 100644 index 00000000000..a6b91e9e8f5 --- /dev/null +++ b/apps/sim/providers/ollama/index.test.ts @@ -0,0 +1,351 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +type StreamUsage = { prompt_tokens: number; completion_tokens: number; total_tokens: number } + +const { mockCreate, mockExecuteTool, streamOnComplete, MockAPIError } = vi.hoisted(() => { + class MockAPIError extends Error { + status?: number + code?: string | null + type?: string + constructor(message: string, opts: { status?: number; code?: string; type?: string } = {}) { + super(message) + this.name = 'APIError' + this.status = opts.status + this.code = opts.code + this.type = opts.type + } + } + return { + mockCreate: vi.fn(), + mockExecuteTool: vi.fn(), + streamOnComplete: { + current: undefined as undefined | ((content: string, usage: StreamUsage) => void), + }, + MockAPIError, + } +}) + +vi.mock('openai', () => { + const OpenAI = vi.fn(() => ({ chat: { completions: { create: mockCreate } } })) + ;(OpenAI as unknown as { APIError: typeof MockAPIError }).APIError = MockAPIError + return { default: OpenAI } +}) + +vi.mock('@/lib/core/utils/urls', () => ({ getOllamaUrl: () => 'http://localhost:11434' })) +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 20 })) +vi.mock('@/providers/attachments', () => ({ + formatMessagesForProvider: (messages: unknown) => messages, +})) +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) +vi.mock('@/providers/ollama/utils', () => ({ + createReadableStreamFromOllamaStream: ( + _stream: unknown, + onComplete: (content: string, usage: StreamUsage) => void + ) => { + streamOnComplete.current = onComplete + return 'OLLAMA_STREAM' + }, +})) +vi.mock('@/providers/utils', () => ({ + calculateCost: () => ({ input: 0, output: 0, total: 0, pricing: null }), + prepareToolExecution: (_tool: unknown, args: Record) => ({ + toolParams: args, + executionParams: args, + }), + sumToolCosts: () => 0, +})) +vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) +vi.mock('@/stores/providers', () => ({ + useProvidersStore: { getState: () => ({ setProviderModels: vi.fn() }) }, +})) + +import { ollamaProvider } from '@/providers/ollama' +import type { ProviderRequest, ProviderResponse, ProviderToolConfig } from '@/providers/types' + +interface StreamingResult { + stream: string + execution: { + output: { + content: string + tokens: { input: number; output: number; total: number } + toolCalls?: { list: unknown[]; count: number } + } + } +} + +type ToolCallChunk = { id: string; type: 'function'; function: { name: string; arguments: string } } + +function completion( + opts: { content?: string | null; toolCalls?: ToolCallChunk[]; usage?: StreamUsage } = {} +) { + return { + choices: [{ message: { content: opts.content ?? null, tool_calls: opts.toolCalls } }], + usage: opts.usage ?? { prompt_tokens: 5, completion_tokens: 3, total_tokens: 8 }, + } +} + +function makeTool(id: string, usageControl?: 'auto' | 'force' | 'none'): ProviderToolConfig { + return { + id, + name: id, + description: `${id} tool`, + params: {}, + parameters: { type: 'object', properties: {}, required: [] }, + ...(usageControl ? { usageControl } : {}), + } +} + +const baseRequest: ProviderRequest = { + model: 'llama3.2', + messages: [{ role: 'user', content: 'hi' }], +} + +describe('ollamaProvider.executeRequest', () => { + beforeEach(() => { + vi.clearAllMocks() + streamOnComplete.current = undefined + mockCreate.mockResolvedValue(completion({ content: 'hello' })) + mockExecuteTool.mockResolvedValue({ success: true, output: { ok: true } }) + }) + + it('assembles system, context, then history in order and forwards params', async () => { + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + systemPrompt: 'be nice', + context: 'ctx', + temperature: 0.5, + maxTokens: 128, + })) as ProviderResponse + + expect(result).toMatchObject({ content: 'hello', model: 'llama3.2' }) + const payload = mockCreate.mock.calls[0][0] + expect(payload.messages).toEqual([ + { role: 'system', content: 'be nice' }, + { role: 'user', content: 'ctx' }, + { role: 'user', content: 'hi' }, + ]) + expect(payload.model).toBe('llama3.2') + expect(payload.temperature).toBe(0.5) + expect(payload.max_tokens).toBe(128) + }) + + it('returns content verbatim (keeps ```json fences) when no responseFormat', async () => { + const fenced = '```json\n{"a":1}\n```' + mockCreate.mockResolvedValue(completion({ content: fenced })) + const result = (await ollamaProvider.executeRequest(baseRequest)) as ProviderResponse + expect(result.content).toBe(fenced) + }) + + it('strips ```json fences and sends a json_schema response_format when requested', async () => { + mockCreate.mockResolvedValue(completion({ content: '```json\n{"a":1}\n```' })) + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + responseFormat: { name: 'r', schema: { type: 'object' }, strict: true }, + })) as ProviderResponse + expect(result.content).toBe('{"a":1}') + expect(mockCreate.mock.calls[0][0].response_format).toMatchObject({ + type: 'json_schema', + json_schema: { name: 'r', schema: { type: 'object' }, strict: true }, + }) + }) + + it('runs the tool loop: parses string args, feeds results back, then terminates', async () => { + mockCreate + .mockResolvedValueOnce( + completion({ + toolCalls: [ + { id: 'call_1', type: 'function', function: { name: 'mytool', arguments: '{"x":1}' } }, + ], + }) + ) + .mockResolvedValueOnce(completion({ content: 'done' })) + + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + tools: [makeTool('mytool')], + })) as ProviderResponse + + expect(mockExecuteTool).toHaveBeenCalledWith('mytool', { x: 1 }, expect.anything()) + expect(mockCreate).toHaveBeenCalledTimes(2) + expect(result.content).toBe('done') + expect(result.toolCalls).toEqual([ + expect.objectContaining({ name: 'mytool', success: true, arguments: { x: 1 } }), + ]) + expect(result.toolResults).toEqual([{ ok: true }]) + + const followUp = mockCreate.mock.calls[1][0].messages + expect(followUp).toContainEqual( + expect.objectContaining({ + role: 'assistant', + content: null, + tool_calls: [ + expect.objectContaining({ + id: 'call_1', + function: { name: 'mytool', arguments: '{"x":1}' }, + }), + ], + }) + ) + expect(followUp).toContainEqual({ + role: 'tool', + tool_call_id: 'call_1', + content: JSON.stringify({ ok: true }), + }) + }) + + it('records a failed tool result without aborting the loop', async () => { + mockExecuteTool.mockResolvedValue({ success: false, error: 'boom' }) + mockCreate + .mockResolvedValueOnce( + completion({ + toolCalls: [ + { id: 'call_1', type: 'function', function: { name: 'mytool', arguments: '{}' } }, + ], + }) + ) + .mockResolvedValueOnce(completion({ content: 'recovered' })) + + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + tools: [makeTool('mytool')], + })) as ProviderResponse + + expect(result.content).toBe('recovered') + expect(result.toolCalls?.[0]).toMatchObject({ name: 'mytool', success: false }) + const toolMsg = mockCreate.mock.calls[1][0].messages.find( + (m: { role: string }) => m.role === 'tool' + ) + expect(JSON.parse(toolMsg.content)).toMatchObject({ error: true, message: 'boom' }) + }) + + it('executes parallel tool calls from a single response', async () => { + mockExecuteTool + .mockResolvedValueOnce({ success: true, output: { from: 'a' } }) + .mockResolvedValueOnce({ success: true, output: { from: 'b' } }) + mockCreate + .mockResolvedValueOnce( + completion({ + toolCalls: [ + { id: 'call_a', type: 'function', function: { name: 'a', arguments: '{}' } }, + { id: 'call_b', type: 'function', function: { name: 'b', arguments: '{}' } }, + ], + }) + ) + .mockResolvedValueOnce(completion({ content: 'summary' })) + + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + tools: [makeTool('a'), makeTool('b')], + })) as ProviderResponse + + expect(mockExecuteTool).toHaveBeenCalledTimes(2) + expect(result.toolCalls?.map((c) => c.name)).toEqual(['a', 'b']) + const toolMsgs = mockCreate.mock.calls[1][0].messages.filter( + (m: { role: string }) => m.role === 'tool' + ) + expect(toolMsgs.map((m: { tool_call_id: string }) => m.tool_call_id)).toEqual([ + 'call_a', + 'call_b', + ]) + }) + + it('filters out tools with usageControl "none"', async () => { + await ollamaProvider.executeRequest({ + ...baseRequest, + tools: [makeTool('keep'), makeTool('drop', 'none')], + }) + const sent = mockCreate.mock.calls[0][0].tools + expect(sent.map((t: { function: { name: string } }) => t.function.name)).toEqual(['keep']) + }) + + it('never forces tools (Ollama ignores tool_choice) and keeps "auto"', async () => { + await ollamaProvider.executeRequest({ ...baseRequest, tools: [makeTool('forced', 'force')] }) + const payload = mockCreate.mock.calls[0][0] + expect(payload.tool_choice).toBe('auto') + expect(payload.tools.map((t: { function: { name: string } }) => t.function.name)).toEqual([ + 'forced', + ]) + }) + + it('surfaces an OpenAI APIError message through ProviderError', async () => { + mockCreate.mockRejectedValue( + new MockAPIError('model not found', { + status: 404, + code: 'not_found', + type: 'invalid_request_error', + }) + ) + await expect(ollamaProvider.executeRequest(baseRequest)).rejects.toThrow('model not found') + }) + + it('streams content and usage when no tools are used', async () => { + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + stream: true, + })) as unknown as StreamingResult + + expect(result.stream).toBe('OLLAMA_STREAM') + expect(mockCreate.mock.calls[0][0].stream_options).toEqual({ include_usage: true }) + + streamOnComplete.current?.('streamed text', { + prompt_tokens: 4, + completion_tokens: 6, + total_tokens: 10, + }) + expect(result.execution.output.content).toBe('streamed text') + expect(result.execution.output.tokens).toMatchObject({ input: 4, output: 6, total: 10 }) + }) + + it('strips ```json fences from streamed content when responseFormat is set', async () => { + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + stream: true, + responseFormat: { name: 'r', schema: { type: 'object' }, strict: true }, + })) as unknown as StreamingResult + + streamOnComplete.current?.('```json\n{"a":1}\n```', { + prompt_tokens: 1, + completion_tokens: 2, + total_tokens: 3, + }) + expect(result.execution.output.content).toBe('{"a":1}') + }) + + it('streams the final response after a tool loop, carrying tool calls', async () => { + mockCreate + .mockResolvedValueOnce( + completion({ + toolCalls: [ + { id: 'call_1', type: 'function', function: { name: 'mytool', arguments: '{}' } }, + ], + }) + ) + .mockResolvedValueOnce(completion({ content: 'intermediate' })) + + const result = (await ollamaProvider.executeRequest({ + ...baseRequest, + stream: true, + tools: [makeTool('mytool')], + })) as unknown as StreamingResult + + expect(result.stream).toBe('OLLAMA_STREAM') + expect(mockExecuteTool).toHaveBeenCalledTimes(1) + + const finalCall = mockCreate.mock.calls[2][0] + expect(finalCall.tools).toBeUndefined() + expect(finalCall.tool_choice).toBeUndefined() + + streamOnComplete.current?.('final answer', { + prompt_tokens: 2, + completion_tokens: 4, + total_tokens: 6, + }) + expect(result.execution.output.content).toBe('final answer') + expect(result.execution.output.toolCalls).toMatchObject({ count: 1 }) + }) +}) diff --git a/apps/sim/providers/ollama/index.ts b/apps/sim/providers/ollama/index.ts index 52332aecdb2..bfe7cff6134 100644 --- a/apps/sim/providers/ollama/index.ts +++ b/apps/sim/providers/ollama/index.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { getErrorMessage, toError } from '@sim/utils/errors' +import { getErrorMessage } from '@sim/utils/errors' import OpenAI from 'openai' import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions' import { getOllamaUrl } from '@/lib/core/utils/urls' @@ -10,6 +10,7 @@ import type { ModelsObject } from '@/providers/ollama/types' import { createReadableStreamFromOllamaStream } from '@/providers/ollama/utils' import { enrichLastModelSegmentFromChatCompletions } from '@/providers/trace-enrichment' import type { + Message, ProviderConfig, ProviderRequest, ProviderResponse, @@ -73,7 +74,7 @@ export const ollamaProvider: ProviderConfig = { baseURL: `${OLLAMA_HOST}/v1`, }) - const allMessages = [] + const allMessages: Message[] = [] if (request.systemPrompt) { allMessages.push({ @@ -92,7 +93,7 @@ export const ollamaProvider: ProviderConfig = { if (request.messages) { allMessages.push(...request.messages) } - const formattedMessages = formatMessagesForProvider(allMessages, 'ollama') + const formattedMessages = formatMessagesForProvider(allMessages, 'ollama') as Message[] const tools = request.tools?.length ? request.tools.map((tool) => ({ @@ -180,7 +181,7 @@ export const ollamaProvider: ProviderConfig = { stream: createReadableStreamFromOllamaStream(streamResponse, (content, usage) => { streamingResult.execution.output.content = content - if (content) { + if (content && request.responseFormat) { streamingResult.execution.output.content = content .replace(/```json\n?|\n?```/g, '') .trim() @@ -264,7 +265,7 @@ export const ollamaProvider: ProviderConfig = { let content = currentResponse.choices[0]?.message?.content || '' - if (content) { + if (content && request.responseFormat) { content = content.replace(/```json\n?|\n?```/g, '') content = content.trim() } @@ -295,6 +296,9 @@ export const ollamaProvider: ProviderConfig = { while (iterationCount < MAX_TOOL_ITERATIONS) { if (currentResponse.choices[0]?.message?.content) { content = currentResponse.choices[0].message.content + if (request.responseFormat) { + content = content.replace(/```json\n?|\n?```/g, '').trim() + } } const toolCallsInResponse = currentResponse.choices[0]?.message?.tool_calls @@ -450,8 +454,9 @@ export const ollamaProvider: ProviderConfig = { if (currentResponse.choices[0]?.message?.content) { content = currentResponse.choices[0].message.content - content = content.replace(/```json\n?|\n?```/g, '') - content = content.trim() + if (request.responseFormat) { + content = content.replace(/```json\n?|\n?```/g, '').trim() + } } if (currentResponse.usage) { @@ -477,10 +482,11 @@ export const ollamaProvider: ProviderConfig = { const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output) + const { tools: _tools, tool_choice: _toolChoice, ...streamPayload } = payload + const streamingParams: ChatCompletionCreateParamsStreaming = { - ...payload, + ...streamPayload, messages: currentMessages, - tool_choice: 'auto', stream: true, stream_options: { include_usage: true }, } @@ -493,7 +499,7 @@ export const ollamaProvider: ProviderConfig = { stream: createReadableStreamFromOllamaStream(streamResponse, (content, usage) => { streamingResult.execution.output.content = content - if (content) { + if (content && request.responseFormat) { streamingResult.execution.output.content = content .replace(/```json\n?|\n?```/g, '') .trim() @@ -589,12 +595,27 @@ export const ollamaProvider: ProviderConfig = { const providerEndTimeISO = new Date(providerEndTime).toISOString() const totalDuration = providerEndTime - providerStartTime + let errorMessage = getErrorMessage(error, 'Unknown error') + let errorType: string | undefined + let errorCode: string | undefined + let status: number | undefined + + if (error instanceof OpenAI.APIError) { + errorMessage = error.message + errorType = error.type + errorCode = error.code ?? undefined + status = error.status + } + logger.error('Error in Ollama request:', { - error, + error: errorMessage, + errorType, + errorCode, + status, duration: totalDuration, }) - throw new ProviderError(toError(error).message, { + throw new ProviderError(errorMessage, { startTime: providerStartTimeISO, endTime: providerEndTimeISO, duration: totalDuration, @@ -602,8 +623,3 @@ export const ollamaProvider: ProviderConfig = { } }, } - -/** - * Enriches the last model segment with per-iteration content from a Chat - * Completions response: assistant text, tool calls, finish reason, token usage. - */ diff --git a/apps/sim/providers/openai/core.ts b/apps/sim/providers/openai/core.ts index 6946f0c0fa3..6f19cef1562 100644 --- a/apps/sim/providers/openai/core.ts +++ b/apps/sim/providers/openai/core.ts @@ -8,6 +8,7 @@ import type { Message, ProviderRequest, ProviderResponse, TimeSegment } from '@/ import { ProviderError } from '@/providers/types' import { calculateCost, + enforceStrictSchema, prepareToolExecution, prepareToolsWithUsageControl, sumToolCosts, @@ -31,60 +32,6 @@ import { type PreparedTools = ReturnType type ToolChoice = PreparedTools['toolChoice'] -/** - * Recursively enforces OpenAI strict mode requirements on a JSON schema. - * - Sets additionalProperties: false on all object types. - * - Ensures required includes ALL property keys. - */ -function enforceStrictSchema(schema: Record): Record { - if (!schema || typeof schema !== 'object') return schema - - const result = { ...schema } - - // If this is an object type, enforce strict requirements - if (result.type === 'object') { - result.additionalProperties = false - - // Recursively process properties and ensure required includes all keys - if (result.properties && typeof result.properties === 'object') { - const propKeys = Object.keys(result.properties as Record) - result.required = propKeys // Strict mode requires ALL properties - result.properties = Object.fromEntries( - Object.entries(result.properties as Record).map(([key, value]) => [ - key, - enforceStrictSchema(value as Record), - ]) - ) - } - } - - // Handle array items - if (result.type === 'array' && result.items) { - result.items = enforceStrictSchema(result.items as Record) - } - - // Handle anyOf, oneOf, allOf - for (const keyword of ['anyOf', 'oneOf', 'allOf']) { - if (Array.isArray(result[keyword])) { - result[keyword] = (result[keyword] as Record[]).map(enforceStrictSchema) - } - } - - // Handle $defs / definitions - for (const defKey of ['$defs', 'definitions']) { - if (result[defKey] && typeof result[defKey] === 'object') { - result[defKey] = Object.fromEntries( - Object.entries(result[defKey] as Record).map(([key, value]) => [ - key, - enforceStrictSchema(value as Record), - ]) - ) - } - } - - return result -} - export interface ResponsesProviderConfig { providerId: string providerLabel: string diff --git a/apps/sim/providers/openrouter/index.test.ts b/apps/sim/providers/openrouter/index.test.ts new file mode 100644 index 00000000000..88339fe4a93 --- /dev/null +++ b/apps/sim/providers/openrouter/index.test.ts @@ -0,0 +1,345 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockCreate, + mockExecuteTool, + mockSupportsNative, + mockPrepareTools, + mockCheckForced, + mockCreateStream, +} = vi.hoisted(() => ({ + mockCreate: vi.fn(), + mockExecuteTool: vi.fn(), + mockSupportsNative: vi.fn(), + mockPrepareTools: vi.fn((tools: unknown) => ({ + tools, + toolChoice: 'auto', + forcedTools: [], + hasFilteredTools: false, + })), + mockCheckForced: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })), + mockCreateStream: vi.fn(), +})) + +vi.mock('openai', () => ({ + default: vi.fn().mockImplementation(() => ({ + chat: { completions: { create: mockCreate } }, + })), +})) + +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 10 })) + +vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) + +vi.mock('@/providers/models', () => ({ + getProviderModels: vi.fn().mockReturnValue([]), + getProviderDefaultModel: vi.fn().mockReturnValue(''), +})) + +vi.mock('@/providers/attachments', () => ({ + formatMessagesForProvider: vi.fn((messages: unknown) => messages), +})) + +vi.mock('@/providers/openrouter/utils', () => ({ + supportsNativeStructuredOutputs: mockSupportsNative, + createReadableStreamFromOpenAIStream: mockCreateStream, + checkForForcedToolUsage: mockCheckForced, +})) + +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) + +vi.mock('@/providers/utils', () => ({ + calculateCost: vi.fn(() => ({ input: 0, output: 0, total: 0 })), + prepareToolsWithUsageControl: mockPrepareTools, + prepareToolExecution: vi.fn((_tool: unknown, toolArgs: Record) => ({ + toolParams: toolArgs, + executionParams: toolArgs, + })), + sumToolCosts: vi.fn(() => 0), + generateSchemaInstructions: vi.fn(() => 'SCHEMA_INSTRUCTIONS'), +})) + +import { openRouterProvider } from '@/providers/openrouter/index' +import type { ProviderRequest, ProviderResponse, ProviderToolConfig } from '@/providers/types' + +interface Usage { + prompt_tokens: number + completion_tokens: number + total_tokens: number +} + +function textResponse( + content: string, + usage: Usage = { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 } +) { + return { + choices: [{ message: { content, tool_calls: undefined }, finish_reason: 'stop' }], + usage, + } +} + +function toolCallResponse(name: string, args: Record, id = 'call_1') { + return { + choices: [ + { + message: { + content: null, + tool_calls: [ + { id, type: 'function', function: { name, arguments: JSON.stringify(args) } }, + ], + }, + finish_reason: 'tool_calls', + }, + ], + usage: { prompt_tokens: 8, completion_tokens: 4, total_tokens: 12 }, + } +} + +function tool(id: string): ProviderToolConfig { + return { + id, + name: id, + description: 'test tool', + params: {}, + parameters: { type: 'object', properties: {}, required: [] }, + } +} + +const baseRequest: ProviderRequest = { + apiKey: 'sk-or-test', + model: 'openrouter/anthropic/claude-3.5-sonnet', + systemPrompt: 'You are helpful.', + messages: [{ role: 'user', content: 'Hello' }], +} + +describe('openRouterProvider.executeRequest', () => { + beforeEach(() => { + vi.clearAllMocks() + mockCreate.mockReset() + mockExecuteTool.mockReset() + mockSupportsNative.mockResolvedValue(false) + }) + + it('requires an API key', async () => { + await expect( + openRouterProvider.executeRequest({ model: 'openrouter/x', messages: [] }) + ).rejects.toThrow('API key is required for OpenRouter') + }) + + it('strips the openrouter/ prefix and returns content + tokens', async () => { + mockCreate.mockResolvedValueOnce(textResponse('Hi there')) + + const res = (await openRouterProvider.executeRequest(baseRequest)) as ProviderResponse + + expect(res.content).toBe('Hi there') + expect(res.model).toBe('anthropic/claude-3.5-sonnet') + expect(res.tokens).toEqual({ input: 10, output: 5, total: 15 }) + + const payload = mockCreate.mock.calls[0][0] + expect(payload.model).toBe('anthropic/claude-3.5-sonnet') + expect(payload.messages[0]).toEqual({ role: 'system', content: 'You are helpful.' }) + expect(payload.messages.at(-1)).toEqual({ role: 'user', content: 'Hello' }) + }) + + it('inserts context as a user message between system and history', async () => { + mockCreate.mockResolvedValueOnce(textResponse('ok')) + + await openRouterProvider.executeRequest({ ...baseRequest, context: 'CTX' }) + + const { messages } = mockCreate.mock.calls[0][0] + expect(messages[0]).toEqual({ role: 'system', content: 'You are helpful.' }) + expect(messages[1]).toEqual({ role: 'user', content: 'CTX' }) + expect(messages[2]).toEqual({ role: 'user', content: 'Hello' }) + }) + + it('forwards maxTokens as max_tokens and temperature', async () => { + mockCreate.mockResolvedValueOnce(textResponse('ok')) + + await openRouterProvider.executeRequest({ ...baseRequest, maxTokens: 256, temperature: 0.4 }) + + const payload = mockCreate.mock.calls[0][0] + expect(payload.max_tokens).toBe(256) + expect(payload.temperature).toBe(0.4) + }) + + it('runs the tool loop: executes the tool, echoes tool_calls, returns the tool result, sums tokens', async () => { + mockCreate + .mockResolvedValueOnce(toolCallResponse('get_weather', { city: 'SF' })) + .mockResolvedValueOnce( + textResponse('It is sunny', { prompt_tokens: 20, completion_tokens: 6, total_tokens: 26 }) + ) + mockExecuteTool.mockResolvedValueOnce({ success: true, output: { temp: 70 } }) + + const res = (await openRouterProvider.executeRequest({ + ...baseRequest, + tools: [tool('get_weather')], + })) as ProviderResponse + + expect(mockExecuteTool).toHaveBeenCalledWith('get_weather', { city: 'SF' }, expect.anything()) + expect(res.content).toBe('It is sunny') + expect(res.toolCalls?.[0]).toMatchObject({ + name: 'get_weather', + result: { temp: 70 }, + success: true, + }) + expect(res.toolResults).toEqual([{ temp: 70 }]) + expect(res.tokens).toEqual({ input: 28, output: 10, total: 38 }) + + const secondMessages = mockCreate.mock.calls[1][0].messages + const assistant = secondMessages.find((m: { role: string }) => m.role === 'assistant') + expect(assistant).toMatchObject({ + content: null, + tool_calls: [{ id: 'call_1', type: 'function', function: { name: 'get_weather' } }], + }) + const toolMsg = secondMessages.find((m: { role: string }) => m.role === 'tool') + expect(toolMsg).toEqual({ + role: 'tool', + tool_call_id: 'call_1', + content: JSON.stringify({ temp: 70 }), + }) + }) + + it('reports a failed tool result as an error payload to the model', async () => { + mockCreate + .mockResolvedValueOnce(toolCallResponse('get_weather', { city: 'SF' })) + .mockResolvedValueOnce(textResponse('done')) + mockExecuteTool.mockResolvedValueOnce({ success: false, output: undefined, error: 'boom' }) + + const res = (await openRouterProvider.executeRequest({ + ...baseRequest, + tools: [tool('get_weather')], + })) as ProviderResponse + + expect(res.toolResults).toBeUndefined() + expect(res.toolCalls?.[0]).toMatchObject({ success: false }) + const toolMsg = mockCreate.mock.calls[1][0].messages.find( + (m: { role: string }) => m.role === 'tool' + ) + expect(JSON.parse(toolMsg.content)).toEqual({ + error: true, + message: 'boom', + tool: 'get_weather', + }) + }) + + it('applies native structured outputs (json_schema + require_parameters) when no tools are active', async () => { + mockSupportsNative.mockResolvedValue(true) + mockCreate.mockResolvedValueOnce(textResponse('{"x":1}')) + + await openRouterProvider.executeRequest({ + ...baseRequest, + responseFormat: { + name: 'out', + schema: { type: 'object', properties: { x: { type: 'number' } } }, + strict: true, + }, + }) + + const payload = mockCreate.mock.calls[0][0] + expect(payload.response_format).toMatchObject({ + type: 'json_schema', + json_schema: { name: 'out', strict: true }, + }) + expect(payload.provider).toMatchObject({ require_parameters: true }) + }) + + it('falls back to json_object + prompt instructions when native structured outputs are unsupported', async () => { + mockSupportsNative.mockResolvedValue(false) + mockCreate.mockResolvedValueOnce(textResponse('{"x":1}')) + + await openRouterProvider.executeRequest({ + ...baseRequest, + responseFormat: { name: 'out', schema: { type: 'object' } }, + }) + + const payload = mockCreate.mock.calls[0][0] + expect(payload.response_format).toEqual({ type: 'json_object' }) + expect(payload.messages.at(-1)).toEqual({ role: 'user', content: 'SCHEMA_INSTRUCTIONS' }) + }) + + it('defers response_format until after the tool loop when tools are active', async () => { + mockSupportsNative.mockResolvedValue(true) + mockCreate + .mockResolvedValueOnce(textResponse('interim')) + .mockResolvedValueOnce(textResponse('{"x":1}')) + + const res = (await openRouterProvider.executeRequest({ + ...baseRequest, + tools: [tool('get_weather')], + responseFormat: { name: 'out', schema: { type: 'object' }, strict: true }, + })) as ProviderResponse + + const toolCall = mockCreate.mock.calls[0][0] + expect(toolCall.tools).toBeDefined() + expect(toolCall.response_format).toBeUndefined() + + const finalCall = mockCreate.mock.calls[1][0] + expect(finalCall.response_format).toMatchObject({ type: 'json_schema' }) + expect(finalCall.tools).toBeUndefined() + expect(finalCall.tool_choice).toBeUndefined() + expect(res.content).toBe('{"x":1}') + }) + + it('forces the next tool after a forced tool is used', async () => { + mockPrepareTools.mockReturnValueOnce({ + tools: [tool('a')], + toolChoice: { type: 'function', function: { name: 'a' } }, + forcedTools: ['a', 'b'], + hasFilteredTools: false, + }) + mockCheckForced.mockReturnValueOnce({ hasUsedForcedTool: true, usedForcedTools: ['a'] }) + mockCreate + .mockResolvedValueOnce(toolCallResponse('a', {})) + .mockResolvedValueOnce(textResponse('done')) + mockExecuteTool.mockResolvedValueOnce({ success: true, output: {} }) + + await openRouterProvider.executeRequest({ ...baseRequest, tools: [tool('a'), tool('b')] }) + + expect(mockCreate.mock.calls[0][0].tool_choice).toEqual({ + type: 'function', + function: { name: 'a' }, + }) + expect(mockCreate.mock.calls[1][0].tool_choice).toEqual({ + type: 'function', + function: { name: 'b' }, + }) + }) + + it('streams directly when there are no tools and sends usage opt-in', async () => { + mockCreate.mockResolvedValueOnce({}) + + const res = await openRouterProvider.executeRequest({ ...baseRequest, stream: true }) + + const payload = mockCreate.mock.calls[0][0] + expect(payload.stream).toBe(true) + expect(payload.stream_options).toEqual({ include_usage: true }) + expect(mockCreateStream).toHaveBeenCalledTimes(1) + expect(res).toHaveProperty('stream') + expect(res).toHaveProperty('execution.output.model', 'anthropic/claude-3.5-sonnet') + }) + + it('stops the tool loop at MAX_TOOL_ITERATIONS', async () => { + mockCreate.mockResolvedValue(toolCallResponse('looping', {})) + mockExecuteTool.mockResolvedValue({ success: true, output: {} }) + + const res = (await openRouterProvider.executeRequest({ + ...baseRequest, + tools: [tool('looping')], + })) as ProviderResponse + + expect(mockCreate).toHaveBeenCalledTimes(11) + expect(mockExecuteTool).toHaveBeenCalledTimes(10) + expect(res.toolCalls?.length).toBe(10) + }) + + it('wraps SDK errors in a ProviderError', async () => { + mockCreate.mockRejectedValueOnce(new Error('rate limited')) + + await expect(openRouterProvider.executeRequest(baseRequest)).rejects.toThrow('rate limited') + }) +}) diff --git a/apps/sim/providers/openrouter/index.ts b/apps/sim/providers/openrouter/index.ts index d3d2535b43d..9bc180bdd11 100644 --- a/apps/sim/providers/openrouter/index.ts +++ b/apps/sim/providers/openrouter/index.ts @@ -376,8 +376,8 @@ export const openRouterProvider: ProviderConfig = { }) let resultContent: any - if (result.success) { - toolResults.push(result.output!) + if (result.success && result.output) { + toolResults.push(result.output) resultContent = result.output } else { resultContent = { @@ -653,8 +653,3 @@ export const openRouterProvider: ProviderConfig = { } }, } - -/** - * Enriches the last model segment with per-iteration content from a Chat - * Completions response: assistant text, tool calls, finish reason, token usage. - */ diff --git a/apps/sim/providers/openrouter/utils.ts b/apps/sim/providers/openrouter/utils.ts index 8d8dade8279..51637f5148d 100644 --- a/apps/sim/providers/openrouter/utils.ts +++ b/apps/sim/providers/openrouter/utils.ts @@ -20,9 +20,6 @@ let modelCapabilitiesCache: Map | null = null let cacheTimestamp = 0 const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes -/** - * Fetches and caches OpenRouter model capabilities from their API. - */ async function fetchModelCapabilities(): Promise> { try { const response = await fetch('https://openrouter.ai/api/v1/models', { @@ -82,18 +79,11 @@ export async function getOpenRouterModelCapabilities( return modelCapabilitiesCache.get(normalizedId) ?? null } -/** - * Checks if a model supports native structured outputs (json_schema). - */ export async function supportsNativeStructuredOutputs(modelId: string): Promise { const capabilities = await getOpenRouterModelCapabilities(modelId) return capabilities?.supportsStructuredOutputs ?? false } -/** - * Creates a ReadableStream from an OpenRouter streaming response. - * Uses the shared OpenAI-compatible streaming utility. - */ export function createReadableStreamFromOpenAIStream( openaiStream: AsyncIterable, onComplete?: (content: string, usage: CompletionUsage) => void @@ -101,10 +91,6 @@ export function createReadableStreamFromOpenAIStream( return createOpenAICompatibleStream(openaiStream, 'OpenRouter', onComplete) } -/** - * Checks if a forced tool was used in an OpenRouter response. - * Uses the shared OpenAI-compatible forced tool usage helper. - */ export function checkForForcedToolUsage( response: any, toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any }, diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index 5815f0aecfb..5cce6ce387e 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -669,6 +669,59 @@ export function calculateCost( } } +/** + * Recursively enforces OpenAI strict-mode requirements on a JSON schema: + * - Sets `additionalProperties: false` on every object type. + * - Forces `required` to include ALL property keys. + * + * Required for any OpenAI-compatible backend that validates strict structured + * outputs (OpenAI, Azure OpenAI, and OpenAI routes behind proxies like LiteLLM), + * which reject schemas missing these constraints with an HTTP 400. + */ +export function enforceStrictSchema(schema: Record): Record { + if (!schema || typeof schema !== 'object') return schema + + const result = { ...schema } + + if (result.type === 'object') { + result.additionalProperties = false + + if (result.properties && typeof result.properties === 'object') { + const propKeys = Object.keys(result.properties as Record) + result.required = propKeys + result.properties = Object.fromEntries( + Object.entries(result.properties as Record).map(([key, value]) => [ + key, + enforceStrictSchema(value as Record), + ]) + ) + } + } + + if (result.type === 'array' && result.items) { + result.items = enforceStrictSchema(result.items as Record) + } + + for (const keyword of ['anyOf', 'oneOf', 'allOf']) { + if (Array.isArray(result[keyword])) { + result[keyword] = (result[keyword] as Record[]).map(enforceStrictSchema) + } + } + + for (const defKey of ['$defs', 'definitions']) { + if (result[defKey] && typeof result[defKey] === 'object') { + result[defKey] = Object.fromEntries( + Object.entries(result[defKey] as Record).map(([key, value]) => [ + key, + enforceStrictSchema(value as Record), + ]) + ) + } + } + + return result +} + /** * Sums the `cost.total` from each tool result returned during a provider tool loop. * Tool results may carry a `cost` object injected by `applyHostedKeyCostToResult`. diff --git a/apps/sim/providers/vllm/index.test.ts b/apps/sim/providers/vllm/index.test.ts new file mode 100644 index 00000000000..4477beeeda7 --- /dev/null +++ b/apps/sim/providers/vllm/index.test.ts @@ -0,0 +1,296 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockCreate, + mockExecuteTool, + mockPrepareTools, + mockCheckForced, + mockCreateStream, + envState, +} = vi.hoisted(() => ({ + mockCreate: vi.fn(), + mockExecuteTool: vi.fn(), + mockPrepareTools: vi.fn(), + mockCheckForced: vi.fn(), + mockCreateStream: vi.fn(), + envState: { + VLLM_BASE_URL: 'http://localhost:8000', + VLLM_API_KEY: undefined as string | undefined, + }, +})) + +vi.mock('openai', () => ({ + default: vi.fn(() => ({ chat: { completions: { create: mockCreate } } })), +})) +vi.mock('@/lib/core/config/env', () => ({ env: envState })) +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 20 })) +vi.mock('@/providers/models', () => ({ + getProviderModels: vi.fn(() => []), + getProviderDefaultModel: vi.fn(() => 'vllm/generic'), +})) +vi.mock('@/providers/attachments', () => ({ + formatMessagesForProvider: vi.fn((messages) => messages), +})) +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) +vi.mock('@/providers/utils', () => ({ + calculateCost: vi.fn(() => ({ input: 0, output: 0, total: 0 })), + prepareToolExecution: vi.fn((_tool, args) => ({ toolParams: args, executionParams: args })), + prepareToolsWithUsageControl: mockPrepareTools, + sumToolCosts: vi.fn(() => 0), +})) +vi.mock('@/providers/vllm/utils', () => ({ + checkForForcedToolUsage: mockCheckForced, + createReadableStreamFromVLLMStream: mockCreateStream, +})) +vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) +vi.mock('@/stores/providers', () => ({ + useProvidersStore: { getState: () => ({ setProviderModels: vi.fn() }) }, +})) + +import type { ProviderToolConfig } from '@/providers/types' +import { vllmProvider } from '@/providers/vllm/index' + +interface ToolCall { + id: string + type: 'function' + function: { name: string; arguments: string } +} + +function chatResponse(content: string | null, toolCalls?: ToolCall[]) { + return { + choices: [{ message: { content, tool_calls: toolCalls } }], + usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, + } +} + +function makeTool(id: string): ProviderToolConfig { + return { + id, + name: id, + description: '', + params: {}, + parameters: { type: 'object', properties: {}, required: [] }, + } +} + +const toolCall = (id: string, name: string, args = '{}'): ToolCall => ({ + id, + type: 'function', + function: { name, arguments: args }, +}) + +/** Payload passed to the Nth `chat.completions.create` call. */ +const createPayload = (callIndex: number) => mockCreate.mock.calls[callIndex][0] + +describe('vllmProvider', () => { + beforeEach(() => { + vi.clearAllMocks() + envState.VLLM_BASE_URL = 'http://localhost:8000' + envState.VLLM_API_KEY = undefined + mockPrepareTools.mockReturnValue({ + tools: [{ type: 'function', function: { name: 'myTool' } }], + toolChoice: 'auto', + forcedTools: [], + hasFilteredTools: false, + }) + mockCheckForced.mockReturnValue({ hasUsedForcedTool: false, usedForcedTools: [] }) + mockCreateStream.mockReturnValue(new ReadableStream({ start: (c) => c.close() })) + mockExecuteTool.mockResolvedValue({ success: true, output: { result: 'ok' } }) + }) + + it('builds a chat payload with the vllm/ prefix stripped and messages assembled in order', async () => { + mockCreate.mockResolvedValueOnce(chatResponse('hello')) + + const result = await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + systemPrompt: 'be helpful', + context: 'prior context', + messages: [{ role: 'user', content: 'hi' }], + temperature: 0.7, + maxTokens: 256, + }) + + const payload = createPayload(0) + expect(payload.model).toBe('llama-3') + expect(payload.temperature).toBe(0.7) + expect(payload.max_completion_tokens).toBe(256) + expect(payload.messages.map((m: { role: string }) => m.role)).toEqual([ + 'system', + 'user', + 'user', + ]) + expect(result.content).toBe('hello') + expect(result.tokens).toEqual({ input: 10, output: 5, total: 15 }) + }) + + it('sends response_format as json_schema with strict when a responseFormat is provided', async () => { + mockCreate.mockResolvedValueOnce(chatResponse('{}')) + + await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'hi' }], + responseFormat: { name: 'out', schema: { type: 'object' }, strict: true }, + }) + + expect(createPayload(0).response_format).toEqual({ + type: 'json_schema', + json_schema: { name: 'out', schema: { type: 'object' }, strict: true }, + }) + }) + + it('strips markdown code fences from structured-output content', async () => { + mockCreate.mockResolvedValueOnce(chatResponse('```json\n{"a":1}\n```')) + + const result = await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'hi' }], + responseFormat: { name: 'out', schema: { type: 'object' }, strict: true }, + }) + + expect(result.content).toBe('{"a":1}') + }) + + it('runs the tool loop: executes tools, appends assistant + tool messages, returns results', async () => { + mockCreate + .mockResolvedValueOnce(chatResponse(null, [toolCall('call_1', 'myTool', '{"x":1}')])) + .mockResolvedValueOnce(chatResponse('final answer')) + + const result = await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'use a tool' }], + tools: [makeTool('myTool')], + }) + + expect(mockExecuteTool).toHaveBeenCalledWith('myTool', { x: 1 }, expect.anything()) + + const [assistantMessage, toolMessage] = createPayload(1).messages.slice(-2) + expect(assistantMessage).toMatchObject({ + role: 'assistant', + content: null, + tool_calls: [{ id: 'call_1', type: 'function', function: { name: 'myTool' } }], + }) + expect(toolMessage).toMatchObject({ role: 'tool', tool_call_id: 'call_1' }) + expect(toolMessage).not.toHaveProperty('name') + + expect(result.content).toBe('final answer') + expect(result.toolCalls).toHaveLength(1) + expect(result.toolCalls?.[0]).toMatchObject({ name: 'myTool', success: true }) + expect(result.toolResults).toHaveLength(1) + }) + + it('records a failed tool result without throwing', async () => { + mockExecuteTool.mockResolvedValueOnce({ success: false, error: 'tool blew up' }) + mockCreate + .mockResolvedValueOnce(chatResponse(null, [toolCall('call_1', 'myTool')])) + .mockResolvedValueOnce(chatResponse('done')) + + const result = await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'go' }], + tools: [makeTool('myTool')], + }) + + expect(result.toolCalls?.[0]).toMatchObject({ name: 'myTool', success: false }) + const toolMessage = createPayload(1).messages.at(-1) + expect(JSON.parse(toolMessage.content)).toMatchObject({ error: true, tool: 'myTool' }) + }) + + it('surfaces a ProviderError when a follow-up model call fails mid-loop', async () => { + mockCreate + .mockResolvedValueOnce(chatResponse(null, [toolCall('call_1', 'myTool')])) + .mockRejectedValueOnce(new Error('connection reset')) + + await expect( + vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'go' }], + tools: [makeTool('myTool')], + }) + ).rejects.toThrow('connection reset') + + expect(mockExecuteTool).toHaveBeenCalledTimes(1) + }) + + it('cycles forced tools: forces the next forced tool after the first is used', async () => { + mockPrepareTools.mockReturnValue({ + tools: [{ type: 'function', function: { name: 'toolA' } }], + toolChoice: { type: 'function', function: { name: 'toolA' } }, + forcedTools: ['toolA', 'toolB'], + hasFilteredTools: false, + }) + mockCheckForced + .mockReturnValueOnce({ hasUsedForcedTool: true, usedForcedTools: ['toolA'] }) + .mockReturnValueOnce({ hasUsedForcedTool: true, usedForcedTools: ['toolA', 'toolB'] }) + mockCreate + .mockResolvedValueOnce(chatResponse(null, [toolCall('c1', 'toolA')])) + .mockResolvedValueOnce(chatResponse('done')) + + await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'go' }], + tools: [makeTool('toolA'), makeTool('toolB')], + }) + + expect(createPayload(1).tool_choice).toEqual({ type: 'function', function: { name: 'toolB' } }) + }) + + it('streams directly when there are no tools, requesting usage in the stream', async () => { + mockCreate.mockResolvedValueOnce({}) + + const result = await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'hi' }], + stream: true, + }) + + expect(mockCreate).toHaveBeenCalledTimes(1) + const payload = createPayload(0) + expect(payload.stream).toBe(true) + expect(payload.stream_options).toEqual({ include_usage: true }) + expect('stream' in result && 'execution' in result).toBe(true) + }) + + it('uses tool_choice "none" on the final streaming call after tool processing', async () => { + mockCreate.mockResolvedValueOnce(chatResponse('answer')).mockResolvedValueOnce({}) + + await vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'hi' }], + stream: true, + tools: [makeTool('myTool')], + }) + + const streamingPayload = createPayload(1) + expect(streamingPayload.stream).toBe(true) + expect(streamingPayload.tool_choice).toBe('none') + }) + + it('throws a ProviderError carrying the vLLM error message on API failure', async () => { + mockCreate.mockRejectedValueOnce({ + error: { message: 'bad request', type: 'invalid', code: 400 }, + }) + + await expect( + vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'hi' }], + }) + ).rejects.toThrow('bad request') + }) + + it('throws when no base URL is configured', async () => { + envState.VLLM_BASE_URL = '' + + await expect( + vllmProvider.executeRequest({ + model: 'vllm/llama-3', + messages: [{ role: 'user', content: 'hi' }], + }) + ).rejects.toThrow('VLLM_BASE_URL is required') + }) +}) diff --git a/apps/sim/providers/vllm/index.ts b/apps/sim/providers/vllm/index.ts index 2de3c695116..87610d9c43e 100644 --- a/apps/sim/providers/vllm/index.ts +++ b/apps/sim/providers/vllm/index.ts @@ -21,9 +21,8 @@ import { prepareToolExecution, prepareToolsWithUsageControl, sumToolCosts, - trackForcedToolUsage, } from '@/providers/utils' -import { createReadableStreamFromVLLMStream } from '@/providers/vllm/utils' +import { checkForForcedToolUsage, createReadableStreamFromVLLMStream } from '@/providers/vllm/utils' import { useProvidersStore } from '@/stores/providers' import { executeTool } from '@/tools' @@ -282,25 +281,7 @@ export const vllmProvider: ProviderConfig = { const forcedTools = preparedTools?.forcedTools || [] let usedForcedTools: string[] = [] - - const checkForForcedToolUsage = ( - response: any, - toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any } - ) => { - if (typeof toolChoice === 'object' && response.choices[0]?.message?.tool_calls) { - const toolCallsResponse = response.choices[0].message.tool_calls - const result = trackForcedToolUsage( - toolCallsResponse, - toolChoice, - logger, - 'vllm', - forcedTools, - usedForcedTools - ) - hasUsedForcedTool = result.hasUsedForcedTool - usedForcedTools = result.usedForcedTools - } - } + let hasUsedForcedTool = false let currentResponse = await vllm.chat.completions.create( payload, @@ -327,8 +308,6 @@ export const vllmProvider: ProviderConfig = { let modelTime = firstResponseTime let toolsTime = 0 - let hasUsedForcedTool = false - const timeSegments: TimeSegment[] = [ { type: 'model', @@ -339,7 +318,16 @@ export const vllmProvider: ProviderConfig = { }, ] - checkForForcedToolUsage(currentResponse, originalToolChoice) + if (originalToolChoice) { + const forcedResult = checkForForcedToolUsage( + currentResponse, + originalToolChoice, + forcedTools, + usedForcedTools + ) + hasUsedForcedTool = forcedResult.hasUsedForcedTool + usedForcedTools = forcedResult.usedForcedTools + } while (iterationCount < MAX_TOOL_ITERATIONS) { if (currentResponse.choices[0]?.message?.content) { @@ -502,7 +490,16 @@ export const vllmProvider: ProviderConfig = { request.abortSignal ? { signal: request.abortSignal } : undefined ) - checkForForcedToolUsage(currentResponse, nextPayload.tool_choice) + if (nextPayload.tool_choice && typeof nextPayload.tool_choice === 'object') { + const forcedResult = checkForForcedToolUsage( + currentResponse, + nextPayload.tool_choice, + forcedTools, + usedForcedTools + ) + hasUsedForcedTool = forcedResult.hasUsedForcedTool + usedForcedTools = forcedResult.usedForcedTools + } const nextModelEndTime = Date.now() const thisModelTime = nextModelEndTime - nextModelStartTime @@ -550,7 +547,7 @@ export const vllmProvider: ProviderConfig = { const streamingParams: ChatCompletionCreateParamsStreaming = { ...payload, messages: currentMessages, - tool_choice: 'auto', + tool_choice: 'none', stream: true, stream_options: { include_usage: true }, } @@ -685,8 +682,3 @@ export const vllmProvider: ProviderConfig = { } }, } - -/** - * Enriches the last model segment with per-iteration content from a Chat - * Completions response: assistant text, tool calls, finish reason, token usage. - */ diff --git a/apps/sim/providers/vllm/utils.ts b/apps/sim/providers/vllm/utils.ts index 6f433291488..2b1db5bf553 100644 --- a/apps/sim/providers/vllm/utils.ts +++ b/apps/sim/providers/vllm/utils.ts @@ -1,6 +1,6 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils' /** * Creates a ReadableStream from a vLLM streaming response. @@ -12,3 +12,16 @@ export function createReadableStreamFromVLLMStream( ): ReadableStream { return createOpenAICompatibleStream(vllmStream, 'vLLM', onComplete) } + +/** + * Checks if a forced tool was used in a vLLM response. + * Uses the shared OpenAI-compatible forced tool usage helper. + */ +export function checkForForcedToolUsage( + response: any, + toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any }, + forcedTools: string[], + usedForcedTools: string[] +): { hasUsedForcedTool: boolean; usedForcedTools: string[] } { + return checkForForcedToolUsageOpenAI(response, toolChoice, 'vLLM', forcedTools, usedForcedTools) +} From 4b0dab4682dcb5f723c7222075553fcca93eeb61 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 29 May 2026 14:34:15 -0700 Subject: [PATCH 09/14] chore(copilot): deprecate mcp server (#4797) * chore(copilot): deprecate mcp * update error codes * deprecate copilot api v1 route --- apps/docs/content/docs/de/copilot/index.mdx | 95 --- apps/docs/content/docs/en/copilot/index.mdx | 84 -- apps/docs/content/docs/es/copilot/index.mdx | 95 --- apps/docs/content/docs/fr/copilot/index.mdx | 95 --- apps/docs/content/docs/ja/copilot/index.mdx | 95 --- apps/docs/content/docs/zh/copilot/index.mdx | 95 --- .../api/mcp/copilot/route.ts | 4 +- .../api/mcp/copilot/route.ts | 4 +- .../oauth-authorization-server/route.ts | 12 +- .../oauth-protected-resource/route.ts | 12 +- apps/sim/app/api/mcp/copilot/route.test.ts | 62 ++ apps/sim/app/api/mcp/copilot/route.ts | 741 +----------------- .../sim/app/api/v1/copilot/chat/route.test.ts | 26 + apps/sim/app/api/v1/copilot/chat/route.ts | 157 +--- apps/sim/lib/mcp/copilot-deprecated.ts | 35 + packages/db/.env.swp | Bin 0 -> 12288 bytes scripts/check-api-validation-contracts.ts | 9 +- 17 files changed, 158 insertions(+), 1463 deletions(-) create mode 100644 apps/sim/app/api/mcp/copilot/route.test.ts create mode 100644 apps/sim/app/api/v1/copilot/chat/route.test.ts create mode 100644 apps/sim/lib/mcp/copilot-deprecated.ts create mode 100644 packages/db/.env.swp diff --git a/apps/docs/content/docs/de/copilot/index.mdx b/apps/docs/content/docs/de/copilot/index.mdx index 116b107beea..e9a92f7e209 100644 --- a/apps/docs/content/docs/de/copilot/index.mdx +++ b/apps/docs/content/docs/de/copilot/index.mdx @@ -246,98 +246,3 @@ Die Copilot-Nutzung wird pro Token des zugrunde liegenden LLM abgerechnet. Wenn Siehe die [Seite zur Kostenberechnung](/execution/costs) für Abrechnungsdetails. -## Copilot MCP - -Sie können Copilot als MCP-Server in Ihrem bevorzugten Editor oder AI-Client verwenden. Damit können Sie Sim-Workflows direkt aus Tools wie Cursor, Claude Code, Claude Desktop und VS Code erstellen, testen, bereitstellen und verwalten. - -### Generieren eines Copilot-API-Schlüssels - -Um sich mit dem Copilot-MCP-Server zu verbinden, benötigen Sie einen **Copilot-API-Schlüssel**: - -1. Gehen Sie zu [sim.ai](https://sim.ai) und melden Sie sich an -2. Navigieren Sie zu **Einstellungen** → **Copilot** -3. Klicken Sie auf **API-Schlüssel generieren** -4. Kopieren Sie den Schlüssel – er wird nur einmal angezeigt - -Der Schlüssel sieht aus wie `sk-sim-copilot-...`. Sie werden ihn in der folgenden Konfiguration verwenden. - -### Cursor - -Fügen Sie Folgendes zu Ihrer `.cursor/mcp.json` (Projektebene) oder den globalen Cursor-MCP-Einstellungen hinzu: - -```json -{ - "mcpServers": { - "sim-copilot": { - "url": "https://www.sim.ai/api/mcp/copilot", - "headers": { - "X-API-Key": "YOUR_COPILOT_API_KEY" - } - } - } -} -``` - -Ersetzen Sie `YOUR_COPILOT_API_KEY` durch den oben generierten Schlüssel. - -### Claude Code - -Führen Sie den folgenden Befehl aus, um den Copilot MCP-Server hinzuzufügen: - -```bash -claude mcp add sim-copilot \ - --transport http \ - https://www.sim.ai/api/mcp/copilot \ - --header "X-API-Key: YOUR_COPILOT_API_KEY" -``` - -Ersetzen Sie `YOUR_COPILOT_API_KEY` durch Ihren Schlüssel. - -### Claude Desktop - -Claude Desktop benötigt [`mcp-remote`](https://www.npmjs.com/package/mcp-remote), um sich mit HTTP-basierten MCP-Servern zu verbinden. Fügen Sie Folgendes zu Ihrer Claude Desktop-Konfigurationsdatei hinzu (`~/Library/Application Support/Claude/claude_desktop_config.json` unter macOS): - -```json -{ - "mcpServers": { - "sim-copilot": { - "command": "npx", - "args": [ - "-y", - "mcp-remote", - "https://www.sim.ai/api/mcp/copilot", - "--header", - "X-API-Key: YOUR_COPILOT_API_KEY" - ] - } - } -} -``` - -Ersetzen Sie `YOUR_COPILOT_API_KEY` durch Ihren Schlüssel. - -### VS Code - -Fügen Sie Folgendes zu Ihrer VS Code `settings.json` oder Workspace `.vscode/settings.json` hinzu: - -```json -{ - "mcp": { - "servers": { - "sim-copilot": { - "type": "http", - "url": "https://www.sim.ai/api/mcp/copilot", - "headers": { - "X-API-Key": "YOUR_COPILOT_API_KEY" - } - } - } - } -} -``` - -Ersetzen Sie `YOUR_COPILOT_API_KEY` durch Ihren Schlüssel. - - - Für selbst gehostete Deployments ersetzen Sie `https://www.sim.ai` durch Ihre selbst gehostete Sim-URL. - diff --git a/apps/docs/content/docs/en/copilot/index.mdx b/apps/docs/content/docs/en/copilot/index.mdx index 81f6dfe6a62..f30f3da691f 100644 --- a/apps/docs/content/docs/en/copilot/index.mdx +++ b/apps/docs/content/docs/en/copilot/index.mdx @@ -50,90 +50,6 @@ For complex requests, Copilot may show its reasoning in an expandable thinking b Copilot usage is billed per token and counts toward your plan's credit usage. If you reach your limit, enable on-demand billing from Settings → Subscription. -## Copilot MCP - -You can use Copilot as an MCP server to build, test, and manage Sim workflows from external editors — Cursor, Claude Code, Claude Desktop, and VS Code. - -### Generating a Copilot API Key - -1. Go to [sim.ai](https://sim.ai) and sign in -2. Navigate to **Settings** → **Copilot** -3. Click **Generate API Key** -4. Copy the key — it is only shown once - -The key will look like `sk-sim-copilot-...`. - -### Cursor - -Add to `.cursor/mcp.json`: - -```json -{ - "mcpServers": { - "sim-copilot": { - "url": "https://www.sim.ai/api/mcp/copilot", - "headers": { - "X-API-Key": "YOUR_COPILOT_API_KEY" - } - } - } -} -``` - -### Claude Code - -```bash -claude mcp add sim-copilot \ - --transport http \ - https://www.sim.ai/api/mcp/copilot \ - --header "X-API-Key: YOUR_COPILOT_API_KEY" -``` - -### Claude Desktop - -Claude Desktop requires [`mcp-remote`](https://www.npmjs.com/package/mcp-remote). Add to `~/Library/Application Support/Claude/claude_desktop_config.json`: - -```json -{ - "mcpServers": { - "sim-copilot": { - "command": "npx", - "args": [ - "-y", - "mcp-remote", - "https://www.sim.ai/api/mcp/copilot", - "--header", - "X-API-Key: YOUR_COPILOT_API_KEY" - ] - } - } -} -``` - -### VS Code - -Add to `settings.json` or `.vscode/settings.json`: - -```json -{ - "mcp": { - "servers": { - "sim-copilot": { - "type": "http", - "url": "https://www.sim.ai/api/mcp/copilot", - "headers": { - "X-API-Key": "YOUR_COPILOT_API_KEY" - } - } - } - } -} -``` - - - For self-hosted deployments, replace `https://www.sim.ai` with your self-hosted Sim URL. - - Consulta la [página de cálculo de costos](/execution/costs) para detalles de facturación. -## Copilot MCP - -Puedes usar Copilot como servidor MCP en tu editor o cliente de IA favorito. Esto te permite construir, probar, desplegar y gestionar flujos de trabajo de Sim directamente desde herramientas como Cursor, Claude Code, Claude Desktop y VS Code. - -### Generar una clave API de Copilot - -Para conectarte al servidor MCP de Copilot, necesitas una **clave API de Copilot**: - -1. Ve a [sim.ai](https://sim.ai) e inicia sesión -2. Navega a **Configuración** → **Copilot** -3. Haz clic en **Generar clave API** -4. Copia la clave — solo se muestra una vez - -La clave se verá como `sk-sim-copilot-...`. Usarás esto en la configuración a continuación. - -### Cursor - -Agrega lo siguiente a tu `.cursor/mcp.json` (nivel de proyecto) o configuración global de MCP de Cursor: - -```json -{ - "mcpServers": { - "sim-copilot": { - "url": "https://www.sim.ai/api/mcp/copilot", - "headers": { - "X-API-Key": "YOUR_COPILOT_API_KEY" - } - } - } -} -``` - -Reemplaza `YOUR_COPILOT_API_KEY` con la clave que generaste anteriormente. - -### Claude Code - -Ejecuta el siguiente comando para añadir el servidor MCP de Copilot: - -```bash -claude mcp add sim-copilot \ - --transport http \ - https://www.sim.ai/api/mcp/copilot \ - --header "X-API-Key: YOUR_COPILOT_API_KEY" -``` - -Reemplaza `YOUR_COPILOT_API_KEY` con tu clave. - -### Claude Desktop - -Claude Desktop requiere [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) para conectarse a servidores MCP basados en HTTP. Añade lo siguiente a tu archivo de configuración de Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json` en macOS): - -```json -{ - "mcpServers": { - "sim-copilot": { - "command": "npx", - "args": [ - "-y", - "mcp-remote", - "https://www.sim.ai/api/mcp/copilot", - "--header", - "X-API-Key: YOUR_COPILOT_API_KEY" - ] - } - } -} -``` - -Reemplaza `YOUR_COPILOT_API_KEY` con tu clave. - -### VS Code - -Añade lo siguiente a tu `settings.json` de VS Code o al `.vscode/settings.json` del espacio de trabajo: - -```json -{ - "mcp": { - "servers": { - "sim-copilot": { - "type": "http", - "url": "https://www.sim.ai/api/mcp/copilot", - "headers": { - "X-API-Key": "YOUR_COPILOT_API_KEY" - } - } - } - } -} -``` - -Reemplaza `YOUR_COPILOT_API_KEY` con tu clave. - - - Para implementaciones auto-alojadas, reemplaza `https://www.sim.ai` con tu URL de Sim auto-alojada. - diff --git a/apps/docs/content/docs/fr/copilot/index.mdx b/apps/docs/content/docs/fr/copilot/index.mdx index b092b26f08b..53dc6c352c5 100644 --- a/apps/docs/content/docs/fr/copilot/index.mdx +++ b/apps/docs/content/docs/fr/copilot/index.mdx @@ -246,98 +246,3 @@ L'utilisation de Copilot est facturée par jeton du LLM sous-jacent. Si vous att Consultez la [page de calcul des coûts](/execution/costs) pour les détails de facturation. -## Copilot MCP - -Vous pouvez utiliser Copilot comme serveur MCP dans votre éditeur ou client IA préféré. Cela vous permet de créer, tester, déployer et gérer des workflows Sim directement depuis des outils comme Cursor, Claude Code, Claude Desktop et VS Code. - -### Générer une clé API Copilot - -Pour vous connecter au serveur MCP Copilot, vous avez besoin d'une **clé API Copilot** : - -1. Rendez-vous sur [sim.ai](https://sim.ai) et connectez-vous -2. Accédez à **Paramètres** → **Copilot** -3. Cliquez sur **Générer une clé API** -4. Copiez la clé — elle n'est affichée qu'une seule fois - -La clé ressemblera à `sk-sim-copilot-...`. Vous l'utiliserez dans la configuration ci-dessous. - -### Cursor - -Ajoutez ce qui suit à votre `.cursor/mcp.json` (niveau projet) ou aux paramètres MCP globaux de Cursor : - -```json -{ - "mcpServers": { - "sim-copilot": { - "url": "https://www.sim.ai/api/mcp/copilot", - "headers": { - "X-API-Key": "YOUR_COPILOT_API_KEY" - } - } - } -} -``` - -Remplacez `YOUR_COPILOT_API_KEY` par la clé que vous avez générée ci-dessus. - -### Claude Code - -Exécutez la commande suivante pour ajouter le serveur MCP Copilot : - -```bash -claude mcp add sim-copilot \ - --transport http \ - https://www.sim.ai/api/mcp/copilot \ - --header "X-API-Key: YOUR_COPILOT_API_KEY" -``` - -Remplacez `YOUR_COPILOT_API_KEY` par votre clé. - -### Claude Desktop - -Claude Desktop nécessite [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) pour se connecter aux serveurs MCP basés sur HTTP. Ajoutez ce qui suit à votre fichier de configuration Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json` sur macOS) : - -```json -{ - "mcpServers": { - "sim-copilot": { - "command": "npx", - "args": [ - "-y", - "mcp-remote", - "https://www.sim.ai/api/mcp/copilot", - "--header", - "X-API-Key: YOUR_COPILOT_API_KEY" - ] - } - } -} -``` - -Remplacez `YOUR_COPILOT_API_KEY` par votre clé. - -### VS Code - -Ajoutez ce qui suit à votre `settings.json` VS Code ou à votre `.vscode/settings.json` d'espace de travail : - -```json -{ - "mcp": { - "servers": { - "sim-copilot": { - "type": "http", - "url": "https://www.sim.ai/api/mcp/copilot", - "headers": { - "X-API-Key": "YOUR_COPILOT_API_KEY" - } - } - } - } -} -``` - -Remplacez `YOUR_COPILOT_API_KEY` par votre clé. - - - Pour les déploiements auto-hébergés, remplacez `https://www.sim.ai` par votre URL Sim auto-hébergée. - diff --git a/apps/docs/content/docs/ja/copilot/index.mdx b/apps/docs/content/docs/ja/copilot/index.mdx index 4714411d6b4..d38f9a42cb2 100644 --- a/apps/docs/content/docs/ja/copilot/index.mdx +++ b/apps/docs/content/docs/ja/copilot/index.mdx @@ -246,98 +246,3 @@ Copilotの使用量は、基盤となるLLMからのトークン単位で課金 課金の詳細については、[コスト計算ページ](/execution/costs)を参照してください。 -## Copilot MCP - -お気に入りのエディタやAIクライアントで、CopilotをMCPサーバーとして使用できます。これにより、Cursor、Claude Code、Claude Desktop、VS Codeなどのツールから直接、Simワークフローの構築、テスト、デプロイ、管理が可能になります。 - -### Copilot APIキーの生成 - -Copilot MCPサーバーに接続するには、**Copilot APIキー**が必要です。 - -1. [sim.ai](https://sim.ai)にアクセスしてサインイン -2. **設定** → **Copilot**に移動 -3. **APIキーを生成**をクリック -4. キーをコピー(一度のみ表示されます) - -キーは`sk-sim-copilot-...`のような形式です。以下の設定で使用します。 - -### Cursor - -`.cursor/mcp.json`(プロジェクトレベル)またはグローバルなCursor MCP設定に以下を追加してください。 - -```json -{ - "mcpServers": { - "sim-copilot": { - "url": "https://www.sim.ai/api/mcp/copilot", - "headers": { - "X-API-Key": "YOUR_COPILOT_API_KEY" - } - } - } -} -``` - -`YOUR_COPILOT_API_KEY`を上記で生成したキーに置き換えてください。 - -### Claude Code - -次のコマンドを実行してCopilot MCPサーバーを追加します: - -```bash -claude mcp add sim-copilot \ - --transport http \ - https://www.sim.ai/api/mcp/copilot \ - --header "X-API-Key: YOUR_COPILOT_API_KEY" -``` - -`YOUR_COPILOT_API_KEY`をあなたのキーに置き換えてください。 - -### Claude Desktop - -Claude DesktopはHTTPベースのMCPサーバーに接続するために[`mcp-remote`](https://www.npmjs.com/package/mcp-remote)が必要です。Claude Desktopの設定ファイル(macOSでは`~/Library/Application Support/Claude/claude_desktop_config.json`)に以下を追加してください: - -```json -{ - "mcpServers": { - "sim-copilot": { - "command": "npx", - "args": [ - "-y", - "mcp-remote", - "https://www.sim.ai/api/mcp/copilot", - "--header", - "X-API-Key: YOUR_COPILOT_API_KEY" - ] - } - } -} -``` - -`YOUR_COPILOT_API_KEY`をあなたのキーに置き換えてください。 - -### VS Code - -VS Codeの`settings.json`またはワークスペースの`.vscode/settings.json`に以下を追加してください: - -```json -{ - "mcp": { - "servers": { - "sim-copilot": { - "type": "http", - "url": "https://www.sim.ai/api/mcp/copilot", - "headers": { - "X-API-Key": "YOUR_COPILOT_API_KEY" - } - } - } - } -} -``` - -`YOUR_COPILOT_API_KEY`をあなたのキーに置き換えてください。 - - - セルフホスト型デプロイメントの場合は、`https://www.sim.ai`をあなたのセルフホスト型SimのURLに置き換えてください。 - diff --git a/apps/docs/content/docs/zh/copilot/index.mdx b/apps/docs/content/docs/zh/copilot/index.mdx index b3b54d134c6..b37b1cdc6dc 100644 --- a/apps/docs/content/docs/zh/copilot/index.mdx +++ b/apps/docs/content/docs/zh/copilot/index.mdx @@ -246,98 +246,3 @@ Copilot 的使用按底层 LLM 的 token 计费。如果达到使用上限,Cop 计费详情请参见 [成本计算页面](/execution/costs)。 -## Copilot MCP - -你可以在常用编辑器或 AI 客户端中将 Copilot 作为 MCP 服务器使用。这样可以直接在 Cursor、Claude Code、Claude Desktop 和 VS Code 等工具中构建、测试、部署和管理 Sim 工作流。 - -### 生成 Copilot API 密钥 - -要连接 Copilot MCP 服务器,你需要一个 **Copilot API 密钥**: - -1. 访问 [sim.ai](https://sim.ai) 并登录 -2. 进入 **设置** → **Copilot** -3. 点击 **生成 API 密钥** -4. 复制密钥 — 该密钥只显示一次 - -密钥类似于 `sk-sim-copilot-...`。你将在下方配置中用到它。 - -### Cursor - -将以下内容添加到你的 `.cursor/mcp.json`(项目级)或全局 Cursor MCP 设置中: - -```json -{ - "mcpServers": { - "sim-copilot": { - "url": "https://www.sim.ai/api/mcp/copilot", - "headers": { - "X-API-Key": "YOUR_COPILOT_API_KEY" - } - } - } -} -``` - -将 `YOUR_COPILOT_API_KEY` 替换为你上面生成的密钥。 - -### Claude Code - -运行以下命令以添加 Copilot MCP 服务器: - -```bash -claude mcp add sim-copilot \ - --transport http \ - https://www.sim.ai/api/mcp/copilot \ - --header "X-API-Key: YOUR_COPILOT_API_KEY" -``` - -将 `YOUR_COPILOT_API_KEY` 替换为你的密钥。 - -### Claude Desktop - -Claude Desktop 需要 [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) 以连接基于 HTTP 的 MCP 服务器。请将以下内容添加到你的 Claude Desktop 配置文件(macOS 上为 `~/Library/Application Support/Claude/claude_desktop_config.json`): - -```json -{ - "mcpServers": { - "sim-copilot": { - "command": "npx", - "args": [ - "-y", - "mcp-remote", - "https://www.sim.ai/api/mcp/copilot", - "--header", - "X-API-Key: YOUR_COPILOT_API_KEY" - ] - } - } -} -``` - -将 `YOUR_COPILOT_API_KEY` 替换为你的密钥。 - -### VS Code - -请将以下内容添加到你的 VS Code `settings.json` 或工作区 `.vscode/settings.json`: - -```json -{ - "mcp": { - "servers": { - "sim-copilot": { - "type": "http", - "url": "https://www.sim.ai/api/mcp/copilot", - "headers": { - "X-API-Key": "YOUR_COPILOT_API_KEY" - } - } - } - } -} -``` - -将 `YOUR_COPILOT_API_KEY` 替换为你的密钥。 - - - 对于自托管部署,请将 `https://www.sim.ai` 替换为你的自托管 Sim URL。 - diff --git a/apps/sim/app/.well-known/oauth-authorization-server/api/mcp/copilot/route.ts b/apps/sim/app/.well-known/oauth-authorization-server/api/mcp/copilot/route.ts index d862fe277f1..55d1b90a958 100644 --- a/apps/sim/app/.well-known/oauth-authorization-server/api/mcp/copilot/route.ts +++ b/apps/sim/app/.well-known/oauth-authorization-server/api/mcp/copilot/route.ts @@ -1,6 +1,6 @@ import type { NextResponse } from 'next/server' -import { createMcpAuthorizationServerMetadataResponse } from '@/lib/mcp/oauth-discovery' +import { copilotMcpDeprecatedResponse } from '@/lib/mcp/copilot-deprecated' export async function GET(): Promise { - return createMcpAuthorizationServerMetadataResponse() + return copilotMcpDeprecatedResponse() } diff --git a/apps/sim/app/.well-known/oauth-protected-resource/api/mcp/copilot/route.ts b/apps/sim/app/.well-known/oauth-protected-resource/api/mcp/copilot/route.ts index a419ebda324..55d1b90a958 100644 --- a/apps/sim/app/.well-known/oauth-protected-resource/api/mcp/copilot/route.ts +++ b/apps/sim/app/.well-known/oauth-protected-resource/api/mcp/copilot/route.ts @@ -1,6 +1,6 @@ import type { NextResponse } from 'next/server' -import { createMcpProtectedResourceMetadataResponse } from '@/lib/mcp/oauth-discovery' +import { copilotMcpDeprecatedResponse } from '@/lib/mcp/copilot-deprecated' export async function GET(): Promise { - return createMcpProtectedResourceMetadataResponse() + return copilotMcpDeprecatedResponse() } diff --git a/apps/sim/app/api/mcp/copilot/.well-known/oauth-authorization-server/route.ts b/apps/sim/app/api/mcp/copilot/.well-known/oauth-authorization-server/route.ts index 3f2976ff027..c54b72dfcb8 100644 --- a/apps/sim/app/api/mcp/copilot/.well-known/oauth-authorization-server/route.ts +++ b/apps/sim/app/api/mcp/copilot/.well-known/oauth-authorization-server/route.ts @@ -1,12 +1,4 @@ -import type { NextRequest, NextResponse } from 'next/server' -import { mcpOauthAuthorizationServerMetadataContract } from '@/lib/api/contracts/mcp-oauth' -import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { createMcpAuthorizationServerMetadataResponse } from '@/lib/mcp/oauth-discovery' +import { copilotMcpDeprecatedResponse } from '@/lib/mcp/copilot-deprecated' -export const GET = withRouteHandler(async (request: NextRequest): Promise => { - const parsed = await parseRequest(mcpOauthAuthorizationServerMetadataContract, request, {}) - if (!parsed.success) return parsed.response as NextResponse - - return createMcpAuthorizationServerMetadataResponse() -}) +export const GET = withRouteHandler(async () => copilotMcpDeprecatedResponse()) diff --git a/apps/sim/app/api/mcp/copilot/.well-known/oauth-protected-resource/route.ts b/apps/sim/app/api/mcp/copilot/.well-known/oauth-protected-resource/route.ts index 1e17b126b31..c54b72dfcb8 100644 --- a/apps/sim/app/api/mcp/copilot/.well-known/oauth-protected-resource/route.ts +++ b/apps/sim/app/api/mcp/copilot/.well-known/oauth-protected-resource/route.ts @@ -1,12 +1,4 @@ -import type { NextRequest, NextResponse } from 'next/server' -import { mcpOauthProtectedResourceMetadataContract } from '@/lib/api/contracts/mcp-oauth' -import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { createMcpProtectedResourceMetadataResponse } from '@/lib/mcp/oauth-discovery' +import { copilotMcpDeprecatedResponse } from '@/lib/mcp/copilot-deprecated' -export const GET = withRouteHandler(async (request: NextRequest): Promise => { - const parsed = await parseRequest(mcpOauthProtectedResourceMetadataContract, request, {}) - if (!parsed.success) return parsed.response as NextResponse - - return createMcpProtectedResourceMetadataResponse() -}) +export const GET = withRouteHandler(async () => copilotMcpDeprecatedResponse()) diff --git a/apps/sim/app/api/mcp/copilot/route.test.ts b/apps/sim/app/api/mcp/copilot/route.test.ts new file mode 100644 index 00000000000..7db9860fbce --- /dev/null +++ b/apps/sim/app/api/mcp/copilot/route.test.ts @@ -0,0 +1,62 @@ +/** + * Tests for the deprecated Copilot MCP route + * + * @vitest-environment node + */ +import { NextRequest } from 'next/server' +import { describe, expect, it } from 'vitest' +import { GET as authServerDiscoveryGET } from '@/app/api/mcp/copilot/.well-known/oauth-authorization-server/route' +import { GET as protectedResourceDiscoveryGET } from '@/app/api/mcp/copilot/.well-known/oauth-protected-resource/route' +import { DELETE, GET, POST } from '@/app/api/mcp/copilot/route' + +const URL = 'http://localhost:3000/api/mcp/copilot' + +describe('Deprecated Copilot MCP route', () => { + it('GET returns 410', async () => { + const response = await GET(new NextRequest(URL)) + expect(response.status).toBe(410) + }) + + it('POST returns 410 with a JSON-RPC error envelope', async () => { + const request = new NextRequest(URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize' }), + }) + const response = await POST(request) + expect(response.status).toBe(410) + + const body = (await response.json()) as { jsonrpc?: string; error?: { message?: string } } + expect(body.jsonrpc).toBe('2.0') + expect(body.error?.message).toContain('deprecated') + }) + + it('POST still returns 410 when an x-api-key header is present', async () => { + const request = new NextRequest(URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-api-key': 'sk-sim-copilot-test' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }), + }) + const response = await POST(request) + expect(response.status).toBe(410) + }) + + it('DELETE returns 410', async () => { + const response = await DELETE(new NextRequest(URL, { method: 'DELETE' })) + expect(response.status).toBe(410) + }) + + it('copilot OAuth authorization-server discovery returns 410', async () => { + const response = await authServerDiscoveryGET( + new NextRequest(`${URL}/.well-known/oauth-authorization-server`) + ) + expect(response.status).toBe(410) + }) + + it('copilot OAuth protected-resource discovery returns 410', async () => { + const response = await protectedResourceDiscoveryGET( + new NextRequest(`${URL}/.well-known/oauth-protected-resource`) + ) + expect(response.status).toBe(410) + }) +}) diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index 2bda0f3670f..6ec1475d2ef 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -1,740 +1,13 @@ -import { Server } from '@modelcontextprotocol/sdk/server/index.js' -import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js' -import { - CallToolRequestSchema, - type CallToolResult, - ErrorCode, - type JSONRPCError, - ListToolsRequestSchema, - type ListToolsResult, - McpError, - type RequestId, -} from '@modelcontextprotocol/sdk/types.js' -import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' -import { generateId } from '@sim/utils/id' -import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' -import { type NextRequest, NextResponse } from 'next/server' -import { mcpRequestBodySchema, mcpToolCallParamsSchema } from '@/lib/api/contracts/mcp' -import { validateOAuthAccessToken } from '@/lib/auth/oauth-token' -import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' -import { generateWorkspaceContext } from '@/lib/copilot/chat/workspace-context' -import { ORCHESTRATION_TIMEOUT_MS, SIM_AGENT_API_URL } from '@/lib/copilot/constants' -import { createRequestId } from '@/lib/copilot/request/http' -import { runHeadlessCopilotLifecycle } from '@/lib/copilot/request/lifecycle/headless' -import { orchestrateSubagentStream } from '@/lib/copilot/request/subagent' -import { ensureHandlersRegistered, executeTool } from '@/lib/copilot/tool-executor' -import { ensureWorkspaceAccess } from '@/lib/copilot/tools/handlers/access' -import { prepareExecutionContext } from '@/lib/copilot/tools/handlers/context' -import { DIRECT_TOOL_DEFS, SUBAGENT_TOOL_DEFS } from '@/lib/copilot/tools/mcp/definitions' -import { env } from '@/lib/core/config/env' -import { RateLimiter } from '@/lib/core/rate-limiter' -import { getBaseUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { resolveWorkflowIdForUser } from '@/lib/workflows/utils' - -const logger = createLogger('CopilotMcpAPI') -const mcpRateLimiter = new RateLimiter() -const DEFAULT_COPILOT_MODEL = 'claude-opus-4-6' +import { + copilotMcpDeprecatedJsonRpcResponse, + copilotMcpDeprecatedResponse, +} from '@/lib/mcp/copilot-deprecated' export const dynamic = 'force-dynamic' -export const runtime = 'nodejs' -export const maxDuration = 3600 - -interface CopilotKeyAuthResult { - success: boolean - userId?: string - error?: string -} - -/** - * Validates a copilot API key by forwarding it to the Go copilot service's - * `/api/validate-key` endpoint. Returns the associated userId on success. - */ -async function authenticateCopilotApiKey(apiKey: string): Promise { - try { - const internalSecret = env.INTERNAL_API_SECRET - if (!internalSecret) { - logger.error('INTERNAL_API_SECRET not configured') - return { success: false, error: 'Server configuration error' } - } - - const { fetchGo } = await import('@/lib/copilot/request/go/fetch') - const res = await fetchGo(`${SIM_AGENT_API_URL}/api/validate-key`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': internalSecret, - }, - body: JSON.stringify({ targetApiKey: apiKey }), - signal: AbortSignal.timeout(10_000), - spanName: 'sim → go /api/validate-key (mcp)', - operation: 'mcp_validate_key', - }) - - if (!res.ok) { - const body = await res.json().catch(() => null) - const upstream = (body as Record)?.message - const status = res.status - - if (status === 401 || status === 403) { - return { - success: false, - error: `Invalid Copilot API key. Generate a new key in Settings → Copilot and set it in the x-api-key header.`, - } - } - if (status === 402) { - return { - success: false, - error: `Usage limit exceeded for this Copilot API key. Upgrade your plan or wait for your quota to reset.`, - } - } - - return { - success: false, - error: String(upstream ?? 'Copilot API key validation failed'), - } - } - - const data = (await res.json()) as { ok?: boolean; userId?: string } - if (!data.ok || !data.userId) { - return { - success: false, - error: 'Invalid Copilot API key. Generate a new key in Settings → Copilot.', - } - } - - return { success: true, userId: data.userId } - } catch (error) { - logger.error('Copilot API key validation failed', { error }) - return { - success: false, - error: - 'Could not validate Copilot API key — the authentication service is temporarily unreachable. This is NOT a problem with the API key itself; please retry shortly.', - } - } -} - -/** - * MCP Server instructions that guide LLMs on how to use the Sim copilot tools. - * This is included in the initialize response to help external LLMs understand - * the workflow lifecycle and best practices. - */ -const MCP_SERVER_INSTRUCTIONS = ` -## Sim Workflow Copilot - -Sim is a workflow automation platform. Workflows are visual pipelines of connected blocks (Agent, Function, Condition, API, integrations, etc.). The Agent block is the core — an LLM with tools, memory, structured output, and knowledge bases. - -### Workflow Lifecycle (Happy Path) - -1. \`list_workspaces\` → know where to work -2. \`create_workflow(name, workspaceId)\` → get a workflowId -3. \`sim_workflow(request, workflowId)\` → plan and build in one pass -4. \`sim_test(request, workflowId)\` → verify it works -5. \`sim_deploy("deploy as api", workflowId)\` → make it accessible externally (optional) - -### Working with Existing Workflows - -When the user refers to a workflow by name or description ("the email one", "my Slack bot"): -1. Use \`sim_discovery\` to find it by functionality -2. Or use \`list_workflows\` and match by name -3. Then pass the workflowId to other tools - -### Organization - -- \`rename_workflow\` — rename a workflow -- \`move_workflow\` — move a workflow into a folder (or back to root by clearing the folder id) -- \`move_folder\` — nest a folder inside another (or move it back to root by clearing the parent id) -- \`create_folder(name, parentId)\` — create nested folder hierarchies - -### Key Rules - -- You can test workflows immediately after building — deployment is only needed for external access (API, chat, MCP). -- Tools that operate on a specific workflow such as \`sim_workflow\`, \`sim_test\`, \`sim_deploy\`, and workflow-scoped \`sim_info\` requests require \`workflowId\`. -- If the user reports errors, route through \`sim_workflow\` and ask it to reproduce, inspect logs, and fix the issue end to end. -- Variable syntax: \`\` for block outputs, \`{{ENV_VAR}}\` for env vars. -` - -type HeaderMap = Record - -function createError(id: RequestId, code: ErrorCode | number, message: string): JSONRPCError { - return { - jsonrpc: '2.0', - id, - error: { code, message }, - } -} - -function readHeader(headers: HeaderMap | undefined, name: string): string | undefined { - if (!headers) return undefined - const value = headers[name.toLowerCase()] - if (Array.isArray(value)) { - return value[0] - } - return value -} - -function buildMcpServer(abortSignal?: AbortSignal): Server { - const server = new Server( - { - name: 'sim-copilot', - version: '1.0.0', - }, - { - capabilities: { tools: {} }, - instructions: MCP_SERVER_INSTRUCTIONS, - } - ) - - server.setRequestHandler(ListToolsRequestSchema, async () => { - const directTools = DIRECT_TOOL_DEFS.map((tool) => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - ...(tool.annotations && { annotations: tool.annotations }), - })) - - const subagentTools = SUBAGENT_TOOL_DEFS.map((tool) => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - ...(tool.annotations && { annotations: tool.annotations }), - })) - - const result: ListToolsResult = { - tools: [...directTools, ...subagentTools], - } - - return result - }) - - server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { - const headers = (extra.requestInfo?.headers || {}) as HeaderMap - const apiKeyHeader = readHeader(headers, 'x-api-key') - const authorizationHeader = readHeader(headers, 'authorization') - - let authResult: CopilotKeyAuthResult = { success: false } - - if (authorizationHeader?.startsWith('Bearer ')) { - const token = authorizationHeader.slice(7) - const oauthResult = await validateOAuthAccessToken(token) - if (oauthResult.success && oauthResult.userId) { - if (!oauthResult.scopes?.includes('mcp:tools')) { - return { - content: [ - { - type: 'text' as const, - text: 'AUTHENTICATION ERROR: OAuth token is missing the required "mcp:tools" scope. Re-authorize with the correct scopes.', - }, - ], - isError: true, - } - } - authResult = { success: true, userId: oauthResult.userId } - } else { - return { - content: [ - { - type: 'text' as const, - text: `AUTHENTICATION ERROR: ${oauthResult.error ?? 'Invalid OAuth access token'} Do NOT retry — re-authorize via OAuth.`, - }, - ], - isError: true, - } - } - } else if (apiKeyHeader) { - authResult = await authenticateCopilotApiKey(apiKeyHeader) - } - - if (!authResult.success || !authResult.userId) { - const errorMsg = apiKeyHeader - ? `AUTHENTICATION ERROR: ${authResult.error} Do NOT retry — this will fail until the user fixes their Copilot API key.` - : 'AUTHENTICATION ERROR: No authentication provided. Provide a Bearer token (OAuth 2.1) or an x-api-key header. Generate a Copilot API key in Settings → Copilot.' - logger.warn('MCP copilot auth failed', { method: request.method }) - return { - content: [ - { - type: 'text' as const, - text: errorMsg, - }, - ], - isError: true, - } - } - - const rateLimitResult = await mcpRateLimiter.checkRateLimitWithSubscription( - authResult.userId, - await getHighestPrioritySubscription(authResult.userId), - 'api-endpoint', - false - ) - - if (!rateLimitResult.allowed) { - return { - content: [ - { - type: 'text' as const, - text: `RATE LIMIT: Too many requests. Please wait and retry after ${rateLimitResult.resetAt.toISOString()}.`, - }, - ], - isError: true, - } - } - - const paramsValidation = mcpToolCallParamsSchema.safeParse(request.params) - if (!paramsValidation.success) { - throw new McpError(ErrorCode.InvalidParams, 'Tool name required') - } - const params = paramsValidation.data - - const result = await handleToolsCall( - { - name: params.name, - arguments: params.arguments, - }, - authResult.userId, - abortSignal - ) - - return result - }) - - return server -} - -async function handleMcpRequestWithSdk( - request: NextRequest, - parsedBody: unknown -): Promise { - const server = buildMcpServer(request.signal) - const transport = new WebStandardStreamableHTTPServerTransport({ - sessionIdGenerator: undefined, - enableJsonResponse: true, - }) - - await server.connect(transport) - - try { - return await transport.handleRequest(request, { parsedBody }) - } finally { - await server.close().catch(() => {}) - await transport.close().catch(() => {}) - } -} - -export const GET = withRouteHandler(async () => { - // Return 405 to signal that server-initiated SSE notifications are not - // supported. Without this, clients like mcp-remote will repeatedly - // reconnect trying to open an SSE stream, flooding the logs with GETs. - return new NextResponse(null, { status: 405 }) -}) - -export const POST = withRouteHandler(async (request: NextRequest) => { - const hasAuth = request.headers.has('authorization') || request.headers.has('x-api-key') - - if (!hasAuth) { - const origin = getBaseUrl().replace(/\/$/, '') - const resourceMetadataUrl = `${origin}/.well-known/oauth-protected-resource/api/mcp/copilot` - return new NextResponse(JSON.stringify({ error: 'unauthorized' }), { - status: 401, - headers: { - 'WWW-Authenticate': `Bearer resource_metadata="${resourceMetadataUrl}", scope="mcp:tools"`, - 'Content-Type': 'application/json', - }, - }) - } - - try { - let parsedBody: unknown - - try { - parsedBody = await request.json() - } catch { - return NextResponse.json(createError(0, ErrorCode.ParseError, 'Invalid JSON body'), { - status: 400, - }) - } - - const bodyValidation = mcpRequestBodySchema.safeParse(parsedBody) - if (!bodyValidation.success) { - return NextResponse.json( - createError(0, ErrorCode.InvalidRequest, 'Invalid JSON-RPC message'), - { - status: 400, - } - ) - } - - return await handleMcpRequestWithSdk(request, bodyValidation.data) - } catch (error) { - if (request.signal.aborted || (error as Error)?.name === 'AbortError') { - return NextResponse.json( - createError(0, ErrorCode.ConnectionClosed, 'Client cancelled request'), - { status: 499 } - ) - } - - logger.error('Error handling MCP request', { error }) - return NextResponse.json(createError(0, ErrorCode.InternalError, 'Internal error'), { - status: 500, - }) - } -}) - -export const DELETE = withRouteHandler(async (request: NextRequest) => { - void request - return NextResponse.json(createError(0, -32000, 'Method not allowed.'), { status: 405 }) -}) - -async function handleToolsCall( - params: { name: string; arguments?: Record }, - userId: string, - abortSignal?: AbortSignal -): Promise { - const args = params.arguments || {} - - const directTool = DIRECT_TOOL_DEFS.find((tool) => tool.name === params.name) - if (directTool) { - return handleDirectToolCall(directTool, args, userId) - } - - const subagentTool = SUBAGENT_TOOL_DEFS.find((tool) => tool.name === params.name) - if (subagentTool) { - return handleSubagentToolCall(subagentTool, args, userId, abortSignal) - } - - throw new McpError(ErrorCode.MethodNotFound, `Tool not found: ${params.name}`) -} - -async function handleDirectToolCall( - toolDef: (typeof DIRECT_TOOL_DEFS)[number], - args: Record, - userId: string -): Promise { - try { - const rawWorkflowId = (args.workflowId as string) || '' - let resolvedWorkspaceId: string | undefined - if (rawWorkflowId) { - const authorization = await authorizeWorkflowByWorkspacePermission({ - workflowId: rawWorkflowId, - userId, - action: 'read', - }) - if (!authorization.allowed) { - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { success: false, error: 'Workflow not found or access denied' }, - null, - 2 - ), - }, - ], - isError: true, - } - } - resolvedWorkspaceId = authorization.workflow?.workspaceId || undefined - } - const execContext = await prepareExecutionContext( - userId, - rawWorkflowId, - (args.chatId as string) || undefined, - { workspaceId: resolvedWorkspaceId } - ) - - const toolCall = { - id: generateId(), - name: toolDef.toolId, - status: 'pending' as const, - params: args as Record, - startTime: Date.now(), - } - - ensureHandlersRegistered() - const result = await executeTool(toolCall.name, toolCall.params || {}, execContext) - - return { - content: [ - { - type: 'text', - text: JSON.stringify(result.output ?? result, null, 2), - }, - ], - isError: !result.success, - } - } catch (error) { - logger.error('Direct tool execution failed', { tool: toolDef.name, error }) - return { - content: [ - { - type: 'text', - text: `Tool execution failed: ${toError(error).message}`, - }, - ], - isError: true, - } - } -} - -/** - * Build mode uses the main /api/mcp orchestrator instead of /api/subagent/workflow. - * The main agent still delegates workflow work to the workflow subagent inside Go; - * this helper simply uses the full headless lifecycle so build requests behave like - * the primary MCP chat flow. - */ -async function handleBuildToolCall( - args: Record, - userId: string, - abortSignal?: AbortSignal -): Promise { - try { - const requestText = (args.request as string) || JSON.stringify(args) - const workflowId = args.workflowId as string | undefined - let resolvedWorkflowName: string | undefined - let resolvedWorkspaceId: string | undefined - - const resolved = workflowId - ? await (async () => { - const authorization = await authorizeWorkflowByWorkspacePermission({ - workflowId, - userId, - action: 'read', - }) - resolvedWorkflowName = authorization.workflow?.name || undefined - resolvedWorkspaceId = authorization.workflow?.workspaceId || undefined - return authorization.allowed - ? { - status: 'resolved' as const, - workflowId, - workflowName: resolvedWorkflowName, - } - : { - status: 'not_found' as const, - message: 'workflowId is required for build. Call create_workflow first.', - } - })() - : await resolveWorkflowIdForUser(userId) - - if (resolved.status === 'resolved') { - resolvedWorkflowName ||= resolved.workflowName - } - - if (!resolved || resolved.status !== 'resolved') { - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - success: false, - error: - resolved?.message ?? - 'workflowId is required for build. Call create_workflow first.', - }, - null, - 2 - ), - }, - ], - isError: true, - } - } - - const chatId = generateId() - const executionContext = await prepareExecutionContext(userId, resolved.workflowId, chatId, { - workspaceId: resolvedWorkspaceId, - }) - resolvedWorkspaceId = executionContext.workspaceId - let workspaceContext: string | undefined - if (resolvedWorkspaceId) { - try { - workspaceContext = await generateWorkspaceContext(resolvedWorkspaceId, userId) - } catch (error) { - logger.warn('Failed to generate workspace context for build tool call', { - workflowId: resolved.workflowId, - workspaceId: resolvedWorkspaceId, - error: toError(error).message, - }) - } - } - - const requestPayload = { - message: requestText, - workflowId: resolved.workflowId, - ...(resolvedWorkflowName ? { workflowName: resolvedWorkflowName } : {}), - ...(resolvedWorkspaceId ? { workspaceId: resolvedWorkspaceId } : {}), - ...(workspaceContext ? { workspaceContext } : {}), - userId, - model: DEFAULT_COPILOT_MODEL, - mode: 'agent', - commands: ['fast'], - messageId: generateId(), - chatId, - } - - const result = await runHeadlessCopilotLifecycle(requestPayload, { - userId, - workflowId: resolved.workflowId, - workspaceId: resolvedWorkspaceId, - chatId, - goRoute: '/api/mcp', - executionContext, - autoExecuteTools: true, - timeout: ORCHESTRATION_TIMEOUT_MS, - interactive: false, - abortSignal, - }) - - const responseData = { - success: result.success, - content: result.content, - toolCalls: result.toolCalls, - error: result.error, - } - - return { - content: [{ type: 'text', text: JSON.stringify(responseData, null, 2) }], - isError: !result.success, - } - } catch (error) { - logger.error('Build tool call failed', { error }) - return { - content: [ - { - type: 'text', - text: `Build failed: ${toError(error).message}`, - }, - ], - isError: true, - } - } -} - -async function handleSubagentToolCall( - toolDef: (typeof SUBAGENT_TOOL_DEFS)[number], - args: Record, - userId: string, - abortSignal?: AbortSignal -): Promise { - if (toolDef.agentId === 'workflow') { - return handleBuildToolCall(args, userId, abortSignal) - } - - try { - const requestText = - (args.request as string) || - (args.message as string) || - (args.error as string) || - JSON.stringify(args) - const simRequestId = createRequestId() - - const context = (args.context as Record) || {} - if (args.plan && !context.plan) { - context.plan = args.plan - } - - // Authorize user-supplied workflowId / workspaceId before forwarding downstream - const rawWorkflowId = args.workflowId as string | undefined - const rawWorkspaceId = args.workspaceId as string | undefined - let resolvedWorkflowId: string | undefined - let resolvedWorkspaceId: string | undefined - - if (rawWorkflowId) { - const authorization = await authorizeWorkflowByWorkspacePermission({ - workflowId: rawWorkflowId, - userId, - action: 'read', - }) - if (!authorization.allowed) { - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { success: false, error: 'Workflow not found or access denied' }, - null, - 2 - ), - }, - ], - isError: true, - } - } - resolvedWorkflowId = rawWorkflowId - resolvedWorkspaceId = authorization.workflow?.workspaceId || undefined - } else if (rawWorkspaceId) { - await ensureWorkspaceAccess(rawWorkspaceId, userId, 'read') - resolvedWorkspaceId = rawWorkspaceId - } - - const result = await orchestrateSubagentStream( - toolDef.agentId, - { - message: requestText, - workflowId: resolvedWorkflowId, - workspaceId: resolvedWorkspaceId, - context, - model: DEFAULT_COPILOT_MODEL, - headless: true, - source: 'mcp', - }, - { - userId, - workflowId: resolvedWorkflowId, - workspaceId: resolvedWorkspaceId, - simRequestId, - abortSignal, - } - ) - - let responseData: unknown - if (result.structuredResult) { - responseData = { - success: result.structuredResult.success ?? result.success, - type: result.structuredResult.type, - summary: result.structuredResult.summary, - data: result.structuredResult.data, - } - } else if (result.error) { - responseData = { - success: false, - error: result.error, - errors: result.errors, - } - } else { - responseData = { - success: result.success, - content: result.content, - } - } +export const GET = withRouteHandler(async () => copilotMcpDeprecatedResponse()) - return { - content: [ - { - type: 'text', - text: JSON.stringify(responseData, null, 2), - }, - ], - isError: !result.success, - } - } catch (error) { - logger.error('Subagent tool call failed', { - tool: toolDef.name, - agentId: toolDef.agentId, - error, - }) +export const POST = withRouteHandler(async () => copilotMcpDeprecatedJsonRpcResponse()) - return { - content: [ - { - type: 'text', - text: `Subagent call failed: ${toError(error).message}`, - }, - ], - isError: true, - } - } -} +export const DELETE = withRouteHandler(async () => copilotMcpDeprecatedJsonRpcResponse()) diff --git a/apps/sim/app/api/v1/copilot/chat/route.test.ts b/apps/sim/app/api/v1/copilot/chat/route.test.ts new file mode 100644 index 00000000000..fef0a7eec88 --- /dev/null +++ b/apps/sim/app/api/v1/copilot/chat/route.test.ts @@ -0,0 +1,26 @@ +/** + * Tests for the deprecated v1 copilot chat API route + * + * @vitest-environment node + */ +import { NextRequest } from 'next/server' +import { describe, expect, it } from 'vitest' +import { POST } from '@/app/api/v1/copilot/chat/route' + +const URL = 'http://localhost:3000/api/v1/copilot/chat' + +describe('Deprecated v1 copilot chat route', () => { + it('POST returns 410 with a success:false error body', async () => { + const request = new NextRequest(URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-api-key': 'sk-test' }, + body: JSON.stringify({ message: 'hello' }), + }) + const response = await POST(request) + expect(response.status).toBe(410) + + const body = (await response.json()) as { success?: boolean; error?: string } + expect(body.success).toBe(false) + expect(body.error).toContain('deprecated') + }) +}) diff --git a/apps/sim/app/api/v1/copilot/chat/route.ts b/apps/sim/app/api/v1/copilot/chat/route.ts index 6eaa1424b77..ac3759d96fc 100644 --- a/apps/sim/app/api/v1/copilot/chat/route.ts +++ b/apps/sim/app/api/v1/copilot/chat/route.ts @@ -1,151 +1,18 @@ -import { createLogger } from '@sim/logger' -import { toError } from '@sim/utils/errors' -import { generateId } from '@sim/utils/id' -import { type NextRequest, NextResponse } from 'next/server' -import { v1CopilotChatContract } from '@/lib/api/contracts/v1/copilot' -import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' -import { runHeadlessCopilotLifecycle } from '@/lib/copilot/request/lifecycle/headless' +import { NextResponse } from 'next/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getWorkflowById, resolveWorkflowIdForUser } from '@/lib/workflows/utils' -import { authenticateRequest } from '@/app/api/v1/middleware' - -export const maxDuration = 3600 - -const logger = createLogger('CopilotHeadlessAPI') -const DEFAULT_COPILOT_MODEL = 'claude-opus-4-6' /** * POST /api/v1/copilot/chat - * Headless copilot endpoint for server-side orchestration. * - * workflowId is optional - if not provided: - * - If workflowName is provided, finds that workflow - * - If exactly one workflow is available, uses that workflow as context - * - Otherwise requires workflowId or workflowName to disambiguate + * Deprecated: the v1 headless copilot chat API has been removed. The endpoint + * returns 410 Gone for all callers. */ -export const POST = withRouteHandler(async (req: NextRequest) => { - let messageId: string | undefined - const authorized = await authenticateRequest(req, 'copilot-chat') - if (authorized instanceof NextResponse) { - return authorized - } - const { userId, rateLimit } = authorized - const auth = { - authenticated: true as const, - userId, - keyType: rateLimit.keyType, - workspaceId: rateLimit.workspaceId, - } - - try { - const parsedRequest = await parseRequest( - v1CopilotChatContract, - req, - {}, - { - validationErrorResponse: (error) => - NextResponse.json( - { - success: false, - error: getValidationErrorMessage(error, 'Invalid request'), - details: error.issues, - }, - { status: 400 } - ), - invalidJsonResponse: () => - NextResponse.json({ success: false, error: 'Invalid request' }, { status: 400 }), - } - ) - if (!parsedRequest.success) return parsedRequest.response - - const parsed = parsedRequest.data.body - const selectedModel = parsed.model || DEFAULT_COPILOT_MODEL - - // Resolve workflow ID - const resolved = await resolveWorkflowIdForUser( - auth.userId, - parsed.workflowId, - parsed.workflowName, - auth.keyType === 'workspace' ? auth.workspaceId : undefined - ) - if (resolved.status !== 'resolved') { - return NextResponse.json( - { - success: false, - error: resolved.message, - }, - { status: 400 } - ) - } - - if (auth.keyType === 'workspace' && auth.workspaceId) { - const workflow = await getWorkflowById(resolved.workflowId) - if (!workflow?.workspaceId || workflow.workspaceId !== auth.workspaceId) { - return NextResponse.json( - { success: false, error: 'API key is not authorized for this workspace' }, - { status: 403 } - ) - } - } - - // Transform mode to transport mode (same as client API) - // build and agent both map to 'agent' on the backend - const effectiveMode = parsed.mode === 'agent' ? 'build' : parsed.mode - const transportMode = effectiveMode === 'build' ? 'agent' : effectiveMode - - // Always generate a chatId - required for artifacts system to work with subagents - const chatId = parsed.chatId || generateId() - - messageId = generateId() - logger.info( - messageId - ? `Received headless copilot chat start request [messageId:${messageId}]` - : 'Received headless copilot chat start request', - { - workflowId: resolved.workflowId, - workflowName: parsed.workflowName, - chatId, - mode: transportMode, - autoExecuteTools: parsed.autoExecuteTools, - timeout: parsed.timeout, - } - ) - const requestPayload = { - message: parsed.message, - workflowId: resolved.workflowId, - userId: auth.userId, - model: selectedModel, - mode: transportMode, - messageId, - chatId, - } - - const result = await runHeadlessCopilotLifecycle(requestPayload, { - userId: auth.userId, - workflowId: resolved.workflowId, - chatId, - goRoute: '/api/mcp', - autoExecuteTools: parsed.autoExecuteTools, - timeout: parsed.timeout, - interactive: false, - }) - - return NextResponse.json({ - success: result.success, - content: result.content, - toolCalls: result.toolCalls, - chatId: result.chatId || chatId, - error: result.error, - }) - } catch (error) { - logger.error( - messageId - ? `Headless copilot request failed [messageId:${messageId}]` - : 'Headless copilot request failed', - { - error: toError(error).message, - } - ) - return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 }) - } -}) +export const POST = withRouteHandler(async () => + NextResponse.json( + { + success: false, + error: 'The v1 copilot chat API has been deprecated and is no longer available.', + }, + { status: 410, headers: { 'Cache-Control': 'no-store' } } + ) +) diff --git a/apps/sim/lib/mcp/copilot-deprecated.ts b/apps/sim/lib/mcp/copilot-deprecated.ts new file mode 100644 index 00000000000..a3be7e7ebe6 --- /dev/null +++ b/apps/sim/lib/mcp/copilot-deprecated.ts @@ -0,0 +1,35 @@ +import { NextResponse } from 'next/server' + +const DEPRECATION_MESSAGE = 'Copilot MCP has been deprecated and is no longer available.' + +/** + * Standard 410 response for the deprecated Copilot MCP surface. Used by the + * copilot MCP resource route and its copilot-specific OAuth discovery routes. + */ +export function copilotMcpDeprecatedResponse(): NextResponse { + return NextResponse.json( + { error: 'gone', message: DEPRECATION_MESSAGE }, + { + status: 410, + headers: { 'Cache-Control': 'no-store' }, + } + ) +} + +/** + * JSON-RPC flavored 410 response for the deprecated Copilot MCP `POST` endpoint, + * so MCP clients surface a clean error envelope instead of an opaque body. + */ +export function copilotMcpDeprecatedJsonRpcResponse(): NextResponse { + return NextResponse.json( + { + jsonrpc: '2.0', + id: null, + error: { code: -32000, message: DEPRECATION_MESSAGE }, + }, + { + status: 410, + headers: { 'Cache-Control': 'no-store' }, + } + ) +} diff --git a/packages/db/.env.swp b/packages/db/.env.swp new file mode 100644 index 0000000000000000000000000000000000000000..27153b0737ce866350e9e2be1f2d286f244ca2e2 GIT binary patch literal 12288 zcmeI&%}T>S5Ww+i@1p1n)Oc>UUmgqyrc_X{AZoozzGyB*leuU1`dowr@S%y(?Z|A{WFTB*x z!!o5VCbm1$I%=P%x;^Z~+O^}lGua8%5|c$aIO;DjqI79DbGk>^d3w_=q4|Eu$VIe#tJ2m}y7009ILKmY**5I_I{ z1Q7T?fw6UEjjYS#o9V^bp^@k;pQV0w8=Fd16SI0b`o5DN6glSpaL_NW+h_Tm7ivSk E0QbB{H2?qr literal 0 HcmV?d00001 diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 5b321a818d0..4fb99356eed 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -83,6 +83,14 @@ const INDIRECT_ZOD_ROUTES = new Set([ // that closes the popup, so the JSON-mode contract framework doesn't fit. // Validation is enforced via state lookup + session-vs-row userId match. 'apps/sim/app/api/mcp/oauth/callback/route.ts', + // Deprecated Copilot MCP surface: these routes are gated to always return + // 410 Gone and consume no client-supplied input. + 'apps/sim/app/api/mcp/copilot/route.ts', + 'apps/sim/app/api/mcp/copilot/.well-known/oauth-authorization-server/route.ts', + 'apps/sim/app/api/mcp/copilot/.well-known/oauth-protected-resource/route.ts', + // Deprecated v1 headless copilot chat API: gated to always return 410 Gone + // and consumes no client-supplied input. + 'apps/sim/app/api/v1/copilot/chat/route.ts', ]) /** @@ -110,7 +118,6 @@ const RAW_JSON_BASELINE_ROUTES = new Set([ 'apps/sim/app/api/invitations/[id]/route.ts', 'apps/sim/app/api/knowledge/[id]/documents/route.ts', 'apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts', - 'apps/sim/app/api/mcp/copilot/route.ts', 'apps/sim/app/api/mcp/serve/[serverId]/route.ts', 'apps/sim/app/api/mcp/servers/route.ts', 'apps/sim/app/api/mcp/servers/[id]/route.ts', From 704362dc5e0e315df91838c7a2dda4bd3e8bce87 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 29 May 2026 15:36:59 -0700 Subject: [PATCH 10/14] feat(integrations): hosted API keys for Findymail, Prospeo, and Wiza (#4777) * feat(integrations): hosted API keys for Findymail, Prospeo, and Wiza Add hosted-key support across all credit-consuming Findymail, Prospeo, and Wiza operations so Sim provides the key when a workspace has not brought its own. Register the three BYOK providers, consolidate Wiza's two-step reveal into a single polling wiza_individual_reveal op, and hide the API key field on hosted Sim for hosted operations. * fix(integrations): harden Wiza reveal polling, soften enrichment getCost guards Address Greptile + Cursor Bugbot review on #4777: return explicit failures from the Wiza individual_reveal poller instead of throwing (thrown errors were swallowed into a false queued success), short-circuit when the initial reveal is already terminal, tolerate transient 5xx/429 during polling, and return 0 (not throw) from Findymail getCost when the contacts/employees array is absent. * chore(integrations): biome formatting after wiza merge resolution * fix(wiza): type isTerminalReveal param structurally for next build typecheck * feat(enrichments): add Findymail, Prospeo, Wiza to work-email waterfall * feat(enrichments): add Wiza + Prospeo phone reveal to phone-number waterfall * feat(enrichments): opportunistic identifiers + LinkedIn URL input across work-email & phone cascades --- .../settings/components/byok/byok.tsx | 24 ++ apps/sim/blocks/blocks/findymail.ts | 14 +- apps/sim/blocks/blocks/prospeo.ts | 21 +- apps/sim/blocks/blocks/wiza.ts | 137 ++++---- .../phone-number/phone-number.test.ts | 78 +++++ .../enrichments/phone-number/phone-number.ts | 75 +++- .../enrichments/work-email/work-email.test.ts | 86 +++++ apps/sim/enrichments/work-email/work-email.ts | 90 ++++- apps/sim/lib/api/contracts/byok-keys.ts | 3 + apps/sim/tools/enrichment-hosting.test.ts | 277 +++++++++++++++ .../findymail/find_email_from_linkedin.ts | 6 + .../tools/findymail/find_email_from_name.ts | 6 + .../tools/findymail/find_emails_by_domain.ts | 10 + apps/sim/tools/findymail/find_employees.ts | 10 + apps/sim/tools/findymail/find_phone.ts | 6 + apps/sim/tools/findymail/get_company.ts | 6 + apps/sim/tools/findymail/hosting.ts | 42 +++ .../tools/findymail/lookup_technologies.ts | 6 + .../tools/findymail/reverse_email_lookup.ts | 8 + .../tools/findymail/search_technologies.ts | 6 + apps/sim/tools/findymail/verify_email.ts | 6 + apps/sim/tools/prospeo/bulk_enrich_company.ts | 9 + apps/sim/tools/prospeo/bulk_enrich_person.ts | 9 + apps/sim/tools/prospeo/enrich_company.ts | 7 + apps/sim/tools/prospeo/enrich_person.ts | 11 + apps/sim/tools/prospeo/hosting.ts | 43 +++ apps/sim/tools/prospeo/search_company.ts | 8 + apps/sim/tools/prospeo/search_person.ts | 8 + apps/sim/tools/registry.ts | 6 +- apps/sim/tools/types.ts | 5 +- apps/sim/tools/wiza/company_enrichment.ts | 6 + apps/sim/tools/wiza/get_individual_reveal.ts | 176 ---------- apps/sim/tools/wiza/hosting.ts | 42 +++ apps/sim/tools/wiza/index.ts | 3 +- apps/sim/tools/wiza/individual_reveal.ts | 330 ++++++++++++++++++ apps/sim/tools/wiza/prospect_search.ts | 7 + .../sim/tools/wiza/start_individual_reveal.ts | 151 -------- apps/sim/tools/wiza/types.ts | 23 +- 38 files changed, 1322 insertions(+), 439 deletions(-) create mode 100644 apps/sim/enrichments/phone-number/phone-number.test.ts create mode 100644 apps/sim/enrichments/work-email/work-email.test.ts create mode 100644 apps/sim/tools/enrichment-hosting.test.ts create mode 100644 apps/sim/tools/findymail/hosting.ts create mode 100644 apps/sim/tools/prospeo/hosting.ts delete mode 100644 apps/sim/tools/wiza/get_individual_reveal.ts create mode 100644 apps/sim/tools/wiza/hosting.ts create mode 100644 apps/sim/tools/wiza/individual_reveal.ts delete mode 100644 apps/sim/tools/wiza/start_individual_reveal.ts diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx index 4aaf902ffaf..998f1c5dcf9 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx @@ -19,6 +19,7 @@ import { AnthropicIcon, BrandfetchIcon, ExaAIIcon, + FindymailIcon, FirecrawlIcon, FireworksIcon, GeminiIcon, @@ -32,7 +33,9 @@ import { ParallelIcon, PeopleDataLabsIcon, PerplexityIcon, + ProspeoIcon, SerperIcon, + WizaIcon, } from '@/components/icons' import { Input } from '@/components/ui' import { BYOKKeySkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton' @@ -172,6 +175,27 @@ const PROVIDERS: { description: 'Person and company enrichment, search, and identity', placeholder: 'Enter your People Data Labs API key', }, + { + id: 'findymail', + name: 'Findymail', + icon: FindymailIcon, + description: 'Email finder, verification, and phone lookup', + placeholder: 'Enter your Findymail API key', + }, + { + id: 'prospeo', + name: 'Prospeo', + icon: ProspeoIcon, + description: 'Person and company enrichment and search', + placeholder: 'Enter your Prospeo API key', + }, + { + id: 'wiza', + name: 'Wiza', + icon: WizaIcon, + description: 'Prospect search, individual reveal, and company enrichment', + placeholder: 'Enter your Wiza API key', + }, ] export function BYOK() { diff --git a/apps/sim/blocks/blocks/findymail.ts b/apps/sim/blocks/blocks/findymail.ts index 4f8bea4911d..a112cf5ee80 100644 --- a/apps/sim/blocks/blocks/findymail.ts +++ b/apps/sim/blocks/blocks/findymail.ts @@ -214,7 +214,7 @@ export const FindymailBlock: BlockConfig = { placeholder: 'e.g. React, TypeScript, Node.js', }, }, - // API Key + // API Key — hidden on hosted Sim for operations with hosted-key support { id: 'apiKey', title: 'API Key', @@ -222,6 +222,18 @@ export const FindymailBlock: BlockConfig = { required: true, placeholder: 'Enter your Findymail API key', password: true, + hideWhenHosted: true, + condition: { field: 'operation', value: 'findymail_get_credits', not: true }, + }, + // API Key — always required for the credit-balance lookup (no hosted key) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your Findymail API key', + password: true, + condition: { field: 'operation', value: 'findymail_get_credits' }, }, ], tools: { diff --git a/apps/sim/blocks/blocks/prospeo.ts b/apps/sim/blocks/blocks/prospeo.ts index 2980efdb96a..b28fc3417a2 100644 --- a/apps/sim/blocks/blocks/prospeo.ts +++ b/apps/sim/blocks/blocks/prospeo.ts @@ -302,7 +302,7 @@ export const ProspeoBlock: BlockConfig = { condition: { field: 'operation', value: 'prospeo_search_suggestions' }, }, - // API Key (always last) + // API Key — hidden on hosted Sim for operations with hosted-key support { id: 'apiKey', title: 'API Key', @@ -310,6 +310,25 @@ export const ProspeoBlock: BlockConfig = { required: true, placeholder: 'Enter your Prospeo API key', password: true, + hideWhenHosted: true, + condition: { + field: 'operation', + value: ['prospeo_search_suggestions', 'prospeo_account_information'], + not: true, + }, + }, + // API Key — always required for the free account/suggestion lookups (no hosted key) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your Prospeo API key', + password: true, + condition: { + field: 'operation', + value: ['prospeo_search_suggestions', 'prospeo_account_information'], + }, }, ], tools: { diff --git a/apps/sim/blocks/blocks/wiza.ts b/apps/sim/blocks/blocks/wiza.ts index 91a1acdff89..fe648c0f7a9 100644 --- a/apps/sim/blocks/blocks/wiza.ts +++ b/apps/sim/blocks/blocks/wiza.ts @@ -24,8 +24,7 @@ export const WizaBlock: BlockConfig = { options: [ { label: 'Prospect Search', id: 'prospect_search' }, { label: 'Company Enrichment', id: 'company_enrichment' }, - { label: 'Start Individual Reveal', id: 'start_individual_reveal' }, - { label: 'Get Individual Reveal', id: 'get_individual_reveal' }, + { label: 'Individual Reveal', id: 'individual_reveal' }, { label: 'Get Credits', id: 'get_credits' }, ], value: () => 'prospect_search', @@ -37,6 +36,17 @@ export const WizaBlock: BlockConfig = { placeholder: 'Enter your Wiza API key', password: true, required: true, + hideWhenHosted: true, + condition: { field: 'operation', value: 'get_credits', not: true }, + }, + { + id: 'apiKey', + title: 'Wiza API Key', + type: 'short-input', + placeholder: 'Enter your Wiza API key', + password: true, + required: true, + condition: { field: 'operation', value: 'get_credits' }, }, // Prospect Search @@ -287,7 +297,7 @@ Return ONLY the JSON object - no explanations, no extra text.`, mode: 'advanced', }, - // Start Individual Reveal + // Individual Reveal { id: 'enrichment_level', title: 'Enrichment Level', @@ -298,85 +308,66 @@ Return ONLY the JSON object - no explanations, no extra text.`, { label: 'Phone', id: 'phone' }, { label: 'Full', id: 'full' }, ], - value: () => 'partial', - condition: { field: 'operation', value: 'start_individual_reveal' }, - required: { field: 'operation', value: 'start_individual_reveal' }, + value: () => 'full', + condition: { field: 'operation', value: 'individual_reveal' }, + required: { field: 'operation', value: 'individual_reveal' }, }, { id: 'profile_url', title: 'LinkedIn Profile URL', type: 'short-input', placeholder: 'https://linkedin.com/in/johndoe', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, }, { id: 'full_name', title: 'Full Name', type: 'short-input', placeholder: 'John Doe', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, }, { id: 'company', title: 'Company', type: 'short-input', placeholder: 'Wiza', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, }, { id: 'domain', title: 'Company Domain', type: 'short-input', placeholder: 'wiza.co', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, }, { id: 'email', title: 'Email', type: 'short-input', placeholder: 'john@wiza.co', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, }, { id: 'accept_work', title: 'Accept Work Emails', type: 'switch', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, mode: 'advanced', }, { id: 'accept_personal', title: 'Accept Personal Emails', type: 'switch', - condition: { field: 'operation', value: 'start_individual_reveal' }, - mode: 'advanced', - }, - { - id: 'callback_url', - title: 'Callback URL', - type: 'short-input', - placeholder: 'https://example.com/wiza-callback', - condition: { field: 'operation', value: 'start_individual_reveal' }, + condition: { field: 'operation', value: 'individual_reveal' }, mode: 'advanced', }, - - // Get Individual Reveal - { - id: 'id', - title: 'Reveal ID', - type: 'short-input', - placeholder: 'Reveal ID returned from Start Individual Reveal', - condition: { field: 'operation', value: 'get_individual_reveal' }, - required: { field: 'operation', value: 'get_individual_reveal' }, - }, ], tools: { access: [ 'wiza_prospect_search', 'wiza_company_enrichment', - 'wiza_start_individual_reveal', - 'wiza_get_individual_reveal', + 'wiza_individual_reveal', 'wiza_get_credits', ], config: { @@ -386,10 +377,8 @@ Return ONLY the JSON object - no explanations, no extra text.`, return 'wiza_prospect_search' case 'company_enrichment': return 'wiza_company_enrichment' - case 'start_individual_reveal': - return 'wiza_start_individual_reveal' - case 'get_individual_reveal': - return 'wiza_get_individual_reveal' + case 'individual_reveal': + return 'wiza_individual_reveal' case 'get_credits': return 'wiza_get_credits' default: @@ -479,8 +468,6 @@ Return ONLY the JSON object - no explanations, no extra text.`, email: { type: 'string', description: 'Email address' }, accept_work: { type: 'boolean', description: 'Whether to accept work emails' }, accept_personal: { type: 'boolean', description: 'Whether to accept personal emails' }, - callback_url: { type: 'string', description: 'Callback URL' }, - id: { type: 'string', description: 'Individual reveal ID' }, }, outputs: { @@ -495,46 +482,44 @@ Return ONLY the JSON object - no explanations, no extra text.`, }, id: { type: 'number', - description: 'Reveal ID (start_individual_reveal, get_individual_reveal)', + description: 'Reveal ID (individual_reveal)', }, status: { type: 'string', - description: - 'Reveal status (start_individual_reveal, get_individual_reveal): queued | resolving | finished | failed', + description: 'Reveal status (individual_reveal): queued | resolving | finished | failed', }, is_complete: { type: 'boolean', - description: - 'Whether the reveal has completed (start_individual_reveal, get_individual_reveal)', + description: 'Whether the reveal has completed (individual_reveal)', }, - name: { type: 'string', description: 'Full name (get_individual_reveal)' }, - company: { type: 'string', description: 'Company name (get_individual_reveal)' }, + name: { type: 'string', description: 'Full name (individual_reveal)' }, + company: { type: 'string', description: 'Company name (individual_reveal)' }, enrichment_level: { type: 'string', - description: 'Enrichment level used (get_individual_reveal)', + description: 'Enrichment level used (individual_reveal)', }, - linkedin_profile_url: { type: 'string', description: 'LinkedIn URL (get_individual_reveal)' }, - title: { type: 'string', description: 'Job title (get_individual_reveal)' }, - location: { type: 'string', description: 'Location (get_individual_reveal)' }, - email: { type: 'string', description: 'Primary email (get_individual_reveal)' }, - email_type: { type: 'string', description: 'Primary email type (get_individual_reveal)' }, + linkedin_profile_url: { type: 'string', description: 'LinkedIn URL (individual_reveal)' }, + title: { type: 'string', description: 'Job title (individual_reveal)' }, + location: { type: 'string', description: 'Location (individual_reveal)' }, + email: { type: 'string', description: 'Primary email (individual_reveal)' }, + email_type: { type: 'string', description: 'Primary email type (individual_reveal)' }, email_status: { type: 'string', - description: 'Primary email status: valid | risky | unfound (get_individual_reveal)', + description: 'Primary email status: valid | risky | unfound (individual_reveal)', }, emails: { type: 'json', - description: 'All emails found (get_individual_reveal): [{email, email_type, email_status}]', + description: 'All emails found (individual_reveal): [{email, email_type, email_status}]', }, - mobile_phone: { type: 'string', description: 'Mobile phone (get_individual_reveal)' }, - phone_number: { type: 'string', description: 'Direct/office phone (get_individual_reveal)' }, + mobile_phone: { type: 'string', description: 'Mobile phone (individual_reveal)' }, + phone_number: { type: 'string', description: 'Direct/office phone (individual_reveal)' }, phone_status: { type: 'string', - description: 'Phone status: found | unfound (get_individual_reveal)', + description: 'Phone status: found | unfound (individual_reveal)', }, phones: { type: 'json', - description: 'All phones found (get_individual_reveal): [{number, pretty_number, type}]', + description: 'All phones found (individual_reveal): [{number, pretty_number, type}]', }, company_name: { type: 'string', @@ -542,41 +527,41 @@ Return ONLY the JSON object - no explanations, no extra text.`, }, company_domain: { type: 'string', - description: 'Company domain (company_enrichment, get_individual_reveal)', + description: 'Company domain (company_enrichment, individual_reveal)', }, domain: { type: 'string', description: 'Domain (company_enrichment)' }, company_industry: { type: 'string', - description: 'Industry (company_enrichment, get_individual_reveal)', + description: 'Industry (company_enrichment, individual_reveal)', }, company_size: { type: 'number', - description: 'Employee count (company_enrichment, get_individual_reveal)', + description: 'Employee count (company_enrichment, individual_reveal)', }, company_size_range: { type: 'string', - description: 'Headcount range (company_enrichment, get_individual_reveal)', + description: 'Headcount range (company_enrichment, individual_reveal)', }, company_founded: { type: 'number', - description: 'Year founded (company_enrichment, get_individual_reveal)', + description: 'Year founded (company_enrichment, individual_reveal)', }, company_revenue_range: { type: 'string', description: 'Revenue range (company_enrichment)', }, - company_revenue: { type: 'string', description: 'Revenue (get_individual_reveal)' }, + company_revenue: { type: 'string', description: 'Revenue (individual_reveal)' }, company_funding: { type: 'string', - description: 'Total funding (company_enrichment, get_individual_reveal)', + description: 'Total funding (company_enrichment, individual_reveal)', }, company_type: { type: 'string', - description: 'Company type (company_enrichment, get_individual_reveal)', + description: 'Company type (company_enrichment, individual_reveal)', }, company_description: { type: 'string', - description: 'Company description (company_enrichment, get_individual_reveal)', + description: 'Company description (company_enrichment, individual_reveal)', }, company_ticker: { type: 'string', description: 'Stock ticker (company_enrichment)' }, company_last_funding_round: { @@ -593,40 +578,40 @@ Return ONLY the JSON object - no explanations, no extra text.`, }, company_location: { type: 'string', - description: 'Full location string (company_enrichment, get_individual_reveal)', + description: 'Full location string (company_enrichment, individual_reveal)', }, company_twitter: { type: 'string', description: 'Twitter URL (company_enrichment)' }, company_facebook: { type: 'string', description: 'Facebook URL (company_enrichment)' }, company_linkedin: { type: 'string', - description: 'LinkedIn URL (company_enrichment, get_individual_reveal)', + description: 'LinkedIn URL (company_enrichment, individual_reveal)', }, company_linkedin_id: { type: 'string', description: 'LinkedIn ID (company_enrichment)' }, company_street: { type: 'string', - description: 'Street address (company_enrichment, get_individual_reveal)', + description: 'Street address (company_enrichment, individual_reveal)', }, company_locality: { type: 'string', - description: 'City (company_enrichment, get_individual_reveal)', + description: 'City (company_enrichment, individual_reveal)', }, company_region: { type: 'string', - description: 'State/region (company_enrichment, get_individual_reveal)', + description: 'State/region (company_enrichment, individual_reveal)', }, company_postal_code: { type: 'string', - description: 'Postal code (company_enrichment, get_individual_reveal)', + description: 'Postal code (company_enrichment, individual_reveal)', }, company_country: { type: 'string', - description: 'Country (company_enrichment, get_individual_reveal)', + description: 'Country (company_enrichment, individual_reveal)', }, - company_subindustry: { type: 'string', description: 'Subindustry (get_individual_reveal)' }, + company_subindustry: { type: 'string', description: 'Subindustry (individual_reveal)' }, credits: { type: 'json', description: - 'Credits deducted — company_enrichment: { api_credits: { total, company_credits } }; get_individual_reveal: { api_credits: { total, email_credits, phone_credits, scrape_credits } }', + 'Credits deducted — company_enrichment: { api_credits: { total, company_credits } }; individual_reveal: { api_credits: { total, email_credits, phone_credits, scrape_credits } }', }, email_credits: { type: 'json', diff --git a/apps/sim/enrichments/phone-number/phone-number.test.ts b/apps/sim/enrichments/phone-number/phone-number.test.ts new file mode 100644 index 00000000000..2aa82c33eed --- /dev/null +++ b/apps/sim/enrichments/phone-number/phone-number.test.ts @@ -0,0 +1,78 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { phoneNumberEnrichment } from '@/enrichments/phone-number/phone-number' +import type { EnrichmentProvider } from '@/enrichments/types' + +function provider(id: string): EnrichmentProvider { + const p = phoneNumberEnrichment.providers.find((x) => x.id === id) + if (!p) throw new Error(`Provider ${id} not found in phone-number cascade`) + return p +} + +const nameDomain = { fullName: 'John Doe', companyDomain: 'https://www.acme.com/careers' } +const linkedinOnly = { fullName: 'John Doe', linkedinUrl: 'https://linkedin.com/in/johndoe' } + +describe('phone-number enrichment cascade', () => { + it('chains PDL then the phone-capable hosted providers', () => { + expect(phoneNumberEnrichment.providers.map((p) => p.id)).toEqual([ + 'pdl', + 'wiza', + 'findymail', + 'prospeo', + ]) + }) + + describe('wiza (opportunistic)', () => { + const p = provider('wiza') + it('reveals phone, using name+domain or LinkedIn profile_url', () => { + expect(p.toolId).toBe('wiza_individual_reveal') + expect(p.buildParams(nameDomain)).toEqual({ + full_name: 'John Doe', + domain: 'acme.com', + enrichment_level: 'phone', + }) + expect(p.buildParams(linkedinOnly)).toEqual({ + full_name: 'John Doe', + profile_url: 'https://linkedin.com/in/johndoe', + enrichment_level: 'phone', + }) + expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() + expect(p.mapOutput({ mobile_phone: '+1555', phones: [] })).toEqual({ phone: '+1555' }) + expect(p.mapOutput({ phones: [{ number: '+1777' }] })).toEqual({ phone: '+1777' }) + }) + }) + + describe('findymail', () => { + const p = provider('findymail') + it('keys off the LinkedIn URL and skips without one', () => { + expect(p.toolId).toBe('findymail_find_phone') + expect(p.buildParams(linkedinOnly)).toEqual({ + linkedin_url: 'https://linkedin.com/in/johndoe', + }) + expect(p.buildParams(nameDomain)).toBeNull() + expect(p.mapOutput({ phone: '+1555' })).toEqual({ phone: '+1555' }) + expect(p.mapOutput({ phone: null })).toBeNull() + }) + }) + + describe('prospeo (opportunistic)', () => { + const p = provider('prospeo') + it('requests mobile enrichment via name+domain or LinkedIn', () => { + expect(p.toolId).toBe('prospeo_enrich_person') + expect(p.buildParams(nameDomain)).toEqual({ + full_name: 'John Doe', + company_website: 'acme.com', + enrich_mobile: true, + }) + expect(p.buildParams(linkedinOnly)).toEqual({ + full_name: 'John Doe', + linkedin_url: 'https://linkedin.com/in/johndoe', + enrich_mobile: true, + }) + expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() + expect(p.mapOutput({ person: { mobile: { mobile: '+1555' } } })).toEqual({ phone: '+1555' }) + }) + }) +}) diff --git a/apps/sim/enrichments/phone-number/phone-number.ts b/apps/sim/enrichments/phone-number/phone-number.ts index 5b76e12e806..6a7ad186cdd 100644 --- a/apps/sim/enrichments/phone-number/phone-number.ts +++ b/apps/sim/enrichments/phone-number/phone-number.ts @@ -4,17 +4,22 @@ import { firstNonEmpty, normalizeDomain, str, toolProvider } from '@/enrichments import type { EnrichmentConfig } from '@/enrichments/types' /** - * Phone Number enrichment. Finds a contact's phone number from their full name - * and (optionally) company domain via a People Data Labs person match. + * Phone Number enrichment. Finds a contact's phone number from a full name plus + * any available identifiers (company domain, LinkedIn URL) via a waterfall: + * People Data Labs (name match) → Wiza reveal → Findymail (LinkedIn) → Prospeo + * mobile. Each provider opportunistically uses whatever identifiers the row + * provides and self-skips when it has none usable, so adding more inputs widens + * coverage without reordering. First phone wins; all providers support hosted keys. */ export const phoneNumberEnrichment: EnrichmentConfig = { id: 'phone-number', name: 'Phone Number', - description: "Find a contact's phone number from their name and company domain.", + description: "Find a contact's phone number from their name, company, or LinkedIn URL.", icon: Phone, inputs: [ { id: 'fullName', name: 'Full name', type: 'string', required: true }, { id: 'companyDomain', name: 'Company domain', type: 'string' }, + { id: 'linkedinUrl', name: 'LinkedIn URL', type: 'string' }, ], outputs: [{ id: 'phone', name: 'phone', type: 'string' }], providers: [ @@ -37,5 +42,69 @@ export const phoneNumberEnrichment: EnrichmentConfig = { return phone ? { phone } : null }, }), + toolProvider({ + id: 'wiza', + label: 'Wiza', + toolId: 'wiza_individual_reveal', + buildParams: (inputs) => { + const linkedin = str(inputs.linkedinUrl) + const fullName = str(inputs.fullName) + const domain = normalizeDomain(inputs.companyDomain) + // Needs a LinkedIn URL or a name+domain pair; skip otherwise. + if (!linkedin && !(fullName && domain)) return null + // 'phone' reveals the mobile number (5 credits). Prefer LinkedIn when present. + return filterUndefined({ + profile_url: linkedin || undefined, + full_name: fullName || undefined, + domain: domain || undefined, + enrichment_level: 'phone', + }) + }, + mapOutput: (output) => { + const phones = Array.isArray(output.phones) + ? (output.phones as Record[]) + : [] + const phone = str(output.mobile_phone) || str(output.phone_number) || str(phones[0]?.number) + return phone ? { phone } : null + }, + }), + toolProvider({ + id: 'findymail', + label: 'Findymail', + toolId: 'findymail_find_phone', + buildParams: (inputs) => { + // Findymail's phone finder keys off a LinkedIn URL only. + const linkedin = str(inputs.linkedinUrl) + if (!linkedin) return null + return { linkedin_url: linkedin } + }, + mapOutput: (output) => { + const phone = str(output.phone) + return phone ? { phone } : null + }, + }), + toolProvider({ + id: 'prospeo', + label: 'Prospeo', + toolId: 'prospeo_enrich_person', + buildParams: (inputs) => { + const linkedin = str(inputs.linkedinUrl) + const fullName = str(inputs.fullName) + const companyWebsite = normalizeDomain(inputs.companyDomain) + if (!linkedin && !(fullName && companyWebsite)) return null + return filterUndefined({ + linkedin_url: linkedin || undefined, + full_name: fullName || undefined, + company_website: companyWebsite || undefined, + enrich_mobile: true, + }) + }, + mapOutput: (output) => { + const person = output.person as Record | undefined + const mobile = person?.mobile as Record | undefined + const phone = str(mobile?.mobile) + return phone ? { phone } : null + }, + }), ], } diff --git a/apps/sim/enrichments/work-email/work-email.test.ts b/apps/sim/enrichments/work-email/work-email.test.ts new file mode 100644 index 00000000000..41bf50bc231 --- /dev/null +++ b/apps/sim/enrichments/work-email/work-email.test.ts @@ -0,0 +1,86 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import type { EnrichmentProvider } from '@/enrichments/types' +import { workEmailEnrichment } from '@/enrichments/work-email/work-email' + +function provider(id: string): EnrichmentProvider { + const p = workEmailEnrichment.providers.find((x) => x.id === id) + if (!p) throw new Error(`Provider ${id} not found in work-email cascade`) + return p +} + +const nameDomain = { fullName: 'John Doe', companyDomain: 'https://www.acme.com/careers' } +const linkedinOnly = { fullName: 'John Doe', linkedinUrl: 'https://linkedin.com/in/johndoe' } + +describe('work-email enrichment cascade', () => { + it('chains the hosted providers in waterfall order', () => { + expect(workEmailEnrichment.providers.map((p) => p.id)).toEqual([ + 'hunter', + 'findymail', + 'findymail-linkedin', + 'prospeo', + 'wiza', + 'pdl', + ]) + }) + + describe('findymail (name)', () => { + const p = provider('findymail') + it('maps name + domain and extracts contact.email', () => { + expect(p.toolId).toBe('findymail_find_email_from_name') + expect(p.buildParams(nameDomain)).toEqual({ name: 'John Doe', domain: 'acme.com' }) + expect(p.mapOutput({ contact: { email: 'j@acme.com' } })).toEqual({ email: 'j@acme.com' }) + expect(p.buildParams(linkedinOnly)).toBeNull() + }) + }) + + describe('findymail-linkedin', () => { + const p = provider('findymail-linkedin') + it('keys off the LinkedIn URL and skips without one', () => { + expect(p.toolId).toBe('findymail_find_email_from_linkedin') + expect(p.buildParams(linkedinOnly)).toEqual({ + linkedin_url: 'https://linkedin.com/in/johndoe', + }) + expect(p.buildParams(nameDomain)).toBeNull() + expect(p.mapOutput({ contact: { email: 'j@acme.com' } })).toEqual({ email: 'j@acme.com' }) + }) + }) + + describe('prospeo (opportunistic)', () => { + const p = provider('prospeo') + it('uses name+domain, or LinkedIn when present', () => { + expect(p.buildParams(nameDomain)).toEqual({ + full_name: 'John Doe', + company_website: 'acme.com', + }) + expect(p.buildParams(linkedinOnly)).toEqual({ + full_name: 'John Doe', + linkedin_url: 'https://linkedin.com/in/johndoe', + }) + expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() + expect(p.mapOutput({ person: { email: { email: 'j@acme.com' } } })).toEqual({ + email: 'j@acme.com', + }) + }) + }) + + describe('wiza (opportunistic)', () => { + const p = provider('wiza') + it('reveals email-only (partial), preferring LinkedIn profile_url', () => { + expect(p.buildParams(nameDomain)).toEqual({ + full_name: 'John Doe', + domain: 'acme.com', + enrichment_level: 'partial', + }) + expect(p.buildParams(linkedinOnly)).toEqual({ + full_name: 'John Doe', + profile_url: 'https://linkedin.com/in/johndoe', + enrichment_level: 'partial', + }) + expect(p.buildParams({ fullName: 'John Doe' })).toBeNull() + expect(p.mapOutput({ email: 'j@acme.com' })).toEqual({ email: 'j@acme.com' }) + }) + }) +}) diff --git a/apps/sim/enrichments/work-email/work-email.ts b/apps/sim/enrichments/work-email/work-email.ts index 0e998406c3a..4166217a1c9 100644 --- a/apps/sim/enrichments/work-email/work-email.ts +++ b/apps/sim/enrichments/work-email/work-email.ts @@ -4,18 +4,23 @@ import { normalizeDomain, splitName, str, toolProvider } from '@/enrichments/pro import type { EnrichmentConfig } from '@/enrichments/types' /** - * Work Email enrichment. Finds a person's work email from their full name and - * company domain, trying Hunter first (deterministic finder) then People Data - * Labs (record match) as a fallback. + * Work Email enrichment. Finds a person's work email from a full name plus any + * available identifiers (company domain, LinkedIn URL) via a provider waterfall: + * deterministic finders first (Hunter, Findymail by name then by LinkedIn), then + * enrichment/reveal providers (Prospeo, Wiza), then People Data Labs as a broad + * record-match fallback. Each provider opportunistically uses whatever + * identifiers the row provides and self-skips when it has none usable, so adding + * more inputs widens coverage. First email wins; all providers support hosted keys. */ export const workEmailEnrichment: EnrichmentConfig = { id: 'work-email', name: 'Work Email', - description: "Find a person's work email from their name and company domain.", + description: "Find a person's work email from their name, company, or LinkedIn URL.", icon: Mail, inputs: [ { id: 'fullName', name: 'Full name', type: 'string', required: true }, - { id: 'companyDomain', name: 'Company domain', type: 'string', required: true }, + { id: 'companyDomain', name: 'Company domain', type: 'string' }, + { id: 'linkedinUrl', name: 'LinkedIn URL', type: 'string' }, ], outputs: [{ id: 'email', name: 'email', type: 'string' }], providers: [ @@ -34,6 +39,81 @@ export const workEmailEnrichment: EnrichmentConfig = { return email ? { email } : null }, }), + toolProvider({ + id: 'findymail', + label: 'Findymail', + toolId: 'findymail_find_email_from_name', + buildParams: (inputs) => { + const name = str(inputs.fullName) + const domain = normalizeDomain(inputs.companyDomain) + if (!name || !domain) return null + return { name, domain } + }, + mapOutput: (output) => { + const contact = output.contact as Record | null + const email = str(contact?.email) + return email ? { email } : null + }, + }), + toolProvider({ + id: 'findymail-linkedin', + label: 'Findymail (LinkedIn)', + toolId: 'findymail_find_email_from_linkedin', + buildParams: (inputs) => { + const linkedin = str(inputs.linkedinUrl) + if (!linkedin) return null + return { linkedin_url: linkedin } + }, + mapOutput: (output) => { + const contact = output.contact as Record | null + const email = str(contact?.email) + return email ? { email } : null + }, + }), + toolProvider({ + id: 'prospeo', + label: 'Prospeo', + toolId: 'prospeo_enrich_person', + buildParams: (inputs) => { + const linkedin = str(inputs.linkedinUrl) + const fullName = str(inputs.fullName) + const companyWebsite = normalizeDomain(inputs.companyDomain) + if (!linkedin && !(fullName && companyWebsite)) return null + return filterUndefined({ + linkedin_url: linkedin || undefined, + full_name: fullName || undefined, + company_website: companyWebsite || undefined, + }) + }, + mapOutput: (output) => { + const person = output.person as Record | undefined + const emailObj = person?.email as Record | undefined + const email = str(emailObj?.email) + return email ? { email } : null + }, + }), + toolProvider({ + id: 'wiza', + label: 'Wiza', + toolId: 'wiza_individual_reveal', + buildParams: (inputs) => { + const linkedin = str(inputs.linkedinUrl) + const fullName = str(inputs.fullName) + const domain = normalizeDomain(inputs.companyDomain) + if (!linkedin && !(fullName && domain)) return null + // 'partial' reveals the email only (2 credits); avoids phone charges. + return filterUndefined({ + profile_url: linkedin || undefined, + full_name: fullName || undefined, + domain: domain || undefined, + enrichment_level: 'partial', + }) + }, + mapOutput: (output) => { + const email = str(output.email) + return email ? { email } : null + }, + }), toolProvider({ id: 'pdl', label: 'People Data Labs', diff --git a/apps/sim/lib/api/contracts/byok-keys.ts b/apps/sim/lib/api/contracts/byok-keys.ts index d52838eb427..0f79dcb217b 100644 --- a/apps/sim/lib/api/contracts/byok-keys.ts +++ b/apps/sim/lib/api/contracts/byok-keys.ts @@ -20,6 +20,9 @@ export const byokProviderIdSchema = z.enum([ 'cohere', 'hunter', 'peopledatalabs', + 'findymail', + 'prospeo', + 'wiza', ]) export const byokKeySchema = z.object({ diff --git a/apps/sim/tools/enrichment-hosting.test.ts b/apps/sim/tools/enrichment-hosting.test.ts new file mode 100644 index 00000000000..fcb33b31d03 --- /dev/null +++ b/apps/sim/tools/enrichment-hosting.test.ts @@ -0,0 +1,277 @@ +/** + * @vitest-environment node + */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { findEmailFromNameTool } from '@/tools/findymail/find_email_from_name' +import { findEmailsByDomainTool } from '@/tools/findymail/find_emails_by_domain' +import { findPhoneTool } from '@/tools/findymail/find_phone' +import { FINDYMAIL_CREDIT_USD } from '@/tools/findymail/hosting' +import { reverseEmailLookupTool } from '@/tools/findymail/reverse_email_lookup' +import { verifyEmailTool } from '@/tools/findymail/verify_email' +import { bulkEnrichPersonTool } from '@/tools/prospeo/bulk_enrich_person' +import { enrichCompanyTool } from '@/tools/prospeo/enrich_company' +import { enrichPersonTool } from '@/tools/prospeo/enrich_person' +import { PROSPEO_CREDIT_USD } from '@/tools/prospeo/hosting' +import { searchPersonTool } from '@/tools/prospeo/search_person' +import type { ToolConfig } from '@/tools/types' +import { wizaCompanyEnrichmentTool } from '@/tools/wiza/company_enrichment' +import { WIZA_CREDIT_USD } from '@/tools/wiza/hosting' +import { wizaIndividualRevealTool } from '@/tools/wiza/individual_reveal' +import { wizaProspectSearchTool } from '@/tools/wiza/prospect_search' + +afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() +}) + +function cost(tool: ToolConfig, params: any, output: Record) { + const pricing = tool.hosting?.pricing + if (!pricing || pricing.type !== 'custom') throw new Error('Expected custom pricing') + const result = pricing.getCost(params, output) + return typeof result === 'number' ? { cost: result } : result +} + +describe('Findymail hosted key pricing', () => { + it('declares hosting with the shared env prefix and BYOK provider', () => { + expect(findEmailFromNameTool.hosting?.envKeyPrefix).toBe('FINDYMAIL_API_KEY') + expect(findEmailFromNameTool.hosting?.byokProviderId).toBe('findymail') + }) + + it('charges one credit only when an email is found', () => { + expect(cost(findEmailFromNameTool, {}, { contact: { email: 'a@b.com' } }).cost).toBeCloseTo( + FINDYMAIL_CREDIT_USD + ) + expect(cost(findEmailFromNameTool, {}, { contact: null }).cost).toBe(0) + }) + + it('charges 10 credits for a found phone', () => { + expect(cost(findPhoneTool, {}, { phone: '+1555' }).cost).toBeCloseTo(10 * FINDYMAIL_CREDIT_USD) + expect(cost(findPhoneTool, {}, { phone: null }).cost).toBe(0) + }) + + it('charges one credit per contact returned by domain search', () => { + expect(cost(findEmailsByDomainTool, {}, { contacts: [{}, {}, {}] }).cost).toBeCloseTo( + 3 * FINDYMAIL_CREDIT_USD + ) + }) + + it('charges 2 credits for a reverse lookup with profile enrichment, 1 without', () => { + expect( + cost(reverseEmailLookupTool, { with_profile: true }, { email: 'a@b.com' }).cost + ).toBeCloseTo(2 * FINDYMAIL_CREDIT_USD) + expect( + cost(reverseEmailLookupTool, { with_profile: false }, { email: 'a@b.com' }).cost + ).toBeCloseTo(FINDYMAIL_CREDIT_USD) + expect( + cost(reverseEmailLookupTool, {}, { email: null, linkedin_url: null, fullName: null }).cost + ).toBe(0) + }) + + it('charges one verifier credit per verification', () => { + expect(cost(verifyEmailTool, {}, { verified: true }).cost).toBeCloseTo(FINDYMAIL_CREDIT_USD) + }) +}) + +describe('Prospeo hosted key pricing', () => { + it('declares hosting with the shared env prefix and BYOK provider', () => { + expect(enrichPersonTool.hosting?.envKeyPrefix).toBe('PROSPEO_API_KEY') + expect(enrichPersonTool.hosting?.byokProviderId).toBe('prospeo') + }) + + it('charges 1 credit for a person match and 10 when a mobile is revealed', () => { + expect(cost(enrichPersonTool, {}, { free_enrichment: false, person: {} }).cost).toBeCloseTo( + PROSPEO_CREDIT_USD + ) + expect( + cost(enrichPersonTool, {}, { free_enrichment: false, person: { mobile: { revealed: true } } }) + .cost + ).toBeCloseTo(10 * PROSPEO_CREDIT_USD) + }) + + it('does not charge on a free or no-match enrichment', () => { + expect(cost(enrichPersonTool, {}, { free_enrichment: true, person: {} }).cost).toBe(0) + expect(cost(enrichPersonTool, {}, { free_enrichment: false, person: null }).cost).toBe(0) + expect(cost(enrichCompanyTool, {}, { free_enrichment: false, company: null }).cost).toBe(0) + }) + + it('uses the API-reported total_cost for bulk endpoints', () => { + expect(cost(bulkEnrichPersonTool, {}, { total_cost: 7 }).cost).toBeCloseTo( + 7 * PROSPEO_CREDIT_USD + ) + }) + + it('throws when bulk total_cost is missing', () => { + expect(() => cost(bulkEnrichPersonTool, {}, { matched: [] })).toThrow(/total_cost/) + }) + + it('charges one credit per non-free search page with results', () => { + expect(cost(searchPersonTool, {}, { free: false, results: [{}] }).cost).toBeCloseTo( + PROSPEO_CREDIT_USD + ) + expect(cost(searchPersonTool, {}, { free: true, results: [{}] }).cost).toBe(0) + expect(cost(searchPersonTool, {}, { free: false, results: [] }).cost).toBe(0) + }) +}) + +describe('Wiza hosted key pricing', () => { + it('declares hosting with the shared env prefix and BYOK provider', () => { + expect(wizaIndividualRevealTool.hosting?.envKeyPrefix).toBe('WIZA_API_KEY') + expect(wizaIndividualRevealTool.hosting?.byokProviderId).toBe('wiza') + }) + + it('charges 2 credits for a valid email and 5 for a phone on individual reveal', () => { + expect( + cost(wizaIndividualRevealTool, {}, { email_status: 'valid', phones: [] }).cost + ).toBeCloseTo(2 * WIZA_CREDIT_USD) + expect( + cost(wizaIndividualRevealTool, {}, { email_status: 'unfound', mobile_phone: '+1555' }).cost + ).toBeCloseTo(5 * WIZA_CREDIT_USD) + expect( + cost(wizaIndividualRevealTool, {}, { email_status: 'valid', phones: [{ number: '+1555' }] }) + .cost + ).toBeCloseTo(7 * WIZA_CREDIT_USD) + expect(cost(wizaIndividualRevealTool, {}, { email_status: 'unfound', phones: [] }).cost).toBe(0) + }) + + it('charges 2 credits per company enrichment match and nothing for prospect search', () => { + expect(cost(wizaCompanyEnrichmentTool, {}, { company_name: 'Wiza' }).cost).toBeCloseTo( + 2 * WIZA_CREDIT_USD + ) + expect( + cost( + wizaCompanyEnrichmentTool, + {}, + { company_name: null, company_domain: null, domain: null } + ).cost + ).toBe(0) + expect(cost(wizaProspectSearchTool, {}, { total: 100, profiles: [] }).cost).toBe(0) + }) + + it('polls the reveal to completion in postProcess', async () => { + vi.useFakeTimers() + const fetchMock = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + data: { + id: 123, + status: 'finished', + email: 'a@b.com', + email_status: 'valid', + emails: [], + phones: [], + }, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { id: 123, status: 'queued', is_complete: false } as any, + } + const promise = wizaIndividualRevealTool.postProcess!( + initial as any, + { apiKey: 'k', enrichment_level: 'full' } as any, + vi.fn() + ) + await vi.advanceTimersByTimeAsync(2000) + const result = await promise + + expect(fetchMock).toHaveBeenCalledWith( + 'https://wiza.co/api/individual_reveals/123', + expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer k' }) }) + ) + expect(result.success).toBe(true) + expect((result.output as any).email).toBe('a@b.com') + expect((result.output as any).status).toBe('finished') + }) + + it('returns immediately without polling when the initial reveal is already terminal', async () => { + const fetchMock = vi.fn() + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { + id: 123, + status: 'finished', + is_complete: true, + email: 'a@b.com', + email_status: 'valid', + emails: [], + phones: [], + } as any, + } + const result = await wizaIndividualRevealTool.postProcess!( + initial as any, + { apiKey: 'k', enrichment_level: 'full' } as any, + vi.fn() + ) + + expect(fetchMock).not.toHaveBeenCalled() + expect(result.success).toBe(true) + expect((result.output as any).email).toBe('a@b.com') + }) + + it('retries transient poll errors and still resolves on a later finished response', async () => { + vi.useFakeTimers() + const fetchMock = vi + .fn() + .mockResolvedValueOnce(new Response('busy', { status: 503 })) + .mockResolvedValueOnce(new Response('rate limited', { status: 429 })) + .mockResolvedValueOnce( + new Response( + JSON.stringify({ + data: { + id: 1, + status: 'finished', + email: 'a@b.com', + email_status: 'valid', + emails: [], + phones: [], + }, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ) + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { id: 1, status: 'queued', is_complete: false } as any, + } + const promise = wizaIndividualRevealTool.postProcess!( + initial as any, + { apiKey: 'k', enrichment_level: 'full' } as any, + vi.fn() + ) + await vi.advanceTimersByTimeAsync(6000) + const result = await promise + + expect(fetchMock).toHaveBeenCalledTimes(3) + expect(result.success).toBe(true) + expect((result.output as any).email).toBe('a@b.com') + }) + + it('returns an explicit failure (not a queued success) after repeated poll errors', async () => { + vi.useFakeTimers() + const fetchMock = vi.fn().mockResolvedValue(new Response('error', { status: 500 })) + vi.stubGlobal('fetch', fetchMock) + + const initial = { + success: true as const, + output: { id: 1, status: 'queued', is_complete: false } as any, + } + const promise = wizaIndividualRevealTool.postProcess!( + initial as any, + { apiKey: 'k', enrichment_level: 'full' } as any, + vi.fn() + ) + await vi.advanceTimersByTimeAsync(6000) + const result = await promise + + expect(result.success).toBe(false) + expect((result.output as any).status).toBe('queued') + }) +}) diff --git a/apps/sim/tools/findymail/find_email_from_linkedin.ts b/apps/sim/tools/findymail/find_email_from_linkedin.ts index 87eb569f960..5d095cd9cf4 100644 --- a/apps/sim/tools/findymail/find_email_from_linkedin.ts +++ b/apps/sim/tools/findymail/find_email_from_linkedin.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailFindEmailFromLinkedInParams, FindymailFindEmailFromLinkedInResponse, @@ -15,6 +16,11 @@ export const findEmailFromLinkedInTool: ToolConfig< "Find someone's email from a LinkedIn profile URL or username. Uses one finder credit when a verified email is found.", version: '1.0.0', + hosting: findymailHosting((_params, output) => { + const contact = output.contact as { email?: string } | null + return contact?.email ? 1 : 0 + }), + params: { linkedin_url: { type: 'string', diff --git a/apps/sim/tools/findymail/find_email_from_name.ts b/apps/sim/tools/findymail/find_email_from_name.ts index 5a22a19c3b8..6a09ed75bd3 100644 --- a/apps/sim/tools/findymail/find_email_from_name.ts +++ b/apps/sim/tools/findymail/find_email_from_name.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailFindEmailFromNameParams, FindymailFindEmailFromNameResponse, @@ -15,6 +16,11 @@ export const findEmailFromNameTool: ToolConfig< "Find someone's email from their name and a company domain or company name. Uses one finder credit when a verified email is found.", version: '1.0.0', + hosting: findymailHosting((_params, output) => { + const contact = output.contact as { email?: string } | null + return contact?.email ? 1 : 0 + }), + params: { name: { type: 'string', diff --git a/apps/sim/tools/findymail/find_emails_by_domain.ts b/apps/sim/tools/findymail/find_emails_by_domain.ts index ebb1de7ee3f..de26fd4f0a3 100644 --- a/apps/sim/tools/findymail/find_emails_by_domain.ts +++ b/apps/sim/tools/findymail/find_emails_by_domain.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailFindEmailsByDomainParams, FindymailFindEmailsByDomainResponse, @@ -15,6 +16,15 @@ export const findEmailsByDomainTool: ToolConfig< 'Find verified contacts at a given domain matching one or more target roles (max 3 roles). Limited to 5 concurrent synchronous requests.', version: '1.0.0', + hosting: findymailHosting((_params, output) => { + // No contacts array means no verified contacts returned — no charge. + if (!Array.isArray(output.contacts)) { + return 0 + } + // 1 finder credit per verified contact returned. + return output.contacts.length + }), + params: { domain: { type: 'string', diff --git a/apps/sim/tools/findymail/find_employees.ts b/apps/sim/tools/findymail/find_employees.ts index aabd08ce472..9051ad8d75e 100644 --- a/apps/sim/tools/findymail/find_employees.ts +++ b/apps/sim/tools/findymail/find_employees.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailFindEmployeesParams, FindymailFindEmployeesResponse, @@ -15,6 +16,15 @@ export const findEmployeesTool: ToolConfig< 'Find employees at a company by website and target job titles. Uses 1 credit per found contact. Does not return email addresses.', version: '1.0.0', + hosting: findymailHosting((_params, output) => { + // No employees array means no contacts found — no charge. + if (!Array.isArray(output.employees)) { + return 0 + } + // 1 finder credit per contact found. + return output.employees.length + }), + params: { website: { type: 'string', diff --git a/apps/sim/tools/findymail/find_phone.ts b/apps/sim/tools/findymail/find_phone.ts index 0222eeed796..8a5c48f1591 100644 --- a/apps/sim/tools/findymail/find_phone.ts +++ b/apps/sim/tools/findymail/find_phone.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailFindPhoneParams, FindymailFindPhoneResponse } from '@/tools/findymail/types' import type { ToolConfig } from '@/tools/types' @@ -8,6 +9,11 @@ export const findPhoneTool: ToolConfig((_params, output) => { + // Phone lookups consume 10 finder credits, only when a number is found. + return output.phone ? 10 : 0 + }), + params: { linkedin_url: { type: 'string', diff --git a/apps/sim/tools/findymail/get_company.ts b/apps/sim/tools/findymail/get_company.ts index 2ee001f8527..64c03bcf831 100644 --- a/apps/sim/tools/findymail/get_company.ts +++ b/apps/sim/tools/findymail/get_company.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailGetCompanyParams, FindymailGetCompanyResponse, @@ -11,6 +12,11 @@ export const getCompanyTool: ToolConfig((_params, output) => { + // 1 finder credit per successful company match. + return output.name || output.domain ? 1 : 0 + }), + params: { linkedin_url: { type: 'string', diff --git a/apps/sim/tools/findymail/hosting.ts b/apps/sim/tools/findymail/hosting.ts new file mode 100644 index 00000000000..e624c13163c --- /dev/null +++ b/apps/sim/tools/findymail/hosting.ts @@ -0,0 +1,42 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for Findymail hosted keys. Provide keys as + * `FINDYMAIL_API_KEY_COUNT` plus `FINDYMAIL_API_KEY_1..N`. + */ +export const FINDYMAIL_API_KEY_PREFIX = 'FINDYMAIL_API_KEY' + +/** + * Dollar cost of a single Findymail finder credit. + * + * Findymail charges per verified result: 1 credit per email, 10 credits per + * phone, and only when a result is found. Estimated from the $99/month Starter + * plan (5,000 credits ≈ $0.0198/credit) — https://www.findymail.com/pricing/. + */ +export const FINDYMAIL_CREDIT_USD = 0.02 + +/** + * Build a Findymail `hosting` config. `getCredits` returns the number of + * Findymail credits the call consumed, derived from the tool's output (per the + * documented per-endpoint credit model at https://www.findymail.com/api/). + */ +export function findymailHosting

( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

{ + return { + envKeyPrefix: FINDYMAIL_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'findymail', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * FINDYMAIL_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/findymail/lookup_technologies.ts b/apps/sim/tools/findymail/lookup_technologies.ts index b7fb2af8d1d..e7360f36151 100644 --- a/apps/sim/tools/findymail/lookup_technologies.ts +++ b/apps/sim/tools/findymail/lookup_technologies.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailLookupTechnologiesParams, FindymailLookupTechnologiesResponse, @@ -15,6 +16,11 @@ export const lookupTechnologiesTool: ToolConfig< 'Get the technology stack for a company by domain. Optionally filter by technology names. 1 finder credit if technologies are found, free otherwise.', version: '1.0.0', + hosting: findymailHosting((_params, output) => { + // 1 finder credit when a technology stack is returned, free otherwise. + return Array.isArray(output.technologies) && output.technologies.length > 0 ? 1 : 0 + }), + params: { domain: { type: 'string', diff --git a/apps/sim/tools/findymail/reverse_email_lookup.ts b/apps/sim/tools/findymail/reverse_email_lookup.ts index 48a0c42ca18..447d112b696 100644 --- a/apps/sim/tools/findymail/reverse_email_lookup.ts +++ b/apps/sim/tools/findymail/reverse_email_lookup.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailReverseEmailLookupParams, FindymailReverseEmailLookupResponse, @@ -14,6 +15,13 @@ export const reverseEmailLookupTool: ToolConfig< 'Find a business profile from an email address. Uses 1 finder credit if a profile is found, 2 credits if returning full profile data.', version: '1.0.0', + hosting: findymailHosting((params, output) => { + const found = Boolean(output.email || output.linkedin_url || output.fullName) + if (!found) return 0 + // 1 credit for a match, 2 when full profile enrichment is requested. + return params.with_profile ? 2 : 1 + }), + params: { email: { type: 'string', diff --git a/apps/sim/tools/findymail/search_technologies.ts b/apps/sim/tools/findymail/search_technologies.ts index a4f7bcadcc5..64d56b785a3 100644 --- a/apps/sim/tools/findymail/search_technologies.ts +++ b/apps/sim/tools/findymail/search_technologies.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailSearchTechnologiesParams, FindymailSearchTechnologiesResponse, @@ -15,6 +16,11 @@ export const searchTechnologiesTool: ToolConfig< 'Search the technology catalog by name. Returns up to 25 technologies. Free endpoint, rate limited to 10 requests per minute.', version: '1.0.0', + hosting: findymailHosting(() => { + // Free catalog search — consumes no Findymail credits. + return 0 + }), + params: { q: { type: 'string', diff --git a/apps/sim/tools/findymail/verify_email.ts b/apps/sim/tools/findymail/verify_email.ts index c239e296cd0..30463e1fe6c 100644 --- a/apps/sim/tools/findymail/verify_email.ts +++ b/apps/sim/tools/findymail/verify_email.ts @@ -1,3 +1,4 @@ +import { findymailHosting } from '@/tools/findymail/hosting' import type { FindymailVerifyEmailParams, FindymailVerifyEmailResponse, @@ -11,6 +12,11 @@ export const verifyEmailTool: ToolConfig(() => { + // Each verification consumes one verifier credit, billed at the finder-credit rate. + return 1 + }), + params: { email: { type: 'string', diff --git a/apps/sim/tools/prospeo/bulk_enrich_company.ts b/apps/sim/tools/prospeo/bulk_enrich_company.ts index c73c6e70f12..f2023921629 100644 --- a/apps/sim/tools/prospeo/bulk_enrich_company.ts +++ b/apps/sim/tools/prospeo/bulk_enrich_company.ts @@ -1,3 +1,4 @@ +import { prospeoHosting } from '@/tools/prospeo/hosting' import { extractProspeoError, type ProspeoBulkEnrichCompanyParams, @@ -15,6 +16,14 @@ export const bulkEnrichCompanyTool: ToolConfig< description: 'Enrich up to 50 company records at once.', version: '1.0.0', + hosting: prospeoHosting((_params, output) => { + // Prospeo reports the exact credits spent for the batch in total_cost. + if (typeof output.total_cost !== 'number') { + throw new Error('Prospeo bulk enrich company response missing total_cost') + } + return output.total_cost + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/prospeo/bulk_enrich_person.ts b/apps/sim/tools/prospeo/bulk_enrich_person.ts index b6d095e9526..e594e9b7cb4 100644 --- a/apps/sim/tools/prospeo/bulk_enrich_person.ts +++ b/apps/sim/tools/prospeo/bulk_enrich_person.ts @@ -1,3 +1,4 @@ +import { prospeoHosting } from '@/tools/prospeo/hosting' import { extractProspeoError, type ProspeoBulkEnrichPersonParams, @@ -15,6 +16,14 @@ export const bulkEnrichPersonTool: ToolConfig< description: 'Enrich up to 50 person records at once.', version: '1.0.0', + hosting: prospeoHosting((_params, output) => { + // Prospeo reports the exact credits spent for the batch in total_cost. + if (typeof output.total_cost !== 'number') { + throw new Error('Prospeo bulk enrich person response missing total_cost') + } + return output.total_cost + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/prospeo/enrich_company.ts b/apps/sim/tools/prospeo/enrich_company.ts index ca4e1ed9b99..7f0b141e3d3 100644 --- a/apps/sim/tools/prospeo/enrich_company.ts +++ b/apps/sim/tools/prospeo/enrich_company.ts @@ -1,3 +1,4 @@ +import { prospeoHosting } from '@/tools/prospeo/hosting' import { extractProspeoError, type ProspeoEnrichCompanyParams, @@ -14,6 +15,12 @@ export const enrichCompanyTool: ToolConfig< description: 'Enrich a company with complete B2B data.', version: '1.0.0', + hosting: prospeoHosting((_params, output) => { + // 1 credit per company match; no charge on a no-match or repeat enrichment. + if (output.free_enrichment === true) return 0 + return output.company ? 1 : 0 + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/prospeo/enrich_person.ts b/apps/sim/tools/prospeo/enrich_person.ts index 44c72a26007..e072dc6bbbf 100644 --- a/apps/sim/tools/prospeo/enrich_person.ts +++ b/apps/sim/tools/prospeo/enrich_person.ts @@ -1,3 +1,4 @@ +import { prospeoHosting } from '@/tools/prospeo/hosting' import { extractProspeoError, type ProspeoEnrichPersonParams, @@ -12,6 +13,16 @@ export const enrichPersonTool: ToolConfig((_params, output) => { + // No charge on a no-match or a repeat enrichment. + if (output.free_enrichment === true) return 0 + const person = output.person as Record | null + if (!person) return 0 + // 10 credits when a mobile is revealed, otherwise 1 for the person match. + const mobile = person.mobile as { revealed?: boolean } | undefined + return mobile?.revealed ? 10 : 1 + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/prospeo/hosting.ts b/apps/sim/tools/prospeo/hosting.ts new file mode 100644 index 00000000000..c5d97b98475 --- /dev/null +++ b/apps/sim/tools/prospeo/hosting.ts @@ -0,0 +1,43 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for Prospeo hosted keys. Provide keys as + * `PROSPEO_API_KEY_COUNT` plus `PROSPEO_API_KEY_1..N`. + */ +export const PROSPEO_API_KEY_PREFIX = 'PROSPEO_API_KEY' + +/** + * Dollar cost of a single Prospeo credit. + * + * Prospeo charges per match: 1 credit per person/company match, 10 credits when + * a mobile is revealed, and never on a no-match or a repeat enrichment. Based on + * the $39/month Starter plan (1,000 credits ≈ $0.039/credit) — https://prospeo.io/pricing. + */ +export const PROSPEO_CREDIT_USD = 0.039 + +/** + * Build a Prospeo `hosting` config. `getCredits` returns the number of Prospeo + * credits the call consumed, derived from the tool's output (prefer the + * API-reported `total_cost` for bulk endpoints; otherwise compute from the + * `free`/`free_enrichment` flag and the match). + */ +export function prospeoHosting

( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

{ + return { + envKeyPrefix: PROSPEO_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'prospeo', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * PROSPEO_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/prospeo/search_company.ts b/apps/sim/tools/prospeo/search_company.ts index f4e8a1c0b52..e16ecaf83c1 100644 --- a/apps/sim/tools/prospeo/search_company.ts +++ b/apps/sim/tools/prospeo/search_company.ts @@ -1,3 +1,4 @@ +import { prospeoHosting } from '@/tools/prospeo/hosting' import { extractProspeoError, PROSPEO_PAGINATION_OUTPUT, @@ -16,6 +17,13 @@ export const searchCompanyTool: ToolConfig< description: 'Search for companies using 20+ filters to build account lists.', version: '1.0.0', + hosting: prospeoHosting((_params, output) => { + // 1 credit per page that returns at least one result; free on 30-day dedup. + if (output.free === true) return 0 + const results = output.results + return Array.isArray(results) && results.length > 0 ? 1 : 0 + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/prospeo/search_person.ts b/apps/sim/tools/prospeo/search_person.ts index c8415ceec9e..883b11e73dc 100644 --- a/apps/sim/tools/prospeo/search_person.ts +++ b/apps/sim/tools/prospeo/search_person.ts @@ -1,3 +1,4 @@ +import { prospeoHosting } from '@/tools/prospeo/hosting' import { extractProspeoError, PROSPEO_PAGINATION_OUTPUT, @@ -14,6 +15,13 @@ export const searchPersonTool: ToolConfig((_params, output) => { + // 1 credit per page that returns at least one result; free on 30-day dedup. + if (output.free === true) return 0 + const results = output.results + return Array.isArray(results) && results.length > 0 ? 1 : 0 + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 3070576fa42..d6558b49423 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -3053,9 +3053,8 @@ import { import { wizaCompanyEnrichmentTool, wizaGetCreditsTool, - wizaGetIndividualRevealTool, + wizaIndividualRevealTool, wizaProspectSearchTool, - wizaStartIndividualRevealTool, } from '@/tools/wiza' import { wordpressCreateCategoryTool, @@ -5436,9 +5435,8 @@ export const tools: Record = { wikipedia_random: wikipediaRandomPageTool, wiza_company_enrichment: wizaCompanyEnrichmentTool, wiza_get_credits: wizaGetCreditsTool, - wiza_get_individual_reveal: wizaGetIndividualRevealTool, + wiza_individual_reveal: wizaIndividualRevealTool, wiza_prospect_search: wizaProspectSearchTool, - wiza_start_individual_reveal: wizaStartIndividualRevealTool, wordpress_create_post: wordpressCreatePostTool, wordpress_update_post: wordpressUpdatePostTool, wordpress_delete_post: wordpressDeletePostTool, diff --git a/apps/sim/tools/types.ts b/apps/sim/tools/types.ts index b89ca98cd7e..24501cb6d96 100644 --- a/apps/sim/tools/types.ts +++ b/apps/sim/tools/types.ts @@ -21,6 +21,9 @@ export type BYOKProviderId = | 'cohere' | 'hunter' | 'peopledatalabs' + | 'findymail' + | 'prospeo' + | 'wiza' export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' @@ -309,7 +312,7 @@ export type ToolHostingPricing

> = PerRequestPricing * Adding more keys only requires updating the count and adding the new env var — * no code changes needed. */ -interface ToolHostingConfig

> { +export interface ToolHostingConfig

> { /** Optional predicate for tools where hosted keys only apply to some parameter combinations. */ enabled?: (params: P) => boolean /** diff --git a/apps/sim/tools/wiza/company_enrichment.ts b/apps/sim/tools/wiza/company_enrichment.ts index 70bfdfec5be..39089404f1c 100644 --- a/apps/sim/tools/wiza/company_enrichment.ts +++ b/apps/sim/tools/wiza/company_enrichment.ts @@ -1,4 +1,5 @@ import type { ToolConfig } from '@/tools/types' +import { wizaHosting } from '@/tools/wiza/hosting' import type { WizaCompanyEnrichmentParams, WizaCompanyEnrichmentResponse } from '@/tools/wiza/types' export const wizaCompanyEnrichmentTool: ToolConfig< @@ -11,6 +12,11 @@ export const wizaCompanyEnrichmentTool: ToolConfig< 'Enrich a company by name, domain, LinkedIn ID, or LinkedIn slug with detailed firmographic data', version: '1.0.0', + hosting: wizaHosting((_params, output) => { + // 2 API credits per successful company match; no charge on a no-match. + return output.company_name || output.company_domain || output.domain ? 2 : 0 + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/wiza/get_individual_reveal.ts b/apps/sim/tools/wiza/get_individual_reveal.ts deleted file mode 100644 index 7c3d4ec5434..00000000000 --- a/apps/sim/tools/wiza/get_individual_reveal.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { ToolConfig } from '@/tools/types' -import type { - WizaGetIndividualRevealParams, - WizaGetIndividualRevealResponse, -} from '@/tools/wiza/types' - -export const wizaGetIndividualRevealTool: ToolConfig< - WizaGetIndividualRevealParams, - WizaGetIndividualRevealResponse -> = { - id: 'wiza_get_individual_reveal', - name: 'Wiza Get Individual Reveal', - description: 'Retrieve the status and enriched data for an individual reveal by ID', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Wiza API key', - }, - id: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'Individual reveal ID returned from Start Individual Reveal', - }, - }, - - request: { - url: (params: WizaGetIndividualRevealParams) => - `https://wiza.co/api/individual_reveals/${encodeURIComponent(String(params.id).trim())}`, - method: 'GET', - headers: (params: WizaGetIndividualRevealParams) => ({ - Authorization: `Bearer ${params.apiKey}`, - 'Content-Type': 'application/json', - }), - }, - - transformResponse: async (response: Response) => { - if (!response.ok) { - const errorText = await response.text() - throw new Error(`Wiza API error: ${response.status} - ${errorText}`) - } - - const json = await response.json() - const d = json.data ?? {} - const emails = Array.isArray(d.emails) ? d.emails : [] - const phones = Array.isArray(d.phones) ? d.phones : [] - - return { - success: true, - output: { - id: d.id ?? null, - status: d.status ?? null, - is_complete: d.is_complete ?? null, - name: d.name ?? null, - company: d.company ?? null, - enrichment_level: d.enrichment_level ?? null, - linkedin_profile_url: d.linkedin_profile_url ?? null, - title: d.title ?? null, - location: d.location ?? null, - email: d.email ?? null, - email_type: d.email_type ?? null, - email_status: d.email_status ?? null, - emails: emails.map((e: Record) => ({ - email: (e.email as string) ?? null, - email_type: (e.email_type as string) ?? null, - email_status: (e.email_status as string) ?? null, - })), - mobile_phone: d.mobile_phone ?? null, - phone_number: d.phone_number ?? null, - phone_status: d.phone_status ?? null, - phones: phones.map((p: Record) => ({ - number: (p.number as string) ?? null, - pretty_number: (p.pretty_number as string) ?? null, - type: (p.type as string) ?? null, - })), - company_size: d.company_size ?? null, - company_size_range: d.company_size_range ?? null, - company_type: d.company_type ?? null, - company_domain: d.company_domain ?? null, - company_locality: d.company_locality ?? null, - company_region: d.company_region ?? null, - company_country: d.company_country ?? null, - company_street: d.company_street ?? null, - company_postal_code: d.company_postal_code ?? null, - company_founded: d.company_founded ?? null, - company_funding: d.company_funding ?? null, - company_revenue: d.company_revenue ?? null, - company_industry: d.company_industry ?? null, - company_subindustry: d.company_subindustry ?? null, - company_linkedin: d.company_linkedin ?? null, - company_location: d.company_location ?? null, - company_description: d.company_description ?? null, - credits: d.credits ?? null, - }, - } - }, - - outputs: { - id: { type: 'number', description: 'Reveal ID', optional: true }, - status: { - type: 'string', - description: 'queued | resolving | finished | failed', - optional: true, - }, - is_complete: { - type: 'boolean', - description: 'Whether the reveal has completed', - optional: true, - }, - name: { type: 'string', description: 'Full name', optional: true }, - company: { type: 'string', description: 'Company name', optional: true }, - enrichment_level: { type: 'string', description: 'Enrichment level used', optional: true }, - linkedin_profile_url: { type: 'string', description: 'LinkedIn URL', optional: true }, - title: { type: 'string', description: 'Job title', optional: true }, - location: { type: 'string', description: 'Location', optional: true }, - email: { type: 'string', description: 'Primary email', optional: true }, - email_type: { type: 'string', description: 'Email type', optional: true }, - email_status: { type: 'string', description: 'valid | risky | unfound', optional: true }, - emails: { - type: 'array', - description: 'All emails found', - optional: true, - items: { - type: 'object', - properties: { - email: { type: 'string' }, - email_type: { type: 'string' }, - email_status: { type: 'string' }, - }, - }, - }, - mobile_phone: { type: 'string', description: 'Mobile phone', optional: true }, - phone_number: { type: 'string', description: 'Direct/office phone', optional: true }, - phone_status: { type: 'string', description: 'found | unfound', optional: true }, - phones: { - type: 'array', - description: 'All phones found', - optional: true, - items: { - type: 'object', - properties: { - number: { type: 'string' }, - pretty_number: { type: 'string' }, - type: { type: 'string' }, - }, - }, - }, - company_size: { type: 'number', description: 'Employee count', optional: true }, - company_size_range: { type: 'string', description: 'Headcount range', optional: true }, - company_type: { type: 'string', description: 'Company type', optional: true }, - company_domain: { type: 'string', description: 'Company domain', optional: true }, - company_locality: { type: 'string', description: 'City', optional: true }, - company_region: { type: 'string', description: 'State/region', optional: true }, - company_country: { type: 'string', description: 'Country', optional: true }, - company_street: { type: 'string', description: 'Street', optional: true }, - company_postal_code: { type: 'string', description: 'Postal code', optional: true }, - company_founded: { type: 'number', description: 'Year founded', optional: true }, - company_funding: { type: 'string', description: 'Funding total', optional: true }, - company_revenue: { type: 'string', description: 'Revenue', optional: true }, - company_industry: { type: 'string', description: 'Industry', optional: true }, - company_subindustry: { type: 'string', description: 'Subindustry', optional: true }, - company_linkedin: { type: 'string', description: 'Company LinkedIn URL', optional: true }, - company_location: { type: 'string', description: 'Full company location', optional: true }, - company_description: { type: 'string', description: 'Company description', optional: true }, - credits: { - type: 'json', - description: - 'Credits deducted for this reveal (api_credits: { total, email_credits, phone_credits, scrape_credits })', - optional: true, - }, - }, -} diff --git a/apps/sim/tools/wiza/hosting.ts b/apps/sim/tools/wiza/hosting.ts new file mode 100644 index 00000000000..43fe367ae34 --- /dev/null +++ b/apps/sim/tools/wiza/hosting.ts @@ -0,0 +1,42 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for Wiza hosted keys. Provide keys as `WIZA_API_KEY_COUNT` + * plus `WIZA_API_KEY_1..N`. + */ +export const WIZA_API_KEY_PREFIX = 'WIZA_API_KEY' + +/** + * Dollar cost of a single Wiza API credit. + * + * Wiza meters API usage in credits at a documented $0.025/credit (2,000-credit + * minimum) — https://help.wiza.co/en/articles/13551713-how-to-purchase-api-credits. + * Credits are deducted only when data is successfully returned: 2 credits per + * valid email, 5 credits per phone, 2 credits per company enrichment. + */ +export const WIZA_CREDIT_USD = 0.025 + +/** + * Build a Wiza `hosting` config. `getCredits` returns the number of Wiza API + * credits the call consumed, derived from the tool's output. + */ +export function wizaHosting

( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

{ + return { + envKeyPrefix: WIZA_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'wiza', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * WIZA_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/wiza/index.ts b/apps/sim/tools/wiza/index.ts index 21df6dec38d..289503b4da1 100644 --- a/apps/sim/tools/wiza/index.ts +++ b/apps/sim/tools/wiza/index.ts @@ -1,6 +1,5 @@ export { wizaCompanyEnrichmentTool } from './company_enrichment' export { wizaGetCreditsTool } from './get_credits' -export { wizaGetIndividualRevealTool } from './get_individual_reveal' +export { wizaIndividualRevealTool } from './individual_reveal' export { wizaProspectSearchTool } from './prospect_search' -export { wizaStartIndividualRevealTool } from './start_individual_reveal' export type * from './types' diff --git a/apps/sim/tools/wiza/individual_reveal.ts b/apps/sim/tools/wiza/individual_reveal.ts new file mode 100644 index 00000000000..291d1791f67 --- /dev/null +++ b/apps/sim/tools/wiza/individual_reveal.ts @@ -0,0 +1,330 @@ +import { sleep } from '@sim/utils/helpers' +import type { ToolConfig } from '@/tools/types' +import { wizaHosting } from '@/tools/wiza/hosting' +import type { + WizaIndividualRevealData, + WizaIndividualRevealParams, + WizaIndividualRevealResponse, +} from '@/tools/wiza/types' + +const POLL_INTERVAL_MS = 2000 +const MAX_POLL_TIME_MS = 120000 +/** Tolerate brief Wiza outages while polling before giving up on an already-started reveal. */ +const MAX_CONSECUTIVE_POLL_ERRORS = 3 + +/** Whether a reveal payload has reached a terminal state and no longer needs polling. */ +function isTerminalReveal(d: { status?: string | null; is_complete?: boolean | null }): boolean { + return d.status === 'finished' || d.status === 'failed' || d.is_complete === true +} + +/** Map a Wiza individual-reveal payload (`data` object) to the tool output shape. */ +function mapRevealData(d: Record): WizaIndividualRevealData { + const emails = Array.isArray(d.emails) ? (d.emails as Record[]) : [] + const phones = Array.isArray(d.phones) ? (d.phones as Record[]) : [] + return { + id: (d.id as number) ?? null, + status: (d.status as string) ?? null, + is_complete: (d.is_complete as boolean) ?? null, + name: (d.name as string) ?? null, + company: (d.company as string) ?? null, + enrichment_level: (d.enrichment_level as string) ?? null, + linkedin_profile_url: (d.linkedin_profile_url as string) ?? null, + title: (d.title as string) ?? null, + location: (d.location as string) ?? null, + email: (d.email as string) ?? null, + email_type: (d.email_type as string) ?? null, + email_status: (d.email_status as string) ?? null, + emails: emails.map((e) => ({ + email: (e.email as string) ?? null, + email_type: (e.email_type as string) ?? null, + email_status: (e.email_status as string) ?? null, + })), + mobile_phone: (d.mobile_phone as string) ?? null, + phone_number: (d.phone_number as string) ?? null, + phone_status: (d.phone_status as string) ?? null, + phones: phones.map((p) => ({ + number: (p.number as string) ?? null, + pretty_number: (p.pretty_number as string) ?? null, + type: (p.type as string) ?? null, + })), + company_size: (d.company_size as number) ?? null, + company_size_range: (d.company_size_range as string) ?? null, + company_type: (d.company_type as string) ?? null, + company_domain: (d.company_domain as string) ?? null, + company_locality: (d.company_locality as string) ?? null, + company_region: (d.company_region as string) ?? null, + company_country: (d.company_country as string) ?? null, + company_street: (d.company_street as string) ?? null, + company_postal_code: (d.company_postal_code as string) ?? null, + company_founded: (d.company_founded as number) ?? null, + company_funding: (d.company_funding as string) ?? null, + company_revenue: (d.company_revenue as string) ?? null, + company_industry: (d.company_industry as string) ?? null, + company_subindustry: (d.company_subindustry as string) ?? null, + company_linkedin: (d.company_linkedin as string) ?? null, + company_location: (d.company_location as string) ?? null, + company_description: (d.company_description as string) ?? null, + credits: (d.credits as Record) ?? null, + } +} + +export const wizaIndividualRevealTool: ToolConfig< + WizaIndividualRevealParams, + WizaIndividualRevealResponse +> = { + id: 'wiza_individual_reveal', + name: 'Wiza Individual Reveal', + description: + 'Reveal a contact via LinkedIn URL, name + company/domain, or email. Starts the reveal and polls until it resolves. Uses 2 credits per valid email and 5 credits per phone, charged only on success.', + version: '1.0.0', + + hosting: wizaHosting((_params, output) => { + let credits = 0 + const emails = Array.isArray(output.emails) + ? (output.emails as { email_status?: string }[]) + : [] + const emailValid = + output.email_status === 'valid' || emails.some((e) => e.email_status === 'valid') + // 2 credits when at least one valid email is returned. + if (emailValid) credits += 2 + const phones = Array.isArray(output.phones) ? output.phones : [] + const phoneFound = Boolean(output.mobile_phone || output.phone_number || phones.length > 0) + // 5 credits when at least one phone is returned. + if (phoneFound) credits += 5 + return credits + }), + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Wiza API key', + }, + enrichment_level: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Enrichment depth: none, partial, phone, or full', + }, + profile_url: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'LinkedIn profile URL (e.g., https://linkedin.com/in/johndoe)', + }, + full_name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Full name (used with company or domain)', + }, + company: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company name (used with full_name)', + }, + domain: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Company domain (used with full_name)', + }, + email: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Email address (use alone or with other identifiers)', + }, + accept_work: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to accept work emails (email_options)', + }, + accept_personal: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to accept personal emails (email_options)', + }, + }, + + request: { + url: 'https://wiza.co/api/individual_reveals', + method: 'POST', + headers: (params: WizaIndividualRevealParams) => ({ + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }), + body: (params: WizaIndividualRevealParams) => { + const individual: Record = {} + if (params.profile_url) individual.profile_url = params.profile_url + if (params.full_name) individual.full_name = params.full_name + if (params.company) individual.company = params.company + if (params.domain) individual.domain = params.domain + if (params.email) individual.email = params.email + + const body: Record = { + individual_reveal: individual, + enrichment_level: params.enrichment_level, + } + + if (params.accept_work !== undefined || params.accept_personal !== undefined) { + const emailOptions: Record = {} + if (params.accept_work !== undefined) emailOptions.accept_work = params.accept_work + if (params.accept_personal !== undefined) { + emailOptions.accept_personal = params.accept_personal + } + body.email_options = emailOptions + } + + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Wiza API error: ${response.status} - ${errorText}`) + } + const json = await response.json() + return { + success: true, + output: mapRevealData(json.data ?? {}), + } + }, + + postProcess: async (result, params) => { + if (!result.success) return result + + // Wiza can resolve synchronously (e.g. a cache hit) — the initial POST payload is + // already mapped, so skip polling when it is terminal. + if (isTerminalReveal(result.output)) { + return { success: result.output.status !== 'failed', output: result.output } + } + + const revealId = result.output.id + if (revealId == null) { + // Return an explicit failure rather than throwing: a thrown error here is swallowed + // by the executor and masked as the queued (incomplete) success result. + return { + success: false, + error: 'Wiza individual reveal did not return an id', + output: result.output, + } + } + + let elapsedTime = 0 + let consecutiveErrors = 0 + while (elapsedTime < MAX_POLL_TIME_MS) { + await sleep(POLL_INTERVAL_MS) + elapsedTime += POLL_INTERVAL_MS + + const statusResponse = await fetch( + `https://wiza.co/api/individual_reveals/${encodeURIComponent(String(revealId))}`, + { + headers: { + Authorization: `Bearer ${params.apiKey}`, + 'Content-Type': 'application/json', + }, + } + ) + + if (!statusResponse.ok) { + // The reveal is already started (and billed by Wiza), so tolerate brief outages and + // retry rather than aborting the whole window on a single transient 5xx/429. + consecutiveErrors += 1 + if (consecutiveErrors >= MAX_CONSECUTIVE_POLL_ERRORS) { + const errorText = await statusResponse.text().catch(() => '') + return { + success: false, + error: `Wiza API error: ${statusResponse.status} - ${errorText}`, + output: result.output, + } + } + continue + } + consecutiveErrors = 0 + + const json = await statusResponse.json() + const data = json.data ?? {} + + if (isTerminalReveal(data)) { + return { + success: data.status !== 'failed', + output: mapRevealData(data), + } + } + } + + return { + success: false, + error: 'Wiza individual reveal did not complete within the polling window', + output: result.output, + } + }, + + outputs: { + id: { type: 'number', description: 'Reveal ID' }, + status: { type: 'string', description: 'queued | resolving | finished | failed' }, + is_complete: { type: 'boolean', description: 'Whether the reveal has completed' }, + name: { type: 'string', description: 'Full name', optional: true }, + company: { type: 'string', description: 'Company name', optional: true }, + enrichment_level: { type: 'string', description: 'Enrichment level used', optional: true }, + linkedin_profile_url: { type: 'string', description: 'LinkedIn URL', optional: true }, + title: { type: 'string', description: 'Job title', optional: true }, + location: { type: 'string', description: 'Location', optional: true }, + email: { type: 'string', description: 'Primary email', optional: true }, + email_type: { type: 'string', description: 'Email type', optional: true }, + email_status: { type: 'string', description: 'valid | risky | unfound', optional: true }, + emails: { + type: 'array', + description: 'All emails found', + optional: true, + items: { + type: 'object', + properties: { + email: { type: 'string' }, + email_type: { type: 'string' }, + email_status: { type: 'string' }, + }, + }, + }, + mobile_phone: { type: 'string', description: 'Mobile phone', optional: true }, + phone_number: { type: 'string', description: 'Direct/office phone', optional: true }, + phone_status: { type: 'string', description: 'found | unfound', optional: true }, + phones: { + type: 'array', + description: 'All phones found', + optional: true, + items: { + type: 'object', + properties: { + number: { type: 'string' }, + pretty_number: { type: 'string' }, + type: { type: 'string' }, + }, + }, + }, + company_size: { type: 'number', description: 'Employee count', optional: true }, + company_size_range: { type: 'string', description: 'Headcount range', optional: true }, + company_type: { type: 'string', description: 'Company type', optional: true }, + company_domain: { type: 'string', description: 'Company domain', optional: true }, + company_locality: { type: 'string', description: 'City', optional: true }, + company_region: { type: 'string', description: 'State/region', optional: true }, + company_country: { type: 'string', description: 'Country', optional: true }, + company_street: { type: 'string', description: 'Street', optional: true }, + company_postal_code: { type: 'string', description: 'Postal code', optional: true }, + company_founded: { type: 'number', description: 'Year founded', optional: true }, + company_funding: { type: 'string', description: 'Funding total', optional: true }, + company_revenue: { type: 'string', description: 'Revenue', optional: true }, + company_industry: { type: 'string', description: 'Industry', optional: true }, + company_subindustry: { type: 'string', description: 'Subindustry', optional: true }, + company_linkedin: { type: 'string', description: 'Company LinkedIn URL', optional: true }, + company_location: { type: 'string', description: 'Full company location', optional: true }, + company_description: { type: 'string', description: 'Company description', optional: true }, + credits: { type: 'json', description: 'Credits consumed by the reveal', optional: true }, + }, +} diff --git a/apps/sim/tools/wiza/prospect_search.ts b/apps/sim/tools/wiza/prospect_search.ts index c507f0cbb61..fb460b39b63 100644 --- a/apps/sim/tools/wiza/prospect_search.ts +++ b/apps/sim/tools/wiza/prospect_search.ts @@ -1,4 +1,5 @@ import type { ToolConfig } from '@/tools/types' +import { wizaHosting } from '@/tools/wiza/hosting' import type { WizaProspectSearchParams, WizaProspectSearchResponse } from '@/tools/wiza/types' export const wizaProspectSearchTool: ToolConfig< @@ -10,6 +11,12 @@ export const wizaProspectSearchTool: ToolConfig< description: "Search Wiza's database of prospects using person, company, and financial filters", version: '1.0.0', + hosting: wizaHosting(() => { + // Prospect search returns profiles without contact data and consumes no credits; + // Wiza charges only on reveal/enrichment. + return 0 + }), + params: { apiKey: { type: 'string', diff --git a/apps/sim/tools/wiza/start_individual_reveal.ts b/apps/sim/tools/wiza/start_individual_reveal.ts deleted file mode 100644 index 902c51f8723..00000000000 --- a/apps/sim/tools/wiza/start_individual_reveal.ts +++ /dev/null @@ -1,151 +0,0 @@ -import type { ToolConfig } from '@/tools/types' -import type { - WizaStartIndividualRevealParams, - WizaStartIndividualRevealResponse, -} from '@/tools/wiza/types' - -export const wizaStartIndividualRevealTool: ToolConfig< - WizaStartIndividualRevealParams, - WizaStartIndividualRevealResponse -> = { - id: 'wiza_start_individual_reveal', - name: 'Wiza Start Individual Reveal', - description: - 'Start an individual reveal to enrich a contact via LinkedIn URL, name+company, or email', - version: '1.0.0', - - params: { - apiKey: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Wiza API key', - }, - enrichment_level: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'Enrichment depth: none, partial, phone, or full', - }, - profile_url: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'LinkedIn profile URL (e.g., https://linkedin.com/in/johndoe)', - }, - full_name: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Full name (used with company or domain)', - }, - company: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Company name (used with full_name)', - }, - domain: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Company domain (used with full_name)', - }, - email: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Email address (use alone or with other identifiers)', - }, - accept_work: { - type: 'boolean', - required: false, - visibility: 'user-or-llm', - description: 'Whether to accept work emails (email_options)', - }, - accept_personal: { - type: 'boolean', - required: false, - visibility: 'user-or-llm', - description: 'Whether to accept personal emails (email_options)', - }, - callback_url: { - type: 'string', - required: false, - visibility: 'user-or-llm', - description: 'Optional URL to receive a callback with the reveal update', - }, - }, - - request: { - url: 'https://wiza.co/api/individual_reveals', - method: 'POST', - headers: (params: WizaStartIndividualRevealParams) => ({ - Authorization: `Bearer ${params.apiKey}`, - 'Content-Type': 'application/json', - }), - body: (params: WizaStartIndividualRevealParams) => { - const individual: Record = {} - if (params.profile_url) individual.profile_url = params.profile_url - if (params.full_name) individual.full_name = params.full_name - if (params.company) individual.company = params.company - if (params.domain) individual.domain = params.domain - if (params.email) individual.email = params.email - - const body: Record = { - individual_reveal: individual, - enrichment_level: params.enrichment_level, - } - - if (params.accept_work !== undefined || params.accept_personal !== undefined) { - const emailOptions: Record = {} - if (params.accept_work !== undefined) emailOptions.accept_work = params.accept_work - if (params.accept_personal !== undefined) { - emailOptions.accept_personal = params.accept_personal - } - body.email_options = emailOptions - } - - if (params.callback_url) body.callback_url = params.callback_url - - return body - }, - }, - - transformResponse: async (response: Response) => { - if (!response.ok) { - const errorText = await response.text() - throw new Error(`Wiza API error: ${response.status} - ${errorText}`) - } - - const json = await response.json() - const d = json.data ?? {} - - return { - success: true, - output: { - id: d.id ?? null, - status: d.status ?? null, - is_complete: d.is_complete ?? null, - }, - } - }, - - outputs: { - id: { - type: 'number', - description: 'Individual reveal ID (use with Get Individual Reveal)', - optional: true, - }, - status: { - type: 'string', - description: 'Reveal status: queued, resolving, finished, or failed', - optional: true, - }, - is_complete: { - type: 'boolean', - description: 'Whether the reveal has completed', - optional: true, - }, - }, -} diff --git a/apps/sim/tools/wiza/types.ts b/apps/sim/tools/wiza/types.ts index 00db0a23563..603a3fa933f 100644 --- a/apps/sim/tools/wiza/types.ts +++ b/apps/sim/tools/wiza/types.ts @@ -105,7 +105,7 @@ export interface WizaCompanyEnrichmentResponse extends ToolResponse { } } -export interface WizaStartIndividualRevealParams { +export interface WizaIndividualRevealParams { apiKey: string enrichment_level: 'none' | 'partial' | 'phone' | 'full' profile_url?: string @@ -115,10 +115,9 @@ export interface WizaStartIndividualRevealParams { email?: string accept_work?: boolean accept_personal?: boolean - callback_url?: string } -interface WizaIndividualRevealData { +export interface WizaIndividualRevealData { id: number | null status: string | null is_complete: boolean | null @@ -164,20 +163,7 @@ interface WizaIndividualRevealData { credits: Record | null } -export interface WizaStartIndividualRevealResponse extends ToolResponse { - output: { - id: number | null - status: string | null - is_complete: boolean | null - } -} - -export interface WizaGetIndividualRevealParams { - apiKey: string - id: string -} - -export interface WizaGetIndividualRevealResponse extends ToolResponse { +export interface WizaIndividualRevealResponse extends ToolResponse { output: WizaIndividualRevealData } @@ -185,5 +171,4 @@ export type WizaResponse = | WizaGetCreditsResponse | WizaProspectSearchResponse | WizaCompanyEnrichmentResponse - | WizaStartIndividualRevealResponse - | WizaGetIndividualRevealResponse + | WizaIndividualRevealResponse From 925dd871e82cc2be4e62a4a6d35668853cd1d564 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 29 May 2026 15:56:11 -0700 Subject: [PATCH 11/14] fix(tables): reduce column header chevron size and fix sidebar shadow bleed (#4800) --- .../column-config-sidebar/column-config-sidebar.tsx | 4 ++-- .../components/enrichments-sidebar/enrichments-sidebar.tsx | 4 ++-- .../components/table-grid/headers/column-header-menu.tsx | 2 +- .../components/workflow-sidebar/workflow-sidebar.tsx | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx index 4905edb3035..d7651bd3c64 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx @@ -53,8 +53,8 @@ export function ColumnConfigSidebar(props: ColumnConfigSidebarProps) { role='dialog' aria-label='Configure column' className={cn( - 'absolute top-0 right-0 bottom-0 z-[var(--z-modal)] flex w-[400px] flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--bg)] shadow-overlay transition-transform duration-200 ease-out', - open ? 'translate-x-0' : 'translate-x-full' + 'absolute top-0 right-0 bottom-0 z-[var(--z-modal)] flex w-[400px] flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--bg)] transition-transform duration-200 ease-out', + open ? 'translate-x-0 shadow-overlay' : 'translate-x-full' )} > {props.config && ( diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx index 3594d0ca029..b77fc386db8 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx @@ -33,8 +33,8 @@ export function EnrichmentsSidebar({ open, ...rest }: EnrichmentsSidebarProps) { role='dialog' aria-label='Enrichments' className={cn( - 'absolute top-0 right-0 bottom-0 z-[var(--z-modal)] flex w-[400px] flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--bg)] shadow-overlay transition-transform duration-200 ease-out', - open ? 'translate-x-0' : 'translate-x-full' + 'absolute top-0 right-0 bottom-0 z-[var(--z-modal)] flex w-[400px] flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--bg)] transition-transform duration-200 ease-out', + open ? 'translate-x-0 shadow-overlay' : 'translate-x-full' )} > {open && } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx index 7a76d6ee9be..463927819f7 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx @@ -317,7 +317,7 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({ draggable={false} aria-label='Column options' > - + {props.config && ( From e1e773f4878617b34c1360ad92860c0314779a94 Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 29 May 2026 16:33:30 -0700 Subject: [PATCH 12/14] feat(slack): add install + privacy section to integration landing page (#4799) * feat(slack): add install + privacy section to integration landing page Adds a hand-authored, slug-keyed landing-content module (separate from the generated integrations.json so it survives regeneration) and renders an install walkthrough + privacy-policy link on integration pages when present. Also refreshes generated docs (data-enrichment entry, icon mappings, tool mdx). * fix(landing): render privacy section independently, align CTA analytics label * docs(landing): clarify the Slack install button is behind sign-in * refactor(landing): bake integration landing content into generated json via docs-gen Moves landing content (install walkthrough + privacy) out of a render-time augment and into the generation pipeline: generate-docs reads the pure-data content map and writes landingContent into integrations.json, so the page reads a single source (integration.landingContent). Canonical types live in integrations/data/types.ts. --- apps/docs/components/icons.tsx | 18 ++++- apps/docs/components/ui/icon-mapping.ts | 5 +- apps/docs/content/docs/en/tools/apollo.mdx | 7 +- .../docs/content/docs/en/tools/enrichment.mdx | 13 +--- apps/docs/content/docs/en/tools/resend.mdx | 27 +++++-- apps/docs/content/docs/en/tools/wiza.mdx | 2 +- apps/docs/content/docs/en/tools/zoominfo.mdx | 4 +- .../integrations/(shell)/[slug]/page.tsx | 73 +++++++++++++++++++ .../integrations/data/icon-mapping.ts | 5 +- .../integrations/data/integrations.json | 50 ++++++++++++- .../integrations/data/landing-content.ts | 41 +++++++++++ .../app/(landing)/integrations/data/types.ts | 24 ++++++ scripts/generate-docs.ts | 17 ++++- 13 files changed, 254 insertions(+), 32 deletions(-) create mode 100644 apps/sim/app/(landing)/integrations/data/landing-content.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 765920716b2..7985328c089 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1,6 +1,22 @@ import type { SVGProps } from 'react' import { useId } from 'react' +export function EnrichmentIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function AgentMailIcon(props: SVGProps) { return ( @@ -4731,7 +4747,7 @@ export function ZoomInfoIcon(props: SVGProps) { return (

+ {landingContent.install.heading} +

+

+ {landingContent.install.intro} +

+
    + {landingContent.install.steps.map((item, index) => ( +
  1. + +
    +

    + {item.title} +

    +

    + {item.body} +

    +
    +
  2. + ))} +
+
+ + Add to {name} + +
+ +
+ + )} + + {/* Privacy & data (integration-specific) */} + {landingContent?.privacy && ( + <> +
+

+ Privacy & data +

+

+ {landingContent.privacy.body}{' '} + + Privacy Policy + + . +

+
+
+ + )} + {/* How to automate */}

= { ashby: AshbyIcon, athena: AthenaIcon, attio: AttioIcon, - azure_devops: AzureDevOpsIcon, + azure_devops: AzureIcon, box: BoxCompanyIcon, brandfetch: BrandfetchIcon, brightdata: BrightDataIcon, @@ -262,6 +262,7 @@ export const blockTypeToIconMap: Record = { elevenlabs: ElevenLabsIcon, emailbison: EmailBisonIcon, enrich: EnrichSoIcon, + enrichment: EnrichmentIcon, evernote: EvernoteIcon, exa: ExaAIIcon, extend_v2: ExtendIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index d859c6c3223..8fc5c3856ff 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -1889,7 +1889,7 @@ "description": "Interact with Azure DevOps pipelines, builds, and work items", "longDescription": "Integrate Azure DevOps into your workflow. List and inspect pipelines and builds, query and manage work items, and add or read comments.", "bgColor": "#0078D4", - "iconName": "AzureDevOpsIcon", + "iconName": "AzureIcon", "docsUrl": "https://docs.sim.ai/tools/azure_devops", "operations": [ { @@ -3137,6 +3137,24 @@ "integrationTypes": ["analytics", "developer-tools"], "tags": ["data-analytics", "automation"] }, + { + "type": "enrichment", + "slug": "data-enrichment", + "name": "Data Enrichment", + "description": "Enrich data with a Sim enrichment", + "longDescription": "Run a Sim enrichment to look up data — work email, phone number, company domain, company info, and more — from the fields you map in. Uses the same provider cascade as table enrichments.", + "bgColor": "#9333EA", + "iconName": "EnrichmentIcon", + "docsUrl": "https://docs.sim.ai/tools/enrichment", + "operations": [], + "operationCount": 0, + "triggers": [], + "triggerCount": 0, + "authType": "none", + "category": "tools", + "integrationTypes": ["sales"], + "tags": ["enrichment"] + }, { "type": "databricks", "slug": "databricks", @@ -13288,7 +13306,35 @@ "authType": "oauth", "category": "tools", "integrationTypes": ["communication", "developer-tools"], - "tags": ["messaging", "webhooks", "automation"] + "tags": ["messaging", "webhooks", "automation"], + "landingContent": { + "install": { + "heading": "Add Sim to your Slack workspace", + "intro": "Sim connects to Slack through Slack’s official OAuth flow. The “Add to Slack” button lives inside your Sim account (after sign-in) — connect from there and the Sim bot is installed in your Slack workspace. The steps below show exactly how to reach it.", + "steps": [ + { + "title": "Create your free Sim account", + "body": "Sign up at sim.ai — no credit card required." + }, + { + "title": "Add a Slack block", + "body": "Open a workflow, drag in a Slack block, and open its credential dropdown." + }, + { + "title": "Connect Slack", + "body": "Click Connect Slack, choose your workspace, and approve the requested permissions. This installs the Sim bot in your Slack workspace." + }, + { + "title": "Invite the bot and build", + "body": "Invite the Sim bot to the channels it should act in, pick a Slack action, wire it into your agent, and run." + } + ] + }, + "privacy": { + "body": "Sim requests only the Slack permissions its actions and triggers need, and never shows private channel names or messages to people who are not members of those channels in Slack.", + "href": "/privacy" + } + } }, { "type": "smtp", diff --git a/apps/sim/app/(landing)/integrations/data/landing-content.ts b/apps/sim/app/(landing)/integrations/data/landing-content.ts new file mode 100644 index 00000000000..7004c1f785a --- /dev/null +++ b/apps/sim/app/(landing)/integrations/data/landing-content.ts @@ -0,0 +1,41 @@ +/** + * Hand-authored, integration-specific landing content, keyed by integration + * slug. This is a pure-data generation input: `scripts/generate-docs.ts` reads + * it and bakes the matching entry into `integrations.json`, so the landing page + * consumes a single source (`integration.landingContent`) with no render-time + * augmentation. Has no app imports so the build script can import it safely. + */ + +import type { IntegrationLandingContent } from '@/app/(landing)/integrations/data/types' + +export const INTEGRATION_LANDING_CONTENT: Record = { + slack: { + install: { + heading: 'Add Sim to your Slack workspace', + intro: + 'Sim connects to Slack through Slack’s official OAuth flow. The “Add to Slack” button lives inside your Sim account (after sign-in) — connect from there and the Sim bot is installed in your Slack workspace. The steps below show exactly how to reach it.', + steps: [ + { + title: 'Create your free Sim account', + body: 'Sign up at sim.ai — no credit card required.', + }, + { + title: 'Add a Slack block', + body: 'Open a workflow, drag in a Slack block, and open its credential dropdown.', + }, + { + title: 'Connect Slack', + body: 'Click Connect Slack, choose your workspace, and approve the requested permissions. This installs the Sim bot in your Slack workspace.', + }, + { + title: 'Invite the bot and build', + body: 'Invite the Sim bot to the channels it should act in, pick a Slack action, wire it into your agent, and run.', + }, + ], + }, + privacy: { + body: 'Sim requests only the Slack permissions its actions and triggers need, and never shows private channel names or messages to people who are not members of those channels in Slack.', + href: '/privacy', + }, + }, +} diff --git a/apps/sim/app/(landing)/integrations/data/types.ts b/apps/sim/app/(landing)/integrations/data/types.ts index 7ba6483d6b8..bcdc0f732ac 100644 --- a/apps/sim/app/(landing)/integrations/data/types.ts +++ b/apps/sim/app/(landing)/integrations/data/types.ts @@ -19,6 +19,29 @@ export interface FAQItem { answer: string } +export interface IntegrationInstallStep { + title: string + body: string +} + +export interface IntegrationLandingContent { + /** + * Install walkthrough for OAuth apps whose connection lives behind sign-in. + * Provides the "Add to {app}" instructions that app marketplaces require + * when the install button sits behind a login. + */ + install?: { + heading: string + intro: string + steps: IntegrationInstallStep[] + } + /** Short data-handling summary shown next to a privacy-policy link. */ + privacy?: { + body: string + href: string + } +} + export interface Integration { type: string slug: string @@ -36,4 +59,5 @@ export interface Integration { category: string integrationTypes?: string[] tags?: string[] + landingContent?: IntegrationLandingContent } diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index c2c50cbbc6a..9b806264a4d 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -1,7 +1,7 @@ #!/usr/bin/env ts-node import fs from 'fs' import path from 'path' -import { fileURLToPath } from 'url' +import { fileURLToPath, pathToFileURL } from 'url' import { glob } from 'glob' console.log('Starting documentation generator...') @@ -183,6 +183,7 @@ interface IntegrationEntry { category: string integrationTypes?: string[] tags?: string[] + landingContent?: Record } /** @@ -665,6 +666,19 @@ async function writeIntegrationsJson(iconMapping: Record): Promi const triggerRegistry = await buildTriggerRegistry() const { desc: toolDescMap, name: toolNameMap } = await buildToolDescriptionMap() + + // Hand-authored, integration-specific landing content (install walkthrough, + // privacy blurb), keyed by slug. Imported as pure data — its only import is + // type-only and erased at runtime — and baked into the entries below so the + // landing page reads a single source instead of augmenting at render time. + const landingContentModule = await import( + pathToFileURL(path.join(LANDING_INTEGRATIONS_DATA_PATH, 'landing-content.ts')).href + ) + const landingContentMap = (landingContentModule.INTEGRATION_LANDING_CONTENT ?? {}) as Record< + string, + Record + > + const integrations: IntegrationEntry[] = [] const seenBaseTypes = new Set() const blockFiles = (await glob(`${BLOCKS_PATH}/*.ts`)).sort() @@ -778,6 +792,7 @@ async function writeIntegrationsJson(iconMapping: Record): Promi } : {}), ...(config.tags ? { tags: config.tags } : {}), + ...(landingContentMap[slug] ? { landingContent: landingContentMap[slug] } : {}), }) } } From f9867c7331e8be4fa9b4a019c70d93170148664c Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 29 May 2026 16:49:58 -0700 Subject: [PATCH 13/14] improvement(enrichments): align enrichments sidebar with design system (#4801) * improvement(enrichments): align enrichments sidebar with design system * fix(enrichments): consistent close button pattern and fix url link hover --- .../enrichments-sidebar/enrichment-config.tsx | 3 +- .../enrichments-sidebar.tsx | 30 ++++++++----------- .../table-grid/cells/cell-render.tsx | 2 +- apps/sim/enrichments/work-email/work-email.ts | 2 +- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx index 2e021797858..257c66861af 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichment-config.tsx @@ -3,7 +3,6 @@ import { useState } from 'react' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' -import { X } from 'lucide-react' import { Badge, Button, @@ -15,7 +14,7 @@ import { Switch, toast, } from '@/components/emcn' -import { ArrowLeft } from '@/components/emcn/icons' +import { ArrowLeft, X } from '@/components/emcn/icons' import type { AddWorkflowGroupBodyInput } from '@/lib/api/contracts/tables' import { cn } from '@/lib/core/utils/cn' import type { ColumnDefinition, WorkflowGroup, WorkflowGroupOutput } from '@/lib/table' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx index b77fc386db8..a2575b4b43f 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/enrichments-sidebar/enrichments-sidebar.tsx @@ -1,9 +1,8 @@ 'use client' import { useState } from 'react' -import { X } from 'lucide-react' -import { Button, Input } from '@/components/emcn' -import { Search } from '@/components/emcn/icons' +import { Input } from '@/components/emcn' +import { Search, X } from '@/components/emcn/icons' import { cn } from '@/lib/core/utils/cn' import type { ColumnDefinition, WorkflowGroup } from '@/lib/table' import { ALL_ENRICHMENTS } from '@/enrichments' @@ -75,15 +74,14 @@ function EnrichmentsSidebarBody({

Enrichment

- +

@@ -121,15 +119,14 @@ function EnrichmentsSidebarBody({

Enrichments

- +
@@ -155,11 +152,10 @@ function EnrichmentsSidebarBody({ const Icon = enrichment.icon return (
  • - +
  • ) })} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx index 91cabea7994..fe6a6bfd3da 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx @@ -320,7 +320,7 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle target='_blank' rel='noopener noreferrer' className={cn( - 'min-w-0 overflow-clip text-ellipsis text-[var(--text-primary)] underline underline-offset-2 hover:opacity-70', + 'min-w-0 overflow-clip text-ellipsis text-[var(--text-primary)] underline underline-offset-2 transition-colors hover-hover:text-[var(--text-secondary)]', isEditing && 'pointer-events-none' )} onClick={(e) => e.stopPropagation()} diff --git a/apps/sim/enrichments/work-email/work-email.ts b/apps/sim/enrichments/work-email/work-email.ts index 4166217a1c9..2076e18eb46 100644 --- a/apps/sim/enrichments/work-email/work-email.ts +++ b/apps/sim/enrichments/work-email/work-email.ts @@ -1,5 +1,5 @@ import { filterUndefined } from '@sim/utils/object' -import { Mail } from 'lucide-react' +import { Mail } from '@/components/emcn/icons' import { normalizeDomain, splitName, str, toolProvider } from '@/enrichments/providers' import type { EnrichmentConfig } from '@/enrichments/types' From c51c41f0f9b31155a97237ba28eea6965a177a12 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 29 May 2026 17:29:52 -0700 Subject: [PATCH 14/14] fix(misc): upgrade path change for new better-auth version, billing issue for workflow block agent usage (#4803) * fix(misc): upgrade path change for new better-auth version, double-billing for workflow block agent usage * fail loudly if stripe sub id missing --- apps/sim/lib/billing/client/upgrade.ts | 47 ++++++++--- .../logs/execution/logging-factory.test.ts | 82 +++++++++++++++++++ .../sim/lib/logs/execution/logging-factory.ts | 23 ++++-- 3 files changed, 135 insertions(+), 17 deletions(-) diff --git a/apps/sim/lib/billing/client/upgrade.ts b/apps/sim/lib/billing/client/upgrade.ts index bbd0b5f8fe7..eae23a285d5 100644 --- a/apps/sim/lib/billing/client/upgrade.ts +++ b/apps/sim/lib/billing/client/upgrade.ts @@ -41,7 +41,8 @@ export function useSubscriptionUpgrade() { throw new Error('User not authenticated') } - let currentSubscriptionId: string | undefined + let currentSubscriptionRowId: string | undefined + let currentStripeSubscriptionId: string | undefined let allSubscriptions: any[] = [] try { const listResult = await client.subscription.list() @@ -49,9 +50,22 @@ export function useSubscriptionUpgrade() { const activePersonalSub = allSubscriptions.find( (sub: any) => hasPaidSubscriptionStatus(sub.status) && sub.referenceId === userId ) - currentSubscriptionId = activePersonalSub?.id + currentSubscriptionRowId = activePersonalSub?.id + currentStripeSubscriptionId = activePersonalSub?.stripeSubscriptionId } catch (_e) { - currentSubscriptionId = undefined + currentSubscriptionRowId = undefined + currentStripeSubscriptionId = undefined + } + + if (currentSubscriptionRowId && !currentStripeSubscriptionId) { + logger.error('Active paid subscription is missing its Stripe subscription ID', { + userId, + subscriptionRowId: currentSubscriptionRowId, + targetPlan, + }) + throw new Error( + 'We could not match your current plan with our payment provider. Please contact support before upgrading so you are not charged twice.' + ) } let referenceId = userId @@ -137,36 +151,45 @@ export function useSubscriptionUpgrade() { ...(annual && { annual: true }), } as const - const finalParams = currentSubscriptionId - ? { ...upgradeParams, subscriptionId: currentSubscriptionId } + const finalParams = currentStripeSubscriptionId + ? { ...upgradeParams, subscriptionId: currentStripeSubscriptionId } : upgradeParams logger.info( - currentSubscriptionId ? 'Upgrading existing subscription' : 'Creating new subscription', - { targetPlan, planName, annual, currentSubscriptionId, referenceId } + currentStripeSubscriptionId + ? 'Upgrading existing subscription' + : 'Creating new subscription', + { + targetPlan, + planName, + annual, + currentStripeSubscriptionId, + currentSubscriptionRowId, + referenceId, + } ) await betterAuthSubscription.upgrade(finalParams) - if (targetPlan === 'team' && currentSubscriptionId && referenceId !== userId) { + if (targetPlan === 'team' && currentSubscriptionRowId && referenceId !== userId) { try { logger.info('Transferring subscription to organization after upgrade', { - subscriptionId: currentSubscriptionId, + subscriptionId: currentSubscriptionRowId, organizationId: referenceId, }) try { await requestJson(subscriptionTransferContract, { - params: { id: currentSubscriptionId }, + params: { id: currentSubscriptionRowId }, body: { organizationId: referenceId }, }) logger.info('Successfully transferred subscription to organization', { - subscriptionId: currentSubscriptionId, + subscriptionId: currentSubscriptionRowId, organizationId: referenceId, }) } catch (transferError) { logger.error('Failed to transfer subscription to organization', { - subscriptionId: currentSubscriptionId, + subscriptionId: currentSubscriptionRowId, organizationId: referenceId, error: transferError instanceof ApiClientError diff --git a/apps/sim/lib/logs/execution/logging-factory.test.ts b/apps/sim/lib/logs/execution/logging-factory.test.ts index 62601650774..7ec7fe11d19 100644 --- a/apps/sim/lib/logs/execution/logging-factory.test.ts +++ b/apps/sim/lib/logs/execution/logging-factory.test.ts @@ -576,4 +576,86 @@ describe('calculateCostSummary', () => { expect(Object.keys(result.charges)).toHaveLength(0) expect(result.totalCost).toBe(BASE_EXECUTION_CHARGE) }) + + test('does not double-count the synthetic workflow root (aggregate cost over leaves)', () => { + // buildTraceSpans wraps every run in a synthetic { type: 'workflow' } root + // whose cost.total is the SUM of its leaves. Counting that root in addition + // to the leaves double-charges the run — the root must be a pass-through. + const traceSpans = [ + { + id: 'workflow-execution', + name: 'Workflow Execution', + type: 'workflow', + cost: { total: 0.04 }, // == agent(0.03) + exa(0.01) + children: [ + { + id: 'agent-1', + name: 'Agent', + type: 'agent', + model: 'gpt-4o', + cost: { input: 0.01, output: 0.02, total: 0.03 }, + tokens: { input: 100, output: 200, total: 300 }, + }, + { + id: 'exa-1', + name: 'Exa Search', + type: 'tool', + cost: { input: 0, output: 0, total: 0.01 }, + }, + ], + }, + ] + + const result = calculateCostSummary(traceSpans) + + // The 0.04 root aggregate is NOT added on top of its leaves. + expect(result.charges['Workflow Execution']).toBeUndefined() + expect(result.models['gpt-4o'].total).toBe(0.03) + expect(result.charges['Exa Search'].total).toBe(0.01) + expect(result.totalCost).toBeCloseTo(0.04 + BASE_EXECUTION_CHARGE, 10) + const ledgerSum = + result.baseExecutionCharge + + Object.values(result.models).reduce((s, m) => s + m.total, 0) + + Object.values(result.charges).reduce((s, c) => s + c.total, 0) + expect(ledgerSum).toBeCloseTo(result.totalCost, 10) + }) + + test('does not double-count nested sub-workflow roots', () => { + // A sub-workflow call nests another synthetic { type: 'workflow' } root + // (captureChildWorkflowLogs runs buildTraceSpans on the child). Both the + // outer root and the inner sub-workflow root carry aggregate costs; only the + // leaf agent inside should be billed. + const traceSpans = [ + { + id: 'workflow-execution', + name: 'Workflow Execution', + type: 'workflow', + cost: { total: 0.03 }, + children: [ + { + id: 'subworkflow-root', + name: 'Workflow Execution', + type: 'workflow', + cost: { total: 0.03 }, + children: [ + { + id: 'child-agent', + name: 'Agent', + type: 'agent', + model: 'gpt-4o', + cost: { input: 0.01, output: 0.02, total: 0.03 }, + tokens: { input: 100, output: 200, total: 300 }, + }, + ], + }, + ], + }, + ] + + const result = calculateCostSummary(traceSpans) + + expect(result.charges['Workflow Execution']).toBeUndefined() + expect(result.models['gpt-4o'].total).toBe(0.03) + expect(result.totalCost).toBeCloseTo(0.03 + BASE_EXECUTION_CHARGE, 10) + }) }) diff --git a/apps/sim/lib/logs/execution/logging-factory.ts b/apps/sim/lib/logs/execution/logging-factory.ts index 33582faab01..35699dde70c 100644 --- a/apps/sim/lib/logs/execution/logging-factory.ts +++ b/apps/sim/lib/logs/execution/logging-factory.ts @@ -165,19 +165,32 @@ export function calculateCostSummary(traceSpans: CostTraceSpan[] | undefined): C const costSpans: BillableTraceSpan[] = [] for (const span of spans) { + // `workflow`-typed spans are aggregate containers, not billable units: the + // synthetic "Workflow Execution" root (added to every run by + // buildTraceSpans) and any nested sub-workflow root carry a `cost.total` + // equal to the SUM of their descendants. Counting that aggregate in + // addition to the descendants double-charges the run, so treat these as + // pass-through: never count their own cost, always recurse into all + // children where the real billable leaves (agents, tools) live. + const isAggregateContainer = span.type === 'workflow' const hasOwnCost = hasBillableCost(span) - if (hasOwnCost) { + const countOwnCost = hasOwnCost && !isAggregateContainer + + if (countOwnCost) { costSpans.push(span) } if (span.children && Array.isArray(span.children)) { - if (hasOwnCost) { - // Parent already accounts for its model segments; only recurse into - // non-model children (e.g. nested workflow spans) to find further - // billable units. + if (countOwnCost) { + // Authoritative leaf (e.g. an agent block whose block-level cost is set + // by the provider response and already accounts for its model + // segments): only recurse into non-model children to find further + // standalone billable units, skipping the model-breakdown duplicates. const nonModelChildren = span.children.filter((child) => !isModelBreakdownSpan(child)) costSpans.push(...collectCostSpans(nonModelChildren)) } else { + // Container (workflow / sub-workflow root) or a no-cost parent: recurse + // into everything so nested billable leaves are counted exactly once. costSpans.push(...collectCostSpans(span.children)) } }