From 1e5ecbf54def4758474299bb8fb97304dbc8ba94 Mon Sep 17 00:00:00 2001 From: lmd59 Date: Mon, 26 Feb 2024 15:34:29 -0500 Subject: [PATCH 01/14] Initial must supports added to data requirement --- src/helpers/DataRequirementHelpers.ts | 175 +++++++++++++++++++++++++- src/types/Calculator.ts | 2 + 2 files changed, 174 insertions(+), 3 deletions(-) diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index 01d2c7cc..ea12b61f 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -1,10 +1,10 @@ import { Extension } from 'fhir/r4'; -import { CalculationOptions, DataTypeQuery, DRCalculationOutput } from '../types/Calculator'; +import { CalculationOptions, DataTypeQuery, DRCalculationOutput, ExpressionStackEntry } from '../types/Calculator'; import { GracefulError } from '../types/errors/GracefulError'; import { EqualsFilter, InFilter, DuringFilter, codeFilterQuery, AttributeFilter } from '../types/QueryFilterTypes'; import { PatientParameters } from '../compartment-definition/PatientParameters'; import { SearchParameters } from '../compartment-definition/SearchParameters'; -import { ELM, ELMIdentifier } from '../types/ELMTypes'; +import { AnyELMExpression, ELM, ELMIdentifier, ELMProperty, ELMQuery } from '../types/ELMTypes'; import { ExtractedLibrary } from '../types/CQLTypes'; import * as Execution from '../execution/Execution'; import { UnexpectedResource } from '../types/errors/CustomErrors'; @@ -16,7 +16,7 @@ import { parseQueryInfo } from './elm/QueryFilterParser'; import * as RetrievesHelper from './elm/RetrievesHelper'; -import { uniqBy } from 'lodash'; +import { uniqBy, isEqual } from 'lodash'; import { DateTime, Interval } from 'cql-execution'; import { parseTimeStringAsUTC } from '../execution/ValueSetHelper'; import * as MeasureBundleHelpers from './MeasureBundleHelpers'; @@ -71,6 +71,13 @@ export async function getDataRequirements( await Promise.all(allRetrievesPromises); + // add must supports + rootLib.library.statements.def.forEach(statement => { + if (statement.expression && statement.name != 'Patient') { + addMustSupport(allRetrieves, statement.expression, rootLib, elmJSONs); + } + }); + const results: fhir4.Library = { resourceType: 'Library', type: { coding: [{ code: 'module-definition', system: 'http://terminology.hl7.org/CodeSystem/library-type' }] }, @@ -81,6 +88,7 @@ export async function getDataRequirements( const dr = generateDataRequirement(retrieve); addFiltersToDataRequirement(retrieve, dr, withErrors); addFhirQueryPatternToDataRequirements(dr); + dr.mustSupport = retrieve.mustSupport; return dr; }), JSON.stringify @@ -411,3 +419,164 @@ function didEncounterDetailedValueFilterErrors(tbd: fhir4.Extension | GracefulEr return false; } } + +// addMustSupport: find any fields as part of this statement.expression, +// then search the allRetrieves for that field's context, and add the field to the correct retrieve's mustSupport +function addMustSupport(allRetrieves: DataTypeQuery[], expression: AnyELMExpression, rootLib: ELM, allELM: ELM[]) { + const propertyExpressions = findPropertyExpressions(expression, [], rootLib.library.identifier.id); + + propertyExpressions.forEach(prop => { + // find all matches for this property in allRetrieves + const retrieveMatches = findRetrieveMatches(prop, allRetrieves, allELM); + // add mustSupport for each match (if not already included) + retrieveMatches.forEach(match => { + if (match.mustSupport) { + if (!match.mustSupport.includes(prop.property.path)) { + match.mustSupport.push(prop.property.path); + } + } else { + match.mustSupport = [prop.property.path]; + } + }); + }); +} + +interface PropertyTracker { + property: ELMProperty; + stack: ExpressionStackEntry[]; +} + +/** + * recurses across all key/values in an ELM tree structure + * finds values with type 'Property' and assumes they are ELMProperty type objects + * + * @param exp the current expression (top node) of the tree to search for Properties + * @param currentStack stack entries that led to this expression (not including this expression) + * @param lib name of library context for this expression + * @returns array of all properties found in this expression's tree + */ +function findPropertyExpressions(exp: object, currentStack: ExpressionStackEntry[], lib: string): PropertyTracker[] { + if ('type' in exp && exp.type && exp.type === 'Property') { + // base case found property expression + const prop = exp as ELMProperty; + if (prop.source) { + // add this expression to current stack before recursing on .source + const thisStackEntry: ExpressionStackEntry = { + type: exp.type, + localId: 'localId' in exp && exp.localId ? (exp.localId as string) : 'unknown', + libraryName: lib + }; + return [ + { property: prop, stack: currentStack }, + ...findPropertyExpressions( + prop.source, + currentStack.concat([thisStackEntry]), + checkLibChange(prop.source) ?? lib + ) + ]; + } else { + return [{ property: prop, stack: currentStack }]; + } + } else { + // not a property expression, recurse on all array members or all children values + return Object.values(exp).flatMap(v => { + const thisStackEntry: ExpressionStackEntry = { + type: 'type' in exp && exp.type ? (exp.type as string) : 'unknown', + localId: 'localId' in exp && exp.localId ? (exp.localId as string) : 'unknown', + libraryName: lib + }; + if (Array.isArray(v)) { + return v.flatMap(elem => + findPropertyExpressions(elem, currentStack.concat([thisStackEntry]), checkLibChange(elem) ?? lib) + ); + } else if (typeof v === 'object') { + return findPropertyExpressions(v, currentStack.concat([thisStackEntry]), checkLibChange(v) ?? lib); + } else { + return []; + } + }); + } +} + +function checkLibChange(value: object): string | null { + // for ExpressionRef and FunctionRef we need the new library context + if ('libraryName' in value && value.libraryName) { + return value.libraryName as string; + } + return null; +} + +// search retrieves for any that match this property's stack and alias context +function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQuery[], allELM: ELM[]): DataTypeQuery[] { + return retrieves.filter(retrieve => { + const stackMatch = prop.stack.findLast(ps => { + // find the last property stack entry that matches any entry in the retrieve stack + return retrieve.expressionStack?.some( + rs => isEqual(ps, rs) //test object equality + ); + }); + + if (stackMatch) { + // find stackMatch in allELM + const library = allELM.find(lib => lib.library.identifier.id === stackMatch.libraryName); + + // statement definition expression should match first of the stack + const topExpression = library?.library.statements.def.find( + d => d.expression.localId === prop.stack[0].localId + )?.expression; + if (!topExpression) { + throw Error(`Could not find expression ${prop.stack[0].localId} in library with id ${stackMatch.libraryName}`); + } + + const localExpression = findExpressionwithLocalId(topExpression, stackMatch.localId); + if (localExpression?.type === 'Query') { + const query = localExpression as ELMQuery; + // confirm alias matches scope + const source = query.source.find(s => s.alias === prop.property.scope); + if ( + source && + retrieve.retrieveLocalId && + findExpressionwithLocalId(source.expression, retrieve.retrieveLocalId) + ) { + return true; + } else { + return false; + } + } else { + // TODO: handle other types + // - TODO: what if no source, i.e. 160 + // TODO: will this always be a query? What else? If not, what's our source for alias matching? + // ... could be a last or first + return false; + } + } + return false; + }); +} + +// exp is top expression with tree of children to search +function findExpressionwithLocalId(exp: object, localId: string): AnyELMExpression | undefined { + if ('localId' in exp && exp.localId && exp.localId === localId) { + return exp as AnyELMExpression; + } else { + let found; + for (let i = 0; i < Object.values(exp).length; i++) { + const v = Object.values(exp)[i]; + if (Array.isArray(v)) { + for (let i = 0; i < v.length; i++) { + const elem = v[i]; + found = findExpressionwithLocalId(elem, localId); + if (found) break; + } + } else if (typeof v === 'object') { + found = findExpressionwithLocalId(v, localId); + } + if (found) break; + } + return found; + } +} + +// Special case TODO: function ref madness +// Special case TODO 2: expression ref layers (pair and debug cases with Hoss) +// Special case TODO 3: last of... means that matching up the source will require special handling diff --git a/src/types/Calculator.ts b/src/types/Calculator.ts index 8b007950..51266dc4 100644 --- a/src/types/Calculator.ts +++ b/src/types/Calculator.ts @@ -297,6 +297,8 @@ export interface DataTypeQuery { queryInfo?: QueryInfo; /** specifies an optional template/profile for the objects that the retrieve returns to conform to */ templateId?: string; + /** array of fields that must be supported in association with this retrieve */ + mustSupport?: string[]; } export interface GapsDataTypeQuery extends DataTypeQuery { From 301565f875059b23a5a026f334c40b13e2e0862b Mon Sep 17 00:00:00 2001 From: lmd59 Date: Tue, 19 Mar 2024 10:44:07 -0400 Subject: [PATCH 02/14] Unit tests, scope updates, and other library traversal --- src/helpers/DataRequirementHelpers.ts | 204 +++++++++++++------- src/helpers/elm/ELMHelpers.ts | 21 +- test/unit/DataRequirementHelpers.test.ts | 236 +++++++++++++++++++++++ 3 files changed, 389 insertions(+), 72 deletions(-) diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index ea12b61f..98bc5179 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -4,7 +4,15 @@ import { GracefulError } from '../types/errors/GracefulError'; import { EqualsFilter, InFilter, DuringFilter, codeFilterQuery, AttributeFilter } from '../types/QueryFilterTypes'; import { PatientParameters } from '../compartment-definition/PatientParameters'; import { SearchParameters } from '../compartment-definition/SearchParameters'; -import { AnyELMExpression, ELM, ELMIdentifier, ELMProperty, ELMQuery } from '../types/ELMTypes'; +import { + AnyELMExpression, + ELM, + ELMAliasedQuerySource, + ELMIdentifier, + ELMLast, + ELMProperty, + ELMQuery +} from '../types/ELMTypes'; import { ExtractedLibrary } from '../types/CQLTypes'; import * as Execution from '../execution/Execution'; import { UnexpectedResource } from '../types/errors/CustomErrors'; @@ -20,6 +28,8 @@ import { uniqBy, isEqual } from 'lodash'; import { DateTime, Interval } from 'cql-execution'; import { parseTimeStringAsUTC } from '../execution/ValueSetHelper'; import * as MeasureBundleHelpers from './MeasureBundleHelpers'; +import { findLibraryReference } from './elm/ELMDependencyHelpers'; +import { findClauseInExpression, findClauseInLibrary, findNamedClausesInExpression } from './elm/ELMHelpers'; const FHIR_QUERY_PATTERN_URL = 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-fhirQueryPattern'; /** @@ -423,7 +433,7 @@ function didEncounterDetailedValueFilterErrors(tbd: fhir4.Extension | GracefulEr // addMustSupport: find any fields as part of this statement.expression, // then search the allRetrieves for that field's context, and add the field to the correct retrieve's mustSupport function addMustSupport(allRetrieves: DataTypeQuery[], expression: AnyELMExpression, rootLib: ELM, allELM: ELM[]) { - const propertyExpressions = findPropertyExpressions(expression, [], rootLib.library.identifier.id); + const propertyExpressions = findPropertyExpressions(expression, [], rootLib, allELM); propertyExpressions.forEach(prop => { // find all matches for this property in allRetrieves @@ -441,7 +451,7 @@ function addMustSupport(allRetrieves: DataTypeQuery[], expression: AnyELMExpress }); } -interface PropertyTracker { +export interface PropertyTracker { property: ELMProperty; stack: ExpressionStackEntry[]; } @@ -452,62 +462,110 @@ interface PropertyTracker { * * @param exp the current expression (top node) of the tree to search for Properties * @param currentStack stack entries that led to this expression (not including this expression) - * @param lib name of library context for this expression + * @param lib library context for this expression + * @param allLib all elm libraries * @returns array of all properties found in this expression's tree */ -function findPropertyExpressions(exp: object, currentStack: ExpressionStackEntry[], lib: string): PropertyTracker[] { +export function findPropertyExpressions( + exp: object, + currentStack: ExpressionStackEntry[], + lib: ELM, + allLib: ELM[] +): PropertyTracker[] { + if (typeof exp !== 'object') { + return []; + } + // ... only do this for objects TODO next + const thisStackEntry: ExpressionStackEntry = { + type: 'type' in exp && exp.type ? (exp.type as string) : 'unknown', + localId: 'localId' in exp && exp.localId ? (exp.localId as string) : 'unknown', + libraryName: lib.library.identifier.id + }; + if ('type' in exp && exp.type && exp.type === 'Property') { // base case found property expression const prop = exp as ELMProperty; if (prop.source) { // add this expression to current stack before recursing on .source - const thisStackEntry: ExpressionStackEntry = { - type: exp.type, - localId: 'localId' in exp && exp.localId ? (exp.localId as string) : 'unknown', - libraryName: lib - }; return [ { property: prop, stack: currentStack }, - ...findPropertyExpressions( - prop.source, - currentStack.concat([thisStackEntry]), - checkLibChange(prop.source) ?? lib - ) + ...findPropertyExpressions(prop.source, currentStack.concat([thisStackEntry]), lib, allLib) ]; } else { return [{ property: prop, stack: currentStack }]; } - } else { - // not a property expression, recurse on all array members or all children values - return Object.values(exp).flatMap(v => { - const thisStackEntry: ExpressionStackEntry = { - type: 'type' in exp && exp.type ? (exp.type as string) : 'unknown', - localId: 'localId' in exp && exp.localId ? (exp.localId as string) : 'unknown', - libraryName: lib - }; - if (Array.isArray(v)) { - return v.flatMap(elem => - findPropertyExpressions(elem, currentStack.concat([thisStackEntry]), checkLibChange(elem) ?? lib) - ); - } else if (typeof v === 'object') { - return findPropertyExpressions(v, currentStack.concat([thisStackEntry]), checkLibChange(v) ?? lib); - } else { - return []; + } else if ( + 'type' in exp && + exp.type && + (exp.type === 'FunctionRef' || exp.type === 'ExpressionRef') && + 'libraryName' in exp && + exp.libraryName && + 'name' in exp && + exp.name + ) { + // handle references that go to different libraries + + // TODO: do we have to worry about ParameterRef as well? + // TODO: if there isn't a library name, are we good to not poke around for a new expression to explode? + + // TODO: do we need to search the entire operand tree, or is the top level okay? + if ('operand' in exp && exp.operand) { + const properties = findPropertyExpressions(exp.operand, currentStack.concat([thisStackEntry]), lib, allLib); + if (properties.length > 0) { + // if we find the property(s) in the operand, we can short-circuit the search without going to a different library + return properties; } - }); + } + + const newLib = findLibraryReference(lib, allLib, exp.libraryName as string); + if (!newLib) { + throw new UnexpectedResource(`Cannot Find Referenced Library: ${exp.libraryName}`); + } + const newExp = findNameinLib(exp.name as string, newLib); + if (!newExp) { + // If we can't uniquely identify the reference, warn and explode immediate expression in current library context + console.warn( + `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.` + ); + return findPropertyExpressions(exp, currentStack.concat([thisStackEntry]), lib, allLib); + } + return findPropertyExpressions(newExp, currentStack.concat([thisStackEntry]), newLib, allLib); + } else if (Array.isArray(exp)) { + return exp.flatMap(elem => findPropertyExpressions(elem, currentStack, lib, allLib)); + } else { + // non property object, recurse all children values + return findPropertyExpressions(Object.values(exp), currentStack.concat([thisStackEntry]), lib, allLib); } } -function checkLibChange(value: object): string | null { - // for ExpressionRef and FunctionRef we need the new library context - if ('libraryName' in value && value.libraryName) { - return value.libraryName as string; +// find the expression in this library that matches the passed name +// if there are issues uniquely identifying one, return null +export function findNameinLib(name: string, lib: ELM): AnyELMExpression | null { + // search statements first and return expression + const namedStatement = lib.library.statements.def.filter(statement => statement.name === name); + if (namedStatement.length > 0) { + if (namedStatement.length === 1) { + return namedStatement[0].expression; + } else { + // if multiple come up, then it's overloaded (we can't handle) + return null; + } } - return null; + + // search expressions in statements + const foundNames = lib.library.statements.def.flatMap(statement => + findNamedClausesInExpression(statement.expression, name) + ); + if (foundNames.length !== 1) { + // if multiple come up, then it's overloaded (we can't handle). If 0 come up, it's ill-formed. + return null; + } + // TODO: fix statement return (don't want to search annotations) + return foundNames[0]; } // search retrieves for any that match this property's stack and alias context -function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQuery[], allELM: ELM[]): DataTypeQuery[] { +export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQuery[], allELM: ELM[]): DataTypeQuery[] { return retrieves.filter(retrieve => { const stackMatch = prop.stack.findLast(ps => { // find the last property stack entry that matches any entry in the retrieve stack @@ -519,25 +577,19 @@ function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQuery[], if (stackMatch) { // find stackMatch in allELM const library = allELM.find(lib => lib.library.identifier.id === stackMatch.libraryName); - - // statement definition expression should match first of the stack - const topExpression = library?.library.statements.def.find( - d => d.expression.localId === prop.stack[0].localId - )?.expression; - if (!topExpression) { - throw Error(`Could not find expression ${prop.stack[0].localId} in library with id ${stackMatch.libraryName}`); + if (!library) { + throw new Error(`Could not find library with id ${stackMatch.libraryName}`); } - - const localExpression = findExpressionwithLocalId(topExpression, stackMatch.localId); - if (localExpression?.type === 'Query') { + const localExpression = findClauseInLibrary(library, stackMatch.localId); + if (!localExpression) { + throw new Error(`Could not find expression ${stackMatch.localId} in library with id ${stackMatch.libraryName}`); + } + // TODO: handle property source? Or I think if property has source, it's always irrelevant + if (localExpression?.type === 'Query' && prop.property.scope) { const query = localExpression as ELMQuery; // confirm alias matches scope - const source = query.source.find(s => s.alias === prop.property.scope); - if ( - source && - retrieve.retrieveLocalId && - findExpressionwithLocalId(source.expression, retrieve.retrieveLocalId) - ) { + const source = findSourcewithScope(query, prop.property.scope); + if (source && retrieve.retrieveLocalId && findClauseInExpression(source.expression, retrieve.retrieveLocalId)) { return true; } else { return false; @@ -554,29 +606,39 @@ function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQuery[], }); } -// exp is top expression with tree of children to search -function findExpressionwithLocalId(exp: object, localId: string): AnyELMExpression | undefined { - if ('localId' in exp && exp.localId && exp.localId === localId) { - return exp as AnyELMExpression; - } else { - let found; - for (let i = 0; i < Object.values(exp).length; i++) { - const v = Object.values(exp)[i]; - if (Array.isArray(v)) { - for (let i = 0; i < v.length; i++) { - const elem = v[i]; - found = findExpressionwithLocalId(elem, localId); - if (found) break; +// TODO: this is incomplete, needs more cases! and less strict type assumptions +export function findSourcewithScope( + exp: ELMQuery | ELMLast | ELMProperty, + scope: string +): ELMAliasedQuerySource | undefined { + let source; + if (exp.type === 'Query') { + source = exp.source?.find(s => s.alias === scope); + } else if (exp.type === 'Last') { + const lastQuery = exp.source as ELMQuery; //TODO: is this okay? -> no + source = lastQuery.source?.find(s => s.alias === scope); + } else if (exp.type === 'Property') { + // TODO: handle this case?? + } + if (!source) { + // also check any let expression sources + if ('let' in exp && exp.let) { + for (let i = 0; !source && i < exp.let.length; i++) { + const letClause = exp.let[i]; + if ('source' in letClause.expression && letClause.expression.source) { + source = findSourcewithScope(letClause.expression, scope); } - } else if (typeof v === 'object') { - found = findExpressionwithLocalId(v, localId); } - if (found) break; } - return found; } + // check across non-query types? + + return source; } // Special case TODO: function ref madness // Special case TODO 2: expression ref layers (pair and debug cases with Hoss) // Special case TODO 3: last of... means that matching up the source will require special handling + +// ... start making unit tests (cql to elm test data) +// 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) diff --git a/src/helpers/elm/ELMHelpers.ts b/src/helpers/elm/ELMHelpers.ts index 737f60a3..378a87ca 100644 --- a/src/helpers/elm/ELMHelpers.ts +++ b/src/helpers/elm/ELMHelpers.ts @@ -36,9 +36,28 @@ export function findClauseInExpression(expression: any, localId: string): ELMExp } } return null; - } else if (expression.localId == localId) { + } else if (expression.localId === localId) { return expression as ELMExpression; } else { return findClauseInExpression(Object.values(expression), localId); } } + +/** + * Recursively search an ELM tree for all expression (clause) with a given name + * + * @param expression The expression tree to search for the clause in. + * @param name The name to look for. + * @returns The expression if found or null. + */ +export function findNamedClausesInExpression(expression: any, name: string): ELMExpression[] { + if (typeof expression === 'string' || typeof expression === 'number' || typeof expression === 'boolean') { + return []; + } else if (Array.isArray(expression)) { + return expression.flatMap(elem => findNamedClausesInExpression(elem, name)); + } else if (expression.name === name) { + return [expression as ELMExpression]; + } else { + return findNamedClausesInExpression(Object.values(expression), name); + } +} diff --git a/test/unit/DataRequirementHelpers.test.ts b/test/unit/DataRequirementHelpers.test.ts index 21c01ce4..b12f6642 100644 --- a/test/unit/DataRequirementHelpers.test.ts +++ b/test/unit/DataRequirementHelpers.test.ts @@ -4,6 +4,7 @@ import { CalculationOptions, DataTypeQuery } from '../../src/types/Calculator'; import { DataRequirement } from 'fhir/r4'; import { DateTime, Interval } from 'cql-execution'; import moment from 'moment'; +import { ELM, ELMAliasedQuerySource, ELMQuery } from '../../src'; describe('DataRequirementHelpers', () => { describe('generateDataRequirement', () => { @@ -308,4 +309,239 @@ describe('DataRequirementHelpers', () => { ); }); }); + describe('findPropertyExpressions', () => { + test('find properties in array', () => { + const expression = { + type: 'anyType', + localId: '0', + anyField: [ + { + localId: '1', + type: 'Property' + }, + { + localId: '2', + type: 'Other' + }, + { + localId: '3', + type: 'Property' + } + ] + }; + const elm: ELM = { + library: { + identifier: { + id: 'libraryId', + version: 'libraryVersion' + }, + schemaIdentifier: { + id: 'schemaId', + version: 'schemaVersion' + }, + usings: {}, + statements: { + def: [ + { + name: 'testStatement', + context: 'Patient', + expression: expression + } + ] + } + } + }; + const propertyExpressions = DataRequirementHelpers.findPropertyExpressions(expression, [], elm, [elm]); + const expectedPropertyExpressions = [ + { + property: { + localId: '1', + type: 'Property' + }, + stack: [ + { + type: 'anyType', + localId: '0', + libraryName: 'libraryId' + } + ] + }, + { + property: { + localId: '3', + type: 'Property' + }, + stack: [ + { + type: 'anyType', + localId: '0', + libraryName: 'libraryId' + } + ] + } + ]; + + expect(propertyExpressions).toEqual(expectedPropertyExpressions); + }); + + test('find properties in object', () => { + const expression = { + type: 'anyType', + localId: '0', + anyField1: { + localId: '1', + type: 'Property' + }, + anyField2: { + localId: '2', + type: 'Other' + }, + anyField3: { + localId: '3', + type: 'Property' + } + }; + const elm: ELM = { + library: { + identifier: { + id: 'libraryId', + version: 'libraryVersion' + }, + schemaIdentifier: { + id: 'schemaId', + version: 'schemaVersion' + }, + usings: {}, + statements: { + def: [ + { + name: 'testStatement', + context: 'Patient', + expression: expression + } + ] + } + } + }; + const propertyExpressions = DataRequirementHelpers.findPropertyExpressions(expression, [], elm, [elm]); + const expectedPropertyExpressions = [ + { + property: { + localId: '1', + type: 'Property' + }, + stack: [ + { + type: 'anyType', + localId: '0', + libraryName: 'libraryId' + } + ] + }, + { + property: { + localId: '3', + type: 'Property' + }, + stack: [ + { + type: 'anyType', + localId: '0', + libraryName: 'libraryId' + } + ] + } + ]; + + expect(propertyExpressions).toEqual(expectedPropertyExpressions); + }); + }); + + describe('findRetrieveMatches', () => { + test('simple retrieve match', () => { + const stack = [ + { + type: 'Query', + localId: '0', + libraryName: 'libraryId' + } + ]; + const retrieve: DataTypeQuery = { + dataType: 'fhir_type', + path: 'status', + templateId: 'http://hl7.org/fhir/StructureDefinition/fhir_type', + expressionStack: stack, + retrieveLocalId: '1' + }; + const property: DataRequirementHelpers.PropertyTracker = { + property: { + localId: '2', + type: 'Property', + path: 'status', + scope: 'TestScope' + }, + stack: stack + }; + const expression: ELMQuery = { + type: 'Query', + localId: '0', + source: [ + { + alias: 'TestScope', + expression: { + type: 'Retrieve', + localId: '1' + } + } + ], + relationship: [], + where: property.property + }; + const elm: ELM = { + library: { + identifier: { + id: 'libraryId', + version: 'libraryVersion' + }, + schemaIdentifier: { + id: 'schemaId', + version: 'schemaVersion' + }, + usings: {}, + statements: { + def: [ + { + name: 'testStatement', + context: 'Patient', + expression: expression + } + ] + } + } + }; + const expectedRetrieveMatches = [retrieve]; + const retrieveMatches = DataRequirementHelpers.findRetrieveMatches(property, [retrieve], [elm]); + expect(retrieveMatches).toEqual(expectedRetrieveMatches); + }); + }); + + describe('findSourceWithScope', () => { + test('simple source', () => { + const source: ELMAliasedQuerySource = { + expression: { + type: 'AnyType', + localId: '1' + }, + alias: 'testScope' + }; + const expression: ELMQuery = { + type: 'Query', + localId: '0', + relationship: [], + source: [source] + }; + + expect(DataRequirementHelpers.findSourcewithScope(expression, 'testScope')).toEqual(source); + }); + }); }); From 4e273e9ada068800ee9d563087e2639d98f8f2a4 Mon Sep 17 00:00:00 2001 From: lmd59 Date: Fri, 22 Mar 2024 10:50:27 -0400 Subject: [PATCH 03/14] allow finding reference in current library --- src/helpers/DataRequirementHelpers.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index 98bc5179..8cfb84db 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -498,17 +498,12 @@ export function findPropertyExpressions( 'type' in exp && exp.type && (exp.type === 'FunctionRef' || exp.type === 'ExpressionRef') && - 'libraryName' in exp && - exp.libraryName && 'name' in exp && exp.name ) { // handle references that go to different libraries // TODO: do we have to worry about ParameterRef as well? - // TODO: if there isn't a library name, are we good to not poke around for a new expression to explode? - - // TODO: do we need to search the entire operand tree, or is the top level okay? if ('operand' in exp && exp.operand) { const properties = findPropertyExpressions(exp.operand, currentStack.concat([thisStackEntry]), lib, allLib); if (properties.length > 0) { @@ -516,10 +511,15 @@ export function findPropertyExpressions( return properties; } } - - const newLib = findLibraryReference(lib, allLib, exp.libraryName as string); - if (!newLib) { - throw new UnexpectedResource(`Cannot Find Referenced Library: ${exp.libraryName}`); + let newLib; + // find new lib if libraryName exists, otherwise use current lib + if ('libraryName' in exp && exp.libraryName) { + newLib = findLibraryReference(lib, allLib, exp.libraryName as string); + if (!newLib) { + throw new UnexpectedResource(`Cannot Find Referenced Library: ${exp.libraryName}`); + } + } else { + newLib = lib; } const newExp = findNameinLib(exp.name as string, newLib); if (!newExp) { @@ -527,7 +527,7 @@ export function findPropertyExpressions( console.warn( `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.` ); - return findPropertyExpressions(exp, currentStack.concat([thisStackEntry]), lib, allLib); + return findPropertyExpressions(Object.values(exp), currentStack.concat([thisStackEntry]), lib, allLib); } return findPropertyExpressions(newExp, currentStack.concat([thisStackEntry]), newLib, allLib); } else if (Array.isArray(exp)) { From dac6340d8019c98b66109b6c1502d7b88cea6187 Mon Sep 17 00:00:00 2001 From: lmd59 Date: Fri, 22 Mar 2024 13:08:35 -0400 Subject: [PATCH 04/14] Add statements to property stack and handle basic function refs --- src/helpers/DataRequirementHelpers.ts | 69 ++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index 8cfb84db..832d393b 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -11,7 +11,8 @@ import { ELMIdentifier, ELMLast, ELMProperty, - ELMQuery + ELMQuery, + ELMStatement } from '../types/ELMTypes'; import { ExtractedLibrary } from '../types/CQLTypes'; import * as Execution from '../execution/Execution'; @@ -84,7 +85,7 @@ export async function getDataRequirements( // add must supports rootLib.library.statements.def.forEach(statement => { if (statement.expression && statement.name != 'Patient') { - addMustSupport(allRetrieves, statement.expression, rootLib, elmJSONs); + addMustSupport(allRetrieves, statement, rootLib, elmJSONs); } }); @@ -432,8 +433,8 @@ function didEncounterDetailedValueFilterErrors(tbd: fhir4.Extension | GracefulEr // addMustSupport: find any fields as part of this statement.expression, // then search the allRetrieves for that field's context, and add the field to the correct retrieve's mustSupport -function addMustSupport(allRetrieves: DataTypeQuery[], expression: AnyELMExpression, rootLib: ELM, allELM: ELM[]) { - const propertyExpressions = findPropertyExpressions(expression, [], rootLib, allELM); +function addMustSupport(allRetrieves: DataTypeQuery[], statement: ELMStatement, rootLib: ELM, allELM: ELM[]) { + const propertyExpressions = findPropertyExpressions(statement, [], rootLib, allELM); propertyExpressions.forEach(prop => { // find all matches for this property in allRetrieves @@ -540,12 +541,12 @@ export function findPropertyExpressions( // find the expression in this library that matches the passed name // if there are issues uniquely identifying one, return null -export function findNameinLib(name: string, lib: ELM): AnyELMExpression | null { +export function findNameinLib(name: string, lib: ELM): AnyELMExpression | ELMStatement | null { // search statements first and return expression const namedStatement = lib.library.statements.def.filter(statement => statement.name === name); if (namedStatement.length > 0) { if (namedStatement.length === 1) { - return namedStatement[0].expression; + return namedStatement[0]; } else { // if multiple come up, then it's overloaded (we can't handle) return null; @@ -566,6 +567,16 @@ export function findNameinLib(name: string, lib: ELM): AnyELMExpression | null { // search retrieves for any that match this property's stack and alias context export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQuery[], allELM: ELM[]): DataTypeQuery[] { + // basic checks that the property is matchable + if (prop.property.source) { + // source must have operandref and name (TODO: check this is true) + if (prop.property.source.type !== 'OperandRef' || !('name' in prop.property.source) || !prop.property.source.name) + return []; + } else { + // must have either source or scope + if (!prop.property.scope) return []; + } + return retrieves.filter(retrieve => { const stackMatch = prop.stack.findLast(ps => { // find the last property stack entry that matches any entry in the retrieve stack @@ -584,15 +595,40 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu if (!localExpression) { throw new Error(`Could not find expression ${stackMatch.localId} in library with id ${stackMatch.libraryName}`); } - // TODO: handle property source? Or I think if property has source, it's always irrelevant - if (localExpression?.type === 'Query' && prop.property.scope) { + if (localExpression?.type === 'Query') { const query = localExpression as ELMQuery; - // confirm alias matches scope - const source = findSourcewithScope(query, prop.property.scope); - if (source && retrieve.retrieveLocalId && findClauseInExpression(source.expression, retrieve.retrieveLocalId)) { + if (prop.property.scope) { + // confirm alias matches scope + const source = findSourcewithScope(query, prop.property.scope); + return ( + source && retrieve.retrieveLocalId && findClauseInExpression(source.expression, retrieve.retrieveLocalId) + ); + } else if (prop.property.source && 'name' in prop.property.source && prop.property.source.name) { + // assume property.source + + // traverse from end of the stack to check all function definitions use retrieve resource as operand + for (let i = prop.stack.length - 1; prop.stack[i].localId !== localExpression.localId; i--) { + // console.log(scores[i]); + if (prop.stack[i].type === 'FunctionDef') { + const lib = allELM.find(e => e.library.identifier.id === prop.stack[i].libraryName); + const functionStatement = lib?.library.statements.def.find(s => s.localId === prop.stack[i].localId); + if (!functionStatement) { + throw Error( + `Unable to find function definition statement with localId ${prop.stack[i].localId} in library ${prop.stack[i].libraryName}` + ); + } + if (!checkFunctionDefMatch(functionStatement, prop.property.source.name)) { + return false; + } + } + } + return true; } else { - return false; + // should never hit this case based on earlier checks + throw Error( + 'Property scope or source name has already been checked, but is still not found. Something is wrong with the logic.' + ); } } else { // TODO: handle other types @@ -606,6 +642,15 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu }); } +// return true if function def operand matches the passed source name +export function checkFunctionDefMatch(statement: ELMStatement, name: string) { + if (Array.isArray(statement.operand)) { + return statement.operand.find(o => 'name' in o && o.name && o.name === name); + } else { + return 'name' in statement.operand && statement.operand.name && statement.operand.name === name; + } +} + // TODO: this is incomplete, needs more cases! and less strict type assumptions export function findSourcewithScope( exp: ELMQuery | ELMLast | ELMProperty, From 6cae5239d0ef48f087040b987b74eb8a5485e001 Mon Sep 17 00:00:00 2001 From: lmd59 Date: Mon, 25 Mar 2024 12:21:30 -0400 Subject: [PATCH 05/14] checkStackFunctionDefs small refactor --- src/helpers/DataRequirementHelpers.ts | 51 ++++++++++++++------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index 832d393b..c5268443 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -603,32 +603,9 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu return ( source && retrieve.retrieveLocalId && findClauseInExpression(source.expression, retrieve.retrieveLocalId) ); - } else if (prop.property.source && 'name' in prop.property.source && prop.property.source.name) { - // assume property.source - - // traverse from end of the stack to check all function definitions use retrieve resource as operand - for (let i = prop.stack.length - 1; prop.stack[i].localId !== localExpression.localId; i--) { - // console.log(scores[i]); - if (prop.stack[i].type === 'FunctionDef') { - const lib = allELM.find(e => e.library.identifier.id === prop.stack[i].libraryName); - const functionStatement = lib?.library.statements.def.find(s => s.localId === prop.stack[i].localId); - if (!functionStatement) { - throw Error( - `Unable to find function definition statement with localId ${prop.stack[i].localId} in library ${prop.stack[i].libraryName}` - ); - } - if (!checkFunctionDefMatch(functionStatement, prop.property.source.name)) { - return false; - } - } - } - - return true; } else { - // should never hit this case based on earlier checks - throw Error( - 'Property scope or source name has already been checked, but is still not found. Something is wrong with the logic.' - ); + // assume property.source (checked above) + return checkStackFunctionDefs(prop, stackMatch.localId, allELM); } } else { // TODO: handle other types @@ -642,6 +619,30 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu }); } +// traverse from end of the stack to check all function definitions use retrieve resource as operand +export function checkStackFunctionDefs(prop: PropertyTracker, matchId: string, allELM: ELM[]) { + if (prop.property.source && 'name' in prop.property.source && prop.property.source.name) { + for (let i = prop.stack.length - 1; prop.stack[i].localId !== matchId; i--) { + // console.log(scores[i]); + if (prop.stack[i].type === 'FunctionDef') { + const lib = allELM.find(e => e.library.identifier.id === prop.stack[i].libraryName); + const functionStatement = lib?.library.statements.def.find(s => s.localId === prop.stack[i].localId); + if (!functionStatement) { + throw Error( + `Unable to find function definition statement with localId ${prop.stack[i].localId} in library ${prop.stack[i].libraryName}` + ); + } + if (!checkFunctionDefMatch(functionStatement, prop.property.source.name)) { + return false; + } + } + } + return true; + } + // stack can only be searched if we have a source with a name + return false; +} + // return true if function def operand matches the passed source name export function checkFunctionDefMatch(statement: ELMStatement, name: string) { if (Array.isArray(statement.operand)) { From af67a2321c1901813aca01fca5b18eebff8924ef Mon Sep 17 00:00:00 2001 From: lmd59 Date: Wed, 27 Mar 2024 12:55:13 -0400 Subject: [PATCH 06/14] Traverse stack for scope in query add main code path make sure stack match is stopped by expression types that pull stack out of the current retrieve context --- src/helpers/DataRequirementHelpers.ts | 165 +++++++++++++++----------- 1 file changed, 94 insertions(+), 71 deletions(-) diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index c5268443..32b13f99 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -82,7 +82,17 @@ export async function getDataRequirements( await Promise.all(allRetrievesPromises); - // add must supports + // add main code path as a mustSupport + allRetrieves.forEach(retrieve => { + if (retrieve.path) { + if (retrieve.mustSupport) { + retrieve.mustSupport?.push(retrieve.path); + } else { + retrieve.mustSupport = [retrieve.path]; + } + } + }); + // add property must supports rootLib.library.statements.def.forEach(statement => { if (statement.expression && statement.name != 'Patient') { addMustSupport(allRetrieves, statement, rootLib, elmJSONs); @@ -584,102 +594,115 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu rs => isEqual(ps, rs) //test object equality ); }); + // TODO: any other things that fall into this category Or/And/Exists + if (stackMatch?.type === 'Or' || stackMatch?.type === 'And' || stackMatch?.type === 'Exists') { + return false; + } if (stackMatch) { - // find stackMatch in allELM - const library = allELM.find(lib => lib.library.identifier.id === stackMatch.libraryName); - if (!library) { - throw new Error(`Could not find library with id ${stackMatch.libraryName}`); - } - const localExpression = findClauseInLibrary(library, stackMatch.localId); - if (!localExpression) { - throw new Error(`Could not find expression ${stackMatch.localId} in library with id ${stackMatch.libraryName}`); - } - if (localExpression?.type === 'Query') { - const query = localExpression as ELMQuery; - if (prop.property.scope) { + const matchIdx = prop.stack.findIndex(s => s.localId === stackMatch.localId); + if (prop.property.scope) { + // travel the stack looking for nearest queries (limited by stackMatch) + const scopedQuery = findStackScopedQuery(prop.stack.slice(matchIdx), prop.property.scope, allELM); + + if (!scopedQuery) return false; + const { query, position, source } = scopedQuery; + // if the query is our stackMatch, stop here, otherwise continue with query source + if (position === 0) { // confirm alias matches scope - const source = findSourcewithScope(query, prop.property.scope); return ( source && retrieve.retrieveLocalId && findClauseInExpression(source.expression, retrieve.retrieveLocalId) ); - } else { - // assume property.source (checked above) - return checkStackFunctionDefs(prop, stackMatch.localId, allELM); } + if ('name' in source.expression && source.expression.name) { + return checkStackFunctionDefs( + prop.stack.slice(matchIdx, matchIdx + position), + source.expression.name, + allELM + ); + } + return false; } else { - // TODO: handle other types - // - TODO: what if no source, i.e. 160 - // TODO: will this always be a query? What else? If not, what's our source for alias matching? - // ... could be a last or first + // assume property.source (checked above) + if (prop.property.source && 'name' in prop.property.source && prop.property.source.name) { + // slice property stack from stackMatch id to end + return checkStackFunctionDefs(prop.stack.slice(matchIdx), prop.property.source.name, allELM); + } return false; } } - return false; }); } -// traverse from end of the stack to check all function definitions use retrieve resource as operand -export function checkStackFunctionDefs(prop: PropertyTracker, matchId: string, allELM: ELM[]) { - if (prop.property.source && 'name' in prop.property.source && prop.property.source.name) { - for (let i = prop.stack.length - 1; prop.stack[i].localId !== matchId; i--) { - // console.log(scores[i]); - if (prop.stack[i].type === 'FunctionDef') { - const lib = allELM.find(e => e.library.identifier.id === prop.stack[i].libraryName); - const functionStatement = lib?.library.statements.def.find(s => s.localId === prop.stack[i].localId); - if (!functionStatement) { - throw Error( - `Unable to find function definition statement with localId ${prop.stack[i].localId} in library ${prop.stack[i].libraryName}` - ); - } - if (!checkFunctionDefMatch(functionStatement, prop.property.source.name)) { - return false; - } +// traverse from end of the stack to find the nearest query that has alias labeled with the passed scope +export function findStackScopedQuery(stack: ExpressionStackEntry[], scope: string, allELM: ELM[]) { + for (let i = stack.length - 1; i >= 0; i--) { + // ... should stop if it sees an expression ref + if (stack[i].type === 'Query') { + // 1..* --> source : AliasedQuerySource... alias + // ¦ + // 0..* --> let : LetClause ...identifier queryletref + // ¦ + // 0..* --> relationship : RelationshipClause ...suchThat + if (stack[i].localId === 'unknown' || stack[i].libraryName === 'unknown') { + // TODO: how do we handle this case (example AI&F query below localId 180 has no localId) + continue; + } + const queryExpression = expressionFromStackEntry(stack[i], allELM); + const source = queryExpression.source.find(s => s.alias === scope); + if (source) { + return { query: queryExpression, position: i, source: source }; } } - return true; } - // stack can only be searched if we have a source with a name - return false; + return null; } -// return true if function def operand matches the passed source name -export function checkFunctionDefMatch(statement: ELMStatement, name: string) { - if (Array.isArray(statement.operand)) { - return statement.operand.find(o => 'name' in o && o.name && o.name === name); - } else { - return 'name' in statement.operand && statement.operand.name && statement.operand.name === name; +// Pull actual expression from stack entry information. Assumes stack entry information exists in libraries (otherwise error) +export function expressionFromStackEntry(stackEntry: ExpressionStackEntry, allELM: ELM[]) { + const lib = allELM.find(e => e.library.identifier.id === stackEntry.libraryName); + if (!lib) { + throw Error(`Could not find library with identifier ${stackEntry.libraryName}`); + } + const expression = findClauseInLibrary(lib, stackEntry.localId) as ELMQuery; + if (!expression) { + throw Error( + `Could not find ${stackEntry.type} type expression in ${stackEntry.libraryName} with localId ${stackEntry.localId}` + ); } + return expression; } -// TODO: this is incomplete, needs more cases! and less strict type assumptions -export function findSourcewithScope( - exp: ELMQuery | ELMLast | ELMProperty, - scope: string -): ELMAliasedQuerySource | undefined { - let source; - if (exp.type === 'Query') { - source = exp.source?.find(s => s.alias === scope); - } else if (exp.type === 'Last') { - const lastQuery = exp.source as ELMQuery; //TODO: is this okay? -> no - source = lastQuery.source?.find(s => s.alias === scope); - } else if (exp.type === 'Property') { - // TODO: handle this case?? - } - if (!source) { - // also check any let expression sources - if ('let' in exp && exp.let) { - for (let i = 0; !source && i < exp.let.length; i++) { - const letClause = exp.let[i]; - if ('source' in letClause.expression && letClause.expression.source) { - source = findSourcewithScope(letClause.expression, scope); - } +// traverse from end of the stack to check all function definitions use name in operand +// TODO: need to traverse from end in order to change approach partway up, or can we traverse from beginning? +export function checkStackFunctionDefs(stack: ExpressionStackEntry[], name: string, allELM: ELM[]) { + // return false if no function defs + if (!stack.find(s => s.type === 'FunctionDef')) return false; + + for (let i = stack.length - 1; i >= 0; i--) { + if (stack[i].type === 'FunctionDef') { + const lib = allELM.find(e => e.library.identifier.id === stack[i].libraryName); + const functionStatement = lib?.library.statements.def.find(s => s.localId === stack[i].localId); + if (!functionStatement) { + throw Error( + `Unable to find function definition statement with localId ${stack[i].localId} in library ${stack[i].libraryName}` + ); + } + if (!checkFunctionDefMatch(functionStatement, name)) { + return false; } } } - // check across non-query types? + return true; +} - return source; +// return true if function def operand matches the passed source name +export function checkFunctionDefMatch(statement: ELMStatement, name: string) { + if (Array.isArray(statement.operand)) { + return statement.operand.find(o => 'name' in o && o.name && o.name === name); + } else { + return 'name' in statement.operand && statement.operand.name && statement.operand.name === name; + } } // Special case TODO: function ref madness From 7f6cc10a344af9e3555e2e72975ca6b05f0dbc9e Mon Sep 17 00:00:00 2001 From: lmd59 Date: Thu, 4 Apr 2024 09:58:43 -0400 Subject: [PATCH 07/14] Fix matching, add let and relationship --- src/helpers/DataRequirementHelpers.ts | 17 +++++++++++------ src/helpers/elm/QueryFilterParser.ts | 2 +- src/types/ELMTypes.ts | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index 32b13f99..1d2d9214 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -600,7 +600,9 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu } if (stackMatch) { - const matchIdx = prop.stack.findIndex(s => s.localId === stackMatch.localId); + const matchIdx = prop.stack.findIndex( + s => s.localId === stackMatch.localId && s.libraryName === stackMatch.libraryName + ); if (prop.property.scope) { // travel the stack looking for nearest queries (limited by stackMatch) const scopedQuery = findStackScopedQuery(prop.stack.slice(matchIdx), prop.property.scope, allELM); @@ -639,11 +641,6 @@ export function findStackScopedQuery(stack: ExpressionStackEntry[], scope: strin for (let i = stack.length - 1; i >= 0; i--) { // ... should stop if it sees an expression ref if (stack[i].type === 'Query') { - // 1..* --> source : AliasedQuerySource... alias - // ¦ - // 0..* --> let : LetClause ...identifier queryletref - // ¦ - // 0..* --> relationship : RelationshipClause ...suchThat if (stack[i].localId === 'unknown' || stack[i].libraryName === 'unknown') { // TODO: how do we handle this case (example AI&F query below localId 180 has no localId) continue; @@ -653,6 +650,14 @@ export function findStackScopedQuery(stack: ExpressionStackEntry[], scope: strin if (source) { return { query: queryExpression, position: i, source: source }; } + const letClause = queryExpression.let?.find(lc => lc.identifier === scope); + if (letClause) { + return { query: queryExpression, position: i, source: letClause }; + } + const relationshipClause = queryExpression.relationship?.find(r => r.alias === scope); + if (relationshipClause) { + return { query: queryExpression, position: i, source: relationshipClause }; + } } } return null; diff --git a/src/helpers/elm/QueryFilterParser.ts b/src/helpers/elm/QueryFilterParser.ts index e6e1664f..df32f5e7 100644 --- a/src/helpers/elm/QueryFilterParser.ts +++ b/src/helpers/elm/QueryFilterParser.ts @@ -233,7 +233,7 @@ function replaceAliasesInFilters(filter: AnyFilter, match: string, replace: stri */ function parseSources(query: ELMQuery): SourceInfo[] { const sources: SourceInfo[] = []; - const querySources = [...query.source, ...query.relationship]; + const querySources = [...query.source, ...(query.relationship || [])]; querySources.forEach(source => { if (source.expression.type == 'Retrieve') { diff --git a/src/types/ELMTypes.ts b/src/types/ELMTypes.ts index 3fca2c6c..814ad284 100644 --- a/src/types/ELMTypes.ts +++ b/src/types/ELMTypes.ts @@ -234,7 +234,7 @@ export interface ELMQuery extends ELMExpression { type: 'Query'; source: ELMAliasedQuerySource[]; let?: ELMLetClause[]; - relationship: ELMRelationshipClause[]; + relationship?: ELMRelationshipClause[]; where?: AnyELMExpression; return?: ELMReturnClause; sort?: any; From 13f0af681f72c6d542701abe55256ba5da0f8f1b Mon Sep 17 00:00:00 2001 From: lmd59 Date: Tue, 16 Apr 2024 09:51:22 -0400 Subject: [PATCH 08/14] Match retrieve to source without retrieve localId --- src/helpers/DataRequirementHelpers.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index 1d2d9214..5483df13 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -611,9 +611,11 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu const { query, position, source } = scopedQuery; // if the query is our stackMatch, stop here, otherwise continue with query source if (position === 0) { - // confirm alias matches scope + // confirm alias matches scope (i.e. the retrieve is somewhere within the source expression tree) return ( - source && retrieve.retrieveLocalId && findClauseInExpression(source.expression, retrieve.retrieveLocalId) + source && + source.expression.localId && + retrieve.expressionStack?.find(st => st.localId === source.expression.localId) ); } if ('name' in source.expression && source.expression.name) { From dfcb4a5f2cb1d4ba61f8054917aacd4b8149ce8d Mon Sep 17 00:00:00 2001 From: lmd59 Date: Tue, 16 Apr 2024 14:18:46 -0400 Subject: [PATCH 09/14] Exception to property finding shortcut for id property --- src/helpers/DataRequirementHelpers.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index 5483df13..6f046b59 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -515,11 +515,16 @@ export function findPropertyExpressions( // handle references that go to different libraries // TODO: do we have to worry about ParameterRef as well? + let operandProperties: PropertyTracker[] = []; if ('operand' in exp && exp.operand) { - const properties = findPropertyExpressions(exp.operand, currentStack.concat([thisStackEntry]), lib, allLib); - if (properties.length > 0) { - // if we find the property(s) in the operand, we can short-circuit the search without going to a different library - return properties; + operandProperties = findPropertyExpressions(exp.operand, currentStack.concat([thisStackEntry]), lib, allLib); + if (operandProperties.length > 0) { + const idProperty = operandProperties.find(p => p.property.path === 'id'); + if (!idProperty) { + // if we find the property(s) in the operand, we can short-circuit the search without going to a different library + // unless it's an "id" property, in which case it may just be a reference finding operation + return operandProperties; + } } } let newLib; @@ -540,7 +545,9 @@ export function findPropertyExpressions( ); return findPropertyExpressions(Object.values(exp), currentStack.concat([thisStackEntry]), lib, allLib); } - return findPropertyExpressions(newExp, currentStack.concat([thisStackEntry]), newLib, allLib); + return operandProperties.concat( + findPropertyExpressions(newExp, currentStack.concat([thisStackEntry]), newLib, allLib) + ); } else if (Array.isArray(exp)) { return exp.flatMap(elem => findPropertyExpressions(elem, currentStack, lib, allLib)); } else { From c4a64394f39526e8a21ed6398e7726247cb1a1e5 Mon Sep 17 00:00:00 2001 From: lmd59 Date: Thu, 18 Apr 2024 13:27:03 -0400 Subject: [PATCH 10/14] Small optimizations and clarifications --- src/helpers/DataRequirementHelpers.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index 6f046b59..f7e2dfd1 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -104,6 +104,14 @@ export async function getDataRequirements( type: { coding: [{ code: 'module-definition', system: 'http://terminology.hl7.org/CodeSystem/library-type' }] }, status: 'unknown' }; + // TODO: combine must supports if there are data requirements from the same retrieve that have different mustSupports + // combine based on resourcetype/primary code path + // allRetrieves[0].templateId + // allRetrieves[0].dataType + // allRetrieves[0].path + // allRetrieves[0].code or allRetrieves[0].valueSet + // ^if these 4 things are the same, then it is the same data requirement and we can combine mustSupports + results.dataRequirement = uniqBy( allRetrieves.map(retrieve => { const dr = generateDataRequirement(retrieve); @@ -601,12 +609,11 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu rs => isEqual(ps, rs) //test object equality ); }); - // TODO: any other things that fall into this category Or/And/Exists - if (stackMatch?.type === 'Or' || stackMatch?.type === 'And' || stackMatch?.type === 'Exists') { - return false; - } if (stackMatch) { + if (stackMatch?.type === 'Or' || stackMatch?.type === 'And' || stackMatch?.type === 'Exists') { + return false; + } const matchIdx = prop.stack.findIndex( s => s.localId === stackMatch.localId && s.libraryName === stackMatch.libraryName ); @@ -622,7 +629,7 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu return ( source && source.expression.localId && - retrieve.expressionStack?.find(st => st.localId === source.expression.localId) + !!retrieve.expressionStack?.find(st => st.localId === source.expression.localId) ); } if ('name' in source.expression && source.expression.name) { @@ -642,6 +649,7 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu return false; } } + return false; }); } From 3ebcd3e884a740522259ffca8a4c0f80e766e23f Mon Sep 17 00:00:00 2001 From: lmd59 Date: Fri, 26 Apr 2024 16:15:15 -0400 Subject: [PATCH 11/14] Combine mustSupports from same retrieve , compare alias references, and traverse across functions keeping track of the correct name --- src/helpers/DataRequirementHelpers.ts | 91 +++++++++++++++++++++------ 1 file changed, 71 insertions(+), 20 deletions(-) diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index f7e2dfd1..6bff3aea 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -8,6 +8,7 @@ import { AnyELMExpression, ELM, ELMAliasedQuerySource, + ELMFunctionRef, ELMIdentifier, ELMLast, ELMProperty, @@ -25,7 +26,7 @@ import { parseQueryInfo } from './elm/QueryFilterParser'; import * as RetrievesHelper from './elm/RetrievesHelper'; -import { uniqBy, isEqual } from 'lodash'; +import { uniqBy, isEqual, union } from 'lodash'; import { DateTime, Interval } from 'cql-execution'; import { parseTimeStringAsUTC } from '../execution/ValueSetHelper'; import * as MeasureBundleHelpers from './MeasureBundleHelpers'; @@ -104,16 +105,28 @@ export async function getDataRequirements( type: { coding: [{ code: 'module-definition', system: 'http://terminology.hl7.org/CodeSystem/library-type' }] }, status: 'unknown' }; - // TODO: combine must supports if there are data requirements from the same retrieve that have different mustSupports - // combine based on resourcetype/primary code path - // allRetrieves[0].templateId - // allRetrieves[0].dataType - // allRetrieves[0].path - // allRetrieves[0].code or allRetrieves[0].valueSet - // ^if these 4 things are the same, then it is the same data requirement and we can combine mustSupports + + // Combine must supports if there are data requirements from the same retrieve that have different mustSupports + // Combine based on this set defining uniqueness: templateId, dataType, path, and code/valueSet + const retrievesHash = allRetrieves.reduce((hash: Record, retrieve) => { + const hashKey = `${retrieve.templateId}-${retrieve.dataType}-${retrieve.path}-${ + retrieve.code?.code || retrieve.valueSet + }`; + if (hash[hashKey]) { + if (hash[hashKey].mustSupport) { + // combine + hash[hashKey].mustSupport = union(hash[hashKey].mustSupport, retrieve.mustSupport); + } else { + hash[hashKey].mustSupport = retrieve.mustSupport; + } + } else { + hash[hashKey] = retrieve; + } + return hash; + }, {}); results.dataRequirement = uniqBy( - allRetrieves.map(retrieve => { + Object.values(retrievesHash).map(retrieve => { const dr = generateDataRequirement(retrieve); addFiltersToDataRequirement(retrieve, dr, withErrors); addFhirQueryPatternToDataRequirements(dr); @@ -129,7 +142,7 @@ export async function getDataRequirements( cql: cqls, elm: elmJSONs, gaps: { - retrieves: allRetrieves + retrieves: Object.values(retrievesHash) } }, withErrors @@ -625,7 +638,7 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu const { query, position, source } = scopedQuery; // if the query is our stackMatch, stop here, otherwise continue with query source if (position === 0) { - // confirm alias matches scope (i.e. the retrieve is somewhere within the source expression tree) + // 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 return ( source && source.expression.localId && @@ -633,7 +646,8 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu ); } if ('name' in source.expression && source.expression.name) { - return checkStackFunctionDefs( + // TODO: do we need any further checks here with the highest reference name? + return !!checkStackFunctionDefs( prop.stack.slice(matchIdx, matchIdx + position), source.expression.name, allELM @@ -644,7 +658,34 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu // assume property.source (checked above) if (prop.property.source && 'name' in prop.property.source && prop.property.source.name) { // slice property stack from stackMatch id to end - return checkStackFunctionDefs(prop.stack.slice(matchIdx), prop.property.source.name, allELM); + const stackSlice = prop.stack.slice(matchIdx); + const checkName = checkStackFunctionDefs(stackSlice, prop.property.source.name, allELM); + if (checkName) { + // get highest FunctionRef in the stack + // check that the operand has an AliasRef type expression with a name equal to... + // top alias in the retrieve stack (up to stackMatch) -> BipolarDiagnosis vs QualifyingEncounter + // const funcRefStackEntry = stackSlice.find(se => se.type === 'FunctionRef'); + // if (!funcRefStackEntry) return true; + // const aliasExp = (expressionFromStackEntry(funcRefStackEntry, allELM) as ELMFunctionRef).operand.find( + // o => o.type === 'AliasRef' + // ); + + const retMatchIdx = retrieve.expressionStack?.findIndex( + s => s.localId === stackMatch.localId && s.libraryName === stackMatch.libraryName + ); + const retSlice = retrieve.expressionStack?.slice(retMatchIdx); + const topAlias = retSlice + ?.map(se => { + const exp = expressionFromStackEntry(stackMatch, allELM); + return 'alias' in exp && exp.alias ? exp.alias : undefined; + }) + .find(a => !!a); + + if (topAlias) { + return topAlias === checkName; + } + return true; + } } return false; } @@ -662,7 +703,7 @@ export function findStackScopedQuery(stack: ExpressionStackEntry[], scope: strin // TODO: how do we handle this case (example AI&F query below localId 180 has no localId) continue; } - const queryExpression = expressionFromStackEntry(stack[i], allELM); + const queryExpression = expressionFromStackEntry(stack[i], allELM) as ELMQuery; const source = queryExpression.source.find(s => s.alias === scope); if (source) { return { query: queryExpression, position: i, source: source }; @@ -686,7 +727,7 @@ export function expressionFromStackEntry(stackEntry: ExpressionStackEntry, allEL if (!lib) { throw Error(`Could not find library with identifier ${stackEntry.libraryName}`); } - const expression = findClauseInLibrary(lib, stackEntry.localId) as ELMQuery; + const expression = findClauseInLibrary(lib, stackEntry.localId); if (!expression) { throw Error( `Could not find ${stackEntry.type} type expression in ${stackEntry.libraryName} with localId ${stackEntry.localId}` @@ -696,12 +737,13 @@ export function expressionFromStackEntry(stackEntry: ExpressionStackEntry, allEL } // traverse from end of the stack to check all function definitions use name in operand -// TODO: need to traverse from end in order to change approach partway up, or can we traverse from beginning? +//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) export function checkStackFunctionDefs(stack: ExpressionStackEntry[], name: string, allELM: ELM[]) { + let checkName = name; // return false if no function defs if (!stack.find(s => s.type === 'FunctionDef')) return false; - for (let i = stack.length - 1; i >= 0; i--) { + for (let i = stack.length - 1; i > 0; i--) { if (stack[i].type === 'FunctionDef') { const lib = allELM.find(e => e.library.identifier.id === stack[i].libraryName); const functionStatement = lib?.library.statements.def.find(s => s.localId === stack[i].localId); @@ -710,12 +752,21 @@ export function checkStackFunctionDefs(stack: ExpressionStackEntry[], name: stri `Unable to find function definition statement with localId ${stack[i].localId} in library ${stack[i].libraryName}` ); } - if (!checkFunctionDefMatch(functionStatement, name)) { - return false; + if (!checkFunctionDefMatch(functionStatement, checkName)) { + // we've hit an operand missmatch, so it's a deadend that should be ignored + return null; + } + // get new name + if (stack[i - 1].type === 'FunctionRef' && stack[i - 1].localId !== 'unknown') { + const functionRef = expressionFromStackEntry(stack[i - 1], allELM) as ELMFunctionRef; + // find first operand with a name. TODO: do we have any other differentiating factors for finding the right operand? + const operand = functionRef.operand.find(o => 'name' in o && o.name); + if (operand && 'name' in operand && operand.name) checkName = operand.name; } } } - return true; + // final checkName can be used for further checks + return checkName; } // return true if function def operand matches the passed source name From c112e3b2078ffdee1ef0b13103b70987d04749a0 Mon Sep 17 00:00:00 2001 From: lmd59 Date: Fri, 26 Apr 2024 16:43:00 -0400 Subject: [PATCH 12/14] Add additional stack entries for relationship clauses --- src/helpers/elm/RetrievesHelper.ts | 7 +++++++ src/types/ELMTypes.ts | 1 + 2 files changed, 8 insertions(+) diff --git a/src/helpers/elm/RetrievesHelper.ts b/src/helpers/elm/RetrievesHelper.ts index 2a3f0734..c65224c5 100644 --- a/src/helpers/elm/RetrievesHelper.ts +++ b/src/helpers/elm/RetrievesHelper.ts @@ -209,6 +209,13 @@ export function findRetrieves( } query.relationship?.forEach(relationshipClause => { + if (relationshipClause.localId) { + recursiveOpts.expressionStack.push({ + libraryName: elm.library.identifier.id, + localId: relationshipClause.localId, + type: relationshipClause.type + }); + } recurse(results, relationshipClause.expression, recursiveOpts); recurse(results, relationshipClause.suchThat, recursiveOpts); }); diff --git a/src/types/ELMTypes.ts b/src/types/ELMTypes.ts index 814ad284..3ac82a2f 100644 --- a/src/types/ELMTypes.ts +++ b/src/types/ELMTypes.ts @@ -253,6 +253,7 @@ export interface ELMAliasedQuerySource { export interface ELMRelationshipClause extends ELMAliasedQuerySource { suchThat: AnyELMExpression; + type: string; } export interface ELMLetClause { From d8f7a4e9f6debd1321a4bd3fdff80417c810865b Mon Sep 17 00:00:00 2001 From: lmd59 Date: Wed, 1 May 2024 12:27:31 -0400 Subject: [PATCH 13/14] Check retrieve stack for alias --- src/helpers/DataRequirementHelpers.ts | 52 +++++++++++++++------------ 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index 6bff3aea..6a8379ca 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -7,10 +7,8 @@ import { SearchParameters } from '../compartment-definition/SearchParameters'; import { AnyELMExpression, ELM, - ELMAliasedQuerySource, ELMFunctionRef, ELMIdentifier, - ELMLast, ELMProperty, ELMQuery, ELMStatement @@ -31,7 +29,7 @@ import { DateTime, Interval } from 'cql-execution'; import { parseTimeStringAsUTC } from '../execution/ValueSetHelper'; import * as MeasureBundleHelpers from './MeasureBundleHelpers'; import { findLibraryReference } from './elm/ELMDependencyHelpers'; -import { findClauseInExpression, findClauseInLibrary, findNamedClausesInExpression } from './elm/ELMHelpers'; +import { findClauseInLibrary, findNamedClausesInExpression } from './elm/ELMHelpers'; const FHIR_QUERY_PATTERN_URL = 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-fhirQueryPattern'; /** @@ -467,6 +465,7 @@ function didEncounterDetailedValueFilterErrors(tbd: fhir4.Extension | GracefulEr function addMustSupport(allRetrieves: DataTypeQuery[], statement: ELMStatement, rootLib: ELM, allELM: ELM[]) { const propertyExpressions = findPropertyExpressions(statement, [], rootLib, allELM); + // TODO: double check that the property is applicable for the retrieve type before adding is as a mustSupport propertyExpressions.forEach(prop => { // find all matches for this property in allRetrieves const retrieveMatches = findRetrieveMatches(prop, allRetrieves, allELM); @@ -630,6 +629,9 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu const matchIdx = prop.stack.findIndex( s => s.localId === stackMatch.localId && s.libraryName === stackMatch.libraryName ); + const retMatchIdx = retrieve.expressionStack?.findIndex( + s => s.localId === stackMatch.localId && s.libraryName === stackMatch.libraryName + ); if (prop.property.scope) { // travel the stack looking for nearest queries (limited by stackMatch) const scopedQuery = findStackScopedQuery(prop.stack.slice(matchIdx), prop.property.scope, allELM); @@ -637,13 +639,10 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu if (!scopedQuery) return false; const { query, position, source } = scopedQuery; // if the query is our stackMatch, stop here, otherwise continue with query source - if (position === 0) { - // 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 - return ( - source && - source.expression.localId && - !!retrieve.expressionStack?.find(st => st.localId === source.expression.localId) - ); + if (position === 0 && 'alias' in source && source.alias && retrieve.expressionStack) { + //TODO: combine this and the if below??? or just get rid of position 0? + // confirm alias matches scope (i.e. follow the alias down the retrieve stack) + return checkRetrieveStackForAlias(retrieve.expressionStack.slice(retMatchIdx), source.alias, allELM); } if ('name' in source.expression && source.expression.name) { // TODO: do we need any further checks here with the highest reference name? @@ -661,18 +660,6 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu const stackSlice = prop.stack.slice(matchIdx); const checkName = checkStackFunctionDefs(stackSlice, prop.property.source.name, allELM); if (checkName) { - // get highest FunctionRef in the stack - // check that the operand has an AliasRef type expression with a name equal to... - // top alias in the retrieve stack (up to stackMatch) -> BipolarDiagnosis vs QualifyingEncounter - // const funcRefStackEntry = stackSlice.find(se => se.type === 'FunctionRef'); - // if (!funcRefStackEntry) return true; - // const aliasExp = (expressionFromStackEntry(funcRefStackEntry, allELM) as ELMFunctionRef).operand.find( - // o => o.type === 'AliasRef' - // ); - - const retMatchIdx = retrieve.expressionStack?.findIndex( - s => s.localId === stackMatch.localId && s.libraryName === stackMatch.libraryName - ); const retSlice = retrieve.expressionStack?.slice(retMatchIdx); const topAlias = retSlice ?.map(se => { @@ -694,6 +681,27 @@ export function findRetrieveMatches(prop: PropertyTracker, retrieves: DataTypeQu }); } +// traverse down a retrieve stack, checking query sources and traversing expression references all the way to the retrieve +// currently checks first query and whether a with/without invalidates a contained retrieve, TODO: may need further checks down the stack +export function checkRetrieveStackForAlias(stack: ExpressionStackEntry[], alias: string, allELM: ELM[]) { + for (let i = 0; i < stack.length - 1; i++) { + if (stack[i].type === 'Query') { + const query = expressionFromStackEntry(stack[i], allELM) as ELMQuery; + // search source or relationship next stack expression + const sourceMatch = query.source.find(s => s.expression.localId === stack[i + 1].localId); + const relationshipMatch = query.relationship?.find(r => r.localId === stack[i + 1].localId); + const expRefChange = + stack[i + 1].type === 'ExpressionRef' && stack.find(se => se.type === 'With' || se.type === 'Without'); + if (sourceMatch) { + return sourceMatch.alias === alias && !expRefChange; + } else if (relationshipMatch) { + return relationshipMatch.alias === alias && !expRefChange; + } + } + } + return true; +} + // traverse from end of the stack to find the nearest query that has alias labeled with the passed scope export function findStackScopedQuery(stack: ExpressionStackEntry[], scope: string, allELM: ELM[]) { for (let i = stack.length - 1; i >= 0; i--) { From 5473102a81cbdf0fdd42eb93ad543f0212d7abfc Mon Sep 17 00:00:00 2001 From: lmd59 Date: Mon, 6 May 2024 12:43:45 -0400 Subject: [PATCH 14/14] Ensure that mustSupports only include allowed properties --- src/code-attributes/propertyPaths.ts | 2618 +++++++++++++++++++++++++ src/helpers/DataRequirementHelpers.ts | 15 +- 2 files changed, 2627 insertions(+), 6 deletions(-) create mode 100644 src/code-attributes/propertyPaths.ts diff --git a/src/code-attributes/propertyPaths.ts b/src/code-attributes/propertyPaths.ts new file mode 100644 index 00000000..9f78930c --- /dev/null +++ b/src/code-attributes/propertyPaths.ts @@ -0,0 +1,2618 @@ +// Source: https://github.com/projecttacoma/fqm-testify/blob/parse-properties/util/propertyPaths.ts + +export const parsedPropertyPaths: Record = { + Account: [ + 'identifier', + 'status', + 'type', + 'name', + 'subject', + 'servicePeriod', + 'coverage', + 'owner', + 'description', + 'guarantor', + 'partOf' + ], + ActivityDefinition: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'subtitle', + 'status', + 'experimental', + 'subject', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'usage', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'topic', + 'author', + 'editor', + 'reviewer', + 'endorser', + 'relatedArtifact', + 'library', + 'kind', + 'profile', + 'code', + 'intent', + 'priority', + 'doNotPerform', + 'timing', + 'location', + 'participant', + 'product', + 'quantity', + 'dosage', + 'bodySite', + 'specimenRequirement', + 'observationRequirement', + 'observationResultRequirement', + 'transform', + 'dynamicValue' + ], + AdverseEvent: [ + 'identifier', + 'actuality', + 'category', + 'event', + 'subject', + 'encounter', + 'date', + 'detected', + 'recordedDate', + 'resultingCondition', + 'location', + 'seriousness', + 'severity', + 'outcome', + 'recorder', + 'contributor', + 'suspectEntity', + 'subjectMedicalHistory', + 'referenceDocument', + 'study' + ], + AllergyIntolerance: [ + 'identifier', + 'clinicalStatus', + 'verificationStatus', + 'type', + 'category', + 'criticality', + 'code', + 'patient', + 'encounter', + 'onset', + 'recordedDate', + 'recorder', + 'asserter', + 'lastOccurrence', + 'note', + 'reaction' + ], + Appointment: [ + 'identifier', + 'status', + 'cancelationReason', + 'serviceCategory', + 'serviceType', + 'specialty', + 'appointmentType', + 'reasonCode', + 'reasonReference', + 'priority', + 'description', + 'supportingInformation', + 'start', + 'end', + 'minutesDuration', + 'slot', + 'created', + 'comment', + 'patientInstruction', + 'basedOn', + 'participant', + 'requestedPeriod' + ], + AppointmentResponse: [ + 'identifier', + 'appointment', + 'start', + 'end', + 'participantType', + 'actor', + 'participantStatus', + 'comment' + ], + AuditEvent: [ + 'type', + 'subtype', + 'action', + 'period', + 'recorded', + 'outcome', + 'outcomeDesc', + 'purposeOfEvent', + 'agent', + 'source', + 'entity' + ], + Basic: ['identifier', 'code', 'subject', 'created', 'author'], + BiologicallyDerivedProduct: [ + 'identifier', + 'productCategory', + 'productCode', + 'status', + 'request', + 'quantity', + 'parent', + 'collection', + 'processing', + 'manipulation', + 'storage' + ], + BodyStructure: [ + 'identifier', + 'active', + 'morphology', + 'location', + 'locationQualifier', + 'description', + 'image', + 'patient' + ], + CapabilityStatement: [ + 'url', + 'version', + 'name', + 'title', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'copyright', + 'kind', + 'instantiates', + 'imports', + 'software', + 'implementation', + 'fhirVersion', + 'format', + 'patchFormat', + 'implementationGuide', + 'rest', + 'messaging', + 'document' + ], + CarePlan: [ + 'identifier', + 'instantiatesCanonical', + 'instantiatesUri', + 'basedOn', + 'replaces', + 'partOf', + 'status', + 'intent', + 'category', + 'title', + 'description', + 'subject', + 'encounter', + 'period', + 'created', + 'author', + 'contributor', + 'careTeam', + 'addresses', + 'supportingInfo', + 'goal', + 'activity', + 'note' + ], + CareTeam: [ + 'identifier', + 'status', + 'category', + 'name', + 'subject', + 'encounter', + 'period', + 'participant', + 'reasonCode', + 'reasonReference', + 'managingOrganization', + 'telecom', + 'note' + ], + CatalogEntry: [ + 'identifier', + 'type', + 'orderable', + 'referencedItem', + 'additionalIdentifier', + 'classification', + 'status', + 'validityPeriod', + 'validTo', + 'lastUpdated', + 'additionalCharacteristic', + 'additionalClassification', + 'relatedEntry' + ], + ChargeItem: [ + 'identifier', + 'definitionUri', + 'definitionCanonical', + 'status', + 'partOf', + 'code', + 'subject', + 'context', + 'occurrence', + 'performer', + 'performingOrganization', + 'requestingOrganization', + 'costCenter', + 'quantity', + 'bodysite', + 'factorOverride', + 'priceOverride', + 'overrideReason', + 'enterer', + 'enteredDate', + 'reason', + 'service', + 'product', + 'account', + 'note', + 'supportingInformation' + ], + ChargeItemDefinition: [ + 'url', + 'identifier', + 'version', + 'title', + 'derivedFromUri', + 'partOf', + 'replaces', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'code', + 'instance', + 'applicability', + 'propertyGroup' + ], + Claim: [ + 'identifier', + 'status', + 'type', + 'subType', + 'use', + 'patient', + 'billablePeriod', + 'created', + 'enterer', + 'insurer', + 'provider', + 'priority', + 'fundsReserve', + 'related', + 'prescription', + 'originalPrescription', + 'payee', + 'referral', + 'facility', + 'careTeam', + 'supportingInfo', + 'diagnosis', + 'procedure', + 'insurance', + 'accident', + 'item', + 'total' + ], + ClaimResponse: [ + 'identifier', + 'status', + 'type', + 'subType', + 'use', + 'patient', + 'created', + 'insurer', + 'requestor', + 'request', + 'outcome', + 'disposition', + 'preAuthRef', + 'preAuthPeriod', + 'payeeType', + 'item', + 'addItem', + 'adjudication', + 'total', + 'payment', + 'fundsReserve', + 'formCode', + 'form', + 'processNote', + 'communicationRequest', + 'insurance', + 'error' + ], + ClinicalImpression: [ + 'identifier', + 'status', + 'statusReason', + 'code', + 'description', + 'subject', + 'encounter', + 'effective', + 'date', + 'assessor', + 'previous', + 'problem', + 'investigation', + 'protocol', + 'summary', + 'finding', + 'prognosisCodeableConcept', + 'prognosisReference', + 'supportingInfo', + 'note' + ], + CodeSystem: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'copyright', + 'caseSensitive', + 'valueSet', + 'hierarchyMeaning', + 'compositional', + 'versionNeeded', + 'content', + 'supplements', + 'count', + 'filter', + 'property', + 'concept' + ], + Communication: [ + 'identifier', + 'instantiatesCanonical', + 'instantiatesUri', + 'basedOn', + 'partOf', + 'inResponseTo', + 'status', + 'statusReason', + 'category', + 'priority', + 'medium', + 'subject', + 'topic', + 'about', + 'encounter', + 'sent', + 'received', + 'recipient', + 'sender', + 'reasonCode', + 'reasonReference', + 'payload', + 'note' + ], + CommunicationRequest: [ + 'identifier', + 'basedOn', + 'replaces', + 'groupIdentifier', + 'status', + 'statusReason', + 'category', + 'priority', + 'doNotPerform', + 'medium', + 'subject', + 'about', + 'encounter', + 'payload', + 'occurrence', + 'authoredOn', + 'requester', + 'recipient', + 'sender', + 'reasonCode', + 'reasonReference', + 'note' + ], + CompartmentDefinition: [ + 'url', + 'version', + 'name', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'purpose', + 'code', + 'search', + 'resource' + ], + Composition: [ + 'identifier', + 'status', + 'type', + 'category', + 'subject', + 'encounter', + 'date', + 'author', + 'title', + 'confidentiality', + 'attester', + 'custodian', + 'relatesTo', + 'event', + 'section' + ], + ConceptMap: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'copyright', + 'source', + 'target', + 'group' + ], + Condition: [ + 'identifier', + 'clinicalStatus', + 'verificationStatus', + 'category', + 'severity', + 'code', + 'bodySite', + 'subject', + 'encounter', + 'onset', + 'abatement', + 'recordedDate', + 'recorder', + 'asserter', + 'stage', + 'evidence', + 'note' + ], + Consent: [ + 'identifier', + 'status', + 'scope', + 'category', + 'patient', + 'dateTime', + 'performer', + 'organization', + 'source', + 'policy', + 'policyRule', + 'verification', + 'provision' + ], + Contract: [ + 'identifier', + 'url', + 'version', + 'status', + 'legalState', + 'instantiatesCanonical', + 'instantiatesUri', + 'contentDerivative', + 'issued', + 'applies', + 'expirationType', + 'subject', + 'authority', + 'domain', + 'site', + 'name', + 'title', + 'subtitle', + 'alias', + 'author', + 'scope', + 'topic', + 'type', + 'subType', + 'contentDefinition', + 'term', + 'supportingInfo', + 'relevantHistory', + 'signer', + 'friendly', + 'legal', + 'rule', + 'legallyBinding' + ], + Coverage: [ + 'identifier', + 'status', + 'type', + 'policyHolder', + 'subscriber', + 'subscriberId', + 'beneficiary', + 'dependent', + 'relationship', + 'period', + 'payor', + 'class', + 'order', + 'network', + 'costToBeneficiary', + 'subrogation', + 'contract' + ], + CoverageEligibilityRequest: [ + 'identifier', + 'status', + 'priority', + 'purpose', + 'patient', + 'serviced', + 'created', + 'enterer', + 'provider', + 'insurer', + 'facility', + 'supportingInfo', + 'insurance', + 'item' + ], + CoverageEligibilityResponse: [ + 'identifier', + 'status', + 'purpose', + 'patient', + 'serviced', + 'created', + 'requestor', + 'request', + 'outcome', + 'disposition', + 'insurer', + 'insurance', + 'preAuthRef', + 'form', + 'error' + ], + DetectedIssue: [ + 'identifier', + 'status', + 'code', + 'severity', + 'patient', + 'identified', + 'author', + 'implicated', + 'evidence', + 'detail', + 'reference', + 'mitigation' + ], + Device: [ + 'identifier', + 'definition', + 'udiCarrier', + 'status', + 'statusReason', + 'distinctIdentifier', + 'manufacturer', + 'manufactureDate', + 'expirationDate', + 'lotNumber', + 'serialNumber', + 'deviceName', + 'modelNumber', + 'partNumber', + 'type', + 'specialization', + 'version', + 'property', + 'patient', + 'owner', + 'contact', + 'location', + 'url', + 'note', + 'safety', + 'parent' + ], + DeviceDefinition: [ + 'identifier', + 'udiDeviceIdentifier', + 'manufacturer', + 'deviceName', + 'modelNumber', + 'type', + 'specialization', + 'version', + 'safety', + 'shelfLifeStorage', + 'physicalCharacteristics', + 'languageCode', + 'capability', + 'property', + 'owner', + 'contact', + 'url', + 'onlineInformation', + 'note', + 'quantity', + 'parentDevice', + 'material' + ], + DeviceMetric: [ + 'identifier', + 'type', + 'unit', + 'source', + 'parent', + 'operationalStatus', + 'color', + 'category', + 'measurementPeriod', + 'calibration' + ], + DeviceRequest: [ + 'identifier', + 'instantiatesCanonical', + 'instantiatesUri', + 'basedOn', + 'priorRequest', + 'groupIdentifier', + 'status', + 'intent', + 'priority', + 'code', + 'parameter', + 'subject', + 'encounter', + 'occurrence', + 'authoredOn', + 'requester', + 'performerType', + 'performer', + 'reasonCode', + 'reasonReference', + 'insurance', + 'supportingInfo', + 'note', + 'relevantHistory' + ], + DeviceUseStatement: [ + 'identifier', + 'basedOn', + 'status', + 'subject', + 'derivedFrom', + 'timing', + 'recordedOn', + 'source', + 'device', + 'reasonCode', + 'reasonReference', + 'bodySite', + 'note' + ], + DiagnosticReport: [ + 'identifier', + 'basedOn', + 'status', + 'category', + 'code', + 'subject', + 'encounter', + 'effective', + 'issued', + 'performer', + 'resultsInterpreter', + 'specimen', + 'result', + 'imagingStudy', + 'media', + 'conclusion', + 'conclusionCode', + 'presentedForm' + ], + DocumentManifest: [ + 'masterIdentifier', + 'identifier', + 'status', + 'type', + 'subject', + 'created', + 'author', + 'recipient', + 'source', + 'description', + 'content', + 'related' + ], + DocumentReference: [ + 'masterIdentifier', + 'identifier', + 'status', + 'docStatus', + 'type', + 'category', + 'subject', + 'date', + 'author', + 'authenticator', + 'custodian', + 'relatesTo', + 'description', + 'securityLabel', + 'content', + 'context' + ], + EffectEvidenceSynthesis: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'status', + 'date', + 'publisher', + 'contact', + 'description', + 'note', + 'useContext', + 'jurisdiction', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'topic', + 'author', + 'editor', + 'reviewer', + 'endorser', + 'relatedArtifact', + 'synthesisType', + 'studyType', + 'population', + 'exposure', + 'exposureAlternative', + 'outcome', + 'sampleSize', + 'resultsByExposure', + 'effectEstimate', + 'certainty' + ], + Encounter: [ + 'identifier', + 'status', + 'statusHistory', + 'class', + 'classHistory', + 'type', + 'serviceType', + 'priority', + 'subject', + 'episodeOfCare', + 'basedOn', + 'participant', + 'appointment', + 'period', + 'length', + 'reasonCode', + 'reasonReference', + 'diagnosis', + 'account', + 'hospitalization', + 'location', + 'serviceProvider', + 'partOf' + ], + Endpoint: [ + 'identifier', + 'status', + 'connectionType', + 'name', + 'managingOrganization', + 'contact', + 'period', + 'payloadType', + 'payloadMimeType', + 'address', + 'header' + ], + EnrollmentRequest: ['identifier', 'status', 'created', 'insurer', 'provider', 'candidate', 'coverage'], + EnrollmentResponse: [ + 'identifier', + 'status', + 'request', + 'outcome', + 'disposition', + 'created', + 'organization', + 'requestProvider' + ], + EpisodeOfCare: [ + 'identifier', + 'status', + 'statusHistory', + 'type', + 'diagnosis', + 'patient', + 'managingOrganization', + 'period', + 'referralRequest', + 'careManager', + 'team', + 'account' + ], + EventDefinition: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'subtitle', + 'status', + 'experimental', + 'subject', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'usage', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'topic', + 'author', + 'editor', + 'reviewer', + 'endorser', + 'relatedArtifact', + 'trigger' + ], + Evidence: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'shortTitle', + 'subtitle', + 'status', + 'date', + 'publisher', + 'contact', + 'description', + 'note', + 'useContext', + 'jurisdiction', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'topic', + 'author', + 'editor', + 'reviewer', + 'endorser', + 'relatedArtifact', + 'exposureBackground', + 'exposureVariant', + 'outcome' + ], + EvidenceVariable: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'shortTitle', + 'subtitle', + 'status', + 'date', + 'publisher', + 'contact', + 'description', + 'note', + 'useContext', + 'jurisdiction', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'topic', + 'author', + 'editor', + 'reviewer', + 'endorser', + 'relatedArtifact', + 'type', + 'characteristic' + ], + ExampleScenario: [ + 'url', + 'identifier', + 'version', + 'name', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'useContext', + 'jurisdiction', + 'copyright', + 'purpose', + 'actor', + 'instance', + 'process', + 'workflow' + ], + ExplanationOfBenefit: [ + 'identifier', + 'status', + 'type', + 'subType', + 'use', + 'patient', + 'billablePeriod', + 'created', + 'enterer', + 'insurer', + 'provider', + 'priority', + 'fundsReserveRequested', + 'fundsReserve', + 'related', + 'prescription', + 'originalPrescription', + 'payee', + 'referral', + 'facility', + 'claim', + 'claimResponse', + 'outcome', + 'disposition', + 'preAuthRef', + 'preAuthRefPeriod', + 'careTeam', + 'supportingInfo', + 'diagnosis', + 'procedure', + 'precedence', + 'insurance', + 'accident', + 'item', + 'addItem', + 'adjudication', + 'total', + 'payment', + 'formCode', + 'form', + 'processNote', + 'benefitPeriod', + 'benefitBalance' + ], + FamilyMemberHistory: [ + 'identifier', + 'instantiatesCanonical', + 'instantiatesUri', + 'status', + 'dataAbsentReason', + 'patient', + 'date', + 'name', + 'relationship', + 'sex', + 'born', + 'age', + 'estimatedAge', + 'deceased', + 'reasonCode', + 'reasonReference', + 'note', + 'condition' + ], + Flag: ['identifier', 'status', 'category', 'code', 'subject', 'period', 'encounter', 'author'], + Goal: [ + 'identifier', + 'lifecycleStatus', + 'achievementStatus', + 'category', + 'priority', + 'description', + 'subject', + 'start', + 'target', + 'statusDate', + 'statusReason', + 'expressedBy', + 'addresses', + 'note', + 'outcomeCode', + 'outcomeReference' + ], + GraphDefinition: [ + 'url', + 'version', + 'name', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'start', + 'profile', + 'link' + ], + Group: [ + 'identifier', + 'active', + 'type', + 'actual', + 'code', + 'name', + 'quantity', + 'managingEntity', + 'characteristic', + 'member' + ], + GuidanceResponse: [ + 'requestIdentifier', + 'identifier', + 'module', + 'status', + 'subject', + 'encounter', + 'occurrenceDateTime', + 'performer', + 'reasonCode', + 'reasonReference', + 'note', + 'evaluationMessage', + 'outputParameters', + 'result', + 'dataRequirement' + ], + HealthcareService: [ + 'identifier', + 'active', + 'providedBy', + 'category', + 'type', + 'specialty', + 'location', + 'name', + 'comment', + 'extraDetails', + 'photo', + 'telecom', + 'coverageArea', + 'serviceProvisionCode', + 'eligibility', + 'program', + 'characteristic', + 'communication', + 'referralMethod', + 'appointmentRequired', + 'availableTime', + 'notAvailable', + 'availabilityExceptions', + 'endpoint' + ], + ImagingStudy: [ + 'identifier', + 'status', + 'modality', + 'subject', + 'encounter', + 'started', + 'basedOn', + 'referrer', + 'interpreter', + 'endpoint', + 'numberOfSeries', + 'numberOfInstances', + 'procedureReference', + 'procedureCode', + 'location', + 'reasonCode', + 'reasonReference', + 'note', + 'description', + 'series' + ], + Immunization: [ + 'identifier', + 'status', + 'statusReason', + 'vaccineCode', + 'patient', + 'encounter', + 'occurrence', + 'recorded', + 'primarySource', + 'reportOrigin', + 'location', + 'manufacturer', + 'lotNumber', + 'expirationDate', + 'site', + 'route', + 'doseQuantity', + 'performer', + 'note', + 'reasonCode', + 'reasonReference', + 'isSubpotent', + 'subpotentReason', + 'education', + 'programEligibility', + 'fundingSource', + 'reaction', + 'protocolApplied' + ], + ImmunizationEvaluation: [ + 'identifier', + 'status', + 'patient', + 'date', + 'authority', + 'targetDisease', + 'immunizationEvent', + 'doseStatus', + 'doseStatusReason', + 'description', + 'series', + 'doseNumber', + 'seriesDoses' + ], + ImmunizationRecommendation: ['identifier', 'patient', 'date', 'authority', 'recommendation'], + ImplementationGuide: [ + 'url', + 'version', + 'name', + 'title', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'copyright', + 'packageId', + 'license', + 'fhirVersion', + 'dependsOn', + 'global', + 'definition', + 'manifest' + ], + InsurancePlan: [ + 'identifier', + 'status', + 'type', + 'name', + 'alias', + 'period', + 'ownedBy', + 'administeredBy', + 'coverageArea', + 'contact', + 'endpoint', + 'network', + 'coverage', + 'plan' + ], + Invoice: [ + 'identifier', + 'status', + 'cancelledReason', + 'type', + 'subject', + 'recipient', + 'date', + 'participant', + 'issuer', + 'account', + 'lineItem', + 'totalPriceComponent', + 'totalNet', + 'totalGross', + 'paymentTerms', + 'note' + ], + Library: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'subtitle', + 'status', + 'experimental', + 'type', + 'subject', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'usage', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'topic', + 'author', + 'editor', + 'reviewer', + 'endorser', + 'relatedArtifact', + 'parameter', + 'dataRequirement', + 'content' + ], + Linkage: ['active', 'author', 'item'], + List: [ + 'identifier', + 'status', + 'mode', + 'title', + 'code', + 'subject', + 'encounter', + 'date', + 'source', + 'orderedBy', + 'note', + 'entry', + 'emptyReason' + ], + Location: [ + 'identifier', + 'status', + 'operationalStatus', + 'name', + 'alias', + 'description', + 'mode', + 'type', + 'telecom', + 'address', + 'physicalType', + 'position', + 'managingOrganization', + 'partOf', + 'hoursOfOperation', + 'availabilityExceptions', + 'endpoint' + ], + Measure: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'subtitle', + 'status', + 'experimental', + 'subject', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'usage', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'topic', + 'author', + 'editor', + 'reviewer', + 'endorser', + 'relatedArtifact', + 'library', + 'disclaimer', + 'scoring', + 'compositeScoring', + 'type', + 'riskAdjustment', + 'rateAggregation', + 'rationale', + 'clinicalRecommendationStatement', + 'improvementNotation', + 'definition', + 'guidance', + 'group', + 'supplementalData' + ], + MeasureReport: [ + 'identifier', + 'status', + 'type', + 'measure', + 'subject', + 'date', + 'reporter', + 'period', + 'improvementNotation', + 'group', + 'evaluatedResource' + ], + Media: [ + 'identifier', + 'basedOn', + 'partOf', + 'status', + 'type', + 'modality', + 'view', + 'subject', + 'encounter', + 'created', + 'issued', + 'operator', + 'reasonCode', + 'bodySite', + 'deviceName', + 'device', + 'height', + 'width', + 'frames', + 'duration', + 'content', + 'note' + ], + Medication: ['identifier', 'code', 'status', 'manufacturer', 'form', 'amount', 'ingredient', 'batch'], + MedicationAdministration: [ + 'identifier', + 'instantiates', + 'partOf', + 'status', + 'statusReason', + 'category', + 'medication', + 'subject', + 'context', + 'supportingInformation', + 'effective', + 'performer', + 'reasonCode', + 'reasonReference', + 'request', + 'device', + 'note', + 'dosage', + 'eventHistory' + ], + MedicationDispense: [ + 'identifier', + 'partOf', + 'status', + 'statusReason', + 'category', + 'medication', + 'subject', + 'context', + 'supportingInformation', + 'performer', + 'location', + 'authorizingPrescription', + 'type', + 'quantity', + 'daysSupply', + 'whenPrepared', + 'whenHandedOver', + 'destination', + 'receiver', + 'note', + 'dosageInstruction', + 'substitution', + 'detectedIssue', + 'eventHistory' + ], + MedicationKnowledge: [ + 'code', + 'status', + 'manufacturer', + 'doseForm', + 'amount', + 'synonym', + 'relatedMedicationKnowledge', + 'associatedMedication', + 'productType', + 'monograph', + 'ingredient', + 'preparationInstruction', + 'intendedRoute', + 'cost', + 'monitoringProgram', + 'administrationGuidelines', + 'medicineClassification', + 'packaging', + 'drugCharacteristic', + 'contraindication', + 'regulatory', + 'kinetics' + ], + MedicationRequest: [ + 'identifier', + 'status', + 'statusReason', + 'intent', + 'category', + 'priority', + 'doNotPerform', + 'reported', + 'medication', + 'subject', + 'encounter', + 'supportingInformation', + 'authoredOn', + 'requester', + 'performer', + 'performerType', + 'recorder', + 'reasonCode', + 'reasonReference', + 'instantiatesCanonical', + 'instantiatesUri', + 'basedOn', + 'groupIdentifier', + 'courseOfTherapyType', + 'insurance', + 'note', + 'dosageInstruction', + 'dispenseRequest', + 'substitution', + 'priorPrescription', + 'detectedIssue', + 'eventHistory' + ], + MedicationStatement: [ + 'identifier', + 'basedOn', + 'partOf', + 'status', + 'statusReason', + 'category', + 'medication', + 'subject', + 'context', + 'effective', + 'dateAsserted', + 'informationSource', + 'derivedFrom', + 'reasonCode', + 'reasonReference', + 'note', + 'dosage' + ], + MedicinalProduct: [ + 'identifier', + 'type', + 'domain', + 'combinedPharmaceuticalDoseForm', + 'legalStatusOfSupply', + 'additionalMonitoringIndicator', + 'specialMeasures', + 'paediatricUseIndicator', + 'productClassification', + 'marketingStatus', + 'pharmaceuticalProduct', + 'packagedMedicinalProduct', + 'attachedDocument', + 'masterFile', + 'contact', + 'clinicalTrial', + 'name', + 'crossReference', + 'manufacturingBusinessOperation', + 'specialDesignation' + ], + MedicinalProductAuthorization: [ + 'identifier', + 'subject', + 'country', + 'jurisdiction', + 'status', + 'statusDate', + 'restoreDate', + 'validityPeriod', + 'dataExclusivityPeriod', + 'dateOfFirstAuthorization', + 'internationalBirthDate', + 'legalBasis', + 'jurisdictionalAuthorization', + 'holder', + 'regulator', + 'procedure' + ], + MedicinalProductContraindication: [ + 'subject', + 'disease', + 'diseaseStatus', + 'comorbidity', + 'therapeuticIndication', + 'otherTherapy', + 'population' + ], + MedicinalProductIndication: [ + 'subject', + 'diseaseSymptomProcedure', + 'diseaseStatus', + 'comorbidity', + 'intendedEffect', + 'duration', + 'otherTherapy', + 'undesirableEffect', + 'population' + ], + MedicinalProductIngredient: [ + 'identifier', + 'role', + 'allergenicIndicator', + 'manufacturer', + 'specifiedSubstance', + 'substance' + ], + MedicinalProductInteraction: ['subject', 'description', 'interactant', 'type', 'effect', 'incidence', 'management'], + MedicinalProductManufactured: [ + 'manufacturedDoseForm', + 'unitOfPresentation', + 'quantity', + 'manufacturer', + 'ingredient', + 'physicalCharacteristics', + 'otherCharacteristics' + ], + MedicinalProductPackaged: [ + 'identifier', + 'subject', + 'description', + 'legalStatusOfSupply', + 'marketingStatus', + 'marketingAuthorization', + 'manufacturer', + 'batchIdentifier', + 'packageItem' + ], + MedicinalProductPharmaceutical: [ + 'identifier', + 'administrableDoseForm', + 'unitOfPresentation', + 'ingredient', + 'device', + 'characteristics', + 'routeOfAdministration' + ], + MedicinalProductUndesirableEffect: [ + 'subject', + 'symptomConditionEffect', + 'classification', + 'frequencyOfOccurrence', + 'population' + ], + MessageDefinition: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'replaces', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'copyright', + 'base', + 'parent', + 'event', + 'category', + 'focus', + 'responseRequired', + 'allowedResponse', + 'graph' + ], + MessageHeader: [ + 'event', + 'destination', + 'sender', + 'enterer', + 'author', + 'source', + 'responsible', + 'reason', + 'response', + 'focus', + 'definition' + ], + MolecularSequence: [ + 'identifier', + 'type', + 'coordinateSystem', + 'patient', + 'specimen', + 'device', + 'performer', + 'quantity', + 'referenceSeq', + 'variant', + 'observedSeq', + 'quality', + 'readCoverage', + 'repository', + 'pointer', + 'structureVariant' + ], + NamingSystem: [ + 'name', + 'status', + 'kind', + 'date', + 'publisher', + 'contact', + 'responsible', + 'type', + 'description', + 'useContext', + 'jurisdiction', + 'usage', + 'uniqueId' + ], + NutritionOrder: [ + 'identifier', + 'instantiatesCanonical', + 'instantiatesUri', + 'instantiates', + 'status', + 'intent', + 'patient', + 'encounter', + 'dateTime', + 'orderer', + 'allergyIntolerance', + 'foodPreferenceModifier', + 'excludeFoodModifier', + 'oralDiet', + 'supplement', + 'enteralFormula', + 'note' + ], + Observation: [ + 'identifier', + 'basedOn', + 'partOf', + 'status', + 'category', + 'code', + 'subject', + 'focus', + 'encounter', + 'effective', + 'issued', + 'performer', + 'value', + 'dataAbsentReason', + 'interpretation', + 'note', + 'bodySite', + 'method', + 'specimen', + 'device', + 'referenceRange', + 'hasMember', + 'derivedFrom', + 'component' + ], + ObservationDefinition: [ + 'category', + 'code', + 'identifier', + 'permittedDataType', + 'multipleResultsAllowed', + 'method', + 'preferredReportName', + 'quantitativeDetails', + 'qualifiedInterval', + 'validCodedValueSet', + 'normalCodedValueSet', + 'abnormalCodedValueSet', + 'criticalCodedValueSet' + ], + OperationDefinition: [ + 'url', + 'version', + 'name', + 'title', + 'status', + 'kind', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'affectsState', + 'code', + 'comment', + 'base', + 'resource', + 'system', + 'type', + 'instance', + 'inputProfile', + 'outputProfile', + 'parameter', + 'overload' + ], + OperationOutcome: ['issue'], + Organization: [ + 'identifier', + 'active', + 'type', + 'name', + 'alias', + 'telecom', + 'address', + 'partOf', + 'contact', + 'endpoint' + ], + OrganizationAffiliation: [ + 'identifier', + 'active', + 'period', + 'organization', + 'participatingOrganization', + 'network', + 'code', + 'specialty', + 'location', + 'healthcareService', + 'telecom', + 'endpoint' + ], + Patient: [ + 'identifier', + 'active', + 'name', + 'telecom', + 'gender', + 'birthDate', + 'deceased', + 'address', + 'maritalStatus', + 'multipleBirth', + 'photo', + 'contact', + 'communication', + 'generalPractitioner', + 'managingOrganization', + 'link' + ], + PaymentNotice: [ + 'identifier', + 'status', + 'request', + 'response', + 'created', + 'provider', + 'payment', + 'paymentDate', + 'payee', + 'recipient', + 'amount', + 'paymentStatus' + ], + PaymentReconciliation: [ + 'identifier', + 'status', + 'period', + 'created', + 'paymentIssuer', + 'request', + 'requestor', + 'outcome', + 'disposition', + 'paymentDate', + 'paymentAmount', + 'paymentIdentifier', + 'detail', + 'formCode', + 'processNote' + ], + Person: [ + 'identifier', + 'name', + 'telecom', + 'gender', + 'birthDate', + 'address', + 'photo', + 'managingOrganization', + 'active', + 'link' + ], + PlanDefinition: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'subtitle', + 'type', + 'status', + 'experimental', + 'subject', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'usage', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'topic', + 'author', + 'editor', + 'reviewer', + 'endorser', + 'relatedArtifact', + 'library', + 'goal', + 'action' + ], + Practitioner: [ + 'identifier', + 'active', + 'name', + 'telecom', + 'address', + 'gender', + 'birthDate', + 'photo', + 'qualification', + 'communication' + ], + PractitionerRole: [ + 'identifier', + 'active', + 'period', + 'practitioner', + 'organization', + 'code', + 'specialty', + 'location', + 'healthcareService', + 'telecom', + 'availableTime', + 'notAvailable', + 'availabilityExceptions', + 'endpoint' + ], + Procedure: [ + 'identifier', + 'instantiatesCanonical', + 'instantiatesUri', + 'basedOn', + 'partOf', + 'status', + 'statusReason', + 'category', + 'code', + 'subject', + 'encounter', + 'performed', + 'recorder', + 'asserter', + 'performer', + 'location', + 'reasonCode', + 'reasonReference', + 'bodySite', + 'outcome', + 'report', + 'complication', + 'complicationDetail', + 'followUp', + 'note', + 'focalDevice', + 'usedReference', + 'usedCode' + ], + Provenance: [ + 'target', + 'occurred', + 'recorded', + 'policy', + 'location', + 'reason', + 'activity', + 'agent', + 'entity', + 'signature' + ], + Questionnaire: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'derivedFrom', + 'status', + 'experimental', + 'subjectType', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'code', + 'item' + ], + QuestionnaireResponse: [ + 'identifier', + 'basedOn', + 'partOf', + 'questionnaire', + 'status', + 'subject', + 'encounter', + 'authored', + 'author', + 'source', + 'item' + ], + RelatedPerson: [ + 'identifier', + 'active', + 'patient', + 'relationship', + 'name', + 'telecom', + 'gender', + 'birthDate', + 'address', + 'photo', + 'period', + 'communication' + ], + RequestGroup: [ + 'identifier', + 'instantiatesCanonical', + 'instantiatesUri', + 'basedOn', + 'replaces', + 'groupIdentifier', + 'status', + 'intent', + 'priority', + 'code', + 'subject', + 'encounter', + 'authoredOn', + 'author', + 'reasonCode', + 'reasonReference', + 'note', + 'action' + ], + ResearchDefinition: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'shortTitle', + 'subtitle', + 'status', + 'experimental', + 'subject', + 'date', + 'publisher', + 'contact', + 'description', + 'comment', + 'useContext', + 'jurisdiction', + 'purpose', + 'usage', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'topic', + 'author', + 'editor', + 'reviewer', + 'endorser', + 'relatedArtifact', + 'library', + 'population', + 'exposure', + 'exposureAlternative', + 'outcome' + ], + ResearchElementDefinition: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'shortTitle', + 'subtitle', + 'status', + 'experimental', + 'subject', + 'date', + 'publisher', + 'contact', + 'description', + 'comment', + 'useContext', + 'jurisdiction', + 'purpose', + 'usage', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'topic', + 'author', + 'editor', + 'reviewer', + 'endorser', + 'relatedArtifact', + 'library', + 'type', + 'variableType', + 'characteristic' + ], + ResearchStudy: [ + 'identifier', + 'title', + 'protocol', + 'partOf', + 'status', + 'primaryPurposeType', + 'phase', + 'category', + 'focus', + 'condition', + 'contact', + 'relatedArtifact', + 'keyword', + 'location', + 'description', + 'enrollment', + 'period', + 'sponsor', + 'principalInvestigator', + 'site', + 'reasonStopped', + 'note', + 'arm', + 'objective' + ], + ResearchSubject: ['identifier', 'status', 'period', 'study', 'individual', 'assignedArm', 'actualArm', 'consent'], + RiskAssessment: [ + 'identifier', + 'basedOn', + 'parent', + 'status', + 'method', + 'code', + 'subject', + 'encounter', + 'occurrence', + 'condition', + 'performer', + 'reasonCode', + 'reasonReference', + 'basis', + 'prediction', + 'mitigation', + 'note' + ], + RiskEvidenceSynthesis: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'status', + 'date', + 'publisher', + 'contact', + 'description', + 'note', + 'useContext', + 'jurisdiction', + 'copyright', + 'approvalDate', + 'lastReviewDate', + 'effectivePeriod', + 'topic', + 'author', + 'editor', + 'reviewer', + 'endorser', + 'relatedArtifact', + 'synthesisType', + 'studyType', + 'population', + 'exposure', + 'outcome', + 'sampleSize', + 'riskEstimate', + 'certainty' + ], + Schedule: [ + 'identifier', + 'active', + 'serviceCategory', + 'serviceType', + 'specialty', + 'actor', + 'planningHorizon', + 'comment' + ], + SearchParameter: [ + 'url', + 'version', + 'name', + 'derivedFrom', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'code', + 'base', + 'type', + 'expression', + 'xpath', + 'xpathUsage', + 'target', + 'multipleOr', + 'multipleAnd', + 'comparator', + 'modifier', + 'chain', + 'component' + ], + ServiceRequest: [ + 'identifier', + 'instantiatesCanonical', + 'instantiatesUri', + 'basedOn', + 'replaces', + 'requisition', + 'status', + 'intent', + 'category', + 'priority', + 'doNotPerform', + 'code', + 'orderDetail', + 'quantity', + 'subject', + 'encounter', + 'occurrence', + 'asNeeded', + 'authoredOn', + 'requester', + 'performerType', + 'performer', + 'locationCode', + 'locationReference', + 'reasonCode', + 'reasonReference', + 'insurance', + 'supportingInfo', + 'specimen', + 'bodySite', + 'note', + 'patientInstruction', + 'relevantHistory' + ], + Slot: [ + 'identifier', + 'serviceCategory', + 'serviceType', + 'specialty', + 'appointmentType', + 'schedule', + 'status', + 'start', + 'end', + 'overbooked', + 'comment' + ], + Specimen: [ + 'identifier', + 'accessionIdentifier', + 'status', + 'type', + 'subject', + 'receivedTime', + 'parent', + 'request', + 'collection', + 'processing', + 'container', + 'condition', + 'note' + ], + SpecimenDefinition: ['identifier', 'typeCollected', 'patientPreparation', 'timeAspect', 'collection', 'typeTested'], + StructureDefinition: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'copyright', + 'keyword', + 'fhirVersion', + 'mapping', + 'kind', + 'abstract', + 'context', + 'contextInvariant', + 'type', + 'baseDefinition', + 'derivation', + 'snapshot', + 'differential' + ], + StructureMap: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'copyright', + 'structure', + 'import', + 'group' + ], + Subscription: ['status', 'contact', 'end', 'reason', 'criteria', 'error', 'channel'], + Substance: ['identifier', 'status', 'category', 'code', 'description', 'instance', 'ingredient'], + SubstanceNucleicAcid: ['sequenceType', 'numberOfSubunits', 'areaOfHybridisation', 'oligoNucleotideType', 'subunit'], + SubstancePolymer: ['class', 'geometry', 'copolymerConnectivity', 'modification', 'monomerSet', 'repeat'], + SubstanceProtein: ['sequenceType', 'numberOfSubunits', 'disulfideLinkage', 'subunit'], + SubstanceReferenceInformation: ['comment', 'gene', 'geneElement', 'classification', 'target'], + SubstanceSourceMaterial: [ + 'sourceMaterialClass', + 'sourceMaterialType', + 'sourceMaterialState', + 'organismId', + 'organismName', + 'parentSubstanceId', + 'parentSubstanceName', + 'countryOfOrigin', + 'geographicalLocation', + 'developmentStage', + 'fractionDescription', + 'organism', + 'partDescription' + ], + SubstanceSpecification: [ + 'identifier', + 'type', + 'status', + 'domain', + 'description', + 'source', + 'comment', + 'moiety', + 'property', + 'referenceInformation', + 'structure', + 'code', + 'name', + 'molecularWeight', + 'relationship', + 'nucleicAcid', + 'polymer', + 'protein', + 'sourceMaterial' + ], + SupplyDelivery: [ + 'identifier', + 'basedOn', + 'partOf', + 'status', + 'patient', + 'type', + 'suppliedItem', + 'occurrence', + 'supplier', + 'destination', + 'receiver' + ], + SupplyRequest: [ + 'identifier', + 'status', + 'category', + 'priority', + 'item', + 'quantity', + 'parameter', + 'occurrence', + 'authoredOn', + 'requester', + 'supplier', + 'reasonCode', + 'reasonReference', + 'deliverFrom', + 'deliverTo' + ], + Task: [ + 'identifier', + 'instantiatesCanonical', + 'instantiatesUri', + 'basedOn', + 'groupIdentifier', + 'partOf', + 'status', + 'statusReason', + 'businessStatus', + 'intent', + 'priority', + 'code', + 'description', + 'focus', + 'for', + 'encounter', + 'executionPeriod', + 'authoredOn', + 'lastModified', + 'requester', + 'performerType', + 'owner', + 'location', + 'reasonCode', + 'reasonReference', + 'insurance', + 'note', + 'relevantHistory', + 'restriction', + 'input', + 'output' + ], + TerminologyCapabilities: [ + 'url', + 'version', + 'name', + 'title', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'copyright', + 'kind', + 'software', + 'implementation', + 'lockedDate', + 'codeSystem', + 'expansion', + 'codeSearch', + 'validateCode', + 'translation', + 'closure' + ], + TestReport: [ + 'identifier', + 'name', + 'status', + 'testScript', + 'result', + 'score', + 'tester', + 'issued', + 'participant', + 'setup', + 'test', + 'teardown' + ], + TestScript: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'purpose', + 'copyright', + 'origin', + 'destination', + 'metadata', + 'fixture', + 'profile', + 'variable', + 'setup', + 'test', + 'teardown' + ], + ValueSet: [ + 'url', + 'identifier', + 'version', + 'name', + 'title', + 'status', + 'experimental', + 'date', + 'publisher', + 'contact', + 'description', + 'useContext', + 'jurisdiction', + 'immutable', + 'purpose', + 'copyright', + 'compose', + 'expansion' + ], + VerificationResult: [ + 'target', + 'targetLocation', + 'need', + 'status', + 'statusDate', + 'validationType', + 'validationProcess', + 'frequency', + 'lastPerformed', + 'nextScheduled', + 'failureAction', + 'primarySource', + 'attestation', + 'validator' + ], + VisionPrescription: [ + 'identifier', + 'status', + 'created', + 'patient', + 'encounter', + 'dateWritten', + 'prescriber', + 'lensSpecification' + ] +}; diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index 6a8379ca..8dbb9684 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -30,6 +30,7 @@ import { parseTimeStringAsUTC } from '../execution/ValueSetHelper'; import * as MeasureBundleHelpers from './MeasureBundleHelpers'; import { findLibraryReference } from './elm/ELMDependencyHelpers'; import { findClauseInLibrary, findNamedClausesInExpression } from './elm/ELMHelpers'; +import { parsedPropertyPaths } from '../code-attributes/propertyPaths'; const FHIR_QUERY_PATTERN_URL = 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-fhirQueryPattern'; /** @@ -465,18 +466,20 @@ function didEncounterDetailedValueFilterErrors(tbd: fhir4.Extension | GracefulEr function addMustSupport(allRetrieves: DataTypeQuery[], statement: ELMStatement, rootLib: ELM, allELM: ELM[]) { const propertyExpressions = findPropertyExpressions(statement, [], rootLib, allELM); - // TODO: double check that the property is applicable for the retrieve type before adding is as a mustSupport propertyExpressions.forEach(prop => { // find all matches for this property in allRetrieves const retrieveMatches = findRetrieveMatches(prop, allRetrieves, allELM); // add mustSupport for each match (if not already included) retrieveMatches.forEach(match => { - if (match.mustSupport) { - if (!match.mustSupport.includes(prop.property.path)) { - match.mustSupport.push(prop.property.path); + // double check that the property is applicable for the retrieve type before adding is as a mustSupport + if (match.dataType in parsedPropertyPaths && parsedPropertyPaths[match.dataType].includes(prop.property.path)) { + if (match.mustSupport) { + if (!match.mustSupport.includes(prop.property.path)) { + match.mustSupport.push(prop.property.path); + } + } else { + match.mustSupport = [prop.property.path]; } - } else { - match.mustSupport = [prop.property.path]; } }); });