@pikku/inspector 0.12.42 → 0.12.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +266 -0
- package/dist/add/add-addon-bans.js +1 -0
- package/dist/add/add-ai-agent.d.ts +3 -1
- package/dist/add/add-ai-agent.js +82 -0
- package/dist/add/add-credential.js +5 -1
- package/dist/add/add-functions.js +10 -9
- package/dist/add/add-gateway.js +3 -0
- package/dist/add/add-http-route.js +2 -2
- package/dist/add/add-keyed-wiring.js +2 -0
- package/dist/add/add-mcp-prompt.js +0 -1
- package/dist/add/add-mcp-resource.js +0 -1
- package/dist/add/add-permission.d.ts +1 -1
- package/dist/add/add-permission.js +3 -177
- package/dist/add/add-queue-worker.js +3 -0
- package/dist/add/add-schedule.js +3 -0
- package/dist/add/add-scope.d.ts +2 -0
- package/dist/add/add-scope.js +146 -0
- package/dist/add/add-trigger.js +3 -0
- package/dist/add/add-wire-remote-addon.d.ts +10 -0
- package/dist/add/add-wire-remote-addon.js +66 -0
- package/dist/add/add-workflow-graph.js +32 -3
- package/dist/add/add-workflow.js +9 -4
- package/dist/error-codes.d.ts +3 -0
- package/dist/error-codes.js +4 -0
- package/dist/inspector.js +11 -5
- package/dist/types.d.ts +19 -13
- package/dist/utils/custom-types-generator.js +2 -1
- package/dist/utils/ensure-function-metadata.d.ts +15 -0
- package/dist/utils/ensure-function-metadata.js +24 -0
- package/dist/utils/filter-inspector-state.js +9 -0
- package/dist/utils/get-property-value.d.ts +7 -2
- package/dist/utils/get-property-value.js +48 -1
- package/dist/utils/load-addon-functions-meta.js +81 -6
- package/dist/utils/permissions.d.ts +1 -21
- package/dist/utils/permissions.js +17 -108
- package/dist/utils/post-process.d.ts +30 -1
- package/dist/utils/post-process.js +150 -72
- package/dist/utils/schema-generator.js +19 -4
- package/dist/utils/serialize-inspector-state.d.ts +13 -8
- package/dist/utils/serialize-inspector-state.js +12 -6
- package/dist/utils/serialize-permissions-groups-meta.d.ts +0 -2
- package/dist/utils/serialize-permissions-groups-meta.js +0 -26
- package/dist/utils/workflow/derive-workflow-plan.js +9 -3
- package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +373 -91
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +274 -100
- package/dist/utils/workflow/dsl/patterns.d.ts +4 -0
- package/dist/utils/workflow/dsl/patterns.js +11 -0
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +76 -5
- package/dist/utils/workflow/graph/finalize-workflows.d.ts +7 -0
- package/dist/utils/workflow/graph/finalize-workflows.js +11 -2
- package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +2 -0
- package/dist/utils/workflow/graph/serialize-workflow-graph.js +4 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +8 -4
- package/dist/visit.js +4 -0
- package/package.json +3 -3
- package/run-tests.sh +1 -1
- package/src/add/add-addon-bans.ts +1 -0
- package/src/add/add-ai-agent.test.ts +87 -0
- package/src/add/add-ai-agent.ts +122 -0
- package/src/add/add-credential.ts +8 -0
- package/src/add/add-functions.ts +10 -12
- package/src/add/add-gateway.ts +11 -0
- package/src/add/add-http-route.ts +2 -8
- package/src/add/add-keyed-wiring.ts +2 -0
- package/src/add/add-mcp-prompt.ts +0 -1
- package/src/add/add-mcp-resource.ts +0 -1
- package/src/add/add-permission.ts +4 -242
- package/src/add/add-queue-worker.ts +11 -0
- package/src/add/add-schedule.ts +11 -0
- package/src/add/add-scope.test.ts +346 -0
- package/src/add/add-scope.ts +225 -0
- package/src/add/add-trigger.ts +11 -0
- package/src/add/add-wire-remote-addon.ts +77 -0
- package/src/add/add-workflow-edge-cases.test.ts +201 -0
- package/src/add/add-workflow-fanout.test.ts +60 -0
- package/src/add/add-workflow-graph-input.test.ts +94 -0
- package/src/add/add-workflow-graph-notes.test.ts +74 -0
- package/src/add/add-workflow-graph.ts +35 -3
- package/src/add/add-workflow-on-error.test.ts +118 -0
- package/src/add/add-workflow-return-switch.test.ts +121 -0
- package/src/add/add-workflow-silent-drops.test.ts +163 -0
- package/src/add/add-workflow-sleep-expression.test.ts +97 -0
- package/src/add/add-workflow.ts +9 -4
- package/src/add/config-value-metadata.test.ts +169 -0
- package/src/add/inline-wiring-function.test.ts +104 -0
- package/src/add/validate-workflow-graph-addons.test.ts +102 -0
- package/src/error-codes.ts +5 -0
- package/src/inspector.ts +14 -4
- package/src/types.ts +16 -17
- package/src/utils/custom-types-generator.test.ts +26 -1
- package/src/utils/custom-types-generator.ts +2 -1
- package/src/utils/ensure-function-metadata.ts +42 -0
- package/src/utils/filter-inspector-state.test.ts +0 -2
- package/src/utils/filter-inspector-state.ts +9 -0
- package/src/utils/get-property-value.test.ts +141 -0
- package/src/utils/get-property-value.ts +62 -1
- package/src/utils/load-addon-functions-meta.test.ts +157 -0
- package/src/utils/load-addon-functions-meta.ts +94 -6
- package/src/utils/load-addon-scopes.test.ts +138 -0
- package/src/utils/permissions.test.ts +7 -232
- package/src/utils/permissions.ts +20 -160
- package/src/utils/post-process.test.ts +269 -4
- package/src/utils/post-process.ts +216 -95
- package/src/utils/schema-generator.test.ts +164 -0
- package/src/utils/schema-generator.ts +26 -3
- package/src/utils/serialize-inspector-state.ts +22 -25
- package/src/utils/serialize-permissions-groups-meta.ts +0 -28
- package/src/utils/workflow/derive-workflow-plan.test.ts +41 -3
- package/src/utils/workflow/derive-workflow-plan.ts +11 -4
- package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +480 -119
- package/src/utils/workflow/dsl/deserialize-fanout-body.test.ts +71 -0
- package/src/utils/workflow/dsl/deserialize-fanout-fidelity.test.ts +99 -0
- package/src/utils/workflow/dsl/deserialize-graph-next.test.ts +161 -0
- package/src/utils/workflow/dsl/deserialize-roundtrip-bindings.test.ts +121 -0
- package/src/utils/workflow/dsl/deserialize-roundtrip-values.test.ts +116 -0
- package/src/utils/workflow/dsl/deserialize-roundtrip.test.ts +145 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +311 -105
- package/src/utils/workflow/dsl/patterns.ts +18 -0
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +82 -10
- package/src/utils/workflow/graph/fanout-node-identity.test.ts +105 -0
- package/src/utils/workflow/graph/finalize-workflows.test.ts +50 -0
- package/src/utils/workflow/graph/finalize-workflows.ts +13 -2
- package/src/utils/workflow/graph/serialize-workflow-graph.ts +6 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +11 -2
- package/src/visit.ts +4 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type {
|
|
7
7
|
SerializedWorkflowGraph,
|
|
8
8
|
SerializedGraphNode,
|
|
9
|
+
SerializedNext,
|
|
9
10
|
DataRef,
|
|
10
11
|
ContextVariable,
|
|
11
12
|
} from '../graph/workflow-graph.types.js'
|
|
@@ -180,16 +181,185 @@ function inputToCode(
|
|
|
180
181
|
return `{\n${lines.join('\n')}\n${indent}}`
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Convert a fanout's per-iteration body into code by walking the `next` chain
|
|
186
|
+
* from the body's entry node. Steps whose result is referenced later in the
|
|
187
|
+
* same iteration are re-emitted as `const` bindings.
|
|
188
|
+
*/
|
|
189
|
+
function fanoutBodyToCode(
|
|
190
|
+
entryNodeId: string | undefined,
|
|
191
|
+
nodes: Record<string, any>,
|
|
192
|
+
indent: string,
|
|
193
|
+
itemVar: string
|
|
194
|
+
): string[] {
|
|
195
|
+
const lines: string[] = []
|
|
196
|
+
const seen = new Set<string>()
|
|
197
|
+
let currentId = entryNodeId
|
|
198
|
+
|
|
199
|
+
while (currentId && nodes[currentId] && !seen.has(currentId)) {
|
|
200
|
+
seen.add(currentId)
|
|
201
|
+
const node = nodes[currentId]
|
|
202
|
+
|
|
203
|
+
if ('rpcName' in node && node.rpcName) {
|
|
204
|
+
const stepName = node.stepName || `Call ${node.rpcName}`
|
|
205
|
+
const input = (node.input || {}) as Record<string, unknown>
|
|
206
|
+
const inputCode = inputToCode(input, indent, itemVar)
|
|
207
|
+
const options = node.options ? optionsToCode(node.options) : ''
|
|
208
|
+
const doCall = `await workflow.do('${escapeSingleQuotes(stepName)}', '${node.rpcName}', ${inputCode}${options ? `, ${options}` : ''})`
|
|
209
|
+
lines.push(
|
|
210
|
+
node.outputVar
|
|
211
|
+
? `${indent}const ${node.outputVar} = ${doCall}`
|
|
212
|
+
: `${indent}${doCall}`
|
|
213
|
+
)
|
|
214
|
+
} else if ('flow' in node) {
|
|
215
|
+
// A body is not RPC-only — a sleep or suspend between calls is part of
|
|
216
|
+
// the iteration and would otherwise be dropped from the regenerated code.
|
|
217
|
+
lines.push(
|
|
218
|
+
...nodeToCode(node, nodes, indent).filter((line) => line !== '')
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
currentId = node.next
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return lines
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Collect every node id that belongs to a parent construct's body, following
|
|
230
|
+
* each body's `next` chain to the parent's exit.
|
|
231
|
+
*/
|
|
232
|
+
function collectChildNodeIds(
|
|
233
|
+
nodes: Record<string, SerializedGraphNode>
|
|
234
|
+
): Set<string> {
|
|
235
|
+
const owned = new Set<string>()
|
|
236
|
+
|
|
237
|
+
// An error handler belongs to the step that routes to it, not to the main
|
|
238
|
+
// flow — emitting it top-level would turn compensation into a step that
|
|
239
|
+
// always runs.
|
|
240
|
+
for (const node of Object.values(nodes)) {
|
|
241
|
+
const onError = (node as any).onError
|
|
242
|
+
if (typeof onError === 'string') {
|
|
243
|
+
owned.add(onError)
|
|
244
|
+
} else if (Array.isArray(onError)) {
|
|
245
|
+
for (const handlerId of onError) owned.add(handlerId)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const walk = (entry: string | undefined, exit: string | undefined) => {
|
|
250
|
+
const seen = new Set<string>()
|
|
251
|
+
let currentId = entry
|
|
252
|
+
while (
|
|
253
|
+
currentId &&
|
|
254
|
+
nodes[currentId] &&
|
|
255
|
+
isWithinBody(currentId, exit, seen)
|
|
256
|
+
) {
|
|
257
|
+
seen.add(currentId)
|
|
258
|
+
owned.add(currentId)
|
|
259
|
+
const next = (nodes[currentId] as any).next
|
|
260
|
+
currentId = typeof next === 'string' ? next : undefined
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const node of Object.values(nodes)) {
|
|
265
|
+
if (!('flow' in node)) continue
|
|
266
|
+
const flowNode = node as any
|
|
267
|
+
const exit = flowNode.next as string | undefined
|
|
268
|
+
|
|
269
|
+
switch (flowNode.flow) {
|
|
270
|
+
case 'branch':
|
|
271
|
+
for (const branch of flowNode.branches || []) walk(branch.entry, exit)
|
|
272
|
+
walk(flowNode.elseEntry, exit)
|
|
273
|
+
break
|
|
274
|
+
case 'switch':
|
|
275
|
+
for (const caseItem of flowNode.cases || []) walk(caseItem.entry, exit)
|
|
276
|
+
walk(flowNode.defaultEntry, exit)
|
|
277
|
+
break
|
|
278
|
+
case 'fanout':
|
|
279
|
+
walk(flowNode.childEntry, undefined)
|
|
280
|
+
break
|
|
281
|
+
case 'parallel':
|
|
282
|
+
for (const childId of flowNode.children || []) walk(childId, exit)
|
|
283
|
+
break
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return owned
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Escape a value for embedding inside a single-quoted string literal.
|
|
292
|
+
*/
|
|
293
|
+
function escapeSingleQuotes(value: string): string {
|
|
294
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* `outputVar` is optional — a graph-authored node may have no binding. Emitting
|
|
299
|
+
* `const undefined = ...` for those would not parse, so the expression is kept
|
|
300
|
+
* as a bare statement instead.
|
|
301
|
+
*/
|
|
302
|
+
function bindingPrefix(outputVar: unknown): string {
|
|
303
|
+
return typeof outputVar === 'string' && outputVar
|
|
304
|
+
? `const ${outputVar} = `
|
|
305
|
+
: ''
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Resolve a node's `onError` node id back to the rpc name the DSL author wrote.
|
|
310
|
+
*/
|
|
311
|
+
function onErrorRpcNameOf(
|
|
312
|
+
node: SerializedGraphNode,
|
|
313
|
+
nodes: Record<string, SerializedGraphNode>
|
|
314
|
+
): string | undefined {
|
|
315
|
+
const onError = (node as any).onError
|
|
316
|
+
const handlerId = Array.isArray(onError) ? onError[0] : onError
|
|
317
|
+
if (typeof handlerId !== 'string') {
|
|
318
|
+
return undefined
|
|
319
|
+
}
|
|
320
|
+
const handler = nodes[handlerId]
|
|
321
|
+
return handler && 'rpcName' in handler
|
|
322
|
+
? (handler.rpcName as string)
|
|
323
|
+
: undefined
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* A duration is `string | number` ('5s' or 5000). Quoting a number would change
|
|
328
|
+
* the value the runtime parses, so only strings get quotes.
|
|
329
|
+
*/
|
|
330
|
+
function durationToCode(duration: unknown): string {
|
|
331
|
+
return typeof duration === 'number'
|
|
332
|
+
? String(duration)
|
|
333
|
+
: `'${escapeSingleQuotes(String(duration))}'`
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Render a switch case value. `switch` compares with ===, so quoting a number
|
|
338
|
+
* or boolean would stop the case ever matching.
|
|
339
|
+
*/
|
|
340
|
+
function caseValueToCode(value: unknown): string {
|
|
341
|
+
if (typeof value === 'string') {
|
|
342
|
+
return `'${escapeSingleQuotes(value)}'`
|
|
343
|
+
}
|
|
344
|
+
return String(value)
|
|
345
|
+
}
|
|
346
|
+
|
|
183
347
|
/**
|
|
184
348
|
* Convert options to code
|
|
185
349
|
*/
|
|
186
|
-
function optionsToCode(
|
|
350
|
+
function optionsToCode(
|
|
351
|
+
options: Record<string, unknown>,
|
|
352
|
+
onErrorRpcName?: string
|
|
353
|
+
): string {
|
|
187
354
|
const parts: string[] = []
|
|
355
|
+
if (onErrorRpcName) {
|
|
356
|
+
parts.push(`onError: '${escapeSingleQuotes(onErrorRpcName)}'`)
|
|
357
|
+
}
|
|
188
358
|
if (options.retries !== undefined) {
|
|
189
359
|
parts.push(`retries: ${options.retries}`)
|
|
190
360
|
}
|
|
191
361
|
if (options.retryDelay !== undefined) {
|
|
192
|
-
parts.push(`retryDelay:
|
|
362
|
+
parts.push(`retryDelay: ${durationToCode(options.retryDelay)}`)
|
|
193
363
|
}
|
|
194
364
|
return parts.length > 0 ? `{ ${parts.join(', ')} }` : ''
|
|
195
365
|
}
|
|
@@ -265,14 +435,26 @@ function collectBranchConditionalVars(
|
|
|
265
435
|
if (branchNode.branches) {
|
|
266
436
|
for (const branch of branchNode.branches) {
|
|
267
437
|
if (branch.entry) {
|
|
268
|
-
collectVarsFromBranch(
|
|
438
|
+
collectVarsFromBranch(
|
|
439
|
+
branch.entry,
|
|
440
|
+
nodes,
|
|
441
|
+
conditionalVars,
|
|
442
|
+
vars,
|
|
443
|
+
branchNode.next as string | undefined
|
|
444
|
+
)
|
|
269
445
|
}
|
|
270
446
|
}
|
|
271
447
|
}
|
|
272
448
|
|
|
273
449
|
// Check else branch
|
|
274
450
|
if (branchNode.elseEntry) {
|
|
275
|
-
collectVarsFromBranch(
|
|
451
|
+
collectVarsFromBranch(
|
|
452
|
+
branchNode.elseEntry,
|
|
453
|
+
nodes,
|
|
454
|
+
conditionalVars,
|
|
455
|
+
vars,
|
|
456
|
+
branchNode.next as string | undefined
|
|
457
|
+
)
|
|
276
458
|
}
|
|
277
459
|
|
|
278
460
|
return vars
|
|
@@ -285,8 +467,12 @@ function collectVarsFromBranch(
|
|
|
285
467
|
nodeId: string,
|
|
286
468
|
nodes: Record<string, SerializedGraphNode>,
|
|
287
469
|
conditionalVars: Set<string>,
|
|
288
|
-
result: string[]
|
|
470
|
+
result: string[],
|
|
471
|
+
exitNodeId?: string,
|
|
472
|
+
seen: Set<string> = new Set()
|
|
289
473
|
): void {
|
|
474
|
+
if (seen.has(nodeId)) return
|
|
475
|
+
seen.add(nodeId)
|
|
290
476
|
const node = nodes[nodeId]
|
|
291
477
|
if (!node) return
|
|
292
478
|
|
|
@@ -301,22 +487,32 @@ function collectVarsFromBranch(
|
|
|
301
487
|
// Follow the chain of nodes within the branch
|
|
302
488
|
if ('next' in node && node.next) {
|
|
303
489
|
const nextId = node.next as string
|
|
304
|
-
// Only follow if it's still within the branch
|
|
305
|
-
if (
|
|
306
|
-
collectVarsFromBranch(
|
|
490
|
+
// Only follow if it's still within the branch body
|
|
491
|
+
if (isWithinBody(nextId, exitNodeId, seen)) {
|
|
492
|
+
collectVarsFromBranch(
|
|
493
|
+
nextId,
|
|
494
|
+
nodes,
|
|
495
|
+
conditionalVars,
|
|
496
|
+
result,
|
|
497
|
+
exitNodeId,
|
|
498
|
+
seen
|
|
499
|
+
)
|
|
307
500
|
}
|
|
308
501
|
}
|
|
309
502
|
}
|
|
310
503
|
|
|
311
504
|
/**
|
|
312
|
-
*
|
|
505
|
+
* A nested body (branch arm, switch case) ends where it rejoins the main flow.
|
|
506
|
+
* That rejoin point is the enclosing flow node's own `next`, so it is passed in
|
|
507
|
+
* explicitly rather than guessed from the node id — ids are step names, which
|
|
508
|
+
* carry no structural marker.
|
|
313
509
|
*/
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
)
|
|
510
|
+
function isWithinBody(
|
|
511
|
+
nodeId: string,
|
|
512
|
+
exitNodeId: string | undefined,
|
|
513
|
+
seen: Set<string>
|
|
514
|
+
): boolean {
|
|
515
|
+
return nodeId !== exitNodeId && !seen.has(nodeId)
|
|
320
516
|
}
|
|
321
517
|
|
|
322
518
|
/**
|
|
@@ -326,23 +522,25 @@ function generateBranchContent(
|
|
|
326
522
|
entryNodeId: string,
|
|
327
523
|
nodes: Record<string, SerializedGraphNode>,
|
|
328
524
|
indent: string,
|
|
329
|
-
conditionalVars: Set<string
|
|
525
|
+
conditionalVars: Set<string>,
|
|
526
|
+
exitNodeId?: string
|
|
330
527
|
): string[] {
|
|
331
528
|
const lines: string[] = []
|
|
529
|
+
const seen = new Set<string>()
|
|
332
530
|
let currentId: string | undefined = entryNodeId
|
|
333
531
|
|
|
334
532
|
while (currentId) {
|
|
335
533
|
const node = nodes[currentId]
|
|
336
534
|
if (!node) break
|
|
535
|
+
seen.add(currentId)
|
|
337
536
|
|
|
338
537
|
const nodeLines = nodeToCode(node, nodes, indent, conditionalVars, true)
|
|
339
538
|
lines.push(...nodeLines)
|
|
340
539
|
|
|
341
|
-
// Follow to next node within the
|
|
342
|
-
if ('next' in node && node.next) {
|
|
343
|
-
const nextId = node.next
|
|
344
|
-
|
|
345
|
-
if (isWithinBranch(nextId)) {
|
|
540
|
+
// Follow to next node within the body
|
|
541
|
+
if ('next' in node && node.next && typeof node.next === 'string') {
|
|
542
|
+
const nextId = node.next
|
|
543
|
+
if (isWithinBody(nextId, exitNodeId, seen)) {
|
|
346
544
|
currentId = nextId
|
|
347
545
|
} else {
|
|
348
546
|
break
|
|
@@ -377,8 +575,9 @@ function nodeToCode(
|
|
|
377
575
|
let doCall = `await workflow.do('${stepName}', '${node.rpcName}', ${inputCode}`
|
|
378
576
|
|
|
379
577
|
// Add options if present
|
|
380
|
-
|
|
381
|
-
|
|
578
|
+
const onErrorRpc = onErrorRpcNameOf(node, nodes)
|
|
579
|
+
if ((node as any).options || onErrorRpc) {
|
|
580
|
+
const optCode = optionsToCode((node as any).options ?? {}, onErrorRpc)
|
|
382
581
|
if (optCode) {
|
|
383
582
|
doCall += `, ${optCode}`
|
|
384
583
|
}
|
|
@@ -406,11 +605,33 @@ function nodeToCode(
|
|
|
406
605
|
switch (flowNode.flow) {
|
|
407
606
|
case 'sleep':
|
|
408
607
|
lines.push(
|
|
409
|
-
`${indent}await workflow.sleep('${flowNode.stepName || 'Sleep'}',
|
|
608
|
+
`${indent}await workflow.sleep('${escapeSingleQuotes(flowNode.stepName || 'Sleep')}', ${
|
|
609
|
+
typeof flowNode.expression === 'string'
|
|
610
|
+
? flowNode.expression
|
|
611
|
+
: durationToCode(flowNode.duration)
|
|
612
|
+
})`
|
|
410
613
|
)
|
|
411
614
|
lines.push('')
|
|
412
615
|
break
|
|
413
616
|
|
|
617
|
+
case 'suspend':
|
|
618
|
+
lines.push(
|
|
619
|
+
`${indent}await workflow.suspend('${escapeSingleQuotes(String(flowNode.reason ?? ''))}')`
|
|
620
|
+
)
|
|
621
|
+
lines.push('')
|
|
622
|
+
break
|
|
623
|
+
|
|
624
|
+
case 'approval': {
|
|
625
|
+
const approvalCall = `await workflow.approval('${escapeSingleQuotes(String(flowNode.reason ?? ''))}')`
|
|
626
|
+
lines.push(
|
|
627
|
+
flowNode.outputVar
|
|
628
|
+
? `${indent}const ${flowNode.outputVar} = ${approvalCall}`
|
|
629
|
+
: `${indent}${approvalCall}`
|
|
630
|
+
)
|
|
631
|
+
lines.push('')
|
|
632
|
+
break
|
|
633
|
+
}
|
|
634
|
+
|
|
414
635
|
case 'cancel':
|
|
415
636
|
const cancelReason =
|
|
416
637
|
flowNode.reason || flowNode.stepName || 'Workflow cancelled'
|
|
@@ -444,7 +665,8 @@ function nodeToCode(
|
|
|
444
665
|
branch.entry,
|
|
445
666
|
nodes,
|
|
446
667
|
indent + ' ',
|
|
447
|
-
conditionalVars
|
|
668
|
+
conditionalVars,
|
|
669
|
+
flowNode.next as string | undefined
|
|
448
670
|
)
|
|
449
671
|
lines.push(...branchLines)
|
|
450
672
|
}
|
|
@@ -458,7 +680,8 @@ function nodeToCode(
|
|
|
458
680
|
flowNode.elseEntry,
|
|
459
681
|
nodes,
|
|
460
682
|
indent + ' ',
|
|
461
|
-
conditionalVars
|
|
683
|
+
conditionalVars,
|
|
684
|
+
flowNode.next as string | undefined
|
|
462
685
|
)
|
|
463
686
|
lines.push(...elseLines)
|
|
464
687
|
lines.push(`${indent}}`)
|
|
@@ -466,96 +689,101 @@ function nodeToCode(
|
|
|
466
689
|
lines.push('')
|
|
467
690
|
break
|
|
468
691
|
|
|
469
|
-
case 'switch':
|
|
692
|
+
case 'switch': {
|
|
693
|
+
const caseExit = flowNode.next as string | undefined
|
|
470
694
|
lines.push(`${indent}switch (${flowNode.expression}) {`)
|
|
471
695
|
for (const caseItem of flowNode.cases || []) {
|
|
472
|
-
lines.push(`${indent} case
|
|
696
|
+
lines.push(`${indent} case ${caseValueToCode(caseItem.value)}:`)
|
|
473
697
|
if (caseItem.entry && nodes[caseItem.entry]) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
698
|
+
lines.push(
|
|
699
|
+
...generateBranchContent(
|
|
700
|
+
caseItem.entry,
|
|
701
|
+
nodes,
|
|
702
|
+
indent + ' ',
|
|
703
|
+
conditionalVars,
|
|
704
|
+
caseExit
|
|
705
|
+
)
|
|
478
706
|
)
|
|
479
|
-
lines.push(...caseLines)
|
|
480
707
|
}
|
|
481
708
|
lines.push(`${indent} break`)
|
|
482
709
|
}
|
|
483
710
|
if (flowNode.defaultEntry && nodes[flowNode.defaultEntry]) {
|
|
484
711
|
lines.push(`${indent} default:`)
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
712
|
+
lines.push(
|
|
713
|
+
...generateBranchContent(
|
|
714
|
+
flowNode.defaultEntry,
|
|
715
|
+
nodes,
|
|
716
|
+
indent + ' ',
|
|
717
|
+
conditionalVars,
|
|
718
|
+
caseExit
|
|
719
|
+
)
|
|
489
720
|
)
|
|
490
|
-
lines.push(...defaultLines)
|
|
491
721
|
lines.push(`${indent} break`)
|
|
492
722
|
}
|
|
493
723
|
lines.push(`${indent}}`)
|
|
494
724
|
lines.push('')
|
|
495
725
|
break
|
|
726
|
+
}
|
|
496
727
|
|
|
497
|
-
case 'parallel':
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
728
|
+
case 'parallel': {
|
|
729
|
+
const childNodes = (flowNode.children || [])
|
|
730
|
+
.map((childId: string) => nodes[childId])
|
|
731
|
+
.filter(
|
|
732
|
+
(child: SerializedGraphNode | undefined) =>
|
|
733
|
+
!!child && 'rpcName' in child && !!child.rpcName
|
|
734
|
+
) as any[]
|
|
735
|
+
|
|
736
|
+
// Results the caller bound (const [a, b] = await Promise.all([...]))
|
|
737
|
+
// must be re-declared, or every later reference to them is unbound.
|
|
738
|
+
const bindings = childNodes.map((child) => child.outputVar)
|
|
739
|
+
const prefix = bindings.some(Boolean)
|
|
740
|
+
? `const [${bindings
|
|
741
|
+
.map((name: string | undefined, i: number) => name ?? `_${i}`)
|
|
742
|
+
.join(', ')}] = `
|
|
743
|
+
: ''
|
|
744
|
+
|
|
745
|
+
lines.push(`${indent}${prefix}await Promise.all([`)
|
|
746
|
+
for (const childNode of childNodes) {
|
|
747
|
+
const stepName = childNode.stepName || `Call ${childNode.rpcName}`
|
|
748
|
+
const input = (childNode.input || {}) as Record<string, unknown>
|
|
749
|
+
const inputCode = inputToCode(input, indent + ' ')
|
|
750
|
+
let call = `workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode}`
|
|
751
|
+
if (childNode.options) {
|
|
752
|
+
const optCode = optionsToCode(childNode.options)
|
|
753
|
+
if (optCode) {
|
|
754
|
+
call += `, ${optCode}`
|
|
509
755
|
}
|
|
510
756
|
}
|
|
757
|
+
call += ')'
|
|
758
|
+
lines.push(`${indent} ${call},`)
|
|
511
759
|
}
|
|
512
760
|
lines.push(`${indent}])`)
|
|
513
761
|
lines.push('')
|
|
514
762
|
break
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
case 'fanout': {
|
|
766
|
+
const bodyLines = fanoutBodyToCode(
|
|
767
|
+
flowNode.childEntry,
|
|
768
|
+
nodes,
|
|
769
|
+
flowNode.mode === 'parallel' ? indent + ' ' : indent + ' ',
|
|
770
|
+
flowNode.itemVar
|
|
771
|
+
)
|
|
515
772
|
|
|
516
|
-
case 'fanout':
|
|
517
773
|
if (flowNode.mode === 'parallel') {
|
|
518
774
|
lines.push(`${indent}await Promise.all(`)
|
|
519
775
|
lines.push(
|
|
520
|
-
`${indent} ${flowNode.sourceVar}.map(async (${flowNode.itemVar})
|
|
776
|
+
`${indent} ${flowNode.sourceVar}.map(async (${flowNode.itemVar}) => {`
|
|
521
777
|
)
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if ('rpcName' in childNode && childNode.rpcName) {
|
|
525
|
-
const stepName = childNode.stepName || `Call ${childNode.rpcName}`
|
|
526
|
-
const input = (childNode.input || {}) as Record<string, unknown>
|
|
527
|
-
const inputCode = inputToCode(
|
|
528
|
-
input,
|
|
529
|
-
indent + ' ',
|
|
530
|
-
flowNode.itemVar
|
|
531
|
-
)
|
|
532
|
-
lines.push(
|
|
533
|
-
`${indent} await workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode})`
|
|
534
|
-
)
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
lines.push(`${indent} )`)
|
|
778
|
+
lines.push(...bodyLines)
|
|
779
|
+
lines.push(`${indent} })`)
|
|
538
780
|
lines.push(`${indent})`)
|
|
539
781
|
} else {
|
|
540
782
|
// Sequential fanout
|
|
541
783
|
lines.push(
|
|
542
784
|
`${indent}for (const ${flowNode.itemVar} of ${flowNode.sourceVar}) {`
|
|
543
785
|
)
|
|
544
|
-
|
|
545
|
-
const childNode = nodes[flowNode.childEntry]
|
|
546
|
-
if ('rpcName' in childNode && childNode.rpcName) {
|
|
547
|
-
const stepName = childNode.stepName || `Call ${childNode.rpcName}`
|
|
548
|
-
const input = (childNode.input || {}) as Record<string, unknown>
|
|
549
|
-
const inputCode = inputToCode(
|
|
550
|
-
input,
|
|
551
|
-
indent + ' ',
|
|
552
|
-
flowNode.itemVar
|
|
553
|
-
)
|
|
554
|
-
lines.push(
|
|
555
|
-
`${indent} await workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode})`
|
|
556
|
-
)
|
|
557
|
-
}
|
|
558
|
-
}
|
|
786
|
+
lines.push(...bodyLines)
|
|
559
787
|
if (flowNode.timeBetween) {
|
|
560
788
|
lines.push(
|
|
561
789
|
`${indent} await workflow.sleep('Wait between iterations', '${flowNode.timeBetween}')`
|
|
@@ -565,10 +793,11 @@ function nodeToCode(
|
|
|
565
793
|
}
|
|
566
794
|
lines.push('')
|
|
567
795
|
break
|
|
796
|
+
}
|
|
568
797
|
|
|
569
798
|
case 'filter':
|
|
570
799
|
lines.push(
|
|
571
|
-
`${indent}
|
|
800
|
+
`${indent}${bindingPrefix(flowNode.outputVar)}${flowNode.sourceVar}.filter((${flowNode.itemVar}) => ${conditionToCode(flowNode.condition)})`
|
|
572
801
|
)
|
|
573
802
|
lines.push('')
|
|
574
803
|
break
|
|
@@ -576,16 +805,19 @@ function nodeToCode(
|
|
|
576
805
|
case 'arrayPredicate':
|
|
577
806
|
const method = flowNode.mode === 'some' ? 'some' : 'every'
|
|
578
807
|
lines.push(
|
|
579
|
-
`${indent}
|
|
808
|
+
`${indent}${bindingPrefix(flowNode.outputVar)}${flowNode.sourceVar}.${method}((${flowNode.itemVar}) => ${conditionToCode(flowNode.condition)})`
|
|
580
809
|
)
|
|
581
810
|
lines.push('')
|
|
582
811
|
break
|
|
583
812
|
|
|
584
813
|
case 'return':
|
|
585
|
-
if (flowNode.outputs) {
|
|
814
|
+
if (flowNode.outputs || flowNode.spread) {
|
|
586
815
|
const returnObj: string[] = []
|
|
816
|
+
for (const spreadVar of (flowNode.spread as string[]) ?? []) {
|
|
817
|
+
returnObj.push(`${indent} ...${spreadVar},`)
|
|
818
|
+
}
|
|
587
819
|
for (const [key, output] of Object.entries(
|
|
588
|
-
flowNode.outputs as Record<string, any>
|
|
820
|
+
(flowNode.outputs ?? {}) as Record<string, any>
|
|
589
821
|
)) {
|
|
590
822
|
let value: string
|
|
591
823
|
if (output.from === 'outputVar') {
|
|
@@ -627,9 +859,11 @@ function nodeToCode(
|
|
|
627
859
|
// Generate variable assignment: varName = value
|
|
628
860
|
const setVar = flowNode.variable
|
|
629
861
|
const setValue =
|
|
630
|
-
typeof flowNode.
|
|
631
|
-
?
|
|
632
|
-
:
|
|
862
|
+
typeof flowNode.expression === 'string'
|
|
863
|
+
? flowNode.expression
|
|
864
|
+
: typeof flowNode.value === 'string'
|
|
865
|
+
? `'${escapeSingleQuotes(flowNode.value)}'`
|
|
866
|
+
: JSON.stringify(flowNode.value)
|
|
633
867
|
lines.push(`${indent}${setVar} = ${setValue}`)
|
|
634
868
|
lines.push('')
|
|
635
869
|
break
|
|
@@ -650,16 +884,42 @@ function findConditionalVars(
|
|
|
650
884
|
const varsInBranches = new Set<string>()
|
|
651
885
|
const varsUsedInReturn = new Set<string>()
|
|
652
886
|
|
|
653
|
-
// Collect variables defined
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
887
|
+
// Collect variables defined inside a branch arm or switch case. Found by
|
|
888
|
+
// walking each body from its entry to the enclosing node's exit — node ids
|
|
889
|
+
// are step names and carry no structural marker to match on.
|
|
890
|
+
for (const node of Object.values(nodes)) {
|
|
891
|
+
if (!('flow' in node)) continue
|
|
892
|
+
const flowNode = node as any
|
|
893
|
+
const exit = flowNode.next as string | undefined
|
|
894
|
+
|
|
895
|
+
const entries: string[] = []
|
|
896
|
+
if (flowNode.flow === 'branch') {
|
|
897
|
+
for (const branch of flowNode.branches || []) {
|
|
898
|
+
if (branch.entry) entries.push(branch.entry)
|
|
899
|
+
}
|
|
900
|
+
if (flowNode.elseEntry) entries.push(flowNode.elseEntry)
|
|
901
|
+
} else if (flowNode.flow === 'switch') {
|
|
902
|
+
for (const caseItem of flowNode.cases || []) {
|
|
903
|
+
if (caseItem.entry) entries.push(caseItem.entry)
|
|
904
|
+
}
|
|
905
|
+
if (flowNode.defaultEntry) entries.push(flowNode.defaultEntry)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
for (const entry of entries) {
|
|
909
|
+
const seen = new Set<string>()
|
|
910
|
+
let currentId: string | undefined = entry
|
|
911
|
+
while (
|
|
912
|
+
currentId &&
|
|
913
|
+
nodes[currentId] &&
|
|
914
|
+
isWithinBody(currentId, exit, seen)
|
|
915
|
+
) {
|
|
916
|
+
seen.add(currentId)
|
|
917
|
+
const bodyNode = nodes[currentId] as any
|
|
918
|
+
if (bodyNode.outputVar) {
|
|
919
|
+
varsInBranches.add(bodyNode.outputVar as string)
|
|
920
|
+
}
|
|
921
|
+
currentId =
|
|
922
|
+
typeof bodyNode.next === 'string' ? bodyNode.next : undefined
|
|
663
923
|
}
|
|
664
924
|
}
|
|
665
925
|
}
|
|
@@ -773,16 +1033,14 @@ export function deserializeDslWorkflow(
|
|
|
773
1033
|
// Process nodes in order
|
|
774
1034
|
const orderedNodes = traverseNodes(workflow.nodes, workflow.entryNodeIds)
|
|
775
1035
|
|
|
1036
|
+
// Nodes emitted as part of a parent (branch arm, switch case, parallel group,
|
|
1037
|
+
// fanout body) must not also be emitted at the top level. Ownership is read
|
|
1038
|
+
// from the parents themselves — node ids are step names and a legitimate step
|
|
1039
|
+
// may be called anything.
|
|
1040
|
+
const ownedByParent = collectChildNodeIds(workflow.nodes)
|
|
1041
|
+
|
|
776
1042
|
for (const node of orderedNodes) {
|
|
777
|
-
|
|
778
|
-
if (
|
|
779
|
-
node.nodeId.includes('_then_') ||
|
|
780
|
-
node.nodeId.includes('_else_') ||
|
|
781
|
-
node.nodeId.includes('_case') ||
|
|
782
|
-
node.nodeId.includes('_default_') ||
|
|
783
|
-
node.nodeId.includes('_child_') ||
|
|
784
|
-
node.nodeId.includes('_item_')
|
|
785
|
-
) {
|
|
1043
|
+
if (ownedByParent.has(node.nodeId)) {
|
|
786
1044
|
continue
|
|
787
1045
|
}
|
|
788
1046
|
|
|
@@ -943,8 +1201,91 @@ function findNextRpcNode(
|
|
|
943
1201
|
|
|
944
1202
|
// It's a flow node - follow its 'next' if it has one
|
|
945
1203
|
const flowNode = nodes[startNextId]
|
|
946
|
-
if (flowNode && 'next' in flowNode && flowNode.next) {
|
|
947
|
-
return findNextRpcNode(flowNode.next
|
|
1204
|
+
if (flowNode && 'next' in flowNode && typeof flowNode.next === 'string') {
|
|
1205
|
+
return findNextRpcNode(flowNode.next, nodes, flowNodeIds, visited)
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
return null
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Resolve a single next target through any intervening flow nodes.
|
|
1213
|
+
*/
|
|
1214
|
+
function resolveNextTarget(
|
|
1215
|
+
target: string,
|
|
1216
|
+
nodes: Record<string, SerializedGraphNode>,
|
|
1217
|
+
flowNodeIds: Set<string>
|
|
1218
|
+
): string | null {
|
|
1219
|
+
const resolved = flowNodeIds.has(target)
|
|
1220
|
+
? findNextRpcNode(target, nodes, flowNodeIds)
|
|
1221
|
+
: target
|
|
1222
|
+
return resolved && !flowNodeIds.has(resolved) ? resolved : null
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Render a `next` in any of its shapes. A `next` is not always a single node
|
|
1227
|
+
* id — it can fan out to an array, route by branch key, or carry conditions.
|
|
1228
|
+
* Coercing those to a string produces node ids that match nothing ('a,b',
|
|
1229
|
+
* '[object Object]'), silently severing the rest of the graph.
|
|
1230
|
+
*/
|
|
1231
|
+
function nextToCode(
|
|
1232
|
+
next: SerializedNext,
|
|
1233
|
+
nodes: Record<string, SerializedGraphNode>,
|
|
1234
|
+
flowNodeIds: Set<string>
|
|
1235
|
+
): string | null {
|
|
1236
|
+
const renderTargets = (targets: string | string[]): string | null => {
|
|
1237
|
+
if (typeof targets === 'string') {
|
|
1238
|
+
const resolved = resolveNextTarget(targets, nodes, flowNodeIds)
|
|
1239
|
+
return resolved ? `'${resolved}'` : null
|
|
1240
|
+
}
|
|
1241
|
+
const resolved = targets
|
|
1242
|
+
.map((t) => resolveNextTarget(t, nodes, flowNodeIds))
|
|
1243
|
+
.filter((t): t is string => t !== null)
|
|
1244
|
+
return resolved.length > 0
|
|
1245
|
+
? `[${resolved.map((t) => `'${t}'`).join(', ')}]`
|
|
1246
|
+
: null
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (typeof next === 'string' || Array.isArray(next)) {
|
|
1250
|
+
return renderTargets(next)
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
if (typeof next === 'object' && next !== null) {
|
|
1254
|
+
if ('conditions' in next && Array.isArray(next.conditions)) {
|
|
1255
|
+
const conditions = next.conditions
|
|
1256
|
+
.map((condition) => {
|
|
1257
|
+
const target = renderTargets(condition.target)
|
|
1258
|
+
return target
|
|
1259
|
+
? `{ expression: '${escapeSingleQuotes(condition.expression)}', target: ${target} }`
|
|
1260
|
+
: null
|
|
1261
|
+
})
|
|
1262
|
+
.filter((c): c is string => c !== null)
|
|
1263
|
+
if (conditions.length === 0) {
|
|
1264
|
+
return null
|
|
1265
|
+
}
|
|
1266
|
+
const parts = [`conditions: [${conditions.join(', ')}]`]
|
|
1267
|
+
if (next.default !== undefined) {
|
|
1268
|
+
const defaultTarget = renderTargets(next.default)
|
|
1269
|
+
if (defaultTarget) {
|
|
1270
|
+
parts.push(`default: ${defaultTarget}`)
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return `{ ${parts.join(', ')} }`
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const entries = Object.entries(next)
|
|
1277
|
+
.map(([key, target]) => {
|
|
1278
|
+
const isTarget =
|
|
1279
|
+
typeof target === 'string' ||
|
|
1280
|
+
(Array.isArray(target) && target.every((t) => typeof t === 'string'))
|
|
1281
|
+
const rendered = isTarget ? renderTargets(target) : null
|
|
1282
|
+
const safeKey = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
1283
|
+
? key
|
|
1284
|
+
: JSON.stringify(key)
|
|
1285
|
+
return rendered ? `${safeKey}: ${rendered}` : null
|
|
1286
|
+
})
|
|
1287
|
+
.filter((e): e is string => e !== null)
|
|
1288
|
+
return entries.length > 0 ? `{ ${entries.join(', ')} }` : null
|
|
948
1289
|
}
|
|
949
1290
|
|
|
950
1291
|
return null
|
|
@@ -1014,14 +1355,28 @@ export function deserializeGraphWorkflow(
|
|
|
1014
1355
|
|
|
1015
1356
|
// Add next if present - follow through flow nodes to find the actual next RPC node
|
|
1016
1357
|
if ('next' in node && node.next) {
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1358
|
+
const nextCode = nextToCode(node.next, workflow.nodes, flowNodeIds)
|
|
1359
|
+
if (nextCode) {
|
|
1360
|
+
configParts.push(`next: ${nextCode}`)
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// onError, retries and retryDelay are all honoured by the graph runner,
|
|
1365
|
+
// so dropping them here silently changes how the workflow behaves.
|
|
1366
|
+
if ('onError' in node && node.onError) {
|
|
1367
|
+
const onErrorCode = nextToCode(node.onError, workflow.nodes, flowNodeIds)
|
|
1368
|
+
if (onErrorCode) {
|
|
1369
|
+
configParts.push(`onError: ${onErrorCode}`)
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
if ('options' in node && node.options) {
|
|
1374
|
+
const { retries, retryDelay } = node.options
|
|
1375
|
+
if (retries !== undefined) {
|
|
1376
|
+
configParts.push(`retries: ${retries}`)
|
|
1377
|
+
}
|
|
1378
|
+
if (retryDelay !== undefined) {
|
|
1379
|
+
configParts.push(`retryDelay: ${durationToCode(retryDelay)}`)
|
|
1025
1380
|
}
|
|
1026
1381
|
}
|
|
1027
1382
|
|
|
@@ -1057,6 +1412,12 @@ export function deserializeGraphWorkflow(
|
|
|
1057
1412
|
if (workflow.tags && workflow.tags.length > 0) {
|
|
1058
1413
|
lines.push(` tags: [${workflow.tags.map((t) => `'${t}'`).join(', ')}],`)
|
|
1059
1414
|
}
|
|
1415
|
+
if (workflow.notes && workflow.notes.length > 0) {
|
|
1416
|
+
const notes = workflow.notes
|
|
1417
|
+
.map((n) => `'${escapeSingleQuotes(n)}'`)
|
|
1418
|
+
.join(', ')
|
|
1419
|
+
lines.push(` notes: [${notes}],`)
|
|
1420
|
+
}
|
|
1060
1421
|
|
|
1061
1422
|
// Generate nodes (RPC mapping)
|
|
1062
1423
|
const rpcMapEntries = Object.entries(nodeRpcMap)
|