diff --git a/CHANGELOG.md b/CHANGELOG.md
index b5b7ecf..dd0507a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,14 @@
# unreleased
-- update all dev dependencies and run tests
+# 8.0.0
+
+Stop execution if an invalid time duration, cycle, or date is encountered.
+
+## Breaking
+
+- invalid TimerEventDefinition timer type value stops execution if using [`bpmn-elements@14`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md)
+- remove expireAt formatting from events with TimerEventDefinion(s)
# 7.1.0
diff --git a/package.json b/package.json
index f854dff..f7bb0a1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@onify/flow-extensions",
- "version": "7.1.0",
+ "version": "8.0.0",
"description": "Onify Flow extensions",
"type": "module",
"module": "src/index.js",
@@ -58,9 +58,10 @@
"prettier": "^3.2.4"
},
"peerDependencies": {
- "bpmn-elements": ">=8"
+ "bpmn-elements": "^14.0.0"
},
"dependencies": {
+ "@0dep/piso": "^0.1.3",
"cron-parser": "^4.9.0"
},
"files": [
diff --git a/src/OnifySequenceFlow.js b/src/OnifySequenceFlow.js
index bcae1fb..c3544c7 100644
--- a/src/OnifySequenceFlow.js
+++ b/src/OnifySequenceFlow.js
@@ -30,7 +30,18 @@ export class OnifySequenceFlow extends SequenceFlow {
const properties = this.extensions.properties;
if (!properties) return super.evaluate(fromMessage, callback);
- super.evaluate(fromMessage, (err, result) => {
+ try {
+ const preProperties = properties.resolve(this.getApi(fromMessage));
+ var evaluateMessage = fromMessage;
+ evaluateMessage.content.properties = {
+ ...fromMessage.content.properties,
+ ...preProperties,
+ };
+ } catch (err) {
+ return callback(err);
+ }
+
+ super.evaluate(evaluateMessage, (err, result) => {
if (err) return callback(err);
try {
@@ -38,7 +49,7 @@ export class OnifySequenceFlow extends SequenceFlow {
if (result) {
overriddenResult = {
...(typeof result === 'object' && result),
- properties: properties.resolve(this.getApi(fromMessage)),
+ properties: properties.resolve(this.getApi(evaluateMessage)),
};
}
return callback(err, overriddenResult);
diff --git a/src/OnifyTimerEventDefinition.js b/src/OnifyTimerEventDefinition.js
index 6ef0f66..bac87f9 100644
--- a/src/OnifyTimerEventDefinition.js
+++ b/src/OnifyTimerEventDefinition.js
@@ -1,5 +1,5 @@
-import { TimerEventDefinition } from 'bpmn-elements';
import cronParser from 'cron-parser';
+import { TimerEventDefinition } from 'bpmn-elements';
export class OnifyTimerEventDefinition extends TimerEventDefinition {
constructor(activity, def) {
@@ -14,12 +14,14 @@ export class OnifyTimerEventDefinition extends TimerEventDefinition {
if (cron.expressions?.length) {
// cronParser.parseString expressions disregards seconds, so we have to parse again
const expireAt = cronParser.parseExpression(value).next().toDate();
+
return {
expireAt,
delay: expireAt - Date.now(),
};
}
}
+
return super.parse(timerType, value);
}
}
diff --git a/src/formatters.js b/src/formatters.js
index 7a03fb8..6113bbb 100755
--- a/src/formatters.js
+++ b/src/formatters.js
@@ -1,7 +1,3 @@
-import cronParser from 'cron-parser';
-
-const iso8601cycle = /^\s*(R\d+\/)?P\w+/i;
-
export class FormatActivity {
constructor(activity) {
this.activity = activity;
@@ -10,7 +6,7 @@ export class FormatActivity {
let timeCycles;
if (activity.eventDefinitions) {
for (const ed of activity.eventDefinitions.filter((e) => e.type === 'bpmn:TimerEventDefinition')) {
- if (ed.supports?.includes('cron')) continue;
+ if (!ed.supports?.includes('cron')) continue;
if (!('timeCycle' in ed)) continue;
timeCycles = timeCycles || [];
timeCycles.push(ed.timeCycle);
@@ -28,25 +24,12 @@ export class FormatActivity {
if (assignee) assigneeValue = elementApi.resolveExpression(assignee);
if (documentation) description = documentation[0]?.text;
- let expireAt;
- const timeCycles = this.timeCycles;
- if (timeCycles) {
- for (const cycle of timeCycles) {
- const cron = elementApi.resolveExpression(cycle);
- if (!cron || iso8601cycle.test(cron)) continue;
-
- const expireAtDt = cronParser.parseExpression(cron).next().toDate();
- if (!expireAt || expireAtDt < expireAt) expireAt = expireAtDt;
- }
- }
-
return {
...(this.resultVariable && { resultVariable: this.resultVariable }),
...(scheduledStart && activity.parent.type === 'bpmn:Process' && { scheduledStart }),
...(user?.length && { candidateUsers: user }),
...(groups?.length && { candidateGroups: groups }),
...(!elementApi.content.description && description && { description: elementApi.resolveExpression(description) }),
- ...(expireAt && { expireAt }),
...(assigneeValue && { assignee: assigneeValue }),
};
}
diff --git a/test/features/process-history-ttl-feaure.js b/test/features/process-history-ttl-feaure.js
index cbc6179..b813374 100644
--- a/test/features/process-history-ttl-feaure.js
+++ b/test/features/process-history-ttl-feaure.js
@@ -1,5 +1,5 @@
+import { parseInterval } from '@0dep/piso';
import testHelpers from '../helpers/testHelpers.js';
-import { ISODuration } from 'bpmn-elements';
Feature('Process history ttl', () => {
Scenario('Flow with process history time to live with number of days', () => {
@@ -32,7 +32,7 @@ Feature('Process history ttl', () => {
});
And('it is parsable as ISO8601 duration', () => {
- expect(ISODuration.parse(historyTtl).days).to.equal(180);
+ expect(parseInterval(historyTtl).duration.result.D).to.equal(180);
});
let processEnd, state;
diff --git a/test/features/sequence-flow-feature.js b/test/features/sequence-flow-feature.js
index abae311..c4c1f7b 100644
--- a/test/features/sequence-flow-feature.js
+++ b/test/features/sequence-flow-feature.js
@@ -108,6 +108,7 @@ Feature('Sequence flow', () => {
});
Scenario('Sequence flow with properties', () => {
+ /** @type {import('bpmn-elements').Definition} */
let flow;
const messages = [];
Given('a flow with one conditional sequence flows with properties', async () => {
@@ -220,6 +221,78 @@ Feature('Sequence flow', () => {
expect(message.content.inbound[0].properties).to.not.be.ok;
});
+ Given('flow with condition that address properties', async () => {
+ const source = `
+
+
+
+
+
+
+
+
+
+
+
+ next(null, content.properties.prop1);
+
+
+
+
+
+
+
+
+
+
+
+
+
+ next(null, content.inbound[0].result);
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ flow = await testHelpers.getOnifyFlow(source, {
+ types: {
+ SequenceFlow: OnifySequenceFlow,
+ },
+ });
+ });
+
+ let sourceActivity;
+ let sourceEnd;
+ When('ran', async () => {
+ sourceActivity = flow.getActivityById('start');
+
+ sourceEnd = sourceActivity.waitFor('end');
+
+ end = flow.waitFor('end');
+ await flow.run();
+ });
+
+ Then('expected sequence flow was taken', async () => {
+ await end;
+ expect(sourceActivity.outbound[0].counters).to.have.property('take', 1);
+ });
+
+ And('sequence flow source end message kept property values', async () => {
+ const source = await sourceEnd;
+ expect(source.content.properties).to.have.property('prop1', false);
+ });
+
+ And('second sequence flow that address first sequence flow result was taken', () => {
+ expect(flow.getActivityById('join').outbound[0].counters).to.have.property('take', 1);
+ });
+
Given('a flow with malformatted sequence flow property expression', async () => {
const source = `
@@ -288,6 +361,85 @@ Feature('Sequence flow', () => {
});
});
+ Scenario('a flow with sequence flow property expression function', () => {
+ let source;
+ let options;
+ let flow;
+ Given('a flow matching scenario', () => {
+ source = `
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ options = {
+ services: {
+ propfn(variables) {
+ const count = ++variables.count;
+ if (count > 2) throw new Error('Testing');
+ return true;
+ },
+ },
+ variables: {
+ count: 0,
+ },
+ types: {
+ SequenceFlow: OnifySequenceFlow,
+ },
+ };
+ });
+
+ let end;
+ When('ran', async () => {
+ flow = await testHelpers.getOnifyFlow(source);
+ end = flow.waitFor('end');
+ await flow.run();
+ });
+
+ Then('flow completes', () => {
+ return end;
+ });
+
+ When('ran again and expression property function fails when resolving properties', async () => {
+ flow = await testHelpers.getOnifyFlow(source, options);
+
+ flow.environment.variables.count = 2;
+
+ end = flow.waitFor('end').catch((err) => err);
+
+ await flow.run();
+ });
+
+ Then('flow run fails due to expression throwing error', async () => {
+ const error = await end;
+ expect(error).to.match(/Testing/);
+ });
+
+ When('ran again and expression property function fails when resolving result', async () => {
+ flow = await testHelpers.getOnifyFlow(source, options);
+
+ flow.environment.variables.count = 1;
+
+ end = flow.waitFor('end').catch((err) => err);
+
+ await flow.run();
+ });
+
+ Then('flow run fails due to expression throwing error', async () => {
+ const error = await end;
+ expect(error).to.match(/Testing/);
+ });
+ });
+
Scenario('Sequence flow with execution listeners', () => {
let flow;
const calls = [];
diff --git a/test/features/timers-feature.js b/test/features/timers-feature.js
index 625dcd8..f58cf5c 100644
--- a/test/features/timers-feature.js
+++ b/test/features/timers-feature.js
@@ -3,6 +3,7 @@ import * as ck from 'chronokinesis';
import testHelpers from '../helpers/testHelpers.js';
import factory from '../helpers/factory.js';
import { OnifyTimerEventDefinition } from '../../src/OnifyTimerEventDefinition.js';
+import { TimerEventDefinition } from 'bpmn-elements';
Feature('Flow timers', () => {
let blueprintSource;
@@ -45,37 +46,25 @@ Feature('Flow timers', () => {
expect(element.type).to.equal('bpmn:ServiceTask');
});
- describe('using OnifyTimerEventDefinition', () => {
- When('started with extended TimerEventDefinition', async () => {
+ describe('using bpmn-elements TimerEventDefinition', () => {
+ let fail;
+ When('started without extended TimerEventDefinition', async () => {
flow = await testHelpers.getOnifyFlow(blueprintSource, {
types: {
- TimerEventDefinition: OnifyTimerEventDefinition,
+ TimerEventDefinition,
},
});
- ck.freeze(Date.UTC(2022, 1, 14, 12, 0));
+ fail = flow.waitFor('error');
flow.run();
});
- Then('run is paused at start event', () => {
- [element] = flow.getPostponed();
- expect(element.type).to.equal('bpmn:StartEvent');
+ Then('run fails', () => {
+ return fail;
});
- And('a timer is registered', () => {
- [timer] = flow.environment.timers.executing;
- expect(timer.delay)
- .to.be.above(0)
- .and.equal(Date.UTC(2022, 1, 15) - new Date().getTime());
- });
-
- When('cron start event is cancelled', () => {
- flow.cancelActivity({ id: element.id });
- });
-
- Then('flow continues run', () => {
- [element] = flow.getPostponed();
- expect(element.type).to.equal('bpmn:ServiceTask');
+ And('a no timer is registered', () => {
+ expect(flow.environment.timers.executing).to.have.length(0);
});
});
});
@@ -220,10 +209,12 @@ Feature('Flow timers', () => {
expect(element.content).to.have.property('description', 'Glockenspiel');
});
- And('expire at is set at nearest occasion', () => {
- expect(element.content)
- .to.have.property('expireAt')
- .that.deep.equal(new Date(Date.UTC(2022, 1, 14, 14)));
+ And('two start timers are running', () => {
+ const started = element.getExecuting();
+ expect(started).to.have.length(2);
+
+ expect(started[0].content).to.have.property('expireAt');
+ expect(started[1].content).to.have.property('expireAt');
});
When('start event is cancelled', () => {
@@ -564,7 +555,43 @@ Feature('Flow timers', () => {
Then('an error is thrown', async () => {
const err = await error;
- expect(err.content.error).to.match(/Validation error/);
+ expect(err.content.error).to.be.instanceof(RangeError);
+ });
+ });
+
+ Scenario('Invalid time date', () => {
+ let flow;
+ Given('a flow matching scenario', async () => {
+ const source = `
+
+
+
+
+ 2023-02-29
+
+
+
+ `;
+
+ flow = await testHelpers.getOnifyFlow(source, {
+ types: {
+ TimerEventDefinition: OnifyTimerEventDefinition,
+ },
+ });
+ });
+
+ let error;
+ When('started', () => {
+ error = flow.waitFor('error');
+ flow.run();
+ });
+
+ Then('an error is thrown', async () => {
+ const err = await error;
+ expect(err.content.error).to.be.instanceof(RangeError);
});
});
diff --git a/test/helpers/testHelpers.js b/test/helpers/testHelpers.js
index 21440c3..01bddfa 100644
--- a/test/helpers/testHelpers.js
+++ b/test/helpers/testHelpers.js
@@ -7,6 +7,7 @@ import * as Elements from 'bpmn-elements';
import * as expressions from '@aircall/expression-parser';
import BpmnModdle from 'bpmn-moddle';
import Debug from 'debug';
+import { OnifyTimerEventDefinition as TimerEventDefinition } from '../../src/OnifyTimerEventDefinition.js';
let exts;
@@ -40,7 +41,7 @@ async function getOnifyFlow(source, options = {}) {
const { types, ...environmentOptions } = options || {};
- const serialized = Serializer(moddle, TypeResolver({ ...Elements, ...types }), extendFn);
+ const serialized = Serializer(moddle, TypeResolver({ ...Elements, TimerEventDefinition, ...types }), extendFn);
return new Elements.Definition(new Elements.Context(serialized), getFlowOptions(serialized.name || serialized.id, environmentOptions));
}
@@ -57,7 +58,7 @@ async function getEngine(name, source, options) {
source,
moddleOptions: await getModdleExtensions(),
...getFlowOptions(name, options),
- elements: { ...Elements, ...options?.elements },
+ elements: { ...Elements, TimerEventDefinition, ...options?.elements },
});
}
diff --git a/test/src/OnifyTimerEventDefinition-test.js b/test/src/OnifyTimerEventDefinition-test.js
index dcf1341..5746575 100644
--- a/test/src/OnifyTimerEventDefinition-test.js
+++ b/test/src/OnifyTimerEventDefinition-test.js
@@ -109,7 +109,7 @@ describe('OnifyTimerEventDefinition', () => {
ck.freeze(Date.UTC(2023, 4, 27));
expect(() => {
def.parse('timeCycle', 'yesterday');
- }).to.throw('invalid');
+ }).to.throw(RangeError);
});
});
});