diff --git a/index.js b/index.js
index f3d316e4..a5508198 100644
--- a/index.js
+++ b/index.js
@@ -7,6 +7,7 @@ module.exports = {
rules: {
'called-decision-or-task-definition': [ 'error', calledDecisionOrTaskDefinitionConfig.camundaCloud10 ],
'called-element': 'error',
+ 'duplicate-task-headers': 'error',
'element-type': [ 'error', elementTypeConfig.camundaCloud10 ],
'error-reference': 'error',
'loop-characteristics': 'error',
@@ -20,6 +21,7 @@ module.exports = {
rules: {
'called-decision-or-task-definition': [ 'error', calledDecisionOrTaskDefinitionConfig.camundaCloud11 ],
'called-element': 'error',
+ 'duplicate-task-headers': 'error',
'element-type': [ 'error', elementTypeConfig.camundaCloud11 ],
'error-reference': 'error',
'loop-characteristics': 'error',
@@ -33,6 +35,7 @@ module.exports = {
rules: {
'called-decision-or-task-definition': [ 'error', calledDecisionOrTaskDefinitionConfig.camundaCloud12 ],
'called-element': 'error',
+ 'duplicate-task-headers': 'error',
'element-type': [ 'error', elementTypeConfig.camundaCloud12 ],
'error-reference': 'error',
'loop-characteristics': 'error',
@@ -46,6 +49,7 @@ module.exports = {
rules: {
'called-decision-or-task-definition': [ 'error', calledDecisionOrTaskDefinitionConfig.camundaCloud13 ],
'called-element': 'error',
+ 'duplicate-task-headers': 'error',
'element-type': [ 'error', elementTypeConfig.camundaCloud12 ],
'error-reference': 'error',
'loop-characteristics': 'error',
@@ -59,6 +63,7 @@ module.exports = {
rules: {
'called-decision-or-task-definition': [ 'error', calledDecisionOrTaskDefinitionConfig.camundaCloud13 ],
'called-element': 'error',
+ 'duplicate-task-headers': 'error',
'element-type': [ 'error', elementTypeConfig.camundaCloud12 ],
'error-reference': 'error',
'loop-characteristics': 'error',
diff --git a/rules/duplicate-task-headers.js b/rules/duplicate-task-headers.js
new file mode 100644
index 00000000..7ec5b2b6
--- /dev/null
+++ b/rules/duplicate-task-headers.js
@@ -0,0 +1,57 @@
+const {
+ is,
+ isAny
+} = require('bpmnlint-utils');
+
+const {
+ findExtensionElement,
+ getMessageEventDefinition,
+ hasDuplicatedPropertyValues
+} = require('./utils/element');
+
+const { reportErrors } = require('./utils/reporter');
+
+module.exports = function() {
+ function check(node, reporter) {
+ if (!is(node, 'bpmn:UserTask') && !isZeebeServiceTask(node)) {
+ return;
+ }
+
+ const taskHeaders = findExtensionElement(node, 'zeebe:TaskHeaders');
+
+ if (!taskHeaders) {
+ return;
+ }
+
+ const errors = hasDuplicatedPropertyValues(taskHeaders, 'values', 'key', node);
+
+ if (errors && errors.length) {
+ reportErrors(node, reporter, errors);
+ }
+ }
+
+ return {
+ check
+ };
+};
+
+// helpers //////////
+
+function isZeebeServiceTask(element) {
+ if (is(element, 'zeebe:ZeebeServiceTask')) {
+ return true;
+ }
+
+ if (isAny(element, [
+ 'bpmn:EndEvent',
+ 'bpmn:IntermediateThrowEvent'
+ ])) {
+ return getMessageEventDefinition(element);
+ }
+
+ if (is(element, 'bpmn:BusinessRuleTask')) {
+ return findExtensionElement(element, 'zeebe:TaskDefinition');
+ }
+
+ return false;
+}
\ No newline at end of file
diff --git a/rules/user-task-form.js b/rules/user-task-form.js
index 31c25f1b..700fbc7c 100644
--- a/rules/user-task-form.js
+++ b/rules/user-task-form.js
@@ -56,6 +56,8 @@ module.exports = function() {
};
};
+// helpers //////////
+
function findUserTaskForm(node, formKey) {
const process = findParent(node, 'bpmn:Process');
diff --git a/rules/utils/element.js b/rules/utils/element.js
index 6e1c994b..faa002d1 100644
--- a/rules/utils/element.js
+++ b/rules/utils/element.js
@@ -5,7 +5,10 @@ const {
some
} = require('min-dash');
-const { isAny } = require('bpmnlint-utils');
+const {
+ is,
+ isAny
+} = require('bpmnlint-utils');
const { getPath } = require('@bpmn-io/moddle-utils');
@@ -13,12 +16,22 @@ const { ERROR_TYPES } = require('./error-types');
module.exports.ERROR_TYPES = ERROR_TYPES;
-module.exports.getEventDefinition = function(node) {
+function getEventDefinition(node) {
const eventDefinitions = node.get('eventDefinitions');
if (eventDefinitions) {
return eventDefinitions[ 0 ];
}
+}
+
+module.exports.getEventDefinition = getEventDefinition;
+
+module.exports.getMessageEventDefinition = function(node) {
+ if (is(node, 'bpmn:ReceiveTask')) {
+ return node;
+ }
+
+ return getEventDefinition(node);
};
function findExtensionElements(node, types) {
diff --git a/test/camunda-cloud/duplicate-task-headers.spec.js b/test/camunda-cloud/duplicate-task-headers.spec.js
new file mode 100644
index 00000000..b52e5628
--- /dev/null
+++ b/test/camunda-cloud/duplicate-task-headers.spec.js
@@ -0,0 +1,398 @@
+const RuleTester = require('bpmnlint/lib/testers/rule-tester');
+
+const rule = require('../../rules/duplicate-task-headers');
+
+const {
+ 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(`
+
+
+
+
+
+
+
+
+
+ `))
+ }
+];
+
+const invalid = [
+ {
+ name: 'service task',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+ `)),
+ report: {
+ id: 'ServiceTask_1',
+ message: 'Properties of type have property with duplicate value of ',
+ path: null,
+ error: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:TaskHeaders',
+ parentNode: 'ServiceTask_1',
+ duplicatedProperty: 'key',
+ duplicatedPropertyValue: 'foo',
+ properties: [
+ 'zeebe:Header',
+ 'zeebe:Header'
+ ],
+ propertiesName: 'values'
+ }
+ }
+ },
+ {
+ name: 'service task (multiple duplicates)',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+
+ `)),
+ report: [
+ {
+ id: 'ServiceTask_1',
+ message: 'Properties of type have property with duplicate value of ',
+ path: null,
+ error: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:TaskHeaders',
+ parentNode: 'ServiceTask_1',
+ duplicatedProperty: 'key',
+ duplicatedPropertyValue: 'foo',
+ properties: [
+ 'zeebe:Header',
+ 'zeebe:Header',
+ 'zeebe:Header'
+ ],
+ propertiesName: 'values'
+ }
+ }
+ ]
+ },
+ {
+ name: 'service task (multiple duplicates)',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+
+
+ `)),
+ report: [
+ {
+ id: 'ServiceTask_1',
+ message: 'Properties of type have property with duplicate value of ',
+ path: null,
+ error: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:TaskHeaders',
+ parentNode: 'ServiceTask_1',
+ duplicatedProperty: 'key',
+ duplicatedPropertyValue: 'foo',
+ properties: [
+ 'zeebe:Header',
+ 'zeebe:Header'
+ ],
+ propertiesName: 'values'
+ }
+ },
+ {
+ id: 'ServiceTask_1',
+ message: 'Properties of type have property with duplicate value of ',
+ path: null,
+ error: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:TaskHeaders',
+ parentNode: 'ServiceTask_1',
+ duplicatedProperty: 'key',
+ duplicatedPropertyValue: 'bar',
+ properties: [
+ 'zeebe:Header',
+ 'zeebe:Header'
+ ],
+ propertiesName: 'values'
+ }
+ }
+ ]
+ },
+ {
+ name: 'service task (no key)',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+ `)),
+ report: {
+ id: 'ServiceTask_1',
+ message: 'Properties of type have property with duplicate value of ',
+ path: null,
+ error: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:TaskHeaders',
+ parentNode: 'ServiceTask_1',
+ duplicatedProperty: 'key',
+ duplicatedPropertyValue: undefined,
+ properties: [
+ 'zeebe:Header',
+ 'zeebe:Header'
+ ],
+ propertiesName: 'values'
+ }
+ }
+ },
+ {
+ name: 'send task',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+ `)),
+ report: {
+ id: 'SendTask_1',
+ message: 'Properties of type have property with duplicate value of ',
+ path: null,
+ error: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:TaskHeaders',
+ parentNode: 'SendTask_1',
+ duplicatedProperty: 'key',
+ duplicatedPropertyValue: 'foo',
+ properties: [
+ 'zeebe:Header',
+ 'zeebe:Header'
+ ],
+ propertiesName: 'values'
+ }
+ }
+ },
+ {
+ name: 'user task',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+ `)),
+ report: {
+ id: 'UserTask_1',
+ message: 'Properties of type have property with duplicate value of ',
+ path: null,
+ error: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:TaskHeaders',
+ parentNode: 'UserTask_1',
+ duplicatedProperty: 'key',
+ duplicatedPropertyValue: 'foo',
+ properties: [
+ 'zeebe:Header',
+ 'zeebe:Header'
+ ],
+ propertiesName: 'values'
+ }
+ }
+ },
+ {
+ name: 'business rule task',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+
+ `)),
+ report: {
+ id: 'BusinessRuleTask_1',
+ message: 'Properties of type have property with duplicate value of ',
+ path: null,
+ error: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:TaskHeaders',
+ parentNode: 'BusinessRuleTask_1',
+ duplicatedProperty: 'key',
+ duplicatedPropertyValue: 'foo',
+ properties: [
+ 'zeebe:Header',
+ 'zeebe:Header'
+ ],
+ propertiesName: 'values'
+ }
+ }
+ },
+ {
+ name: 'script task',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+ `)),
+ report: {
+ id: 'ScriptTask_1',
+ message: 'Properties of type have property with duplicate value of ',
+ path: null,
+ error: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:TaskHeaders',
+ parentNode: 'ScriptTask_1',
+ duplicatedProperty: 'key',
+ duplicatedPropertyValue: 'foo',
+ properties: [
+ 'zeebe:Header',
+ 'zeebe:Header'
+ ],
+ propertiesName: 'values'
+ }
+ }
+ },
+ {
+ name: 'message throw event',
+ moddleElement: createModdle(createProcess(`
+
+
+
+
+
+
+
+
+
+ `)),
+ report: {
+ id: 'MessageEvent_1',
+ message: 'Properties of type have property with duplicate value of ',
+ path: null,
+ error: {
+ type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
+ node: 'zeebe:TaskHeaders',
+ parentNode: 'MessageEvent_1',
+ duplicatedProperty: 'key',
+ duplicatedPropertyValue: 'foo',
+ properties: [
+ 'zeebe:Header',
+ 'zeebe:Header'
+ ],
+ propertiesName: 'values'
+ }
+ }
+ }
+];
+
+RuleTester.verify('task-headers', rule, {
+ valid,
+ invalid
+});
\ No newline at end of file
diff --git a/test/configs.spec.js b/test/configs.spec.js
index a1c73bae..6ad90a4f 100644
--- a/test/configs.spec.js
+++ b/test/configs.spec.js
@@ -7,6 +7,7 @@ describe('configs', function() {
it('camunda-cloud-1-0', expectRules(configs, 'camunda-cloud-1-0', {
'called-decision-or-task-definition': 'error',
'called-element': 'error',
+ 'duplicate-task-headers': 'error',
'element-type': 'error',
'error-reference': 'error',
'loop-characteristics': 'error',
@@ -20,6 +21,7 @@ describe('configs', function() {
it('camunda-cloud-1-1', expectRules(configs, 'camunda-cloud-1-1', {
'called-decision-or-task-definition': 'error',
'called-element': 'error',
+ 'duplicate-task-headers': 'error',
'element-type': 'error',
'error-reference': 'error',
'loop-characteristics': 'error',
@@ -33,6 +35,7 @@ describe('configs', function() {
it('camunda-cloud-1-2', expectRules(configs, 'camunda-cloud-1-2', {
'called-decision-or-task-definition': 'error',
'called-element': 'error',
+ 'duplicate-task-headers': 'error',
'element-type': 'error',
'error-reference': 'error',
'loop-characteristics': 'error',
@@ -46,6 +49,7 @@ describe('configs', function() {
it('camunda-cloud-1-3', expectRules(configs, 'camunda-cloud-1-3', {
'called-decision-or-task-definition': 'error',
'called-element': 'error',
+ 'duplicate-task-headers': 'error',
'element-type': 'error',
'error-reference': 'error',
'loop-characteristics': 'error',
@@ -59,6 +63,7 @@ describe('configs', function() {
it('camunda-cloud-8-0', expectRules(configs, 'camunda-cloud-8-0', {
'called-decision-or-task-definition': 'error',
'called-element': 'error',
+ 'duplicate-task-headers': 'error',
'element-type': 'error',
'error-reference': 'error',
'loop-characteristics': 'error',