@objectstack/service-automation 9.5.1 → 9.7.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.d.cts CHANGED
@@ -137,6 +137,36 @@ interface RegisteredConnector {
137
137
  readonly def: Connector;
138
138
  readonly handlers: Record<string, ConnectorActionHandler>;
139
139
  }
140
+ /**
141
+ * Context handed to a named handler function invoked from a `script` node
142
+ * (#1870). Mirrors {@link ConnectorActionContext} but carries the node's mapped
143
+ * `input` so the function reads its arguments without reaching into the raw
144
+ * variable map. The function's return value becomes the node output.
145
+ */
146
+ interface FlowFunctionContext {
147
+ /** Inputs mapped from the node's `config.inputs` (already in scope). */
148
+ readonly input: Record<string, unknown>;
149
+ /** Live flow variable map — read prior-node output / write results. */
150
+ readonly variables: Map<string, unknown>;
151
+ /** The flow execution / trigger context. */
152
+ readonly automation: AutomationContext;
153
+ readonly logger: Logger;
154
+ }
155
+ /**
156
+ * A named handler function callable from a `script` node. Returns the node's
157
+ * output (any JSON-serializable value); returning `undefined` yields an empty
158
+ * output. Authored packages contribute these via `defineStack({ functions })`,
159
+ * which the host bridges in through {@link AutomationEngine.setFunctionResolver}.
160
+ */
161
+ type FlowFunctionHandler = (ctx: FlowFunctionContext) => unknown | Promise<unknown>;
162
+ /**
163
+ * Resolves a function name to its handler. Injected by the host (the automation
164
+ * plugin bridges it to ObjectQL's `resolveFunction`, fed by `bundle.functions`),
165
+ * so the engine stays decoupled from any specific function registry. Returns
166
+ * `undefined` for an unknown name, letting the `script` node fail the step
167
+ * loudly instead of silently no-op'ing (#1870).
168
+ */
169
+ type FlowFunctionResolver = (name: string) => FlowFunctionHandler | undefined;
140
170
  /**
141
171
  * A designer-facing view of one connector action — identity + its JSON-Schema
142
172
  * input/output. The runtime handler is intentionally omitted; this is metadata.
@@ -301,6 +331,8 @@ declare class AutomationEngine implements IAutomationService {
301
331
  private boundFlowTriggers;
302
332
  /** Connectors registered by integration plugins, keyed by connector name (ADR-0018 §Addendum). */
303
333
  private connectors;
334
+ /** Bridge to the host function registry for `script`-node calls (#1870), if wired. */
335
+ private functionResolver;
304
336
  private executionLogs;
305
337
  private readonly maxLogSize;
306
338
  private logger;
@@ -415,6 +447,20 @@ declare class AutomationEngine implements IAutomationService {
415
447
  * is not registered, so the node can fail the step with a clear error.
416
448
  */
417
449
  resolveConnectorAction(connectorId: string, actionId: string): ConnectorActionHandler | undefined;
450
+ /**
451
+ * Wire the engine to the host's named-function registry (#1870). The
452
+ * automation plugin calls this in `start()` with a resolver backed by
453
+ * ObjectQL's `resolveFunction` (populated from `bundle.functions` /
454
+ * `defineStack({ functions })`), so a `script` node can invoke an
455
+ * authored function by name. Passing `null` detaches the bridge.
456
+ */
457
+ setFunctionResolver(resolver: FlowFunctionResolver | null): void;
458
+ /**
459
+ * Resolve a named function for a `script` node. Returns `undefined` when no
460
+ * resolver is wired or the name is unregistered — the node then fails the
461
+ * step with a clear error rather than silently no-op'ing.
462
+ */
463
+ resolveFunction(name: string): FlowFunctionHandler | undefined;
418
464
  /** Get all registered connector names. */
419
465
  getRegisteredConnectors(): string[];
420
466
  /**
@@ -5009,22 +5055,6 @@ declare function registerLogicNodes(engine: AutomationEngine, ctx: PluginContext
5009
5055
  */
5010
5056
  declare function registerCrudNodes(engine: AutomationEngine, ctx: PluginContext): void;
5011
5057
 
5012
- /**
5013
- * Screen / Script built-in nodes — 'screen' and 'script' executors.
5014
- * Part of the core flow capability, so the {@link AutomationServicePlugin}
5015
- * seeds them directly (ADR-0018) rather than shipping a separate plugin.
5016
- *
5017
- * - 'screen' nodes collect user input. A screen that declares `config.fields`
5018
- * (or sets `config.waitForInput === true`) suspends the run on entry via the
5019
- * engine's durable pause (ADR-0019), surfacing a `ScreenSpec` for the client
5020
- * to render; the run continues via `resume()` with the collected values (set
5021
- * as bare flow variables). A field-less screen — or one with
5022
- * `waitForInput === false` — stays a server pass-through (input vars, if any,
5023
- * are already injected from `context.params`).
5024
- * - 'script' nodes dispatch by `config.actionType`. Currently only 'email'
5025
- * has a (logger-backed) implementation; unknown action types still succeed
5026
- * so flows can continue and downstream nodes can react.
5027
- */
5028
5058
  declare function registerScreenNodes(engine: AutomationEngine, ctx: PluginContext): void;
5029
5059
 
5030
5060
  declare function registerHttpNodes(engine: AutomationEngine, ctx: PluginContext): void;
package/dist/index.d.ts CHANGED
@@ -137,6 +137,36 @@ interface RegisteredConnector {
137
137
  readonly def: Connector;
138
138
  readonly handlers: Record<string, ConnectorActionHandler>;
139
139
  }
140
+ /**
141
+ * Context handed to a named handler function invoked from a `script` node
142
+ * (#1870). Mirrors {@link ConnectorActionContext} but carries the node's mapped
143
+ * `input` so the function reads its arguments without reaching into the raw
144
+ * variable map. The function's return value becomes the node output.
145
+ */
146
+ interface FlowFunctionContext {
147
+ /** Inputs mapped from the node's `config.inputs` (already in scope). */
148
+ readonly input: Record<string, unknown>;
149
+ /** Live flow variable map — read prior-node output / write results. */
150
+ readonly variables: Map<string, unknown>;
151
+ /** The flow execution / trigger context. */
152
+ readonly automation: AutomationContext;
153
+ readonly logger: Logger;
154
+ }
155
+ /**
156
+ * A named handler function callable from a `script` node. Returns the node's
157
+ * output (any JSON-serializable value); returning `undefined` yields an empty
158
+ * output. Authored packages contribute these via `defineStack({ functions })`,
159
+ * which the host bridges in through {@link AutomationEngine.setFunctionResolver}.
160
+ */
161
+ type FlowFunctionHandler = (ctx: FlowFunctionContext) => unknown | Promise<unknown>;
162
+ /**
163
+ * Resolves a function name to its handler. Injected by the host (the automation
164
+ * plugin bridges it to ObjectQL's `resolveFunction`, fed by `bundle.functions`),
165
+ * so the engine stays decoupled from any specific function registry. Returns
166
+ * `undefined` for an unknown name, letting the `script` node fail the step
167
+ * loudly instead of silently no-op'ing (#1870).
168
+ */
169
+ type FlowFunctionResolver = (name: string) => FlowFunctionHandler | undefined;
140
170
  /**
141
171
  * A designer-facing view of one connector action — identity + its JSON-Schema
142
172
  * input/output. The runtime handler is intentionally omitted; this is metadata.
@@ -301,6 +331,8 @@ declare class AutomationEngine implements IAutomationService {
301
331
  private boundFlowTriggers;
302
332
  /** Connectors registered by integration plugins, keyed by connector name (ADR-0018 §Addendum). */
303
333
  private connectors;
334
+ /** Bridge to the host function registry for `script`-node calls (#1870), if wired. */
335
+ private functionResolver;
304
336
  private executionLogs;
305
337
  private readonly maxLogSize;
306
338
  private logger;
@@ -415,6 +447,20 @@ declare class AutomationEngine implements IAutomationService {
415
447
  * is not registered, so the node can fail the step with a clear error.
416
448
  */
417
449
  resolveConnectorAction(connectorId: string, actionId: string): ConnectorActionHandler | undefined;
450
+ /**
451
+ * Wire the engine to the host's named-function registry (#1870). The
452
+ * automation plugin calls this in `start()` with a resolver backed by
453
+ * ObjectQL's `resolveFunction` (populated from `bundle.functions` /
454
+ * `defineStack({ functions })`), so a `script` node can invoke an
455
+ * authored function by name. Passing `null` detaches the bridge.
456
+ */
457
+ setFunctionResolver(resolver: FlowFunctionResolver | null): void;
458
+ /**
459
+ * Resolve a named function for a `script` node. Returns `undefined` when no
460
+ * resolver is wired or the name is unregistered — the node then fails the
461
+ * step with a clear error rather than silently no-op'ing.
462
+ */
463
+ resolveFunction(name: string): FlowFunctionHandler | undefined;
418
464
  /** Get all registered connector names. */
419
465
  getRegisteredConnectors(): string[];
420
466
  /**
@@ -5009,22 +5055,6 @@ declare function registerLogicNodes(engine: AutomationEngine, ctx: PluginContext
5009
5055
  */
5010
5056
  declare function registerCrudNodes(engine: AutomationEngine, ctx: PluginContext): void;
5011
5057
 
5012
- /**
5013
- * Screen / Script built-in nodes — 'screen' and 'script' executors.
5014
- * Part of the core flow capability, so the {@link AutomationServicePlugin}
5015
- * seeds them directly (ADR-0018) rather than shipping a separate plugin.
5016
- *
5017
- * - 'screen' nodes collect user input. A screen that declares `config.fields`
5018
- * (or sets `config.waitForInput === true`) suspends the run on entry via the
5019
- * engine's durable pause (ADR-0019), surfacing a `ScreenSpec` for the client
5020
- * to render; the run continues via `resume()` with the collected values (set
5021
- * as bare flow variables). A field-less screen — or one with
5022
- * `waitForInput === false` — stays a server pass-through (input vars, if any,
5023
- * are already injected from `context.params`).
5024
- * - 'script' nodes dispatch by `config.actionType`. Currently only 'email'
5025
- * has a (logger-backed) implementation; unknown action types still succeed
5026
- * so flows can continue and downstream nodes can react.
5027
- */
5028
5058
  declare function registerScreenNodes(engine: AutomationEngine, ctx: PluginContext): void;
5029
5059
 
5030
5060
  declare function registerHttpNodes(engine: AutomationEngine, ctx: PluginContext): void;
package/dist/index.js CHANGED
@@ -30,6 +30,8 @@ var _AutomationEngine = class _AutomationEngine {
30
30
  this.boundFlowTriggers = /* @__PURE__ */ new Map();
31
31
  /** Connectors registered by integration plugins, keyed by connector name (ADR-0018 §Addendum). */
32
32
  this.connectors = /* @__PURE__ */ new Map();
33
+ /** Bridge to the host function registry for `script`-node calls (#1870), if wired. */
34
+ this.functionResolver = null;
33
35
  this.executionLogs = [];
34
36
  /**
35
37
  * Runs paused at a node, keyed by runId (ADR-0019). In-memory hot cache —
@@ -317,6 +319,24 @@ var _AutomationEngine = class _AutomationEngine {
317
319
  resolveConnectorAction(connectorId, actionId) {
318
320
  return this.connectors.get(connectorId)?.handlers[actionId];
319
321
  }
322
+ /**
323
+ * Wire the engine to the host's named-function registry (#1870). The
324
+ * automation plugin calls this in `start()` with a resolver backed by
325
+ * ObjectQL's `resolveFunction` (populated from `bundle.functions` /
326
+ * `defineStack({ functions })`), so a `script` node can invoke an
327
+ * authored function by name. Passing `null` detaches the bridge.
328
+ */
329
+ setFunctionResolver(resolver) {
330
+ this.functionResolver = resolver;
331
+ }
332
+ /**
333
+ * Resolve a named function for a `script` node. Returns `undefined` when no
334
+ * resolver is wired or the name is unregistered — the node then fails the
335
+ * step with a clear error rather than silently no-op'ing.
336
+ */
337
+ resolveFunction(name) {
338
+ return this.functionResolver?.(name) ?? void 0;
339
+ }
320
340
  /** Get all registered connector names. */
321
341
  getRegisteredConnectors() {
322
342
  return [...this.connectors.keys()];
@@ -1860,7 +1880,7 @@ function resolveToken(token, variables, context) {
1860
1880
  if (path[0] === "Email") return resolvePath(context.user, ["email", ...path.slice(1)]) ?? void 0;
1861
1881
  return resolvePath(context.user, path);
1862
1882
  }
1863
- if (/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(trimmed)) {
1883
+ if (/^[A-Za-z_$][\w$]*(?:\.(?:[A-Za-z_$][\w$]*|\d+))*$/.test(trimmed)) {
1864
1884
  const segments = trimmed.split(".");
1865
1885
  const head = segments[0];
1866
1886
  if (variables.has(head)) {
@@ -2234,14 +2254,21 @@ function registerCrudNodes(engine, ctx) {
2234
2254
  const data = getData();
2235
2255
  if (!data) {
2236
2256
  ctx.logger.warn(`[create_record] no data engine; skipping ${objectName}`);
2237
- if (outputVariable) variables.set(outputVariable, `mock-${objectName}-${Date.now()}`);
2238
- return { success: true, output: { id: `mock-${objectName}-${Date.now()}`, object: objectName } };
2257
+ const mockId = `mock-${objectName}-${Date.now()}`;
2258
+ if (outputVariable) variables.set(outputVariable, { id: mockId });
2259
+ return { success: true, output: { id: mockId, object: objectName } };
2239
2260
  }
2240
2261
  try {
2241
2262
  const created = await data.insert(objectName, fields);
2242
- const insertedId = Array.isArray(created) ? created[0]?.id : created?.id ?? created;
2243
- if (outputVariable) variables.set(outputVariable, insertedId);
2244
- return { success: true, output: { id: insertedId, record: created, object: objectName } };
2263
+ const createdRecord = Array.isArray(created) ? created[0] : created;
2264
+ const insertedId = createdRecord && typeof createdRecord === "object" ? createdRecord.id : createdRecord;
2265
+ if (outputVariable) {
2266
+ variables.set(
2267
+ outputVariable,
2268
+ createdRecord && typeof createdRecord === "object" ? createdRecord : { id: insertedId }
2269
+ );
2270
+ }
2271
+ return { success: true, output: { id: insertedId, record: createdRecord, object: objectName } };
2245
2272
  } catch (err) {
2246
2273
  return { success: false, error: `create_record(${objectName}) failed: ${err.message}` };
2247
2274
  }
@@ -2308,6 +2335,7 @@ function registerCrudNodes(engine, ctx) {
2308
2335
 
2309
2336
  // src/builtin/screen-nodes.ts
2310
2337
  import { defineActionDescriptor as defineActionDescriptor7 } from "@objectstack/spec/automation";
2338
+ var SCRIPT_BUILTIN_ACTION_TYPES = /* @__PURE__ */ new Set(["email", "slack"]);
2311
2339
  function registerScreenNodes(engine, ctx) {
2312
2340
  engine.registerNodeExecutor({
2313
2341
  type: "screen",
@@ -2363,24 +2391,53 @@ function registerScreenNodes(engine, ctx) {
2363
2391
  category: "logic",
2364
2392
  source: "builtin"
2365
2393
  }),
2366
- async execute(node, _variables, _context) {
2394
+ async execute(node, variables, context) {
2367
2395
  const cfg = node.config ?? {};
2368
- const actionType = cfg.actionType ?? "noop";
2369
- if (actionType === "email") {
2396
+ const fnRaw = cfg.function ?? cfg.functionName;
2397
+ const fnName = typeof fnRaw === "string" && fnRaw.trim() ? fnRaw.trim() : void 0;
2398
+ const actionType = typeof cfg.actionType === "string" && cfg.actionType.trim() ? cfg.actionType.trim() : void 0;
2399
+ if (!fnName && actionType && SCRIPT_BUILTIN_ACTION_TYPES.has(actionType)) {
2370
2400
  ctx.logger.info(
2371
- `[Script:email] template=${String(cfg.template)} recipients=${JSON.stringify(cfg.recipients)} vars=${JSON.stringify(cfg.variables)}`
2401
+ `[Script:${actionType}] template=${String(cfg.template)} recipients=${JSON.stringify(cfg.recipients)} vars=${JSON.stringify(cfg.variables)}`
2372
2402
  );
2373
2403
  return {
2374
2404
  success: true,
2375
- output: {
2376
- actionType,
2377
- template: cfg.template,
2378
- recipients: cfg.recipients
2379
- }
2405
+ output: { actionType, template: cfg.template, recipients: cfg.recipients }
2406
+ };
2407
+ }
2408
+ const inlineScript = typeof cfg.script === "string" && cfg.script.trim() ? cfg.script : void 0;
2409
+ if (!fnName && inlineScript) {
2410
+ ctx.logger.warn(
2411
+ `[Script] node '${node.id}': inline \`config.script\` is not executed by the built-in runtime (no server-side JS sandbox) \u2014 this node is a no-op. To run server logic, move it into a registered function and call it via \`config.function\` + \`defineStack({ functions })\`.`
2412
+ );
2413
+ return { success: true, output: { script: "not-executed" } };
2414
+ }
2415
+ const target = fnName ?? (actionType === "invoke_function" ? void 0 : actionType);
2416
+ if (!target) {
2417
+ return {
2418
+ success: false,
2419
+ error: actionType === "invoke_function" ? `script node '${node.id}': actionType 'invoke_function' requires \`config.function\` (or \`functionName\`) naming the function to call.` : `script node '${node.id}': declares neither \`actionType\` nor \`function\` \u2014 nothing to run.`
2420
+ };
2421
+ }
2422
+ const handler = engine.resolveFunction(target);
2423
+ if (!handler) {
2424
+ return {
2425
+ success: false,
2426
+ error: `script node '${node.id}': '${target}' is not a built-in action (${[...SCRIPT_BUILTIN_ACTION_TYPES].join(", ")}) and no function named '${target}' is registered. Register it via \`defineStack({ functions: { '${target}': fn } })\`, or fix the name (#1870).`
2427
+ };
2428
+ }
2429
+ const input = interpolate(cfg.inputs ?? cfg.input ?? {}, variables, context);
2430
+ const outputVariable = typeof cfg.outputVariable === "string" && cfg.outputVariable.trim() ? cfg.outputVariable.trim() : void 0;
2431
+ try {
2432
+ const result = await handler({ input, variables, automation: context, logger: ctx.logger });
2433
+ if (outputVariable) variables.set(outputVariable, result);
2434
+ return { success: true, output: { function: target, result } };
2435
+ } catch (err) {
2436
+ return {
2437
+ success: false,
2438
+ error: `script function '${target}' (node '${node.id}') failed: ${err.message}`
2380
2439
  };
2381
2440
  }
2382
- ctx.logger.info(`[Script:${actionType}] node=${node.id} executed (no-op handler)`);
2383
- return { success: true, output: { actionType } };
2384
2441
  }
2385
2442
  });
2386
2443
  ctx.logger.info("[Screen/Script Nodes] 2 built-in node executors registered");
@@ -3030,6 +3087,18 @@ var AutomationServicePlugin = class {
3030
3087
  ctx.logger.info("[Automation] No ObjectQL engine \u2014 suspended runs kept in-memory only");
3031
3088
  }
3032
3089
  }
3090
+ try {
3091
+ const fnRegistry = ctx.getService("objectql");
3092
+ if (fnRegistry && typeof fnRegistry.resolveFunction === "function") {
3093
+ this.engine.setFunctionResolver((name) => {
3094
+ const fn = fnRegistry.resolveFunction(name);
3095
+ return typeof fn === "function" ? (fnCtx) => fn(fnCtx) : void 0;
3096
+ });
3097
+ ctx.logger.debug("[Automation] script-node function registry bridged to objectql.resolveFunction");
3098
+ }
3099
+ } catch {
3100
+ ctx.logger.debug("[Automation] objectql not present \u2014 script-node function calls will fail loudly when used");
3101
+ }
3033
3102
  try {
3034
3103
  const ql = ctx.getService("objectql");
3035
3104
  if (!ql) {