bazis 0.121.0 → 0.124.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/module.ts +5 -4
- package/cli/templates/passport.ts +1 -1
- package/core/agent/AgentRuntime.ts +7 -2
- package/core/agent/AgentToolExecutor.ts +34 -11
- package/core/agent/README.md +10 -9
- package/core/agent/ToolRunner.ts +135 -0
- package/core/agent/index.ts +3 -0
- package/core/agent/internal/AgentExecutionDriver.ts +9 -2
- package/core/agent/internal/ToolAudit.projector.ts +16 -0
- package/core/agent/metadata.ts +25 -0
- package/core/agent/moduleContributions-v1.ts +5 -1
- package/core/agent/toolExecutionOptions.ts +15 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/workflow/Workflow.module.ts +15 -0
- package/core/workflow/access/WorkflowAccess.ts +52 -0
- package/core/workflow/contracts/WorkflowError.ts +28 -0
- package/core/workflow/contracts/WorkflowInputValidator.ts +24 -0
- package/core/workflow/contracts/types.ts +30 -0
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +24 -0
- package/core/workflow/definitions_module/http/Processes.controller.ts +92 -0
- package/core/workflow/definitions_module/model/Definitions.dbContext.ts +16 -0
- package/core/workflow/definitions_module/model/WfProcess.model.ts +52 -0
- package/core/workflow/definitions_module/model/WfVersion.model.ts +33 -0
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +79 -0
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +234 -0
- package/core/workflow/index.ts +43 -0
- package/core/workflow/internal/WorkflowTools.ts +36 -0
- package/core/workflow/internal/http.ts +63 -0
- package/core/workflow/runtime_module/WorkflowRuntime.module.ts +53 -0
- package/core/workflow/runtime_module/background/WorkflowRetention.ts +66 -0
- package/core/workflow/runtime_module/background/WorkflowWorker.ts +92 -0
- package/core/workflow/runtime_module/config/Runtime.config.ts +27 -0
- package/core/workflow/runtime_module/events/Runtime.events.ts +36 -0
- package/core/workflow/runtime_module/http/Instances.controller.ts +72 -0
- package/core/workflow/runtime_module/internal/GraphCache.ts +37 -0
- package/core/workflow/runtime_module/internal/ToolStep.ts +60 -0
- package/core/workflow/runtime_module/internal/WorkflowEngine.ts +356 -0
- package/core/workflow/runtime_module/model/Runtime.dbContext.ts +25 -0
- package/core/workflow/runtime_module/model/WfHistory.model.ts +38 -0
- package/core/workflow/runtime_module/model/WfInstance.model.ts +70 -0
- package/core/workflow/runtime_module/model/WfJob.model.ts +49 -0
- package/core/workflow/runtime_module/model/WfSubscription.model.ts +30 -0
- package/core/workflow/runtime_module/model/WfToken.model.ts +39 -0
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +82 -0
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +167 -0
- package/library/workflow/ProcessDocument.ts +94 -0
- package/library/workflow/conditions.ts +44 -0
- package/library/workflow/duration.ts +10 -0
- package/library/workflow/formFields.ts +112 -0
- package/library/workflow/index.ts +24 -0
- package/library/workflow/nodeKinds.ts +111 -0
- package/library/workflow/paths.ts +39 -0
- package/library/workflow/validateProcess.ts +310 -0
- package/package.json +4 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Instance data that conditions and step inputs read. */
|
|
2
|
+
export interface InstanceData {
|
|
3
|
+
readonly input: Readonly<Record<string, unknown>>;
|
|
4
|
+
readonly vars: Readonly<Record<string, unknown>>;
|
|
5
|
+
readonly nodes: Readonly<Record<string, unknown>>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** `$input.amount`, `$vars.payment.id`, `$nodes.Pay.paymentId`. */
|
|
9
|
+
export const DATA_PATH = /^\$(input|vars|nodes)((\.[A-Za-z_][A-Za-z0-9_]*)+)$/;
|
|
10
|
+
|
|
11
|
+
/** `true` if the value is a path to instance data. */
|
|
12
|
+
export function isPath(value: unknown): value is string {
|
|
13
|
+
return typeof value === "string" && DATA_PATH.test(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The value at a path, or `undefined`. Only own keys are read, never `__proto__` and the like. */
|
|
17
|
+
export function readPath(data: InstanceData, path: string): unknown {
|
|
18
|
+
const match = DATA_PATH.exec(path);
|
|
19
|
+
if (!match) return undefined;
|
|
20
|
+
let current: unknown = data[match[1] as keyof InstanceData];
|
|
21
|
+
for (const key of match[2]!.slice(1).split(".")) {
|
|
22
|
+
if (typeof current !== "object" || current === null || !Object.prototype.hasOwnProperty.call(current, key)) return undefined;
|
|
23
|
+
current = (current as Record<string, unknown>)[key];
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A step input: a path string becomes the instance value, objects and lists are
|
|
30
|
+
* resolved deeply, anything else is a literal. A missing value becomes `null`.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveInput(value: unknown, data: InstanceData): unknown {
|
|
33
|
+
if (isPath(value)) return readPath(data, value) ?? null;
|
|
34
|
+
if (Array.isArray(value)) return value.map((item) => resolveInput(item, data));
|
|
35
|
+
if (typeof value === "object" && value !== null) {
|
|
36
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolveInput(item, data)]));
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { checkFormSpec, formFieldCodes, type FormSpec } from "./formFields";
|
|
2
|
+
import { nodeKindOf, type NodeRole } from "./nodeKinds";
|
|
3
|
+
import { DATA_PATH } from "./paths";
|
|
4
|
+
import { PROCESS_SCHEMA_VERSION, type ConditionOp } from "./ProcessDocument";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One finding of {@link validateProcess}. An `error` blocks publishing, a
|
|
8
|
+
* `warning` is a hint for a designer (a node without a shape). `node` / `flow`
|
|
9
|
+
* name what to highlight.
|
|
10
|
+
*/
|
|
11
|
+
export interface ProcessIssue {
|
|
12
|
+
readonly severity: "error" | "warning";
|
|
13
|
+
readonly code: ProcessIssueCode;
|
|
14
|
+
readonly message: string;
|
|
15
|
+
readonly node?: string;
|
|
16
|
+
readonly flow?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ProcessIssueCode =
|
|
20
|
+
| "schema"
|
|
21
|
+
| "too_large"
|
|
22
|
+
| "input_invalid"
|
|
23
|
+
| "id_invalid"
|
|
24
|
+
| "unknown_kind"
|
|
25
|
+
| "title_invalid"
|
|
26
|
+
| "config_invalid"
|
|
27
|
+
| "unknown_tool"
|
|
28
|
+
| "flow_dangling"
|
|
29
|
+
| "flow_self"
|
|
30
|
+
| "flow_duplicate"
|
|
31
|
+
| "no_start"
|
|
32
|
+
| "no_end"
|
|
33
|
+
| "start_incoming"
|
|
34
|
+
| "start_outgoing"
|
|
35
|
+
| "end_outgoing"
|
|
36
|
+
| "end_incoming"
|
|
37
|
+
| "activity_incoming"
|
|
38
|
+
| "activity_outgoing"
|
|
39
|
+
| "gateway_flows"
|
|
40
|
+
| "boundary_attach"
|
|
41
|
+
| "boundary_incoming"
|
|
42
|
+
| "boundary_outgoing"
|
|
43
|
+
| "condition_missing"
|
|
44
|
+
| "condition_invalid"
|
|
45
|
+
| "condition_not_allowed"
|
|
46
|
+
| "default_invalid"
|
|
47
|
+
| "unreachable"
|
|
48
|
+
| "dead_end"
|
|
49
|
+
| "diagram_missing"
|
|
50
|
+
| "diagram_orphan";
|
|
51
|
+
|
|
52
|
+
export const PROCESS_LIMITS = Object.freeze({ nodes: 500, flows: 1000, bytes: 1_000_000, conditionDepth: 8 });
|
|
53
|
+
|
|
54
|
+
const ID = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
55
|
+
const OPS = new Set<ConditionOp>(["eq", "ne", "gt", "gte", "lt", "lte", "in", "contains", "empty", "notEmpty"]);
|
|
56
|
+
const UNARY = new Set<ConditionOp>(["empty", "notEmpty"]);
|
|
57
|
+
|
|
58
|
+
type Json = Record<string, unknown>;
|
|
59
|
+
|
|
60
|
+
function isObject(value: unknown): value is Json {
|
|
61
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function finite(value: unknown): boolean {
|
|
65
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ValidateProcessOptions {
|
|
69
|
+
/**
|
|
70
|
+
* Whether a Tool may be a service task step: exists and lists `workflow` in its
|
|
71
|
+
* `callers`. Without it Tool names are only checked for their form.
|
|
72
|
+
*/
|
|
73
|
+
readonly tools?: (name: string) => boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks a whole process document: the JSON shape, node configs and the graph.
|
|
78
|
+
* Never throws and returns every finding: a draft is kept with errors, while
|
|
79
|
+
* publishing needs no `error` among them.
|
|
80
|
+
*/
|
|
81
|
+
export function validateProcess(document: unknown, options: ValidateProcessOptions = {}): ProcessIssue[] {
|
|
82
|
+
const out: ProcessIssue[] = [];
|
|
83
|
+
const error = (code: ProcessIssueCode, message: string, at: { node?: string; flow?: string } = {}) => out.push({ severity: "error", code, message, ...at });
|
|
84
|
+
const warning = (code: ProcessIssueCode, message: string, at: { node?: string; flow?: string } = {}) => out.push({ severity: "warning", code, message, ...at });
|
|
85
|
+
|
|
86
|
+
if (!isObject(document)) return [{ severity: "error", code: "schema", message: "document must be an object" }];
|
|
87
|
+
if (JSON.stringify(document).length > PROCESS_LIMITS.bytes) return [{ severity: "error", code: "too_large", message: `document is larger than ${PROCESS_LIMITS.bytes} bytes` }];
|
|
88
|
+
if (document.schemaVersion !== PROCESS_SCHEMA_VERSION) error("schema", `schemaVersion must be ${PROCESS_SCHEMA_VERSION}`);
|
|
89
|
+
if (!isObject(document.nodes)) error("schema", "nodes must be an object");
|
|
90
|
+
if (!isObject(document.flows)) error("schema", "flows must be an object");
|
|
91
|
+
if (document.diagram !== undefined && !isDiagram(document.diagram)) error("schema", "diagram must have shapes and edges objects with numeric coordinates");
|
|
92
|
+
if (out.length > 0) return out;
|
|
93
|
+
if (document.input !== undefined) {
|
|
94
|
+
const problem = checkFormSpec(document.input, "input");
|
|
95
|
+
if (problem) error("input_invalid", problem);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const nodes = document.nodes as Json;
|
|
99
|
+
const flows = document.flows as Json;
|
|
100
|
+
if (Object.keys(nodes).length > PROCESS_LIMITS.nodes) return [{ severity: "error", code: "too_large", message: `more than ${PROCESS_LIMITS.nodes} nodes` }];
|
|
101
|
+
if (Object.keys(flows).length > PROCESS_LIMITS.flows) return [{ severity: "error", code: "too_large", message: `more than ${PROCESS_LIMITS.flows} flows` }];
|
|
102
|
+
|
|
103
|
+
// Nodes: id, kind, title, config.
|
|
104
|
+
const roles = new Map<string, NodeRole>();
|
|
105
|
+
const kinds = new Map<string, string>();
|
|
106
|
+
const attached = new Map<string, string>();
|
|
107
|
+
for (const [id, raw] of Object.entries(nodes)) {
|
|
108
|
+
if (!ID.test(id)) error("id_invalid", `node id ${JSON.stringify(id)} is invalid`, { node: id });
|
|
109
|
+
if (!isObject(raw)) {
|
|
110
|
+
error("schema", "node must be an object", { node: id });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const definition = nodeKindOf(raw.kind);
|
|
114
|
+
if (!definition) {
|
|
115
|
+
error("unknown_kind", `unknown node kind ${JSON.stringify(raw.kind)}`, { node: id });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
roles.set(id, definition.role);
|
|
119
|
+
kinds.set(id, definition.kind);
|
|
120
|
+
if (typeof raw.title !== "string" || raw.title.trim().length === 0 || raw.title.length > 200) error("title_invalid", "title must be 1–200 characters", { node: id });
|
|
121
|
+
if (raw.config !== undefined && !isObject(raw.config)) {
|
|
122
|
+
error("config_invalid", "config must be an object", { node: id });
|
|
123
|
+
} else {
|
|
124
|
+
const config = (raw.config as Json | undefined) ?? {};
|
|
125
|
+
const problem = definition.check(config);
|
|
126
|
+
if (problem) error("config_invalid", problem, { node: id });
|
|
127
|
+
else if (definition.kind === "serviceTask" && options.tools && !options.tools(config.tool as string)) {
|
|
128
|
+
error("unknown_tool", `Tool ${JSON.stringify(config.tool)} does not exist or does not list "workflow" in its callers`, { node: id });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (definition.role === "boundary") {
|
|
132
|
+
if (typeof raw.attachedTo === "string") attached.set(id, raw.attachedTo);
|
|
133
|
+
else error("boundary_attach", "boundary node needs attachedTo", { node: id });
|
|
134
|
+
} else if (raw.attachedTo !== undefined) {
|
|
135
|
+
error("boundary_attach", "only boundary nodes can be attached", { node: id });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const [id, host] of attached) {
|
|
139
|
+
if (roles.get(host) !== "activity") error("boundary_attach", `attachedTo ${JSON.stringify(host)} is not a step`, { node: id });
|
|
140
|
+
else if (kinds.get(id) === "boundaryError" && kinds.get(host) !== "serviceTask") error("boundary_attach", "error boundary attaches only to a service task", { node: id });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// What conditions may read: declared input fields and existing nodes.
|
|
144
|
+
const inputSpec = document.input !== undefined && checkFormSpec(document.input, "input") === null ? document.input as FormSpec : undefined;
|
|
145
|
+
const scope: ConditionScope = { input: inputSpec === undefined ? null : new Set(formFieldCodes(inputSpec)), nodes: roles };
|
|
146
|
+
|
|
147
|
+
// Flows: ends, duplicates, conditions.
|
|
148
|
+
const outgoing = new Map<string, string[]>();
|
|
149
|
+
const incoming = new Map<string, string[]>();
|
|
150
|
+
const pairs = new Set<string>();
|
|
151
|
+
for (const [id, raw] of Object.entries(flows)) {
|
|
152
|
+
if (!ID.test(id)) error("id_invalid", `flow id ${JSON.stringify(id)} is invalid`, { flow: id });
|
|
153
|
+
if (Object.hasOwn(nodes, id)) error("id_invalid", "flow id clashes with a node id", { flow: id });
|
|
154
|
+
if (!isObject(raw)) {
|
|
155
|
+
error("schema", "flow must be an object", { flow: id });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const { source, target } = raw;
|
|
159
|
+
if (typeof source !== "string" || typeof target !== "string" || !roles.has(source) || !roles.has(target)) {
|
|
160
|
+
if (typeof source !== "string" || typeof target !== "string" || !Object.hasOwn(nodes, source) || !Object.hasOwn(nodes, target)) {
|
|
161
|
+
error("flow_dangling", "source and target must be existing nodes", { flow: id });
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (source === target) error("flow_self", "flow connects a node to itself", { flow: id });
|
|
166
|
+
const pair = `${source}\u0000${target}`;
|
|
167
|
+
if (pairs.has(pair)) error("flow_duplicate", "another flow already connects these nodes", { flow: id });
|
|
168
|
+
pairs.add(pair);
|
|
169
|
+
outgoing.set(source, [...outgoing.get(source) ?? [], id]);
|
|
170
|
+
incoming.set(target, [...incoming.get(target) ?? [], id]);
|
|
171
|
+
if (raw.title !== undefined && (typeof raw.title !== "string" || raw.title.length > 200)) error("title_invalid", "flow title must be up to 200 characters", { flow: id });
|
|
172
|
+
if (raw.isDefault !== undefined && typeof raw.isDefault !== "boolean") error("default_invalid", "isDefault must be a boolean", { flow: id });
|
|
173
|
+
const fromChoice = kinds.get(source) === "exclusiveGateway";
|
|
174
|
+
if (raw.condition !== undefined) {
|
|
175
|
+
if (!fromChoice) error("condition_not_allowed", "only flows leaving an exclusive gateway have conditions", { flow: id });
|
|
176
|
+
else {
|
|
177
|
+
const problem = checkCondition(raw.condition, 0, scope);
|
|
178
|
+
if (problem) error("condition_invalid", problem, { flow: id });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (raw.isDefault === true && !fromChoice) error("default_invalid", "only flows leaving an exclusive gateway can be default", { flow: id });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Rules by node role.
|
|
185
|
+
const starts = [...roles].filter(([, role]) => role === "start").map(([id]) => id);
|
|
186
|
+
if (starts.length === 0) error("no_start", "process needs a start node");
|
|
187
|
+
if (![...roles.values()].includes("end")) error("no_end", "process needs an end node");
|
|
188
|
+
for (const [id, role] of roles) {
|
|
189
|
+
const ins = incoming.get(id)?.length ?? 0;
|
|
190
|
+
const outs = outgoing.get(id) ?? [];
|
|
191
|
+
switch (role) {
|
|
192
|
+
case "start":
|
|
193
|
+
if (ins > 0) error("start_incoming", "start node cannot have incoming flows", { node: id });
|
|
194
|
+
if (outs.length !== 1) error("start_outgoing", "start node needs exactly one outgoing flow", { node: id });
|
|
195
|
+
break;
|
|
196
|
+
case "end":
|
|
197
|
+
if (outs.length > 0) error("end_outgoing", "end node cannot have outgoing flows", { node: id });
|
|
198
|
+
if (ins === 0) error("end_incoming", "end node needs an incoming flow", { node: id });
|
|
199
|
+
break;
|
|
200
|
+
case "activity":
|
|
201
|
+
if (ins === 0) error("activity_incoming", "step needs an incoming flow", { node: id });
|
|
202
|
+
if (outs.length !== 1) error("activity_outgoing", "step needs exactly one outgoing flow; branch with a gateway", { node: id });
|
|
203
|
+
break;
|
|
204
|
+
case "boundary":
|
|
205
|
+
if (ins > 0) error("boundary_incoming", "boundary node cannot have incoming flows", { node: id });
|
|
206
|
+
if (outs.length !== 1) error("boundary_outgoing", "boundary node needs exactly one outgoing flow", { node: id });
|
|
207
|
+
break;
|
|
208
|
+
case "gateway":
|
|
209
|
+
if (ins === 0 || outs.length === 0 || (ins < 2 && outs.length < 2)) {
|
|
210
|
+
error("gateway_flows", "gateway needs incoming and outgoing flows and must split or join", { node: id });
|
|
211
|
+
}
|
|
212
|
+
if (kinds.get(id) === "exclusiveGateway" && outs.length > 1) checkChoice(id, outs, flows, error);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Reachability: forward from starts and back from ends. A boundary node is reached through its step.
|
|
218
|
+
const forward = new Map<string, string[]>();
|
|
219
|
+
for (const [id, list] of outgoing) forward.set(id, list.map((flow) => (flows[flow] as Json).target as string));
|
|
220
|
+
for (const [boundary, host] of attached) if (roles.has(host)) forward.set(host, [...forward.get(host) ?? [], boundary]);
|
|
221
|
+
const backward = new Map<string, string[]>();
|
|
222
|
+
for (const [from, list] of forward) for (const to of list) backward.set(to, [...backward.get(to) ?? [], from]);
|
|
223
|
+
const reached = walk(starts, forward);
|
|
224
|
+
const ending = walk([...roles].filter(([, role]) => role === "end").map(([id]) => id), backward);
|
|
225
|
+
for (const id of roles.keys()) {
|
|
226
|
+
if (starts.length > 0 && !reached.has(id)) error("unreachable", "node cannot be reached from any start", { node: id });
|
|
227
|
+
else if (!ending.has(id) && roles.get(id) !== "end") error("dead_end", "no end can be reached from this node", { node: id });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Layout: hints only.
|
|
231
|
+
const diagram = document.diagram as { shapes: Json; edges: Json } | undefined;
|
|
232
|
+
if (diagram) {
|
|
233
|
+
for (const id of Object.keys(nodes)) if (!Object.hasOwn(diagram.shapes, id)) warning("diagram_missing", "node has no shape on the canvas", { node: id });
|
|
234
|
+
for (const id of Object.keys(diagram.shapes)) if (!Object.hasOwn(nodes, id)) warning("diagram_orphan", "shape without a node", { node: id });
|
|
235
|
+
for (const id of Object.keys(diagram.edges)) if (!Object.hasOwn(flows, id)) warning("diagram_orphan", "edge without a flow", { flow: id });
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** 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?: { node?: string; flow?: string }) => void): void {
|
|
242
|
+
const defaults = outs.filter((flow) => (flows[flow] as Json).isDefault === true);
|
|
243
|
+
if (defaults.length > 1) error("default_invalid", "exclusive gateway can have only one default flow", { node: id });
|
|
244
|
+
for (const flow of outs) {
|
|
245
|
+
const raw = flows[flow] as Json;
|
|
246
|
+
if (raw.isDefault === true) {
|
|
247
|
+
if (raw.condition !== undefined) error("default_invalid", "default flow cannot have a condition", { flow });
|
|
248
|
+
} else if (raw.condition === undefined) {
|
|
249
|
+
error("condition_missing", "flow leaving an exclusive gateway needs a condition or isDefault", { flow });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface ConditionScope {
|
|
255
|
+
/** Input field codes; `null` when no input is declared and `$input` is not checked. */
|
|
256
|
+
readonly input: ReadonlySet<string> | null;
|
|
257
|
+
readonly nodes: ReadonlyMap<string, unknown>;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function checkCondition(condition: unknown, depth: number, scope: ConditionScope): string | null {
|
|
261
|
+
if (depth > PROCESS_LIMITS.conditionDepth) return `condition is nested deeper than ${PROCESS_LIMITS.conditionDepth}`;
|
|
262
|
+
if (!isObject(condition)) return "condition must be an object";
|
|
263
|
+
const keys = Object.keys(condition);
|
|
264
|
+
if (keys.length === 1 && (keys[0] === "all" || keys[0] === "any")) {
|
|
265
|
+
const list = condition[keys[0]];
|
|
266
|
+
if (!Array.isArray(list) || list.length === 0) return `${keys[0]} must be a non-empty list`;
|
|
267
|
+
for (const item of list) {
|
|
268
|
+
const problem = checkCondition(item, depth + 1, scope);
|
|
269
|
+
if (problem) return problem;
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
if (keys.length === 1 && keys[0] === "not") return checkCondition(condition.not, depth + 1, scope);
|
|
274
|
+
if (typeof condition.path !== "string" || !DATA_PATH.test(condition.path)) return "condition.path must look like $input.field, $vars.name or $nodes.Node.field";
|
|
275
|
+
const [root, first] = condition.path.split(".");
|
|
276
|
+
if (root === "$input" && scope.input && !scope.input.has(first!)) return `condition.path ${condition.path}: input has no field ${first}`;
|
|
277
|
+
if (root === "$nodes" && !scope.nodes.has(first!)) return `condition.path ${condition.path}: no node ${first}`;
|
|
278
|
+
if (typeof condition.op !== "string" || !OPS.has(condition.op as ConditionOp)) return `condition.op must be one of ${[...OPS].join(", ")}`;
|
|
279
|
+
const op = condition.op as ConditionOp;
|
|
280
|
+
if (keys.some((key) => key !== "path" && key !== "op" && key !== "value")) return "condition has unknown keys";
|
|
281
|
+
if (UNARY.has(op)) return condition.value === undefined ? null : `${op} takes no value`;
|
|
282
|
+
if (condition.value === undefined) return `${op} needs a value`;
|
|
283
|
+
if (op === "in" && !Array.isArray(condition.value)) return "in needs a list value";
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isDiagram(value: unknown): boolean {
|
|
288
|
+
if (!isObject(value) || !isObject(value.shapes) || !isObject(value.edges)) return false;
|
|
289
|
+
const shapesOk = Object.values(value.shapes).every((shape) => isObject(shape) && finite(shape.x) && finite(shape.y) && finite(shape.width) && finite(shape.height));
|
|
290
|
+
const edgesOk = Object.values(value.edges).every((edge) => isObject(edge) && Array.isArray(edge.waypoints) && edge.waypoints.every((point) => isObject(point) && finite(point.x) && finite(point.y)));
|
|
291
|
+
return shapesOk && edgesOk;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function walk(from: readonly string[], next: ReadonlyMap<string, readonly string[]>): Set<string> {
|
|
295
|
+
const seen = new Set(from);
|
|
296
|
+
const queue = [...from];
|
|
297
|
+
while (queue.length > 0) {
|
|
298
|
+
for (const to of next.get(queue.pop()!) ?? []) {
|
|
299
|
+
if (seen.has(to)) continue;
|
|
300
|
+
seen.add(to);
|
|
301
|
+
queue.push(to);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return seen;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** `true` if the findings block publishing. */
|
|
308
|
+
export function hasErrors(issues: readonly ProcessIssue[]): boolean {
|
|
309
|
+
return issues.some((issue) => issue.severity === "error");
|
|
310
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bazis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.124.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",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"./core/orm": "./core/orm/index.ts",
|
|
44
44
|
"./core/testing": "./core/testing/index.ts",
|
|
45
45
|
"./core/websocket": "./core/websocket/index.ts",
|
|
46
|
+
"./core/workflow": "./core/workflow/index.ts",
|
|
46
47
|
"./library/boundary": "./library/boundary/index.ts",
|
|
47
48
|
"./library/http-client": "./library/http-client/index.ts",
|
|
48
49
|
"./library/jsonapi": "./library/jsonapi/index.ts",
|
|
@@ -52,7 +53,8 @@
|
|
|
52
53
|
"./library/redaction": "./library/redaction/index.ts",
|
|
53
54
|
"./library/terminal": "./library/terminal/index.ts",
|
|
54
55
|
"./library/ui": "./library/ui/index.ts",
|
|
55
|
-
"./library/validation": "./library/validation/index.ts"
|
|
56
|
+
"./library/validation": "./library/validation/index.ts",
|
|
57
|
+
"./library/workflow": "./library/workflow/index.ts"
|
|
56
58
|
},
|
|
57
59
|
"bin": {
|
|
58
60
|
"bazis": "./cli/main.ts"
|