Skip to content

Commit a67aa3d

Browse files
committed
improvement(credentials): move client side automigration to server side
1 parent e55d41f commit a67aa3d

File tree

4 files changed

+126
-29
lines changed

4 files changed

+126
-29
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { useCredentialSets } from '@/hooks/queries/credential-sets'
2424
import { useOAuthCredentials } from '@/hooks/queries/oauth-credentials'
2525
import { useOrganizations } from '@/hooks/queries/organization'
2626
import { useSubscriptionData } from '@/hooks/queries/subscription'
27-
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
2827
import { useCredentialRefreshTriggers } from '@/hooks/use-credential-refresh-triggers'
2928
import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status'
3029
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -55,7 +54,6 @@ export function CredentialSelector({
5554
const [isEditing, setIsEditing] = useState(false)
5655
const { activeWorkflowId } = useWorkflowRegistry()
5756
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)
58-
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
5957

6058
const requiredScopes = subBlock.requiredScopes || []
6159
const label = subBlock.placeholder || 'Select credential'
@@ -136,11 +134,6 @@ export function CredentialSelector({
136134
if (!response.ok || cancelled) return
137135
const data = await response.json()
138136
if (!cancelled && data.credential?.displayName) {
139-
if (data.credential.id !== selectedId) {
140-
collaborativeSetSubblockValue(blockId, subBlock.id, data.credential.id, {
141-
skipDependsOn: true,
142-
})
143-
}
144137
setInaccessibleCredentialName(data.credential.displayName)
145138
}
146139
} catch {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tools/credential-selector.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,6 @@ export function ToolCredentialSelector({
114114
if (!response.ok || cancelled) return
115115
const data = await response.json()
116116
if (!cancelled && data.credential?.displayName) {
117-
if (data.credential.id !== selectedId) {
118-
onChangeRef.current(data.credential.id)
119-
}
120117
setInaccessibleCredentialName(data.credential.displayName)
121118
}
122119
} catch {

apps/sim/hooks/use-collaborative-workflow.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,12 +1232,7 @@ export function useCollaborativeWorkflow() {
12321232
)
12331233

12341234
const collaborativeSetSubblockValue = useCallback(
1235-
(
1236-
blockId: string,
1237-
subblockId: string,
1238-
value: any,
1239-
options?: { _visited?: Set<string>; skipDependsOn?: boolean }
1240-
) => {
1235+
(blockId: string, subblockId: string, value: any, options?: { _visited?: Set<string> }) => {
12411236
if (isApplyingRemoteChange.current) return
12421237

12431238
if (isBaselineDiffView) {
@@ -1263,8 +1258,6 @@ export function useCollaborativeWorkflow() {
12631258
})
12641259
}
12651260

1266-
if (options?.skipDependsOn) return
1267-
12681261
// Handle dependent subblock clearing (recursive calls)
12691262
try {
12701263
const visited = options?._visited || new Set<string>()

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

Lines changed: 125 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import {
77
workflowEdges,
88
workflowSubflows,
99
} from '@sim/db'
10+
import { credential } from '@sim/db/schema'
1011
import { createLogger } from '@sim/logger'
1112
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
12-
import { and, desc, eq, sql } from 'drizzle-orm'
13+
import { and, desc, eq, inArray, sql } from 'drizzle-orm'
1314
import type { Edge } from 'reactflow'
1415
import { v4 as uuidv4 } from 'uuid'
1516
import type { DbOrTx } from '@/lib/db/types'
@@ -99,8 +100,10 @@ export async function loadDeployedWorkflowState(workflowId: string): Promise<Dep
99100

100101
const state = active.state as WorkflowState & { variables?: Record<string, unknown> }
101102

103+
const { blocks: migratedBlocks } = await migrateCredentialIds(state.blocks || {})
104+
102105
return {
103-
blocks: state.blocks || {},
106+
blocks: migratedBlocks,
104107
edges: state.edges || [],
105108
loops: state.loops || {},
106109
parallels: state.parallels || {},
@@ -185,6 +188,97 @@ export function migrateAgentBlocksToMessagesFormat(
185188
)
186189
}
187190

191+
const CREDENTIAL_SUBBLOCK_IDS = new Set(['credential', 'triggerCredentials'])
192+
193+
/**
194+
* Migrates legacy `account.id` values to `credential.id` in OAuth subblocks.
195+
* Collects all potential legacy IDs in a single batch query for efficiency.
196+
* Also migrates `tool.params.credential` in agent block tool arrays.
197+
*/
198+
async function migrateCredentialIds(
199+
blocks: Record<string, BlockState>
200+
): Promise<{ blocks: Record<string, BlockState>; migrated: boolean }> {
201+
const potentialLegacyIds = new Set<string>()
202+
203+
for (const block of Object.values(blocks)) {
204+
for (const [subBlockId, subBlock] of Object.entries(block.subBlocks || {})) {
205+
const value = (subBlock as { value?: unknown }).value
206+
if (
207+
CREDENTIAL_SUBBLOCK_IDS.has(subBlockId) &&
208+
typeof value === 'string' &&
209+
value &&
210+
!value.startsWith('cred_')
211+
) {
212+
potentialLegacyIds.add(value)
213+
}
214+
215+
if (subBlockId === 'tools' && Array.isArray(value)) {
216+
for (const tool of value) {
217+
const credParam = tool?.params?.credential
218+
if (typeof credParam === 'string' && credParam && !credParam.startsWith('cred_')) {
219+
potentialLegacyIds.add(credParam)
220+
}
221+
}
222+
}
223+
}
224+
}
225+
226+
if (potentialLegacyIds.size === 0) {
227+
return { blocks, migrated: false }
228+
}
229+
230+
const rows = await db
231+
.select({ id: credential.id, accountId: credential.accountId })
232+
.from(credential)
233+
.where(inArray(credential.accountId, [...potentialLegacyIds]))
234+
235+
if (rows.length === 0) {
236+
return { blocks, migrated: false }
237+
}
238+
239+
const accountToCredential = new Map(rows.map((r) => [r.accountId!, r.id]))
240+
241+
const migratedBlocks = Object.fromEntries(
242+
Object.entries(blocks).map(([blockId, block]) => {
243+
let blockChanged = false
244+
const newSubBlocks = { ...block.subBlocks }
245+
246+
for (const [subBlockId, subBlock] of Object.entries(newSubBlocks)) {
247+
if (CREDENTIAL_SUBBLOCK_IDS.has(subBlockId) && typeof subBlock.value === 'string') {
248+
const newId = accountToCredential.get(subBlock.value)
249+
if (newId) {
250+
newSubBlocks[subBlockId] = { ...subBlock, value: newId }
251+
blockChanged = true
252+
}
253+
}
254+
255+
if (subBlockId === 'tools' && Array.isArray(subBlock.value)) {
256+
let toolsChanged = false
257+
const newTools = (subBlock.value as any[]).map((tool: any) => {
258+
const credParam = tool?.params?.credential
259+
if (typeof credParam === 'string') {
260+
const newId = accountToCredential.get(credParam)
261+
if (newId) {
262+
toolsChanged = true
263+
return { ...tool, params: { ...tool.params, credential: newId } }
264+
}
265+
}
266+
return tool
267+
})
268+
if (toolsChanged) {
269+
newSubBlocks[subBlockId] = { ...subBlock, value: newTools as any }
270+
blockChanged = true
271+
}
272+
}
273+
}
274+
275+
return [blockId, blockChanged ? { ...block, subBlocks: newSubBlocks } : block]
276+
})
277+
)
278+
279+
return { blocks: migratedBlocks, migrated: true }
280+
}
281+
188282
/**
189283
* Load workflow state from normalized tables
190284
* Returns null if no data found (fallback to JSON blob)
@@ -236,9 +330,31 @@ export async function loadWorkflowFromNormalizedTables(
236330
const { blocks: sanitizedBlocks } = sanitizeAgentToolsInBlocks(blocksMap)
237331

238332
// Migrate old agent block format (systemPrompt/userPrompt) to new messages array format
239-
// This ensures backward compatibility for workflows created before the messages-input refactor
240333
const migratedBlocks = migrateAgentBlocksToMessagesFormat(sanitizedBlocks)
241334

335+
// Migrate legacy account.id → credential.id in OAuth subblocks
336+
const { blocks: credMigratedBlocks, migrated: credentialsMigrated } =
337+
await migrateCredentialIds(migratedBlocks)
338+
339+
if (credentialsMigrated) {
340+
Promise.resolve().then(async () => {
341+
try {
342+
for (const [blockId, block] of Object.entries(credMigratedBlocks)) {
343+
if (block.subBlocks !== migratedBlocks[blockId]?.subBlocks) {
344+
await db
345+
.update(workflowBlocks)
346+
.set({ subBlocks: block.subBlocks, updatedAt: new Date() })
347+
.where(
348+
and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId))
349+
)
350+
}
351+
}
352+
} catch (err) {
353+
logger.warn('Failed to persist credential ID migration', { workflowId, error: err })
354+
}
355+
})
356+
}
357+
242358
// Convert edges to the expected format
243359
const edgesArray: Edge[] = edges.map((edge) => ({
244360
id: edge.id,
@@ -275,15 +391,13 @@ export async function loadWorkflowFromNormalizedTables(
275391
forEachItems: (config as Loop).forEachItems ?? '',
276392
whileCondition: (config as Loop).whileCondition ?? '',
277393
doWhileCondition: (config as Loop).doWhileCondition ?? '',
278-
enabled: migratedBlocks[subflow.id]?.enabled ?? true,
394+
enabled: credMigratedBlocks[subflow.id]?.enabled ?? true,
279395
}
280396
loops[subflow.id] = loop
281397

282-
// Sync block.data with loop config to ensure all fields are present
283-
// This allows switching between loop types without losing data
284-
if (migratedBlocks[subflow.id]) {
285-
const block = migratedBlocks[subflow.id]
286-
migratedBlocks[subflow.id] = {
398+
if (credMigratedBlocks[subflow.id]) {
399+
const block = credMigratedBlocks[subflow.id]
400+
credMigratedBlocks[subflow.id] = {
287401
...block,
288402
data: {
289403
...block.data,
@@ -304,7 +418,7 @@ export async function loadWorkflowFromNormalizedTables(
304418
(config as Parallel).parallelType === 'collection'
305419
? (config as Parallel).parallelType
306420
: 'count',
307-
enabled: migratedBlocks[subflow.id]?.enabled ?? true,
421+
enabled: credMigratedBlocks[subflow.id]?.enabled ?? true,
308422
}
309423
parallels[subflow.id] = parallel
310424
} else {
@@ -313,7 +427,7 @@ export async function loadWorkflowFromNormalizedTables(
313427
})
314428

315429
return {
316-
blocks: migratedBlocks,
430+
blocks: credMigratedBlocks,
317431
edges: edgesArray,
318432
loops,
319433
parallels,

0 commit comments

Comments
 (0)