@onify/flow-extensions 9.1.1 → 10.0.1

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.
@@ -1,34 +1,52 @@
1
1
  import { getExtensions } from './getExtensions.js';
2
2
 
3
3
  export class OnifyElementExtensions {
4
+ /**
5
+ * @param {import('bpmn-elements').Activity} activity
6
+ * @param {import('bpmn-elements').ContextInstance} context
7
+ */
4
8
  constructor(activity, context) {
5
9
  this.activity = activity;
6
10
  this.context = context;
7
11
  this.formatQ = activity.broker.getQueue('format-run-q');
8
12
  this._asyncFormatOnEnter = this._asyncFormatOnEnter.bind(this);
9
13
 
10
- const { Service } = (this.extensions = getExtensions(activity, context));
14
+ const { Service, format, properties, io, form } = (this.extensions = getExtensions(activity, context));
11
15
  if (Service) {
12
16
  activity.behaviour.Service = Service;
13
17
  }
18
+
19
+ this._formatOnEnter = Boolean(format?.hasFormatting || properties || form || io?.input.length || io?.output.length);
20
+ this._formatOnEnd = Boolean(properties || io?.output.length || format?.resultVariable);
21
+ }
22
+ /**
23
+ * No camunda extension data to act on — the element runs without this extension
24
+ * @returns {boolean}
25
+ */
26
+ get isEmpty() {
27
+ return !this._formatOnEnter && !this._formatOnEnd && !this.extensions.listeners && !this.extensions.Service;
14
28
  }
15
29
  activate(message) {
16
30
  const activity = this.activity;
17
31
  const executionListeners = this.extensions.listeners;
18
32
 
19
- if (message.fields.redelivered && message.fields.routingKey === 'run.start') {
20
- activity.on('start', this._asyncFormatOnEnter, { consumerTag: '_onify-extension-on-enter' });
21
- } else {
22
- activity.on('enter', this._asyncFormatOnEnter, { consumerTag: '_onify-extension-on-enter' });
33
+ if (this._formatOnEnter) {
34
+ if (message.fields.redelivered && message.fields.routingKey === 'run.start') {
35
+ activity.on('start', this._asyncFormatOnEnter, { consumerTag: '_onify-extension-on-enter' });
36
+ } else {
37
+ activity.on('enter', this._asyncFormatOnEnter, { consumerTag: '_onify-extension-on-enter' });
38
+ }
23
39
  }
24
40
 
25
- activity.on(
26
- 'activity.execution.completed',
27
- (elementApi) => {
28
- return this._onExecutionCompleted(elementApi);
29
- },
30
- { consumerTag: '_onify-extension-on-executed' }
31
- );
41
+ if (this._formatOnEnd) {
42
+ activity.on(
43
+ 'activity.execution.completed',
44
+ (elementApi) => {
45
+ return this._onExecutionCompleted(elementApi);
46
+ },
47
+ { consumerTag: '_onify-extension-on-executed' }
48
+ );
49
+ }
32
50
 
33
51
  if (executionListeners?.onStart) {
34
52
  activity.on(
@@ -2,6 +2,10 @@ import { FormatError } from './Errors.js';
2
2
  import { getExtensions } from './getExtensions.js';
3
3
 
4
4
  export class OnifyProcessExtensions {
5
+ /**
6
+ * @param {import('bpmn-elements').Process} bp
7
+ * @param {import('bpmn-elements').ContextInstance} context
8
+ */
5
9
  constructor(bp, context) {
6
10
  this.process = bp;
7
11
  this.context = context;
@@ -2,30 +2,38 @@ import { SequenceFlow } from 'bpmn-elements';
2
2
  import { getExtensions } from './getExtensions.js';
3
3
 
4
4
  export class OnifySequenceFlow extends SequenceFlow {
5
+ /**
6
+ * @param {import('bpmn-elements').SequenceFlowDefinition} flowDef
7
+ * @param {import('bpmn-elements').ContextInstance} context
8
+ */
5
9
  constructor(flowDef, context) {
6
10
  super(flowDef, context);
7
11
  this.extensions = getExtensions(this, context);
8
- this._activate();
12
+ this.#activate();
9
13
  }
10
- _activate() {
14
+ #activate() {
11
15
  if (!this.extensions.listeners?.onTake) return;
12
16
 
13
17
  this.broker.subscribeTmp(
14
18
  'event',
15
19
  'flow.take',
16
20
  (_, msg) => {
17
- this._executeListeners(msg);
21
+ this.#executeListeners(msg);
18
22
  },
19
23
  { noAck: true, consumerTag: '_onify-execution-listener' }
20
24
  );
21
25
  }
22
- async _executeListeners(message) {
26
+ async #executeListeners(message) {
23
27
  try {
24
28
  await this.extensions.listeners.execute('take', message);
25
29
  } catch (err) {
26
30
  this.logger.error(`<${this.id}> execution listener error: ${err}`);
27
31
  }
28
32
  }
33
+ /**
34
+ * @param {import('bpmn-elements').ElementBrokerMessage} fromMessage
35
+ * @param {(err: Error | null, result?: boolean | unknown) => void} callback
36
+ */
29
37
  evaluate(fromMessage, callback) {
30
38
  const properties = this.extensions.properties;
31
39
  if (!properties) return super.evaluate(fromMessage, callback);
@@ -1,43 +1,44 @@
1
1
  import { OnifyElementExtensions } from './OnifyElementExtensions.js';
2
2
 
3
3
  export class OnifySubProcessExtensions extends OnifyElementExtensions {
4
- constructor(activity, context) {
5
- super(activity, context);
6
- }
7
4
  activate(runMessage) {
8
5
  const activity = this.activity;
9
6
  const broker = activity.broker;
10
7
  const executionListeners = this.extensions.listeners;
11
8
 
12
- if (runMessage.fields.redelivered && runMessage.fields.routingKey === 'run.start') {
13
- this._setupListener(
14
- broker,
15
- 'activity.start',
16
- (elementApi) => {
17
- return this._asyncFormatOnEnter(elementApi);
18
- },
19
- '_onify-extension-on-enter'
20
- );
21
- } else {
9
+ if (this._formatOnEnter) {
10
+ if (runMessage.fields.redelivered && runMessage.fields.routingKey === 'run.start') {
11
+ this._setupListener(
12
+ broker,
13
+ 'activity.start',
14
+ (elementApi) => {
15
+ return this._asyncFormatOnEnter(elementApi);
16
+ },
17
+ '_onify-extension-on-enter'
18
+ );
19
+ } else {
20
+ this._setupListener(
21
+ broker,
22
+ 'activity.enter',
23
+ (elementApi) => {
24
+ return this._asyncFormatOnEnter(elementApi);
25
+ },
26
+ '_onify-extension-on-enter'
27
+ );
28
+ }
29
+ }
30
+
31
+ if (this._formatOnEnd) {
22
32
  this._setupListener(
23
33
  broker,
24
- 'activity.enter',
34
+ 'activity.execution.completed',
25
35
  (elementApi) => {
26
- return this._asyncFormatOnEnter(elementApi);
36
+ return this._onExecutionCompleted(elementApi);
27
37
  },
28
- '_onify-extension-on-enter'
38
+ '_onify-extension-on-executed'
29
39
  );
30
40
  }
31
41
 
32
- this._setupListener(
33
- broker,
34
- 'activity.execution.completed',
35
- (elementApi) => {
36
- return this._onExecutionCompleted(elementApi);
37
- },
38
- '_onify-extension-on-executed'
39
- );
40
-
41
42
  if (executionListeners?.onStart) {
42
43
  this._setupListener(
43
44
  broker,
@@ -2,12 +2,18 @@ import { Cron } from 'croner';
2
2
  import { TimerEventDefinition } from 'bpmn-elements';
3
3
 
4
4
  export class OnifyTimerEventDefinition extends TimerEventDefinition {
5
- constructor(activity, def) {
6
- super(activity, def);
7
- Object.defineProperty(this, 'supports', {
8
- value: ['cron', 'iso8601'],
9
- });
5
+ /**
6
+ * Supported timeCycle formats
7
+ * @returns {string[]}
8
+ */
9
+ get supports() {
10
+ return ['cron', 'iso8601'];
10
11
  }
12
+ /**
13
+ * @param {import('bpmn-elements').TimerType} timerType
14
+ * @param {string} value
15
+ * @returns {import('bpmn-elements').parsedTimer}
16
+ */
11
17
  parse(timerType, value) {
12
18
  if (timerType === 'timeCycle') {
13
19
  try {
@@ -27,7 +33,7 @@ export class OnifyTimerEventDefinition extends TimerEventDefinition {
27
33
  this.logger.error(`<${this.activity?.id}> failed to parse timeCycle: ${rangeError.message}`);
28
34
  this.logger.error(`<${this.activity?.id}> failed to parse timeCycle as cron: ${err.message}`);
29
35
 
30
- throw new RangeError(`Failed to parse timeCycle <${value?.substring(0, 255)}> as ISO 8601 interval or cron`);
36
+ throw new RangeError(`Failed to parse timeCycle <${value?.substring(0, 255)}> as ISO 8601 interval or cron`, { cause: err });
31
37
  }
32
38
  }
33
39
 
@@ -1,6 +1,6 @@
1
1
  import { NotImplemented } from './Errors.js';
2
2
 
3
- export default function ServiceExpression(activity) {
3
+ export function ServiceExpression(activity) {
4
4
  if (!(this instanceof ServiceExpression)) return new ServiceExpression(activity);
5
5
  this.activity = activity;
6
6
  this.type = `${activity.type}:expression`;
package/src/formatters.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export class FormatActivity {
2
+ /**
3
+ * @param {import('bpmn-elements').Activity} activity
4
+ */
2
5
  constructor(activity) {
3
6
  this.activity = activity;
4
7
  this.resultVariable = activity.behaviour.resultVariable;
@@ -13,7 +16,20 @@ export class FormatActivity {
13
16
  }
14
17
  }
15
18
  this.timeCycles = timeCycles;
19
+
20
+ const { documentation, candidateUsers, candidateGroups, scheduledStart, assignee } = activity.behaviour;
21
+ this.hasFormatting = Boolean(
22
+ this.resultVariable ||
23
+ candidateUsers ||
24
+ candidateGroups ||
25
+ assignee ||
26
+ documentation?.[0]?.text ||
27
+ (scheduledStart && activity.parent?.type === 'bpmn:Process')
28
+ );
16
29
  }
30
+ /**
31
+ * @param {import('bpmn-elements').IApi<import('bpmn-elements').Activity>} elementApi
32
+ */
17
33
  resolve(elementApi) {
18
34
  let user, groups, assigneeValue, description;
19
35
  const activity = this.activity;
@@ -36,6 +52,9 @@ export class FormatActivity {
36
52
  }
37
53
 
38
54
  export class FormatProcess {
55
+ /**
56
+ * @param {import('bpmn-elements').Process} bp
57
+ */
39
58
  constructor(bp) {
40
59
  this.process = bp;
41
60
  this._historyTTL = undefined;
@@ -43,6 +62,9 @@ export class FormatProcess {
43
62
  this._historyTTL = bp.context.definitionContext.getTimersByElementId(bp.id).find((t) => t.timer.type === 'historyTimeToLive');
44
63
  }
45
64
  }
65
+ /**
66
+ * @param {import('bpmn-elements').IApi<import('bpmn-elements').Process>} elementApi
67
+ */
46
68
  resolve(elementApi) {
47
69
  let user, groups, description;
48
70
  const bp = this.process;
@@ -61,6 +83,11 @@ export class FormatProcess {
61
83
  }
62
84
  }
63
85
 
86
+ /**
87
+ * @param {import('bpmn-elements').IApi<import('bpmn-elements').Activity>} elementApi
88
+ * @param {string} str
89
+ * @returns {string[] | undefined}
90
+ */
64
91
  function resolveAndSplit(elementApi, str) {
65
92
  if (Array.isArray(str)) return str.filter(Boolean);
66
93
  if (typeof str !== 'string') return;
@@ -1,12 +1,29 @@
1
1
  import { FormatActivity, FormatProcess } from './formatters.js';
2
2
  import { InputOutput } from './IO.js';
3
- import Connector from './Connector.js';
4
- import ExecutionListeners from './ExecutionListeners.js';
5
- import IOForm from './IOForm.js';
6
- import IOProperties from './IOProperties.js';
7
- import ServiceExpression from './ServiceExpression.js';
3
+ import { Connector } from './Connector.js';
4
+ import { ExecutionListeners } from './ExecutionListeners.js';
5
+ import { IOForm } from './IOForm.js';
6
+ import { IOProperties } from './IOProperties.js';
7
+ import { ServiceExpression } from './ServiceExpression.js';
8
8
 
9
+ /**
10
+ * @typedef {object} OnifyExtensions assembled element extensions
11
+ * @property {FormatActivity | FormatProcess} [format] enter/end formatting
12
+ * @property {import('bpmn-elements').IActivityBehaviour} [Service] service factory
13
+ * @property {InputOutput} [io] camunda:InputOutput
14
+ * @property {IOProperties} [properties] camunda:Properties
15
+ * @property {IOForm} [form] camunda:FormData
16
+ * @property {ExecutionListeners} [listeners] camunda:ExecutionListener
17
+ */
18
+
19
+ /**
20
+ * Assemble extensions for an element
21
+ * @param {import('bpmn-elements').Activity | import('bpmn-elements').Process} element
22
+ * @param {import('bpmn-elements').ContextInstance} context
23
+ * @returns {OnifyExtensions}
24
+ */
9
25
  export function getExtensions(element, context) {
26
+ /** @type {OnifyExtensions} */
10
27
  const result = {};
11
28
 
12
29
  switch (element.type) {
@@ -26,8 +43,9 @@ export function getExtensions(element, context) {
26
43
  if (expression) result.Service = ServiceExpression;
27
44
 
28
45
  const extensions = element.behaviour.extensionElements?.values;
29
- if (extensions) {
46
+ if (Array.isArray(extensions)) {
30
47
  for (const ext of extensions) {
48
+ if (!ext) continue;
31
49
  switch (ext.$type) {
32
50
  case 'camunda:Properties':
33
51
  if (ext.values?.length) result.properties = new IOProperties(element, ext);
package/src/index.js CHANGED
@@ -6,6 +6,12 @@ import { OnifySubProcessExtensions } from './OnifySubProcessExtensions.js';
6
6
  export { OnifySequenceFlow } from './OnifySequenceFlow.js';
7
7
  export { OnifyTimerEventDefinition } from './OnifyTimerEventDefinition.js';
8
8
 
9
+ /**
10
+ * Onify flow extensions factory, pass to the engine as `extensions: { onify: extensions }`
11
+ * @param {import('bpmn-elements').ElementBase} element
12
+ * @param {import('bpmn-elements').ContextInstance} context
13
+ * @returns {import('bpmn-elements').IExtension | undefined} undefined for an element without camunda extension data, letting bpmn-elements skip it (and e.g. attach its built-in `assignOutput` extension)
14
+ */
9
15
  export function extensions(element, context) {
10
16
  switch (element.type) {
11
17
  case 'bpmn:Process':
@@ -16,19 +22,25 @@ export function extensions(element, context) {
16
22
  return new OnifySubProcessExtensions(element, context);
17
23
  case 'bpmn:BoundaryEvent':
18
24
  return new OnifyBoundaryEventExtensions(element, context);
19
- default:
20
- return new OnifyElementExtensions(element, context);
25
+ default: {
26
+ const elementExtensions = new OnifyElementExtensions(element, context);
27
+ if (elementExtensions.isEmpty) return undefined;
28
+ return elementExtensions;
29
+ }
21
30
  }
22
31
  }
23
32
 
33
+ /**
34
+ * Extend function for moddle-context-serializer, registers scripts and timers at serialize time
35
+ * @param {import('bpmn-moddle').BaseElement & Record<string, any>} behaviour
36
+ * @param {import('moddle-context-serializer').ExtendContext} context
37
+ */
24
38
  export function extendFn(behaviour, context) {
25
39
  switch (behaviour.$type) {
26
40
  case 'bpmn:StartEvent': {
27
- if (!behaviour.eventDefinitions) break;
41
+ if (!Array.isArray(behaviour.eventDefinitions)) break;
28
42
 
29
- const timer = behaviour.eventDefinitions.find(
30
- ({ type, behaviour: edBehaviour }) => edBehaviour && type === 'bpmn:TimerEventDefinition'
31
- );
43
+ const timer = behaviour.eventDefinitions.find((ed) => ed?.behaviour && ed.type === 'bpmn:TimerEventDefinition');
32
44
  if (timer && timer.behaviour.timeCycle) Object.assign(behaviour, { scheduledStart: timer.behaviour.timeCycle });
33
45
 
34
46
  break;
@@ -50,6 +62,7 @@ export function extendFn(behaviour, context) {
50
62
 
51
63
  let listener = 0;
52
64
  for (const extension of behaviour.extensionElements.values) {
65
+ if (!extension) continue;
53
66
  switch (extension.$type) {
54
67
  case 'camunda:InputOutput':
55
68
  registerIOScripts(behaviour.id, context, extension.$type, extension);
@@ -64,16 +77,27 @@ export function extendFn(behaviour, context) {
64
77
  }
65
78
  }
66
79
 
80
+ /**
81
+ * @param {string} parentId
82
+ * @param {import('bpmn-elements').ContextInstance} context
83
+ * @param {string} type
84
+ * @param {{ inputParameters?: any[], outputParameters?: any[], [x:string]: any }} ioBehaviour
85
+ */
67
86
  function registerIOScripts(parentId, context, type, ioBehaviour) {
68
87
  if (!ioBehaviour) return;
69
88
 
70
- const { inputParameters = [], outputParameters = [] } = ioBehaviour;
71
- for (const { $type, name, definition } of inputParameters.concat(outputParameters)) {
89
+ const { inputParameters, outputParameters } = ioBehaviour;
90
+ const parameters = [
91
+ ...(Array.isArray(inputParameters) ? inputParameters : []),
92
+ ...(Array.isArray(outputParameters) ? outputParameters : []),
93
+ ];
94
+ for (const parm of parameters) {
95
+ const definition = parm?.definition;
72
96
  if (!definition) continue;
73
97
  if (definition.$type !== 'camunda:Script') continue;
74
98
 
75
- const ioType = `${type}/${$type}`;
76
- const filename = `${parentId}/${ioType}/${name}`;
99
+ const ioType = `${type}/${parm.$type}`;
100
+ const filename = `${parentId}/${ioType}/${parm.name}`;
77
101
 
78
102
  context.addScript(filename, {
79
103
  id: filename,
@@ -85,6 +109,13 @@ function registerIOScripts(parentId, context, type, ioBehaviour) {
85
109
  }
86
110
  }
87
111
 
112
+ /**
113
+ * @param {string} parentId script parent id
114
+ * @param {import('bpmn-elements').ContextInstance} context
115
+ * @param {string} type
116
+ * @param {{event:string, script?: {scriptFormat:string, value?:string, resource?:string, [x:string]: any}, [x:string]: any}} listener
117
+ * @param {Number} pos
118
+ */
88
119
  function registerListenerScript(parentId, context, type, listener, pos) {
89
120
  const { event, script } = listener;
90
121
  if (!script) return;
@@ -99,6 +130,9 @@ function registerListenerScript(parentId, context, type, listener, pos) {
99
130
  });
100
131
  }
101
132
 
133
+ /**
134
+ * @param {{id:string, $type:string, historyTimeToLive?:string, [x:string]: any}} behaviour
135
+ */
102
136
  function getHistoryTimeToLiveTimer(behaviour) {
103
137
  const { id, $type: type, historyTimeToLive } = behaviour;
104
138