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