bpmnlint-plugin-camunda-compat 2.21.0 → 2.22.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/index.js +9 -2
- package/package.json +2 -2
- package/rules/camunda-cloud/duplicate-execution-listeners.js +33 -0
- package/rules/camunda-cloud/execution-listener.js +34 -0
- package/rules/camunda-cloud/no-execution-listeners.js +21 -0
- package/rules/camunda-cloud/no-loop.js +204 -33
- package/rules/utils/element.js +49 -0
- package/rules/utils/error-types.js +1 -0
package/index.js
CHANGED
@@ -12,6 +12,7 @@ const camundaCloud10Rules = withConfig({
|
|
12
12
|
'loop-characteristics': 'error',
|
13
13
|
'message-reference': 'error',
|
14
14
|
'no-candidate-users': 'error',
|
15
|
+
'no-execution-listeners': 'error',
|
15
16
|
'no-expression': 'error',
|
16
17
|
'no-loop': 'error',
|
17
18
|
'no-multiple-none-start-events': 'error',
|
@@ -75,8 +76,11 @@ const camundaCloud85Rules = withConfig({
|
|
75
76
|
'wait-for-completion': 'error'
|
76
77
|
}, { version: '8.5' });
|
77
78
|
|
78
|
-
const camundaCloud86Rules = withConfig(
|
79
|
-
omit(camundaCloud85Rules, 'inclusive-gateway'
|
79
|
+
const camundaCloud86Rules = withConfig({
|
80
|
+
...omit(camundaCloud85Rules, [ 'inclusive-gateway', 'no-execution-listeners' ]),
|
81
|
+
'duplicate-execution-listeners': 'error',
|
82
|
+
'execution-listener': 'error'
|
83
|
+
}, { version: '8.6' });
|
80
84
|
|
81
85
|
const camundaPlatform719Rules = withConfig({
|
82
86
|
'history-time-to-live': 'info'
|
@@ -105,12 +109,14 @@ const rules = {
|
|
105
109
|
'called-element': './rules/camunda-cloud/called-element',
|
106
110
|
'collapsed-subprocess': './rules/camunda-cloud/collapsed-subprocess',
|
107
111
|
'connector-properties': './rules/camunda-cloud/connector-properties',
|
112
|
+
'duplicate-execution-listeners': './rules/camunda-cloud/duplicate-execution-listeners',
|
108
113
|
'duplicate-task-headers': './rules/camunda-cloud/duplicate-task-headers',
|
109
114
|
'error-reference': './rules/camunda-cloud/error-reference',
|
110
115
|
'escalation-boundary-event-attached-to-ref': './rules/camunda-cloud/escalation-boundary-event-attached-to-ref',
|
111
116
|
'escalation-reference': './rules/camunda-cloud/escalation-reference',
|
112
117
|
'event-based-gateway-target': './rules/camunda-cloud/event-based-gateway-target',
|
113
118
|
'executable-process': './rules/camunda-cloud/executable-process',
|
119
|
+
'execution-listener': './rules/camunda-cloud/execution-listener',
|
114
120
|
'feel': './rules/camunda-cloud/feel',
|
115
121
|
'history-time-to-live': './rules/camunda-platform/history-time-to-live',
|
116
122
|
'implementation': './rules/camunda-cloud/implementation',
|
@@ -119,6 +125,7 @@ const rules = {
|
|
119
125
|
'loop-characteristics': './rules/camunda-cloud/loop-characteristics',
|
120
126
|
'message-reference': './rules/camunda-cloud/message-reference',
|
121
127
|
'no-candidate-users': './rules/camunda-cloud/no-candidate-users',
|
128
|
+
'no-execution-listeners': './rules/camunda-cloud/no-execution-listeners',
|
122
129
|
'no-expression': './rules/camunda-cloud/no-expression',
|
123
130
|
'no-loop': './rules/camunda-cloud/no-loop',
|
124
131
|
'no-multiple-none-start-events': './rules/camunda-cloud/no-multiple-none-start-events',
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "bpmnlint-plugin-camunda-compat",
|
3
|
-
"version": "2.
|
3
|
+
"version": "2.22.0",
|
4
4
|
"description": "A bpmnlint plug-in for Camunda compatibility",
|
5
5
|
"main": "index.js",
|
6
6
|
"scripts": {
|
@@ -34,7 +34,7 @@
|
|
34
34
|
"modeler-moddle": "^0.2.0",
|
35
35
|
"sinon": "^17.0.1",
|
36
36
|
"sinon-chai": "^3.7.0",
|
37
|
-
"zeebe-bpmn-moddle": "^1.
|
37
|
+
"zeebe-bpmn-moddle": "^1.4.0"
|
38
38
|
},
|
39
39
|
"dependencies": {
|
40
40
|
"@bpmn-io/feel-lint": "^1.2.0",
|
@@ -0,0 +1,33 @@
|
|
1
|
+
const {
|
2
|
+
findExtensionElement,
|
3
|
+
hasDuplicatedPropertiesValues
|
4
|
+
} = require('../utils/element');
|
5
|
+
|
6
|
+
const { reportErrors } = require('../utils/reporter');
|
7
|
+
|
8
|
+
const { skipInNonExecutableProcess } = require('../utils/rule');
|
9
|
+
|
10
|
+
module.exports = skipInNonExecutableProcess(function() {
|
11
|
+
function check(node, reporter) {
|
12
|
+
const executionListeners = findExtensionElement(node, 'zeebe:ExecutionListeners');
|
13
|
+
|
14
|
+
if (!executionListeners) {
|
15
|
+
return;
|
16
|
+
}
|
17
|
+
|
18
|
+
const errors = hasDuplicatedExecutionListeners(executionListeners, node);
|
19
|
+
|
20
|
+
if (errors && errors.length) {
|
21
|
+
reportErrors(node, reporter, errors);
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
return {
|
26
|
+
check
|
27
|
+
};
|
28
|
+
});
|
29
|
+
|
30
|
+
// helpers //////////
|
31
|
+
function hasDuplicatedExecutionListeners(executionListeners, parentNode = null) {
|
32
|
+
return hasDuplicatedPropertiesValues(executionListeners, 'listeners', [ 'eventType', 'type' ], parentNode);
|
33
|
+
}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
const {
|
2
|
+
findExtensionElement,
|
3
|
+
hasProperties
|
4
|
+
} = require('../utils/element');
|
5
|
+
|
6
|
+
const { reportErrors } = require('../utils/reporter');
|
7
|
+
|
8
|
+
const { skipInNonExecutableProcess } = require('../utils/rule');
|
9
|
+
|
10
|
+
|
11
|
+
module.exports = skipInNonExecutableProcess(function() {
|
12
|
+
function check(node, reporter) {
|
13
|
+
const executionListeners = findExtensionElement(node, 'zeebe:ExecutionListeners');
|
14
|
+
|
15
|
+
if (!executionListeners) {
|
16
|
+
return;
|
17
|
+
}
|
18
|
+
|
19
|
+
const listeners = executionListeners.get('listeners');
|
20
|
+
const errors = listeners.flatMap(listener => hasProperties(listener, {
|
21
|
+
type: {
|
22
|
+
required: true
|
23
|
+
}
|
24
|
+
}, node));
|
25
|
+
|
26
|
+
if (errors.length) {
|
27
|
+
reportErrors(node, reporter, errors);
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
return {
|
32
|
+
check
|
33
|
+
};
|
34
|
+
});
|
@@ -0,0 +1,21 @@
|
|
1
|
+
const { reportErrors } = require('../utils/reporter');
|
2
|
+
|
3
|
+
const { skipInNonExecutableProcess } = require('../utils/rule');
|
4
|
+
|
5
|
+
const { hasNoExtensionElement } = require('../utils/element');
|
6
|
+
|
7
|
+
const ALLOWED_VERSION = '8.6';
|
8
|
+
|
9
|
+
module.exports = skipInNonExecutableProcess(function() {
|
10
|
+
function check(node, reporter) {
|
11
|
+
const errors = hasNoExtensionElement(node, 'zeebe:ExecutionListeners', node, ALLOWED_VERSION);
|
12
|
+
|
13
|
+
if (errors && errors.length) {
|
14
|
+
reportErrors(node, reporter, errors);
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
return {
|
19
|
+
check
|
20
|
+
};
|
21
|
+
});
|
@@ -14,6 +14,10 @@ const { ERROR_TYPES } = require('../utils/error-types');
|
|
14
14
|
|
15
15
|
const { skipInNonExecutableProcess } = require('../utils/rule');
|
16
16
|
|
17
|
+
/**
|
18
|
+
* @typedef {import('bpmn-moddle').BaseElement} ModdleElement
|
19
|
+
**/
|
20
|
+
|
17
21
|
const LOOP_REQUIRED_ELEMENT_TYPES = [
|
18
22
|
'bpmn:CallActivity',
|
19
23
|
'bpmn:ManualTask',
|
@@ -38,16 +42,22 @@ module.exports = skipInNonExecutableProcess(function() {
|
|
38
42
|
return;
|
39
43
|
}
|
40
44
|
|
41
|
-
|
45
|
+
// 1. Remove all elements that can be part of an infinite loop
|
46
|
+
const relevantNodes = getFlowElements(node)
|
42
47
|
.filter(flowElement => {
|
43
48
|
return isAnyExactly(flowElement, LOOP_ELEMENT_TYPES);
|
44
|
-
})
|
45
|
-
|
46
|
-
|
47
|
-
|
49
|
+
});
|
50
|
+
|
51
|
+
// 2. Remove all non-required elements. This produces a graph that only contains the required elements,
|
52
|
+
// with annotated edges that preserve the original path.
|
53
|
+
// Any loop found within the simplified graph is a valid loop, as all Vertices in the graph are `LOOP_REQUIRED_ELEMENT_TYPES`.
|
54
|
+
const minimalGraph = simplifyGraph(relevantNodes);
|
48
55
|
|
49
|
-
|
50
|
-
|
56
|
+
// 3. Use breadth-first search to find loops in the simplified Graph.
|
57
|
+
const errors = findLoops(minimalGraph, node);
|
58
|
+
|
59
|
+
if (errors) {
|
60
|
+
reportErrors(node, reporter, errors);
|
51
61
|
}
|
52
62
|
}
|
53
63
|
|
@@ -56,51 +66,112 @@ module.exports = skipInNonExecutableProcess(function() {
|
|
56
66
|
};
|
57
67
|
});
|
58
68
|
|
59
|
-
|
69
|
+
/**
|
70
|
+
* @typedef {Object} GraphNode
|
71
|
+
* @property {ModdleElement} element the bpmn element this node represents
|
72
|
+
* @property {Map<ModdleElement, Array<ModdleElement>>} incoming Maps the target node with the shortest path to it
|
73
|
+
* @property {Map<ModdleElement, Array<ModdleElement>>} outgoing Maps the source node with the shortest path to it
|
74
|
+
*/
|
60
75
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
76
|
+
/**
|
77
|
+
* Simplifies the graph by removing all non-`LOOP_REQUIRED_ELEMENT_TYPES` elements and connecting incoming and outgoing nodes directly.
|
78
|
+
* Annotates the edges with the original path. Uses breadth-first search to find paths.
|
79
|
+
*
|
80
|
+
* @param {Array<ModdleElement>} flowElements
|
81
|
+
* @returns {Map<ModdleElement, GraphNode>}
|
82
|
+
*/
|
83
|
+
function simplifyGraph(flowElements) {
|
65
84
|
|
66
|
-
|
85
|
+
// Transform Array<ModdleElement> into Map<ModdleElement, GraphNode>
|
86
|
+
const graph = elementsToGraph(flowElements);
|
67
87
|
|
68
|
-
|
69
|
-
|
70
|
-
return null;
|
71
|
-
}
|
88
|
+
breadthFirstSearch(graph, (node) => {
|
89
|
+
const { element, outgoing } = node;
|
72
90
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
91
|
+
// Remove non-required element and connect incoming and outgoing nodes directly
|
92
|
+
if (!isAnyExactly(element, LOOP_REQUIRED_ELEMENT_TYPES)) {
|
93
|
+
connectNodes(graph, node);
|
94
|
+
}
|
95
|
+
|
96
|
+
return Array.from(outgoing.keys(), key => graph.get(key));
|
97
|
+
});
|
98
|
+
|
99
|
+
// Clean up all references to removed elements
|
100
|
+
graph.forEach(({ incoming, outgoing }) => {
|
101
|
+
incoming.forEach((_, key) => {
|
102
|
+
if (!graph.has(key)) {
|
103
|
+
incoming.delete(key);
|
104
|
+
}
|
105
|
+
});
|
106
|
+
|
107
|
+
outgoing.forEach((_, key) => {
|
108
|
+
if (!graph.has(key)) {
|
109
|
+
outgoing.delete(key);
|
110
|
+
}
|
111
|
+
});
|
112
|
+
});
|
79
113
|
|
80
|
-
|
81
|
-
|
114
|
+
return graph;
|
115
|
+
}
|
116
|
+
|
117
|
+
|
118
|
+
/**
|
119
|
+
* Uses breadth-first search to find loops in the graph and generate errors.
|
120
|
+
*
|
121
|
+
* @param {Map<ModdleElement, GraphNode>} graph The simplified graph containing only required elements
|
122
|
+
* @param {ModdleElement} root used for reporting the errors
|
123
|
+
* @returns {Array<Object>} errors
|
124
|
+
*/
|
125
|
+
function findLoops(graph, root) {
|
126
|
+
const errors = [];
|
127
|
+
|
128
|
+
// Traverse graph using breadth-first search, remembering the path. If we find a loop, report it.
|
129
|
+
breadthFirstSearch(graph, (node) => {
|
130
|
+
const { element, outgoing, path = [] } = node;
|
131
|
+
|
132
|
+
const nextElements = [ ];
|
133
|
+
outgoing.forEach((connectionPath, nextElement) => {
|
134
|
+
const newPath = [ ...path, element, ...connectionPath ];
|
135
|
+
|
136
|
+
// We already visited this node, we found a loop
|
137
|
+
if (newPath.includes(nextElement)) {
|
138
|
+
errors.push(handleLoop(newPath, nextElement, root));
|
139
|
+
} else {
|
140
|
+
const nextNode = graph.get(nextElement);
|
141
|
+
nextNode.path = nextNode.path || newPath;
|
142
|
+
nextElements.push(nextNode);
|
143
|
+
}
|
144
|
+
});
|
145
|
+
|
146
|
+
return nextElements;
|
147
|
+
});
|
148
|
+
|
149
|
+
return errors.filter(Boolean);
|
150
|
+
}
|
151
|
+
|
152
|
+
const handleLoop = (path, currentNode, root) => {
|
153
|
+
const loop = path.slice(path.indexOf(currentNode));
|
154
|
+
|
155
|
+
if (isIgnoredLoop(loop)) {
|
82
156
|
return null;
|
83
157
|
}
|
84
158
|
|
85
|
-
const elements = visitedFlowElements.slice(visitedFlowElements.indexOf(flowElement));
|
86
|
-
|
87
|
-
// (4) is a loop
|
88
159
|
return {
|
89
|
-
message: `Loop detected: ${
|
160
|
+
message: `Loop detected: ${ loop.map(({ id }) => id).join(' -> ') } -> ${ currentNode.id }`,
|
90
161
|
path: null,
|
91
162
|
data: {
|
92
163
|
type: ERROR_TYPES.LOOP_NOT_ALLOWED,
|
93
|
-
node:
|
164
|
+
node: root,
|
94
165
|
parentNode: null,
|
95
|
-
elements:
|
166
|
+
elements: loop.map(({ id }) => id)
|
96
167
|
}
|
97
168
|
};
|
98
|
-
}
|
169
|
+
};
|
99
170
|
|
100
171
|
function getFlowElements(node) {
|
101
172
|
return node.get('flowElements').reduce((flowElements, flowElement) => {
|
102
173
|
if (is(flowElement, 'bpmn:FlowElementsContainer')) {
|
103
|
-
return [ ...flowElements, ...getFlowElements(flowElement) ];
|
174
|
+
return [ ...flowElements, flowElement, ...getFlowElements(flowElement) ];
|
104
175
|
}
|
105
176
|
|
106
177
|
return [ ...flowElements, flowElement ];
|
@@ -143,4 +214,104 @@ function isIgnoredLoop(elements) {
|
|
143
214
|
|
144
215
|
function isFeel(value) {
|
145
216
|
return isString(value) && value.startsWith('=');
|
217
|
+
}
|
218
|
+
|
219
|
+
const getOrSet = (map, key, defaultValue) => {
|
220
|
+
if (!map.has(key)) {
|
221
|
+
map.set(key, defaultValue);
|
222
|
+
}
|
223
|
+
|
224
|
+
return map.get(key);
|
225
|
+
};
|
226
|
+
|
227
|
+
const setIfAbsent = (map, key, value) => {
|
228
|
+
map.has(key) || map.set(key, value);
|
229
|
+
};
|
230
|
+
|
231
|
+
/**
|
232
|
+
* Transform Array of flow elements into a Graph structure, adding implicit connections (e.g. SubProcess -> StartEvent)
|
233
|
+
* via `getNextFlowElements`.
|
234
|
+
*
|
235
|
+
* @param {Array<ModdleElement>} flowElements
|
236
|
+
* @returns Map<ModdleElement, GraphNode>
|
237
|
+
*/
|
238
|
+
function elementsToGraph(flowElements) {
|
239
|
+
return flowElements.reduce((currentMap, element) => {
|
240
|
+
const currentNode = getOrSet(currentMap, element, {
|
241
|
+
element,
|
242
|
+
incoming: new Map(),
|
243
|
+
outgoing: new Map(),
|
244
|
+
});
|
245
|
+
|
246
|
+
const nextFlowElements = getNextFlowElements(element);
|
247
|
+
|
248
|
+
nextFlowElements.forEach(nextElement => {
|
249
|
+
const nextNode = getOrSet(currentMap, nextElement, {
|
250
|
+
element: nextElement,
|
251
|
+
incoming: new Map(),
|
252
|
+
outgoing: new Map(),
|
253
|
+
});
|
254
|
+
|
255
|
+
nextNode.incoming.set(element, []);
|
256
|
+
currentNode.outgoing.set(nextElement, []);
|
257
|
+
});
|
258
|
+
|
259
|
+
return currentMap;
|
260
|
+
}, new Map());
|
261
|
+
}
|
262
|
+
|
263
|
+
/**
|
264
|
+
* Connects incoming and outgoing nodes directly, add current node to the path and remove node from graph.
|
265
|
+
*/
|
266
|
+
function connectNodes(graph, node) {
|
267
|
+
const { element, incoming, outgoing } = node;
|
268
|
+
|
269
|
+
incoming.forEach((fromPath, fromKey) => {
|
270
|
+
outgoing.forEach((toPath, toKey) => {
|
271
|
+
const fromNode = graph.get(fromKey);
|
272
|
+
const toNode = graph.get(toKey);
|
273
|
+
|
274
|
+
if (!fromNode || !toNode) {
|
275
|
+
return;
|
276
|
+
}
|
277
|
+
|
278
|
+
// We only care about the shortest path, so we don't need to update the path if it's already set
|
279
|
+
setIfAbsent(fromNode.outgoing, toKey, [ ...fromPath, element, ...toPath ]);
|
280
|
+
setIfAbsent(toNode.incoming, fromKey, [ ...fromPath, element, ...toPath ]);
|
281
|
+
});
|
282
|
+
});
|
283
|
+
|
284
|
+
graph.delete(element);
|
285
|
+
}
|
286
|
+
|
287
|
+
/**
|
288
|
+
* Iterates over all nodes in the graph using breadth-first search.
|
289
|
+
*
|
290
|
+
* @param {Map<ModdleElement, GraphNode>} graph
|
291
|
+
* @param {Function} iterationCallback
|
292
|
+
*/
|
293
|
+
function breadthFirstSearch(graph, iterationCallback) {
|
294
|
+
const unvisited = new Set(graph.values());
|
295
|
+
|
296
|
+
while (unvisited.size) {
|
297
|
+
let firstElement = unvisited.values().next().value;
|
298
|
+
unvisited.delete(firstElement);
|
299
|
+
|
300
|
+
const elementsToVisit = [ firstElement ];
|
301
|
+
|
302
|
+
while (elementsToVisit.length) {
|
303
|
+
const node = elementsToVisit.shift();
|
304
|
+
|
305
|
+
const nextElements = iterationCallback(node);
|
306
|
+
|
307
|
+
nextElements.forEach(nextElement => {
|
308
|
+
if (!unvisited.has(nextElement)) {
|
309
|
+
return;
|
310
|
+
}
|
311
|
+
|
312
|
+
unvisited.delete(nextElement);
|
313
|
+
elementsToVisit.push(nextElement);
|
314
|
+
});
|
315
|
+
}
|
316
|
+
}
|
146
317
|
}
|
package/rules/utils/element.js
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
const {
|
2
|
+
filter,
|
2
3
|
isArray,
|
3
4
|
isDefined,
|
4
5
|
isFunction,
|
@@ -6,6 +7,7 @@ const {
|
|
6
7
|
isObject,
|
7
8
|
isString,
|
8
9
|
isUndefined,
|
10
|
+
matchPattern,
|
9
11
|
some
|
10
12
|
} = require('min-dash');
|
11
13
|
|
@@ -133,6 +135,53 @@ module.exports.hasDuplicatedPropertyValues = function(node, propertiesName, prop
|
|
133
135
|
return [];
|
134
136
|
};
|
135
137
|
|
138
|
+
// @TODO(@barmac): use tree algorithm to reduce complexity
|
139
|
+
module.exports.hasDuplicatedPropertiesValues = function(node, containerPropertyName, propertiesNames, parentNode = null) {
|
140
|
+
const properties = node.get(containerPropertyName);
|
141
|
+
|
142
|
+
// (1) find duplicates
|
143
|
+
const duplicates = properties.reduce((foundDuplicates, property, index) => {
|
144
|
+
const previous = properties.slice(0, index);
|
145
|
+
const isDuplicate = previous.find(p => propertiesNames.every(propertyName => p.get(propertyName) === property.get(propertyName)));
|
146
|
+
|
147
|
+
if (isDuplicate) {
|
148
|
+
return foundDuplicates.concat(property);
|
149
|
+
}
|
150
|
+
|
151
|
+
return foundDuplicates;
|
152
|
+
}, []);
|
153
|
+
|
154
|
+
// (2) report error for each duplicate
|
155
|
+
if (duplicates.length) {
|
156
|
+
return duplicates.map(duplicate => {
|
157
|
+
const propertiesMap = {};
|
158
|
+
for (const property of propertiesNames) {
|
159
|
+
propertiesMap[property] = duplicate.get(property);
|
160
|
+
}
|
161
|
+
|
162
|
+
// (3) find properties with duplicate
|
163
|
+
const duplicateProperties = filter(properties, matchPattern(propertiesMap));
|
164
|
+
const duplicatesSummary = propertiesNames.map(propertyName => `property <${ propertyName }> with duplicate value of <${ propertiesMap[propertyName] }>`).join(', ');
|
165
|
+
|
166
|
+
// (4) report error
|
167
|
+
return {
|
168
|
+
message: `Properties of type <${ duplicate.$type }> have properties with duplicate values (${ duplicatesSummary })`,
|
169
|
+
path: null,
|
170
|
+
data: {
|
171
|
+
type: ERROR_TYPES.PROPERTY_VALUES_DUPLICATED,
|
172
|
+
node,
|
173
|
+
parentNode: parentNode == node ? null : parentNode,
|
174
|
+
duplicatedProperties: propertiesMap,
|
175
|
+
properties: duplicateProperties,
|
176
|
+
propertiesName: containerPropertyName
|
177
|
+
}
|
178
|
+
};
|
179
|
+
});
|
180
|
+
}
|
181
|
+
|
182
|
+
return [];
|
183
|
+
};
|
184
|
+
|
136
185
|
module.exports.hasProperties = function(node, properties, parentNode = null) {
|
137
186
|
return Object.entries(properties).reduce((results, property) => {
|
138
187
|
const [ propertyName, propertyChecks ] = property;
|
@@ -19,6 +19,7 @@ module.exports.ERROR_TYPES = Object.freeze({
|
|
19
19
|
PROPERTY_REQUIRED: 'camunda.propertyRequired',
|
20
20
|
PROPERTY_TYPE_NOT_ALLOWED: 'camunda.propertyTypeNotAllowed',
|
21
21
|
PROPERTY_VALUE_DUPLICATED: 'camunda.propertyValueDuplicated',
|
22
|
+
PROPERTY_VALUES_DUPLICATED: 'camunda.propertiesValuesDuplicated',
|
22
23
|
PROPERTY_VALUE_NOT_ALLOWED: 'camunda.propertyValueNotAllowed',
|
23
24
|
PROPERTY_VALUE_REQUIRED: 'camunda.propertyValueRequired',
|
24
25
|
SECRET_EXPRESSION_FORMAT_DEPRECATED: 'camunda.secretExpressionFormatDeprecated'
|