@itmetsam/samurai-framework 0.2.2 → 0.3.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/README.md +5 -0
- package/bin/samurai.mjs +7 -5
- package/dist/cli/args.d.ts +5 -1
- package/dist/cli/args.js +6 -1
- package/dist/cli/flow-reporter.d.ts +23 -0
- package/dist/cli/flow-reporter.js +97 -0
- package/dist/cli/flow.d.ts +4 -0
- package/dist/cli/flow.js +240 -0
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +14 -0
- package/dist/flows/core/check.d.ts +42 -0
- package/dist/flows/core/check.js +202 -0
- package/dist/flows/core/expression/check.d.ts +21 -0
- package/dist/flows/core/expression/check.js +95 -0
- package/dist/flows/core/expression/evaluate.d.ts +22 -0
- package/dist/flows/core/expression/evaluate.js +141 -0
- package/dist/flows/core/expression/help.d.ts +25 -0
- package/dist/flows/core/expression/help.js +75 -0
- package/dist/flows/core/expression/index.d.ts +5 -0
- package/dist/flows/core/expression/index.js +5 -0
- package/dist/flows/core/expression/parse.d.ts +61 -0
- package/dist/flows/core/expression/parse.js +317 -0
- package/dist/flows/core/expression/scope.d.ts +7 -0
- package/dist/flows/core/expression/scope.js +25 -0
- package/dist/flows/core/graph.d.ts +11 -0
- package/dist/flows/core/graph.js +65 -0
- package/dist/flows/core/index.d.ts +16 -0
- package/dist/flows/core/index.js +16 -0
- package/dist/flows/core/keys.d.ts +8 -0
- package/dist/flows/core/keys.js +46 -0
- package/dist/flows/core/nodes/api.d.ts +9 -0
- package/dist/flows/core/nodes/api.js +12 -0
- package/dist/flows/core/nodes/condition.d.ts +7 -0
- package/dist/flows/core/nodes/condition.js +21 -0
- package/dist/flows/core/nodes/database.d.ts +9 -0
- package/dist/flows/core/nodes/database.js +12 -0
- package/dist/flows/core/nodes/define.d.ts +128 -0
- package/dist/flows/core/nodes/define.js +21 -0
- package/dist/flows/core/nodes/email.d.ts +9 -0
- package/dist/flows/core/nodes/email.js +12 -0
- package/dist/flows/core/nodes/end.d.ts +1 -0
- package/dist/flows/core/nodes/end.js +7 -0
- package/dist/flows/core/nodes/group.d.ts +5 -0
- package/dist/flows/core/nodes/group.js +18 -0
- package/dist/flows/core/nodes/index.d.ts +107 -0
- package/dist/flows/core/nodes/index.js +81 -0
- package/dist/flows/core/nodes/parallel.d.ts +1 -0
- package/dist/flows/core/nodes/parallel.js +11 -0
- package/dist/flows/core/nodes/path.d.ts +5 -0
- package/dist/flows/core/nodes/path.js +21 -0
- package/dist/flows/core/nodes/script.d.ts +9 -0
- package/dist/flows/core/nodes/script.js +12 -0
- package/dist/flows/core/nodes/set-variable.d.ts +13 -0
- package/dist/flows/core/nodes/set-variable.js +39 -0
- package/dist/flows/core/nodes/start.d.ts +1 -0
- package/dist/flows/core/nodes/start.js +7 -0
- package/dist/flows/core/nodes/test.d.ts +1 -0
- package/dist/flows/core/nodes/test.js +18 -0
- package/dist/flows/core/nodes/wait.d.ts +15 -0
- package/dist/flows/core/nodes/wait.js +18 -0
- package/dist/flows/core/schema.d.ts +55 -0
- package/dist/flows/core/schema.js +33 -0
- package/dist/flows/run/events.d.ts +43 -0
- package/dist/flows/run/events.js +1 -0
- package/dist/flows/run/files.d.ts +12 -0
- package/dist/flows/run/files.js +54 -0
- package/dist/flows/run/handlers/condition.d.ts +2 -0
- package/dist/flows/run/handlers/condition.js +17 -0
- package/dist/flows/run/handlers/end.d.ts +2 -0
- package/dist/flows/run/handlers/end.js +1 -0
- package/dist/flows/run/handlers/group.d.ts +2 -0
- package/dist/flows/run/handlers/group.js +7 -0
- package/dist/flows/run/handlers/index.d.ts +6 -0
- package/dist/flows/run/handlers/index.js +22 -0
- package/dist/flows/run/handlers/parallel.d.ts +3 -0
- package/dist/flows/run/handlers/parallel.js +2 -0
- package/dist/flows/run/handlers/set-variable.d.ts +2 -0
- package/dist/flows/run/handlers/set-variable.js +22 -0
- package/dist/flows/run/handlers/start.d.ts +2 -0
- package/dist/flows/run/handlers/start.js +1 -0
- package/dist/flows/run/handlers/support.d.ts +26 -0
- package/dist/flows/run/handlers/support.js +22 -0
- package/dist/flows/run/handlers/test.d.ts +2 -0
- package/dist/flows/run/handlers/test.js +7 -0
- package/dist/flows/run/handlers/wait.d.ts +2 -0
- package/dist/flows/run/handlers/wait.js +10 -0
- package/dist/flows/run/index.d.ts +8 -0
- package/dist/flows/run/index.js +7 -0
- package/dist/flows/run/project.d.ts +43 -0
- package/dist/flows/run/project.js +163 -0
- package/dist/flows/run/report.d.ts +39 -0
- package/dist/flows/run/report.js +46 -0
- package/dist/flows/run/run.d.ts +42 -0
- package/dist/flows/run/run.js +244 -0
- package/dist/locator/locator.d.ts +8 -0
- package/dist/locator/locator.js +46 -0
- package/dist/recorder/capture.d.ts +2 -0
- package/dist/recorder/capture.js +14 -0
- package/dist/recorder/normalise.d.ts +5 -0
- package/dist/recorder/normalise.js +12 -0
- package/dist/recorder/recorder.js +20 -18
- package/dist/runner/groups.d.ts +39 -0
- package/dist/runner/groups.js +58 -0
- package/dist/runner/run.d.ts +4 -0
- package/dist/runner/run.js +12 -0
- package/dist/runner/test-runner.d.ts +12 -2
- package/dist/runner/test-runner.js +36 -6
- package/dist/steps/emit.js +2 -0
- package/dist/steps/model.d.ts +6 -0
- package/dist/steps/parse.js +4 -0
- package/package.json +9 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { FlowNodeModel } from "../schema.js";
|
|
2
|
+
/** A problem `checkFlow` or a node's `validate` reports */
|
|
3
|
+
export interface FlowProblem {
|
|
4
|
+
level: "error" | "warning";
|
|
5
|
+
message: string;
|
|
6
|
+
nodeId?: string;
|
|
7
|
+
}
|
|
8
|
+
/** What a node's `validate` can read besides the node itself */
|
|
9
|
+
export interface NodeCtx {
|
|
10
|
+
/** The definition's `label`: the one place a node's name is written */
|
|
11
|
+
label: string;
|
|
12
|
+
/** The test ids that exist; undefined when checkFlow was not given them (no reference check) */
|
|
13
|
+
tests?: ReadonlySet<string>;
|
|
14
|
+
/** The group names that exist; undefined when checkFlow was not given them */
|
|
15
|
+
groups?: ReadonlySet<string>;
|
|
16
|
+
}
|
|
17
|
+
export interface Option {
|
|
18
|
+
label: string;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
interface FieldBase {
|
|
22
|
+
/** Where the value lives in the node: `title`, `subtitle`, `continueSession`, `config.<key>`, `ref.testId` or `ref.group` */
|
|
23
|
+
path: string;
|
|
24
|
+
label?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* One setting of a node, as data. The editor decides how to render it. The app's
|
|
28
|
+
* `custom` field type (a React component) is not part of core: the editor attaches
|
|
29
|
+
* those to a kind itself.
|
|
30
|
+
*/
|
|
31
|
+
export type Field = (FieldBase & {
|
|
32
|
+
type: "text";
|
|
33
|
+
placeholder?: string;
|
|
34
|
+
default?: string;
|
|
35
|
+
}) | (FieldBase & {
|
|
36
|
+
type: "code";
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
default?: string;
|
|
39
|
+
}) | (FieldBase & {
|
|
40
|
+
type: "expression";
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
default?: string;
|
|
43
|
+
}) | (FieldBase & {
|
|
44
|
+
type: "number";
|
|
45
|
+
unit?: string;
|
|
46
|
+
default?: number;
|
|
47
|
+
}) | (FieldBase & {
|
|
48
|
+
type: "select";
|
|
49
|
+
options: Option[];
|
|
50
|
+
default?: string;
|
|
51
|
+
}) | (FieldBase & {
|
|
52
|
+
type: "segmented";
|
|
53
|
+
options: Option[];
|
|
54
|
+
default?: string;
|
|
55
|
+
}) | (FieldBase & {
|
|
56
|
+
type: "switch";
|
|
57
|
+
default?: boolean;
|
|
58
|
+
}) | (FieldBase & {
|
|
59
|
+
type: "test";
|
|
60
|
+
}) | (FieldBase & {
|
|
61
|
+
type: "group";
|
|
62
|
+
});
|
|
63
|
+
/** How a run treats a node in the graph */
|
|
64
|
+
export type Shape = "start" | "end" | "step" | "branches";
|
|
65
|
+
/** The tones of the design system's flow chips */
|
|
66
|
+
export type BranchTone = "neutral" | "success" | "warning" | "error" | "info";
|
|
67
|
+
/** One named way out of a `branches` node */
|
|
68
|
+
export interface Branch {
|
|
69
|
+
/** Stored as the edge's `sourceHandle` */
|
|
70
|
+
name: string;
|
|
71
|
+
/** The chip on the canvas; leave it out for no chip */
|
|
72
|
+
label?: string;
|
|
73
|
+
/** The chip's colour: a theme tone, or any CSS colour such as "#7c3aed" */
|
|
74
|
+
color?: BranchTone | (string & {});
|
|
75
|
+
}
|
|
76
|
+
/** Branches that come and go: filled ones plus one empty to drop onto, at least `min` in all */
|
|
77
|
+
export interface DynamicBranches {
|
|
78
|
+
dynamic: true;
|
|
79
|
+
min: number;
|
|
80
|
+
}
|
|
81
|
+
/** The type a field's value has in `config` */
|
|
82
|
+
type ValueOf<F> = F extends {
|
|
83
|
+
type: "number";
|
|
84
|
+
} ? number : F extends {
|
|
85
|
+
type: "switch";
|
|
86
|
+
} ? boolean : string;
|
|
87
|
+
/** `config` as the definition's `config.*` fields describe it */
|
|
88
|
+
export type ConfigOf<F extends readonly Field[]> = {
|
|
89
|
+
[K in F[number] as K["path"] extends `config.${infer P}` ? P : never]: ValueOf<K>;
|
|
90
|
+
};
|
|
91
|
+
/** A node whose `config` is typed (and filled from the field defaults) */
|
|
92
|
+
export type TypedNode<C> = Omit<FlowNodeModel, "config"> & {
|
|
93
|
+
config: C;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* The core half of a node kind: data and pure functions only. What a kind does in a
|
|
97
|
+
* run is a handler in `flows/run`; how it looks is the editor's half.
|
|
98
|
+
*/
|
|
99
|
+
export interface NodeCoreDefinition<K extends string = string, C = Record<string, unknown>> {
|
|
100
|
+
/** Stored in flow files as the node's `kind` */
|
|
101
|
+
kind: K;
|
|
102
|
+
/** The kind's name; messages use it */
|
|
103
|
+
label: string;
|
|
104
|
+
shape: Shape;
|
|
105
|
+
/**
|
|
106
|
+
* `branches`: a fixed list (the first is the one kept on delete), or
|
|
107
|
+
* `{ dynamic: true, min }` for a list that grows as branches are filled, named `branch-<n>`
|
|
108
|
+
*/
|
|
109
|
+
branches?: readonly Branch[] | DynamicBranches;
|
|
110
|
+
fields: readonly Field[];
|
|
111
|
+
/** The node can write outputs (shared-context variables) */
|
|
112
|
+
outputs?: boolean;
|
|
113
|
+
/** The node has an "on failure" choice */
|
|
114
|
+
onFailure?: boolean;
|
|
115
|
+
/** Pure: problems with this node alone. `node.config` has the field defaults filled in. */
|
|
116
|
+
validate?: (node: TypedNode<C>, ctx: NodeCtx) => FlowProblem[];
|
|
117
|
+
}
|
|
118
|
+
/** Any definition, for the registry */
|
|
119
|
+
export type AnyNodeDefinition = NodeCoreDefinition<string, any>;
|
|
120
|
+
/** Declares a node kind; the type of `config` comes from the `config.*` fields */
|
|
121
|
+
export declare function defineNode<const K extends string, const F extends readonly Field[]>(def: Omit<NodeCoreDefinition<K, ConfigOf<F>>, "fields"> & {
|
|
122
|
+
fields: F;
|
|
123
|
+
}): NodeCoreDefinition<K, ConfigOf<F>>;
|
|
124
|
+
/** The values the definition's `config.*` fields start with */
|
|
125
|
+
export declare function configDefaults(def: Pick<AnyNodeDefinition, "fields">): Record<string, unknown>;
|
|
126
|
+
/** The node with every field default filled in, so `validate` sees typed config even for old files */
|
|
127
|
+
export declare function withDefaults<C>(def: NodeCoreDefinition<string, C>, node: FlowNodeModel): TypedNode<C>;
|
|
128
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Declares a node kind; the type of `config` comes from the `config.*` fields */
|
|
2
|
+
export function defineNode(def) {
|
|
3
|
+
return def;
|
|
4
|
+
}
|
|
5
|
+
/** The values the definition's `config.*` fields start with */
|
|
6
|
+
export function configDefaults(def) {
|
|
7
|
+
const config = {};
|
|
8
|
+
for (const f of def.fields)
|
|
9
|
+
if (f.path.startsWith("config.") &&
|
|
10
|
+
"default" in f &&
|
|
11
|
+
f.default !== undefined)
|
|
12
|
+
config[f.path.slice("config.".length)] = f.default;
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
15
|
+
/** The node with every field default filled in, so `validate` sees typed config even for old files */
|
|
16
|
+
export function withDefaults(def, node) {
|
|
17
|
+
return {
|
|
18
|
+
...node,
|
|
19
|
+
config: { ...configDefaults(def), ...node.config },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const email: import("./define.js").NodeCoreDefinition<"email", import("./define.js").ConfigOf<readonly [{
|
|
2
|
+
readonly path: "title";
|
|
3
|
+
readonly type: "text";
|
|
4
|
+
readonly label: "Title";
|
|
5
|
+
}, {
|
|
6
|
+
readonly path: "subtitle";
|
|
7
|
+
readonly type: "text";
|
|
8
|
+
readonly label: "Description";
|
|
9
|
+
}]>>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineNode } from "./define.js";
|
|
2
|
+
// Declared so flows can hold it; no handler runs it yet (unsupported).
|
|
3
|
+
export const email = defineNode({
|
|
4
|
+
kind: "email",
|
|
5
|
+
label: "Email inbox",
|
|
6
|
+
shape: "step",
|
|
7
|
+
fields: [
|
|
8
|
+
{ path: "title", type: "text", label: "Title" },
|
|
9
|
+
{ path: "subtitle", type: "text", label: "Description" },
|
|
10
|
+
],
|
|
11
|
+
outputs: true,
|
|
12
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const end: import("./define.js").NodeCoreDefinition<"end", import("./define.js").ConfigOf<readonly []>>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineNode } from "./define.js";
|
|
2
|
+
// The app's editor adds the `continueSession` field (a custom component) to this kind.
|
|
3
|
+
export const group = defineNode({
|
|
4
|
+
kind: "group",
|
|
5
|
+
label: "Group",
|
|
6
|
+
shape: "step",
|
|
7
|
+
fields: [{ path: "ref.group", type: "group", label: "Group" }],
|
|
8
|
+
outputs: true,
|
|
9
|
+
validate: (node, ctx) => ctx.groups && !(node.ref?.group && ctx.groups.has(node.ref.group))
|
|
10
|
+
? [
|
|
11
|
+
{
|
|
12
|
+
level: "error",
|
|
13
|
+
nodeId: node.id,
|
|
14
|
+
message: `The ${ctx.label.toLowerCase()} ${node.ref?.group ?? ""} no longer exists.`,
|
|
15
|
+
},
|
|
16
|
+
]
|
|
17
|
+
: [],
|
|
18
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { AnyNodeDefinition, Branch, DynamicBranches } from "./define.js";
|
|
2
|
+
declare const registered: readonly [import("./define.js").NodeCoreDefinition<"start", import("./define.js").ConfigOf<readonly []>>, import("./define.js").NodeCoreDefinition<"end", import("./define.js").ConfigOf<readonly []>>, import("./define.js").NodeCoreDefinition<"test", import("./define.js").ConfigOf<readonly []>>, import("./define.js").NodeCoreDefinition<"group", import("./define.js").ConfigOf<readonly [{
|
|
3
|
+
readonly path: "ref.group";
|
|
4
|
+
readonly type: "group";
|
|
5
|
+
readonly label: "Group";
|
|
6
|
+
}]>>, import("./define.js").NodeCoreDefinition<"condition", import("./define.js").ConfigOf<readonly [{
|
|
7
|
+
readonly path: "config.expr";
|
|
8
|
+
readonly type: "expression";
|
|
9
|
+
readonly label: "Expression";
|
|
10
|
+
readonly placeholder: "nodes.login.status == \"passed\"";
|
|
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
|
+
}]>>];
|
|
71
|
+
export type NodeKind = (typeof registered)[number]["kind"];
|
|
72
|
+
export declare const NODES: readonly AnyNodeDefinition[];
|
|
73
|
+
/** The definition of a kind; throws for an unknown one */
|
|
74
|
+
export declare function nodeDef(kind: string): AnyNodeDefinition;
|
|
75
|
+
/** How a run treats a node in the graph */
|
|
76
|
+
export declare const shapeOf: (node: {
|
|
77
|
+
kind: string;
|
|
78
|
+
}) => import("./define.js").Shape;
|
|
79
|
+
/** The branch names a dynamic list gives its branches: branch-0, branch-1, ... */
|
|
80
|
+
export declare const dynamicName: (index: number) => string;
|
|
81
|
+
/** The dynamic branch settings of a `branches` node; undefined for a fixed list and any other shape */
|
|
82
|
+
export declare const dynamicOf: (node: {
|
|
83
|
+
kind: string;
|
|
84
|
+
}) => DynamicBranches | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* The branches a new `branches` node starts with, in order: its fixed list, or
|
|
87
|
+
* `min` empty ones for a dynamic list. Empty for any other shape.
|
|
88
|
+
*/
|
|
89
|
+
export declare const branchesOf: (node: {
|
|
90
|
+
kind: string;
|
|
91
|
+
}) => readonly Branch[];
|
|
92
|
+
/** The branch an edge out of a `branches` node leaves by (a dynamic list has no labels or colours) */
|
|
93
|
+
export declare const branchOf: (node: {
|
|
94
|
+
kind: string;
|
|
95
|
+
}, handle: string | undefined) => Branch | undefined;
|
|
96
|
+
/** The names of the branches `branchesOf` lists */
|
|
97
|
+
export declare const branchNames: (node: {
|
|
98
|
+
kind: string;
|
|
99
|
+
}) => 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
|
+
export * from "./define.js";
|
|
107
|
+
export * from "./path.js";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { api } from "./api.js";
|
|
2
|
+
import { condition } from "./condition.js";
|
|
3
|
+
import { database } from "./database.js";
|
|
4
|
+
import { email } from "./email.js";
|
|
5
|
+
import { end } from "./end.js";
|
|
6
|
+
import { group } from "./group.js";
|
|
7
|
+
import { parallel } from "./parallel.js";
|
|
8
|
+
import { script } from "./script.js";
|
|
9
|
+
import { setVariable } from "./set-variable.js";
|
|
10
|
+
import { start } from "./start.js";
|
|
11
|
+
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
|
+
];
|
|
28
|
+
export const NODES = registered;
|
|
29
|
+
/** The definition of a kind; throws for an unknown one */
|
|
30
|
+
export function nodeDef(kind) {
|
|
31
|
+
const def = NODES.find((d) => d.kind === kind);
|
|
32
|
+
if (!def)
|
|
33
|
+
throw new Error(`Unknown node kind: ${kind}`);
|
|
34
|
+
return def;
|
|
35
|
+
}
|
|
36
|
+
/** How a run treats a node in the graph */
|
|
37
|
+
export const shapeOf = (node) => nodeDef(node.kind).shape;
|
|
38
|
+
/** The branch names a dynamic list gives its branches: branch-0, branch-1, ... */
|
|
39
|
+
export const dynamicName = (index) => `branch-${index}`;
|
|
40
|
+
/** The dynamic branch settings of a `branches` node; undefined for a fixed list and any other shape */
|
|
41
|
+
export const dynamicOf = (node) => {
|
|
42
|
+
const def = nodeDef(node.kind);
|
|
43
|
+
return def.shape === "branches" &&
|
|
44
|
+
def.branches &&
|
|
45
|
+
!Array.isArray(def.branches)
|
|
46
|
+
? def.branches
|
|
47
|
+
: undefined;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The branches a new `branches` node starts with, in order: its fixed list, or
|
|
51
|
+
* `min` empty ones for a dynamic list. Empty for any other shape.
|
|
52
|
+
*/
|
|
53
|
+
export const branchesOf = (node) => {
|
|
54
|
+
const def = nodeDef(node.kind);
|
|
55
|
+
if (def.shape !== "branches" || !def.branches)
|
|
56
|
+
return [];
|
|
57
|
+
if (Array.isArray(def.branches))
|
|
58
|
+
return def.branches;
|
|
59
|
+
return Array.from({ length: def.branches.min }, (_, i) => ({ name: dynamicName(i) }));
|
|
60
|
+
};
|
|
61
|
+
/** The branch an edge out of a `branches` node leaves by (a dynamic list has no labels or colours) */
|
|
62
|
+
export const branchOf = (node, handle) => dynamicOf(node)
|
|
63
|
+
? handle
|
|
64
|
+
? { name: handle }
|
|
65
|
+
: undefined
|
|
66
|
+
: branchesOf(node).find((b) => b.name === handle);
|
|
67
|
+
/** The names of the branches `branchesOf` lists */
|
|
68
|
+
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
|
+
export * from "./define.js";
|
|
81
|
+
export * from "./path.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const parallel: import("./define.js").NodeCoreDefinition<"parallel", import("./define.js").ConfigOf<readonly []>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineNode } from "./define.js";
|
|
2
|
+
// Every branch leads on to the node after the Parallel. Branches are dropped
|
|
3
|
+
// onto one at a time: the list is always its filled branches plus one empty.
|
|
4
|
+
export const parallel = defineNode({
|
|
5
|
+
kind: "parallel",
|
|
6
|
+
label: "Parallel",
|
|
7
|
+
shape: "branches",
|
|
8
|
+
branches: { dynamic: true, min: 2 },
|
|
9
|
+
fields: [],
|
|
10
|
+
onFailure: true,
|
|
11
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FlowNodeModel } from "../schema.js";
|
|
2
|
+
/** Reads a field's value from the node: `title`, `subtitle`, `continueSession`, `config.*` or `ref.*` */
|
|
3
|
+
export declare function getPath(node: FlowNodeModel, path: string): unknown;
|
|
4
|
+
/** The patch that writes a field's value, keeping the other keys of `config` and `ref` */
|
|
5
|
+
export declare function setPath(node: FlowNodeModel, path: string, value: unknown): Partial<FlowNodeModel>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Splits `config.expr` into `["config", "expr"]`; `title` is a top-level key */
|
|
2
|
+
function split(path) {
|
|
3
|
+
const i = path.indexOf(".");
|
|
4
|
+
return i < 0 ? [path, undefined] : [path.slice(0, i), path.slice(i + 1)];
|
|
5
|
+
}
|
|
6
|
+
/** Reads a field's value from the node: `title`, `subtitle`, `continueSession`, `config.*` or `ref.*` */
|
|
7
|
+
export function getPath(node, path) {
|
|
8
|
+
const [head, rest] = split(path);
|
|
9
|
+
const top = node[head];
|
|
10
|
+
if (rest === undefined)
|
|
11
|
+
return top;
|
|
12
|
+
return top?.[rest];
|
|
13
|
+
}
|
|
14
|
+
/** The patch that writes a field's value, keeping the other keys of `config` and `ref` */
|
|
15
|
+
export function setPath(node, path, value) {
|
|
16
|
+
const [head, rest] = split(path);
|
|
17
|
+
if (rest === undefined)
|
|
18
|
+
return { [head]: value };
|
|
19
|
+
const top = node[head];
|
|
20
|
+
return { [head]: { ...top, [rest]: value } };
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const script: import("./define.js").NodeCoreDefinition<"script", import("./define.js").ConfigOf<readonly [{
|
|
2
|
+
readonly path: "title";
|
|
3
|
+
readonly type: "text";
|
|
4
|
+
readonly label: "Title";
|
|
5
|
+
}, {
|
|
6
|
+
readonly path: "subtitle";
|
|
7
|
+
readonly type: "text";
|
|
8
|
+
readonly label: "Description";
|
|
9
|
+
}]>>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineNode } from "./define.js";
|
|
2
|
+
// Declared so flows can hold it; no handler runs it yet (unsupported).
|
|
3
|
+
export const script = defineNode({
|
|
4
|
+
kind: "script",
|
|
5
|
+
label: "Script",
|
|
6
|
+
shape: "step",
|
|
7
|
+
fields: [
|
|
8
|
+
{ path: "title", type: "text", label: "Title" },
|
|
9
|
+
{ path: "subtitle", type: "text", label: "Description" },
|
|
10
|
+
],
|
|
11
|
+
outputs: true,
|
|
12
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const setVariable: import("./define.js").NodeCoreDefinition<"set-variable", import("./define.js").ConfigOf<readonly [{
|
|
2
|
+
readonly path: "config.name";
|
|
3
|
+
readonly type: "text";
|
|
4
|
+
readonly label: "Name";
|
|
5
|
+
readonly placeholder: "orderId";
|
|
6
|
+
readonly default: "";
|
|
7
|
+
}, {
|
|
8
|
+
readonly path: "config.value";
|
|
9
|
+
readonly type: "expression";
|
|
10
|
+
readonly label: "Value";
|
|
11
|
+
readonly placeholder: "\"order-\" + run.environment";
|
|
12
|
+
readonly default: "";
|
|
13
|
+
}]>>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineNode } from "./define.js";
|
|
2
|
+
export const setVariable = defineNode({
|
|
3
|
+
kind: "set-variable",
|
|
4
|
+
label: "Set variable",
|
|
5
|
+
shape: "step",
|
|
6
|
+
fields: [
|
|
7
|
+
{
|
|
8
|
+
path: "config.name",
|
|
9
|
+
type: "text",
|
|
10
|
+
label: "Name",
|
|
11
|
+
placeholder: "orderId",
|
|
12
|
+
default: "",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
path: "config.value",
|
|
16
|
+
type: "expression",
|
|
17
|
+
label: "Value",
|
|
18
|
+
placeholder: '"order-" + run.environment',
|
|
19
|
+
default: "",
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
validate: (node, ctx) => node.config.name === ""
|
|
23
|
+
? [
|
|
24
|
+
{
|
|
25
|
+
level: "error",
|
|
26
|
+
nodeId: node.id,
|
|
27
|
+
message: `${ctx.label}: give the variable a name.`,
|
|
28
|
+
},
|
|
29
|
+
]
|
|
30
|
+
: /^[a-z][a-z0-9_]*$/.test(node.config.name)
|
|
31
|
+
? []
|
|
32
|
+
: [
|
|
33
|
+
{
|
|
34
|
+
level: "error",
|
|
35
|
+
nodeId: node.id,
|
|
36
|
+
message: `${ctx.label}: use lowercase letters, digits and _ for the name.`,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const start: import("./define.js").NodeCoreDefinition<"start", import("./define.js").ConfigOf<readonly []>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const test: import("./define.js").NodeCoreDefinition<"test", import("./define.js").ConfigOf<readonly []>>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineNode } from "./define.js";
|
|
2
|
+
// The app's editor adds the `continueSession` field (a custom component) to this kind.
|
|
3
|
+
export const test = defineNode({
|
|
4
|
+
kind: "test",
|
|
5
|
+
label: "Test",
|
|
6
|
+
shape: "step",
|
|
7
|
+
fields: [],
|
|
8
|
+
outputs: true,
|
|
9
|
+
validate: (node, ctx) => ctx.tests && !(node.ref?.testId && ctx.tests.has(node.ref.testId))
|
|
10
|
+
? [
|
|
11
|
+
{
|
|
12
|
+
level: "error",
|
|
13
|
+
nodeId: node.id,
|
|
14
|
+
message: `A ${ctx.label.toLowerCase()} in this flow was deleted or moved.`,
|
|
15
|
+
},
|
|
16
|
+
]
|
|
17
|
+
: [],
|
|
18
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const wait: import("./define.js").NodeCoreDefinition<"wait", import("./define.js").ConfigOf<readonly [{
|
|
2
|
+
readonly path: "title";
|
|
3
|
+
readonly type: "text";
|
|
4
|
+
readonly label: "Title";
|
|
5
|
+
}, {
|
|
6
|
+
readonly path: "subtitle";
|
|
7
|
+
readonly type: "text";
|
|
8
|
+
readonly label: "Description";
|
|
9
|
+
}, {
|
|
10
|
+
readonly path: "config.ms";
|
|
11
|
+
readonly type: "number";
|
|
12
|
+
readonly label: "Duration";
|
|
13
|
+
readonly unit: "ms";
|
|
14
|
+
readonly default: 1000;
|
|
15
|
+
}]>>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineNode } from "./define.js";
|
|
2
|
+
export const wait = defineNode({
|
|
3
|
+
kind: "wait",
|
|
4
|
+
label: "Wait",
|
|
5
|
+
shape: "step",
|
|
6
|
+
fields: [
|
|
7
|
+
{ path: "title", type: "text", label: "Title" },
|
|
8
|
+
{ path: "subtitle", type: "text", label: "Description" },
|
|
9
|
+
{
|
|
10
|
+
path: "config.ms",
|
|
11
|
+
type: "number",
|
|
12
|
+
label: "Duration",
|
|
13
|
+
unit: "ms",
|
|
14
|
+
default: 1000,
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
outputs: true,
|
|
18
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface FlowNodeOutput {
|
|
2
|
+
/** Name of the shared-context variable the node writes */
|
|
3
|
+
name: string;
|
|
4
|
+
}
|
|
5
|
+
export interface FlowNodeModel {
|
|
6
|
+
id: string;
|
|
7
|
+
/** Short name expressions use: nodes.<key>. Lowercase letters, digits and _, unique in the flow */
|
|
8
|
+
key?: string;
|
|
9
|
+
/** A registered node kind (see the renderer's node definitions) */
|
|
10
|
+
kind: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
subtitle?: string;
|
|
13
|
+
/** test: the project test id (file::name). group: the group name */
|
|
14
|
+
ref?: {
|
|
15
|
+
testId?: string;
|
|
16
|
+
group?: string;
|
|
17
|
+
};
|
|
18
|
+
outputs?: FlowNodeOutput[];
|
|
19
|
+
/** What to do when the node fails; "stop" ends the run there */
|
|
20
|
+
onFailure: "stop" | "continue";
|
|
21
|
+
/** Continue in the previous node's browser session (kept in the file; the framework opens a fresh browser per test today) */
|
|
22
|
+
continueSession?: boolean;
|
|
23
|
+
/** Per-kind settings, named by each definition's `config.*` fields*/
|
|
24
|
+
config?: Record<string, unknown>;
|
|
25
|
+
position: {
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface FlowEdgeModel {
|
|
31
|
+
id: string;
|
|
32
|
+
source: string;
|
|
33
|
+
target: string;
|
|
34
|
+
/** The named branch of a node with branches (a condition's "true" or "false", a parallel's "branch-0") */
|
|
35
|
+
sourceHandle?: string;
|
|
36
|
+
}
|
|
37
|
+
/** One flow, as stored in <project>/flows/<id>.flow.json */
|
|
38
|
+
export interface FlowFile {
|
|
39
|
+
id: string;
|
|
40
|
+
/** The file format version; a missing one means 1. See readFlowFile */
|
|
41
|
+
version?: number;
|
|
42
|
+
name: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
/** The environment the flow runs in; the header's selection otherwise */
|
|
45
|
+
envDefault?: string;
|
|
46
|
+
nodes: FlowNodeModel[];
|
|
47
|
+
edges: FlowEdgeModel[];
|
|
48
|
+
}
|
|
49
|
+
/** The flow file format version this framework writes and reads */
|
|
50
|
+
export declare const FLOW_VERSION = 1;
|
|
51
|
+
/**
|
|
52
|
+
* Read a parsed flow file (JSON.parse output) into a FlowFile.
|
|
53
|
+
* A missing `version` is 1; older files are migrated; a file from a newer SAMURAI throws.
|
|
54
|
+
*/
|
|
55
|
+
export declare function readFlowFile(raw: unknown): FlowFile;
|