@@ -124,6 +124,58 @@ const mockBlocksFromDb = [
124124 parentId: 'loop-1',
125125 extent: 'parent',
126126 },
127+ {
128+ id: 'loop-1',
129+ workflowId: mockWorkflowId,
130+ type: 'loop',
131+ name: 'Loop Container',
132+ positionX: 50,
133+ positionY: 50,
134+ enabled: true,
135+ horizontalHandles: true,
136+ advancedMode: false,
137+ triggerMode: false,
138+ height: 250,
139+ subBlocks: {},
140+ outputs: {},
141+ data: { width: 500, height: 300, loopType: 'for', count: 5 },
142+ parentId: null,
143+ extent: null,
144+ },
145+ {
146+ id: 'parallel-1',
147+ workflowId: mockWorkflowId,
148+ type: 'parallel',
149+ name: 'Parallel Container',
150+ positionX: 600,
151+ positionY: 50,
152+ enabled: true,
153+ horizontalHandles: true,
154+ advancedMode: false,
155+ triggerMode: false,
156+ height: 250,
157+ subBlocks: {},
158+ outputs: {},
159+ data: { width: 500, height: 300, parallelType: 'count', count: 3 },
160+ parentId: null,
161+ extent: null,
162+ },
163+ {
164+ id: 'block-3',
165+ workflowId: mockWorkflowId,
166+ type: 'api',
167+ name: 'Parallel Child',
168+ positionX: 650,
169+ positionY: 150,
170+ enabled: true,
171+ horizontalHandles: true,
172+ height: 200,
173+ subBlocks: {},
174+ outputs: {},
175+ data: { parentId: 'parallel-1', extent: 'parent' },
176+ parentId: 'parallel-1',
177+ extent: 'parent',
178+ },
127179]
128180
129181const mockEdgesFromDb = [
@@ -187,6 +239,42 @@ const mockWorkflowState: WorkflowState = {
187239 height: 200,
188240 data: { parentId: 'loop-1', extent: 'parent' },
189241 },
242+ 'loop-1': {
243+ id: 'loop-1',
244+ type: 'loop',
245+ name: 'Loop Container',
246+ position: { x: 200, y: 50 },
247+ subBlocks: {},
248+ outputs: {},
249+ enabled: true,
250+ horizontalHandles: true,
251+ height: 250,
252+ data: { width: 500, height: 300, count: 5, loopType: 'for' },
253+ },
254+ 'parallel-1': {
255+ id: 'parallel-1',
256+ type: 'parallel',
257+ name: 'Parallel Container',
258+ position: { x: 600, y: 50 },
259+ subBlocks: {},
260+ outputs: {},
261+ enabled: true,
262+ horizontalHandles: true,
263+ height: 250,
264+ data: { width: 500, height: 300, parallelType: 'count', count: 3 },
265+ },
266+ 'block-3': {
267+ id: 'block-3',
268+ type: 'api',
269+ name: 'Parallel Child',
270+ position: { x: 650, y: 150 },
271+ subBlocks: {},
272+ outputs: {},
273+ enabled: true,
274+ horizontalHandles: true,
275+ height: 180,
276+ data: { parentId: 'parallel-1', extent: 'parent' },
277+ },
190278 },
191279 edges: [
192280 {
@@ -567,20 +655,36 @@ describe('Database Helpers', () => {
567655
568656 await dbHelpers.saveWorkflowToNormalizedTables(mockWorkflowId, mockWorkflowState)
569657
570- expect(capturedBlockInserts).toHaveLength(2)
571- expect(capturedBlockInserts[0]).toMatchObject({
572- id: 'block-1',
573- workflowId: mockWorkflowId,
574- type: 'starter',
575- name: 'Start Block',
576- positionX: '100',
577- positionY: '100',
578- enabled: true,
579- horizontalHandles: true,
580- height: '150',
581- parentId: null,
582- extent: null,
583- })
658+ expect(capturedBlockInserts).toHaveLength(5)
659+ expect(capturedBlockInserts).toEqual(
660+ expect.arrayContaining([
661+ expect.objectContaining({
662+ id: 'block-1',
663+ workflowId: mockWorkflowId,
664+ type: 'starter',
665+ name: 'Start Block',
666+ positionX: '100',
667+ positionY: '100',
668+ enabled: true,
669+ horizontalHandles: true,
670+ height: '150',
671+ parentId: null,
672+ extent: null,
673+ }),
674+ expect.objectContaining({
675+ id: 'loop-1',
676+ workflowId: mockWorkflowId,
677+ type: 'loop',
678+ parentId: null,
679+ }),
680+ expect.objectContaining({
681+ id: 'parallel-1',
682+ workflowId: mockWorkflowId,
683+ type: 'parallel',
684+ parentId: null,
685+ }),
686+ ])
687+ )
584688
585689 expect(capturedEdgeInserts).toHaveLength(1)
586690 expect(capturedEdgeInserts[0]).toMatchObject({
@@ -599,6 +703,48 @@ describe('Database Helpers', () => {
599703 type: 'loop',
600704 })
601705 })
706+
707+ it('should regenerate missing loop and parallel definitions from block data', async () => {
708+ let capturedSubflowInserts: any[] = []
709+
710+ const mockTransaction = vi.fn().mockImplementation(async (callback) => {
711+ const tx = {
712+ select: vi.fn().mockReturnValue({
713+ from: vi.fn().mockReturnValue({
714+ where: vi.fn().mockResolvedValue([]),
715+ }),
716+ }),
717+ delete: vi.fn().mockReturnValue({
718+ where: vi.fn().mockResolvedValue([]),
719+ }),
720+ insert: vi.fn().mockReturnValue({
721+ values: vi.fn().mockImplementation((data) => {
722+ if (data.length > 0 && (data[0].type === 'loop' || data[0].type === 'parallel')) {
723+ capturedSubflowInserts = data
724+ }
725+ return Promise.resolve([])
726+ }),
727+ }),
728+ }
729+ return await callback(tx)
730+ })
731+
732+ mockDb.transaction = mockTransaction
733+
734+ const staleWorkflowState = JSON.parse(JSON.stringify(mockWorkflowState)) as WorkflowState
735+ staleWorkflowState.loops = {}
736+ staleWorkflowState.parallels = {}
737+
738+ await dbHelpers.saveWorkflowToNormalizedTables(mockWorkflowId, staleWorkflowState)
739+
740+ expect(capturedSubflowInserts).toHaveLength(2)
741+ expect(capturedSubflowInserts).toEqual(
742+ expect.arrayContaining([
743+ expect.objectContaining({ id: 'loop-1', type: 'loop' }),
744+ expect.objectContaining({ id: 'parallel-1', type: 'parallel' }),
745+ ])
746+ )
747+ })
602748 })
603749
604750 describe('workflowExistsInNormalizedTables', () => {
0 commit comments