@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
|
@@ -126,16 +126,157 @@ function inputToCode(input, indent, itemVar) {
|
|
|
126
126
|
});
|
|
127
127
|
return `{\n${lines.join('\n')}\n${indent}}`;
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Convert a fanout's per-iteration body into code by walking the `next` chain
|
|
131
|
+
* from the body's entry node. Steps whose result is referenced later in the
|
|
132
|
+
* same iteration are re-emitted as `const` bindings.
|
|
133
|
+
*/
|
|
134
|
+
function fanoutBodyToCode(entryNodeId, nodes, indent, itemVar) {
|
|
135
|
+
const lines = [];
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
let currentId = entryNodeId;
|
|
138
|
+
while (currentId && nodes[currentId] && !seen.has(currentId)) {
|
|
139
|
+
seen.add(currentId);
|
|
140
|
+
const node = nodes[currentId];
|
|
141
|
+
if ('rpcName' in node && node.rpcName) {
|
|
142
|
+
const stepName = node.stepName || `Call ${node.rpcName}`;
|
|
143
|
+
const input = (node.input || {});
|
|
144
|
+
const inputCode = inputToCode(input, indent, itemVar);
|
|
145
|
+
const options = node.options ? optionsToCode(node.options) : '';
|
|
146
|
+
const doCall = `await workflow.do('${escapeSingleQuotes(stepName)}', '${node.rpcName}', ${inputCode}${options ? `, ${options}` : ''})`;
|
|
147
|
+
lines.push(node.outputVar
|
|
148
|
+
? `${indent}const ${node.outputVar} = ${doCall}`
|
|
149
|
+
: `${indent}${doCall}`);
|
|
150
|
+
}
|
|
151
|
+
else if ('flow' in node) {
|
|
152
|
+
// A body is not RPC-only — a sleep or suspend between calls is part of
|
|
153
|
+
// the iteration and would otherwise be dropped from the regenerated code.
|
|
154
|
+
lines.push(...nodeToCode(node, nodes, indent).filter((line) => line !== ''));
|
|
155
|
+
}
|
|
156
|
+
currentId = node.next;
|
|
157
|
+
}
|
|
158
|
+
return lines;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Collect every node id that belongs to a parent construct's body, following
|
|
162
|
+
* each body's `next` chain to the parent's exit.
|
|
163
|
+
*/
|
|
164
|
+
function collectChildNodeIds(nodes) {
|
|
165
|
+
const owned = new Set();
|
|
166
|
+
// An error handler belongs to the step that routes to it, not to the main
|
|
167
|
+
// flow — emitting it top-level would turn compensation into a step that
|
|
168
|
+
// always runs.
|
|
169
|
+
for (const node of Object.values(nodes)) {
|
|
170
|
+
const onError = node.onError;
|
|
171
|
+
if (typeof onError === 'string') {
|
|
172
|
+
owned.add(onError);
|
|
173
|
+
}
|
|
174
|
+
else if (Array.isArray(onError)) {
|
|
175
|
+
for (const handlerId of onError)
|
|
176
|
+
owned.add(handlerId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const walk = (entry, exit) => {
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
let currentId = entry;
|
|
182
|
+
while (currentId &&
|
|
183
|
+
nodes[currentId] &&
|
|
184
|
+
isWithinBody(currentId, exit, seen)) {
|
|
185
|
+
seen.add(currentId);
|
|
186
|
+
owned.add(currentId);
|
|
187
|
+
const next = nodes[currentId].next;
|
|
188
|
+
currentId = typeof next === 'string' ? next : undefined;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
for (const node of Object.values(nodes)) {
|
|
192
|
+
if (!('flow' in node))
|
|
193
|
+
continue;
|
|
194
|
+
const flowNode = node;
|
|
195
|
+
const exit = flowNode.next;
|
|
196
|
+
switch (flowNode.flow) {
|
|
197
|
+
case 'branch':
|
|
198
|
+
for (const branch of flowNode.branches || [])
|
|
199
|
+
walk(branch.entry, exit);
|
|
200
|
+
walk(flowNode.elseEntry, exit);
|
|
201
|
+
break;
|
|
202
|
+
case 'switch':
|
|
203
|
+
for (const caseItem of flowNode.cases || [])
|
|
204
|
+
walk(caseItem.entry, exit);
|
|
205
|
+
walk(flowNode.defaultEntry, exit);
|
|
206
|
+
break;
|
|
207
|
+
case 'fanout':
|
|
208
|
+
walk(flowNode.childEntry, undefined);
|
|
209
|
+
break;
|
|
210
|
+
case 'parallel':
|
|
211
|
+
for (const childId of flowNode.children || [])
|
|
212
|
+
walk(childId, exit);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return owned;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Escape a value for embedding inside a single-quoted string literal.
|
|
220
|
+
*/
|
|
221
|
+
function escapeSingleQuotes(value) {
|
|
222
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* `outputVar` is optional — a graph-authored node may have no binding. Emitting
|
|
226
|
+
* `const undefined = ...` for those would not parse, so the expression is kept
|
|
227
|
+
* as a bare statement instead.
|
|
228
|
+
*/
|
|
229
|
+
function bindingPrefix(outputVar) {
|
|
230
|
+
return typeof outputVar === 'string' && outputVar
|
|
231
|
+
? `const ${outputVar} = `
|
|
232
|
+
: '';
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Resolve a node's `onError` node id back to the rpc name the DSL author wrote.
|
|
236
|
+
*/
|
|
237
|
+
function onErrorRpcNameOf(node, nodes) {
|
|
238
|
+
const onError = node.onError;
|
|
239
|
+
const handlerId = Array.isArray(onError) ? onError[0] : onError;
|
|
240
|
+
if (typeof handlerId !== 'string') {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
const handler = nodes[handlerId];
|
|
244
|
+
return handler && 'rpcName' in handler
|
|
245
|
+
? handler.rpcName
|
|
246
|
+
: undefined;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* A duration is `string | number` ('5s' or 5000). Quoting a number would change
|
|
250
|
+
* the value the runtime parses, so only strings get quotes.
|
|
251
|
+
*/
|
|
252
|
+
function durationToCode(duration) {
|
|
253
|
+
return typeof duration === 'number'
|
|
254
|
+
? String(duration)
|
|
255
|
+
: `'${escapeSingleQuotes(String(duration))}'`;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Render a switch case value. `switch` compares with ===, so quoting a number
|
|
259
|
+
* or boolean would stop the case ever matching.
|
|
260
|
+
*/
|
|
261
|
+
function caseValueToCode(value) {
|
|
262
|
+
if (typeof value === 'string') {
|
|
263
|
+
return `'${escapeSingleQuotes(value)}'`;
|
|
264
|
+
}
|
|
265
|
+
return String(value);
|
|
266
|
+
}
|
|
129
267
|
/**
|
|
130
268
|
* Convert options to code
|
|
131
269
|
*/
|
|
132
|
-
function optionsToCode(options) {
|
|
270
|
+
function optionsToCode(options, onErrorRpcName) {
|
|
133
271
|
const parts = [];
|
|
272
|
+
if (onErrorRpcName) {
|
|
273
|
+
parts.push(`onError: '${escapeSingleQuotes(onErrorRpcName)}'`);
|
|
274
|
+
}
|
|
134
275
|
if (options.retries !== undefined) {
|
|
135
276
|
parts.push(`retries: ${options.retries}`);
|
|
136
277
|
}
|
|
137
278
|
if (options.retryDelay !== undefined) {
|
|
138
|
-
parts.push(`retryDelay:
|
|
279
|
+
parts.push(`retryDelay: ${durationToCode(options.retryDelay)}`);
|
|
139
280
|
}
|
|
140
281
|
return parts.length > 0 ? `{ ${parts.join(', ')} }` : '';
|
|
141
282
|
}
|
|
@@ -193,20 +334,23 @@ function collectBranchConditionalVars(branchNode, nodes, conditionalVars) {
|
|
|
193
334
|
if (branchNode.branches) {
|
|
194
335
|
for (const branch of branchNode.branches) {
|
|
195
336
|
if (branch.entry) {
|
|
196
|
-
collectVarsFromBranch(branch.entry, nodes, conditionalVars, vars);
|
|
337
|
+
collectVarsFromBranch(branch.entry, nodes, conditionalVars, vars, branchNode.next);
|
|
197
338
|
}
|
|
198
339
|
}
|
|
199
340
|
}
|
|
200
341
|
// Check else branch
|
|
201
342
|
if (branchNode.elseEntry) {
|
|
202
|
-
collectVarsFromBranch(branchNode.elseEntry, nodes, conditionalVars, vars);
|
|
343
|
+
collectVarsFromBranch(branchNode.elseEntry, nodes, conditionalVars, vars, branchNode.next);
|
|
203
344
|
}
|
|
204
345
|
return vars;
|
|
205
346
|
}
|
|
206
347
|
/**
|
|
207
348
|
* Recursively collect output variables from a branch that are in conditionalVars
|
|
208
349
|
*/
|
|
209
|
-
function collectVarsFromBranch(nodeId, nodes, conditionalVars, result) {
|
|
350
|
+
function collectVarsFromBranch(nodeId, nodes, conditionalVars, result, exitNodeId, seen = new Set()) {
|
|
351
|
+
if (seen.has(nodeId))
|
|
352
|
+
return;
|
|
353
|
+
seen.add(nodeId);
|
|
210
354
|
const node = nodes[nodeId];
|
|
211
355
|
if (!node)
|
|
212
356
|
return;
|
|
@@ -220,37 +364,39 @@ function collectVarsFromBranch(nodeId, nodes, conditionalVars, result) {
|
|
|
220
364
|
// Follow the chain of nodes within the branch
|
|
221
365
|
if ('next' in node && node.next) {
|
|
222
366
|
const nextId = node.next;
|
|
223
|
-
// Only follow if it's still within the branch
|
|
224
|
-
if (
|
|
225
|
-
collectVarsFromBranch(nextId, nodes, conditionalVars, result);
|
|
367
|
+
// Only follow if it's still within the branch body
|
|
368
|
+
if (isWithinBody(nextId, exitNodeId, seen)) {
|
|
369
|
+
collectVarsFromBranch(nextId, nodes, conditionalVars, result, exitNodeId, seen);
|
|
226
370
|
}
|
|
227
371
|
}
|
|
228
372
|
}
|
|
229
373
|
/**
|
|
230
|
-
*
|
|
374
|
+
* A nested body (branch arm, switch case) ends where it rejoins the main flow.
|
|
375
|
+
* That rejoin point is the enclosing flow node's own `next`, so it is passed in
|
|
376
|
+
* explicitly rather than guessed from the node id — ids are step names, which
|
|
377
|
+
* carry no structural marker.
|
|
231
378
|
*/
|
|
232
|
-
function
|
|
233
|
-
return
|
|
234
|
-
nodeId.includes('_else_') ||
|
|
235
|
-
nodeId.includes('_branch'));
|
|
379
|
+
function isWithinBody(nodeId, exitNodeId, seen) {
|
|
380
|
+
return nodeId !== exitNodeId && !seen.has(nodeId);
|
|
236
381
|
}
|
|
237
382
|
/**
|
|
238
383
|
* Generate code for branch content (then/else blocks)
|
|
239
384
|
*/
|
|
240
|
-
function generateBranchContent(entryNodeId, nodes, indent, conditionalVars) {
|
|
385
|
+
function generateBranchContent(entryNodeId, nodes, indent, conditionalVars, exitNodeId) {
|
|
241
386
|
const lines = [];
|
|
387
|
+
const seen = new Set();
|
|
242
388
|
let currentId = entryNodeId;
|
|
243
389
|
while (currentId) {
|
|
244
390
|
const node = nodes[currentId];
|
|
245
391
|
if (!node)
|
|
246
392
|
break;
|
|
393
|
+
seen.add(currentId);
|
|
247
394
|
const nodeLines = nodeToCode(node, nodes, indent, conditionalVars, true);
|
|
248
395
|
lines.push(...nodeLines);
|
|
249
|
-
// Follow to next node within the
|
|
250
|
-
if ('next' in node && node.next) {
|
|
396
|
+
// Follow to next node within the body
|
|
397
|
+
if ('next' in node && node.next && typeof node.next === 'string') {
|
|
251
398
|
const nextId = node.next;
|
|
252
|
-
|
|
253
|
-
if (isWithinBranch(nextId)) {
|
|
399
|
+
if (isWithinBody(nextId, exitNodeId, seen)) {
|
|
254
400
|
currentId = nextId;
|
|
255
401
|
}
|
|
256
402
|
else {
|
|
@@ -276,8 +422,9 @@ function nodeToCode(node, nodes, indent, conditionalVars = new Set(), isInsideBr
|
|
|
276
422
|
const outputVar = node.outputVar;
|
|
277
423
|
let doCall = `await workflow.do('${stepName}', '${node.rpcName}', ${inputCode}`;
|
|
278
424
|
// Add options if present
|
|
279
|
-
|
|
280
|
-
|
|
425
|
+
const onErrorRpc = onErrorRpcNameOf(node, nodes);
|
|
426
|
+
if (node.options || onErrorRpc) {
|
|
427
|
+
const optCode = optionsToCode(node.options ?? {}, onErrorRpc);
|
|
281
428
|
if (optCode) {
|
|
282
429
|
doCall += `, ${optCode}`;
|
|
283
430
|
}
|
|
@@ -303,9 +450,23 @@ function nodeToCode(node, nodes, indent, conditionalVars = new Set(), isInsideBr
|
|
|
303
450
|
const flowNode = node;
|
|
304
451
|
switch (flowNode.flow) {
|
|
305
452
|
case 'sleep':
|
|
306
|
-
lines.push(`${indent}await workflow.sleep('${flowNode.stepName || 'Sleep'}',
|
|
453
|
+
lines.push(`${indent}await workflow.sleep('${escapeSingleQuotes(flowNode.stepName || 'Sleep')}', ${typeof flowNode.expression === 'string'
|
|
454
|
+
? flowNode.expression
|
|
455
|
+
: durationToCode(flowNode.duration)})`);
|
|
307
456
|
lines.push('');
|
|
308
457
|
break;
|
|
458
|
+
case 'suspend':
|
|
459
|
+
lines.push(`${indent}await workflow.suspend('${escapeSingleQuotes(String(flowNode.reason ?? ''))}')`);
|
|
460
|
+
lines.push('');
|
|
461
|
+
break;
|
|
462
|
+
case 'approval': {
|
|
463
|
+
const approvalCall = `await workflow.approval('${escapeSingleQuotes(String(flowNode.reason ?? ''))}')`;
|
|
464
|
+
lines.push(flowNode.outputVar
|
|
465
|
+
? `${indent}const ${flowNode.outputVar} = ${approvalCall}`
|
|
466
|
+
: `${indent}${approvalCall}`);
|
|
467
|
+
lines.push('');
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
309
470
|
case 'cancel':
|
|
310
471
|
const cancelReason = flowNode.reason || flowNode.stepName || 'Workflow cancelled';
|
|
311
472
|
lines.push(`${indent}throw new WorkflowCancelledException('${cancelReason}')`);
|
|
@@ -325,7 +486,7 @@ function nodeToCode(node, nodes, indent, conditionalVars = new Set(), isInsideBr
|
|
|
325
486
|
const keyword = i === 0 ? 'if' : 'else if';
|
|
326
487
|
lines.push(`${indent}${keyword} (${condition}) {`);
|
|
327
488
|
if (branch.entry && nodes[branch.entry]) {
|
|
328
|
-
const branchLines = generateBranchContent(branch.entry, nodes, indent + ' ', conditionalVars);
|
|
489
|
+
const branchLines = generateBranchContent(branch.entry, nodes, indent + ' ', conditionalVars, flowNode.next);
|
|
329
490
|
lines.push(...branchLines);
|
|
330
491
|
}
|
|
331
492
|
lines.push(`${indent}}`);
|
|
@@ -333,75 +494,75 @@ function nodeToCode(node, nodes, indent, conditionalVars = new Set(), isInsideBr
|
|
|
333
494
|
// Generate else block if present
|
|
334
495
|
if (flowNode.elseEntry && nodes[flowNode.elseEntry]) {
|
|
335
496
|
lines.push(`${indent}else {`);
|
|
336
|
-
const elseLines = generateBranchContent(flowNode.elseEntry, nodes, indent + ' ', conditionalVars);
|
|
497
|
+
const elseLines = generateBranchContent(flowNode.elseEntry, nodes, indent + ' ', conditionalVars, flowNode.next);
|
|
337
498
|
lines.push(...elseLines);
|
|
338
499
|
lines.push(`${indent}}`);
|
|
339
500
|
}
|
|
340
501
|
lines.push('');
|
|
341
502
|
break;
|
|
342
|
-
case 'switch':
|
|
503
|
+
case 'switch': {
|
|
504
|
+
const caseExit = flowNode.next;
|
|
343
505
|
lines.push(`${indent}switch (${flowNode.expression}) {`);
|
|
344
506
|
for (const caseItem of flowNode.cases || []) {
|
|
345
|
-
lines.push(`${indent} case
|
|
507
|
+
lines.push(`${indent} case ${caseValueToCode(caseItem.value)}:`);
|
|
346
508
|
if (caseItem.entry && nodes[caseItem.entry]) {
|
|
347
|
-
|
|
348
|
-
lines.push(...caseLines);
|
|
509
|
+
lines.push(...generateBranchContent(caseItem.entry, nodes, indent + ' ', conditionalVars, caseExit));
|
|
349
510
|
}
|
|
350
511
|
lines.push(`${indent} break`);
|
|
351
512
|
}
|
|
352
513
|
if (flowNode.defaultEntry && nodes[flowNode.defaultEntry]) {
|
|
353
514
|
lines.push(`${indent} default:`);
|
|
354
|
-
|
|
355
|
-
lines.push(...defaultLines);
|
|
515
|
+
lines.push(...generateBranchContent(flowNode.defaultEntry, nodes, indent + ' ', conditionalVars, caseExit));
|
|
356
516
|
lines.push(`${indent} break`);
|
|
357
517
|
}
|
|
358
518
|
lines.push(`${indent}}`);
|
|
359
519
|
lines.push('');
|
|
360
520
|
break;
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
521
|
+
}
|
|
522
|
+
case 'parallel': {
|
|
523
|
+
const childNodes = (flowNode.children || [])
|
|
524
|
+
.map((childId) => nodes[childId])
|
|
525
|
+
.filter((child) => !!child && 'rpcName' in child && !!child.rpcName);
|
|
526
|
+
// Results the caller bound (const [a, b] = await Promise.all([...]))
|
|
527
|
+
// must be re-declared, or every later reference to them is unbound.
|
|
528
|
+
const bindings = childNodes.map((child) => child.outputVar);
|
|
529
|
+
const prefix = bindings.some(Boolean)
|
|
530
|
+
? `const [${bindings
|
|
531
|
+
.map((name, i) => name ?? `_${i}`)
|
|
532
|
+
.join(', ')}] = `
|
|
533
|
+
: '';
|
|
534
|
+
lines.push(`${indent}${prefix}await Promise.all([`);
|
|
535
|
+
for (const childNode of childNodes) {
|
|
536
|
+
const stepName = childNode.stepName || `Call ${childNode.rpcName}`;
|
|
537
|
+
const input = (childNode.input || {});
|
|
538
|
+
const inputCode = inputToCode(input, indent + ' ');
|
|
539
|
+
let call = `workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode}`;
|
|
540
|
+
if (childNode.options) {
|
|
541
|
+
const optCode = optionsToCode(childNode.options);
|
|
542
|
+
if (optCode) {
|
|
543
|
+
call += `, ${optCode}`;
|
|
371
544
|
}
|
|
372
545
|
}
|
|
546
|
+
call += ')';
|
|
547
|
+
lines.push(`${indent} ${call},`);
|
|
373
548
|
}
|
|
374
549
|
lines.push(`${indent}])`);
|
|
375
550
|
lines.push('');
|
|
376
551
|
break;
|
|
377
|
-
|
|
552
|
+
}
|
|
553
|
+
case 'fanout': {
|
|
554
|
+
const bodyLines = fanoutBodyToCode(flowNode.childEntry, nodes, flowNode.mode === 'parallel' ? indent + ' ' : indent + ' ', flowNode.itemVar);
|
|
378
555
|
if (flowNode.mode === 'parallel') {
|
|
379
556
|
lines.push(`${indent}await Promise.all(`);
|
|
380
|
-
lines.push(`${indent} ${flowNode.sourceVar}.map(async (${flowNode.itemVar})
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if ('rpcName' in childNode && childNode.rpcName) {
|
|
384
|
-
const stepName = childNode.stepName || `Call ${childNode.rpcName}`;
|
|
385
|
-
const input = (childNode.input || {});
|
|
386
|
-
const inputCode = inputToCode(input, indent + ' ', flowNode.itemVar);
|
|
387
|
-
lines.push(`${indent} await workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode})`);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
lines.push(`${indent} )`);
|
|
557
|
+
lines.push(`${indent} ${flowNode.sourceVar}.map(async (${flowNode.itemVar}) => {`);
|
|
558
|
+
lines.push(...bodyLines);
|
|
559
|
+
lines.push(`${indent} })`);
|
|
391
560
|
lines.push(`${indent})`);
|
|
392
561
|
}
|
|
393
562
|
else {
|
|
394
563
|
// Sequential fanout
|
|
395
564
|
lines.push(`${indent}for (const ${flowNode.itemVar} of ${flowNode.sourceVar}) {`);
|
|
396
|
-
|
|
397
|
-
const childNode = nodes[flowNode.childEntry];
|
|
398
|
-
if ('rpcName' in childNode && childNode.rpcName) {
|
|
399
|
-
const stepName = childNode.stepName || `Call ${childNode.rpcName}`;
|
|
400
|
-
const input = (childNode.input || {});
|
|
401
|
-
const inputCode = inputToCode(input, indent + ' ', flowNode.itemVar);
|
|
402
|
-
lines.push(`${indent} await workflow.do('${stepName}', '${childNode.rpcName}', ${inputCode})`);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
565
|
+
lines.push(...bodyLines);
|
|
405
566
|
if (flowNode.timeBetween) {
|
|
406
567
|
lines.push(`${indent} await workflow.sleep('Wait between iterations', '${flowNode.timeBetween}')`);
|
|
407
568
|
}
|
|
@@ -409,19 +570,23 @@ function nodeToCode(node, nodes, indent, conditionalVars = new Set(), isInsideBr
|
|
|
409
570
|
}
|
|
410
571
|
lines.push('');
|
|
411
572
|
break;
|
|
573
|
+
}
|
|
412
574
|
case 'filter':
|
|
413
|
-
lines.push(`${indent}
|
|
575
|
+
lines.push(`${indent}${bindingPrefix(flowNode.outputVar)}${flowNode.sourceVar}.filter((${flowNode.itemVar}) => ${conditionToCode(flowNode.condition)})`);
|
|
414
576
|
lines.push('');
|
|
415
577
|
break;
|
|
416
578
|
case 'arrayPredicate':
|
|
417
579
|
const method = flowNode.mode === 'some' ? 'some' : 'every';
|
|
418
|
-
lines.push(`${indent}
|
|
580
|
+
lines.push(`${indent}${bindingPrefix(flowNode.outputVar)}${flowNode.sourceVar}.${method}((${flowNode.itemVar}) => ${conditionToCode(flowNode.condition)})`);
|
|
419
581
|
lines.push('');
|
|
420
582
|
break;
|
|
421
583
|
case 'return':
|
|
422
|
-
if (flowNode.outputs) {
|
|
584
|
+
if (flowNode.outputs || flowNode.spread) {
|
|
423
585
|
const returnObj = [];
|
|
424
|
-
for (const
|
|
586
|
+
for (const spreadVar of flowNode.spread ?? []) {
|
|
587
|
+
returnObj.push(`${indent} ...${spreadVar},`);
|
|
588
|
+
}
|
|
589
|
+
for (const [key, output] of Object.entries((flowNode.outputs ?? {}))) {
|
|
425
590
|
let value;
|
|
426
591
|
if (output.from === 'outputVar') {
|
|
427
592
|
value = output.path
|
|
@@ -462,9 +627,11 @@ function nodeToCode(node, nodes, indent, conditionalVars = new Set(), isInsideBr
|
|
|
462
627
|
case 'set':
|
|
463
628
|
// Generate variable assignment: varName = value
|
|
464
629
|
const setVar = flowNode.variable;
|
|
465
|
-
const setValue = typeof flowNode.
|
|
466
|
-
?
|
|
467
|
-
:
|
|
630
|
+
const setValue = typeof flowNode.expression === 'string'
|
|
631
|
+
? flowNode.expression
|
|
632
|
+
: typeof flowNode.value === 'string'
|
|
633
|
+
? `'${escapeSingleQuotes(flowNode.value)}'`
|
|
634
|
+
: JSON.stringify(flowNode.value);
|
|
468
635
|
lines.push(`${indent}${setVar} = ${setValue}`);
|
|
469
636
|
lines.push('');
|
|
470
637
|
break;
|
|
@@ -480,14 +647,44 @@ function findConditionalVars(nodes) {
|
|
|
480
647
|
const conditionalVars = new Set();
|
|
481
648
|
const varsInBranches = new Set();
|
|
482
649
|
const varsUsedInReturn = new Set();
|
|
483
|
-
// Collect variables defined
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
650
|
+
// Collect variables defined inside a branch arm or switch case. Found by
|
|
651
|
+
// walking each body from its entry to the enclosing node's exit — node ids
|
|
652
|
+
// are step names and carry no structural marker to match on.
|
|
653
|
+
for (const node of Object.values(nodes)) {
|
|
654
|
+
if (!('flow' in node))
|
|
655
|
+
continue;
|
|
656
|
+
const flowNode = node;
|
|
657
|
+
const exit = flowNode.next;
|
|
658
|
+
const entries = [];
|
|
659
|
+
if (flowNode.flow === 'branch') {
|
|
660
|
+
for (const branch of flowNode.branches || []) {
|
|
661
|
+
if (branch.entry)
|
|
662
|
+
entries.push(branch.entry);
|
|
663
|
+
}
|
|
664
|
+
if (flowNode.elseEntry)
|
|
665
|
+
entries.push(flowNode.elseEntry);
|
|
666
|
+
}
|
|
667
|
+
else if (flowNode.flow === 'switch') {
|
|
668
|
+
for (const caseItem of flowNode.cases || []) {
|
|
669
|
+
if (caseItem.entry)
|
|
670
|
+
entries.push(caseItem.entry);
|
|
671
|
+
}
|
|
672
|
+
if (flowNode.defaultEntry)
|
|
673
|
+
entries.push(flowNode.defaultEntry);
|
|
674
|
+
}
|
|
675
|
+
for (const entry of entries) {
|
|
676
|
+
const seen = new Set();
|
|
677
|
+
let currentId = entry;
|
|
678
|
+
while (currentId &&
|
|
679
|
+
nodes[currentId] &&
|
|
680
|
+
isWithinBody(currentId, exit, seen)) {
|
|
681
|
+
seen.add(currentId);
|
|
682
|
+
const bodyNode = nodes[currentId];
|
|
683
|
+
if (bodyNode.outputVar) {
|
|
684
|
+
varsInBranches.add(bodyNode.outputVar);
|
|
685
|
+
}
|
|
686
|
+
currentId =
|
|
687
|
+
typeof bodyNode.next === 'string' ? bodyNode.next : undefined;
|
|
491
688
|
}
|
|
492
689
|
}
|
|
493
690
|
}
|
|
@@ -571,14 +768,13 @@ export function deserializeDslWorkflow(workflow, options = {}) {
|
|
|
571
768
|
}
|
|
572
769
|
// Process nodes in order
|
|
573
770
|
const orderedNodes = traverseNodes(workflow.nodes, workflow.entryNodeIds);
|
|
771
|
+
// Nodes emitted as part of a parent (branch arm, switch case, parallel group,
|
|
772
|
+
// fanout body) must not also be emitted at the top level. Ownership is read
|
|
773
|
+
// from the parents themselves — node ids are step names and a legitimate step
|
|
774
|
+
// may be called anything.
|
|
775
|
+
const ownedByParent = collectChildNodeIds(workflow.nodes);
|
|
574
776
|
for (const node of orderedNodes) {
|
|
575
|
-
|
|
576
|
-
if (node.nodeId.includes('_then_') ||
|
|
577
|
-
node.nodeId.includes('_else_') ||
|
|
578
|
-
node.nodeId.includes('_case') ||
|
|
579
|
-
node.nodeId.includes('_default_') ||
|
|
580
|
-
node.nodeId.includes('_child_') ||
|
|
581
|
-
node.nodeId.includes('_item_')) {
|
|
777
|
+
if (ownedByParent.has(node.nodeId)) {
|
|
582
778
|
continue;
|
|
583
779
|
}
|
|
584
780
|
const nodeLines = nodeToCode(node, workflow.nodes, ' ', conditionalVars);
|
|
@@ -699,11 +895,79 @@ function findNextRpcNode(startNextId, nodes, flowNodeIds, visited = new Set()) {
|
|
|
699
895
|
}
|
|
700
896
|
// It's a flow node - follow its 'next' if it has one
|
|
701
897
|
const flowNode = nodes[startNextId];
|
|
702
|
-
if (flowNode && 'next' in flowNode && flowNode.next) {
|
|
898
|
+
if (flowNode && 'next' in flowNode && typeof flowNode.next === 'string') {
|
|
703
899
|
return findNextRpcNode(flowNode.next, nodes, flowNodeIds, visited);
|
|
704
900
|
}
|
|
705
901
|
return null;
|
|
706
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* Resolve a single next target through any intervening flow nodes.
|
|
905
|
+
*/
|
|
906
|
+
function resolveNextTarget(target, nodes, flowNodeIds) {
|
|
907
|
+
const resolved = flowNodeIds.has(target)
|
|
908
|
+
? findNextRpcNode(target, nodes, flowNodeIds)
|
|
909
|
+
: target;
|
|
910
|
+
return resolved && !flowNodeIds.has(resolved) ? resolved : null;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Render a `next` in any of its shapes. A `next` is not always a single node
|
|
914
|
+
* id — it can fan out to an array, route by branch key, or carry conditions.
|
|
915
|
+
* Coercing those to a string produces node ids that match nothing ('a,b',
|
|
916
|
+
* '[object Object]'), silently severing the rest of the graph.
|
|
917
|
+
*/
|
|
918
|
+
function nextToCode(next, nodes, flowNodeIds) {
|
|
919
|
+
const renderTargets = (targets) => {
|
|
920
|
+
if (typeof targets === 'string') {
|
|
921
|
+
const resolved = resolveNextTarget(targets, nodes, flowNodeIds);
|
|
922
|
+
return resolved ? `'${resolved}'` : null;
|
|
923
|
+
}
|
|
924
|
+
const resolved = targets
|
|
925
|
+
.map((t) => resolveNextTarget(t, nodes, flowNodeIds))
|
|
926
|
+
.filter((t) => t !== null);
|
|
927
|
+
return resolved.length > 0
|
|
928
|
+
? `[${resolved.map((t) => `'${t}'`).join(', ')}]`
|
|
929
|
+
: null;
|
|
930
|
+
};
|
|
931
|
+
if (typeof next === 'string' || Array.isArray(next)) {
|
|
932
|
+
return renderTargets(next);
|
|
933
|
+
}
|
|
934
|
+
if (typeof next === 'object' && next !== null) {
|
|
935
|
+
if ('conditions' in next && Array.isArray(next.conditions)) {
|
|
936
|
+
const conditions = next.conditions
|
|
937
|
+
.map((condition) => {
|
|
938
|
+
const target = renderTargets(condition.target);
|
|
939
|
+
return target
|
|
940
|
+
? `{ expression: '${escapeSingleQuotes(condition.expression)}', target: ${target} }`
|
|
941
|
+
: null;
|
|
942
|
+
})
|
|
943
|
+
.filter((c) => c !== null);
|
|
944
|
+
if (conditions.length === 0) {
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
const parts = [`conditions: [${conditions.join(', ')}]`];
|
|
948
|
+
if (next.default !== undefined) {
|
|
949
|
+
const defaultTarget = renderTargets(next.default);
|
|
950
|
+
if (defaultTarget) {
|
|
951
|
+
parts.push(`default: ${defaultTarget}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return `{ ${parts.join(', ')} }`;
|
|
955
|
+
}
|
|
956
|
+
const entries = Object.entries(next)
|
|
957
|
+
.map(([key, target]) => {
|
|
958
|
+
const isTarget = typeof target === 'string' ||
|
|
959
|
+
(Array.isArray(target) && target.every((t) => typeof t === 'string'));
|
|
960
|
+
const rendered = isTarget ? renderTargets(target) : null;
|
|
961
|
+
const safeKey = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
962
|
+
? key
|
|
963
|
+
: JSON.stringify(key);
|
|
964
|
+
return rendered ? `${safeKey}: ${rendered}` : null;
|
|
965
|
+
})
|
|
966
|
+
.filter((e) => e !== null);
|
|
967
|
+
return entries.length > 0 ? `{ ${entries.join(', ')} }` : null;
|
|
968
|
+
}
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
707
971
|
/**
|
|
708
972
|
* Deserialize a graph workflow to pikkuWorkflowGraph code
|
|
709
973
|
*/
|
|
@@ -752,14 +1016,26 @@ export function deserializeGraphWorkflow(workflow, options = {}) {
|
|
|
752
1016
|
const configParts = [];
|
|
753
1017
|
// Add next if present - follow through flow nodes to find the actual next RPC node
|
|
754
1018
|
if ('next' in node && node.next) {
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1019
|
+
const nextCode = nextToCode(node.next, workflow.nodes, flowNodeIds);
|
|
1020
|
+
if (nextCode) {
|
|
1021
|
+
configParts.push(`next: ${nextCode}`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
// onError, retries and retryDelay are all honoured by the graph runner,
|
|
1025
|
+
// so dropping them here silently changes how the workflow behaves.
|
|
1026
|
+
if ('onError' in node && node.onError) {
|
|
1027
|
+
const onErrorCode = nextToCode(node.onError, workflow.nodes, flowNodeIds);
|
|
1028
|
+
if (onErrorCode) {
|
|
1029
|
+
configParts.push(`onError: ${onErrorCode}`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if ('options' in node && node.options) {
|
|
1033
|
+
const { retries, retryDelay } = node.options;
|
|
1034
|
+
if (retries !== undefined) {
|
|
1035
|
+
configParts.push(`retries: ${retries}`);
|
|
1036
|
+
}
|
|
1037
|
+
if (retryDelay !== undefined) {
|
|
1038
|
+
configParts.push(`retryDelay: ${durationToCode(retryDelay)}`);
|
|
763
1039
|
}
|
|
764
1040
|
}
|
|
765
1041
|
// Add input if present
|
|
@@ -791,6 +1067,12 @@ export function deserializeGraphWorkflow(workflow, options = {}) {
|
|
|
791
1067
|
if (workflow.tags && workflow.tags.length > 0) {
|
|
792
1068
|
lines.push(` tags: [${workflow.tags.map((t) => `'${t}'`).join(', ')}],`);
|
|
793
1069
|
}
|
|
1070
|
+
if (workflow.notes && workflow.notes.length > 0) {
|
|
1071
|
+
const notes = workflow.notes
|
|
1072
|
+
.map((n) => `'${escapeSingleQuotes(n)}'`)
|
|
1073
|
+
.join(', ');
|
|
1074
|
+
lines.push(` notes: [${notes}],`);
|
|
1075
|
+
}
|
|
794
1076
|
// Generate nodes (RPC mapping)
|
|
795
1077
|
const rpcMapEntries = Object.entries(nodeRpcMap);
|
|
796
1078
|
if (rpcMapEntries.length > 0) {
|