@objectstack/service-automation 9.2.0 → 9.4.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 +76 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -2
- package/dist/index.d.ts +59 -2
- package/dist/index.js +76 -3
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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";
|