@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
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
FanoutStepMeta,
|
|
9
9
|
CancelStepMeta,
|
|
10
10
|
SuspendStepMeta,
|
|
11
|
+
SleepStepMeta,
|
|
12
|
+
ApprovalStepMeta,
|
|
11
13
|
SetStepMeta,
|
|
12
14
|
SwitchStepMeta,
|
|
13
15
|
SwitchCaseMeta,
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
extractActorFromOptions,
|
|
26
28
|
isWorkflowSleepCall,
|
|
27
29
|
isWorkflowSuspendCall,
|
|
30
|
+
isWorkflowApprovalCall,
|
|
28
31
|
isThrowCancelException,
|
|
29
32
|
extractCancelReason,
|
|
30
33
|
isParallelFanout,
|
|
@@ -320,12 +323,16 @@ function extractVariableDeclaration(
|
|
|
320
323
|
): WorkflowStepMeta | null {
|
|
321
324
|
const declList = statement.declarationList
|
|
322
325
|
if (declList.declarations.length !== 1) {
|
|
326
|
+
context.errors.push({
|
|
327
|
+
message: `A single declaration statement may only declare one variable in DSL workflows. Split '${statement.getText().slice(0, 60)}' into separate statements.`,
|
|
328
|
+
node: statement,
|
|
329
|
+
})
|
|
323
330
|
return null
|
|
324
331
|
}
|
|
325
332
|
|
|
326
333
|
const decl = declList.declarations[0]
|
|
327
334
|
if (!ts.isIdentifier(decl.name)) {
|
|
328
|
-
return
|
|
335
|
+
return extractDestructuredDeclaration(statement, decl, context)
|
|
329
336
|
}
|
|
330
337
|
|
|
331
338
|
const varName = decl.name.text
|
|
@@ -435,6 +442,70 @@ function extractVariableDeclaration(
|
|
|
435
442
|
return null
|
|
436
443
|
}
|
|
437
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Extract a declaration whose binding is a destructuring pattern.
|
|
447
|
+
*
|
|
448
|
+
* `const [a, b] = await Promise.all([...])` is the idiomatic way to run steps
|
|
449
|
+
* in parallel and keep both results, so each element of the pattern is bound to
|
|
450
|
+
* the matching child step's output. Every other destructuring shape reports a
|
|
451
|
+
* diagnostic rather than silently dropping the step.
|
|
452
|
+
*/
|
|
453
|
+
function extractDestructuredDeclaration(
|
|
454
|
+
statement: ts.VariableStatement,
|
|
455
|
+
decl: ts.VariableDeclaration,
|
|
456
|
+
context: ExtractionContext
|
|
457
|
+
): WorkflowStepMeta | null {
|
|
458
|
+
const init = decl.initializer
|
|
459
|
+
|
|
460
|
+
if (
|
|
461
|
+
init &&
|
|
462
|
+
ts.isAwaitExpression(init) &&
|
|
463
|
+
ts.isCallExpression(init.expression) &&
|
|
464
|
+
isParallelGroup(init.expression) &&
|
|
465
|
+
ts.isArrayBindingPattern(decl.name)
|
|
466
|
+
) {
|
|
467
|
+
const elements = decl.name.elements
|
|
468
|
+
const allSimple = elements.every(
|
|
469
|
+
(el) =>
|
|
470
|
+
ts.isBindingElement(el) &&
|
|
471
|
+
ts.isIdentifier(el.name) &&
|
|
472
|
+
!el.dotDotDotToken
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if (allSimple) {
|
|
476
|
+
const step = extractParallelGroup(init.expression, context)
|
|
477
|
+
if (step) {
|
|
478
|
+
if (elements.length !== step.children.length) {
|
|
479
|
+
context.errors.push({
|
|
480
|
+
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.`,
|
|
481
|
+
node: statement,
|
|
482
|
+
})
|
|
483
|
+
return null
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
elements.forEach((el, i) => {
|
|
487
|
+
const name = (el as ts.BindingElement).name as ts.Identifier
|
|
488
|
+
const varName = name.text
|
|
489
|
+
step.children[i].outputVar = varName
|
|
490
|
+
const type = context.checker.getTypeAtLocation(name)
|
|
491
|
+
context.outputVars.set(varName, { type, node: name })
|
|
492
|
+
if (isArrayType(type, context.checker)) {
|
|
493
|
+
context.arrayVars.add(varName)
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
return step
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
context.errors.push({
|
|
503
|
+
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.`,
|
|
504
|
+
node: statement,
|
|
505
|
+
})
|
|
506
|
+
return null
|
|
507
|
+
}
|
|
508
|
+
|
|
438
509
|
/**
|
|
439
510
|
* Extract expression statement (await workflow.do(...) without assignment)
|
|
440
511
|
*/
|
|
@@ -476,11 +547,11 @@ function extractExpressionStatement(
|
|
|
476
547
|
value: literalValue,
|
|
477
548
|
} as SetStepMeta
|
|
478
549
|
}
|
|
479
|
-
// Non-literal assignment to context var -
|
|
550
|
+
// Non-literal assignment to context var - keep the source expression
|
|
480
551
|
return {
|
|
481
552
|
type: 'set',
|
|
482
553
|
variable: outputVar,
|
|
483
|
-
|
|
554
|
+
expression: getSourceText(expr.right),
|
|
484
555
|
} as SetStepMeta
|
|
485
556
|
}
|
|
486
557
|
}
|
|
@@ -520,6 +591,22 @@ function extractExpressionStatement(
|
|
|
520
591
|
return extractSuspendStep(call, context)
|
|
521
592
|
}
|
|
522
593
|
|
|
594
|
+
if (isWorkflowApprovalCall(call, context.checker)) {
|
|
595
|
+
const step = extractApprovalStep(call, context, outputVar)
|
|
596
|
+
|
|
597
|
+
// Unlike suspend, an approval yields a value, so a downstream step can
|
|
598
|
+
// reference it — track the binding the same way workflow.do() does.
|
|
599
|
+
if (outputVar && step) {
|
|
600
|
+
const type = context.checker.getTypeAtLocation(expr)
|
|
601
|
+
context.outputVars.set(outputVar, { type, node: expr })
|
|
602
|
+
if (isArrayType(type, context.checker)) {
|
|
603
|
+
context.arrayVars.add(outputVar)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return step
|
|
608
|
+
}
|
|
609
|
+
|
|
523
610
|
// Check for parallel group or fanout
|
|
524
611
|
if (isParallelFanout(call)) {
|
|
525
612
|
return extractParallelFanout(call, context)
|
|
@@ -659,6 +746,16 @@ function extractStepOptions(
|
|
|
659
746
|
} catch {
|
|
660
747
|
// Ignore extraction errors for retryDelay
|
|
661
748
|
}
|
|
749
|
+
} else if (propName === 'onError') {
|
|
750
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
751
|
+
options.onError = prop.initializer.text
|
|
752
|
+
} else {
|
|
753
|
+
context.errors.push({
|
|
754
|
+
message:
|
|
755
|
+
'onError must be a literal RPC name so it can be wired and drawn in the graph.',
|
|
756
|
+
node: prop.initializer,
|
|
757
|
+
})
|
|
758
|
+
}
|
|
662
759
|
} else if (propName === 'description') {
|
|
663
760
|
try {
|
|
664
761
|
options.description = extractStringLiteral(
|
|
@@ -696,7 +793,19 @@ function extractSleepStep(
|
|
|
696
793
|
if (numValue !== null) {
|
|
697
794
|
duration = numValue
|
|
698
795
|
} else {
|
|
699
|
-
|
|
796
|
+
try {
|
|
797
|
+
duration = extractStringLiteral(args[1], context.checker)
|
|
798
|
+
} catch {
|
|
799
|
+
// A duration computed at runtime (a loop variable, a field off the
|
|
800
|
+
// input) is legal: the closure evaluates it. Record the source text so
|
|
801
|
+
// the graph can show what it waits on, rather than failing the workflow.
|
|
802
|
+
return {
|
|
803
|
+
type: 'sleep',
|
|
804
|
+
stepName,
|
|
805
|
+
duration: '',
|
|
806
|
+
expression: args[1].getText(),
|
|
807
|
+
}
|
|
808
|
+
}
|
|
700
809
|
}
|
|
701
810
|
|
|
702
811
|
return {
|
|
@@ -738,6 +847,54 @@ function extractSuspendStep(
|
|
|
738
847
|
}
|
|
739
848
|
}
|
|
740
849
|
|
|
850
|
+
/**
|
|
851
|
+
* Extract approval step from workflow.approval() call
|
|
852
|
+
*/
|
|
853
|
+
function extractApprovalStep(
|
|
854
|
+
call: ts.CallExpression,
|
|
855
|
+
context: ExtractionContext,
|
|
856
|
+
outputVar?: string
|
|
857
|
+
): ApprovalStepMeta | null {
|
|
858
|
+
const args = call.arguments
|
|
859
|
+
if (args.length < 1) return null
|
|
860
|
+
|
|
861
|
+
try {
|
|
862
|
+
const reason = extractStringLiteral(args[0], context.checker)
|
|
863
|
+
const step: ApprovalStepMeta = {
|
|
864
|
+
type: 'approval',
|
|
865
|
+
reason,
|
|
866
|
+
}
|
|
867
|
+
if (outputVar) {
|
|
868
|
+
step.outputVar = outputVar
|
|
869
|
+
}
|
|
870
|
+
// The `schema` option is a runtime value validated inside the workflow body,
|
|
871
|
+
// so it is deliberately not serialized here — only `expiry`, which the graph
|
|
872
|
+
// and planned-step ladder need in order to describe the gate.
|
|
873
|
+
const options = args[1]
|
|
874
|
+
if (options && ts.isObjectLiteralExpression(options)) {
|
|
875
|
+
for (const prop of options.properties) {
|
|
876
|
+
if (
|
|
877
|
+
ts.isPropertyAssignment(prop) &&
|
|
878
|
+
prop.name.getText() === 'expiry' &&
|
|
879
|
+
(ts.isStringLiteral(prop.initializer) ||
|
|
880
|
+
ts.isNumericLiteral(prop.initializer))
|
|
881
|
+
) {
|
|
882
|
+
step.expiry = ts.isNumericLiteral(prop.initializer)
|
|
883
|
+
? Number(prop.initializer.text)
|
|
884
|
+
: prop.initializer.text
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return step
|
|
889
|
+
} catch (error) {
|
|
890
|
+
context.errors.push({
|
|
891
|
+
message: `Failed to extract approval step: ${error instanceof Error ? error.message : String(error)}`,
|
|
892
|
+
node: call,
|
|
893
|
+
})
|
|
894
|
+
return null
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
741
898
|
/**
|
|
742
899
|
* Extract cancel step from throw WorkflowCancelledException statement
|
|
743
900
|
*/
|
|
@@ -1037,6 +1194,92 @@ function extractArrayPredicate(
|
|
|
1037
1194
|
}
|
|
1038
1195
|
}
|
|
1039
1196
|
|
|
1197
|
+
/**
|
|
1198
|
+
* Extract an ordered list of workflow.do steps from a per-iteration body.
|
|
1199
|
+
*
|
|
1200
|
+
* Unlike if/switch blocks, a fanout body is a real per-iteration scope, so
|
|
1201
|
+
* `const x = await workflow.do(...)` is meaningful here: the binding is
|
|
1202
|
+
* registered on the child context so later steps in the same iteration can
|
|
1203
|
+
* reference it.
|
|
1204
|
+
*/
|
|
1205
|
+
function extractFanoutBodyStep(
|
|
1206
|
+
stmt: ts.Statement,
|
|
1207
|
+
childContext: ExtractionContext,
|
|
1208
|
+
body: Array<RpcStepMeta | SleepStepMeta | SuspendStepMeta>
|
|
1209
|
+
): void {
|
|
1210
|
+
if (ts.isVariableStatement(stmt)) {
|
|
1211
|
+
const declList = stmt.declarationList
|
|
1212
|
+
if (declList.declarations.length !== 1) {
|
|
1213
|
+
return
|
|
1214
|
+
}
|
|
1215
|
+
const decl = declList.declarations[0]
|
|
1216
|
+
const init = decl.initializer
|
|
1217
|
+
if (
|
|
1218
|
+
!init ||
|
|
1219
|
+
!ts.isAwaitExpression(init) ||
|
|
1220
|
+
!ts.isCallExpression(init.expression)
|
|
1221
|
+
) {
|
|
1222
|
+
return
|
|
1223
|
+
}
|
|
1224
|
+
const call = init.expression
|
|
1225
|
+
if (!isWorkflowDoCall(call, childContext.checker)) {
|
|
1226
|
+
return
|
|
1227
|
+
}
|
|
1228
|
+
const varName = ts.isIdentifier(decl.name) ? decl.name.text : undefined
|
|
1229
|
+
const step = extractRpcStep(call, childContext, varName)
|
|
1230
|
+
if (!step) {
|
|
1231
|
+
return
|
|
1232
|
+
}
|
|
1233
|
+
if (varName) {
|
|
1234
|
+
const type = childContext.checker.getTypeAtLocation(decl)
|
|
1235
|
+
childContext.outputVars.set(varName, { type, node: decl })
|
|
1236
|
+
if (isArrayType(type, childContext.checker)) {
|
|
1237
|
+
childContext.arrayVars.add(varName)
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
body.push(step)
|
|
1241
|
+
return
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (ts.isExpressionStatement(stmt) || ts.isReturnStatement(stmt)) {
|
|
1245
|
+
const expr = ts.isReturnStatement(stmt) ? stmt.expression : stmt.expression
|
|
1246
|
+
if (!expr) {
|
|
1247
|
+
return
|
|
1248
|
+
}
|
|
1249
|
+
const call = ts.isAwaitExpression(expr)
|
|
1250
|
+
? ts.isCallExpression(expr.expression)
|
|
1251
|
+
? expr.expression
|
|
1252
|
+
: null
|
|
1253
|
+
: ts.isCallExpression(expr)
|
|
1254
|
+
? expr
|
|
1255
|
+
: null
|
|
1256
|
+
if (!call) {
|
|
1257
|
+
return
|
|
1258
|
+
}
|
|
1259
|
+
if (isWorkflowSleepCall(call, childContext.checker)) {
|
|
1260
|
+
const sleepStep = extractSleepStep(call, childContext)
|
|
1261
|
+
if (sleepStep && sleepStep.type === 'sleep') {
|
|
1262
|
+
body.push(sleepStep)
|
|
1263
|
+
}
|
|
1264
|
+
return
|
|
1265
|
+
}
|
|
1266
|
+
if (isWorkflowSuspendCall(call, childContext.checker)) {
|
|
1267
|
+
const suspendStep = extractSuspendStep(call, childContext)
|
|
1268
|
+
if (suspendStep && suspendStep.type === 'suspend') {
|
|
1269
|
+
body.push(suspendStep)
|
|
1270
|
+
}
|
|
1271
|
+
return
|
|
1272
|
+
}
|
|
1273
|
+
if (!isWorkflowDoCall(call, childContext.checker)) {
|
|
1274
|
+
return
|
|
1275
|
+
}
|
|
1276
|
+
const step = extractRpcStep(call, childContext)
|
|
1277
|
+
if (step) {
|
|
1278
|
+
body.push(step)
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1040
1283
|
/**
|
|
1041
1284
|
* Extract parallel fanout from Promise.all(array.map(...))
|
|
1042
1285
|
*/
|
|
@@ -1075,66 +1318,49 @@ function extractParallelFanout(
|
|
|
1075
1318
|
|
|
1076
1319
|
const itemVar = itemParam.name.text
|
|
1077
1320
|
|
|
1078
|
-
//
|
|
1079
|
-
let doCall: ts.CallExpression | null = null
|
|
1080
|
-
|
|
1081
|
-
if (ts.isCallExpression(mapFn.body)) {
|
|
1082
|
-
doCall = mapFn.body
|
|
1083
|
-
} else if (ts.isAwaitExpression(mapFn.body)) {
|
|
1084
|
-
// Handle: async (email) => await workflow.do(...)
|
|
1085
|
-
if (ts.isCallExpression(mapFn.body.expression)) {
|
|
1086
|
-
doCall = mapFn.body.expression
|
|
1087
|
-
}
|
|
1088
|
-
} else if (ts.isBlock(mapFn.body)) {
|
|
1089
|
-
// Look for workflow.do in block
|
|
1090
|
-
for (const stmt of mapFn.body.statements) {
|
|
1091
|
-
if (ts.isExpressionStatement(stmt)) {
|
|
1092
|
-
// Handle: await workflow.do(...)
|
|
1093
|
-
if (
|
|
1094
|
-
ts.isAwaitExpression(stmt.expression) &&
|
|
1095
|
-
ts.isCallExpression(stmt.expression.expression)
|
|
1096
|
-
) {
|
|
1097
|
-
doCall = stmt.expression.expression
|
|
1098
|
-
break
|
|
1099
|
-
}
|
|
1100
|
-
} else if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
1101
|
-
if (ts.isCallExpression(stmt.expression)) {
|
|
1102
|
-
doCall = stmt.expression
|
|
1103
|
-
break
|
|
1104
|
-
} else if (ts.isAwaitExpression(stmt.expression)) {
|
|
1105
|
-
// Handle: return await workflow.do(...)
|
|
1106
|
-
if (ts.isCallExpression(stmt.expression.expression)) {
|
|
1107
|
-
doCall = stmt.expression.expression
|
|
1108
|
-
break
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
if (!doCall || !isWorkflowDoCall(doCall, context.checker)) {
|
|
1116
|
-
return null
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
// Create a temporary context for the child step with the loop variable
|
|
1321
|
+
// Create a temporary context for the child steps with the loop variable
|
|
1120
1322
|
const childContext: ExtractionContext = {
|
|
1121
1323
|
...context,
|
|
1122
1324
|
outputVars: new Map(context.outputVars),
|
|
1325
|
+
arrayVars: new Set(context.arrayVars),
|
|
1123
1326
|
loopVars: new Set([...context.loopVars, itemVar]),
|
|
1124
1327
|
}
|
|
1125
1328
|
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1329
|
+
const body: FanoutStepMeta['body'] = []
|
|
1330
|
+
|
|
1331
|
+
if (ts.isBlock(mapFn.body)) {
|
|
1332
|
+
for (const stmt of mapFn.body.statements) {
|
|
1333
|
+
extractFanoutBodyStep(stmt, childContext, body)
|
|
1334
|
+
}
|
|
1335
|
+
} else {
|
|
1336
|
+
// Concise body: (item) => workflow.do(...) / async (item) => await workflow.do(...)
|
|
1337
|
+
const expr = mapFn.body
|
|
1338
|
+
const doCall = ts.isAwaitExpression(expr)
|
|
1339
|
+
? ts.isCallExpression(expr.expression)
|
|
1340
|
+
? expr.expression
|
|
1341
|
+
: null
|
|
1342
|
+
: ts.isCallExpression(expr)
|
|
1343
|
+
? expr
|
|
1344
|
+
: null
|
|
1345
|
+
|
|
1346
|
+
if (doCall && isWorkflowDoCall(doCall, context.checker)) {
|
|
1347
|
+
const step = extractRpcStep(doCall, childContext)
|
|
1348
|
+
if (step) {
|
|
1349
|
+
body.push(step)
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (body.length === 0) {
|
|
1128
1355
|
return null
|
|
1129
1356
|
}
|
|
1130
1357
|
|
|
1131
1358
|
return {
|
|
1132
1359
|
type: 'fanout',
|
|
1133
|
-
stepName: childStep.stepName,
|
|
1134
1360
|
sourceVar,
|
|
1135
1361
|
itemVar,
|
|
1136
1362
|
mode: 'parallel',
|
|
1137
|
-
|
|
1363
|
+
body,
|
|
1138
1364
|
}
|
|
1139
1365
|
}
|
|
1140
1366
|
|
|
@@ -1189,65 +1415,34 @@ function extractSequentialFanout(
|
|
|
1189
1415
|
|
|
1190
1416
|
const { itemVar, sourceVar } = vars
|
|
1191
1417
|
|
|
1192
|
-
// Extract child
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1418
|
+
// Extract child steps and optional sleep from the loop body. A brace-less
|
|
1419
|
+
// body (`for (const x of xs) await workflow.do(...)`) is a single statement
|
|
1420
|
+
// rather than a block, and must still be extracted.
|
|
1421
|
+
const bodyStatements = ts.isBlock(statement.statement)
|
|
1422
|
+
? statement.statement.statements
|
|
1423
|
+
: [statement.statement]
|
|
1196
1424
|
|
|
1197
|
-
|
|
1425
|
+
const body: FanoutStepMeta['body'] = []
|
|
1198
1426
|
let timeBetween: string | undefined = undefined
|
|
1199
1427
|
|
|
1200
1428
|
// Create a child context with the loop variable added
|
|
1201
1429
|
const childContext: ExtractionContext = {
|
|
1202
1430
|
...context,
|
|
1203
1431
|
outputVars: new Map(context.outputVars),
|
|
1432
|
+
arrayVars: new Set(context.arrayVars),
|
|
1204
1433
|
loopVars: new Set([...context.loopVars, itemVar]),
|
|
1205
1434
|
}
|
|
1206
1435
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
for (const stmt of statement.statement.statements) {
|
|
1210
|
-
// Look for workflow.do in VariableStatement (const x = await workflow.do(...))
|
|
1211
|
-
if (ts.isVariableStatement(stmt)) {
|
|
1212
|
-
const declList = stmt.declarationList
|
|
1213
|
-
if (declList.declarations.length === 1) {
|
|
1214
|
-
const decl = declList.declarations[0]
|
|
1215
|
-
const init = decl.initializer
|
|
1216
|
-
if (
|
|
1217
|
-
init &&
|
|
1218
|
-
ts.isAwaitExpression(init) &&
|
|
1219
|
-
ts.isCallExpression(init.expression)
|
|
1220
|
-
) {
|
|
1221
|
-
const call = init.expression
|
|
1222
|
-
if (isWorkflowDoCall(call, context.checker)) {
|
|
1223
|
-
workflowDoCount++
|
|
1224
|
-
const varName = ts.isIdentifier(decl.name)
|
|
1225
|
-
? decl.name.text
|
|
1226
|
-
: undefined
|
|
1227
|
-
const step = extractRpcStep(call, childContext, varName)
|
|
1228
|
-
if (step) {
|
|
1229
|
-
childStep = step
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1436
|
+
for (const stmt of bodyStatements) {
|
|
1437
|
+
extractFanoutBodyStep(stmt, childContext, body)
|
|
1235
1438
|
|
|
1236
|
-
// Look for workflow.
|
|
1439
|
+
// Look for workflow.sleep in ExpressionStatement
|
|
1237
1440
|
if (ts.isExpressionStatement(stmt)) {
|
|
1238
1441
|
const expr = stmt.expression
|
|
1239
1442
|
|
|
1240
1443
|
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
1241
1444
|
const call = expr.expression
|
|
1242
1445
|
|
|
1243
|
-
if (isWorkflowDoCall(call, context.checker)) {
|
|
1244
|
-
workflowDoCount++
|
|
1245
|
-
const step = extractRpcStep(call, childContext)
|
|
1246
|
-
if (step) {
|
|
1247
|
-
childStep = step
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
1446
|
if (isWorkflowSleepCall(call, context.checker)) {
|
|
1252
1447
|
// Extract duration for timeBetween
|
|
1253
1448
|
const args = call.arguments
|
|
@@ -1305,26 +1500,16 @@ function extractSequentialFanout(
|
|
|
1305
1500
|
}
|
|
1306
1501
|
}
|
|
1307
1502
|
|
|
1308
|
-
if (
|
|
1309
|
-
return null
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// If there are multiple workflow.do calls, the loop is too complex for DSL
|
|
1313
|
-
if (workflowDoCount > 1) {
|
|
1314
|
-
context.errors.push({
|
|
1315
|
-
message: `For-of loop has ${workflowDoCount} workflow.do calls but DSL only supports 1. Use pikkuWorkflowComplexFunc for complex loops.`,
|
|
1316
|
-
node: statement,
|
|
1317
|
-
})
|
|
1503
|
+
if (body.length === 0) {
|
|
1318
1504
|
return null
|
|
1319
1505
|
}
|
|
1320
1506
|
|
|
1321
1507
|
return {
|
|
1322
1508
|
type: 'fanout',
|
|
1323
|
-
stepName: childStep.stepName,
|
|
1324
1509
|
sourceVar,
|
|
1325
1510
|
itemVar,
|
|
1326
1511
|
mode: 'sequential',
|
|
1327
|
-
|
|
1512
|
+
body,
|
|
1328
1513
|
timeBetween,
|
|
1329
1514
|
}
|
|
1330
1515
|
}
|
|
@@ -1427,11 +1612,21 @@ function extractReturn(
|
|
|
1427
1612
|
}
|
|
1428
1613
|
}
|
|
1429
1614
|
|
|
1615
|
+
// `return r` returns every field of r — record it as a spread of one.
|
|
1616
|
+
if (ts.isIdentifier(statement.expression)) {
|
|
1617
|
+
const varName = statement.expression.text
|
|
1618
|
+
if (context.outputVars.has(varName) || context.contextVars.has(varName)) {
|
|
1619
|
+
return { type: 'return', outputs: {}, spread: [varName] }
|
|
1620
|
+
}
|
|
1621
|
+
return null
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1430
1624
|
if (!ts.isObjectLiteralExpression(statement.expression)) {
|
|
1431
1625
|
return null
|
|
1432
1626
|
}
|
|
1433
1627
|
|
|
1434
1628
|
const outputs: Record<string, OutputBinding> = {}
|
|
1629
|
+
const spread: string[] = []
|
|
1435
1630
|
|
|
1436
1631
|
for (const prop of statement.expression.properties) {
|
|
1437
1632
|
if (
|
|
@@ -1462,16 +1657,22 @@ function extractReturn(
|
|
|
1462
1657
|
if (binding) {
|
|
1463
1658
|
outputs[propName] = binding
|
|
1464
1659
|
}
|
|
1660
|
+
} else if (
|
|
1661
|
+
ts.isSpreadAssignment(prop) &&
|
|
1662
|
+
ts.isIdentifier(prop.expression)
|
|
1663
|
+
) {
|
|
1664
|
+
spread.push(prop.expression.text)
|
|
1465
1665
|
}
|
|
1466
1666
|
}
|
|
1467
1667
|
|
|
1468
|
-
if (Object.keys(outputs).length === 0) {
|
|
1668
|
+
if (Object.keys(outputs).length === 0 && spread.length === 0) {
|
|
1469
1669
|
return null
|
|
1470
1670
|
}
|
|
1471
1671
|
|
|
1472
1672
|
return {
|
|
1473
1673
|
type: 'return',
|
|
1474
1674
|
outputs,
|
|
1675
|
+
...(spread.length > 0 ? { spread } : {}),
|
|
1475
1676
|
}
|
|
1476
1677
|
}
|
|
1477
1678
|
|
|
@@ -1586,6 +1787,11 @@ function extractInputSource(
|
|
|
1586
1787
|
return { from: 'input', path: propName }
|
|
1587
1788
|
}
|
|
1588
1789
|
|
|
1790
|
+
// A field of the fanout item: users.map((u) => do(..., { id: u.id }))
|
|
1791
|
+
if (context.loopVars.has(objName)) {
|
|
1792
|
+
return { from: 'item', path: propName }
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1589
1795
|
if (context.outputVars.has(objName)) {
|
|
1590
1796
|
return { from: 'outputVar', name: objName, path: propName }
|
|
1591
1797
|
}
|
|
@@ -115,6 +115,24 @@ export function isWorkflowSuspendCall(
|
|
|
115
115
|
)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Check if a call expression is workflow.approval()
|
|
120
|
+
*/
|
|
121
|
+
export function isWorkflowApprovalCall(
|
|
122
|
+
node: ts.CallExpression,
|
|
123
|
+
_checker: ts.TypeChecker
|
|
124
|
+
): boolean {
|
|
125
|
+
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const propAccess = node.expression
|
|
130
|
+
return (
|
|
131
|
+
propAccess.name.text === 'approval' &&
|
|
132
|
+
isWorkflowWireIdentifier(propAccess.expression)
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
118
136
|
/**
|
|
119
137
|
* Check if a throw statement throws WorkflowCancelledException
|
|
120
138
|
* Matches: throw new WorkflowCancelledException(...) or throw WorkflowCancelledException(...)
|