bazis 0.131.0 → 0.133.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 +27 -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 +66 -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 +35 -10
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +90 -28
- package/core/workflow/generated/deps.ts +15 -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 +4 -2
- package/core/workflow/runtime_module/access/Runtime.permissions.ts +5 -3
- package/core/workflow/runtime_module/background/WorkflowRetention.ts +3 -3
- package/core/workflow/runtime_module/background/WorkflowWorker.ts +2 -2
- package/core/workflow/runtime_module/events/Runtime.events.ts +6 -6
- package/core/workflow/runtime_module/http/Instances.controller.ts +30 -13
- package/core/workflow/runtime_module/internal/ChildCalls.ts +186 -0
- package/core/workflow/runtime_module/internal/GraphCache.ts +7 -1
- package/core/workflow/runtime_module/internal/RunLimits.ts +124 -0
- package/core/workflow/runtime_module/internal/Schedules.ts +2 -2
- package/core/workflow/runtime_module/internal/TimerStep.ts +11 -1
- package/core/workflow/runtime_module/internal/ToolStep.ts +43 -12
- package/core/workflow/runtime_module/internal/WorkflowEngine.ts +559 -123
- package/core/workflow/runtime_module/internal/WorkflowJobs.ts +39 -10
- 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 +34 -6
- package/core/workflow/runtime_module/model/WfJob.model.ts +29 -4
- package/core/workflow/runtime_module/model/WfSubscription.model.ts +1 -1
- package/core/workflow/runtime_module/model/WfToken.model.ts +15 -3
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +43 -4
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +56 -13
- package/core/workflow/tasks_module/events/TaskEvents.ts +4 -8
- 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 +4 -4
- package/library/openapi/codegen.ts +3 -0
- package/library/validation/types/ValidatorOptions.ts +2 -0
- package/library/workflow/ProcessDocument.ts +73 -23
- package/library/workflow/expressionSyntax.ts +243 -0
- package/library/workflow/expressions.ts +347 -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 +72 -0
- package/library/workflow/mermaid.ts +39 -13
- package/library/workflow/nodeKinds.ts +147 -17
- package/library/workflow/palette.ts +110 -0
- package/library/workflow/paths.ts +37 -6
- package/library/workflow/structure.ts +311 -0
- package/library/workflow/upgradeDocument.ts +84 -0
- package/library/workflow/validateProcess.ts +246 -52
- package/package.json +1 -1
- package/library/workflow/conditions.ts +0 -44
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import type { ProcessDiagram, ProcessDocument, ProcessFlow, ProcessNode, ProcessSettings } from "./ProcessDocument";
|
|
2
|
+
import type { Expr } from "./expressions";
|
|
3
|
+
import type { VariableSpec } from "./formFields";
|
|
4
|
+
import type { LocalizedText } from "./localized";
|
|
5
|
+
import { nodeKindOf } from "./nodeKinds";
|
|
6
|
+
|
|
7
|
+
/** One branch of a fork: the flow it leaves by and its steps. */
|
|
8
|
+
export interface StructureBranch {
|
|
9
|
+
readonly flow: string;
|
|
10
|
+
readonly title?: LocalizedText;
|
|
11
|
+
readonly condition?: Expr;
|
|
12
|
+
readonly isDefault?: boolean;
|
|
13
|
+
/** The flow from its last step into the merge or the end; `flow` itself for an empty branch. */
|
|
14
|
+
readonly exit: string;
|
|
15
|
+
readonly exitTitle?: LocalizedText;
|
|
16
|
+
readonly steps: readonly StructureStep[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A boundary event of a step and what it leads to. */
|
|
20
|
+
export interface StructureBoundary {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly node: ProcessNode;
|
|
23
|
+
readonly flow: string;
|
|
24
|
+
readonly title?: LocalizedText;
|
|
25
|
+
readonly steps: readonly StructureStep[];
|
|
26
|
+
/** The end its branch leads to; without it the branch comes back to the step after the host. */
|
|
27
|
+
readonly end?: { readonly id: string; readonly node: ProcessNode; readonly flow: string; readonly title?: LocalizedText };
|
|
28
|
+
/** The flow that comes back when the branch has steps and no end of its own. */
|
|
29
|
+
readonly exit?: string;
|
|
30
|
+
readonly exitTitle?: LocalizedText;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A step of the list: a single node, a loop around a list, an exclusive fork or a parallel block. */
|
|
34
|
+
export type StructureStep =
|
|
35
|
+
| {
|
|
36
|
+
readonly type: "loop";
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly node: ProcessNode;
|
|
39
|
+
/** The steps of an iteration. */
|
|
40
|
+
readonly body: readonly StructureStep[];
|
|
41
|
+
/** The end of the body. */
|
|
42
|
+
readonly bodyEnd: { readonly id: string; readonly node: ProcessNode };
|
|
43
|
+
readonly boundaries?: readonly StructureBoundary[];
|
|
44
|
+
readonly next: string;
|
|
45
|
+
readonly nextTitle?: LocalizedText;
|
|
46
|
+
}
|
|
47
|
+
| {
|
|
48
|
+
readonly type: "step";
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly node: ProcessNode;
|
|
51
|
+
readonly boundaries?: readonly StructureBoundary[];
|
|
52
|
+
/** The flow to the next step. */
|
|
53
|
+
readonly next: string;
|
|
54
|
+
readonly nextTitle?: LocalizedText;
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
readonly type: "choice" | "parallel";
|
|
58
|
+
readonly id: string;
|
|
59
|
+
readonly node: ProcessNode;
|
|
60
|
+
readonly branches: readonly StructureBranch[];
|
|
61
|
+
/** Where the branches meet; without it they all lead to the end, and the fork is the last step of its list. */
|
|
62
|
+
readonly merge?: { readonly id: string; readonly node: ProcessNode; readonly next: string; readonly nextTitle?: LocalizedText };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** A start of the list and the flow from it to the first step. */
|
|
66
|
+
export interface StructureStart {
|
|
67
|
+
readonly id: string;
|
|
68
|
+
readonly node: ProcessNode;
|
|
69
|
+
readonly flow: string;
|
|
70
|
+
readonly title?: LocalizedText;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A process as a designer draws it: starts, a list of steps, an end. */
|
|
74
|
+
export interface ProcessStructure {
|
|
75
|
+
readonly variables?: readonly VariableSpec[];
|
|
76
|
+
readonly settings?: ProcessSettings;
|
|
77
|
+
readonly diagram?: ProcessDiagram;
|
|
78
|
+
readonly extensions?: Readonly<Record<string, unknown>>;
|
|
79
|
+
readonly schemaVersion: ProcessDocument["schemaVersion"];
|
|
80
|
+
readonly starts: readonly StructureStart[];
|
|
81
|
+
readonly steps: readonly StructureStep[];
|
|
82
|
+
readonly end: { readonly id: string; readonly node: ProcessNode };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class NotStructured extends Error {}
|
|
86
|
+
|
|
87
|
+
const titleOf = (flow: ProcessFlow) => (flow.title === undefined ? {} : { title: flow.title });
|
|
88
|
+
|
|
89
|
+
/** Reads a graph as a tree; a shape outside the subset throws {@link NotStructured}. */
|
|
90
|
+
class Reader {
|
|
91
|
+
private readonly outgoing = new Map<string, (readonly [string, ProcessFlow])[]>();
|
|
92
|
+
private readonly incoming = new Map<string, (readonly [string, ProcessFlow])[]>();
|
|
93
|
+
private readonly boundaries = new Map<string, (readonly [string, ProcessNode])[]>();
|
|
94
|
+
private readonly visited = new Set<string>();
|
|
95
|
+
|
|
96
|
+
constructor(private readonly document: ProcessDocument) {
|
|
97
|
+
for (const [id, flow] of Object.entries(document.flows)) {
|
|
98
|
+
this.outgoing.set(flow.source, [...this.outgoing.get(flow.source) ?? [], [id, flow]]);
|
|
99
|
+
this.incoming.set(flow.target, [...this.incoming.get(flow.target) ?? [], [id, flow]]);
|
|
100
|
+
}
|
|
101
|
+
for (const [id, node] of Object.entries(document.nodes)) {
|
|
102
|
+
if (node.attachedTo !== undefined) this.boundaries.set(node.attachedTo, [...this.boundaries.get(node.attachedTo) ?? [], [id, node]]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private node(id: string): ProcessNode {
|
|
107
|
+
const node = this.document.nodes[id];
|
|
108
|
+
if (!node) throw new NotStructured();
|
|
109
|
+
return node;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private single(id: string): readonly [string, ProcessFlow] {
|
|
113
|
+
const flows = this.outgoing.get(id) ?? [];
|
|
114
|
+
if (flows.length !== 1) throw new NotStructured();
|
|
115
|
+
return flows[0]!;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private visit(id: string): void {
|
|
119
|
+
if (this.visited.has(id)) throw new NotStructured();
|
|
120
|
+
this.visited.add(id);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
read(): ProcessStructure {
|
|
124
|
+
const { nodes: _nodes, flows: _flows, ...rest } = this.document;
|
|
125
|
+
const entries = Object.entries(this.document.nodes);
|
|
126
|
+
const starts = entries.filter(([, node]) => nodeKindOf(node.kind)?.role === "start");
|
|
127
|
+
if (starts.length === 0) throw new NotStructured();
|
|
128
|
+
const shaped = starts.map(([id, node]) => {
|
|
129
|
+
const [flow, { target, ...data }] = this.single(id);
|
|
130
|
+
return { start: { id, node, flow, ...titleOf({ ...data, target }) }, target };
|
|
131
|
+
});
|
|
132
|
+
const first = shaped[0]!.target;
|
|
133
|
+
if (shaped.some(({ target }) => target !== first)) throw new NotStructured();
|
|
134
|
+
for (const { start } of shaped) this.visit(start.id);
|
|
135
|
+
const { steps, stop } = this.sequence(first);
|
|
136
|
+
const end = this.node(stop);
|
|
137
|
+
if (nodeKindOf(end.kind)?.role !== "end") throw new NotStructured();
|
|
138
|
+
this.visited.add(stop);
|
|
139
|
+
if (this.visited.size !== entries.length) throw new NotStructured();
|
|
140
|
+
return { ...rest, starts: shaped.map(({ start }) => start), steps, end: { id: stop, node: end } };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Steps from `from` up to a merge or an end, which is not taken. */
|
|
144
|
+
private sequence(from: string): { readonly steps: StructureStep[]; readonly stop: string; readonly exit?: { readonly flow: string; readonly title?: LocalizedText } } {
|
|
145
|
+
const steps: StructureStep[] = [];
|
|
146
|
+
let current = from;
|
|
147
|
+
let exit: { flow: string; title?: LocalizedText } | undefined;
|
|
148
|
+
for (;;) {
|
|
149
|
+
const node = this.node(current);
|
|
150
|
+
const role = nodeKindOf(node.kind)?.role;
|
|
151
|
+
if (role === "end") return { steps, stop: current, ...(exit === undefined ? {} : { exit }) };
|
|
152
|
+
if (role === "activity") {
|
|
153
|
+
this.visit(current);
|
|
154
|
+
const [flowId, flow] = this.single(current);
|
|
155
|
+
const boundaries = (this.boundaries.get(current) ?? []).map(([id, boundary]) => this.boundary(id, boundary, flow.target));
|
|
156
|
+
const common = { id: current, node, ...(boundaries.length === 0 ? {} : { boundaries }), next: flowId, ...(flow.title === undefined ? {} : { nextTitle: flow.title }) };
|
|
157
|
+
steps.push(node.kind === "loop" ? { type: "loop", ...common, ...this.body(current) } : { type: "step", ...common });
|
|
158
|
+
exit = { flow: flowId, ...(flow.title === undefined ? {} : { title: flow.title }) };
|
|
159
|
+
current = flow.target;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (role !== "gateway") throw new NotStructured();
|
|
163
|
+
const outs = this.outgoing.get(current) ?? [];
|
|
164
|
+
// A join: whoever walks here stops; the fork that owns it goes on from it.
|
|
165
|
+
if (outs.length === 1) return { steps, stop: current, ...(exit === undefined ? {} : { exit }) };
|
|
166
|
+
const { step, stop } = this.fork(current, node, outs);
|
|
167
|
+
steps.push(step);
|
|
168
|
+
if (step.merge === undefined) return { steps, stop };
|
|
169
|
+
exit = { flow: step.merge.next, ...(step.merge.nextTitle === undefined ? {} : { title: step.merge.nextTitle }) };
|
|
170
|
+
current = this.document.flows[step.merge.next]!.target;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The body of a loop: a list from its one entry to its one end. */
|
|
175
|
+
private body(loop: string): { readonly body: StructureStep[]; readonly bodyEnd: { readonly id: string; readonly node: ProcessNode } } {
|
|
176
|
+
const members = Object.entries(this.document.nodes).filter(([, node]) => node.parent === loop && node.attachedTo === undefined);
|
|
177
|
+
const entries = members.filter(([id]) => (this.incoming.get(id)?.length ?? 0) === 0);
|
|
178
|
+
if (entries.length !== 1) throw new NotStructured();
|
|
179
|
+
const { steps, stop } = this.sequence(entries[0]![0]);
|
|
180
|
+
const end = this.node(stop);
|
|
181
|
+
if (nodeKindOf(end.kind)?.role !== "end" || end.parent !== loop || steps.length === 0) throw new NotStructured();
|
|
182
|
+
this.visited.add(stop);
|
|
183
|
+
return { body: steps, bodyEnd: { id: stop, node: end } };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private fork(id: string, node: ProcessNode, outs: readonly (readonly [string, ProcessFlow])[]): { readonly step: Extract<StructureStep, { type: "choice" | "parallel" }>; readonly stop: string } {
|
|
187
|
+
if ((this.incoming.get(id)?.length ?? 0) !== 1) throw new NotStructured();
|
|
188
|
+
this.visit(id);
|
|
189
|
+
let merge: string | undefined;
|
|
190
|
+
const branches = outs.map(([flowId, flow]): StructureBranch => {
|
|
191
|
+
const { steps, stop, exit } = this.sequence(flow.target);
|
|
192
|
+
if (merge !== undefined && merge !== stop) throw new NotStructured();
|
|
193
|
+
merge = stop;
|
|
194
|
+
return {
|
|
195
|
+
flow: flowId,
|
|
196
|
+
...titleOf(flow),
|
|
197
|
+
...(flow.condition === undefined ? {} : { condition: flow.condition }),
|
|
198
|
+
...(flow.isDefault === undefined ? {} : { isDefault: flow.isDefault }),
|
|
199
|
+
exit: exit?.flow ?? flowId,
|
|
200
|
+
...(exit?.title === undefined ? {} : { exitTitle: exit.title }),
|
|
201
|
+
steps,
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
const type = node.kind === "parallelGateway" ? "parallel" : "choice";
|
|
205
|
+
const mergeNode = this.node(merge!);
|
|
206
|
+
if (nodeKindOf(mergeNode.kind)?.role === "end") return { step: { type, id, node, branches }, stop: merge! };
|
|
207
|
+
if (mergeNode.kind !== node.kind) throw new NotStructured();
|
|
208
|
+
this.visit(merge!);
|
|
209
|
+
const [next, nextFlow] = this.single(merge!);
|
|
210
|
+
return { step: { type, id, node, branches, merge: { id: merge!, node: mergeNode, next, ...(nextFlow.title === undefined ? {} : { nextTitle: nextFlow.title }) } }, stop: merge! };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private boundary(id: string, node: ProcessNode, after: string): StructureBoundary {
|
|
214
|
+
this.visit(id);
|
|
215
|
+
const [flow, data] = this.single(id);
|
|
216
|
+
const base = { id, node, flow, ...titleOf(data) };
|
|
217
|
+
if (data.target === after) return { ...base, steps: [] };
|
|
218
|
+
const { steps, stop, exit } = this.sequence(data.target);
|
|
219
|
+
const stopNode = this.node(stop);
|
|
220
|
+
if (stop === after) {
|
|
221
|
+
if (exit === undefined) throw new NotStructured();
|
|
222
|
+
return { ...base, steps, exit: exit.flow, ...(exit.title === undefined ? {} : { exitTitle: exit.title }) };
|
|
223
|
+
}
|
|
224
|
+
// An end: its own, or the end of the process. A list without steps leads to it by the boundary's own flow.
|
|
225
|
+
if (nodeKindOf(stopNode.kind)?.role !== "end") throw new NotStructured();
|
|
226
|
+
this.visited.add(stop);
|
|
227
|
+
const last = exit ?? { flow, ...titleOf(data) };
|
|
228
|
+
return { ...base, steps, end: { id: stop, node: stopNode, flow: last.flow, ...(last.title === undefined ? {} : { title: last.title }) } };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* A graph as a tree of steps, or `undefined` when it is outside the subset a
|
|
234
|
+
* list can draw (WORKFLOW-002 §3.6): sequences, an exclusive gateway with one
|
|
235
|
+
* merge, a parallel gateway with a join, a loop, boundary events on steps.
|
|
236
|
+
*/
|
|
237
|
+
export function toStructure(document: ProcessDocument): ProcessStructure | undefined {
|
|
238
|
+
try {
|
|
239
|
+
return new Reader(document).read();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (error instanceof NotStructured) return undefined;
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Whether a designer can edit the process as a list. Otherwise it shows the graph read only. */
|
|
247
|
+
export function isStructured(document: ProcessDocument): boolean {
|
|
248
|
+
return toStructure(document) !== undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** A tree of steps as a graph: `fromStructure(toStructure(d))` has the nodes and flows of `d`. */
|
|
252
|
+
export function fromStructure(structure: ProcessStructure): ProcessDocument {
|
|
253
|
+
const { starts, steps, end, ...rest } = structure;
|
|
254
|
+
const nodes: Record<string, ProcessNode> = {};
|
|
255
|
+
const flows: Record<string, ProcessFlow> = {};
|
|
256
|
+
const flow = (id: string, source: string, target: string, data: Partial<ProcessFlow> = {}) => {
|
|
257
|
+
flows[id] = { source, target, ...data };
|
|
258
|
+
};
|
|
259
|
+
const titled = (title: LocalizedText | undefined) => (title === undefined ? {} : { title });
|
|
260
|
+
|
|
261
|
+
/** The first node of a list, or where it goes when it is empty. */
|
|
262
|
+
const entry = (list: readonly StructureStep[], after: string) => list[0]?.id ?? after;
|
|
263
|
+
|
|
264
|
+
/** Writes a list that leads to `after`; `leave` writes the flow out of its last step. */
|
|
265
|
+
const write = (list: readonly StructureStep[], after: string, leave?: { readonly flow: string; readonly title?: LocalizedText }) => {
|
|
266
|
+
list.forEach((step, index) => {
|
|
267
|
+
const target = index + 1 < list.length ? list[index + 1]!.id : after;
|
|
268
|
+
const last = index + 1 === list.length && leave !== undefined;
|
|
269
|
+
nodes[step.id] = step.node;
|
|
270
|
+
if (step.type === "step" || step.type === "loop") {
|
|
271
|
+
flow(last ? leave.flow : step.next, step.id, target, titled(last ? leave.title : step.nextTitle));
|
|
272
|
+
for (const boundary of step.boundaries ?? []) writeBoundary(boundary, step, target);
|
|
273
|
+
if (step.type === "loop") {
|
|
274
|
+
write(step.body, step.bodyEnd.id);
|
|
275
|
+
nodes[step.bodyEnd.id] = step.bodyEnd.node;
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const meet = step.merge?.id ?? target;
|
|
280
|
+
for (const branch of step.branches) {
|
|
281
|
+
const { flow: id, title, condition, isDefault, steps: inner, exit, exitTitle } = branch;
|
|
282
|
+
flow(id, step.id, entry(inner, meet), { ...titled(title), ...(condition === undefined ? {} : { condition }), ...(isDefault === undefined ? {} : { isDefault }) });
|
|
283
|
+
write(inner, meet, inner.length === 0 ? undefined : { flow: exit, ...(exitTitle === undefined ? {} : { title: exitTitle }) });
|
|
284
|
+
}
|
|
285
|
+
if (step.merge === undefined) return;
|
|
286
|
+
nodes[step.merge.id] = step.merge.node;
|
|
287
|
+
flow(last ? leave.flow : step.merge.next, step.merge.id, target, titled(last ? leave.title : step.merge.nextTitle));
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const writeBoundary = (boundary: StructureBoundary, _host: StructureStep, after: string) => {
|
|
292
|
+
nodes[boundary.id] = boundary.node;
|
|
293
|
+
const target = boundary.end?.id ?? after;
|
|
294
|
+
flow(boundary.flow, boundary.id, entry(boundary.steps, target), titled(boundary.title));
|
|
295
|
+
if (boundary.end !== undefined) nodes[boundary.end.id] = boundary.end.node;
|
|
296
|
+
const leave = boundary.end !== undefined
|
|
297
|
+
? { flow: boundary.end.flow, ...(boundary.end.title === undefined ? {} : { title: boundary.end.title }) }
|
|
298
|
+
: boundary.exit === undefined ? undefined : { flow: boundary.exit, ...(boundary.exitTitle === undefined ? {} : { title: boundary.exitTitle }) };
|
|
299
|
+
write(boundary.steps, target, boundary.steps.length === 0 ? undefined : leave);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const first = entry(steps, end.id);
|
|
303
|
+
for (const start of starts) {
|
|
304
|
+
nodes[start.id] = start.node;
|
|
305
|
+
flow(start.flow, start.id, first, titled(start.title));
|
|
306
|
+
}
|
|
307
|
+
write(steps, end.id);
|
|
308
|
+
nodes[end.id] = end.node;
|
|
309
|
+
return { ...rest, nodes, flows };
|
|
310
|
+
}
|
|
311
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Expr } from "./expressions";
|
|
2
|
+
import type { VariableSpec } from "./formFields";
|
|
3
|
+
import { isPath } from "./paths";
|
|
4
|
+
import { PROCESS_SCHEMA_VERSION, type ProcessDocument, type ProcessFlow, type ProcessNode } from "./ProcessDocument";
|
|
5
|
+
|
|
6
|
+
/** The version 1 of the format, for {@link upgradeDocument}. */
|
|
7
|
+
export const PROCESS_SCHEMA_VERSION_V1 = "bazis.workflow/v1";
|
|
8
|
+
|
|
9
|
+
/** A condition of version 1: a tree of `all` / `any` / `not` and comparisons. */
|
|
10
|
+
export type ConditionV1 =
|
|
11
|
+
| { readonly all: readonly ConditionV1[] }
|
|
12
|
+
| { readonly any: readonly ConditionV1[] }
|
|
13
|
+
| { readonly not: ConditionV1 }
|
|
14
|
+
| { readonly path: string; readonly op: string; readonly value?: unknown };
|
|
15
|
+
|
|
16
|
+
export interface FormFieldV1 {
|
|
17
|
+
readonly code: string;
|
|
18
|
+
readonly type: string;
|
|
19
|
+
readonly title?: string;
|
|
20
|
+
readonly required?: boolean;
|
|
21
|
+
readonly values?: readonly (string | number)[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ProcessDocumentV1 {
|
|
25
|
+
readonly schemaVersion: typeof PROCESS_SCHEMA_VERSION_V1;
|
|
26
|
+
readonly input?: { readonly fields: readonly FormFieldV1[] };
|
|
27
|
+
readonly nodes: Readonly<Record<string, { readonly kind: string; readonly title: string; readonly attachedTo?: string; readonly config?: Readonly<Record<string, unknown>> }>>;
|
|
28
|
+
readonly flows: Readonly<Record<string, { readonly source: string; readonly target: string; readonly title?: string; readonly condition?: ConditionV1; readonly isDefault?: boolean }>>;
|
|
29
|
+
readonly diagram?: ProcessDocument["diagram"];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
33
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A document of version 1 as version 2: `input` becomes `in` variables, form
|
|
38
|
+
* fields take `name` instead of `code`, conditions become expressions, titles
|
|
39
|
+
* stay as they are. A pure function. A draft may hold errors, so what is not of
|
|
40
|
+
* the expected shape is left as it is for the validation to report; anything
|
|
41
|
+
* that is not version 1 comes back unchanged.
|
|
42
|
+
*/
|
|
43
|
+
export function upgradeDocument(document: ProcessDocumentV1): ProcessDocument;
|
|
44
|
+
export function upgradeDocument(document: unknown): unknown;
|
|
45
|
+
export function upgradeDocument(document: unknown): unknown {
|
|
46
|
+
if (!isObject(document) || document.schemaVersion !== PROCESS_SCHEMA_VERSION_V1) return document;
|
|
47
|
+
const { input, nodes, flows, ...rest } = document;
|
|
48
|
+
const fields = isObject(input) && Array.isArray(input.fields) ? input.fields : undefined;
|
|
49
|
+
return {
|
|
50
|
+
...rest,
|
|
51
|
+
schemaVersion: PROCESS_SCHEMA_VERSION,
|
|
52
|
+
...(fields === undefined ? {} : { variables: fields.map((field) => upgradeField(field, "in")) }),
|
|
53
|
+
nodes: isObject(nodes) ? Object.fromEntries(Object.entries(nodes).map(([id, node]) => [id, upgradeNode(node)])) : nodes,
|
|
54
|
+
flows: isObject(flows) ? Object.fromEntries(Object.entries(flows).map(([id, flow]) => [id, upgradeFlow(flow)])) : flows,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function upgradeField(field: unknown, direction?: VariableSpec["direction"]): unknown {
|
|
59
|
+
if (!isObject(field)) return field;
|
|
60
|
+
const { code, ...rest } = field;
|
|
61
|
+
return { name: code, ...rest, ...(direction === undefined ? {} : { direction }) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function upgradeNode(node: unknown): ProcessNode | unknown {
|
|
65
|
+
if (!isObject(node) || !isObject(node.config) || !isObject(node.config.form) || !Array.isArray(node.config.form.fields)) return node;
|
|
66
|
+
return { ...node, config: { ...node.config, form: { ...node.config.form, fields: node.config.form.fields.map((field) => upgradeField(field)) } } };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function upgradeFlow(flow: unknown): ProcessFlow | unknown {
|
|
70
|
+
if (!isObject(flow) || flow.condition === undefined) return flow;
|
|
71
|
+
return { ...flow, condition: upgradeCondition(flow.condition) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `all` → `and`, `any` → `or`, `not` → `not`, a comparison → a call with the path first. */
|
|
75
|
+
export function upgradeCondition(condition: unknown): Expr {
|
|
76
|
+
if (!isObject(condition)) return condition as Expr;
|
|
77
|
+
if (Array.isArray(condition.all)) return { op: "and", args: condition.all.map(upgradeCondition) };
|
|
78
|
+
if (Array.isArray(condition.any)) return { op: "or", args: condition.any.map(upgradeCondition) };
|
|
79
|
+
if (condition.not !== undefined) return { op: "not", args: [upgradeCondition(condition.not)] };
|
|
80
|
+
if (typeof condition.path !== "string" || typeof condition.op !== "string") return condition as Expr;
|
|
81
|
+
const path = { path: condition.path };
|
|
82
|
+
if (condition.value === undefined) return { op: condition.op, args: [path] } as Expr;
|
|
83
|
+
return { op: condition.op, args: [path, isPath(condition.value) ? { path: condition.value } : condition.value as Expr] } as Expr;
|
|
84
|
+
}
|