Skip to content

Commit 6489916

Browse files
committed
cleanup migration code
1 parent 8e116d6 commit 6489916

File tree

3 files changed

+136
-82
lines changed

3 files changed

+136
-82
lines changed

apps/sim/lib/workflows/migrations/subblock-migrations.test.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function makeBlock(overrides: Partial<BlockState> & { type: string }): BlockStat
2020
describe('migrateSubblockIds', () => {
2121
describe('knowledge block', () => {
2222
it('should rename knowledgeBaseId to knowledgeBaseSelector', () => {
23-
const blocks: Record<string, BlockState> = {
23+
const input: Record<string, BlockState> = {
2424
b1: makeBlock({
2525
type: 'knowledge',
2626
subBlocks: {
@@ -34,7 +34,7 @@ describe('migrateSubblockIds', () => {
3434
}),
3535
}
3636

37-
const migrated = migrateSubblockIds(blocks)
37+
const { blocks, migrated } = migrateSubblockIds(input)
3838

3939
expect(migrated).toBe(true)
4040
expect(blocks['b1'].subBlocks['knowledgeBaseSelector']).toEqual({
@@ -47,7 +47,7 @@ describe('migrateSubblockIds', () => {
4747
})
4848

4949
it('should prefer new key when both old and new exist', () => {
50-
const blocks: Record<string, BlockState> = {
50+
const input: Record<string, BlockState> = {
5151
b1: makeBlock({
5252
type: 'knowledge',
5353
subBlocks: {
@@ -65,15 +65,15 @@ describe('migrateSubblockIds', () => {
6565
}),
6666
}
6767

68-
const migrated = migrateSubblockIds(blocks)
68+
const { blocks, migrated } = migrateSubblockIds(input)
6969

7070
expect(migrated).toBe(true)
7171
expect(blocks['b1'].subBlocks['knowledgeBaseSelector'].value).toBe('fresh-kb')
7272
expect(blocks['b1'].subBlocks['knowledgeBaseId']).toBeUndefined()
7373
})
7474

7575
it('should not touch blocks that already use the new key', () => {
76-
const blocks: Record<string, BlockState> = {
76+
const input: Record<string, BlockState> = {
7777
b1: makeBlock({
7878
type: 'knowledge',
7979
subBlocks: {
@@ -86,15 +86,36 @@ describe('migrateSubblockIds', () => {
8686
}),
8787
}
8888

89-
const migrated = migrateSubblockIds(blocks)
89+
const { blocks, migrated } = migrateSubblockIds(input)
9090

9191
expect(migrated).toBe(false)
9292
expect(blocks['b1'].subBlocks['knowledgeBaseSelector'].value).toBe('kb-uuid')
9393
})
9494
})
9595

96+
it('should not mutate the input blocks', () => {
97+
const input: Record<string, BlockState> = {
98+
b1: makeBlock({
99+
type: 'knowledge',
100+
subBlocks: {
101+
knowledgeBaseId: {
102+
id: 'knowledgeBaseId',
103+
type: 'knowledge-base-selector',
104+
value: 'kb-uuid',
105+
},
106+
},
107+
}),
108+
}
109+
110+
const { blocks } = migrateSubblockIds(input)
111+
112+
expect(input['b1'].subBlocks['knowledgeBaseId']).toBeDefined()
113+
expect(blocks['b1'].subBlocks['knowledgeBaseSelector']).toBeDefined()
114+
expect(blocks).not.toBe(input)
115+
})
116+
96117
it('should skip blocks with no registered migrations', () => {
97-
const blocks: Record<string, BlockState> = {
118+
const input: Record<string, BlockState> = {
98119
b1: makeBlock({
99120
type: 'function',
100121
subBlocks: {
@@ -103,14 +124,14 @@ describe('migrateSubblockIds', () => {
103124
}),
104125
}
105126

106-
const migrated = migrateSubblockIds(blocks)
127+
const { blocks, migrated } = migrateSubblockIds(input)
107128

108129
expect(migrated).toBe(false)
109130
expect(blocks['b1'].subBlocks['code'].value).toBe('console.log("hi")')
110131
})
111132

112133
it('should migrate multiple blocks in one pass', () => {
113-
const blocks: Record<string, BlockState> = {
134+
const input: Record<string, BlockState> = {
114135
b1: makeBlock({
115136
id: 'b1',
116137
type: 'knowledge',
@@ -142,7 +163,7 @@ describe('migrateSubblockIds', () => {
142163
}),
143164
}
144165

145-
const migrated = migrateSubblockIds(blocks)
166+
const { blocks, migrated } = migrateSubblockIds(input)
146167

147168
expect(migrated).toBe(true)
148169
expect(blocks['b1'].subBlocks['knowledgeBaseSelector'].value).toBe('kb-1')
@@ -151,11 +172,11 @@ describe('migrateSubblockIds', () => {
151172
})
152173

153174
it('should handle blocks with empty subBlocks', () => {
154-
const blocks: Record<string, BlockState> = {
175+
const input: Record<string, BlockState> = {
155176
b1: makeBlock({ type: 'knowledge', subBlocks: {} }),
156177
}
157178

158-
const migrated = migrateSubblockIds(blocks)
179+
const { migrated } = migrateSubblockIds(input)
159180

160181
expect(migrated).toBe(false)
161182
})

apps/sim/lib/workflows/migrations/subblock-migrations.ts

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,58 +20,71 @@ export const SUBBLOCK_ID_MIGRATIONS: Record<string, Record<string, string>> = {
2020

2121
/**
2222
* Migrates legacy subblock IDs inside a single block's subBlocks map.
23-
* If an old key is found and the new key does not already exist, the entry
24-
* is moved to the new key. When both exist the new key wins (user already
25-
* re-saved) and the old entry is removed to avoid orphans.
26-
*
27-
* Returns true if any migration was applied.
23+
* Returns a new subBlocks record if anything changed, or the original if not.
2824
*/
29-
function migrateBlockSubblockIds(block: BlockState, renames: Record<string, string>): boolean {
30-
const subBlocks = block.subBlocks
31-
if (!subBlocks) return false
32-
25+
function migrateBlockSubblockIds(
26+
subBlocks: Record<string, BlockState['subBlocks'][string]>,
27+
renames: Record<string, string>
28+
): { subBlocks: Record<string, BlockState['subBlocks'][string]>; migrated: boolean } {
3329
let migrated = false
3430

31+
for (const oldId of Object.keys(renames)) {
32+
if (oldId in subBlocks) {
33+
migrated = true
34+
break
35+
}
36+
}
37+
38+
if (!migrated) return { subBlocks, migrated: false }
39+
40+
const result = { ...subBlocks }
41+
3542
for (const [oldId, newId] of Object.entries(renames)) {
36-
if (!(oldId in subBlocks)) continue
43+
if (!(oldId in result)) continue
3744

38-
if (newId in subBlocks) {
39-
delete subBlocks[oldId]
40-
migrated = true
45+
if (newId in result) {
46+
delete result[oldId]
4147
continue
4248
}
4349

44-
const oldEntry = subBlocks[oldId]
45-
subBlocks[newId] = { ...oldEntry, id: newId }
46-
delete subBlocks[oldId]
47-
migrated = true
50+
const oldEntry = result[oldId]
51+
result[newId] = { ...oldEntry, id: newId }
52+
delete result[oldId]
4853
}
4954

50-
return migrated
55+
return { subBlocks: result, migrated: true }
5156
}
5257

5358
/**
5459
* Applies subblock-ID migrations to every block in a workflow.
55-
* Safe to call on any state – blocks whose type has no registered
56-
* migrations are left untouched.
57-
*
58-
* Mutates `blocks` in place and returns whether anything changed.
60+
* Returns a new blocks record with migrated subBlocks where needed.
5961
*/
60-
export function migrateSubblockIds(blocks: Record<string, BlockState>): boolean {
62+
export function migrateSubblockIds(blocks: Record<string, BlockState>): {
63+
blocks: Record<string, BlockState>
64+
migrated: boolean
65+
} {
6166
let anyMigrated = false
67+
const result: Record<string, BlockState> = {}
6268

63-
for (const block of Object.values(blocks)) {
69+
for (const [blockId, block] of Object.entries(blocks)) {
6470
const renames = SUBBLOCK_ID_MIGRATIONS[block.type]
65-
if (!renames) continue
71+
if (!renames || !block.subBlocks) {
72+
result[blockId] = block
73+
continue
74+
}
6675

67-
if (migrateBlockSubblockIds(block, renames)) {
76+
const { subBlocks, migrated } = migrateBlockSubblockIds(block.subBlocks, renames)
77+
if (migrated) {
6878
logger.info('Migrated legacy subblock IDs', {
6979
blockId: block.id,
7080
blockType: block.type,
7181
})
7282
anyMigrated = true
83+
result[blockId] = { ...block, subBlocks }
84+
} else {
85+
result[blockId] = block
7386
}
7487
}
7588

76-
return anyMigrated
89+
return { blocks: result, migrated: anyMigrated }
7790
}

apps/sim/lib/workflows/persistence/utils.ts

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,13 @@ export async function loadDeployedWorkflowState(
114114
resolvedWorkspaceId = wfRow?.workspaceId ?? undefined
115115
}
116116

117-
const resolvedBlocks = state.blocks || {}
118-
const { blocks: credMigratedBlocks } = resolvedWorkspaceId
119-
? await migrateCredentialIds(resolvedBlocks, resolvedWorkspaceId)
120-
: { blocks: resolvedBlocks }
121-
122-
migrateSubblockIds(credMigratedBlocks)
117+
const { blocks: migratedBlocks } = await applyBlockMigrations(
118+
state.blocks || {},
119+
resolvedWorkspaceId
120+
)
123121

124122
return {
125-
blocks: credMigratedBlocks,
123+
blocks: migratedBlocks,
126124
edges: state.edges || [],
127125
loops: state.loops || {},
128126
parallels: state.parallels || {},
@@ -136,6 +134,50 @@ export async function loadDeployedWorkflowState(
136134
}
137135
}
138136

137+
interface MigrationContext {
138+
blocks: Record<string, BlockState>
139+
workspaceId?: string
140+
migrated: boolean
141+
}
142+
143+
type BlockMigration = (ctx: MigrationContext) => MigrationContext | Promise<MigrationContext>
144+
145+
function createMigrationPipeline(migrations: BlockMigration[]) {
146+
return async (
147+
blocks: Record<string, BlockState>,
148+
workspaceId?: string
149+
): Promise<{ blocks: Record<string, BlockState>; migrated: boolean }> => {
150+
let ctx: MigrationContext = { blocks, workspaceId, migrated: false }
151+
for (const migration of migrations) {
152+
ctx = await migration(ctx)
153+
}
154+
return { blocks: ctx.blocks, migrated: ctx.migrated }
155+
}
156+
}
157+
158+
const applyBlockMigrations = createMigrationPipeline([
159+
(ctx) => {
160+
const { blocks } = sanitizeAgentToolsInBlocks(ctx.blocks)
161+
return { ...ctx, blocks }
162+
},
163+
164+
(ctx) => ({
165+
...ctx,
166+
blocks: migrateAgentBlocksToMessagesFormat(ctx.blocks),
167+
}),
168+
169+
async (ctx) => {
170+
if (!ctx.workspaceId) return ctx
171+
const { blocks, migrated } = await migrateCredentialIds(ctx.blocks, ctx.workspaceId)
172+
return { ...ctx, blocks, migrated: ctx.migrated || migrated }
173+
},
174+
175+
(ctx) => {
176+
const { blocks, migrated } = migrateSubblockIds(ctx.blocks)
177+
return { ...ctx, blocks, migrated: ctx.migrated || migrated }
178+
},
179+
])
180+
139181
/**
140182
* Migrates agent blocks from old format (systemPrompt/userPrompt) to new format (messages array)
141183
* This ensures backward compatibility for workflows created before the messages-input refactor.
@@ -359,22 +401,16 @@ export async function loadWorkflowFromNormalizedTables(
359401
blocksMap[block.id] = assembled
360402
})
361403

362-
// Sanitize any invalid custom tools in agent blocks to prevent client crashes
363-
const { blocks: sanitizedBlocks } = sanitizeAgentToolsInBlocks(blocksMap)
364-
365-
// Migrate old agent block format (systemPrompt/userPrompt) to new messages array format
366-
const migratedBlocks = migrateAgentBlocksToMessagesFormat(sanitizedBlocks)
367-
368-
// Migrate legacy account.id → credential.id in OAuth subblocks
369-
const { blocks: credMigratedBlocks, migrated: credentialsMigrated } = workflowRow?.workspaceId
370-
? await migrateCredentialIds(migratedBlocks, workflowRow.workspaceId)
371-
: { blocks: migratedBlocks, migrated: false }
404+
const { blocks: finalBlocks, migrated } = await applyBlockMigrations(
405+
blocksMap,
406+
workflowRow?.workspaceId ?? undefined
407+
)
372408

373-
if (credentialsMigrated) {
409+
if (migrated) {
374410
Promise.resolve().then(async () => {
375411
try {
376-
for (const [blockId, block] of Object.entries(credMigratedBlocks)) {
377-
if (block.subBlocks !== migratedBlocks[blockId]?.subBlocks) {
412+
for (const [blockId, block] of Object.entries(finalBlocks)) {
413+
if (block.subBlocks !== blocksMap[blockId]?.subBlocks) {
378414
await db
379415
.update(workflowBlocks)
380416
.set({ subBlocks: block.subBlocks, updatedAt: new Date() })
@@ -384,23 +420,7 @@ export async function loadWorkflowFromNormalizedTables(
384420
}
385421
}
386422
} catch (err) {
387-
logger.warn('Failed to persist credential ID migration', { workflowId, error: err })
388-
}
389-
})
390-
}
391-
392-
const subblockMigrated = migrateSubblockIds(credMigratedBlocks)
393-
if (subblockMigrated) {
394-
Promise.resolve().then(async () => {
395-
try {
396-
for (const [blockId, block] of Object.entries(credMigratedBlocks)) {
397-
await db
398-
.update(workflowBlocks)
399-
.set({ subBlocks: block.subBlocks, updatedAt: new Date() })
400-
.where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId)))
401-
}
402-
} catch (err) {
403-
logger.warn('Failed to persist subblock ID migration', { workflowId, error: err })
423+
logger.warn('Failed to persist block migrations', { workflowId, error: err })
404424
}
405425
})
406426
}
@@ -441,13 +461,13 @@ export async function loadWorkflowFromNormalizedTables(
441461
forEachItems: (config as Loop).forEachItems ?? '',
442462
whileCondition: (config as Loop).whileCondition ?? '',
443463
doWhileCondition: (config as Loop).doWhileCondition ?? '',
444-
enabled: credMigratedBlocks[subflow.id]?.enabled ?? true,
464+
enabled: finalBlocks[subflow.id]?.enabled ?? true,
445465
}
446466
loops[subflow.id] = loop
447467

448-
if (credMigratedBlocks[subflow.id]) {
449-
const block = credMigratedBlocks[subflow.id]
450-
credMigratedBlocks[subflow.id] = {
468+
if (finalBlocks[subflow.id]) {
469+
const block = finalBlocks[subflow.id]
470+
finalBlocks[subflow.id] = {
451471
...block,
452472
data: {
453473
...block.data,
@@ -468,7 +488,7 @@ export async function loadWorkflowFromNormalizedTables(
468488
(config as Parallel).parallelType === 'collection'
469489
? (config as Parallel).parallelType
470490
: 'count',
471-
enabled: credMigratedBlocks[subflow.id]?.enabled ?? true,
491+
enabled: finalBlocks[subflow.id]?.enabled ?? true,
472492
}
473493
parallels[subflow.id] = parallel
474494
} else {
@@ -477,7 +497,7 @@ export async function loadWorkflowFromNormalizedTables(
477497
})
478498

479499
return {
480-
blocks: credMigratedBlocks,
500+
blocks: finalBlocks,
481501
edges: edgesArray,
482502
loops,
483503
parallels,

0 commit comments

Comments
 (0)