Skip to content

Commit 353ef31

Browse files
author
Theodore Li
committed
Refactor to iterate per workspace to avoid overconsuming memory
1 parent 3ccad9d commit 353ef31

File tree

1 file changed

+71
-66
lines changed

1 file changed

+71
-66
lines changed

packages/db/scripts/migrate-block-api-keys-to-byok.ts

Lines changed: 71 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,37 @@ type RawKeyRef = {
213213
userId: string
214214
}
215215

216+
type EnvLookup = {
217+
wsEnvVars: Record<string, string>
218+
personalEnvCache: Map<string, Record<string, string>>
219+
}
220+
221+
async function resolveKey(
222+
ref: RawKeyRef,
223+
context: string,
224+
env: EnvLookup
225+
): Promise<{ key: string | null; envVarFailed: boolean }> {
226+
if (!isEnvVarReference(ref.rawValue)) return { key: ref.rawValue, envVarFailed: false }
227+
228+
const varName = extractEnvVarName(ref.rawValue)
229+
if (!varName) return { key: null, envVarFailed: true }
230+
231+
const personalVars = env.personalEnvCache.get(ref.userId)
232+
const encryptedValue = env.wsEnvVars[varName] ?? personalVars?.[varName]
233+
if (!encryptedValue) {
234+
console.warn(` [WARN] Env var "${varName}" not found (${context})`)
235+
return { key: null, envVarFailed: true }
236+
}
237+
238+
try {
239+
const decrypted = await decryptSecret(encryptedValue)
240+
return { key: decrypted, envVarFailed: false }
241+
} catch (error) {
242+
console.warn(` [WARN] Failed to decrypt env var "${varName}" (${context}): ${error}`)
243+
return { key: null, envVarFailed: true }
244+
}
245+
}
246+
216247
// ---------- Main ----------
217248
async function run() {
218249
console.log(`Mode: ${DRY_RUN ? 'DRY RUN (audit + preview)' : 'LIVE'}`)
@@ -232,72 +263,69 @@ async function run() {
232263
}
233264

234265
try {
235-
// 1. Find all blocks that match our mapped types or contain nested tools
266+
// 1. Get distinct workspace IDs that have matching blocks
236267
const mappedBlockTypes = Object.keys(BLOCK_TYPE_TO_PROVIDER)
237268
const agentTypes = Object.keys(TOOL_INPUT_SUBBLOCK_IDS)
238269
const allBlockTypes = [...new Set([...mappedBlockTypes, ...agentTypes])]
239270

240-
const rows = await db
241-
.select({
242-
blockId: workflowBlocks.id,
243-
blockName: workflowBlocks.name,
244-
blockType: workflowBlocks.type,
245-
subBlocks: workflowBlocks.subBlocks,
246-
workflowId: workflow.id,
247-
workflowName: workflow.name,
248-
userId: workflow.userId,
249-
workspaceId: workflow.workspaceId,
250-
})
271+
const workspaceIdRows = await db
272+
.selectDistinct({ workspaceId: workflow.workspaceId })
251273
.from(workflowBlocks)
252274
.innerJoin(workflow, eq(workflowBlocks.workflowId, workflow.id))
253275
.where(
254-
sql`${workflowBlocks.type} IN (${sql.join(
276+
sql`${workflow.workspaceId} IS NOT NULL AND ${workflowBlocks.type} IN (${sql.join(
255277
allBlockTypes.map((t) => sql`${t}`),
256278
sql`, `
257279
)})`
258280
)
259281

260-
// Group rows by workspace
261-
const workspaceRows = new Map<string, typeof rows>()
262-
let skippedNoWorkspace = 0
263-
for (const row of rows) {
264-
if (!row.workspaceId) {
265-
skippedNoWorkspace++
266-
continue
267-
}
268-
if (!workspaceRows.has(row.workspaceId)) workspaceRows.set(row.workspaceId, [])
269-
workspaceRows.get(row.workspaceId)!.push(row)
270-
}
271-
272-
console.log(`Found ${rows.length} candidate blocks across ${workspaceRows.size} workspaces`)
273-
if (skippedNoWorkspace > 0) console.log(`Skipped ${skippedNoWorkspace} blocks with no workspace`)
274-
console.log()
282+
const workspaceIds = workspaceIdRows
283+
.map((r) => r.workspaceId)
284+
.filter((id): id is string => id !== null)
285+
286+
console.log(`Found ${workspaceIds.length} workspaces with candidate blocks\n`)
287+
288+
// 2. Process one workspace at a time
289+
for (const workspaceId of workspaceIds) {
290+
const blocks = await db
291+
.select({
292+
blockId: workflowBlocks.id,
293+
blockName: workflowBlocks.name,
294+
blockType: workflowBlocks.type,
295+
subBlocks: workflowBlocks.subBlocks,
296+
workflowId: workflow.id,
297+
workflowName: workflow.name,
298+
userId: workflow.userId,
299+
})
300+
.from(workflowBlocks)
301+
.innerJoin(workflow, eq(workflowBlocks.workflowId, workflow.id))
302+
.where(
303+
sql`${workflow.workspaceId} = ${workspaceId} AND ${workflowBlocks.type} IN (${sql.join(
304+
allBlockTypes.map((t) => sql`${t}`),
305+
sql`, `
306+
)})`
307+
)
275308

276-
// 2. Iterate per workspace
277-
for (const [workspaceId, blocks] of workspaceRows) {
278309
console.log(`[Workspace ${workspaceId}] ${blocks.length} blocks`)
279310

280311
// 2a. Extract all raw key references grouped by provider
281312
const providerKeys = new Map<string, RawKeyRef[]>()
282313

283-
function addRef(providerId: string, ref: RawKeyRef) {
284-
if (!providerKeys.has(providerId)) providerKeys.set(providerId, [])
285-
providerKeys.get(providerId)!.push(ref)
286-
}
287-
288314
for (const block of blocks) {
289315
const subBlocks = block.subBlocks as Record<string, { value?: any }>
290316

291317
const providerId = BLOCK_TYPE_TO_PROVIDER[block.blockType]
292318
if (providerId) {
293319
const val = subBlocks?.apiKey?.value
294320
if (typeof val === 'string' && val.trim()) {
295-
addRef(providerId, {
321+
const refs = providerKeys.get(providerId) ?? []
322+
refs.push({
296323
rawValue: val,
297324
blockName: block.blockName,
298325
workflowName: block.workflowName,
299326
userId: block.userId,
300327
})
328+
providerKeys.set(providerId, refs)
301329
}
302330
}
303331

@@ -310,12 +338,14 @@ async function run() {
310338
if (!toolType || !toolApiKey || !toolApiKey.trim()) continue
311339
const toolProviderId = BLOCK_TYPE_TO_PROVIDER[toolType]
312340
if (!toolProviderId) continue
313-
addRef(toolProviderId, {
341+
const refs = providerKeys.get(toolProviderId) ?? []
342+
refs.push({
314343
rawValue: toolApiKey,
315344
blockName: `${block.blockName} > tool "${tool.title || toolType}"`,
316345
workflowName: block.workflowName,
317346
userId: block.userId,
318347
})
348+
providerKeys.set(toolProviderId, refs)
319349
}
320350
}
321351
}
@@ -361,34 +391,7 @@ async function run() {
361391
}
362392
}
363393

364-
async function resolveKey(
365-
ref: RawKeyRef,
366-
context: string
367-
): Promise<string | null> {
368-
if (!isEnvVarReference(ref.rawValue)) return ref.rawValue
369-
370-
const varName = extractEnvVarName(ref.rawValue)
371-
if (!varName) {
372-
stats.envVarFailures++
373-
return null
374-
}
375-
376-
const personalVars = personalEnvCache.get(ref.userId)
377-
const encryptedValue = wsEnvVars[varName] ?? personalVars?.[varName]
378-
if (!encryptedValue) {
379-
console.warn(` [WARN] Env var "${varName}" not found (${context})`)
380-
stats.envVarFailures++
381-
return null
382-
}
383-
384-
try {
385-
return await decryptSecret(encryptedValue)
386-
} catch (error) {
387-
console.warn(` [WARN] Failed to decrypt env var "${varName}" (${context}): ${error}`)
388-
stats.envVarFailures++
389-
return null
390-
}
391-
}
394+
const envLookup: EnvLookup = { wsEnvVars, personalEnvCache }
392395

393396
// 2c. For each provider, detect conflicts then resolve and insert
394397
stats.workspacesProcessed++
@@ -397,7 +400,9 @@ async function run() {
397400
// Resolve all keys for this provider to check for conflicts
398401
const resolved: { ref: RawKeyRef; key: string }[] = []
399402
for (const ref of refs) {
400-
const key = await resolveKey(ref, `"${ref.blockName}" in "${ref.workflowName}"`)
403+
const context = `"${ref.blockName}" in "${ref.workflowName}"`
404+
const { key, envVarFailed } = await resolveKey(ref, context, envLookup)
405+
if (envVarFailed) stats.envVarFailures++
401406
if (key?.trim()) resolved.push({ ref, key })
402407
}
403408

0 commit comments

Comments
 (0)