11import { Extension } from 'fhir/r4' ;
2- import { CalculationOptions , DataTypeQuery , DRCalculationOutput } from '../types/Calculator' ;
2+ import { CalculationOptions , DataTypeQuery , DRCalculationOutput , ExpressionStackEntry } from '../types/Calculator' ;
33import { GracefulError } from '../types/errors/GracefulError' ;
44import { EqualsFilter , InFilter , DuringFilter , codeFilterQuery , AttributeFilter } from '../types/QueryFilterTypes' ;
55import { PatientParameters } from '../compartment-definition/PatientParameters' ;
66import { SearchParameters } from '../compartment-definition/SearchParameters' ;
7- import { ELM , ELMIdentifier } from '../types/ELMTypes' ;
7+ import { AnyELMExpression , ELM , ELMIdentifier , ELMProperty , ELMQuery } from '../types/ELMTypes' ;
88import { ExtractedLibrary } from '../types/CQLTypes' ;
99import * as Execution from '../execution/Execution' ;
1010import { UnexpectedResource } from '../types/errors/CustomErrors' ;
@@ -16,7 +16,7 @@ import {
1616 parseQueryInfo
1717} from './elm/QueryFilterParser' ;
1818import * as RetrievesHelper from './elm/RetrievesHelper' ;
19- import { uniqBy } from 'lodash' ;
19+ import { uniqBy , isEqual } from 'lodash' ;
2020import { DateTime , Interval } from 'cql-execution' ;
2121import { parseTimeStringAsUTC } from '../execution/ValueSetHelper' ;
2222import * as MeasureBundleHelpers from './MeasureBundleHelpers' ;
@@ -71,6 +71,13 @@ export async function getDataRequirements(
7171
7272 await Promise . all ( allRetrievesPromises ) ;
7373
74+ // add must supports
75+ rootLib . library . statements . def . forEach ( statement => {
76+ if ( statement . expression && statement . name != 'Patient' ) {
77+ addMustSupport ( allRetrieves , statement . expression , rootLib , elmJSONs ) ;
78+ }
79+ } ) ;
80+
7481 const results : fhir4 . Library = {
7582 resourceType : 'Library' ,
7683 type : { coding : [ { code : 'module-definition' , system : 'http://terminology.hl7.org/CodeSystem/library-type' } ] } ,
@@ -81,6 +88,7 @@ export async function getDataRequirements(
8188 const dr = generateDataRequirement ( retrieve ) ;
8289 addFiltersToDataRequirement ( retrieve , dr , withErrors ) ;
8390 addFhirQueryPatternToDataRequirements ( dr ) ;
91+ dr . mustSupport = retrieve . mustSupport ;
8492 return dr ;
8593 } ) ,
8694 JSON . stringify
@@ -411,3 +419,164 @@ function didEncounterDetailedValueFilterErrors(tbd: fhir4.Extension | GracefulEr
411419 return false ;
412420 }
413421}
422+
423+ // addMustSupport: find any fields as part of this statement.expression,
424+ // then search the allRetrieves for that field's context, and add the field to the correct retrieve's mustSupport
425+ function addMustSupport ( allRetrieves : DataTypeQuery [ ] , expression : AnyELMExpression , rootLib : ELM , allELM : ELM [ ] ) {
426+ const propertyExpressions = findPropertyExpressions ( expression , [ ] , rootLib . library . identifier . id ) ;
427+
428+ propertyExpressions . forEach ( prop => {
429+ // find all matches for this property in allRetrieves
430+ const retrieveMatches = findRetrieveMatches ( prop , allRetrieves , allELM ) ;
431+ // add mustSupport for each match (if not already included)
432+ retrieveMatches . forEach ( match => {
433+ if ( match . mustSupport ) {
434+ if ( ! match . mustSupport . includes ( prop . property . path ) ) {
435+ match . mustSupport . push ( prop . property . path ) ;
436+ }
437+ } else {
438+ match . mustSupport = [ prop . property . path ] ;
439+ }
440+ } ) ;
441+ } ) ;
442+ }
443+
444+ interface PropertyTracker {
445+ property : ELMProperty ;
446+ stack : ExpressionStackEntry [ ] ;
447+ }
448+
449+ /**
450+ * recurses across all key/values in an ELM tree structure
451+ * finds values with type 'Property' and assumes they are ELMProperty type objects
452+ *
453+ * @param exp the current expression (top node) of the tree to search for Properties
454+ * @param currentStack stack entries that led to this expression (not including this expression)
455+ * @param lib name of library context for this expression
456+ * @returns array of all properties found in this expression's tree
457+ */
458+ function findPropertyExpressions ( exp : object , currentStack : ExpressionStackEntry [ ] , lib : string ) : PropertyTracker [ ] {
459+ if ( 'type' in exp && exp . type && exp . type === 'Property' ) {
460+ // base case found property expression
461+ const prop = exp as ELMProperty ;
462+ if ( prop . source ) {
463+ // add this expression to current stack before recursing on .source
464+ const thisStackEntry : ExpressionStackEntry = {
465+ type : exp . type ,
466+ localId : 'localId' in exp && exp . localId ? ( exp . localId as string ) : 'unknown' ,
467+ libraryName : lib
468+ } ;
469+ return [
470+ { property : prop , stack : currentStack } ,
471+ ...findPropertyExpressions (
472+ prop . source ,
473+ currentStack . concat ( [ thisStackEntry ] ) ,
474+ checkLibChange ( prop . source ) ?? lib
475+ )
476+ ] ;
477+ } else {
478+ return [ { property : prop , stack : currentStack } ] ;
479+ }
480+ } else {
481+ // not a property expression, recurse on all array members or all children values
482+ return Object . values ( exp ) . flatMap ( v => {
483+ const thisStackEntry : ExpressionStackEntry = {
484+ type : 'type' in exp && exp . type ? ( exp . type as string ) : 'unknown' ,
485+ localId : 'localId' in exp && exp . localId ? ( exp . localId as string ) : 'unknown' ,
486+ libraryName : lib
487+ } ;
488+ if ( Array . isArray ( v ) ) {
489+ return v . flatMap ( elem =>
490+ findPropertyExpressions ( elem , currentStack . concat ( [ thisStackEntry ] ) , checkLibChange ( elem ) ?? lib )
491+ ) ;
492+ } else if ( typeof v === 'object' ) {
493+ return findPropertyExpressions ( v , currentStack . concat ( [ thisStackEntry ] ) , checkLibChange ( v ) ?? lib ) ;
494+ } else {
495+ return [ ] ;
496+ }
497+ } ) ;
498+ }
499+ }
500+
501+ function checkLibChange ( value : object ) : string | null {
502+ // for ExpressionRef and FunctionRef we need the new library context
503+ if ( 'libraryName' in value && value . libraryName ) {
504+ return value . libraryName as string ;
505+ }
506+ return null ;
507+ }
508+
509+ // search retrieves for any that match this property's stack and alias context
510+ function findRetrieveMatches ( prop : PropertyTracker , retrieves : DataTypeQuery [ ] , allELM : ELM [ ] ) : DataTypeQuery [ ] {
511+ return retrieves . filter ( retrieve => {
512+ const stackMatch = prop . stack . findLast ( ps => {
513+ // find the last property stack entry that matches any entry in the retrieve stack
514+ return retrieve . expressionStack ?. some (
515+ rs => isEqual ( ps , rs ) //test object equality
516+ ) ;
517+ } ) ;
518+
519+ if ( stackMatch ) {
520+ // find stackMatch in allELM
521+ const library = allELM . find ( lib => lib . library . identifier . id === stackMatch . libraryName ) ;
522+
523+ // statement definition expression should match first of the stack
524+ const topExpression = library ?. library . statements . def . find (
525+ d => d . expression . localId === prop . stack [ 0 ] . localId
526+ ) ?. expression ;
527+ if ( ! topExpression ) {
528+ throw Error ( `Could not find expression ${ prop . stack [ 0 ] . localId } in library with id ${ stackMatch . libraryName } ` ) ;
529+ }
530+
531+ const localExpression = findExpressionwithLocalId ( topExpression , stackMatch . localId ) ;
532+ if ( localExpression ?. type === 'Query' ) {
533+ const query = localExpression as ELMQuery ;
534+ // confirm alias matches scope
535+ const source = query . source . find ( s => s . alias === prop . property . scope ) ;
536+ if (
537+ source &&
538+ retrieve . retrieveLocalId &&
539+ findExpressionwithLocalId ( source . expression , retrieve . retrieveLocalId )
540+ ) {
541+ return true ;
542+ } else {
543+ return false ;
544+ }
545+ } else {
546+ // TODO: handle other types
547+ // - TODO: what if no source, i.e. 160
548+ // TODO: will this always be a query? What else? If not, what's our source for alias matching?
549+ // ... could be a last or first
550+ return false ;
551+ }
552+ }
553+ return false ;
554+ } ) ;
555+ }
556+
557+ // exp is top expression with tree of children to search
558+ function findExpressionwithLocalId ( exp : object , localId : string ) : AnyELMExpression | undefined {
559+ if ( 'localId' in exp && exp . localId && exp . localId === localId ) {
560+ return exp as AnyELMExpression ;
561+ } else {
562+ let found ;
563+ for ( let i = 0 ; i < Object . values ( exp ) . length ; i ++ ) {
564+ const v = Object . values ( exp ) [ i ] ;
565+ if ( Array . isArray ( v ) ) {
566+ for ( let i = 0 ; i < v . length ; i ++ ) {
567+ const elem = v [ i ] ;
568+ found = findExpressionwithLocalId ( elem , localId ) ;
569+ if ( found ) break ;
570+ }
571+ } else if ( typeof v === 'object' ) {
572+ found = findExpressionwithLocalId ( v , localId ) ;
573+ }
574+ if ( found ) break ;
575+ }
576+ return found ;
577+ }
578+ }
579+
580+ // Special case TODO: function ref madness
581+ // Special case TODO 2: expression ref layers (pair and debug cases with Hoss)
582+ // Special case TODO 3: last of... means that matching up the source will require special handling
0 commit comments