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); }); }); });