@objectstack/service-automation 3.0.7 → 3.0.9
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +17 -0
- package/dist/index.cjs +467 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -2
- package/dist/index.d.ts +102 -2
- package/dist/index.js +467 -26
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/engine.test.ts +817 -0
- package/src/engine.ts +574 -24
- package/src/plugins/logic-nodes-plugin.ts +2 -13
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/service-automation@3.0.
|
|
2
|
+
> @objectstack/service-automation@3.0.9 build /home/runner/work/spec/spec/packages/services/service-automation
|
|
3
3
|
> tsup --config ../../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[32mCJS[39m [1mdist/index.cjs [22m[
|
|
14
|
-
[32mCJS[39m [1mdist/index.cjs.map [22m[
|
|
15
|
-
[32mCJS[39m ⚡️ Build success in
|
|
16
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
17
|
-
[32mESM[39m [1mdist/index.js.map [22m[
|
|
18
|
-
[32mESM[39m ⚡️ Build success in
|
|
13
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m26.75 KB[39m
|
|
14
|
+
[32mCJS[39m [1mdist/index.cjs.map [22m[32m57.83 KB[39m
|
|
15
|
+
[32mCJS[39m ⚡️ Build success in 107ms
|
|
16
|
+
[32mESM[39m [1mdist/index.js [22m[32m25.53 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m57.16 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 114ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.cts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 14047ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m8.00 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.cts [22m[32m8.00 KB[39m
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @objectstack/service-automation
|
|
2
|
+
|
|
3
|
+
## 3.0.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [15e0df6]
|
|
8
|
+
- @objectstack/spec@3.0.9
|
|
9
|
+
- @objectstack/core@3.0.9
|
|
10
|
+
|
|
11
|
+
## 3.0.8
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [5a968a2]
|
|
16
|
+
- @objectstack/spec@3.0.8
|
|
17
|
+
- @objectstack/core@3.0.8
|
package/dist/index.cjs
CHANGED
|
@@ -33,8 +33,13 @@ var import_automation = require("@objectstack/spec/automation");
|
|
|
33
33
|
var AutomationEngine = class {
|
|
34
34
|
constructor(logger) {
|
|
35
35
|
this.flows = /* @__PURE__ */ new Map();
|
|
36
|
+
this.flowEnabled = /* @__PURE__ */ new Map();
|
|
37
|
+
this.flowVersionHistory = /* @__PURE__ */ new Map();
|
|
36
38
|
this.nodeExecutors = /* @__PURE__ */ new Map();
|
|
37
39
|
this.triggers = /* @__PURE__ */ new Map();
|
|
40
|
+
this.executionLogs = [];
|
|
41
|
+
this.maxLogSize = 1e3;
|
|
42
|
+
this.runCounter = 0;
|
|
38
43
|
this.logger = logger;
|
|
39
44
|
}
|
|
40
45
|
// ── Plugin Extension API ──────────────────────────────
|
|
@@ -72,22 +77,73 @@ var AutomationEngine = class {
|
|
|
72
77
|
// ── IAutomationService Contract Implementation ────────
|
|
73
78
|
registerFlow(name, definition) {
|
|
74
79
|
const parsed = import_automation.FlowSchema.parse(definition);
|
|
80
|
+
this.detectCycles(parsed);
|
|
81
|
+
const history = this.flowVersionHistory.get(name) ?? [];
|
|
82
|
+
history.push({
|
|
83
|
+
version: parsed.version,
|
|
84
|
+
definition: parsed,
|
|
85
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
86
|
+
});
|
|
87
|
+
this.flowVersionHistory.set(name, history);
|
|
75
88
|
this.flows.set(name, parsed);
|
|
76
|
-
this.
|
|
89
|
+
if (!this.flowEnabled.has(name)) {
|
|
90
|
+
this.flowEnabled.set(name, true);
|
|
91
|
+
}
|
|
92
|
+
this.logger.info(`Flow registered: ${name} (version ${parsed.version})`);
|
|
77
93
|
}
|
|
78
94
|
unregisterFlow(name) {
|
|
79
95
|
this.flows.delete(name);
|
|
96
|
+
this.flowEnabled.delete(name);
|
|
97
|
+
this.flowVersionHistory.delete(name);
|
|
80
98
|
this.logger.info(`Flow unregistered: ${name}`);
|
|
81
99
|
}
|
|
82
100
|
async listFlows() {
|
|
83
101
|
return [...this.flows.keys()];
|
|
84
102
|
}
|
|
103
|
+
async getFlow(name) {
|
|
104
|
+
return this.flows.get(name) ?? null;
|
|
105
|
+
}
|
|
106
|
+
async toggleFlow(name, enabled) {
|
|
107
|
+
if (!this.flows.has(name)) {
|
|
108
|
+
throw new Error(`Flow '${name}' not found`);
|
|
109
|
+
}
|
|
110
|
+
this.flowEnabled.set(name, enabled);
|
|
111
|
+
this.logger.info(`Flow '${name}' ${enabled ? "enabled" : "disabled"}`);
|
|
112
|
+
}
|
|
113
|
+
/** Get flow version history */
|
|
114
|
+
getFlowVersionHistory(name) {
|
|
115
|
+
return this.flowVersionHistory.get(name) ?? [];
|
|
116
|
+
}
|
|
117
|
+
/** Rollback flow to a specific version */
|
|
118
|
+
rollbackFlow(name, version) {
|
|
119
|
+
const history = this.flowVersionHistory.get(name);
|
|
120
|
+
if (!history) {
|
|
121
|
+
throw new Error(`Flow '${name}' has no version history`);
|
|
122
|
+
}
|
|
123
|
+
const entry = history.find((h) => h.version === version);
|
|
124
|
+
if (!entry) {
|
|
125
|
+
throw new Error(`Version ${version} not found for flow '${name}'`);
|
|
126
|
+
}
|
|
127
|
+
this.flows.set(name, entry.definition);
|
|
128
|
+
this.logger.info(`Flow '${name}' rolled back to version ${version}`);
|
|
129
|
+
}
|
|
130
|
+
async listRuns(flowName, options) {
|
|
131
|
+
const limit = options?.limit ?? 20;
|
|
132
|
+
const logs = this.executionLogs.filter((l) => l.flowName === flowName);
|
|
133
|
+
return logs.slice(-limit).reverse();
|
|
134
|
+
}
|
|
135
|
+
async getRun(runId) {
|
|
136
|
+
return this.executionLogs.find((l) => l.id === runId) ?? null;
|
|
137
|
+
}
|
|
85
138
|
async execute(flowName, context) {
|
|
86
139
|
const startTime = Date.now();
|
|
87
140
|
const flow = this.flows.get(flowName);
|
|
88
141
|
if (!flow) {
|
|
89
142
|
return { success: false, error: `Flow '${flowName}' not found` };
|
|
90
143
|
}
|
|
144
|
+
if (this.flowEnabled.get(flowName) === false) {
|
|
145
|
+
return { success: false, error: `Flow '${flowName}' is disabled` };
|
|
146
|
+
}
|
|
91
147
|
const variables = /* @__PURE__ */ new Map();
|
|
92
148
|
if (flow.variables) {
|
|
93
149
|
for (const v of flow.variables) {
|
|
@@ -99,12 +155,16 @@ var AutomationEngine = class {
|
|
|
99
155
|
if (context?.record) {
|
|
100
156
|
variables.set("$record", context.record);
|
|
101
157
|
}
|
|
158
|
+
const runId = `run_${++this.runCounter}`;
|
|
159
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
160
|
+
const steps = [];
|
|
102
161
|
try {
|
|
103
162
|
const startNode = flow.nodes.find((n) => n.type === "start");
|
|
104
163
|
if (!startNode) {
|
|
105
164
|
return { success: false, error: "Flow has no start node" };
|
|
106
165
|
}
|
|
107
|
-
|
|
166
|
+
this.validateNodeInputSchemas(flow, variables);
|
|
167
|
+
await this.executeNode(startNode, flow, variables, context ?? {}, steps);
|
|
108
168
|
const output = {};
|
|
109
169
|
if (flow.variables) {
|
|
110
170
|
for (const v of flow.variables) {
|
|
@@ -113,73 +173,461 @@ var AutomationEngine = class {
|
|
|
113
173
|
}
|
|
114
174
|
}
|
|
115
175
|
}
|
|
176
|
+
const durationMs = Date.now() - startTime;
|
|
177
|
+
this.recordLog({
|
|
178
|
+
id: runId,
|
|
179
|
+
flowName,
|
|
180
|
+
flowVersion: flow.version,
|
|
181
|
+
status: "completed",
|
|
182
|
+
startedAt,
|
|
183
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
184
|
+
durationMs,
|
|
185
|
+
trigger: {
|
|
186
|
+
type: context?.event ?? "manual",
|
|
187
|
+
userId: context?.userId,
|
|
188
|
+
object: context?.object
|
|
189
|
+
},
|
|
190
|
+
steps,
|
|
191
|
+
output
|
|
192
|
+
});
|
|
116
193
|
return {
|
|
117
194
|
success: true,
|
|
118
195
|
output,
|
|
119
|
-
durationMs
|
|
196
|
+
durationMs
|
|
120
197
|
};
|
|
121
198
|
} catch (err) {
|
|
122
199
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
200
|
+
const durationMs = Date.now() - startTime;
|
|
201
|
+
this.recordLog({
|
|
202
|
+
id: runId,
|
|
203
|
+
flowName,
|
|
204
|
+
flowVersion: flow.version,
|
|
205
|
+
status: "failed",
|
|
206
|
+
startedAt,
|
|
207
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
208
|
+
durationMs,
|
|
209
|
+
trigger: {
|
|
210
|
+
type: context?.event ?? "manual",
|
|
211
|
+
userId: context?.userId,
|
|
212
|
+
object: context?.object
|
|
213
|
+
},
|
|
214
|
+
steps,
|
|
215
|
+
error: errorMessage
|
|
216
|
+
});
|
|
123
217
|
if (flow.errorHandling?.strategy === "retry") {
|
|
124
218
|
return this.retryExecution(flowName, context, startTime, flow.errorHandling);
|
|
125
219
|
}
|
|
126
220
|
return {
|
|
127
221
|
success: false,
|
|
128
222
|
error: errorMessage,
|
|
129
|
-
durationMs
|
|
223
|
+
durationMs
|
|
130
224
|
};
|
|
131
225
|
}
|
|
132
226
|
}
|
|
133
227
|
// ── DAG Traversal Core ──────────────────────────────────
|
|
134
|
-
|
|
228
|
+
recordLog(entry) {
|
|
229
|
+
this.executionLogs.push(entry);
|
|
230
|
+
if (this.executionLogs.length > this.maxLogSize) {
|
|
231
|
+
this.executionLogs.splice(0, this.executionLogs.length - this.maxLogSize);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Detect cycles in the flow graph (DAG validation).
|
|
236
|
+
* Uses DFS with coloring (white/gray/black) to detect back edges.
|
|
237
|
+
* Throws an error with cycle details if a cycle is found.
|
|
238
|
+
*/
|
|
239
|
+
detectCycles(flow) {
|
|
240
|
+
const WHITE = 0, GRAY = 1, BLACK = 2;
|
|
241
|
+
const color = /* @__PURE__ */ new Map();
|
|
242
|
+
const parent = /* @__PURE__ */ new Map();
|
|
243
|
+
const adj = /* @__PURE__ */ new Map();
|
|
244
|
+
for (const node of flow.nodes) {
|
|
245
|
+
color.set(node.id, WHITE);
|
|
246
|
+
adj.set(node.id, []);
|
|
247
|
+
}
|
|
248
|
+
for (const edge of flow.edges) {
|
|
249
|
+
const targets = adj.get(edge.source);
|
|
250
|
+
if (targets) targets.push(edge.target);
|
|
251
|
+
}
|
|
252
|
+
const dfs = (nodeId) => {
|
|
253
|
+
color.set(nodeId, GRAY);
|
|
254
|
+
for (const neighbor of adj.get(nodeId) ?? []) {
|
|
255
|
+
if (color.get(neighbor) === GRAY) {
|
|
256
|
+
const cycle = [neighbor, nodeId];
|
|
257
|
+
let cur = nodeId;
|
|
258
|
+
while (cur !== neighbor) {
|
|
259
|
+
cur = parent.get(cur);
|
|
260
|
+
if (cur) cycle.push(cur);
|
|
261
|
+
else break;
|
|
262
|
+
}
|
|
263
|
+
return cycle.reverse();
|
|
264
|
+
}
|
|
265
|
+
if (color.get(neighbor) === WHITE) {
|
|
266
|
+
parent.set(neighbor, nodeId);
|
|
267
|
+
const result = dfs(neighbor);
|
|
268
|
+
if (result) return result;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
color.set(nodeId, BLACK);
|
|
272
|
+
return null;
|
|
273
|
+
};
|
|
274
|
+
for (const node of flow.nodes) {
|
|
275
|
+
if (color.get(node.id) === WHITE) {
|
|
276
|
+
const cycle = dfs(node.id);
|
|
277
|
+
if (cycle) {
|
|
278
|
+
throw new Error(`Flow contains a cycle: ${cycle.join(" \u2192 ")}. Only DAG flows are allowed.`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Get the runtime type name of a value for schema validation.
|
|
285
|
+
*/
|
|
286
|
+
getValueType(value) {
|
|
287
|
+
if (Array.isArray(value)) return "array";
|
|
288
|
+
if (typeof value === "object" && value !== null) return "object";
|
|
289
|
+
return typeof value;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Validate node input schemas before execution.
|
|
293
|
+
* Checks that node config matches declared inputSchema if present.
|
|
294
|
+
*/
|
|
295
|
+
validateNodeInputSchemas(flow, _variables) {
|
|
296
|
+
for (const node of flow.nodes) {
|
|
297
|
+
if (node.inputSchema && node.config) {
|
|
298
|
+
for (const [paramName, paramDef] of Object.entries(node.inputSchema)) {
|
|
299
|
+
if (paramDef.required && !(paramName in node.config)) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`Node '${node.id}' missing required input parameter '${paramName}'`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const value = node.config[paramName];
|
|
305
|
+
if (value !== void 0) {
|
|
306
|
+
const actualType = this.getValueType(value);
|
|
307
|
+
if (actualType !== paramDef.type) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Node '${node.id}' parameter '${paramName}' expected type '${paramDef.type}' but got '${actualType}'`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Execute a node with timeout support, fault edge handling, and step logging.
|
|
319
|
+
*/
|
|
320
|
+
async executeNode(node, flow, variables, context, steps) {
|
|
135
321
|
if (node.type === "end") return;
|
|
322
|
+
const stepStart = Date.now();
|
|
323
|
+
const stepStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
136
324
|
const executor = this.nodeExecutors.get(node.type);
|
|
137
325
|
if (!executor) {
|
|
138
326
|
if (node.type !== "start") {
|
|
327
|
+
steps.push({
|
|
328
|
+
nodeId: node.id,
|
|
329
|
+
nodeType: node.type,
|
|
330
|
+
status: "failure",
|
|
331
|
+
startedAt: stepStartedAt,
|
|
332
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
333
|
+
durationMs: Date.now() - stepStart,
|
|
334
|
+
error: { code: "NO_EXECUTOR", message: `No executor registered for node type '${node.type}'` }
|
|
335
|
+
});
|
|
139
336
|
throw new Error(`No executor registered for node type '${node.type}'`);
|
|
140
337
|
}
|
|
338
|
+
steps.push({
|
|
339
|
+
nodeId: node.id,
|
|
340
|
+
nodeType: node.type,
|
|
341
|
+
status: "success",
|
|
342
|
+
startedAt: stepStartedAt,
|
|
343
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
344
|
+
durationMs: Date.now() - stepStart
|
|
345
|
+
});
|
|
141
346
|
} else {
|
|
142
|
-
|
|
347
|
+
let result;
|
|
348
|
+
try {
|
|
349
|
+
if (node.timeoutMs && node.timeoutMs > 0) {
|
|
350
|
+
result = await this.executeWithTimeout(
|
|
351
|
+
executor.execute(node, variables, context),
|
|
352
|
+
node.timeoutMs,
|
|
353
|
+
node.id
|
|
354
|
+
);
|
|
355
|
+
} else {
|
|
356
|
+
result = await executor.execute(node, variables, context);
|
|
357
|
+
}
|
|
358
|
+
} catch (execErr) {
|
|
359
|
+
const errMsg = execErr instanceof Error ? execErr.message : String(execErr);
|
|
360
|
+
steps.push({
|
|
361
|
+
nodeId: node.id,
|
|
362
|
+
nodeType: node.type,
|
|
363
|
+
status: "failure",
|
|
364
|
+
startedAt: stepStartedAt,
|
|
365
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
366
|
+
durationMs: Date.now() - stepStart,
|
|
367
|
+
error: { code: "EXECUTION_ERROR", message: errMsg }
|
|
368
|
+
});
|
|
369
|
+
const faultEdge = flow.edges.find((e) => e.source === node.id && e.type === "fault");
|
|
370
|
+
if (faultEdge) {
|
|
371
|
+
variables.set("$error", { nodeId: node.id, message: errMsg });
|
|
372
|
+
const faultTarget = flow.nodes.find((n) => n.id === faultEdge.target);
|
|
373
|
+
if (faultTarget) {
|
|
374
|
+
await this.executeNode(faultTarget, flow, variables, context, steps);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
throw execErr;
|
|
379
|
+
}
|
|
143
380
|
if (!result.success) {
|
|
144
|
-
|
|
381
|
+
const errMsg = result.error ?? "Unknown error";
|
|
382
|
+
steps.push({
|
|
383
|
+
nodeId: node.id,
|
|
384
|
+
nodeType: node.type,
|
|
385
|
+
status: "failure",
|
|
386
|
+
startedAt: stepStartedAt,
|
|
387
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
388
|
+
durationMs: Date.now() - stepStart,
|
|
389
|
+
error: { code: "NODE_FAILURE", message: errMsg }
|
|
390
|
+
});
|
|
391
|
+
variables.set("$error", { nodeId: node.id, message: errMsg, output: result.output });
|
|
392
|
+
const faultEdge = flow.edges.find((e) => e.source === node.id && e.type === "fault");
|
|
393
|
+
if (faultEdge) {
|
|
394
|
+
const faultTarget = flow.nodes.find((n) => n.id === faultEdge.target);
|
|
395
|
+
if (faultTarget) {
|
|
396
|
+
await this.executeNode(faultTarget, flow, variables, context, steps);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
throw new Error(`Node '${node.id}' failed: ${errMsg}`);
|
|
145
401
|
}
|
|
402
|
+
steps.push({
|
|
403
|
+
nodeId: node.id,
|
|
404
|
+
nodeType: node.type,
|
|
405
|
+
status: "success",
|
|
406
|
+
startedAt: stepStartedAt,
|
|
407
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
408
|
+
durationMs: Date.now() - stepStart
|
|
409
|
+
});
|
|
146
410
|
if (result.output) {
|
|
147
411
|
for (const [key, value] of Object.entries(result.output)) {
|
|
148
412
|
variables.set(`${node.id}.${key}`, value);
|
|
149
413
|
}
|
|
150
414
|
}
|
|
151
415
|
}
|
|
152
|
-
const outEdges = flow.edges.filter(
|
|
416
|
+
const outEdges = flow.edges.filter(
|
|
417
|
+
(e) => e.source === node.id && e.type !== "fault"
|
|
418
|
+
);
|
|
419
|
+
const conditionalEdges = [];
|
|
420
|
+
const unconditionalEdges = [];
|
|
153
421
|
for (const edge of outEdges) {
|
|
154
|
-
if (edge.condition
|
|
155
|
-
|
|
422
|
+
if (edge.condition) {
|
|
423
|
+
conditionalEdges.push(edge);
|
|
424
|
+
} else {
|
|
425
|
+
unconditionalEdges.push(edge);
|
|
156
426
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
427
|
+
}
|
|
428
|
+
for (const edge of conditionalEdges) {
|
|
429
|
+
if (this.evaluateCondition(edge.condition, variables)) {
|
|
430
|
+
const nextNode = flow.nodes.find((n) => n.id === edge.target);
|
|
431
|
+
if (nextNode) {
|
|
432
|
+
await this.executeNode(nextNode, flow, variables, context, steps);
|
|
433
|
+
}
|
|
160
434
|
}
|
|
161
435
|
}
|
|
436
|
+
if (unconditionalEdges.length > 0) {
|
|
437
|
+
const parallelTasks = unconditionalEdges.map((edge) => flow.nodes.find((n) => n.id === edge.target)).filter((n) => n != null).map((nextNode) => this.executeNode(nextNode, flow, variables, context, steps));
|
|
438
|
+
await Promise.all(parallelTasks);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Execute a promise with timeout using Promise.race.
|
|
443
|
+
*/
|
|
444
|
+
executeWithTimeout(promise, timeoutMs, nodeId) {
|
|
445
|
+
return Promise.race([
|
|
446
|
+
promise,
|
|
447
|
+
new Promise(
|
|
448
|
+
(_, reject) => setTimeout(() => reject(new Error(`Node '${nodeId}' timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
449
|
+
)
|
|
450
|
+
]);
|
|
162
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Safe expression evaluator.
|
|
454
|
+
* Uses simple operator-based parsing without `new Function`.
|
|
455
|
+
* Supports: comparisons (>, <, >=, <=, ==, !=, ===, !==),
|
|
456
|
+
* boolean literals (true, false), and basic arithmetic.
|
|
457
|
+
*/
|
|
163
458
|
evaluateCondition(expression, variables) {
|
|
164
459
|
let resolved = expression;
|
|
165
460
|
for (const [key, value] of variables) {
|
|
166
461
|
resolved = resolved.split(`{${key}}`).join(String(value));
|
|
167
462
|
}
|
|
463
|
+
resolved = resolved.trim();
|
|
168
464
|
try {
|
|
169
|
-
|
|
465
|
+
if (resolved === "true") return true;
|
|
466
|
+
if (resolved === "false") return false;
|
|
467
|
+
const operators = ["===", "!==", ">=", "<=", "!=", "==", ">", "<"];
|
|
468
|
+
for (const op of operators) {
|
|
469
|
+
const idx = resolved.indexOf(op);
|
|
470
|
+
if (idx !== -1) {
|
|
471
|
+
const left = resolved.slice(0, idx).trim();
|
|
472
|
+
const right = resolved.slice(idx + op.length).trim();
|
|
473
|
+
return this.compareValues(left, op, right);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const numVal = Number(resolved);
|
|
477
|
+
if (!isNaN(numVal)) return numVal !== 0;
|
|
478
|
+
return false;
|
|
170
479
|
} catch {
|
|
171
480
|
return false;
|
|
172
481
|
}
|
|
173
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Compare two string-represented values with an operator.
|
|
485
|
+
*/
|
|
486
|
+
compareValues(left, op, right) {
|
|
487
|
+
const lNum = Number(left);
|
|
488
|
+
const rNum = Number(right);
|
|
489
|
+
const bothNumeric = !isNaN(lNum) && !isNaN(rNum) && left !== "" && right !== "";
|
|
490
|
+
if (bothNumeric) {
|
|
491
|
+
switch (op) {
|
|
492
|
+
case ">":
|
|
493
|
+
return lNum > rNum;
|
|
494
|
+
case "<":
|
|
495
|
+
return lNum < rNum;
|
|
496
|
+
case ">=":
|
|
497
|
+
return lNum >= rNum;
|
|
498
|
+
case "<=":
|
|
499
|
+
return lNum <= rNum;
|
|
500
|
+
case "==":
|
|
501
|
+
case "===":
|
|
502
|
+
return lNum === rNum;
|
|
503
|
+
case "!=":
|
|
504
|
+
case "!==":
|
|
505
|
+
return lNum !== rNum;
|
|
506
|
+
default:
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
switch (op) {
|
|
511
|
+
case "==":
|
|
512
|
+
case "===":
|
|
513
|
+
return left === right;
|
|
514
|
+
case "!=":
|
|
515
|
+
case "!==":
|
|
516
|
+
return left !== right;
|
|
517
|
+
case ">":
|
|
518
|
+
return left > right;
|
|
519
|
+
case "<":
|
|
520
|
+
return left < right;
|
|
521
|
+
case ">=":
|
|
522
|
+
return left >= right;
|
|
523
|
+
case "<=":
|
|
524
|
+
return left <= right;
|
|
525
|
+
default:
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Retry execution with exponential backoff, jitter, and recursive protection.
|
|
531
|
+
* Uses an iterative loop with an internal retry flag to prevent recursive call stacking.
|
|
532
|
+
*/
|
|
174
533
|
async retryExecution(flowName, context, startTime, errorHandling) {
|
|
175
534
|
const maxRetries = errorHandling.maxRetries ?? 3;
|
|
176
|
-
const
|
|
535
|
+
const baseDelay = errorHandling.retryDelayMs ?? 1e3;
|
|
536
|
+
const multiplier = errorHandling.backoffMultiplier ?? 1;
|
|
537
|
+
const maxDelay = errorHandling.maxRetryDelayMs ?? 3e4;
|
|
538
|
+
const useJitter = errorHandling.jitter ?? false;
|
|
539
|
+
let lastError = "Max retries exceeded";
|
|
177
540
|
for (let i = 0; i < maxRetries; i++) {
|
|
541
|
+
let delay = Math.min(baseDelay * Math.pow(multiplier, i), maxDelay);
|
|
542
|
+
if (useJitter) {
|
|
543
|
+
delay = delay * (0.5 + Math.random() * 0.5);
|
|
544
|
+
}
|
|
178
545
|
await new Promise((r) => setTimeout(r, delay));
|
|
179
|
-
const result = await this.
|
|
546
|
+
const result = await this.executeWithoutRetry(flowName, context);
|
|
180
547
|
if (result.success) return result;
|
|
548
|
+
lastError = result.error ?? "Unknown error";
|
|
549
|
+
}
|
|
550
|
+
return { success: false, error: lastError, durationMs: Date.now() - startTime };
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Execute a flow without triggering retry logic (used by retryExecution to prevent recursion).
|
|
554
|
+
*/
|
|
555
|
+
async executeWithoutRetry(flowName, context) {
|
|
556
|
+
const startTime = Date.now();
|
|
557
|
+
const flow = this.flows.get(flowName);
|
|
558
|
+
if (!flow) {
|
|
559
|
+
return { success: false, error: `Flow '${flowName}' not found` };
|
|
560
|
+
}
|
|
561
|
+
if (this.flowEnabled.get(flowName) === false) {
|
|
562
|
+
return { success: false, error: `Flow '${flowName}' is disabled` };
|
|
563
|
+
}
|
|
564
|
+
const variables = /* @__PURE__ */ new Map();
|
|
565
|
+
if (flow.variables) {
|
|
566
|
+
for (const v of flow.variables) {
|
|
567
|
+
if (v.isInput && context?.params?.[v.name] !== void 0) {
|
|
568
|
+
variables.set(v.name, context.params[v.name]);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (context?.record) {
|
|
573
|
+
variables.set("$record", context.record);
|
|
574
|
+
}
|
|
575
|
+
const runId = `run_${++this.runCounter}`;
|
|
576
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
577
|
+
const steps = [];
|
|
578
|
+
try {
|
|
579
|
+
const startNode = flow.nodes.find((n) => n.type === "start");
|
|
580
|
+
if (!startNode) {
|
|
581
|
+
return { success: false, error: "Flow has no start node" };
|
|
582
|
+
}
|
|
583
|
+
await this.executeNode(startNode, flow, variables, context ?? {}, steps);
|
|
584
|
+
const output = {};
|
|
585
|
+
if (flow.variables) {
|
|
586
|
+
for (const v of flow.variables) {
|
|
587
|
+
if (v.isOutput) {
|
|
588
|
+
output[v.name] = variables.get(v.name);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const durationMs = Date.now() - startTime;
|
|
593
|
+
this.recordLog({
|
|
594
|
+
id: runId,
|
|
595
|
+
flowName,
|
|
596
|
+
flowVersion: flow.version,
|
|
597
|
+
status: "completed",
|
|
598
|
+
startedAt,
|
|
599
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
600
|
+
durationMs,
|
|
601
|
+
trigger: {
|
|
602
|
+
type: context?.event ?? "manual",
|
|
603
|
+
userId: context?.userId,
|
|
604
|
+
object: context?.object
|
|
605
|
+
},
|
|
606
|
+
steps,
|
|
607
|
+
output
|
|
608
|
+
});
|
|
609
|
+
return { success: true, output, durationMs };
|
|
610
|
+
} catch (err) {
|
|
611
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
612
|
+
const durationMs = Date.now() - startTime;
|
|
613
|
+
this.recordLog({
|
|
614
|
+
id: runId,
|
|
615
|
+
flowName,
|
|
616
|
+
flowVersion: flow.version,
|
|
617
|
+
status: "failed",
|
|
618
|
+
startedAt,
|
|
619
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
620
|
+
durationMs,
|
|
621
|
+
trigger: {
|
|
622
|
+
type: context?.event ?? "manual",
|
|
623
|
+
userId: context?.userId,
|
|
624
|
+
object: context?.object
|
|
625
|
+
},
|
|
626
|
+
steps,
|
|
627
|
+
error: errorMessage
|
|
628
|
+
});
|
|
629
|
+
return { success: false, error: errorMessage, durationMs };
|
|
181
630
|
}
|
|
182
|
-
return { success: false, error: "Max retries exceeded", durationMs: Date.now() - startTime };
|
|
183
631
|
}
|
|
184
632
|
};
|
|
185
633
|
|
|
@@ -277,15 +725,8 @@ var LogicNodesPlugin = class {
|
|
|
277
725
|
const config = node.config;
|
|
278
726
|
const conditions = config?.conditions ?? [];
|
|
279
727
|
for (const cond of conditions) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
expr = expr.split(`{${k}}`).join(String(v));
|
|
283
|
-
}
|
|
284
|
-
try {
|
|
285
|
-
if (new Function(`return (${expr})`)()) {
|
|
286
|
-
return { success: true, branchLabel: cond.label };
|
|
287
|
-
}
|
|
288
|
-
} catch {
|
|
728
|
+
if (engine.evaluateCondition(cond.expression, variables)) {
|
|
729
|
+
return { success: true, branchLabel: cond.label };
|
|
289
730
|
}
|
|
290
731
|
}
|
|
291
732
|
return { success: true, branchLabel: "default" };
|