@objectstack/service-automation 9.0.0 → 9.1.0

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/dist/index.js CHANGED
@@ -460,6 +460,8 @@ var AutomationEngine = class {
460
460
  }
461
461
  const runId = this.nextRunId();
462
462
  variables.set("$runId", runId);
463
+ variables.set("$flowName", flowName);
464
+ variables.set("$flowLabel", flow.label ?? flowName);
463
465
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
464
466
  const steps = [];
465
467
  try {
@@ -668,7 +670,11 @@ var AutomationEngine = class {
668
670
  const steps = run.steps;
669
671
  const context = run.context;
670
672
  try {
671
- await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);
673
+ if (typeof run.correlation === "string" && run.correlation.startsWith("map:")) {
674
+ await this.executeNode(node, flow, variables, context, steps);
675
+ } else {
676
+ await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);
677
+ }
672
678
  const output = {};
673
679
  if (flow.variables) {
674
680
  for (const v of flow.variables) {
@@ -772,10 +778,12 @@ var AutomationEngine = class {
772
778
  * caller who resumed the child.
773
779
  */
774
780
  async bubbleToParent(run, output) {
775
- const parentRunId = run.context?.$parentRunId;
781
+ const ctx = run.context;
782
+ const parentRunId = ctx?.$parentRunId;
776
783
  if (typeof parentRunId !== "string" || !parentRunId) return;
777
784
  try {
778
- const sig = this.buildSubflowResumeSignal(run.context, output);
785
+ const mapNode = ctx?.$parentMapNode;
786
+ const sig = typeof mapNode === "string" && mapNode ? { variables: { [`${mapNode}.$mapItemOutput`]: output ?? null, [`${mapNode}.$mapItemDone`]: true } } : this.buildSubflowResumeSignal(run.context, output);
779
787
  const parentRes = await this.resumeInternal(parentRunId, sig, false);
780
788
  if (!parentRes.success) {
781
789
  this.logger.warn(
@@ -2766,6 +2774,96 @@ function registerSubflowNode(engine, ctx) {
2766
2774
  ctx.logger.info("[Subflow Node] 1 built-in node executor registered");
2767
2775
  }
2768
2776
 
2777
+ // src/builtin/map-node.ts
2778
+ import { defineActionDescriptor as defineActionDescriptor13 } from "@objectstack/spec/automation";
2779
+ var MAX_MAP_ITEMS = 1e4;
2780
+ function registerMapNode(engine, ctx) {
2781
+ engine.registerNodeExecutor({
2782
+ type: "map",
2783
+ descriptor: defineActionDescriptor13({
2784
+ type: "map",
2785
+ version: "1.0.0",
2786
+ name: "Map",
2787
+ description: "Run a per-item subflow for each element of a collection, one at a time (each item may pause).",
2788
+ icon: "list-check",
2789
+ category: "logic",
2790
+ source: "builtin",
2791
+ // Each item's subflow may pause, so the map suspends and resumes per item.
2792
+ supportsPause: true,
2793
+ isAsync: true
2794
+ }),
2795
+ async execute(node, variables, context) {
2796
+ const cfg = node.config ?? {};
2797
+ const flowName = typeof cfg.flowName === "string" ? cfg.flowName : typeof cfg.flow === "string" ? cfg.flow : void 0;
2798
+ if (!flowName) {
2799
+ return { success: false, error: `map '${node.id}': config.flowName (the per-item subflow) is required` };
2800
+ }
2801
+ const iteratorVariable = typeof cfg.iteratorVariable === "string" && cfg.iteratorVariable ? cfg.iteratorVariable : "item";
2802
+ const indexVariable = typeof cfg.indexVariable === "string" && cfg.indexVariable ? cfg.indexVariable : void 0;
2803
+ const outVar = typeof cfg.outputVariable === "string" && cfg.outputVariable ? cfg.outputVariable : void 0;
2804
+ const rawCollection = cfg.collection;
2805
+ let collection;
2806
+ if (Array.isArray(rawCollection)) {
2807
+ collection = rawCollection;
2808
+ } else if (typeof rawCollection === "string") {
2809
+ collection = interpolate(rawCollection, variables, context ?? {});
2810
+ if (collection == null && variables.has(rawCollection)) collection = variables.get(rawCollection);
2811
+ }
2812
+ if (!Array.isArray(collection)) {
2813
+ return { success: false, error: `map '${node.id}': collection '${String(rawCollection)}' did not resolve to an array` };
2814
+ }
2815
+ if (collection.length > MAX_MAP_ITEMS) {
2816
+ return { success: false, error: `map '${node.id}': collection length ${collection.length} exceeds the ${MAX_MAP_ITEMS} cap` };
2817
+ }
2818
+ const stateKey = `${node.id}.$mapState`;
2819
+ const state = variables.get(stateKey) ?? {
2820
+ started: 0,
2821
+ results: []
2822
+ };
2823
+ if (variables.get(`${node.id}.$mapItemDone`) === true) {
2824
+ state.results.push(variables.get(`${node.id}.$mapItemOutput`) ?? null);
2825
+ variables.delete(`${node.id}.$mapItemDone`);
2826
+ variables.delete(`${node.id}.$mapItemOutput`);
2827
+ }
2828
+ const parentRunId = variables.get("$runId");
2829
+ while (state.started < collection.length) {
2830
+ const idx = state.started;
2831
+ const item = collection[idx];
2832
+ variables.set(iteratorVariable, item);
2833
+ if (indexVariable) variables.set(indexVariable, idx);
2834
+ const rawInput = cfg.input && typeof cfg.input === "object" ? cfg.input : {};
2835
+ const params = interpolate(rawInput, variables, context ?? {});
2836
+ const itemIsRecord = item != null && typeof item === "object" && typeof item.id === "string";
2837
+ const itemObject = typeof cfg.itemObject === "string" ? cfg.itemObject : context?.object;
2838
+ const childContext = {
2839
+ ...context ?? {},
2840
+ params,
2841
+ ...itemIsRecord ? { record: item, object: itemObject } : {},
2842
+ ...parentRunId != null ? { $parentRunId: String(parentRunId), $parentMapNode: node.id } : {}
2843
+ };
2844
+ const child = await engine.execute(flowName, childContext);
2845
+ if (child.status === "paused") {
2846
+ if (!child.runId) {
2847
+ return { success: false, error: `map '${node.id}': item ${idx} paused without a run id \u2014 cannot link the runs` };
2848
+ }
2849
+ state.started = idx + 1;
2850
+ variables.set(stateKey, state);
2851
+ return { success: true, suspend: true, correlation: `map:${child.runId}` };
2852
+ }
2853
+ if (!child.success) {
2854
+ return { success: false, error: `map '${node.id}': item ${idx} (subflow '${flowName}') failed: ${child.error ?? "unknown error"}` };
2855
+ }
2856
+ state.started = idx + 1;
2857
+ state.results.push(child.output ?? null);
2858
+ }
2859
+ variables.set(stateKey, state);
2860
+ if (outVar) variables.set(outVar, state.results);
2861
+ return { success: true, output: { results: state.results, count: state.results.length } };
2862
+ }
2863
+ });
2864
+ ctx.logger.info("[Map Node] 1 built-in node executor registered");
2865
+ }
2866
+
2769
2867
  // src/builtin/index.ts
2770
2868
  function installBuiltinNodes(engine, ctx) {
2771
2869
  registerLogicNodes(engine, ctx);
@@ -2779,6 +2877,7 @@ function installBuiltinNodes(engine, ctx) {
2779
2877
  registerNotifyNode(engine, ctx);
2780
2878
  registerWaitNode(engine, ctx);
2781
2879
  registerSubflowNode(engine, ctx);
2880
+ registerMapNode(engine, ctx);
2782
2881
  const types = engine.getRegisteredNodeTypes();
2783
2882
  ctx.logger.info(
2784
2883
  `[Automation] ${types.length} built-in node executors installed: ${types.join(", ")}`