@itmetsam/samurai-framework 0.2.3 → 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/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 +98 -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 +45 -0
- package/dist/flows/core/check.js +205 -0
- package/dist/flows/core/expression/check.d.ts +23 -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 +8 -0
- package/dist/flows/core/expression/scope.js +34 -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 +17 -0
- package/dist/flows/core/index.js +17 -0
- package/dist/flows/core/keys.d.ts +8 -0
- package/dist/flows/core/keys.js +46 -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/define.d.ts +132 -0
- package/dist/flows/core/nodes/define.js +21 -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 +45 -0
- package/dist/flows/core/nodes/index.js +55 -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/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/schema.d.ts +55 -0
- package/dist/flows/core/schema.js +33 -0
- package/dist/flows/core/steps.d.ts +42 -0
- package/dist/flows/core/steps.js +47 -0
- package/dist/flows/run/events.d.ts +45 -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 +4 -0
- package/dist/flows/run/handlers/index.js +17 -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/start.d.ts +2 -0
- package/dist/flows/run/handlers/start.js +1 -0
- package/dist/flows/run/handlers/support.d.ts +29 -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/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 +165 -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 +250 -0
- 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/steps/set-variable.d.ts +16 -0
- 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/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/package.json +13 -1
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
class Fail extends Error {
|
|
2
|
+
constructor(message, at) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.at = at;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
const PUNCT = [
|
|
8
|
+
"==",
|
|
9
|
+
"!=",
|
|
10
|
+
"<=",
|
|
11
|
+
">=",
|
|
12
|
+
"&&",
|
|
13
|
+
"||",
|
|
14
|
+
"<",
|
|
15
|
+
">",
|
|
16
|
+
"!",
|
|
17
|
+
"+",
|
|
18
|
+
"-",
|
|
19
|
+
"*",
|
|
20
|
+
"/",
|
|
21
|
+
"%",
|
|
22
|
+
"(",
|
|
23
|
+
")",
|
|
24
|
+
"[",
|
|
25
|
+
"]",
|
|
26
|
+
".",
|
|
27
|
+
",",
|
|
28
|
+
];
|
|
29
|
+
function tokenize(text) {
|
|
30
|
+
const tokens = [];
|
|
31
|
+
let i = 0;
|
|
32
|
+
// A "/" starts a regex only where a value is expected: after "(" or ","
|
|
33
|
+
const valueExpected = () => {
|
|
34
|
+
const last = tokens[tokens.length - 1];
|
|
35
|
+
return last?.kind === "punct" && (last.value === "(" || last.value === ",");
|
|
36
|
+
};
|
|
37
|
+
while (i < text.length) {
|
|
38
|
+
const c = text[i];
|
|
39
|
+
if (/\s/.test(c)) {
|
|
40
|
+
i++;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const start = i;
|
|
44
|
+
if (/[0-9]/.test(c)) {
|
|
45
|
+
while (/[0-9]/.test(text[i] ?? ""))
|
|
46
|
+
i++;
|
|
47
|
+
if (text[i] === "." && /[0-9]/.test(text[i + 1] ?? "")) {
|
|
48
|
+
i++;
|
|
49
|
+
while (/[0-9]/.test(text[i] ?? ""))
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
if (text[i] === ".")
|
|
53
|
+
throw new Fail("Invalid number", start);
|
|
54
|
+
tokens.push({
|
|
55
|
+
kind: "num",
|
|
56
|
+
value: Number(text.slice(start, i)),
|
|
57
|
+
start,
|
|
58
|
+
end: i,
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (/[A-Za-z_$]/.test(c)) {
|
|
63
|
+
while (/[\w$]/.test(text[i] ?? ""))
|
|
64
|
+
i++;
|
|
65
|
+
tokens.push({
|
|
66
|
+
kind: "ident",
|
|
67
|
+
value: text.slice(start, i),
|
|
68
|
+
start,
|
|
69
|
+
end: i,
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (c === '"' || c === "'") {
|
|
74
|
+
let value = "";
|
|
75
|
+
i++;
|
|
76
|
+
while (i < text.length && text[i] !== c) {
|
|
77
|
+
if (text[i] === "\\" && i + 1 < text.length)
|
|
78
|
+
i++;
|
|
79
|
+
value += text[i++];
|
|
80
|
+
}
|
|
81
|
+
if (i >= text.length)
|
|
82
|
+
throw new Fail("Missing closing quote", start);
|
|
83
|
+
i++;
|
|
84
|
+
tokens.push({ kind: "str", value, start, end: i });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (c === "/" && valueExpected()) {
|
|
88
|
+
i++;
|
|
89
|
+
let pattern = "";
|
|
90
|
+
while (i < text.length && text[i] !== "/") {
|
|
91
|
+
if (text[i] === "\\" && i + 1 < text.length)
|
|
92
|
+
pattern += text[i++];
|
|
93
|
+
pattern += text[i++];
|
|
94
|
+
}
|
|
95
|
+
if (i >= text.length)
|
|
96
|
+
throw new Fail("Missing closing /", start);
|
|
97
|
+
i++;
|
|
98
|
+
let flags = "";
|
|
99
|
+
while (/[gimsuy]/.test(text[i] ?? ""))
|
|
100
|
+
flags += text[i++];
|
|
101
|
+
tokens.push({ kind: "regex", pattern, flags, start, end: i });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (c === "=" && text[i + 1] !== "=")
|
|
105
|
+
throw new Fail("Use == to compare", start);
|
|
106
|
+
const p = PUNCT.find((x) => text.startsWith(x, i));
|
|
107
|
+
if (!p)
|
|
108
|
+
throw new Fail(`Unexpected "${c}"`, start);
|
|
109
|
+
i += p.length;
|
|
110
|
+
tokens.push({ kind: "punct", value: p, start, end: i });
|
|
111
|
+
}
|
|
112
|
+
tokens.push({ kind: "end", start: text.length, end: text.length });
|
|
113
|
+
return tokens;
|
|
114
|
+
}
|
|
115
|
+
const BINARY = {
|
|
116
|
+
"||": 1,
|
|
117
|
+
"&&": 2,
|
|
118
|
+
"==": 3,
|
|
119
|
+
"!=": 3,
|
|
120
|
+
"<": 4,
|
|
121
|
+
"<=": 4,
|
|
122
|
+
">": 4,
|
|
123
|
+
">=": 4,
|
|
124
|
+
"+": 5,
|
|
125
|
+
"-": 5,
|
|
126
|
+
"*": 6,
|
|
127
|
+
"/": 6,
|
|
128
|
+
"%": 6,
|
|
129
|
+
};
|
|
130
|
+
function parser(text, tokens) {
|
|
131
|
+
let pos = 0;
|
|
132
|
+
const peek = () => tokens[pos];
|
|
133
|
+
const next = () => tokens[pos++];
|
|
134
|
+
const isPunct = (v) => peek().kind === "punct" && peek().value === v;
|
|
135
|
+
const show = (t) => t.kind === "end" ? "the end" : `"${text.slice(t.start, t.end)}"`;
|
|
136
|
+
function expect(v, after) {
|
|
137
|
+
if (!isPunct(v)) {
|
|
138
|
+
const shown = text.slice(after.start, peek().start).trim();
|
|
139
|
+
throw new Fail(`Expected "${v}" after "${shown}"`, peek().start);
|
|
140
|
+
}
|
|
141
|
+
next();
|
|
142
|
+
}
|
|
143
|
+
function primary() {
|
|
144
|
+
const t = next();
|
|
145
|
+
switch (t.kind) {
|
|
146
|
+
case "num":
|
|
147
|
+
return { type: "literal", value: t.value, start: t.start };
|
|
148
|
+
case "str":
|
|
149
|
+
return { type: "literal", value: t.value, start: t.start };
|
|
150
|
+
case "regex":
|
|
151
|
+
return {
|
|
152
|
+
type: "regex",
|
|
153
|
+
pattern: t.pattern,
|
|
154
|
+
flags: t.flags,
|
|
155
|
+
start: t.start,
|
|
156
|
+
};
|
|
157
|
+
case "ident": {
|
|
158
|
+
if (t.value === "true" || t.value === "false")
|
|
159
|
+
return { type: "literal", value: t.value === "true", start: t.start };
|
|
160
|
+
if (t.value === "null")
|
|
161
|
+
return { type: "literal", value: null, start: t.start };
|
|
162
|
+
if (isPunct("(")) {
|
|
163
|
+
next();
|
|
164
|
+
const args = [];
|
|
165
|
+
if (!isPunct(")")) {
|
|
166
|
+
do
|
|
167
|
+
args.push(expression(0));
|
|
168
|
+
while (isPunct(",") && next());
|
|
169
|
+
}
|
|
170
|
+
const call = {
|
|
171
|
+
type: "call",
|
|
172
|
+
callee: t.value,
|
|
173
|
+
args,
|
|
174
|
+
start: t.start,
|
|
175
|
+
};
|
|
176
|
+
expect(")", call);
|
|
177
|
+
return call;
|
|
178
|
+
}
|
|
179
|
+
return { type: "name", name: t.value, start: t.start };
|
|
180
|
+
}
|
|
181
|
+
case "punct":
|
|
182
|
+
if (t.value === "(") {
|
|
183
|
+
const inner = expression(0);
|
|
184
|
+
expect(")", { ...inner, start: t.start + 1 });
|
|
185
|
+
return inner;
|
|
186
|
+
}
|
|
187
|
+
if (t.value === "!" || t.value === "-")
|
|
188
|
+
return {
|
|
189
|
+
type: "unary",
|
|
190
|
+
op: t.value,
|
|
191
|
+
arg: postfix(primary()),
|
|
192
|
+
start: t.start,
|
|
193
|
+
};
|
|
194
|
+
throw new Fail(`Unexpected ${show(t)}`, t.start);
|
|
195
|
+
case "end":
|
|
196
|
+
throw new Fail(pos === 1 ? "Write an expression" : "The expression ends too early", t.start);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function postfix(expr) {
|
|
200
|
+
for (;;) {
|
|
201
|
+
if (isPunct(".")) {
|
|
202
|
+
next();
|
|
203
|
+
const name = next();
|
|
204
|
+
if (name.kind !== "ident")
|
|
205
|
+
throw new Fail(`Expected a name after "."`, name.start);
|
|
206
|
+
expr = {
|
|
207
|
+
type: "member",
|
|
208
|
+
object: expr,
|
|
209
|
+
property: name.value,
|
|
210
|
+
start: expr.start,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
else if (isPunct("[")) {
|
|
214
|
+
next();
|
|
215
|
+
const property = expression(0);
|
|
216
|
+
expect("]", property);
|
|
217
|
+
expr = { type: "member", object: expr, property, start: expr.start };
|
|
218
|
+
}
|
|
219
|
+
else if (isPunct("(")) {
|
|
220
|
+
throw new Fail("Only helpers can be called, like contains(x, y)", expr.start);
|
|
221
|
+
}
|
|
222
|
+
else
|
|
223
|
+
return expr;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function expression(min) {
|
|
227
|
+
let left = postfix(primary());
|
|
228
|
+
for (;;) {
|
|
229
|
+
const t = peek();
|
|
230
|
+
const prec = t.kind === "punct" ? BINARY[t.value] : undefined;
|
|
231
|
+
if (prec === undefined || prec <= min) {
|
|
232
|
+
if (min === 0 &&
|
|
233
|
+
t.kind !== "end" &&
|
|
234
|
+
!(t.kind === "punct" && [")", "]", ","].includes(t.value)))
|
|
235
|
+
throw new Fail(`Unexpected ${show(t)}`, t.start);
|
|
236
|
+
return left;
|
|
237
|
+
}
|
|
238
|
+
next();
|
|
239
|
+
const right = expression(prec);
|
|
240
|
+
left = {
|
|
241
|
+
type: "binary",
|
|
242
|
+
op: t.value,
|
|
243
|
+
left,
|
|
244
|
+
right,
|
|
245
|
+
start: left.start,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { expression, peek };
|
|
250
|
+
}
|
|
251
|
+
/** A member chain with only static parts, as a path; undefined otherwise */
|
|
252
|
+
function chainOf(e) {
|
|
253
|
+
if (e.type === "name")
|
|
254
|
+
return { path: [e.name], start: e.start };
|
|
255
|
+
if (e.type !== "member")
|
|
256
|
+
return undefined;
|
|
257
|
+
const inner = chainOf(e.object);
|
|
258
|
+
if (!inner)
|
|
259
|
+
return undefined;
|
|
260
|
+
const prop = typeof e.property === "string"
|
|
261
|
+
? e.property
|
|
262
|
+
: e.property.type === "literal" &&
|
|
263
|
+
(typeof e.property.value === "string" ||
|
|
264
|
+
typeof e.property.value === "number")
|
|
265
|
+
? String(e.property.value)
|
|
266
|
+
: undefined;
|
|
267
|
+
return prop === undefined
|
|
268
|
+
? undefined
|
|
269
|
+
: { path: [...inner.path, prop], start: inner.start };
|
|
270
|
+
}
|
|
271
|
+
function collect(e, refs, calls) {
|
|
272
|
+
switch (e.type) {
|
|
273
|
+
case "name":
|
|
274
|
+
refs.push({ path: [e.name], start: e.start });
|
|
275
|
+
return;
|
|
276
|
+
case "member": {
|
|
277
|
+
const chain = chainOf(e);
|
|
278
|
+
if (chain)
|
|
279
|
+
refs.push(chain);
|
|
280
|
+
else {
|
|
281
|
+
collect(e.object, refs, calls);
|
|
282
|
+
if (typeof e.property !== "string")
|
|
283
|
+
collect(e.property, refs, calls);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
case "unary":
|
|
288
|
+
return collect(e.arg, refs, calls);
|
|
289
|
+
case "binary":
|
|
290
|
+
collect(e.left, refs, calls);
|
|
291
|
+
return collect(e.right, refs, calls);
|
|
292
|
+
case "call":
|
|
293
|
+
calls.push({ name: e.callee, args: e.args.length, start: e.start });
|
|
294
|
+
for (const a of e.args)
|
|
295
|
+
collect(a, refs, calls);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/** Parses an expression into its syntax tree, the references it reads and the helpers it calls */
|
|
300
|
+
export function parseExpression(text) {
|
|
301
|
+
try {
|
|
302
|
+
const tokens = tokenize(text);
|
|
303
|
+
const { expression, peek } = parser(text, tokens);
|
|
304
|
+
const expr = expression(0);
|
|
305
|
+
if (peek().kind !== "end")
|
|
306
|
+
throw new Fail(`Unexpected "${text.slice(peek().start, peek().end)}"`, peek().start);
|
|
307
|
+
const refs = [];
|
|
308
|
+
const calls = [];
|
|
309
|
+
collect(expr, refs, calls);
|
|
310
|
+
return { ok: true, expr, refs, calls };
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
313
|
+
if (e instanceof Fail)
|
|
314
|
+
return { ok: false, error: { message: e.message, at: e.at } };
|
|
315
|
+
throw e;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AnyNodeDefinition } from "../nodes/define.js";
|
|
2
|
+
import type { FlowFile } from "../schema.js";
|
|
3
|
+
import type { ExpressionScope } from "./check.js";
|
|
4
|
+
/** What an expression at `nodeId` may use; `nodes` adds definitions core doesn't have (steps) */
|
|
5
|
+
export declare function scopeAt(flow: FlowFile, nodeId: string, environment: {
|
|
6
|
+
name: string;
|
|
7
|
+
variables: Record<string, unknown>;
|
|
8
|
+
}, nodes?: readonly AnyNodeDefinition[]): ExpressionScope;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ancestors, dominates, reachOrder } from "../graph.js";
|
|
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);
|
|
7
|
+
const keyOf = (id) => flow.nodes.find((n) => n.id === id)?.key;
|
|
8
|
+
const keys = (ids) => [...ids].map(keyOf).filter((k) => !!k);
|
|
9
|
+
const before = ancestors(flow, nodeId);
|
|
10
|
+
const after = new Set(reachOrder(flow, nodeId));
|
|
11
|
+
after.delete(nodeId);
|
|
12
|
+
const vars = flow.nodes
|
|
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
|
+
})
|
|
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
|
+
}));
|
|
23
|
+
return {
|
|
24
|
+
env: {
|
|
25
|
+
name: environment.name,
|
|
26
|
+
variables: Object.keys(environment.variables),
|
|
27
|
+
},
|
|
28
|
+
vars: [...new Set(vars)],
|
|
29
|
+
before: keys(before),
|
|
30
|
+
after: keys(after),
|
|
31
|
+
all: keys(flow.nodes.map((n) => n.id)),
|
|
32
|
+
outputs,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FlowEdgeModel, FlowFile } from "./schema.js";
|
|
2
|
+
/** The nodes reached from `from`, in the order a walk first meets them (`from` included) */
|
|
3
|
+
export declare function reachOrder(flow: FlowFile, from: string): string[];
|
|
4
|
+
/** Where the branches of a node meet: the first node the first branch reaches that every other branch reaches too */
|
|
5
|
+
export declare function meetOf(flow: FlowFile, node: string): string | undefined;
|
|
6
|
+
/** The edges out of a dynamic `branches` node in the order their branches were filled (by their branch-<n> name) */
|
|
7
|
+
export declare const inFillOrder: (edges: FlowEdgeModel[]) => FlowEdgeModel[];
|
|
8
|
+
/** Every node with a path to `nodeId` (not the node itself) */
|
|
9
|
+
export declare function ancestors(flow: FlowFile, nodeId: string): Set<string>;
|
|
10
|
+
/** Whether every path from Start to `n` passes `d` */
|
|
11
|
+
export declare function dominates(flow: FlowFile, d: string, n: string): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** The nodes reached from `from`, in the order a walk first meets them (`from` included) */
|
|
2
|
+
export function reachOrder(flow, from) {
|
|
3
|
+
const order = [];
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
const visit = (id) => {
|
|
6
|
+
if (seen.has(id))
|
|
7
|
+
return;
|
|
8
|
+
seen.add(id);
|
|
9
|
+
order.push(id);
|
|
10
|
+
for (const e of flow.edges)
|
|
11
|
+
if (e.source === id)
|
|
12
|
+
visit(e.target);
|
|
13
|
+
};
|
|
14
|
+
visit(from);
|
|
15
|
+
return order;
|
|
16
|
+
}
|
|
17
|
+
/** Where the branches of a node meet: the first node the first branch reaches that every other branch reaches too */
|
|
18
|
+
export function meetOf(flow, node) {
|
|
19
|
+
const out = flow.edges.filter((e) => e.source === node);
|
|
20
|
+
if (out.length === 0)
|
|
21
|
+
return undefined;
|
|
22
|
+
const others = out.slice(1).map((e) => new Set(reachOrder(flow, e.target)));
|
|
23
|
+
return reachOrder(flow, out[0].target).find((id) => others.every((reach) => reach.has(id)));
|
|
24
|
+
}
|
|
25
|
+
/** The edges out of a dynamic `branches` node in the order their branches were filled (by their branch-<n> name) */
|
|
26
|
+
export const inFillOrder = (edges) => {
|
|
27
|
+
const index = (e) => Number(e.sourceHandle?.match(/^branch-(\d+)$/)?.[1] ?? 1e9);
|
|
28
|
+
return [...edges].sort((a, b) => index(a) - index(b));
|
|
29
|
+
};
|
|
30
|
+
/** Every node with a path to `nodeId` (not the node itself) */
|
|
31
|
+
export function ancestors(flow, nodeId) {
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const visit = (id) => {
|
|
34
|
+
for (const e of flow.edges)
|
|
35
|
+
if (e.target === id && !seen.has(e.source)) {
|
|
36
|
+
seen.add(e.source);
|
|
37
|
+
visit(e.source);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
visit(nodeId);
|
|
41
|
+
seen.delete(nodeId);
|
|
42
|
+
return seen;
|
|
43
|
+
}
|
|
44
|
+
/** Whether every path from Start to `n` passes `d` */
|
|
45
|
+
export function dominates(flow, d, n) {
|
|
46
|
+
const start = flow.nodes.find((x) => x.kind === "start");
|
|
47
|
+
if (!start || d === n)
|
|
48
|
+
return d === n;
|
|
49
|
+
if (d === start.id)
|
|
50
|
+
return true;
|
|
51
|
+
const seen = new Set([d]);
|
|
52
|
+
const stack = [start.id];
|
|
53
|
+
while (stack.length) {
|
|
54
|
+
const id = stack.pop();
|
|
55
|
+
if (id === n)
|
|
56
|
+
return false;
|
|
57
|
+
if (seen.has(id))
|
|
58
|
+
continue;
|
|
59
|
+
seen.add(id);
|
|
60
|
+
for (const e of flow.edges)
|
|
61
|
+
if (e.source === id)
|
|
62
|
+
stack.push(e.target);
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure half of flows: the file format, keys, graph helpers and the expression language.
|
|
3
|
+
* No Node APIs, so a browser bundle (the SAMURAI app's renderer) can import it.
|
|
4
|
+
*/
|
|
5
|
+
export * from "./schema.js";
|
|
6
|
+
export * from "./keys.js";
|
|
7
|
+
export * from "./graph.js";
|
|
8
|
+
export * from "./expression/index.js";
|
|
9
|
+
export * from "./nodes/index.js";
|
|
10
|
+
export * from "./check.js";
|
|
11
|
+
export * from "./steps.js";
|
|
12
|
+
/**
|
|
13
|
+
* Version of the contract between the SAMURAI app and this framework.
|
|
14
|
+
* Increments whenever the app and the framework must agree on something new
|
|
15
|
+
* (a node kind, a field, an event), so the app can tell when the framework is too old.
|
|
16
|
+
*/
|
|
17
|
+
export declare const FLOWS_API = 2;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure half of flows: the file format, keys, graph helpers and the expression language.
|
|
3
|
+
* No Node APIs, so a browser bundle (the SAMURAI app's renderer) can import it.
|
|
4
|
+
*/
|
|
5
|
+
export * from "./schema.js";
|
|
6
|
+
export * from "./keys.js";
|
|
7
|
+
export * from "./graph.js";
|
|
8
|
+
export * from "./expression/index.js";
|
|
9
|
+
export * from "./nodes/index.js";
|
|
10
|
+
export * from "./check.js";
|
|
11
|
+
export * from "./steps.js";
|
|
12
|
+
/**
|
|
13
|
+
* Version of the contract between the SAMURAI app and this framework.
|
|
14
|
+
* Increments whenever the app and the framework must agree on something new
|
|
15
|
+
* (a node kind, a field, an event), so the app can tell when the framework is too old.
|
|
16
|
+
*/
|
|
17
|
+
export const FLOWS_API = 2;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FlowFile, FlowNodeModel } from "./schema.js";
|
|
2
|
+
export declare const KEY_PATTERN: RegExp;
|
|
3
|
+
/** What a node's key is made from: its title, else its test's name (last part), else its group, else its kind */
|
|
4
|
+
export declare function keyBase(node: FlowNodeModel): string;
|
|
5
|
+
/** `base`, or `base_2`, `base_3` … when taken */
|
|
6
|
+
export declare function uniqueKey(base: string, taken: Set<string>): string;
|
|
7
|
+
/** Gives every node a valid, unique key; nodes that have one keep it (the first of a duplicate keeps it) */
|
|
8
|
+
export declare function withKeys(flow: FlowFile): FlowFile;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
2
|
+
const slug = (text) => {
|
|
3
|
+
const s = text
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
6
|
+
.replace(/^_+|_+$/g, "");
|
|
7
|
+
return !s ? "" : /^[a-z]/.test(s) ? s : `n${s}`;
|
|
8
|
+
};
|
|
9
|
+
/** What a node's key is made from: its title, else its test's name (last part), else its group, else its kind */
|
|
10
|
+
export function keyBase(node) {
|
|
11
|
+
const testName = node.ref?.testId?.split("::").pop()?.split(" > ").pop();
|
|
12
|
+
return (slug(node.title ?? "") ||
|
|
13
|
+
slug(testName ?? "") ||
|
|
14
|
+
slug(node.ref?.group ?? "") ||
|
|
15
|
+
slug(node.kind) ||
|
|
16
|
+
"node");
|
|
17
|
+
}
|
|
18
|
+
/** `base`, or `base_2`, `base_3` … when taken */
|
|
19
|
+
export function uniqueKey(base, taken) {
|
|
20
|
+
if (!taken.has(base))
|
|
21
|
+
return base;
|
|
22
|
+
let i = 2;
|
|
23
|
+
while (taken.has(`${base}_${i}`))
|
|
24
|
+
i++;
|
|
25
|
+
return `${base}_${i}`;
|
|
26
|
+
}
|
|
27
|
+
/** Gives every node a valid, unique key; nodes that have one keep it (the first of a duplicate keeps it) */
|
|
28
|
+
export function withKeys(flow) {
|
|
29
|
+
const taken = new Set();
|
|
30
|
+
const keeps = new Set();
|
|
31
|
+
for (const node of flow.nodes)
|
|
32
|
+
if (node.key && KEY_PATTERN.test(node.key) && !taken.has(node.key)) {
|
|
33
|
+
taken.add(node.key);
|
|
34
|
+
keeps.add(node);
|
|
35
|
+
}
|
|
36
|
+
if (keeps.size === flow.nodes.length)
|
|
37
|
+
return flow;
|
|
38
|
+
const nodes = flow.nodes.map((node) => {
|
|
39
|
+
if (keeps.has(node))
|
|
40
|
+
return node;
|
|
41
|
+
const key = uniqueKey(keyBase(node), taken);
|
|
42
|
+
taken.add(key);
|
|
43
|
+
return { ...node, key };
|
|
44
|
+
});
|
|
45
|
+
return { ...flow, nodes };
|
|
46
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const condition: import("./define.js").NodeCoreDefinition<"condition", import("./define.js").ConfigOf<readonly [{
|
|
2
|
+
readonly path: "config.expr";
|
|
3
|
+
readonly type: "expression";
|
|
4
|
+
readonly label: "Expression";
|
|
5
|
+
readonly placeholder: "nodes.login.status == \"passed\"";
|
|
6
|
+
readonly default: "";
|
|
7
|
+
}]>>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineNode } from "./define.js";
|
|
2
|
+
export const condition = defineNode({
|
|
3
|
+
kind: "condition",
|
|
4
|
+
label: "Condition",
|
|
5
|
+
shape: "branches",
|
|
6
|
+
branches: [
|
|
7
|
+
{ name: "true", label: "true", color: "success" },
|
|
8
|
+
{ name: "false", label: "false", color: "error" },
|
|
9
|
+
],
|
|
10
|
+
fields: [
|
|
11
|
+
{
|
|
12
|
+
path: "config.expr",
|
|
13
|
+
type: "expression",
|
|
14
|
+
label: "Expression",
|
|
15
|
+
placeholder: 'nodes.login.status == "passed"',
|
|
16
|
+
default: "",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
outputs: true,
|
|
20
|
+
onFailure: true,
|
|
21
|
+
});
|