@itmetsam/samurai-framework 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/flow-reporter.js +2 -1
- package/dist/flows/core/check.d.ts +4 -1
- package/dist/flows/core/check.js +17 -14
- package/dist/flows/core/expression/check.d.ts +2 -0
- package/dist/flows/core/expression/check.js +3 -3
- package/dist/flows/core/expression/evaluate.d.ts +1 -1
- package/dist/flows/core/expression/scope.d.ts +3 -2
- package/dist/flows/core/expression/scope.js +15 -6
- package/dist/flows/core/index.d.ts +2 -1
- package/dist/flows/core/index.js +2 -1
- package/dist/flows/core/nodes/define.d.ts +4 -0
- package/dist/flows/core/nodes/index.d.ts +4 -66
- package/dist/flows/core/nodes/index.js +12 -38
- package/dist/flows/core/steps.d.ts +42 -0
- package/dist/flows/core/steps.js +47 -0
- package/dist/flows/run/events.d.ts +3 -1
- package/dist/flows/run/handlers/index.d.ts +1 -3
- package/dist/flows/run/handlers/index.js +3 -8
- package/dist/flows/run/handlers/support.d.ts +4 -1
- package/dist/flows/run/handlers/support.js +1 -1
- package/dist/flows/run/index.d.ts +1 -1
- package/dist/flows/run/index.js +1 -1
- package/dist/flows/run/project.js +2 -0
- package/dist/flows/run/run.d.ts +1 -1
- package/dist/flows/run/run.js +7 -1
- package/dist/flows/steps/api.d.ts +23 -0
- package/dist/flows/steps/api.js +40 -0
- package/dist/flows/steps/catalog.d.ts +16 -0
- package/dist/flows/steps/catalog.js +29 -0
- package/dist/flows/steps/define.d.ts +49 -0
- package/dist/flows/steps/define.js +40 -0
- package/dist/flows/steps/handler.d.ts +4 -0
- package/dist/flows/steps/handler.js +70 -0
- package/dist/flows/steps/index.d.ts +5 -0
- package/dist/flows/steps/index.js +4 -0
- package/dist/flows/{core/nodes → steps}/set-variable.d.ts +4 -1
- package/dist/flows/steps/set-variable.js +20 -0
- package/dist/flows/steps/wait.d.ts +8 -0
- package/dist/flows/steps/wait.js +14 -0
- package/package.json +5 -1
- package/dist/flows/core/nodes/api.d.ts +0 -9
- package/dist/flows/core/nodes/api.js +0 -12
- package/dist/flows/core/nodes/database.d.ts +0 -9
- package/dist/flows/core/nodes/database.js +0 -12
- package/dist/flows/core/nodes/email.d.ts +0 -9
- package/dist/flows/core/nodes/email.js +0 -12
- package/dist/flows/core/nodes/script.d.ts +0 -9
- package/dist/flows/core/nodes/script.js +0 -12
- package/dist/flows/core/nodes/set-variable.js +0 -39
- package/dist/flows/core/nodes/wait.d.ts +0 -15
- package/dist/flows/core/nodes/wait.js +0 -18
- package/dist/flows/run/handlers/set-variable.d.ts +0 -2
- package/dist/flows/run/handlers/set-variable.js +0 -22
- package/dist/flows/run/handlers/wait.d.ts +0 -2
- package/dist/flows/run/handlers/wait.js +0 -10
|
@@ -1,5 +1,6 @@
|
|
|
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. */
|
|
@@ -28,7 +29,7 @@ export class FlowReporter {
|
|
|
28
29
|
label = nodeDef(node.kind).label;
|
|
29
30
|
}
|
|
30
31
|
catch {
|
|
31
|
-
label = node.kind;
|
|
32
|
+
label = STEP_NODES.find((d) => d.kind === node.kind)?.label ?? node.kind;
|
|
32
33
|
}
|
|
33
34
|
const base = node.title || label;
|
|
34
35
|
const wrote = result.wrote ? Object.keys(result.wrote) : [];
|
|
@@ -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[];
|
package/dist/flows/core/check.js
CHANGED
|
@@ -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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
-
|
|
85
|
-
|
|
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(
|
|
87
|
+
message: `nodes.${name}.${field} doesn't exist. Use ${list(fields)}`,
|
|
88
88
|
at,
|
|
89
89
|
};
|
|
90
90
|
}
|
|
@@ -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
|
-
|
|
3
|
-
|
|
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.
|
|
11
|
-
n
|
|
12
|
-
|
|
13
|
-
|
|
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 =
|
|
17
|
+
export declare const FLOWS_API = 2;
|
package/dist/flows/core/index.js
CHANGED
|
@@ -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 =
|
|
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 []
|
|
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
|
-
/**
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
/**
|
|
37
|
-
|
|
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 =
|
|
43
|
-
return def
|
|
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 =
|
|
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,47 @@
|
|
|
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 = ["status", "durationMs", "error"];
|
|
5
|
+
const TITLE_FIELDS = [
|
|
6
|
+
{ path: "title", type: "text", label: "Title" },
|
|
7
|
+
{ path: "subtitle", type: "text", label: "Description" },
|
|
8
|
+
];
|
|
9
|
+
const empty = (value) => value === undefined || (typeof value === "string" && value.trim() === "");
|
|
10
|
+
/** Problems with `node`'s values against the fields' rules, as "Label: message" */
|
|
11
|
+
export function fieldProblems(fields, node, label) {
|
|
12
|
+
const problems = [];
|
|
13
|
+
const add = (message) => problems.push({ level: "error", nodeId: node.id, message: `${label}: ${message}` });
|
|
14
|
+
for (const f of fields) {
|
|
15
|
+
const name = f.label ?? f.path;
|
|
16
|
+
const value = getPath(node, f.path);
|
|
17
|
+
if (f.required && empty(value)) {
|
|
18
|
+
add(`${name} is required.`);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === "number") {
|
|
22
|
+
if (f.min !== undefined && value < f.min)
|
|
23
|
+
add(`${name} must be at least ${f.min}.`);
|
|
24
|
+
if (f.max !== undefined && value > f.max)
|
|
25
|
+
add(`${name} must be at most ${f.max}.`);
|
|
26
|
+
}
|
|
27
|
+
if (f.pattern && typeof value === "string" && value !== "" && !new RegExp(f.pattern).test(value))
|
|
28
|
+
add(f.patternMessage ?? `${name} doesn't match ${f.pattern}.`);
|
|
29
|
+
}
|
|
30
|
+
return problems;
|
|
31
|
+
}
|
|
32
|
+
/** The definition core's walker and checkFlow use for a step; `validate` is the step's own (engine only) */
|
|
33
|
+
export function stepNodeOf(info, validate) {
|
|
34
|
+
return {
|
|
35
|
+
kind: info.kind,
|
|
36
|
+
label: info.label,
|
|
37
|
+
shape: "step",
|
|
38
|
+
fields: [...TITLE_FIELDS, ...info.fields],
|
|
39
|
+
onFailure: true,
|
|
40
|
+
results: (info.outputs ?? []).map((o) => o.name),
|
|
41
|
+
...(info.writesVar !== undefined && { writesVar: info.writesVar }),
|
|
42
|
+
validate: (node, ctx) => [
|
|
43
|
+
...fieldProblems(info.fields, node, ctx.label),
|
|
44
|
+
...(validate?.(node, ctx) ?? []),
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -8,8 +8,10 @@ export interface NodeResult {
|
|
|
8
8
|
note?: string;
|
|
9
9
|
/** Condition: the branch taken */
|
|
10
10
|
branch?: string;
|
|
11
|
-
/**
|
|
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[];
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { STEP_HANDLERS } from "../../steps/catalog.js";
|
|
2
2
|
import { condition } from "./condition.js";
|
|
3
3
|
import { end } from "./end.js";
|
|
4
4
|
import { group } from "./group.js";
|
|
5
5
|
import { parallel } from "./parallel.js";
|
|
6
|
-
import { setVariable } from "./set-variable.js";
|
|
7
6
|
import { start } from "./start.js";
|
|
8
7
|
import { test } from "./test.js";
|
|
9
|
-
|
|
10
|
-
/** The kinds a run can execute, by kind */
|
|
8
|
+
/** The kinds a run can execute, by kind: the closed ones and every built-in step */
|
|
11
9
|
export const HANDLERS = {
|
|
12
10
|
start,
|
|
13
11
|
end,
|
|
@@ -15,8 +13,5 @@ export const HANDLERS = {
|
|
|
15
13
|
group,
|
|
16
14
|
condition,
|
|
17
15
|
parallel,
|
|
18
|
-
|
|
19
|
-
"set-variable": setVariable,
|
|
16
|
+
...STEP_HANDLERS,
|
|
20
17
|
};
|
|
21
|
-
/** Kinds that exist in `flows/core` and have no handler yet (core's one list) */
|
|
22
|
-
export const UNSUPPORTED = UNSUPPORTED_KINDS;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ExpressionContext, type FlowFile, type FlowNodeModel } from "../../core/index.js";
|
|
1
|
+
import { type AnyNodeDefinition, type ExpressionContext, type FlowFile, type FlowNodeModel } from "../../core/index.js";
|
|
2
2
|
import type { RunEvent } from "../../../runner/reporter.js";
|
|
3
3
|
import type { NodeResult, NodeTest } from "../events.js";
|
|
4
4
|
/** What a handler can use of the run */
|
|
@@ -14,6 +14,9 @@ export interface HandlerRun {
|
|
|
14
14
|
runTests: (tests: NodeTest[], onEvent: (e: RunEvent) => void) => Promise<"passed" | "failed" | "cancelled">;
|
|
15
15
|
sleep: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
16
16
|
onTestEvent: (nodeId: string, e: RunEvent) => void;
|
|
17
|
+
/** Definitions of kinds core doesn't have (steps), for expression scopes */
|
|
18
|
+
nodes: readonly AnyNodeDefinition[];
|
|
19
|
+
log: (nodeId: string, level: "info" | "warn" | "error", text: string) => void;
|
|
17
20
|
}
|
|
18
21
|
/** What a handler decides; the walker adds timing */
|
|
19
22
|
export type Outcome = Omit<NodeResult, "durationMs"> | "cancelled";
|
|
@@ -19,4 +19,4 @@ export async function runNodeTests(node, run, list) {
|
|
|
19
19
|
: { status: "failed", error: "A test failed." };
|
|
20
20
|
}
|
|
21
21
|
/** The checker's problem with an expression, as the builder would show it: a bypassed check fails the node the same way */
|
|
22
|
-
export const problemAt = (node, run, text) => checkExpression(parseExpression(text), scopeAt(run.flow, node.id, run.environment))?.message;
|
|
22
|
+
export const problemAt = (node, run, text) => checkExpression(parseExpression(text), scopeAt(run.flow, node.id, run.environment, run.nodes))?.message;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Running flows: handlers, the walker, a project's flow files. Node only; see `flows/core` for the pure half. */
|
|
2
2
|
export * from "./events.js";
|
|
3
|
-
export { HANDLERS
|
|
3
|
+
export { HANDLERS } from "./handlers/index.js";
|
|
4
4
|
export type { Handler, HandlerRun, Outcome } from "./handlers/index.js";
|
|
5
5
|
export { runFlow, type RunFlowOptions } from "./run.js";
|
|
6
6
|
export { FLOW_SUFFIX, flowsDir, listFlows, readFlow, readFlowPath, readFlows, } from "./files.js";
|
package/dist/flows/run/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Running flows: handlers, the walker, a project's flow files. Node only; see `flows/core` for the pure half. */
|
|
2
2
|
export * from "./events.js";
|
|
3
|
-
export { HANDLERS
|
|
3
|
+
export { HANDLERS } from "./handlers/index.js";
|
|
4
4
|
export { runFlow } from "./run.js";
|
|
5
5
|
export { FLOW_SUFFIX, flowsDir, listFlows, readFlow, readFlowPath, readFlows, } from "./files.js";
|
|
6
6
|
export * from "./report.js";
|
|
@@ -6,6 +6,7 @@ import { withProject } from "../../runner/project.js";
|
|
|
6
6
|
import { runTests } from "../../runner/run.js";
|
|
7
7
|
import TestRunner from "../../runner/test-runner.js";
|
|
8
8
|
import { readFlow } from "./files.js";
|
|
9
|
+
import { STEP_NODES } from "../steps/catalog.js";
|
|
9
10
|
import { HANDLERS } from "./handlers/index.js";
|
|
10
11
|
import { runFlow } from "./run.js";
|
|
11
12
|
/** The flow has errors; nothing was run */
|
|
@@ -78,6 +79,7 @@ async function prepare(options) {
|
|
|
78
79
|
environment,
|
|
79
80
|
handled: new Set(Object.keys(HANDLERS)),
|
|
80
81
|
unsupported,
|
|
82
|
+
nodes: STEP_NODES,
|
|
81
83
|
});
|
|
82
84
|
return {
|
|
83
85
|
projectDir,
|
package/dist/flows/run/run.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface RunFlowOptions {
|
|
|
30
30
|
unsupported?: "fail" | "skip";
|
|
31
31
|
/** The handlers by kind. @default HANDLERS */
|
|
32
32
|
handlers?: Readonly<Record<string, Handler>>;
|
|
33
|
-
/** Definitions of kinds
|
|
33
|
+
/** Definitions of kinds core doesn't have. @default the built-in steps */
|
|
34
34
|
nodes?: readonly AnyNodeDefinition[];
|
|
35
35
|
}
|
|
36
36
|
/**
|