@@ -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
367409BaseVariableResolver . $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