Skip to content

Commit dd5f177

Browse files
committed
Initial must supports added to data requirement
1 parent 4f04586 commit dd5f177

2 files changed

Lines changed: 174 additions & 3 deletions

File tree

src/helpers/DataRequirementHelpers.ts

Lines changed: 172 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Extension } from 'fhir/r4';
2-
import { CalculationOptions, DataTypeQuery, DRCalculationOutput } from '../types/Calculator';
2+
import { CalculationOptions, DataTypeQuery, DRCalculationOutput, ExpressionStackEntry } from '../types/Calculator';
33
import { GracefulError } from '../types/errors/GracefulError';
44
import { EqualsFilter, InFilter, DuringFilter, codeFilterQuery, AttributeFilter } from '../types/QueryFilterTypes';
55
import { PatientParameters } from '../compartment-definition/PatientParameters';
66
import { SearchParameters } from '../compartment-definition/SearchParameters';
7-
import { ELM, ELMIdentifier } from '../types/ELMTypes';
7+
import { AnyELMExpression, ELM, ELMIdentifier, ELMProperty, ELMQuery } from '../types/ELMTypes';
88
import { ExtractedLibrary } from '../types/CQLTypes';
99
import * as Execution from '../execution/Execution';
1010
import { UnexpectedResource } from '../types/errors/CustomErrors';
@@ -16,7 +16,7 @@ import {
1616
parseQueryInfo
1717
} from './elm/QueryFilterParser';
1818
import * as RetrievesHelper from './elm/RetrievesHelper';
19-
import { uniqBy } from 'lodash';
19+
import { uniqBy, isEqual } from 'lodash';
2020
import { DateTime, Interval } from 'cql-execution';
2121
import { parseTimeStringAsUTC } from '../execution/ValueSetHelper';
2222
import * 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

src/types/Calculator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ export interface DataTypeQuery {
293293
queryInfo?: QueryInfo;
294294
/** specifies an optional template/profile for the objects that the retrieve returns to conform to */
295295
templateId?: string;
296+
/** array of fields that must be supported in association with this retrieve */
297+
mustSupport?: string[];
296298
}
297299

298300
export interface GapsDataTypeQuery extends DataTypeQuery {

0 commit comments

Comments
 (0)