@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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/service-automation@3.0.7 build /home/runner/work/spec/spec/packages/services/service-automation
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
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- CJS dist/index.cjs 12.25 KB
14
- CJS dist/index.cjs.map 28.34 KB
15
- CJS ⚡️ Build success in 59ms
16
- ESM dist/index.js 11.02 KB
17
- ESM dist/index.js.map 27.68 KB
18
- ESM ⚡️ Build success in 60ms
13
+ CJS dist/index.cjs 26.75 KB
14
+ CJS dist/index.cjs.map 57.83 KB
15
+ CJS ⚡️ Build success in 107ms
16
+ ESM dist/index.js 25.53 KB
17
+ ESM dist/index.js.map 57.16 KB
18
+ ESM ⚡️ Build success in 114ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 7223ms
21
- DTS dist/index.d.ts 4.96 KB
22
- DTS dist/index.d.cts 4.96 KB
20
+ DTS ⚡️ Build success in 14047ms
21
+ DTS dist/index.d.ts 8.00 KB
22
+ DTS dist/index.d.cts 8.00 KB
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.logger.info(`Flow registered: ${name}`);
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
- await this.executeNode(startNode, flow, variables, context ?? {});
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: Date.now() - startTime
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: Date.now() - startTime
223
+ durationMs
130
224
  };
131
225
  }
132
226
  }
133
227
  // ── DAG Traversal Core ──────────────────────────────────
134
- async executeNode(node, flow, variables, context) {
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
- const result = await executor.execute(node, variables, context);
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
- throw new Error(`Node '${node.id}' failed: ${result.error}`);
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((e) => e.source === node.id);
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 && !this.evaluateCondition(edge.condition, variables)) {
155
- continue;
422
+ if (edge.condition) {
423
+ conditionalEdges.push(edge);
424
+ } else {
425
+ unconditionalEdges.push(edge);
156
426
  }
157
- const nextNode = flow.nodes.find((n) => n.id === edge.target);
158
- if (nextNode) {
159
- await this.executeNode(nextNode, flow, variables, context);
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
- return new Function(`return (${resolved})`)();
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 delay = errorHandling.retryDelayMs ?? 1e3;
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.execute(flowName, context);
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
- let expr = cond.expression;
281
- for (const [k, v] of variables) {
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" };