@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.cjs CHANGED
@@ -496,6 +496,8 @@ var AutomationEngine = class {
496
496
  }
497
497
  const runId = this.nextRunId();
498
498
  variables.set("$runId", runId);
499
+ variables.set("$flowName", flowName);
500
+ variables.set("$flowLabel", flow.label ?? flowName);
499
501
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
500
502
  const steps = [];
501
503
  try {
@@ -704,7 +706,11 @@ var AutomationEngine = class {
704
706
  const steps = run.steps;
705
707
  const context = run.context;
706
708
  try {
707
- await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);
709
+ if (typeof run.correlation === "string" && run.correlation.startsWith("map:")) {
710
+ await this.executeNode(node, flow, variables, context, steps);
711
+ } else {
712
+ await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);
713
+ }
708
714
  const output = {};
709
715
  if (flow.variables) {
710
716
  for (const v of flow.variables) {
@@ -808,10 +814,12 @@ var AutomationEngine = class {
808
814
  * caller who resumed the child.
809
815
  */
810
816
  async bubbleToParent(run, output) {
811
- const parentRunId = run.context?.$parentRunId;
817
+ const ctx = run.context;
818
+ const parentRunId = ctx?.$parentRunId;
812
819
  if (typeof parentRunId !== "string" || !parentRunId) return;
813
820
  try {
814
- const sig = this.buildSubflowResumeSignal(run.context, output);
821
+ const mapNode = ctx?.$parentMapNode;
822
+ const sig = typeof mapNode === "string" && mapNode ? { variables: { [`${mapNode}.$mapItemOutput`]: output ?? null, [`${mapNode}.$mapItemDone`]: true } } : this.buildSubflowResumeSignal(run.context, output);
815
823
  const parentRes = await this.resumeInternal(parentRunId, sig, false);
816
824
  if (!parentRes.success) {
817
825
  this.logger.warn(
@@ -2802,6 +2810,96 @@ function registerSubflowNode(engine, ctx) {
2802
2810
  ctx.logger.info("[Subflow Node] 1 built-in node executor registered");
2803
2811
  }
2804
2812
 
2813
+ // src/builtin/map-node.ts
2814
+ var import_automation13 = require("@objectstack/spec/automation");
2815
+ var MAX_MAP_ITEMS = 1e4;
2816
+ function registerMapNode(engine, ctx) {
2817
+ engine.registerNodeExecutor({
2818
+ type: "map",
2819
+ descriptor: (0, import_automation13.defineActionDescriptor)({
2820
+ type: "map",
2821
+ version: "1.0.0",
2822
+ name: "Map",
2823
+ description: "Run a per-item subflow for each element of a collection, one at a time (each item may pause).",
2824
+ icon: "list-check",
2825
+ category: "logic",
2826
+ source: "builtin",
2827
+ // Each item's subflow may pause, so the map suspends and resumes per item.
2828
+ supportsPause: true,
2829
+ isAsync: true
2830
+ }),
2831
+ async execute(node, variables, context) {
2832
+ const cfg = node.config ?? {};
2833
+ const flowName = typeof cfg.flowName === "string" ? cfg.flowName : typeof cfg.flow === "string" ? cfg.flow : void 0;
2834
+ if (!flowName) {
2835
+ return { success: false, error: `map '${node.id}': config.flowName (the per-item subflow) is required` };
2836
+ }
2837
+ const iteratorVariable = typeof cfg.iteratorVariable === "string" && cfg.iteratorVariable ? cfg.iteratorVariable : "item";
2838
+ const indexVariable = typeof cfg.indexVariable === "string" && cfg.indexVariable ? cfg.indexVariable : void 0;
2839
+ const outVar = typeof cfg.outputVariable === "string" && cfg.outputVariable ? cfg.outputVariable : void 0;
2840
+ const rawCollection = cfg.collection;
2841
+ let collection;
2842
+ if (Array.isArray(rawCollection)) {
2843
+ collection = rawCollection;
2844
+ } else if (typeof rawCollection === "string") {
2845
+ collection = interpolate(rawCollection, variables, context ?? {});
2846
+ if (collection == null && variables.has(rawCollection)) collection = variables.get(rawCollection);
2847
+ }
2848
+ if (!Array.isArray(collection)) {
2849
+ return { success: false, error: `map '${node.id}': collection '${String(rawCollection)}' did not resolve to an array` };
2850
+ }
2851
+ if (collection.length > MAX_MAP_ITEMS) {
2852
+ return { success: false, error: `map '${node.id}': collection length ${collection.length} exceeds the ${MAX_MAP_ITEMS} cap` };
2853
+ }
2854
+ const stateKey = `${node.id}.$mapState`;
2855
+ const state = variables.get(stateKey) ?? {
2856
+ started: 0,
2857
+ results: []
2858
+ };
2859
+ if (variables.get(`${node.id}.$mapItemDone`) === true) {
2860
+ state.results.push(variables.get(`${node.id}.$mapItemOutput`) ?? null);
2861
+ variables.delete(`${node.id}.$mapItemDone`);
2862
+ variables.delete(`${node.id}.$mapItemOutput`);
2863
+ }
2864
+ const parentRunId = variables.get("$runId");
2865
+ while (state.started < collection.length) {
2866
+ const idx = state.started;
2867
+ const item = collection[idx];
2868
+ variables.set(iteratorVariable, item);
2869
+ if (indexVariable) variables.set(indexVariable, idx);
2870
+ const rawInput = cfg.input && typeof cfg.input === "object" ? cfg.input : {};
2871
+ const params = interpolate(rawInput, variables, context ?? {});
2872
+ const itemIsRecord = item != null && typeof item === "object" && typeof item.id === "string";
2873
+ const itemObject = typeof cfg.itemObject === "string" ? cfg.itemObject : context?.object;
2874
+ const childContext = {
2875
+ ...context ?? {},
2876
+ params,
2877
+ ...itemIsRecord ? { record: item, object: itemObject } : {},
2878
+ ...parentRunId != null ? { $parentRunId: String(parentRunId), $parentMapNode: node.id } : {}
2879
+ };
2880
+ const child = await engine.execute(flowName, childContext);
2881
+ if (child.status === "paused") {
2882
+ if (!child.runId) {
2883
+ return { success: false, error: `map '${node.id}': item ${idx} paused without a run id \u2014 cannot link the runs` };
2884
+ }
2885
+ state.started = idx + 1;
2886
+ variables.set(stateKey, state);
2887
+ return { success: true, suspend: true, correlation: `map:${child.runId}` };
2888
+ }
2889
+ if (!child.success) {
2890
+ return { success: false, error: `map '${node.id}': item ${idx} (subflow '${flowName}') failed: ${child.error ?? "unknown error"}` };
2891
+ }
2892
+ state.started = idx + 1;
2893
+ state.results.push(child.output ?? null);
2894
+ }
2895
+ variables.set(stateKey, state);
2896
+ if (outVar) variables.set(outVar, state.results);
2897
+ return { success: true, output: { results: state.results, count: state.results.length } };
2898
+ }
2899
+ });
2900
+ ctx.logger.info("[Map Node] 1 built-in node executor registered");
2901
+ }
2902
+
2805
2903
  // src/builtin/index.ts
2806
2904
  function installBuiltinNodes(engine, ctx) {
2807
2905
  registerLogicNodes(engine, ctx);
@@ -2815,6 +2913,7 @@ function installBuiltinNodes(engine, ctx) {
2815
2913
  registerNotifyNode(engine, ctx);
2816
2914
  registerWaitNode(engine, ctx);
2817
2915
  registerSubflowNode(engine, ctx);
2916
+ registerMapNode(engine, ctx);
2818
2917
  const types = engine.getRegisteredNodeTypes();
2819
2918
  ctx.logger.info(
2820
2919
  `[Automation] ${types.length} built-in node executors installed: ${types.join(", ")}`