mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-18 03:18:01 -05:00
Compare commits
6 Commits
fix/socket
...
fix/block-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
484eb365db | ||
|
|
73c029ffc7 | ||
|
|
5de7228dd9 | ||
|
|
75898c69ed | ||
|
|
b14672887b | ||
|
|
d024c1e489 |
@@ -168,12 +168,17 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
|
||||
)
|
||||
})
|
||||
|
||||
export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlockNodeData>) {
|
||||
export const NoteBlock = memo(function NoteBlock({
|
||||
id,
|
||||
data,
|
||||
selected,
|
||||
}: NodeProps<NoteBlockNodeData>) {
|
||||
const { type, config, name } = data
|
||||
|
||||
const { activeWorkflowId, isEnabled, handleClick, hasRing, ringStyles } = useBlockVisual({
|
||||
blockId: id,
|
||||
data,
|
||||
isSelected: selected,
|
||||
})
|
||||
const storedValues = useSubBlockStore(
|
||||
useCallback(
|
||||
|
||||
@@ -66,7 +66,7 @@ export interface SubflowNodeData {
|
||||
* @param props - Node properties containing data and id
|
||||
* @returns Rendered subflow node component
|
||||
*/
|
||||
export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeData>) => {
|
||||
export const SubflowNodeComponent = memo(({ data, id, selected }: NodeProps<SubflowNodeData>) => {
|
||||
const { getNodes } = useReactFlow()
|
||||
const blockRef = useRef<HTMLDivElement>(null)
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
@@ -134,13 +134,15 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeDat
|
||||
|
||||
/**
|
||||
* Determine the ring styling based on subflow state priority:
|
||||
* 1. Focused (selected in editor) or preview selected - blue ring
|
||||
* 1. Focused (selected in editor), selected (shift-click/box), or preview selected - blue ring
|
||||
* 2. Diff status (version comparison) - green/orange ring
|
||||
*/
|
||||
const hasRing = isFocused || isPreviewSelected || diffStatus === 'new' || diffStatus === 'edited'
|
||||
const isSelected = !isPreview && selected
|
||||
const hasRing =
|
||||
isFocused || isSelected || isPreviewSelected || diffStatus === 'new' || diffStatus === 'edited'
|
||||
const ringStyles = cn(
|
||||
hasRing && 'ring-[1.75px]',
|
||||
(isFocused || isPreviewSelected) && 'ring-[var(--brand-secondary)]',
|
||||
(isFocused || isSelected || isPreviewSelected) && 'ring-[var(--brand-secondary)]',
|
||||
diffStatus === 'new' && 'ring-[var(--brand-tertiary-2)]',
|
||||
diffStatus === 'edited' && 'ring-[var(--warning)]'
|
||||
)
|
||||
@@ -167,7 +169,7 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeDat
|
||||
data-node-id={id}
|
||||
data-type='subflowNode'
|
||||
data-nesting-level={nestingLevel}
|
||||
data-subflow-selected={isFocused || isPreviewSelected}
|
||||
data-subflow-selected={isFocused || isSelected || isPreviewSelected}
|
||||
>
|
||||
{!isPreview && (
|
||||
<ActionBar blockId={id} blockType={data.kind} disabled={!userPermissions.canEdit} />
|
||||
|
||||
@@ -208,7 +208,6 @@ const tryParseJson = (value: unknown): unknown => {
|
||||
export const getDisplayValue = (value: unknown): string => {
|
||||
if (value == null || value === '') return '-'
|
||||
|
||||
// Try parsing JSON strings first
|
||||
const parsedValue = tryParseJson(value)
|
||||
|
||||
if (isMessagesArray(parsedValue)) {
|
||||
@@ -557,6 +556,7 @@ const SubBlockRow = ({
|
||||
export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
id,
|
||||
data,
|
||||
selected,
|
||||
}: NodeProps<WorkflowBlockProps>) {
|
||||
const { type, config, name, isPending } = data
|
||||
|
||||
@@ -574,7 +574,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
hasRing,
|
||||
ringStyles,
|
||||
runPathStatus,
|
||||
} = useBlockVisual({ blockId: id, data, isPending })
|
||||
} = useBlockVisual({ blockId: id, data, isPending, isSelected: selected })
|
||||
|
||||
const currentBlock = currentWorkflow.getBlockById(id)
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ interface UseBlockVisualProps {
|
||||
data: WorkflowBlockProps
|
||||
/** Whether the block is pending execution */
|
||||
isPending?: boolean
|
||||
/** Whether the block is selected (via shift-click or selection box) */
|
||||
isSelected?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,7 +30,12 @@ interface UseBlockVisualProps {
|
||||
* @param props - The hook properties
|
||||
* @returns Visual state, click handler, and ring styling for the block
|
||||
*/
|
||||
export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVisualProps) {
|
||||
export function useBlockVisual({
|
||||
blockId,
|
||||
data,
|
||||
isPending = false,
|
||||
isSelected = false,
|
||||
}: UseBlockVisualProps) {
|
||||
const isPreview = data.isPreview ?? false
|
||||
const isPreviewSelected = data.isPreviewSelected ?? false
|
||||
|
||||
@@ -42,7 +49,6 @@ export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVis
|
||||
isDeletedBlock,
|
||||
} = useBlockState(blockId, currentWorkflow, data)
|
||||
|
||||
// Check if the editor panel is open for this block
|
||||
const currentBlockId = usePanelEditorStore((state) => state.currentBlockId)
|
||||
const activeTab = usePanelStore((state) => state.activeTab)
|
||||
const isEditorOpen = !isPreview && currentBlockId === blockId && activeTab === 'editor'
|
||||
@@ -68,6 +74,7 @@ export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVis
|
||||
diffStatus: isPreview ? undefined : diffStatus,
|
||||
runPathStatus,
|
||||
isPreviewSelection: isPreview && isPreviewSelected,
|
||||
isSelected: isPreview ? false : isSelected,
|
||||
}),
|
||||
[
|
||||
isExecuting,
|
||||
@@ -78,6 +85,7 @@ export function useBlockVisual({ blockId, data, isPending = false }: UseBlockVis
|
||||
runPathStatus,
|
||||
isPreview,
|
||||
isPreviewSelected,
|
||||
isSelected,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface BlockRingOptions {
|
||||
diffStatus: BlockDiffStatus
|
||||
runPathStatus: BlockRunPathStatus
|
||||
isPreviewSelection?: boolean
|
||||
/** Whether the block is selected via shift-click or selection box (shows blue ring) */
|
||||
isSelected?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,11 +34,13 @@ export function getBlockRingStyles(options: BlockRingOptions): {
|
||||
diffStatus,
|
||||
runPathStatus,
|
||||
isPreviewSelection,
|
||||
isSelected,
|
||||
} = options
|
||||
|
||||
const hasRing =
|
||||
isExecuting ||
|
||||
isEditorOpen ||
|
||||
isSelected ||
|
||||
isPending ||
|
||||
diffStatus === 'new' ||
|
||||
diffStatus === 'edited' ||
|
||||
@@ -46,25 +50,37 @@ export function getBlockRingStyles(options: BlockRingOptions): {
|
||||
const ringClassName = cn(
|
||||
// Executing block: pulsing success ring with prominent thickness (highest priority)
|
||||
isExecuting && 'ring-[3.5px] ring-[var(--border-success)] animate-ring-pulse',
|
||||
// Editor open or preview selection: static blue ring
|
||||
// Editor open, selected, or preview selection: static blue ring
|
||||
!isExecuting &&
|
||||
(isEditorOpen || isPreviewSelection) &&
|
||||
(isEditorOpen || isSelected || isPreviewSelection) &&
|
||||
'ring-[1.75px] ring-[var(--brand-secondary)]',
|
||||
// Non-active states use standard ring utilities
|
||||
!isExecuting && !isEditorOpen && !isPreviewSelection && hasRing && 'ring-[1.75px]',
|
||||
!isExecuting &&
|
||||
!isEditorOpen &&
|
||||
!isSelected &&
|
||||
!isPreviewSelection &&
|
||||
hasRing &&
|
||||
'ring-[1.75px]',
|
||||
// Pending state: warning ring
|
||||
!isExecuting && !isEditorOpen && isPending && 'ring-[var(--warning)]',
|
||||
!isExecuting && !isEditorOpen && !isSelected && isPending && 'ring-[var(--warning)]',
|
||||
// Deleted state (highest priority after active/pending)
|
||||
!isExecuting && !isEditorOpen && !isPending && isDeletedBlock && 'ring-[var(--text-error)]',
|
||||
!isExecuting &&
|
||||
!isEditorOpen &&
|
||||
!isSelected &&
|
||||
!isPending &&
|
||||
isDeletedBlock &&
|
||||
'ring-[var(--text-error)]',
|
||||
// Diff states
|
||||
!isExecuting &&
|
||||
!isEditorOpen &&
|
||||
!isSelected &&
|
||||
!isPending &&
|
||||
!isDeletedBlock &&
|
||||
diffStatus === 'new' &&
|
||||
'ring-[var(--brand-tertiary-2)]',
|
||||
!isExecuting &&
|
||||
!isEditorOpen &&
|
||||
!isSelected &&
|
||||
!isPending &&
|
||||
!isDeletedBlock &&
|
||||
diffStatus === 'edited' &&
|
||||
@@ -72,6 +88,7 @@ export function getBlockRingStyles(options: BlockRingOptions): {
|
||||
// Run path states (lowest priority - only show if no other states active)
|
||||
!isExecuting &&
|
||||
!isEditorOpen &&
|
||||
!isSelected &&
|
||||
!isPending &&
|
||||
!isDeletedBlock &&
|
||||
!diffStatus &&
|
||||
@@ -79,6 +96,7 @@ export function getBlockRingStyles(options: BlockRingOptions): {
|
||||
'ring-[var(--border-success)]',
|
||||
!isExecuting &&
|
||||
!isEditorOpen &&
|
||||
!isSelected &&
|
||||
!isPending &&
|
||||
!isDeletedBlock &&
|
||||
!diffStatus &&
|
||||
|
||||
@@ -700,7 +700,23 @@ const WorkflowContent = React.memo(() => {
|
||||
triggerMode,
|
||||
})
|
||||
|
||||
collaborativeBatchAddBlocks([block], autoConnectEdge ? [autoConnectEdge] : [], {}, {}, {})
|
||||
const subBlockValues: Record<string, Record<string, unknown>> = {}
|
||||
if (block.subBlocks && Object.keys(block.subBlocks).length > 0) {
|
||||
subBlockValues[id] = {}
|
||||
for (const [subBlockId, subBlock] of Object.entries(block.subBlocks)) {
|
||||
if (subBlock.value !== null && subBlock.value !== undefined) {
|
||||
subBlockValues[id][subBlockId] = subBlock.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collaborativeBatchAddBlocks(
|
||||
[block],
|
||||
autoConnectEdge ? [autoConnectEdge] : [],
|
||||
{},
|
||||
{},
|
||||
subBlockValues
|
||||
)
|
||||
usePanelEditorStore.getState().setCurrentBlockId(id)
|
||||
},
|
||||
[collaborativeBatchAddBlocks, setSelectedEdges]
|
||||
|
||||
@@ -406,21 +406,13 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
socketInstance.on('cursor-update', (data) => {
|
||||
setPresenceUsers((prev) => {
|
||||
const existingIndex = prev.findIndex((user) => user.socketId === data.socketId)
|
||||
if (existingIndex !== -1) {
|
||||
return prev.map((user) =>
|
||||
user.socketId === data.socketId ? { ...user, cursor: data.cursor } : user
|
||||
)
|
||||
if (existingIndex === -1) {
|
||||
logger.debug('Received cursor-update for unknown user', { socketId: data.socketId })
|
||||
return prev
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
socketId: data.socketId,
|
||||
userId: data.userId,
|
||||
userName: data.userName,
|
||||
avatarUrl: data.avatarUrl,
|
||||
cursor: data.cursor,
|
||||
},
|
||||
]
|
||||
return prev.map((user) =>
|
||||
user.socketId === data.socketId ? { ...user, cursor: data.cursor } : user
|
||||
)
|
||||
})
|
||||
eventHandlers.current.cursorUpdate?.(data)
|
||||
})
|
||||
@@ -428,21 +420,15 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
socketInstance.on('selection-update', (data) => {
|
||||
setPresenceUsers((prev) => {
|
||||
const existingIndex = prev.findIndex((user) => user.socketId === data.socketId)
|
||||
if (existingIndex !== -1) {
|
||||
return prev.map((user) =>
|
||||
user.socketId === data.socketId ? { ...user, selection: data.selection } : user
|
||||
)
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
if (existingIndex === -1) {
|
||||
logger.debug('Received selection-update for unknown user', {
|
||||
socketId: data.socketId,
|
||||
userId: data.userId,
|
||||
userName: data.userName,
|
||||
avatarUrl: data.avatarUrl,
|
||||
selection: data.selection,
|
||||
},
|
||||
]
|
||||
})
|
||||
return prev
|
||||
}
|
||||
return prev.map((user) =>
|
||||
user.socketId === data.socketId ? { ...user, selection: data.selection } : user
|
||||
)
|
||||
})
|
||||
eventHandlers.current.selectionUpdate?.(data)
|
||||
})
|
||||
|
||||
@@ -6,10 +6,14 @@ import type { ResolutionContext } from './reference'
|
||||
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
/**
|
||||
* Creates a minimal workflow for testing.
|
||||
*/
|
||||
function createTestWorkflow(blocks: Array<{ id: string; name?: string; type?: string }> = []) {
|
||||
function createTestWorkflow(
|
||||
blocks: Array<{
|
||||
id: string
|
||||
name?: string
|
||||
type?: string
|
||||
outputs?: Record<string, any>
|
||||
}> = []
|
||||
) {
|
||||
return {
|
||||
version: '1.0',
|
||||
blocks: blocks.map((b) => ({
|
||||
@@ -17,7 +21,7 @@ function createTestWorkflow(blocks: Array<{ id: string; name?: string; type?: st
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: b.type ?? 'function', params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
outputs: b.outputs ?? {},
|
||||
metadata: { id: b.type ?? 'function', name: b.name ?? b.id },
|
||||
enabled: true,
|
||||
})),
|
||||
@@ -126,7 +130,7 @@ describe('BlockResolver', () => {
|
||||
expect(resolver.resolve('<source.items.1.id>', ctx)).toBe(2)
|
||||
})
|
||||
|
||||
it.concurrent('should return undefined for non-existent path', () => {
|
||||
it.concurrent('should return undefined for non-existent path when no schema defined', () => {
|
||||
const workflow = createTestWorkflow([{ id: 'source' }])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
@@ -136,6 +140,48 @@ describe('BlockResolver', () => {
|
||||
expect(resolver.resolve('<source.nonexistent>', ctx)).toBeUndefined()
|
||||
})
|
||||
|
||||
it.concurrent('should throw error for path not in output schema', () => {
|
||||
const workflow = createTestWorkflow([
|
||||
{
|
||||
id: 'source',
|
||||
outputs: {
|
||||
validField: { type: 'string', description: 'A valid field' },
|
||||
nested: {
|
||||
child: { type: 'number', description: 'Nested child' },
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
source: { validField: 'value', nested: { child: 42 } },
|
||||
})
|
||||
|
||||
expect(() => resolver.resolve('<source.invalidField>', ctx)).toThrow(
|
||||
/"invalidField" doesn't exist on block "source"/
|
||||
)
|
||||
expect(() => resolver.resolve('<source.invalidField>', ctx)).toThrow(/Available fields:/)
|
||||
})
|
||||
|
||||
it.concurrent('should return undefined for path in schema but missing in data', () => {
|
||||
const workflow = createTestWorkflow([
|
||||
{
|
||||
id: 'source',
|
||||
outputs: {
|
||||
requiredField: { type: 'string', description: 'Always present' },
|
||||
optionalField: { type: 'string', description: 'Sometimes missing' },
|
||||
},
|
||||
},
|
||||
])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
const ctx = createTestContext('current', {
|
||||
source: { requiredField: 'value' },
|
||||
})
|
||||
|
||||
expect(resolver.resolve('<source.requiredField>', ctx)).toBe('value')
|
||||
expect(resolver.resolve('<source.optionalField>', ctx)).toBeUndefined()
|
||||
})
|
||||
|
||||
it.concurrent('should return undefined for non-existent block', () => {
|
||||
const workflow = createTestWorkflow([{ id: 'existing' }])
|
||||
const resolver = new BlockResolver(workflow)
|
||||
|
||||
@@ -9,14 +9,75 @@ import {
|
||||
type ResolutionContext,
|
||||
type Resolver,
|
||||
} from '@/executor/variables/resolvers/reference'
|
||||
import type { SerializedWorkflow } from '@/serializer/types'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
|
||||
function isPathInOutputSchema(
|
||||
outputs: Record<string, any> | undefined,
|
||||
pathParts: string[]
|
||||
): boolean {
|
||||
if (!outputs || pathParts.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
let current: any = outputs
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
const part = pathParts[i]
|
||||
|
||||
if (/^\d+$/.test(part)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (current === null || current === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (part in current) {
|
||||
current = current[part]
|
||||
continue
|
||||
}
|
||||
|
||||
if (current.properties && part in current.properties) {
|
||||
current = current.properties[part]
|
||||
continue
|
||||
}
|
||||
|
||||
if (current.type === 'array' && current.items) {
|
||||
if (current.items.properties && part in current.items.properties) {
|
||||
current = current.items.properties[part]
|
||||
continue
|
||||
}
|
||||
if (part in current.items) {
|
||||
current = current.items[part]
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if ('type' in current && typeof current.type === 'string') {
|
||||
if (!current.properties && !current.items) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function getSchemaFieldNames(outputs: Record<string, any> | undefined): string[] {
|
||||
if (!outputs) return []
|
||||
return Object.keys(outputs)
|
||||
}
|
||||
|
||||
export class BlockResolver implements Resolver {
|
||||
private nameToBlockId: Map<string, string>
|
||||
private blockById: Map<string, SerializedBlock>
|
||||
|
||||
constructor(private workflow: SerializedWorkflow) {
|
||||
this.nameToBlockId = new Map()
|
||||
this.blockById = new Map()
|
||||
for (const block of workflow.blocks) {
|
||||
this.blockById.set(block.id, block)
|
||||
if (block.metadata?.name) {
|
||||
this.nameToBlockId.set(normalizeName(block.metadata.name), block.id)
|
||||
}
|
||||
@@ -47,7 +108,9 @@ export class BlockResolver implements Resolver {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const block = this.blockById.get(blockId)
|
||||
const output = this.getBlockOutput(blockId, context)
|
||||
|
||||
if (output === undefined) {
|
||||
return undefined
|
||||
}
|
||||
@@ -63,9 +126,6 @@ export class BlockResolver implements Resolver {
|
||||
return result
|
||||
}
|
||||
|
||||
// If failed, check if we should try backwards compatibility fallback
|
||||
const block = this.workflow.blocks.find((b) => b.id === blockId)
|
||||
|
||||
// Response block backwards compatibility:
|
||||
// Old: <responseBlock.response.data> -> New: <responseBlock.data>
|
||||
// Only apply fallback if:
|
||||
@@ -108,6 +168,14 @@ export class BlockResolver implements Resolver {
|
||||
}
|
||||
}
|
||||
|
||||
const schemaFields = getSchemaFieldNames(block?.outputs)
|
||||
if (schemaFields.length > 0 && !isPathInOutputSchema(block?.outputs, pathParts)) {
|
||||
throw new Error(
|
||||
`"${pathParts.join('.')}" doesn't exist on block "${blockName}". ` +
|
||||
`Available fields: ${schemaFields.join(', ')}`
|
||||
)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -2499,7 +2499,9 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, any> = {
|
||||
async execute(params: EditWorkflowParams, context?: { userId: string }): Promise<any> {
|
||||
const logger = createLogger('EditWorkflowServerTool')
|
||||
const { operations, workflowId, currentUserWorkflow } = params
|
||||
if (!operations || operations.length === 0) throw new Error('operations are required')
|
||||
if (!Array.isArray(operations) || operations.length === 0) {
|
||||
throw new Error('operations are required and must be an array')
|
||||
}
|
||||
if (!workflowId) throw new Error('workflowId is required')
|
||||
|
||||
logger.info('Executing edit_workflow', {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import crypto from 'crypto'
|
||||
import {
|
||||
db,
|
||||
webhook,
|
||||
workflow,
|
||||
workflowBlocks,
|
||||
workflowDeploymentVersion,
|
||||
@@ -22,7 +21,6 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w
|
||||
const logger = createLogger('WorkflowDBHelpers')
|
||||
|
||||
export type WorkflowDeploymentVersion = InferSelectModel<typeof workflowDeploymentVersion>
|
||||
type WebhookRecord = InferSelectModel<typeof webhook>
|
||||
type SubflowInsert = InferInsertModel<typeof workflowSubflows>
|
||||
|
||||
export interface WorkflowDeploymentVersionResponse {
|
||||
@@ -337,18 +335,6 @@ export async function saveWorkflowToNormalizedTables(
|
||||
|
||||
// Start a transaction
|
||||
await db.transaction(async (tx) => {
|
||||
// Snapshot existing webhooks before deletion to preserve them through the cycle
|
||||
let existingWebhooks: WebhookRecord[] = []
|
||||
try {
|
||||
existingWebhooks = await tx.select().from(webhook).where(eq(webhook.workflowId, workflowId))
|
||||
} catch (webhookError) {
|
||||
// Webhook table might not be available in test environments
|
||||
logger.debug('Could not load webhooks before save, skipping preservation', {
|
||||
error: webhookError instanceof Error ? webhookError.message : String(webhookError),
|
||||
})
|
||||
}
|
||||
|
||||
// Clear existing data for this workflow
|
||||
await Promise.all([
|
||||
tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)),
|
||||
tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)),
|
||||
@@ -419,42 +405,6 @@ export async function saveWorkflowToNormalizedTables(
|
||||
if (subflowInserts.length > 0) {
|
||||
await tx.insert(workflowSubflows).values(subflowInserts)
|
||||
}
|
||||
|
||||
// Re-insert preserved webhooks if any exist and their blocks still exist
|
||||
if (existingWebhooks.length > 0) {
|
||||
try {
|
||||
const webhookInserts = existingWebhooks
|
||||
.filter((wh) => !!state.blocks?.[wh.blockId ?? ''])
|
||||
.map((wh) => ({
|
||||
id: wh.id,
|
||||
workflowId: wh.workflowId,
|
||||
blockId: wh.blockId,
|
||||
path: wh.path,
|
||||
provider: wh.provider,
|
||||
providerConfig: wh.providerConfig,
|
||||
credentialSetId: wh.credentialSetId,
|
||||
isActive: wh.isActive,
|
||||
createdAt: wh.createdAt,
|
||||
updatedAt: new Date(),
|
||||
}))
|
||||
|
||||
if (webhookInserts.length > 0) {
|
||||
await tx.insert(webhook).values(webhookInserts)
|
||||
logger.debug(`Preserved ${webhookInserts.length} webhook(s) through workflow save`, {
|
||||
workflowId,
|
||||
})
|
||||
}
|
||||
} catch (webhookInsertError) {
|
||||
// Webhook preservation is optional - don't fail the entire save if it errors
|
||||
logger.warn('Could not preserve webhooks during save', {
|
||||
error:
|
||||
webhookInsertError instanceof Error
|
||||
? webhookInsertError.message
|
||||
: String(webhookInsertError),
|
||||
workflowId,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as schema from '@sim/db'
|
||||
import { webhook, workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import type { InferSelectModel } from 'drizzle-orm'
|
||||
import { and, eq, inArray, or, sql } from 'drizzle-orm'
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
import postgres from 'postgres'
|
||||
@@ -1175,14 +1174,6 @@ async function handleWorkflowOperationTx(
|
||||
parallelCount: Object.keys(parallels || {}).length,
|
||||
})
|
||||
|
||||
// Snapshot existing webhooks before deletion to preserve them through the cycle
|
||||
// (workflowBlocks has CASCADE DELETE to webhook table)
|
||||
const existingWebhooks = await tx
|
||||
.select()
|
||||
.from(webhook)
|
||||
.where(eq(webhook.workflowId, workflowId))
|
||||
|
||||
// Delete all existing blocks (this will cascade delete edges and webhooks via ON DELETE CASCADE)
|
||||
await tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId))
|
||||
|
||||
// Delete all existing subflows
|
||||
@@ -1248,32 +1239,6 @@ async function handleWorkflowOperationTx(
|
||||
await tx.insert(workflowSubflows).values(parallelValues)
|
||||
}
|
||||
|
||||
// Re-insert preserved webhooks if any exist and their blocks still exist
|
||||
type WebhookRecord = InferSelectModel<typeof webhook>
|
||||
if (existingWebhooks.length > 0) {
|
||||
const webhookInserts = existingWebhooks
|
||||
.filter((wh: WebhookRecord) => !!blocks?.[wh.blockId ?? ''])
|
||||
.map((wh: WebhookRecord) => ({
|
||||
id: wh.id,
|
||||
workflowId: wh.workflowId,
|
||||
blockId: wh.blockId,
|
||||
path: wh.path,
|
||||
provider: wh.provider,
|
||||
providerConfig: wh.providerConfig,
|
||||
credentialSetId: wh.credentialSetId,
|
||||
isActive: wh.isActive,
|
||||
createdAt: wh.createdAt,
|
||||
updatedAt: new Date(),
|
||||
}))
|
||||
|
||||
if (webhookInserts.length > 0) {
|
||||
await tx.insert(webhook).values(webhookInserts)
|
||||
logger.debug(`Preserved ${webhookInserts.length} webhook(s) through state replacement`, {
|
||||
workflowId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Successfully replaced workflow state for ${workflowId}`)
|
||||
break
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user