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