Skip to content

Commit 3ebcd3e

Browse files
committed
Combine mustSupports from same retrieve
, compare alias references, and traverse across functions keeping track of the correct name
1 parent c4a6439 commit 3ebcd3e

1 file changed

Lines changed: 71 additions & 20 deletions

File tree

src/helpers/DataRequirementHelpers.ts

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
AnyELMExpression,
99
ELM,
1010
ELMAliasedQuerySource,
11+
ELMFunctionRef,
1112
ELMIdentifier,
1213
ELMLast,
1314
ELMProperty,
@@ -25,7 +26,7 @@ import {
2526
parseQueryInfo
2627
} from './elm/QueryFilterParser';
2728
import * as RetrievesHelper from './elm/RetrievesHelper';
28-
import { uniqBy, isEqual } from 'lodash';
29+
import { uniqBy, isEqual, union } from 'lodash';
2930
import { DateTime, Interval } from 'cql-execution';
3031
import { parseTimeStringAsUTC } from '../execution/ValueSetHelper';
3132
import * as MeasureBundleHelpers from './MeasureBundleHelpers';
@@ -104,16 +105,28 @@ export async function getDataRequirements(
104105
type: { coding: [{ code: 'module-definition', system: 'http://terminology.hl7.org/CodeSystem/library-type' }] },
105106
status: 'unknown'
106107
};
107-
// TODO: combine must supports if there are data requirements from the same retrieve that have different mustSupports
108-
// combine based on resourcetype/primary code path
109-
// allRetrieves[0].templateId
110-
// allRetrieves[0].dataType
111-
// allRetrieves[0].path
112-
// allRetrieves[0].code or allRetrieves[0].valueSet
113-
// ^if these 4 things are the same, then it is the same data requirement and we can combine mustSupports
108+
109+
// Combine must supports if there are data requirements from the same retrieve that have different mustSupports
110+
// Combine based on this set defining uniqueness: templateId, dataType, path, and code/valueSet
111+
const retrievesHash = allRetrieves.reduce((hash: Record<string, DataTypeQuery>, retrieve) => {
112+
const hashKey = `${retrieve.templateId}-${retrieve.dataType}-${retrieve.path}-${
113+
retrieve.code?.code || retrieve.valueSet
114+
}`;
115+
if (hash[hashKey]) {
116+
if (hash[hashKey].mustSupport) {
117+
// combine
118+
hash[hashKey].mustSupport = union(hash[hashKey].mustSupport, retrieve.mustSupport);
119+
} else {
120+
hash[hashKey].mustSupport = retrieve.mustSupport;
121+
}
122+
} else {
123+
hash[hashKey] = retrieve;
124+
}
125+
return hash;
126+
}, {});
114127

115128
results.dataRequirement = uniqBy(
116-
allRetrieves.map(retrieve => {
129+
Object.values(retrievesHash).map(retrieve => {
117130
const dr = generateDataRequirement(retrieve);
118131
addFiltersToDataRequirement(retrieve, dr, withErrors);
119132
addFhirQueryPatternToDataRequirements(dr);
@@ -129,7 +142,7 @@ export async function getDataRequirements(
129142
cql: cqls,
130143
elm: elmJSONs,
131144
gaps: {
132-
retrieves: allRetrieves
145+
retrieves: Object.values(retrievesHash)
133146
}
134147
},
135148
withErrors
@@ -625,15 +638,16 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu
625638
const { query, position, source } = scopedQuery;
626639
// if the query is our stackMatch, stop here, otherwise continue with query source
627640
if (position === 0) {
628-
// confirm alias matches scope (i.e. the retrieve is somewhere within the source expression tree)
641+
// confirm alias matches scope (i.e. the retrieve is somewhere within the source expression tree) // TODO: this is not sufficient, multiple retrieves could be defined within the tree of this source, not just the right one
629642
return (
630643
source &&
631644
source.expression.localId &&
632645
!!retrieve.expressionStack?.find(st => st.localId === source.expression.localId)
633646
);
634647
}
635648
if ('name' in source.expression && source.expression.name) {
636-
return checkStackFunctionDefs(
649+
// TODO: do we need any further checks here with the highest reference name?
650+
return !!checkStackFunctionDefs(
637651
prop.stack.slice(matchIdx, matchIdx + position),
638652
source.expression.name,
639653
allELM
@@ -644,7 +658,34 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu
644658
// assume property.source (checked above)
645659
if (prop.property.source && 'name' in prop.property.source && prop.property.source.name) {
646660
// slice property stack from stackMatch id to end
647-
return checkStackFunctionDefs(prop.stack.slice(matchIdx), prop.property.source.name, allELM);
661+
const stackSlice = prop.stack.slice(matchIdx);
662+
const checkName = checkStackFunctionDefs(stackSlice, prop.property.source.name, allELM);
663+
if (checkName) {
664+
// get highest FunctionRef in the stack
665+
// check that the operand has an AliasRef type expression with a name equal to...
666+
// top alias in the retrieve stack (up to stackMatch) -> BipolarDiagnosis vs QualifyingEncounter
667+
// const funcRefStackEntry = stackSlice.find(se => se.type === 'FunctionRef');
668+
// if (!funcRefStackEntry) return true;
669+
// const aliasExp = (expressionFromStackEntry(funcRefStackEntry, allELM) as ELMFunctionRef).operand.find(
670+
// o => o.type === 'AliasRef'
671+
// );
672+
673+
const retMatchIdx = retrieve.expressionStack?.findIndex(
674+
s => s.localId === stackMatch.localId && s.libraryName === stackMatch.libraryName
675+
);
676+
const retSlice = retrieve.expressionStack?.slice(retMatchIdx);
677+
const topAlias = retSlice
678+
?.map(se => {
679+
const exp = expressionFromStackEntry(stackMatch, allELM);
680+
return 'alias' in exp && exp.alias ? exp.alias : undefined;
681+
})
682+
.find(a => !!a);
683+
684+
if (topAlias) {
685+
return topAlias === checkName;
686+
}
687+
return true;
688+
}
648689
}
649690
return false;
650691
}
@@ -662,7 +703,7 @@ export function findStackScopedQuery(stack: ExpressionStackEntry[], scope: strin
662703
// TODO: how do we handle this case (example AI&F query below localId 180 has no localId)
663704
continue;
664705
}
665-
const queryExpression = expressionFromStackEntry(stack[i], allELM);
706+
const queryExpression = expressionFromStackEntry(stack[i], allELM) as ELMQuery;
666707
const source = queryExpression.source.find(s => s.alias === scope);
667708
if (source) {
668709
return { query: queryExpression, position: i, source: source };
@@ -686,7 +727,7 @@ export function expressionFromStackEntry(stackEntry: ExpressionStackEntry, allEL
686727
if (!lib) {
687728
throw Error(`Could not find library with identifier ${stackEntry.libraryName}`);
688729
}
689-
const expression = findClauseInLibrary(lib, stackEntry.localId) as ELMQuery;
730+
const expression = findClauseInLibrary(lib, stackEntry.localId);
690731
if (!expression) {
691732
throw Error(
692733
`Could not find ${stackEntry.type} type expression in ${stackEntry.libraryName} with localId ${stackEntry.localId}`
@@ -696,12 +737,13 @@ export function expressionFromStackEntry(stackEntry: ExpressionStackEntry, allEL
696737
}
697738

698739
// traverse from end of the stack to check all function definitions use name in operand
699-
// TODO: need to traverse from end in order to change approach partway up, or can we traverse from beginning?
740+
//check functiondef to functionref changeover and output last name/alias at the end (switches to an AliasRef instead of operand ref, then can use for comparison at the top level)
700741
export function checkStackFunctionDefs(stack: ExpressionStackEntry[], name: string, allELM: ELM[]) {
742+
let checkName = name;
701743
// return false if no function defs
702744
if (!stack.find(s => s.type === 'FunctionDef')) return false;
703745

704-
for (let i = stack.length - 1; i >= 0; i--) {
746+
for (let i = stack.length - 1; i > 0; i--) {
705747
if (stack[i].type === 'FunctionDef') {
706748
const lib = allELM.find(e => e.library.identifier.id === stack[i].libraryName);
707749
const functionStatement = lib?.library.statements.def.find(s => s.localId === stack[i].localId);
@@ -710,12 +752,21 @@ export function checkStackFunctionDefs(stack: ExpressionStackEntry[], name: stri
710752
`Unable to find function definition statement with localId ${stack[i].localId} in library ${stack[i].libraryName}`
711753
);
712754
}
713-
if (!checkFunctionDefMatch(functionStatement, name)) {
714-
return false;
755+
if (!checkFunctionDefMatch(functionStatement, checkName)) {
756+
// we've hit an operand missmatch, so it's a deadend that should be ignored
757+
return null;
758+
}
759+
// get new name
760+
if (stack[i - 1].type === 'FunctionRef' && stack[i - 1].localId !== 'unknown') {
761+
const functionRef = expressionFromStackEntry(stack[i - 1], allELM) as ELMFunctionRef;
762+
// find first operand with a name. TODO: do we have any other differentiating factors for finding the right operand?
763+
const operand = functionRef.operand.find(o => 'name' in o && o.name);
764+
if (operand && 'name' in operand && operand.name) checkName = operand.name;
715765
}
716766
}
717767
}
718-
return true;
768+
// final checkName can be used for further checks
769+
return checkName;
719770
}
720771

721772
// return true if function def operand matches the passed source name

0 commit comments

Comments
 (0)