@@ -4,7 +4,15 @@ import { 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 { AnyELMExpression , ELM , ELMIdentifier , ELMProperty , ELMQuery } from '../types/ELMTypes' ;
7+ import {
8+ AnyELMExpression ,
9+ ELM ,
10+ ELMAliasedQuerySource ,
11+ ELMIdentifier ,
12+ ELMLast ,
13+ ELMProperty ,
14+ ELMQuery
15+ } from '../types/ELMTypes' ;
816import { ExtractedLibrary } from '../types/CQLTypes' ;
917import * as Execution from '../execution/Execution' ;
1018import { UnexpectedResource } from '../types/errors/CustomErrors' ;
@@ -20,6 +28,8 @@ import { uniqBy, isEqual } from 'lodash';
2028import { DateTime , Interval } from 'cql-execution' ;
2129import { parseTimeStringAsUTC } from '../execution/ValueSetHelper' ;
2230import * as MeasureBundleHelpers from './MeasureBundleHelpers' ;
31+ import { findLibraryReference } from './elm/ELMDependencyHelpers' ;
32+ import { findClauseInExpression , findClauseInLibrary , findNamedClausesInExpression } from './elm/ELMHelpers' ;
2333const FHIR_QUERY_PATTERN_URL = 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-fhirQueryPattern' ;
2434
2535/**
@@ -423,7 +433,7 @@ function didEncounterDetailedValueFilterErrors(tbd: fhir4.Extension | GracefulEr
423433// addMustSupport: find any fields as part of this statement.expression,
424434// then search the allRetrieves for that field's context, and add the field to the correct retrieve's mustSupport
425435function addMustSupport ( allRetrieves : DataTypeQuery [ ] , expression : AnyELMExpression , rootLib : ELM , allELM : ELM [ ] ) {
426- const propertyExpressions = findPropertyExpressions ( expression , [ ] , rootLib . library . identifier . id ) ;
436+ const propertyExpressions = findPropertyExpressions ( expression , [ ] , rootLib , allELM ) ;
427437
428438 propertyExpressions . forEach ( prop => {
429439 // find all matches for this property in allRetrieves
@@ -441,7 +451,7 @@ function addMustSupport(allRetrieves: DataTypeQuery[], expression: AnyELMExpress
441451 } ) ;
442452}
443453
444- interface PropertyTracker {
454+ export interface PropertyTracker {
445455 property : ELMProperty ;
446456 stack : ExpressionStackEntry [ ] ;
447457}
@@ -452,62 +462,110 @@ interface PropertyTracker {
452462 *
453463 * @param exp the current expression (top node) of the tree to search for Properties
454464 * @param currentStack stack entries that led to this expression (not including this expression)
455- * @param lib name of library context for this expression
465+ * @param lib library context for this expression
466+ * @param allLib all elm libraries
456467 * @returns array of all properties found in this expression's tree
457468 */
458- function findPropertyExpressions ( exp : object , currentStack : ExpressionStackEntry [ ] , lib : string ) : PropertyTracker [ ] {
469+ export function findPropertyExpressions (
470+ exp : object ,
471+ currentStack : ExpressionStackEntry [ ] ,
472+ lib : ELM ,
473+ allLib : ELM [ ]
474+ ) : PropertyTracker [ ] {
475+ if ( typeof exp !== 'object' ) {
476+ return [ ] ;
477+ }
478+ // ... only do this for objects TODO next
479+ const thisStackEntry : ExpressionStackEntry = {
480+ type : 'type' in exp && exp . type ? ( exp . type as string ) : 'unknown' ,
481+ localId : 'localId' in exp && exp . localId ? ( exp . localId as string ) : 'unknown' ,
482+ libraryName : lib . library . identifier . id
483+ } ;
484+
459485 if ( 'type' in exp && exp . type && exp . type === 'Property' ) {
460486 // base case found property expression
461487 const prop = exp as ELMProperty ;
462488 if ( prop . source ) {
463489 // 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- } ;
469490 return [
470491 { property : prop , stack : currentStack } ,
471- ...findPropertyExpressions (
472- prop . source ,
473- currentStack . concat ( [ thisStackEntry ] ) ,
474- checkLibChange ( prop . source ) ?? lib
475- )
492+ ...findPropertyExpressions ( prop . source , currentStack . concat ( [ thisStackEntry ] ) , lib , allLib )
476493 ] ;
477494 } else {
478495 return [ { property : prop , stack : currentStack } ] ;
479496 }
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 [ ] ;
497+ } else if (
498+ 'type' in exp &&
499+ exp . type &&
500+ ( exp . type === 'FunctionRef' || exp . type === 'ExpressionRef' ) &&
501+ 'libraryName' in exp &&
502+ exp . libraryName &&
503+ 'name' in exp &&
504+ exp . name
505+ ) {
506+ // handle references that go to different libraries
507+
508+ // TODO: do we have to worry about ParameterRef as well?
509+ // TODO: if there isn't a library name, are we good to not poke around for a new expression to explode?
510+
511+ // TODO: do we need to search the entire operand tree, or is the top level okay?
512+ if ( 'operand' in exp && exp . operand ) {
513+ const properties = findPropertyExpressions ( exp . operand , currentStack . concat ( [ thisStackEntry ] ) , lib , allLib ) ;
514+ if ( properties . length > 0 ) {
515+ // if we find the property(s) in the operand, we can short-circuit the search without going to a different library
516+ return properties ;
496517 }
497- } ) ;
518+ }
519+
520+ const newLib = findLibraryReference ( lib , allLib , exp . libraryName as string ) ;
521+ if ( ! newLib ) {
522+ throw new UnexpectedResource ( `Cannot Find Referenced Library: ${ exp . libraryName } ` ) ;
523+ }
524+ const newExp = findNameinLib ( exp . name as string , newLib ) ;
525+ if ( ! newExp ) {
526+ // If we can't uniquely identify the reference, warn and explode immediate expression in current library context
527+ console . warn (
528+ `Issue with searching for properties within ${ exp . name } in library ${ lib . library . identifier . id } . Could not identify reference because it is overloaded or doesn't exist.`
529+ ) ;
530+ return findPropertyExpressions ( exp , currentStack . concat ( [ thisStackEntry ] ) , lib , allLib ) ;
531+ }
532+ return findPropertyExpressions ( newExp , currentStack . concat ( [ thisStackEntry ] ) , newLib , allLib ) ;
533+ } else if ( Array . isArray ( exp ) ) {
534+ return exp . flatMap ( elem => findPropertyExpressions ( elem , currentStack , lib , allLib ) ) ;
535+ } else {
536+ // non property object, recurse all children values
537+ return findPropertyExpressions ( Object . values ( exp ) , currentStack . concat ( [ thisStackEntry ] ) , lib , allLib ) ;
498538 }
499539}
500540
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 ;
541+ // find the expression in this library that matches the passed name
542+ // if there are issues uniquely identifying one, return null
543+ export function findNameinLib ( name : string , lib : ELM ) : AnyELMExpression | null {
544+ // search statements first and return expression
545+ const namedStatement = lib . library . statements . def . filter ( statement => statement . name === name ) ;
546+ if ( namedStatement . length > 0 ) {
547+ if ( namedStatement . length === 1 ) {
548+ return namedStatement [ 0 ] . expression ;
549+ } else {
550+ // if multiple come up, then it's overloaded (we can't handle)
551+ return null ;
552+ }
505553 }
506- return null ;
554+
555+ // search expressions in statements
556+ const foundNames = lib . library . statements . def . flatMap ( statement =>
557+ findNamedClausesInExpression ( statement . expression , name )
558+ ) ;
559+ if ( foundNames . length !== 1 ) {
560+ // if multiple come up, then it's overloaded (we can't handle). If 0 come up, it's ill-formed.
561+ return null ;
562+ }
563+ // TODO: fix statement return (don't want to search annotations)
564+ return foundNames [ 0 ] ;
507565}
508566
509567// search retrieves for any that match this property's stack and alias context
510- function findRetrieveMatches ( prop : PropertyTracker , retrieves : DataTypeQuery [ ] , allELM : ELM [ ] ) : DataTypeQuery [ ] {
568+ export function findRetrieveMatches ( prop : PropertyTracker , retrieves : DataTypeQuery [ ] , allELM : ELM [ ] ) : DataTypeQuery [ ] {
511569 return retrieves . filter ( retrieve => {
512570 const stackMatch = prop . stack . findLast ( ps => {
513571 // find the last property stack entry that matches any entry in the retrieve stack
@@ -519,25 +577,19 @@ function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQuery[],
519577 if ( stackMatch ) {
520578 // find stackMatch in allELM
521579 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 } ` ) ;
580+ if ( ! library ) {
581+ throw new Error ( `Could not find library with id ${ stackMatch . libraryName } ` ) ;
529582 }
530-
531- const localExpression = findExpressionwithLocalId ( topExpression , stackMatch . localId ) ;
532- if ( localExpression ?. type === 'Query' ) {
583+ const localExpression = findClauseInLibrary ( library , stackMatch . localId ) ;
584+ if ( ! localExpression ) {
585+ throw new Error ( `Could not find expression ${ stackMatch . localId } in library with id ${ stackMatch . libraryName } ` ) ;
586+ }
587+ // TODO: handle property source? Or I think if property has source, it's always irrelevant
588+ if ( localExpression ?. type === 'Query' && prop . property . scope ) {
533589 const query = localExpression as ELMQuery ;
534590 // 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- ) {
591+ const source = findSourcewithScope ( query , prop . property . scope ) ;
592+ if ( source && retrieve . retrieveLocalId && findClauseInExpression ( source . expression , retrieve . retrieveLocalId ) ) {
541593 return true ;
542594 } else {
543595 return false ;
@@ -554,29 +606,39 @@ function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQuery[],
554606 } ) ;
555607}
556608
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 ;
609+ // TODO: this is incomplete, needs more cases! and less strict type assumptions
610+ export function findSourcewithScope (
611+ exp : ELMQuery | ELMLast | ELMProperty ,
612+ scope : string
613+ ) : ELMAliasedQuerySource | undefined {
614+ let source ;
615+ if ( exp . type === 'Query' ) {
616+ source = exp . source ?. find ( s => s . alias === scope ) ;
617+ } else if ( exp . type === 'Last' ) {
618+ const lastQuery = exp . source as ELMQuery ; //TODO: is this okay? -> no
619+ source = lastQuery . source ?. find ( s => s . alias === scope ) ;
620+ } else if ( exp . type === 'Property' ) {
621+ // TODO: handle this case??
622+ }
623+ if ( ! source ) {
624+ // also check any let expression sources
625+ if ( 'let' in exp && exp . let ) {
626+ for ( let i = 0 ; ! source && i < exp . let . length ; i ++ ) {
627+ const letClause = exp . let [ i ] ;
628+ if ( 'source' in letClause . expression && letClause . expression . source ) {
629+ source = findSourcewithScope ( letClause . expression , scope ) ;
570630 }
571- } else if ( typeof v === 'object' ) {
572- found = findExpressionwithLocalId ( v , localId ) ;
573631 }
574- if ( found ) break ;
575632 }
576- return found ;
577633 }
634+ // check across non-query types?
635+
636+ return source ;
578637}
579638
580639// Special case TODO: function ref madness
581640// Special case TODO 2: expression ref layers (pair and debug cases with Hoss)
582641// Special case TODO 3: last of... means that matching up the source will require special handling
642+
643+ // ... start making unit tests (cql to elm test data)
644+ // think about what else needs to be tested (i.e. which create function to identify which retrieve is providing the results for an expression ref)
0 commit comments