@waron97/prbot 3.2.1 → 3.3.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 +26 -18
- package/agrippa-pb.md +141 -68
- package/package.json +1 -1
- package/src/agrippa/commands/clone.js +7 -1
- package/src/agrippa/commands/cloneLrp.js +114 -0
- package/src/agrippa/commands/initPhase.js +44 -34
- package/src/agrippa/commands/pb.js +46 -13
- package/src/agrippa/commands/pull.js +79 -3
- package/src/agrippa/commands/pullLrp.js +55 -0
- package/src/agrippa/commands/push.js +81 -11
- package/src/agrippa/commands/pushLrp.js +56 -0
- package/src/agrippa/index.js +28 -19
- package/src/agrippa/lib/lrpApi.js +153 -0
- package/src/agrippa/lib/pbEdit.js +31 -10
- package/src/agrippa/lib/pbModel.js +317 -86
- package/src/agrippa/lib/pbProject.js +22 -4
- package/src/agrippa/lib/pbScriptTemplate.js +29 -0
- package/src/commands/autopr.js +1 -1
package/src/agrippa/index.js
CHANGED
|
@@ -42,13 +42,16 @@ program
|
|
|
42
42
|
|
|
43
43
|
program
|
|
44
44
|
.command('clone')
|
|
45
|
-
.description(
|
|
45
|
+
.description(
|
|
46
|
+
'Clone a phase, MFA, process-builder wizard, or long-running process into this workspace'
|
|
47
|
+
)
|
|
46
48
|
.option('--phase', 'Clone a phase (select a workflow)')
|
|
47
49
|
.option('--mfa', 'Clone a Model Function Access record')
|
|
48
50
|
.option('--pb', 'Clone a process-builder wizard')
|
|
51
|
+
.option('--lrp', 'Clone a long-running process')
|
|
49
52
|
.option('--id <id>', 'Record ID to clone (phase/mfa)')
|
|
50
|
-
.option('--name <
|
|
51
|
-
.option('--path <path>', 'Destination path (file for MFA, base dir for workflow/pb)')
|
|
53
|
+
.option('--name <name>', 'document_id (--pb) or process name (--lrp) to clone')
|
|
54
|
+
.option('--path <path>', 'Destination path (file for MFA, base dir for workflow/pb/lrp)')
|
|
52
55
|
.action((opts) =>
|
|
53
56
|
clone(opts).catch((err) => {
|
|
54
57
|
console.error(`Error: ${err.message}`);
|
|
@@ -68,9 +71,9 @@ program
|
|
|
68
71
|
|
|
69
72
|
program
|
|
70
73
|
.command('push')
|
|
71
|
-
.description('Push local changes to RIP / Process Builder (backs up remote first)')
|
|
72
|
-
.option('--publish', 'Auto-publish pushed
|
|
73
|
-
.option('--skip-publish', 'Skip publishing pushed wizards (no prompt)')
|
|
74
|
+
.description('Push local changes to RIP / Process Builder / LRP (backs up remote first)')
|
|
75
|
+
.option('--publish', 'Auto-publish pushed wizards and auto-deploy pushed LRPs')
|
|
76
|
+
.option('--skip-publish', 'Skip publishing/deploying pushed wizards and LRPs (no prompt)')
|
|
74
77
|
.action((opts) =>
|
|
75
78
|
push(opts).catch((err) => {
|
|
76
79
|
console.error(`Error: ${err.message}`);
|
|
@@ -108,25 +111,29 @@ program
|
|
|
108
111
|
})
|
|
109
112
|
);
|
|
110
113
|
|
|
111
|
-
// ---- pb: local editing helpers for a cloned process-builder wizard ----
|
|
114
|
+
// ---- pb: local editing helpers for a cloned process-builder wizard or LRP ----
|
|
112
115
|
const die = (err) => {
|
|
113
116
|
console.error(`Error: ${err.message}`);
|
|
114
117
|
process.exit(1);
|
|
115
118
|
};
|
|
116
119
|
const pb = program
|
|
117
120
|
.command('pb')
|
|
118
|
-
.description(
|
|
121
|
+
.description(
|
|
122
|
+
'Edit a cloned process-builder wizard or long-running process (local; run `pb format` after edits)'
|
|
123
|
+
);
|
|
119
124
|
|
|
120
125
|
pb.command('format')
|
|
121
126
|
.description('Auto-lay-out the diagram (left→right) and rewrite geometry')
|
|
122
|
-
.option('--pb <
|
|
127
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP (else single-entry / fuzzy prompt)')
|
|
123
128
|
.action((opts) => pbFormat(opts).catch(die));
|
|
124
129
|
|
|
125
130
|
pb.command('add')
|
|
126
131
|
.description('Add a node (scaffolds script/page); stub geometry, run format after')
|
|
127
132
|
.requiredOption(
|
|
128
133
|
'--type <type>',
|
|
129
|
-
'Node type: scriptTask|serviceTask|userTask|exclusiveGateway|subProcess|transaction|
|
|
134
|
+
'Node type: scriptTask|serviceTask|userTask|exclusiveGateway|subProcess|transaction|' +
|
|
135
|
+
'startEvent|endEvent|boundaryEvent|intermediateCatchEvent|intermediateThrowEvent|' +
|
|
136
|
+
'callActivity|parallelGateway|eventBasedGateway (userTask is process-builder only)'
|
|
130
137
|
)
|
|
131
138
|
.option('--name <name>', 'Node name')
|
|
132
139
|
.option('--parent <id>', 'Place inside this subProcess/transaction')
|
|
@@ -136,13 +143,13 @@ pb.command('add')
|
|
|
136
143
|
'exactly one edge must already run --from → --to)'
|
|
137
144
|
)
|
|
138
145
|
.option('--to <id>', 'Insert between two already-connected nodes: target id (requires --from)')
|
|
139
|
-
.option('--pb <
|
|
146
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
140
147
|
.action((opts) => pbAdd(opts).catch(die));
|
|
141
148
|
|
|
142
149
|
pb.command('rm')
|
|
143
150
|
.description('Remove a node, its edges, and its script/page files')
|
|
144
151
|
.requiredOption('--id <id>', 'Node id to remove')
|
|
145
|
-
.option('--pb <
|
|
152
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
146
153
|
.action((opts) => pbRemove(opts).catch(die));
|
|
147
154
|
|
|
148
155
|
pb.command('connect')
|
|
@@ -153,7 +160,7 @@ pb.command('connect')
|
|
|
153
160
|
.option('--condition <expr>', 'Condition expression, e.g. ${isAlive}')
|
|
154
161
|
.option('--condition-type <type>', 'xsi:type for the condition (default tFormalExpression)')
|
|
155
162
|
.option('--default', 'Mark this as the source gateway default flow')
|
|
156
|
-
.option('--pb <
|
|
163
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
157
164
|
.action((opts) => pbConnect(opts).catch(die));
|
|
158
165
|
|
|
159
166
|
pb.command('disconnect')
|
|
@@ -161,7 +168,7 @@ pb.command('disconnect')
|
|
|
161
168
|
.option('--id <id>', 'Edge id to remove')
|
|
162
169
|
.option('--from <id>', 'Source node id')
|
|
163
170
|
.option('--to <id>', 'Target node id')
|
|
164
|
-
.option('--pb <
|
|
171
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
165
172
|
.action((opts) => pbDisconnect(opts).catch(die));
|
|
166
173
|
|
|
167
174
|
pb.command('set-default')
|
|
@@ -169,23 +176,25 @@ pb.command('set-default')
|
|
|
169
176
|
.option('--id <id>', 'Edge id to mark default')
|
|
170
177
|
.option('--from <id>', 'Source gateway id')
|
|
171
178
|
.option('--to <id>', 'Target node id')
|
|
172
|
-
.option('--pb <
|
|
179
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
173
180
|
.action((opts) => pbSetDefault(opts).catch(die));
|
|
174
181
|
|
|
175
182
|
pb.command('lint')
|
|
176
|
-
.description(
|
|
177
|
-
|
|
183
|
+
.description(
|
|
184
|
+
'Check diagram for structural issues (edge names, incoming-edge rules, gateway rules)'
|
|
185
|
+
)
|
|
186
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
178
187
|
.action((opts) => pbLint(opts).catch(die));
|
|
179
188
|
|
|
180
189
|
pb.command('ls')
|
|
181
190
|
.description('List nodes and edges (discover ids without reading the YAML)')
|
|
182
|
-
.option('--pb <
|
|
191
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
183
192
|
.action((opts) => pbList(opts).catch(die));
|
|
184
193
|
|
|
185
194
|
pb.command('preview')
|
|
186
195
|
.description('Render the diagram to an SVG (dev check of format output)')
|
|
187
196
|
.option('--out <file>', 'Output path (default <project>/preview.svg)')
|
|
188
|
-
.option('--pb <
|
|
197
|
+
.option('--pb <document_id_or_name>', 'Target wizard/LRP')
|
|
189
198
|
.action((opts) => pbPreview(opts).catch(die));
|
|
190
199
|
|
|
191
200
|
program.parse();
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Symphony long-running-process (LRP) API client. Base = protocol+host of
|
|
2
|
+
// IMPORTEXPORT_URL (the tabulator/deployBpmn endpoints live on the Symphony
|
|
3
|
+
// host itself, not under the import/export path). Auth uses the same
|
|
4
|
+
// Keycloak bearer as everything else (getToken()).
|
|
5
|
+
//
|
|
6
|
+
// Unlike PBs, LRPs have no stable guid: the tabulator `id` changes on every
|
|
7
|
+
// save/version bump (confirmed live — a re-fetched id differs from a
|
|
8
|
+
// previously captured one for the same process). The process `name` is the
|
|
9
|
+
// only stable identifier, so every write re-resolves the current id/tenantId
|
|
10
|
+
// by name immediately before acting on it.
|
|
11
|
+
|
|
12
|
+
import fetch from 'node-fetch';
|
|
13
|
+
|
|
14
|
+
function getSymphonyBase() {
|
|
15
|
+
const url = process.env.IMPORTEXPORT_URL;
|
|
16
|
+
if (!url) throw new Error('IMPORTEXPORT_URL is not configured. Run `prbot init`.');
|
|
17
|
+
const parsed = new URL(url);
|
|
18
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// List/search processes by name (server-side `like` filter when nameFilter is
|
|
22
|
+
// set; unfiltered page otherwise). Returns the richer per-row fields the save
|
|
23
|
+
// body needs (tenantId, svg), not just id/name.
|
|
24
|
+
async function listLrps(token, nameFilter, signal) {
|
|
25
|
+
const base = getSymphonyBase();
|
|
26
|
+
const size = nameFilter ? 20 : 12;
|
|
27
|
+
const params = encodeURIComponent(JSON.stringify({ page: 1, size, sorters: [], filters: [] }));
|
|
28
|
+
const otherfilters = encodeURIComponent(
|
|
29
|
+
JSON.stringify([
|
|
30
|
+
{ field: 'id', type: '=', value: null },
|
|
31
|
+
{ field: 'name', type: 'like', value: nameFilter ?? null },
|
|
32
|
+
{ field: 'tenantId', type: '=', value: null },
|
|
33
|
+
{ field: 'latestVersion', type: '=', value: true },
|
|
34
|
+
])
|
|
35
|
+
);
|
|
36
|
+
const othersort = encodeURIComponent(
|
|
37
|
+
JSON.stringify({ field: 'lastModifiedDate', dir: 'desc' })
|
|
38
|
+
);
|
|
39
|
+
const url = `${base}/symphony/restInfo/ajax/tabulator?params=${params}&connector=SymphBpmnFileTabCon&otherfilters=${otherfilters}&card=true&othersort=${othersort}`;
|
|
40
|
+
|
|
41
|
+
const res = await fetch(url, {
|
|
42
|
+
headers: { accept: 'application/json', Authorization: `Bearer ${token}` },
|
|
43
|
+
signal,
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) throw new Error(`LRP list failed with ${res.status}: ${await res.text()}`);
|
|
46
|
+
const json = await res.json();
|
|
47
|
+
|
|
48
|
+
const rows = [];
|
|
49
|
+
for (const row of json.data || []) {
|
|
50
|
+
for (let i = 1; i <= 4; i++) {
|
|
51
|
+
const cell = row[`cellContent${i}`];
|
|
52
|
+
if (cell && cell.id && cell.name) {
|
|
53
|
+
rows.push({
|
|
54
|
+
id: String(cell.id),
|
|
55
|
+
name: cell.name,
|
|
56
|
+
tenantId: cell.tenantId,
|
|
57
|
+
version: cell.version,
|
|
58
|
+
status: cell.status,
|
|
59
|
+
bpmnFileSvg: cell.bpmnFileSvg,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return rows;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Resolve the current row for a process by its stable name (exact match
|
|
68
|
+
// against the server-side `like` search results) — the id-by-name lookup
|
|
69
|
+
// that replaces PB's guid-based addressing everywhere.
|
|
70
|
+
async function resolveLrpByName(token, name) {
|
|
71
|
+
const rows = await listLrps(token, name);
|
|
72
|
+
const row = rows.find((r) => r.name === name);
|
|
73
|
+
if (!row) throw new Error(`No long-running process found with name "${name}"`);
|
|
74
|
+
return row;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fetch the full BPMN XML + description for one process by id. The endpoint
|
|
78
|
+
// returns an HTML/JS fragment (not JSON) — the real payload is a base64
|
|
79
|
+
// `doc.value` assignment plus a set of `bpmn_*` hidden inputs.
|
|
80
|
+
async function getLrpXml(token, id) {
|
|
81
|
+
const base = getSymphonyBase();
|
|
82
|
+
const url = `${base}/symphony/restInfo/ajax/tabulator/id/${id}?connector=SymphBpmnFileTabCon&modelroot=/management/development/edit`;
|
|
83
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
84
|
+
if (!res.ok) throw new Error(`LRP detail fetch failed with ${res.status}: ${await res.text()}`);
|
|
85
|
+
const text = await res.text();
|
|
86
|
+
|
|
87
|
+
const docMatch = text.match(/doc\.value\s*=\s*'([^']+)'/);
|
|
88
|
+
if (!docMatch) throw new Error('Could not find doc.value in LRP detail response');
|
|
89
|
+
const filenameMatch = text.match(/filename\.value\s*=\s*'([^']+)'/);
|
|
90
|
+
if (!filenameMatch) throw new Error('Could not find filename.value in LRP detail response');
|
|
91
|
+
const descMatch = text.match(/name="bpmn_description"\s+value="([^"]*)"/);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
xml: Buffer.from(docMatch[1], 'base64').toString('utf-8'),
|
|
95
|
+
filename: filenameMatch[1],
|
|
96
|
+
description: descMatch ? descMatch[1] : '',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Save (PATCH) the wizard's BPMN back to Symphony. `row` must carry the
|
|
101
|
+
// CURRENT id/tenantId (re-resolved by name immediately before this call —
|
|
102
|
+
// see resolveLrpByName) and `bpmnFileSvg` (the last-known diagram thumbnail,
|
|
103
|
+
// echoed back unchanged per the agreed approach — it's display-only, the
|
|
104
|
+
// server deploys from the XML, not the SVG). `newVersion:false` saves the
|
|
105
|
+
// process in place rather than creating a new version.
|
|
106
|
+
async function saveLrp(token, row, xml, description = '') {
|
|
107
|
+
const base = getSymphonyBase();
|
|
108
|
+
const url = `${base}/symphony/restInfo/ajax/tabulator/${row.id}?connector=SymphBpmnFileTabCon`;
|
|
109
|
+
const body = {
|
|
110
|
+
id: row.id,
|
|
111
|
+
tenantId: row.tenantId,
|
|
112
|
+
newVersion: false,
|
|
113
|
+
description,
|
|
114
|
+
name: row.name,
|
|
115
|
+
bpmnFile: Buffer.from(xml, 'utf-8').toString('base64'),
|
|
116
|
+
bpmnFileSvg: row.bpmnFileSvg ?? '',
|
|
117
|
+
oldTenantId: row.tenantId,
|
|
118
|
+
};
|
|
119
|
+
const res = await fetch(url, {
|
|
120
|
+
method: 'PATCH',
|
|
121
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
122
|
+
body: JSON.stringify(body),
|
|
123
|
+
});
|
|
124
|
+
if (!res.ok) throw new Error(`LRP save failed with ${res.status}: ${await res.text()}`);
|
|
125
|
+
const responseText = await res.text();
|
|
126
|
+
return responseText ? JSON.parse(responseText) : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Resolve-by-name + fetch detail in one call — the shape both pull's
|
|
130
|
+
// classification step and pullLrpEntry/pushLrpEntry need: the current row
|
|
131
|
+
// (id/tenantId/svg, re-resolved fresh — never trust a stale one) plus a
|
|
132
|
+
// decompose-ready payload.
|
|
133
|
+
async function fetchUpstream(token, name) {
|
|
134
|
+
const row = await resolveLrpByName(token, name);
|
|
135
|
+
const { xml, description } = await getLrpXml(token, row.id);
|
|
136
|
+
return { row: { ...row, description }, payload: { built_page: xml } };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Deploy a saved process so live consumers see the latest version.
|
|
140
|
+
async function deployLrp(token, id) {
|
|
141
|
+
const base = getSymphonyBase();
|
|
142
|
+
const url = `${base}/symphony/restInfo/ajax/deployBpmn`;
|
|
143
|
+
const res = await fetch(url, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
146
|
+
body: JSON.stringify({ id }),
|
|
147
|
+
});
|
|
148
|
+
if (!res.ok) throw new Error(`LRP deploy failed with ${res.status}: ${await res.text()}`);
|
|
149
|
+
const responseText = await res.text();
|
|
150
|
+
return responseText ? JSON.parse(responseText) : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { listLrps, resolveLrpByName, getLrpXml, saveLrp, deployLrp, fetchUpstream };
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { stringify as yamlStringify } from 'yaml';
|
|
16
16
|
import { pad, toSlug } from './pbProject.js';
|
|
17
|
+
import { SCRIPT_TEMPLATE } from './pbScriptTemplate.js';
|
|
17
18
|
|
|
18
19
|
// Fixed element sizes (measured across all fixtures). Containers are sized by
|
|
19
20
|
// the layout engine; we seed a small default so a stubbed clone stays valid.
|
|
@@ -27,6 +28,12 @@ const SIZE = {
|
|
|
27
28
|
userTask: [84, 84],
|
|
28
29
|
subProcess: [350, 200],
|
|
29
30
|
transaction: [350, 200],
|
|
31
|
+
// LRP-only (measured across the real LRP corpus):
|
|
32
|
+
intermediateCatchEvent: [36, 36],
|
|
33
|
+
intermediateThrowEvent: [36, 36],
|
|
34
|
+
callActivity: [84, 84],
|
|
35
|
+
parallelGateway: [50, 50],
|
|
36
|
+
eventBasedGateway: [50, 50],
|
|
30
37
|
};
|
|
31
38
|
|
|
32
39
|
// BPMN id prefixes by node type (mirrors the upstream naming convention).
|
|
@@ -40,6 +47,11 @@ const PREFIX = {
|
|
|
40
47
|
userTask: 'UserTask',
|
|
41
48
|
subProcess: 'SubProcess',
|
|
42
49
|
transaction: 'Transaction',
|
|
50
|
+
intermediateCatchEvent: 'IntermediateCatchEvent',
|
|
51
|
+
intermediateThrowEvent: 'IntermediateThrowEvent',
|
|
52
|
+
callActivity: 'CallActivity',
|
|
53
|
+
parallelGateway: 'ParallelGateway',
|
|
54
|
+
eventBasedGateway: 'EventBasedGateway',
|
|
43
55
|
};
|
|
44
56
|
|
|
45
57
|
const CONTAINER = new Set(['subProcess', 'transaction']);
|
|
@@ -126,7 +138,7 @@ function nextScriptSeq(existingScriptFiles) {
|
|
|
126
138
|
|
|
127
139
|
// ---------- add ----------
|
|
128
140
|
|
|
129
|
-
// Add a node. For scriptTask, scaffolds
|
|
141
|
+
// Add a node. For scriptTask, scaffolds a templated script file and refs it. For
|
|
130
142
|
// userTask, scaffolds a minimal page file + a manifest page entry (guid=null →
|
|
131
143
|
// push will create it upstream). Container nodes start empty + expanded.
|
|
132
144
|
function addNode(structure, manifest, opts, ctx = {}) {
|
|
@@ -144,7 +156,7 @@ function addNode(structure, manifest, opts, ctx = {}) {
|
|
|
144
156
|
if (type === 'scriptTask') {
|
|
145
157
|
const seq = nextScriptSeq(ctx.existingScripts);
|
|
146
158
|
const file = `scripts/${pad(seq)}_${toSlug(name || id)}.js`;
|
|
147
|
-
writes[file] =
|
|
159
|
+
writes[file] = SCRIPT_TEMPLATE;
|
|
148
160
|
node.script = file;
|
|
149
161
|
} else if (type === 'userTask') {
|
|
150
162
|
const formKey = toSlug(name || id);
|
|
@@ -434,19 +446,24 @@ function lintEdgeNames(structure) {
|
|
|
434
446
|
for (const e of outs) {
|
|
435
447
|
if (e.id === def) continue;
|
|
436
448
|
if (!e.name) {
|
|
437
|
-
issues.push(
|
|
438
|
-
`${n.id} → ${e.target} (${e.id}): non-default flow has no name.`
|
|
439
|
-
);
|
|
449
|
+
issues.push(`${n.id} → ${e.target} (${e.id}): non-default flow has no name.`);
|
|
440
450
|
}
|
|
441
451
|
}
|
|
442
452
|
});
|
|
443
453
|
return issues;
|
|
444
454
|
}
|
|
445
455
|
|
|
456
|
+
// Gateway types that legitimately fan multiple flows into one node (a "join").
|
|
457
|
+
// parallelGateway/eventBasedGateway joins are common in the real LRP corpus
|
|
458
|
+
// (61 occurrences) — only exclusiveGateway's merge semantics need a lint rule.
|
|
459
|
+
const JOIN_CAPABLE_GATEWAYS = new Set(['exclusiveGateway', 'parallelGateway', 'eventBasedGateway']);
|
|
460
|
+
|
|
446
461
|
// Flag incoming-edge violations:
|
|
447
|
-
// - Only
|
|
462
|
+
// - Only a gateway may have >1 incoming flows (plain tasks/events are 1-in).
|
|
448
463
|
// - An exclusiveGateway may not have both >1 incoming AND >1 outgoing (pick one
|
|
449
|
-
// direction for merging or splitting, not both at once)
|
|
464
|
+
// direction for merging or splitting, not both at once) — parallel/event-based
|
|
465
|
+
// gateways may legitimately fork-then-join across the diagram, so this
|
|
466
|
+
// specific rule stays scoped to exclusiveGateway.
|
|
450
467
|
function lintIncomingEdges(structure) {
|
|
451
468
|
const inCount = {};
|
|
452
469
|
const outCount = {};
|
|
@@ -461,9 +478,9 @@ function lintIncomingEdges(structure) {
|
|
|
461
478
|
eachNode(structure.nodes, null, (n) => {
|
|
462
479
|
const inc = inCount[n.id] || 0;
|
|
463
480
|
const out = outCount[n.id] || 0;
|
|
464
|
-
if (inc > 1 && n.type
|
|
481
|
+
if (inc > 1 && !JOIN_CAPABLE_GATEWAYS.has(n.type)) {
|
|
465
482
|
issues.push(
|
|
466
|
-
`${n.id} (${n.name || n.type}): only
|
|
483
|
+
`${n.id} (${n.name || n.type}): only a gateway may have multiple incoming flows (has ${inc}).`
|
|
467
484
|
);
|
|
468
485
|
}
|
|
469
486
|
if (n.type === 'exclusiveGateway' && inc > 1 && out > 1) {
|
|
@@ -477,7 +494,11 @@ function lintIncomingEdges(structure) {
|
|
|
477
494
|
|
|
478
495
|
// Run all lint rules and return combined issues.
|
|
479
496
|
function lintAll(structure) {
|
|
480
|
-
return [
|
|
497
|
+
return [
|
|
498
|
+
...lintGateways(structure),
|
|
499
|
+
...lintEdgeNames(structure),
|
|
500
|
+
...lintIncomingEdges(structure),
|
|
501
|
+
];
|
|
481
502
|
}
|
|
482
503
|
|
|
483
504
|
// Remove an edge by id, or by --from/--to pair.
|