bpmnlint-plugin-camunda-compat 0.13.0 → 0.14.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 CHANGED
@@ -1,48 +1,56 @@
1
1
  const { omit } = require('min-dash');
2
2
 
3
- const calledDecisionOrTaskDefinitionConfig = require('./rules/called-decision-or-task-definition/config'),
4
- elementTypeConfig = require('./rules/element-type/config'),
5
- timerConfig = require('./rules/timer/config');
6
-
7
3
  const camundaCloud10Rules = {
8
- 'called-decision-or-task-definition': [ 'error', calledDecisionOrTaskDefinitionConfig.camundaCloud10 ],
4
+ 'called-decision-or-task-definition': [ 'error', { version: '1.0' } ],
9
5
  'called-element': 'error',
6
+ 'collapsed-subprocess': 'error',
10
7
  'duplicate-task-headers': 'error',
11
- 'element-type': [ 'error', elementTypeConfig.camundaCloud10 ],
8
+ 'element-type': [ 'error', { version: '1.0' } ],
12
9
  'error-reference': 'error',
13
10
  'loop-characteristics': 'error',
14
11
  'message-reference': 'error',
15
12
  'no-template': 'error',
16
13
  'no-zeebe-properties': 'error',
17
14
  'subscription': 'error',
18
- 'timer': [ 'error', timerConfig.camundaCloud10 ],
19
- 'user-task-form': 'error'
15
+ 'timer': [ 'error', { version: '1.0' } ],
16
+ 'user-task-form': 'error',
17
+ 'feel': 'error'
20
18
  };
21
19
 
22
20
  const camundaCloud11Rules = {
23
21
  ...camundaCloud10Rules,
24
- 'called-decision-or-task-definition': [ 'error', calledDecisionOrTaskDefinitionConfig.camundaCloud11 ],
25
- 'element-type': [ 'error', elementTypeConfig.camundaCloud11 ]
22
+ 'called-decision-or-task-definition': [ 'error', { version: '1.1' } ],
23
+ 'element-type': [ 'error', { version: '1.1' } ],
24
+ 'timer': [ 'error', { version: '1.1' } ]
26
25
  };
27
26
 
28
27
  const camundaCloud12Rules = {
29
28
  ...camundaCloud11Rules,
30
- 'called-decision-or-task-definition': [ 'error', calledDecisionOrTaskDefinitionConfig.camundaCloud12 ],
31
- 'element-type': [ 'error', elementTypeConfig.camundaCloud12 ]
29
+ 'called-decision-or-task-definition': [ 'error', { version: '1.2' } ],
30
+ 'element-type': [ 'error', { version: '1.2' } ],
31
+ 'timer': [ 'error', { version: '1.2' } ]
32
32
  };
33
33
 
34
34
  const camundaCloud13Rules = {
35
35
  ...camundaCloud12Rules,
36
- 'called-decision-or-task-definition': [ 'error', calledDecisionOrTaskDefinitionConfig.camundaCloud13 ]
36
+ 'called-decision-or-task-definition': [ 'error', { version: '1.3' } ],
37
+ 'element-type': [ 'error', { version: '1.3' } ],
38
+ 'timer': [ 'error', { version: '1.3' } ]
37
39
  };
38
40
 
39
- const camundaCloud80Rules = omit(camundaCloud13Rules, 'no-template');
41
+ const camundaCloud80Rules = {
42
+ ...omit(camundaCloud13Rules, 'no-template'),
43
+ 'called-decision-or-task-definition': [ 'error', { version: '8.0' } ],
44
+ 'element-type': [ 'error', { version: '8.0' } ],
45
+ 'timer': [ 'error', { version: '8.0' } ]
46
+ };
40
47
 
41
48
  const camundaCloud81Rules = {
42
49
  ...omit(camundaCloud80Rules, 'no-zeebe-properties'),
43
- 'element-type': [ 'error', elementTypeConfig.camundaCloud81 ],
50
+ 'called-decision-or-task-definition': [ 'error', { version: '8.1' } ],
51
+ 'element-type': [ 'error', { version: '8.1' } ],
44
52
  'inclusive-gateway': 'error',
45
- 'timer': [ 'error', timerConfig.camundaCloud81 ]
53
+ 'timer': [ 'error', { version: '8.1' } ]
46
54
  };
47
55
 
48
56
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bpmnlint-plugin-camunda-compat",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "A bpmnlint plug-in for Camunda Platform compatibility",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -36,9 +36,11 @@
36
36
  "zeebe-bpmn-moddle": "^0.14.0"
37
37
  },
38
38
  "dependencies": {
39
+ "@bpmn-io/feel-lint": "^0.1.0",
39
40
  "@bpmn-io/moddle-utils": "^0.1.0",
40
41
  "bpmnlint-utils": "^1.0.2",
41
- "min-dash": "^3.8.1"
42
+ "min-dash": "^3.8.1",
43
+ "semver-compare": "^1.0.0"
42
44
  },
43
45
  "files": [
44
46
  "rules",
@@ -1,33 +1,12 @@
1
- const camundaCloud10ElementsTaskDefinition = [
2
- 'bpmn:ServiceTask'
3
- ];
4
-
5
- const camundaCloud11ElementsTaskDefinition = [
6
- ...camundaCloud10ElementsTaskDefinition,
7
- 'bpmn:BusinessRuleTask',
8
- 'bpmn:ScriptTask',
9
- 'bpmn:SendTask'
10
- ];
11
-
12
- const camundaCloud12ElementsTaskDefinition = [
13
- ...camundaCloud11ElementsTaskDefinition,
14
- 'bpmn:IntermediateThrowEvent'
15
- ];
16
-
17
1
  module.exports = {
18
- camundaCloud10: {
19
- elementsTaskDefinition: camundaCloud10ElementsTaskDefinition
2
+ calledDecision: {
3
+ 'bpmn:BusinessRuleTask': '1.3'
20
4
  },
21
- camundaCloud11: {
22
- elementsTaskDefinition: camundaCloud11ElementsTaskDefinition
23
- },
24
- camundaCloud12: {
25
- elementsTaskDefinition: camundaCloud12ElementsTaskDefinition
26
- },
27
- camundaCloud13: {
28
- elementsCalledDecision: [
29
- 'bpmn:BusinessRuleTask'
30
- ],
31
- elementsTaskDefinition: camundaCloud12ElementsTaskDefinition
5
+ taskDefinition: {
6
+ 'bpmn:BusinessRuleTask': '1.1',
7
+ 'bpmn:IntermediateThrowEvent': '1.2',
8
+ 'bpmn:ScriptTask': '1.1',
9
+ 'bpmn:SendTask': '1.1',
10
+ 'bpmn:ServiceTask': '1.0'
32
11
  }
33
12
  };
@@ -1,10 +1,11 @@
1
- const {
2
- is,
3
- isAny
4
- } = require('bpmnlint-utils');
1
+ const { is } = require('bpmnlint-utils');
5
2
 
6
3
  const { getPath } = require('@bpmn-io/moddle-utils');
7
4
 
5
+ const config = require('./config');
6
+
7
+ const { greaterOrEqual } = require('../utils/version');
8
+
8
9
  const {
9
10
  findExtensionElement,
10
11
  getEventDefinition,
@@ -16,14 +17,11 @@ const { reportErrors } = require('../utils/reporter');
16
17
 
17
18
  const { ERROR_TYPES } = require('../utils/error-types');
18
19
 
19
- module.exports = function(config) {
20
- const {
21
- elementsCalledDecision = [],
22
- elementsTaskDefinition = []
23
- } = config;
24
-
20
+ module.exports = function({ version }) {
25
21
  function check(node, reporter) {
26
- if (!isAny(node, elementsCalledDecision) && !isAny(node, elementsTaskDefinition)) {
22
+ if (
23
+ (!config.calledDecision[ node.$type ] || !greaterOrEqual(version, config.calledDecision[ node.$type ]))
24
+ && (!config.taskDefinition[ node.$type ] || !greaterOrEqual(version, config.taskDefinition[ node.$type ]))) {
27
25
  return;
28
26
  }
29
27
 
@@ -38,15 +36,20 @@ module.exports = function(config) {
38
36
 
39
37
  if (calledDecision && !taskDefinition) {
40
38
 
41
- if (!isAny(node, elementsCalledDecision)) {
39
+ if (!isCalledDecisionAllowed(node, version)) {
40
+ const allowedVersion = config.calledDecision[ node.$type ] || null;
41
+
42
42
  reportErrors(node, reporter, {
43
- message: 'Extension element of type <zeebe:CalledDecision> not allowed',
43
+ message: allowedVersion
44
+ ? `Extension element of type <zeebe:CalledDecision> only allowed by Camunda Platform ${ allowedVersion } or newer`
45
+ : 'Extension element of type <zeebe:CalledDecision> not allowed',
44
46
  path: getPath(calledDecision, node),
45
- error: {
47
+ data: {
46
48
  type: ERROR_TYPES.EXTENSION_ELEMENT_NOT_ALLOWED,
47
49
  node,
48
50
  parentNode: null,
49
- extensionElement: calledDecision
51
+ extensionElement: calledDecision,
52
+ allowedVersion
50
53
  }
51
54
  });
52
55
 
@@ -71,15 +74,20 @@ module.exports = function(config) {
71
74
 
72
75
  if (!calledDecision && taskDefinition) {
73
76
 
74
- if (!isAny(node, elementsTaskDefinition)) {
77
+ if (!isTaskDefinitionAllowed(node, version)) {
78
+ const allowedVersion = config.taskDefinition[ node.$type ] || null;
79
+
75
80
  reportErrors(node, reporter, {
76
- message: 'Extension element of type <zeebe:TaskDefinition> not allowed',
81
+ message: allowedVersion
82
+ ? `Extension element of type <zeebe:TaskDefinition> only allowed by Camunda Platform ${ allowedVersion } or newer`
83
+ : 'Extension element of type <zeebe:TaskDefinition> not allowed',
77
84
  path: getPath(taskDefinition, node),
78
- error: {
85
+ data: {
79
86
  type: ERROR_TYPES.EXTENSION_ELEMENT_NOT_ALLOWED,
80
87
  node,
81
88
  parentNode: null,
82
- extensionElement: taskDefinition
89
+ extensionElement: taskDefinition,
90
+ allowedVersion
83
91
  }
84
92
  });
85
93
 
@@ -97,12 +105,12 @@ module.exports = function(config) {
97
105
  }
98
106
  }
99
107
 
100
- if (isAny(node, elementsCalledDecision) && isAny(node, elementsTaskDefinition)) {
108
+ if (isCalledDecisionAllowed(node, version) && isTaskDefinitionAllowed(node, version)) {
101
109
  errors = hasExtensionElement(node, [
102
110
  'zeebe:CalledDecision',
103
111
  'zeebe:TaskDefinition'
104
112
  ], node);
105
- } else if (isAny(node, elementsCalledDecision)) {
113
+ } else if (isCalledDecisionAllowed(node, version)) {
106
114
  errors = hasExtensionElement(node, 'zeebe:CalledDecision', node);
107
115
  } else {
108
116
  errors = hasExtensionElement(node, 'zeebe:TaskDefinition', node);
@@ -118,4 +126,16 @@ module.exports = function(config) {
118
126
  return {
119
127
  check
120
128
  };
121
- };
129
+ };
130
+
131
+ function isCalledDecisionAllowed(node, version) {
132
+ const { calledDecision } = config;
133
+
134
+ return calledDecision[ node.$type ] && greaterOrEqual(version, calledDecision[ node.$type ]);
135
+ }
136
+
137
+ function isTaskDefinitionAllowed(node, version) {
138
+ const { taskDefinition } = config;
139
+
140
+ return taskDefinition[ node.$type ] && greaterOrEqual(version, taskDefinition[ node.$type ]);
141
+ }
@@ -0,0 +1,38 @@
1
+ const { is } = require('bpmnlint-utils');
2
+
3
+ const { ERROR_TYPES } = require('./utils/element');
4
+
5
+ const { reportErrors } = require('./utils/reporter');
6
+
7
+ module.exports = function() {
8
+ function check(di, reporter) {
9
+
10
+ if (!isCollapsedSubProcess(di)) {
11
+ return;
12
+ }
13
+
14
+ const node = di.bpmnElement;
15
+
16
+ const error = {
17
+ message: `A <${ node.$type }> must be expanded.`,
18
+ data: {
19
+ type: ERROR_TYPES.ELEMENT_COLLAPSED_NOT_ALLOWED,
20
+ node: node,
21
+ parentNode: null
22
+ }
23
+ };
24
+
25
+ reportErrors(node, reporter, error);
26
+ }
27
+
28
+ return {
29
+ check
30
+ };
31
+ };
32
+
33
+
34
+ function isCollapsedSubProcess(di) {
35
+ return is(di, 'bpmndi:BPMNShape') &&
36
+ is(di.bpmnElement, 'bpmn:SubProcess') &&
37
+ di.get('isExpanded') === false;
38
+ }
@@ -1,125 +1,49 @@
1
- const camundaCloud10Elements = [
2
- 'bpmn:Association',
3
- 'bpmn:BoundaryEvent',
4
- 'bpmn:CallActivity',
5
- 'bpmn:Collaboration',
6
- 'bpmn:Definitions',
7
- 'bpmn:EndEvent',
8
- 'bpmn:EventBasedGateway',
9
- 'bpmn:ExclusiveGateway',
10
- 'bpmn:Group',
11
- 'bpmn:IntermediateCatchEvent',
12
- 'bpmn:MessageFlow',
13
- 'bpmn:ParallelGateway',
14
- 'bpmn:Participant',
15
- 'bpmn:Process',
16
- 'bpmn:ReceiveTask',
17
- 'bpmn:SequenceFlow',
18
- 'bpmn:ServiceTask',
19
- 'bpmn:StartEvent',
20
- 'bpmn:SubProcess',
21
- 'bpmn:TextAnnotation',
22
- 'bpmn:UserTask'
23
- ];
24
-
25
- const camundaCloud11Elements = [
26
- ...camundaCloud10Elements,
27
- 'bpmn:BusinessRuleTask',
28
- 'bpmn:IntermediateThrowEvent',
29
- 'bpmn:ManualTask',
30
- 'bpmn:ScriptTask',
31
- 'bpmn:SendTask'
32
- ];
33
-
34
- const camundaCloud12Elements = [
35
- ...camundaCloud11Elements
36
- ];
37
-
38
- const camundaCloud81Elements = [
39
- ...camundaCloud12Elements,
40
- 'bpmn:InclusiveGateway'
41
- ];
42
-
43
- const camundaCloud10ElementsNoEventDefinitionRequired = [
44
- 'bpmn:EndEvent',
45
- 'bpmn:StartEvent'
46
- ];
47
-
48
- const camundaCloud11ElementsNoEventDefinitionRequired = [
49
- ...camundaCloud10ElementsNoEventDefinitionRequired,
50
- 'bpmn:IntermediateThrowEvent'
51
- ];
52
-
53
- const camundaCloud12ElementsNoEventDefinitionRequired = [
54
- ...camundaCloud11ElementsNoEventDefinitionRequired
55
- ];
56
-
57
- const camundaCloud81ElementsNoEventDefinitionRequired = [
58
- ...camundaCloud12ElementsNoEventDefinitionRequired
59
- ];
60
-
61
- const camundaCloud10EventDefinitions = {
62
- 'bpmn:BoundaryEvent': [
63
- 'bpmn:ErrorEventDefinition',
64
- 'bpmn:MessageEventDefinition',
65
- 'bpmn:TimerEventDefinition'
66
- ],
67
- 'bpmn:EndEvent': [
68
- 'bpmn:ErrorEventDefinition'
69
- ],
70
- 'bpmn:IntermediateCatchEvent': [
71
- 'bpmn:MessageEventDefinition',
72
- 'bpmn:TimerEventDefinition'
73
- ],
74
- 'bpmn:StartEvent': [
75
- 'bpmn:ErrorEventDefinition',
76
- 'bpmn:MessageEventDefinition',
77
- 'bpmn:TimerEventDefinition'
78
- ]
79
- };
80
-
81
- const camundaCloud11EventDefinitions = {
82
- ...camundaCloud10EventDefinitions
83
- };
84
-
85
- const camundaCloud12EventDefinitions = {
86
- ...camundaCloud11EventDefinitions,
87
- 'bpmn:EndEvent': [
88
- ...camundaCloud11EventDefinitions['bpmn:EndEvent'],
89
- 'bpmn:MessageEventDefinition'
90
- ],
91
- 'bpmn:IntermediateThrowEvent': [
92
- 'bpmn:MessageEventDefinition'
93
- ]
94
- };
95
-
96
- const camundaCloud81EventDefinitions = {
97
- ...camundaCloud12EventDefinitions,
98
- 'bpmn:EndEvent': [
99
- ...camundaCloud12EventDefinitions['bpmn:EndEvent'],
100
- 'bpmn:TerminateEventDefinition'
101
- ]
102
- };
103
-
104
1
  module.exports = {
105
- camundaCloud10: {
106
- elements: camundaCloud10Elements,
107
- elementsNoEventDefinitionRequired: camundaCloud10ElementsNoEventDefinitionRequired,
108
- eventDefinitions: camundaCloud10EventDefinitions
2
+ 'bpmn:Association': '1.0',
3
+ 'bpmn:BoundaryEvent': {
4
+ 'bpmn:ErrorEventDefinition': '1.0',
5
+ 'bpmn:MessageEventDefinition': '1.0',
6
+ 'bpmn:TimerEventDefinition': '1.0'
109
7
  },
110
- camundaCloud11: {
111
- elements: camundaCloud11Elements,
112
- elementsNoEventDefinitionRequired: camundaCloud11ElementsNoEventDefinitionRequired,
113
- eventDefinitions: camundaCloud11EventDefinitions
8
+ 'bpmn:BusinessRuleTask': '1.1',
9
+ 'bpmn:CallActivity': '1.0',
10
+ 'bpmn:Collaboration': '1.0',
11
+ 'bpmn:Definitions': '1.0',
12
+ 'bpmn:EndEvent': {
13
+ '_': '1.0',
14
+ 'bpmn:ErrorEventDefinition': '1.0',
15
+ 'bpmn:MessageEventDefinition': '1.2',
16
+ 'bpmn:TerminateEventDefinition': '8.1'
114
17
  },
115
- camundaCloud12: {
116
- elements: camundaCloud12Elements,
117
- elementsNoEventDefinitionRequired: camundaCloud12ElementsNoEventDefinitionRequired,
118
- eventDefinitions: camundaCloud12EventDefinitions
18
+ 'bpmn:EventBasedGateway': '1.0',
19
+ 'bpmn:ExclusiveGateway': '1.0',
20
+ 'bpmn:Group': '1.0',
21
+ 'bpmn:InclusiveGateway': '8.1',
22
+ 'bpmn:IntermediateCatchEvent': {
23
+ 'bpmn:MessageEventDefinition': '1.0',
24
+ 'bpmn:TimerEventDefinition': '1.0'
119
25
  },
120
- camundaCloud81: {
121
- elements: camundaCloud81Elements,
122
- elementsNoEventDefinitionRequired: camundaCloud81ElementsNoEventDefinitionRequired,
123
- eventDefinitions: camundaCloud81EventDefinitions
124
- }
26
+ 'bpmn:IntermediateThrowEvent': {
27
+ '_': '1.1',
28
+ 'bpmn:MessageEventDefinition': '1.2'
29
+ },
30
+ 'bpmn:ManualTask': '1.1',
31
+ 'bpmn:MessageFlow': '1.0',
32
+ 'bpmn:ParallelGateway': '1.0',
33
+ 'bpmn:Participant': '1.0',
34
+ 'bpmn:Process': '1.0',
35
+ 'bpmn:ReceiveTask': '1.0',
36
+ 'bpmn:ScriptTask': '1.1',
37
+ 'bpmn:SendTask': '1.1',
38
+ 'bpmn:SequenceFlow': '1.0',
39
+ 'bpmn:ServiceTask': '1.0',
40
+ 'bpmn:StartEvent': {
41
+ '_': '1.0',
42
+ 'bpmn:ErrorEventDefinition': '1.0',
43
+ 'bpmn:MessageEventDefinition': '1.0',
44
+ 'bpmn:TimerEventDefinition': '1.0'
45
+ },
46
+ 'bpmn:SubProcess': '1.0',
47
+ 'bpmn:TextAnnotation': '1.0',
48
+ 'bpmn:UserTask': '1.0'
125
49
  };
@@ -1,53 +1,40 @@
1
+ const { isString } = require('min-dash');
2
+
1
3
  const { isAny } = require('bpmnlint-utils');
2
4
 
3
- const {
4
- isAnyExactly,
5
- getEventDefinition
6
- } = require('../utils/element');
5
+ const config = require('./config');
6
+
7
+ const { greaterOrEqual } = require('../utils/version');
8
+
9
+ const { getEventDefinition } = require('../utils/element');
7
10
 
8
11
  const { ERROR_TYPES } = require('../utils/error-types');
9
12
 
10
13
  const { reportErrors } = require('../utils/reporter');
11
14
 
12
- module.exports = function(config) {
13
- const {
14
- elements,
15
- elementsNoEventDefinitionRequired,
16
- eventDefinitions
17
- } = config;
18
-
15
+ module.exports = function({ version }) {
19
16
  function check(node, reporter) {
20
17
  if (!isAny(node, [ 'bpmn:FlowElement', 'bpmn:FlowElementsContainer' ])) {
21
18
  return;
22
19
  }
23
20
 
24
- if (!isAnyExactly(node, elements)) {
25
- const error = {
26
- message: `Element of type <${ node.$type }> not allowed`,
27
- path: null,
28
- error: {
29
- type: ERROR_TYPES.ELEMENT_TYPE_NOT_ALLOWED,
30
- node,
31
- parentNode: null
32
- }
33
- };
21
+ const element = config[ node.$type ];
34
22
 
35
- reportErrors(node, reporter, error);
23
+ if (!element || (isString(element) && !greaterOrEqual(version, element))) {
36
24
 
37
- return;
38
- }
39
-
40
- const eventDefinition = getEventDefinition(node);
25
+ // (1) Element not allowed
26
+ const allowedVersion = element || null;
41
27
 
42
- if (isAny(node, [ 'bpmn:CatchEvent', 'ThrowEvent' ]) && !eventDefinition && !elementsNoEventDefinitionRequired.includes(node.$type)) {
43
28
  const error = {
44
- message: `Element of type <${ node.$type }> must have property <eventDefinitions>`,
45
- path: [ 'eventDefinitions' ],
46
- error: {
47
- type: ERROR_TYPES.PROPERTY_REQUIRED,
29
+ message: allowedVersion
30
+ ? `Element of type <${ node.$type }> only allowed by Camunda Platform ${ allowedVersion } or newer`
31
+ : `Element of type <${ node.$type }> not allowed`,
32
+ path: null,
33
+ data: {
34
+ type: ERROR_TYPES.ELEMENT_TYPE_NOT_ALLOWED,
48
35
  node,
49
36
  parentNode: null,
50
- requiredProperty: 'eventDefinitions'
37
+ allowedVersion
51
38
  }
52
39
  };
53
40
 
@@ -56,27 +43,56 @@ module.exports = function(config) {
56
43
  return;
57
44
  }
58
45
 
59
- if (!eventDefinition) {
46
+ if (!isAny(node, [ 'bpmn:CatchEvent', 'bpmn:ThrowEvent' ])) {
60
47
  return;
61
48
  }
62
49
 
63
- if (!eventDefinitions[ node.$type ] || !isAny(eventDefinition, eventDefinitions[ node.$type ])) {
64
- const error = {
65
- message: `Property of type <${ eventDefinition.$type }> not allowed`,
66
- path: [
67
- 'eventDefinitions',
68
- 0
69
- ],
70
- error: {
71
- type: ERROR_TYPES.PROPERTY_TYPE_NOT_ALLOWED,
72
- node: node,
73
- parentNode: null,
74
- property: 'eventDefinitions',
75
- requiredPropertyType: eventDefinitions[ node.$type ]
76
- }
77
- };
50
+ const eventDefinition = getEventDefinition(node);
78
51
 
79
- reportErrors(node, reporter, error);
52
+ if (eventDefinition) {
53
+ if (!element[ eventDefinition.$type ] || !greaterOrEqual(version, element[ eventDefinition.$type ])) {
54
+
55
+ // (2) Element with event definition not allowed
56
+ const allowedVersion = element[ eventDefinition.$type ] || null;
57
+
58
+ const error = {
59
+ message: allowedVersion
60
+ ? `Element of type <${ node.$type }> with event definition of type <${ eventDefinition.$type }> only allowed by Camunda Platform ${ allowedVersion } or newer`
61
+ : `Element of type <${ node.$type }> with event definition of type <${ eventDefinition.$type }> not allowed`,
62
+ path: null,
63
+ data: {
64
+ type: ERROR_TYPES.ELEMENT_TYPE_NOT_ALLOWED,
65
+ node,
66
+ parentNode: null,
67
+ eventDefinition,
68
+ allowedVersion
69
+ }
70
+ };
71
+
72
+ reportErrors(node, reporter, error);
73
+ }
74
+ } else {
75
+ if (!element[ '_' ] || !greaterOrEqual(version, element[ '_' ])) {
76
+
77
+ // (3) Element without event definition not allowed
78
+ const allowedVersion = element[ '_' ] || null;
79
+
80
+ const error = {
81
+ message: allowedVersion
82
+ ? `Element of type <${ node.$type }> with no event definition only allowed by Camunda Platform ${ allowedVersion } or newer`
83
+ : `Element of type <${ node.$type }> with no event definition not allowed`,
84
+ path: null,
85
+ data: {
86
+ type: ERROR_TYPES.ELEMENT_TYPE_NOT_ALLOWED,
87
+ node,
88
+ parentNode: null,
89
+ eventDefinition,
90
+ allowedVersion
91
+ }
92
+ };
93
+
94
+ reportErrors(node, reporter, error);
95
+ }
80
96
  }
81
97
  }
82
98
 
package/rules/feel.js ADDED
@@ -0,0 +1,81 @@
1
+ const { isString } = require('min-dash');
2
+
3
+ const { is } = require('bpmnlint-utils');
4
+
5
+ const { lintExpression } = require('@bpmn-io/feel-lint');
6
+
7
+ const { getPath } = require('@bpmn-io/moddle-utils');
8
+
9
+ const { reportErrors } = require('./utils/reporter');
10
+
11
+ const { ERROR_TYPES } = require('./utils/error-types');
12
+
13
+ module.exports = function() {
14
+ function check(node, reporter) {
15
+ if (is(node, 'bpmn:Expression')) {
16
+ return;
17
+ }
18
+
19
+ const parentNode = findFlowElement(node);
20
+
21
+ if (!parentNode) {
22
+ return;
23
+ }
24
+
25
+ const errors = [];
26
+
27
+ Object.entries(node).forEach(([ propertyName, propertyValue ]) => {
28
+ if (propertyValue && is(propertyValue, 'bpmn:Expression')) {
29
+ propertyValue = propertyValue.get('body');
30
+ }
31
+
32
+ if (isFeelProperty([ propertyName, propertyValue ])) {
33
+ const lintErrors = lintExpression(propertyValue.substring(1));
34
+
35
+ // syntax error
36
+ if (lintErrors.find(({ type }) => type === 'syntaxError')) {
37
+ const path = getPath(node, parentNode);
38
+
39
+ errors.push(
40
+ {
41
+ message: `Property <${ propertyName }> is not valid FEEL expression`,
42
+ path: path
43
+ ? [ ...path, propertyName ]
44
+ : [ propertyName ],
45
+ data: {
46
+ type: ERROR_TYPES.FEEL_EXPRESSION_INVALID,
47
+ node,
48
+ parentNode,
49
+ property: propertyName
50
+ }
51
+ }
52
+ );
53
+ }
54
+ }
55
+ });
56
+
57
+ if (errors && errors.length) {
58
+ reportErrors(parentNode, reporter, errors);
59
+ }
60
+ }
61
+
62
+ return {
63
+ check
64
+ };
65
+ };
66
+
67
+ const isFeelProperty = ([ propertyName, value ]) => {
68
+ return !isIgnoredProperty(propertyName) && isString(value) && value.startsWith('=');
69
+ };
70
+
71
+ const isIgnoredProperty = propertyName => {
72
+ return propertyName.startsWith('$');
73
+ };
74
+
75
+ const findFlowElement = node => {
76
+ while (node && !is(node, 'bpmn:FlowElement')) {
77
+ node = node.$parent;
78
+ }
79
+
80
+ return node;
81
+ };
@@ -16,7 +16,7 @@ module.exports = function() {
16
16
  const error = {
17
17
  message: `Element of type <${ node.$type }> must have one property <incoming> of type <bpmn:SequenceFlow>`,
18
18
  path: [ 'incoming' ],
19
- error: {
19
+ data: {
20
20
  type: ERROR_TYPES.PROPERTY_NOT_ALLOWED,
21
21
  node,
22
22
  parentNode: null,
@@ -6,7 +6,8 @@ module.exports = function() {
6
6
  function check(node, reporter) {
7
7
  const errors = hasProperties(node, {
8
8
  modelerTemplate: {
9
- allowed: false
9
+ allowed: false,
10
+ allowedVersion: '8.0'
10
11
  }
11
12
  }, node);
12
13
 
@@ -10,7 +10,7 @@ module.exports = function() {
10
10
  return;
11
11
  }
12
12
 
13
- const errors = hasNoExtensionElement(node, 'zeebe:Properties', node);
13
+ const errors = hasNoExtensionElement(node, 'zeebe:Properties', node, '8.1');
14
14
 
15
15
  if (errors && errors.length) {
16
16
  reportErrors(node, reporter, errors);
@@ -1,17 +1,4 @@
1
- const camundaCloud10Formats = [
2
- 'iso8601'
3
- ];
4
-
5
- const camundaCloud81Formats = [
6
- ...camundaCloud10Formats,
7
- 'cron'
8
- ];
9
-
10
1
  module.exports = {
11
- camundaCloud10: {
12
- formats: camundaCloud10Formats
13
- },
14
- camundaCloud81: {
15
- formats: camundaCloud81Formats
16
- }
17
- };
2
+ 'cron': '8.1',
3
+ 'iso8601': '1.0'
4
+ };
@@ -2,8 +2,13 @@ const {
2
2
  is
3
3
  } = require('bpmnlint-utils');
4
4
 
5
+ const config = require('./config');
6
+
7
+ const { greaterOrEqual } = require('../utils/version');
8
+
5
9
  const {
6
10
  getEventDefinition,
11
+ hasExpression,
7
12
  hasProperties,
8
13
  hasProperty
9
14
  } = require('../utils/element');
@@ -18,9 +23,7 @@ const {
18
23
 
19
24
  const { reportErrors } = require('../utils/reporter');
20
25
 
21
- module.exports = function(config = {}) {
22
- const { formats = [] } = config;
23
-
26
+ module.exports = function({ version }) {
24
27
  function check(node, reporter) {
25
28
  if (!is(node, 'bpmn:Event')) {
26
29
  return;
@@ -40,17 +43,7 @@ module.exports = function(config = {}) {
40
43
  return;
41
44
  }
42
45
 
43
- const timeProperty = getTimeProperty(eventDefinition);
44
-
45
- errors = checkTimePropertyNotEmpty(timeProperty, node);
46
-
47
- if (errors && errors.length) {
48
- reportErrors(node, reporter, errors);
49
-
50
- return;
51
- }
52
-
53
- errors = checkTimePropertyCorrectFormat(eventDefinition, node, formats);
46
+ errors = checkTimeProperty(eventDefinition, node, version);
54
47
 
55
48
  if (errors && errors.length) {
56
49
  reportErrors(node, reporter, errors);
@@ -78,36 +71,22 @@ function checkTimePropertyExists(eventDefinition, event) {
78
71
  return [];
79
72
  }
80
73
 
81
- function checkTimePropertyNotEmpty(timeProperty, event) {
82
- return hasProperties(timeProperty, {
83
- body: {
84
- required: true,
85
- }
86
- }, event);
87
- }
88
-
89
- function checkTimePropertyCorrectFormat(eventDefinition, event, formats) {
74
+ function checkTimeProperty(eventDefinition, event, version) {
90
75
  const timeCycle = eventDefinition.get('timeCycle'),
91
76
  timeDate = eventDefinition.get('timeDate'),
92
77
  timeDuration = eventDefinition.get('timeDuration');
93
78
 
94
79
  if (timeCycle) {
95
- return hasProperties(timeCycle, {
96
- body: {
97
- allowed: cycle => validateCycle(cycle, formats)
98
- }
80
+ return hasExpression(eventDefinition, 'timeCycle', {
81
+ allowed: cycle => validateCycle(cycle, version)
99
82
  }, event);
100
83
  } else if (timeDate) {
101
- return hasProperties(timeDate, {
102
- body: {
103
- allowed: date => validateDate(date, formats)
104
- }
84
+ return hasExpression(eventDefinition, 'timeDate', {
85
+ allowed: date => validateDate(date, version)
105
86
  }, event);
106
87
  } else if (timeDuration) {
107
- return hasProperties(timeDuration, {
108
- body: {
109
- allowed: duration => validateDuration(duration, formats)
110
- }
88
+ return hasExpression(eventDefinition, 'timeDuration', {
89
+ allowed: duration => validateDuration(duration, version)
111
90
  }, event);
112
91
  }
113
92
  }
@@ -115,45 +94,41 @@ function checkTimePropertyCorrectFormat(eventDefinition, event, formats) {
115
94
 
116
95
 
117
96
  // helper //////////////
118
- function getTimeProperty(eventDefinition) {
119
- return eventDefinition.get('timeCycle') || eventDefinition.get('timeDate') || eventDefinition.get('timeDuration');
120
- }
121
-
122
97
  function isInterruptingBoundaryEvent(event) {
123
98
  return is(event, 'bpmn:BoundaryEvent') && event.get('cancelActivity') !== false;
124
99
  }
125
100
 
126
- function validateDate(date, formats) {
101
+ function validateDate(date, version) {
127
102
  if (validateExpression(date)) {
128
103
  return true;
129
104
  }
130
105
 
131
- if (formats.includes('iso8601') && validateISO8601Date(date)) {
132
- return true;
106
+ if (validateISO8601Date(date)) {
107
+ return greaterOrEqual(version, config.iso8601);
133
108
  }
134
109
  }
135
110
 
136
- function validateCycle(cycle, formats) {
111
+ function validateCycle(cycle, version) {
137
112
  if (validateExpression(cycle)) {
138
113
  return true;
139
114
  }
140
115
 
141
- if (formats.includes('iso8601') && validateISO8601Cycle(cycle)) {
142
- return true;
116
+ if (validateISO8601Cycle(cycle)) {
117
+ return greaterOrEqual(version, config.iso8601);
143
118
  }
144
119
 
145
- if (formats.includes('cron') && validateCronExpression(cycle)) {
146
- return true;
120
+ if (validateCronExpression(cycle)) {
121
+ return greaterOrEqual(version, config.cron) || { allowedVersion: config.cron };
147
122
  }
148
123
  }
149
124
 
150
- function validateDuration(duration, formats) {
125
+ function validateDuration(duration, version) {
151
126
  if (validateExpression(duration)) {
152
127
  return true;
153
128
  }
154
129
 
155
- if (formats.includes('iso8601') && validateISO8601Duration(duration)) {
156
- return true;
130
+ if (validateISO8601Duration(duration)) {
131
+ return greaterOrEqual(version, config.iso8601);
157
132
  }
158
133
  }
159
134
 
@@ -3,6 +3,7 @@ const {
3
3
  isDefined,
4
4
  isFunction,
5
5
  isNil,
6
+ isObject,
6
7
  some
7
8
  } = require('min-dash');
8
9
 
@@ -114,7 +115,7 @@ module.exports.hasDuplicatedPropertyValues = function(node, propertiesName, prop
114
115
  return {
115
116
  message: `Properties of type <${ duplicateProperties[ 0 ].$type }> have property <${ propertyName }> with duplicate value of <${ duplicate }>`,
116
117
  path: null,
117
- error: {
118
+ data: {
118
119
  type: ERROR_TYPES.PROPERTY_VALUE_DUPLICATED,
119
120
  node,
120
121
  parentNode: parentNode == node ? null : parentNode,
@@ -134,6 +135,8 @@ module.exports.hasProperties = function(node, properties, parentNode = null) {
134
135
  return Object.entries(properties).reduce((results, property) => {
135
136
  const [ propertyName, propertyChecks ] = property;
136
137
 
138
+ const { allowedVersion = null } = propertyChecks;
139
+
137
140
  const path = getPath(node, parentNode);
138
141
 
139
142
  const propertyValue = node.get(propertyName);
@@ -146,7 +149,7 @@ module.exports.hasProperties = function(node, properties, parentNode = null) {
146
149
  path: path
147
150
  ? [ ...path, propertyName ]
148
151
  : [ propertyName ],
149
- error: {
152
+ data: {
150
153
  type: ERROR_TYPES.PROPERTY_REQUIRED,
151
154
  node,
152
155
  parentNode: parentNode == node ? null : parentNode,
@@ -167,7 +170,7 @@ module.exports.hasProperties = function(node, properties, parentNode = null) {
167
170
  path: path
168
171
  ? [ ...path, propertyName ]
169
172
  : [ propertyName ],
170
- error: {
173
+ data: {
171
174
  type: ERROR_TYPES.PROPERTY_DEPENDEND_REQUIRED,
172
175
  node,
173
176
  parentNode: parentNode == node ? null : parentNode,
@@ -191,16 +194,19 @@ module.exports.hasProperties = function(node, properties, parentNode = null) {
191
194
  return [
192
195
  ...results,
193
196
  {
194
- message: `Element of type <${ node.$type }> must not have property <${ propertyName }> of type <${ propertyValue.$type }>`,
197
+ message: allowedVersion
198
+ ? `Property <${ propertyName }> of type <${ propertyValue.$type }> only allowed by Camunda Platform ${ allowedVersion } or newer`
199
+ : `Property <${ propertyName }> of type <${ propertyValue.$type }> not allowed`,
195
200
  path: path
196
201
  ? [ ...path, propertyName ]
197
202
  : [ propertyName ],
198
- error: {
203
+ data: {
199
204
  type: ERROR_TYPES.PROPERTY_TYPE_NOT_ALLOWED,
200
205
  node,
201
206
  parentNode: parentNode == node ? null : parentNode,
202
207
  property: propertyName,
203
- allowedPropertyType: propertyChecks.type
208
+ allowedPropertyType: propertyChecks.type,
209
+ allowedVersion
204
210
  }
205
211
  }
206
212
  ];
@@ -210,15 +216,18 @@ module.exports.hasProperties = function(node, properties, parentNode = null) {
210
216
  return [
211
217
  ...results,
212
218
  {
213
- message: `Element of type <${ node.$type }> must not have property <${ propertyName }>`,
219
+ message: allowedVersion
220
+ ? `Property <${ propertyName }> only allowed by Camunda Platform ${ allowedVersion } or newer`
221
+ : `Property <${ propertyName }> not allowed`,
214
222
  path: path
215
223
  ? [ ...path, propertyName ]
216
224
  : [ propertyName ],
217
- error: {
225
+ data: {
218
226
  type: ERROR_TYPES.PROPERTY_NOT_ALLOWED,
219
227
  node,
220
228
  parentNode: parentNode == node ? null : parentNode,
221
- property: propertyName
229
+ property: propertyName,
230
+ allowedVersion
222
231
  }
223
232
  }
224
233
  ];
@@ -228,15 +237,18 @@ module.exports.hasProperties = function(node, properties, parentNode = null) {
228
237
  return [
229
238
  ...results,
230
239
  {
231
- message: `Property <${ propertyName }> of element of type <${ node.$type }> must not have value of <${ truncate(propertyValue) }>`,
240
+ message: allowedVersion
241
+ ? `Property value of <${ truncate(propertyValue) }> only allowed by Camunda Platform ${ allowedVersion } or newer`
242
+ : `Property value of <${ truncate(propertyValue) }> not allowed`,
232
243
  path: path
233
244
  ? [ ...path, propertyName ]
234
245
  : [ propertyName ],
235
- error: {
246
+ data: {
236
247
  type: ERROR_TYPES.PROPERTY_VALUE_NOT_ALLOWED,
237
248
  node,
238
249
  parentNode: parentNode == node ? null : parentNode,
239
250
  property: propertyName,
251
+ allowedVersion
240
252
  }
241
253
  }
242
254
  ];
@@ -256,7 +268,7 @@ module.exports.hasProperty = function(node, types, parentNode = null) {
256
268
  {
257
269
  message: `Element of type <${ node.$type }> must have one property of type ${ formatTypes(typesArray, true) }`,
258
270
  path: getPath(node, parentNode),
259
- error: {
271
+ data: {
260
272
  type: ERROR_TYPES.PROPERTY_REQUIRED,
261
273
  node,
262
274
  parentNode: parentNode == node ? null : parentNode,
@@ -290,7 +302,7 @@ module.exports.hasExtensionElement = function(node, types, parentNode = null) {
290
302
  {
291
303
  message: `Element of type <${ node.$type }> must have one extension element of type ${ formatTypes(typesArray, true) }`,
292
304
  path: getPath(node, parentNode),
293
- error: {
305
+ data: {
294
306
  type: ERROR_TYPES.EXTENSION_ELEMENT_REQUIRED,
295
307
  node,
296
308
  parentNode: parentNode == node ? null : parentNode,
@@ -303,19 +315,85 @@ module.exports.hasExtensionElement = function(node, types, parentNode = null) {
303
315
  return [];
304
316
  };
305
317
 
306
- module.exports.hasNoExtensionElement = function(node, type, parentNode = null) {
318
+ module.exports.hasNoExtensionElement = function(node, type, parentNode = null, allowedVersion = null) {
307
319
  const extensionElement = findExtensionElement(node, type);
308
320
 
309
321
  if (extensionElement) {
310
322
  return [
311
323
  {
312
- message: `Element of type <${ node.$type }> must not have extension element of type <${ type }>`,
324
+ message: allowedVersion
325
+ ? `Extension element of type <${ type }> only allowed by Camunda Platform ${ allowedVersion }`
326
+ : `Extension element of type <${ type }> not allowed`,
313
327
  path: getPath(extensionElement, parentNode),
314
- error: {
328
+ data: {
315
329
  type: ERROR_TYPES.EXTENSION_ELEMENT_NOT_ALLOWED,
316
330
  node,
317
331
  parentNode: parentNode == node ? null : parentNode,
318
- extensionElement
332
+ extensionElement,
333
+ allowedVersion
334
+ }
335
+ }
336
+ ];
337
+ }
338
+
339
+ return [];
340
+ };
341
+
342
+ module.exports.hasExpression = function(node, propertyName, check, parentNode = null) {
343
+ const expression = node.get(propertyName);
344
+
345
+ if (!expression) {
346
+ throw new Error('Expression not found');
347
+ }
348
+
349
+ const body = expression.get('body');
350
+
351
+ const path = getPath(node, parentNode);
352
+
353
+ if (!body) {
354
+ if (check.required !== false) {
355
+ return [
356
+ {
357
+ message: `Property <${ propertyName }> must have expression value`,
358
+ path: path
359
+ ? [ ...path, propertyName ]
360
+ : null,
361
+ data: {
362
+ type: ERROR_TYPES.EXPRESSION_REQUIRED,
363
+ node: expression,
364
+ parentNode,
365
+ property: propertyName
366
+ }
367
+ }
368
+ ];
369
+ }
370
+
371
+ return [];
372
+ }
373
+
374
+ const allowed = check.allowed(body);
375
+
376
+ if (allowed !== true) {
377
+ let allowedVersion = null;
378
+
379
+ if (isObject(allowed)) {
380
+ ({ allowedVersion } = allowed);
381
+ }
382
+
383
+ return [
384
+ {
385
+ message: allowedVersion
386
+ ? `Expression value of <${ body }> only allowed by Camunda Platform ${ allowedVersion }`
387
+ : `Expression value of <${ body }> not allowed`,
388
+ path: path
389
+ ? [ ...path, propertyName ]
390
+ : null,
391
+ data: {
392
+ type: ERROR_TYPES.EXPRESSION_VALUE_NOT_ALLOWED,
393
+ node: expression,
394
+ parentNode,
395
+ property: propertyName,
396
+ allowedVersion
319
397
  }
320
398
  }
321
399
  ];
@@ -1,11 +1,15 @@
1
1
  module.exports.ERROR_TYPES = Object.freeze({
2
- ELEMENT_TYPE_NOT_ALLOWED: 'elementTypeNotAllowed',
3
- EXTENSION_ELEMENT_NOT_ALLOWED: 'extensionElementNotAllowed',
4
- EXTENSION_ELEMENT_REQUIRED: 'extensionElementRequired',
5
- PROPERTY_DEPENDEND_REQUIRED: 'propertyDependendRequired',
6
- PROPERTY_NOT_ALLOWED: 'propertyNotAllowed',
7
- PROPERTY_REQUIRED: 'propertyRequired',
8
- PROPERTY_TYPE_NOT_ALLOWED: 'propertyTypeNotAllowed',
9
- PROPERTY_VALUE_DUPLICATED: 'propertyValueDuplicated',
10
- PROPERTY_VALUE_NOT_ALLOWED: 'propertyValueNotAllowed',
2
+ ELEMENT_COLLAPSED_NOT_ALLOWED: 'camunda.elementCollapsedNotAllowed',
3
+ ELEMENT_TYPE_NOT_ALLOWED: 'camunda.elementTypeNotAllowed',
4
+ EXPRESSION_REQUIRED: 'camunda.expressionRequired',
5
+ EXPRESSION_VALUE_NOT_ALLOWED: 'camunda.expressionValueNotAllowed',
6
+ EXTENSION_ELEMENT_NOT_ALLOWED: 'camunda.extensionElementNotAllowed',
7
+ EXTENSION_ELEMENT_REQUIRED: 'camunda.extensionElementRequired',
8
+ FEEL_EXPRESSION_INVALID: 'camunda.feelExpressionInvalid',
9
+ PROPERTY_DEPENDEND_REQUIRED: 'camunda.propertyDependendRequired',
10
+ PROPERTY_NOT_ALLOWED: 'camunda.propertyNotAllowed',
11
+ PROPERTY_REQUIRED: 'camunda.propertyRequired',
12
+ PROPERTY_TYPE_NOT_ALLOWED: 'camunda.propertyTypeNotAllowed',
13
+ PROPERTY_VALUE_DUPLICATED: 'camunda.propertyValueDuplicated',
14
+ PROPERTY_VALUE_NOT_ALLOWED: 'camunda.propertyValueNotAllowed'
11
15
  });
@@ -1,4 +1,14 @@
1
- const ISO_DATE_REGEX = /^\d{4}(-\d\d){2}T(\d\d:){2}\d\d(Z|([+-]\d\d:\d\d)(\[\w+\/\w+\])?)?$/;
1
+ const YEAR = '\\d{4}';
2
+ const MONTH = '(?<month>0[1-9]|1[0-2])';
3
+ const DAY = '(0[1-9]|[12][0-9]|3[01])';
4
+ const DATE = `(?<date>${YEAR}-${MONTH}-${DAY})`;
5
+ const HOUR = '(0[0-9]|1[0-9]|2[0-3])';
6
+ const MINUTE = '[0-5][0-9]';
7
+ const SECOND = '[0-5][0-9]';
8
+ const ZONE_ID = '(\\[[^\\]]+\\])';
9
+ const TIMEZONE = `(Z|([+-](0[0-9]|1[0-3]):[0-5][0-9]${ZONE_ID}?))`;
10
+
11
+ const ISO_DATE_REGEX = new RegExp(`^${DATE}T${HOUR}:${MINUTE}:${SECOND}${TIMEZONE}$`);
2
12
  const ISO_DURATION = 'P(?!$)(\\d+(\\.\\d+)?[Yy])?(\\d+(\\.\\d+)?[Mm])?(\\d+(\\.\\d+)?[Dd])?(T(?!$)(\\d+(\\.\\d+)?[Hh])?(\\d+(\\.\\d+)?[Mm])?(\\d+(\\.\\d+)?[Ss])?)?$';
3
13
  const ISO_DURATION_REGEX = new RegExp(`^${ISO_DURATION}$`);
4
14
  const ISO_CYCLE = `R(-1|\\d+)?/${ISO_DURATION}`;
@@ -15,9 +25,27 @@ function validateCycle(value) {
15
25
  }
16
26
 
17
27
  function validateDate(value) {
18
- return ISO_DATE_REGEX.test(value);
28
+ const result = ISO_DATE_REGEX.exec(value);
29
+
30
+ if (!result) {
31
+ return false;
32
+ }
33
+
34
+ return isDateValid(result);
19
35
  }
20
36
 
21
37
  function validateDuration(value) {
22
38
  return ISO_DURATION_REGEX.test(value);
23
39
  }
40
+
41
+ function isDateValid(result) {
42
+ const {
43
+ date,
44
+ month
45
+ } = result.groups;
46
+
47
+ const dateParsedMonth = new Date(date).getMonth() + 1;
48
+ const parsedMonth = Number.parseInt(month, 10);
49
+
50
+ return dateParsedMonth === parsedMonth;
51
+ }
@@ -0,0 +1,5 @@
1
+ const cmp = require('semver-compare');
2
+
3
+ module.exports.greaterOrEqual = function(a, b) {
4
+ return cmp(a, b) !== -1;
5
+ };