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',