@bpmnkit/core 0.1.2 → 0.2.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/README.md +30 -1
- package/dist/bpmn/bpmn-builder.d.ts +209 -3
- package/dist/bpmn/bpmn-builder.js +456 -16
- package/dist/bpmn/bpmn-model.d.ts +110 -0
- package/dist/bpmn/bpmn-parser.js +1413 -528
- package/dist/bpmn/bpmn-serializer.js +101 -19
- package/dist/bpmn/compact.d.ts +17 -2
- package/dist/bpmn/compact.js +3 -3
- package/dist/bpmn/full-operations.d.ts +89 -0
- package/dist/bpmn/full-operations.js +478 -0
- package/dist/bpmn/index.d.ts +19 -0
- package/dist/bpmn/index.js +21 -0
- package/dist/bpmn/optimize/feel.js +2 -2
- package/dist/bpmn/optimize/patterns.js +23 -16
- package/dist/bpmn/optimize/tasks.js +30 -7
- package/dist/bpmn/optimize/utils.js +2 -4
- package/dist/bpmn/optimize/variable-flow.js +58 -67
- package/dist/bpmn/semantic-hash.d.ts +93 -0
- package/dist/bpmn/semantic-hash.js +155 -0
- package/dist/bpmn/sha256.d.ts +17 -0
- package/dist/bpmn/sha256.js +95 -0
- package/dist/bpmn/zeebe-extensions.d.ts +56 -0
- package/dist/bpmn/zeebe-extensions.js +79 -0
- package/dist/bpmn/zeebe-placement.d.ts +12 -0
- package/dist/bpmn/zeebe-placement.js +140 -0
- package/dist/errors.d.ts +40 -1
- package/dist/errors.js +41 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +7 -3
- package/dist/layout/semantic/graph.d.ts +9 -1
- package/dist/layout/semantic/graph.js +42 -17
- package/dist/layout/semantic/route.js +102 -42
- package/dist/node/index.d.ts +10 -0
- package/dist/node/index.js +9 -0
- package/dist/node/write.d.ts +81 -0
- package/dist/node/write.js +167 -0
- package/dist/types/id-generator.js +11 -3
- package/dist/xml/index.d.ts +3 -1
- package/dist/xml/index.js +2 -1
- package/dist/xml/xml-parser.d.ts +32 -0
- package/dist/xml/xml-parser.js +394 -143
- package/package.json +8 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { generateId } from "../types/id-generator.js";
|
|
2
2
|
import { applyAutoLayout } from "./auto-layout.js";
|
|
3
3
|
import { restConnectorRetries, restConnectorTaskType, restConnectorToIoMappingInputs, restConnectorToTaskHeaders, } from "./rest-connector.js";
|
|
4
|
-
import { zeebeExtensionsToXmlElements } from "./zeebe-extensions.js";
|
|
4
|
+
import { ensureZeebeExtension, zeebeExtensionsToXmlElements, } from "./zeebe-extensions.js";
|
|
5
5
|
// Keep in sync with packages/core/package.json version
|
|
6
6
|
const EXPORTER_VERSION = "0.0.23";
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
@@ -207,6 +207,17 @@ function buildAdHocLoopCharacteristics(lc) {
|
|
|
207
207
|
],
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
|
+
const GATEWAY_ELEMENT_TYPES = new Set([
|
|
211
|
+
"exclusiveGateway",
|
|
212
|
+
"parallelGateway",
|
|
213
|
+
"inclusiveGateway",
|
|
214
|
+
"eventBasedGateway",
|
|
215
|
+
"complexGateway",
|
|
216
|
+
]);
|
|
217
|
+
/** Gateways are the elements for which several outgoing flows are the point. */
|
|
218
|
+
function isGatewayType(type) {
|
|
219
|
+
return GATEWAY_ELEMENT_TYPES.has(type);
|
|
220
|
+
}
|
|
210
221
|
function recomputeIncomingOutgoing(elements, flows) {
|
|
211
222
|
for (const el of elements) {
|
|
212
223
|
el.incoming = [];
|
|
@@ -570,6 +581,8 @@ function traceBackToSplit(nodeId, splitGateways, flows) {
|
|
|
570
581
|
export class BranchBuilder {
|
|
571
582
|
/** @internal */
|
|
572
583
|
_elements = [];
|
|
584
|
+
/** Ids in `_elements`, for O(1) duplicate checks. */
|
|
585
|
+
_ids = new Set();
|
|
573
586
|
/** @internal */
|
|
574
587
|
_flows = [];
|
|
575
588
|
/** @internal */
|
|
@@ -617,6 +630,7 @@ export class BranchBuilder {
|
|
|
617
630
|
}
|
|
618
631
|
addElement(element) {
|
|
619
632
|
this._elements.push(element);
|
|
633
|
+
this._ids.add(element.id);
|
|
620
634
|
if (this.lastNodeId) {
|
|
621
635
|
const flowId = generateId("Flow");
|
|
622
636
|
const flow = {
|
|
@@ -811,6 +825,7 @@ export class BranchBuilder {
|
|
|
811
825
|
}
|
|
812
826
|
// Push directly — no sequence flow, boundary events attach via attachedToRef
|
|
813
827
|
this._elements.push(element);
|
|
828
|
+
this._ids.add(element.id);
|
|
814
829
|
this.lastNodeId = element.id;
|
|
815
830
|
this.isFirstElement = false;
|
|
816
831
|
return this;
|
|
@@ -901,10 +916,11 @@ export class BranchBuilder {
|
|
|
901
916
|
}
|
|
902
917
|
// Event sub-processes have no incoming/outgoing sequence flows and must not
|
|
903
918
|
// advance the branch cursor — mirrors ProcessBuilder.eventSubProcess.
|
|
904
|
-
if (this.
|
|
919
|
+
if (this._ids.has(element.id)) {
|
|
905
920
|
throw new Error(`Duplicate element ID "${element.id}"`);
|
|
906
921
|
}
|
|
907
922
|
this._elements.push(element);
|
|
923
|
+
this._ids.add(element.id);
|
|
908
924
|
return this;
|
|
909
925
|
}
|
|
910
926
|
/**
|
|
@@ -921,10 +937,11 @@ export class BranchBuilder {
|
|
|
921
937
|
const b = new BranchBuilder(this.currentGatewayId, name, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
922
938
|
callback(b);
|
|
923
939
|
for (const el of b._elements) {
|
|
924
|
-
if (this.
|
|
940
|
+
if (this._ids.has(el.id)) {
|
|
925
941
|
throw new Error(`Duplicate element ID "${el.id}"`);
|
|
926
942
|
}
|
|
927
943
|
this._elements.push(el);
|
|
944
|
+
this._ids.add(el.id);
|
|
928
945
|
}
|
|
929
946
|
for (const fl of b._flows)
|
|
930
947
|
this._flows.push(fl);
|
|
@@ -961,6 +978,8 @@ export class BranchBuilder {
|
|
|
961
978
|
export class SubProcessContentBuilder {
|
|
962
979
|
/** @internal */
|
|
963
980
|
_elements = [];
|
|
981
|
+
/** Ids in `_elements`, for O(1) duplicate checks. */
|
|
982
|
+
_ids = new Set();
|
|
964
983
|
/** @internal */
|
|
965
984
|
_flows = [];
|
|
966
985
|
/** @internal */
|
|
@@ -977,10 +996,11 @@ export class SubProcessContentBuilder {
|
|
|
977
996
|
this.rootMessages = rootMessages;
|
|
978
997
|
}
|
|
979
998
|
addElement(element) {
|
|
980
|
-
if (this.
|
|
999
|
+
if (this._ids.has(element.id)) {
|
|
981
1000
|
throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
|
|
982
1001
|
}
|
|
983
1002
|
this._elements.push(element);
|
|
1003
|
+
this._ids.add(element.id);
|
|
984
1004
|
if (this.lastNodeId) {
|
|
985
1005
|
this._flows.push({
|
|
986
1006
|
id: generateId("Flow"),
|
|
@@ -1012,10 +1032,11 @@ export class SubProcessContentBuilder {
|
|
|
1012
1032
|
* the current cursor and does not move it.
|
|
1013
1033
|
*/
|
|
1014
1034
|
addDisconnected(element) {
|
|
1015
|
-
if (this.
|
|
1035
|
+
if (this._ids.has(element.id)) {
|
|
1016
1036
|
throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
|
|
1017
1037
|
}
|
|
1018
1038
|
this._elements.push(element);
|
|
1039
|
+
this._ids.add(element.id);
|
|
1019
1040
|
return this;
|
|
1020
1041
|
}
|
|
1021
1042
|
// ---- Events ----
|
|
@@ -1136,10 +1157,11 @@ export class SubProcessContentBuilder {
|
|
|
1136
1157
|
const b = new BranchBuilder(this.currentGatewayId, name);
|
|
1137
1158
|
callback(b);
|
|
1138
1159
|
for (const el of b._elements) {
|
|
1139
|
-
if (this.
|
|
1160
|
+
if (this._ids.has(el.id)) {
|
|
1140
1161
|
throw new Error(`Duplicate element ID "${el.id}"`);
|
|
1141
1162
|
}
|
|
1142
1163
|
this._elements.push(el);
|
|
1164
|
+
this._ids.add(el.id);
|
|
1143
1165
|
}
|
|
1144
1166
|
for (const fl of b._flows)
|
|
1145
1167
|
this._flows.push(fl);
|
|
@@ -1182,7 +1204,7 @@ export class SubProcessContentBuilder {
|
|
|
1182
1204
|
return this;
|
|
1183
1205
|
}
|
|
1184
1206
|
element(elementId) {
|
|
1185
|
-
if (!this.
|
|
1207
|
+
if (!this._ids.has(elementId)) {
|
|
1186
1208
|
throw new Error(`Element "${elementId}" not found in sub-process`);
|
|
1187
1209
|
}
|
|
1188
1210
|
this.lastNodeId = elementId;
|
|
@@ -1210,6 +1232,7 @@ export class SubProcessContentBuilder {
|
|
|
1210
1232
|
}
|
|
1211
1233
|
// Push directly — no sequence flow, boundary events attach via attachedToRef
|
|
1212
1234
|
this._elements.push(element);
|
|
1235
|
+
this._ids.add(element.id);
|
|
1213
1236
|
this.lastNodeId = element.id;
|
|
1214
1237
|
return this;
|
|
1215
1238
|
}
|
|
@@ -1297,10 +1320,11 @@ export class SubProcessContentBuilder {
|
|
|
1297
1320
|
}
|
|
1298
1321
|
// Event sub-processes have no incoming/outgoing sequence flows and must not
|
|
1299
1322
|
// advance the cursor — mirrors ProcessBuilder.eventSubProcess.
|
|
1300
|
-
if (this.
|
|
1323
|
+
if (this._ids.has(element.id)) {
|
|
1301
1324
|
throw new Error(`Duplicate element ID "${element.id}" in sub-process`);
|
|
1302
1325
|
}
|
|
1303
1326
|
this._elements.push(element);
|
|
1327
|
+
this._ids.add(element.id);
|
|
1304
1328
|
return this;
|
|
1305
1329
|
}
|
|
1306
1330
|
}
|
|
@@ -1312,8 +1336,12 @@ export class ProcessBuilder {
|
|
|
1312
1336
|
processId;
|
|
1313
1337
|
processName;
|
|
1314
1338
|
_isExecutable = true;
|
|
1339
|
+
/** Whether `executable()` was called, so continue-mode leaves it alone if not. */
|
|
1340
|
+
_executableSet = false;
|
|
1315
1341
|
_versionTag;
|
|
1316
1342
|
flowElements = [];
|
|
1343
|
+
/** Ids in `flowElements`, for O(1) duplicate and existence checks. */
|
|
1344
|
+
elementIds = new Set();
|
|
1317
1345
|
sequenceFlows = [];
|
|
1318
1346
|
rootErrors = [];
|
|
1319
1347
|
rootMessages = [];
|
|
@@ -1329,9 +1357,140 @@ export class ProcessBuilder {
|
|
|
1329
1357
|
_executionPlatformVersion = "8.9.0";
|
|
1330
1358
|
_serviceTaskDefaults = {};
|
|
1331
1359
|
_savedMainFlowId = undefined;
|
|
1360
|
+
/** Set by {@link ProcessBuilder.from}; makes `build()` update rather than generate. */
|
|
1361
|
+
_source;
|
|
1362
|
+
/** Pre-existing flow endpoints, so `build()` can prove it did not rewire them. */
|
|
1363
|
+
_sourceFlowTargets = new Map();
|
|
1364
|
+
/** An `insertAfter` flow waiting to be reattached to what gets built next. */
|
|
1365
|
+
_pendingSpliceFlowId;
|
|
1366
|
+
/** Flows the caller deliberately spliced, exempt from the rewiring guard. */
|
|
1367
|
+
_splicedFlowIds = new Set();
|
|
1332
1368
|
constructor(processId) {
|
|
1333
1369
|
this.processId = processId;
|
|
1334
1370
|
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Continues an existing model rather than generating a new one.
|
|
1373
|
+
*
|
|
1374
|
+
* `build()` then returns *that document* with this process's contents
|
|
1375
|
+
* replaced, so everything the builder has no opinion about — other processes,
|
|
1376
|
+
* the collaboration, lanes, diagram interchange, root elements, imports,
|
|
1377
|
+
* unmodelled content — is still there afterwards. Generating a replacement
|
|
1378
|
+
* from scratch is what loses those.
|
|
1379
|
+
*
|
|
1380
|
+
* The input is not mutated; the builder works on a copy.
|
|
1381
|
+
*
|
|
1382
|
+
* @param definitions - The parsed model to continue.
|
|
1383
|
+
* @param processId - Which process to continue. Named explicitly, because
|
|
1384
|
+
* "the first process" is a guess that goes wrong on a collaboration.
|
|
1385
|
+
* @example
|
|
1386
|
+
* ```typescript
|
|
1387
|
+
* const updated = ProcessBuilder.from(Bpmn.parse(xml), "order")
|
|
1388
|
+
* .at("validate")
|
|
1389
|
+
* .serviceTask("notify", { name: "Notify", taskType: "notify" })
|
|
1390
|
+
* .build()
|
|
1391
|
+
* ```
|
|
1392
|
+
*/
|
|
1393
|
+
static from(definitions, processId) {
|
|
1394
|
+
const copy = structuredClone(definitions);
|
|
1395
|
+
const process = copy.processes.find((candidate) => candidate.id === processId);
|
|
1396
|
+
if (process === undefined) {
|
|
1397
|
+
const available = copy.processes.map((candidate) => candidate.id);
|
|
1398
|
+
throw new Error(`Process "${processId}" is not in this document. It contains: ${available.length > 0 ? available.map((id) => `"${id}"`).join(", ") : "no processes"}`);
|
|
1399
|
+
}
|
|
1400
|
+
const builder = new ProcessBuilder(processId);
|
|
1401
|
+
builder._source = { definitions: copy, process };
|
|
1402
|
+
builder.processName = process.name;
|
|
1403
|
+
builder.flowElements.push(...process.flowElements);
|
|
1404
|
+
for (const element of process.flowElements)
|
|
1405
|
+
builder.elementIds.add(element.id);
|
|
1406
|
+
builder.sequenceFlows.push(...process.sequenceFlows);
|
|
1407
|
+
builder._textAnnotations.push(...process.textAnnotations);
|
|
1408
|
+
builder._associations.push(...process.associations);
|
|
1409
|
+
// Seeded so a message event reuses the document's existing message rather
|
|
1410
|
+
// than declaring a second one with the same name.
|
|
1411
|
+
builder.rootErrors.push(...copy.errors);
|
|
1412
|
+
builder.rootMessages.push(...copy.messages);
|
|
1413
|
+
builder.rootSignals.push(...copy.signals);
|
|
1414
|
+
builder.rootEscalations.push(...copy.escalations);
|
|
1415
|
+
builder._sourceFlowTargets = new Map(process.sequenceFlows.map((flow) => [flow.id, `${flow.sourceRef}→${flow.targetRef}`]));
|
|
1416
|
+
return builder;
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Moves the cursor to an existing flow node, so the next call chains from it.
|
|
1420
|
+
*
|
|
1421
|
+
* @param nodeId - A flow node directly contained by this process. Nodes inside
|
|
1422
|
+
* a sub-process are not reachable: continuing into one means building that
|
|
1423
|
+
* sub-process, not this one.
|
|
1424
|
+
*/
|
|
1425
|
+
at(nodeId) {
|
|
1426
|
+
const node = this.flowElements.find((element) => element.id === nodeId);
|
|
1427
|
+
if (node === undefined) {
|
|
1428
|
+
throw new Error(`"${nodeId}" is not a flow node in process "${this.processId}". Nodes inside a sub-process cannot be continued from here.`);
|
|
1429
|
+
}
|
|
1430
|
+
if (node.type === "endEvent") {
|
|
1431
|
+
throw new Error(`Cannot continue from end event "${nodeId}": an end event has no outgoing sequence flow.`);
|
|
1432
|
+
}
|
|
1433
|
+
const hasOutgoing = this.sequenceFlows.some((flow) => flow.sourceRef === nodeId);
|
|
1434
|
+
if (hasOutgoing && !isGatewayType(node.type)) {
|
|
1435
|
+
throw new Error(`"${nodeId}" already has an outgoing sequence flow. Continuing from it would give a ${node.type} two outgoing flows, which is an uncontrolled split. Use insertAfter("${nodeId}") to splice into that path instead.`);
|
|
1436
|
+
}
|
|
1437
|
+
this.moveCursor(node);
|
|
1438
|
+
return this;
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Splices what you build next into the path leaving an existing node.
|
|
1442
|
+
*
|
|
1443
|
+
* `insertAfter("validate")` followed by `.serviceTask("notify", …)` turns
|
|
1444
|
+
* `validate → end` into `validate → notify → end`. The existing flow keeps its
|
|
1445
|
+
* id and its target and only changes where it starts, so an edge nobody asked
|
|
1446
|
+
* to move keeps its identity in the diagram and in a diff.
|
|
1447
|
+
*
|
|
1448
|
+
* This is the counterpart to {@link at}, which continues from a node whose
|
|
1449
|
+
* path is open. Which one you mean is not guessable, so it is not guessed.
|
|
1450
|
+
*
|
|
1451
|
+
* @param nodeId - A flow node with exactly one outgoing sequence flow.
|
|
1452
|
+
*/
|
|
1453
|
+
insertAfter(nodeId) {
|
|
1454
|
+
const node = this.flowElements.find((element) => element.id === nodeId);
|
|
1455
|
+
if (node === undefined) {
|
|
1456
|
+
throw new Error(`"${nodeId}" is not a flow node in process "${this.processId}".`);
|
|
1457
|
+
}
|
|
1458
|
+
const outgoing = this.sequenceFlows.filter((flow) => flow.sourceRef === nodeId);
|
|
1459
|
+
if (outgoing.length === 0) {
|
|
1460
|
+
throw new Error(`"${nodeId}" has no outgoing sequence flow, so there is nothing to insert into. Use at("${nodeId}") to continue from it.`);
|
|
1461
|
+
}
|
|
1462
|
+
if (outgoing.length > 1) {
|
|
1463
|
+
throw new Error(`"${nodeId}" has ${outgoing.length} outgoing sequence flows, so "after" is ambiguous. Name the flow's target and insert before that instead.`);
|
|
1464
|
+
}
|
|
1465
|
+
this.resolvePendingSplice();
|
|
1466
|
+
const flow = outgoing[0];
|
|
1467
|
+
this._pendingSpliceFlowId = flow.id;
|
|
1468
|
+
this._splicedFlowIds.add(flow.id);
|
|
1469
|
+
this.moveCursor(node);
|
|
1470
|
+
return this;
|
|
1471
|
+
}
|
|
1472
|
+
/** Points the cursor at an existing node and forgets any branch state. */
|
|
1473
|
+
moveCursor(node) {
|
|
1474
|
+
this.lastNodeId = node.id;
|
|
1475
|
+
this.currentGatewayId = isGatewayType(node.type) ? node.id : undefined;
|
|
1476
|
+
this.openBranchEnds = [];
|
|
1477
|
+
this._savedMainFlowId = undefined;
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Reattaches the flow a pending `insertAfter` detached, to whatever the cursor
|
|
1481
|
+
* has reached. With nothing built in between the cursor has not moved and this
|
|
1482
|
+
* is a no-op, which is the right answer for `insertAfter(x)` followed by
|
|
1483
|
+
* nothing.
|
|
1484
|
+
*/
|
|
1485
|
+
resolvePendingSplice() {
|
|
1486
|
+
const flowId = this._pendingSpliceFlowId;
|
|
1487
|
+
if (flowId === undefined)
|
|
1488
|
+
return;
|
|
1489
|
+
this._pendingSpliceFlowId = undefined;
|
|
1490
|
+
const flow = this.sequenceFlows.find((candidate) => candidate.id === flowId);
|
|
1491
|
+
if (flow !== undefined && this.lastNodeId !== undefined)
|
|
1492
|
+
flow.sourceRef = this.lastNodeId;
|
|
1493
|
+
}
|
|
1335
1494
|
/** Enable auto-layout: `build()` will run the layout engine and populate diagram interchange data. */
|
|
1336
1495
|
withAutoLayout() {
|
|
1337
1496
|
this._autoLayout = true;
|
|
@@ -1356,6 +1515,7 @@ export class ProcessBuilder {
|
|
|
1356
1515
|
/** Set whether this process is executable. */
|
|
1357
1516
|
executable(value) {
|
|
1358
1517
|
this._isExecutable = value;
|
|
1518
|
+
this._executableSet = true;
|
|
1359
1519
|
return this;
|
|
1360
1520
|
}
|
|
1361
1521
|
/** Set the process version tag. */
|
|
@@ -1638,10 +1798,11 @@ export class ProcessBuilder {
|
|
|
1638
1798
|
const b = new BranchBuilder(this.currentGatewayId, name, this.rootErrors, this.rootMessages, this.rootSignals, this.rootEscalations);
|
|
1639
1799
|
callback(b);
|
|
1640
1800
|
for (const el of b._elements) {
|
|
1641
|
-
if (this.
|
|
1801
|
+
if (this.elementIds.has(el.id)) {
|
|
1642
1802
|
throw new Error(`Duplicate element ID "${el.id}"`);
|
|
1643
1803
|
}
|
|
1644
1804
|
this.flowElements.push(el);
|
|
1805
|
+
this.elementIds.add(el.id);
|
|
1645
1806
|
}
|
|
1646
1807
|
for (const fl of b._flows) {
|
|
1647
1808
|
this.sequenceFlows.push(fl);
|
|
@@ -1696,7 +1857,7 @@ export class ProcessBuilder {
|
|
|
1696
1857
|
* @throws If no element with the given ID exists.
|
|
1697
1858
|
*/
|
|
1698
1859
|
element(elementId) {
|
|
1699
|
-
const found = this.
|
|
1860
|
+
const found = this.elementIds.has(elementId);
|
|
1700
1861
|
if (!found) {
|
|
1701
1862
|
throw new Error(`Element "${elementId}" not found in process "${this.processId}"`);
|
|
1702
1863
|
}
|
|
@@ -1761,11 +1922,12 @@ export class ProcessBuilder {
|
|
|
1761
1922
|
// advance the flow cursor — the surrounding process wires around them.
|
|
1762
1923
|
// openBranchEnds is intentionally NOT drained here; the next normal
|
|
1763
1924
|
// addFlowElement call will drain it and connect branch ends to that element.
|
|
1764
|
-
if (this.
|
|
1925
|
+
if (this.elementIds.has(element.id)) {
|
|
1765
1926
|
throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
|
|
1766
1927
|
}
|
|
1767
1928
|
this._savedMainFlowId = undefined;
|
|
1768
1929
|
this.flowElements.push(element);
|
|
1930
|
+
this.elementIds.add(element.id);
|
|
1769
1931
|
return this;
|
|
1770
1932
|
}
|
|
1771
1933
|
// ---- Annotations ----
|
|
@@ -1797,19 +1959,50 @@ export class ProcessBuilder {
|
|
|
1797
1959
|
*
|
|
1798
1960
|
* Resolves all forward-referenced `incoming` / `outgoing` arrays and wraps
|
|
1799
1961
|
* the process in a {@link BpmnDefinitions} ready for XML serialization.
|
|
1962
|
+
*
|
|
1963
|
+
* **The join contract.** Branches built with `.branch()` converge implicitly:
|
|
1964
|
+
* where several paths from one gateway reach the same element, a matching join
|
|
1965
|
+
* gateway is inserted for you. That is convenient by hand and a trap for
|
|
1966
|
+
* generated code, which cannot see the element it did not write. Pass
|
|
1967
|
+
* `{ explicitJoins: true }` to be told instead of helped — the build throws,
|
|
1968
|
+
* naming the gateways it would have inserted, and you declare them yourself
|
|
1969
|
+
* with `.connectTo(joinId)`.
|
|
1970
|
+
*
|
|
1971
|
+
* A join you declare only counts if it *matches the split*: an exclusive split
|
|
1972
|
+
* converging on a parallel gateway is not the gateway inference would have
|
|
1973
|
+
* added, so it is still inferred — and with `explicitJoins` that refusal is the
|
|
1974
|
+
* only thing that tells you.
|
|
1975
|
+
*
|
|
1976
|
+
* {@link ProcessBuilder.from} never infers joins at all, whatever this option
|
|
1977
|
+
* says: inference reads the whole topology, and on a document you were handed
|
|
1978
|
+
* that means rewriting edges you never touched.
|
|
1979
|
+
*
|
|
1980
|
+
* @param options - `explicitJoins` refuses inferred join gateways. `strict` is
|
|
1981
|
+
* the former name for it and still works.
|
|
1800
1982
|
*/
|
|
1801
1983
|
build(options) {
|
|
1984
|
+
this.resolvePendingSplice();
|
|
1802
1985
|
const beforeCount = this.flowElements.length;
|
|
1803
|
-
|
|
1804
|
-
|
|
1986
|
+
// Continuing a document never infers joins. `insertJoinGateways` reads the
|
|
1987
|
+
// whole topology, so on a parsed model it retargets edges the caller never
|
|
1988
|
+
// touched — the corpus has a document where a no-op continue would have
|
|
1989
|
+
// invented a join gateway and rerouted two flows into it. Building a branch
|
|
1990
|
+
// that needs a join here means saying so with `.connectTo(joinId)`.
|
|
1991
|
+
if (this._source === undefined)
|
|
1992
|
+
insertJoinGateways(this.flowElements, this.sequenceFlows);
|
|
1993
|
+
const explicitJoins = options?.explicitJoins ?? options?.strict ?? false;
|
|
1994
|
+
if (explicitJoins && this.flowElements.length > beforeCount) {
|
|
1805
1995
|
const inserted = this.flowElements
|
|
1806
1996
|
.slice(beforeCount)
|
|
1807
1997
|
.map((e) => e.id)
|
|
1808
1998
|
.join(", ");
|
|
1809
|
-
throw new Error(`
|
|
1999
|
+
throw new Error(`Inferred join gateways: ${inserted}. Declare them with .connectTo(joinId), or drop { explicitJoins: true } to keep the inference.`);
|
|
1810
2000
|
}
|
|
1811
2001
|
this.validate();
|
|
2002
|
+
this.assertSourceTopologyIntact();
|
|
1812
2003
|
recomputeIncomingOutgoing(this.flowElements, this.sequenceFlows);
|
|
2004
|
+
if (this._source !== undefined)
|
|
2005
|
+
return this.buildOntoSource(this._source);
|
|
1813
2006
|
const extensionElements = [];
|
|
1814
2007
|
if (this._versionTag) {
|
|
1815
2008
|
extensionElements.push({
|
|
@@ -1871,6 +2064,78 @@ export class ProcessBuilder {
|
|
|
1871
2064
|
};
|
|
1872
2065
|
return this._autoLayout ? applyAutoLayout(defs) : defs;
|
|
1873
2066
|
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Writes this process's contents back into the document it came from.
|
|
2069
|
+
*
|
|
2070
|
+
* Everything not listed here is kept by identity — other processes, the
|
|
2071
|
+
* collaboration, diagram interchange, root elements, the process's own lanes,
|
|
2072
|
+
* documentation, extensions and unmodelled content. That is the whole point of
|
|
2073
|
+
* continuing rather than regenerating.
|
|
2074
|
+
*
|
|
2075
|
+
* Diagram interchange is *not* regenerated: existing shapes keep their
|
|
2076
|
+
* positions, and elements added here have none until `withAutoLayout()` or a
|
|
2077
|
+
* later `applyAutoLayout()` gives them one.
|
|
2078
|
+
*/
|
|
2079
|
+
buildOntoSource(source) {
|
|
2080
|
+
const { definitions, process } = source;
|
|
2081
|
+
process.flowElements = this.flowElements;
|
|
2082
|
+
process.sequenceFlows = this.sequenceFlows;
|
|
2083
|
+
process.textAnnotations = this._textAnnotations;
|
|
2084
|
+
process.associations = this._associations;
|
|
2085
|
+
if (this.processName !== undefined)
|
|
2086
|
+
process.name = this.processName;
|
|
2087
|
+
// Only when asked. BPMN reads an absent `isExecutable` as false, so writing
|
|
2088
|
+
// the builder's `true` default onto a process that never carried it makes a
|
|
2089
|
+
// non-executable process executable — a change nobody requested.
|
|
2090
|
+
if (this._executableSet)
|
|
2091
|
+
process.isExecutable = this._isExecutable;
|
|
2092
|
+
if (this._versionTag !== undefined) {
|
|
2093
|
+
// Set the attribute on the existing element rather than replacing the
|
|
2094
|
+
// bag: a real process carries other extensions next to it.
|
|
2095
|
+
ensureZeebeExtension({ type: "process", extensionElements: process.extensionElements }, "zeebe:versionTag").attributes.value = this._versionTag;
|
|
2096
|
+
}
|
|
2097
|
+
definitions.errors = this.rootErrors;
|
|
2098
|
+
definitions.messages = this.rootMessages;
|
|
2099
|
+
definitions.signals = this.rootSignals;
|
|
2100
|
+
definitions.escalations = this.rootEscalations;
|
|
2101
|
+
return this._autoLayout ? applyAutoLayout(definitions) : definitions;
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* Refuses to rewire a flow that was already in the document.
|
|
2105
|
+
*
|
|
2106
|
+
* `insertJoinGateways` retargets converging flows, which is right for a
|
|
2107
|
+
* topology this builder just created and wrong for one it was handed: a
|
|
2108
|
+
* document would come back with edges the caller never touched pointing
|
|
2109
|
+
* somewhere else. Continuing a model has to leave the model alone.
|
|
2110
|
+
*/
|
|
2111
|
+
assertSourceTopologyIntact() {
|
|
2112
|
+
if (this._source === undefined)
|
|
2113
|
+
return;
|
|
2114
|
+
const rewired = [];
|
|
2115
|
+
const byId = new Map(this.sequenceFlows.map((flow) => [flow.id, flow]));
|
|
2116
|
+
for (const [id, endpoints] of this._sourceFlowTargets) {
|
|
2117
|
+
const flow = byId.get(id);
|
|
2118
|
+
if (flow === undefined) {
|
|
2119
|
+
rewired.push(`${id} (removed)`);
|
|
2120
|
+
continue;
|
|
2121
|
+
}
|
|
2122
|
+
// A spliced flow was moved on purpose, but only its source may move:
|
|
2123
|
+
// `insertAfter` inserts into a path, it does not redirect one.
|
|
2124
|
+
const [, target] = endpoints.split("→");
|
|
2125
|
+
if (this._splicedFlowIds.has(id)) {
|
|
2126
|
+
if (flow.targetRef !== target) {
|
|
2127
|
+
rewired.push(`${id} (spliced flow now targets ${flow.targetRef}, not ${target})`);
|
|
2128
|
+
}
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
const now = `${flow.sourceRef}→${flow.targetRef}`;
|
|
2132
|
+
if (now !== endpoints)
|
|
2133
|
+
rewired.push(`${id} (${endpoints} became ${now})`);
|
|
2134
|
+
}
|
|
2135
|
+
if (rewired.length > 0) {
|
|
2136
|
+
throw new Error(`Building would rewire sequence flows this document already had: ${rewired.join(", ")}. Continuing a model must not change the part you did not touch.`);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
1874
2139
|
validate() {
|
|
1875
2140
|
const elementIds = new Set(this.flowElements.map((el) => el.id));
|
|
1876
2141
|
for (const flow of this.sequenceFlows) {
|
|
@@ -1885,10 +2150,11 @@ export class ProcessBuilder {
|
|
|
1885
2150
|
}
|
|
1886
2151
|
}
|
|
1887
2152
|
addFlowElement(element) {
|
|
1888
|
-
if (this.
|
|
2153
|
+
if (this.elementIds.has(element.id)) {
|
|
1889
2154
|
throw new Error(`Duplicate element ID "${element.id}" in process "${this.processId}"`);
|
|
1890
2155
|
}
|
|
1891
2156
|
this.flowElements.push(element);
|
|
2157
|
+
this.elementIds.add(element.id);
|
|
1892
2158
|
// Compensation handlers are outside the normal token flow: link via association
|
|
1893
2159
|
// from the preceding compensation boundary event, then restore the main-flow cursor.
|
|
1894
2160
|
if (element.isForCompensation) {
|
|
@@ -1947,6 +2213,9 @@ export class DiagramBuilder {
|
|
|
1947
2213
|
_processes = [];
|
|
1948
2214
|
_errors = [];
|
|
1949
2215
|
_messages = [];
|
|
2216
|
+
_participants = [];
|
|
2217
|
+
_messageFlows = [];
|
|
2218
|
+
_collaborationId = "Collaboration_1";
|
|
1950
2219
|
_executionPlatformVersion = "8.9.0";
|
|
1951
2220
|
constructor(id) {
|
|
1952
2221
|
this._id = id;
|
|
@@ -1965,6 +2234,152 @@ export class DiagramBuilder {
|
|
|
1965
2234
|
this._messages.push(...defs.messages);
|
|
1966
2235
|
return this;
|
|
1967
2236
|
}
|
|
2237
|
+
/** Renames the collaboration element. Defaults to `"Collaboration_1"`. */
|
|
2238
|
+
collaborationId(id) {
|
|
2239
|
+
this._collaborationId = id;
|
|
2240
|
+
return this;
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* Adds a pool.
|
|
2244
|
+
*
|
|
2245
|
+
* Omit `processId` for a black box — a participant whose internals are not
|
|
2246
|
+
* modelled. That is a real BPMN construct, not an incomplete one: it is how
|
|
2247
|
+
* you draw the counterparty you exchange messages with but do not execute.
|
|
2248
|
+
*
|
|
2249
|
+
* @param id - The participant's element id, used verbatim.
|
|
2250
|
+
*/
|
|
2251
|
+
participant(id, options = {}) {
|
|
2252
|
+
const participant = { id, unknownAttributes: {} };
|
|
2253
|
+
if (options.name !== undefined)
|
|
2254
|
+
participant.name = options.name;
|
|
2255
|
+
if (options.processId !== undefined)
|
|
2256
|
+
participant.processRef = options.processId;
|
|
2257
|
+
this._participants.push(participant);
|
|
2258
|
+
return this;
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Declares a root-level message, which a message flow may name and a Camunda 8
|
|
2262
|
+
* message subscription correlates on.
|
|
2263
|
+
*
|
|
2264
|
+
* `ProcessBuilder` already creates messages by name for message events, so
|
|
2265
|
+
* only call this for a message no event declared — typically one carried by a
|
|
2266
|
+
* message flow between pools.
|
|
2267
|
+
*
|
|
2268
|
+
* @param id - The message's element id, used verbatim.
|
|
2269
|
+
*/
|
|
2270
|
+
message(id, options = {}) {
|
|
2271
|
+
const message = { id, unknownAttributes: {} };
|
|
2272
|
+
if (options.name !== undefined)
|
|
2273
|
+
message.name = options.name;
|
|
2274
|
+
const extensions = zeebeExtensionsToXmlElements(options.correlationKey === undefined
|
|
2275
|
+
? {}
|
|
2276
|
+
: { subscription: { correlationKey: options.correlationKey } });
|
|
2277
|
+
if (extensions.length > 0)
|
|
2278
|
+
message.extensionElements = extensions;
|
|
2279
|
+
this._messages.push(message);
|
|
2280
|
+
return this;
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Connects two pools.
|
|
2284
|
+
*
|
|
2285
|
+
* `source` and `target` name either participants or flow nodes inside them.
|
|
2286
|
+
* Both forms are valid BPMN and the layout engine reads either, but they must
|
|
2287
|
+
* be in *different* pools — a message flow is what crosses a pool boundary,
|
|
2288
|
+
* and one that does not is the error this catches.
|
|
2289
|
+
*
|
|
2290
|
+
* @param id - The message flow's element id, used verbatim.
|
|
2291
|
+
*/
|
|
2292
|
+
messageFlow(id, options) {
|
|
2293
|
+
const flow = {
|
|
2294
|
+
id,
|
|
2295
|
+
sourceRef: options.source,
|
|
2296
|
+
targetRef: options.target,
|
|
2297
|
+
unknownAttributes: {},
|
|
2298
|
+
};
|
|
2299
|
+
if (options.name !== undefined)
|
|
2300
|
+
flow.name = options.name;
|
|
2301
|
+
if (options.messageRef !== undefined)
|
|
2302
|
+
flow.messageRef = options.messageRef;
|
|
2303
|
+
this._messageFlows.push(flow);
|
|
2304
|
+
return this;
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Reports every way the declared collaboration would not survive contact with
|
|
2308
|
+
* a modeler, so `build()` can refuse rather than emit a file that opens broken.
|
|
2309
|
+
*/
|
|
2310
|
+
collaborationProblems() {
|
|
2311
|
+
const problems = [];
|
|
2312
|
+
const seen = new Set();
|
|
2313
|
+
for (const id of [
|
|
2314
|
+
...this._participants.map((p) => p.id),
|
|
2315
|
+
...this._messageFlows.map((f) => f.id),
|
|
2316
|
+
]) {
|
|
2317
|
+
if (seen.has(id))
|
|
2318
|
+
problems.push(`Duplicate element ID "${id}"`);
|
|
2319
|
+
seen.add(id);
|
|
2320
|
+
}
|
|
2321
|
+
const processIds = new Set(this._processes.map((process) => process.id));
|
|
2322
|
+
const claimed = new Map();
|
|
2323
|
+
for (const participant of this._participants) {
|
|
2324
|
+
const processId = participant.processRef;
|
|
2325
|
+
if (processId === undefined)
|
|
2326
|
+
continue;
|
|
2327
|
+
if (!processIds.has(processId)) {
|
|
2328
|
+
problems.push(`Participant "${participant.id}" references process "${processId}", which this diagram does not contain`);
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2331
|
+
const owner = claimed.get(processId);
|
|
2332
|
+
if (owner !== undefined) {
|
|
2333
|
+
problems.push(`Participants "${owner}" and "${participant.id}" both reference process "${processId}"; a process belongs to one pool`);
|
|
2334
|
+
}
|
|
2335
|
+
claimed.set(processId, participant.id);
|
|
2336
|
+
}
|
|
2337
|
+
const owningParticipant = this.participantIndex();
|
|
2338
|
+
const messageIds = new Set(this._messages.map((message) => message.id));
|
|
2339
|
+
for (const flow of this._messageFlows) {
|
|
2340
|
+
for (const [role, ref] of [
|
|
2341
|
+
["source", flow.sourceRef],
|
|
2342
|
+
["target", flow.targetRef],
|
|
2343
|
+
]) {
|
|
2344
|
+
if (!owningParticipant.has(ref)) {
|
|
2345
|
+
problems.push(`Message flow "${flow.id}" names ${role} "${ref}", which is not a participant or a flow node in one`);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
const from = owningParticipant.get(flow.sourceRef);
|
|
2349
|
+
const to = owningParticipant.get(flow.targetRef);
|
|
2350
|
+
if (from !== undefined && from === to) {
|
|
2351
|
+
problems.push(`Message flow "${flow.id}" starts and ends in participant "${from}"; a message flow crosses pools, a sequence flow stays inside one`);
|
|
2352
|
+
}
|
|
2353
|
+
if (flow.messageRef !== undefined && !messageIds.has(flow.messageRef)) {
|
|
2354
|
+
problems.push(`Message flow "${flow.id}" references message "${flow.messageRef}", which this diagram does not declare`);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
return problems;
|
|
2358
|
+
}
|
|
2359
|
+
/** Maps every participant id and every flow node id to its owning participant. */
|
|
2360
|
+
participantIndex() {
|
|
2361
|
+
const index = new Map();
|
|
2362
|
+
const byProcess = new Map();
|
|
2363
|
+
for (const participant of this._participants) {
|
|
2364
|
+
index.set(participant.id, participant.id);
|
|
2365
|
+
if (participant.processRef !== undefined)
|
|
2366
|
+
byProcess.set(participant.processRef, participant.id);
|
|
2367
|
+
}
|
|
2368
|
+
const walk = (elements, owner) => {
|
|
2369
|
+
for (const element of elements) {
|
|
2370
|
+
index.set(element.id, owner);
|
|
2371
|
+
if ("flowElements" in element && Array.isArray(element.flowElements)) {
|
|
2372
|
+
walk(element.flowElements, owner);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2376
|
+
for (const process of this._processes) {
|
|
2377
|
+
const owner = byProcess.get(process.id);
|
|
2378
|
+
if (owner !== undefined)
|
|
2379
|
+
walk(process.flowElements, owner);
|
|
2380
|
+
}
|
|
2381
|
+
return index;
|
|
2382
|
+
}
|
|
1968
2383
|
build() {
|
|
1969
2384
|
return {
|
|
1970
2385
|
id: this._id,
|
|
@@ -1988,10 +2403,35 @@ export class DiagramBuilder {
|
|
|
1988
2403
|
escalations: [],
|
|
1989
2404
|
messages: this._messages,
|
|
1990
2405
|
signals: [],
|
|
1991
|
-
collaborations:
|
|
2406
|
+
collaborations: this.buildCollaborations(),
|
|
1992
2407
|
processes: this._processes,
|
|
1993
2408
|
diagrams: [],
|
|
1994
2409
|
};
|
|
1995
2410
|
}
|
|
2411
|
+
/**
|
|
2412
|
+
* A document with no participants has no collaboration — an empty
|
|
2413
|
+
* `<bpmn:collaboration/>` is not a neutral addition, it makes every process a
|
|
2414
|
+
* pool-less participant in a modeler.
|
|
2415
|
+
*/
|
|
2416
|
+
buildCollaborations() {
|
|
2417
|
+
if (this._participants.length === 0 && this._messageFlows.length === 0)
|
|
2418
|
+
return [];
|
|
2419
|
+
const problems = this.collaborationProblems();
|
|
2420
|
+
if (problems.length > 0) {
|
|
2421
|
+
throw new Error(`Invalid collaboration:\n ${problems.join("\n ")}`);
|
|
2422
|
+
}
|
|
2423
|
+
return [
|
|
2424
|
+
{
|
|
2425
|
+
id: this._collaborationId,
|
|
2426
|
+
participants: this._participants,
|
|
2427
|
+
messageFlows: this._messageFlows,
|
|
2428
|
+
textAnnotations: [],
|
|
2429
|
+
associations: [],
|
|
2430
|
+
groups: [],
|
|
2431
|
+
extensionElements: [],
|
|
2432
|
+
unknownAttributes: {},
|
|
2433
|
+
},
|
|
2434
|
+
];
|
|
2435
|
+
}
|
|
1996
2436
|
}
|
|
1997
2437
|
//# sourceMappingURL=bpmn-builder.js.map
|