@bpmnkit/core 0.0.16 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/bpmn/bpmn-builder.js +1 -1
- package/dist/bpmn/bpmn-model.d.ts +2 -0
- package/dist/bpmn/bpmn-parser.js +2 -1
- package/dist/bpmn/bpmn-serializer.js +4 -1
- package/dist/bpmn/input-validation.d.ts +85 -0
- package/dist/bpmn/input-validation.js +497 -0
- package/dist/bpmn/optimize/variable-flow.js +78 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/@bpmnkit/core)
|
|
7
7
|
[](https://github.com/bpmnkit/monorepo/blob/main/LICENSE)
|
|
8
8
|
[](https://github.com/bpmnkit/monorepo)
|
|
9
|
+
[](https://github.com/bpmnkit/monorepo)
|
|
10
|
+
[](https://github.com/bpmnkit/monorepo)
|
|
9
11
|
|
|
10
12
|
[Website](https://bpmnkit.com) · [Documentation](https://docs.bpmnkit.com) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/core/CHANGELOG.md)
|
|
11
13
|
</div>
|
|
@@ -133,7 +133,7 @@ function buildMultiInstance(options) {
|
|
|
133
133
|
children: [],
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
|
-
return { extensionElements: extChildren };
|
|
136
|
+
return { isSequential: options.isSequential || undefined, extensionElements: extChildren };
|
|
137
137
|
}
|
|
138
138
|
function buildAdHocLoopCharacteristics(lc) {
|
|
139
139
|
const attrs = {
|
|
@@ -88,6 +88,8 @@ export interface BpmnCompensateEventDefinition {
|
|
|
88
88
|
export type BpmnEventDefinition = BpmnTimerEventDefinition | BpmnErrorEventDefinition | BpmnEscalationEventDefinition | BpmnMessageEventDefinition | BpmnSignalEventDefinition | BpmnConditionalEventDefinition | BpmnLinkEventDefinition | BpmnCancelEventDefinition | BpmnTerminateEventDefinition | BpmnCompensateEventDefinition;
|
|
89
89
|
/** Multi-instance loop configuration attached to a task or sub-process. */
|
|
90
90
|
export interface BpmnMultiInstanceLoopCharacteristics {
|
|
91
|
+
/** When true, iterations run one at a time (sequential). When false or absent, runs in parallel. */
|
|
92
|
+
isSequential?: boolean;
|
|
91
93
|
extensionElements: XmlElement[];
|
|
92
94
|
}
|
|
93
95
|
/** A FEEL condition expression on a sequence flow outgoing from a gateway. */
|
package/dist/bpmn/bpmn-parser.js
CHANGED
|
@@ -230,7 +230,8 @@ function parseLoopCharacteristics(element) {
|
|
|
230
230
|
const loopEl = findChild(element, "multiInstanceLoopCharacteristics");
|
|
231
231
|
if (!loopEl)
|
|
232
232
|
return undefined;
|
|
233
|
-
|
|
233
|
+
const isSequential = loopEl.attributes.isSequential === "true" ? true : undefined;
|
|
234
|
+
return { isSequential, extensionElements: parseExtensionElements(loopEl) };
|
|
234
235
|
}
|
|
235
236
|
// ---------------------------------------------------------------------------
|
|
236
237
|
// Flow elements
|
|
@@ -137,8 +137,11 @@ function serializeExtensionElements(extensions, bp) {
|
|
|
137
137
|
function serializeLoopCharacteristics(lc, bp) {
|
|
138
138
|
if (!lc)
|
|
139
139
|
return [];
|
|
140
|
+
const attrs = {};
|
|
141
|
+
if (lc.isSequential)
|
|
142
|
+
attrs.isSequential = "true";
|
|
140
143
|
return [
|
|
141
|
-
el(`${bp}:multiInstanceLoopCharacteristics`,
|
|
144
|
+
el(`${bp}:multiInstanceLoopCharacteristics`, attrs, [
|
|
142
145
|
...serializeExtensionElements(lc.extensionElements, bp),
|
|
143
146
|
]),
|
|
144
147
|
];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { BpmnDefinitions } from "./bpmn-model.js";
|
|
2
|
+
/** Supported variable types for input validation. */
|
|
3
|
+
export type ValidationVariableType = "string" | "number" | "boolean" | "context" | "list" | "any";
|
|
4
|
+
/** A single input variable definition used to build a validation DMN. */
|
|
5
|
+
export interface InputVariableDef {
|
|
6
|
+
/** Variable name (must be a valid FEEL identifier). */
|
|
7
|
+
name: string;
|
|
8
|
+
/** Expected type. */
|
|
9
|
+
type: ValidationVariableType;
|
|
10
|
+
/** When true, generates a required check row. */
|
|
11
|
+
required: boolean;
|
|
12
|
+
/** Minimum numeric value (number type only). */
|
|
13
|
+
min?: number;
|
|
14
|
+
/** Maximum numeric value (number type only). */
|
|
15
|
+
max?: number;
|
|
16
|
+
/** Minimum string length (string type only). */
|
|
17
|
+
minLength?: number;
|
|
18
|
+
/** Maximum string length (string type only). */
|
|
19
|
+
maxLength?: number;
|
|
20
|
+
/** Regex pattern the value must match (string type only). */
|
|
21
|
+
pattern?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Location of a validation structure wired after a start event. */
|
|
24
|
+
export interface ValidationStructure {
|
|
25
|
+
businessRuleTaskId: string;
|
|
26
|
+
decisionId: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns the deterministic DMN decision ID for the validation decision linked
|
|
30
|
+
* to a given start event.
|
|
31
|
+
*/
|
|
32
|
+
export declare function validationDecisionId(startEventId: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Builds a validation DMN XML string from a list of variable definitions.
|
|
35
|
+
*
|
|
36
|
+
* Uses the Collect hit policy so every violated rule contributes an error
|
|
37
|
+
* string to the `validationErrors` list. An empty list means valid input.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildValidationDmn(startEventId: string, variables: InputVariableDef[]): string;
|
|
40
|
+
/**
|
|
41
|
+
* Detects whether a validation structure (Business Rule Task → XOR gateway →
|
|
42
|
+
* Error End Event) is already wired immediately after the given start event.
|
|
43
|
+
*
|
|
44
|
+
* Returns `{ businessRuleTaskId, decisionId }` on success, or `null` if no
|
|
45
|
+
* validation structure is found.
|
|
46
|
+
*/
|
|
47
|
+
export declare function findValidationStructure(defs: BpmnDefinitions, startEventId: string): ValidationStructure | null;
|
|
48
|
+
/**
|
|
49
|
+
* Inserts the standard validation structure after a start event:
|
|
50
|
+
*
|
|
51
|
+
* ```
|
|
52
|
+
* [Start Event] → [Business Rule Task] → [XOR Gateway]
|
|
53
|
+
* ├── = count(validationErrors) = 0 → [original next]
|
|
54
|
+
* └── (default) → [Error End Event: VALIDATION_FAILED]
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* If the start event has an existing outgoing flow, that flow is redirected to
|
|
58
|
+
* originate from the XOR gateway (valid path) with the condition
|
|
59
|
+
* `= count(validationErrors) = 0`. The start event now connects to the Business
|
|
60
|
+
* Rule Task instead.
|
|
61
|
+
*
|
|
62
|
+
* If the start event has no outgoing flow the validation elements are inserted
|
|
63
|
+
* with only the error path connected; the caller can wire the valid path later.
|
|
64
|
+
*
|
|
65
|
+
* @returns Updated `BpmnDefinitions` — the original is not mutated.
|
|
66
|
+
*/
|
|
67
|
+
export declare function insertValidationStructure(defs: BpmnDefinitions, startEventId: string, decisionId: string): BpmnDefinitions;
|
|
68
|
+
/**
|
|
69
|
+
* Removes the validation structure wired after the given start event, if any.
|
|
70
|
+
*
|
|
71
|
+
* Restores the start event's outgoing connection to the element that the XOR
|
|
72
|
+
* gateway's valid path was pointing to. The generated error definition is also
|
|
73
|
+
* removed from `defs.errors`.
|
|
74
|
+
*
|
|
75
|
+
* @returns Updated `BpmnDefinitions` — the original is not mutated.
|
|
76
|
+
*/
|
|
77
|
+
export declare function removeValidationStructure(defs: BpmnDefinitions, startEventId: string): BpmnDefinitions;
|
|
78
|
+
/**
|
|
79
|
+
* Parses a validation DMN XML and returns the input column expression names.
|
|
80
|
+
* Used by the Process Runner to display expected variable hints.
|
|
81
|
+
*
|
|
82
|
+
* Returns an empty array on any parse error.
|
|
83
|
+
*/
|
|
84
|
+
export declare function getValidationInputNames(dmnXml: string): string[];
|
|
85
|
+
//# sourceMappingURL=input-validation.d.ts.map
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { DecisionTableBuilder } from "../dmn/dmn-builder.js";
|
|
2
|
+
import { parseDmn } from "../dmn/dmn-parser.js";
|
|
3
|
+
import { generateId } from "../types/id-generator.js";
|
|
4
|
+
import { getZeebeExtensions } from "./utils.js";
|
|
5
|
+
import { zeebeExtensionsToXmlElements } from "./zeebe-extensions.js";
|
|
6
|
+
// ── Decision ID ───────────────────────────────────────────────────────────────
|
|
7
|
+
/**
|
|
8
|
+
* Returns the deterministic DMN decision ID for the validation decision linked
|
|
9
|
+
* to a given start event.
|
|
10
|
+
*/
|
|
11
|
+
export function validationDecisionId(startEventId) {
|
|
12
|
+
return `${startEventId}_inputValidation`;
|
|
13
|
+
}
|
|
14
|
+
// ── DMN factory ───────────────────────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Builds a validation DMN XML string from a list of variable definitions.
|
|
17
|
+
*
|
|
18
|
+
* Uses the Collect hit policy so every violated rule contributes an error
|
|
19
|
+
* string to the `validationErrors` list. An empty list means valid input.
|
|
20
|
+
*/
|
|
21
|
+
export function buildValidationDmn(startEventId, variables) {
|
|
22
|
+
const decisionId = validationDecisionId(startEventId);
|
|
23
|
+
const builder = new DecisionTableBuilder(decisionId).name("Input Validation").hitPolicy("COLLECT");
|
|
24
|
+
for (const v of variables) {
|
|
25
|
+
builder.input({
|
|
26
|
+
label: v.name,
|
|
27
|
+
expression: v.name,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
builder.output({ label: "Error", name: "error", typeRef: "string" });
|
|
31
|
+
for (let i = 0; i < variables.length; i++) {
|
|
32
|
+
const v = variables[i];
|
|
33
|
+
if (!v)
|
|
34
|
+
continue;
|
|
35
|
+
/** Build a rule that fires (on violation) for variable at index i. */
|
|
36
|
+
const addRow = (entry, message) => {
|
|
37
|
+
const inputs = variables.map((_, j) => (j === i ? entry : ""));
|
|
38
|
+
builder.rule({ inputs, outputs: [`"${message}"`] });
|
|
39
|
+
};
|
|
40
|
+
if (v.required) {
|
|
41
|
+
addRow("null", `${v.name} is required`);
|
|
42
|
+
}
|
|
43
|
+
if (v.type === "string") {
|
|
44
|
+
addRow("not(instance of string)", `${v.name} must be a string`);
|
|
45
|
+
}
|
|
46
|
+
else if (v.type === "number") {
|
|
47
|
+
addRow("not(instance of number)", `${v.name} must be a number`);
|
|
48
|
+
}
|
|
49
|
+
else if (v.type === "boolean") {
|
|
50
|
+
addRow("not(instance of boolean)", `${v.name} must be a boolean`);
|
|
51
|
+
}
|
|
52
|
+
else if (v.type === "context") {
|
|
53
|
+
addRow("not(instance of context)", `${v.name} must be a context`);
|
|
54
|
+
}
|
|
55
|
+
else if (v.type === "list") {
|
|
56
|
+
addRow("not(instance of list)", `${v.name} must be a list`);
|
|
57
|
+
}
|
|
58
|
+
// "any" accepts all types — no type-check rule generated
|
|
59
|
+
if (v.type === "string") {
|
|
60
|
+
if (v.minLength !== undefined) {
|
|
61
|
+
addRow(`string length(?) < ${v.minLength}`, `${v.name} must be at least ${v.minLength} characters`);
|
|
62
|
+
}
|
|
63
|
+
if (v.maxLength !== undefined) {
|
|
64
|
+
addRow(`string length(?) > ${v.maxLength}`, `${v.name} must be at most ${v.maxLength} characters`);
|
|
65
|
+
}
|
|
66
|
+
if (v.pattern) {
|
|
67
|
+
addRow(`not(matches(?, "${v.pattern}"))`, `${v.name} has invalid format`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (v.type === "number") {
|
|
71
|
+
if (v.min !== undefined) {
|
|
72
|
+
addRow(`< ${v.min}`, `${v.name} must be >= ${v.min}`);
|
|
73
|
+
}
|
|
74
|
+
if (v.max !== undefined) {
|
|
75
|
+
addRow(`> ${v.max}`, `${v.name} must be <= ${v.max}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return builder.toXml();
|
|
80
|
+
}
|
|
81
|
+
// ── BPMN structure detection ──────────────────────────────────────────────────
|
|
82
|
+
/** Returns the process that contains the given element id, or undefined. */
|
|
83
|
+
function findProcessFor(defs, elementId) {
|
|
84
|
+
return defs.processes.find((p) => p.flowElements.some((e) => e.id === elementId));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Detects whether a validation structure (Business Rule Task → XOR gateway →
|
|
88
|
+
* Error End Event) is already wired immediately after the given start event.
|
|
89
|
+
*
|
|
90
|
+
* Returns `{ businessRuleTaskId, decisionId }` on success, or `null` if no
|
|
91
|
+
* validation structure is found.
|
|
92
|
+
*/
|
|
93
|
+
export function findValidationStructure(defs, startEventId) {
|
|
94
|
+
const process = findProcessFor(defs, startEventId);
|
|
95
|
+
if (!process)
|
|
96
|
+
return null;
|
|
97
|
+
for (const flow of process.sequenceFlows) {
|
|
98
|
+
if (flow.sourceRef !== startEventId)
|
|
99
|
+
continue;
|
|
100
|
+
const target = process.flowElements.find((e) => e.id === flow.targetRef);
|
|
101
|
+
if (!target || target.type !== "businessRuleTask")
|
|
102
|
+
continue;
|
|
103
|
+
const ext = getZeebeExtensions(target.extensionElements);
|
|
104
|
+
if (ext.calledDecision?.resultVariable !== "validationErrors")
|
|
105
|
+
continue;
|
|
106
|
+
return { businessRuleTaskId: target.id, decisionId: ext.calledDecision.decisionId };
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
// ── BPMN structure insertion ──────────────────────────────────────────────────
|
|
111
|
+
/** Standard element dimensions used when placing new validation elements. */
|
|
112
|
+
const SIZES = {
|
|
113
|
+
event: { w: 36, h: 36 },
|
|
114
|
+
task: { w: 100, h: 80 },
|
|
115
|
+
gateway: { w: 50, h: 50 },
|
|
116
|
+
};
|
|
117
|
+
const H_GAP = 80;
|
|
118
|
+
const V_GAP = 80;
|
|
119
|
+
/**
|
|
120
|
+
* Inserts the standard validation structure after a start event:
|
|
121
|
+
*
|
|
122
|
+
* ```
|
|
123
|
+
* [Start Event] → [Business Rule Task] → [XOR Gateway]
|
|
124
|
+
* ├── = count(validationErrors) = 0 → [original next]
|
|
125
|
+
* └── (default) → [Error End Event: VALIDATION_FAILED]
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* If the start event has an existing outgoing flow, that flow is redirected to
|
|
129
|
+
* originate from the XOR gateway (valid path) with the condition
|
|
130
|
+
* `= count(validationErrors) = 0`. The start event now connects to the Business
|
|
131
|
+
* Rule Task instead.
|
|
132
|
+
*
|
|
133
|
+
* If the start event has no outgoing flow the validation elements are inserted
|
|
134
|
+
* with only the error path connected; the caller can wire the valid path later.
|
|
135
|
+
*
|
|
136
|
+
* @returns Updated `BpmnDefinitions` — the original is not mutated.
|
|
137
|
+
*/
|
|
138
|
+
export function insertValidationStructure(defs, startEventId, decisionId) {
|
|
139
|
+
const processIdx = defs.processes.findIndex((p) => p.flowElements.some((e) => e.id === startEventId));
|
|
140
|
+
const process = defs.processes[processIdx];
|
|
141
|
+
if (processIdx < 0 || !process)
|
|
142
|
+
return defs;
|
|
143
|
+
// ── Generate IDs ─────────────────────────────────────────────────────────
|
|
144
|
+
const brtId = generateId("Activity");
|
|
145
|
+
const gwId = generateId("Gateway");
|
|
146
|
+
const errEndId = generateId("Event");
|
|
147
|
+
const errorId = generateId("Error");
|
|
148
|
+
const flowStartToBrt = generateId("Flow");
|
|
149
|
+
const flowBrtToGw = generateId("Flow");
|
|
150
|
+
const flowGwToError = generateId("Flow");
|
|
151
|
+
// ── Find existing outgoing flow from start event ──────────────────────────
|
|
152
|
+
const originalOutgoingFlowId = process.sequenceFlows.find((f) => f.sourceRef === startEventId)?.id;
|
|
153
|
+
// ── Build new flow elements ───────────────────────────────────────────────
|
|
154
|
+
const brt = {
|
|
155
|
+
type: "businessRuleTask",
|
|
156
|
+
id: brtId,
|
|
157
|
+
name: "Validate Input",
|
|
158
|
+
incoming: [flowStartToBrt],
|
|
159
|
+
outgoing: [flowBrtToGw],
|
|
160
|
+
extensionElements: zeebeExtensionsToXmlElements({
|
|
161
|
+
calledDecision: { decisionId, resultVariable: "validationErrors" },
|
|
162
|
+
}),
|
|
163
|
+
unknownAttributes: {},
|
|
164
|
+
};
|
|
165
|
+
const gwOutgoing = [flowGwToError];
|
|
166
|
+
if (originalOutgoingFlowId)
|
|
167
|
+
gwOutgoing.unshift(originalOutgoingFlowId);
|
|
168
|
+
const gateway = {
|
|
169
|
+
type: "exclusiveGateway",
|
|
170
|
+
id: gwId,
|
|
171
|
+
name: "Input valid?",
|
|
172
|
+
default: flowGwToError,
|
|
173
|
+
incoming: [flowBrtToGw],
|
|
174
|
+
outgoing: gwOutgoing,
|
|
175
|
+
extensionElements: [],
|
|
176
|
+
unknownAttributes: {},
|
|
177
|
+
};
|
|
178
|
+
const errEnd = {
|
|
179
|
+
type: "endEvent",
|
|
180
|
+
id: errEndId,
|
|
181
|
+
name: "Invalid Input",
|
|
182
|
+
incoming: [flowGwToError],
|
|
183
|
+
outgoing: [],
|
|
184
|
+
eventDefinitions: [{ type: "error", errorRef: errorId }],
|
|
185
|
+
extensionElements: [],
|
|
186
|
+
unknownAttributes: {},
|
|
187
|
+
};
|
|
188
|
+
const errorDef = {
|
|
189
|
+
id: errorId,
|
|
190
|
+
name: "VALIDATION_FAILED",
|
|
191
|
+
errorCode: "VALIDATION_FAILED",
|
|
192
|
+
};
|
|
193
|
+
// ── New sequence flows ────────────────────────────────────────────────────
|
|
194
|
+
const newFlows = [
|
|
195
|
+
{
|
|
196
|
+
id: flowStartToBrt,
|
|
197
|
+
sourceRef: startEventId,
|
|
198
|
+
targetRef: brtId,
|
|
199
|
+
extensionElements: [],
|
|
200
|
+
unknownAttributes: {},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: flowBrtToGw,
|
|
204
|
+
sourceRef: brtId,
|
|
205
|
+
targetRef: gwId,
|
|
206
|
+
extensionElements: [],
|
|
207
|
+
unknownAttributes: {},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: flowGwToError,
|
|
211
|
+
sourceRef: gwId,
|
|
212
|
+
targetRef: errEndId,
|
|
213
|
+
extensionElements: [],
|
|
214
|
+
unknownAttributes: {},
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
// ── Update start event outgoing list ──────────────────────────────────────
|
|
218
|
+
const updatedElements = process.flowElements.map((el) => {
|
|
219
|
+
if (el.id !== startEventId)
|
|
220
|
+
return el;
|
|
221
|
+
const without = el.outgoing.filter((o) => o !== originalOutgoingFlowId);
|
|
222
|
+
return { ...el, outgoing: [...without, flowStartToBrt] };
|
|
223
|
+
});
|
|
224
|
+
// ── Redirect original outgoing flow ───────────────────────────────────────
|
|
225
|
+
const updatedFlows = process.sequenceFlows.map((f) => {
|
|
226
|
+
if (f.id !== originalOutgoingFlowId)
|
|
227
|
+
return f;
|
|
228
|
+
return {
|
|
229
|
+
...f,
|
|
230
|
+
sourceRef: gwId,
|
|
231
|
+
conditionExpression: {
|
|
232
|
+
text: "= count(validationErrors) = 0",
|
|
233
|
+
attributes: {},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
const newProcess = {
|
|
238
|
+
...process,
|
|
239
|
+
flowElements: [...updatedElements, brt, gateway, errEnd],
|
|
240
|
+
sequenceFlows: [...updatedFlows, ...newFlows],
|
|
241
|
+
};
|
|
242
|
+
// ── DI: compute positions ─────────────────────────────────────────────────
|
|
243
|
+
let updatedDefs = {
|
|
244
|
+
...defs,
|
|
245
|
+
errors: [...defs.errors, errorDef],
|
|
246
|
+
processes: defs.processes.map((p, i) => (i === processIdx ? newProcess : p)),
|
|
247
|
+
};
|
|
248
|
+
updatedDefs = _insertValidationDi(updatedDefs, startEventId, {
|
|
249
|
+
brtId,
|
|
250
|
+
gwId,
|
|
251
|
+
errEndId,
|
|
252
|
+
flowStartToBrt,
|
|
253
|
+
flowBrtToGw,
|
|
254
|
+
flowGwToError,
|
|
255
|
+
originalOutgoingFlowId,
|
|
256
|
+
});
|
|
257
|
+
return updatedDefs;
|
|
258
|
+
}
|
|
259
|
+
/** Adds DI shapes and edges for the newly inserted validation elements. */
|
|
260
|
+
function _insertValidationDi(defs, startEventId, ids) {
|
|
261
|
+
if (!defs.diagrams[0])
|
|
262
|
+
return defs;
|
|
263
|
+
const plane = defs.diagrams[0].plane;
|
|
264
|
+
const startShape = plane.shapes.find((s) => s.bpmnElement === startEventId);
|
|
265
|
+
// Default position if no DI exists for the start event
|
|
266
|
+
const sx = startShape?.bounds.x ?? 152;
|
|
267
|
+
const sy = startShape?.bounds.y ?? 202;
|
|
268
|
+
const sw = SIZES.event.w;
|
|
269
|
+
const sh = SIZES.event.h;
|
|
270
|
+
// Vertical center of start event
|
|
271
|
+
const centerY = sy + sh / 2;
|
|
272
|
+
// Positions (top-left corner of each element)
|
|
273
|
+
const brtX = sx + sw + H_GAP;
|
|
274
|
+
const brtY = centerY - SIZES.task.h / 2;
|
|
275
|
+
const gwX = brtX + SIZES.task.w + H_GAP;
|
|
276
|
+
const gwY = centerY - SIZES.gateway.h / 2;
|
|
277
|
+
const errX = gwX + SIZES.gateway.w / 2 - SIZES.event.w / 2;
|
|
278
|
+
const errY = gwY + SIZES.gateway.h + V_GAP;
|
|
279
|
+
// Centers for waypoint computation
|
|
280
|
+
const startCx = sx + sw / 2;
|
|
281
|
+
const startCy = centerY;
|
|
282
|
+
const brtCx = brtX + SIZES.task.w / 2;
|
|
283
|
+
const brtCy = centerY;
|
|
284
|
+
const gwCx = gwX + SIZES.gateway.w / 2;
|
|
285
|
+
const gwCy = centerY;
|
|
286
|
+
const errCx = errX + SIZES.event.w / 2;
|
|
287
|
+
const errCy = errY + SIZES.event.h / 2;
|
|
288
|
+
const newShapes = [
|
|
289
|
+
{
|
|
290
|
+
id: generateId("Shape"),
|
|
291
|
+
bpmnElement: ids.brtId,
|
|
292
|
+
bounds: { x: brtX, y: brtY, width: SIZES.task.w, height: SIZES.task.h },
|
|
293
|
+
unknownAttributes: {},
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
id: generateId("Shape"),
|
|
297
|
+
bpmnElement: ids.gwId,
|
|
298
|
+
isMarkerVisible: true,
|
|
299
|
+
bounds: { x: gwX, y: gwY, width: SIZES.gateway.w, height: SIZES.gateway.h },
|
|
300
|
+
unknownAttributes: {},
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
id: generateId("Shape"),
|
|
304
|
+
bpmnElement: ids.errEndId,
|
|
305
|
+
bounds: { x: errX, y: errY, width: SIZES.event.w, height: SIZES.event.h },
|
|
306
|
+
unknownAttributes: {},
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
const newEdges = [
|
|
310
|
+
{
|
|
311
|
+
id: generateId("Edge"),
|
|
312
|
+
bpmnElement: ids.flowStartToBrt,
|
|
313
|
+
waypoints: [
|
|
314
|
+
{ x: startCx + sw / 2, y: startCy },
|
|
315
|
+
{ x: brtX, y: brtCy },
|
|
316
|
+
],
|
|
317
|
+
unknownAttributes: {},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: generateId("Edge"),
|
|
321
|
+
bpmnElement: ids.flowBrtToGw,
|
|
322
|
+
waypoints: [
|
|
323
|
+
{ x: brtX + SIZES.task.w, y: brtCy },
|
|
324
|
+
{ x: gwX, y: gwCy },
|
|
325
|
+
],
|
|
326
|
+
unknownAttributes: {},
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: generateId("Edge"),
|
|
330
|
+
bpmnElement: ids.flowGwToError,
|
|
331
|
+
waypoints: [
|
|
332
|
+
{ x: gwCx, y: gwY + SIZES.gateway.h },
|
|
333
|
+
{ x: errCx, y: errY },
|
|
334
|
+
],
|
|
335
|
+
unknownAttributes: {},
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
// Update the redirected outgoing flow edge to start from the gateway
|
|
339
|
+
const updatedEdges = plane.edges.map((e) => {
|
|
340
|
+
if (e.bpmnElement !== ids.originalOutgoingFlowId)
|
|
341
|
+
return e;
|
|
342
|
+
const firstWp = e.waypoints[0];
|
|
343
|
+
const rest = e.waypoints.slice(1);
|
|
344
|
+
return {
|
|
345
|
+
...e,
|
|
346
|
+
waypoints: [
|
|
347
|
+
// Replace first waypoint with gateway right-edge
|
|
348
|
+
{ x: gwX + SIZES.gateway.w, y: gwCy },
|
|
349
|
+
...(firstWp ? rest : []),
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
const updatedPlane = {
|
|
354
|
+
...plane,
|
|
355
|
+
shapes: [...plane.shapes, ...newShapes],
|
|
356
|
+
edges: [...updatedEdges, ...newEdges],
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
...defs,
|
|
360
|
+
diagrams: [
|
|
361
|
+
{
|
|
362
|
+
...defs.diagrams[0],
|
|
363
|
+
plane: updatedPlane,
|
|
364
|
+
},
|
|
365
|
+
...defs.diagrams.slice(1),
|
|
366
|
+
],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
// ── BPMN structure removal ────────────────────────────────────────────────────
|
|
370
|
+
/**
|
|
371
|
+
* Removes the validation structure wired after the given start event, if any.
|
|
372
|
+
*
|
|
373
|
+
* Restores the start event's outgoing connection to the element that the XOR
|
|
374
|
+
* gateway's valid path was pointing to. The generated error definition is also
|
|
375
|
+
* removed from `defs.errors`.
|
|
376
|
+
*
|
|
377
|
+
* @returns Updated `BpmnDefinitions` — the original is not mutated.
|
|
378
|
+
*/
|
|
379
|
+
export function removeValidationStructure(defs, startEventId) {
|
|
380
|
+
const processIdx = defs.processes.findIndex((p) => p.flowElements.some((e) => e.id === startEventId));
|
|
381
|
+
const process = defs.processes[processIdx];
|
|
382
|
+
if (processIdx < 0 || !process)
|
|
383
|
+
return defs;
|
|
384
|
+
// Locate the BRT immediately after the start event
|
|
385
|
+
const startToBrtFlow = process.sequenceFlows.find((f) => f.sourceRef === startEventId);
|
|
386
|
+
if (!startToBrtFlow)
|
|
387
|
+
return defs;
|
|
388
|
+
const brt = process.flowElements.find((e) => e.id === startToBrtFlow.targetRef && e.type === "businessRuleTask");
|
|
389
|
+
if (!brt)
|
|
390
|
+
return defs;
|
|
391
|
+
const ext = getZeebeExtensions(brt.extensionElements);
|
|
392
|
+
if (ext.calledDecision?.resultVariable !== "validationErrors")
|
|
393
|
+
return defs;
|
|
394
|
+
// BRT → gateway
|
|
395
|
+
const brtToGwFlow = process.sequenceFlows.find((f) => f.sourceRef === brt.id);
|
|
396
|
+
const gateway = brtToGwFlow
|
|
397
|
+
? process.flowElements.find((e) => e.id === brtToGwFlow.targetRef && e.type === "exclusiveGateway")
|
|
398
|
+
: undefined;
|
|
399
|
+
if (!gateway || gateway.type !== "exclusiveGateway")
|
|
400
|
+
return defs;
|
|
401
|
+
// Gateway → error end event (default/no-condition flow)
|
|
402
|
+
const gwFlows = process.sequenceFlows.filter((f) => f.sourceRef === gateway.id);
|
|
403
|
+
const errorFlow = gwFlows.find((f) => !f.conditionExpression);
|
|
404
|
+
const validFlow = gwFlows.find((f) => f.conditionExpression);
|
|
405
|
+
const errEnd = errorFlow
|
|
406
|
+
? process.flowElements.find((e) => e.id === errorFlow.targetRef && e.type === "endEvent")
|
|
407
|
+
: undefined;
|
|
408
|
+
// Collect IDs to remove
|
|
409
|
+
const removeElementIds = new Set([brt.id, gateway.id, ...(errEnd ? [errEnd.id] : [])]);
|
|
410
|
+
const removeFlowIds = new Set([
|
|
411
|
+
startToBrtFlow.id,
|
|
412
|
+
...(brtToGwFlow ? [brtToGwFlow.id] : []),
|
|
413
|
+
...(errorFlow ? [errorFlow.id] : []),
|
|
414
|
+
]);
|
|
415
|
+
// Find error ref to remove from defs.errors
|
|
416
|
+
const errEndEl = errEnd?.type === "endEvent" ? errEnd : undefined;
|
|
417
|
+
const errorRefId = errEndEl?.eventDefinitions[0]?.type === "error"
|
|
418
|
+
? errEndEl.eventDefinitions[0].errorRef
|
|
419
|
+
: undefined;
|
|
420
|
+
// Reconnect: restore the valid-path flow to come from the start event
|
|
421
|
+
const updatedFlows = process.sequenceFlows
|
|
422
|
+
.filter((f) => !removeFlowIds.has(f.id))
|
|
423
|
+
.map((f) => {
|
|
424
|
+
if (f.id !== validFlow?.id)
|
|
425
|
+
return f;
|
|
426
|
+
return { ...f, sourceRef: startEventId, conditionExpression: undefined };
|
|
427
|
+
});
|
|
428
|
+
// Restore start event outgoing list
|
|
429
|
+
const updatedElements = process.flowElements
|
|
430
|
+
.filter((e) => !removeElementIds.has(e.id))
|
|
431
|
+
.map((el) => {
|
|
432
|
+
if (el.id !== startEventId)
|
|
433
|
+
return el;
|
|
434
|
+
const without = el.outgoing.filter((o) => o !== startToBrtFlow.id);
|
|
435
|
+
const restored = validFlow ? [validFlow.id, ...without] : without;
|
|
436
|
+
return { ...el, outgoing: restored };
|
|
437
|
+
});
|
|
438
|
+
const newProcess = {
|
|
439
|
+
...process,
|
|
440
|
+
flowElements: updatedElements,
|
|
441
|
+
sequenceFlows: updatedFlows,
|
|
442
|
+
};
|
|
443
|
+
// Remove DI shapes and edges for removed elements
|
|
444
|
+
let updatedDefs = {
|
|
445
|
+
...defs,
|
|
446
|
+
errors: errorRefId ? defs.errors.filter((e) => e.id !== errorRefId) : defs.errors,
|
|
447
|
+
processes: defs.processes.map((p, i) => (i === processIdx ? newProcess : p)),
|
|
448
|
+
};
|
|
449
|
+
updatedDefs = _removeValidationDi(updatedDefs, removeElementIds, removeFlowIds, validFlow?.id);
|
|
450
|
+
return updatedDefs;
|
|
451
|
+
}
|
|
452
|
+
function _removeValidationDi(defs, removeElementIds, removeFlowIds, restoredFlowId) {
|
|
453
|
+
if (!defs.diagrams[0])
|
|
454
|
+
return defs;
|
|
455
|
+
const plane = defs.diagrams[0].plane;
|
|
456
|
+
const updatedShapes = plane.shapes.filter((s) => !removeElementIds.has(s.bpmnElement));
|
|
457
|
+
const updatedEdges = plane.edges
|
|
458
|
+
.filter((e) => !removeFlowIds.has(e.bpmnElement))
|
|
459
|
+
.map((e) => {
|
|
460
|
+
// The restored flow's edge now starts from the start event again.
|
|
461
|
+
// We don't know the start event's exact right-edge position here, but
|
|
462
|
+
// leaving the waypoints as-is is acceptable — auto-layout can fix them.
|
|
463
|
+
if (e.bpmnElement !== restoredFlowId)
|
|
464
|
+
return e;
|
|
465
|
+
return e; // leave waypoints unchanged; user can re-layout
|
|
466
|
+
});
|
|
467
|
+
return {
|
|
468
|
+
...defs,
|
|
469
|
+
diagrams: [
|
|
470
|
+
{
|
|
471
|
+
...defs.diagrams[0],
|
|
472
|
+
plane: { ...plane, shapes: updatedShapes, edges: updatedEdges },
|
|
473
|
+
},
|
|
474
|
+
...defs.diagrams.slice(1),
|
|
475
|
+
],
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
// ── DMN variable name extraction ──────────────────────────────────────────────
|
|
479
|
+
/**
|
|
480
|
+
* Parses a validation DMN XML and returns the input column expression names.
|
|
481
|
+
* Used by the Process Runner to display expected variable hints.
|
|
482
|
+
*
|
|
483
|
+
* Returns an empty array on any parse error.
|
|
484
|
+
*/
|
|
485
|
+
export function getValidationInputNames(dmnXml) {
|
|
486
|
+
try {
|
|
487
|
+
const defs = parseDmn(dmnXml);
|
|
488
|
+
const decision = defs.decisions[0];
|
|
489
|
+
if (!decision?.decisionTable)
|
|
490
|
+
return [];
|
|
491
|
+
return decision.decisionTable.inputs.map((i) => i.inputExpression.text ?? "").filter(Boolean);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
//# sourceMappingURL=input-validation.js.map
|
|
@@ -426,6 +426,84 @@ export function analyzeVariableFlow(p) {
|
|
|
426
426
|
produces: scopeVars,
|
|
427
427
|
});
|
|
428
428
|
}
|
|
429
|
+
// ── Multi-instance sub-process inner scopes ──────────────────────────────────
|
|
430
|
+
// For each multi-instance sub-process, emit edge-scope findings for inner
|
|
431
|
+
// sequence flows seeded with the iteration variable (inputElement).
|
|
432
|
+
for (const el of p.flowElements) {
|
|
433
|
+
if (el.type !== "subProcess")
|
|
434
|
+
continue;
|
|
435
|
+
const sp = el;
|
|
436
|
+
if (!sp.loopCharacteristics)
|
|
437
|
+
continue;
|
|
438
|
+
// Extract inputElement from zeebe:loopCharacteristics extension element
|
|
439
|
+
const loopExt = sp.loopCharacteristics.extensionElements.find((e) => e.name === "zeebe:loopCharacteristics");
|
|
440
|
+
const inputElement = loopExt?.attributes.inputElement?.trim();
|
|
441
|
+
if (!inputElement)
|
|
442
|
+
continue;
|
|
443
|
+
// Collect variables produced by inner elements (from IO mapping outputs)
|
|
444
|
+
const innerProduces = new Map();
|
|
445
|
+
for (const inner of sp.flowElements) {
|
|
446
|
+
const io = readZeebeIoMapping(inner.extensionElements);
|
|
447
|
+
if (io) {
|
|
448
|
+
for (const out of io.outputs) {
|
|
449
|
+
if (out.target.trim() === "")
|
|
450
|
+
continue;
|
|
451
|
+
const list = innerProduces.get(inner.id) ?? [];
|
|
452
|
+
if (!list.includes(out.target.trim()))
|
|
453
|
+
list.push(out.target.trim());
|
|
454
|
+
innerProduces.set(inner.id, list);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Build inner reverse adjacency for BFS
|
|
459
|
+
const innerReverseAdj = new Map();
|
|
460
|
+
for (const flow of sp.sequenceFlows) {
|
|
461
|
+
const set = innerReverseAdj.get(flow.targetRef) ?? new Set();
|
|
462
|
+
set.add(flow.sourceRef);
|
|
463
|
+
innerReverseAdj.set(flow.targetRef, set);
|
|
464
|
+
}
|
|
465
|
+
function innerAllPredecessors(elementId) {
|
|
466
|
+
const visited = new Set();
|
|
467
|
+
const queue = [elementId];
|
|
468
|
+
while (queue.length > 0) {
|
|
469
|
+
const current = queue.shift();
|
|
470
|
+
if (current === undefined)
|
|
471
|
+
break;
|
|
472
|
+
const preds = innerReverseAdj.get(current);
|
|
473
|
+
if (preds === undefined)
|
|
474
|
+
continue;
|
|
475
|
+
for (const pred of preds) {
|
|
476
|
+
if (!visited.has(pred)) {
|
|
477
|
+
visited.add(pred);
|
|
478
|
+
queue.push(pred);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return visited;
|
|
483
|
+
}
|
|
484
|
+
// Emit edge-scope findings for inner sequence flows
|
|
485
|
+
for (const flow of sp.sequenceFlows) {
|
|
486
|
+
const predIds = innerAllPredecessors(flow.sourceRef);
|
|
487
|
+
predIds.add(flow.sourceRef);
|
|
488
|
+
// Scope always includes the iteration variable, plus anything inner elements produce
|
|
489
|
+
const inScope = new Set([inputElement]);
|
|
490
|
+
for (const predId of predIds) {
|
|
491
|
+
for (const v of innerProduces.get(predId) ?? [])
|
|
492
|
+
inScope.add(v);
|
|
493
|
+
}
|
|
494
|
+
const scopeVars = [...inScope].sort();
|
|
495
|
+
findings.push({
|
|
496
|
+
id: `data-flow/edge-scope:${flow.id}`,
|
|
497
|
+
category: "data-flow",
|
|
498
|
+
severity: "info",
|
|
499
|
+
message: `Variables in scope at flow "${flow.id}": ${scopeVars.join(", ")}.`,
|
|
500
|
+
suggestion: "",
|
|
501
|
+
processId,
|
|
502
|
+
elementIds: [flow.id],
|
|
503
|
+
produces: scopeVars,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
429
507
|
return findings;
|
|
430
508
|
}
|
|
431
509
|
//# sourceMappingURL=variable-flow.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export { ELEMENT_SIZES, GRID_CELL_HEIGHT } from "./layout/index.js";
|
|
|
36
36
|
export { compactify, expand } from "./bpmn/compact.js";
|
|
37
37
|
export { applyOperations } from "./bpmn/operations.js";
|
|
38
38
|
export type { BpmnOperation } from "./bpmn/operations.js";
|
|
39
|
+
export { buildValidationDmn, findValidationStructure, getValidationInputNames, insertValidationStructure, removeValidationStructure, validationDecisionId, } from "./bpmn/input-validation.js";
|
|
40
|
+
export type { InputVariableDef, ValidationStructure, ValidationVariableType, } from "./bpmn/input-validation.js";
|
|
39
41
|
export { exportSvg } from "./bpmn/svg.js";
|
|
40
42
|
export type { SvgExportOptions } from "./bpmn/svg.js";
|
|
41
43
|
export type { CompactDiagram, CompactElement, CompactFlow, CompactProcess, } from "./bpmn/compact.js";
|
package/dist/index.js
CHANGED
|
@@ -18,5 +18,6 @@ export { benchmarkLayout, compareLayouts, formatBenchmarkResult, generateAutoLay
|
|
|
18
18
|
export { ELEMENT_SIZES, GRID_CELL_HEIGHT } from "./layout/index.js";
|
|
19
19
|
export { compactify, expand } from "./bpmn/compact.js";
|
|
20
20
|
export { applyOperations } from "./bpmn/operations.js";
|
|
21
|
+
export { buildValidationDmn, findValidationStructure, getValidationInputNames, insertValidationStructure, removeValidationStructure, validationDecisionId, } from "./bpmn/input-validation.js";
|
|
21
22
|
export { exportSvg } from "./bpmn/svg.js";
|
|
22
23
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bpmnkit/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dist/**/*.d.ts"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@bpmnkit/feel": "0.0.
|
|
20
|
+
"@bpmnkit/feel": "0.0.16"
|
|
21
21
|
},
|
|
22
22
|
"description": "TypeScript-first BPMN 2.0 SDK — parse, build, layout, and optimize diagrams",
|
|
23
23
|
"keywords": [
|