From d533ea27e1d729d7b908f3ef6405952d15785fe3 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 15:51:14 -0800 Subject: [PATCH 01/25] feat(canvas): added the ability to lock blocks --- apps/docs/content/docs/en/tools/pulse.mdx | 2 +- .../components/action-bar/action-bar.tsx | 117 +- .../components/block-menu/block-menu.tsx | 31 +- .../components/canvas-menu/canvas-menu.tsx | 6 +- .../panel/components/editor/editor.tsx | 47 +- .../w/[workflowId]/components/panel/panel.tsx | 29 +- .../components/subflows/subflow-node.tsx | 6 +- .../workflow-block/hooks/use-block-state.ts | 8 + .../workflow-block/workflow-block.tsx | 4 +- .../w/[workflowId]/hooks/use-block-visual.ts | 2 + .../hooks/use-canvas-context-menu.ts | 1 + .../w/[workflowId]/utils/auto-layout-utils.ts | 10 + .../[workspaceId]/w/[workflowId]/workflow.tsx | 198 +- apps/sim/hooks/use-collaborative-workflow.ts | 88 +- apps/sim/hooks/use-undo-redo.ts | 106 +- .../tools/server/workflow/edit-workflow.ts | 36 + .../lib/workflows/comparison/compare.test.ts | 20 + apps/sim/lib/workflows/comparison/compare.ts | 2 +- apps/sim/lib/workflows/defaults.ts | 1 + .../lib/workflows/diff/diff-engine.test.ts | 173 + apps/sim/lib/workflows/diff/diff-engine.ts | 1 + apps/sim/lib/workflows/persistence/utils.ts | 2 + apps/sim/socket/constants.ts | 2 + apps/sim/socket/database/operations.ts | 116 +- .../sim/socket/middleware/permissions.test.ts | 6 + apps/sim/socket/middleware/permissions.ts | 1 + apps/sim/socket/validation/schemas.ts | 12 + apps/sim/stores/undo-redo/types.ts | 9 + apps/sim/stores/undo-redo/utils.ts | 9 + apps/sim/stores/workflows/utils.test.ts | 100 + apps/sim/stores/workflows/utils.ts | 2 + .../stores/workflows/workflow/store.test.ts | 149 + apps/sim/stores/workflows/workflow/store.ts | 110 +- apps/sim/stores/workflows/workflow/types.ts | 6 + .../db/migrations/0150_flimsy_hemingway.sql | 1 + .../db/migrations/meta/0150_snapshot.json | 10354 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 1 + .../testing/src/factories/block.factory.ts | 2 + 39 files changed, 11681 insertions(+), 96 deletions(-) create mode 100644 apps/sim/lib/workflows/diff/diff-engine.test.ts create mode 100644 packages/db/migrations/0150_flimsy_hemingway.sql create mode 100644 packages/db/migrations/meta/0150_snapshot.json diff --git a/apps/docs/content/docs/en/tools/pulse.mdx b/apps/docs/content/docs/en/tools/pulse.mdx index 92d2319e00..a804d99529 100644 --- a/apps/docs/content/docs/en/tools/pulse.mdx +++ b/apps/docs/content/docs/en/tools/pulse.mdx @@ -11,7 +11,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" /> {/* MANUAL-CONTENT-START:intro */} -The [Pulse](https://www.pulseapi.com/) tool enables seamless extraction of text and structured content from a wide variety of documents—including PDFs, images, and Office files—using state-of-the-art OCR (Optical Character Recognition) powered by Pulse. Designed for automated agentic workflows, Pulse Parser makes it easy to unlock valuable information trapped in unstructured documents and integrate the extracted content directly into your workflow. +The [Pulse](https://www.runpulse.com) tool enables seamless extraction of text and structured content from a wide variety of documents—including PDFs, images, and Office files—using state-of-the-art OCR (Optical Character Recognition) powered by Pulse. Designed for automated agentic workflows, Pulse Parser makes it easy to unlock valuable information trapped in unstructured documents and integrate the extracted content directly into your workflow. With Pulse, you can: diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx index 3e0d78180c..54659d7420 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx @@ -1,5 +1,5 @@ import { memo, useCallback } from 'react' -import { ArrowLeftRight, ArrowUpDown, Circle, CircleOff, LogOut } from 'lucide-react' +import { ArrowLeftRight, ArrowUpDown, Circle, CircleOff, Lock, LogOut, Unlock } from 'lucide-react' import { Button, Copy, PlayOutline, Tooltip, Trash2 } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers' @@ -49,6 +49,7 @@ export const ActionBar = memo( collaborativeBatchRemoveBlocks, collaborativeBatchToggleBlockEnabled, collaborativeBatchToggleBlockHandles, + collaborativeBatchToggleLocked, } = useCollaborativeWorkflow() const { setPendingSelection } = useWorkflowRegistry() const { handleRunFromBlock } = useWorkflowExecution() @@ -84,21 +85,25 @@ export const ActionBar = memo( ) }, [blockId, addNotification, collaborativeBatchAddBlocks, setPendingSelection]) - const { isEnabled, horizontalHandles, parentId, parentType } = useWorkflowStore( - useCallback( - (state) => { - const block = state.blocks[blockId] - const parentId = block?.data?.parentId - return { - isEnabled: block?.enabled ?? true, - horizontalHandles: block?.horizontalHandles ?? false, - parentId, - parentType: parentId ? state.blocks[parentId]?.type : undefined, - } - }, - [blockId] + const { isEnabled, horizontalHandles, parentId, parentType, isLocked, isParentLocked } = + useWorkflowStore( + useCallback( + (state) => { + const block = state.blocks[blockId] + const parentId = block?.data?.parentId + const parentBlock = parentId ? state.blocks[parentId] : undefined + return { + isEnabled: block?.enabled ?? true, + horizontalHandles: block?.horizontalHandles ?? false, + parentId, + parentType: parentBlock?.type, + isLocked: block?.locked ?? false, + isParentLocked: parentBlock?.locked ?? false, + } + }, + [blockId] + ) ) - ) const { activeWorkflowId } = useWorkflowRegistry() const { isExecuting, getLastExecutionSnapshot } = useExecutionStore() @@ -161,25 +166,27 @@ export const ActionBar = memo( {!isNoteBlock && !isInsideSubflow && ( - + + + {(() => { if (disabled) return getTooltipMessage('Run from block') if (isExecuting) return 'Execution in progress' - if (!dependenciesSatisfied) return 'Run upstream blocks first' + if (!dependenciesSatisfied) return 'Run previous blocks first' return 'Run from block' })()} @@ -193,22 +200,42 @@ export const ActionBar = memo( variant='ghost' onClick={(e) => { e.stopPropagation() - if (!disabled) { + if (!disabled && !isLocked) { collaborativeBatchToggleBlockEnabled([blockId]) } }} className={ACTION_BUTTON_STYLES} - disabled={disabled} + disabled={disabled || isLocked} > {isEnabled ? : } - {getTooltipMessage(isEnabled ? 'Disable Block' : 'Enable Block')} + {isLocked + ? 'Block is locked' + : getTooltipMessage(isEnabled ? 'Disable Block' : 'Enable Block')} )} + {userPermissions.canAdmin && ( + + + + + {isLocked ? 'Unlock Block' : 'Lock Block'} + + )} + {!isStartBlock && !isResponseBlock && ( @@ -237,12 +264,12 @@ export const ActionBar = memo( variant='ghost' onClick={(e) => { e.stopPropagation() - if (!disabled) { + if (!disabled && !isLocked) { collaborativeBatchToggleBlockHandles([blockId]) } }} className={ACTION_BUTTON_STYLES} - disabled={disabled} + disabled={disabled || isLocked} > {horizontalHandles ? ( @@ -252,7 +279,9 @@ export const ActionBar = memo( - {getTooltipMessage(horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports')} + {isLocked + ? 'Block is locked' + : getTooltipMessage(horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports')} )} @@ -264,19 +293,23 @@ export const ActionBar = memo( variant='ghost' onClick={(e) => { e.stopPropagation() - if (!disabled && userPermissions.canEdit) { + if (!disabled && userPermissions.canEdit && !isLocked && !isParentLocked) { window.dispatchEvent( new CustomEvent('remove-from-subflow', { detail: { blockIds: [blockId] } }) ) } }} className={ACTION_BUTTON_STYLES} - disabled={disabled || !userPermissions.canEdit} + disabled={disabled || !userPermissions.canEdit || isLocked || isParentLocked} > - {getTooltipMessage('Remove from Subflow')} + + {isLocked || isParentLocked + ? 'Block is locked' + : getTooltipMessage('Remove from Subflow')} + )} @@ -286,17 +319,19 @@ export const ActionBar = memo( variant='ghost' onClick={(e) => { e.stopPropagation() - if (!disabled) { + if (!disabled && !isLocked) { collaborativeBatchRemoveBlocks([blockId]) } }} className={ACTION_BUTTON_STYLES} - disabled={disabled} + disabled={disabled || isLocked} > - {getTooltipMessage('Delete Block')} + + {isLocked ? 'Block is locked' : getTooltipMessage('Delete Block')} + ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx index e02bf4ff51..65c0a65d21 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx @@ -20,6 +20,7 @@ export interface BlockInfo { horizontalHandles: boolean parentId?: string parentType?: string + locked?: boolean } /** @@ -46,10 +47,17 @@ export interface BlockMenuProps { showRemoveFromSubflow?: boolean /** Whether run from block is available (has snapshot, was executed, not inside subflow) */ canRunFromBlock?: boolean + /** Whether to disable edit actions (user can't edit OR blocks are locked) */ disableEdit?: boolean + /** Whether the user has edit permission (ignoring locked state) */ + userCanEdit?: boolean isExecuting?: boolean /** Whether the selected block is a trigger (has no incoming edges) */ isPositionalTrigger?: boolean + /** Callback to toggle locked state of selected blocks */ + onToggleLocked?: () => void + /** Whether the user has admin permissions */ + canAdmin?: boolean } /** @@ -78,13 +86,18 @@ export function BlockMenu({ showRemoveFromSubflow = false, canRunFromBlock = false, disableEdit = false, + userCanEdit = true, isExecuting = false, isPositionalTrigger = false, + onToggleLocked, + canAdmin = false, }: BlockMenuProps) { const isSingleBlock = selectedBlocks.length === 1 const allEnabled = selectedBlocks.every((b) => b.enabled) const allDisabled = selectedBlocks.every((b) => !b.enabled) + const allLocked = selectedBlocks.every((b) => b.locked) + const allUnlocked = selectedBlocks.every((b) => !b.locked) const hasSingletonBlock = selectedBlocks.some( (b) => @@ -108,6 +121,12 @@ export function BlockMenu({ return 'Toggle Enabled' } + const getToggleLockedLabel = () => { + if (allLocked) return 'Unlock' + if (allUnlocked) return 'Lock' + return 'Toggle Lock' + } + return ( {!hasSingletonBlock && ( { onDuplicate() onClose() @@ -195,6 +214,16 @@ export function BlockMenu({ Remove from Subflow )} + {canAdmin && onToggleLocked && ( + { + onToggleLocked() + onClose() + }} + > + {getToggleLockedLabel()} + + )} {/* Single block actions */} {isSingleBlock && } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx index d7c3e1a5d4..e091849c82 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/canvas-menu/canvas-menu.tsx @@ -34,6 +34,8 @@ export interface CanvasMenuProps { canUndo?: boolean canRedo?: boolean isInvitationsDisabled?: boolean + /** Whether the workflow has locked blocks (disables auto-layout) */ + hasLockedBlocks?: boolean } /** @@ -60,6 +62,7 @@ export function CanvasMenu({ disableEdit = false, canUndo = false, canRedo = false, + hasLockedBlocks = false, }: CanvasMenuProps) { return ( { onAutoLayout() onClose() }} + title={hasLockedBlocks ? 'Unlock blocks to use auto-layout' : undefined} > Auto-layout ⇧L diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index 3d47e1fa4f..e2ed145ed7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -9,6 +9,7 @@ import { ChevronUp, ExternalLink, Loader2, + Lock, Pencil, } from 'lucide-react' import { useParams } from 'next/navigation' @@ -46,6 +47,7 @@ import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { usePanelEditorStore } from '@/stores/panel' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' /** Stable empty object to avoid creating new references */ const EMPTY_SUBBLOCK_VALUES = {} as Record @@ -110,6 +112,14 @@ export function Editor() { // Get user permissions const userPermissions = useUserPermissionsContext() + // Check if block is locked (or inside a locked container) and compute edit permission + // Locked blocks cannot be edited by anyone (admins can only lock/unlock) + const blocks = useWorkflowStore((state) => state.blocks) + const parentId = currentBlock?.data?.parentId as string | undefined + const isParentLocked = parentId ? (blocks[parentId]?.locked ?? false) : false + const isLocked = (currentBlock?.locked ?? false) || isParentLocked + const canEditBlock = userPermissions.canEdit && !isLocked + // Get active workflow ID const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) @@ -150,9 +160,7 @@ export function Editor() { blockSubBlockValues, canonicalIndex ) - const displayAdvancedOptions = userPermissions.canEdit - ? advancedMode - : advancedMode || advancedValuesPresent + const displayAdvancedOptions = canEditBlock ? advancedMode : advancedMode || advancedValuesPresent const hasAdvancedOnlyFields = useMemo(() => { for (const subBlock of subBlocksForCanonical) { @@ -223,9 +231,9 @@ export function Editor() { // Advanced mode toggle handler const handleToggleAdvancedMode = useCallback(() => { - if (!currentBlockId || !userPermissions.canEdit) return + if (!currentBlockId || !canEditBlock) return collaborativeToggleBlockAdvancedMode(currentBlockId) - }, [currentBlockId, userPermissions.canEdit, collaborativeToggleBlockAdvancedMode]) + }, [currentBlockId, canEditBlock, collaborativeToggleBlockAdvancedMode]) // Rename state const [isRenaming, setIsRenaming] = useState(false) @@ -236,10 +244,10 @@ export function Editor() { * Handles starting the rename process. */ const handleStartRename = useCallback(() => { - if (!userPermissions.canEdit || !currentBlock) return + if (!canEditBlock || !currentBlock) return setEditedName(currentBlock.name || '') setIsRenaming(true) - }, [userPermissions.canEdit, currentBlock]) + }, [canEditBlock, currentBlock]) /** * Handles saving the renamed block. @@ -358,6 +366,19 @@ export function Editor() { )}
+ {/* Locked indicator */} + {isLocked && currentBlock && ( + + +
+ +
+
+ +

Block is locked

+
+
+ )} {/* Rename button */} {currentBlock && ( @@ -366,7 +387,7 @@ export function Editor() { variant='ghost' className='p-0' onClick={isRenaming ? handleSaveRename : handleStartRename} - disabled={!userPermissions.canEdit} + disabled={!canEditBlock} aria-label={isRenaming ? 'Save name' : 'Rename block'} > {isRenaming ? ( @@ -434,7 +455,7 @@ export function Editor() { incomingConnections={incomingConnections} handleConnectionsResizeMouseDown={handleConnectionsResizeMouseDown} toggleConnectionsCollapsed={toggleConnectionsCollapsed} - userCanEdit={userPermissions.canEdit} + userCanEdit={canEditBlock} isConnectionsAtMinHeight={isConnectionsAtMinHeight} /> ) : ( @@ -542,14 +563,14 @@ export function Editor() { config={subBlock} isPreview={false} subBlockValues={subBlockState} - disabled={!userPermissions.canEdit} + disabled={!canEditBlock} fieldDiffStatus={undefined} allowExpandInPreview={false} canonicalToggle={ isCanonicalSwap && canonicalMode && canonicalId ? { mode: canonicalMode, - disabled: !userPermissions.canEdit, + disabled: !canEditBlock, onToggle: () => { if (!currentBlockId) return const nextMode = @@ -579,7 +600,7 @@ export function Editor() { ) })} - {hasAdvancedOnlyFields && userPermissions.canEdit && ( + {hasAdvancedOnlyFields && canEditBlock && (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index cea18901e9..c119add2c5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -45,11 +45,13 @@ import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowI import { useDeleteWorkflow, useImportWorkflow } from '@/app/workspace/[workspaceId]/w/hooks' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useChatStore } from '@/stores/chat/store' +import { useNotificationStore } from '@/stores/notifications/store' import type { PanelTab } from '@/stores/panel' import { usePanelStore, useVariablesStore as usePanelVariablesStore } from '@/stores/panel' import { useVariablesStore } from '@/stores/variables/store' import { getWorkflowWithValues } from '@/stores/workflows' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' const logger = createLogger('Panel') /** @@ -119,6 +121,11 @@ export const Panel = memo(function Panel() { hydration.phase === 'state-loading' const { handleAutoLayout: autoLayoutWithFitView } = useAutoLayout(activeWorkflowId || null) + // Check for locked blocks (disables auto-layout) + const hasLockedBlocks = useWorkflowStore((state) => + Object.values(state.blocks).some((block) => block.locked) + ) + // Delete workflow hook const { isDeleting, handleDeleteWorkflow } = useDeleteWorkflow({ workspaceId, @@ -230,11 +237,24 @@ export const Panel = memo(function Panel() { setIsAutoLayouting(true) try { - await autoLayoutWithFitView() + const result = await autoLayoutWithFitView() + if (!result.success && result.error) { + useNotificationStore.getState().addNotification({ + level: 'info', + message: result.error, + workflowId: activeWorkflowId || undefined, + }) + } } finally { setIsAutoLayouting(false) } - }, [isExecuting, userPermissions.canEdit, isAutoLayouting, autoLayoutWithFitView]) + }, [ + isExecuting, + userPermissions.canEdit, + isAutoLayouting, + autoLayoutWithFitView, + activeWorkflowId, + ]) /** * Handles exporting workflow as JSON @@ -404,7 +424,10 @@ export const Panel = memo(function Panel() { Auto layout diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx index 11f131065b..96c85791f1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx @@ -80,6 +80,7 @@ export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps
- {!isEnabled && disabled} +
+ {!isEnabled && disabled} + {isLocked && locked} +
{!isPreview && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-block-state.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-block-state.ts index f14d4080c4..658e0095e2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-block-state.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-block-state.ts @@ -18,6 +18,8 @@ export interface UseBlockStateReturn { diffStatus: DiffStatus /** Whether this is a deleted block in diff mode */ isDeletedBlock: boolean + /** Whether the block is locked */ + isLocked: boolean } /** @@ -40,6 +42,11 @@ export function useBlockState( ? (data.blockState?.enabled ?? true) : (currentBlock?.enabled ?? true) + // Determine if block is locked + const isLocked = data.isPreview + ? (data.blockState?.locked ?? false) + : (currentBlock?.locked ?? false) + // Get diff status const diffStatus: DiffStatus = currentWorkflow.isDiffMode && currentBlock && hasDiffStatus(currentBlock) @@ -68,5 +75,6 @@ export function useBlockState( isActive, diffStatus, isDeletedBlock: isDeletedBlock ?? false, + isLocked, } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index b3ef432442..636fd559d1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -672,6 +672,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ currentWorkflow, activeWorkflowId, isEnabled, + isLocked, handleClick, hasRing, ringStyles, @@ -1100,7 +1101,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ {name}
-
+
{isWorkflowSelector && childWorkflowId && typeof childIsDeployed === 'boolean' && @@ -1133,6 +1134,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({ )} {!isEnabled && disabled} + {isLocked && locked} {type === 'schedule' && shouldShowScheduleBadge && scheduleInfo?.isDisabled && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts index cd99dc9a8b..e8982bb8da 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-visual.ts @@ -47,6 +47,7 @@ export function useBlockVisual({ isActive: isExecuting, diffStatus, isDeletedBlock, + isLocked, } = useBlockState(blockId, currentWorkflow, data) const currentBlockId = usePanelEditorStore((state) => state.currentBlockId) @@ -103,6 +104,7 @@ export function useBlockVisual({ currentWorkflow, activeWorkflowId, isEnabled, + isLocked, handleClick, hasRing, ringStyles, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts index 9ecfe51f2d..334e473f8a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts @@ -39,6 +39,7 @@ export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasCo horizontalHandles: block?.horizontalHandles ?? false, parentId, parentType, + locked: block?.locked ?? false, } }), [blocks] diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout-utils.ts index 2be615c8df..5f494b29db 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout-utils.ts @@ -52,6 +52,16 @@ export async function applyAutoLayoutAndUpdateStore( return { success: false, error: 'No blocks to layout' } } + // Check for locked blocks - auto-layout is disabled when blocks are locked + const hasLockedBlocks = Object.values(blocks).some((block) => block.locked) + if (hasLockedBlocks) { + logger.info('Auto layout skipped: workflow contains locked blocks', { workflowId }) + return { + success: false, + error: 'Auto-layout is disabled when blocks are locked. Unlock blocks to use auto-layout.', + } + } + // Merge with default options const layoutOptions = { spacing: { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 2e305431ab..1148f966b2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -543,6 +543,7 @@ const WorkflowContent = React.memo(() => { collaborativeBatchRemoveBlocks, collaborativeBatchToggleBlockEnabled, collaborativeBatchToggleBlockHandles, + collaborativeBatchToggleLocked, undo, redo, } = useCollaborativeWorkflow() @@ -1068,9 +1069,31 @@ const WorkflowContent = React.memo(() => { }, [contextMenuBlocks, copyBlocks, executePasteOperation]) const handleContextDelete = useCallback(() => { - const blockIds = contextMenuBlocks.map((b) => b.id) - collaborativeBatchRemoveBlocks(blockIds) - }, [contextMenuBlocks, collaborativeBatchRemoveBlocks]) + let blockIds = contextMenuBlocks.map((b) => b.id) + // Filter out locked blocks and blocks inside locked containers + const protectedBlockIds = contextMenuBlocks + .filter((b) => b.locked || (b.parentId && blocks[b.parentId]?.locked)) + .map((b) => b.id) + if (protectedBlockIds.length > 0) { + blockIds = blockIds.filter((id) => !protectedBlockIds.includes(id)) + if (protectedBlockIds.length === contextMenuBlocks.length) { + addNotification({ + level: 'info', + message: 'Cannot delete locked blocks or blocks inside locked containers', + workflowId: activeWorkflowId || undefined, + }) + return + } + addNotification({ + level: 'info', + message: `Skipped ${protectedBlockIds.length} protected block(s)`, + workflowId: activeWorkflowId || undefined, + }) + } + if (blockIds.length > 0) { + collaborativeBatchRemoveBlocks(blockIds) + } + }, [contextMenuBlocks, collaborativeBatchRemoveBlocks, addNotification, activeWorkflowId, blocks]) const handleContextToggleEnabled = useCallback(() => { const blockIds = contextMenuBlocks.map((block) => block.id) @@ -1082,6 +1105,11 @@ const WorkflowContent = React.memo(() => { collaborativeBatchToggleBlockHandles(blockIds) }, [contextMenuBlocks, collaborativeBatchToggleBlockHandles]) + const handleContextToggleLocked = useCallback(() => { + const blockIds = contextMenuBlocks.map((block) => block.id) + collaborativeBatchToggleLocked(blockIds) + }, [contextMenuBlocks, collaborativeBatchToggleLocked]) + const handleContextRemoveFromSubflow = useCallback(() => { const blocksToRemove = contextMenuBlocks.filter( (block) => block.parentId && (block.parentType === 'loop' || block.parentType === 'parallel') @@ -1951,7 +1979,6 @@ const WorkflowContent = React.memo(() => { const loadingWorkflowRef = useRef(null) const currentWorkflowExists = Boolean(workflows[workflowIdParam]) - /** Initializes workflow when it exists in registry and needs hydration. */ useEffect(() => { const currentId = workflowIdParam const currentWorkspaceHydration = hydration.workspaceId @@ -2128,6 +2155,7 @@ const WorkflowContent = React.memo(() => { parentId: block.data?.parentId, extent: block.data?.extent || undefined, dragHandle: '.workflow-drag-handle', + draggable: !block.locked, data: { ...block.data, name: block.name, @@ -2163,6 +2191,7 @@ const WorkflowContent = React.memo(() => { position, parentId: block.data?.parentId, dragHandle, + draggable: !block.locked, extent: (() => { // Clamp children to subflow body (exclude header) const parentId = block.data?.parentId as string | undefined @@ -2491,12 +2520,29 @@ const WorkflowContent = React.memo(() => { const edgeIdsToRemove = changes .filter((change: any) => change.type === 'remove') .map((change: any) => change.id) + .filter((edgeId: string) => { + // Prevent removing edges connected to locked blocks or blocks inside locked containers + const edge = edges.find((e) => e.id === edgeId) + if (!edge) return true + const sourceBlock = blocks[edge.source] + const targetBlock = blocks[edge.target] + const sourceParentLocked = + sourceBlock?.data?.parentId && blocks[sourceBlock.data.parentId]?.locked + const targetParentLocked = + targetBlock?.data?.parentId && blocks[targetBlock.data.parentId]?.locked + return ( + !sourceBlock?.locked && + !targetBlock?.locked && + !sourceParentLocked && + !targetParentLocked + ) + }) if (edgeIdsToRemove.length > 0) { collaborativeBatchRemoveEdges(edgeIdsToRemove) } }, - [collaborativeBatchRemoveEdges] + [collaborativeBatchRemoveEdges, edges, blocks] ) /** @@ -2558,6 +2604,27 @@ const WorkflowContent = React.memo(() => { if (!sourceNode || !targetNode) return + // Prevent connections to/from locked blocks or blocks inside locked containers + const sourceBlock = blocks[connection.source] + const targetBlock = blocks[connection.target] + const sourceParentLocked = + sourceBlock?.data?.parentId && blocks[sourceBlock.data.parentId]?.locked + const targetParentLocked = + targetBlock?.data?.parentId && blocks[targetBlock.data.parentId]?.locked + if ( + sourceBlock?.locked || + targetBlock?.locked || + sourceParentLocked || + targetParentLocked + ) { + addNotification({ + level: 'info', + message: 'Cannot connect to locked blocks or blocks inside locked containers', + workflowId: activeWorkflowId || undefined, + }) + return + } + // Get parent information (handle container start node case) const sourceParentId = blocks[sourceNode.id]?.data?.parentId || @@ -2620,7 +2687,7 @@ const WorkflowContent = React.memo(() => { connectionCompletedRef.current = true } }, - [addEdge, getNodes, blocks] + [addEdge, getNodes, blocks, addNotification, activeWorkflowId] ) /** @@ -2715,6 +2782,9 @@ const WorkflowContent = React.memo(() => { // Only consider container nodes that aren't the dragged node if (n.type !== 'subflowNode' || n.id === node.id) return false + // Don't allow dropping into locked containers + if (blocks[n.id]?.locked) return false + // Get the container's absolute position const containerAbsolutePos = getNodeAbsolutePosition(n.id) @@ -2807,6 +2877,12 @@ const WorkflowContent = React.memo(() => { /** Captures initial parent ID and position when drag starts. */ const onNodeDragStart = useCallback( (_event: React.MouseEvent, node: any) => { + // Prevent dragging locked blocks + const block = blocks[node.id] + if (block?.locked) { + return + } + // Store the original parent ID when starting to drag const currentParentId = blocks[node.id]?.data?.parentId || null setDragStartParentId(currentParentId) @@ -2835,7 +2911,14 @@ const WorkflowContent = React.memo(() => { } }) }, - [blocks, setDragStartPosition, getNodes, potentialParentId, setPotentialParentId] + [ + blocks, + setDragStartPosition, + getNodes, + potentialParentId, + setPotentialParentId, + effectivePermissions.canAdmin, + ] ) /** Handles node drag stop to establish parent-child relationships. */ @@ -2897,6 +2980,17 @@ const WorkflowContent = React.memo(() => { // Don't process parent changes if the node hasn't actually changed parent or is being moved within same parent if (potentialParentId === dragStartParentId) return + // Prevent moving blocks out of locked containers + if (dragStartParentId && blocks[dragStartParentId]?.locked) { + addNotification({ + level: 'info', + message: 'Cannot move blocks out of locked containers', + workflowId: activeWorkflowId || undefined, + }) + setPotentialParentId(dragStartParentId) // Reset to original parent + return + } + // Check if this is a starter block - starter blocks should never be in containers const isStarterBlock = node.data?.type === 'starter' if (isStarterBlock) { @@ -3293,6 +3387,29 @@ const WorkflowContent = React.memo(() => { /** Stable delete handler to avoid creating new function references per edge. */ const handleEdgeDelete = useCallback( (edgeId: string) => { + // Prevent removing edges connected to locked blocks or blocks inside locked containers + const edge = edges.find((e) => e.id === edgeId) + if (edge) { + const sourceBlock = blocks[edge.source] + const targetBlock = blocks[edge.target] + const sourceParentLocked = + sourceBlock?.data?.parentId && blocks[sourceBlock.data.parentId]?.locked + const targetParentLocked = + targetBlock?.data?.parentId && blocks[targetBlock.data.parentId]?.locked + if ( + sourceBlock?.locked || + targetBlock?.locked || + sourceParentLocked || + targetParentLocked + ) { + addNotification({ + level: 'info', + message: 'Cannot remove connections from locked blocks', + workflowId: activeWorkflowId || undefined, + }) + return + } + } removeEdge(edgeId) // Remove this edge from selection (find by edge ID value) setSelectedEdges((prev) => { @@ -3305,7 +3422,7 @@ const WorkflowContent = React.memo(() => { return next }) }, - [removeEdge] + [removeEdge, edges, blocks, addNotification, activeWorkflowId] ) /** Transforms edges to include selection state and delete handlers. Memoized to prevent re-renders. */ @@ -3346,9 +3463,26 @@ const WorkflowContent = React.memo(() => { // Handle edge deletion first (edges take priority if selected) if (selectedEdges.size > 0) { - // Get all selected edge IDs and batch delete them - const edgeIds = Array.from(selectedEdges.values()) - collaborativeBatchRemoveEdges(edgeIds) + // Get all selected edge IDs and filter out edges connected to locked blocks or blocks inside locked containers + const edgeIds = Array.from(selectedEdges.values()).filter((edgeId) => { + const edge = edges.find((e) => e.id === edgeId) + if (!edge) return true + const sourceBlock = blocks[edge.source] + const targetBlock = blocks[edge.target] + const sourceParentLocked = + sourceBlock?.data?.parentId && blocks[sourceBlock.data.parentId]?.locked + const targetParentLocked = + targetBlock?.data?.parentId && blocks[targetBlock.data.parentId]?.locked + return ( + !sourceBlock?.locked && + !targetBlock?.locked && + !sourceParentLocked && + !targetParentLocked + ) + }) + if (edgeIds.length > 0) { + collaborativeBatchRemoveEdges(edgeIds) + } setSelectedEdges(new Map()) return } @@ -3364,8 +3498,32 @@ const WorkflowContent = React.memo(() => { } event.preventDefault() - const selectedIds = selectedNodes.map((node) => node.id) - collaborativeBatchRemoveBlocks(selectedIds) + let selectedIds = selectedNodes.map((node) => node.id) + // Filter out locked blocks and blocks inside locked containers + const protectedIds = selectedIds.filter( + (id) => + blocks[id]?.locked || + (blocks[id]?.data?.parentId && blocks[blocks[id]?.data?.parentId]?.locked) + ) + if (protectedIds.length > 0) { + selectedIds = selectedIds.filter((id) => !protectedIds.includes(id)) + if (protectedIds.length === selectedNodes.length) { + addNotification({ + level: 'info', + message: 'Cannot delete locked blocks or blocks inside locked containers', + workflowId: activeWorkflowId || undefined, + }) + return + } + addNotification({ + level: 'info', + message: `Skipped ${protectedIds.length} protected block(s)`, + workflowId: activeWorkflowId || undefined, + }) + } + if (selectedIds.length > 0) { + collaborativeBatchRemoveBlocks(selectedIds) + } } window.addEventListener('keydown', handleKeyDown) @@ -3376,6 +3534,10 @@ const WorkflowContent = React.memo(() => { getNodes, collaborativeBatchRemoveBlocks, effectivePermissions.canEdit, + blocks, + edges, + addNotification, + activeWorkflowId, ]) return ( @@ -3496,12 +3658,19 @@ const WorkflowContent = React.memo(() => { (b) => b.parentId && (b.parentType === 'loop' || b.parentType === 'parallel') )} canRunFromBlock={runFromBlockState.canRun} - disableEdit={!effectivePermissions.canEdit} + disableEdit={ + !effectivePermissions.canEdit || + contextMenuBlocks.some((b) => b.locked) || + contextMenuBlocks.some((b) => b.parentId && blocks[b.parentId]?.locked) + } + userCanEdit={effectivePermissions.canEdit} isExecuting={isExecuting} isPositionalTrigger={ contextMenuBlocks.length === 1 && edges.filter((e) => e.target === contextMenuBlocks[0]?.id).length === 0 } + onToggleLocked={handleContextToggleLocked} + canAdmin={effectivePermissions.canAdmin} /> { disableEdit={!effectivePermissions.canEdit} canUndo={canUndo} canRedo={canRedo} + hasLockedBlocks={Object.values(blocks).some((b) => b.locked)} /> )} diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index ea06003306..d2a88cc891 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -404,6 +404,20 @@ export function useCollaborativeWorkflow() { logger.info('Successfully applied batch-toggle-handles from remote user') break } + case BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED: { + const { blockIds } = payload + logger.info('Received batch-toggle-locked from remote user', { + userId, + count: (blockIds || []).length, + }) + + if (blockIds && blockIds.length > 0) { + useWorkflowStore.getState().batchToggleLocked(blockIds) + } + + logger.info('Successfully applied batch-toggle-locked from remote user') + break + } case BLOCKS_OPERATIONS.BATCH_UPDATE_PARENT: { const { updates } = payload logger.info('Received batch-update-parent from remote user', { @@ -812,14 +826,27 @@ export function useCollaborativeWorkflow() { if (ids.length === 0) return + const currentBlocks = useWorkflowStore.getState().blocks const previousStates: Record = {} const validIds: string[] = [] + // For each ID, collect non-locked blocks and their children for undo/redo for (const id of ids) { - const block = useWorkflowStore.getState().blocks[id] - if (block) { - previousStates[id] = block.enabled - validIds.push(id) + const block = currentBlocks[id] + if (!block) continue + + // Skip locked blocks + if (block.locked) continue + validIds.push(id) + previousStates[id] = block.enabled + + // If it's a loop or parallel, also capture children's previous states for undo/redo + if (block.type === 'loop' || block.type === 'parallel') { + Object.entries(currentBlocks).forEach(([blockId, b]) => { + if (b.data?.parentId === id && !b.locked) { + previousStates[blockId] = b.enabled + } + }) } } @@ -1014,6 +1041,58 @@ export function useCollaborativeWorkflow() { [isBaselineDiffView, addToQueue, activeWorkflowId, session?.user?.id, undoRedo] ) + const collaborativeBatchToggleLocked = useCallback( + (ids: string[]) => { + if (isBaselineDiffView) { + return + } + + if (ids.length === 0) return + + const currentBlocks = useWorkflowStore.getState().blocks + const previousStates: Record = {} + const validIds: string[] = [] + + // For each ID, collect blocks and their children for undo/redo + for (const id of ids) { + const block = currentBlocks[id] + if (!block) continue + + validIds.push(id) + previousStates[id] = block.locked ?? false + + // If it's a loop or parallel, also capture children's previous states for undo/redo + if (block.type === 'loop' || block.type === 'parallel') { + Object.entries(currentBlocks).forEach(([blockId, b]) => { + if (b.data?.parentId === id) { + previousStates[blockId] = b.locked ?? false + } + }) + } + } + + if (validIds.length === 0) return + + const operationId = crypto.randomUUID() + + addToQueue({ + id: operationId, + operation: { + operation: BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED, + target: OPERATION_TARGETS.BLOCKS, + payload: { blockIds: validIds, previousStates }, + }, + workflowId: activeWorkflowId || '', + userId: session?.user?.id || 'unknown', + }) + + useWorkflowStore.getState().batchToggleLocked(validIds) + + undoRedo.recordBatchToggleLocked(validIds, previousStates) + }, + [isBaselineDiffView, addToQueue, activeWorkflowId, session?.user?.id, undoRedo] + ) + const collaborativeBatchAddEdges = useCallback( (edges: Edge[], options?: { skipUndoRedo?: boolean }) => { if (isBaselineDiffView) { @@ -1680,6 +1759,7 @@ export function useCollaborativeWorkflow() { collaborativeToggleBlockAdvancedMode, collaborativeSetBlockCanonicalMode, collaborativeBatchToggleBlockHandles, + collaborativeBatchToggleLocked, collaborativeBatchAddBlocks, collaborativeBatchRemoveBlocks, collaborativeBatchAddEdges, diff --git a/apps/sim/hooks/use-undo-redo.ts b/apps/sim/hooks/use-undo-redo.ts index 06cd9326d6..7448bb7db6 100644 --- a/apps/sim/hooks/use-undo-redo.ts +++ b/apps/sim/hooks/use-undo-redo.ts @@ -20,6 +20,7 @@ import { type BatchRemoveEdgesOperation, type BatchToggleEnabledOperation, type BatchToggleHandlesOperation, + type BatchToggleLockedOperation, type BatchUpdateParentOperation, captureLatestEdges, captureLatestSubBlockValues, @@ -416,6 +417,36 @@ export function useUndoRedo() { [activeWorkflowId, userId] ) + const recordBatchToggleLocked = useCallback( + (blockIds: string[], previousStates: Record) => { + if (!activeWorkflowId || blockIds.length === 0) return + + const operation: BatchToggleLockedOperation = { + id: crypto.randomUUID(), + type: UNDO_REDO_OPERATIONS.BATCH_TOGGLE_LOCKED, + timestamp: Date.now(), + workflowId: activeWorkflowId, + userId, + data: { blockIds, previousStates }, + } + + const inverse: BatchToggleLockedOperation = { + id: crypto.randomUUID(), + type: UNDO_REDO_OPERATIONS.BATCH_TOGGLE_LOCKED, + timestamp: Date.now(), + workflowId: activeWorkflowId, + userId, + data: { blockIds, previousStates }, + } + + const entry = createOperationEntry(operation, inverse) + useUndoRedoStore.getState().push(activeWorkflowId, userId, entry) + + logger.debug('Recorded batch toggle locked', { blockIds, previousStates }) + }, + [activeWorkflowId, userId] + ) + const undo = useCallback(async () => { if (!activeWorkflowId) return @@ -816,7 +847,9 @@ export function useUndoRedo() { const toggleOp = entry.inverse as BatchToggleEnabledOperation const { blockIds, previousStates } = toggleOp.data - const validBlockIds = blockIds.filter((id) => useWorkflowStore.getState().blocks[id]) + // Restore all blocks in previousStates (includes children of containers) + const allBlockIds = Object.keys(previousStates) + const validBlockIds = allBlockIds.filter((id) => useWorkflowStore.getState().blocks[id]) if (validBlockIds.length === 0) { logger.debug('Undo batch-toggle-enabled skipped; no blocks exist') break @@ -827,14 +860,14 @@ export function useUndoRedo() { operation: { operation: BLOCKS_OPERATIONS.BATCH_TOGGLE_ENABLED, target: OPERATION_TARGETS.BLOCKS, - payload: { blockIds: validBlockIds, previousStates }, + payload: { blockIds, previousStates }, }, workflowId: activeWorkflowId, userId, }) // Use setBlockEnabled to directly restore to previous state - // This is more robust than conditional toggle in collaborative scenarios + // This restores all affected blocks including children of containers validBlockIds.forEach((blockId) => { useWorkflowStore.getState().setBlockEnabled(blockId, previousStates[blockId]) }) @@ -868,6 +901,36 @@ export function useUndoRedo() { }) break } + case UNDO_REDO_OPERATIONS.BATCH_TOGGLE_LOCKED: { + const toggleOp = entry.inverse as BatchToggleLockedOperation + const { blockIds, previousStates } = toggleOp.data + + // Restore all blocks in previousStates (includes children of containers) + const allBlockIds = Object.keys(previousStates) + const validBlockIds = allBlockIds.filter((id) => useWorkflowStore.getState().blocks[id]) + if (validBlockIds.length === 0) { + logger.debug('Undo batch-toggle-locked skipped; no blocks exist') + break + } + + addToQueue({ + id: opId, + operation: { + operation: BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED, + target: OPERATION_TARGETS.BLOCKS, + payload: { blockIds, previousStates }, + }, + workflowId: activeWorkflowId, + userId, + }) + + // Use setBlockLocked to directly restore to previous state + // This restores all affected blocks including children of containers + validBlockIds.forEach((blockId) => { + useWorkflowStore.getState().setBlockLocked(blockId, previousStates[blockId]) + }) + break + } case UNDO_REDO_OPERATIONS.APPLY_DIFF: { const applyDiffInverse = entry.inverse as any const { baselineSnapshot } = applyDiffInverse.data @@ -1442,7 +1505,9 @@ export function useUndoRedo() { const toggleOp = entry.operation as BatchToggleEnabledOperation const { blockIds, previousStates } = toggleOp.data - const validBlockIds = blockIds.filter((id) => useWorkflowStore.getState().blocks[id]) + // Process all blocks in previousStates (includes children of containers) + const allBlockIds = Object.keys(previousStates) + const validBlockIds = allBlockIds.filter((id) => useWorkflowStore.getState().blocks[id]) if (validBlockIds.length === 0) { logger.debug('Redo batch-toggle-enabled skipped; no blocks exist') break @@ -1453,7 +1518,7 @@ export function useUndoRedo() { operation: { operation: BLOCKS_OPERATIONS.BATCH_TOGGLE_ENABLED, target: OPERATION_TARGETS.BLOCKS, - payload: { blockIds: validBlockIds, previousStates }, + payload: { blockIds, previousStates }, }, workflowId: activeWorkflowId, userId, @@ -1494,6 +1559,36 @@ export function useUndoRedo() { }) break } + case UNDO_REDO_OPERATIONS.BATCH_TOGGLE_LOCKED: { + const toggleOp = entry.operation as BatchToggleLockedOperation + const { blockIds, previousStates } = toggleOp.data + + // Process all blocks in previousStates (includes children of containers) + const allBlockIds = Object.keys(previousStates) + const validBlockIds = allBlockIds.filter((id) => useWorkflowStore.getState().blocks[id]) + if (validBlockIds.length === 0) { + logger.debug('Redo batch-toggle-locked skipped; no blocks exist') + break + } + + addToQueue({ + id: opId, + operation: { + operation: BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED, + target: OPERATION_TARGETS.BLOCKS, + payload: { blockIds, previousStates }, + }, + workflowId: activeWorkflowId, + userId, + }) + + // Use setBlockLocked to directly set to toggled state + // Redo sets to !previousStates (the state after the original toggle) + validBlockIds.forEach((blockId) => { + useWorkflowStore.getState().setBlockLocked(blockId, !previousStates[blockId]) + }) + break + } case UNDO_REDO_OPERATIONS.APPLY_DIFF: { // Redo apply-diff means re-applying the proposed state with diff markers const applyDiffOp = entry.operation as any @@ -1815,6 +1910,7 @@ export function useUndoRedo() { recordBatchUpdateParent, recordBatchToggleEnabled, recordBatchToggleHandles, + recordBatchToggleLocked, recordApplyDiff, recordAcceptDiff, recordRejectDiff, diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index 61866dbd9f..d4fe120cf4 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -54,6 +54,7 @@ type SkippedItemType = | 'block_not_found' | 'invalid_block_type' | 'block_not_allowed' + | 'block_locked' | 'tool_not_allowed' | 'invalid_edge_target' | 'invalid_edge_source' @@ -618,6 +619,7 @@ function createBlockFromParams( subBlocks: {}, outputs: outputs, data: parentId ? { parentId, extent: 'parent' as const } : {}, + locked: false, } // Add validated inputs as subBlocks @@ -1520,6 +1522,17 @@ function applyOperationsToWorkflowState( break } + // Check if block is locked + if (modifiedState.blocks[block_id].locked) { + logSkippedItem(skippedItems, { + type: 'block_locked', + operationType: 'delete', + blockId: block_id, + reason: `Block "${block_id}" is locked and cannot be deleted`, + }) + break + } + // Find all child blocks to remove const blocksToRemove = new Set([block_id]) const findChildren = (parentId: string) => { @@ -1555,6 +1568,17 @@ function applyOperationsToWorkflowState( const block = modifiedState.blocks[block_id] + // Check if block is locked + if (block.locked) { + logSkippedItem(skippedItems, { + type: 'block_locked', + operationType: 'edit', + blockId: block_id, + reason: `Block "${block_id}" is locked and cannot be edited`, + }) + break + } + // Ensure block has essential properties if (!block.type) { logger.warn(`Block ${block_id} missing type property, skipping edit`, { @@ -2209,6 +2233,18 @@ function applyOperationsToWorkflowState( break } + // Check if subflow is locked + if (subflowBlock.locked) { + logSkippedItem(skippedItems, { + type: 'block_locked', + operationType: 'insert_into_subflow', + blockId: block_id, + reason: `Subflow "${subflowId}" is locked - cannot insert block "${block_id}"`, + details: { subflowId }, + }) + break + } + if (subflowBlock.type !== 'loop' && subflowBlock.type !== 'parallel') { logger.error('Subflow block has invalid type', { subflowId, diff --git a/apps/sim/lib/workflows/comparison/compare.test.ts b/apps/sim/lib/workflows/comparison/compare.test.ts index a2e98127ba..be7b6e9c58 100644 --- a/apps/sim/lib/workflows/comparison/compare.test.ts +++ b/apps/sim/lib/workflows/comparison/compare.test.ts @@ -296,6 +296,26 @@ describe('hasWorkflowChanged', () => { }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) + + it.concurrent('should detect locked/unlocked changes', () => { + const state1 = createWorkflowState({ + blocks: { block1: createBlock('block1', { locked: false }) }, + }) + const state2 = createWorkflowState({ + blocks: { block1: createBlock('block1', { locked: true }) }, + }) + expect(hasWorkflowChanged(state1, state2)).toBe(true) + }) + + it.concurrent('should not detect changes when locked state is the same', () => { + const state1 = createWorkflowState({ + blocks: { block1: createBlock('block1', { locked: true }) }, + }) + const state2 = createWorkflowState({ + blocks: { block1: createBlock('block1', { locked: true }) }, + }) + expect(hasWorkflowChanged(state1, state2)).toBe(false) + }) }) describe('SubBlock Changes', () => { diff --git a/apps/sim/lib/workflows/comparison/compare.ts b/apps/sim/lib/workflows/comparison/compare.ts index da118fe1cc..ce37dd86a2 100644 --- a/apps/sim/lib/workflows/comparison/compare.ts +++ b/apps/sim/lib/workflows/comparison/compare.ts @@ -157,7 +157,7 @@ export function generateWorkflowDiffSummary( } // Check other block properties (boolean fields) // Use !! to normalize: null/undefined/false are all equivalent (falsy) - const blockFields = ['horizontalHandles', 'advancedMode', 'triggerMode'] as const + const blockFields = ['horizontalHandles', 'advancedMode', 'triggerMode', 'locked'] as const for (const field of blockFields) { if (!!currentBlock[field] !== !!previousBlock[field]) { changes.push({ diff --git a/apps/sim/lib/workflows/defaults.ts b/apps/sim/lib/workflows/defaults.ts index 590594aa53..d93dd0b135 100644 --- a/apps/sim/lib/workflows/defaults.ts +++ b/apps/sim/lib/workflows/defaults.ts @@ -100,6 +100,7 @@ function buildStartBlockState( triggerMode: false, height: 0, data: {}, + locked: false, } return { blockState, subBlockValues } diff --git a/apps/sim/lib/workflows/diff/diff-engine.test.ts b/apps/sim/lib/workflows/diff/diff-engine.test.ts new file mode 100644 index 0000000000..aecbd801ed --- /dev/null +++ b/apps/sim/lib/workflows/diff/diff-engine.test.ts @@ -0,0 +1,173 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types' + +// Mock all external dependencies before imports +vi.mock('@sim/logger', () => ({ + createLogger: () => ({ + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }), +})) + +vi.mock('@/stores/workflows/workflow/store', () => ({ + useWorkflowStore: { + getState: () => ({ + getWorkflowState: () => ({ blocks: {}, edges: [], loops: {}, parallels: {} }), + }), + }, +})) + +vi.mock('@/stores/workflows/utils', () => ({ + mergeSubblockState: (blocks: Record) => blocks, +})) + +vi.mock('@/lib/workflows/sanitization/key-validation', () => ({ + isValidKey: (key: string) => key !== 'undefined' && key !== 'null' && key !== '', +})) + +vi.mock('@/lib/workflows/autolayout', () => ({ + transferBlockHeights: vi.fn(), + applyTargetedLayout: (blocks: Record) => blocks, + applyAutoLayout: () => ({ success: true, blocks: {} }), +})) + +vi.mock('@/lib/workflows/autolayout/constants', () => ({ + DEFAULT_HORIZONTAL_SPACING: 500, + DEFAULT_VERTICAL_SPACING: 400, + DEFAULT_LAYOUT_OPTIONS: {}, +})) + +vi.mock('@/stores/workflows/workflow/utils', () => ({ + generateLoopBlocks: () => ({}), + generateParallelBlocks: () => ({}), +})) + +import { WorkflowDiffEngine } from './diff-engine' + +function createMockBlock(overrides: Partial = {}): BlockState { + return { + id: 'block-1', + type: 'agent', + name: 'Test Block', + enabled: true, + position: { x: 0, y: 0 }, + subBlocks: {}, + outputs: {}, + ...overrides, + } as BlockState +} + +function createMockWorkflowState(blocks: Record): WorkflowState { + return { + blocks, + edges: [], + loops: {}, + parallels: {}, + } +} + +describe('WorkflowDiffEngine', () => { + let engine: WorkflowDiffEngine + + beforeEach(() => { + engine = new WorkflowDiffEngine() + vi.clearAllMocks() + }) + + describe('hasBlockChanged detection', () => { + describe('locked state changes', () => { + it.concurrent( + 'should detect when block locked state changes from false to true', + async () => { + const freshEngine = new WorkflowDiffEngine() + const baseline = createMockWorkflowState({ + 'block-1': createMockBlock({ id: 'block-1', locked: false }), + }) + + const proposed = createMockWorkflowState({ + 'block-1': createMockBlock({ id: 'block-1', locked: true }), + }) + + const result = await freshEngine.createDiffFromWorkflowState( + proposed, + undefined, + baseline + ) + + expect(result.success).toBe(true) + expect(result.diff?.diffAnalysis?.edited_blocks).toContain('block-1') + } + ) + + it.concurrent('should not detect change when locked state is the same', async () => { + const freshEngine = new WorkflowDiffEngine() + const baseline = createMockWorkflowState({ + 'block-1': createMockBlock({ id: 'block-1', locked: true }), + }) + + const proposed = createMockWorkflowState({ + 'block-1': createMockBlock({ id: 'block-1', locked: true }), + }) + + const result = await freshEngine.createDiffFromWorkflowState(proposed, undefined, baseline) + + expect(result.success).toBe(true) + expect(result.diff?.diffAnalysis?.edited_blocks).not.toContain('block-1') + }) + + it.concurrent('should detect change when locked goes from undefined to true', async () => { + const freshEngine = new WorkflowDiffEngine() + const baseline = createMockWorkflowState({ + 'block-1': createMockBlock({ id: 'block-1' }), // locked undefined + }) + + const proposed = createMockWorkflowState({ + 'block-1': createMockBlock({ id: 'block-1', locked: true }), + }) + + const result = await freshEngine.createDiffFromWorkflowState(proposed, undefined, baseline) + + expect(result.success).toBe(true) + // The hasBlockChanged function uses !!locked for comparison + // so undefined -> true should be detected as a change + expect(result.diff?.diffAnalysis?.edited_blocks).toContain('block-1') + }) + + it.concurrent('should not detect change when both locked states are falsy', async () => { + const freshEngine = new WorkflowDiffEngine() + const baseline = createMockWorkflowState({ + 'block-1': createMockBlock({ id: 'block-1' }), // locked undefined + }) + + const proposed = createMockWorkflowState({ + 'block-1': createMockBlock({ id: 'block-1', locked: false }), // locked false + }) + + const result = await freshEngine.createDiffFromWorkflowState(proposed, undefined, baseline) + + expect(result.success).toBe(true) + // undefined and false should both be falsy, so !! comparison makes them equal + expect(result.diff?.diffAnalysis?.edited_blocks).not.toContain('block-1') + }) + }) + }) + + describe('diff lifecycle', () => { + it.concurrent('should start with no diff', () => { + const freshEngine = new WorkflowDiffEngine() + expect(freshEngine.hasDiff()).toBe(false) + expect(freshEngine.getCurrentDiff()).toBeUndefined() + }) + + it.concurrent('should clear diff', () => { + const freshEngine = new WorkflowDiffEngine() + freshEngine.clearDiff() + expect(freshEngine.hasDiff()).toBe(false) + }) + }) +}) diff --git a/apps/sim/lib/workflows/diff/diff-engine.ts b/apps/sim/lib/workflows/diff/diff-engine.ts index f22365d145..adb6eeae79 100644 --- a/apps/sim/lib/workflows/diff/diff-engine.ts +++ b/apps/sim/lib/workflows/diff/diff-engine.ts @@ -215,6 +215,7 @@ function hasBlockChanged(currentBlock: BlockState, proposedBlock: BlockState): b if (currentBlock.name !== proposedBlock.name) return true if (currentBlock.enabled !== proposedBlock.enabled) return true if (currentBlock.triggerMode !== proposedBlock.triggerMode) return true + if (!!currentBlock.locked !== !!proposedBlock.locked) return true // Compare subBlocks const currentSubKeys = Object.keys(currentBlock.subBlocks || {}) diff --git a/apps/sim/lib/workflows/persistence/utils.ts b/apps/sim/lib/workflows/persistence/utils.ts index e38fb27ba3..fc990682a1 100644 --- a/apps/sim/lib/workflows/persistence/utils.ts +++ b/apps/sim/lib/workflows/persistence/utils.ts @@ -226,6 +226,7 @@ export async function loadWorkflowFromNormalizedTables( subBlocks: (block.subBlocks as BlockState['subBlocks']) || {}, outputs: (block.outputs as BlockState['outputs']) || {}, data: blockData, + locked: block.locked, } blocksMap[block.id] = assembled @@ -363,6 +364,7 @@ export async function saveWorkflowToNormalizedTables( data: block.data || {}, parentId: block.data?.parentId || null, extent: block.data?.extent || null, + locked: block.locked ?? false, })) await tx.insert(workflowBlocks).values(blockInserts) diff --git a/apps/sim/socket/constants.ts b/apps/sim/socket/constants.ts index fb2b1fc566..b3afff2e67 100644 --- a/apps/sim/socket/constants.ts +++ b/apps/sim/socket/constants.ts @@ -17,6 +17,7 @@ export const BLOCKS_OPERATIONS = { BATCH_TOGGLE_ENABLED: 'batch-toggle-enabled', BATCH_TOGGLE_HANDLES: 'batch-toggle-handles', BATCH_UPDATE_PARENT: 'batch-update-parent', + BATCH_TOGGLE_LOCKED: 'batch-toggle-locked', } as const export type BlocksOperation = (typeof BLOCKS_OPERATIONS)[keyof typeof BLOCKS_OPERATIONS] @@ -85,6 +86,7 @@ export const UNDO_REDO_OPERATIONS = { BATCH_UPDATE_PARENT: 'batch-update-parent', BATCH_TOGGLE_ENABLED: 'batch-toggle-enabled', BATCH_TOGGLE_HANDLES: 'batch-toggle-handles', + BATCH_TOGGLE_LOCKED: 'batch-toggle-locked', APPLY_DIFF: 'apply-diff', ACCEPT_DIFF: 'accept-diff', REJECT_DIFF: 'reject-diff', diff --git a/apps/sim/socket/database/operations.ts b/apps/sim/socket/database/operations.ts index 991eac1a09..faf3d3176f 100644 --- a/apps/sim/socket/database/operations.ts +++ b/apps/sim/socket/database/operations.ts @@ -529,6 +529,7 @@ async function handleBlocksOperationTx( advancedMode: (block.advancedMode as boolean) ?? false, triggerMode: (block.triggerMode as boolean) ?? false, height: (block.height as number) || 0, + locked: (block.locked as boolean) ?? false, } }) @@ -741,22 +742,59 @@ async function handleBlocksOperationTx( `Batch toggling enabled state for ${blockIds.length} blocks in workflow ${workflowId}` ) - const blocks = await tx - .select({ id: workflowBlocks.id, enabled: workflowBlocks.enabled }) + // Get all blocks in workflow to find children and check locked state + const allBlocks = await tx + .select({ + id: workflowBlocks.id, + enabled: workflowBlocks.enabled, + locked: workflowBlocks.locked, + type: workflowBlocks.type, + data: workflowBlocks.data, + }) .from(workflowBlocks) - .where(and(eq(workflowBlocks.workflowId, workflowId), inArray(workflowBlocks.id, blockIds))) + .where(eq(workflowBlocks.workflowId, workflowId)) - for (const block of blocks) { + type BlockRecord = (typeof allBlocks)[number] + const blocksById: Record = Object.fromEntries( + allBlocks.map((b: BlockRecord) => [b.id, b]) + ) + const blocksToToggle = new Set() + + // Collect all blocks to toggle including children of containers + for (const id of blockIds) { + const block = blocksById[id] + if (!block || block.locked) continue + + blocksToToggle.add(id) + + // If it's a loop or parallel, also include all children + if (block.type === 'loop' || block.type === 'parallel') { + for (const b of allBlocks) { + const parentId = (b.data as Record | null)?.parentId + if (parentId === id && !b.locked) { + blocksToToggle.add(b.id) + } + } + } + } + + // Determine target enabled state based on first block + const firstBlock = blocksById[blockIds[0]] + if (!firstBlock) break + const targetEnabled = !firstBlock.enabled + + // Update all affected blocks + for (const blockId of blocksToToggle) { await tx .update(workflowBlocks) .set({ - enabled: !block.enabled, + enabled: targetEnabled, updatedAt: new Date(), }) - .where(and(eq(workflowBlocks.id, block.id), eq(workflowBlocks.workflowId, workflowId))) + .where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId))) } - logger.debug(`Batch toggled enabled state for ${blocks.length} blocks`) + logger.debug(`Batch toggled enabled state for ${blocksToToggle.size} blocks`) break } @@ -787,6 +825,69 @@ async function handleBlocksOperationTx( break } + case BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED: { + const { blockIds } = payload + if (!Array.isArray(blockIds) || blockIds.length === 0) { + return + } + + logger.info(`Batch toggling locked for ${blockIds.length} blocks in workflow ${workflowId}`) + + // Get all blocks in workflow to find children + const allBlocks = await tx + .select({ + id: workflowBlocks.id, + locked: workflowBlocks.locked, + type: workflowBlocks.type, + data: workflowBlocks.data, + }) + .from(workflowBlocks) + .where(eq(workflowBlocks.workflowId, workflowId)) + + type LockedBlockRecord = (typeof allBlocks)[number] + const blocksById: Record = Object.fromEntries( + allBlocks.map((b: LockedBlockRecord) => [b.id, b]) + ) + const blocksToToggle = new Set() + + // Collect all blocks to toggle including children of containers + for (const id of blockIds) { + const block = blocksById[id] + if (!block) continue + + blocksToToggle.add(id) + + // If it's a loop or parallel, also include all children + if (block.type === 'loop' || block.type === 'parallel') { + for (const b of allBlocks) { + const parentId = (b.data as Record | null)?.parentId + if (parentId === id) { + blocksToToggle.add(b.id) + } + } + } + } + + // Determine target locked state based on first block + const firstBlock = blocksById[blockIds[0]] + if (!firstBlock) break + const targetLocked = !firstBlock.locked + + // Update all affected blocks + for (const blockId of blocksToToggle) { + await tx + .update(workflowBlocks) + .set({ + locked: targetLocked, + updatedAt: new Date(), + }) + .where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId))) + } + + logger.debug(`Batch toggled locked for ${blocksToToggle.size} blocks`) + break + } + case BLOCKS_OPERATIONS.BATCH_UPDATE_PARENT: { const { updates } = payload if (!Array.isArray(updates) || updates.length === 0) { @@ -1198,6 +1299,7 @@ async function handleWorkflowOperationTx( advancedMode: block.advancedMode ?? false, triggerMode: block.triggerMode ?? false, height: block.height || 0, + locked: block.locked ?? false, })) await tx.insert(workflowBlocks).values(blockValues) diff --git a/apps/sim/socket/middleware/permissions.test.ts b/apps/sim/socket/middleware/permissions.test.ts index a7029163e1..00b08ba9bd 100644 --- a/apps/sim/socket/middleware/permissions.test.ts +++ b/apps/sim/socket/middleware/permissions.test.ts @@ -214,6 +214,12 @@ describe('checkRolePermission', () => { readAllowed: false, }, { operation: 'toggle-handles', adminAllowed: true, writeAllowed: true, readAllowed: false }, + { + operation: 'batch-toggle-locked', + adminAllowed: true, + writeAllowed: true, + readAllowed: false, + }, { operation: 'batch-update-positions', adminAllowed: true, diff --git a/apps/sim/socket/middleware/permissions.ts b/apps/sim/socket/middleware/permissions.ts index f3c574d205..6bae2bb93f 100644 --- a/apps/sim/socket/middleware/permissions.ts +++ b/apps/sim/socket/middleware/permissions.ts @@ -30,6 +30,7 @@ const WRITE_OPERATIONS: string[] = [ BLOCKS_OPERATIONS.BATCH_REMOVE_BLOCKS, BLOCKS_OPERATIONS.BATCH_TOGGLE_ENABLED, BLOCKS_OPERATIONS.BATCH_TOGGLE_HANDLES, + BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED, BLOCKS_OPERATIONS.BATCH_UPDATE_PARENT, // Edge operations EDGE_OPERATIONS.ADD, diff --git a/apps/sim/socket/validation/schemas.ts b/apps/sim/socket/validation/schemas.ts index c6d14d4efe..fc48500ab6 100644 --- a/apps/sim/socket/validation/schemas.ts +++ b/apps/sim/socket/validation/schemas.ts @@ -208,6 +208,17 @@ export const BatchToggleHandlesSchema = z.object({ operationId: z.string().optional(), }) +export const BatchToggleLockedSchema = z.object({ + operation: z.literal(BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED), + target: z.literal(OPERATION_TARGETS.BLOCKS), + payload: z.object({ + blockIds: z.array(z.string()), + previousStates: z.record(z.boolean()), + }), + timestamp: z.number(), + operationId: z.string().optional(), +}) + export const BatchUpdateParentSchema = z.object({ operation: z.literal(BLOCKS_OPERATIONS.BATCH_UPDATE_PARENT), target: z.literal(OPERATION_TARGETS.BLOCKS), @@ -231,6 +242,7 @@ export const WorkflowOperationSchema = z.union([ BatchRemoveBlocksSchema, BatchToggleEnabledSchema, BatchToggleHandlesSchema, + BatchToggleLockedSchema, BatchUpdateParentSchema, EdgeOperationSchema, BatchAddEdgesSchema, diff --git a/apps/sim/stores/undo-redo/types.ts b/apps/sim/stores/undo-redo/types.ts index f68aa66e68..8d5df192fd 100644 --- a/apps/sim/stores/undo-redo/types.ts +++ b/apps/sim/stores/undo-redo/types.ts @@ -97,6 +97,14 @@ export interface BatchToggleHandlesOperation extends BaseOperation { } } +export interface BatchToggleLockedOperation extends BaseOperation { + type: typeof UNDO_REDO_OPERATIONS.BATCH_TOGGLE_LOCKED + data: { + blockIds: string[] + previousStates: Record + } +} + export interface ApplyDiffOperation extends BaseOperation { type: typeof UNDO_REDO_OPERATIONS.APPLY_DIFF data: { @@ -136,6 +144,7 @@ export type Operation = | BatchUpdateParentOperation | BatchToggleEnabledOperation | BatchToggleHandlesOperation + | BatchToggleLockedOperation | ApplyDiffOperation | AcceptDiffOperation | RejectDiffOperation diff --git a/apps/sim/stores/undo-redo/utils.ts b/apps/sim/stores/undo-redo/utils.ts index 5a04579b44..d07225b3f5 100644 --- a/apps/sim/stores/undo-redo/utils.ts +++ b/apps/sim/stores/undo-redo/utils.ts @@ -167,6 +167,15 @@ export function createInverseOperation(operation: Operation): Operation { }, } + case UNDO_REDO_OPERATIONS.BATCH_TOGGLE_LOCKED: + return { + ...operation, + data: { + blockIds: operation.data.blockIds, + previousStates: operation.data.previousStates, + }, + } + default: { const exhaustiveCheck: never = operation throw new Error(`Unhandled operation type: ${(exhaustiveCheck as Operation).type}`) diff --git a/apps/sim/stores/workflows/utils.test.ts b/apps/sim/stores/workflows/utils.test.ts index c82d1a7128..ef59363425 100644 --- a/apps/sim/stores/workflows/utils.test.ts +++ b/apps/sim/stores/workflows/utils.test.ts @@ -432,4 +432,104 @@ describe('regenerateBlockIds', () => { expect(duplicatedBlock.position).toEqual({ x: 280, y: 70 }) expect(duplicatedBlock.data?.parentId).toBe(loopId) }) + + it('should preserve locked state when pasting a locked block', () => { + const blockId = 'block-1' + + const blocksToCopy = { + [blockId]: createAgentBlock({ + id: blockId, + name: 'Locked Agent', + position: { x: 100, y: 50 }, + locked: true, + }), + } + + const result = regenerateBlockIds( + blocksToCopy, + [], + {}, + {}, + {}, + positionOffset, + {}, + getUniqueBlockName + ) + + const newBlocks = Object.values(result.blocks) + expect(newBlocks).toHaveLength(1) + + const pastedBlock = newBlocks[0] + expect(pastedBlock.locked).toBe(true) + }) + + it('should preserve unlocked state when pasting an unlocked block', () => { + const blockId = 'block-1' + + const blocksToCopy = { + [blockId]: createAgentBlock({ + id: blockId, + name: 'Unlocked Agent', + position: { x: 100, y: 50 }, + locked: false, + }), + } + + const result = regenerateBlockIds( + blocksToCopy, + [], + {}, + {}, + {}, + positionOffset, + {}, + getUniqueBlockName + ) + + const newBlocks = Object.values(result.blocks) + expect(newBlocks).toHaveLength(1) + + const pastedBlock = newBlocks[0] + expect(pastedBlock.locked).toBe(false) + }) + + it('should preserve mixed locked states when pasting multiple blocks', () => { + const lockedId = 'locked-1' + const unlockedId = 'unlocked-1' + + const blocksToCopy = { + [lockedId]: createAgentBlock({ + id: lockedId, + name: 'Locked Agent', + position: { x: 100, y: 50 }, + locked: true, + }), + [unlockedId]: createFunctionBlock({ + id: unlockedId, + name: 'Unlocked Function', + position: { x: 200, y: 50 }, + locked: false, + }), + } + + const result = regenerateBlockIds( + blocksToCopy, + [], + {}, + {}, + {}, + positionOffset, + {}, + getUniqueBlockName + ) + + const newBlocks = Object.values(result.blocks) + expect(newBlocks).toHaveLength(2) + + const lockedBlock = newBlocks.find((b) => b.name.includes('Locked')) + const unlockedBlock = newBlocks.find((b) => b.name.includes('Unlocked')) + + expect(lockedBlock?.locked).toBe(true) + expect(unlockedBlock?.locked).toBe(false) + }) }) diff --git a/apps/sim/stores/workflows/utils.ts b/apps/sim/stores/workflows/utils.ts index 22531fd4ba..2657c4b6cf 100644 --- a/apps/sim/stores/workflows/utils.ts +++ b/apps/sim/stores/workflows/utils.ts @@ -203,6 +203,7 @@ export function prepareBlockState(options: PrepareBlockStateOptions): BlockState advancedMode: false, triggerMode, height: 0, + locked: false, } } @@ -481,6 +482,7 @@ export function regenerateBlockIds( position: newPosition, // Temporarily keep data as-is, we'll fix parentId in second pass data: block.data ? { ...block.data } : block.data, + // locked state is preserved via spread (same as Figma) } newBlocks[newId] = newBlock diff --git a/apps/sim/stores/workflows/workflow/store.test.ts b/apps/sim/stores/workflows/workflow/store.test.ts index 1ed122c238..ed32623f19 100644 --- a/apps/sim/stores/workflows/workflow/store.test.ts +++ b/apps/sim/stores/workflows/workflow/store.test.ts @@ -782,6 +782,155 @@ describe('workflow store', () => { }) }) + describe('batchToggleLocked', () => { + it('should toggle block locked state', () => { + const { addBlock, batchToggleLocked } = useWorkflowStore.getState() + + addBlock('block-1', 'function', 'Test', { x: 0, y: 0 }) + + // Initial state is undefined (falsy) + expect(useWorkflowStore.getState().blocks['block-1'].locked).toBeFalsy() + + batchToggleLocked(['block-1']) + expect(useWorkflowStore.getState().blocks['block-1'].locked).toBe(true) + + batchToggleLocked(['block-1']) + expect(useWorkflowStore.getState().blocks['block-1'].locked).toBe(false) + }) + + it('should cascade lock to children when locking a loop', () => { + const { addBlock, batchToggleLocked } = useWorkflowStore.getState() + + addBlock('loop-1', 'loop', 'My Loop', { x: 0, y: 0 }, { loopType: 'for', count: 3 }) + addBlock( + 'child-1', + 'function', + 'Child', + { x: 50, y: 50 }, + { parentId: 'loop-1' }, + 'loop-1', + 'parent' + ) + + batchToggleLocked(['loop-1']) + + const { blocks } = useWorkflowStore.getState() + expect(blocks['loop-1'].locked).toBe(true) + expect(blocks['child-1'].locked).toBe(true) + }) + + it('should cascade unlock to children when unlocking a parallel', () => { + const { addBlock, batchToggleLocked } = useWorkflowStore.getState() + + addBlock('parallel-1', 'parallel', 'My Parallel', { x: 0, y: 0 }, { count: 3 }) + addBlock( + 'child-1', + 'function', + 'Child', + { x: 50, y: 50 }, + { parentId: 'parallel-1' }, + 'parallel-1', + 'parent' + ) + + // Lock first + batchToggleLocked(['parallel-1']) + expect(useWorkflowStore.getState().blocks['child-1'].locked).toBe(true) + + // Unlock + batchToggleLocked(['parallel-1']) + + const { blocks } = useWorkflowStore.getState() + expect(blocks['parallel-1'].locked).toBe(false) + expect(blocks['child-1'].locked).toBe(false) + }) + + it('should toggle multiple blocks at once', () => { + const { addBlock, batchToggleLocked } = useWorkflowStore.getState() + + addBlock('block-1', 'function', 'Test 1', { x: 0, y: 0 }) + addBlock('block-2', 'function', 'Test 2', { x: 100, y: 0 }) + + batchToggleLocked(['block-1', 'block-2']) + + const { blocks } = useWorkflowStore.getState() + expect(blocks['block-1'].locked).toBe(true) + expect(blocks['block-2'].locked).toBe(true) + }) + }) + + describe('setBlockLocked', () => { + it('should set block locked state', () => { + const { addBlock, setBlockLocked } = useWorkflowStore.getState() + + addBlock('block-1', 'function', 'Test', { x: 0, y: 0 }) + + setBlockLocked('block-1', true) + expect(useWorkflowStore.getState().blocks['block-1'].locked).toBe(true) + + setBlockLocked('block-1', false) + expect(useWorkflowStore.getState().blocks['block-1'].locked).toBe(false) + }) + + it('should not update if locked state is already the target value', () => { + const { addBlock, setBlockLocked } = useWorkflowStore.getState() + + addBlock('block-1', 'function', 'Test', { x: 0, y: 0 }) + + // First set to true + setBlockLocked('block-1', true) + expect(useWorkflowStore.getState().blocks['block-1'].locked).toBe(true) + + // Setting to true again should still be true + setBlockLocked('block-1', true) + expect(useWorkflowStore.getState().blocks['block-1'].locked).toBe(true) + }) + }) + + describe('duplicateBlock with locked', () => { + it('should preserve locked state when duplicating a locked block', () => { + const { addBlock, setBlockLocked, duplicateBlock } = useWorkflowStore.getState() + + addBlock('original', 'agent', 'Original Agent', { x: 0, y: 0 }) + setBlockLocked('original', true) + + expect(useWorkflowStore.getState().blocks.original.locked).toBe(true) + + duplicateBlock('original') + + const { blocks } = useWorkflowStore.getState() + const blockIds = Object.keys(blocks) + + expect(blockIds.length).toBe(2) + + const duplicatedId = blockIds.find((id) => id !== 'original') + expect(duplicatedId).toBeDefined() + + if (duplicatedId) { + // Original should still be locked + expect(blocks.original.locked).toBe(true) + // Duplicate should also be locked (preserves state like Figma) + expect(blocks[duplicatedId].locked).toBe(true) + } + }) + + it('should preserve unlocked state when duplicating an unlocked block', () => { + const { addBlock, duplicateBlock } = useWorkflowStore.getState() + + addBlock('original', 'agent', 'Original Agent', { x: 0, y: 0 }) + + duplicateBlock('original') + + const { blocks } = useWorkflowStore.getState() + const blockIds = Object.keys(blocks) + const duplicatedId = blockIds.find((id) => id !== 'original') + + if (duplicatedId) { + expect(blocks[duplicatedId].locked).toBeFalsy() + } + }) + }) + describe('updateBlockName', () => { beforeEach(() => { useWorkflowStore.setState({ diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 00eeac9b88..4235996bac 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -128,6 +128,7 @@ export const useWorkflowStore = create()( advancedMode?: boolean triggerMode?: boolean height?: number + locked?: boolean } ) => { const blockConfig = getBlock(type) @@ -155,6 +156,7 @@ export const useWorkflowStore = create()( triggerMode: blockProperties?.triggerMode ?? false, height: blockProperties?.height ?? 0, data: nodeData, + locked: blockProperties?.locked ?? false, }, }, edges: [...get().edges], @@ -232,6 +234,7 @@ export const useWorkflowStore = create()( height: blockProperties?.height ?? 0, layout: {}, data: nodeData, + locked: blockProperties?.locked ?? false, }, }, edges: [...get().edges], @@ -338,6 +341,7 @@ export const useWorkflowStore = create()( triggerMode?: boolean height?: number data?: Record + locked?: boolean }>, edges?: Edge[], subBlockValues?: Record>, @@ -362,6 +366,7 @@ export const useWorkflowStore = create()( triggerMode: block.triggerMode ?? false, height: block.height ?? 0, data: block.data, + locked: block.locked ?? false, } } @@ -480,12 +485,46 @@ export const useWorkflowStore = create()( }, batchToggleEnabled: (ids: string[]) => { - const newBlocks = { ...get().blocks } + if (ids.length === 0) return + + const currentBlocks = get().blocks + const newBlocks = { ...currentBlocks } + const blocksToToggle = new Set() + + // For each ID, collect blocks to toggle (skip locked blocks) + // If it's a container, also include non-locked children for (const id of ids) { - if (newBlocks[id]) { - newBlocks[id] = { ...newBlocks[id], enabled: !newBlocks[id].enabled } + const block = currentBlocks[id] + if (!block) continue + + // Skip locked blocks + if (!block.locked) { + blocksToToggle.add(id) + } + + // If it's a loop or parallel, also include non-locked children + if (block.type === 'loop' || block.type === 'parallel') { + Object.entries(currentBlocks).forEach(([blockId, b]) => { + if (b.data?.parentId === id && !b.locked) { + blocksToToggle.add(blockId) + } + }) } } + + // If no blocks can be toggled, exit early + if (blocksToToggle.size === 0) return + + // Determine target enabled state based on first toggleable block + const firstToggleableId = Array.from(blocksToToggle)[0] + const firstBlock = currentBlocks[firstToggleableId] + const targetEnabled = !firstBlock.enabled + + // Apply the enabled state to all toggleable blocks + for (const blockId of blocksToToggle) { + newBlocks[blockId] = { ...newBlocks[blockId], enabled: targetEnabled } + } + set({ blocks: newBlocks, edges: [...get().edges] }) get().updateLastSaved() }, @@ -670,6 +709,7 @@ export const useWorkflowStore = create()( name: newName, position: offsetPosition, subBlocks: newSubBlocks, + // locked state is preserved via spread (same as Figma) }, }, edges: [...get().edges], @@ -1277,6 +1317,70 @@ export const useWorkflowStore = create()( getDragStartPosition: () => { return get().dragStartPosition || null }, + + setBlockLocked: (id: string, locked: boolean) => { + const block = get().blocks[id] + if (!block || block.locked === locked) return + + const newState = { + blocks: { + ...get().blocks, + [id]: { + ...block, + locked, + }, + }, + edges: [...get().edges], + loops: { ...get().loops }, + parallels: { ...get().parallels }, + } + + set(newState) + get().updateLastSaved() + }, + + batchToggleLocked: (ids: string[]) => { + if (ids.length === 0) return + + const currentBlocks = get().blocks + const newBlocks = { ...currentBlocks } + const blocksToToggle = new Set() + + // For each ID, collect blocks to toggle + // If it's a container, also include all children + for (const id of ids) { + const block = currentBlocks[id] + if (!block) continue + + blocksToToggle.add(id) + + // If it's a loop or parallel, also include all children + if (block.type === 'loop' || block.type === 'parallel') { + Object.entries(currentBlocks).forEach(([blockId, b]) => { + if (b.data?.parentId === id) { + blocksToToggle.add(blockId) + } + }) + } + } + + // If no blocks found, exit early + if (blocksToToggle.size === 0) return + + // Determine target locked state based on first block in original ids + const firstBlock = currentBlocks[ids[0]] + if (!firstBlock) return + + const targetLocked = !firstBlock.locked + + // Apply the locked state to all blocks + for (const blockId of blocksToToggle) { + newBlocks[blockId] = { ...newBlocks[blockId], locked: targetLocked } + } + + set({ blocks: newBlocks, edges: [...get().edges] }) + get().updateLastSaved() + }, }), { name: 'workflow-store' } ) diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index f348bf0f62..fde78aae4e 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -87,6 +87,7 @@ export interface BlockState { triggerMode?: boolean data?: BlockData layout?: BlockLayoutState + locked?: boolean } export interface SubBlockState { @@ -131,6 +132,7 @@ export interface Loop { whileCondition?: string // JS expression that evaluates to boolean (for while loops) doWhileCondition?: string // JS expression that evaluates to boolean (for do-while loops) enabled: boolean + locked?: boolean } export interface Parallel { @@ -140,6 +142,7 @@ export interface Parallel { count?: number // Number of parallel executions for count-based parallel parallelType?: 'count' | 'collection' // Explicit parallel type to avoid inference bugs enabled: boolean + locked?: boolean } export interface Variable { @@ -189,6 +192,7 @@ export interface WorkflowActions { advancedMode?: boolean triggerMode?: boolean height?: number + locked?: boolean } ) => void updateNodeDimensions: (id: string, dimensions: { width: number; height: number }) => void @@ -249,6 +253,8 @@ export interface WorkflowActions { workflowState: WorkflowState, options?: { updateLastSaved?: boolean } ) => void + setBlockLocked: (id: string, locked: boolean) => void + batchToggleLocked: (ids: string[]) => void } export type WorkflowStore = WorkflowState & WorkflowActions diff --git a/packages/db/migrations/0150_flimsy_hemingway.sql b/packages/db/migrations/0150_flimsy_hemingway.sql new file mode 100644 index 0000000000..4cca53db3d --- /dev/null +++ b/packages/db/migrations/0150_flimsy_hemingway.sql @@ -0,0 +1 @@ +ALTER TABLE "workflow_blocks" ADD COLUMN "locked" boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/meta/0150_snapshot.json b/packages/db/migrations/meta/0150_snapshot.json new file mode 100644 index 0000000000..a8a31ca618 --- /dev/null +++ b/packages/db/migrations/meta/0150_snapshot.json @@ -0,0 +1,10354 @@ +{ + "id": "441fdc43-5739-4294-9a60-97b1778f910d", + "prevId": "ae520ea1-0b55-4436-9010-327d904480fb", + "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 + }, + "created_at": { + "name": "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_workspace_id_idx": { + "name": "a2a_agent_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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, + "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_id_idx": { + "name": "a2a_push_notification_config_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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.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": {} + }, + "account_user_provider_unique": { + "name": "account_user_provider_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "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 + }, + "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": {} + } + }, + "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.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": "'[]'" + }, + "created_at": { + "name": "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, + "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_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": true + }, + "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 + }, + "created_at": { + "name": "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_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": {} + } + }, + "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" + } + }, + "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.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_organization_id_idx": { + "name": "credential_set_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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_set_id_idx": { + "name": "credential_set_member_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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.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 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": 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 + }, + "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_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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "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, + "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": {} + } + }, + "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 + }, + "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": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "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()" + } + }, + "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": {} + } + }, + "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": {}, + "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": {} + } + }, + "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.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_idx": { + "name": "mcp_servers_workspace_deleted_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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": {} + } + }, + "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.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 + }, + "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.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 + } + }, + "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": {} + } + }, + "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.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_organization_id_idx": { + "name": "permission_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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_org_name_unique": { + "name": "permission_group_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": {} + }, + "permission_group_org_auto_add_unique": { + "name": "permission_group_org_auto_add_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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_user_id_unique": { + "name": "permission_group_member_user_id_unique", + "columns": [ + { + "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_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 + } + }, + "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": "'dark'" + }, + "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 + }, + "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": "'[]'" + }, + "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.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "is_super_user": { + "name": "is_super_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["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": "integer", + "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": "'20'" + }, + "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'" + }, + "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": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "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.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 + }, + "created_at": { + "name": "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, + "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": {} + } + }, + "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 + }, + "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": "'{}'" + } + }, + "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_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": {} + } + }, + "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": {} + } + }, + "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 + }, + "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()" + } + }, + "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": {} + } + }, + "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 + }, + "created_at": { + "name": "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": {} + } + }, + "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": "'{}'" + }, + "created_at": { + "name": "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, + "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": 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 + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "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, + "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": {} + } + }, + "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" + } + }, + "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 + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "created_at": { + "name": "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": { + "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_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 + }, + "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": {} + } + }, + "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_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 + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "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_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_files_key_unique": { + "name": "workspace_files_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "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 + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "workspace_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "org_invitation_id": { + "name": "org_invitation_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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "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.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "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.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"] + }, + "public.workspace_invitation_status": { + "name": "workspace_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled"] + } + }, + "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 8d95b78ffd..00b3bf160f 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1044,6 +1044,13 @@ "when": 1769656977701, "tag": "0149_next_cerise", "breakpoints": true + }, + { + "idx": 150, + "version": "7", + "when": 1769897862156, + "tag": "0150_flimsy_hemingway", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 5c3b8eb9ea..6517af8e3b 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -189,6 +189,7 @@ export const workflowBlocks = pgTable( isWide: boolean('is_wide').notNull().default(false), advancedMode: boolean('advanced_mode').notNull().default(false), triggerMode: boolean('trigger_mode').notNull().default(false), + locked: boolean('locked').notNull().default(false), height: decimal('height').notNull().default('0'), subBlocks: jsonb('sub_blocks').notNull().default('{}'), diff --git a/packages/testing/src/factories/block.factory.ts b/packages/testing/src/factories/block.factory.ts index 8ef6907f51..1020f651f8 100644 --- a/packages/testing/src/factories/block.factory.ts +++ b/packages/testing/src/factories/block.factory.ts @@ -21,6 +21,7 @@ export interface BlockFactoryOptions { triggerMode?: boolean data?: BlockData parentId?: string + locked?: boolean } /** @@ -67,6 +68,7 @@ export function createBlock(options: BlockFactoryOptions = {}): any { height: options.height ?? 0, advancedMode: options.advancedMode ?? false, triggerMode: options.triggerMode ?? false, + locked: options.locked ?? false, data: Object.keys(data).length > 0 ? data : undefined, layout: {}, } From 6907c8873651e2a936ee982c2e9e915c77d0dfe9 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 17:33:02 -0800 Subject: [PATCH 02/25] unlock duplicates of locked blocks --- .../[workspaceId]/w/[workflowId]/workflow.tsx | 7 +++--- apps/sim/stores/workflows/utils.test.ts | 22 +++++++++---------- apps/sim/stores/workflows/utils.ts | 3 ++- .../stores/workflows/workflow/store.test.ts | 8 +++---- apps/sim/stores/workflows/workflow/store.ts | 2 +- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 1148f966b2..649271070a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -2980,11 +2980,12 @@ const WorkflowContent = React.memo(() => { // Don't process parent changes if the node hasn't actually changed parent or is being moved within same parent if (potentialParentId === dragStartParentId) return - // Prevent moving blocks out of locked containers - if (dragStartParentId && blocks[dragStartParentId]?.locked) { + // Prevent moving locked blocks out of locked containers + // Unlocked blocks (e.g., duplicates) can be moved out freely + if (dragStartParentId && blocks[dragStartParentId]?.locked && blocks[node.id]?.locked) { addNotification({ level: 'info', - message: 'Cannot move blocks out of locked containers', + message: 'Cannot move locked blocks out of locked containers', workflowId: activeWorkflowId || undefined, }) setPotentialParentId(dragStartParentId) // Reset to original parent diff --git a/apps/sim/stores/workflows/utils.test.ts b/apps/sim/stores/workflows/utils.test.ts index ef59363425..4f2fc51652 100644 --- a/apps/sim/stores/workflows/utils.test.ts +++ b/apps/sim/stores/workflows/utils.test.ts @@ -433,7 +433,7 @@ describe('regenerateBlockIds', () => { expect(duplicatedBlock.data?.parentId).toBe(loopId) }) - it('should preserve locked state when pasting a locked block', () => { + it('should unlock pasted block when source is locked', () => { const blockId = 'block-1' const blocksToCopy = { @@ -459,11 +459,12 @@ describe('regenerateBlockIds', () => { const newBlocks = Object.values(result.blocks) expect(newBlocks).toHaveLength(1) + // Pasted blocks are always unlocked so users can edit them const pastedBlock = newBlocks[0] - expect(pastedBlock.locked).toBe(true) + expect(pastedBlock.locked).toBe(false) }) - it('should preserve unlocked state when pasting an unlocked block', () => { + it('should keep pasted block unlocked when source is unlocked', () => { const blockId = 'block-1' const blocksToCopy = { @@ -493,20 +494,20 @@ describe('regenerateBlockIds', () => { expect(pastedBlock.locked).toBe(false) }) - it('should preserve mixed locked states when pasting multiple blocks', () => { + it('should unlock all pasted blocks regardless of source locked state', () => { const lockedId = 'locked-1' const unlockedId = 'unlocked-1' const blocksToCopy = { [lockedId]: createAgentBlock({ id: lockedId, - name: 'Locked Agent', + name: 'Originally Locked Agent', position: { x: 100, y: 50 }, locked: true, }), [unlockedId]: createFunctionBlock({ id: unlockedId, - name: 'Unlocked Function', + name: 'Originally Unlocked Function', position: { x: 200, y: 50 }, locked: false, }), @@ -526,10 +527,9 @@ describe('regenerateBlockIds', () => { const newBlocks = Object.values(result.blocks) expect(newBlocks).toHaveLength(2) - const lockedBlock = newBlocks.find((b) => b.name.includes('Locked')) - const unlockedBlock = newBlocks.find((b) => b.name.includes('Unlocked')) - - expect(lockedBlock?.locked).toBe(true) - expect(unlockedBlock?.locked).toBe(false) + // All pasted blocks should be unlocked so users can edit them + for (const block of newBlocks) { + expect(block.locked).toBe(false) + } }) }) diff --git a/apps/sim/stores/workflows/utils.ts b/apps/sim/stores/workflows/utils.ts index 2657c4b6cf..b1d3805fa2 100644 --- a/apps/sim/stores/workflows/utils.ts +++ b/apps/sim/stores/workflows/utils.ts @@ -482,7 +482,8 @@ export function regenerateBlockIds( position: newPosition, // Temporarily keep data as-is, we'll fix parentId in second pass data: block.data ? { ...block.data } : block.data, - // locked state is preserved via spread (same as Figma) + // Duplicated blocks are always unlocked so users can edit them + locked: false, } newBlocks[newId] = newBlock diff --git a/apps/sim/stores/workflows/workflow/store.test.ts b/apps/sim/stores/workflows/workflow/store.test.ts index ed32623f19..106dc15a1b 100644 --- a/apps/sim/stores/workflows/workflow/store.test.ts +++ b/apps/sim/stores/workflows/workflow/store.test.ts @@ -888,7 +888,7 @@ describe('workflow store', () => { }) describe('duplicateBlock with locked', () => { - it('should preserve locked state when duplicating a locked block', () => { + it('should unlock duplicate when duplicating a locked block', () => { const { addBlock, setBlockLocked, duplicateBlock } = useWorkflowStore.getState() addBlock('original', 'agent', 'Original Agent', { x: 0, y: 0 }) @@ -909,12 +909,12 @@ describe('workflow store', () => { if (duplicatedId) { // Original should still be locked expect(blocks.original.locked).toBe(true) - // Duplicate should also be locked (preserves state like Figma) - expect(blocks[duplicatedId].locked).toBe(true) + // Duplicate should be unlocked so users can edit it + expect(blocks[duplicatedId].locked).toBe(false) } }) - it('should preserve unlocked state when duplicating an unlocked block', () => { + it('should create unlocked duplicate when duplicating an unlocked block', () => { const { addBlock, duplicateBlock } = useWorkflowStore.getState() addBlock('original', 'agent', 'Original Agent', { x: 0, y: 0 }) diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 4235996bac..3d11dc3f0c 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -709,7 +709,7 @@ export const useWorkflowStore = create()( name: newName, position: offsetPosition, subBlocks: newSubBlocks, - // locked state is preserved via spread (same as Figma) + locked: false, }, }, edges: [...get().edges], From 63eba0f6fb4c89b38972f65ed6ed5550656f0608 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 18:40:21 -0800 Subject: [PATCH 03/25] fix(duplicate): place duplicate outside locked container When duplicating a block that's inside a locked loop/parallel, the duplicate is now placed outside the container since nothing should be added to a locked container. Co-Authored-By: Claude Opus 4.5 --- .../stores/workflows/workflow/store.test.ts | 68 +++++++++++++++++++ apps/sim/stores/workflows/workflow/store.ts | 31 ++++++++- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/apps/sim/stores/workflows/workflow/store.test.ts b/apps/sim/stores/workflows/workflow/store.test.ts index 3cc98cbd21..d4814dcfd2 100644 --- a/apps/sim/stores/workflows/workflow/store.test.ts +++ b/apps/sim/stores/workflows/workflow/store.test.ts @@ -1291,6 +1291,74 @@ describe('workflow store', () => { expect(blocks[duplicatedId].locked).toBeFalsy() } }) + + it('should place duplicate outside locked container when duplicating block inside locked loop', () => { + const { batchToggleLocked, duplicateBlock } = useWorkflowStore.getState() + + // Create a loop with a child block + addBlock('loop-1', 'loop', 'My Loop', { x: 0, y: 0 }, { loopType: 'for', count: 3 }) + addBlock( + 'child-1', + 'function', + 'Child', + { x: 50, y: 50 }, + { parentId: 'loop-1' }, + 'loop-1', + 'parent' + ) + + // Lock the loop (which cascades to the child) + batchToggleLocked(['loop-1']) + expect(useWorkflowStore.getState().blocks['child-1'].locked).toBe(true) + + // Duplicate the child block + duplicateBlock('child-1') + + const { blocks } = useWorkflowStore.getState() + const blockIds = Object.keys(blocks) + + expect(blockIds.length).toBe(3) // loop, original child, duplicate + + const duplicatedId = blockIds.find((id) => id !== 'loop-1' && id !== 'child-1') + expect(duplicatedId).toBeDefined() + + if (duplicatedId) { + // Duplicate should be unlocked + expect(blocks[duplicatedId].locked).toBe(false) + // Duplicate should NOT have a parentId (placed outside the locked container) + expect(blocks[duplicatedId].data?.parentId).toBeUndefined() + // Original should still be inside the loop + expect(blocks['child-1'].data?.parentId).toBe('loop-1') + } + }) + + it('should keep duplicate inside unlocked container when duplicating block inside unlocked loop', () => { + const { duplicateBlock } = useWorkflowStore.getState() + + // Create a loop with a child block (not locked) + addBlock('loop-1', 'loop', 'My Loop', { x: 0, y: 0 }, { loopType: 'for', count: 3 }) + addBlock( + 'child-1', + 'function', + 'Child', + { x: 50, y: 50 }, + { parentId: 'loop-1' }, + 'loop-1', + 'parent' + ) + + // Duplicate the child block (loop is NOT locked) + duplicateBlock('child-1') + + const { blocks } = useWorkflowStore.getState() + const blockIds = Object.keys(blocks) + const duplicatedId = blockIds.find((id) => id !== 'loop-1' && id !== 'child-1') + + if (duplicatedId) { + // Duplicate should still be inside the loop since it's not locked + expect(blocks[duplicatedId].data?.parentId).toBe('loop-1') + } + }) }) describe('updateBlockName', () => { diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 45f8d23f21..3e28d717bb 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -563,9 +563,33 @@ export const useWorkflowStore = create()( if (!block) return const newId = crypto.randomUUID() - const offsetPosition = { - x: block.position.x + DEFAULT_DUPLICATE_OFFSET.x, - y: block.position.y + DEFAULT_DUPLICATE_OFFSET.y, + + // Check if block is inside a locked container - if so, place duplicate outside + const parentId = block.data?.parentId + const parentBlock = parentId ? get().blocks[parentId] : undefined + const isParentLocked = parentBlock?.locked ?? false + + // If parent is locked, calculate position outside the container + let offsetPosition: Position + const newData = block.data ? { ...block.data } : undefined + + if (isParentLocked && parentBlock) { + // Place duplicate outside the locked container (to the right of it) + const containerWidth = parentBlock.data?.width ?? 400 + offsetPosition = { + x: parentBlock.position.x + containerWidth + 50, + y: parentBlock.position.y, + } + // Remove parent relationship since we're placing outside + if (newData) { + newData.parentId = undefined + newData.extent = undefined + } + } else { + offsetPosition = { + x: block.position.x + DEFAULT_DUPLICATE_OFFSET.x, + y: block.position.y + DEFAULT_DUPLICATE_OFFSET.y, + } } const newName = getUniqueBlockName(block.name, get().blocks) @@ -594,6 +618,7 @@ export const useWorkflowStore = create()( position: offsetPosition, subBlocks: newSubBlocks, locked: false, + data: newData, }, }, edges: [...get().edges], From c19263e25fc76ce6d0cb2003e57d511d0f8f08e9 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 18:42:06 -0800 Subject: [PATCH 04/25] fix(duplicate): unlock all blocks when duplicating workflow - Server-side workflow duplication now sets locked: false for all blocks - regenerateWorkflowStateIds also unlocks blocks for templates - Client-side regenerateBlockIds already handled this (for paste/import) Co-Authored-By: Claude Opus 4.5 --- apps/sim/lib/workflows/persistence/duplicate.ts | 1 + apps/sim/lib/workflows/persistence/utils.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/workflows/persistence/duplicate.ts b/apps/sim/lib/workflows/persistence/duplicate.ts index d73df91cc5..8e006e0769 100644 --- a/apps/sim/lib/workflows/persistence/duplicate.ts +++ b/apps/sim/lib/workflows/persistence/duplicate.ts @@ -189,6 +189,7 @@ export async function duplicateWorkflow( parentId: newParentId, extent: newExtent, data: updatedData, + locked: false, // Duplicated blocks should always be unlocked createdAt: now, updatedAt: now, } diff --git a/apps/sim/lib/workflows/persistence/utils.ts b/apps/sim/lib/workflows/persistence/utils.ts index fc990682a1..b747177e3e 100644 --- a/apps/sim/lib/workflows/persistence/utils.ts +++ b/apps/sim/lib/workflows/persistence/utils.ts @@ -629,7 +629,8 @@ export function regenerateWorkflowStateIds(state: RegenerateStateInput): Regener // Regenerate blocks with updated references Object.entries(state.blocks || {}).forEach(([oldId, block]) => { const newId = blockIdMapping.get(oldId)! - const newBlock: BlockState = { ...block, id: newId } + // Duplicated blocks are always unlocked so users can edit them + const newBlock: BlockState = { ...block, id: newId, locked: false } // Update parentId reference if it exists if (newBlock.data?.parentId) { From 3f908d6121b8e4339b6aae051c8c6f45b3b41f88 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 18:45:42 -0800 Subject: [PATCH 05/25] fix code block disabled state, allow unlock from editor --- .../panel/components/editor/editor.tsx | 23 +++++++++++++++---- .../components/emcn/components/code/code.tsx | 4 ++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index e2ed145ed7..742e17c0c1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -11,6 +11,7 @@ import { Loader2, Lock, Pencil, + Unlock, } from 'lucide-react' import { useParams } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' @@ -227,6 +228,7 @@ export function Editor() { collaborativeSetBlockCanonicalMode, collaborativeUpdateBlockName, collaborativeToggleBlockAdvancedMode, + collaborativeBatchToggleLocked, } = useCollaborativeWorkflow() // Advanced mode toggle handler @@ -366,16 +368,27 @@ export function Editor() { )}
- {/* Locked indicator */} + {/* Locked indicator - clickable to unlock if user has admin permissions */} {isLocked && currentBlock && ( -
- -
+ {userPermissions.canAdmin ? ( + + ) : ( +
+ +
+ )}
-

Block is locked

+

{userPermissions.canAdmin ? 'Unlock block' : 'Block is locked'}

)} diff --git a/apps/sim/components/emcn/components/code/code.tsx b/apps/sim/components/emcn/components/code/code.tsx index 58250adc1c..e0a40846bf 100644 --- a/apps/sim/components/emcn/components/code/code.tsx +++ b/apps/sim/components/emcn/components/code/code.tsx @@ -458,8 +458,8 @@ export function getCodeEditorProps(options?: { 'caret-[var(--text-primary)] dark:caret-white', // Font smoothing '[-webkit-font-smoothing:antialiased] [-moz-osx-font-smoothing:grayscale]', - // Disable interaction for streaming/preview - (isStreaming || isPreview) && 'pointer-events-none' + // Disable interaction for streaming/preview/disabled + (isStreaming || isPreview || disabled) && 'pointer-events-none' ), } } From 73856af86df1d43e0981df9f83a5b97c86c8590e Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 18:56:57 -0800 Subject: [PATCH 06/25] fix(lock): address code review feedback - Fix toggle enabled using first toggleable block, not first block - Delete button now checks isParentLocked - Lock button now has disabled state - Editor lock icon distinguishes block vs parent lock state Co-Authored-By: Claude Opus 4.5 --- .../components/action-bar/action-bar.tsx | 11 +++++++---- .../components/panel/components/editor/editor.tsx | 12 +++++++++--- apps/sim/socket/database/operations.ts | 6 ++++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx index 54659d7420..d7daf5fbbe 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx @@ -225,9 +225,12 @@ export const ActionBar = memo( variant='ghost' onClick={(e) => { e.stopPropagation() - collaborativeBatchToggleLocked([blockId]) + if (!disabled) { + collaborativeBatchToggleLocked([blockId]) + } }} className={ACTION_BUTTON_STYLES} + disabled={disabled} > {isLocked ? : } @@ -319,18 +322,18 @@ export const ActionBar = memo( variant='ghost' onClick={(e) => { e.stopPropagation() - if (!disabled && !isLocked) { + if (!disabled && !isLocked && !isParentLocked) { collaborativeBatchRemoveBlocks([blockId]) } }} className={ACTION_BUTTON_STYLES} - disabled={disabled || isLocked} + disabled={disabled || isLocked || isParentLocked} > - {isLocked ? 'Block is locked' : getTooltipMessage('Delete Block')} + {isLocked || isParentLocked ? 'Block is locked' : getTooltipMessage('Delete Block')}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index 742e17c0c1..a5b769dfd4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -368,11 +368,11 @@ export function Editor() { )}
- {/* Locked indicator - clickable to unlock if user has admin permissions */} + {/* Locked indicator - clickable to unlock if user has admin permissions and block itself is locked */} {isLocked && currentBlock && ( - {userPermissions.canAdmin ? ( + {userPermissions.canAdmin && currentBlock.locked ? ( - {isLocked ? 'Unlock Block' : 'Lock Block'} + + {isLocked && isParentLocked + ? 'Parent container is locked' + : isLocked + ? 'Unlock Block' + : 'Lock Block'} + )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index a5b769dfd4..40e45cb878 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -368,11 +368,11 @@ export function Editor() { )}
- {/* Locked indicator - clickable to unlock if user has admin permissions and block itself is locked */} + {/* Locked indicator - clickable to unlock if user has admin permissions, block is locked, and parent is not locked */} {isLocked && currentBlock && ( - {userPermissions.canAdmin && currentBlock.locked ? ( + {userPermissions.canAdmin && currentBlock.locked && !isParentLocked ? ( - {isLocked + {isLocked || isParentLocked ? 'Block is locked' : getTooltipMessage(isEnabled ? 'Disable Block' : 'Enable Block')} @@ -274,12 +274,12 @@ export const ActionBar = memo( variant='ghost' onClick={(e) => { e.stopPropagation() - if (!disabled && !isLocked) { + if (!disabled && !isLocked && !isParentLocked) { collaborativeBatchToggleBlockHandles([blockId]) } }} className={ACTION_BUTTON_STYLES} - disabled={disabled || isLocked} + disabled={disabled || isLocked || isParentLocked} > {horizontalHandles ? ( @@ -289,7 +289,7 @@ export const ActionBar = memo( - {isLocked + {isLocked || isParentLocked ? 'Block is locked' : getTooltipMessage(horizontalHandles ? 'Vertical Ports' : 'Horizontal Ports')} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx index 65c0a65d21..e8c5861b8e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx @@ -21,6 +21,7 @@ export interface BlockInfo { parentId?: string parentType?: string locked?: boolean + isParentLocked?: boolean } /** @@ -98,6 +99,8 @@ export function BlockMenu({ const allDisabled = selectedBlocks.every((b) => !b.enabled) const allLocked = selectedBlocks.every((b) => b.locked) const allUnlocked = selectedBlocks.every((b) => !b.locked) + // Can't unlock blocks that have locked parents + const hasBlockWithLockedParent = selectedBlocks.some((b) => b.locked && b.isParentLocked) const hasSingletonBlock = selectedBlocks.some( (b) => @@ -216,12 +219,15 @@ export function BlockMenu({ )} {canAdmin && onToggleLocked && ( { - onToggleLocked() - onClose() + if (!hasBlockWithLockedParent) { + onToggleLocked() + onClose() + } }} > - {getToggleLockedLabel()} + {hasBlockWithLockedParent ? 'Parent is locked' : getToggleLockedLabel()} )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts index 334e473f8a..c95e975634 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts @@ -31,7 +31,8 @@ export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasCo nodes.map((n) => { const block = blocks[n.id] const parentId = block?.data?.parentId - const parentType = parentId ? blocks[parentId]?.type : undefined + const parentBlock = parentId ? blocks[parentId] : undefined + const parentType = parentBlock?.type return { id: n.id, type: block?.type || '', @@ -40,6 +41,7 @@ export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasCo parentId, parentType, locked: block?.locked ?? false, + isParentLocked: parentBlock?.locked ?? false, } }), [blocks] From da5e0aa07d783dd9cca455e5ff728df1d0e79581 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 19:05:14 -0800 Subject: [PATCH 09/25] fix(enable): consistent behavior - can't enable if parent disabled Same pattern as lock: must enable parent container first before enabling children inside it. Co-Authored-By: Claude Opus 4.5 --- .../components/action-bar/action-bar.tsx | 56 ++++++++++++------- .../components/block-menu/block-menu.tsx | 13 +++-- .../hooks/use-canvas-context-menu.ts | 1 + 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx index 7c044c5a49..1154d44a49 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/action-bar/action-bar.tsx @@ -85,25 +85,33 @@ export const ActionBar = memo( ) }, [blockId, addNotification, collaborativeBatchAddBlocks, setPendingSelection]) - const { isEnabled, horizontalHandles, parentId, parentType, isLocked, isParentLocked } = - useWorkflowStore( - useCallback( - (state) => { - const block = state.blocks[blockId] - const parentId = block?.data?.parentId - const parentBlock = parentId ? state.blocks[parentId] : undefined - return { - isEnabled: block?.enabled ?? true, - horizontalHandles: block?.horizontalHandles ?? false, - parentId, - parentType: parentBlock?.type, - isLocked: block?.locked ?? false, - isParentLocked: parentBlock?.locked ?? false, - } - }, - [blockId] - ) + const { + isEnabled, + horizontalHandles, + parentId, + parentType, + isLocked, + isParentLocked, + isParentDisabled, + } = useWorkflowStore( + useCallback( + (state) => { + const block = state.blocks[blockId] + const parentId = block?.data?.parentId + const parentBlock = parentId ? state.blocks[parentId] : undefined + return { + isEnabled: block?.enabled ?? true, + horizontalHandles: block?.horizontalHandles ?? false, + parentId, + parentType: parentBlock?.type, + isLocked: block?.locked ?? false, + isParentLocked: parentBlock?.locked ?? false, + isParentDisabled: parentBlock ? !parentBlock.enabled : false, + } + }, + [blockId] ) + ) const { activeWorkflowId } = useWorkflowRegistry() const { isExecuting, getLastExecutionSnapshot } = useExecutionStore() @@ -200,12 +208,16 @@ export const ActionBar = memo( variant='ghost' onClick={(e) => { e.stopPropagation() - if (!disabled && !isLocked && !isParentLocked) { + // Can't enable if parent is disabled (must enable parent first) + const cantEnable = !isEnabled && isParentDisabled + if (!disabled && !isLocked && !isParentLocked && !cantEnable) { collaborativeBatchToggleBlockEnabled([blockId]) } }} className={ACTION_BUTTON_STYLES} - disabled={disabled || isLocked || isParentLocked} + disabled={ + disabled || isLocked || isParentLocked || (!isEnabled && isParentDisabled) + } > {isEnabled ? : } @@ -213,7 +225,9 @@ export const ActionBar = memo( {isLocked || isParentLocked ? 'Block is locked' - : getTooltipMessage(isEnabled ? 'Disable Block' : 'Enable Block')} + : !isEnabled && isParentDisabled + ? 'Parent container is disabled' + : getTooltipMessage(isEnabled ? 'Disable Block' : 'Enable Block')} )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx index e8c5861b8e..1bddaad2cd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx @@ -22,6 +22,7 @@ export interface BlockInfo { parentType?: string locked?: boolean isParentLocked?: boolean + isParentDisabled?: boolean } /** @@ -101,6 +102,8 @@ export function BlockMenu({ const allUnlocked = selectedBlocks.every((b) => !b.locked) // Can't unlock blocks that have locked parents const hasBlockWithLockedParent = selectedBlocks.some((b) => b.locked && b.isParentLocked) + // Can't enable blocks that have disabled parents + const hasBlockWithDisabledParent = selectedBlocks.some((b) => !b.enabled && b.isParentDisabled) const hasSingletonBlock = selectedBlocks.some( (b) => @@ -186,13 +189,15 @@ export function BlockMenu({ {!allNoteBlocks && } {!allNoteBlocks && ( { - onToggleEnabled() - onClose() + if (!disableEdit && !hasBlockWithDisabledParent) { + onToggleEnabled() + onClose() + } }} > - {getToggleEnabledLabel()} + {hasBlockWithDisabledParent ? 'Parent is disabled' : getToggleEnabledLabel()} )} {!allNoteBlocks && !isSubflow && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts index c95e975634..13a9968e74 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-canvas-context-menu.ts @@ -42,6 +42,7 @@ export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasCo parentType, locked: block?.locked ?? false, isParentLocked: parentBlock?.locked ?? false, + isParentDisabled: parentBlock ? !parentBlock.enabled : false, } }), [blocks] From ee9f2e33c9f22a57b1e1f35f746f08af89e2dd34 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 19:06:34 -0800 Subject: [PATCH 10/25] docs(quick-reference): add lock block action Added documentation for the lock/unlock block feature (admin only). Note: Image placeholder added, pending actual screenshot. Co-Authored-By: Claude Opus 4.5 --- apps/docs/content/docs/en/quick-reference/index.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/docs/content/docs/en/quick-reference/index.mdx b/apps/docs/content/docs/en/quick-reference/index.mdx index 2b1439a4ca..1831918b83 100644 --- a/apps/docs/content/docs/en/quick-reference/index.mdx +++ b/apps/docs/content/docs/en/quick-reference/index.mdx @@ -180,6 +180,11 @@ A quick lookup for everyday actions in the Sim workflow editor. For keyboard sho Right-click → **Enable/Disable** + + Lock/Unlock a block + Hover block → Click lock icon (Admin only) + + Toggle handle orientation Right-click → **Toggle Handles** From ab4b09c484408acd2386d8eab14e2932634565d1 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 19:12:21 -0800 Subject: [PATCH 11/25] remove prefix square brackets in error notif --- .../sim/executor/handlers/workflow/workflow-handler.ts | 10 +++++----- apps/sim/executor/utils/errors.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index 4d0c4d1433..1c780ccb0e 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -212,11 +212,11 @@ export class WorkflowBlockHandler implements BlockHandler { /** * Parses a potentially nested workflow error message to extract: * - The chain of workflow names - * - The actual root error message (preserving the block prefix for the failing block) + * - The actual root error message (preserving the block name prefix for the failing block) * * Handles formats like: * - "workflow-name" failed: error - * - [block_type] Block Name: "workflow-name" failed: error + * - Block Name: "workflow-name" failed: error * - Workflow chain: A → B | error */ private parseNestedWorkflowError(message: string): { chain: string[]; rootError: string } { @@ -234,8 +234,8 @@ export class WorkflowBlockHandler implements BlockHandler { // Extract workflow names from patterns like: // - "workflow-name" failed: - // - [block_type] Block Name: "workflow-name" failed: - const workflowPattern = /(?:\[[^\]]+\]\s*[^:]+:\s*)?"([^"]+)"\s*failed:\s*/g + // - Block Name: "workflow-name" failed: + const workflowPattern = /(?:\[[^\]]+\]\s*)?(?:[^:]+:\s*)?"([^"]+)"\s*failed:\s*/g let match: RegExpExecArray | null let lastIndex = 0 @@ -247,7 +247,7 @@ export class WorkflowBlockHandler implements BlockHandler { } // The root error is everything after the last match - // Keep the block prefix (e.g., [function] Function 1:) so we know which block failed + // Keep the block name prefix (e.g., Function 1:) so we know which block failed const rootError = lastIndex > 0 ? remaining.slice(lastIndex) : remaining return { chain, rootError: rootError.trim() || 'Unknown error' } diff --git a/apps/sim/executor/utils/errors.ts b/apps/sim/executor/utils/errors.ts index f92c9c1ff4..17137730a0 100644 --- a/apps/sim/executor/utils/errors.ts +++ b/apps/sim/executor/utils/errors.ts @@ -47,7 +47,7 @@ export function buildBlockExecutionError(details: BlockExecutionErrorDetails): E const blockName = details.block.metadata?.name || details.block.id const blockType = details.block.metadata?.id || 'unknown' - const error = new Error(`[${blockType}] ${blockName}: ${errorMessage}`) + const error = new Error(`${blockName}: ${errorMessage}`) Object.assign(error, { blockId: details.block.id, From 7714dad8abb902000ed124b5c6e5a80762a01aca Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 19:16:34 -0800 Subject: [PATCH 12/25] add lock block image --- .../static/quick-reference/lock-block.png | Bin 0 -> 34412 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/docs/public/static/quick-reference/lock-block.png diff --git a/apps/docs/public/static/quick-reference/lock-block.png b/apps/docs/public/static/quick-reference/lock-block.png new file mode 100644 index 0000000000000000000000000000000000000000..67a50e04e3a674f8a457b28c80d110f580806ea6 GIT binary patch literal 34412 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 Date: Sat, 31 Jan 2026 19:28:49 -0800 Subject: [PATCH 13/25] fix(block-menu): paste should not be disabled for locked selection Paste creates new blocks, doesn't modify selected ones. Changed from disableEdit (includes lock state) to !userCanEdit (permission only), matching the Duplicate action behavior. Co-Authored-By: Claude Opus 4.5 --- .../w/[workflowId]/components/block-menu/block-menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx index 1bddaad2cd..3de4722195 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/block-menu/block-menu.tsx @@ -164,7 +164,7 @@ export function BlockMenu({ { onPaste() onClose() From c987b6ff6d1272748edc153020b7252c6c90aed4 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 19:35:01 -0800 Subject: [PATCH 14/25] refactor(workflow): extract block deletion protection into shared utility Extract duplicated block protection logic from workflow.tsx into a reusable filterProtectedBlocks helper in utils/block-protection-utils.ts. This ensures consistent behavior between context menu delete and keyboard delete operations. Co-Authored-By: Claude Opus 4.5 --- .../utils/block-protection-utils.ts | 57 +++++++++++++++++++ .../w/[workflowId]/utils/index.ts | 1 + .../[workspaceId]/w/[workflowId]/workflow.tsx | 38 ++++++------- 3 files changed, 75 insertions(+), 21 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts new file mode 100644 index 0000000000..800effd0ce --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts @@ -0,0 +1,57 @@ +import type { BlockState } from '@/stores/workflows/workflow/types' + +/** + * Result of filtering protected blocks from a deletion operation + */ +export interface FilterProtectedBlocksResult { + /** Block IDs that can be deleted (not protected) */ + deletableIds: string[] + /** Block IDs that are protected and cannot be deleted */ + protectedIds: string[] + /** Whether all blocks are protected (deletion should be cancelled entirely) */ + allProtected: boolean +} + +/** + * Checks if a block is protected from deletion. + * A block is protected if it is locked or if its parent container is locked. + * + * @param blockId - The ID of the block to check + * @param blocks - Record of all blocks in the workflow + * @returns True if the block is protected from deletion + */ +export function isBlockProtected(blockId: string, blocks: Record): boolean { + const block = blocks[blockId] + if (!block) return false + + // Block is locked directly + if (block.locked) return true + + // Block is inside a locked container + const parentId = block.data?.parentId + if (parentId && blocks[parentId]?.locked) return true + + return false +} + +/** + * Filters out protected blocks from a list of block IDs for deletion. + * Protected blocks are those that are locked or inside a locked container. + * + * @param blockIds - Array of block IDs to filter + * @param blocks - Record of all blocks in the workflow + * @returns Result containing deletable IDs, protected IDs, and whether all are protected + */ +export function filterProtectedBlocks( + blockIds: string[], + blocks: Record +): FilterProtectedBlocksResult { + const protectedIds = blockIds.filter((id) => isBlockProtected(id, blocks)) + const deletableIds = blockIds.filter((id) => !protectedIds.includes(id)) + + return { + deletableIds, + protectedIds, + allProtected: protectedIds.length === blockIds.length && blockIds.length > 0, + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts index d2845af28b..88772d16fa 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/index.ts @@ -1,4 +1,5 @@ export * from './auto-layout-utils' +export * from './block-protection-utils' export * from './block-ring-utils' export * from './node-position-utils' export * from './workflow-canvas-helpers' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 649271070a..85e0447dfd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -55,6 +55,7 @@ import { clearDragHighlights, computeClampedPositionUpdates, estimateBlockDimensions, + filterProtectedBlocks, getClampedPositionForNode, isInEditableElement, resolveParentChildSelectionConflicts, @@ -1069,14 +1070,11 @@ const WorkflowContent = React.memo(() => { }, [contextMenuBlocks, copyBlocks, executePasteOperation]) const handleContextDelete = useCallback(() => { - let blockIds = contextMenuBlocks.map((b) => b.id) - // Filter out locked blocks and blocks inside locked containers - const protectedBlockIds = contextMenuBlocks - .filter((b) => b.locked || (b.parentId && blocks[b.parentId]?.locked)) - .map((b) => b.id) - if (protectedBlockIds.length > 0) { - blockIds = blockIds.filter((id) => !protectedBlockIds.includes(id)) - if (protectedBlockIds.length === contextMenuBlocks.length) { + const blockIds = contextMenuBlocks.map((b) => b.id) + const { deletableIds, protectedIds, allProtected } = filterProtectedBlocks(blockIds, blocks) + + if (protectedIds.length > 0) { + if (allProtected) { addNotification({ level: 'info', message: 'Cannot delete locked blocks or blocks inside locked containers', @@ -1086,12 +1084,12 @@ const WorkflowContent = React.memo(() => { } addNotification({ level: 'info', - message: `Skipped ${protectedBlockIds.length} protected block(s)`, + message: `Skipped ${protectedIds.length} protected block(s)`, workflowId: activeWorkflowId || undefined, }) } - if (blockIds.length > 0) { - collaborativeBatchRemoveBlocks(blockIds) + if (deletableIds.length > 0) { + collaborativeBatchRemoveBlocks(deletableIds) } }, [contextMenuBlocks, collaborativeBatchRemoveBlocks, addNotification, activeWorkflowId, blocks]) @@ -3499,16 +3497,14 @@ const WorkflowContent = React.memo(() => { } event.preventDefault() - let selectedIds = selectedNodes.map((node) => node.id) - // Filter out locked blocks and blocks inside locked containers - const protectedIds = selectedIds.filter( - (id) => - blocks[id]?.locked || - (blocks[id]?.data?.parentId && blocks[blocks[id]?.data?.parentId]?.locked) + const selectedIds = selectedNodes.map((node) => node.id) + const { deletableIds, protectedIds, allProtected } = filterProtectedBlocks( + selectedIds, + blocks ) + if (protectedIds.length > 0) { - selectedIds = selectedIds.filter((id) => !protectedIds.includes(id)) - if (protectedIds.length === selectedNodes.length) { + if (allProtected) { addNotification({ level: 'info', message: 'Cannot delete locked blocks or blocks inside locked containers', @@ -3522,8 +3518,8 @@ const WorkflowContent = React.memo(() => { workflowId: activeWorkflowId || undefined, }) } - if (selectedIds.length > 0) { - collaborativeBatchRemoveBlocks(selectedIds) + if (deletableIds.length > 0) { + collaborativeBatchRemoveBlocks(deletableIds) } } From 8dad4d43b221cd390e29c6e9808427a280384866 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 19:41:22 -0800 Subject: [PATCH 15/25] refactor(workflow): extend block protection utilities for edge protection Add isEdgeProtected, filterUnprotectedEdges, and hasProtectedBlocks utilities. Refactor workflow.tsx to use these helpers for: - onEdgesChange edge removal filtering - onConnect connection prevention - onNodeDragStart drag prevention - Keyboard edge deletion - Block menu disableEdit calculation Co-Authored-By: Claude Opus 4.5 --- .../utils/block-protection-utils.ts | 48 +++++++++- .../[workspaceId]/w/[workflowId]/workflow.tsx | 88 +++++-------------- 2 files changed, 69 insertions(+), 67 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts index 800effd0ce..8380dd9859 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts @@ -13,12 +13,12 @@ export interface FilterProtectedBlocksResult { } /** - * Checks if a block is protected from deletion. + * Checks if a block is protected from editing/deletion. * A block is protected if it is locked or if its parent container is locked. * * @param blockId - The ID of the block to check * @param blocks - Record of all blocks in the workflow - * @returns True if the block is protected from deletion + * @returns True if the block is protected */ export function isBlockProtected(blockId: string, blocks: Record): boolean { const block = blocks[blockId] @@ -34,6 +34,21 @@ export function isBlockProtected(blockId: string, blocks: Record +): boolean { + return isBlockProtected(edge.source, blocks) || isBlockProtected(edge.target, blocks) +} + /** * Filters out protected blocks from a list of block IDs for deletion. * Protected blocks are those that are locked or inside a locked container. @@ -55,3 +70,32 @@ export function filterProtectedBlocks( allProtected: protectedIds.length === blockIds.length && blockIds.length > 0, } } + +/** + * Filters edges to only include those that are not protected. + * + * @param edges - Array of edges to filter + * @param blocks - Record of all blocks in the workflow + * @returns Array of edges that can be modified (not protected) + */ +export function filterUnprotectedEdges( + edges: T[], + blocks: Record +): T[] { + return edges.filter((edge) => !isEdgeProtected(edge, blocks)) +} + +/** + * Checks if any blocks in the selection are protected. + * Useful for determining if edit actions should be disabled. + * + * @param blockIds - Array of block IDs to check + * @param blocks - Record of all blocks in the workflow + * @returns True if any block is protected + */ +export function hasProtectedBlocks( + blockIds: string[], + blocks: Record +): boolean { + return blockIds.some((id) => isBlockProtected(id, blocks)) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 85e0447dfd..1874bcab6a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -57,6 +57,9 @@ import { estimateBlockDimensions, filterProtectedBlocks, getClampedPositionForNode, + hasProtectedBlocks, + isBlockProtected, + isEdgeProtected, isInEditableElement, resolveParentChildSelectionConflicts, validateTriggerPaste, @@ -2519,21 +2522,10 @@ const WorkflowContent = React.memo(() => { .filter((change: any) => change.type === 'remove') .map((change: any) => change.id) .filter((edgeId: string) => { - // Prevent removing edges connected to locked blocks or blocks inside locked containers + // Prevent removing edges connected to protected blocks const edge = edges.find((e) => e.id === edgeId) if (!edge) return true - const sourceBlock = blocks[edge.source] - const targetBlock = blocks[edge.target] - const sourceParentLocked = - sourceBlock?.data?.parentId && blocks[sourceBlock.data.parentId]?.locked - const targetParentLocked = - targetBlock?.data?.parentId && blocks[targetBlock.data.parentId]?.locked - return ( - !sourceBlock?.locked && - !targetBlock?.locked && - !sourceParentLocked && - !targetParentLocked - ) + return !isEdgeProtected(edge, blocks) }) if (edgeIdsToRemove.length > 0) { @@ -2602,19 +2594,8 @@ const WorkflowContent = React.memo(() => { if (!sourceNode || !targetNode) return - // Prevent connections to/from locked blocks or blocks inside locked containers - const sourceBlock = blocks[connection.source] - const targetBlock = blocks[connection.target] - const sourceParentLocked = - sourceBlock?.data?.parentId && blocks[sourceBlock.data.parentId]?.locked - const targetParentLocked = - targetBlock?.data?.parentId && blocks[targetBlock.data.parentId]?.locked - if ( - sourceBlock?.locked || - targetBlock?.locked || - sourceParentLocked || - targetParentLocked - ) { + // Prevent connections to/from protected blocks + if (isEdgeProtected(connection, blocks)) { addNotification({ level: 'info', message: 'Cannot connect to locked blocks or blocks inside locked containers', @@ -2875,9 +2856,8 @@ const WorkflowContent = React.memo(() => { /** Captures initial parent ID and position when drag starts. */ const onNodeDragStart = useCallback( (_event: React.MouseEvent, node: any) => { - // Prevent dragging locked blocks - const block = blocks[node.id] - if (block?.locked) { + // Prevent dragging protected blocks + if (isBlockProtected(node.id, blocks)) { return } @@ -3386,28 +3366,15 @@ const WorkflowContent = React.memo(() => { /** Stable delete handler to avoid creating new function references per edge. */ const handleEdgeDelete = useCallback( (edgeId: string) => { - // Prevent removing edges connected to locked blocks or blocks inside locked containers + // Prevent removing edges connected to protected blocks const edge = edges.find((e) => e.id === edgeId) - if (edge) { - const sourceBlock = blocks[edge.source] - const targetBlock = blocks[edge.target] - const sourceParentLocked = - sourceBlock?.data?.parentId && blocks[sourceBlock.data.parentId]?.locked - const targetParentLocked = - targetBlock?.data?.parentId && blocks[targetBlock.data.parentId]?.locked - if ( - sourceBlock?.locked || - targetBlock?.locked || - sourceParentLocked || - targetParentLocked - ) { - addNotification({ - level: 'info', - message: 'Cannot remove connections from locked blocks', - workflowId: activeWorkflowId || undefined, - }) - return - } + if (edge && isEdgeProtected(edge, blocks)) { + addNotification({ + level: 'info', + message: 'Cannot remove connections from locked blocks', + workflowId: activeWorkflowId || undefined, + }) + return } removeEdge(edgeId) // Remove this edge from selection (find by edge ID value) @@ -3462,22 +3429,11 @@ const WorkflowContent = React.memo(() => { // Handle edge deletion first (edges take priority if selected) if (selectedEdges.size > 0) { - // Get all selected edge IDs and filter out edges connected to locked blocks or blocks inside locked containers + // Get all selected edge IDs and filter out edges connected to protected blocks const edgeIds = Array.from(selectedEdges.values()).filter((edgeId) => { const edge = edges.find((e) => e.id === edgeId) if (!edge) return true - const sourceBlock = blocks[edge.source] - const targetBlock = blocks[edge.target] - const sourceParentLocked = - sourceBlock?.data?.parentId && blocks[sourceBlock.data.parentId]?.locked - const targetParentLocked = - targetBlock?.data?.parentId && blocks[targetBlock.data.parentId]?.locked - return ( - !sourceBlock?.locked && - !targetBlock?.locked && - !sourceParentLocked && - !targetParentLocked - ) + return !isEdgeProtected(edge, blocks) }) if (edgeIds.length > 0) { collaborativeBatchRemoveEdges(edgeIds) @@ -3657,8 +3613,10 @@ const WorkflowContent = React.memo(() => { canRunFromBlock={runFromBlockState.canRun} disableEdit={ !effectivePermissions.canEdit || - contextMenuBlocks.some((b) => b.locked) || - contextMenuBlocks.some((b) => b.parentId && blocks[b.parentId]?.locked) + hasProtectedBlocks( + contextMenuBlocks.map((b) => b.id), + blocks + ) } userCanEdit={effectivePermissions.canEdit} isExecuting={isExecuting} From 4c05ae1f34d730a612b27d1eadf44a0c5f73c39f Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 19:56:00 -0800 Subject: [PATCH 16/25] fix(lock): address review comments for lock feature 1. Store batchToggleEnabled now uses continue to skip locked blocks entirely, matching database operation behavior 2. Copilot add operation now checks if parent container is locked before adding nested nodes (defensive check for consistency) 3. Remove unused filterUnprotectedEdges function Co-Authored-By: Claude Opus 4.5 --- .../w/[workflowId]/utils/block-protection-utils.ts | 14 -------------- .../copilot/tools/server/workflow/edit-workflow.ts | 13 +++++++++++++ apps/sim/stores/workflows/workflow/store.ts | 10 +++++----- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts index 8380dd9859..797c7f2c91 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-protection-utils.ts @@ -71,20 +71,6 @@ export function filterProtectedBlocks( } } -/** - * Filters edges to only include those that are not protected. - * - * @param edges - Array of edges to filter - * @param blocks - Record of all blocks in the workflow - * @returns Array of edges that can be modified (not protected) - */ -export function filterUnprotectedEdges( - edges: T[], - blocks: Record -): T[] { - return edges.filter((edge) => !isEdgeProtected(edge, blocks)) -} - /** * Checks if any blocks in the selection are protected. * Useful for determining if edit actions should be disabled. diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index d4fe120cf4..54080236d0 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -2146,6 +2146,19 @@ function applyOperationsToWorkflowState( // Handle nested nodes (for loops/parallels created from scratch) if (params.nestedNodes) { + // Defensive check: verify parent is not locked before adding children + // (Parent was just created with locked: false, but check for consistency) + const parentBlock = modifiedState.blocks[block_id] + if (parentBlock?.locked) { + logSkippedItem(skippedItems, { + type: 'block_locked', + operationType: 'add_nested_nodes', + blockId: block_id, + reason: `Container "${block_id}" is locked - cannot add nested nodes`, + }) + break + } + Object.entries(params.nestedNodes).forEach(([childId, childBlock]: [string, any]) => { // Validate childId is a valid string if (!isValidKey(childId)) { diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 3e28d717bb..e58228ff3e 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -373,16 +373,16 @@ export const useWorkflowStore = create()( const newBlocks = { ...currentBlocks } const blocksToToggle = new Set() - // For each ID, collect blocks to toggle (skip locked blocks) + // For each ID, collect blocks to toggle (skip locked blocks entirely) // If it's a container, also include non-locked children for (const id of ids) { const block = currentBlocks[id] if (!block) continue - // Skip locked blocks - if (!block.locked) { - blocksToToggle.add(id) - } + // Skip locked blocks entirely (including their children) + if (block.locked) continue + + blocksToToggle.add(id) // If it's a loop or parallel, also include non-locked children if (block.type === 'loop' || block.type === 'parallel') { From 802884f8146c8146498e970427b5a4c7f957d19e Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 20:04:06 -0800 Subject: [PATCH 17/25] fix(copilot): add lock checks for insert and extract operations - insert_into_subflow: Check if existing block being moved is locked - extract_from_subflow: Check if block or parent subflow is locked These operations now match the UI behavior where locked blocks cannot be moved into/out of containers. Co-Authored-By: Claude Opus 4.5 --- .../tools/server/workflow/edit-workflow.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index 54080236d0..cfc231bbcc 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -2296,6 +2296,17 @@ function applyOperationsToWorkflowState( break } + // Check if existing block is locked + if (existingBlock.locked) { + logSkippedItem(skippedItems, { + type: 'block_locked', + operationType: 'insert_into_subflow', + blockId: block_id, + reason: `Block "${block_id}" is locked and cannot be moved into a subflow`, + }) + break + } + // Moving existing block into subflow - just update parent existingBlock.data = { ...existingBlock.data, @@ -2441,6 +2452,30 @@ function applyOperationsToWorkflowState( break } + // Check if block is locked + if (block.locked) { + logSkippedItem(skippedItems, { + type: 'block_locked', + operationType: 'extract_from_subflow', + blockId: block_id, + reason: `Block "${block_id}" is locked and cannot be extracted from subflow`, + }) + break + } + + // Check if parent subflow is locked + const parentSubflow = modifiedState.blocks[subflowId] + if (parentSubflow?.locked) { + logSkippedItem(skippedItems, { + type: 'block_locked', + operationType: 'extract_from_subflow', + blockId: block_id, + reason: `Subflow "${subflowId}" is locked - cannot extract block "${block_id}"`, + details: { subflowId }, + }) + break + } + // Verify it's actually a child of this subflow if (block.data?.parentId !== subflowId) { logger.warn('Block is not a child of specified subflow', { From 0eea69b07d8245562c88ee28abb915381f944dd2 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 20:17:35 -0800 Subject: [PATCH 18/25] fix(lock): prevent duplicates inside locked containers via regenerateBlockIds 1. regenerateBlockIds now checks if existing parent is locked before keeping the block inside it. If parent is locked, the duplicate is placed outside (parentId cleared) instead of creating an inconsistent state. 2. Remove unnecessary effectivePermissions.canAdmin and potentialParentId from onNodeDragStart dependency array. Co-Authored-By: Claude Opus 4.5 --- .../workspace/[workspaceId]/w/[workflowId]/workflow.tsx | 9 +-------- apps/sim/stores/workflows/utils.ts | 5 +++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 1874bcab6a..6240e0ac75 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -2889,14 +2889,7 @@ const WorkflowContent = React.memo(() => { } }) }, - [ - blocks, - setDragStartPosition, - getNodes, - potentialParentId, - setPotentialParentId, - effectivePermissions.canAdmin, - ] + [blocks, setDragStartPosition, getNodes, setPotentialParentId] ) /** Handles node drag stop to establish parent-child relationships. */ diff --git a/apps/sim/stores/workflows/utils.ts b/apps/sim/stores/workflows/utils.ts index b1d3805fa2..18bf38fb10 100644 --- a/apps/sim/stores/workflows/utils.ts +++ b/apps/sim/stores/workflows/utils.ts @@ -511,14 +511,15 @@ export function regenerateBlockIds( parentId: newParentId, extent: 'parent', } - } else if (existingBlockNames[oldParentId]) { - // Parent exists in existing workflow - keep original parentId (block stays in same subflow) + } else if (existingBlockNames[oldParentId] && !existingBlockNames[oldParentId].locked) { + // Parent exists in existing workflow and is not locked - keep original parentId block.data = { ...block.data, parentId: oldParentId, extent: 'parent', } } else { + // Parent doesn't exist anywhere OR parent is locked - clear the relationship // Parent doesn't exist anywhere - clear the relationship block.data = { ...block.data, parentId: undefined, extent: undefined } } From 395e6ed59146ac63de2967de4b1519851afd8359 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 20:29:14 -0800 Subject: [PATCH 19/25] fix(lock): fix toggle locked target state and draggable check 1. BATCH_TOGGLE_LOCKED now uses first block from blocksToToggle set instead of blockIds[0], matching BATCH_TOGGLE_ENABLED pattern. Also added early exit if blocksToToggle is empty. 2. Blocks inside locked containers are now properly non-draggable. Changed draggable check from !block.locked to use isBlockProtected() which checks both block lock and parent container lock. Co-Authored-By: Claude Opus 4.5 --- .../app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx | 4 ++-- apps/sim/socket/database/operations.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 6240e0ac75..07bb0c143a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -2156,7 +2156,7 @@ const WorkflowContent = React.memo(() => { parentId: block.data?.parentId, extent: block.data?.extent || undefined, dragHandle: '.workflow-drag-handle', - draggable: !block.locked, + draggable: !isBlockProtected(block.id, blocks), data: { ...block.data, name: block.name, @@ -2192,7 +2192,7 @@ const WorkflowContent = React.memo(() => { position, parentId: block.data?.parentId, dragHandle, - draggable: !block.locked, + draggable: !isBlockProtected(block.id, blocks), extent: (() => { // Clamp children to subflow body (exclude header) const parentId = block.data?.parentId as string | undefined diff --git a/apps/sim/socket/database/operations.ts b/apps/sim/socket/database/operations.ts index b2491f1e45..2aae11de3d 100644 --- a/apps/sim/socket/database/operations.ts +++ b/apps/sim/socket/database/operations.ts @@ -870,8 +870,10 @@ async function handleBlocksOperationTx( } } - // Determine target locked state based on first block - const firstBlock = blocksById[blockIds[0]] + // Determine target locked state based on first toggleable block + if (blocksToToggle.size === 0) break + const firstToggleableId = Array.from(blocksToToggle)[0] + const firstBlock = blocksById[firstToggleableId] if (!firstBlock) break const targetLocked = !firstBlock.locked From ef4acfdf3964cbf08331bb3a74b7fa4f1ed20eba Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 20:34:00 -0800 Subject: [PATCH 20/25] fix(copilot): check parent lock in edit and delete operations Both edit and delete operations now check if the block's parent container is locked, not just if the block itself is locked. This ensures consistent behavior with the UI which uses isBlockProtected utility that checks both direct lock and parent lock. Co-Authored-By: Claude Opus 4.5 --- .../tools/server/workflow/edit-workflow.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index cfc231bbcc..51c2669c2a 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -1522,13 +1522,20 @@ function applyOperationsToWorkflowState( break } - // Check if block is locked - if (modifiedState.blocks[block_id].locked) { + // Check if block is locked or inside a locked container + const deleteBlock = modifiedState.blocks[block_id] + const deleteParentId = deleteBlock.data?.parentId as string | undefined + const deleteParentLocked = deleteParentId + ? modifiedState.blocks[deleteParentId]?.locked + : false + if (deleteBlock.locked || deleteParentLocked) { logSkippedItem(skippedItems, { type: 'block_locked', operationType: 'delete', blockId: block_id, - reason: `Block "${block_id}" is locked and cannot be deleted`, + reason: deleteParentLocked + ? `Block "${block_id}" is inside locked container "${deleteParentId}" and cannot be deleted` + : `Block "${block_id}" is locked and cannot be deleted`, }) break } @@ -1568,13 +1575,17 @@ function applyOperationsToWorkflowState( const block = modifiedState.blocks[block_id] - // Check if block is locked - if (block.locked) { + // Check if block is locked or inside a locked container + const editParentId = block.data?.parentId as string | undefined + const editParentLocked = editParentId ? modifiedState.blocks[editParentId]?.locked : false + if (block.locked || editParentLocked) { logSkippedItem(skippedItems, { type: 'block_locked', operationType: 'edit', blockId: block_id, - reason: `Block "${block_id}" is locked and cannot be edited`, + reason: editParentLocked + ? `Block "${block_id}" is inside locked container "${editParentId}" and cannot be edited` + : `Block "${block_id}" is locked and cannot be edited`, }) break } From 3664a56fca20d9c58c9f69596a3c4fde21886502 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 20:45:40 -0800 Subject: [PATCH 21/25] fix(socket): add server-side lock validation and admin-only permissions 1. BATCH_TOGGLE_LOCKED now requires admin role - non-admin users with write role can no longer bypass UI restriction via direct socket messages 2. BATCH_REMOVE_BLOCKS now validates lock status server-side - filters out protected blocks (locked or inside locked parent) before deletion 3. Remove duplicate/outdated comment in regenerateBlockIds Co-Authored-By: Claude Opus 4.5 --- apps/sim/socket/database/operations.ts | 88 +++++++++++++++-------- apps/sim/socket/middleware/permissions.ts | 8 ++- apps/sim/stores/workflows/utils.ts | 1 - 3 files changed, 64 insertions(+), 33 deletions(-) diff --git a/apps/sim/socket/database/operations.ts b/apps/sim/socket/database/operations.ts index 2aae11de3d..7cef59a197 100644 --- a/apps/sim/socket/database/operations.ts +++ b/apps/sim/socket/database/operations.ts @@ -625,44 +625,74 @@ async function handleBlocksOperationTx( logger.info(`Batch removing ${ids.length} blocks from workflow ${workflowId}`) - // Collect all block IDs including children of subflows - const allBlocksToDelete = new Set(ids) + // Fetch all blocks to check lock status and filter out protected blocks + const allBlocks = await tx + .select({ + id: workflowBlocks.id, + type: workflowBlocks.type, + locked: workflowBlocks.locked, + data: workflowBlocks.data, + }) + .from(workflowBlocks) + .where(eq(workflowBlocks.workflowId, workflowId)) - for (const id of ids) { - const blockToRemove = await tx - .select({ type: workflowBlocks.type }) - .from(workflowBlocks) - .where(and(eq(workflowBlocks.id, id), eq(workflowBlocks.workflowId, workflowId))) - .limit(1) + type BlockRecord = (typeof allBlocks)[number] + const blocksById: Record = Object.fromEntries( + allBlocks.map((b: BlockRecord) => [b.id, b]) + ) - if (blockToRemove.length > 0 && isSubflowBlockType(blockToRemove[0].type)) { - const childBlocks = await tx - .select({ id: workflowBlocks.id }) - .from(workflowBlocks) - .where( - and( - eq(workflowBlocks.workflowId, workflowId), - sql`${workflowBlocks.data}->>'parentId' = ${id}` - ) - ) + // Helper to check if a block is protected (locked or inside locked parent) + const isProtected = (blockId: string): boolean => { + const block = blocksById[blockId] + if (!block) return false + if (block.locked) return true + const parentId = (block.data as Record | null)?.parentId as + | string + | undefined + if (parentId && blocksById[parentId]?.locked) return true + return false + } - childBlocks.forEach((child: { id: string }) => allBlocksToDelete.add(child.id)) + // Filter out protected blocks from deletion request + const deletableIds = ids.filter((id) => !isProtected(id)) + if (deletableIds.length === 0) { + logger.info('All requested blocks are protected, skipping deletion') + return + } + + if (deletableIds.length < ids.length) { + logger.info( + `Filtered out ${ids.length - deletableIds.length} protected blocks from deletion` + ) + } + + // Collect all block IDs including children of subflows + const allBlocksToDelete = new Set(deletableIds) + + for (const id of deletableIds) { + const block = blocksById[id] + if (block && isSubflowBlockType(block.type)) { + // Include all children of the subflow (they should be deleted with parent) + for (const b of allBlocks) { + const parentId = (b.data as Record | null)?.parentId + if (parentId === id) { + allBlocksToDelete.add(b.id) + } + } } } const blockIdsArray = Array.from(allBlocksToDelete) - // Collect parent IDs BEFORE deleting blocks + // Collect parent IDs BEFORE deleting blocks (use blocksById, already fetched) const parentIds = new Set() - for (const id of ids) { - const parentInfo = await tx - .select({ parentId: sql`${workflowBlocks.data}->>'parentId'` }) - .from(workflowBlocks) - .where(and(eq(workflowBlocks.id, id), eq(workflowBlocks.workflowId, workflowId))) - .limit(1) - - if (parentInfo.length > 0 && parentInfo[0].parentId) { - parentIds.add(parentInfo[0].parentId) + for (const id of deletableIds) { + const block = blocksById[id] + const parentId = (block?.data as Record | null)?.parentId as + | string + | undefined + if (parentId) { + parentIds.add(parentId) } } diff --git a/apps/sim/socket/middleware/permissions.ts b/apps/sim/socket/middleware/permissions.ts index 6bae2bb93f..b160a061be 100644 --- a/apps/sim/socket/middleware/permissions.ts +++ b/apps/sim/socket/middleware/permissions.ts @@ -14,7 +14,10 @@ import { const logger = createLogger('SocketPermissions') -// All write operations (admin and write roles have same permissions) +// Admin-only operations (require admin role) +const ADMIN_ONLY_OPERATIONS: string[] = [BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED] + +// Write operations (admin and write roles both have these permissions) const WRITE_OPERATIONS: string[] = [ // Block operations BLOCK_OPERATIONS.UPDATE_POSITION, @@ -30,7 +33,6 @@ const WRITE_OPERATIONS: string[] = [ BLOCKS_OPERATIONS.BATCH_REMOVE_BLOCKS, BLOCKS_OPERATIONS.BATCH_TOGGLE_ENABLED, BLOCKS_OPERATIONS.BATCH_TOGGLE_HANDLES, - BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED, BLOCKS_OPERATIONS.BATCH_UPDATE_PARENT, // Edge operations EDGE_OPERATIONS.ADD, @@ -52,7 +54,7 @@ const READ_OPERATIONS: string[] = [ // Define operation permissions based on role const ROLE_PERMISSIONS: Record = { - admin: WRITE_OPERATIONS, + admin: [...ADMIN_ONLY_OPERATIONS, ...WRITE_OPERATIONS], write: WRITE_OPERATIONS, read: READ_OPERATIONS, } diff --git a/apps/sim/stores/workflows/utils.ts b/apps/sim/stores/workflows/utils.ts index 18bf38fb10..03039b7f81 100644 --- a/apps/sim/stores/workflows/utils.ts +++ b/apps/sim/stores/workflows/utils.ts @@ -520,7 +520,6 @@ export function regenerateBlockIds( } } else { // Parent doesn't exist anywhere OR parent is locked - clear the relationship - // Parent doesn't exist anywhere - clear the relationship block.data = { ...block.data, parentId: undefined, extent: undefined } } } From 3fbcfc662cd655aec1d3004c979ab4deb6f3878c Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 20:49:07 -0800 Subject: [PATCH 22/25] test(socket): update permission test for admin-only lock toggle batch-toggle-locked is now admin-only, so write role should be denied. Co-Authored-By: Claude Opus 4.5 --- apps/sim/socket/middleware/permissions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/socket/middleware/permissions.test.ts b/apps/sim/socket/middleware/permissions.test.ts index 00b08ba9bd..784d4ea7ff 100644 --- a/apps/sim/socket/middleware/permissions.test.ts +++ b/apps/sim/socket/middleware/permissions.test.ts @@ -217,7 +217,7 @@ describe('checkRolePermission', () => { { operation: 'batch-toggle-locked', adminAllowed: true, - writeAllowed: true, + writeAllowed: false, // Admin-only operation readAllowed: false, }, { From 52d9f3162199bbf7adbb73a8141bd1ad4bcf7e2d Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 21:01:55 -0800 Subject: [PATCH 23/25] fix(undo-redo): use consistent target state for toggle redo The redo logic for BATCH_TOGGLE_ENABLED and BATCH_TOGGLE_LOCKED was incorrectly computing each block's new state as !previousStates[blockId]. However, the store's batchToggleEnabled/batchToggleLocked set ALL blocks to the SAME target state based on the first block's previous state. Now redo computes targetState = !previousStates[firstBlockId] and applies it to all blocks, matching the store's behavior. Co-Authored-By: Claude Opus 4.5 --- apps/sim/hooks/use-undo-redo.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/sim/hooks/use-undo-redo.ts b/apps/sim/hooks/use-undo-redo.ts index 75c72e3991..252f0785a6 100644 --- a/apps/sim/hooks/use-undo-redo.ts +++ b/apps/sim/hooks/use-undo-redo.ts @@ -1447,10 +1447,12 @@ export function useUndoRedo() { userId, }) - // Use setBlockEnabled to directly set to toggled state - // Redo sets to !previousStates (the state after the original toggle) + // Compute target state the same way batchToggleEnabled does: + // use !firstBlock.enabled, where firstBlock is blockIds[0] + const firstBlockId = blockIds[0] + const targetEnabled = !previousStates[firstBlockId] validBlockIds.forEach((blockId) => { - useWorkflowStore.getState().setBlockEnabled(blockId, !previousStates[blockId]) + useWorkflowStore.getState().setBlockEnabled(blockId, targetEnabled) }) break } @@ -1505,10 +1507,12 @@ export function useUndoRedo() { userId, }) - // Use setBlockLocked to directly set to toggled state - // Redo sets to !previousStates (the state after the original toggle) + // Compute target state the same way batchToggleLocked does: + // use !firstBlock.locked, where firstBlock is blockIds[0] + const firstBlockId = blockIds[0] + const targetLocked = !previousStates[firstBlockId] validBlockIds.forEach((blockId) => { - useWorkflowStore.getState().setBlockLocked(blockId, !previousStates[blockId]) + useWorkflowStore.getState().setBlockLocked(blockId, targetLocked) }) break } From 813ec9b758b891d93bc39963a3ae288fa6dc6910 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 21:12:50 -0800 Subject: [PATCH 24/25] fix(socket): add comprehensive lock validation across operations Based on audit findings, adds lock validation to multiple operations: 1. BATCH_TOGGLE_HANDLES - now skips locked/protected blocks at: - Store layer (batchToggleHandles) - Collaborative hook (collaborativeBatchToggleBlockHandles) - Server socket handler 2. BATCH_ADD_BLOCKS - server now filters blocks being added to locked parent containers 3. BATCH_UPDATE_PARENT - server now: - Skips protected blocks (locked or inside locked container) - Prevents moving blocks into locked containers All validations use consistent isProtected() helper that checks both direct lock and parent container lock. Co-Authored-By: Claude Opus 4.5 --- apps/sim/hooks/use-collaborative-workflow.ts | 17 ++- apps/sim/socket/database/operations.ts | 134 ++++++++++++++++--- apps/sim/stores/workflows/workflow/store.ts | 23 +++- 3 files changed, 147 insertions(+), 27 deletions(-) diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index f2e7782bfe..629078d382 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -1019,12 +1019,25 @@ export function useCollaborativeWorkflow() { if (ids.length === 0) return + const blocks = useWorkflowStore.getState().blocks + + // Helper to check if a block is protected (locked or inside locked parent) + const isProtected = (blockId: string): boolean => { + const block = blocks[blockId] + if (!block) return false + if (block.locked) return true + const parentId = block.data?.parentId + if (parentId && blocks[parentId]?.locked) return true + return false + } + const previousStates: Record = {} const validIds: string[] = [] for (const id of ids) { - const block = useWorkflowStore.getState().blocks[id] - if (block) { + const block = blocks[id] + // Skip locked blocks and blocks inside locked containers + if (block && !isProtected(id)) { previousStates[id] = block.horizontalHandles ?? false validIds.push(id) } diff --git a/apps/sim/socket/database/operations.ts b/apps/sim/socket/database/operations.ts index 7cef59a197..525bab6e50 100644 --- a/apps/sim/socket/database/operations.ts +++ b/apps/sim/socket/database/operations.ts @@ -507,7 +507,37 @@ async function handleBlocksOperationTx( }) if (blocks && blocks.length > 0) { - const blockValues = blocks.map((block: Record) => { + // Fetch existing blocks to check for locked parents + const existingBlocks = await tx + .select({ id: workflowBlocks.id, locked: workflowBlocks.locked }) + .from(workflowBlocks) + .where(eq(workflowBlocks.workflowId, workflowId)) + + type ExistingBlockRecord = (typeof existingBlocks)[number] + const lockedParentIds = new Set( + existingBlocks + .filter((b: ExistingBlockRecord) => b.locked) + .map((b: ExistingBlockRecord) => b.id) + ) + + // Filter out blocks being added to locked parents + const allowedBlocks = (blocks as Array>).filter((block) => { + const parentId = (block.data as Record | null)?.parentId as + | string + | undefined + if (parentId && lockedParentIds.has(parentId)) { + logger.info(`Skipping block ${block.id} - parent ${parentId} is locked`) + return false + } + return true + }) + + if (allowedBlocks.length === 0) { + logger.info('All blocks filtered out due to locked parents, skipping add') + break + } + + const blockValues = allowedBlocks.map((block: Record) => { const blockId = block.id as string const mergedSubBlocks = mergeSubBlockValues( block.subBlocks as Record, @@ -538,7 +568,7 @@ async function handleBlocksOperationTx( // Create subflow entries for loop/parallel blocks (skip if already in payload) const loopIds = new Set(loops ? Object.keys(loops) : []) const parallelIds = new Set(parallels ? Object.keys(parallels) : []) - for (const block of blocks) { + for (const block of allowedBlocks) { const blockId = block.id as string if (block.type === 'loop' && !loopIds.has(blockId)) { await tx.insert(workflowSubflows).values({ @@ -567,7 +597,7 @@ async function handleBlocksOperationTx( // Update parent subflow node lists const parentIds = new Set() - for (const block of blocks) { + for (const block of allowedBlocks) { const parentId = (block.data as Record)?.parentId as string | undefined if (parentId) { parentIds.add(parentId) @@ -838,22 +868,53 @@ async function handleBlocksOperationTx( logger.info(`Batch toggling handles for ${blockIds.length} blocks in workflow ${workflowId}`) - const blocks = await tx - .select({ id: workflowBlocks.id, horizontalHandles: workflowBlocks.horizontalHandles }) + // Fetch all blocks to check lock status and filter out protected blocks + const allBlocks = await tx + .select({ + id: workflowBlocks.id, + horizontalHandles: workflowBlocks.horizontalHandles, + locked: workflowBlocks.locked, + data: workflowBlocks.data, + }) .from(workflowBlocks) - .where(and(eq(workflowBlocks.workflowId, workflowId), inArray(workflowBlocks.id, blockIds))) + .where(eq(workflowBlocks.workflowId, workflowId)) - for (const block of blocks) { + type HandleBlockRecord = (typeof allBlocks)[number] + const blocksById: Record = Object.fromEntries( + allBlocks.map((b: HandleBlockRecord) => [b.id, b]) + ) + + // Helper to check if a block is protected (locked or inside locked parent) + const isProtected = (blockId: string): boolean => { + const block = blocksById[blockId] + if (!block) return false + if (block.locked) return true + const parentId = (block.data as Record | null)?.parentId as + | string + | undefined + if (parentId && blocksById[parentId]?.locked) return true + return false + } + + // Filter to only toggle handles on unprotected blocks + const blocksToToggle = blockIds.filter((id) => blocksById[id] && !isProtected(id)) + if (blocksToToggle.length === 0) { + logger.info('All requested blocks are protected, skipping handles toggle') + break + } + + for (const blockId of blocksToToggle) { + const block = blocksById[blockId] await tx .update(workflowBlocks) .set({ horizontalHandles: !block.horizontalHandles, updatedAt: new Date(), }) - .where(and(eq(workflowBlocks.id, block.id), eq(workflowBlocks.workflowId, workflowId))) + .where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId))) } - logger.debug(`Batch toggled handles for ${blocks.length} blocks`) + logger.debug(`Batch toggled handles for ${blocksToToggle.length} blocks`) break } @@ -930,19 +991,54 @@ async function handleBlocksOperationTx( logger.info(`Batch updating parent for ${updates.length} blocks in workflow ${workflowId}`) + // Fetch all blocks to check lock status + const allBlocks = await tx + .select({ + id: workflowBlocks.id, + locked: workflowBlocks.locked, + data: workflowBlocks.data, + }) + .from(workflowBlocks) + .where(eq(workflowBlocks.workflowId, workflowId)) + + type ParentBlockRecord = (typeof allBlocks)[number] + const blocksById: Record = Object.fromEntries( + allBlocks.map((b: ParentBlockRecord) => [b.id, b]) + ) + + // Helper to check if a block is protected (locked or inside locked parent) + const isProtected = (blockId: string): boolean => { + const block = blocksById[blockId] + if (!block) return false + if (block.locked) return true + const currentParentId = (block.data as Record | null)?.parentId as + | string + | undefined + if (currentParentId && blocksById[currentParentId]?.locked) return true + return false + } + for (const update of updates) { const { id, parentId, position } = update if (!id) continue + // Skip protected blocks (locked or inside locked container) + if (isProtected(id)) { + logger.info(`Skipping block ${id} parent update - block is protected`) + continue + } + + // Skip if trying to move into a locked container + if (parentId && blocksById[parentId]?.locked) { + logger.info(`Skipping block ${id} parent update - target parent ${parentId} is locked`) + continue + } + // Fetch current parent to update subflow node lists - const [existing] = await tx - .select({ - id: workflowBlocks.id, - parentId: sql`${workflowBlocks.data}->>'parentId'`, - }) - .from(workflowBlocks) - .where(and(eq(workflowBlocks.id, id), eq(workflowBlocks.workflowId, workflowId))) - .limit(1) + const existing = blocksById[id] + const existingParentId = (existing?.data as Record | null)?.parentId as + | string + | undefined if (!existing) { logger.warn(`Block ${id} not found for batch-update-parent`) @@ -987,8 +1083,8 @@ async function handleBlocksOperationTx( await updateSubflowNodeList(tx, workflowId, parentId) } // If the block had a previous parent, update that parent's node list as well - if (existing?.parentId && existing.parentId !== parentId) { - await updateSubflowNodeList(tx, workflowId, existing.parentId) + if (existingParentId && existingParentId !== parentId) { + await updateSubflowNodeList(tx, workflowId, existingParentId) } } diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index e58228ff3e..ed8db278fa 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -412,13 +412,24 @@ export const useWorkflowStore = create()( }, batchToggleHandles: (ids: string[]) => { - const newBlocks = { ...get().blocks } + const currentBlocks = get().blocks + const newBlocks = { ...currentBlocks } + + // Helper to check if a block is protected (locked or inside locked parent) + const isProtected = (blockId: string): boolean => { + const block = currentBlocks[blockId] + if (!block) return false + if (block.locked) return true + const parentId = block.data?.parentId + if (parentId && currentBlocks[parentId]?.locked) return true + return false + } + for (const id of ids) { - if (newBlocks[id]) { - newBlocks[id] = { - ...newBlocks[id], - horizontalHandles: !newBlocks[id].horizontalHandles, - } + if (!newBlocks[id] || isProtected(id)) continue + newBlocks[id] = { + ...newBlocks[id], + horizontalHandles: !newBlocks[id].horizontalHandles, } } set({ blocks: newBlocks, edges: [...get().edges] }) From a826b9785d697243ffe198d27efd3a41228eac9e Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 31 Jan 2026 21:15:33 -0800 Subject: [PATCH 25/25] refactor(workflow): use pre-computed lock state from contextMenuBlocks contextMenuBlocks already has locked and isParentLocked properties computed in use-canvas-context-menu.ts, so there's no need to look up blocks again via hasProtectedBlocks. Co-Authored-By: Claude Opus 4.5 --- .../app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 07bb0c143a..78d1327705 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -57,7 +57,6 @@ import { estimateBlockDimensions, filterProtectedBlocks, getClampedPositionForNode, - hasProtectedBlocks, isBlockProtected, isEdgeProtected, isInEditableElement, @@ -3606,10 +3605,7 @@ const WorkflowContent = React.memo(() => { canRunFromBlock={runFromBlockState.canRun} disableEdit={ !effectivePermissions.canEdit || - hasProtectedBlocks( - contextMenuBlocks.map((b) => b.id), - blocks - ) + contextMenuBlocks.some((b) => b.locked || b.isParentLocked) } userCanEdit={effectivePermissions.canEdit} isExecuting={isExecuting}