@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
2
|
+
import { isWorkflowDoCall, isWorkflowExpectEventuallyCall, extractActorFromOptions, isWorkflowSleepCall, isWorkflowSuspendCall, isWorkflowApprovalCall, isThrowCancelException, extractCancelReason, isParallelFanout, isParallelGroup, isSequentialFanout, isArrayFilter, isArraySome, isArrayEvery, extractForOfVariable, isArrayType, getSourceText, extractSourcePath, } from './patterns.js';
|
|
3
3
|
import { validateNoDisallowedPatterns, validateAwaitedCalls, formatValidationErrors, } from './validation.js';
|
|
4
4
|
import { extractStringLiteral, extractNumberLiteral, } from '../../extract-node-value.js';
|
|
5
5
|
/**
|
|
@@ -197,11 +197,15 @@ function extractStep(statement, context) {
|
|
|
197
197
|
function extractVariableDeclaration(statement, context) {
|
|
198
198
|
const declList = statement.declarationList;
|
|
199
199
|
if (declList.declarations.length !== 1) {
|
|
200
|
+
context.errors.push({
|
|
201
|
+
message: `A single declaration statement may only declare one variable in DSL workflows. Split '${statement.getText().slice(0, 60)}' into separate statements.`,
|
|
202
|
+
node: statement,
|
|
203
|
+
});
|
|
200
204
|
return null;
|
|
201
205
|
}
|
|
202
206
|
const decl = declList.declarations[0];
|
|
203
207
|
if (!ts.isIdentifier(decl.name)) {
|
|
204
|
-
return
|
|
208
|
+
return extractDestructuredDeclaration(statement, decl, context);
|
|
205
209
|
}
|
|
206
210
|
const varName = decl.name.text;
|
|
207
211
|
const init = decl.initializer;
|
|
@@ -295,6 +299,55 @@ function extractVariableDeclaration(statement, context) {
|
|
|
295
299
|
}
|
|
296
300
|
return null;
|
|
297
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Extract a declaration whose binding is a destructuring pattern.
|
|
304
|
+
*
|
|
305
|
+
* `const [a, b] = await Promise.all([...])` is the idiomatic way to run steps
|
|
306
|
+
* in parallel and keep both results, so each element of the pattern is bound to
|
|
307
|
+
* the matching child step's output. Every other destructuring shape reports a
|
|
308
|
+
* diagnostic rather than silently dropping the step.
|
|
309
|
+
*/
|
|
310
|
+
function extractDestructuredDeclaration(statement, decl, context) {
|
|
311
|
+
const init = decl.initializer;
|
|
312
|
+
if (init &&
|
|
313
|
+
ts.isAwaitExpression(init) &&
|
|
314
|
+
ts.isCallExpression(init.expression) &&
|
|
315
|
+
isParallelGroup(init.expression) &&
|
|
316
|
+
ts.isArrayBindingPattern(decl.name)) {
|
|
317
|
+
const elements = decl.name.elements;
|
|
318
|
+
const allSimple = elements.every((el) => ts.isBindingElement(el) &&
|
|
319
|
+
ts.isIdentifier(el.name) &&
|
|
320
|
+
!el.dotDotDotToken);
|
|
321
|
+
if (allSimple) {
|
|
322
|
+
const step = extractParallelGroup(init.expression, context);
|
|
323
|
+
if (step) {
|
|
324
|
+
if (elements.length !== step.children.length) {
|
|
325
|
+
context.errors.push({
|
|
326
|
+
message: `Destructuring binds ${elements.length} name(s) but Promise.all has ${step.children.length} step(s). They must match so each result can be bound to its step.`,
|
|
327
|
+
node: statement,
|
|
328
|
+
});
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
elements.forEach((el, i) => {
|
|
332
|
+
const name = el.name;
|
|
333
|
+
const varName = name.text;
|
|
334
|
+
step.children[i].outputVar = varName;
|
|
335
|
+
const type = context.checker.getTypeAtLocation(name);
|
|
336
|
+
context.outputVars.set(varName, { type, node: name });
|
|
337
|
+
if (isArrayType(type, context.checker)) {
|
|
338
|
+
context.arrayVars.add(varName);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
return step;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
context.errors.push({
|
|
346
|
+
message: `Destructuring a step result is not supported in DSL workflows. Assign it to a variable first (e.g. \`const result = await workflow.do(...)\`) and read its properties.`,
|
|
347
|
+
node: statement,
|
|
348
|
+
});
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
298
351
|
/**
|
|
299
352
|
* Extract expression statement (await workflow.do(...) without assignment)
|
|
300
353
|
*/
|
|
@@ -327,11 +380,11 @@ function extractExpressionStatement(statement, context) {
|
|
|
327
380
|
value: literalValue,
|
|
328
381
|
};
|
|
329
382
|
}
|
|
330
|
-
// Non-literal assignment to context var -
|
|
383
|
+
// Non-literal assignment to context var - keep the source expression
|
|
331
384
|
return {
|
|
332
385
|
type: 'set',
|
|
333
386
|
variable: outputVar,
|
|
334
|
-
|
|
387
|
+
expression: getSourceText(expr.right),
|
|
335
388
|
};
|
|
336
389
|
}
|
|
337
390
|
}
|
|
@@ -363,6 +416,19 @@ function extractExpressionStatement(statement, context) {
|
|
|
363
416
|
if (isWorkflowSuspendCall(call, context.checker)) {
|
|
364
417
|
return extractSuspendStep(call, context);
|
|
365
418
|
}
|
|
419
|
+
if (isWorkflowApprovalCall(call, context.checker)) {
|
|
420
|
+
const step = extractApprovalStep(call, context, outputVar);
|
|
421
|
+
// Unlike suspend, an approval yields a value, so a downstream step can
|
|
422
|
+
// reference it — track the binding the same way workflow.do() does.
|
|
423
|
+
if (outputVar && step) {
|
|
424
|
+
const type = context.checker.getTypeAtLocation(expr);
|
|
425
|
+
context.outputVars.set(outputVar, { type, node: expr });
|
|
426
|
+
if (isArrayType(type, context.checker)) {
|
|
427
|
+
context.arrayVars.add(outputVar);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return step;
|
|
431
|
+
}
|
|
366
432
|
// Check for parallel group or fanout
|
|
367
433
|
if (isParallelFanout(call)) {
|
|
368
434
|
return extractParallelFanout(call, context);
|
|
@@ -477,6 +543,17 @@ function extractStepOptions(optionsNode, context) {
|
|
|
477
543
|
// Ignore extraction errors for retryDelay
|
|
478
544
|
}
|
|
479
545
|
}
|
|
546
|
+
else if (propName === 'onError') {
|
|
547
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
548
|
+
options.onError = prop.initializer.text;
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
context.errors.push({
|
|
552
|
+
message: 'onError must be a literal RPC name so it can be wired and drawn in the graph.',
|
|
553
|
+
node: prop.initializer,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
480
557
|
else if (propName === 'description') {
|
|
481
558
|
try {
|
|
482
559
|
options.description = extractStringLiteral(prop.initializer, context.checker);
|
|
@@ -505,7 +582,20 @@ function extractSleepStep(call, context) {
|
|
|
505
582
|
duration = numValue;
|
|
506
583
|
}
|
|
507
584
|
else {
|
|
508
|
-
|
|
585
|
+
try {
|
|
586
|
+
duration = extractStringLiteral(args[1], context.checker);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
// A duration computed at runtime (a loop variable, a field off the
|
|
590
|
+
// input) is legal: the closure evaluates it. Record the source text so
|
|
591
|
+
// the graph can show what it waits on, rather than failing the workflow.
|
|
592
|
+
return {
|
|
593
|
+
type: 'sleep',
|
|
594
|
+
stepName,
|
|
595
|
+
duration: '',
|
|
596
|
+
expression: args[1].getText(),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
509
599
|
}
|
|
510
600
|
return {
|
|
511
601
|
type: 'sleep',
|
|
@@ -543,6 +633,48 @@ function extractSuspendStep(call, context) {
|
|
|
543
633
|
return null;
|
|
544
634
|
}
|
|
545
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* Extract approval step from workflow.approval() call
|
|
638
|
+
*/
|
|
639
|
+
function extractApprovalStep(call, context, outputVar) {
|
|
640
|
+
const args = call.arguments;
|
|
641
|
+
if (args.length < 1)
|
|
642
|
+
return null;
|
|
643
|
+
try {
|
|
644
|
+
const reason = extractStringLiteral(args[0], context.checker);
|
|
645
|
+
const step = {
|
|
646
|
+
type: 'approval',
|
|
647
|
+
reason,
|
|
648
|
+
};
|
|
649
|
+
if (outputVar) {
|
|
650
|
+
step.outputVar = outputVar;
|
|
651
|
+
}
|
|
652
|
+
// The `schema` option is a runtime value validated inside the workflow body,
|
|
653
|
+
// so it is deliberately not serialized here — only `expiry`, which the graph
|
|
654
|
+
// and planned-step ladder need in order to describe the gate.
|
|
655
|
+
const options = args[1];
|
|
656
|
+
if (options && ts.isObjectLiteralExpression(options)) {
|
|
657
|
+
for (const prop of options.properties) {
|
|
658
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
659
|
+
prop.name.getText() === 'expiry' &&
|
|
660
|
+
(ts.isStringLiteral(prop.initializer) ||
|
|
661
|
+
ts.isNumericLiteral(prop.initializer))) {
|
|
662
|
+
step.expiry = ts.isNumericLiteral(prop.initializer)
|
|
663
|
+
? Number(prop.initializer.text)
|
|
664
|
+
: prop.initializer.text;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return step;
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
context.errors.push({
|
|
672
|
+
message: `Failed to extract approval step: ${error instanceof Error ? error.message : String(error)}`,
|
|
673
|
+
node: call,
|
|
674
|
+
});
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
546
678
|
/**
|
|
547
679
|
* Extract cancel step from throw WorkflowCancelledException statement
|
|
548
680
|
*/
|
|
@@ -780,6 +912,84 @@ function extractArrayPredicate(call, context, outputVar) {
|
|
|
780
912
|
outputVar,
|
|
781
913
|
};
|
|
782
914
|
}
|
|
915
|
+
/**
|
|
916
|
+
* Extract an ordered list of workflow.do steps from a per-iteration body.
|
|
917
|
+
*
|
|
918
|
+
* Unlike if/switch blocks, a fanout body is a real per-iteration scope, so
|
|
919
|
+
* `const x = await workflow.do(...)` is meaningful here: the binding is
|
|
920
|
+
* registered on the child context so later steps in the same iteration can
|
|
921
|
+
* reference it.
|
|
922
|
+
*/
|
|
923
|
+
function extractFanoutBodyStep(stmt, childContext, body) {
|
|
924
|
+
if (ts.isVariableStatement(stmt)) {
|
|
925
|
+
const declList = stmt.declarationList;
|
|
926
|
+
if (declList.declarations.length !== 1) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const decl = declList.declarations[0];
|
|
930
|
+
const init = decl.initializer;
|
|
931
|
+
if (!init ||
|
|
932
|
+
!ts.isAwaitExpression(init) ||
|
|
933
|
+
!ts.isCallExpression(init.expression)) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const call = init.expression;
|
|
937
|
+
if (!isWorkflowDoCall(call, childContext.checker)) {
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const varName = ts.isIdentifier(decl.name) ? decl.name.text : undefined;
|
|
941
|
+
const step = extractRpcStep(call, childContext, varName);
|
|
942
|
+
if (!step) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (varName) {
|
|
946
|
+
const type = childContext.checker.getTypeAtLocation(decl);
|
|
947
|
+
childContext.outputVars.set(varName, { type, node: decl });
|
|
948
|
+
if (isArrayType(type, childContext.checker)) {
|
|
949
|
+
childContext.arrayVars.add(varName);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
body.push(step);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (ts.isExpressionStatement(stmt) || ts.isReturnStatement(stmt)) {
|
|
956
|
+
const expr = ts.isReturnStatement(stmt) ? stmt.expression : stmt.expression;
|
|
957
|
+
if (!expr) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const call = ts.isAwaitExpression(expr)
|
|
961
|
+
? ts.isCallExpression(expr.expression)
|
|
962
|
+
? expr.expression
|
|
963
|
+
: null
|
|
964
|
+
: ts.isCallExpression(expr)
|
|
965
|
+
? expr
|
|
966
|
+
: null;
|
|
967
|
+
if (!call) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (isWorkflowSleepCall(call, childContext.checker)) {
|
|
971
|
+
const sleepStep = extractSleepStep(call, childContext);
|
|
972
|
+
if (sleepStep && sleepStep.type === 'sleep') {
|
|
973
|
+
body.push(sleepStep);
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (isWorkflowSuspendCall(call, childContext.checker)) {
|
|
978
|
+
const suspendStep = extractSuspendStep(call, childContext);
|
|
979
|
+
if (suspendStep && suspendStep.type === 'suspend') {
|
|
980
|
+
body.push(suspendStep);
|
|
981
|
+
}
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (!isWorkflowDoCall(call, childContext.checker)) {
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const step = extractRpcStep(call, childContext);
|
|
988
|
+
if (step) {
|
|
989
|
+
body.push(step);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
783
993
|
/**
|
|
784
994
|
* Extract parallel fanout from Promise.all(array.map(...))
|
|
785
995
|
*/
|
|
@@ -808,63 +1018,45 @@ function extractParallelFanout(call, context) {
|
|
|
808
1018
|
return null;
|
|
809
1019
|
}
|
|
810
1020
|
const itemVar = itemParam.name.text;
|
|
811
|
-
//
|
|
812
|
-
let doCall = null;
|
|
813
|
-
if (ts.isCallExpression(mapFn.body)) {
|
|
814
|
-
doCall = mapFn.body;
|
|
815
|
-
}
|
|
816
|
-
else if (ts.isAwaitExpression(mapFn.body)) {
|
|
817
|
-
// Handle: async (email) => await workflow.do(...)
|
|
818
|
-
if (ts.isCallExpression(mapFn.body.expression)) {
|
|
819
|
-
doCall = mapFn.body.expression;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
else if (ts.isBlock(mapFn.body)) {
|
|
823
|
-
// Look for workflow.do in block
|
|
824
|
-
for (const stmt of mapFn.body.statements) {
|
|
825
|
-
if (ts.isExpressionStatement(stmt)) {
|
|
826
|
-
// Handle: await workflow.do(...)
|
|
827
|
-
if (ts.isAwaitExpression(stmt.expression) &&
|
|
828
|
-
ts.isCallExpression(stmt.expression.expression)) {
|
|
829
|
-
doCall = stmt.expression.expression;
|
|
830
|
-
break;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
else if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
834
|
-
if (ts.isCallExpression(stmt.expression)) {
|
|
835
|
-
doCall = stmt.expression;
|
|
836
|
-
break;
|
|
837
|
-
}
|
|
838
|
-
else if (ts.isAwaitExpression(stmt.expression)) {
|
|
839
|
-
// Handle: return await workflow.do(...)
|
|
840
|
-
if (ts.isCallExpression(stmt.expression.expression)) {
|
|
841
|
-
doCall = stmt.expression.expression;
|
|
842
|
-
break;
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
if (!doCall || !isWorkflowDoCall(doCall, context.checker)) {
|
|
849
|
-
return null;
|
|
850
|
-
}
|
|
851
|
-
// Create a temporary context for the child step with the loop variable
|
|
1021
|
+
// Create a temporary context for the child steps with the loop variable
|
|
852
1022
|
const childContext = {
|
|
853
1023
|
...context,
|
|
854
1024
|
outputVars: new Map(context.outputVars),
|
|
1025
|
+
arrayVars: new Set(context.arrayVars),
|
|
855
1026
|
loopVars: new Set([...context.loopVars, itemVar]),
|
|
856
1027
|
};
|
|
857
|
-
const
|
|
858
|
-
if (
|
|
1028
|
+
const body = [];
|
|
1029
|
+
if (ts.isBlock(mapFn.body)) {
|
|
1030
|
+
for (const stmt of mapFn.body.statements) {
|
|
1031
|
+
extractFanoutBodyStep(stmt, childContext, body);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
// Concise body: (item) => workflow.do(...) / async (item) => await workflow.do(...)
|
|
1036
|
+
const expr = mapFn.body;
|
|
1037
|
+
const doCall = ts.isAwaitExpression(expr)
|
|
1038
|
+
? ts.isCallExpression(expr.expression)
|
|
1039
|
+
? expr.expression
|
|
1040
|
+
: null
|
|
1041
|
+
: ts.isCallExpression(expr)
|
|
1042
|
+
? expr
|
|
1043
|
+
: null;
|
|
1044
|
+
if (doCall && isWorkflowDoCall(doCall, context.checker)) {
|
|
1045
|
+
const step = extractRpcStep(doCall, childContext);
|
|
1046
|
+
if (step) {
|
|
1047
|
+
body.push(step);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (body.length === 0) {
|
|
859
1052
|
return null;
|
|
860
1053
|
}
|
|
861
1054
|
return {
|
|
862
1055
|
type: 'fanout',
|
|
863
|
-
stepName: childStep.stepName,
|
|
864
1056
|
sourceVar,
|
|
865
1057
|
itemVar,
|
|
866
1058
|
mode: 'parallel',
|
|
867
|
-
|
|
1059
|
+
body,
|
|
868
1060
|
};
|
|
869
1061
|
}
|
|
870
1062
|
/**
|
|
@@ -904,55 +1096,28 @@ function extractSequentialFanout(statement, context) {
|
|
|
904
1096
|
return null;
|
|
905
1097
|
}
|
|
906
1098
|
const { itemVar, sourceVar } = vars;
|
|
907
|
-
// Extract child
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1099
|
+
// Extract child steps and optional sleep from the loop body. A brace-less
|
|
1100
|
+
// body (`for (const x of xs) await workflow.do(...)`) is a single statement
|
|
1101
|
+
// rather than a block, and must still be extracted.
|
|
1102
|
+
const bodyStatements = ts.isBlock(statement.statement)
|
|
1103
|
+
? statement.statement.statements
|
|
1104
|
+
: [statement.statement];
|
|
1105
|
+
const body = [];
|
|
912
1106
|
let timeBetween = undefined;
|
|
913
1107
|
// Create a child context with the loop variable added
|
|
914
1108
|
const childContext = {
|
|
915
1109
|
...context,
|
|
916
1110
|
outputVars: new Map(context.outputVars),
|
|
1111
|
+
arrayVars: new Set(context.arrayVars),
|
|
917
1112
|
loopVars: new Set([...context.loopVars, itemVar]),
|
|
918
1113
|
};
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
// Look for workflow.
|
|
922
|
-
if (ts.isVariableStatement(stmt)) {
|
|
923
|
-
const declList = stmt.declarationList;
|
|
924
|
-
if (declList.declarations.length === 1) {
|
|
925
|
-
const decl = declList.declarations[0];
|
|
926
|
-
const init = decl.initializer;
|
|
927
|
-
if (init &&
|
|
928
|
-
ts.isAwaitExpression(init) &&
|
|
929
|
-
ts.isCallExpression(init.expression)) {
|
|
930
|
-
const call = init.expression;
|
|
931
|
-
if (isWorkflowDoCall(call, context.checker)) {
|
|
932
|
-
workflowDoCount++;
|
|
933
|
-
const varName = ts.isIdentifier(decl.name)
|
|
934
|
-
? decl.name.text
|
|
935
|
-
: undefined;
|
|
936
|
-
const step = extractRpcStep(call, childContext, varName);
|
|
937
|
-
if (step) {
|
|
938
|
-
childStep = step;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
// Look for workflow.do in ExpressionStatement (await workflow.do(...))
|
|
1114
|
+
for (const stmt of bodyStatements) {
|
|
1115
|
+
extractFanoutBodyStep(stmt, childContext, body);
|
|
1116
|
+
// Look for workflow.sleep in ExpressionStatement
|
|
945
1117
|
if (ts.isExpressionStatement(stmt)) {
|
|
946
1118
|
const expr = stmt.expression;
|
|
947
1119
|
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
948
1120
|
const call = expr.expression;
|
|
949
|
-
if (isWorkflowDoCall(call, context.checker)) {
|
|
950
|
-
workflowDoCount++;
|
|
951
|
-
const step = extractRpcStep(call, childContext);
|
|
952
|
-
if (step) {
|
|
953
|
-
childStep = step;
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
1121
|
if (isWorkflowSleepCall(call, context.checker)) {
|
|
957
1122
|
// Extract duration for timeBetween
|
|
958
1123
|
const args = call.arguments;
|
|
@@ -1005,24 +1170,15 @@ function extractSequentialFanout(statement, context) {
|
|
|
1005
1170
|
}
|
|
1006
1171
|
}
|
|
1007
1172
|
}
|
|
1008
|
-
if (
|
|
1009
|
-
return null;
|
|
1010
|
-
}
|
|
1011
|
-
// If there are multiple workflow.do calls, the loop is too complex for DSL
|
|
1012
|
-
if (workflowDoCount > 1) {
|
|
1013
|
-
context.errors.push({
|
|
1014
|
-
message: `For-of loop has ${workflowDoCount} workflow.do calls but DSL only supports 1. Use pikkuWorkflowComplexFunc for complex loops.`,
|
|
1015
|
-
node: statement,
|
|
1016
|
-
});
|
|
1173
|
+
if (body.length === 0) {
|
|
1017
1174
|
return null;
|
|
1018
1175
|
}
|
|
1019
1176
|
return {
|
|
1020
1177
|
type: 'fanout',
|
|
1021
|
-
stepName: childStep.stepName,
|
|
1022
1178
|
sourceVar,
|
|
1023
1179
|
itemVar,
|
|
1024
1180
|
mode: 'sequential',
|
|
1025
|
-
|
|
1181
|
+
body,
|
|
1026
1182
|
timeBetween,
|
|
1027
1183
|
};
|
|
1028
1184
|
}
|
|
@@ -1108,10 +1264,19 @@ function extractReturn(statement, context) {
|
|
|
1108
1264
|
return extractSleepStep(call, context);
|
|
1109
1265
|
}
|
|
1110
1266
|
}
|
|
1267
|
+
// `return r` returns every field of r — record it as a spread of one.
|
|
1268
|
+
if (ts.isIdentifier(statement.expression)) {
|
|
1269
|
+
const varName = statement.expression.text;
|
|
1270
|
+
if (context.outputVars.has(varName) || context.contextVars.has(varName)) {
|
|
1271
|
+
return { type: 'return', outputs: {}, spread: [varName] };
|
|
1272
|
+
}
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1111
1275
|
if (!ts.isObjectLiteralExpression(statement.expression)) {
|
|
1112
1276
|
return null;
|
|
1113
1277
|
}
|
|
1114
1278
|
const outputs = {};
|
|
1279
|
+
const spread = [];
|
|
1115
1280
|
for (const prop of statement.expression.properties) {
|
|
1116
1281
|
if (ts.isPropertyAssignment(prop) ||
|
|
1117
1282
|
ts.isShorthandPropertyAssignment(prop)) {
|
|
@@ -1140,13 +1305,18 @@ function extractReturn(statement, context) {
|
|
|
1140
1305
|
outputs[propName] = binding;
|
|
1141
1306
|
}
|
|
1142
1307
|
}
|
|
1308
|
+
else if (ts.isSpreadAssignment(prop) &&
|
|
1309
|
+
ts.isIdentifier(prop.expression)) {
|
|
1310
|
+
spread.push(prop.expression.text);
|
|
1311
|
+
}
|
|
1143
1312
|
}
|
|
1144
|
-
if (Object.keys(outputs).length === 0) {
|
|
1313
|
+
if (Object.keys(outputs).length === 0 && spread.length === 0) {
|
|
1145
1314
|
return null;
|
|
1146
1315
|
}
|
|
1147
1316
|
return {
|
|
1148
1317
|
type: 'return',
|
|
1149
1318
|
outputs,
|
|
1319
|
+
...(spread.length > 0 ? { spread } : {}),
|
|
1150
1320
|
};
|
|
1151
1321
|
}
|
|
1152
1322
|
/**
|
|
@@ -1242,6 +1412,10 @@ function extractInputSource(node, context) {
|
|
|
1242
1412
|
if (objName === context.inputParamName) {
|
|
1243
1413
|
return { from: 'input', path: propName };
|
|
1244
1414
|
}
|
|
1415
|
+
// A field of the fanout item: users.map((u) => do(..., { id: u.id }))
|
|
1416
|
+
if (context.loopVars.has(objName)) {
|
|
1417
|
+
return { from: 'item', path: propName };
|
|
1418
|
+
}
|
|
1245
1419
|
if (context.outputVars.has(objName)) {
|
|
1246
1420
|
return { from: 'outputVar', name: objName, path: propName };
|
|
1247
1421
|
}
|
|
@@ -31,6 +31,10 @@ export declare function isWorkflowSleepCall(node: ts.CallExpression, checker: ts
|
|
|
31
31
|
* Check if a call expression is workflow.suspend()
|
|
32
32
|
*/
|
|
33
33
|
export declare function isWorkflowSuspendCall(node: ts.CallExpression, _checker: ts.TypeChecker): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Check if a call expression is workflow.approval()
|
|
36
|
+
*/
|
|
37
|
+
export declare function isWorkflowApprovalCall(node: ts.CallExpression, _checker: ts.TypeChecker): boolean;
|
|
34
38
|
/**
|
|
35
39
|
* Check if a throw statement throws WorkflowCancelledException
|
|
36
40
|
* Matches: throw new WorkflowCancelledException(...) or throw WorkflowCancelledException(...)
|
|
@@ -78,6 +78,17 @@ export function isWorkflowSuspendCall(node, _checker) {
|
|
|
78
78
|
return (propAccess.name.text === 'suspend' &&
|
|
79
79
|
isWorkflowWireIdentifier(propAccess.expression));
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if a call expression is workflow.approval()
|
|
83
|
+
*/
|
|
84
|
+
export function isWorkflowApprovalCall(node, _checker) {
|
|
85
|
+
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
const propAccess = node.expression;
|
|
89
|
+
return (propAccess.name.text === 'approval' &&
|
|
90
|
+
isWorkflowWireIdentifier(propAccess.expression));
|
|
91
|
+
}
|
|
81
92
|
/**
|
|
82
93
|
* Check if a throw statement throws WorkflowCancelledException
|
|
83
94
|
* Matches: throw new WorkflowCancelledException(...) or throw WorkflowCancelledException(...)
|
|
@@ -56,6 +56,7 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
56
56
|
const node = {
|
|
57
57
|
nodeId,
|
|
58
58
|
rpcName: step.rpcName,
|
|
59
|
+
stepName: step.stepName,
|
|
59
60
|
next: nextNodeId,
|
|
60
61
|
};
|
|
61
62
|
if (step.actor) {
|
|
@@ -82,16 +83,59 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
82
83
|
if (step.options) {
|
|
83
84
|
node.options = {
|
|
84
85
|
retries: step.options.retries,
|
|
85
|
-
retryDelay: step.options.retryDelay
|
|
86
|
+
retryDelay: step.options.retryDelay,
|
|
86
87
|
};
|
|
87
88
|
}
|
|
89
|
+
// An onError handler is written as an rpc name, but the graph routes to
|
|
90
|
+
// node ids — materialise a node for it so the console has something to
|
|
91
|
+
// draw the error edge to, exactly as in a hand-authored graph.
|
|
92
|
+
if (step.options?.onError) {
|
|
93
|
+
const handlerId = `${nodeId}_onError`;
|
|
94
|
+
node.onError = handlerId;
|
|
95
|
+
return [
|
|
96
|
+
node,
|
|
97
|
+
{
|
|
98
|
+
nodeId: handlerId,
|
|
99
|
+
rpcName: step.options.onError,
|
|
100
|
+
stepName: `${step.stepName} (on error)`,
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
}
|
|
88
104
|
return [node];
|
|
89
105
|
}
|
|
90
106
|
case 'sleep': {
|
|
91
107
|
const node = {
|
|
92
108
|
nodeId,
|
|
93
109
|
flow: 'sleep',
|
|
110
|
+
stepName: step.stepName,
|
|
94
111
|
duration: step.duration,
|
|
112
|
+
expression: step.expression,
|
|
113
|
+
next: nextNodeId,
|
|
114
|
+
};
|
|
115
|
+
return [node];
|
|
116
|
+
}
|
|
117
|
+
case 'suspend': {
|
|
118
|
+
// A suspend yields no value, but it must still be a node: the previous
|
|
119
|
+
// step's `next` points at it, so omitting it dead-ends traversal and
|
|
120
|
+
// silently deletes every step that follows.
|
|
121
|
+
const node = {
|
|
122
|
+
nodeId,
|
|
123
|
+
flow: 'suspend',
|
|
124
|
+
reason: step.reason,
|
|
125
|
+
next: nextNodeId,
|
|
126
|
+
};
|
|
127
|
+
return [node];
|
|
128
|
+
}
|
|
129
|
+
case 'approval': {
|
|
130
|
+
// Unlike `suspend` — which has no output and so is deliberately absent
|
|
131
|
+
// from the graph — an approval yields a value a later step can reference,
|
|
132
|
+
// so it must be a real node for $ref resolution and graphHash to see it.
|
|
133
|
+
const node = {
|
|
134
|
+
nodeId,
|
|
135
|
+
flow: 'approval',
|
|
136
|
+
reason: step.reason,
|
|
137
|
+
outputVar: step.outputVar,
|
|
138
|
+
expiry: step.expiry,
|
|
95
139
|
next: nextNodeId,
|
|
96
140
|
};
|
|
97
141
|
return [node];
|
|
@@ -146,13 +190,28 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
146
190
|
case 'switch': {
|
|
147
191
|
const caseNodes = [];
|
|
148
192
|
const cases = [];
|
|
193
|
+
// A case with no statements falls through to the next one. Dropping it
|
|
194
|
+
// would lose the value entirely, so it is held until an entry exists.
|
|
195
|
+
let fallingThrough = [];
|
|
149
196
|
for (let i = 0; i < step.cases.length; i++) {
|
|
150
197
|
const caseSteps = convertStepsToNodes(step.cases[i].steps, `${nodeId}_case${i}`);
|
|
151
|
-
if (caseSteps.length
|
|
198
|
+
if (caseSteps.length === 0) {
|
|
199
|
+
fallingThrough.push({
|
|
200
|
+
value: step.cases[i].value,
|
|
201
|
+
expression: step.cases[i].expression,
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
{
|
|
206
|
+
const entry = caseSteps[0].nodeId;
|
|
207
|
+
for (const pending of fallingThrough) {
|
|
208
|
+
cases.push({ ...pending, entry });
|
|
209
|
+
}
|
|
210
|
+
fallingThrough = [];
|
|
152
211
|
cases.push({
|
|
153
212
|
value: step.cases[i].value,
|
|
154
213
|
expression: step.cases[i].expression,
|
|
155
|
-
entry
|
|
214
|
+
entry,
|
|
156
215
|
});
|
|
157
216
|
// Link last case node to next (unless terminal flow)
|
|
158
217
|
if (nextNodeId) {
|
|
@@ -177,6 +236,13 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
177
236
|
caseNodes.push(...defaultNodes);
|
|
178
237
|
}
|
|
179
238
|
}
|
|
239
|
+
// Cases falling through past the last one land in default, or exit.
|
|
240
|
+
const trailingEntry = defaultEntry ?? nextNodeId;
|
|
241
|
+
if (trailingEntry) {
|
|
242
|
+
for (const pending of fallingThrough) {
|
|
243
|
+
cases.push({ ...pending, entry: trailingEntry });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
180
246
|
const node = {
|
|
181
247
|
nodeId,
|
|
182
248
|
flow: 'switch',
|
|
@@ -207,8 +273,11 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
207
273
|
return [node, ...childNodes];
|
|
208
274
|
}
|
|
209
275
|
case 'fanout': {
|
|
210
|
-
// Convert
|
|
211
|
-
const childNodes =
|
|
276
|
+
// Convert the per-iteration body into a `next`-chained sequence
|
|
277
|
+
const childNodes = [];
|
|
278
|
+
for (let i = 0; i < step.body.length; i++) {
|
|
279
|
+
childNodes.push(...convertStepToNode(step.body[i], i, step.body, `${nodeId}_item`));
|
|
280
|
+
}
|
|
212
281
|
const node = {
|
|
213
282
|
nodeId,
|
|
214
283
|
flow: 'fanout',
|
|
@@ -251,6 +320,7 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
251
320
|
nodeId,
|
|
252
321
|
flow: 'return',
|
|
253
322
|
outputs: step.outputs,
|
|
323
|
+
spread: step.spread,
|
|
254
324
|
};
|
|
255
325
|
return [node];
|
|
256
326
|
}
|
|
@@ -268,6 +338,7 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
268
338
|
flow: 'set',
|
|
269
339
|
variable: step.variable,
|
|
270
340
|
value: step.value,
|
|
341
|
+
expression: step.expression,
|
|
271
342
|
next: nextNodeId,
|
|
272
343
|
};
|
|
273
344
|
return [node];
|