Skip to content

Commit 9cf9aba

Browse files
committed
adjust for #93
1 parent aba8acd commit 9cf9aba

File tree

6 files changed

+480
-113
lines changed

6 files changed

+480
-113
lines changed

README.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
An extension for [bpmn-js](https://github.com/bpmn-io/bpmn-js) that makes the data model of the diagram available to other components.
66

77
> [!NOTE]
8-
> As of version `v3` this library exposes both written and consumed variables.
8+
> As of version `v3` this library exposes both written and consumed variables, you can filter them via options.
99
1010
## Usage
1111

@@ -42,14 +42,29 @@ const elementRegistry = modeler.get('elementRegistry');
4242
// retrieve variables relevant to an element
4343
const task = elementRegistry.get('Task_1');
4444

45-
// variables available in scope of <task>
45+
// default: variables relevant to <task> in its visible scopes
4646
await variableResolver.getVariablesForElement(task);
4747

48-
// variables read by <task>, excluding local ones
49-
await variableResolver.getVariablesForElement(task, { read: true, local: false });
48+
// variables read by <task> only
49+
await variableResolver.getVariablesForElement(task, {
50+
read: true,
51+
written: false
52+
});
5053

5154
// all variables written by <task>
52-
await variableResolver.getVariablesForElement(task, { written: true });
55+
await variableResolver.getVariablesForElement(task, { written: true, read: false });
56+
57+
// local variables only (scope === queried element)
58+
await variableResolver.getVariablesForElement(task, {
59+
local: true,
60+
external: false
61+
});
62+
63+
// non-local variables only (scope !== queried element)
64+
await variableResolver.getVariablesForElement(task, {
65+
local: false,
66+
external: true
67+
});
5368

5469
// retrieve all variables defined in a process
5570
const processElement = elementRegistry.get('Process_1');
@@ -58,6 +73,16 @@ const processElement = elementRegistry.get('Process_1');
5873
await variableResolver.getProcessVariables(processElement);
5974
```
6075

76+
`getVariablesForElement(element, options)` supports five filter switches:
77+
78+
| Option | Default | Description |
79+
| --- | --- | --- |
80+
| `read` | `true` | Include variables consumed by the queried element |
81+
| `written` | `true` | Include variables written/created by the queried element |
82+
| `local` | `true` | Include variables local to the queried element scope |
83+
| `external` | `true` | Include variables outside the queried element scope |
84+
| `outputMappings` | `true` | Count output-mapping reads as reads |
85+
6186
### Adding a variable extractor
6287

6388
To add your own variables, extend the `variableProvider` class in your extension. It only needs to implement the `getVariables` method, which takes an element as an argument and returns an array of variables you want to add to the scope of the element. The function can be asynchronous.

lib/base/VariableResolver.js

Lines changed: 158 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ import { getParents } from './util/elementsUtil';
1818
* @property {Array<ModdleElement>} origin
1919
* @property {ModdleElement} [scope]
2020
* @property {Array<Object>} provider
21+
* @property {Array<string|ModdleElement>} [usedBy] Elements or variable names consuming this variable
22+
* @property {Array<string>} [readFrom] Source tags describing where this variable is read from
23+
*/
24+
25+
/**
26+
* @typedef {Object} VariablesFilterOptions
27+
* @property {boolean} [read=true] Include consumed variables
28+
* @property {boolean} [written=true] Include variables written in the queried element
29+
* @property {boolean} [local=true] Include variables in the queried element scope
30+
* @property {boolean} [external=true] Include variables outside the queried element scope
31+
* @property {boolean} [outputMappings=true] Include reads originating from output mappings
32+
*/
33+
34+
/**
35+
* @typedef {ProcessVariable} AvailableVariable
2136
*/
2237

2338
/**
@@ -179,6 +194,18 @@ export class BaseVariableResolver {
179194
});
180195
}
181196
}
197+
198+
if (variable.readFrom) {
199+
if (!existingVariable.readFrom) {
200+
existingVariable.readFrom = [ ...variable.readFrom ];
201+
} else {
202+
variable.readFrom.forEach(source => {
203+
if (!existingVariable.readFrom.includes(source)) {
204+
existingVariable.readFrom.push(source);
205+
}
206+
});
207+
}
208+
}
182209
} else {
183210
mergedVariables.push(variable);
184211
}
@@ -263,12 +290,18 @@ export class BaseVariableResolver {
263290
/**
264291
* Returns all variables in the scope of the given element.
265292
*
293+
* All filter switches default to `true`
294+
*
295+
* Use `{ read: true, written: false }` to retrieve read-only variables.
296+
*
266297
* @async
267298
* @param {ModdleElement} element
268-
* @returns {Promise<Array<ProcessVariable>>} variables
299+
* @param {VariablesFilterOptions} [options]
300+
* @returns {Promise<Array<AvailableVariable>>} variables
269301
*/
270-
async getVariablesForElement(element) {
302+
async getVariablesForElement(element, options = {}) {
271303
const bo = getBusinessObject(element);
304+
const filterOptions = normalizeFilterOptions(options);
272305

273306
const root = getRootElement(bo);
274307
const allVariables = await this.getProcessVariables(root);
@@ -320,14 +353,14 @@ export class BaseVariableResolver {
320353
return false;
321354
});
322355

323-
return deduplicatedVariables.map(variable => {
356+
const projectedScopedVariables = deduplicatedVariables.map(variable => {
324357
if (!variable.usedBy || !Array.isArray(variable.usedBy)) {
325358
return variable;
326359
}
327360

328361
const usedBy = filterUsedByForElement(variable, bo);
329362

330-
if (usedBy.length === variable.usedBy.length) {
363+
if (isSameUsageList(variable.usedBy, usedBy)) {
331364
return variable;
332365
}
333366

@@ -336,32 +369,41 @@ export class BaseVariableResolver {
336369
usedBy: usedBy.length ? usedBy : undefined
337370
};
338371
});
372+
373+
const consumedVariables = allVariables.filter(variable => {
374+
return !variable.scope
375+
&& Array.isArray(variable.usedBy)
376+
&& variable.usedBy.some(usage => usage && usage.id === bo.id);
377+
});
378+
379+
let candidates = projectedScopedVariables;
380+
381+
if (filterOptions.read && !filterOptions.written) {
382+
candidates = [ ...projectedScopedVariables, ...consumedVariables ];
383+
} else if (filterOptions.read && filterOptions.written && !projectedScopedVariables.length) {
384+
385+
// Preserve current default behavior: only fall back to consumed variables
386+
// when no scoped/ancestor variables are available.
387+
candidates = consumedVariables;
388+
}
389+
390+
return candidates.filter(variable => {
391+
const isLocal = !!(variable.scope && variable.scope.id === bo.id);
392+
const hasReadUsage = !!(variable.usedBy && variable.usedBy.length);
393+
const readSources = Array.isArray(variable.readFrom) ? variable.readFrom : [];
394+
const hasOutputMappingRead = hasReadSource(readSources, 'output-mapping');
395+
const hasNonOutputRead = hasReadUsage && (!readSources.length || readSources.some(source => source !== 'output-mapping'));
396+
const isRead = hasNonOutputRead || (filterOptions.outputMappings && hasOutputMappingRead);
397+
const isWritten = !!(variable.origin && variable.origin.some(origin => origin && origin.id === bo.id));
398+
399+
return matchesTypeFilter(isRead, isWritten, filterOptions)
400+
&& matchesScopeFilter(isLocal, filterOptions);
401+
});
339402
}
340403

341404
_getScope(element, containerElement, variableName, checkYourself) {
342405
throw new Error('not implemented VariableResolver#_getScope');
343406
}
344-
345-
/**
346-
* Returns consumed variables for an element — variables
347-
* the element needs as input for its expressions and mappings.
348-
*
349-
* Uses `getVariables()` instead of `getVariablesForElement()` to
350-
* bypass the name-based deduplication that would drop requirement
351-
* entries for variables that also exist in ancestor scopes.
352-
*
353-
* @param {Object} element
354-
* @returns {Promise<Array<AvailableVariable>>}
355-
*/
356-
async getConsumedVariablesForElement(element) {
357-
const allVariablesByRoot = await this.parsedVariables.get();
358-
const allVariables = Object.values(allVariablesByRoot).flat();
359-
360-
return allVariables.filter(v =>
361-
!v.scope
362-
&& v.usedBy && v.usedBy.some((a) => a.id === element.id)
363-
);
364-
}
365407
}
366408

367409
BaseVariableResolver.$inject = [ 'eventBus', 'bpmnjs' ];
@@ -496,35 +538,109 @@ function filterUsedByForElement(variable, element) {
496538
const elements = variable.usedBy.filter(usage => usage && usage.id);
497539

498540
if (!variable.scope) {
499-
return [ ...names, ...elements.filter(usage => isElementInScope(usage, element)) ];
541+
return elements;
500542
}
501543

502544
// Querying the variable's own scope: show local consumers.
503545
if (element.id === variable.scope.id) {
504-
return [
505-
...names,
506-
...elements.filter(usage => isElementInScope(usage, variable.scope))
507-
];
546+
const localConsumers = elements.filter(usage => isElementInScope(usage, variable.scope));
547+
548+
if (localConsumers.length) {
549+
return localConsumers;
550+
}
551+
552+
// For local mapping dependencies represented as names, expose the
553+
// querying element as the consumer.
554+
return names.length ? [ element ] : [];
508555
}
509556

510557
// Querying an ancestor scope: show consumers outside the variable's own scope.
511558
if (isElementInScope(variable.scope, element)) {
512-
return [
513-
...names,
514-
...elements.filter(usage =>
515-
isElementInScope(usage, element)
516-
&& !isElementInScope(usage, variable.scope)
517-
)
518-
];
559+
return elements.filter(usage =>
560+
isElementInScope(usage, element)
561+
&& !isElementInScope(usage, variable.scope)
562+
);
519563
}
520564

521565
// Querying a child scope: show consumers in that child scope only.
522566
if (isElementInScope(element, variable.scope)) {
523-
return [
524-
...names,
525-
...elements.filter(usage => isElementInScope(usage, element))
526-
];
567+
return elements.filter(usage => isElementInScope(usage, element));
527568
}
528569

529-
return names;
570+
return [];
571+
}
572+
573+
function normalizeFilterOptions(options) {
574+
options = options || {};
575+
576+
return {
577+
read: options.read !== false,
578+
written: options.written !== false,
579+
local: options.local !== false,
580+
external: options.external !== false,
581+
outputMappings: options.outputMappings !== false
582+
};
583+
}
584+
585+
function matchesTypeFilter(isRead, isWritten, options) {
586+
if (options.read && options.written) {
587+
return true;
588+
}
589+
590+
if (options.read) {
591+
return isRead;
592+
}
593+
594+
if (options.written) {
595+
return isWritten;
596+
}
597+
598+
return false;
599+
}
600+
601+
function matchesScopeFilter(isLocal, options) {
602+
if (options.local && options.external) {
603+
return true;
604+
}
605+
606+
if (options.local) {
607+
return isLocal;
608+
}
609+
610+
if (options.external) {
611+
return !isLocal;
612+
}
613+
614+
return false;
615+
}
616+
617+
function isSameUsageList(usagesA, usagesB) {
618+
if (!Array.isArray(usagesA) || !Array.isArray(usagesB)) {
619+
return false;
620+
}
621+
622+
if (usagesA.length !== usagesB.length) {
623+
return false;
624+
}
625+
626+
const keysA = usagesA.map(getUsageKey).sort();
627+
const keysB = usagesB.map(getUsageKey).sort();
628+
629+
return keysA.every((key, index) => key === keysB[index]);
630+
}
631+
632+
function getUsageKey(usage) {
633+
if (typeof usage === 'string') {
634+
return `name:${usage}`;
635+
}
636+
637+
if (usage && usage.id) {
638+
return `id:${usage.id}`;
639+
}
640+
641+
return String(usage);
642+
}
643+
644+
function hasReadSource(readFrom, sourceName) {
645+
return Array.isArray(readFrom) && readFrom.includes(sourceName);
530646
}

0 commit comments

Comments
 (0)