diff --git a/index.js b/index.js
index 611b381..d9c2982 100644
--- a/index.js
+++ b/index.js
@@ -75,8 +75,10 @@ const camundaCloud85Rules = withConfig({
'wait-for-completion': 'error'
}, { version: '8.5' });
-const camundaCloud86Rules = withConfig(
- omit(camundaCloud85Rules, 'inclusive-gateway'), { version: '8.6' });
+const camundaCloud86Rules = withConfig({
+ ...omit(camundaCloud85Rules, 'inclusive-gateway'),
+ 'duplicate-execution-listeners': 'error'
+}, { version: '8.6' });
const camundaPlatform719Rules = withConfig({
'history-time-to-live': 'info'
@@ -105,6 +107,7 @@ const rules = {
'called-element': './rules/camunda-cloud/called-element',
'collapsed-subprocess': './rules/camunda-cloud/collapsed-subprocess',
'connector-properties': './rules/camunda-cloud/connector-properties',
+ 'duplicate-execution-listeners': './rules/camunda-cloud/duplicate-execution-listeners',
'duplicate-task-headers': './rules/camunda-cloud/duplicate-task-headers',
'error-reference': './rules/camunda-cloud/error-reference',
'escalation-boundary-event-attached-to-ref': './rules/camunda-cloud/escalation-boundary-event-attached-to-ref',
diff --git a/package-lock.json b/package-lock.json
index ee54dac..d275ee0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,7 +26,7 @@
"modeler-moddle": "^0.2.0",
"sinon": "^17.0.1",
"sinon-chai": "^3.7.0",
- "zeebe-bpmn-moddle": "^1.1.0"
+ "zeebe-bpmn-moddle": "^1.4.0"
},
"engines": {
"node": "*"
@@ -3118,10 +3118,11 @@
}
},
"node_modules/zeebe-bpmn-moddle": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/zeebe-bpmn-moddle/-/zeebe-bpmn-moddle-1.1.0.tgz",
- "integrity": "sha512-ES/UZFO0VmKvAzL4+cD3VcQpKvlmgLtnFKTyiv0DdDcxNrdQg1rI0OmUdrKMiybAbtAgPDkVXZCusE3kkXwEyQ==",
- "dev": true
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/zeebe-bpmn-moddle/-/zeebe-bpmn-moddle-1.4.0.tgz",
+ "integrity": "sha512-XSm0fMHPjQ5cmEGxga02du9arxb5NKH5ve7VQ0LFSes4wGcrZ/oJjaR3NlBqH6xTTD3g+Mcbh45+yesydPUQPg==",
+ "dev": true,
+ "license": "MIT"
}
},
"dependencies": {
@@ -5405,9 +5406,9 @@
"dev": true
},
"zeebe-bpmn-moddle": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/zeebe-bpmn-moddle/-/zeebe-bpmn-moddle-1.1.0.tgz",
- "integrity": "sha512-ES/UZFO0VmKvAzL4+cD3VcQpKvlmgLtnFKTyiv0DdDcxNrdQg1rI0OmUdrKMiybAbtAgPDkVXZCusE3kkXwEyQ==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/zeebe-bpmn-moddle/-/zeebe-bpmn-moddle-1.4.0.tgz",
+ "integrity": "sha512-XSm0fMHPjQ5cmEGxga02du9arxb5NKH5ve7VQ0LFSes4wGcrZ/oJjaR3NlBqH6xTTD3g+Mcbh45+yesydPUQPg==",
"dev": true
}
}
diff --git a/package.json b/package.json
index f16d0ea..6bc90f3 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"modeler-moddle": "^0.2.0",
"sinon": "^17.0.1",
"sinon-chai": "^3.7.0",
- "zeebe-bpmn-moddle": "^1.1.0"
+ "zeebe-bpmn-moddle": "^1.4.0"
},
"dependencies": {
"@bpmn-io/feel-lint": "^1.2.0",
diff --git a/rules/camunda-cloud/duplicate-execution-listeners.js b/rules/camunda-cloud/duplicate-execution-listeners.js
new file mode 100644
index 0000000..3fd2600
--- /dev/null
+++ b/rules/camunda-cloud/duplicate-execution-listeners.js
@@ -0,0 +1,87 @@
+const {
+ is,
+ isAny
+} = require('bpmnlint-utils');
+
+const {
+ ERROR_TYPES,
+ findExtensionElement,
+ hasDuplicatedPropertyValues
+} = require('../utils/element');
+
+const { reportErrors } = require('../utils/reporter');
+
+const { skipInNonExecutableProcess } = require('../utils/rule');
+
+module.exports = skipInNonExecutableProcess(function() {
+ function check(node, reporter) {
+ const executionListeners = findExtensionElement(node, 'zeebe:ExecutionListeners');
+
+ if (!executionListeners) {
+ return;
+ }
+
+ const errors = hasDuplicatedExecutionListeners(executionListeners, node);
+
+ if (errors && errors.length) {
+ reportErrors(node, reporter, errors);
+ }
+ }
+
+ return {
+ check
+ };
+});
+
+// helpers //////////
+function hasDuplicatedExecutionListeners(executionListeners, parentNode = null) {
+ const listeners = executionListeners.get('listeners');
+
+ // (1) find duplicates
+ const duplicates = [];
+ const events = new Map();
+ for (const listener of listeners) {
+ const eventName = listener.get('eventType'),
+ type = listener.get('type');
+
+ if (!events.has(eventName)) {
+ events.set(eventName, new Set([ type ]));
+ continue;
+ }
+
+ const types = events.get(eventName);
+ if (types.has(type)) {
+ duplicates.push(listener);
+ } else {
+ types.add(type);
+ }
+ }
+
+ // (2) report error for each duplicate
+ if (duplicates.length) {
+ return duplicates.map(duplicate => {
+ const eventName = duplicate.get('eventType'),
+ type = duplicate.get('type');
+
+ // (3) find properties with duplicate
+ const duplicateProperties = listeners.filter(listener => listener.get('eventType') === eventName && listener.get('type') === type);
+
+ // (4) report error
+ return {
+ message: `Duplicate execution listener with event type <${eventName}> and job type <${type}>`,
+ path: null,
+ data: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: executionListeners,
+ parentNode: parentNode === executionListeners ? null : parentNode,
+ duplicatedProperty: 'type',
+ duplicatedPropertyValue: type,
+ properties: duplicateProperties,
+ propertiesName: 'listeners'
+ }
+ };
+ });
+ }
+
+ return [];
+}
\ No newline at end of file
diff --git a/test/camunda-cloud/duplicate-execution-listeners.spec.js b/test/camunda-cloud/duplicate-execution-listeners.spec.js
new file mode 100644
index 0000000..0dc5e7d
--- /dev/null
+++ b/test/camunda-cloud/duplicate-execution-listeners.spec.js
@@ -0,0 +1,283 @@
+const RuleTester = require('bpmnlint/lib/testers/rule-tester');
+
+const rule = require('../../rules/camunda-cloud/duplicate-execution-listeners');
+
+const {
+ createDefinitions,
+ createModdle,
+ createProcess
+} = require('../helper');
+
+const { ERROR_TYPES } = require('../../rules/utils/element');
+
+const valid = [
+ {
+ name: 'service task',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+ `))
+ },
+ {
+ name: 'send task',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+ `))
+ },
+ {
+ name: 'user task',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+ `))
+ },
+ {
+ name: 'business rule task',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+
+ `))
+ },
+ {
+ name: 'script task',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+
+
+ `))
+ },
+ {
+ name: 'message throw event',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+
+ `))
+ },
+ {
+ name: 'service task (non-executable process)',
+ config: { version: '8.6' },
+ moddleElement: createModdle(createDefinitions(`
+
+
+
+
+
+
+
+
+
+
+ `))
+ }
+];
+
+const invalid = [
+ {
+ name: 'service task',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+ `)),
+ report: {
+ id: 'ServiceTask_1',
+ message: 'Duplicate execution listener with event type and job type ',
+ path: null,
+ data: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:ExecutionListeners',
+ parentNode: 'ServiceTask_1',
+ duplicatedProperty: 'type',
+ duplicatedPropertyValue: 'duplicate',
+ properties: [
+ 'zeebe:ExecutionListener',
+ 'zeebe:ExecutionListener'
+ ],
+ propertiesName: 'listeners'
+ }
+ }
+ },
+ {
+ name: 'service task (multiple duplicates)',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+
+ `)),
+ report: [
+ {
+ id: 'ServiceTask_1',
+ message: 'Duplicate execution listener with event type and job type ',
+ path: null,
+ data: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:ExecutionListeners',
+ parentNode: 'ServiceTask_1',
+ duplicatedProperty: 'type',
+ duplicatedPropertyValue: 'duplicate',
+ properties: [
+ 'zeebe:ExecutionListener',
+ 'zeebe:ExecutionListener',
+ 'zeebe:ExecutionListener'
+ ],
+ propertiesName: 'listeners'
+ }
+ },
+ {
+ id: 'ServiceTask_1',
+ message: 'Duplicate execution listener with event type and job type ',
+ path: null,
+ data: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:ExecutionListeners',
+ parentNode: 'ServiceTask_1',
+ duplicatedProperty: 'type',
+ duplicatedPropertyValue: 'duplicate',
+ properties: [
+ 'zeebe:ExecutionListener',
+ 'zeebe:ExecutionListener',
+ 'zeebe:ExecutionListener'
+ ],
+ propertiesName: 'listeners'
+ }
+ }
+ ]
+ },
+ {
+ name: 'service task (multiple duplicates)',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+
+
+ `)),
+ report: [
+ {
+ id: 'ServiceTask_1',
+ message: 'Duplicate execution listener with event type and job type ',
+ path: null,
+ data: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:ExecutionListeners',
+ parentNode: 'ServiceTask_1',
+ duplicatedProperty: 'type',
+ duplicatedPropertyValue: 'duplicate',
+ properties: [
+ 'zeebe:ExecutionListener',
+ 'zeebe:ExecutionListener'
+ ],
+ propertiesName: 'listeners'
+ }
+ },
+ {
+ id: 'ServiceTask_1',
+ message: 'Duplicate execution listener with event type and job type ',
+ path: null,
+ data: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:ExecutionListeners',
+ parentNode: 'ServiceTask_1',
+ duplicatedProperty: 'type',
+ duplicatedPropertyValue: 'duplicate_2',
+ properties: [
+ 'zeebe:ExecutionListener',
+ 'zeebe:ExecutionListener'
+ ],
+ propertiesName: 'listeners'
+ }
+ }
+ ]
+ },
+ {
+ name: 'service task (no type)',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+ `)),
+ report: {
+ id: 'ServiceTask_1',
+ message: 'Duplicate execution listener with event type and job type <>',
+ path: null,
+ data: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:ExecutionListeners',
+ parentNode: 'ServiceTask_1',
+ duplicatedProperty: 'type',
+ duplicatedPropertyValue: '',
+ properties: [
+ 'zeebe:ExecutionListener',
+ 'zeebe:ExecutionListener'
+ ],
+ propertiesName: 'listeners'
+ }
+ }
+ }
+];
+
+RuleTester.verify('duplicate-execution-listeners', rule, {
+ valid,
+ invalid
+});
\ No newline at end of file
diff --git a/test/config/configs.spec.js b/test/config/configs.spec.js
index db129f9..0d418dc 100644
--- a/test/config/configs.spec.js
+++ b/test/config/configs.spec.js
@@ -316,6 +316,7 @@ describe('configs', function() {
it('camunda-cloud-8-6', expectRules(configs, 'camunda-cloud-8-6', {
'called-element': [ 'error', { version: '8.6' } ],
'connector-properties': [ 'warn', { version: '8.6' } ],
+ 'duplicate-execution-listeners': [ 'error', { version: '8.6' } ],
'duplicate-task-headers': [ 'error', { version: '8.6' } ],
'element-type': [ 'error', { version: '8.6' } ],
'error-reference': [ 'error', { version: '8.6' } ],
@@ -368,6 +369,7 @@ describe('configs', function() {
'called-element': 'error',
'collapsed-subprocess': 'error',
'connector-properties': 'warn',
+ 'duplicate-execution-listeners': 'error',
'duplicate-task-headers': 'error',
'error-reference': 'error',
'escalation-boundary-event-attached-to-ref': 'error',