bazis 0.131.0 → 0.132.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/cli/templates/process.ts +1 -1
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/workflow/{runtime_module/config/Runtime.config.ts → contracts/Workflow.config.ts} +15 -6
- package/core/workflow/contracts/WorkflowExtensions.ts +40 -0
- package/core/workflow/contracts/WorkflowInputValidator.ts +6 -5
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +16 -3
- package/core/workflow/definitions_module/access/Definitions.permissions.ts +4 -3
- package/core/workflow/definitions_module/access/Folder.references.ts +37 -0
- package/core/workflow/definitions_module/access/Process.references.ts +1 -1
- package/core/workflow/definitions_module/access/ProcessFolder.lookup.ts +20 -0
- package/core/workflow/definitions_module/code/CatalogImport.ts +95 -0
- package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +26 -16
- package/core/workflow/definitions_module/code/codeProcesses.ts +7 -3
- package/core/workflow/definitions_module/code/defineProcess.ts +10 -4
- package/core/workflow/definitions_module/http/Processes.controller.ts +65 -23
- package/core/workflow/definitions_module/model/Definitions.dbContext.ts +2 -2
- package/core/workflow/definitions_module/model/WfProcess.model.ts +19 -5
- package/core/workflow/definitions_module/model/WfVersion.model.ts +1 -1
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +33 -10
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +88 -28
- package/core/workflow/generated/deps.ts +11 -3
- package/core/workflow/index.ts +4 -2
- package/core/workflow/internal/WorkflowLanguages.ts +35 -0
- package/core/workflow/internal/canonicalJson.ts +8 -0
- package/core/workflow/internal/http.ts +20 -1
- package/core/workflow/runtime_module/WorkflowRuntime.module.ts +0 -2
- package/core/workflow/runtime_module/access/Runtime.permissions.ts +5 -3
- package/core/workflow/runtime_module/background/WorkflowRetention.ts +1 -1
- package/core/workflow/runtime_module/background/WorkflowWorker.ts +2 -2
- package/core/workflow/runtime_module/events/Runtime.events.ts +2 -2
- package/core/workflow/runtime_module/http/Instances.controller.ts +16 -11
- package/core/workflow/runtime_module/internal/Schedules.ts +2 -2
- package/core/workflow/runtime_module/internal/ToolStep.ts +40 -9
- package/core/workflow/runtime_module/internal/WorkflowEngine.ts +60 -10
- package/core/workflow/runtime_module/internal/WorkflowJobs.ts +2 -4
- package/core/workflow/runtime_module/model/Runtime.dbContext.ts +2 -2
- package/core/workflow/runtime_module/model/WfHistory.model.ts +2 -2
- package/core/workflow/runtime_module/model/WfInstance.model.ts +28 -3
- package/core/workflow/runtime_module/model/WfJob.model.ts +17 -1
- package/core/workflow/runtime_module/model/WfSubscription.model.ts +1 -1
- package/core/workflow/runtime_module/model/WfToken.model.ts +11 -1
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +5 -1
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +9 -7
- package/core/workflow/tasks_module/http/Tasks.controller.ts +14 -13
- package/core/workflow/tasks_module/model/Tasks.dbContext.ts +2 -2
- package/core/workflow/tasks_module/model/WfTask.model.ts +8 -4
- package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +3 -3
- package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +3 -3
- package/library/openapi/codegen.ts +3 -0
- package/library/validation/types/ValidatorOptions.ts +2 -0
- package/library/workflow/ProcessDocument.ts +59 -23
- package/library/workflow/expressionSyntax.ts +243 -0
- package/library/workflow/expressions.ts +343 -0
- package/library/workflow/formFields.ts +118 -33
- package/library/workflow/index.ts +48 -9
- package/library/workflow/localized.ts +35 -0
- package/library/workflow/masking.ts +58 -0
- package/library/workflow/mermaid.ts +18 -8
- package/library/workflow/nodeKinds.ts +88 -17
- package/library/workflow/palette.ts +108 -0
- package/library/workflow/paths.ts +31 -6
- package/library/workflow/structure.ts +282 -0
- package/library/workflow/upgradeDocument.ts +84 -0
- package/library/workflow/validateProcess.ts +130 -48
- package/package.json +1 -1
- package/library/workflow/conditions.ts +0 -44
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { checkExpression, checkPath, isExpressionNode, type ExpressionScope } from "./expressions";
|
|
2
|
+
import { checkExtensions, variableProblems } from "./formFields";
|
|
3
|
+
import { checkLocalizedText } from "./localized";
|
|
4
|
+
import { ISO_DURATION, nodeKindOf, type NodeRole } from "./nodeKinds";
|
|
5
|
+
import { isPath } from "./paths";
|
|
6
|
+
import { PROCESS_SCHEMA_VERSION, type ProcessDocument } from "./ProcessDocument";
|
|
7
|
+
import { toStructure } from "./structure";
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* One finding of {@link validateProcess}. An `error` blocks publishing, a
|
|
@@ -14,12 +17,16 @@ export interface ProcessIssue {
|
|
|
14
17
|
readonly message: string;
|
|
15
18
|
readonly node?: string;
|
|
16
19
|
readonly flow?: string;
|
|
20
|
+
readonly variable?: string;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export type ProcessIssueCode =
|
|
20
24
|
| "schema"
|
|
21
25
|
| "too_large"
|
|
22
|
-
| "
|
|
26
|
+
| "variable_invalid"
|
|
27
|
+
| "settings_invalid"
|
|
28
|
+
| "extension_invalid"
|
|
29
|
+
| "extension_unknown"
|
|
23
30
|
| "id_invalid"
|
|
24
31
|
| "unknown_kind"
|
|
25
32
|
| "title_invalid"
|
|
@@ -47,13 +54,14 @@ export type ProcessIssueCode =
|
|
|
47
54
|
| "unreachable"
|
|
48
55
|
| "dead_end"
|
|
49
56
|
| "diagram_missing"
|
|
50
|
-
| "diagram_orphan"
|
|
57
|
+
| "diagram_orphan"
|
|
58
|
+
| "not_structured";
|
|
51
59
|
|
|
52
|
-
export const PROCESS_LIMITS = Object.freeze({ nodes: 500, flows: 1000, bytes: 1_000_000
|
|
60
|
+
export const PROCESS_LIMITS = Object.freeze({ nodes: 500, flows: 1000, bytes: 1_000_000 });
|
|
53
61
|
|
|
54
62
|
const ID = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
55
|
-
|
|
56
|
-
const
|
|
63
|
+
/** Paths that come with later stages of WORKFLOW-002: until then they are an error, not a silent literal. */
|
|
64
|
+
const NOT_YET = /^\$(secrets|item|index)(\.|$)/;
|
|
57
65
|
|
|
58
66
|
type Json = Record<string, unknown>;
|
|
59
67
|
|
|
@@ -71,6 +79,8 @@ export interface ValidateProcessOptions {
|
|
|
71
79
|
* `callers`. Without it Tool names are only checked for their form.
|
|
72
80
|
*/
|
|
73
81
|
readonly tools?: (name: string) => boolean;
|
|
82
|
+
/** Whether an `extensions` namespace has a check. Without it namespaces are only checked for their form. */
|
|
83
|
+
readonly extensions?: (namespace: string) => boolean;
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
/**
|
|
@@ -80,8 +90,8 @@ export interface ValidateProcessOptions {
|
|
|
80
90
|
*/
|
|
81
91
|
export function validateProcess(document: unknown, options: ValidateProcessOptions = {}): ProcessIssue[] {
|
|
82
92
|
const out: ProcessIssue[] = [];
|
|
83
|
-
const error = (code: ProcessIssueCode, message: string, at:
|
|
84
|
-
const warning = (code: ProcessIssueCode, message: string, at:
|
|
93
|
+
const error = (code: ProcessIssueCode, message: string, at: IssueTarget = {}) => out.push({ severity: "error", code, message, ...at });
|
|
94
|
+
const warning = (code: ProcessIssueCode, message: string, at: IssueTarget = {}) => out.push({ severity: "warning", code, message, ...at });
|
|
85
95
|
|
|
86
96
|
if (!isObject(document)) return [{ severity: "error", code: "schema", message: "document must be an object" }];
|
|
87
97
|
if (JSON.stringify(document).length > PROCESS_LIMITS.bytes) return [{ severity: "error", code: "too_large", message: `document is larger than ${PROCESS_LIMITS.bytes} bytes` }];
|
|
@@ -90,10 +100,15 @@ export function validateProcess(document: unknown, options: ValidateProcessOptio
|
|
|
90
100
|
if (!isObject(document.flows)) error("schema", "flows must be an object");
|
|
91
101
|
if (document.diagram !== undefined && !isDiagram(document.diagram)) error("schema", "diagram must have shapes and edges objects with numeric coordinates");
|
|
92
102
|
if (out.length > 0) return out;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
const variables = document.variables === undefined ? [] : variableProblems(document.variables);
|
|
104
|
+
for (const { message, variable } of variables) error("variable_invalid", message, variable === undefined ? {} : { variable });
|
|
105
|
+
const declared = variables.length === 0 && Array.isArray(document.variables) ? document.variables as readonly Json[] : [];
|
|
106
|
+
if (document.settings !== undefined) {
|
|
107
|
+
const problem = checkSettings(document.settings);
|
|
108
|
+
if (problem) error("settings_invalid", problem);
|
|
96
109
|
}
|
|
110
|
+
checkNamespaces(document.extensions, "extensions", {}, options, error);
|
|
111
|
+
for (const variable of declared) checkNamespaces(variable.extensions, "extensions", { variable: String(variable.name) }, options, error);
|
|
97
112
|
|
|
98
113
|
const nodes = document.nodes as Json;
|
|
99
114
|
const flows = document.flows as Json;
|
|
@@ -117,7 +132,9 @@ export function validateProcess(document: unknown, options: ValidateProcessOptio
|
|
|
117
132
|
}
|
|
118
133
|
roles.set(id, definition.role);
|
|
119
134
|
kinds.set(id, definition.kind);
|
|
120
|
-
|
|
135
|
+
const titleProblem = checkLocalizedText(raw.title, 200);
|
|
136
|
+
if (titleProblem) error("title_invalid", `title ${titleProblem}`, { node: id });
|
|
137
|
+
checkNamespaces(raw.extensions, "extensions", { node: id }, options, error);
|
|
121
138
|
if (raw.config !== undefined && !isObject(raw.config)) {
|
|
122
139
|
error("config_invalid", "config must be an object", { node: id });
|
|
123
140
|
} else {
|
|
@@ -140,9 +157,22 @@ export function validateProcess(document: unknown, options: ValidateProcessOptio
|
|
|
140
157
|
else if (kinds.get(id) === "boundaryError" && kinds.get(host) !== "serviceTask") error("boundary_attach", "error boundary attaches only to a service task", { node: id });
|
|
141
158
|
}
|
|
142
159
|
|
|
143
|
-
// What
|
|
144
|
-
const
|
|
145
|
-
const
|
|
160
|
+
// What expressions may read: declared variables, what steps save and existing nodes.
|
|
161
|
+
const named = (direction: (value: unknown) => boolean) => new Set(declared.filter((variable) => direction(variable.direction)).map((variable) => String(variable.name)));
|
|
162
|
+
const writable = named((direction) => direction !== "in");
|
|
163
|
+
const saved = new Set(writable);
|
|
164
|
+
for (const raw of Object.values(nodes)) {
|
|
165
|
+
const saveTo = isObject(raw) && isObject(raw.config) ? raw.config.saveTo : undefined;
|
|
166
|
+
if (typeof saveTo === "string") saved.add(saveTo);
|
|
167
|
+
}
|
|
168
|
+
const known = variables.length === 0;
|
|
169
|
+
const scope: ExpressionScope = { ...(known ? { input: named((direction) => direction === "in"), vars: saved } : {}), nodes: new Set(Object.keys(nodes)) };
|
|
170
|
+
|
|
171
|
+
// What steps read: an assign writes declared variables, inputs and waits read what exists.
|
|
172
|
+
for (const [id, raw] of Object.entries(nodes)) {
|
|
173
|
+
if (!isObject(raw) || !isObject(raw.config) || definitionProblem(raw)) continue;
|
|
174
|
+
for (const problem of configExpressionProblems(raw.kind as string, raw.config, scope, known ? writable : undefined)) error("config_invalid", problem, { node: id });
|
|
175
|
+
}
|
|
146
176
|
|
|
147
177
|
// Flows: ends, duplicates, conditions.
|
|
148
178
|
const outgoing = new Map<string, string[]>();
|
|
@@ -168,13 +198,16 @@ export function validateProcess(document: unknown, options: ValidateProcessOptio
|
|
|
168
198
|
pairs.add(pair);
|
|
169
199
|
outgoing.set(source, [...outgoing.get(source) ?? [], id]);
|
|
170
200
|
incoming.set(target, [...incoming.get(target) ?? [], id]);
|
|
171
|
-
if (raw.title !== undefined
|
|
201
|
+
if (raw.title !== undefined) {
|
|
202
|
+
const problem = checkLocalizedText(raw.title, 200);
|
|
203
|
+
if (problem) error("title_invalid", `flow title ${problem}`, { flow: id });
|
|
204
|
+
}
|
|
172
205
|
if (raw.isDefault !== undefined && typeof raw.isDefault !== "boolean") error("default_invalid", "isDefault must be a boolean", { flow: id });
|
|
173
206
|
const fromChoice = kinds.get(source) === "exclusiveGateway";
|
|
174
207
|
if (raw.condition !== undefined) {
|
|
175
208
|
if (!fromChoice) error("condition_not_allowed", "only flows leaving an exclusive gateway have conditions", { flow: id });
|
|
176
209
|
else {
|
|
177
|
-
const problem =
|
|
210
|
+
const problem = checkExpression(raw.condition, scope);
|
|
178
211
|
if (problem) error("condition_invalid", problem, { flow: id });
|
|
179
212
|
}
|
|
180
213
|
}
|
|
@@ -234,11 +267,14 @@ export function validateProcess(document: unknown, options: ValidateProcessOptio
|
|
|
234
267
|
for (const id of Object.keys(diagram.shapes)) if (!Object.hasOwn(nodes, id)) warning("diagram_orphan", "shape without a node", { node: id });
|
|
235
268
|
for (const id of Object.keys(diagram.edges)) if (!Object.hasOwn(flows, id)) warning("diagram_orphan", "edge without a flow", { flow: id });
|
|
236
269
|
}
|
|
270
|
+
if (!hasErrors(out) && toStructure(document as unknown as ProcessDocument) === undefined) {
|
|
271
|
+
warning("not_structured", "the graph cannot be drawn as a list of steps: a designer shows it read only");
|
|
272
|
+
}
|
|
237
273
|
return out;
|
|
238
274
|
}
|
|
239
275
|
|
|
240
276
|
/** An exclusive gateway: every outgoing flow has a condition, except at most one default flow. */
|
|
241
|
-
function checkChoice(id: string, outs: readonly string[], flows: Json, error: (code: ProcessIssueCode, message: string, at?:
|
|
277
|
+
function checkChoice(id: string, outs: readonly string[], flows: Json, error: (code: ProcessIssueCode, message: string, at?: IssueTarget) => void): void {
|
|
242
278
|
const defaults = outs.filter((flow) => (flows[flow] as Json).isDefault === true);
|
|
243
279
|
if (defaults.length > 1) error("default_invalid", "exclusive gateway can have only one default flow", { node: id });
|
|
244
280
|
for (const flow of outs) {
|
|
@@ -251,37 +287,83 @@ function checkChoice(id: string, outs: readonly string[], flows: Json, error: (c
|
|
|
251
287
|
}
|
|
252
288
|
}
|
|
253
289
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
290
|
+
/** The settings this version of the framework runs; a setting that would be stored and ignored is an error. */
|
|
291
|
+
function checkSettings(value: unknown): string | null {
|
|
292
|
+
if (!isObject(value)) return "settings must be an object";
|
|
293
|
+
for (const [key, item] of Object.entries(value)) {
|
|
294
|
+
if (key === "stepTimeout") {
|
|
295
|
+
if (typeof item !== "string" || !ISO_DURATION.test(item)) return "settings.stepTimeout must be an ISO duration without months and years";
|
|
296
|
+
} else if (key === "log") {
|
|
297
|
+
if (!isObject(item)) return "settings.log must be an object";
|
|
298
|
+
for (const [flag, on] of Object.entries(item)) {
|
|
299
|
+
if (flag !== "variables" && flag !== "toolPayloads" && flag !== "maskPersonal") return `settings.log.${flag} is unknown`;
|
|
300
|
+
if (typeof on !== "boolean") return `settings.log.${flag} must be a boolean`;
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
return `settings.${key} is not supported`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
258
307
|
}
|
|
259
308
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
309
|
+
type IssueTarget = { node?: string; flow?: string; variable?: string };
|
|
310
|
+
|
|
311
|
+
/** `extensions` of the root or a node: its form, then whether its namespaces have a check. */
|
|
312
|
+
function checkNamespaces(value: unknown, at: string, target: IssueTarget, options: ValidateProcessOptions, error: (code: ProcessIssueCode, message: string, at?: IssueTarget) => void): void {
|
|
313
|
+
if (value === undefined) return;
|
|
314
|
+
const problem = checkExtensions(value, at);
|
|
315
|
+
if (problem) {
|
|
316
|
+
error("extension_invalid", problem, target);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (options.extensions === undefined) return;
|
|
320
|
+
for (const namespace of Object.keys(value as Json)) {
|
|
321
|
+
if (!options.extensions(namespace)) error("extension_unknown", `no check is registered for the extensions namespace ${namespace}`, target);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function definitionProblem(raw: Json): boolean {
|
|
326
|
+
const definition = nodeKindOf(raw.kind);
|
|
327
|
+
return definition === undefined || definition.check(raw.config as Json) !== null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** The expressions and paths in a step's `config`, against what the document declares. */
|
|
331
|
+
function configExpressionProblems(kind: string, config: Json, scope: ExpressionScope, writable: ReadonlySet<string> | undefined): string[] {
|
|
332
|
+
const out: string[] = [];
|
|
333
|
+
if (kind === "assign") {
|
|
334
|
+
for (const [index, item] of (config.assignments as readonly Json[]).entries()) {
|
|
335
|
+
const name = item.var as string;
|
|
336
|
+
if (writable && !writable.has(name)) out.push(`config.assignments[${index}].var: ${name} is not a declared local or out variable`);
|
|
337
|
+
const problem = checkExpression(item.value, scope);
|
|
338
|
+
if (problem) out.push(`config.assignments[${index}].value: ${problem}`);
|
|
270
339
|
}
|
|
271
|
-
return null;
|
|
272
340
|
}
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
341
|
+
if (kind === "serviceTask" && config.input !== undefined) walkInput(config.input, "config.input", scope, out);
|
|
342
|
+
for (const key of ["at", "correlation"]) {
|
|
343
|
+
if (typeof config[key] === "string") {
|
|
344
|
+
const problem = checkPath(config[key], scope);
|
|
345
|
+
if (problem) out.push(`config.${key}: ${problem}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function walkInput(value: unknown, at: string, scope: ExpressionScope, out: string[]): void {
|
|
352
|
+
if (typeof value === "string") {
|
|
353
|
+
if (NOT_YET.test(value)) out.push(`${at}: ${value} is not supported`);
|
|
354
|
+
else if (isPath(value)) {
|
|
355
|
+
const problem = checkPath(value, scope);
|
|
356
|
+
if (problem) out.push(`${at}: ${problem}`);
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (isExpressionNode(value)) {
|
|
361
|
+
const problem = checkExpression(value, scope);
|
|
362
|
+
if (problem) out.push(`${at}: ${problem}`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (Array.isArray(value)) value.forEach((item, index) => walkInput(item, `${at}[${index}]`, scope, out));
|
|
366
|
+
else if (isObject(value)) for (const [key, item] of Object.entries(value)) walkInput(item, `${at}.${key}`, scope, out);
|
|
285
367
|
}
|
|
286
368
|
|
|
287
369
|
function isDiagram(value: unknown): boolean {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.132.0",
|
|
4
4
|
"description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alexander Alenin",
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { Condition } from "./ProcessDocument";
|
|
2
|
-
import { isPath, readPath, type InstanceData } from "./paths";
|
|
3
|
-
|
|
4
|
-
/** Evaluates a flow condition. Pure: no calls, no side effects. */
|
|
5
|
-
export function evaluateCondition(condition: Condition, data: InstanceData): boolean {
|
|
6
|
-
if ("all" in condition) return condition.all.every((item) => evaluateCondition(item, data));
|
|
7
|
-
if ("any" in condition) return condition.any.some((item) => evaluateCondition(item, data));
|
|
8
|
-
if ("not" in condition) return !evaluateCondition(condition.not, data);
|
|
9
|
-
const actual = readPath(data, condition.path);
|
|
10
|
-
const expected = isPath(condition.value) ? readPath(data, condition.value) : condition.value;
|
|
11
|
-
switch (condition.op) {
|
|
12
|
-
case "eq": return same(actual, expected);
|
|
13
|
-
case "ne": return !same(actual, expected);
|
|
14
|
-
case "gt": return compare(actual, expected, (a, b) => a > b);
|
|
15
|
-
case "gte": return compare(actual, expected, (a, b) => a >= b);
|
|
16
|
-
case "lt": return compare(actual, expected, (a, b) => a < b);
|
|
17
|
-
case "lte": return compare(actual, expected, (a, b) => a <= b);
|
|
18
|
-
case "in": return Array.isArray(expected) && expected.some((item) => same(actual, item));
|
|
19
|
-
case "contains":
|
|
20
|
-
if (typeof actual === "string") return typeof expected === "string" && actual.includes(expected);
|
|
21
|
-
return Array.isArray(actual) && actual.some((item) => same(item, expected));
|
|
22
|
-
case "empty": return isEmpty(actual);
|
|
23
|
-
case "notEmpty": return !isEmpty(actual);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function same(a: unknown, b: unknown): boolean {
|
|
28
|
-
if (typeof a === "number" && typeof b === "string" || typeof a === "string" && typeof b === "number") return Number(a) === Number(b);
|
|
29
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Numbers compare as numbers (a decimal arrives as a string), other strings as strings (ISO dates compare right). */
|
|
33
|
-
function compare(a: unknown, b: unknown, test: (a: number | string, b: number | string) => boolean): boolean {
|
|
34
|
-
if (a === null || a === undefined || b === null || b === undefined) return false;
|
|
35
|
-
const na = Number(a), nb = Number(b);
|
|
36
|
-
if (typeof a !== "boolean" && typeof b !== "boolean" && a !== "" && b !== "" && Number.isFinite(na) && Number.isFinite(nb)) return test(na, nb);
|
|
37
|
-
return typeof a === "string" && typeof b === "string" ? test(a, b) : false;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function isEmpty(value: unknown): boolean {
|
|
41
|
-
if (value === null || value === undefined || value === "") return true;
|
|
42
|
-
if (Array.isArray(value)) return value.length === 0;
|
|
43
|
-
return typeof value === "object" && Object.keys(value).length === 0;
|
|
44
|
-
}
|