@teleporthq/teleport-plugin-next-workflows 0.45.14 → 0.45.16
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/__tests__/cron-route-loop-templateparams.test.ts +5 -4
- package/__tests__/custom-js-loop-scope.test.ts +10 -5
- package/__tests__/loop-collection-unwrap.test.ts +6 -1
- package/__tests__/server-loop-body-branches.test.ts +188 -0
- package/__tests__/server-loop-types.test.ts +371 -0
- package/__tests__/server-nested-loops.test.ts +392 -0
- package/__tests__/streaming-route-if-branch.test.ts +107 -0
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +57 -248
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +84 -27
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/server-runtime-code.d.ts.map +1 -1
- package/dist/cjs/server-runtime-code.js +67 -0
- package/dist/cjs/server-runtime-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +57 -248
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +84 -27
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/server-runtime-code.d.ts.map +1 -1
- package/dist/esm/server-runtime-code.js +67 -0
- package/dist/esm/server-runtime-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/api-route-generator.ts +57 -248
- package/src/executor-generator.ts +84 -27
- package/src/server-runtime-code.ts +67 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teleporthq/teleport-plugin-next-workflows",
|
|
3
|
-
"version": "0.45.
|
|
3
|
+
"version": "0.45.16",
|
|
4
4
|
"description": "A plugin for workflow code generation in Next.js projects.",
|
|
5
5
|
"author": "teleportHQ",
|
|
6
6
|
"license": "MIT",
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"@teleporthq/teleport-shared": "^0.45.0",
|
|
70
70
|
"@teleporthq/teleport-types": "^0.45.0"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "ff4b04c211701b9bc5fdce1f32e42226241b0792"
|
|
73
73
|
}
|
|
@@ -443,6 +443,8 @@ module.exports = async function handler(req, res) {
|
|
|
443
443
|
// the caller as if that branch had run (a custom node answers the output
|
|
444
444
|
// of the last node it ran).
|
|
445
445
|
if (context.__skippedNodes && context.__skippedNodes[node.id]) continue;
|
|
446
|
+
// A node of a loop body ran inside its loop, an if-statement included.
|
|
447
|
+
if (context.__loopBodyNodeIds && context.__loopBodyNodeIds[node.id]) continue;
|
|
446
448
|
const resolved = resolveNodeConfig(node, context);
|
|
447
449
|
// Every data node lives in a SERVER segment, so this guard — which turns an
|
|
448
450
|
// unresolved {{…}} into a validation error instead of letting the literal
|
|
@@ -458,118 +460,34 @@ module.exports = async function handler(req, res) {
|
|
|
458
460
|
if (node.type === 'general-if-statement') {
|
|
459
461
|
const condResult = utils.evaluateCondition(resolved, context, node.config);
|
|
460
462
|
context[node.id] = { result: condResult };
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
var skipEdge = segEdgesIf.find(function(e) { return e.source === node.id && e.sourceHandle === skipHandle; });
|
|
464
|
-
if (skipEdge) {
|
|
465
|
-
// BFS to collect all nodes in the non-taken branch
|
|
466
|
-
var skipIds = {};
|
|
467
|
-
var skipQueue = [skipEdge.target];
|
|
468
|
-
while (skipQueue.length > 0) {
|
|
469
|
-
var sid = skipQueue.shift();
|
|
470
|
-
if (skipIds[sid] || sid === node.id) continue;
|
|
471
|
-
skipIds[sid] = true;
|
|
472
|
-
segEdgesIf.forEach(function(e) {
|
|
473
|
-
if (e.source === sid && !skipIds[e.target] && e.target !== node.id) skipQueue.push(e.target);
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
if (!context.__skippedNodes) context.__skippedNodes = {};
|
|
477
|
-
Object.assign(context.__skippedNodes, skipIds);
|
|
478
|
-
}
|
|
463
|
+
if (!context.__skippedNodes) context.__skippedNodes = {};
|
|
464
|
+
Object.assign(context.__skippedNodes, utils.nonTakenBranchNodeIds(SEGMENT_CONFIG.edges, node.id, condResult));
|
|
479
465
|
continue;
|
|
480
466
|
}
|
|
481
467
|
|
|
482
|
-
//
|
|
483
|
-
//
|
|
468
|
+
// A loop runs its body once per pass of its type, a loop inside the body
|
|
469
|
+
// included (see runServerLoop in the server runtime).
|
|
484
470
|
if (node.type === 'general-loop') {
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const queue = [loopBodyEdge.target];
|
|
493
|
-
while (queue.length > 0) {
|
|
494
|
-
const nid = queue.shift();
|
|
495
|
-
if (bodyNodeIds[nid] || nid === node.id) continue;
|
|
496
|
-
bodyNodeIds[nid] = true;
|
|
497
|
-
SEGMENT_CONFIG.edges.forEach(function(e) {
|
|
498
|
-
if (e.source === nid && (e.sourceHandle === 'loop-body-out' || !e.sourceHandle)) {
|
|
499
|
-
if (e.target !== node.id) queue.push(e.target);
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
const bodyNodes = sortedNodes.filter(function(n) { return bodyNodeIds[n.id]; });
|
|
505
|
-
// For loopType=map, the schema (general-loop in node-context-schemas.json)
|
|
506
|
-
// defines results as the array of last body node outputs (same as
|
|
507
|
-
// Array.prototype.map). Pick the last body node so downstream nodes
|
|
508
|
-
// (e.g. state-update-global-state) consume the transformed value
|
|
509
|
-
// instead of the per-iteration scaffolding object.
|
|
510
|
-
const __loopType = resolved.loopType || 'forEach';
|
|
511
|
-
const __lastBodyNode = bodyNodes[bodyNodes.length - 1];
|
|
512
|
-
|
|
513
|
-
// Resolve the collection to iterate over. unwrapWorkflowCollection
|
|
514
|
-
// handles the common envelope shape that helper nodes return —
|
|
515
|
-
// transform-array { result, ... }, integration-airtable { records, ... },
|
|
516
|
-
// utility-semantic-search { matches, ... } — by unwrapping a single
|
|
517
|
-
// array property when the upstream binding pointed at the parent node
|
|
518
|
-
// instead of the inner field. See runtime-utils for the full rules.
|
|
519
|
-
var collection = utils.unwrapWorkflowCollection(resolved.collection);
|
|
520
|
-
var loopResults = [];
|
|
521
|
-
|
|
522
|
-
// Track this loop's body scope so general-custom-js (and any handler
|
|
523
|
-
// that consults the scope stack) can build the params / innerParams
|
|
524
|
-
// arrays the workflow editor advertises: params = nodes BEFORE the
|
|
525
|
-
// outermost loop, innerParamsN = nodes inside the loop at depth N.
|
|
526
|
-
if (!context.__loopScopeStack) context.__loopScopeStack = [];
|
|
527
|
-
context.__loopScopeStack.push({ loopNodeId: node.id, bodyNodeIds: bodyNodeIds });
|
|
528
|
-
// Persistent registry of every loop scaffold node we've entered, so
|
|
529
|
-
// a sibling that runs AFTER an inner loop completes still recognises
|
|
530
|
-
// that inner loop id as scaffolding (not as a regular workflow node).
|
|
531
|
-
if (!context.__loopNodeIds) context.__loopNodeIds = {};
|
|
532
|
-
context.__loopNodeIds[node.id] = true;
|
|
533
|
-
|
|
534
|
-
for (var li = 0; li < collection.length; li++) {
|
|
535
|
-
context[node.id] = utils.loopIterationContext(collection, li);
|
|
536
|
-
for (var bi = 0; bi < bodyNodes.length; bi++) {
|
|
537
|
-
var bNode = bodyNodes[bi];
|
|
538
|
-
var bResolved = resolveNodeConfig(bNode, context);
|
|
539
|
-
bResolved.__nodeId = bNode.id;
|
|
540
|
-
if (bNode.type === 'general-if-statement') {
|
|
541
|
-
context[bNode.id] = { result: utils.evaluateCondition(bResolved, context, bNode.config) };
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
var bHandler = nodeHandlers[bNode.type];
|
|
545
|
-
if (!bHandler) continue;
|
|
546
|
-
if (utils.isFireAndForgetNode(bNode)) {
|
|
547
|
-
utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(bNode, bHandler, bResolved, context));
|
|
548
|
-
context[bNode.id] = null;
|
|
549
|
-
continue;
|
|
550
|
-
}
|
|
551
|
-
var bResult = await bHandler(bResolved, context);
|
|
552
|
-
if (bResult && (bResult.success === false || (typeof bResult.error === 'string' && bResult.error))) {
|
|
553
|
-
throw new Error(bResult.error || 'Loop body node execution failed');
|
|
554
|
-
}
|
|
555
|
-
context[bNode.id] = bResult;
|
|
471
|
+
const bodyNodeIds = await utils.runServerLoop(node, resolved, sortedNodes, SEGMENT_CONFIG.edges, context, async function(bNode, bResolved) {
|
|
472
|
+
var bHandler = nodeHandlers[bNode.type];
|
|
473
|
+
if (!bHandler) return;
|
|
474
|
+
if (utils.isFireAndForgetNode(bNode)) {
|
|
475
|
+
utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(bNode, bHandler, bResolved, context));
|
|
476
|
+
context[bNode.id] = null;
|
|
477
|
+
return;
|
|
556
478
|
}
|
|
557
|
-
|
|
558
|
-
|
|
479
|
+
var bResult = await bHandler(bResolved, context);
|
|
480
|
+
if (bResult && (bResult.success === false || (typeof bResult.error === 'string' && bResult.error))) {
|
|
481
|
+
throw new Error(bResult.error || 'Loop body node execution failed');
|
|
559
482
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
context[node.id] = { completed: true, iterations: collection.length, results: loopResults };
|
|
565
|
-
// Store body node IDs so the outer loop can skip them
|
|
483
|
+
context[bNode.id] = bResult;
|
|
484
|
+
});
|
|
485
|
+
// The walk below must not run a body node again.
|
|
566
486
|
if (!context.__loopBodyNodeIds) context.__loopBodyNodeIds = {};
|
|
567
487
|
Object.assign(context.__loopBodyNodeIds, bodyNodeIds);
|
|
568
488
|
continue;
|
|
569
489
|
}
|
|
570
490
|
|
|
571
|
-
// Skip nodes that were part of a loop body (already executed inside the loop)
|
|
572
|
-
if (context.__loopBodyNodeIds && context.__loopBodyNodeIds[node.id]) continue;
|
|
573
491
|
|
|
574
492
|
// Switch nodes evaluate a value, match a case, execute that branch, skip all others
|
|
575
493
|
if (node.type === 'general-switch') {
|
|
@@ -865,6 +783,8 @@ module.exports = async function handler(req, res) {
|
|
|
865
783
|
for (let i = 0; i < sortedNodes.length; i++) {
|
|
866
784
|
const node = sortedNodes[i];
|
|
867
785
|
if (executed[node.id]) continue;
|
|
786
|
+
// The branch an if-statement or a switch did not take never runs.
|
|
787
|
+
if (context.__skippedNodes && context.__skippedNodes[node.id]) continue;
|
|
868
788
|
const resolved = resolveNodeConfig(node, context);
|
|
869
789
|
// See the non-streaming segment above — without this the unresolved-token
|
|
870
790
|
// guard never runs on the server, which is where every data node executes.
|
|
@@ -877,71 +797,27 @@ module.exports = async function handler(req, res) {
|
|
|
877
797
|
const condResult = utils.evaluateCondition(resolved, context, node.config);
|
|
878
798
|
context[node.id] = { result: condResult };
|
|
879
799
|
executed[node.id] = true;
|
|
800
|
+
if (!context.__skippedNodes) context.__skippedNodes = {};
|
|
801
|
+
Object.assign(context.__skippedNodes, utils.nonTakenBranchNodeIds(SEGMENT_CONFIG.edges, node.id, condResult));
|
|
880
802
|
if (streamStarted) {
|
|
881
803
|
__streamNodeResult(res, node.id, context[node.id]);
|
|
882
804
|
}
|
|
883
805
|
continue;
|
|
884
806
|
}
|
|
885
807
|
|
|
886
|
-
// Loop nodes in streaming segments
|
|
808
|
+
// Loop nodes in streaming segments: same runner as the regular-segment
|
|
809
|
+
// loop above, a loop inside the body included.
|
|
887
810
|
if (node.type === 'general-loop') {
|
|
888
|
-
var
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
if (ssBodyNodeIds[sslnid] || sslnid === node.id) continue;
|
|
896
|
-
ssBodyNodeIds[sslnid] = true;
|
|
897
|
-
SEGMENT_CONFIG.edges.forEach(function(e) {
|
|
898
|
-
if (e.source === sslnid && (e.sourceHandle === 'loop-body-out' || !e.sourceHandle)) {
|
|
899
|
-
if (e.target !== node.id) sslq.push(e.target);
|
|
900
|
-
}
|
|
901
|
-
});
|
|
811
|
+
var ssBodyNodeIds = await utils.runServerLoop(node, resolved, sortedNodes, SEGMENT_CONFIG.edges, context, async function(ssbNode, ssbRes) {
|
|
812
|
+
var ssbHandler = nodeHandlers[ssbNode.type];
|
|
813
|
+
if (!ssbHandler) return;
|
|
814
|
+
if (utils.isFireAndForgetNode(ssbNode)) {
|
|
815
|
+
utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(ssbNode, ssbHandler, ssbRes, context));
|
|
816
|
+
context[ssbNode.id] = null;
|
|
817
|
+
return;
|
|
902
818
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
// See the regular-segment loop above: loopType=map must publish the
|
|
906
|
-
// last body node output as results[i] so the schema contract holds.
|
|
907
|
-
var ssLoopType = resolved.loopType || 'forEach';
|
|
908
|
-
var ssLastBodyNode = ssBodyNodes[ssBodyNodes.length - 1];
|
|
909
|
-
// Same envelope unwrap as the regular-segment loop above.
|
|
910
|
-
var ssLoopCollection = utils.unwrapWorkflowCollection(resolved.collection);
|
|
911
|
-
var ssLoopResults = [];
|
|
912
|
-
// Same loop-scope tracking as the regular-segment loop above. Custom-js
|
|
913
|
-
// bodies can read context.__loopScopeStack to recover the params /
|
|
914
|
-
// innerParams contract documented in workflow-utils.ts.
|
|
915
|
-
if (!context.__loopScopeStack) context.__loopScopeStack = [];
|
|
916
|
-
context.__loopScopeStack.push({ loopNodeId: node.id, bodyNodeIds: ssBodyNodeIds });
|
|
917
|
-
if (!context.__loopNodeIds) context.__loopNodeIds = {};
|
|
918
|
-
context.__loopNodeIds[node.id] = true;
|
|
919
|
-
for (var ssli = 0; ssli < ssLoopCollection.length; ssli++) {
|
|
920
|
-
context[node.id] = utils.loopIterationContext(ssLoopCollection, ssli);
|
|
921
|
-
for (var ssbi = 0; ssbi < ssBodyNodes.length; ssbi++) {
|
|
922
|
-
var ssbNode = ssBodyNodes[ssbi];
|
|
923
|
-
var ssbRes = resolveNodeConfig(ssbNode, context);
|
|
924
|
-
ssbRes.__nodeId = ssbNode.id;
|
|
925
|
-
if (ssbNode.type === 'general-if-statement') {
|
|
926
|
-
context[ssbNode.id] = { result: utils.evaluateCondition(ssbRes, context, ssbNode.config) };
|
|
927
|
-
continue;
|
|
928
|
-
}
|
|
929
|
-
var ssbHandler = nodeHandlers[ssbNode.type];
|
|
930
|
-
if (!ssbHandler) continue;
|
|
931
|
-
if (utils.isFireAndForgetNode(ssbNode)) {
|
|
932
|
-
utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(ssbNode, ssbHandler, ssbRes, context));
|
|
933
|
-
context[ssbNode.id] = null;
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
var ssbResult = await ssbHandler(ssbRes, context);
|
|
937
|
-
context[ssbNode.id] = ssbResult;
|
|
938
|
-
}
|
|
939
|
-
if (ssLoopType === 'map') {
|
|
940
|
-
ssLoopResults.push(ssLastBodyNode ? context[ssLastBodyNode.id] : context[node.id]);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
context.__loopScopeStack.pop();
|
|
944
|
-
context[node.id] = { completed: true, iterations: ssLoopCollection.length, results: ssLoopResults };
|
|
819
|
+
context[ssbNode.id] = await ssbHandler(ssbRes, context);
|
|
820
|
+
});
|
|
945
821
|
Object.keys(ssBodyNodeIds).forEach(function(k) { executed[k] = true; });
|
|
946
822
|
executed[node.id] = true;
|
|
947
823
|
if (streamStarted) {
|
|
@@ -1668,11 +1544,11 @@ const generateNodeExecutionLoop = (
|
|
|
1668
1544
|
// top-level one does: its handler only hands back the marker.
|
|
1669
1545
|
const loopBodyCustomNodeBlock = options?.hasCustomNodes
|
|
1670
1546
|
? `
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1547
|
+
if (bResult && bResult.__customNode) {
|
|
1548
|
+
var bcnFn = typeof __customNodes !== 'undefined' ? __customNodes[bResult.customNodeId] : null;
|
|
1549
|
+
context[bNode.id] = bcnFn ? await bcnFn(context, bResult.parameters, nodeHandlers) : null;
|
|
1550
|
+
return;
|
|
1551
|
+
}`
|
|
1676
1552
|
: ''
|
|
1677
1553
|
|
|
1678
1554
|
return ` var sortedNodes = ${configVar}.nodes.slice().sort(function(a, b) { return a.stepNumber - b.stepNumber; });
|
|
@@ -1682,6 +1558,8 @@ ${resumeBlock}
|
|
|
1682
1558
|
// Never resolved nor run on a non-taken branch, an if-statement included
|
|
1683
1559
|
// (see the segment executor above).
|
|
1684
1560
|
if (context.__skippedNodes && context.__skippedNodes[node.id]) continue;
|
|
1561
|
+
// A node of a loop body ran inside its loop, an if-statement included.
|
|
1562
|
+
if (context.__loopBodyNodeIds && context.__loopBodyNodeIds[node.id]) continue;
|
|
1685
1563
|
var resolved = resolveNodeConfig(node, context);
|
|
1686
1564
|
// See the other segment executors — the unresolved-token guard must run
|
|
1687
1565
|
// wherever data nodes execute, and they all execute on the server.
|
|
@@ -1692,102 +1570,33 @@ ${resumeBlock}
|
|
|
1692
1570
|
if (node.type === 'general-if-statement') {
|
|
1693
1571
|
var condResult = utils.evaluateCondition(resolved, context, node.config);
|
|
1694
1572
|
context[node.id] = { result: condResult };
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
var skipEdge = cfgEdges.find(function(e) { return e.source === node.id && e.sourceHandle === skipHandle; });
|
|
1698
|
-
if (skipEdge) {
|
|
1699
|
-
var skipIds = {};
|
|
1700
|
-
var skipQueue = [skipEdge.target];
|
|
1701
|
-
while (skipQueue.length > 0) {
|
|
1702
|
-
var sid = skipQueue.shift();
|
|
1703
|
-
if (skipIds[sid] || sid === node.id) continue;
|
|
1704
|
-
skipIds[sid] = true;
|
|
1705
|
-
cfgEdges.forEach(function(e) {
|
|
1706
|
-
if (e.source === sid && !skipIds[e.target] && e.target !== node.id) skipQueue.push(e.target);
|
|
1707
|
-
});
|
|
1708
|
-
}
|
|
1709
|
-
if (!context.__skippedNodes) context.__skippedNodes = {};
|
|
1710
|
-
Object.assign(context.__skippedNodes, skipIds);
|
|
1711
|
-
}
|
|
1573
|
+
if (!context.__skippedNodes) context.__skippedNodes = {};
|
|
1574
|
+
Object.assign(context.__skippedNodes, utils.nonTakenBranchNodeIds(${configVar}.edges, node.id, condResult));
|
|
1712
1575
|
continue;
|
|
1713
1576
|
}
|
|
1714
1577
|
|
|
1578
|
+
// Same runner as the segment routes, a loop inside the body included
|
|
1579
|
+
// (see runServerLoop in the server runtime).
|
|
1715
1580
|
if (node.type === 'general-loop') {
|
|
1716
|
-
var
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
var nid = lQueue.shift();
|
|
1724
|
-
if (bodyNodeIds[nid] || nid === node.id) continue;
|
|
1725
|
-
bodyNodeIds[nid] = true;
|
|
1726
|
-
${configVar}.edges.forEach(function(e) {
|
|
1727
|
-
if (e.source === nid && (e.sourceHandle === 'loop-body-out' || !e.sourceHandle)) {
|
|
1728
|
-
if (e.target !== node.id) lQueue.push(e.target);
|
|
1729
|
-
}
|
|
1730
|
-
});
|
|
1581
|
+
var bodyNodeIds = await utils.runServerLoop(node, resolved, sortedNodes, ${configVar}.edges, context, async function(bNode, bResolved) {
|
|
1582
|
+
var bHandler = nodeHandlers[bNode.type];
|
|
1583
|
+
if (!bHandler) return;
|
|
1584
|
+
if (utils.isFireAndForgetNode(bNode)) {
|
|
1585
|
+
utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(bNode, bHandler, bResolved, context));
|
|
1586
|
+
context[bNode.id] = null;
|
|
1587
|
+
return;
|
|
1731
1588
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
// defines results as the array of last body node outputs (same as
|
|
1736
|
-
// Array.prototype.map). Pick the last body node so downstream nodes
|
|
1737
|
-
// (e.g. state-update-global-state) consume the transformed value.
|
|
1738
|
-
var __wfLoopType = resolved.loopType || 'forEach';
|
|
1739
|
-
var __wfLastBodyNode = bodyNodes[bodyNodes.length - 1];
|
|
1740
|
-
|
|
1741
|
-
// Same envelope unwrap as the regular-segment loop above.
|
|
1742
|
-
var collection = utils.unwrapWorkflowCollection(resolved.collection);
|
|
1743
|
-
var loopResults = [];
|
|
1744
|
-
|
|
1745
|
-
// Same loop-scope tracking as the regular-segment loop above so
|
|
1746
|
-
// general-custom-js inside the loop body sees the right params /
|
|
1747
|
-
// innerParams. Required for webhook + cron routes.
|
|
1748
|
-
if (!context.__loopScopeStack) context.__loopScopeStack = [];
|
|
1749
|
-
context.__loopScopeStack.push({ loopNodeId: node.id, bodyNodeIds: bodyNodeIds });
|
|
1750
|
-
if (!context.__loopNodeIds) context.__loopNodeIds = {};
|
|
1751
|
-
context.__loopNodeIds[node.id] = true;
|
|
1752
|
-
|
|
1753
|
-
for (var li = 0; li < collection.length; li++) {
|
|
1754
|
-
context[node.id] = utils.loopIterationContext(collection, li);
|
|
1755
|
-
for (var bi = 0; bi < bodyNodes.length; bi++) {
|
|
1756
|
-
var bNode = bodyNodes[bi];
|
|
1757
|
-
var bResolved = resolveNodeConfig(bNode, context);
|
|
1758
|
-
bResolved.__nodeId = bNode.id;
|
|
1759
|
-
if (bNode.type === 'general-if-statement') {
|
|
1760
|
-
context[bNode.id] = { result: utils.evaluateCondition(bResolved, context, bNode.config) };
|
|
1761
|
-
continue;
|
|
1762
|
-
}
|
|
1763
|
-
var bHandler = nodeHandlers[bNode.type];
|
|
1764
|
-
if (!bHandler) continue;
|
|
1765
|
-
if (utils.isFireAndForgetNode(bNode)) {
|
|
1766
|
-
utils.registerPendingNodePromise(context, utils.startFireAndForgetNode(bNode, bHandler, bResolved, context));
|
|
1767
|
-
context[bNode.id] = null;
|
|
1768
|
-
continue;
|
|
1769
|
-
}
|
|
1770
|
-
var bResult = await bHandler(bResolved, context);${loopBodyCustomNodeBlock}
|
|
1771
|
-
if (bResult && (bResult.success === false || (typeof bResult.error === 'string' && bResult.error))) {
|
|
1772
|
-
throw new Error(bResult.error || 'Loop body node execution failed');
|
|
1773
|
-
}
|
|
1774
|
-
context[bNode.id] = bResult;
|
|
1775
|
-
}
|
|
1776
|
-
if (__wfLoopType === 'map') {
|
|
1777
|
-
loopResults.push(__wfLastBodyNode ? context[__wfLastBodyNode.id] : context[node.id]);
|
|
1589
|
+
var bResult = await bHandler(bResolved, context);${loopBodyCustomNodeBlock}
|
|
1590
|
+
if (bResult && (bResult.success === false || (typeof bResult.error === 'string' && bResult.error))) {
|
|
1591
|
+
throw new Error(bResult.error || 'Loop body node execution failed');
|
|
1778
1592
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
context.__loopScopeStack.pop();
|
|
1782
|
-
|
|
1783
|
-
context[node.id] = { completed: true, iterations: collection.length, results: loopResults };
|
|
1593
|
+
context[bNode.id] = bResult;
|
|
1594
|
+
});
|
|
1784
1595
|
if (!context.__loopBodyNodeIds) context.__loopBodyNodeIds = {};
|
|
1785
1596
|
Object.assign(context.__loopBodyNodeIds, bodyNodeIds);
|
|
1786
1597
|
continue;
|
|
1787
1598
|
}
|
|
1788
1599
|
|
|
1789
|
-
if (context.__loopBodyNodeIds && context.__loopBodyNodeIds[node.id]) continue;
|
|
1790
|
-
|
|
1791
1600
|
if (node.type === 'general-switch') {
|
|
1792
1601
|
// Only an authored case expression is compiled (see matchSwitchCase).
|
|
1793
1602
|
var swMatchedCase = utils.matchSwitchCase(node.config, resolved, context);
|
|
@@ -161,6 +161,81 @@ function loopIterationContext(collection, index) {
|
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// How every loop type steps, for the client executor and for each server
|
|
165
|
+
// route alike. next(context) hands back what one pass publishes before its
|
|
166
|
+
// body runs ({ key, value }, stored as context[key]), or null once the loop is
|
|
167
|
+
// over. A collection is walked under the loop's own id; a counted or a
|
|
168
|
+
// conditional loop publishes its counter under "<loopId>_iter". A while / until
|
|
169
|
+
// condition is tested before every pass, against the context of that pass.
|
|
170
|
+
var DEFAULT_LOOP_MAX_ITERATIONS = 1000;
|
|
171
|
+
|
|
172
|
+
function createLoopSteps(loopNode, config, context, collection) {
|
|
173
|
+
var loopType = config.loopType || 'forEach';
|
|
174
|
+
var iterKey = loopNode.id + '_iter';
|
|
175
|
+
var passes = 0;
|
|
176
|
+
var next;
|
|
177
|
+
if (loopType === 'forEach' || loopType === 'map') {
|
|
178
|
+
next = function() {
|
|
179
|
+
return passes < collection.length
|
|
180
|
+
? { key: loopNode.id, value: loopIterationContext(collection, passes) }
|
|
181
|
+
: null;
|
|
182
|
+
};
|
|
183
|
+
} else if (loopType === 'for') {
|
|
184
|
+
var cursor = Number(resolveValue(config.startIndex, context)) || 0;
|
|
185
|
+
var end = Number(resolveValue(config.endIndex, context)) || 0;
|
|
186
|
+
var step = Number(resolveValue(config.step, context)) || 1;
|
|
187
|
+
next = function() {
|
|
188
|
+
if (!(step > 0 ? cursor < end : cursor > end)) return null;
|
|
189
|
+
var index = cursor;
|
|
190
|
+
cursor += step;
|
|
191
|
+
return { key: iterKey, value: { index: index } };
|
|
192
|
+
};
|
|
193
|
+
} else {
|
|
194
|
+
var maxIterations = Number(resolveValue(config.maxIterations, context)) || DEFAULT_LOOP_MAX_ITERATIONS;
|
|
195
|
+
var authoredCondition = loopNode.config && loopNode.config.condition;
|
|
196
|
+
next = function(passContext) {
|
|
197
|
+
if (passes >= maxIterations) return null;
|
|
198
|
+
var scaffold = { iteration: passes };
|
|
199
|
+
var testContext = Object.assign({}, passContext);
|
|
200
|
+
testContext[iterKey] = scaffold;
|
|
201
|
+
var condition = resolveValue(config.condition, testContext);
|
|
202
|
+
// Only the authored condition is compiled (see runExpression).
|
|
203
|
+
var holds = runExpression(authoredCondition, condition, ['context'], [testContext], testContext);
|
|
204
|
+
if (loopType === 'until' ? holds : !holds) return null;
|
|
205
|
+
return { key: iterKey, value: scaffold };
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
next: function(passContext) {
|
|
210
|
+
var pass = next(passContext || context);
|
|
211
|
+
if (pass) passes++;
|
|
212
|
+
return pass;
|
|
213
|
+
},
|
|
214
|
+
iterations: function() { return passes; }
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// The nodes of the branch an if-statement did NOT take: everything reachable
|
|
219
|
+
// from that branch's edge, never walking back through the if itself nor past
|
|
220
|
+
// stopAtId (the loop a body belongs to, whose loop-back edge leads out of it).
|
|
221
|
+
function nonTakenBranchNodeIds(edges, ifNodeId, conditionHeld, stopAtId) {
|
|
222
|
+
var skipHandle = conditionHeld ? 'false' : 'true';
|
|
223
|
+
var all = edges || [];
|
|
224
|
+
var skipEdge = all.find(function(e) { return e.source === ifNodeId && e.sourceHandle === skipHandle; });
|
|
225
|
+
var skipIds = {};
|
|
226
|
+
if (!skipEdge) return skipIds;
|
|
227
|
+
var queue = [skipEdge.target];
|
|
228
|
+
while (queue.length > 0) {
|
|
229
|
+
var id = queue.shift();
|
|
230
|
+
if (skipIds[id] || id === ifNodeId || id === stopAtId) continue;
|
|
231
|
+
skipIds[id] = true;
|
|
232
|
+
all.forEach(function(e) {
|
|
233
|
+
if (e.source === id && !skipIds[e.target]) queue.push(e.target);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return skipIds;
|
|
237
|
+
}
|
|
238
|
+
|
|
164
239
|
function resolveCtxRef(ref, context) {
|
|
165
240
|
var elem = context.triggerElement;
|
|
166
241
|
if (!elem || typeof elem.getAttribute !== 'function') return undefined;
|
|
@@ -1496,35 +1571,15 @@ async function executeLoop(loopNode, config, allNodes, edges, context, nodeHandl
|
|
|
1496
1571
|
context[loopNode.id] = loopType === 'map'
|
|
1497
1572
|
? { results: results, iterations: collection.length }
|
|
1498
1573
|
: { completed: true, iterations: collection.length };
|
|
1499
|
-
} else if (loopType === 'for') {
|
|
1500
|
-
const start = resolveValue(config.startIndex, context) || 0;
|
|
1501
|
-
const end = resolveValue(config.endIndex, context) || 0;
|
|
1502
|
-
const step = resolveValue(config.step, context) || 1;
|
|
1503
|
-
let count = 0;
|
|
1504
|
-
for (let fi = start; step > 0 ? fi < end : fi > end; fi += step) {
|
|
1505
|
-
const forCtx = Object.assign({}, context);
|
|
1506
|
-
forCtx[loopNode.id + '_iter'] = { index: fi };
|
|
1507
|
-
await executeNodes(bodyNodes, edges, forCtx, nodeHandlers, workflowConfig, callServerSegment, executionId);
|
|
1508
|
-
Object.assign(context, forCtx);
|
|
1509
|
-
count++;
|
|
1510
|
-
}
|
|
1511
|
-
context[loopNode.id] = { completed: true, iterations: count };
|
|
1512
1574
|
} else {
|
|
1513
|
-
const
|
|
1514
|
-
let
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
// Only the authored condition is compiled (see runExpression).
|
|
1520
|
-
const condResult = runExpression(loopNode.config && loopNode.config.condition, condStr, ['context'], [whileCtx], whileCtx);
|
|
1521
|
-
if (loopType === 'while' && !condResult) break;
|
|
1522
|
-
if (loopType === 'until' && condResult) break;
|
|
1523
|
-
await executeNodes(bodyNodes, edges, whileCtx, nodeHandlers, workflowConfig, callServerSegment, executionId);
|
|
1524
|
-
Object.assign(context, whileCtx);
|
|
1525
|
-
iter++;
|
|
1575
|
+
const steps = createLoopSteps(loopNode, config, context);
|
|
1576
|
+
for (let pass = steps.next(context); pass; pass = steps.next(context)) {
|
|
1577
|
+
const passCtx = Object.assign({}, context);
|
|
1578
|
+
passCtx[pass.key] = pass.value;
|
|
1579
|
+
await executeNodes(bodyNodes, edges, passCtx, nodeHandlers, workflowConfig, callServerSegment, executionId);
|
|
1580
|
+
Object.assign(context, passCtx);
|
|
1526
1581
|
}
|
|
1527
|
-
context[loopNode.id] = { completed: true, iterations:
|
|
1582
|
+
context[loopNode.id] = { completed: true, iterations: steps.iterations() };
|
|
1528
1583
|
}
|
|
1529
1584
|
} finally {
|
|
1530
1585
|
context.__loopScopeStack.pop();
|
|
@@ -1820,6 +1875,8 @@ module.exports = {
|
|
|
1820
1875
|
resolveRichTextContext,
|
|
1821
1876
|
unwrapWorkflowCollection,
|
|
1822
1877
|
loopIterationContext,
|
|
1878
|
+
createLoopSteps,
|
|
1879
|
+
nonTakenBranchNodeIds,
|
|
1823
1880
|
snapshotIncomingContext,
|
|
1824
1881
|
segmentReply,
|
|
1825
1882
|
withoutCredentialColumns,
|
|
@@ -860,7 +860,74 @@ function handOffPendingNodePromises(context) {
|
|
|
860
860
|
return Promise.resolve();
|
|
861
861
|
}
|
|
862
862
|
|
|
863
|
+
// One loop of a server route, a loop inside its body included: every route
|
|
864
|
+
// kind runs its loops through here and differs only in runBodyNode(node,
|
|
865
|
+
// resolvedConfig), which runs one ordinary body node and stores its result.
|
|
866
|
+
// The body is walked in step order, once per pass of the loop type (see
|
|
867
|
+
// createLoopSteps). An if-statement skips the branch it does not take for
|
|
868
|
+
// that pass only; a loop found in the body runs all of its own passes on
|
|
869
|
+
// every pass of this one, and its body nodes belong to it alone.
|
|
870
|
+
// Returns every node id between the loop and its loop-back edge (the bodies
|
|
871
|
+
// of inner loops included), which the route must not run again.
|
|
872
|
+
async function runServerLoop(loopNode, config, sortedNodes, edges, context, runBodyNode) {
|
|
873
|
+
var bodyNodeIds = {};
|
|
874
|
+
utils.getLoopBodyNodeIds(loopNode.id, edges).forEach(function(id) { bodyNodeIds[id] = true; });
|
|
875
|
+
var innerLoopBodyIds = {};
|
|
876
|
+
sortedNodes.forEach(function(n) {
|
|
877
|
+
if (!bodyNodeIds[n.id] || n.type !== 'general-loop') return;
|
|
878
|
+
utils.getLoopBodyNodeIds(n.id, edges).forEach(function(id) { innerLoopBodyIds[id] = true; });
|
|
879
|
+
});
|
|
880
|
+
var bodyNodes = sortedNodes.filter(function(n) { return bodyNodeIds[n.id] && !innerLoopBodyIds[n.id]; });
|
|
881
|
+
// A map loop publishes the output of its last body node per pass, as
|
|
882
|
+
// Array.prototype.map does (general-loop in node-context-schemas.json).
|
|
883
|
+
var isMap = (config.loopType || 'forEach') === 'map';
|
|
884
|
+
var lastBodyNode = bodyNodes[bodyNodes.length - 1];
|
|
885
|
+
// A helper node's envelope ({ result }, { records }, { matches }) is
|
|
886
|
+
// unwrapped to the one array it holds.
|
|
887
|
+
var collection = utils.unwrapWorkflowCollection(config.collection);
|
|
888
|
+
var results = [];
|
|
889
|
+
|
|
890
|
+
// The scope stack lets custom JavaScript tell the nodes before the outermost
|
|
891
|
+
// loop (params) from those inside a loop at each depth (innerParams); the
|
|
892
|
+
// registry keeps a finished inner loop recognisable as scaffolding.
|
|
893
|
+
if (!context.__loopScopeStack) context.__loopScopeStack = [];
|
|
894
|
+
context.__loopScopeStack.push({ loopNodeId: loopNode.id, bodyNodeIds: bodyNodeIds });
|
|
895
|
+
if (!context.__loopNodeIds) context.__loopNodeIds = {};
|
|
896
|
+
context.__loopNodeIds[loopNode.id] = true;
|
|
897
|
+
|
|
898
|
+
var steps = utils.createLoopSteps(loopNode, config, context, collection);
|
|
899
|
+
for (var pass = steps.next(context); pass; pass = steps.next(context)) {
|
|
900
|
+
context[pass.key] = pass.value;
|
|
901
|
+
var skippedThisPass = {};
|
|
902
|
+
for (var i = 0; i < bodyNodes.length; i++) {
|
|
903
|
+
var bodyNode = bodyNodes[i];
|
|
904
|
+
if (skippedThisPass[bodyNode.id]) continue;
|
|
905
|
+
var resolved = utils.resolveNodeConfig(bodyNode, context);
|
|
906
|
+
resolved.__nodeId = bodyNode.id;
|
|
907
|
+
if (bodyNode.type === 'general-if-statement') {
|
|
908
|
+
var held = utils.evaluateCondition(resolved, context, bodyNode.config);
|
|
909
|
+
context[bodyNode.id] = { result: held };
|
|
910
|
+
Object.assign(skippedThisPass, utils.nonTakenBranchNodeIds(edges, bodyNode.id, held, loopNode.id));
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
if (bodyNode.type === 'general-loop') {
|
|
914
|
+
await runServerLoop(bodyNode, resolved, sortedNodes, edges, context, runBodyNode);
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
await runBodyNode(bodyNode, resolved);
|
|
918
|
+
}
|
|
919
|
+
if (isMap) {
|
|
920
|
+
results.push(lastBodyNode ? context[lastBodyNode.id] : context[loopNode.id]);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
context.__loopScopeStack.pop();
|
|
925
|
+
context[loopNode.id] = { completed: true, iterations: steps.iterations(), results: results };
|
|
926
|
+
return bodyNodeIds;
|
|
927
|
+
}
|
|
928
|
+
|
|
863
929
|
module.exports = Object.assign({}, utils, {
|
|
930
|
+
runServerLoop: runServerLoop,
|
|
864
931
|
handOffPendingNodePromises: handOffPendingNodePromises,
|
|
865
932
|
claimSegmentContext: claimSegmentContext,
|
|
866
933
|
segmentReply: segmentReply,
|