diff --git a/.eslintrc.json b/.eslintrc.json index 7417086..931d2bd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -7,7 +7,8 @@ "shared-node-browser": true, "es6": true }, - "extends": "eslint:recommended", + "plugins": ["import"], + "extends": ["eslint:recommended", "plugin:import/recommended"], "rules": { "brace-style": [2, "1tbs", { "allowSingleLine": false @@ -37,6 +38,7 @@ "eol-last": 2, "eqeqeq": 2, "handle-callback-err": 2, + "import/extensions": [2, "always"], "indent": [2, 2, { "SwitchCase": 1 }], diff --git a/CHANGELOG.md b/CHANGELOG.md index 9445e50..bbc24d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ Changelog ========= +# 6.0.0 + +- Require peer dependency `bpmn-elements >= 8` +- Support timer event definition cron time cycle by overloading parse function. Requires `bpmn-elements >= 10` + # 5.0.2 - Correct package module file... diff --git a/index.d.ts b/index.d.ts index 602998a..87deab3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,8 +1,11 @@ declare module '@onify/flow-extensions' { - import { SequenceFlow, ElementBase, Context, IExtension } from 'bpmn-elements'; + import { SequenceFlow, TimerEventDefinition, ElementBase, Context, IExtension } from 'bpmn-elements'; import { extendFn as extendFunction } from 'moddle-context-serializer'; export class OnifySequenceFlow extends SequenceFlow {} + export class OnifyTimerEventDefinition extends TimerEventDefinition { + readonly supports: string[]; + } export function extensions(element: ElementBase, context: Context): IExtension; export const extendFn: extendFunction; } diff --git a/package.json b/package.json index 487d9cd..0664c7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@onify/flow-extensions", - "version": "5.0.2", + "version": "6.0.0", "description": "Onify Flow extensions", "type": "module", "module": "src/index.js", @@ -38,26 +38,30 @@ "license": "MIT", "devDependencies": { "@aircall/expression-parser": "^1.0.4", - "@babel/cli": "^7.21.5", - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "@babel/register": "^7.21.0", + "@babel/cli": "^7.23.4", + "@babel/core": "^7.23.5", + "@babel/preset-env": "^7.23.5", + "@babel/register": "^7.22.15", "bpmn-elements-8-1": "npm:bpmn-elements@8.1", - "bpmn-engine": "^16.0.0", + "bpmn-engine": "^17.1.1", "bpmn-engine-14": "npm:bpmn-engine@14", "bpmn-moddle": "^8.0.1", - "c8": "^7.13.0", + "c8": "^8.0.1", "camunda-bpmn-moddle": "^7.0.1", - "chai": "^4.3.7", - "chronokinesis": "^5.0.2", + "chai": "^4.3.10", + "chronokinesis": "^6.0.0", "debug": "^4.3.4", - "eslint": "^8.39.0", + "eslint": "^8.55.0", + "eslint-plugin-import": "^2.29.0", "mocha": "^10.2.0", "mocha-cakes-2": "^3.3.0", - "moddle-context-serializer": "^3.2.2" + "moddle-context-serializer": "^4.1.1" + }, + "peerDependencies": { + "bpmn-elements": ">=8" }, "dependencies": { - "cron-parser": "^4.8.1" + "cron-parser": "^4.9.0" }, "files": [ "src", diff --git a/src/OnifyTimerEventDefinition.js b/src/OnifyTimerEventDefinition.js new file mode 100644 index 0000000..6ef0f66 --- /dev/null +++ b/src/OnifyTimerEventDefinition.js @@ -0,0 +1,25 @@ +import { TimerEventDefinition } from 'bpmn-elements'; +import cronParser from 'cron-parser'; + +export class OnifyTimerEventDefinition extends TimerEventDefinition { + constructor(activity, def) { + super(activity, def); + Object.defineProperty(this, 'supports', { + value: ['cron'], + }); + } + parse(timerType, value) { + let cron; + if (timerType === 'timeCycle' && (cron = cronParser.parseString(value))) { + 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 cd908b2..8abd724 100755 --- a/src/formatters.js +++ b/src/formatters.js @@ -10,6 +10,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 (!('timeCycle' in ed)) continue; timeCycles = timeCycles || []; timeCycles.push(ed.timeCycle); diff --git a/src/index.js b/src/index.js index a52964d..723addc 100644 --- a/src/index.js +++ b/src/index.js @@ -1,19 +1,14 @@ -import {OnifyProcessExtensions} from './OnifyProcessExtensions.js'; -import {OnifyElementExtensions} from './OnifyElementExtensions.js'; -import {OnifySequenceFlow} from './OnifySequenceFlow.js'; +import { OnifyProcessExtensions } from './OnifyProcessExtensions.js'; +import { OnifyElementExtensions } from './OnifyElementExtensions.js'; +export { OnifySequenceFlow } from './OnifySequenceFlow.js'; +export { OnifyTimerEventDefinition } from './OnifyTimerEventDefinition.js'; -export { - extensions, - extendFn, - OnifySequenceFlow, -}; - -function extensions(element, context) { +export function extensions(element, context) { if (element.type === 'bpmn:Process') return new OnifyProcessExtensions(element, context); return new OnifyElementExtensions(element, context); } -function extendFn(behaviour, context) { +export function extendFn(behaviour, context) { if (behaviour.$type === 'bpmn:StartEvent' && behaviour.eventDefinitions) { const timer = behaviour.eventDefinitions.find(({type, behaviour: edBehaviour}) => edBehaviour && type === 'bpmn:TimerEventDefinition'); if (timer && timer.behaviour.timeCycle) Object.assign(behaviour, {scheduledStart: timer.behaviour.timeCycle}); diff --git a/test/extensions-test.js b/test/extensions-test.js index e3b745c..4dd4c92 100644 --- a/test/extensions-test.js +++ b/test/extensions-test.js @@ -1,5 +1,5 @@ -import {default as Serializer, TypeResolver} from 'moddle-context-serializer'; -import {extendFn} from '../src/index.js'; +import {Serializer, TypeResolver} from 'moddle-context-serializer'; +import * as flowExtensions from '../src/index.js'; import * as Elements from 'bpmn-elements'; import factory from './helpers/factory.js'; import testHelpers from './helpers/testHelpers.js'; @@ -10,90 +10,66 @@ describe('extensions', () => { moddleExtensions = await testHelpers.getModdleExtensions(); }); - it('extendFn registers scripts', async () => { - const source = factory.resource('activedirectory-index-users.bpmn'); - const moddleContext = await testHelpers.moddleContext(source, moddleExtensions); - const serialized = Serializer(moddleContext, TypeResolver(Elements), extendFn); - - expect(serialized.elements.scripts.length).to.equal(5); - - for (const script of serialized.elements.scripts) { - expect(script, script.name).to.have.property('script'); - expect(script.script, script.name).to.have.property('type').that.is.ok; - } + describe('exports', () => { + it('has expected export', () => { + expect(flowExtensions).to.have.property('extensions').that.is.a('function'); + expect(flowExtensions).to.have.property('extendFn').that.is.a('function'); + expect(flowExtensions).to.have.property('OnifySequenceFlow').that.is.a('function'); + expect(flowExtensions).to.have.property('OnifyTimerEventDefinition').that.is.a('function'); + }); }); - it('extendFn registers extension scripts with type', async () => { - const source = ` - - - - - - next(); - - - next(); - - - - - `; - const moddleContext = await testHelpers.moddleContext(source, moddleExtensions); - const serialized = Serializer(moddleContext, TypeResolver(Elements), extendFn); + describe('extendFn', () => { + it('extendFn registers scripts', async () => { + const source = factory.resource('activedirectory-index-users.bpmn'); + const moddleContext = await testHelpers.moddleContext(source, moddleExtensions); + const serialized = Serializer(moddleContext, TypeResolver(Elements), flowExtensions.extendFn); - expect(serialized.elements.scripts.length).to.equal(2); + expect(serialized.elements.scripts.length).to.equal(5); - for (const script of serialized.elements.scripts) { - expect(script, script.name).to.have.property('script'); - expect(script.script, script.name).to.have.property('type', 'camunda:ExecutionListener'); - } - }); + for (const script of serialized.elements.scripts) { + expect(script, script.name).to.have.property('script'); + expect(script.script, script.name).to.have.property('type').that.is.ok; + } + }); - it('extendFn registers io scripts with type', async () => { - const source = ` - - - - - - - next(null, 'GET'); - - /my/items/workspace-1 - - next(null, { id: content.id, statuscode }); - - - - - - `; - const moddleContext = await testHelpers.moddleContext(source, moddleExtensions); - const serialized = Serializer(moddleContext, TypeResolver(Elements), extendFn); + it('extendFn registers extension scripts with type', async () => { + const source = ` + + + + + + next(); + + + next(); + + + + + `; + const moddleContext = await testHelpers.moddleContext(source, moddleExtensions); + const serialized = Serializer(moddleContext, TypeResolver(Elements), flowExtensions.extendFn); - const scripts = serialized.elements.scripts; - expect(scripts.length).to.equal(2); - expect(scripts[0], scripts[0].name).to.have.property('script'); - expect(scripts[0].script, scripts[0].name).to.have.property('type', 'camunda:InputOutput/camunda:InputParameter'); - expect(scripts[1], scripts[1].name).to.have.property('script'); - expect(scripts[1].script, scripts[1].name).to.have.property('type', 'camunda:InputOutput/camunda:OutputParameter'); - }); + expect(serialized.elements.scripts.length).to.equal(2); + + for (const script of serialized.elements.scripts) { + expect(script, script.name).to.have.property('script'); + expect(script.script, script.name).to.have.property('type', 'camunda:ExecutionListener'); + } + }); - it('extendFn registers connector io scripts with type', async () => { - const source = ` - - - - - - onifyApiRequest + it('extendFn registers io scripts with type', async () => { + const source = ` + + + + next(null, 'GET'); @@ -103,22 +79,57 @@ describe('extensions', () => { next(null, { id: content.id, statuscode }); - - - \${content.output.result.statuscode} - - - - - `; - const moddleContext = await testHelpers.moddleContext(source, moddleExtensions); - const serialized = Serializer(moddleContext, TypeResolver(Elements), extendFn); + + + + `; + const moddleContext = await testHelpers.moddleContext(source, moddleExtensions); + const serialized = Serializer(moddleContext, TypeResolver(Elements), flowExtensions.extendFn); + + const scripts = serialized.elements.scripts; + expect(scripts.length).to.equal(2); + expect(scripts[0], scripts[0].name).to.have.property('script'); + expect(scripts[0].script, scripts[0].name).to.have.property('type', 'camunda:InputOutput/camunda:InputParameter'); + expect(scripts[1], scripts[1].name).to.have.property('script'); + expect(scripts[1].script, scripts[1].name).to.have.property('type', 'camunda:InputOutput/camunda:OutputParameter'); + }); + + it('extendFn registers connector io scripts with type', async () => { + const source = ` + + + + + + onifyApiRequest + + + next(null, 'GET'); + + /my/items/workspace-1 + + next(null, { id: content.id, statuscode }); + + + + + \${content.output.result.statuscode} + + + + + `; + const moddleContext = await testHelpers.moddleContext(source, moddleExtensions); + const serialized = Serializer(moddleContext, TypeResolver(Elements), flowExtensions.extendFn); - const scripts = serialized.elements.scripts; - expect(scripts.length).to.equal(2); - expect(scripts[0], scripts[0].name).to.have.property('script'); - expect(scripts[0].script, scripts[0].name).to.have.property('type', 'camunda:Connector/camunda:InputParameter'); - expect(scripts[1], scripts[1].name).to.have.property('script'); - expect(scripts[1].script, scripts[1].name).to.have.property('type', 'camunda:Connector/camunda:OutputParameter'); + const scripts = serialized.elements.scripts; + expect(scripts.length).to.equal(2); + expect(scripts[0], scripts[0].name).to.have.property('script'); + expect(scripts[0].script, scripts[0].name).to.have.property('type', 'camunda:Connector/camunda:InputParameter'); + expect(scripts[1], scripts[1].name).to.have.property('script'); + expect(scripts[1].script, scripts[1].name).to.have.property('type', 'camunda:Connector/camunda:OutputParameter'); + }); }); }); diff --git a/test/features/connector-feature.js b/test/features/connector-feature.js index f48d6c0..f7f365b 100644 --- a/test/features/connector-feature.js +++ b/test/features/connector-feature.js @@ -1,4 +1,4 @@ -import ck from 'chronokinesis'; +import * as ck from 'chronokinesis'; import testHelpers from '../helpers/testHelpers.js'; import factory from '../helpers/factory.js'; diff --git a/test/features/engine-feature.js b/test/features/engine-feature.js index 1350c29..4869d51 100644 --- a/test/features/engine-feature.js +++ b/test/features/engine-feature.js @@ -1,4 +1,4 @@ -import ck from 'chronokinesis'; +import * as ck from 'chronokinesis'; import factory from '../helpers/factory.js'; import testHelpers from '../helpers/testHelpers.js'; import {EventEmitter} from 'events'; diff --git a/test/features/execution-listener-feature.js b/test/features/execution-listener-feature.js index 7390e5e..020555b 100644 --- a/test/features/execution-listener-feature.js +++ b/test/features/execution-listener-feature.js @@ -1,4 +1,4 @@ -import {default as Serializer, TypeResolver} from 'moddle-context-serializer'; +import {Serializer, TypeResolver} from 'moddle-context-serializer'; import {extendFn} from '../../src/index.js'; import * as Elements from 'bpmn-elements'; import factory from '../helpers/factory.js'; diff --git a/test/features/process-feature.js b/test/features/process-feature.js index bc25b90..b13fd54 100644 --- a/test/features/process-feature.js +++ b/test/features/process-feature.js @@ -132,4 +132,193 @@ Feature('Flow process', () => { expect(err.content.error).to.match(/Parser Error/i); }); }); + + Scenario('Process with candidate starter groups and users in various formats', () => { + let source, flow; + Given('a flow with candidate groups as string split by comma with empty and blanks', async () => { + source = ` + + + + + `; + + flow = await testHelpers.getOnifyFlow(source); + }); + + let started; + When('started', () => { + started = flow.waitFor('process.start'); + return flow.run(); + }); + + Then('process have groups in lowercase without empty', async () => { + const bp = await started; + expect(bp.environment.variables).to.have.property('candidateStarterGroups').that.deep.equal(['admins', 'users']); + bp.stop(); + }); + + Given('a flow with candidate groups expression referencing an environment array', async () => { + source = ` + + + + + `; + + flow = await testHelpers.getOnifyFlow(source, { + settings: { + groups: ['admins', '', false, 'USERS'], + }, + }); + }); + + When('started', () => { + started = flow.waitFor('process.start'); + return flow.run(); + }); + + Then('process have groups in without empty', async () => { + const bp = await started; + expect(bp.environment.variables).to.have.property('candidateStarterGroups').that.deep.equal(['admins', 'USERS']); + bp.stop(); + }); + + Given('a flow with candidate groups expression referencing an environment object', async () => { + source = ` + + + + + `; + + flow = await testHelpers.getOnifyFlow(source, { + settings: { + groups: {foo: 'bar'}, + }, + }); + }); + + When('started', () => { + started = flow.waitFor('process.start'); + return flow.run(); + }); + + Then('process have NO groups since type is not accepted', async () => { + const bp = await started; + expect(bp.environment.variables).to.not.have.property('candidateStarterGroups'); + bp.stop(); + }); + + Given('a flow ran with extension that sets candidate groups to string', async () => { + source = ` + + + + + `; + + flow = await testHelpers.getOnifyFlow(source, { + extensions: { + myExtension(bp) { + if (bp.type !== 'bpmn:Process') return; + bp.behaviour.candidateStarterGroups = 'admin'; + return { + activate() {}, + deactivate() {}, + }; + }, + }, + }); + }); + + When('started', () => { + started = flow.waitFor('process.start'); + return flow.run(); + }); + + Then('process have groups', async () => { + const bp = await started; + expect(bp.environment.variables).to.have.property('candidateStarterGroups').that.deep.equal(['admin']); + bp.stop(); + }); + + Given('a flow ran with extension that sets candidate groups to array', async () => { + source = ` + + + + + `; + + flow = await testHelpers.getOnifyFlow(source, { + extensions: { + myExtension(bp) { + if (bp.type !== 'bpmn:Process') return; + bp.behaviour.candidateStarterGroups = ['admin', '', 'Users']; + return { + activate() {}, + deactivate() {}, + }; + }, + }, + }); + }); + + When('started', () => { + started = flow.waitFor('process.start'); + return flow.run(); + }); + + Then('process have groups', async () => { + const bp = await started; + expect(bp.environment.variables).to.have.property('candidateStarterGroups').that.deep.equal(['admin', 'Users']); + bp.stop(); + }); + + Given('a flow ran with extension that sets candidate groups to an object', async () => { + source = ` + + + + + `; + + flow = await testHelpers.getOnifyFlow(source, { + extensions: { + myExtension(bp) { + if (bp.type !== 'bpmn:Process') return; + bp.behaviour.candidateStarterGroups = {foo: 'bar'}; + return { + activate() {}, + deactivate() {}, + }; + }, + }, + }); + }); + + When('started', () => { + started = flow.waitFor('process.start'); + return flow.run(); + }); + + Then('process have NO groups since type is not accepted', async () => { + const bp = await started; + expect(bp.environment.variables).to.not.have.property('candidateStarterGroups'); + bp.stop(); + }); + }); }); diff --git a/test/features/recover-feature.js b/test/features/recover-feature.js index c7c49af..187f6bf 100755 --- a/test/features/recover-feature.js +++ b/test/features/recover-feature.js @@ -1,4 +1,5 @@ import testHelpers from '../helpers/testHelpers.js'; +import factory from '../helpers/factory.js'; Feature('Recover flow', () => { Scenario('A flow is recovered with output parameters', () => { @@ -554,4 +555,66 @@ Feature('Recover flow', () => { expect(flow.environment.output).to.have.property('result').that.deep.equal({ id: 'service', foo: 'bar', statuscode: 200 }); }); }); + + Scenario('recover different versions where elements has disappeared', () => { + let sourceV1, sourceV2, flow, options; + const serviceCalls = []; + Given('version 1 of a process with user tasks and a variety of elements', async () => { + sourceV1 = factory.resource('mother-of-all.bpmn'); + + options = { + services: { + onifyApiRequest(...args) { + serviceCalls.push(args); + args.pop()(null, { statuscode: 200 }); + }, + }, + }; + + flow = await testHelpers.getOnifyFlow(sourceV1, options); + }); + + And('a second version where almost everything has disappeared', () => { + sourceV2 = ` + + + + + + + + + `; + }); + + let execution, started; + When('started', async () => { + started = flow.waitFor('activity.start'); + execution = await flow.run(); + }); + + let state; + And('state is saved immediately after start', async () => { + await started; + await execution.stop(); + + state = await flow.getState(); + }); + + let end; + When('definition recovered and resumed with second version', async () => { + flow = await testHelpers.recoverOnifyFlow(sourceV2, state, options); + end = flow.waitFor('end'); + execution = await flow.resume(); + }); + + And('pending task is signaled', () => { + const [pendingTask] = flow.getPostponed(); + pendingTask.signal(); + }); + + Then('flow run completes', () => { + return end; + }); + }); }); diff --git a/test/features/serialize-feature.js b/test/features/serialize-feature.js index e51e875..9b72c17 100644 --- a/test/features/serialize-feature.js +++ b/test/features/serialize-feature.js @@ -1,7 +1,7 @@ import testHelpers from '../helpers/testHelpers.js'; import factory from '../helpers/factory.js'; import * as Elements from 'bpmn-elements'; -import {default as Serializer, TypeResolver} from 'moddle-context-serializer'; +import {Serializer, TypeResolver} from 'moddle-context-serializer'; import {extendFn} from '../../src/index.js'; Feature('Extend function', () => { @@ -54,5 +54,37 @@ Feature('Extend function', () => { expect(registered.script).to.have.property('scriptFormat'); expect(registered.script).to.have.property('body'); }); + + Given('a source with sequence flow execution listener', async () => { + const source = factory.resource('sequence-flow-properties.bpmn'); + moddleContext = await testHelpers.moddleContext(source, await testHelpers.getModdleExtensions()); + }); + + When('serialized', () => { + serialized = Serializer(moddleContext, TypeResolver(Elements), extendFn); + }); + + Then('all scripts are registered', () => { + expect(serialized.elements.scripts).to.have.length(4); + }); + + And('registered SequenceFlow execution listener scripts', () => { + const [listener0, listener1] = serialized.elements.scripts.filter((s) => s.parent.id === 'to-script'); + expect(listener0).to.have.property('name', 'to-script/camunda:ExecutionListener/take/0'); + expect(listener0).to.have.property('parent').that.deep.equal({ + id: 'to-script', + type: 'bpmn:SequenceFlow', + }); + expect(listener0.script).to.have.property('scriptFormat'); + expect(listener0.script).to.have.property('body'); + + expect(listener1).to.have.property('name', 'to-script/camunda:ExecutionListener/take/1'); + expect(listener1).to.have.property('parent').that.deep.equal({ + id: 'to-script', + type: 'bpmn:SequenceFlow', + }); + expect(listener1.script).to.have.property('scriptFormat'); + expect(listener1.script).to.have.property('resource'); + }); }); }); diff --git a/test/features/timers-feature.js b/test/features/timers-feature.js index bdf823f..b5f5d91 100644 --- a/test/features/timers-feature.js +++ b/test/features/timers-feature.js @@ -1,6 +1,8 @@ -import ck from 'chronokinesis'; +import * as ck from 'chronokinesis'; + import testHelpers from '../helpers/testHelpers.js'; import factory from '../helpers/factory.js'; +import {OnifyTimerEventDefinition} from '../../src/OnifyTimerEventDefinition.js'; Feature('Flow timers', () => { let blueprintSource; @@ -40,6 +42,38 @@ Feature('Flow timers', () => { [element] = flow.getPostponed(); expect(element.type).to.equal('bpmn:ServiceTask'); }); + + describe('using OnifyTimerEventDefinition', () => { + When('started with extended TimerEventDefinition', async () => { + flow = await testHelpers.getOnifyFlow(blueprintSource, { + types: { + TimerEventDefinition: OnifyTimerEventDefinition, + }, + }); + + ck.freeze(Date.UTC(2022, 1, 14, 12, 0)); + flow.run(); + }); + + Then('run is paused at start event', () => { + [element] = flow.getPostponed(); + expect(element.type).to.equal('bpmn:StartEvent'); + }); + + 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'); + }); + }); }); Scenario('Scheduled flow with date', () => { diff --git a/test/helpers/testHelpers.js b/test/helpers/testHelpers.js index 9c09afa..7b538ac 100644 --- a/test/helpers/testHelpers.js +++ b/test/helpers/testHelpers.js @@ -1,4 +1,4 @@ -import {default as Serializer, TypeResolver} from 'moddle-context-serializer'; +import {Serializer, TypeResolver} from 'moddle-context-serializer'; import {Engine} from 'bpmn-engine'; import {extensions, extendFn} from '../../src/index.js'; import {FlowScripts} from './FlowScripts.js'; @@ -33,10 +33,7 @@ async function getOnifyFlow(source, options) { } const serialized = Serializer(moddle, TypeResolver({...Elements, ...options?.types}), extendFn); - return new Elements.Definition(new Elements.Context(serialized), { - ...getFlowOptions(serialized.name || serialized.id), - ...options, - }); + return new Elements.Definition(new Elements.Context(serialized), getFlowOptions(serialized.name || serialized.id, options)); } async function getEngine(name, source, options) { @@ -44,9 +41,7 @@ async function getEngine(name, source, options) { name, source, moddleOptions: await getModdleExtensions(), - extensions: {onify: extensions}, - ...getFlowOptions(name), - ...options, + ...getFlowOptions(name, options), elements: {...Elements, ...options?.elements}, }); } @@ -54,22 +49,20 @@ async function getEngine(name, source, options) { async function recoverOnifyFlow(source, state, options) { const moddle = await moddleContext(source, await getModdleExtensions()); const serialized = Serializer(moddle, TypeResolver(Elements), extendFn); - return new Elements.Definition(new Elements.Context(serialized), { - ...getFlowOptions(state.name || state.id), - ...options, - }).recover(state); + return new Elements.Definition(new Elements.Context(serialized), getFlowOptions(state.name || state.id, options)).recover(state); } -function getFlowOptions(name, options) { +function getFlowOptions(name, options = {}) { + const {extensions: extensionsOption, services, ...rest} = options; return { Logger, - extensions: {extensions}, + extensions: {...extensionsOption, onify: extensions}, services: { httpRequest() {}, onifyApiRequest() {}, onifyElevatedApiRequest() {}, parseJSON() {}, - ...options?.services, + ...services, }, scripts: new FlowScripts(name, './test/resources', { encrypt() {}, @@ -80,6 +73,7 @@ function getFlowOptions(name, options) { }, }), expressions, + ...rest, }; } diff --git a/test/resources/mother-of-all.bpmn b/test/resources/mother-of-all.bpmn new file mode 100644 index 0000000..3c32a05 --- /dev/null +++ b/test/resources/mother-of-all.bpmn @@ -0,0 +1,384 @@ + + + + + + + + + + toFirstScriptTask + + + toFirstScriptTask + toReturnScriptTask + toFirstUserTask + next() + + + toFirstUserTask + toSubProcess + + + toSubProcess + toInclusiveGateway + + + toSubScriptTask + + + toSubScriptTaskTimeout + + PT0.1S + + + + + toSubScriptTask + toSubScriptTaskTimeout + next(); + + + + toPickMe2 + toJoin2 + + + toJoin2 + toJoin1 + toJoin3 + toDecision + + + toPickMe3 + toJoin3 + + + toPickMe1 + toJoin1 + + + toDecision + toLoop + toFinal + + + toLoop + toReturnScriptTask + environment.variables.stopLoop = true; +next(); + + + toInclusiveGateway + toPickMe2 + toPickMe3 + toPickMe1 + + + toFinal + + + + + next(null, !environment.variables.stopLoop); + + + + + + + + + + + + + + + + SequenceFlow_1ifeyo8 + + + + SequenceFlow_1uyrch1 + + + SequenceFlow_0o4woz0 + + + + SequenceFlow_1ifeyo8 + SequenceFlow_1uyrch1 + + + SequenceFlow_0o4woz0 + + + + + + + Use serviceFn + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/resources/sequence-flow-properties.bpmn b/test/resources/sequence-flow-properties.bpmn index 74db9c5..0608300 100644 --- a/test/resources/sequence-flow-properties.bpmn +++ b/test/resources/sequence-flow-properties.bpmn @@ -22,6 +22,9 @@ next(); baz + + + diff --git a/test/src/OnifyTimerEventDefinition-test.js b/test/src/OnifyTimerEventDefinition-test.js new file mode 100644 index 0000000..f6d5093 --- /dev/null +++ b/test/src/OnifyTimerEventDefinition-test.js @@ -0,0 +1,112 @@ +import {Environment} from 'bpmn-elements'; +import * as ck from 'chronokinesis'; +import {Engine} from 'bpmn-engine'; +import * as bpmnElements from 'bpmn-elements'; +import * as bpmnElements8 from 'bpmn-elements-8-1'; + +import {OnifyTimerEventDefinition} from '../../src/OnifyTimerEventDefinition.js'; + +class TimerEventDefinition8Overload extends bpmnElements8.TimerEventDefinition { + parse() { + throw new Error('Not implemented'); + } +} + +describe('OnifyTimerEventDefinition', () => { + let def; + before(() => { + def = new OnifyTimerEventDefinition({ + environment: new Environment(), + }, { + type: 'bpmn:TimerEventDefinition', + }); + }); + afterEach(ck.reset); + + describe('engine', () => { + const source = ` + + + + + \${environment.variables.cron} + + + + `; + + it('parses cron when using extended TimerEventDefinition', async () => { + const engine = new Engine({ + source, + elements: { + ...bpmnElements, + TimerEventDefinition: OnifyTimerEventDefinition, + }, + variables: { + cron: '* * * * *', + }, + }); + + ck.freeze(Date.UTC(2023, 4, 27)); + const execution = await engine.execute(); + + const [activityApi] = execution.getPostponed(); + expect(activityApi.getExecuting()[0].content).to.have.property('timeout', 60000); + + engine.stop(); + }); + + it('ignores cron parse if bpmn-elements < 10 is used', async () => { + const engine = new Engine({ + source, + elements: { + ...bpmnElements8, + TimerEventDefinition: TimerEventDefinition8Overload, + }, + variables: { + cron: '* * * * *', + }, + }); + + ck.freeze(Date.UTC(2023, 4, 27)); + const execution = await engine.execute(); + + const [activityApi] = execution.getPostponed(); + expect(activityApi.getExecuting()[0].content).to.not.have.property('timeout'); + + engine.stop(); + }); + }); + + describe('parse overload', () => { + it('parses standard cron time cycle', () => { + ck.freeze(Date.UTC(2023, 4, 27)); + expect(def.parse('timeCycle', '* * * * *')).to.deep.equal({delay: 60000, expireAt: new Date('2023-05-27T00:01:00.000Z')}); + }); + + it('parses fractional cron time cycle', () => { + ck.freeze(Date.UTC(2023, 4, 27)); + expect(def.parse('timeCycle', '*/5 * * * * *')).to.deep.equal({delay: 5000, expireAt: new Date('2023-05-27T00:00:05.000Z')}); + }); + + it('parses every 5 minutes cron time cycle', () => { + ck.freeze(Date.UTC(2023, 4, 27)); + expect(def.parse('timeCycle', '*/5 * * * *')).to.deep.equal({delay: 300000, expireAt: new Date('2023-05-27T00:05:00.000Z')}); + }); + + it('returns ISO duration if no cron match', () => { + ck.freeze(Date.UTC(2023, 4, 27)); + expect(def.parse('timeCycle', 'PT10S')).to.deep.contain({delay: 10000, expireAt: new Date('2023-05-27T00:00:10.000Z')}); + }); + + it('throws if invalid time cycle', () => { + ck.freeze(Date.UTC(2023, 4, 27)); + expect(() => { + def.parse('timeCycle', 'yesterday'); + }).to.throw('invalid'); + }); + }); +});