callman-core 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/runner/runCollection.d.ts.map +1 -1
- package/dist/runner/runCollection.js.map +1 -1
- package/dist/scenario-runner/condition.d.ts +67 -0
- package/dist/scenario-runner/condition.d.ts.map +1 -0
- package/dist/scenario-runner/condition.js +1508 -0
- package/dist/scenario-runner/condition.js.map +1 -0
- package/dist/scenario-runner/conditionTypes.d.ts +78 -0
- package/dist/scenario-runner/conditionTypes.d.ts.map +1 -0
- package/dist/scenario-runner/conditionTypes.js +2 -0
- package/dist/scenario-runner/conditionTypes.js.map +1 -0
- package/dist/scenario-runner/executionPolicyRunner.d.ts +24 -0
- package/dist/scenario-runner/executionPolicyRunner.d.ts.map +1 -0
- package/dist/scenario-runner/executionPolicyRunner.js +48 -0
- package/dist/scenario-runner/executionPolicyRunner.js.map +1 -0
- package/dist/scenario-runner/graphTraversal.d.ts +12 -0
- package/dist/scenario-runner/graphTraversal.d.ts.map +1 -0
- package/dist/scenario-runner/graphTraversal.js +21 -0
- package/dist/scenario-runner/graphTraversal.js.map +1 -0
- package/dist/scenario-runner/index.d.ts +7 -0
- package/dist/scenario-runner/index.d.ts.map +1 -0
- package/dist/scenario-runner/index.js +7 -0
- package/dist/scenario-runner/index.js.map +1 -0
- package/dist/scenario-runner/nodePolicy.d.ts +11 -0
- package/dist/scenario-runner/nodePolicy.d.ts.map +1 -0
- package/dist/scenario-runner/nodePolicy.js +107 -0
- package/dist/scenario-runner/nodePolicy.js.map +1 -0
- package/dist/scenario-runner/retryExecutor.d.ts +30 -0
- package/dist/scenario-runner/retryExecutor.d.ts.map +1 -0
- package/dist/scenario-runner/retryExecutor.js +50 -0
- package/dist/scenario-runner/retryExecutor.js.map +1 -0
- package/dist/scenario-runner/runScenario.d.ts +3 -0
- package/dist/scenario-runner/runScenario.d.ts.map +1 -0
- package/dist/scenario-runner/runScenario.js +1133 -0
- package/dist/scenario-runner/runScenario.js.map +1 -0
- package/dist/scenario-runner/templateResolver.d.ts +36 -0
- package/dist/scenario-runner/templateResolver.d.ts.map +1 -0
- package/dist/scenario-runner/templateResolver.js +440 -0
- package/dist/scenario-runner/templateResolver.js.map +1 -0
- package/dist/scenario-runner/types.d.ts +596 -0
- package/dist/scenario-runner/types.d.ts.map +1 -0
- package/dist/scenario-runner/types.js +2 -0
- package/dist/scenario-runner/types.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1508 @@
|
|
|
1
|
+
const createId = (prefix) => typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
|
2
|
+
? crypto.randomUUID()
|
|
3
|
+
: `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4
|
+
const BUILT_IN_RUNTIME_SUGGESTIONS = [
|
|
5
|
+
{
|
|
6
|
+
description: "Latest HTTP status code from the most recent request node.",
|
|
7
|
+
exampleValue: "200",
|
|
8
|
+
group: "Built-in",
|
|
9
|
+
path: "response.status",
|
|
10
|
+
valueType: "number",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
description: "Latest HTTP response body payload.",
|
|
14
|
+
exampleValue: '{"ok":true}',
|
|
15
|
+
group: "Built-in",
|
|
16
|
+
path: "response.body",
|
|
17
|
+
valueType: "string",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
description: "Latest HTTP response text body.",
|
|
21
|
+
exampleValue: "OK",
|
|
22
|
+
group: "Built-in",
|
|
23
|
+
path: "response.text",
|
|
24
|
+
valueType: "string",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
description: "Latest HTTP response headers object.",
|
|
28
|
+
exampleValue: '{"content-type":"application/json"}',
|
|
29
|
+
group: "Built-in",
|
|
30
|
+
path: "response.headers",
|
|
31
|
+
valueType: "string",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
description: "Latest HTTP request duration in milliseconds.",
|
|
35
|
+
exampleValue: "124",
|
|
36
|
+
group: "Built-in",
|
|
37
|
+
path: "response.durationMs",
|
|
38
|
+
valueType: "number",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
description: "Latest database result rows for unlabeled DB access.",
|
|
42
|
+
exampleValue: "[...]",
|
|
43
|
+
group: "Built-in",
|
|
44
|
+
path: "db.result",
|
|
45
|
+
valueType: "array",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
description: "Latest Kafka event payload for unlabeled Kafka access.",
|
|
49
|
+
exampleValue: '{"pin":"1234"}',
|
|
50
|
+
group: "Built-in",
|
|
51
|
+
path: "kafkaEvent",
|
|
52
|
+
valueType: "string",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
description: "Latest Redis result for unlabeled Redis access.",
|
|
56
|
+
exampleValue: "cached-value",
|
|
57
|
+
group: "Built-in",
|
|
58
|
+
path: "redis.result",
|
|
59
|
+
valueType: "string",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
description: "Scenario environment variables.",
|
|
63
|
+
exampleValue: "token",
|
|
64
|
+
group: "Built-in",
|
|
65
|
+
path: "env",
|
|
66
|
+
valueType: "string",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
description: "Global runtime variables.",
|
|
70
|
+
exampleValue: "sharedToken",
|
|
71
|
+
group: "Built-in",
|
|
72
|
+
path: "global",
|
|
73
|
+
valueType: "string",
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
export const CONDITION_OPERATOR_META = [
|
|
77
|
+
{ category: "Equality", label: "Equals", value: "equals" },
|
|
78
|
+
{ category: "Equality", label: "Not Equals", value: "not_equals" },
|
|
79
|
+
{ category: "Equality", label: "Strict Equals", value: "strict_equals" },
|
|
80
|
+
{ category: "Equality", label: "Strict Not Equals", value: "strict_not_equals" },
|
|
81
|
+
{ category: "Numeric", label: "Greater Than", value: "greater_than" },
|
|
82
|
+
{ category: "Numeric", label: "Less Than", value: "less_than" },
|
|
83
|
+
{ category: "Numeric", label: "Greater Or Equal", value: "greater_or_equal" },
|
|
84
|
+
{ category: "Numeric", label: "Less Or Equal", value: "less_or_equal" },
|
|
85
|
+
{ category: "Numeric", label: "Between", value: "between" },
|
|
86
|
+
{ category: "String", label: "Contains", value: "contains" },
|
|
87
|
+
{ category: "String", label: "Not Contains", value: "not_contains" },
|
|
88
|
+
{ category: "String", label: "Starts With", value: "starts_with" },
|
|
89
|
+
{ category: "String", label: "Ends With", value: "ends_with" },
|
|
90
|
+
{ category: "String", label: "Regex", value: "regex" },
|
|
91
|
+
{ category: "Existence", isUnary: true, label: "Exists", value: "exists" },
|
|
92
|
+
{ category: "Existence", isUnary: true, label: "Not Exists", value: "not_exists" },
|
|
93
|
+
{ category: "Existence", isUnary: true, label: "Is Empty", value: "is_empty" },
|
|
94
|
+
{ category: "Existence", isUnary: true, label: "Not Empty", value: "not_empty" },
|
|
95
|
+
{ category: "Existence", isUnary: true, label: "Is Null", value: "is_null" },
|
|
96
|
+
{ category: "Existence", isUnary: true, label: "Not Null", value: "not_null" },
|
|
97
|
+
{ category: "Collection", label: "In", value: "in" },
|
|
98
|
+
{ category: "Collection", label: "Not In", value: "not_in" },
|
|
99
|
+
];
|
|
100
|
+
const OPERATOR_ALIAS_MAP = [
|
|
101
|
+
{ alias: "!==", operator: "strict_not_equals" },
|
|
102
|
+
{ alias: "===", operator: "strict_equals" },
|
|
103
|
+
{ alias: "!=", operator: "not_equals" },
|
|
104
|
+
{ alias: "==", operator: "equals" },
|
|
105
|
+
{ alias: ">=", operator: "greater_or_equal" },
|
|
106
|
+
{ alias: "<=", operator: "less_or_equal" },
|
|
107
|
+
{ alias: ">", operator: "greater_than" },
|
|
108
|
+
{ alias: "<", operator: "less_than" },
|
|
109
|
+
{ alias: "not_contains", operator: "not_contains" },
|
|
110
|
+
{ alias: "not contains", operator: "not_contains" },
|
|
111
|
+
{ alias: "contains", operator: "contains" },
|
|
112
|
+
{ alias: "starts_with", operator: "starts_with" },
|
|
113
|
+
{ alias: "starts with", operator: "starts_with" },
|
|
114
|
+
{ alias: "ends_with", operator: "ends_with" },
|
|
115
|
+
{ alias: "ends with", operator: "ends_with" },
|
|
116
|
+
{ alias: "regex", operator: "regex" },
|
|
117
|
+
{ alias: "not_exists", operator: "not_exists" },
|
|
118
|
+
{ alias: "not exists", operator: "not_exists" },
|
|
119
|
+
{ alias: "exists", operator: "exists" },
|
|
120
|
+
{ alias: "is_empty", operator: "is_empty" },
|
|
121
|
+
{ alias: "is empty", operator: "is_empty" },
|
|
122
|
+
{ alias: "not_empty", operator: "not_empty" },
|
|
123
|
+
{ alias: "not empty", operator: "not_empty" },
|
|
124
|
+
{ alias: "is_null", operator: "is_null" },
|
|
125
|
+
{ alias: "is null", operator: "is_null" },
|
|
126
|
+
{ alias: "not_null", operator: "not_null" },
|
|
127
|
+
{ alias: "not null", operator: "not_null" },
|
|
128
|
+
{ alias: "not_in", operator: "not_in" },
|
|
129
|
+
{ alias: "not in", operator: "not_in" },
|
|
130
|
+
{ alias: "in", operator: "in" },
|
|
131
|
+
{ alias: "between", operator: "between" },
|
|
132
|
+
];
|
|
133
|
+
const LOGICAL_OPERATOR_ALIAS_MAP = [
|
|
134
|
+
{ alias: "&&", operator: "AND" },
|
|
135
|
+
{ alias: "AND", operator: "AND" },
|
|
136
|
+
{ alias: "||", operator: "OR" },
|
|
137
|
+
{ alias: "OR", operator: "OR" },
|
|
138
|
+
];
|
|
139
|
+
const LEGACY_OPERATOR_MAP = {
|
|
140
|
+
"==": "equals",
|
|
141
|
+
"!=": "not_equals",
|
|
142
|
+
">": "greater_than",
|
|
143
|
+
"<": "less_than",
|
|
144
|
+
">=": "greater_or_equal",
|
|
145
|
+
"<=": "less_or_equal",
|
|
146
|
+
contains: "contains",
|
|
147
|
+
};
|
|
148
|
+
const VALUE_TYPE_LABELS = {
|
|
149
|
+
string: "String",
|
|
150
|
+
number: "Number",
|
|
151
|
+
boolean: "Boolean",
|
|
152
|
+
null: "Null",
|
|
153
|
+
array: "Array",
|
|
154
|
+
};
|
|
155
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
156
|
+
const normalizeWhitespace = (value) => value.replace(/\s+/g, " ").trim();
|
|
157
|
+
const normalizeAlias = (value) => value.trim().toUpperCase();
|
|
158
|
+
const isBoundaryCharacter = (value) => value.length === 0 || /[\s()[\]{},"']/.test(value);
|
|
159
|
+
const matchesAliasAtStart = (input, alias) => {
|
|
160
|
+
if (/^[<>=!&|]+$/.test(alias)) {
|
|
161
|
+
return input.startsWith(alias);
|
|
162
|
+
}
|
|
163
|
+
const normalizedAlias = normalizeAlias(alias);
|
|
164
|
+
const candidate = input.slice(0, alias.length);
|
|
165
|
+
if (/[A-Z_ ]/.test(normalizedAlias) && normalizeAlias(candidate) !== normalizedAlias) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (!/[A-Z_ ]/.test(normalizedAlias) && candidate !== alias) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return isBoundaryCharacter(input[alias.length] ?? "");
|
|
172
|
+
};
|
|
173
|
+
const isUnaryOperator = (operator) => operator === "exists" ||
|
|
174
|
+
operator === "not_exists" ||
|
|
175
|
+
operator === "is_empty" ||
|
|
176
|
+
operator === "not_empty" ||
|
|
177
|
+
operator === "is_null" ||
|
|
178
|
+
operator === "not_null";
|
|
179
|
+
const isNumericOperator = (operator) => operator === "greater_than" ||
|
|
180
|
+
operator === "less_than" ||
|
|
181
|
+
operator === "greater_or_equal" ||
|
|
182
|
+
operator === "less_or_equal" ||
|
|
183
|
+
operator === "between";
|
|
184
|
+
const isCollectionOperator = (operator) => operator === "in" || operator === "not_in";
|
|
185
|
+
const getOperatorToken = (operator) => {
|
|
186
|
+
switch (operator) {
|
|
187
|
+
case "equals":
|
|
188
|
+
return "==";
|
|
189
|
+
case "not_equals":
|
|
190
|
+
return "!=";
|
|
191
|
+
case "strict_equals":
|
|
192
|
+
return "===";
|
|
193
|
+
case "strict_not_equals":
|
|
194
|
+
return "!==";
|
|
195
|
+
case "greater_than":
|
|
196
|
+
return ">";
|
|
197
|
+
case "less_than":
|
|
198
|
+
return "<";
|
|
199
|
+
case "greater_or_equal":
|
|
200
|
+
return ">=";
|
|
201
|
+
case "less_or_equal":
|
|
202
|
+
return "<=";
|
|
203
|
+
case "between":
|
|
204
|
+
return "between";
|
|
205
|
+
case "contains":
|
|
206
|
+
return "contains";
|
|
207
|
+
case "not_contains":
|
|
208
|
+
return "not contains";
|
|
209
|
+
case "starts_with":
|
|
210
|
+
return "starts_with";
|
|
211
|
+
case "ends_with":
|
|
212
|
+
return "ends_with";
|
|
213
|
+
case "regex":
|
|
214
|
+
return "regex";
|
|
215
|
+
case "exists":
|
|
216
|
+
return "exists";
|
|
217
|
+
case "not_exists":
|
|
218
|
+
return "not exists";
|
|
219
|
+
case "is_empty":
|
|
220
|
+
return "is empty";
|
|
221
|
+
case "not_empty":
|
|
222
|
+
return "not empty";
|
|
223
|
+
case "is_null":
|
|
224
|
+
return "is null";
|
|
225
|
+
case "not_null":
|
|
226
|
+
return "not null";
|
|
227
|
+
case "in":
|
|
228
|
+
return "in";
|
|
229
|
+
case "not_in":
|
|
230
|
+
return "not in";
|
|
231
|
+
default:
|
|
232
|
+
return "==";
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const formatDisplayValue = (value) => {
|
|
236
|
+
if (typeof value === "string") {
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
240
|
+
return String(value);
|
|
241
|
+
}
|
|
242
|
+
if (value === null) {
|
|
243
|
+
return "null";
|
|
244
|
+
}
|
|
245
|
+
if (typeof value === "undefined") {
|
|
246
|
+
return "undefined";
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
return JSON.stringify(value);
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return String(value);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
const flattenScenarioNodes = (nodes) => nodes.flatMap((node) => node.type === "subscenario"
|
|
256
|
+
? [node, ...flattenScenarioNodes(node.data.config.nodes)]
|
|
257
|
+
: [node]);
|
|
258
|
+
const createSuggestion = (group, path, description, valueType, exampleValue) => ({
|
|
259
|
+
description,
|
|
260
|
+
...(exampleValue ? { exampleValue } : {}),
|
|
261
|
+
group,
|
|
262
|
+
path,
|
|
263
|
+
valueType,
|
|
264
|
+
});
|
|
265
|
+
const buildNodeSuggestions = (label, type) => {
|
|
266
|
+
const group = label;
|
|
267
|
+
switch (type) {
|
|
268
|
+
case "request":
|
|
269
|
+
return [
|
|
270
|
+
createSuggestion(group, `${label}.status`, "HTTP status code.", "number", "200"),
|
|
271
|
+
createSuggestion(group, `${label}.body`, "Resolved response body payload.", "string"),
|
|
272
|
+
createSuggestion(group, `${label}.data`, "Response data shortcut.", "string"),
|
|
273
|
+
createSuggestion(group, `${label}.headers`, "Response headers object.", "string"),
|
|
274
|
+
createSuggestion(group, `${label}.cookies`, "Response cookies object.", "string"),
|
|
275
|
+
createSuggestion(group, `${label}.durationMs`, "Request duration in milliseconds.", "number", "124"),
|
|
276
|
+
];
|
|
277
|
+
case "db":
|
|
278
|
+
return [
|
|
279
|
+
createSuggestion(group, `${label}.rows`, "Database query rows.", "array"),
|
|
280
|
+
createSuggestion(group, `${label}.rows[0]`, "First database row.", "string"),
|
|
281
|
+
createSuggestion(group, `${label}.rowCount`, "Total database rows returned.", "number", "1"),
|
|
282
|
+
];
|
|
283
|
+
case "kafka":
|
|
284
|
+
return [
|
|
285
|
+
createSuggestion(group, `${label}.meta`, "Kafka metadata object.", "string"),
|
|
286
|
+
createSuggestion(group, `${label}.value`, "Kafka payload value.", "string"),
|
|
287
|
+
createSuggestion(group, `${label}.text`, "Kafka payload text.", "string"),
|
|
288
|
+
createSuggestion(group, `${label}.headers`, "Kafka headers object.", "string"),
|
|
289
|
+
createSuggestion(group, `${label}.key`, "Kafka message key.", "string"),
|
|
290
|
+
createSuggestion(group, `${label}.timestamp`, "Kafka event timestamp.", "string"),
|
|
291
|
+
];
|
|
292
|
+
case "redis":
|
|
293
|
+
return [
|
|
294
|
+
createSuggestion(group, `${label}`, "Redis command result.", "string"),
|
|
295
|
+
createSuggestion(group, `${label}[0]`, "First Redis result when multiple commands run.", "string"),
|
|
296
|
+
];
|
|
297
|
+
case "condition":
|
|
298
|
+
return [
|
|
299
|
+
createSuggestion(group, `${label}.result`, "Condition boolean result.", "boolean", "true"),
|
|
300
|
+
createSuggestion(group, `${label}.preview`, "Human-readable condition preview.", "string"),
|
|
301
|
+
createSuggestion(group, `${label}.expression`, "Executable condition expression.", "string"),
|
|
302
|
+
createSuggestion(group, `${label}.mode`, "Condition editor mode.", "string", "builder"),
|
|
303
|
+
];
|
|
304
|
+
case "script":
|
|
305
|
+
return [
|
|
306
|
+
createSuggestion(group, `${label}`, "Returned Script Node output.", "string"),
|
|
307
|
+
];
|
|
308
|
+
case "aggregator":
|
|
309
|
+
return [
|
|
310
|
+
createSuggestion(group, `${label}`, "Aggregator JSON output.", "string"),
|
|
311
|
+
];
|
|
312
|
+
case "subscenario":
|
|
313
|
+
return [
|
|
314
|
+
createSuggestion(group, `${label}`, "Grouped output from the sub-scenario.", "string"),
|
|
315
|
+
];
|
|
316
|
+
default:
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
const dedupeSuggestions = (suggestions) => {
|
|
321
|
+
const seen = new Set();
|
|
322
|
+
return suggestions.filter((suggestion) => {
|
|
323
|
+
if (seen.has(suggestion.path)) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
seen.add(suggestion.path);
|
|
327
|
+
return true;
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
const getDefaultLiteralValue = (valueType) => {
|
|
331
|
+
switch (valueType) {
|
|
332
|
+
case "number":
|
|
333
|
+
return 0;
|
|
334
|
+
case "boolean":
|
|
335
|
+
return false;
|
|
336
|
+
case "null":
|
|
337
|
+
return null;
|
|
338
|
+
case "array":
|
|
339
|
+
return [];
|
|
340
|
+
default:
|
|
341
|
+
return "";
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const getDefaultRightOperand = (operator) => {
|
|
345
|
+
if (isUnaryOperator(operator)) {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
if (operator === "between" || isCollectionOperator(operator)) {
|
|
349
|
+
return createConditionOperand("literal", "array", operator === "between" ? [0, 1] : []);
|
|
350
|
+
}
|
|
351
|
+
return createConditionOperand("literal", "string", "");
|
|
352
|
+
};
|
|
353
|
+
const parseLegacyConditionOperator = (value) => LEGACY_OPERATOR_MAP[value] ?? "equals";
|
|
354
|
+
const coerceUnknownToBoolean = (value) => {
|
|
355
|
+
if (typeof value === "boolean") {
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
if (typeof value === "string") {
|
|
359
|
+
const normalized = value.trim().toLowerCase();
|
|
360
|
+
if (normalized === "true") {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
if (normalized === "false") {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
};
|
|
369
|
+
const coerceUnknownToNumber = (value) => {
|
|
370
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
if (typeof value === "string" && value.trim()) {
|
|
374
|
+
const parsed = Number(value);
|
|
375
|
+
if (Number.isFinite(parsed)) {
|
|
376
|
+
return parsed;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
};
|
|
381
|
+
const coerceOperandValue = (value, valueType) => {
|
|
382
|
+
if (valueType === "null") {
|
|
383
|
+
return value === null ? null : value === "null" ? null : undefined;
|
|
384
|
+
}
|
|
385
|
+
if (valueType === "boolean") {
|
|
386
|
+
return coerceUnknownToBoolean(value) ?? undefined;
|
|
387
|
+
}
|
|
388
|
+
if (valueType === "number") {
|
|
389
|
+
return coerceUnknownToNumber(value) ?? undefined;
|
|
390
|
+
}
|
|
391
|
+
if (valueType === "array") {
|
|
392
|
+
if (Array.isArray(value)) {
|
|
393
|
+
return value;
|
|
394
|
+
}
|
|
395
|
+
if (typeof value === "string") {
|
|
396
|
+
try {
|
|
397
|
+
const parsed = JSON.parse(value);
|
|
398
|
+
return Array.isArray(parsed) ? parsed : undefined;
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
406
|
+
if (typeof value === "string") {
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
if (value === null || typeof value === "undefined") {
|
|
410
|
+
return "";
|
|
411
|
+
}
|
|
412
|
+
return formatDisplayValue(value);
|
|
413
|
+
};
|
|
414
|
+
const parseLiteralValue = (value, valueType) => {
|
|
415
|
+
const coercedValue = coerceOperandValue(value, valueType);
|
|
416
|
+
if (typeof coercedValue === "undefined") {
|
|
417
|
+
return getDefaultLiteralValue(valueType);
|
|
418
|
+
}
|
|
419
|
+
return coercedValue;
|
|
420
|
+
};
|
|
421
|
+
const parseRuntimePathSegments = (path) => {
|
|
422
|
+
const segments = [];
|
|
423
|
+
let buffer = "";
|
|
424
|
+
let index = 0;
|
|
425
|
+
const pushBuffer = () => {
|
|
426
|
+
const trimmed = buffer.trim();
|
|
427
|
+
if (trimmed) {
|
|
428
|
+
segments.push(trimmed);
|
|
429
|
+
}
|
|
430
|
+
buffer = "";
|
|
431
|
+
};
|
|
432
|
+
while (index < path.length) {
|
|
433
|
+
const character = path[index] ?? "";
|
|
434
|
+
if (character === ".") {
|
|
435
|
+
pushBuffer();
|
|
436
|
+
index += 1;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (character === "[") {
|
|
440
|
+
pushBuffer();
|
|
441
|
+
const closingIndex = path.indexOf("]", index + 1);
|
|
442
|
+
if (closingIndex === -1) {
|
|
443
|
+
buffer += path.slice(index);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
const indexValue = path.slice(index + 1, closingIndex).trim();
|
|
447
|
+
const numericIndex = Number(indexValue);
|
|
448
|
+
if (Number.isInteger(numericIndex)) {
|
|
449
|
+
segments.push(numericIndex);
|
|
450
|
+
}
|
|
451
|
+
else if (indexValue) {
|
|
452
|
+
segments.push(indexValue);
|
|
453
|
+
}
|
|
454
|
+
index = closingIndex + 1;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
buffer += character;
|
|
458
|
+
index += 1;
|
|
459
|
+
}
|
|
460
|
+
pushBuffer();
|
|
461
|
+
return segments;
|
|
462
|
+
};
|
|
463
|
+
const resolveRuntimePath = (roots, path) => {
|
|
464
|
+
const segments = parseRuntimePathSegments(path);
|
|
465
|
+
if (segments.length === 0) {
|
|
466
|
+
return {
|
|
467
|
+
exists: false,
|
|
468
|
+
value: undefined,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
let currentValue = roots;
|
|
472
|
+
for (const segment of segments) {
|
|
473
|
+
if (typeof segment === "number") {
|
|
474
|
+
if (!Array.isArray(currentValue) || segment < 0 || segment >= currentValue.length) {
|
|
475
|
+
return {
|
|
476
|
+
exists: false,
|
|
477
|
+
value: undefined,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
currentValue = currentValue[segment];
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
if (!isRecord(currentValue)) {
|
|
484
|
+
return {
|
|
485
|
+
exists: false,
|
|
486
|
+
value: undefined,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (!Object.prototype.hasOwnProperty.call(currentValue, segment)) {
|
|
490
|
+
return {
|
|
491
|
+
exists: false,
|
|
492
|
+
value: undefined,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
currentValue = currentValue[segment];
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
exists: true,
|
|
499
|
+
value: currentValue,
|
|
500
|
+
};
|
|
501
|
+
};
|
|
502
|
+
const createRuntimeRoots = ({ dbResult, environment, globals, kafkaEvent, redisResult, responseRoot, workflowContext, }) => {
|
|
503
|
+
const roots = {
|
|
504
|
+
...(workflowContext ?? {}),
|
|
505
|
+
response: responseRoot ?? null,
|
|
506
|
+
lastResponse: responseRoot ?? null,
|
|
507
|
+
env: {
|
|
508
|
+
...environment,
|
|
509
|
+
dbResult: dbResult ?? null,
|
|
510
|
+
kafkaEvent: kafkaEvent ?? null,
|
|
511
|
+
kafkaevent: kafkaEvent ?? null,
|
|
512
|
+
lastResponse: responseRoot ?? null,
|
|
513
|
+
redisResult: redisResult ?? null,
|
|
514
|
+
},
|
|
515
|
+
global: globals,
|
|
516
|
+
globals,
|
|
517
|
+
db: {
|
|
518
|
+
result: dbResult ?? null,
|
|
519
|
+
},
|
|
520
|
+
kafkaEvent: kafkaEvent ?? null,
|
|
521
|
+
kafkaevent: kafkaEvent ?? null,
|
|
522
|
+
redis: {
|
|
523
|
+
result: redisResult ?? null,
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
return roots;
|
|
527
|
+
};
|
|
528
|
+
const serializeLiteralOperand = (operand) => {
|
|
529
|
+
if (operand.valueType === "null") {
|
|
530
|
+
return "null";
|
|
531
|
+
}
|
|
532
|
+
if (operand.valueType === "boolean") {
|
|
533
|
+
return operand.value ? "true" : "false";
|
|
534
|
+
}
|
|
535
|
+
if (operand.valueType === "number") {
|
|
536
|
+
return String(typeof operand.value === "number" ? operand.value : 0);
|
|
537
|
+
}
|
|
538
|
+
if (operand.valueType === "array") {
|
|
539
|
+
try {
|
|
540
|
+
return JSON.stringify(Array.isArray(operand.value) ? operand.value : []);
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
return "[]";
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return JSON.stringify(String(operand.value ?? ""));
|
|
547
|
+
};
|
|
548
|
+
const serializeOperandForExpression = (operand) => operand.mode === "runtime"
|
|
549
|
+
? `{{${String(operand.value ?? "").trim()}}}`
|
|
550
|
+
: serializeLiteralOperand(operand);
|
|
551
|
+
const serializeOperandForPreview = (operand) => operand.mode === "runtime"
|
|
552
|
+
? String(operand.value ?? "").trim() || "runtime.path"
|
|
553
|
+
: serializeLiteralOperand(operand);
|
|
554
|
+
const tokenizeConditionExpression = (expression) => {
|
|
555
|
+
const tokens = [];
|
|
556
|
+
let index = 0;
|
|
557
|
+
const pushToken = (type, value, position = index) => {
|
|
558
|
+
tokens.push({
|
|
559
|
+
type,
|
|
560
|
+
value,
|
|
561
|
+
position,
|
|
562
|
+
});
|
|
563
|
+
};
|
|
564
|
+
while (index < expression.length) {
|
|
565
|
+
const remaining = expression.slice(index);
|
|
566
|
+
if (/^\s+/.test(remaining)) {
|
|
567
|
+
index += remaining.match(/^\s+/)?.[0].length ?? 1;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (remaining.startsWith("{{")) {
|
|
571
|
+
const closingIndex = expression.indexOf("}}", index + 2);
|
|
572
|
+
if (closingIndex === -1) {
|
|
573
|
+
throw new Error(`Unclosed runtime token at character ${index + 1}.`);
|
|
574
|
+
}
|
|
575
|
+
pushToken("runtime", expression.slice(index + 2, closingIndex).trim(), index);
|
|
576
|
+
index = closingIndex + 2;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (remaining[0] === "(" || remaining[0] === ")") {
|
|
580
|
+
pushToken("paren", remaining[0] ?? "", index);
|
|
581
|
+
index += 1;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const logicalMatch = LOGICAL_OPERATOR_ALIAS_MAP.find(({ alias }) => matchesAliasAtStart(remaining, alias));
|
|
585
|
+
if (logicalMatch) {
|
|
586
|
+
pushToken("logic", logicalMatch.operator, index);
|
|
587
|
+
index += logicalMatch.alias.length;
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const operatorMatch = OPERATOR_ALIAS_MAP.find(({ alias }) => matchesAliasAtStart(remaining, alias));
|
|
591
|
+
if (operatorMatch) {
|
|
592
|
+
pushToken("operator", operatorMatch.operator, index);
|
|
593
|
+
index += operatorMatch.alias.length;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (remaining[0] === `"` || remaining[0] === `'`) {
|
|
597
|
+
const quote = remaining[0];
|
|
598
|
+
let cursor = index + 1;
|
|
599
|
+
let escaped = false;
|
|
600
|
+
while (cursor < expression.length) {
|
|
601
|
+
const character = expression[cursor] ?? "";
|
|
602
|
+
if (character === "\\" && !escaped) {
|
|
603
|
+
escaped = true;
|
|
604
|
+
cursor += 1;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (character === quote && !escaped) {
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
escaped = false;
|
|
611
|
+
cursor += 1;
|
|
612
|
+
}
|
|
613
|
+
if (expression[cursor] !== quote) {
|
|
614
|
+
throw new Error(`Unclosed string literal at character ${index + 1}.`);
|
|
615
|
+
}
|
|
616
|
+
pushToken("string", expression.slice(index, cursor + 1), index);
|
|
617
|
+
index = cursor + 1;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (remaining[0] === "[") {
|
|
621
|
+
let depth = 0;
|
|
622
|
+
let cursor = index;
|
|
623
|
+
let activeQuote = null;
|
|
624
|
+
let escaped = false;
|
|
625
|
+
while (cursor < expression.length) {
|
|
626
|
+
const character = expression[cursor] ?? "";
|
|
627
|
+
if (activeQuote) {
|
|
628
|
+
if (character === "\\" && !escaped) {
|
|
629
|
+
escaped = true;
|
|
630
|
+
cursor += 1;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (character === activeQuote && !escaped) {
|
|
634
|
+
activeQuote = null;
|
|
635
|
+
}
|
|
636
|
+
escaped = false;
|
|
637
|
+
cursor += 1;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (character === `"` || character === `'`) {
|
|
641
|
+
activeQuote = character;
|
|
642
|
+
cursor += 1;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (character === "[") {
|
|
646
|
+
depth += 1;
|
|
647
|
+
}
|
|
648
|
+
if (character === "]") {
|
|
649
|
+
depth -= 1;
|
|
650
|
+
if (depth === 0) {
|
|
651
|
+
cursor += 1;
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
cursor += 1;
|
|
656
|
+
}
|
|
657
|
+
if (depth !== 0) {
|
|
658
|
+
throw new Error(`Unclosed array literal at character ${index + 1}.`);
|
|
659
|
+
}
|
|
660
|
+
pushToken("array", expression.slice(index, cursor), index);
|
|
661
|
+
index = cursor;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const numberMatch = remaining.match(/^-?\d+(?:\.\d+)?/);
|
|
665
|
+
if (numberMatch) {
|
|
666
|
+
pushToken("number", numberMatch[0], index);
|
|
667
|
+
index += numberMatch[0].length;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const booleanOrNullMatch = remaining.match(/^(true|false|null)\b/i);
|
|
671
|
+
if (booleanOrNullMatch) {
|
|
672
|
+
const matched = booleanOrNullMatch[1]?.toLowerCase() ?? "";
|
|
673
|
+
pushToken(matched === "null" ? "null" : "boolean", matched, index);
|
|
674
|
+
index += booleanOrNullMatch[0].length;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
throw new Error(`Unexpected token near "${remaining.slice(0, 20)}" at character ${index + 1}.`);
|
|
678
|
+
}
|
|
679
|
+
return tokens;
|
|
680
|
+
};
|
|
681
|
+
const parseCursorToken = (cursor) => cursor.tokens[cursor.index] ?? null;
|
|
682
|
+
const consumeCursorToken = (cursor, expectedType) => {
|
|
683
|
+
const token = parseCursorToken(cursor);
|
|
684
|
+
if (!token) {
|
|
685
|
+
throw new Error("Unexpected end of expression.");
|
|
686
|
+
}
|
|
687
|
+
if (expectedType && token.type !== expectedType) {
|
|
688
|
+
throw new Error(`Expected ${expectedType} at character ${token.position + 1}.`);
|
|
689
|
+
}
|
|
690
|
+
cursor.index += 1;
|
|
691
|
+
return token;
|
|
692
|
+
};
|
|
693
|
+
const mergeGroupChildren = (operator, children) => ({
|
|
694
|
+
id: createId("condition-group"),
|
|
695
|
+
type: "group",
|
|
696
|
+
operator,
|
|
697
|
+
children: children.flatMap((child) => child.type === "group" && child.operator === operator ? child.children : [child]),
|
|
698
|
+
});
|
|
699
|
+
const parseOperandToken = (token) => {
|
|
700
|
+
if (token.type === "runtime") {
|
|
701
|
+
return createConditionOperand("runtime", "string", token.value.trim());
|
|
702
|
+
}
|
|
703
|
+
if (token.type === "number") {
|
|
704
|
+
return createConditionOperand("literal", "number", Number(token.value));
|
|
705
|
+
}
|
|
706
|
+
if (token.type === "boolean") {
|
|
707
|
+
return createConditionOperand("literal", "boolean", token.value === "true");
|
|
708
|
+
}
|
|
709
|
+
if (token.type === "null") {
|
|
710
|
+
return createConditionOperand("literal", "null", null);
|
|
711
|
+
}
|
|
712
|
+
if (token.type === "array") {
|
|
713
|
+
try {
|
|
714
|
+
return createConditionOperand("literal", "array", JSON.parse(token.value.replace(/'/g, `"`)));
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
throw new Error(`Invalid array literal at character ${token.position + 1}.`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (token.type === "string") {
|
|
721
|
+
try {
|
|
722
|
+
const normalizedToken = token.value.startsWith("'") && token.value.endsWith("'")
|
|
723
|
+
? `"${token.value.slice(1, -1).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
|
|
724
|
+
: token.value;
|
|
725
|
+
return createConditionOperand("literal", "string", JSON.parse(normalizedToken));
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
return createConditionOperand("literal", "string", token.value.slice(1, -1));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
throw new Error(`Expected operand at character ${token.position + 1}.`);
|
|
732
|
+
};
|
|
733
|
+
const parseConditionRule = (cursor) => {
|
|
734
|
+
const leftToken = consumeCursorToken(cursor);
|
|
735
|
+
if (leftToken.type !== "runtime" &&
|
|
736
|
+
leftToken.type !== "number" &&
|
|
737
|
+
leftToken.type !== "boolean" &&
|
|
738
|
+
leftToken.type !== "null" &&
|
|
739
|
+
leftToken.type !== "array" &&
|
|
740
|
+
leftToken.type !== "string") {
|
|
741
|
+
throw new Error(`Expected operand at character ${leftToken.position + 1}.`);
|
|
742
|
+
}
|
|
743
|
+
const operatorToken = consumeCursorToken(cursor, "operator");
|
|
744
|
+
const operator = operatorToken.value;
|
|
745
|
+
if (isUnaryOperator(operator)) {
|
|
746
|
+
return {
|
|
747
|
+
id: createId("condition-rule"),
|
|
748
|
+
type: "rule",
|
|
749
|
+
left: parseOperandToken(leftToken),
|
|
750
|
+
operator,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
const rightToken = consumeCursorToken(cursor);
|
|
754
|
+
if (rightToken.type !== "runtime" &&
|
|
755
|
+
rightToken.type !== "number" &&
|
|
756
|
+
rightToken.type !== "boolean" &&
|
|
757
|
+
rightToken.type !== "null" &&
|
|
758
|
+
rightToken.type !== "array" &&
|
|
759
|
+
rightToken.type !== "string") {
|
|
760
|
+
throw new Error(`Expected right operand at character ${rightToken.position + 1}.`);
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
id: createId("condition-rule"),
|
|
764
|
+
type: "rule",
|
|
765
|
+
left: parseOperandToken(leftToken),
|
|
766
|
+
operator,
|
|
767
|
+
right: parseOperandToken(rightToken),
|
|
768
|
+
};
|
|
769
|
+
};
|
|
770
|
+
const parsePrimaryGroupNode = (cursor) => {
|
|
771
|
+
const token = parseCursorToken(cursor);
|
|
772
|
+
if (!token) {
|
|
773
|
+
throw new Error("Unexpected end of expression.");
|
|
774
|
+
}
|
|
775
|
+
if (token.type === "paren" && token.value === "(") {
|
|
776
|
+
consumeCursorToken(cursor, "paren");
|
|
777
|
+
const nested = parseOrExpression(cursor);
|
|
778
|
+
const closingToken = consumeCursorToken(cursor, "paren");
|
|
779
|
+
if (closingToken.value !== ")") {
|
|
780
|
+
throw new Error(`Expected closing parenthesis at character ${closingToken.position + 1}.`);
|
|
781
|
+
}
|
|
782
|
+
return nested;
|
|
783
|
+
}
|
|
784
|
+
return parseConditionRule(cursor);
|
|
785
|
+
};
|
|
786
|
+
const parseAndExpression = (cursor) => {
|
|
787
|
+
const children = [parsePrimaryGroupNode(cursor)];
|
|
788
|
+
while (parseCursorToken(cursor)?.type === "logic" && parseCursorToken(cursor)?.value === "AND") {
|
|
789
|
+
consumeCursorToken(cursor, "logic");
|
|
790
|
+
children.push(parsePrimaryGroupNode(cursor));
|
|
791
|
+
}
|
|
792
|
+
return children.length === 1 ? children[0] : mergeGroupChildren("AND", children);
|
|
793
|
+
};
|
|
794
|
+
const parseOrExpression = (cursor) => {
|
|
795
|
+
const children = [parseAndExpression(cursor)];
|
|
796
|
+
while (parseCursorToken(cursor)?.type === "logic" && parseCursorToken(cursor)?.value === "OR") {
|
|
797
|
+
consumeCursorToken(cursor, "logic");
|
|
798
|
+
children.push(parseAndExpression(cursor));
|
|
799
|
+
}
|
|
800
|
+
return children.length === 1 ? children[0] : mergeGroupChildren("OR", children);
|
|
801
|
+
};
|
|
802
|
+
const ensureGroupRoot = (node) => node.type === "group"
|
|
803
|
+
? node
|
|
804
|
+
: {
|
|
805
|
+
id: createId("condition-root"),
|
|
806
|
+
type: "group",
|
|
807
|
+
operator: "AND",
|
|
808
|
+
children: [node],
|
|
809
|
+
};
|
|
810
|
+
const serializeRuleExpression = (rule, humanReadable) => {
|
|
811
|
+
const left = humanReadable
|
|
812
|
+
? serializeOperandForPreview(rule.left)
|
|
813
|
+
: serializeOperandForExpression(rule.left);
|
|
814
|
+
const operator = getOperatorToken(rule.operator);
|
|
815
|
+
if (isUnaryOperator(rule.operator)) {
|
|
816
|
+
return `${left} ${operator}`;
|
|
817
|
+
}
|
|
818
|
+
const rightOperand = rule.right ?? getDefaultRightOperand(rule.operator);
|
|
819
|
+
const right = humanReadable
|
|
820
|
+
? serializeOperandForPreview(rightOperand ?? createConditionOperand("literal", "string", ""))
|
|
821
|
+
: serializeOperandForExpression(rightOperand ?? createConditionOperand("literal", "string", ""));
|
|
822
|
+
return `${left} ${operator} ${right}`;
|
|
823
|
+
};
|
|
824
|
+
const serializeGroupExpression = (group, humanReadable, nested = false) => {
|
|
825
|
+
if (group.children.length === 0) {
|
|
826
|
+
return humanReadable ? "Empty group" : "()";
|
|
827
|
+
}
|
|
828
|
+
const renderedChildren = group.children.map((child) => child.type === "group"
|
|
829
|
+
? serializeGroupExpression(child, humanReadable, true)
|
|
830
|
+
: serializeRuleExpression(child, humanReadable));
|
|
831
|
+
const joined = renderedChildren.join(` ${group.operator} `);
|
|
832
|
+
if (!nested && group.children.length === 1) {
|
|
833
|
+
return joined;
|
|
834
|
+
}
|
|
835
|
+
return `(${joined})`;
|
|
836
|
+
};
|
|
837
|
+
const collectConditionRuleCount = (group) => group.children.reduce((count, child) => count + (child.type === "rule" ? 1 : collectConditionRuleCount(child)), 0);
|
|
838
|
+
const collectConditionGroupCount = (group) => 1 +
|
|
839
|
+
group.children.reduce((count, child) => count + (child.type === "group" ? collectConditionGroupCount(child) : 0), 0);
|
|
840
|
+
const cloneConditionOperand = (operand) => ({
|
|
841
|
+
mode: operand.mode,
|
|
842
|
+
valueType: operand.valueType,
|
|
843
|
+
value: Array.isArray(operand.value) ? [...operand.value] : operand.value,
|
|
844
|
+
});
|
|
845
|
+
const sanitizeConditionOperand = (value, fallbackMode = "literal") => {
|
|
846
|
+
if (!isRecord(value)) {
|
|
847
|
+
return createConditionOperand(fallbackMode);
|
|
848
|
+
}
|
|
849
|
+
const mode = value.mode === "runtime" ? "runtime" : "literal";
|
|
850
|
+
const rawValueType = String(value.valueType ?? "");
|
|
851
|
+
const valueType = rawValueType === "number" ||
|
|
852
|
+
rawValueType === "boolean" ||
|
|
853
|
+
rawValueType === "null" ||
|
|
854
|
+
rawValueType === "array" ||
|
|
855
|
+
rawValueType === "string"
|
|
856
|
+
? rawValueType
|
|
857
|
+
: mode === "runtime"
|
|
858
|
+
? "string"
|
|
859
|
+
: "string";
|
|
860
|
+
return {
|
|
861
|
+
mode,
|
|
862
|
+
valueType,
|
|
863
|
+
value: mode === "runtime"
|
|
864
|
+
? String(value.value ?? "").trim()
|
|
865
|
+
: parseLiteralValue(value.value, valueType),
|
|
866
|
+
};
|
|
867
|
+
};
|
|
868
|
+
const sanitizeConditionRule = (value) => {
|
|
869
|
+
if (!isRecord(value) || value.type !== "rule") {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
const rawOperator = String(value.operator ?? "");
|
|
873
|
+
const operator = CONDITION_OPERATOR_META.find((entry) => entry.value === rawOperator)?.value;
|
|
874
|
+
if (!operator) {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
return {
|
|
878
|
+
id: typeof value.id === "string" && value.id.trim() ? value.id : createId("condition-rule"),
|
|
879
|
+
type: "rule",
|
|
880
|
+
left: sanitizeConditionOperand(value.left, "runtime"),
|
|
881
|
+
operator,
|
|
882
|
+
...(isUnaryOperator(operator)
|
|
883
|
+
? {}
|
|
884
|
+
: {
|
|
885
|
+
right: sanitizeConditionOperand(value.right ?? getDefaultRightOperand(operator), "literal"),
|
|
886
|
+
}),
|
|
887
|
+
};
|
|
888
|
+
};
|
|
889
|
+
const sanitizeConditionGroup = (value) => {
|
|
890
|
+
if (!isRecord(value) || value.type !== "group") {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
const operator = value.operator === "OR" ? "OR" : "AND";
|
|
894
|
+
const children = Array.isArray(value.children)
|
|
895
|
+
? value.children
|
|
896
|
+
.map((child) => sanitizeConditionGroup(child) ?? sanitizeConditionRule(child))
|
|
897
|
+
.filter((child) => child !== null)
|
|
898
|
+
: [];
|
|
899
|
+
return {
|
|
900
|
+
id: typeof value.id === "string" && value.id.trim() ? value.id : createId("condition-group"),
|
|
901
|
+
type: "group",
|
|
902
|
+
operator,
|
|
903
|
+
children,
|
|
904
|
+
};
|
|
905
|
+
};
|
|
906
|
+
const buildValidationIssue = (id, message, severity) => ({
|
|
907
|
+
id,
|
|
908
|
+
message,
|
|
909
|
+
severity,
|
|
910
|
+
});
|
|
911
|
+
const validateOperand = (operand, id, issues, runtimeRootKeys) => {
|
|
912
|
+
if (!operand) {
|
|
913
|
+
issues.push(buildValidationIssue(id, "Operand is missing.", "error"));
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (operand.mode === "runtime") {
|
|
917
|
+
const path = String(operand.value ?? "").trim();
|
|
918
|
+
if (!path) {
|
|
919
|
+
issues.push(buildValidationIssue(id, "Runtime path is required.", "error"));
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const root = parseRuntimePathSegments(path)[0];
|
|
923
|
+
if (runtimeRootKeys &&
|
|
924
|
+
typeof root === "string" &&
|
|
925
|
+
root.trim() &&
|
|
926
|
+
!runtimeRootKeys.has(root)) {
|
|
927
|
+
issues.push(buildValidationIssue(id, `Runtime root "${root}" is not available in this workflow context.`, "warning"));
|
|
928
|
+
}
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
if (operand.valueType === "array") {
|
|
932
|
+
if (!Array.isArray(operand.value)) {
|
|
933
|
+
issues.push(buildValidationIssue(id, "Array operand must be a JSON array.", "error"));
|
|
934
|
+
}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (operand.valueType === "number" && coerceUnknownToNumber(operand.value) === null) {
|
|
938
|
+
issues.push(buildValidationIssue(id, "Number operand must be numeric.", "error"));
|
|
939
|
+
}
|
|
940
|
+
if (operand.valueType === "boolean" && coerceUnknownToBoolean(operand.value) === null) {
|
|
941
|
+
issues.push(buildValidationIssue(id, "Boolean operand must be true or false.", "error"));
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
const validateRule = (rule, issues, runtimeRootKeys) => {
|
|
945
|
+
validateOperand(rule.left, `${rule.id}:left`, issues, runtimeRootKeys);
|
|
946
|
+
if (!isUnaryOperator(rule.operator)) {
|
|
947
|
+
validateOperand(rule.right, `${rule.id}:right`, issues, runtimeRootKeys);
|
|
948
|
+
}
|
|
949
|
+
if (rule.operator === "between") {
|
|
950
|
+
if (!rule.right || rule.right.valueType !== "array" || !Array.isArray(rule.right.value)) {
|
|
951
|
+
issues.push(buildValidationIssue(rule.id, "Between expects an array literal with exactly two numeric values.", "error"));
|
|
952
|
+
}
|
|
953
|
+
else if (rule.right.value.length !== 2 ||
|
|
954
|
+
rule.right.value.some((value) => coerceUnknownToNumber(value) === null)) {
|
|
955
|
+
issues.push(buildValidationIssue(rule.id, "Between expects two numeric boundary values.", "error"));
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (isCollectionOperator(rule.operator)) {
|
|
959
|
+
if (!rule.right || rule.right.valueType !== "array" || !Array.isArray(rule.right.value)) {
|
|
960
|
+
issues.push(buildValidationIssue(rule.id, "In operators require an array on the right side.", "error"));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (rule.operator === "regex" && rule.right?.mode === "literal") {
|
|
964
|
+
try {
|
|
965
|
+
buildConditionRegex(String(rule.right.value ?? ""));
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
issues.push(buildValidationIssue(rule.id, error instanceof Error ? error.message : "Regex pattern is invalid.", "error"));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (isNumericOperator(rule.operator)) {
|
|
972
|
+
if (rule.left.valueType !== "number") {
|
|
973
|
+
issues.push(buildValidationIssue(`${rule.id}:numeric-left`, "Left operand should use Number type for numeric comparisons.", "warning"));
|
|
974
|
+
}
|
|
975
|
+
if (rule.operator !== "between" &&
|
|
976
|
+
rule.right &&
|
|
977
|
+
rule.right.valueType !== "number" &&
|
|
978
|
+
rule.right.mode !== "runtime") {
|
|
979
|
+
issues.push(buildValidationIssue(`${rule.id}:numeric-right`, "Right operand should use Number type for numeric comparisons.", "warning"));
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
const validateGroup = (group, issues, runtimeRootKeys) => {
|
|
984
|
+
if (group.children.length === 0) {
|
|
985
|
+
issues.push(buildValidationIssue(group.id, "Group must contain at least one rule.", "error"));
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
group.children.forEach((child) => {
|
|
989
|
+
if (child.type === "group") {
|
|
990
|
+
validateGroup(child, issues, runtimeRootKeys);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
validateRule(child, issues, runtimeRootKeys);
|
|
994
|
+
});
|
|
995
|
+
};
|
|
996
|
+
const buildConditionRegex = (value) => {
|
|
997
|
+
const trimmed = value.trim();
|
|
998
|
+
const literalMatch = trimmed.match(/^\/(.+)\/([a-z]*)$/i);
|
|
999
|
+
if (literalMatch) {
|
|
1000
|
+
return new RegExp(literalMatch[1] ?? "", literalMatch[2] ?? "");
|
|
1001
|
+
}
|
|
1002
|
+
return new RegExp(trimmed);
|
|
1003
|
+
};
|
|
1004
|
+
const compareLooseEquality = (left, right) => {
|
|
1005
|
+
if (Array.isArray(left) || Array.isArray(right) || isRecord(left) || isRecord(right)) {
|
|
1006
|
+
try {
|
|
1007
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1008
|
+
}
|
|
1009
|
+
catch {
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (left === right) {
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
if (typeof left === "string" && typeof right !== "string") {
|
|
1017
|
+
const coercedLeft = coerceOperandValue(left, typeof right === "number" ? "number" : typeof right === "boolean" ? "boolean" : right === null ? "null" : "string");
|
|
1018
|
+
return typeof coercedLeft === "undefined" ? false : compareLooseEquality(coercedLeft, right);
|
|
1019
|
+
}
|
|
1020
|
+
if (typeof right === "string" && typeof left !== "string") {
|
|
1021
|
+
const coercedRight = coerceOperandValue(right, typeof left === "number" ? "number" : typeof left === "boolean" ? "boolean" : left === null ? "null" : "string");
|
|
1022
|
+
return typeof coercedRight === "undefined" ? false : compareLooseEquality(left, coercedRight);
|
|
1023
|
+
}
|
|
1024
|
+
return formatDisplayValue(left) === formatDisplayValue(right);
|
|
1025
|
+
};
|
|
1026
|
+
const resolveOperand = (operand, runtimeScope) => {
|
|
1027
|
+
if (operand.mode === "literal") {
|
|
1028
|
+
const literalValue = parseLiteralValue(operand.value, operand.valueType);
|
|
1029
|
+
return {
|
|
1030
|
+
displayValue: formatDisplayValue(literalValue),
|
|
1031
|
+
exists: true,
|
|
1032
|
+
operand,
|
|
1033
|
+
path: null,
|
|
1034
|
+
value: literalValue,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
const path = String(operand.value ?? "").trim();
|
|
1038
|
+
const resolution = resolveRuntimePath(createRuntimeRoots(runtimeScope), path);
|
|
1039
|
+
const coercedValue = typeof resolution.value === "undefined"
|
|
1040
|
+
? undefined
|
|
1041
|
+
: coerceOperandValue(resolution.value, operand.valueType);
|
|
1042
|
+
return {
|
|
1043
|
+
displayValue: formatDisplayValue(typeof coercedValue === "undefined" ? resolution.value : coercedValue),
|
|
1044
|
+
exists: resolution.exists,
|
|
1045
|
+
operand,
|
|
1046
|
+
path,
|
|
1047
|
+
value: typeof coercedValue === "undefined" ? resolution.value : coercedValue,
|
|
1048
|
+
};
|
|
1049
|
+
};
|
|
1050
|
+
const evaluateRule = (rule, runtimeScope) => {
|
|
1051
|
+
const left = resolveOperand(rule.left, runtimeScope);
|
|
1052
|
+
const right = rule.right ? resolveOperand(rule.right, runtimeScope) : undefined;
|
|
1053
|
+
let result = false;
|
|
1054
|
+
switch (rule.operator) {
|
|
1055
|
+
case "equals":
|
|
1056
|
+
result = compareLooseEquality(left.value, right?.value);
|
|
1057
|
+
break;
|
|
1058
|
+
case "not_equals":
|
|
1059
|
+
result = !compareLooseEquality(left.value, right?.value);
|
|
1060
|
+
break;
|
|
1061
|
+
case "strict_equals":
|
|
1062
|
+
result = left.value === right?.value;
|
|
1063
|
+
break;
|
|
1064
|
+
case "strict_not_equals":
|
|
1065
|
+
result = left.value !== right?.value;
|
|
1066
|
+
break;
|
|
1067
|
+
case "greater_than":
|
|
1068
|
+
result =
|
|
1069
|
+
coerceUnknownToNumber(left.value) !== null &&
|
|
1070
|
+
coerceUnknownToNumber(right?.value) !== null &&
|
|
1071
|
+
(coerceUnknownToNumber(left.value) ?? 0) > (coerceUnknownToNumber(right?.value) ?? 0);
|
|
1072
|
+
break;
|
|
1073
|
+
case "less_than":
|
|
1074
|
+
result =
|
|
1075
|
+
coerceUnknownToNumber(left.value) !== null &&
|
|
1076
|
+
coerceUnknownToNumber(right?.value) !== null &&
|
|
1077
|
+
(coerceUnknownToNumber(left.value) ?? 0) < (coerceUnknownToNumber(right?.value) ?? 0);
|
|
1078
|
+
break;
|
|
1079
|
+
case "greater_or_equal":
|
|
1080
|
+
result =
|
|
1081
|
+
coerceUnknownToNumber(left.value) !== null &&
|
|
1082
|
+
coerceUnknownToNumber(right?.value) !== null &&
|
|
1083
|
+
(coerceUnknownToNumber(left.value) ?? 0) >= (coerceUnknownToNumber(right?.value) ?? 0);
|
|
1084
|
+
break;
|
|
1085
|
+
case "less_or_equal":
|
|
1086
|
+
result =
|
|
1087
|
+
coerceUnknownToNumber(left.value) !== null &&
|
|
1088
|
+
coerceUnknownToNumber(right?.value) !== null &&
|
|
1089
|
+
(coerceUnknownToNumber(left.value) ?? 0) <= (coerceUnknownToNumber(right?.value) ?? 0);
|
|
1090
|
+
break;
|
|
1091
|
+
case "between": {
|
|
1092
|
+
const range = Array.isArray(right?.value) ? right?.value : [];
|
|
1093
|
+
const leftNumber = coerceUnknownToNumber(left.value);
|
|
1094
|
+
const lowerBound = coerceUnknownToNumber(range[0]);
|
|
1095
|
+
const upperBound = coerceUnknownToNumber(range[1]);
|
|
1096
|
+
result =
|
|
1097
|
+
leftNumber !== null &&
|
|
1098
|
+
lowerBound !== null &&
|
|
1099
|
+
upperBound !== null &&
|
|
1100
|
+
leftNumber >= lowerBound &&
|
|
1101
|
+
leftNumber <= upperBound;
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
case "contains":
|
|
1105
|
+
result = Array.isArray(left.value)
|
|
1106
|
+
? left.value.some((entry) => compareLooseEquality(entry, right?.value))
|
|
1107
|
+
: formatDisplayValue(left.value).includes(formatDisplayValue(right?.value));
|
|
1108
|
+
break;
|
|
1109
|
+
case "not_contains":
|
|
1110
|
+
result = Array.isArray(left.value)
|
|
1111
|
+
? !left.value.some((entry) => compareLooseEquality(entry, right?.value))
|
|
1112
|
+
: !formatDisplayValue(left.value).includes(formatDisplayValue(right?.value));
|
|
1113
|
+
break;
|
|
1114
|
+
case "starts_with":
|
|
1115
|
+
result = formatDisplayValue(left.value).startsWith(formatDisplayValue(right?.value));
|
|
1116
|
+
break;
|
|
1117
|
+
case "ends_with":
|
|
1118
|
+
result = formatDisplayValue(left.value).endsWith(formatDisplayValue(right?.value));
|
|
1119
|
+
break;
|
|
1120
|
+
case "regex":
|
|
1121
|
+
result = buildConditionRegex(formatDisplayValue(right?.value)).test(formatDisplayValue(left.value));
|
|
1122
|
+
break;
|
|
1123
|
+
case "exists":
|
|
1124
|
+
result = left.exists;
|
|
1125
|
+
break;
|
|
1126
|
+
case "not_exists":
|
|
1127
|
+
result = !left.exists;
|
|
1128
|
+
break;
|
|
1129
|
+
case "is_empty":
|
|
1130
|
+
result = left.exists
|
|
1131
|
+
? left.value === "" ||
|
|
1132
|
+
(Array.isArray(left.value) && left.value.length === 0) ||
|
|
1133
|
+
(isRecord(left.value) && Object.keys(left.value).length === 0)
|
|
1134
|
+
: false;
|
|
1135
|
+
break;
|
|
1136
|
+
case "not_empty":
|
|
1137
|
+
result = left.exists
|
|
1138
|
+
? !(left.value === "" ||
|
|
1139
|
+
(Array.isArray(left.value) && left.value.length === 0) ||
|
|
1140
|
+
(isRecord(left.value) && Object.keys(left.value).length === 0))
|
|
1141
|
+
: false;
|
|
1142
|
+
break;
|
|
1143
|
+
case "is_null":
|
|
1144
|
+
result = left.value === null;
|
|
1145
|
+
break;
|
|
1146
|
+
case "not_null":
|
|
1147
|
+
result = left.value !== null;
|
|
1148
|
+
break;
|
|
1149
|
+
case "in":
|
|
1150
|
+
result = Array.isArray(right?.value)
|
|
1151
|
+
? right.value.some((entry) => compareLooseEquality(left.value, entry))
|
|
1152
|
+
: false;
|
|
1153
|
+
break;
|
|
1154
|
+
case "not_in":
|
|
1155
|
+
result = Array.isArray(right?.value)
|
|
1156
|
+
? !right.value.some((entry) => compareLooseEquality(left.value, entry))
|
|
1157
|
+
: false;
|
|
1158
|
+
break;
|
|
1159
|
+
default:
|
|
1160
|
+
result = false;
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
id: rule.id,
|
|
1165
|
+
type: "rule",
|
|
1166
|
+
operator: rule.operator,
|
|
1167
|
+
result,
|
|
1168
|
+
left,
|
|
1169
|
+
...(right ? { right } : {}),
|
|
1170
|
+
};
|
|
1171
|
+
};
|
|
1172
|
+
const evaluateGroup = (group, runtimeScope) => {
|
|
1173
|
+
const children = group.children.map((child) => child.type === "group"
|
|
1174
|
+
? evaluateGroup(child, runtimeScope)
|
|
1175
|
+
: evaluateRule(child, runtimeScope));
|
|
1176
|
+
return {
|
|
1177
|
+
id: group.id,
|
|
1178
|
+
type: "group",
|
|
1179
|
+
operator: group.operator,
|
|
1180
|
+
result: group.operator === "AND"
|
|
1181
|
+
? children.every((child) => child.result)
|
|
1182
|
+
: children.some((child) => child.result),
|
|
1183
|
+
children,
|
|
1184
|
+
};
|
|
1185
|
+
};
|
|
1186
|
+
const replaceChildInGroup = (group, childId, nextChild) => ({
|
|
1187
|
+
...group,
|
|
1188
|
+
children: group.children.map((child) => {
|
|
1189
|
+
if (child.id === childId) {
|
|
1190
|
+
return nextChild;
|
|
1191
|
+
}
|
|
1192
|
+
if (child.type === "group") {
|
|
1193
|
+
return replaceChildInGroup(child, childId, nextChild);
|
|
1194
|
+
}
|
|
1195
|
+
return child;
|
|
1196
|
+
}),
|
|
1197
|
+
});
|
|
1198
|
+
const appendChildToConditionGroup = (group, groupId, nextChild) => ({
|
|
1199
|
+
...group,
|
|
1200
|
+
children: group.id === groupId
|
|
1201
|
+
? [...group.children, nextChild]
|
|
1202
|
+
: group.children.map((child) => child.type === "group"
|
|
1203
|
+
? appendChildToConditionGroup(child, groupId, nextChild)
|
|
1204
|
+
: child),
|
|
1205
|
+
});
|
|
1206
|
+
const removeChildFromConditionGroup = (group, groupId, childId) => ({
|
|
1207
|
+
...group,
|
|
1208
|
+
children: group.id === groupId
|
|
1209
|
+
? group.children.filter((child) => child.id !== childId)
|
|
1210
|
+
: group.children.map((child) => child.type === "group"
|
|
1211
|
+
? removeChildFromConditionGroup(child, groupId, childId)
|
|
1212
|
+
: child),
|
|
1213
|
+
});
|
|
1214
|
+
const updateConditionGroup = (group, groupId, updater) => {
|
|
1215
|
+
if (group.id === groupId) {
|
|
1216
|
+
return updater(group);
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
...group,
|
|
1220
|
+
children: group.children.map((child) => child.type === "group" ? updateConditionGroup(child, groupId, updater) : child),
|
|
1221
|
+
};
|
|
1222
|
+
};
|
|
1223
|
+
const updateConditionRule = (group, ruleId, updater) => ({
|
|
1224
|
+
...group,
|
|
1225
|
+
children: group.children.map((child) => {
|
|
1226
|
+
if (child.type === "rule") {
|
|
1227
|
+
return child.id === ruleId ? updater(child) : child;
|
|
1228
|
+
}
|
|
1229
|
+
return updateConditionRule(child, ruleId, updater);
|
|
1230
|
+
}),
|
|
1231
|
+
});
|
|
1232
|
+
const getConditionQuickSummaryParts = (config) => {
|
|
1233
|
+
const ruleCount = collectConditionRuleCount(config.rootGroup);
|
|
1234
|
+
const groupCount = collectConditionGroupCount(config.rootGroup);
|
|
1235
|
+
return [
|
|
1236
|
+
config.mode === "expert" ? "Expert" : "Builder",
|
|
1237
|
+
`${ruleCount} rule${ruleCount === 1 ? "" : "s"}`,
|
|
1238
|
+
`${groupCount} group${groupCount === 1 ? "" : "s"}`,
|
|
1239
|
+
];
|
|
1240
|
+
};
|
|
1241
|
+
export const CONDITION_VALUE_TYPES = [
|
|
1242
|
+
"string",
|
|
1243
|
+
"number",
|
|
1244
|
+
"boolean",
|
|
1245
|
+
"null",
|
|
1246
|
+
"array",
|
|
1247
|
+
];
|
|
1248
|
+
export const createConditionOperand = (mode = "runtime", valueType = mode === "runtime" ? "string" : "string", value) => ({
|
|
1249
|
+
mode,
|
|
1250
|
+
valueType,
|
|
1251
|
+
value: typeof value !== "undefined"
|
|
1252
|
+
? mode === "runtime"
|
|
1253
|
+
? String(value ?? "").trim()
|
|
1254
|
+
: parseLiteralValue(value, valueType)
|
|
1255
|
+
: mode === "runtime"
|
|
1256
|
+
? ""
|
|
1257
|
+
: getDefaultLiteralValue(valueType),
|
|
1258
|
+
});
|
|
1259
|
+
export const createConditionRule = (overrides = {}) => {
|
|
1260
|
+
const operator = overrides.operator ?? "equals";
|
|
1261
|
+
return {
|
|
1262
|
+
id: overrides.id ?? createId("condition-rule"),
|
|
1263
|
+
type: "rule",
|
|
1264
|
+
left: overrides.left ? cloneConditionOperand(overrides.left) : createConditionOperand("runtime"),
|
|
1265
|
+
operator,
|
|
1266
|
+
...(isUnaryOperator(operator)
|
|
1267
|
+
? {}
|
|
1268
|
+
: {
|
|
1269
|
+
right: overrides.right
|
|
1270
|
+
? cloneConditionOperand(overrides.right)
|
|
1271
|
+
: getDefaultRightOperand(operator),
|
|
1272
|
+
}),
|
|
1273
|
+
};
|
|
1274
|
+
};
|
|
1275
|
+
export const createConditionGroup = (operator = "AND", children = [createConditionRule()]) => ({
|
|
1276
|
+
id: createId("condition-group"),
|
|
1277
|
+
type: "group",
|
|
1278
|
+
operator,
|
|
1279
|
+
children,
|
|
1280
|
+
});
|
|
1281
|
+
export const createDefaultConditionConfig = () => {
|
|
1282
|
+
const rootGroup = createConditionGroup("AND", [
|
|
1283
|
+
createConditionRule({
|
|
1284
|
+
left: createConditionOperand("runtime", "number", "response.status"),
|
|
1285
|
+
operator: "equals",
|
|
1286
|
+
right: createConditionOperand("literal", "number", 200),
|
|
1287
|
+
}),
|
|
1288
|
+
]);
|
|
1289
|
+
return {
|
|
1290
|
+
mode: "builder",
|
|
1291
|
+
rootGroup,
|
|
1292
|
+
expertExpression: serializeConditionGroupToExpression(rootGroup),
|
|
1293
|
+
};
|
|
1294
|
+
};
|
|
1295
|
+
export const serializeConditionGroupToExpression = (group) => serializeGroupExpression(group, false);
|
|
1296
|
+
export const serializeConditionConfigToExpression = (config) => config.mode === "expert" && config.expertExpression?.trim()
|
|
1297
|
+
? config.expertExpression.trim()
|
|
1298
|
+
: serializeConditionGroupToExpression(config.rootGroup);
|
|
1299
|
+
export const serializeConditionGroupToPreview = (group) => serializeGroupExpression(group, true);
|
|
1300
|
+
export const serializeConditionConfigToPreview = (config) => config.mode === "expert" && config.expertExpression?.trim()
|
|
1301
|
+
? config.expertExpression.trim()
|
|
1302
|
+
: serializeConditionGroupToPreview(config.rootGroup);
|
|
1303
|
+
export const parseConditionExpressionToGroup = (expression) => {
|
|
1304
|
+
const tokens = tokenizeConditionExpression(expression);
|
|
1305
|
+
const cursor = {
|
|
1306
|
+
index: 0,
|
|
1307
|
+
tokens,
|
|
1308
|
+
};
|
|
1309
|
+
const group = ensureGroupRoot(parseOrExpression(cursor));
|
|
1310
|
+
if (parseCursorToken(cursor)) {
|
|
1311
|
+
throw new Error(`Unexpected token at character ${(parseCursorToken(cursor)?.position ?? 0) + 1}.`);
|
|
1312
|
+
}
|
|
1313
|
+
return group;
|
|
1314
|
+
};
|
|
1315
|
+
export const parseConditionExpressionSafely = (expression) => {
|
|
1316
|
+
try {
|
|
1317
|
+
return {
|
|
1318
|
+
group: parseConditionExpressionToGroup(expression),
|
|
1319
|
+
issues: [],
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
catch (error) {
|
|
1323
|
+
return {
|
|
1324
|
+
group: createDefaultConditionConfig().rootGroup,
|
|
1325
|
+
issues: [
|
|
1326
|
+
buildValidationIssue("expert:parse", error instanceof Error ? error.message : "Condition expression is invalid.", "error"),
|
|
1327
|
+
],
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
export const normalizeConditionNodeConfig = (value) => {
|
|
1332
|
+
if (isRecord(value) && typeof value.expression === "string") {
|
|
1333
|
+
const expression = value.expression.trim();
|
|
1334
|
+
if (!expression) {
|
|
1335
|
+
return createDefaultConditionConfig();
|
|
1336
|
+
}
|
|
1337
|
+
try {
|
|
1338
|
+
const rootGroup = parseConditionExpressionToGroup(expression);
|
|
1339
|
+
return {
|
|
1340
|
+
mode: "builder",
|
|
1341
|
+
rootGroup,
|
|
1342
|
+
expertExpression: serializeConditionGroupToExpression(rootGroup),
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
catch {
|
|
1346
|
+
return {
|
|
1347
|
+
mode: "expert",
|
|
1348
|
+
rootGroup: createDefaultConditionConfig().rootGroup,
|
|
1349
|
+
expertExpression: expression,
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (!isRecord(value)) {
|
|
1354
|
+
return createDefaultConditionConfig();
|
|
1355
|
+
}
|
|
1356
|
+
const mode = value.mode === "expert" ? "expert" : "builder";
|
|
1357
|
+
const rootGroup = sanitizeConditionGroup(value.rootGroup) ?? createDefaultConditionConfig().rootGroup;
|
|
1358
|
+
const expertExpression = typeof value.expertExpression === "string" ? value.expertExpression : null;
|
|
1359
|
+
return {
|
|
1360
|
+
mode,
|
|
1361
|
+
rootGroup,
|
|
1362
|
+
expertExpression: expertExpression ??
|
|
1363
|
+
serializeConditionGroupToExpression(rootGroup),
|
|
1364
|
+
};
|
|
1365
|
+
};
|
|
1366
|
+
export const validateConditionConfig = (config, runtimeRootKeys) => {
|
|
1367
|
+
const issues = [];
|
|
1368
|
+
const rootKeySet = runtimeRootKeys ? new Set(runtimeRootKeys) : null;
|
|
1369
|
+
let targetGroup = config.rootGroup;
|
|
1370
|
+
if (config.mode === "expert" && config.expertExpression?.trim()) {
|
|
1371
|
+
const parseResult = parseConditionExpressionSafely(config.expertExpression);
|
|
1372
|
+
issues.push(...parseResult.issues);
|
|
1373
|
+
if (parseResult.issues.some((issue) => issue.severity === "error")) {
|
|
1374
|
+
return issues;
|
|
1375
|
+
}
|
|
1376
|
+
targetGroup = parseResult.group;
|
|
1377
|
+
}
|
|
1378
|
+
validateGroup(targetGroup, issues, rootKeySet);
|
|
1379
|
+
return issues;
|
|
1380
|
+
};
|
|
1381
|
+
export const getConditionRuleCount = (group) => collectConditionRuleCount(group);
|
|
1382
|
+
export const getConditionGroupCount = (group) => collectConditionGroupCount(group);
|
|
1383
|
+
export const getConditionQuickSummary = (config) => getConditionQuickSummaryParts(config).join(" • ");
|
|
1384
|
+
export const getConditionHeadline = (config) => {
|
|
1385
|
+
const ruleCount = collectConditionRuleCount(config.rootGroup);
|
|
1386
|
+
const groupCount = collectConditionGroupCount(config.rootGroup);
|
|
1387
|
+
return `IF (${ruleCount} rule${ruleCount === 1 ? "" : "s"}, ${groupCount} group${groupCount === 1 ? "" : "s"})`;
|
|
1388
|
+
};
|
|
1389
|
+
export const getConditionValueTypeLabel = (valueType) => VALUE_TYPE_LABELS[valueType];
|
|
1390
|
+
export const getExecutableConditionGroup = (config) => {
|
|
1391
|
+
if (config.mode === "expert" && config.expertExpression?.trim()) {
|
|
1392
|
+
return parseConditionExpressionToGroup(config.expertExpression);
|
|
1393
|
+
}
|
|
1394
|
+
return config.rootGroup;
|
|
1395
|
+
};
|
|
1396
|
+
export const evaluateConditionConfig = (config, runtimeScope) => {
|
|
1397
|
+
const executableGroup = getExecutableConditionGroup(config);
|
|
1398
|
+
const tree = evaluateGroup(executableGroup, runtimeScope);
|
|
1399
|
+
return {
|
|
1400
|
+
expression: serializeConditionConfigToExpression(config),
|
|
1401
|
+
result: tree.result,
|
|
1402
|
+
tree,
|
|
1403
|
+
};
|
|
1404
|
+
};
|
|
1405
|
+
export const getConditionRuntimeRootKeys = (config) => new Set(Object.keys(createRuntimeRoots(config)));
|
|
1406
|
+
export const updateConditionGroupOperator = (rootGroup, groupId, operator) => updateConditionGroup(rootGroup, groupId, (group) => ({
|
|
1407
|
+
...group,
|
|
1408
|
+
operator,
|
|
1409
|
+
}));
|
|
1410
|
+
export const appendRuleToConditionGroup = (rootGroup, groupId) => appendChildToConditionGroup(rootGroup, groupId, createConditionRule());
|
|
1411
|
+
export const appendGroupToConditionGroup = (rootGroup, groupId) => appendChildToConditionGroup(rootGroup, groupId, createConditionGroup("AND"));
|
|
1412
|
+
export const removeConditionChild = (rootGroup, groupId, childId) => removeChildFromConditionGroup(rootGroup, groupId, childId);
|
|
1413
|
+
export const replaceConditionGroup = (rootGroup, nextGroup) => replaceChildInGroup(rootGroup, nextGroup.id, nextGroup);
|
|
1414
|
+
export const patchConditionRule = (rootGroup, ruleId, updater) => updateConditionRule(rootGroup, ruleId, updater);
|
|
1415
|
+
export const patchConditionGroup = (rootGroup, groupId, updater) => updateConditionGroup(rootGroup, groupId, updater);
|
|
1416
|
+
export const updateConditionRuleOperator = (rootGroup, ruleId, operator) => updateConditionRule(rootGroup, ruleId, (rule) => ({
|
|
1417
|
+
...rule,
|
|
1418
|
+
operator,
|
|
1419
|
+
...(isUnaryOperator(operator)
|
|
1420
|
+
? { right: undefined }
|
|
1421
|
+
: {
|
|
1422
|
+
right: rule.right &&
|
|
1423
|
+
((operator !== "between" && operator !== "in" && operator !== "not_in") ||
|
|
1424
|
+
rule.right.valueType === "array")
|
|
1425
|
+
? cloneConditionOperand(rule.right)
|
|
1426
|
+
: getDefaultRightOperand(operator),
|
|
1427
|
+
}),
|
|
1428
|
+
}));
|
|
1429
|
+
export const updateConditionRuleLeftOperand = (rootGroup, ruleId, operand) => updateConditionRule(rootGroup, ruleId, (rule) => ({
|
|
1430
|
+
...rule,
|
|
1431
|
+
left: cloneConditionOperand(operand),
|
|
1432
|
+
}));
|
|
1433
|
+
export const updateConditionRuleRightOperand = (rootGroup, ruleId, operand) => updateConditionRule(rootGroup, ruleId, (rule) => ({
|
|
1434
|
+
...rule,
|
|
1435
|
+
right: cloneConditionOperand(operand),
|
|
1436
|
+
}));
|
|
1437
|
+
export const buildConditionOutputPayload = (config, evaluation) => ({
|
|
1438
|
+
debug: evaluation.tree,
|
|
1439
|
+
expression: evaluation.expression,
|
|
1440
|
+
mode: config.mode,
|
|
1441
|
+
preview: serializeConditionConfigToPreview(config),
|
|
1442
|
+
result: evaluation.result,
|
|
1443
|
+
});
|
|
1444
|
+
export const getConditionOperatorLabel = (operator) => CONDITION_OPERATOR_META.find((entry) => entry.value === operator)?.label ?? operator;
|
|
1445
|
+
export const isConditionOperatorUnary = (operator) => isUnaryOperator(operator);
|
|
1446
|
+
export const buildConditionRuntimeRootSetFromKeys = (keys) => new Set(keys);
|
|
1447
|
+
export const buildConditionRuntimeRootKeysForNodes = (nodes) => {
|
|
1448
|
+
const nodeLabels = flattenScenarioNodes(nodes)
|
|
1449
|
+
.map((node) => node.data.label.trim())
|
|
1450
|
+
.filter(Boolean);
|
|
1451
|
+
return new Set([
|
|
1452
|
+
"response",
|
|
1453
|
+
"env",
|
|
1454
|
+
"global",
|
|
1455
|
+
"globals",
|
|
1456
|
+
"db",
|
|
1457
|
+
"kafkaEvent",
|
|
1458
|
+
"kafkaevent",
|
|
1459
|
+
"redis",
|
|
1460
|
+
"lastResponse",
|
|
1461
|
+
...nodeLabels,
|
|
1462
|
+
]);
|
|
1463
|
+
};
|
|
1464
|
+
export const buildConditionRuntimeSuggestions = ({ nodes, environmentKeys = [], }) => dedupeSuggestions([
|
|
1465
|
+
...BUILT_IN_RUNTIME_SUGGESTIONS,
|
|
1466
|
+
...environmentKeys.map((key) => createSuggestion("Environment", `env.${key}`, `Environment variable "${key}".`, "string")),
|
|
1467
|
+
...flattenScenarioNodes(nodes).flatMap((node) => {
|
|
1468
|
+
const label = node.data.label.trim();
|
|
1469
|
+
return label ? buildNodeSuggestions(label, node.type) : [];
|
|
1470
|
+
}),
|
|
1471
|
+
]);
|
|
1472
|
+
export const filterConditionRuntimeSuggestions = ({ query, suggestions, limit = 40, }) => {
|
|
1473
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1474
|
+
const filtered = normalizedQuery
|
|
1475
|
+
? suggestions.filter((suggestion) => {
|
|
1476
|
+
const haystacks = [
|
|
1477
|
+
suggestion.group.toLowerCase(),
|
|
1478
|
+
suggestion.path.toLowerCase(),
|
|
1479
|
+
suggestion.description.toLowerCase(),
|
|
1480
|
+
];
|
|
1481
|
+
return haystacks.some((haystack) => haystack.includes(normalizedQuery));
|
|
1482
|
+
})
|
|
1483
|
+
: suggestions;
|
|
1484
|
+
return filtered.slice(0, limit);
|
|
1485
|
+
};
|
|
1486
|
+
export const createConditionConfigFromLegacyExpression = (expression) => {
|
|
1487
|
+
const normalizedExpression = normalizeWhitespace(expression);
|
|
1488
|
+
if (!normalizedExpression) {
|
|
1489
|
+
return createDefaultConditionConfig();
|
|
1490
|
+
}
|
|
1491
|
+
try {
|
|
1492
|
+
const rootGroup = parseConditionExpressionToGroup(normalizedExpression);
|
|
1493
|
+
return {
|
|
1494
|
+
mode: "builder",
|
|
1495
|
+
rootGroup,
|
|
1496
|
+
expertExpression: serializeConditionGroupToExpression(rootGroup),
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
return {
|
|
1501
|
+
mode: "expert",
|
|
1502
|
+
rootGroup: createDefaultConditionConfig().rootGroup,
|
|
1503
|
+
expertExpression: normalizedExpression,
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
export const convertLegacyConditionOperator = (value) => parseLegacyConditionOperator(value);
|
|
1508
|
+
//# sourceMappingURL=condition.js.map
|