@itmetsam/samurai-framework 0.3.0 → 0.5.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.
Files changed (66) hide show
  1. package/dist/cli/flow-reporter.d.ts +5 -2
  2. package/dist/cli/flow-reporter.js +4 -2
  3. package/dist/cli/flow.d.ts +1 -1
  4. package/dist/cli/flow.js +4 -2
  5. package/dist/config/config.d.ts +5 -0
  6. package/dist/config/config.js +13 -0
  7. package/dist/flows/core/check.d.ts +4 -1
  8. package/dist/flows/core/check.js +17 -14
  9. package/dist/flows/core/expression/check.d.ts +2 -0
  10. package/dist/flows/core/expression/check.js +3 -3
  11. package/dist/flows/core/expression/evaluate.d.ts +1 -1
  12. package/dist/flows/core/expression/scope.d.ts +3 -2
  13. package/dist/flows/core/expression/scope.js +15 -6
  14. package/dist/flows/core/index.d.ts +2 -1
  15. package/dist/flows/core/index.js +2 -1
  16. package/dist/flows/core/nodes/define.d.ts +4 -0
  17. package/dist/flows/core/nodes/index.d.ts +4 -66
  18. package/dist/flows/core/nodes/index.js +12 -38
  19. package/dist/flows/core/steps.d.ts +42 -0
  20. package/dist/flows/core/steps.js +58 -0
  21. package/dist/flows/run/events.d.ts +3 -1
  22. package/dist/flows/run/handlers/index.d.ts +1 -3
  23. package/dist/flows/run/handlers/index.js +3 -8
  24. package/dist/flows/run/handlers/support.d.ts +4 -1
  25. package/dist/flows/run/handlers/support.js +1 -1
  26. package/dist/flows/run/index.d.ts +2 -1
  27. package/dist/flows/run/index.js +2 -1
  28. package/dist/flows/run/project.js +18 -19
  29. package/dist/flows/run/run.d.ts +1 -1
  30. package/dist/flows/run/run.js +7 -1
  31. package/dist/flows/steps/api.d.ts +23 -0
  32. package/dist/flows/steps/api.js +63 -0
  33. package/dist/flows/steps/catalog.d.ts +22 -0
  34. package/dist/flows/steps/catalog.js +36 -0
  35. package/dist/flows/steps/define.d.ts +51 -0
  36. package/dist/flows/steps/define.js +51 -0
  37. package/dist/flows/steps/handler.d.ts +4 -0
  38. package/dist/flows/steps/handler.js +82 -0
  39. package/dist/flows/steps/index.d.ts +6 -0
  40. package/dist/flows/steps/index.js +5 -0
  41. package/dist/flows/{core/nodes → steps}/set-variable.d.ts +4 -1
  42. package/dist/flows/steps/set-variable.js +31 -0
  43. package/dist/flows/steps/set.d.ts +26 -0
  44. package/dist/flows/steps/set.js +43 -0
  45. package/dist/flows/steps/wait.d.ts +8 -0
  46. package/dist/flows/steps/wait.js +21 -0
  47. package/dist/runner/groups.d.ts +5 -0
  48. package/dist/runner/groups.js +13 -0
  49. package/dist/runner/test-runner.js +4 -3
  50. package/dist/types/config.d.ts +7 -0
  51. package/package.json +5 -1
  52. package/dist/flows/core/nodes/api.d.ts +0 -9
  53. package/dist/flows/core/nodes/api.js +0 -12
  54. package/dist/flows/core/nodes/database.d.ts +0 -9
  55. package/dist/flows/core/nodes/database.js +0 -12
  56. package/dist/flows/core/nodes/email.d.ts +0 -9
  57. package/dist/flows/core/nodes/email.js +0 -12
  58. package/dist/flows/core/nodes/script.d.ts +0 -9
  59. package/dist/flows/core/nodes/script.js +0 -12
  60. package/dist/flows/core/nodes/set-variable.js +0 -39
  61. package/dist/flows/core/nodes/wait.d.ts +0 -15
  62. package/dist/flows/core/nodes/wait.js +0 -18
  63. package/dist/flows/run/handlers/set-variable.d.ts +0 -2
  64. package/dist/flows/run/handlers/set-variable.js +0 -22
  65. package/dist/flows/run/handlers/wait.d.ts +0 -2
  66. package/dist/flows/run/handlers/wait.js +0 -10
@@ -1,10 +1,12 @@
1
- import { type FlowFile } from "../flows/core/index.js";
1
+ import { type AnyNodeDefinition, type FlowFile } from "../flows/core/index.js";
2
2
  import { type FlowEvent } from "../flows/run/index.js";
3
3
  export interface FlowReporterOptions {
4
4
  flow: FlowFile;
5
5
  write: (text: string) => void;
6
6
  /** Colour with ANSI codes */
7
7
  color: boolean;
8
+ /** Definitions of the project's steps, for their labels. @default the built-in steps */
9
+ steps?: readonly AnyNodeDefinition[];
8
10
  }
9
11
  /** Prints a flow run for a person: one line per node as it ends, then a count. Start and End are not listed. */
10
12
  export declare class FlowReporter {
@@ -12,8 +14,9 @@ export declare class FlowReporter {
12
14
  private readonly paint;
13
15
  private readonly nodes;
14
16
  private readonly flow;
17
+ private readonly steps;
15
18
  private started;
16
- constructor({ flow, write, color }: FlowReporterOptions);
19
+ constructor({ flow, write, color, steps }: FlowReporterOptions);
17
20
  private shapeOf;
18
21
  private listed;
19
22
  private nameOf;
@@ -1,11 +1,13 @@
1
1
  import { nodeDef, } from "../flows/core/index.js";
2
2
  import { maskSecrets as maskText, } from "../flows/run/index.js";
3
+ import { STEP_NODES } from "../flows/steps/index.js";
3
4
  const took = (ms) => ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
4
5
  const SKIPPED_BY_SHAPE = new Set(["start", "end"]);
5
6
  /** Prints a flow run for a person: one line per node as it ends, then a count. Start and End are not listed. */
6
7
  export class FlowReporter {
7
- constructor({ flow, write, color }) {
8
+ constructor({ flow, write, color, steps }) {
8
9
  this.started = Date.now();
10
+ this.steps = steps ?? STEP_NODES;
9
11
  this.flow = flow;
10
12
  this.write = write;
11
13
  this.paint = (code, text) => color ? `\u001b[${code}m${text}\u001b[0m` : text;
@@ -28,7 +30,7 @@ export class FlowReporter {
28
30
  label = nodeDef(node.kind).label;
29
31
  }
30
32
  catch {
31
- label = node.kind;
33
+ label = this.steps.find((d) => d.kind === node.kind)?.label ?? node.kind;
32
34
  }
33
35
  const base = node.title || label;
34
36
  const wrote = result.wrote ? Object.keys(result.wrote) : [];
@@ -1,4 +1,4 @@
1
1
  import type { Io } from "./main.js";
2
- export declare const FLOW_USAGE = "Usage:\n samurai flow list [--json] List the flows in ./flows\n samurai flow check [<flow>...] [options] Check flows without running them (all flows when none is given)\n samurai flow run <flow>... [options] Run flows, in the order given\n samurai flow run --all [options] Run every flow in ./flows, in file name order\n\nA flow is an id (flows/<id>.flow.json) or a path to a .flow.json file.\n\nOptions for check and run:\n --env <name> Environment to use (or SAMURAI_ENV)\n --allow-unsupported Skip nodes that can't run yet (HTTP request, ...) instead of refusing the flow\n --json Machine-readable output: JSON lines for run, one array for check and list\n\nOptions for run:\n --timeout <ms> Time one test may take\n --expect-timeout <ms> Time actions and assertions retry\n --headless, --no-headless Run the browser without a window, or with one\n --port <n> Browser debugging port (default 9223)\n\nEach run writes result/flows/<id>.json.\nExit code: 0 when every node that should run passed, 1 when a node failed or the run was cancelled,\n2 for a usage, configuration or flow-file error, or a flow that has errors (nothing ran).\n";
2
+ export declare const FLOW_USAGE = "Usage:\n samurai flow list [--json] List the flows in ./flows\n samurai flow check [<flow>...] [options] Check flows without running them (all flows when none is given)\n samurai flow run <flow>... [options] Run flows, in the order given\n samurai flow run --all [options] Run every flow in ./flows, in file name order\n\nA flow is an id (flows/<id>.flow.json) or a path to a .flow.json file.\n\nOptions for check and run:\n --env <name> Environment to use (or SAMURAI_ENV)\n --allow-unsupported Skip nodes whose kind has no handler instead of refusing the flow (every built-in kind runs)\n --json Machine-readable output: JSON lines for run, one array for check and list\n\nOptions for run:\n --timeout <ms> Time one test may take\n --expect-timeout <ms> Time actions and assertions retry\n --headless, --no-headless Run the browser without a window, or with one\n --port <n> Browser debugging port (default 9223)\n\nEach run writes result/flows/<id>.json.\nExit code: 0 when every node that should run passed, 1 when a node failed or the run was cancelled,\n2 for a usage, configuration or flow-file error, or a flow that has errors (nothing ran).\n";
3
3
  /** `samurai flow ...`: `argv` is what follows `flow`. Returns the exit code */
4
4
  export declare function flowCommand(argv: string[], io: Io): Promise<number>;
package/dist/cli/flow.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { parseCommandLine, parseRunnerFlags, parseRunOverrides, } from "./args.js";
3
3
  import { FlowReporter } from "./flow-reporter.js";
4
+ import { projectSteps } from "../flows/steps/index.js";
4
5
  import {} from "../flows/core/index.js";
5
6
  import { FLOW_SUFFIX, FlowCheckError, FlowReportBuilder, checkFlowInProject, flowsDir, listFlows, readFlow, readFlowPath, runFlowInProject, writeFlowReport, } from "../flows/run/index.js";
6
7
  export const FLOW_USAGE = `Usage:
@@ -13,7 +14,7 @@ A flow is an id (flows/<id>.flow.json) or a path to a .flow.json file.
13
14
 
14
15
  Options for check and run:
15
16
  --env <name> Environment to use (or SAMURAI_ENV)
16
- --allow-unsupported Skip nodes that can't run yet (HTTP request, ...) instead of refusing the flow
17
+ --allow-unsupported Skip nodes whose kind has no handler instead of refusing the flow (every built-in kind runs)
17
18
  --json Machine-readable output: JSON lines for run, one array for check and list
18
19
 
19
20
  Options for run:
@@ -136,6 +137,7 @@ async function run(refs, io, options) {
136
137
  io.err(`${flow.id} can't run:\n${problemLines(io, described)}Nothing was run.\n`);
137
138
  return 2;
138
139
  };
140
+ const { nodes: steps } = await projectSteps({ projectDir });
139
141
  // Several flows: refuse them all before the first one runs
140
142
  if (flows.length > 1) {
141
143
  for (const flow of flows) {
@@ -159,7 +161,7 @@ async function run(refs, io, options) {
159
161
  const builder = new FlowReportBuilder(flow);
160
162
  const human = options.json
161
163
  ? undefined
162
- : new FlowReporter({ flow, write: io.out, color: io.color });
164
+ : new FlowReporter({ flow, write: io.out, color: io.color, steps });
163
165
  if (flows.indexOf(flow) > 0 && !options.json)
164
166
  io.out("\n");
165
167
  let summary;
@@ -19,6 +19,11 @@ export declare function projectDir(): string;
19
19
  export declare function browsersDir(): string;
20
20
  /** The whole `samurai.config.ts` of the project (the working directory unless a project is active) */
21
21
  export declare function loadConfig(): Promise<SamuraiTestConfig>;
22
+ /** A project's config without keeping the project active (`withProject` is one at a time): the given one, else its samurai.config.ts */
23
+ export declare function projectConfig(options: {
24
+ projectDir?: string;
25
+ config?: SamuraiTestConfig;
26
+ }): Promise<SamuraiTestConfig>;
22
27
  export declare function readConfig<K extends keyof SamuraiTestConfig>(key: K): Promise<SamuraiTestConfig[K]>;
23
28
  export declare function defineConfig(config: SamuraiTestConfig): SamuraiTestConfig;
24
29
  export {};
@@ -32,6 +32,19 @@ export async function loadConfig() {
32
32
  }
33
33
  return config.default;
34
34
  }
35
+ /** A project's config without keeping the project active (`withProject` is one at a time): the given one, else its samurai.config.ts */
36
+ export async function projectConfig(options) {
37
+ if (options.config)
38
+ return options.config;
39
+ const previous = activeProject;
40
+ setActiveProject({ dir: path.resolve(options.projectDir ?? process.cwd()) });
41
+ try {
42
+ return await loadConfig();
43
+ }
44
+ finally {
45
+ setActiveProject(previous);
46
+ }
47
+ }
35
48
  export async function readConfig(key) {
36
49
  const value = (await loadConfig())[key];
37
50
  logger.debug("Resolved config value: ", { value });
@@ -1,4 +1,5 @@
1
1
  import { type FlowProblem } from "./nodes/index.js";
2
+ import type { AnyNodeDefinition } from "./nodes/define.js";
2
3
  import type { FlowFile, FlowNodeModel } from "./schema.js";
3
4
  export type { FlowProblem } from "./nodes/index.js";
4
5
  export interface CheckOptions {
@@ -27,6 +28,8 @@ export interface CheckOptions {
27
28
  * The SAMURAI app passes the editor's titles (a Test node shows its test's name).
28
29
  */
29
30
  titles?: (node: FlowNodeModel) => string | undefined;
31
+ /** Definitions of kinds core doesn't have (steps). A node of a kind in neither is "Unknown step". */
32
+ nodes?: readonly AnyNodeDefinition[];
30
33
  }
31
34
  /**
32
35
  * Checks a flow before a run: the shape of the graph, keys, references that still
@@ -39,4 +42,4 @@ export declare function checkFlow(flow: FlowFile, options?: CheckOptions): FlowP
39
42
  export declare function expressionProblems(flow: FlowFile, environment: {
40
43
  name: string;
41
44
  variables: Record<string, unknown>;
42
- }): FlowProblem[];
45
+ }, nodes?: readonly AnyNodeDefinition[]): FlowProblem[];
@@ -3,14 +3,15 @@ import { KEY_PATTERN } from "./keys.js";
3
3
  import { branchNames, dynamicOf, getPath, nodeDef, withDefaults, } from "./nodes/index.js";
4
4
  /** What the node is called in a message: its title, else its key, else its kind's label */
5
5
  const titleOf = (node, def) => node.title || node.key || def?.label || node.kind;
6
- const defOf = (kind) => {
7
- try {
8
- return nodeDef(kind);
9
- }
10
- catch {
11
- return undefined;
12
- }
13
- };
6
+ const definitions = (nodes = []) => (kind) => nodes.find((d) => d.kind === kind) ??
7
+ (() => {
8
+ try {
9
+ return nodeDef(kind);
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ })();
14
15
  /** Node ids in the order a run reaches them: from Start, following edges */
15
16
  function reachableFrom(flow, startId) {
16
17
  const out = new Map();
@@ -36,6 +37,7 @@ function reachableFrom(flow, startId) {
36
37
  * its key, else its kind's label (the app names it by what the editor shows).
37
38
  */
38
39
  export function checkFlow(flow, options = {}) {
40
+ const defOf = definitions(options.nodes);
39
41
  const problems = [];
40
42
  const tests = options.tests ? new Set(options.tests) : undefined;
41
43
  const groups = options.groups ? new Set(options.groups) : undefined;
@@ -45,7 +47,7 @@ export function checkFlow(flow, options = {}) {
45
47
  problems.push({
46
48
  level: "error",
47
49
  nodeId: n.id,
48
- message: `Unknown node kind: ${n.kind}`,
50
+ message: `Unknown step: ${n.kind}`,
49
51
  });
50
52
  const starts = flow.nodes.filter((n) => shapeOf(n) === "start");
51
53
  const ends = flow.nodes.filter((n) => shapeOf(n) === "end");
@@ -138,9 +140,9 @@ export function checkFlow(flow, options = {}) {
138
140
  else
139
141
  seen.set(n.key, n.id);
140
142
  }
141
- problems.push(...emptyExpressions(flow));
143
+ problems.push(...emptyExpressions(flow, defOf));
142
144
  if (options.environment)
143
- problems.push(...expressionProblems(flow, options.environment));
145
+ problems.push(...expressionProblems(flow, options.environment, options.nodes));
144
146
  const nodeCount = flow.nodes.filter((n) => {
145
147
  const s = shapeOf(n);
146
148
  return s !== "start" && s !== "end";
@@ -157,7 +159,7 @@ export function checkFlow(flow, options = {}) {
157
159
  return problems;
158
160
  }
159
161
  /** Every expression field left empty: it would stop the run at its node */
160
- function emptyExpressions(flow) {
162
+ function emptyExpressions(flow, defOf) {
161
163
  return flow.nodes.flatMap((n) => {
162
164
  const def = defOf(n.kind);
163
165
  if (!def)
@@ -176,7 +178,8 @@ function emptyExpressions(flow) {
176
178
  });
177
179
  }
178
180
  /** Every expression field that doesn't check out, as "Label: message" */
179
- export function expressionProblems(flow, environment) {
181
+ export function expressionProblems(flow, environment, nodes = []) {
182
+ const defOf = definitions(nodes);
180
183
  return flow.nodes.flatMap((n) => {
181
184
  const def = defOf(n.kind);
182
185
  if (!def)
@@ -187,7 +190,7 @@ export function expressionProblems(flow, environment) {
187
190
  const text = getPath(n, f.path);
188
191
  if (typeof text !== "string" || text.trim() === "")
189
192
  return [];
190
- const problem = problemOf(text, scopeAt(flow, n.id, environment));
193
+ const problem = problemOf(text, scopeAt(flow, n.id, environment, nodes));
191
194
  return problem
192
195
  ? [
193
196
  {
@@ -1,5 +1,7 @@
1
1
  import { type ExpressionError, type Parsed } from "./parse.js";
2
2
  export interface ExpressionScope {
3
+ /** Output names of the nodes that have them, by key */
4
+ outputs?: Record<string, readonly string[]>;
3
5
  /** The flow environment's name and its variable names */
4
6
  env: {
5
7
  name: string;
@@ -81,10 +81,10 @@ export function checkExpression(parsed, scope) {
81
81
  message: `${name} is on another branch, so it has no result here`,
82
82
  at,
83
83
  };
84
- if (field !== undefined &&
85
- !NODE_FIELDS.includes(field))
84
+ const fields = [...NODE_FIELDS, ...(scope.outputs?.[name] ?? [])];
85
+ if (field !== undefined && !fields.includes(field))
86
86
  return {
87
- message: `nodes.${name}.${field} doesn't exist. Use ${list(NODE_FIELDS)}`,
87
+ message: `nodes.${name}.${field} doesn't exist. Use ${list(fields)}`,
88
88
  at,
89
89
  };
90
90
  }
@@ -6,7 +6,7 @@ export interface ExpressionContext {
6
6
  status: string;
7
7
  durationMs?: number;
8
8
  error?: string;
9
- }>;
9
+ } & Record<string, unknown>>;
10
10
  run: {
11
11
  environment: string;
12
12
  trigger: string;
@@ -1,7 +1,8 @@
1
+ import type { AnyNodeDefinition } from "../nodes/define.js";
1
2
  import type { FlowFile } from "../schema.js";
2
3
  import type { ExpressionScope } from "./check.js";
3
- /** What an expression at `nodeId` may use */
4
+ /** What an expression at `nodeId` may use; `nodes` adds definitions core doesn't have (steps) */
4
5
  export declare function scopeAt(flow: FlowFile, nodeId: string, environment: {
5
6
  name: string;
6
7
  variables: Record<string, unknown>;
7
- }): ExpressionScope;
8
+ }, nodes?: readonly AnyNodeDefinition[]): ExpressionScope;
@@ -1,17 +1,25 @@
1
1
  import { ancestors, dominates, reachOrder } from "../graph.js";
2
- /** What an expression at `nodeId` may use */
3
- export function scopeAt(flow, nodeId, environment) {
2
+ import { NODES } from "../nodes/index.js";
3
+ import { getPath } from "../nodes/path.js";
4
+ /** What an expression at `nodeId` may use; `nodes` adds definitions core doesn't have (steps) */
5
+ export function scopeAt(flow, nodeId, environment, nodes = []) {
6
+ const defOf = (kind) => nodes.find((d) => d.kind === kind) ?? NODES.find((d) => d.kind === kind);
4
7
  const keyOf = (id) => flow.nodes.find((n) => n.id === id)?.key;
5
8
  const keys = (ids) => [...ids].map(keyOf).filter((k) => !!k);
6
9
  const before = ancestors(flow, nodeId);
7
10
  const after = new Set(reachOrder(flow, nodeId));
8
11
  after.delete(nodeId);
9
12
  const vars = flow.nodes
10
- .filter((n) => n.kind === "set-variable" &&
11
- n.id !== nodeId &&
12
- dominates(flow, n.id, nodeId))
13
- .map((n) => n.config?.name)
13
+ .filter((n) => n.id !== nodeId && dominates(flow, n.id, nodeId))
14
+ .map((n) => {
15
+ const path = defOf(n.kind)?.writesVar;
16
+ return path ? getPath(n, path) : undefined;
17
+ })
14
18
  .filter((name) => typeof name === "string" && name !== "");
19
+ const outputs = Object.fromEntries(flow.nodes.flatMap((n) => {
20
+ const results = defOf(n.kind)?.results;
21
+ return n.key && results?.length ? [[n.key, results]] : [];
22
+ }));
15
23
  return {
16
24
  env: {
17
25
  name: environment.name,
@@ -21,5 +29,6 @@ export function scopeAt(flow, nodeId, environment) {
21
29
  before: keys(before),
22
30
  after: keys(after),
23
31
  all: keys(flow.nodes.map((n) => n.id)),
32
+ outputs,
24
33
  };
25
34
  }
@@ -8,9 +8,10 @@ export * from "./graph.js";
8
8
  export * from "./expression/index.js";
9
9
  export * from "./nodes/index.js";
10
10
  export * from "./check.js";
11
+ export * from "./steps.js";
11
12
  /**
12
13
  * Version of the contract between the SAMURAI app and this framework.
13
14
  * Increments whenever the app and the framework must agree on something new
14
15
  * (a node kind, a field, an event), so the app can tell when the framework is too old.
15
16
  */
16
- export declare const FLOWS_API = 1;
17
+ export declare const FLOWS_API = 2;
@@ -8,9 +8,10 @@ export * from "./graph.js";
8
8
  export * from "./expression/index.js";
9
9
  export * from "./nodes/index.js";
10
10
  export * from "./check.js";
11
+ export * from "./steps.js";
11
12
  /**
12
13
  * Version of the contract between the SAMURAI app and this framework.
13
14
  * Increments whenever the app and the framework must agree on something new
14
15
  * (a node kind, a field, an event), so the app can tell when the framework is too old.
15
16
  */
16
- export const FLOWS_API = 1;
17
+ export const FLOWS_API = 2;
@@ -110,6 +110,10 @@ export interface NodeCoreDefinition<K extends string = string, C = Record<string
110
110
  fields: readonly Field[];
111
111
  /** The node can write outputs (shared-context variables) */
112
112
  outputs?: boolean;
113
+ /** A step's output names: readable later as `nodes.<key>.<name>` */
114
+ results?: readonly string[];
115
+ /** The path of the field holding the name of a variable the node writes (`vars.<name>`) */
116
+ writesVar?: string;
113
117
  /** The node has an "on failure" choice */
114
118
  onFailure?: boolean;
115
119
  /** Pure: problems with this node alone. `node.config` has the field defaults filled in. */
@@ -9,70 +9,14 @@ declare const registered: readonly [import("./define.js").NodeCoreDefinition<"st
9
9
  readonly label: "Expression";
10
10
  readonly placeholder: "nodes.login.status == \"passed\"";
11
11
  readonly default: "";
12
- }]>>, import("./define.js").NodeCoreDefinition<"parallel", import("./define.js").ConfigOf<readonly []>>, import("./define.js").NodeCoreDefinition<"wait", import("./define.js").ConfigOf<readonly [{
13
- readonly path: "title";
14
- readonly type: "text";
15
- readonly label: "Title";
16
- }, {
17
- readonly path: "subtitle";
18
- readonly type: "text";
19
- readonly label: "Description";
20
- }, {
21
- readonly path: "config.ms";
22
- readonly type: "number";
23
- readonly label: "Duration";
24
- readonly unit: "ms";
25
- readonly default: 1000;
26
- }]>>, import("./define.js").NodeCoreDefinition<"set-variable", import("./define.js").ConfigOf<readonly [{
27
- readonly path: "config.name";
28
- readonly type: "text";
29
- readonly label: "Name";
30
- readonly placeholder: "orderId";
31
- readonly default: "";
32
- }, {
33
- readonly path: "config.value";
34
- readonly type: "expression";
35
- readonly label: "Value";
36
- readonly placeholder: "\"order-\" + run.environment";
37
- readonly default: "";
38
- }]>>, import("./define.js").NodeCoreDefinition<"api", import("./define.js").ConfigOf<readonly [{
39
- readonly path: "title";
40
- readonly type: "text";
41
- readonly label: "Title";
42
- }, {
43
- readonly path: "subtitle";
44
- readonly type: "text";
45
- readonly label: "Description";
46
- }]>>, import("./define.js").NodeCoreDefinition<"database", import("./define.js").ConfigOf<readonly [{
47
- readonly path: "title";
48
- readonly type: "text";
49
- readonly label: "Title";
50
- }, {
51
- readonly path: "subtitle";
52
- readonly type: "text";
53
- readonly label: "Description";
54
- }]>>, import("./define.js").NodeCoreDefinition<"email", import("./define.js").ConfigOf<readonly [{
55
- readonly path: "title";
56
- readonly type: "text";
57
- readonly label: "Title";
58
- }, {
59
- readonly path: "subtitle";
60
- readonly type: "text";
61
- readonly label: "Description";
62
- }]>>, import("./define.js").NodeCoreDefinition<"script", import("./define.js").ConfigOf<readonly [{
63
- readonly path: "title";
64
- readonly type: "text";
65
- readonly label: "Title";
66
- }, {
67
- readonly path: "subtitle";
68
- readonly type: "text";
69
- readonly label: "Description";
70
- }]>>];
12
+ }]>>, import("./define.js").NodeCoreDefinition<"parallel", import("./define.js").ConfigOf<readonly []>>];
71
13
  export type NodeKind = (typeof registered)[number]["kind"];
72
14
  export declare const NODES: readonly AnyNodeDefinition[];
73
15
  /** The definition of a kind; throws for an unknown one */
74
16
  export declare function nodeDef(kind: string): AnyNodeDefinition;
75
- /** How a run treats a node in the graph */
17
+ /** The kinds core defines itself; every other kind is a step */
18
+ export declare const CLOSED_KINDS: readonly string[];
19
+ /** How a run treats a node in the graph: a kind core doesn't define is a step */
76
20
  export declare const shapeOf: (node: {
77
21
  kind: string;
78
22
  }) => import("./define.js").Shape;
@@ -97,11 +41,5 @@ export declare const branchOf: (node: {
97
41
  export declare const branchNames: (node: {
98
42
  kind: string;
99
43
  }) => string[];
100
- /**
101
- * Kinds defined in core that a run has no handler for yet (`flows/run` skips or refuses
102
- * them). The one list: `flows/run` uses it too, and the builder warns with it without
103
- * importing `flows/run`.
104
- */
105
- export declare const UNSUPPORTED_KINDS: readonly string[];
106
44
  export * from "./define.js";
107
45
  export * from "./path.js";
@@ -1,30 +1,11 @@
1
- import { api } from "./api.js";
2
1
  import { condition } from "./condition.js";
3
- import { database } from "./database.js";
4
- import { email } from "./email.js";
5
2
  import { end } from "./end.js";
6
3
  import { group } from "./group.js";
7
4
  import { parallel } from "./parallel.js";
8
- import { script } from "./script.js";
9
- import { setVariable } from "./set-variable.js";
10
5
  import { start } from "./start.js";
11
6
  import { test } from "./test.js";
12
- import { wait } from "./wait.js";
13
- // Library order
14
- const registered = [
15
- start,
16
- end,
17
- test,
18
- group,
19
- condition,
20
- parallel,
21
- wait,
22
- setVariable,
23
- api,
24
- database,
25
- email,
26
- script,
27
- ];
7
+ // Library order. Every other kind is a step (`flows/steps`).
8
+ const registered = [start, end, test, group, condition, parallel];
28
9
  export const NODES = registered;
29
10
  /** The definition of a kind; throws for an unknown one */
30
11
  export function nodeDef(kind) {
@@ -33,14 +14,18 @@ export function nodeDef(kind) {
33
14
  throw new Error(`Unknown node kind: ${kind}`);
34
15
  return def;
35
16
  }
36
- /** How a run treats a node in the graph */
37
- export const shapeOf = (node) => nodeDef(node.kind).shape;
17
+ /** The definition of a kind core registers; undefined for any other (a step) */
18
+ const closedDef = (kind) => NODES.find((d) => d.kind === kind);
19
+ /** The kinds core defines itself; every other kind is a step */
20
+ export const CLOSED_KINDS = NODES.map((d) => d.kind);
21
+ /** How a run treats a node in the graph: a kind core doesn't define is a step */
22
+ export const shapeOf = (node) => closedDef(node.kind)?.shape ?? "step";
38
23
  /** The branch names a dynamic list gives its branches: branch-0, branch-1, ... */
39
24
  export const dynamicName = (index) => `branch-${index}`;
40
25
  /** The dynamic branch settings of a `branches` node; undefined for a fixed list and any other shape */
41
26
  export const dynamicOf = (node) => {
42
- const def = nodeDef(node.kind);
43
- return def.shape === "branches" &&
27
+ const def = closedDef(node.kind);
28
+ return def?.shape === "branches" &&
44
29
  def.branches &&
45
30
  !Array.isArray(def.branches)
46
31
  ? def.branches
@@ -51,8 +36,8 @@ export const dynamicOf = (node) => {
51
36
  * `min` empty ones for a dynamic list. Empty for any other shape.
52
37
  */
53
38
  export const branchesOf = (node) => {
54
- const def = nodeDef(node.kind);
55
- if (def.shape !== "branches" || !def.branches)
39
+ const def = closedDef(node.kind);
40
+ if (!def || def.shape !== "branches" || !def.branches)
56
41
  return [];
57
42
  if (Array.isArray(def.branches))
58
43
  return def.branches;
@@ -66,16 +51,5 @@ export const branchOf = (node, handle) => dynamicOf(node)
66
51
  : branchesOf(node).find((b) => b.name === handle);
67
52
  /** The names of the branches `branchesOf` lists */
68
53
  export const branchNames = (node) => branchesOf(node).map((b) => b.name);
69
- /**
70
- * Kinds defined in core that a run has no handler for yet (`flows/run` skips or refuses
71
- * them). The one list: `flows/run` uses it too, and the builder warns with it without
72
- * importing `flows/run`.
73
- */
74
- export const UNSUPPORTED_KINDS = [
75
- "api",
76
- "database",
77
- "email",
78
- "script",
79
- ];
80
54
  export * from "./define.js";
81
55
  export * from "./path.js";
@@ -0,0 +1,42 @@
1
+ import type { Field, FlowProblem, NodeCoreDefinition } from "./nodes/define.js";
2
+ import type { FlowNodeModel } from "./schema.js";
3
+ /** Rules a step field carries: checked live in the builder and again by checkFlow */
4
+ export interface FieldRules {
5
+ required?: boolean;
6
+ /** `number` fields */
7
+ min?: number;
8
+ max?: number;
9
+ /** `text` fields: a regular expression the value must match */
10
+ pattern?: string;
11
+ /** Said instead of the default when `pattern` doesn't match */
12
+ patternMessage?: string;
13
+ }
14
+ export type StepField = Field & FieldRules;
15
+ export type StepSection = "control" | "integrations";
16
+ export interface StepOutput {
17
+ name: string;
18
+ label?: string;
19
+ }
20
+ /** A step as data: what the app's catalog holds. No functions. */
21
+ export interface StepInfo {
22
+ /** Stored in flow files as the node's `kind` */
23
+ kind: string;
24
+ label: string;
25
+ /** A design-system icon name */
26
+ icon: string;
27
+ section: StepSection;
28
+ /** `config.*` fields; Title and Description are added by `stepNodeOf` */
29
+ fields: readonly StepField[];
30
+ /** The line under the title: `{config.x}` is replaced with the field's value */
31
+ summary?: string;
32
+ outputs?: readonly StepOutput[];
33
+ /** The path of the field holding a variable name the step writes */
34
+ writesVar?: string;
35
+ }
36
+ export declare const STEP_KIND: RegExp;
37
+ /** A node's own result fields: an output can't take these names */
38
+ export declare const RESERVED_OUTPUTS: readonly string[];
39
+ /** Problems with `node`'s values against the fields' rules, as "Label: message" */
40
+ export declare function fieldProblems(fields: readonly StepField[], node: FlowNodeModel, label: string): FlowProblem[];
41
+ /** The definition core's walker and checkFlow use for a step; `validate` is the step's own (engine only) */
42
+ export declare function stepNodeOf(info: StepInfo, validate?: NodeCoreDefinition["validate"]): NodeCoreDefinition;
@@ -0,0 +1,58 @@
1
+ import { getPath } from "./nodes/path.js";
2
+ export const STEP_KIND = /^[a-z][a-z0-9-]*$/;
3
+ /** A node's own result fields: an output can't take these names */
4
+ export const RESERVED_OUTPUTS = [
5
+ "status",
6
+ "durationMs",
7
+ "error",
8
+ ];
9
+ const TITLE_FIELDS = [
10
+ { path: "title", type: "text", label: "Title" },
11
+ { path: "subtitle", type: "text", label: "Description" },
12
+ ];
13
+ const empty = (value) => value === undefined || (typeof value === "string" && value.trim() === "");
14
+ /** Problems with `node`'s values against the fields' rules, as "Label: message" */
15
+ export function fieldProblems(fields, node, label) {
16
+ const problems = [];
17
+ const add = (message) => problems.push({
18
+ level: "error",
19
+ nodeId: node.id,
20
+ message: `${label}: ${message}`,
21
+ });
22
+ for (const f of fields) {
23
+ const name = f.label ?? f.path;
24
+ const value = getPath(node, f.path);
25
+ if (f.required && empty(value)) {
26
+ add(`${name} is required.`);
27
+ continue;
28
+ }
29
+ if (typeof value === "number") {
30
+ if (f.min !== undefined && value < f.min)
31
+ add(`${name} must be at least ${f.min}.`);
32
+ if (f.max !== undefined && value > f.max)
33
+ add(`${name} must be at most ${f.max}.`);
34
+ }
35
+ if (f.pattern &&
36
+ typeof value === "string" &&
37
+ value !== "" &&
38
+ !new RegExp(f.pattern).test(value))
39
+ add(f.patternMessage ?? `${name} doesn't match ${f.pattern}.`);
40
+ }
41
+ return problems;
42
+ }
43
+ /** The definition core's walker and checkFlow use for a step; `validate` is the step's own (engine only) */
44
+ export function stepNodeOf(info, validate) {
45
+ return {
46
+ kind: info.kind,
47
+ label: info.label,
48
+ shape: "step",
49
+ fields: [...TITLE_FIELDS, ...info.fields],
50
+ onFailure: true,
51
+ results: (info.outputs ?? []).map((o) => o.name),
52
+ ...(info.writesVar !== undefined && { writesVar: info.writesVar }),
53
+ validate: (node, ctx) => [
54
+ ...fieldProblems(info.fields, node, ctx.label),
55
+ ...(validate?.(node, ctx) ?? []),
56
+ ],
57
+ };
58
+ }
@@ -8,8 +8,10 @@ export interface NodeResult {
8
8
  note?: string;
9
9
  /** Condition: the branch taken */
10
10
  branch?: string;
11
- /** Set variable: what it wrote */
11
+ /** What the node wrote to vars */
12
12
  wrote?: Record<string, unknown>;
13
+ /** A step's outputs, readable later as `nodes.<key>.<name>` */
14
+ outputs?: Record<string, unknown>;
13
15
  }
14
16
  /** One test of a node: absolute file and full name */
15
17
  export interface NodeTest {
@@ -1,6 +1,4 @@
1
1
  import type { Handler } from "./support.js";
2
2
  export type { Handler, HandlerRun, Outcome } from "./support.js";
3
- /** The kinds a run can execute, by kind */
3
+ /** The kinds a run can execute, by kind: the closed ones and every built-in step */
4
4
  export declare const HANDLERS: Readonly<Record<string, Handler>>;
5
- /** Kinds that exist in `flows/core` and have no handler yet (core's one list) */
6
- export declare const UNSUPPORTED: readonly string[];