@pikku/inspector 0.12.43 → 0.12.45
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 +261 -0
- package/dist/add/add-credential.js +2 -0
- package/dist/add/add-keyed-wiring.js +2 -0
- package/dist/add/add-workflow.js +9 -4
- package/dist/utils/schema-generator.js +19 -4
- package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +373 -91
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +218 -99
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +62 -5
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +3 -3
- package/package.json +3 -3
- package/run-tests.sh +1 -1
- package/src/add/add-auth.test.ts +4 -4
- package/src/add/add-credential.ts +2 -0
- package/src/add/add-keyed-wiring.ts +2 -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-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/utils/schema-generator.test.ts +164 -0
- package/src/utils/schema-generator.ts +26 -3
- 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 +245 -105
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +67 -10
- package/src/utils/workflow/graph/fanout-node-identity.test.ts +105 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +3 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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
|
}
|
|
@@ -490,6 +543,17 @@ function extractStepOptions(optionsNode, context) {
|
|
|
490
543
|
// Ignore extraction errors for retryDelay
|
|
491
544
|
}
|
|
492
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
|
+
}
|
|
493
557
|
else if (propName === 'description') {
|
|
494
558
|
try {
|
|
495
559
|
options.description = extractStringLiteral(prop.initializer, context.checker);
|
|
@@ -518,7 +582,20 @@ function extractSleepStep(call, context) {
|
|
|
518
582
|
duration = numValue;
|
|
519
583
|
}
|
|
520
584
|
else {
|
|
521
|
-
|
|
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
|
+
}
|
|
522
599
|
}
|
|
523
600
|
return {
|
|
524
601
|
type: 'sleep',
|
|
@@ -835,6 +912,84 @@ function extractArrayPredicate(call, context, outputVar) {
|
|
|
835
912
|
outputVar,
|
|
836
913
|
};
|
|
837
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
|
+
}
|
|
838
993
|
/**
|
|
839
994
|
* Extract parallel fanout from Promise.all(array.map(...))
|
|
840
995
|
*/
|
|
@@ -863,63 +1018,45 @@ function extractParallelFanout(call, context) {
|
|
|
863
1018
|
return null;
|
|
864
1019
|
}
|
|
865
1020
|
const itemVar = itemParam.name.text;
|
|
866
|
-
//
|
|
867
|
-
let doCall = null;
|
|
868
|
-
if (ts.isCallExpression(mapFn.body)) {
|
|
869
|
-
doCall = mapFn.body;
|
|
870
|
-
}
|
|
871
|
-
else if (ts.isAwaitExpression(mapFn.body)) {
|
|
872
|
-
// Handle: async (email) => await workflow.do(...)
|
|
873
|
-
if (ts.isCallExpression(mapFn.body.expression)) {
|
|
874
|
-
doCall = mapFn.body.expression;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
else if (ts.isBlock(mapFn.body)) {
|
|
878
|
-
// Look for workflow.do in block
|
|
879
|
-
for (const stmt of mapFn.body.statements) {
|
|
880
|
-
if (ts.isExpressionStatement(stmt)) {
|
|
881
|
-
// Handle: await workflow.do(...)
|
|
882
|
-
if (ts.isAwaitExpression(stmt.expression) &&
|
|
883
|
-
ts.isCallExpression(stmt.expression.expression)) {
|
|
884
|
-
doCall = stmt.expression.expression;
|
|
885
|
-
break;
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
else if (ts.isReturnStatement(stmt) && stmt.expression) {
|
|
889
|
-
if (ts.isCallExpression(stmt.expression)) {
|
|
890
|
-
doCall = stmt.expression;
|
|
891
|
-
break;
|
|
892
|
-
}
|
|
893
|
-
else if (ts.isAwaitExpression(stmt.expression)) {
|
|
894
|
-
// Handle: return await workflow.do(...)
|
|
895
|
-
if (ts.isCallExpression(stmt.expression.expression)) {
|
|
896
|
-
doCall = stmt.expression.expression;
|
|
897
|
-
break;
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
if (!doCall || !isWorkflowDoCall(doCall, context.checker)) {
|
|
904
|
-
return null;
|
|
905
|
-
}
|
|
906
|
-
// 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
|
|
907
1022
|
const childContext = {
|
|
908
1023
|
...context,
|
|
909
1024
|
outputVars: new Map(context.outputVars),
|
|
1025
|
+
arrayVars: new Set(context.arrayVars),
|
|
910
1026
|
loopVars: new Set([...context.loopVars, itemVar]),
|
|
911
1027
|
};
|
|
912
|
-
const
|
|
913
|
-
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) {
|
|
914
1052
|
return null;
|
|
915
1053
|
}
|
|
916
1054
|
return {
|
|
917
1055
|
type: 'fanout',
|
|
918
|
-
stepName: childStep.stepName,
|
|
919
1056
|
sourceVar,
|
|
920
1057
|
itemVar,
|
|
921
1058
|
mode: 'parallel',
|
|
922
|
-
|
|
1059
|
+
body,
|
|
923
1060
|
};
|
|
924
1061
|
}
|
|
925
1062
|
/**
|
|
@@ -959,55 +1096,28 @@ function extractSequentialFanout(statement, context) {
|
|
|
959
1096
|
return null;
|
|
960
1097
|
}
|
|
961
1098
|
const { itemVar, sourceVar } = vars;
|
|
962
|
-
// Extract child
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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 = [];
|
|
967
1106
|
let timeBetween = undefined;
|
|
968
1107
|
// Create a child context with the loop variable added
|
|
969
1108
|
const childContext = {
|
|
970
1109
|
...context,
|
|
971
1110
|
outputVars: new Map(context.outputVars),
|
|
1111
|
+
arrayVars: new Set(context.arrayVars),
|
|
972
1112
|
loopVars: new Set([...context.loopVars, itemVar]),
|
|
973
1113
|
};
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
// Look for workflow.
|
|
977
|
-
if (ts.isVariableStatement(stmt)) {
|
|
978
|
-
const declList = stmt.declarationList;
|
|
979
|
-
if (declList.declarations.length === 1) {
|
|
980
|
-
const decl = declList.declarations[0];
|
|
981
|
-
const init = decl.initializer;
|
|
982
|
-
if (init &&
|
|
983
|
-
ts.isAwaitExpression(init) &&
|
|
984
|
-
ts.isCallExpression(init.expression)) {
|
|
985
|
-
const call = init.expression;
|
|
986
|
-
if (isWorkflowDoCall(call, context.checker)) {
|
|
987
|
-
workflowDoCount++;
|
|
988
|
-
const varName = ts.isIdentifier(decl.name)
|
|
989
|
-
? decl.name.text
|
|
990
|
-
: undefined;
|
|
991
|
-
const step = extractRpcStep(call, childContext, varName);
|
|
992
|
-
if (step) {
|
|
993
|
-
childStep = step;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
// 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
|
|
1000
1117
|
if (ts.isExpressionStatement(stmt)) {
|
|
1001
1118
|
const expr = stmt.expression;
|
|
1002
1119
|
if (ts.isAwaitExpression(expr) && ts.isCallExpression(expr.expression)) {
|
|
1003
1120
|
const call = expr.expression;
|
|
1004
|
-
if (isWorkflowDoCall(call, context.checker)) {
|
|
1005
|
-
workflowDoCount++;
|
|
1006
|
-
const step = extractRpcStep(call, childContext);
|
|
1007
|
-
if (step) {
|
|
1008
|
-
childStep = step;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
1121
|
if (isWorkflowSleepCall(call, context.checker)) {
|
|
1012
1122
|
// Extract duration for timeBetween
|
|
1013
1123
|
const args = call.arguments;
|
|
@@ -1060,24 +1170,15 @@ function extractSequentialFanout(statement, context) {
|
|
|
1060
1170
|
}
|
|
1061
1171
|
}
|
|
1062
1172
|
}
|
|
1063
|
-
if (
|
|
1064
|
-
return null;
|
|
1065
|
-
}
|
|
1066
|
-
// If there are multiple workflow.do calls, the loop is too complex for DSL
|
|
1067
|
-
if (workflowDoCount > 1) {
|
|
1068
|
-
context.errors.push({
|
|
1069
|
-
message: `For-of loop has ${workflowDoCount} workflow.do calls but DSL only supports 1. Use pikkuWorkflowComplexFunc for complex loops.`,
|
|
1070
|
-
node: statement,
|
|
1071
|
-
});
|
|
1173
|
+
if (body.length === 0) {
|
|
1072
1174
|
return null;
|
|
1073
1175
|
}
|
|
1074
1176
|
return {
|
|
1075
1177
|
type: 'fanout',
|
|
1076
|
-
stepName: childStep.stepName,
|
|
1077
1178
|
sourceVar,
|
|
1078
1179
|
itemVar,
|
|
1079
1180
|
mode: 'sequential',
|
|
1080
|
-
|
|
1181
|
+
body,
|
|
1081
1182
|
timeBetween,
|
|
1082
1183
|
};
|
|
1083
1184
|
}
|
|
@@ -1163,10 +1264,19 @@ function extractReturn(statement, context) {
|
|
|
1163
1264
|
return extractSleepStep(call, context);
|
|
1164
1265
|
}
|
|
1165
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
|
+
}
|
|
1166
1275
|
if (!ts.isObjectLiteralExpression(statement.expression)) {
|
|
1167
1276
|
return null;
|
|
1168
1277
|
}
|
|
1169
1278
|
const outputs = {};
|
|
1279
|
+
const spread = [];
|
|
1170
1280
|
for (const prop of statement.expression.properties) {
|
|
1171
1281
|
if (ts.isPropertyAssignment(prop) ||
|
|
1172
1282
|
ts.isShorthandPropertyAssignment(prop)) {
|
|
@@ -1195,13 +1305,18 @@ function extractReturn(statement, context) {
|
|
|
1195
1305
|
outputs[propName] = binding;
|
|
1196
1306
|
}
|
|
1197
1307
|
}
|
|
1308
|
+
else if (ts.isSpreadAssignment(prop) &&
|
|
1309
|
+
ts.isIdentifier(prop.expression)) {
|
|
1310
|
+
spread.push(prop.expression.text);
|
|
1311
|
+
}
|
|
1198
1312
|
}
|
|
1199
|
-
if (Object.keys(outputs).length === 0) {
|
|
1313
|
+
if (Object.keys(outputs).length === 0 && spread.length === 0) {
|
|
1200
1314
|
return null;
|
|
1201
1315
|
}
|
|
1202
1316
|
return {
|
|
1203
1317
|
type: 'return',
|
|
1204
1318
|
outputs,
|
|
1319
|
+
...(spread.length > 0 ? { spread } : {}),
|
|
1205
1320
|
};
|
|
1206
1321
|
}
|
|
1207
1322
|
/**
|
|
@@ -1297,6 +1412,10 @@ function extractInputSource(node, context) {
|
|
|
1297
1412
|
if (objName === context.inputParamName) {
|
|
1298
1413
|
return { from: 'input', path: propName };
|
|
1299
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
|
+
}
|
|
1300
1419
|
if (context.outputVars.has(objName)) {
|
|
1301
1420
|
return { from: 'outputVar', name: objName, path: propName };
|
|
1302
1421
|
}
|
|
@@ -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,45 @@ 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,
|
|
95
125
|
next: nextNodeId,
|
|
96
126
|
};
|
|
97
127
|
return [node];
|
|
@@ -160,13 +190,28 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
160
190
|
case 'switch': {
|
|
161
191
|
const caseNodes = [];
|
|
162
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 = [];
|
|
163
196
|
for (let i = 0; i < step.cases.length; i++) {
|
|
164
197
|
const caseSteps = convertStepsToNodes(step.cases[i].steps, `${nodeId}_case${i}`);
|
|
165
|
-
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 = [];
|
|
166
211
|
cases.push({
|
|
167
212
|
value: step.cases[i].value,
|
|
168
213
|
expression: step.cases[i].expression,
|
|
169
|
-
entry
|
|
214
|
+
entry,
|
|
170
215
|
});
|
|
171
216
|
// Link last case node to next (unless terminal flow)
|
|
172
217
|
if (nextNodeId) {
|
|
@@ -191,6 +236,13 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
191
236
|
caseNodes.push(...defaultNodes);
|
|
192
237
|
}
|
|
193
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
|
+
}
|
|
194
246
|
const node = {
|
|
195
247
|
nodeId,
|
|
196
248
|
flow: 'switch',
|
|
@@ -221,8 +273,11 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
221
273
|
return [node, ...childNodes];
|
|
222
274
|
}
|
|
223
275
|
case 'fanout': {
|
|
224
|
-
// Convert
|
|
225
|
-
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
|
+
}
|
|
226
281
|
const node = {
|
|
227
282
|
nodeId,
|
|
228
283
|
flow: 'fanout',
|
|
@@ -265,6 +320,7 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
265
320
|
nodeId,
|
|
266
321
|
flow: 'return',
|
|
267
322
|
outputs: step.outputs,
|
|
323
|
+
spread: step.spread,
|
|
268
324
|
};
|
|
269
325
|
return [node];
|
|
270
326
|
}
|
|
@@ -282,6 +338,7 @@ function convertStepToNode(step, index, steps, nodeIdPrefix = 'step') {
|
|
|
282
338
|
flow: 'set',
|
|
283
339
|
variable: step.variable,
|
|
284
340
|
value: step.value,
|
|
341
|
+
expression: step.expression,
|
|
285
342
|
next: nextNodeId,
|
|
286
343
|
};
|
|
287
344
|
return [node];
|
|
@@ -58,15 +58,15 @@ export type SerializedNext = string | string[] | {
|
|
|
58
58
|
export interface NodeOptions {
|
|
59
59
|
/** Number of retry attempts on failure */
|
|
60
60
|
retries?: number;
|
|
61
|
-
/** Delay between retries (e.g., '1s', '5s') */
|
|
62
|
-
retryDelay?: string;
|
|
61
|
+
/** Delay between retries (e.g., '1s', '5s', or a number of milliseconds) */
|
|
62
|
+
retryDelay?: string | number;
|
|
63
63
|
/** Timeout for node execution (e.g., '30s', '5m') */
|
|
64
64
|
timeout?: string;
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* Flow node types for control flow (no RPC call)
|
|
68
68
|
*/
|
|
69
|
-
export type FlowType = 'sleep' | 'approval' | 'branch' | 'parallel' | 'fanout' | 'inline' | 'switch' | 'filter' | 'arrayPredicate' | 'return' | 'cancel' | 'set';
|
|
69
|
+
export type FlowType = 'sleep' | 'suspend' | 'approval' | 'branch' | 'parallel' | 'fanout' | 'inline' | 'switch' | 'filter' | 'arrayPredicate' | 'return' | 'cancel' | 'set';
|
|
70
70
|
import type { ContextVariable, WorkflowContext, WorkflowPlannedStep } from '@pikku/core/workflow';
|
|
71
71
|
export type { ContextVariable, WorkflowContext };
|
|
72
72
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.45",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"zod-to-ts": "^2.1.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@pikku/core": "^0.12.
|
|
47
|
+
"@pikku/core": "^0.12.67"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@pikku/core": "^0.12.
|
|
50
|
+
"@pikku/core": "^0.12.67",
|
|
51
51
|
"@types/node": "^24.11.0"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
package/run-tests.sh
CHANGED
package/src/add/add-auth.test.ts
CHANGED
|
@@ -190,11 +190,11 @@ describe('addAuth inspector', () => {
|
|
|
190
190
|
[
|
|
191
191
|
"import { pikkuBetterAuth } from '@pikku/better-auth'",
|
|
192
192
|
"import { betterAuth } from 'better-auth'",
|
|
193
|
-
"import { bearer,
|
|
193
|
+
"import { bearer, apiKey, twoFactor } from 'better-auth/plugins'",
|
|
194
194
|
'export const auth = pikkuBetterAuth(() =>',
|
|
195
195
|
' betterAuth({',
|
|
196
196
|
" socialProviders: { github: { clientId: 'x', clientSecret: 'y' } },",
|
|
197
|
-
' plugins: [bearer(),
|
|
197
|
+
' plugins: [bearer(), apiKey(), twoFactor({ issuer: "pikku" })],',
|
|
198
198
|
' })',
|
|
199
199
|
')',
|
|
200
200
|
].join('\n')
|
|
@@ -204,10 +204,10 @@ describe('addAuth inspector', () => {
|
|
|
204
204
|
try {
|
|
205
205
|
const state = await inspect(makeLogger(criticals), [file], { rootDir })
|
|
206
206
|
assert.equal(criticals.length, 0)
|
|
207
|
-
assert.deepEqual(state.auth.plugins, ['bearer', '
|
|
207
|
+
assert.deepEqual(state.auth.plugins, ['bearer', 'apiKey', 'twoFactor'])
|
|
208
208
|
assert.deepEqual(state.auth.definition?.plugins, [
|
|
209
209
|
'bearer',
|
|
210
|
-
'
|
|
210
|
+
'apiKey',
|
|
211
211
|
'twoFactor',
|
|
212
212
|
])
|
|
213
213
|
} finally {
|
|
@@ -45,6 +45,7 @@ export const addCredential: AddWiring = (
|
|
|
45
45
|
| string
|
|
46
46
|
| null
|
|
47
47
|
const typeValue = getPropertyValue(obj, 'type') as string | null
|
|
48
|
+
const docsUrlValue = getPropertyValue(obj, 'docsUrl') as string | null
|
|
48
49
|
|
|
49
50
|
if (!nameValue) {
|
|
50
51
|
logger.critical(
|
|
@@ -179,6 +180,7 @@ export const addCredential: AddWiring = (
|
|
|
179
180
|
displayName: displayNameValue,
|
|
180
181
|
description: descriptionValue || undefined,
|
|
181
182
|
type: typeValue as 'singleton' | 'wire',
|
|
183
|
+
docsUrl: docsUrlValue || undefined,
|
|
182
184
|
schema: schemaLookupName,
|
|
183
185
|
oauth2,
|
|
184
186
|
sourceFile,
|
|
@@ -56,6 +56,7 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
|
|
|
56
56
|
const rotationPeriodValue = getPropertyValue(obj, 'rotationPeriod') as
|
|
57
57
|
| string
|
|
58
58
|
| null
|
|
59
|
+
const docsUrlValue = getPropertyValue(obj, 'docsUrl') as string | null
|
|
59
60
|
const idValue = getPropertyValue(obj, config.idField) as string | null
|
|
60
61
|
|
|
61
62
|
let schemaVariableName: string | null = null
|
|
@@ -165,6 +166,7 @@ export const createAddKeyedWiring = (config: KeyedWiringConfig): AddWiring => {
|
|
|
165
166
|
displayName: displayNameValue,
|
|
166
167
|
description: descriptionValue || undefined,
|
|
167
168
|
rotationPeriod: rotationPeriodValue || undefined,
|
|
169
|
+
docsUrl: docsUrlValue || undefined,
|
|
168
170
|
[config.idField]: idValue,
|
|
169
171
|
schema: schemaLookupName,
|
|
170
172
|
sourceFile,
|