@0dep/bpmn-extensions 0.0.2 → 0.0.4

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,7 +1,15 @@
1
1
  import { getExtensions } from './getExtensions.js';
2
+ import { getFeelScope } from './feel.js';
2
3
 
3
4
  const SUBPROCESS_TYPES = new Set(['bpmn:SubProcess', 'bpmn:AdHocSubProcess', 'bpmn:Transaction']);
4
5
 
6
+ // Consumer tags shared between activate (here and in the SubProcessExtensions override) and
7
+ // deactivate — deactivate cancels by tag, so both must subscribe with these exact tags.
8
+ export const ON_ENTER_TAG = '0dep-bpmn-extensions:on-enter';
9
+ export const ON_EXECUTED_TAG = '0dep-bpmn-extensions:on-executed';
10
+ export const ON_START_TAG = '0dep-bpmn-extensions:on-start';
11
+ export const ON_END_TAG = '0dep-bpmn-extensions:on-end';
12
+
5
13
  /**
6
14
  * Activity-level extensions.
7
15
  *
@@ -10,43 +18,55 @@ const SUBPROCESS_TYPES = new Set(['bpmn:SubProcess', 'bpmn:AdHocSubProcess', 'bp
10
18
  * proceeding — the same mechanism bpmn-elements uses for extension formatting.
11
19
  */
12
20
  export class ElementExtensions {
13
- constructor(activity) {
21
+ /**
22
+ * @param {import('bpmn-elements').Activity} activity
23
+ * @param {import('bpmn-elements').ContextInstance} context
24
+ */
25
+ constructor(activity, context) {
14
26
  this.activity = activity;
15
27
  this.formatQ = activity.broker.getQueue('format-run-q');
16
28
 
17
- this.extensions = getExtensions(activity);
29
+ /** @type {import('./getExtensions.js').ExtensionHandlers} */
30
+ this.extensions = getExtensions(activity, context);
18
31
  const Service = this.extensions.Service;
19
32
  if (Service) activity.behaviour.Service = Service;
20
33
  }
34
+ /**
35
+ * Activate extensions
36
+ * @param {import('bpmn-elements').ElementBrokerMessage} message
37
+ */
21
38
  activate(message) {
22
39
  const activity = this.activity;
23
40
  const listeners = this.extensions.listeners;
24
41
 
25
42
  if (message.fields.redelivered && message.fields.routingKey === 'run.start') {
26
- activity.on('start', (api) => this._onEnter(api), { consumerTag: '0dep-bpmn-extensions:on-enter' });
43
+ activity.on('start', (api) => this._onEnter(api), { consumerTag: ON_ENTER_TAG });
27
44
  } else {
28
- activity.on('enter', (api) => this._onEnter(api), { consumerTag: '0dep-bpmn-extensions:on-enter' });
45
+ activity.on('enter', (api) => this._onEnter(api), { consumerTag: ON_ENTER_TAG });
29
46
  }
30
47
 
31
48
  activity.on('activity.execution.completed', (api) => this._onExecuted(api), {
32
- consumerTag: '0dep-bpmn-extensions:on-executed',
49
+ consumerTag: ON_EXECUTED_TAG,
33
50
  });
34
51
 
35
52
  if (listeners?.onStart) {
36
- activity.on('start', (api) => this._onListener('start', api), { consumerTag: '0dep-bpmn-extensions:on-start' });
53
+ activity.on('start', (api) => this._onListener('start', api), { consumerTag: ON_START_TAG });
37
54
  }
38
55
  if (listeners?.onEnd) {
39
- activity.on('end', (api) => this._onListener('end', api), { consumerTag: '0dep-bpmn-extensions:on-end' });
56
+ activity.on('end', (api) => this._onListener('end', api), { consumerTag: ON_END_TAG });
40
57
  }
41
58
  }
42
59
  deactivate() {
43
60
  const broker = this.activity.broker;
44
- broker.cancel('0dep-bpmn-extensions:on-enter');
45
- broker.cancel('0dep-bpmn-extensions:on-executed');
46
- broker.cancel('0dep-bpmn-extensions:on-start');
47
- broker.cancel('0dep-bpmn-extensions:on-end');
61
+ broker.cancel(ON_ENTER_TAG);
62
+ broker.cancel(ON_EXECUTED_TAG);
63
+ broker.cancel(ON_START_TAG);
64
+ broker.cancel(ON_END_TAG);
48
65
  }
49
- /** @internal Shared with SubProcessExtensions. */
66
+ /**
67
+ * @internal Shared with SubProcessExtensions.
68
+ * @param {import('bpmn-elements').IApi<import('bpmn-elements').Activity>} elementApi
69
+ */
50
70
  async _onEnter(elementApi) {
51
71
  this.formatQ.queueMessage({ routingKey: 'run.enter.format' }, { endRoutingKey: 'run.enter.complete' }, { persistent: false });
52
72
  try {
@@ -56,7 +76,10 @@ export class ElementExtensions {
56
76
  elementApi.broker.publish('format', 'run.enter.error', { error: err }, { persistent: false });
57
77
  }
58
78
  }
59
- /** @internal Shared with SubProcessExtensions. */
79
+ /**
80
+ * @internal Shared with SubProcessExtensions.
81
+ * @param {import('bpmn-elements').IApi<import('bpmn-elements').Activity>} elementApi
82
+ */
60
83
  async _onExecuted(elementApi) {
61
84
  this.formatQ.queueMessage({ routingKey: 'run.end.format' }, { endRoutingKey: 'run.end.complete' }, { persistent: false });
62
85
  try {
@@ -66,7 +89,10 @@ export class ElementExtensions {
66
89
  elementApi.broker.publish('format', 'run.end.error', { error: err }, { persistent: false });
67
90
  }
68
91
  }
69
- /** @internal Shared with SubProcessExtensions. */
92
+ /**
93
+ * @internal Shared with SubProcessExtensions.
94
+ * @param {import('bpmn-elements').IApi<import('bpmn-elements').Activity>} elementApi
95
+ */
70
96
  async _onListener(eventType, elementApi) {
71
97
  const routingKey = `run.listener.${eventType}`;
72
98
  this.formatQ.queueMessage({ routingKey }, { endRoutingKey: `${routingKey}.complete` }, { persistent: false });
@@ -78,13 +104,17 @@ export class ElementExtensions {
78
104
  elementApi.broker.publish('format', `${routingKey}.error`, { error: err }, { persistent: false });
79
105
  }
80
106
  }
107
+ /**
108
+ * @param {import('bpmn-elements').IApi<import('bpmn-elements').Activity>} elementApi
109
+ */
81
110
  #formatOnEnter(elementApi) {
82
- const { format, io, headers, properties, form } = this.extensions;
111
+ const { format, io, headers, properties, form, subscription } = this.extensions;
83
112
  const result = { ...format.resolve(elementApi) };
84
113
 
85
114
  if (headers) result.headers = headers.resolve();
86
115
  if (properties) result.properties = properties.resolve(elementApi);
87
116
  if (form) result.form = form.resolve(elementApi);
117
+ if (subscription) result.subscription = subscription.resolve(elementApi);
88
118
  if (io?.hasInput) {
89
119
  result.input = io.getInput(elementApi);
90
120
  // A (sub) process input mapping creates local variables visible to its children; a job
@@ -96,13 +126,16 @@ export class ElementExtensions {
96
126
  Object.assign(elementApi.content, result);
97
127
  return result;
98
128
  }
129
+ /**
130
+ * @param {import('bpmn-elements').IApi<import('bpmn-elements').Activity>} elementApi
131
+ */
99
132
  #formatOnExecuted(elementApi) {
100
133
  const { io, script, calledDecision, loop } = this.extensions;
101
134
  let jobResult = elementApi.content.output;
102
135
 
103
136
  // Multi-instance: aggregate the per-instance outputs into the `outputCollection` array.
104
137
  if (loop?.hasOutputCollection) {
105
- const output = { [loop.outputCollection]: loop.aggregate(jobResult, elementApi.environment.variables) };
138
+ const output = { [loop.outputCollection]: loop.aggregate(jobResult, getFeelScope(elementApi.environment)) };
106
139
  this.#assignOutput(elementApi, output);
107
140
  return { output };
108
141
  }
@@ -132,6 +165,10 @@ export class ElementExtensions {
132
165
  }
133
166
  return {};
134
167
  }
168
+ /**
169
+ * @param {import('bpmn-elements').IApi<import('bpmn-elements').Activity>} elementApi
170
+ * @param {any} output
171
+ */
135
172
  #assignOutput(elementApi, output) {
136
173
  const environment = this.activity.environment;
137
174
  // Visible to downstream FEEL within the process, and surfaced as process output
@@ -1,4 +1,4 @@
1
- import { isFeelExpression, evaluateFeel } from './feel.js';
1
+ import { isFeelExpression, evaluateFeel, getFeelScope } from './feel.js';
2
2
 
3
3
  /**
4
4
  * FEEL-aware expressions implementation for bpmn-elements.
@@ -7,7 +7,7 @@ import { isFeelExpression, evaluateFeel } from './feel.js';
7
7
  * definition resolves FEEL expressions (`= ...`) instead of the default
8
8
  * `${...}` template expressions.
9
9
  *
10
- * @returns {import('bpmn-elements').IExpressions}
10
+ * @returns {import('bpmn-elements').IExpressions & {isExpression: (text:string) => boolean, hasExpression: (text:string) => boolean}}
11
11
  */
12
12
  export function FeelExpressions() {
13
13
  return {
@@ -27,23 +27,12 @@ function hasExpression(text) {
27
27
 
28
28
  /**
29
29
  * Resolve a templated string. Static literals are returned as-is; FEEL expressions are
30
- * evaluated against the environment variables (overlaid with any element-local variables).
30
+ * evaluated in the environment scope (see `getFeelScope`), overlaid with any element-local
31
+ * variables carried on the message content (e.g. a multi-instance item).
31
32
  * @param {string} templatedString
32
33
  * @param {{ environment?: import('bpmn-elements').Environment, content?: any }} [context]
33
34
  */
34
35
  function resolveExpression(templatedString, context) {
35
36
  if (!isFeelExpression(templatedString)) return templatedString;
36
- return evaluateFeel(templatedString, getScope(context));
37
- }
38
-
39
- /**
40
- * Build the FEEL variable scope from a resolution context. Process/definition variables form
41
- * the base scope, overlaid with any local variables carried on the element message content
42
- * (e.g. a multi-instance item).
43
- */
44
- function getScope(context) {
45
- const environment = context && context.environment;
46
- const variables = environment ? environment.variables : {};
47
- const local = context && context.content && context.content.variables;
48
- return { ...variables, ...local };
37
+ return evaluateFeel(templatedString, getFeelScope(context?.environment, context?.content?.variables));
49
38
  }
@@ -6,8 +6,12 @@ import { getExtensions } from './getExtensions.js';
6
6
  * the result to the process variables.
7
7
  */
8
8
  export class ProcessExtensions {
9
+ /**
10
+ * @param {import('bpmn-elements').Process} bp
11
+ */
9
12
  constructor(bp) {
10
13
  this.process = bp;
14
+ /** @type {import('./getExtensions.js').ExtensionHandlers} */
11
15
  this.extensions = getExtensions(bp);
12
16
  }
13
17
  activate() {
@@ -21,6 +25,9 @@ export class ProcessExtensions {
21
25
  deactivate() {
22
26
  this.process.broker.cancel('0dep-bpmn-extensions:on-enter');
23
27
  }
28
+ /**
29
+ * @param {import('bpmn-elements').IApi<import('bpmn-elements').Process>} elementApi
30
+ */
24
31
  #onEnter(elementApi) {
25
32
  try {
26
33
  // bpmn-elements (>=18.0.4) forwards a call activity's formatted input — its io mapping plus,
@@ -1,4 +1,4 @@
1
- import { ElementExtensions } from './ElementExtensions.js';
1
+ import { ElementExtensions, ON_ENTER_TAG, ON_EXECUTED_TAG, ON_START_TAG, ON_END_TAG } from './ElementExtensions.js';
2
2
 
3
3
  /**
4
4
  * Sub-process extensions.
@@ -14,14 +14,14 @@ export class SubProcessExtensions extends ElementExtensions {
14
14
  const listeners = this.extensions.listeners;
15
15
  const enterKey = message.fields.redelivered && message.fields.routingKey === 'run.start' ? 'activity.start' : 'activity.enter';
16
16
 
17
- this.#subscribe(enterKey, (api) => this._onEnter(api), '0dep-bpmn-extensions:on-enter');
18
- this.#subscribe('activity.execution.completed', (api) => this._onExecuted(api), '0dep-bpmn-extensions:on-executed');
17
+ this.#subscribe(enterKey, (api) => this._onEnter(api), ON_ENTER_TAG);
18
+ this.#subscribe('activity.execution.completed', (api) => this._onExecuted(api), ON_EXECUTED_TAG);
19
19
 
20
20
  if (listeners?.onStart) {
21
- this.#subscribe('activity.start', (api) => this._onListener('start', api), '0dep-bpmn-extensions:on-start');
21
+ this.#subscribe('activity.start', (api) => this._onListener('start', api), ON_START_TAG);
22
22
  }
23
23
  if (listeners?.onEnd) {
24
- this.#subscribe('activity.end', (api) => this._onListener('end', api), '0dep-bpmn-extensions:on-end');
24
+ this.#subscribe('activity.end', (api) => this._onListener('end', api), ON_END_TAG);
25
25
  }
26
26
  }
27
27
  #subscribe(routingKey, callback, consumerTag) {
@@ -1,4 +1,4 @@
1
- import { isFeelExpression, evaluateFeel } from '../feel.js';
1
+ import { isFeelExpression, evaluateFeel, getFeelScope } from '../feel.js';
2
2
 
3
3
  /**
4
4
  * A bpmn-elements `scripts` implementation for script tasks.
@@ -16,7 +16,15 @@ export function FeelScripts() {
16
16
  }
17
17
 
18
18
  class FeelScriptRegistry {
19
- register() {}
19
+ register() {
20
+ return undefined;
21
+ }
22
+ /**
23
+ * @param {string} _scriptFormat
24
+ * @param {{ id: string, [x: string]: any }} activity
25
+ * @returns {import('bpmn-elements').Script | undefined} the FEEL script, or undefined when
26
+ * the activity has no `zeebe:script`
27
+ */
20
28
  getScript(_scriptFormat, activity) {
21
29
  const expression = getScriptExpression(activity);
22
30
  if (expression === undefined) return undefined;
@@ -30,7 +38,7 @@ class FeelScript {
30
38
  }
31
39
  execute(scope, callback) {
32
40
  try {
33
- const value = isFeelExpression(this.expression) ? evaluateFeel(this.expression, { ...scope.environment.variables }) : this.expression;
41
+ const value = isFeelExpression(this.expression) ? evaluateFeel(this.expression, getFeelScope(scope.environment)) : this.expression;
34
42
  callback(null, value);
35
43
  } catch (err) {
36
44
  callback(err);
@@ -1,4 +1,4 @@
1
- import { resolveValue } from '../feel.js';
1
+ import { resolveValue, getFeelScope } from '../feel.js';
2
2
 
3
3
  /**
4
4
  * `zeebe:formDefinition`.
@@ -13,7 +13,7 @@ export class Form {
13
13
  this.formDefinition = formDefinition;
14
14
  }
15
15
  resolve(elementApi) {
16
- const scope = elementApi.environment.variables;
16
+ const scope = getFeelScope(elementApi.environment);
17
17
  const { formId, formKey, externalReference, bindingType, versionTag } = this.formDefinition;
18
18
  return {
19
19
  ...(formId !== undefined && { formId: resolveValue(formId, scope) }),
@@ -1,4 +1,4 @@
1
- import { resolveValue } from '../feel.js';
1
+ import { resolveValue, getFeelScope } from '../feel.js';
2
2
 
3
3
  /**
4
4
  * `zeebe:ioMapping`.
@@ -28,7 +28,7 @@ export class IoMapping {
28
28
  * @param {import('bpmn-elements').IApi<any>} elementApi
29
29
  */
30
30
  getInput(elementApi) {
31
- const scope = elementApi.environment.variables;
31
+ const scope = getFeelScope(elementApi.environment);
32
32
  const result = {};
33
33
  for (const { source, target } of this.inputParameters) {
34
34
  if (!target) continue;
@@ -42,7 +42,7 @@ export class IoMapping {
42
42
  * @param {Record<string, any>} [jobResult] Variables produced by the job
43
43
  */
44
44
  getOutput(elementApi, jobResult) {
45
- const scope = { ...elementApi.environment.variables, ...jobResult };
45
+ const scope = getFeelScope(elementApi.environment, jobResult);
46
46
  const result = {};
47
47
  for (const { source, target } of this.outputParameters) {
48
48
  if (!target) continue;
@@ -21,7 +21,7 @@ export class LoopCharacteristics {
21
21
  }
22
22
  /**
23
23
  * @param {Record<string, any>|undefined} indexedOutput bpmn-elements index-keyed instance outputs
24
- * @param {Record<string, any>} baseScope process variables in scope for `outputElement`
24
+ * @param {Record<string, any>} baseScope base scope for `outputElement` (services + process variables)
25
25
  * @returns {any[]}
26
26
  */
27
27
  aggregate(indexedOutput, baseScope) {
@@ -1,4 +1,4 @@
1
- import { resolveValue } from '../feel.js';
1
+ import { resolveValue, getFeelScope } from '../feel.js';
2
2
 
3
3
  /**
4
4
  * `zeebe:properties`.
@@ -11,7 +11,7 @@ export class Properties {
11
11
  this.properties = properties.properties || [];
12
12
  }
13
13
  resolve(elementApi) {
14
- const scope = elementApi.environment.variables;
14
+ const scope = getFeelScope(elementApi.environment);
15
15
  const result = {};
16
16
  for (const { name, value } of this.properties) {
17
17
  if (name === undefined) continue;
@@ -0,0 +1,30 @@
1
+ import { resolveValue, getFeelScope } from '../feel.js';
2
+
3
+ /**
4
+ * `zeebe:subscription` — rides on the referenced `bpmn:Message`, not on the catching element.
5
+ *
6
+ * Resolves the correlation key in the activity's variable scope on enter and exposes it on the
7
+ * element content as `subscription`. Correlation itself is the embedding application's job:
8
+ * read the resolved key off the waiting activity (`getPostponed()` → `content.subscription`)
9
+ * and signal the matching one (`api.signal(...)`).
10
+ */
11
+ export class Subscription {
12
+ /**
13
+ * @param {{ id: string, name?: string }} messageRef The serialized message reference
14
+ * @param {{ correlationKey: string }} subscription The `zeebe:Subscription` behaviour
15
+ */
16
+ constructor(messageRef, subscription) {
17
+ this.message = { id: messageRef.id, name: messageRef.name };
18
+ this.correlationKey = subscription.correlationKey;
19
+ }
20
+ /**
21
+ * @param {import('bpmn-elements').IApi<any>} elementApi
22
+ * @returns {{ message: { id: string, name?: string }, correlationKey: any }}
23
+ */
24
+ resolve(elementApi) {
25
+ return {
26
+ message: this.message,
27
+ correlationKey: resolveValue(this.correlationKey, getFeelScope(elementApi.environment)),
28
+ };
29
+ }
30
+ }
@@ -1,5 +1,5 @@
1
1
  import { ServiceError } from '../Errors.js';
2
- import { resolveValue } from '../feel.js';
2
+ import { resolveValue, getFeelScope } from '../feel.js';
3
3
 
4
4
  /**
5
5
  * `zeebe:taskDefinition`.
@@ -22,13 +22,19 @@ export function JobService(taskType, activity) {
22
22
  this.activity = activity;
23
23
  }
24
24
 
25
+ /**
26
+ * Execute
27
+ * @param {import('bpmn-elements').ElementBrokerMessage} executionMessage
28
+ * @param {CallableFunction} callback
29
+ * @returns {any}
30
+ */
25
31
  JobService.prototype.execute = function execute(executionMessage, callback) {
26
32
  const activity = this.activity;
27
33
  const environment = activity.environment;
28
34
 
29
35
  let jobType;
30
36
  try {
31
- jobType = resolveValue(this.taskType, environment.variables);
37
+ jobType = resolveValue(this.taskType, getFeelScope(environment));
32
38
  } catch (err) {
33
39
  return callback(err);
34
40
  }
@@ -1,16 +1,23 @@
1
- import { resolveValue } from '../feel.js';
1
+ import { resolveValue, getFeelScope } from '../feel.js';
2
2
 
3
3
  /**
4
4
  * Format an activity on enter from its behaviour and extensions: documentation and,
5
5
  * for user tasks, the `zeebe:assignmentDefinition` (assignee, candidate users/groups).
6
6
  */
7
7
  export class FormatActivity {
8
+ /**
9
+ * @param {import('bpmn-elements').Activity} activity
10
+ * @param {any} assignmentDefinition
11
+ */
8
12
  constructor(activity, assignmentDefinition) {
9
13
  this.activity = activity;
10
14
  this.assignmentDefinition = assignmentDefinition;
11
15
  }
16
+ /**
17
+ * @param {import('bpmn-elements').IApi<import('bpmn-elements').Activity>} elementApi
18
+ */
12
19
  resolve(elementApi) {
13
- const scope = elementApi.environment.variables;
20
+ const scope = getFeelScope(elementApi.environment);
14
21
  const result = {};
15
22
 
16
23
  const documentation = this.activity.behaviour.documentation;
@@ -37,15 +44,21 @@ export class FormatActivity {
37
44
  * Format a process on enter: documentation.
38
45
  */
39
46
  export class FormatProcess {
47
+ /**
48
+ * @param {import('bpmn-elements').Process} bp
49
+ */
40
50
  constructor(bp) {
41
51
  this.process = bp;
42
52
  }
53
+ /**
54
+ * @param {import('bpmn-elements').IApi<import('bpmn-elements').Process>} elementApi
55
+ */
43
56
  resolve(elementApi) {
44
57
  const result = {};
45
58
  const documentation = this.process.behaviour.documentation;
46
59
  if (documentation && !elementApi.content.description) {
47
60
  const text = documentation[0]?.text;
48
- if (text) result.description = resolveValue(text, elementApi.environment.variables);
61
+ if (text) result.description = resolveValue(text, getFeelScope(elementApi.environment));
49
62
  }
50
63
  return result;
51
64
  }
@@ -58,7 +71,7 @@ export class FormatProcess {
58
71
  function resolveList(value, scope) {
59
72
  if (!value) return undefined;
60
73
  const resolved = resolveValue(value, scope);
61
- if (Array.isArray(resolved)) return resolved.filter((v) => v !== null && v !== undefined);
74
+ if (Array.isArray(resolved)) return resolved.filter(Boolean);
62
75
  if (typeof resolved !== 'string') return undefined;
63
76
  return resolved
64
77
  .split(',')
package/src/feel.js CHANGED
@@ -40,11 +40,11 @@ export function evaluateFeel(expression, context) {
40
40
  * @param {string} expression FEEL unary test
41
41
  * @param {any} input The value being tested, available as `?` in the test
42
42
  * @param {Record<string, any>} [context] Additional variables in scope
43
- * @returns {boolean}
43
+ * @returns {boolean} true only when the test is satisfied — an undecidable (null) test is false
44
44
  */
45
45
  export function evaluateFeelUnaryTest(expression, input, context) {
46
- const { value } = unaryTest(stripFeel(expression), { ...(context || {}), '?': input });
47
- return value;
46
+ const { value } = unaryTest(stripFeel(expression), { ...context, '?': input });
47
+ return value === true;
48
48
  }
49
49
 
50
50
  /**
@@ -58,3 +58,18 @@ export function resolveValue(value, context) {
58
58
  if (isFeelExpression(value)) return evaluateFeel(value, context);
59
59
  return value;
60
60
  }
61
+
62
+ /**
63
+ * Build the FEEL evaluation scope for an environment. The environment services are exposed
64
+ * under `services`, callable as FEEL functions (e.g. `services.isEligible(order)`) — resume-safe,
65
+ * since the host re-supplies services at recover time, unlike functions smuggled through
66
+ * variables, which JSON state serialization drops. The environment variables form the base
67
+ * scope, overlaid with any local variables (a job result, element-local variables such as a
68
+ * multi-instance item); a variable named `services` shadows the overlay.
69
+ * @param {import('bpmn-elements').Environment} [environment]
70
+ * @param {Record<string, any>} [localVariables] scope-local overlay
71
+ * @returns {Record<string, any>}
72
+ */
73
+ export function getFeelScope(environment, localVariables) {
74
+ return { services: environment?.services, ...environment?.variables, ...localVariables };
75
+ }
@@ -6,24 +6,31 @@ import { ExecutionListeners } from './extensions/ExecutionListeners.js';
6
6
  import { Form } from './extensions/Form.js';
7
7
  import { LoopCharacteristics } from './extensions/LoopCharacteristics.js';
8
8
  import { JobService } from './extensions/TaskDefinition.js';
9
+ import { Subscription } from './extensions/Subscription.js';
10
+
11
+ /**
12
+ * The extension handlers assembled for one element.
13
+ * @typedef {object} ExtensionHandlers
14
+ * @property {FormatActivity | FormatProcess} format
15
+ * @property {IoMapping} [io]
16
+ * @property {TaskHeaders} [headers]
17
+ * @property {Properties} [properties]
18
+ * @property {ExecutionListeners} [listeners]
19
+ * @property {Form} [form]
20
+ * @property {LoopCharacteristics} [loop]
21
+ * @property {any} [script]
22
+ * @property {any} [calledDecision]
23
+ * @property {Function} [Service]
24
+ * @property {Subscription} [subscription]
25
+ */
9
26
 
10
27
  /**
11
28
  * Inspect an element's extension elements and assemble the extension handlers it needs.
12
29
  * @param {import('bpmn-elements').Activity | import('bpmn-elements').Process} element
13
- * @returns {{
14
- * format: FormatActivity | FormatProcess,
15
- * io?: IoMapping,
16
- * headers?: TaskHeaders,
17
- * properties?: Properties,
18
- * listeners?: ExecutionListeners,
19
- * form?: Form,
20
- * loop?: LoopCharacteristics,
21
- * script?: any,
22
- * calledDecision?: any,
23
- * Service?: Function,
24
- * }}
30
+ * @param {import('bpmn-elements').ContextInstance} [context]
31
+ * @returns {ExtensionHandlers}
25
32
  */
26
- export function getExtensions(element) {
33
+ export function getExtensions(element, context) {
27
34
  const result = {};
28
35
  const isProcess = element.type === 'bpmn:Process';
29
36
 
@@ -67,6 +74,18 @@ export function getExtensions(element) {
67
74
  }
68
75
  }
69
76
 
77
+ // A message catch (receive task / message event) may reference a bpmn:Message carrying a
78
+ // zeebe:subscription with the correlation key. The subscription lives on the message element,
79
+ // which the instantiated bpmn-elements Message strips — read it off the serialized context.
80
+ const messageRef =
81
+ element.behaviour.messageRef ?? element.behaviour.eventDefinitions?.find((ed) => ed.behaviour?.messageRef)?.behaviour.messageRef;
82
+ if (messageRef?.id && context) {
83
+ const subscription = context.definitionContext
84
+ .getActivityById(messageRef.id)
85
+ ?.behaviour.extensionElements?.values?.find((v) => v.$type === 'zeebe:Subscription');
86
+ if (subscription?.correlationKey) result.subscription = new Subscription(messageRef, subscription);
87
+ }
88
+
70
89
  // Output side of a multi-instance `zeebe:loopCharacteristics` (the input side is lifted by extendFn).
71
90
  const loopExtension = element.behaviour.loopCharacteristics?.behaviour?.extensionElements?.values?.find(
72
91
  (v) => v.$type === 'zeebe:LoopCharacteristics'
package/src/index.js CHANGED
@@ -5,15 +5,15 @@ import { SubProcessExtensions } from './SubProcessExtensions.js';
5
5
  export { ElementExtensions, ProcessExtensions, SubProcessExtensions };
6
6
  export { FeelExpressions } from './Expressions.js';
7
7
  export { FeelScripts } from './extensions/FeelScripts.js';
8
- export { isFeelExpression, stripFeel, evaluateFeel, evaluateFeelUnaryTest, resolveValue } from './feel.js';
8
+ export { isFeelExpression, stripFeel, evaluateFeel, evaluateFeelUnaryTest, resolveValue, getFeelScope } from './feel.js';
9
9
  export { JobService } from './extensions/TaskDefinition.js';
10
10
  export { ServiceError, FormatError } from './Errors.js';
11
11
 
12
12
  /**
13
13
  * A flow extension activated by bpmn-elements around an element's run.
14
14
  * @typedef {object} FlowExtension
15
- * @property {(message: any) => void} activate
16
- * @property {(message?: any) => void} deactivate
15
+ * @property {(message: import('bpmn-elements').ElementBrokerMessage) => void} activate
16
+ * @property {(message?: import('bpmn-elements').ElementBrokerMessage) => void} deactivate
17
17
  */
18
18
 
19
19
  /**