@objectstack/service-automation 7.3.0 → 7.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 +968 -300
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +330 -54
- package/dist/index.d.ts +330 -54
- package/dist/index.js +962 -303
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -22,25 +22,51 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AutomationEngine: () => AutomationEngine,
|
|
24
24
|
AutomationServicePlugin: () => AutomationServicePlugin,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
installBuiltinNodes: () => installBuiltinNodes,
|
|
26
|
+
registerConnectorNodes: () => registerConnectorNodes,
|
|
27
|
+
registerCrudNodes: () => registerCrudNodes,
|
|
28
|
+
registerHttpNodes: () => registerHttpNodes,
|
|
29
|
+
registerLogicNodes: () => registerLogicNodes,
|
|
30
|
+
registerScreenNodes: () => registerScreenNodes
|
|
29
31
|
});
|
|
30
32
|
module.exports = __toCommonJS(index_exports);
|
|
31
33
|
|
|
32
34
|
// src/engine.ts
|
|
33
35
|
var import_automation = require("@objectstack/spec/automation");
|
|
36
|
+
var import_integration = require("@objectstack/spec/integration");
|
|
37
|
+
var import_formula = require("@objectstack/formula");
|
|
38
|
+
var FlowSuspendSignal = class {
|
|
39
|
+
constructor(nodeId, correlation, screen) {
|
|
40
|
+
this.nodeId = nodeId;
|
|
41
|
+
this.correlation = correlation;
|
|
42
|
+
this.screen = screen;
|
|
43
|
+
this.__flowSuspend = true;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function isSuspendSignal(err) {
|
|
47
|
+
return typeof err === "object" && err !== null && err.__flowSuspend === true;
|
|
48
|
+
}
|
|
34
49
|
var AutomationEngine = class {
|
|
35
50
|
constructor(logger) {
|
|
36
51
|
this.flows = /* @__PURE__ */ new Map();
|
|
37
52
|
this.flowEnabled = /* @__PURE__ */ new Map();
|
|
38
53
|
this.flowVersionHistory = /* @__PURE__ */ new Map();
|
|
39
54
|
this.nodeExecutors = /* @__PURE__ */ new Map();
|
|
55
|
+
this.actionDescriptors = /* @__PURE__ */ new Map();
|
|
40
56
|
this.triggers = /* @__PURE__ */ new Map();
|
|
57
|
+
/**
|
|
58
|
+
* Flows currently wired to a trigger, keyed by flow name → the trigger
|
|
59
|
+
* `type` that owns the binding. Used to avoid double-binding and to know
|
|
60
|
+
* which trigger to `stop()` when a flow is unregistered/disabled.
|
|
61
|
+
*/
|
|
62
|
+
this.boundFlowTriggers = /* @__PURE__ */ new Map();
|
|
63
|
+
/** Connectors registered by integration plugins, keyed by connector name (ADR-0018 §Addendum). */
|
|
64
|
+
this.connectors = /* @__PURE__ */ new Map();
|
|
41
65
|
this.executionLogs = [];
|
|
42
66
|
this.maxLogSize = 1e3;
|
|
43
67
|
this.runCounter = 0;
|
|
68
|
+
/** Runs paused at a node, keyed by runId (ADR-0019). In-memory, see {@link SuspendedRun}. */
|
|
69
|
+
this.suspendedRuns = /* @__PURE__ */ new Map();
|
|
44
70
|
this.logger = logger;
|
|
45
71
|
}
|
|
46
72
|
// ── Plugin Extension API ──────────────────────────────
|
|
@@ -50,27 +76,200 @@ var AutomationEngine = class {
|
|
|
50
76
|
this.logger.warn(`Node executor '${executor.type}' replaced`);
|
|
51
77
|
}
|
|
52
78
|
this.nodeExecutors.set(executor.type, executor);
|
|
79
|
+
if (executor.descriptor) {
|
|
80
|
+
const descriptorType = executor.descriptor.type;
|
|
81
|
+
if (descriptorType !== executor.type) {
|
|
82
|
+
this.logger.warn(
|
|
83
|
+
`Node executor '${executor.type}' publishes a descriptor for type '${descriptorType}' \u2014 registering under both.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
this.actionDescriptors.set(descriptorType, executor.descriptor);
|
|
87
|
+
}
|
|
53
88
|
this.logger.info(`Node executor registered: ${executor.type}`);
|
|
54
89
|
}
|
|
55
90
|
/** Unregister a node executor (hot-unplug) */
|
|
56
91
|
unregisterNodeExecutor(type) {
|
|
92
|
+
const executor = this.nodeExecutors.get(type);
|
|
57
93
|
this.nodeExecutors.delete(type);
|
|
94
|
+
this.actionDescriptors.delete(type);
|
|
95
|
+
if (executor?.descriptor) {
|
|
96
|
+
this.actionDescriptors.delete(executor.descriptor.type);
|
|
97
|
+
}
|
|
58
98
|
this.logger.info(`Node executor unregistered: ${type}`);
|
|
59
99
|
}
|
|
60
100
|
/** Register a trigger (called by plugins) */
|
|
61
101
|
registerTrigger(trigger) {
|
|
62
102
|
this.triggers.set(trigger.type, trigger);
|
|
63
103
|
this.logger.info(`Trigger registered: ${trigger.type}`);
|
|
104
|
+
for (const name of this.flows.keys()) {
|
|
105
|
+
if (this.boundFlowTriggers.has(name)) continue;
|
|
106
|
+
const resolved = this.resolveTriggerBinding(name);
|
|
107
|
+
if (resolved?.triggerType === trigger.type) {
|
|
108
|
+
this.activateFlowTrigger(name);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
64
111
|
}
|
|
65
112
|
/** Unregister a trigger (hot-unplug) */
|
|
66
113
|
unregisterTrigger(type) {
|
|
114
|
+
for (const [name, boundType] of [...this.boundFlowTriggers]) {
|
|
115
|
+
if (boundType !== type) continue;
|
|
116
|
+
try {
|
|
117
|
+
this.triggers.get(type)?.stop(name);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
this.logger.warn(`Trigger '${type}' stop('${name}') failed: ${err.message}`);
|
|
120
|
+
}
|
|
121
|
+
this.boundFlowTriggers.delete(name);
|
|
122
|
+
}
|
|
67
123
|
this.triggers.delete(type);
|
|
68
124
|
this.logger.info(`Trigger unregistered: ${type}`);
|
|
69
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Derive a flow's trigger binding from its `start` node, or `undefined` if
|
|
128
|
+
* the flow has no auto-trigger (manual / screen / api). The convention —
|
|
129
|
+
* established by the showcase flows — is that the start node carries the
|
|
130
|
+
* trigger details in its `config`: `{ objectName, triggerType, condition }`
|
|
131
|
+
* for record-change, or a `schedule` descriptor for time-based flows.
|
|
132
|
+
*/
|
|
133
|
+
resolveTriggerBinding(flowName) {
|
|
134
|
+
const flow = this.flows.get(flowName);
|
|
135
|
+
if (!flow) return void 0;
|
|
136
|
+
const startNode = flow.nodes.find((n) => n.type === "start");
|
|
137
|
+
const config = startNode?.config ?? {};
|
|
138
|
+
const triggerType = typeof config.triggerType === "string" ? config.triggerType : void 0;
|
|
139
|
+
if (triggerType && triggerType.startsWith("record-")) {
|
|
140
|
+
return {
|
|
141
|
+
triggerType: "record_change",
|
|
142
|
+
binding: {
|
|
143
|
+
flowName,
|
|
144
|
+
object: typeof config.objectName === "string" ? config.objectName : void 0,
|
|
145
|
+
event: triggerType,
|
|
146
|
+
condition: config.condition ?? void 0,
|
|
147
|
+
config
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (config.schedule != null || flow.type === "schedule") {
|
|
152
|
+
return {
|
|
153
|
+
triggerType: "schedule",
|
|
154
|
+
binding: { flowName, schedule: config.schedule, condition: config.condition ?? void 0, config }
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Bind a flow to its matching registered trigger (idempotent). No-op when
|
|
161
|
+
* the flow has no trigger binding or no trigger is registered for its type
|
|
162
|
+
* yet — {@link registerTrigger} re-attempts activation when one arrives.
|
|
163
|
+
*/
|
|
164
|
+
activateFlowTrigger(flowName) {
|
|
165
|
+
if (this.boundFlowTriggers.has(flowName)) return;
|
|
166
|
+
const resolved = this.resolveTriggerBinding(flowName);
|
|
167
|
+
if (!resolved) return;
|
|
168
|
+
const trigger = this.triggers.get(resolved.triggerType);
|
|
169
|
+
if (!trigger) return;
|
|
170
|
+
try {
|
|
171
|
+
trigger.start(resolved.binding, (ctx) => this.execute(flowName, ctx).then(() => void 0));
|
|
172
|
+
this.boundFlowTriggers.set(flowName, resolved.triggerType);
|
|
173
|
+
this.logger.info(`Flow '${flowName}' bound to trigger '${resolved.triggerType}'`);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
this.logger.warn(`Failed to bind flow '${flowName}' to trigger '${resolved.triggerType}': ${err.message}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** Unbind a flow from its trigger, if bound. */
|
|
179
|
+
deactivateFlowTrigger(flowName) {
|
|
180
|
+
const boundType = this.boundFlowTriggers.get(flowName);
|
|
181
|
+
if (!boundType) return;
|
|
182
|
+
try {
|
|
183
|
+
this.triggers.get(boundType)?.stop(flowName);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
this.logger.warn(`Trigger '${boundType}' stop('${flowName}') failed: ${err.message}`);
|
|
186
|
+
}
|
|
187
|
+
this.boundFlowTriggers.delete(flowName);
|
|
188
|
+
}
|
|
189
|
+
/** Active flow→trigger bindings (observability / tests). */
|
|
190
|
+
getActiveTriggerBindings() {
|
|
191
|
+
return [...this.boundFlowTriggers].map(([flowName, triggerType]) => ({ flowName, triggerType }));
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Register a connector (called by integration plugins, ADR-0018 §Addendum).
|
|
195
|
+
* Validates the definition against {@link ConnectorSchema} and asserts every
|
|
196
|
+
* declared action has a handler, so a half-wired connector fails loudly at
|
|
197
|
+
* registration rather than silently at dispatch. Re-registering the same
|
|
198
|
+
* name replaces (mirrors {@link registerNodeExecutor}).
|
|
199
|
+
*/
|
|
200
|
+
registerConnector(def, handlers) {
|
|
201
|
+
const parsed = import_integration.ConnectorSchema.parse(def);
|
|
202
|
+
for (const action of parsed.actions ?? []) {
|
|
203
|
+
if (typeof handlers[action.key] !== "function") {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Connector '${parsed.name}': action '${action.key}' is declared but no handler was provided`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (this.connectors.has(parsed.name)) {
|
|
210
|
+
this.logger.warn(`Connector '${parsed.name}' replaced`);
|
|
211
|
+
}
|
|
212
|
+
this.connectors.set(parsed.name, { def: parsed, handlers });
|
|
213
|
+
this.logger.info(
|
|
214
|
+
`Connector registered: ${parsed.name} (${Object.keys(handlers).length} action handlers)`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
/** Unregister a connector (hot-unplug). */
|
|
218
|
+
unregisterConnector(name) {
|
|
219
|
+
this.connectors.delete(name);
|
|
220
|
+
this.logger.info(`Connector unregistered: ${name}`);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Resolve the handler for a connector action, used by the baseline
|
|
224
|
+
* `connector_action` node. Returns `undefined` when the connector or action
|
|
225
|
+
* is not registered, so the node can fail the step with a clear error.
|
|
226
|
+
*/
|
|
227
|
+
resolveConnectorAction(connectorId, actionId) {
|
|
228
|
+
return this.connectors.get(connectorId)?.handlers[actionId];
|
|
229
|
+
}
|
|
230
|
+
/** Get all registered connector names. */
|
|
231
|
+
getRegisteredConnectors() {
|
|
232
|
+
return [...this.connectors.keys()];
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get a designer-facing descriptor for every registered connector — its
|
|
236
|
+
* identity plus the actions it exposes (input/output JSON Schema). Backs
|
|
237
|
+
* `GET /api/v1/automation/connectors` so the designer can fill the
|
|
238
|
+
* `connector_action` node's connector / action / input pickers (ADR-0022).
|
|
239
|
+
* Handlers are omitted — they are runtime code, not metadata.
|
|
240
|
+
*/
|
|
241
|
+
getConnectorDescriptors() {
|
|
242
|
+
return [...this.connectors.values()].map(({ def }) => ({
|
|
243
|
+
name: def.name,
|
|
244
|
+
label: def.label,
|
|
245
|
+
type: def.type,
|
|
246
|
+
description: def.description,
|
|
247
|
+
icon: def.icon,
|
|
248
|
+
actions: (def.actions ?? []).map((a) => ({
|
|
249
|
+
key: a.key,
|
|
250
|
+
label: a.label,
|
|
251
|
+
description: a.description,
|
|
252
|
+
inputSchema: a.inputSchema,
|
|
253
|
+
outputSchema: a.outputSchema
|
|
254
|
+
}))
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
70
257
|
/** Get all registered node types */
|
|
71
258
|
getRegisteredNodeTypes() {
|
|
72
259
|
return [...this.nodeExecutors.keys()];
|
|
73
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Get all published action descriptors (ADR-0018). Backs both flow
|
|
263
|
+
* validation and the designer palette (`GET /api/v1/automation/actions`).
|
|
264
|
+
* Only executors that published a descriptor appear here.
|
|
265
|
+
*/
|
|
266
|
+
getActionDescriptors() {
|
|
267
|
+
return [...this.actionDescriptors.values()];
|
|
268
|
+
}
|
|
269
|
+
/** Get the action descriptor for a single node type, if published. */
|
|
270
|
+
getActionDescriptor(type) {
|
|
271
|
+
return this.actionDescriptors.get(type);
|
|
272
|
+
}
|
|
74
273
|
/** Get all registered trigger types */
|
|
75
274
|
getRegisteredTriggerTypes() {
|
|
76
275
|
return [...this.triggers.keys()];
|
|
@@ -79,6 +278,7 @@ var AutomationEngine = class {
|
|
|
79
278
|
registerFlow(name, definition) {
|
|
80
279
|
const parsed = import_automation.FlowSchema.parse(definition);
|
|
81
280
|
this.detectCycles(parsed);
|
|
281
|
+
this.validateNodeTypes(name, parsed);
|
|
82
282
|
const history = this.flowVersionHistory.get(name) ?? [];
|
|
83
283
|
history.push({
|
|
84
284
|
version: parsed.version,
|
|
@@ -91,8 +291,13 @@ var AutomationEngine = class {
|
|
|
91
291
|
this.flowEnabled.set(name, true);
|
|
92
292
|
}
|
|
93
293
|
this.logger.info(`Flow registered: ${name} (version ${parsed.version})`);
|
|
294
|
+
this.deactivateFlowTrigger(name);
|
|
295
|
+
if (this.flowEnabled.get(name) !== false) {
|
|
296
|
+
this.activateFlowTrigger(name);
|
|
297
|
+
}
|
|
94
298
|
}
|
|
95
299
|
unregisterFlow(name) {
|
|
300
|
+
this.deactivateFlowTrigger(name);
|
|
96
301
|
this.flows.delete(name);
|
|
97
302
|
this.flowEnabled.delete(name);
|
|
98
303
|
this.flowVersionHistory.delete(name);
|
|
@@ -110,6 +315,11 @@ var AutomationEngine = class {
|
|
|
110
315
|
}
|
|
111
316
|
this.flowEnabled.set(name, enabled);
|
|
112
317
|
this.logger.info(`Flow '${name}' ${enabled ? "enabled" : "disabled"}`);
|
|
318
|
+
if (enabled) {
|
|
319
|
+
this.activateFlowTrigger(name);
|
|
320
|
+
} else {
|
|
321
|
+
this.deactivateFlowTrigger(name);
|
|
322
|
+
}
|
|
113
323
|
}
|
|
114
324
|
/** Get flow version history */
|
|
115
325
|
getFlowVersionHistory(name) {
|
|
@@ -155,8 +365,16 @@ var AutomationEngine = class {
|
|
|
155
365
|
}
|
|
156
366
|
if (context?.record) {
|
|
157
367
|
variables.set("$record", context.record);
|
|
368
|
+
variables.set("record", context.record);
|
|
369
|
+
for (const [k, v] of Object.entries(context.record)) {
|
|
370
|
+
if (!variables.has(k)) variables.set(k, v);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (context?.previous) {
|
|
374
|
+
variables.set("previous", context.previous);
|
|
158
375
|
}
|
|
159
376
|
const runId = `run_${++this.runCounter}`;
|
|
377
|
+
variables.set("$runId", runId);
|
|
160
378
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
161
379
|
const steps = [];
|
|
162
380
|
try {
|
|
@@ -164,6 +382,14 @@ var AutomationEngine = class {
|
|
|
164
382
|
if (!startNode) {
|
|
165
383
|
return { success: false, error: "Flow has no start node" };
|
|
166
384
|
}
|
|
385
|
+
const startCondition = startNode.config?.condition;
|
|
386
|
+
if (startCondition !== void 0 && startCondition !== null && startCondition !== "") {
|
|
387
|
+
const condExpr = typeof startCondition === "string" ? { dialect: "cel", source: startCondition } : startCondition;
|
|
388
|
+
if (!this.evaluateCondition(condExpr, variables)) {
|
|
389
|
+
this.logger.debug(`Flow '${flowName}' skipped: start condition not met`);
|
|
390
|
+
return { success: true, output: { skipped: true, reason: "condition_not_met" } };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
167
393
|
this.validateNodeInputSchemas(flow, variables);
|
|
168
394
|
await this.executeNode(startNode, flow, variables, context ?? {}, steps);
|
|
169
395
|
const output = {};
|
|
@@ -197,6 +423,43 @@ var AutomationEngine = class {
|
|
|
197
423
|
durationMs
|
|
198
424
|
};
|
|
199
425
|
} catch (err) {
|
|
426
|
+
if (isSuspendSignal(err)) {
|
|
427
|
+
const durationMs2 = Date.now() - startTime;
|
|
428
|
+
this.suspendedRuns.set(runId, {
|
|
429
|
+
runId,
|
|
430
|
+
flowName,
|
|
431
|
+
flowVersion: flow.version,
|
|
432
|
+
nodeId: err.nodeId,
|
|
433
|
+
variables: Object.fromEntries(variables),
|
|
434
|
+
steps,
|
|
435
|
+
context: context ?? {},
|
|
436
|
+
startedAt,
|
|
437
|
+
startTime,
|
|
438
|
+
correlation: err.correlation,
|
|
439
|
+
screen: err.screen
|
|
440
|
+
});
|
|
441
|
+
this.recordLog({
|
|
442
|
+
id: runId,
|
|
443
|
+
flowName,
|
|
444
|
+
flowVersion: flow.version,
|
|
445
|
+
status: "paused",
|
|
446
|
+
startedAt,
|
|
447
|
+
durationMs: durationMs2,
|
|
448
|
+
trigger: {
|
|
449
|
+
type: context?.event ?? "manual",
|
|
450
|
+
userId: context?.userId,
|
|
451
|
+
object: context?.object
|
|
452
|
+
},
|
|
453
|
+
steps
|
|
454
|
+
});
|
|
455
|
+
return {
|
|
456
|
+
success: true,
|
|
457
|
+
status: "paused",
|
|
458
|
+
runId,
|
|
459
|
+
durationMs: durationMs2,
|
|
460
|
+
screen: err.screen
|
|
461
|
+
};
|
|
462
|
+
}
|
|
200
463
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
201
464
|
const durationMs = Date.now() - startTime;
|
|
202
465
|
this.recordLog({
|
|
@@ -225,6 +488,135 @@ var AutomationEngine = class {
|
|
|
225
488
|
};
|
|
226
489
|
}
|
|
227
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Resume a run suspended at a node (ADR-0019 durable pause). Restores the
|
|
493
|
+
* snapshotted variables, merges `signal.output` under the suspended node's
|
|
494
|
+
* id, and continues traversal from that node's out-edges — optionally
|
|
495
|
+
* restricted to the edge labelled `signal.branchLabel` (e.g. the approval
|
|
496
|
+
* decision). The continuation may itself suspend again, in which case this
|
|
497
|
+
* returns `{ status: 'paused', runId }` afresh.
|
|
498
|
+
*/
|
|
499
|
+
async resume(runId, signal) {
|
|
500
|
+
const run = this.suspendedRuns.get(runId);
|
|
501
|
+
if (!run) {
|
|
502
|
+
return { success: false, error: `No suspended run '${runId}'` };
|
|
503
|
+
}
|
|
504
|
+
const flow = this.flows.get(run.flowName);
|
|
505
|
+
if (!flow) {
|
|
506
|
+
return { success: false, error: `Flow '${run.flowName}' not found for run '${runId}'` };
|
|
507
|
+
}
|
|
508
|
+
const node = flow.nodes.find((n) => n.id === run.nodeId);
|
|
509
|
+
if (!node) {
|
|
510
|
+
return { success: false, error: `Suspended node '${run.nodeId}' no longer exists in flow '${run.flowName}'` };
|
|
511
|
+
}
|
|
512
|
+
this.suspendedRuns.delete(runId);
|
|
513
|
+
const variables = new Map(Object.entries(run.variables));
|
|
514
|
+
if (signal?.output) {
|
|
515
|
+
for (const [key, value] of Object.entries(signal.output)) {
|
|
516
|
+
variables.set(`${run.nodeId}.${key}`, value);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (signal?.variables) {
|
|
520
|
+
for (const [key, value] of Object.entries(signal.variables)) {
|
|
521
|
+
variables.set(key, value);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const steps = run.steps;
|
|
525
|
+
const context = run.context;
|
|
526
|
+
try {
|
|
527
|
+
await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);
|
|
528
|
+
const output = {};
|
|
529
|
+
if (flow.variables) {
|
|
530
|
+
for (const v of flow.variables) {
|
|
531
|
+
if (v.isOutput) output[v.name] = variables.get(v.name);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const durationMs = Date.now() - run.startTime;
|
|
535
|
+
this.recordLog({
|
|
536
|
+
id: runId,
|
|
537
|
+
flowName: run.flowName,
|
|
538
|
+
flowVersion: run.flowVersion,
|
|
539
|
+
status: "completed",
|
|
540
|
+
startedAt: run.startedAt,
|
|
541
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
542
|
+
durationMs,
|
|
543
|
+
trigger: {
|
|
544
|
+
type: context.event ?? "manual",
|
|
545
|
+
userId: context.userId,
|
|
546
|
+
object: context.object
|
|
547
|
+
},
|
|
548
|
+
steps,
|
|
549
|
+
output
|
|
550
|
+
});
|
|
551
|
+
return { success: true, output, durationMs };
|
|
552
|
+
} catch (err) {
|
|
553
|
+
if (isSuspendSignal(err)) {
|
|
554
|
+
const durationMs2 = Date.now() - run.startTime;
|
|
555
|
+
this.suspendedRuns.set(runId, {
|
|
556
|
+
...run,
|
|
557
|
+
nodeId: err.nodeId,
|
|
558
|
+
variables: Object.fromEntries(variables),
|
|
559
|
+
steps,
|
|
560
|
+
correlation: err.correlation,
|
|
561
|
+
screen: err.screen
|
|
562
|
+
});
|
|
563
|
+
this.recordLog({
|
|
564
|
+
id: runId,
|
|
565
|
+
flowName: run.flowName,
|
|
566
|
+
flowVersion: run.flowVersion,
|
|
567
|
+
status: "paused",
|
|
568
|
+
startedAt: run.startedAt,
|
|
569
|
+
durationMs: durationMs2,
|
|
570
|
+
trigger: {
|
|
571
|
+
type: context.event ?? "manual",
|
|
572
|
+
userId: context.userId,
|
|
573
|
+
object: context.object
|
|
574
|
+
},
|
|
575
|
+
steps
|
|
576
|
+
});
|
|
577
|
+
return { success: true, status: "paused", runId, durationMs: durationMs2, screen: err.screen };
|
|
578
|
+
}
|
|
579
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
580
|
+
const durationMs = Date.now() - run.startTime;
|
|
581
|
+
this.recordLog({
|
|
582
|
+
id: runId,
|
|
583
|
+
flowName: run.flowName,
|
|
584
|
+
flowVersion: run.flowVersion,
|
|
585
|
+
status: "failed",
|
|
586
|
+
startedAt: run.startedAt,
|
|
587
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
588
|
+
durationMs,
|
|
589
|
+
trigger: {
|
|
590
|
+
type: context.event ?? "manual",
|
|
591
|
+
userId: context.userId,
|
|
592
|
+
object: context.object
|
|
593
|
+
},
|
|
594
|
+
steps,
|
|
595
|
+
error: errorMessage
|
|
596
|
+
});
|
|
597
|
+
return { success: false, error: errorMessage, durationMs };
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* List the runs currently suspended awaiting {@link resume} (ADR-0019).
|
|
602
|
+
* Backs operability surfaces such as a "pending approvals" view.
|
|
603
|
+
*/
|
|
604
|
+
listSuspendedRuns() {
|
|
605
|
+
return [...this.suspendedRuns.values()].map((r) => ({
|
|
606
|
+
runId: r.runId,
|
|
607
|
+
flowName: r.flowName,
|
|
608
|
+
nodeId: r.nodeId,
|
|
609
|
+
correlation: r.correlation
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* The screen a paused run is currently waiting on (screen-flow runtime), or
|
|
614
|
+
* `null` if the run isn't suspended / didn't pause at a screen node. Lets a
|
|
615
|
+
* UI flow-runner re-fetch the form after a refresh.
|
|
616
|
+
*/
|
|
617
|
+
getSuspendedScreen(runId) {
|
|
618
|
+
return this.suspendedRuns.get(runId)?.screen ?? null;
|
|
619
|
+
}
|
|
228
620
|
// ── DAG Traversal Core ──────────────────────────────────
|
|
229
621
|
recordLog(entry) {
|
|
230
622
|
this.executionLogs.push(entry);
|
|
@@ -232,6 +624,29 @@ var AutomationEngine = class {
|
|
|
232
624
|
this.executionLogs.splice(0, this.executionLogs.length - this.maxLogSize);
|
|
233
625
|
}
|
|
234
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* Validate each node's `type` against the live action registry (ADR-0018).
|
|
629
|
+
* A type is known if it is structural (start/end), has a registered
|
|
630
|
+
* executor, or has a published action descriptor. Unknown types are
|
|
631
|
+
* warned about (not rejected) so flows authored against a temporarily
|
|
632
|
+
* absent plugin still register; the runtime surfaces a hard NO_EXECUTOR
|
|
633
|
+
* error if such a node is actually executed.
|
|
634
|
+
*/
|
|
635
|
+
validateNodeTypes(flowName, flow) {
|
|
636
|
+
const known = /* @__PURE__ */ new Set([
|
|
637
|
+
...import_automation.FLOW_STRUCTURAL_NODE_TYPES,
|
|
638
|
+
...this.nodeExecutors.keys(),
|
|
639
|
+
...this.actionDescriptors.keys()
|
|
640
|
+
]);
|
|
641
|
+
const unknown = [...new Set(
|
|
642
|
+
flow.nodes.map((n) => n.type).filter((t) => !known.has(t))
|
|
643
|
+
)];
|
|
644
|
+
if (unknown.length > 0) {
|
|
645
|
+
this.logger.warn(
|
|
646
|
+
`Flow '${flowName}' references node type(s) with no registered executor or descriptor: ${unknown.join(", ")}. They will fail at execution time unless a plugin registers them. Registered types: ${[...known].join(", ") || "(none)"}`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
235
650
|
/**
|
|
236
651
|
* Detect cycles in the flow graph (DAG validation).
|
|
237
652
|
* Uses DFS with coloring (white/gray/black) to detect back edges.
|
|
@@ -413,10 +828,30 @@ var AutomationEngine = class {
|
|
|
413
828
|
variables.set(`${node.id}.${key}`, value);
|
|
414
829
|
}
|
|
415
830
|
}
|
|
831
|
+
if (result.suspend) {
|
|
832
|
+
throw new FlowSuspendSignal(node.id, result.correlation, result.screen);
|
|
833
|
+
}
|
|
416
834
|
}
|
|
417
|
-
|
|
835
|
+
await this.traverseNext(node, flow, variables, context, steps);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Traverse a node's out-edges and execute its successors. Split out of
|
|
839
|
+
* {@link executeNode} so {@link resume} can re-enter traversal from a
|
|
840
|
+
* suspended node without re-running the node body.
|
|
841
|
+
*
|
|
842
|
+
* @param branchLabel - When set (e.g. from a resume signal), restrict
|
|
843
|
+
* traversal to out-edges whose `label` matches — this is how an Approval
|
|
844
|
+
* node's `approve`/`reject` decision selects its downstream branch. When
|
|
845
|
+
* no edge carries the label, traversal falls back to the normal edge set.
|
|
846
|
+
*/
|
|
847
|
+
async traverseNext(node, flow, variables, context, steps, branchLabel) {
|
|
848
|
+
let outEdges = flow.edges.filter(
|
|
418
849
|
(e) => e.source === node.id && e.type !== "fault"
|
|
419
850
|
);
|
|
851
|
+
if (branchLabel) {
|
|
852
|
+
const labeled = outEdges.filter((e) => e.label === branchLabel);
|
|
853
|
+
if (labeled.length > 0) outEdges = labeled;
|
|
854
|
+
}
|
|
420
855
|
const conditionalEdges = [];
|
|
421
856
|
const unconditionalEdges = [];
|
|
422
857
|
for (const edge of outEdges) {
|
|
@@ -465,7 +900,6 @@ var AutomationEngine = class {
|
|
|
465
900
|
}
|
|
466
901
|
if (dialect === "cel" || isEnvelope && !dialect) {
|
|
467
902
|
try {
|
|
468
|
-
const { ExpressionEngine } = require("@objectstack/formula");
|
|
469
903
|
const vars = {};
|
|
470
904
|
for (const [key, value] of variables) {
|
|
471
905
|
const segs = key.split(".");
|
|
@@ -478,9 +912,9 @@ var AutomationEngine = class {
|
|
|
478
912
|
}
|
|
479
913
|
cursor[segs[segs.length - 1]] = value;
|
|
480
914
|
}
|
|
481
|
-
const result = ExpressionEngine.evaluate(
|
|
915
|
+
const result = import_formula.ExpressionEngine.evaluate(
|
|
482
916
|
{ dialect: "cel", source: exprStr },
|
|
483
|
-
{ extra: { vars }, record: vars }
|
|
917
|
+
{ extra: { ...vars, vars }, record: vars }
|
|
484
918
|
);
|
|
485
919
|
if (!result.ok) return false;
|
|
486
920
|
return Boolean(result.value);
|
|
@@ -663,76 +1097,81 @@ var AutomationEngine = class {
|
|
|
663
1097
|
}
|
|
664
1098
|
};
|
|
665
1099
|
|
|
666
|
-
// src/
|
|
667
|
-
var
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
console.warn("[Automation:start] entering start()");
|
|
690
|
-
if (!this.engine) {
|
|
691
|
-
console.warn("[Automation:start] engine missing, bailing");
|
|
692
|
-
return;
|
|
1100
|
+
// src/builtin/logic-nodes.ts
|
|
1101
|
+
var import_automation2 = require("@objectstack/spec/automation");
|
|
1102
|
+
function registerLogicNodes(engine, ctx) {
|
|
1103
|
+
engine.registerNodeExecutor({
|
|
1104
|
+
type: "decision",
|
|
1105
|
+
descriptor: (0, import_automation2.defineActionDescriptor)({
|
|
1106
|
+
type: "decision",
|
|
1107
|
+
version: "1.0.0",
|
|
1108
|
+
name: "Decision",
|
|
1109
|
+
description: "Branch execution based on conditions.",
|
|
1110
|
+
icon: "git-branch",
|
|
1111
|
+
category: "logic",
|
|
1112
|
+
source: "builtin"
|
|
1113
|
+
}),
|
|
1114
|
+
async execute(node, variables, _context) {
|
|
1115
|
+
const config = node.config;
|
|
1116
|
+
const conditions = config?.conditions ?? [];
|
|
1117
|
+
for (const cond of conditions) {
|
|
1118
|
+
if (engine.evaluateCondition(cond.expression, variables)) {
|
|
1119
|
+
return { success: true, branchLabel: cond.label };
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return { success: true, branchLabel: "default" };
|
|
693
1123
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1124
|
+
});
|
|
1125
|
+
engine.registerNodeExecutor({
|
|
1126
|
+
type: "assignment",
|
|
1127
|
+
descriptor: (0, import_automation2.defineActionDescriptor)({
|
|
1128
|
+
type: "assignment",
|
|
1129
|
+
version: "1.0.0",
|
|
1130
|
+
name: "Assignment",
|
|
1131
|
+
description: "Set flow variables.",
|
|
1132
|
+
icon: "variable",
|
|
1133
|
+
category: "logic",
|
|
1134
|
+
source: "builtin"
|
|
1135
|
+
}),
|
|
1136
|
+
async execute(node, variables, _context) {
|
|
1137
|
+
const config = node.config ?? {};
|
|
1138
|
+
for (const [key, value] of Object.entries(config)) {
|
|
1139
|
+
variables.set(key, value);
|
|
707
1140
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1141
|
+
return { success: true };
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
engine.registerNodeExecutor({
|
|
1145
|
+
type: "loop",
|
|
1146
|
+
descriptor: (0, import_automation2.defineActionDescriptor)({
|
|
1147
|
+
type: "loop",
|
|
1148
|
+
version: "1.0.0",
|
|
1149
|
+
name: "Loop",
|
|
1150
|
+
description: "Iterate over a collection.",
|
|
1151
|
+
icon: "repeat",
|
|
1152
|
+
category: "logic",
|
|
1153
|
+
source: "builtin"
|
|
1154
|
+
}),
|
|
1155
|
+
async execute(node, variables, _context) {
|
|
1156
|
+
const config = node.config;
|
|
1157
|
+
const collectionName = config?.collection;
|
|
1158
|
+
if (collectionName) {
|
|
1159
|
+
const collection = variables.get(collectionName);
|
|
1160
|
+
if (Array.isArray(collection)) {
|
|
1161
|
+
variables.set("$loopItems", collection);
|
|
1162
|
+
variables.set("$loopIndex", 0);
|
|
720
1163
|
}
|
|
721
1164
|
}
|
|
722
|
-
|
|
723
|
-
ctx.logger.info(`[Automation] Pulled ${registered} flow(s) from ObjectQL registry`);
|
|
724
|
-
}
|
|
725
|
-
} catch (err) {
|
|
726
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
727
|
-
ctx.logger.warn(`[Automation] flow pull from ObjectQL registry failed: ${msg}`);
|
|
1165
|
+
return { success: true };
|
|
728
1166
|
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1167
|
+
});
|
|
1168
|
+
ctx.logger.info("[Logic Nodes] 3 built-in node executors registered");
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// src/builtin/crud-nodes.ts
|
|
1172
|
+
var import_automation3 = require("@objectstack/spec/automation");
|
|
734
1173
|
|
|
735
|
-
// src/
|
|
1174
|
+
// src/builtin/template.ts
|
|
736
1175
|
function resolvePath(base, path) {
|
|
737
1176
|
let cur = base;
|
|
738
1177
|
for (const seg of path) {
|
|
@@ -829,269 +1268,498 @@ function interpolate(value, variables, context) {
|
|
|
829
1268
|
return value;
|
|
830
1269
|
}
|
|
831
1270
|
|
|
832
|
-
// src/
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
return ctx.getService("data") ?? ctx.getService("objectql");
|
|
845
|
-
} catch {
|
|
846
|
-
return void 0;
|
|
847
|
-
}
|
|
848
|
-
};
|
|
849
|
-
engine.registerNodeExecutor({
|
|
1271
|
+
// src/builtin/crud-nodes.ts
|
|
1272
|
+
function registerCrudNodes(engine, ctx) {
|
|
1273
|
+
const getData = () => {
|
|
1274
|
+
try {
|
|
1275
|
+
return ctx.getService("data") ?? ctx.getService("objectql");
|
|
1276
|
+
} catch {
|
|
1277
|
+
return void 0;
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
engine.registerNodeExecutor({
|
|
1281
|
+
type: "get_record",
|
|
1282
|
+
descriptor: (0, import_automation3.defineActionDescriptor)({
|
|
850
1283
|
type: "get_record",
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1284
|
+
version: "1.0.0",
|
|
1285
|
+
name: "Get Records",
|
|
1286
|
+
description: "Query records from an object.",
|
|
1287
|
+
icon: "search",
|
|
1288
|
+
category: "data",
|
|
1289
|
+
source: "builtin"
|
|
1290
|
+
}),
|
|
1291
|
+
async execute(node, variables, context) {
|
|
1292
|
+
const cfg = node.config ?? {};
|
|
1293
|
+
const objectName = String(cfg.objectName ?? cfg.object ?? "");
|
|
1294
|
+
if (!objectName) return { success: false, error: "get_record: objectName required" };
|
|
1295
|
+
const filter = interpolate(cfg.filter ?? cfg.filters ?? {}, variables, context);
|
|
1296
|
+
const fields = cfg.fields;
|
|
1297
|
+
const limit = typeof cfg.limit === "number" ? cfg.limit : void 0;
|
|
1298
|
+
const outputVariable = cfg.outputVariable;
|
|
1299
|
+
const data = getData();
|
|
1300
|
+
if (!data) {
|
|
1301
|
+
ctx.logger.warn(`[get_record] no data engine; skipping ${objectName}`);
|
|
1302
|
+
return { success: true, output: { records: [], object: objectName } };
|
|
1303
|
+
}
|
|
1304
|
+
try {
|
|
1305
|
+
if (limit && limit > 1) {
|
|
1306
|
+
const records = await data.find(objectName, { where: filter, fields, limit });
|
|
1307
|
+
if (outputVariable) variables.set(outputVariable, records);
|
|
1308
|
+
return { success: true, output: { records, object: objectName } };
|
|
875
1309
|
}
|
|
1310
|
+
const record = await data.findOne(objectName, { where: filter, fields });
|
|
1311
|
+
if (outputVariable) variables.set(outputVariable, record);
|
|
1312
|
+
return { success: true, output: { record, id: record?.id, object: objectName } };
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
return { success: false, error: `get_record(${objectName}) failed: ${err.message}` };
|
|
876
1315
|
}
|
|
877
|
-
}
|
|
878
|
-
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
engine.registerNodeExecutor({
|
|
1319
|
+
type: "create_record",
|
|
1320
|
+
descriptor: (0, import_automation3.defineActionDescriptor)({
|
|
879
1321
|
type: "create_record",
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
return { success: false, error: `create_record(${objectName}) failed: ${err.message}` };
|
|
899
|
-
}
|
|
1322
|
+
version: "1.0.0",
|
|
1323
|
+
name: "Create Record",
|
|
1324
|
+
description: "Insert a new record into an object.",
|
|
1325
|
+
icon: "plus-circle",
|
|
1326
|
+
category: "data",
|
|
1327
|
+
source: "builtin"
|
|
1328
|
+
}),
|
|
1329
|
+
async execute(node, variables, context) {
|
|
1330
|
+
const cfg = node.config ?? {};
|
|
1331
|
+
const objectName = String(cfg.objectName ?? cfg.object ?? "");
|
|
1332
|
+
if (!objectName) return { success: false, error: "create_record: objectName required" };
|
|
1333
|
+
const fields = interpolate(cfg.fields ?? {}, variables, context);
|
|
1334
|
+
const outputVariable = cfg.outputVariable;
|
|
1335
|
+
const data = getData();
|
|
1336
|
+
if (!data) {
|
|
1337
|
+
ctx.logger.warn(`[create_record] no data engine; skipping ${objectName}`);
|
|
1338
|
+
if (outputVariable) variables.set(outputVariable, `mock-${objectName}-${Date.now()}`);
|
|
1339
|
+
return { success: true, output: { id: `mock-${objectName}-${Date.now()}`, object: objectName } };
|
|
900
1340
|
}
|
|
901
|
-
|
|
902
|
-
|
|
1341
|
+
try {
|
|
1342
|
+
const created = await data.insert(objectName, fields);
|
|
1343
|
+
const insertedId = Array.isArray(created) ? created[0]?.id : created?.id ?? created;
|
|
1344
|
+
if (outputVariable) variables.set(outputVariable, insertedId);
|
|
1345
|
+
return { success: true, output: { id: insertedId, record: created, object: objectName } };
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
return { success: false, error: `create_record(${objectName}) failed: ${err.message}` };
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
engine.registerNodeExecutor({
|
|
1352
|
+
type: "update_record",
|
|
1353
|
+
descriptor: (0, import_automation3.defineActionDescriptor)({
|
|
903
1354
|
type: "update_record",
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
}
|
|
1355
|
+
version: "1.0.0",
|
|
1356
|
+
name: "Update Records",
|
|
1357
|
+
description: "Update records matching a filter.",
|
|
1358
|
+
icon: "edit",
|
|
1359
|
+
category: "data",
|
|
1360
|
+
source: "builtin"
|
|
1361
|
+
}),
|
|
1362
|
+
async execute(node, variables, context) {
|
|
1363
|
+
const cfg = node.config ?? {};
|
|
1364
|
+
const objectName = String(cfg.objectName ?? cfg.object ?? "");
|
|
1365
|
+
if (!objectName) return { success: false, error: "update_record: objectName required" };
|
|
1366
|
+
const filter = interpolate(cfg.filter ?? cfg.filters ?? {}, variables, context);
|
|
1367
|
+
const fields = interpolate(cfg.fields ?? {}, variables, context);
|
|
1368
|
+
const data = getData();
|
|
1369
|
+
if (!data) {
|
|
1370
|
+
ctx.logger.warn(`[update_record] no data engine; skipping ${objectName}`);
|
|
1371
|
+
return { success: true };
|
|
921
1372
|
}
|
|
922
|
-
|
|
923
|
-
|
|
1373
|
+
try {
|
|
1374
|
+
const result = await data.update(objectName, fields, { where: filter });
|
|
1375
|
+
return { success: true, output: { result, object: objectName } };
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
return { success: false, error: `update_record(${objectName}) failed: ${err.message}` };
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
engine.registerNodeExecutor({
|
|
1382
|
+
type: "delete_record",
|
|
1383
|
+
descriptor: (0, import_automation3.defineActionDescriptor)({
|
|
924
1384
|
type: "delete_record",
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1385
|
+
version: "1.0.0",
|
|
1386
|
+
name: "Delete Records",
|
|
1387
|
+
description: "Delete records matching a filter.",
|
|
1388
|
+
icon: "trash",
|
|
1389
|
+
category: "data",
|
|
1390
|
+
source: "builtin"
|
|
1391
|
+
}),
|
|
1392
|
+
async execute(node, variables, context) {
|
|
1393
|
+
const cfg = node.config ?? {};
|
|
1394
|
+
const objectName = String(cfg.objectName ?? cfg.object ?? "");
|
|
1395
|
+
if (!objectName) return { success: false, error: "delete_record: objectName required" };
|
|
1396
|
+
const filter = interpolate(cfg.filter ?? cfg.filters ?? {}, variables, context);
|
|
1397
|
+
const data = getData();
|
|
1398
|
+
if (!data) return { success: true };
|
|
1399
|
+
try {
|
|
1400
|
+
const result = await data.delete(objectName, { where: filter });
|
|
1401
|
+
return { success: true, output: { result, object: objectName } };
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
return { success: false, error: `delete_record(${objectName}) failed: ${err.message}` };
|
|
938
1404
|
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
}
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
ctx.logger.info("[CRUD Nodes] 4 built-in node executors registered (data-backed)");
|
|
1408
|
+
}
|
|
943
1409
|
|
|
944
|
-
// src/
|
|
945
|
-
var
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
engine.registerNodeExecutor({
|
|
968
|
-
type: "assignment",
|
|
969
|
-
async execute(node, variables, _context) {
|
|
970
|
-
const config = node.config ?? {};
|
|
971
|
-
for (const [key, value] of Object.entries(config)) {
|
|
972
|
-
variables.set(key, value);
|
|
973
|
-
}
|
|
1410
|
+
// src/builtin/screen-nodes.ts
|
|
1411
|
+
var import_automation4 = require("@objectstack/spec/automation");
|
|
1412
|
+
function registerScreenNodes(engine, ctx) {
|
|
1413
|
+
engine.registerNodeExecutor({
|
|
1414
|
+
type: "screen",
|
|
1415
|
+
descriptor: (0, import_automation4.defineActionDescriptor)({
|
|
1416
|
+
type: "screen",
|
|
1417
|
+
version: "1.0.0",
|
|
1418
|
+
name: "Screen",
|
|
1419
|
+
description: "Collect user input via a screen (human-input element).",
|
|
1420
|
+
icon: "window",
|
|
1421
|
+
category: "human",
|
|
1422
|
+
source: "builtin",
|
|
1423
|
+
// Human-input nodes suspend the flow awaiting input.
|
|
1424
|
+
supportsPause: true,
|
|
1425
|
+
isAsync: true
|
|
1426
|
+
}),
|
|
1427
|
+
async execute(node, _variables, _context) {
|
|
1428
|
+
const cfg = node.config ?? {};
|
|
1429
|
+
const rawFields = Array.isArray(cfg.fields) ? cfg.fields : [];
|
|
1430
|
+
const hasFields = rawFields.length > 0;
|
|
1431
|
+
const shouldPause = cfg.waitForInput === true || hasFields && cfg.waitForInput !== false;
|
|
1432
|
+
if (!shouldPause) {
|
|
974
1433
|
return { success: true };
|
|
975
1434
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1435
|
+
const fields = rawFields.map((f) => ({
|
|
1436
|
+
name: String(f.name ?? ""),
|
|
1437
|
+
label: f.label != null ? String(f.label) : void 0,
|
|
1438
|
+
type: f.type != null ? String(f.type) : void 0,
|
|
1439
|
+
required: f.required === true,
|
|
1440
|
+
options: Array.isArray(f.options) ? f.options : void 0,
|
|
1441
|
+
defaultValue: f.defaultValue,
|
|
1442
|
+
placeholder: f.placeholder != null ? String(f.placeholder) : void 0
|
|
1443
|
+
})).filter((f) => f.name.length > 0);
|
|
1444
|
+
return {
|
|
1445
|
+
success: true,
|
|
1446
|
+
suspend: true,
|
|
1447
|
+
screen: {
|
|
1448
|
+
nodeId: node.id,
|
|
1449
|
+
title: cfg.title ?? node.label ?? "Input",
|
|
1450
|
+
description: cfg.description,
|
|
1451
|
+
fields
|
|
988
1452
|
}
|
|
989
|
-
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
engine.registerNodeExecutor({
|
|
1457
|
+
type: "script",
|
|
1458
|
+
descriptor: (0, import_automation4.defineActionDescriptor)({
|
|
1459
|
+
type: "script",
|
|
1460
|
+
version: "1.0.0",
|
|
1461
|
+
name: "Script",
|
|
1462
|
+
description: "Run a custom script action.",
|
|
1463
|
+
icon: "code",
|
|
1464
|
+
category: "logic",
|
|
1465
|
+
source: "builtin"
|
|
1466
|
+
}),
|
|
1467
|
+
async execute(node, _variables, _context) {
|
|
1468
|
+
const cfg = node.config ?? {};
|
|
1469
|
+
const actionType = cfg.actionType ?? "noop";
|
|
1470
|
+
if (actionType === "email") {
|
|
1471
|
+
ctx.logger.info(
|
|
1472
|
+
`[Script:email] template=${String(cfg.template)} recipients=${JSON.stringify(cfg.recipients)} vars=${JSON.stringify(cfg.variables)}`
|
|
1473
|
+
);
|
|
1474
|
+
return {
|
|
1475
|
+
success: true,
|
|
1476
|
+
output: {
|
|
1477
|
+
actionType,
|
|
1478
|
+
template: cfg.template,
|
|
1479
|
+
recipients: cfg.recipients
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
990
1482
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
};
|
|
1483
|
+
ctx.logger.info(`[Script:${actionType}] node=${node.id} executed (no-op handler)`);
|
|
1484
|
+
return { success: true, output: { actionType } };
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
ctx.logger.info("[Screen/Script Nodes] 2 built-in node executors registered");
|
|
1488
|
+
}
|
|
995
1489
|
|
|
996
|
-
// src/
|
|
997
|
-
var
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
this.dependencies = ["com.objectstack.service-automation"];
|
|
1003
|
-
}
|
|
1004
|
-
async init(ctx) {
|
|
1005
|
-
const engine = ctx.getService("automation");
|
|
1006
|
-
engine.registerNodeExecutor({
|
|
1490
|
+
// src/builtin/http-nodes.ts
|
|
1491
|
+
var import_automation5 = require("@objectstack/spec/automation");
|
|
1492
|
+
function registerHttpNodes(engine, ctx) {
|
|
1493
|
+
engine.registerNodeExecutor({
|
|
1494
|
+
type: "http_request",
|
|
1495
|
+
descriptor: (0, import_automation5.defineActionDescriptor)({
|
|
1007
1496
|
type: "http_request",
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1497
|
+
version: "1.0.0",
|
|
1498
|
+
name: "HTTP Request",
|
|
1499
|
+
description: "Call an external HTTP endpoint. (ADR-0018: migrates to outbox-backed `http`.)",
|
|
1500
|
+
icon: "globe",
|
|
1501
|
+
category: "io",
|
|
1502
|
+
source: "builtin",
|
|
1503
|
+
// ADR-0018 §M3 target: route via service-messaging outbox for
|
|
1504
|
+
// retry/idempotency/dead-letter. Today this is a bare fetch().
|
|
1505
|
+
needsOutbox: false,
|
|
1506
|
+
supportsRetry: true,
|
|
1507
|
+
paradigms: ["flow", "workflow_rule", "approval"]
|
|
1508
|
+
}),
|
|
1509
|
+
async execute(node, _variables, _context) {
|
|
1510
|
+
const config = node.config;
|
|
1511
|
+
const url = config?.url;
|
|
1512
|
+
const method = config?.method ?? "GET";
|
|
1513
|
+
const headers = config?.headers;
|
|
1514
|
+
const body = config?.body;
|
|
1515
|
+
if (!url) {
|
|
1516
|
+
return { success: false, error: "http_request: url is required" };
|
|
1517
|
+
}
|
|
1518
|
+
const response = await fetch(url, {
|
|
1519
|
+
method,
|
|
1520
|
+
headers,
|
|
1521
|
+
body: body ? JSON.stringify(body) : void 0
|
|
1522
|
+
});
|
|
1523
|
+
const data = await response.json();
|
|
1524
|
+
return {
|
|
1525
|
+
success: response.ok,
|
|
1526
|
+
output: { response: data, status: response.status },
|
|
1527
|
+
error: response.ok ? void 0 : `HTTP ${response.status}`
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
ctx.logger.info("[HTTP] 1 built-in node executor registered (http_request)");
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// src/builtin/connector-nodes.ts
|
|
1535
|
+
var import_automation6 = require("@objectstack/spec/automation");
|
|
1536
|
+
function registerConnectorNodes(engine, ctx) {
|
|
1537
|
+
engine.registerNodeExecutor({
|
|
1538
|
+
type: "connector_action",
|
|
1539
|
+
descriptor: (0, import_automation6.defineActionDescriptor)({
|
|
1540
|
+
type: "connector_action",
|
|
1541
|
+
version: "1.0.0",
|
|
1542
|
+
name: "Connector Action",
|
|
1543
|
+
description: "Invoke an action on a registered connector (Slack, Salesforce, a REST API, \u2026). The connector itself is contributed by an integration plugin via registerConnector().",
|
|
1544
|
+
icon: "plug",
|
|
1545
|
+
category: "io",
|
|
1546
|
+
source: "builtin",
|
|
1547
|
+
supportsRetry: true,
|
|
1548
|
+
// Present in all three authoring paradigms (ADR-0018 §registry table).
|
|
1549
|
+
paradigms: ["flow", "workflow_rule", "approval"],
|
|
1550
|
+
// Config contract — drives the Studio property form and flow validation.
|
|
1551
|
+
configSchema: {
|
|
1552
|
+
type: "object",
|
|
1553
|
+
required: ["connectorId", "actionId"],
|
|
1554
|
+
properties: {
|
|
1555
|
+
connectorId: { type: "string", description: "Registered connector name" },
|
|
1556
|
+
actionId: { type: "string", description: "Action key declared by the connector" },
|
|
1557
|
+
input: { type: "object", description: "Mapped inputs for the action" }
|
|
1016
1558
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
const data = await response.json();
|
|
1559
|
+
}
|
|
1560
|
+
}),
|
|
1561
|
+
async execute(node, variables, context) {
|
|
1562
|
+
const cfg = node.connectorConfig;
|
|
1563
|
+
if (!cfg?.connectorId || !cfg?.actionId) {
|
|
1023
1564
|
return {
|
|
1024
|
-
success:
|
|
1025
|
-
|
|
1026
|
-
error: response.ok ? void 0 : `HTTP ${response.status}`
|
|
1565
|
+
success: false,
|
|
1566
|
+
error: `connector_action '${node.id}': connectorConfig.connectorId and .actionId are required`
|
|
1027
1567
|
};
|
|
1028
1568
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1569
|
+
const handler = engine.resolveConnectorAction(cfg.connectorId, cfg.actionId);
|
|
1570
|
+
if (!handler) {
|
|
1571
|
+
return {
|
|
1572
|
+
success: false,
|
|
1573
|
+
error: `connector_action '${node.id}': no handler for '${cfg.connectorId}.${cfg.actionId}' \u2014 is the connector plugin registered?`
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
const handlerCtx = {
|
|
1577
|
+
variables,
|
|
1578
|
+
automation: context,
|
|
1579
|
+
logger: ctx.logger
|
|
1580
|
+
};
|
|
1581
|
+
try {
|
|
1582
|
+
const output = await handler(cfg.input ?? {}, handlerCtx);
|
|
1583
|
+
return { success: true, output };
|
|
1584
|
+
} catch (err) {
|
|
1585
|
+
return {
|
|
1586
|
+
success: false,
|
|
1587
|
+
error: `connector_action(${cfg.connectorId}.${cfg.actionId}) failed: ${err.message}`
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
ctx.logger.info("[Connector] 1 built-in node executor registered (connector_action)");
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// src/builtin/notify-node.ts
|
|
1596
|
+
var import_automation7 = require("@objectstack/spec/automation");
|
|
1597
|
+
function toStringList(value) {
|
|
1598
|
+
if (Array.isArray(value)) return value.map((v) => String(v)).filter(Boolean);
|
|
1599
|
+
if (typeof value === "string" && value.trim()) return [value.trim()];
|
|
1600
|
+
return [];
|
|
1601
|
+
}
|
|
1602
|
+
function registerNotifyNode(engine, ctx) {
|
|
1603
|
+
const getMessaging = () => {
|
|
1604
|
+
try {
|
|
1605
|
+
return ctx.getService("messaging");
|
|
1606
|
+
} catch {
|
|
1607
|
+
return void 0;
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
engine.registerNodeExecutor({
|
|
1611
|
+
type: "notify",
|
|
1612
|
+
descriptor: (0, import_automation7.defineActionDescriptor)({
|
|
1613
|
+
type: "notify",
|
|
1614
|
+
version: "1.0.0",
|
|
1615
|
+
name: "Notify",
|
|
1616
|
+
description: "Send an outbound notification to users via the messaging service (inbox / email / push / \u2026).",
|
|
1617
|
+
icon: "bell",
|
|
1618
|
+
category: "io",
|
|
1619
|
+
source: "builtin",
|
|
1620
|
+
supportsRetry: true,
|
|
1621
|
+
paradigms: ["flow", "workflow_rule", "approval"]
|
|
1622
|
+
}),
|
|
1623
|
+
async execute(node, variables, context) {
|
|
1624
|
+
const cfg = node.config ?? {};
|
|
1625
|
+
const recipients = toStringList(interpolate(cfg.recipients ?? cfg.to ?? [], variables, context));
|
|
1626
|
+
const title = String(interpolate(cfg.title ?? cfg.subject ?? "", variables, context) ?? "");
|
|
1627
|
+
const body = String(interpolate(cfg.message ?? cfg.body ?? "", variables, context) ?? "");
|
|
1628
|
+
const channels = toStringList(cfg.channels);
|
|
1629
|
+
const topic = cfg.topic ? String(cfg.topic) : void 0;
|
|
1630
|
+
const severity = cfg.severity ? String(cfg.severity) : void 0;
|
|
1631
|
+
const actionUrl = cfg.actionUrl ? String(interpolate(cfg.actionUrl, variables, context) ?? "") : void 0;
|
|
1632
|
+
const payload = cfg.payload ? interpolate(cfg.payload, variables, context) : void 0;
|
|
1633
|
+
if (!title) return { success: false, error: "notify: title (or subject) is required" };
|
|
1634
|
+
if (recipients.length === 0) {
|
|
1635
|
+
return { success: false, error: "notify: at least one recipient is required" };
|
|
1636
|
+
}
|
|
1637
|
+
const messaging = getMessaging();
|
|
1638
|
+
if (!messaging) {
|
|
1639
|
+
ctx.logger.warn(
|
|
1640
|
+
`[notify] no messaging service registered; notification "${title}" not delivered`
|
|
1039
1641
|
);
|
|
1040
|
-
return {
|
|
1642
|
+
return {
|
|
1643
|
+
success: true,
|
|
1644
|
+
output: { delivered: 0, failed: 0, skipped: true }
|
|
1645
|
+
};
|
|
1041
1646
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1647
|
+
try {
|
|
1648
|
+
const result = await messaging.emit({
|
|
1649
|
+
topic: topic ?? "notify",
|
|
1650
|
+
audience: recipients,
|
|
1651
|
+
payload: { ...payload ?? {}, title, body, url: actionUrl },
|
|
1652
|
+
severity,
|
|
1653
|
+
channels: channels.length ? channels : void 0
|
|
1654
|
+
});
|
|
1655
|
+
return {
|
|
1656
|
+
success: true,
|
|
1657
|
+
output: {
|
|
1658
|
+
notificationId: result.notificationId,
|
|
1659
|
+
delivered: result.delivered,
|
|
1660
|
+
failed: result.failed
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
} catch (err) {
|
|
1664
|
+
return { success: false, error: `notify failed: ${err.message}` };
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
ctx.logger.info("[Notify] 1 built-in node executor registered (notify)");
|
|
1669
|
+
}
|
|
1046
1670
|
|
|
1047
|
-
// src/
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1671
|
+
// src/builtin/index.ts
|
|
1672
|
+
function installBuiltinNodes(engine, ctx) {
|
|
1673
|
+
registerLogicNodes(engine, ctx);
|
|
1674
|
+
registerCrudNodes(engine, ctx);
|
|
1675
|
+
registerScreenNodes(engine, ctx);
|
|
1676
|
+
registerHttpNodes(engine, ctx);
|
|
1677
|
+
registerConnectorNodes(engine, ctx);
|
|
1678
|
+
registerNotifyNode(engine, ctx);
|
|
1679
|
+
const types = engine.getRegisteredNodeTypes();
|
|
1680
|
+
ctx.logger.info(
|
|
1681
|
+
`[Automation] ${types.length} built-in node executors installed: ${types.join(", ")}`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// src/plugin.ts
|
|
1686
|
+
var AutomationServicePlugin = class {
|
|
1687
|
+
constructor(options = {}) {
|
|
1688
|
+
this.name = "com.objectstack.service-automation";
|
|
1051
1689
|
this.version = "1.0.0";
|
|
1052
1690
|
this.type = "standard";
|
|
1053
|
-
|
|
1691
|
+
// Soft dependency on metadata: we look it up at start() and tolerate absence.
|
|
1692
|
+
// Do NOT declare a hard kernel dependency, so this plugin works in environments
|
|
1693
|
+
// where MetadataPlugin is not registered.
|
|
1694
|
+
this.dependencies = [];
|
|
1695
|
+
this.options = options;
|
|
1054
1696
|
}
|
|
1055
1697
|
async init(ctx) {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1698
|
+
this.engine = new AutomationEngine(ctx.logger);
|
|
1699
|
+
ctx.registerService("automation", this.engine);
|
|
1700
|
+
installBuiltinNodes(this.engine, ctx);
|
|
1701
|
+
if (this.options.debug) {
|
|
1702
|
+
ctx.hook("automation:beforeExecute", async (flowName) => {
|
|
1703
|
+
ctx.logger.debug(`[Automation] Before execute: ${flowName}`);
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
ctx.logger.info("[Automation] Engine initialized");
|
|
1707
|
+
}
|
|
1708
|
+
async start(ctx) {
|
|
1709
|
+
console.warn("[Automation:start] entering start()");
|
|
1710
|
+
if (!this.engine) {
|
|
1711
|
+
console.warn("[Automation:start] engine missing, bailing");
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
await ctx.trigger("automation:ready", this.engine);
|
|
1715
|
+
const nodeTypes = this.engine.getRegisteredNodeTypes();
|
|
1716
|
+
ctx.logger.info(
|
|
1717
|
+
`[Automation] Engine started with ${nodeTypes.length} node types: ${nodeTypes.join(", ") || "(none)"}`
|
|
1718
|
+
);
|
|
1719
|
+
try {
|
|
1720
|
+
const ql = ctx.getService("objectql");
|
|
1721
|
+
if (!ql) {
|
|
1722
|
+
console.warn("[Automation] objectql service not found at start()");
|
|
1723
|
+
} else if (!ql.registry) {
|
|
1724
|
+
console.warn("[Automation] objectql.registry is undefined at start()");
|
|
1725
|
+
} else if (typeof ql.registry.listItems !== "function") {
|
|
1726
|
+
console.warn("[Automation] objectql.registry.listItems is not a function");
|
|
1061
1727
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
const
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
output: {
|
|
1075
|
-
actionType,
|
|
1076
|
-
template: cfg.template,
|
|
1077
|
-
recipients: cfg.recipients
|
|
1078
|
-
}
|
|
1079
|
-
};
|
|
1728
|
+
const flows = ql?.registry?.listItems?.("flow") ?? [];
|
|
1729
|
+
console.warn(`[Automation] flow pull: registry returned ${flows.length} flow(s)`);
|
|
1730
|
+
let registered = 0;
|
|
1731
|
+
for (const f of flows) {
|
|
1732
|
+
const def = f;
|
|
1733
|
+
if (!def?.name) continue;
|
|
1734
|
+
try {
|
|
1735
|
+
this.engine.registerFlow(def.name, def);
|
|
1736
|
+
registered++;
|
|
1737
|
+
} catch (e) {
|
|
1738
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1739
|
+
ctx.logger.warn(`[Automation] failed to register flow ${def.name}: ${msg}`);
|
|
1080
1740
|
}
|
|
1081
|
-
ctx.logger.info(`[Script:${actionType}] node=${node.id} executed (no-op handler)`);
|
|
1082
|
-
return { success: true, output: { actionType } };
|
|
1083
1741
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1742
|
+
if (registered > 0) {
|
|
1743
|
+
ctx.logger.info(`[Automation] Pulled ${registered} flow(s) from ObjectQL registry`);
|
|
1744
|
+
}
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1747
|
+
ctx.logger.warn(`[Automation] flow pull from ObjectQL registry failed: ${msg}`);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
async destroy() {
|
|
1751
|
+
this.engine = void 0;
|
|
1086
1752
|
}
|
|
1087
1753
|
};
|
|
1088
1754
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1089
1755
|
0 && (module.exports = {
|
|
1090
1756
|
AutomationEngine,
|
|
1091
1757
|
AutomationServicePlugin,
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1758
|
+
installBuiltinNodes,
|
|
1759
|
+
registerConnectorNodes,
|
|
1760
|
+
registerCrudNodes,
|
|
1761
|
+
registerHttpNodes,
|
|
1762
|
+
registerLogicNodes,
|
|
1763
|
+
registerScreenNodes
|
|
1096
1764
|
});
|
|
1097
1765
|
//# sourceMappingURL=index.cjs.map
|