@objectstack/service-automation 9.2.0 → 9.3.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.d.cts CHANGED
@@ -262,6 +262,13 @@ interface SuspendedRunStore {
262
262
  list(): Promise<SuspendedRun[]>;
263
263
  }
264
264
  declare class AutomationEngine implements IAutomationService {
265
+ /**
266
+ * ADR-0044: maximum times a single node may be (re-)entered at the top
267
+ * level of one run before the engine aborts it as a runaway back-edge
268
+ * loop. Generous on purpose — the product guard (`maxRevisions`) sits
269
+ * orders of magnitude lower.
270
+ */
271
+ static readonly MAX_NODE_REENTRIES = 100;
265
272
  private flows;
266
273
  private flowEnabled;
267
274
  private flowVersionHistory;
@@ -355,7 +362,7 @@ declare class AutomationEngine implements IAutomationService {
355
362
  unregisterTrigger(type: string): void;
356
363
  /**
357
364
  * Derive a flow's trigger binding from its `start` node, or `undefined` if
358
- * the flow has no auto-trigger (manual / screen / api). The convention —
365
+ * the flow has no auto-trigger (manual / screen). The convention —
359
366
  * established by the showcase flows — is that the start node carries the
360
367
  * trigger details in its `config`: `{ objectName, triggerType, condition }`
361
368
  * for record-change, or a `schedule` descriptor for time-based flows.
@@ -476,6 +483,16 @@ declare class AutomationEngine implements IAutomationService {
476
483
  * descendant fails — the ancestor awaiting it can never be resumed.
477
484
  */
478
485
  private failSuspendedRun;
486
+ /**
487
+ * Cancel a suspended run (ADR-0044): consume its continuation and record a
488
+ * terminal `cancelled` log so it stops surfacing as resumable. The
489
+ * engine-level primitive behind "the submitter abandoned the revision
490
+ * window" — recalling there leaves the run paused at a wait node with no
491
+ * reject edge to resume down, so the run must end, not continue. Returns
492
+ * `false` when no suspended run exists under the id (already terminal /
493
+ * unknown), which callers treat as idempotent success.
494
+ */
495
+ cancelRun(runId: string, reason?: string): Promise<boolean>;
479
496
  /**
480
497
  * Walk a failed run's `$parentRunId` chain and fail each suspended
481
498
  * ancestor (see {@link failSuspendedRun}). Bounded so a corrupt context
@@ -541,6 +558,13 @@ declare class AutomationEngine implements IAutomationService {
541
558
  * Detect cycles in the flow graph (DAG validation).
542
559
  * Uses DFS with coloring (white/gray/black) to detect back edges.
543
560
  * Throws an error with cycle details if a cycle is found.
561
+ *
562
+ * ADR-0044: edges explicitly typed `back` (declared back-edges — e.g. a
563
+ * revise/rework loop re-entering an approval node) are excluded from the
564
+ * analysis: the graph **minus `back` edges** must be a DAG. An unmarked
565
+ * cycle is still rejected — authors opt in edge by edge. At run time a
566
+ * `back` edge traverses like any default edge; the re-entry runaway guard
567
+ * lives in {@link executeNode}.
544
568
  */
545
569
  private detectCycles;
546
570
  /**
@@ -1135,6 +1159,37 @@ declare const SysAutomationRun: Omit<{
1135
1159
  }[] | undefined;
1136
1160
  searchableFields?: string[] | undefined;
1137
1161
  filterableFields?: string[] | undefined;
1162
+ userFilters?: {
1163
+ element: "toggle" | "tabs" | "dropdown";
1164
+ fields?: {
1165
+ field: string;
1166
+ label?: string | undefined;
1167
+ type?: "boolean" | "text" | "select" | "multi-select" | "date-range" | undefined;
1168
+ options?: {
1169
+ value: string | number | boolean;
1170
+ label: string;
1171
+ color?: string | undefined;
1172
+ }[] | undefined;
1173
+ showCount?: boolean | undefined;
1174
+ defaultValues?: (string | number | boolean)[] | undefined;
1175
+ }[] | undefined;
1176
+ tabs?: {
1177
+ name: string;
1178
+ pinned: boolean;
1179
+ isDefault: boolean;
1180
+ visible: boolean;
1181
+ label?: string | undefined;
1182
+ icon?: string | undefined;
1183
+ view?: string | undefined;
1184
+ filter?: {
1185
+ field: string;
1186
+ operator: string;
1187
+ value?: string | number | boolean | (string | number)[] | null | undefined;
1188
+ }[] | undefined;
1189
+ order?: number | undefined;
1190
+ }[] | undefined;
1191
+ showAllRecords?: boolean | undefined;
1192
+ } | undefined;
1138
1193
  resizable?: boolean | undefined;
1139
1194
  striped?: boolean | undefined;
1140
1195
  bordered?: boolean | undefined;
@@ -1247,7 +1302,7 @@ declare const SysAutomationRun: Omit<{
1247
1302
  } | undefined;
1248
1303
  appearance?: {
1249
1304
  showDescription: boolean;
1250
- allowedVisualizations?: ("map" | "grid" | "kanban" | "calendar" | "gantt" | "gallery" | "timeline")[] | undefined;
1305
+ allowedVisualizations?: ("map" | "grid" | "kanban" | "calendar" | "gantt" | "gallery" | "timeline" | "chart")[] | undefined;
1251
1306
  } | undefined;
1252
1307
  tabs?: {
1253
1308
  name: string;
@@ -1441,6 +1496,8 @@ declare const SysAutomationRun: Omit<{
1441
1496
  method?: "POST" | "PATCH" | "PUT" | "DELETE" | undefined;
1442
1497
  bodyExtra?: Record<string, unknown> | undefined;
1443
1498
  mode?: "custom" | "delete" | "edit" | "create" | undefined;
1499
+ opensInNewTab?: boolean | undefined;
1500
+ newTabUrl?: string | undefined;
1444
1501
  timeout?: number | undefined;
1445
1502
  aria?: {
1446
1503
  ariaLabel?: string | undefined;
package/dist/index.d.ts CHANGED
@@ -262,6 +262,13 @@ interface SuspendedRunStore {
262
262
  list(): Promise<SuspendedRun[]>;
263
263
  }
264
264
  declare class AutomationEngine implements IAutomationService {
265
+ /**
266
+ * ADR-0044: maximum times a single node may be (re-)entered at the top
267
+ * level of one run before the engine aborts it as a runaway back-edge
268
+ * loop. Generous on purpose — the product guard (`maxRevisions`) sits
269
+ * orders of magnitude lower.
270
+ */
271
+ static readonly MAX_NODE_REENTRIES = 100;
265
272
  private flows;
266
273
  private flowEnabled;
267
274
  private flowVersionHistory;
@@ -355,7 +362,7 @@ declare class AutomationEngine implements IAutomationService {
355
362
  unregisterTrigger(type: string): void;
356
363
  /**
357
364
  * Derive a flow's trigger binding from its `start` node, or `undefined` if
358
- * the flow has no auto-trigger (manual / screen / api). The convention —
365
+ * the flow has no auto-trigger (manual / screen). The convention —
359
366
  * established by the showcase flows — is that the start node carries the
360
367
  * trigger details in its `config`: `{ objectName, triggerType, condition }`
361
368
  * for record-change, or a `schedule` descriptor for time-based flows.
@@ -476,6 +483,16 @@ declare class AutomationEngine implements IAutomationService {
476
483
  * descendant fails — the ancestor awaiting it can never be resumed.
477
484
  */
478
485
  private failSuspendedRun;
486
+ /**
487
+ * Cancel a suspended run (ADR-0044): consume its continuation and record a
488
+ * terminal `cancelled` log so it stops surfacing as resumable. The
489
+ * engine-level primitive behind "the submitter abandoned the revision
490
+ * window" — recalling there leaves the run paused at a wait node with no
491
+ * reject edge to resume down, so the run must end, not continue. Returns
492
+ * `false` when no suspended run exists under the id (already terminal /
493
+ * unknown), which callers treat as idempotent success.
494
+ */
495
+ cancelRun(runId: string, reason?: string): Promise<boolean>;
479
496
  /**
480
497
  * Walk a failed run's `$parentRunId` chain and fail each suspended
481
498
  * ancestor (see {@link failSuspendedRun}). Bounded so a corrupt context
@@ -541,6 +558,13 @@ declare class AutomationEngine implements IAutomationService {
541
558
  * Detect cycles in the flow graph (DAG validation).
542
559
  * Uses DFS with coloring (white/gray/black) to detect back edges.
543
560
  * Throws an error with cycle details if a cycle is found.
561
+ *
562
+ * ADR-0044: edges explicitly typed `back` (declared back-edges — e.g. a
563
+ * revise/rework loop re-entering an approval node) are excluded from the
564
+ * analysis: the graph **minus `back` edges** must be a DAG. An unmarked
565
+ * cycle is still rejected — authors opt in edge by edge. At run time a
566
+ * `back` edge traverses like any default edge; the re-entry runaway guard
567
+ * lives in {@link executeNode}.
544
568
  */
545
569
  private detectCycles;
546
570
  /**
@@ -1135,6 +1159,37 @@ declare const SysAutomationRun: Omit<{
1135
1159
  }[] | undefined;
1136
1160
  searchableFields?: string[] | undefined;
1137
1161
  filterableFields?: string[] | undefined;
1162
+ userFilters?: {
1163
+ element: "toggle" | "tabs" | "dropdown";
1164
+ fields?: {
1165
+ field: string;
1166
+ label?: string | undefined;
1167
+ type?: "boolean" | "text" | "select" | "multi-select" | "date-range" | undefined;
1168
+ options?: {
1169
+ value: string | number | boolean;
1170
+ label: string;
1171
+ color?: string | undefined;
1172
+ }[] | undefined;
1173
+ showCount?: boolean | undefined;
1174
+ defaultValues?: (string | number | boolean)[] | undefined;
1175
+ }[] | undefined;
1176
+ tabs?: {
1177
+ name: string;
1178
+ pinned: boolean;
1179
+ isDefault: boolean;
1180
+ visible: boolean;
1181
+ label?: string | undefined;
1182
+ icon?: string | undefined;
1183
+ view?: string | undefined;
1184
+ filter?: {
1185
+ field: string;
1186
+ operator: string;
1187
+ value?: string | number | boolean | (string | number)[] | null | undefined;
1188
+ }[] | undefined;
1189
+ order?: number | undefined;
1190
+ }[] | undefined;
1191
+ showAllRecords?: boolean | undefined;
1192
+ } | undefined;
1138
1193
  resizable?: boolean | undefined;
1139
1194
  striped?: boolean | undefined;
1140
1195
  bordered?: boolean | undefined;
@@ -1247,7 +1302,7 @@ declare const SysAutomationRun: Omit<{
1247
1302
  } | undefined;
1248
1303
  appearance?: {
1249
1304
  showDescription: boolean;
1250
- allowedVisualizations?: ("map" | "grid" | "kanban" | "calendar" | "gantt" | "gallery" | "timeline")[] | undefined;
1305
+ allowedVisualizations?: ("map" | "grid" | "kanban" | "calendar" | "gantt" | "gallery" | "timeline" | "chart")[] | undefined;
1251
1306
  } | undefined;
1252
1307
  tabs?: {
1253
1308
  name: string;
@@ -1441,6 +1496,8 @@ declare const SysAutomationRun: Omit<{
1441
1496
  method?: "POST" | "PATCH" | "PUT" | "DELETE" | undefined;
1442
1497
  bodyExtra?: Record<string, unknown> | undefined;
1443
1498
  mode?: "custom" | "delete" | "edit" | "create" | undefined;
1499
+ opensInNewTab?: boolean | undefined;
1500
+ newTabUrl?: string | undefined;
1444
1501
  timeout?: number | undefined;
1445
1502
  aria?: {
1446
1503
  ariaLabel?: string | undefined;
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ var FlowSuspendSignal = class {
13
13
  function isSuspendSignal(err) {
14
14
  return typeof err === "object" && err !== null && err.__flowSuspend === true;
15
15
  }
16
- var AutomationEngine = class {
16
+ var _AutomationEngine = class _AutomationEngine {
17
17
  constructor(logger, store) {
18
18
  this.flows = /* @__PURE__ */ new Map();
19
19
  this.flowEnabled = /* @__PURE__ */ new Map();
@@ -208,7 +208,7 @@ var AutomationEngine = class {
208
208
  }
209
209
  /**
210
210
  * Derive a flow's trigger binding from its `start` node, or `undefined` if
211
- * the flow has no auto-trigger (manual / screen / api). The convention —
211
+ * the flow has no auto-trigger (manual / screen). The convention —
212
212
  * established by the showcase flows — is that the start node carries the
213
213
  * trigger details in its `config`: `{ objectName, triggerType, condition }`
214
214
  * for record-change, or a `schedule` descriptor for time-based flows.
@@ -237,6 +237,12 @@ var AutomationEngine = class {
237
237
  binding: { flowName, schedule: config.schedule, condition: config.condition ?? void 0, config }
238
238
  };
239
239
  }
240
+ if (flow.type === "api" || triggerType === "api") {
241
+ return {
242
+ triggerType: "api",
243
+ binding: { flowName, condition: config.condition ?? void 0, config }
244
+ };
245
+ }
240
246
  return void 0;
241
247
  }
242
248
  /**
@@ -820,6 +826,46 @@ var AutomationEngine = class {
820
826
  error
821
827
  });
822
828
  }
829
+ /**
830
+ * Cancel a suspended run (ADR-0044): consume its continuation and record a
831
+ * terminal `cancelled` log so it stops surfacing as resumable. The
832
+ * engine-level primitive behind "the submitter abandoned the revision
833
+ * window" — recalling there leaves the run paused at a wait node with no
834
+ * reject edge to resume down, so the run must end, not continue. Returns
835
+ * `false` when no suspended run exists under the id (already terminal /
836
+ * unknown), which callers treat as idempotent success.
837
+ */
838
+ async cancelRun(runId, reason) {
839
+ let run = this.suspendedRuns.get(runId) ?? null;
840
+ if (!run && this.store) {
841
+ try {
842
+ run = await this.store.load(runId);
843
+ } catch (err) {
844
+ this.logger.warn(
845
+ `[automation] cancelRun: failed to load suspended run '${runId}' from durable store: ${err.message}`
846
+ );
847
+ }
848
+ }
849
+ if (!run) return false;
850
+ await this.forgetSuspendedRun(runId);
851
+ this.recordLog({
852
+ id: run.runId,
853
+ flowName: run.flowName,
854
+ flowVersion: run.flowVersion,
855
+ status: "cancelled",
856
+ startedAt: run.startedAt,
857
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
858
+ durationMs: Date.now() - run.startTime,
859
+ trigger: {
860
+ type: run.context?.event ?? "manual",
861
+ userId: run.context?.userId,
862
+ object: run.context?.object
863
+ },
864
+ steps: run.steps,
865
+ error: reason
866
+ });
867
+ return true;
868
+ }
823
869
  /**
824
870
  * Walk a failed run's `$parentRunId` chain and fail each suspended
825
871
  * ancestor (see {@link failSuspendedRun}). Bounded so a corrupt context
@@ -951,6 +997,13 @@ ${failures.join("\n")}`
951
997
  * Detect cycles in the flow graph (DAG validation).
952
998
  * Uses DFS with coloring (white/gray/black) to detect back edges.
953
999
  * Throws an error with cycle details if a cycle is found.
1000
+ *
1001
+ * ADR-0044: edges explicitly typed `back` (declared back-edges — e.g. a
1002
+ * revise/rework loop re-entering an approval node) are excluded from the
1003
+ * analysis: the graph **minus `back` edges** must be a DAG. An unmarked
1004
+ * cycle is still rejected — authors opt in edge by edge. At run time a
1005
+ * `back` edge traverses like any default edge; the re-entry runaway guard
1006
+ * lives in {@link executeNode}.
954
1007
  */
955
1008
  detectCycles(flow) {
956
1009
  const WHITE = 0, GRAY = 1, BLACK = 2;
@@ -962,6 +1015,7 @@ ${failures.join("\n")}`
962
1015
  adj.set(node.id, []);
963
1016
  }
964
1017
  for (const edge of flow.edges) {
1018
+ if (edge.type === "back") continue;
965
1019
  const targets = adj.get(edge.source);
966
1020
  if (targets) targets.push(edge.target);
967
1021
  }
@@ -991,7 +1045,9 @@ ${failures.join("\n")}`
991
1045
  if (color.get(node.id) === WHITE) {
992
1046
  const cycle = dfs(node.id);
993
1047
  if (cycle) {
994
- throw new Error(`Flow contains a cycle: ${cycle.join(" \u2192 ")}. Only DAG flows are allowed.`);
1048
+ throw new Error(
1049
+ `Flow contains a cycle: ${cycle.join(" \u2192 ")}. Only DAG flows are allowed \u2014 to author an intentional rework loop, mark the cycle-closing edge with type: 'back' (ADR-0044).`
1050
+ );
995
1051
  }
996
1052
  }
997
1053
  }
@@ -1035,6 +1091,15 @@ ${failures.join("\n")}`
1035
1091
  */
1036
1092
  async executeNode(node, flow, variables, context, steps) {
1037
1093
  if (node.type === "end") return;
1094
+ const priorVisits = steps.reduce(
1095
+ (n, s) => s.nodeId === node.id && s.parentNodeId === void 0 ? n + 1 : n,
1096
+ 0
1097
+ );
1098
+ if (priorVisits >= _AutomationEngine.MAX_NODE_REENTRIES) {
1099
+ throw new Error(
1100
+ `Node '${node.id}' was entered ${priorVisits} times in one run \u2014 aborting as a runaway loop (back-edge cycles must terminate; see ADR-0044)`
1101
+ );
1102
+ }
1038
1103
  const stepStart = Date.now();
1039
1104
  const stepStartedAt = (/* @__PURE__ */ new Date()).toISOString();
1040
1105
  const executor = this.nodeExecutors.get(node.type);
@@ -1460,6 +1525,14 @@ ${failures.join("\n")}`
1460
1525
  }
1461
1526
  }
1462
1527
  };
1528
+ /**
1529
+ * ADR-0044: maximum times a single node may be (re-)entered at the top
1530
+ * level of one run before the engine aborts it as a runaway back-edge
1531
+ * loop. Generous on purpose — the product guard (`maxRevisions`) sits
1532
+ * orders of magnitude lower.
1533
+ */
1534
+ _AutomationEngine.MAX_NODE_REENTRIES = 100;
1535
+ var AutomationEngine = _AutomationEngine;
1463
1536
 
1464
1537
  // src/suspended-run-store.ts
1465
1538
  var TABLE = "sys_automation_run";