mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix(copilot-subflows): copilot-added subflows id mismatch (#1977)
This commit is contained in:
committed by
GitHub
parent
5457d4bc7b
commit
b3caef1f31
@@ -10,6 +10,8 @@ import { extractAndPersistCustomTools } from '@/lib/workflows/custom-tools-persi
|
||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
|
||||
import { getWorkflowAccessContext } from '@/lib/workflows/utils'
|
||||
import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('WorkflowStateAPI')
|
||||
|
||||
@@ -175,11 +177,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
{} as typeof state.blocks
|
||||
)
|
||||
|
||||
const typedBlocks = filteredBlocks as Record<string, BlockState>
|
||||
const canonicalLoops = generateLoopBlocks(typedBlocks)
|
||||
const canonicalParallels = generateParallelBlocks(typedBlocks)
|
||||
|
||||
const workflowState = {
|
||||
blocks: filteredBlocks,
|
||||
edges: state.edges,
|
||||
loops: state.loops || {},
|
||||
parallels: state.parallels || {},
|
||||
loops: canonicalLoops,
|
||||
parallels: canonicalParallels,
|
||||
lastSaved: state.lastSaved || Date.now(),
|
||||
isDeployed: state.isDeployed || false,
|
||||
deployedAt: state.deployedAt,
|
||||
|
||||
@@ -124,6 +124,58 @@ const mockBlocksFromDb = [
|
||||
parentId: 'loop-1',
|
||||
extent: 'parent',
|
||||
},
|
||||
{
|
||||
id: 'loop-1',
|
||||
workflowId: mockWorkflowId,
|
||||
type: 'loop',
|
||||
name: 'Loop Container',
|
||||
positionX: 50,
|
||||
positionY: 50,
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
advancedMode: false,
|
||||
triggerMode: false,
|
||||
height: 250,
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
data: { width: 500, height: 300, loopType: 'for', count: 5 },
|
||||
parentId: null,
|
||||
extent: null,
|
||||
},
|
||||
{
|
||||
id: 'parallel-1',
|
||||
workflowId: mockWorkflowId,
|
||||
type: 'parallel',
|
||||
name: 'Parallel Container',
|
||||
positionX: 600,
|
||||
positionY: 50,
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
advancedMode: false,
|
||||
triggerMode: false,
|
||||
height: 250,
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
data: { width: 500, height: 300, parallelType: 'count', count: 3 },
|
||||
parentId: null,
|
||||
extent: null,
|
||||
},
|
||||
{
|
||||
id: 'block-3',
|
||||
workflowId: mockWorkflowId,
|
||||
type: 'api',
|
||||
name: 'Parallel Child',
|
||||
positionX: 650,
|
||||
positionY: 150,
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
height: 200,
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
data: { parentId: 'parallel-1', extent: 'parent' },
|
||||
parentId: 'parallel-1',
|
||||
extent: 'parent',
|
||||
},
|
||||
]
|
||||
|
||||
const mockEdgesFromDb = [
|
||||
@@ -187,6 +239,42 @@ const mockWorkflowState: WorkflowState = {
|
||||
height: 200,
|
||||
data: { parentId: 'loop-1', extent: 'parent' },
|
||||
},
|
||||
'loop-1': {
|
||||
id: 'loop-1',
|
||||
type: 'loop',
|
||||
name: 'Loop Container',
|
||||
position: { x: 200, y: 50 },
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
height: 250,
|
||||
data: { width: 500, height: 300, count: 5, loopType: 'for' },
|
||||
},
|
||||
'parallel-1': {
|
||||
id: 'parallel-1',
|
||||
type: 'parallel',
|
||||
name: 'Parallel Container',
|
||||
position: { x: 600, y: 50 },
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
height: 250,
|
||||
data: { width: 500, height: 300, parallelType: 'count', count: 3 },
|
||||
},
|
||||
'block-3': {
|
||||
id: 'block-3',
|
||||
type: 'api',
|
||||
name: 'Parallel Child',
|
||||
position: { x: 650, y: 150 },
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
height: 180,
|
||||
data: { parentId: 'parallel-1', extent: 'parent' },
|
||||
},
|
||||
},
|
||||
edges: [
|
||||
{
|
||||
@@ -567,20 +655,36 @@ describe('Database Helpers', () => {
|
||||
|
||||
await dbHelpers.saveWorkflowToNormalizedTables(mockWorkflowId, mockWorkflowState)
|
||||
|
||||
expect(capturedBlockInserts).toHaveLength(2)
|
||||
expect(capturedBlockInserts[0]).toMatchObject({
|
||||
id: 'block-1',
|
||||
workflowId: mockWorkflowId,
|
||||
type: 'starter',
|
||||
name: 'Start Block',
|
||||
positionX: '100',
|
||||
positionY: '100',
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
height: '150',
|
||||
parentId: null,
|
||||
extent: null,
|
||||
})
|
||||
expect(capturedBlockInserts).toHaveLength(5)
|
||||
expect(capturedBlockInserts).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'block-1',
|
||||
workflowId: mockWorkflowId,
|
||||
type: 'starter',
|
||||
name: 'Start Block',
|
||||
positionX: '100',
|
||||
positionY: '100',
|
||||
enabled: true,
|
||||
horizontalHandles: true,
|
||||
height: '150',
|
||||
parentId: null,
|
||||
extent: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'loop-1',
|
||||
workflowId: mockWorkflowId,
|
||||
type: 'loop',
|
||||
parentId: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'parallel-1',
|
||||
workflowId: mockWorkflowId,
|
||||
type: 'parallel',
|
||||
parentId: null,
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
expect(capturedEdgeInserts).toHaveLength(1)
|
||||
expect(capturedEdgeInserts[0]).toMatchObject({
|
||||
@@ -599,6 +703,48 @@ describe('Database Helpers', () => {
|
||||
type: 'loop',
|
||||
})
|
||||
})
|
||||
|
||||
it('should regenerate missing loop and parallel definitions from block data', async () => {
|
||||
let capturedSubflowInserts: any[] = []
|
||||
|
||||
const mockTransaction = vi.fn().mockImplementation(async (callback) => {
|
||||
const tx = {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
}),
|
||||
}),
|
||||
delete: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockResolvedValue([]),
|
||||
}),
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockImplementation((data) => {
|
||||
if (data.length > 0 && (data[0].type === 'loop' || data[0].type === 'parallel')) {
|
||||
capturedSubflowInserts = data
|
||||
}
|
||||
return Promise.resolve([])
|
||||
}),
|
||||
}),
|
||||
}
|
||||
return await callback(tx)
|
||||
})
|
||||
|
||||
mockDb.transaction = mockTransaction
|
||||
|
||||
const staleWorkflowState = JSON.parse(JSON.stringify(mockWorkflowState)) as WorkflowState
|
||||
staleWorkflowState.loops = {}
|
||||
staleWorkflowState.parallels = {}
|
||||
|
||||
await dbHelpers.saveWorkflowToNormalizedTables(mockWorkflowId, staleWorkflowState)
|
||||
|
||||
expect(capturedSubflowInserts).toHaveLength(2)
|
||||
expect(capturedSubflowInserts).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'loop-1', type: 'loop' }),
|
||||
expect.objectContaining({ id: 'parallel-1', type: 'parallel' }),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('workflowExistsInNormalizedTables', () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation'
|
||||
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import { SUBFLOW_TYPES } from '@/stores/workflows/workflow/types'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('WorkflowDBHelpers')
|
||||
|
||||
@@ -248,6 +249,10 @@ export async function saveWorkflowToNormalizedTables(
|
||||
state: WorkflowState
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const blockRecords = state.blocks as Record<string, BlockState>
|
||||
const canonicalLoops = generateLoopBlocks(blockRecords)
|
||||
const canonicalParallels = generateParallelBlocks(blockRecords)
|
||||
|
||||
// Start a transaction
|
||||
await db.transaction(async (tx) => {
|
||||
// Snapshot existing webhooks before deletion to preserve them through the cycle
|
||||
@@ -310,7 +315,7 @@ export async function saveWorkflowToNormalizedTables(
|
||||
const subflowInserts: any[] = []
|
||||
|
||||
// Add loops
|
||||
Object.values(state.loops || {}).forEach((loop) => {
|
||||
Object.values(canonicalLoops).forEach((loop) => {
|
||||
subflowInserts.push({
|
||||
id: loop.id,
|
||||
workflowId: workflowId,
|
||||
@@ -320,7 +325,7 @@ export async function saveWorkflowToNormalizedTables(
|
||||
})
|
||||
|
||||
// Add parallels
|
||||
Object.values(state.parallels || {}).forEach((parallel) => {
|
||||
Object.values(canonicalParallels).forEach((parallel) => {
|
||||
subflowInserts.push({
|
||||
id: parallel.id,
|
||||
workflowId: workflowId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Edge } from 'reactflow'
|
||||
import { sanitizeWorkflowForSharing } from '@/lib/workflows/credential-extractor'
|
||||
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
import { TRIGGER_PERSISTED_SUBBLOCK_IDS } from '@/triggers/consts'
|
||||
|
||||
/**
|
||||
@@ -386,12 +387,15 @@ export function sanitizeForCopilot(state: WorkflowState): CopilotWorkflowState {
|
||||
* Users need positions to restore the visual layout when importing
|
||||
*/
|
||||
export function sanitizeForExport(state: WorkflowState): ExportWorkflowState {
|
||||
const canonicalLoops = generateLoopBlocks(state.blocks || {})
|
||||
const canonicalParallels = generateParallelBlocks(state.blocks || {})
|
||||
|
||||
// Preserve edges, loops, parallels, metadata, and variables
|
||||
const fullState = {
|
||||
blocks: state.blocks,
|
||||
edges: state.edges,
|
||||
loops: state.loops || {},
|
||||
parallels: state.parallels || {},
|
||||
loops: canonicalLoops,
|
||||
parallels: canonicalParallels,
|
||||
metadata: state.metadata,
|
||||
variables: state.variables,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getBlock } from '@/blocks'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
import { getTool } from '@/tools/utils'
|
||||
|
||||
const logger = createLogger('Serializer')
|
||||
@@ -41,12 +42,15 @@ export class Serializer {
|
||||
serializeWorkflow(
|
||||
blocks: Record<string, BlockState>,
|
||||
edges: Edge[],
|
||||
loops: Record<string, Loop>,
|
||||
loops?: Record<string, Loop>,
|
||||
parallels?: Record<string, Parallel>,
|
||||
validateRequired = false
|
||||
): SerializedWorkflow {
|
||||
const safeLoops = loops || {}
|
||||
const safeParallels = parallels || {}
|
||||
const canonicalLoops = generateLoopBlocks(blocks)
|
||||
const canonicalParallels = generateParallelBlocks(blocks)
|
||||
const safeLoops = Object.keys(canonicalLoops).length > 0 ? canonicalLoops : loops || {}
|
||||
const safeParallels =
|
||||
Object.keys(canonicalParallels).length > 0 ? canonicalParallels : parallels || {}
|
||||
const accessibleBlocksMap = this.computeAccessibleBlockIds(
|
||||
blocks,
|
||||
edges,
|
||||
|
||||
Reference in New Issue
Block a user