diff --git a/src/calculation/Calculator.ts b/src/calculation/Calculator.ts index 4e292503..6c2ccc3e 100644 --- a/src/calculation/Calculator.ts +++ b/src/calculation/Calculator.ts @@ -23,10 +23,10 @@ import * as MeasureBundleHelpers from '../helpers/MeasureBundleHelpers'; import * as ResultsHelpers from './ClauseResultsBuilder'; import * as DataRequirementHelpers from '../helpers/DataRequirementHelpers'; import MeasureReportBuilder from './MeasureReportBuilder'; -import * as GapsInCareHelpers from '../gaps/GapsReportBuilder'; +import * as GapsReportBuilder from './GapsReportBuilder'; import { generateHTML, generateClauseCoverageHTML } from './HTMLBuilder'; -import { parseQueryInfo } from '../gaps/QueryFilterParser'; -import * as RetrievesHelper from '../gaps/RetrievesFinder'; +import { parseQueryInfo } from '../helpers/elm/QueryFilterParser'; +import * as RetrievesHelper from '../helpers/elm/RetrievesHelper'; import { GracefulError } from '../types/errors/GracefulError'; import { ErrorWithDebugInfo, @@ -40,7 +40,7 @@ import { pruneDetailedResults } from '../helpers/DetailedResultsHelpers'; import { clearElmInfoCache } from '../helpers/elm/ELMInfoCache'; import _, { omit } from 'lodash'; import { ELM } from '../types/ELMTypes'; -import { getReportBuilder } from '../helpers/reportBuilderFactory'; +import { getReportBuilder } from '../helpers/ReportBuilderFactory'; /** * Calculate measure against a set of patients. Returning detailed results for each patient and population group. @@ -536,7 +536,7 @@ export async function calculateGapsInCare( errorLog.push(...retrievesErrors); // Add detailed info to queries based on clause results - const gapsRetrieves = GapsInCareHelpers.processQueriesForGaps(baseRetrieves, dr); + const gapsRetrieves = GapsReportBuilder.processQueriesForGaps(baseRetrieves, dr); const grPromises = gapsRetrieves.map(async retrieve => { // If the retrieves have a localId for the query and a known library name, we can get more info @@ -558,11 +558,11 @@ export async function calculateGapsInCare( await Promise.all(grPromises); const { results: detailedGapsRetrieves, withErrors: reasonDetailErrors } = - GapsInCareHelpers.calculateReasonDetail(gapsRetrieves, improvementNotation, dr); + GapsReportBuilder.calculateReasonDetail(gapsRetrieves, improvementNotation, dr); errorLog.push(...reasonDetailErrors); - const { detectedIssues, withErrors: detectedIssueErrors } = GapsInCareHelpers.generateDetectedIssueResources( + const { detectedIssues, withErrors: detectedIssueErrors } = GapsReportBuilder.generateDetectedIssueResources( detailedGapsRetrieves, matchingMeasureReport, improvementNotation @@ -570,7 +570,7 @@ export async function calculateGapsInCare( errorLog.push(...detectedIssueErrors); const patient = res.patientObject?._json as fhir4.Patient; - const gapsBundle = GapsInCareHelpers.generateGapsInCareBundle(detectedIssues, matchingMeasureReport, patient); + const gapsBundle = GapsReportBuilder.generateGapsInCareBundle(detectedIssues, matchingMeasureReport, patient); result.push(gapsBundle); if (debugOutput && options.enableDebugOutput) { debugOutput.gaps = { diff --git a/src/calculation/ClauseResultsBuilder.ts b/src/calculation/ClauseResultsBuilder.ts index b55ae2ee..ecafabe6 100644 --- a/src/calculation/ClauseResultsBuilder.ts +++ b/src/calculation/ClauseResultsBuilder.ts @@ -1,4 +1,4 @@ -import * as ClauseResultsHelpers from './ClauseResultsHelpers'; +import * as ClauseResultsHelpers from '../helpers/ClauseResultsHelpers'; import * as MeasureBundleHelpers from '../helpers/MeasureBundleHelpers'; import * as ELMDependencyHelper from '../helpers/elm/ELMDependencyHelpers'; import { ELM, LibraryDependencyInfo } from '../types/ELMTypes'; diff --git a/src/gaps/GapsReportBuilder.ts b/src/calculation/GapsReportBuilder.ts similarity index 99% rename from src/gaps/GapsReportBuilder.ts rename to src/calculation/GapsReportBuilder.ts index 77dc4fdd..8d9c8334 100644 --- a/src/gaps/GapsReportBuilder.ts +++ b/src/calculation/GapsReportBuilder.ts @@ -9,12 +9,6 @@ import { ReasonDetailData } from '../types/Calculator'; import { FinalResult, ImprovementNotation, CareGapReasonCode, CareGapReasonCodeDisplay } from '../types/Enums'; -import { - flattenFilters, - generateDetailedCodeFilter, - generateDetailedDateFilter, - generateDetailedValueFilter -} from '../helpers/DataRequirementHelpers'; import { EqualsFilter, InFilter, @@ -26,6 +20,12 @@ import { } from '../types/QueryFilterTypes'; import { GracefulError, isOfTypeGracefulError } from '../types/errors/GracefulError'; import { compareValues } from '../helpers/ValueComparisonHelpers'; +import { + flattenFilters, + generateDetailedCodeFilter, + generateDetailedDateFilter, + generateDetailedValueFilter +} from '../helpers/elm/QueryFilterParser'; /** * Iterate through base queries and add clause results for parent query and retrieve @@ -249,7 +249,7 @@ export function generateGuidanceResponses( codeFilter: [{ ...dataTypeCodeFilter }] }; - addFiltersToDataRequirement(q, dataRequirement, withErrors); + addGapFiltersToDataRequirement(q, dataRequirement, withErrors); const guidanceResponse: fhir4.GuidanceResponse = { resourceType: 'GuidanceResponse', @@ -608,8 +608,8 @@ function getGapReasonCode(filter: AnyFilter): CareGapReasonCode | GracefulError * @param withErrors Errors object which will eventually be returned to the user if populated * @returns void, but populated the dataRequirement filters */ -export function addFiltersToDataRequirement( - q: GapsDataTypeQuery | DataTypeQuery, +export function addGapFiltersToDataRequirement( + q: GapsDataTypeQuery, dataRequirement: fhir4.DataRequirement, withErrors: GracefulError[] ) { diff --git a/src/calculation/ClauseResultsHelpers.ts b/src/helpers/ClauseResultsHelpers.ts similarity index 100% rename from src/calculation/ClauseResultsHelpers.ts rename to src/helpers/ClauseResultsHelpers.ts diff --git a/src/helpers/DataRequirementHelpers.ts b/src/helpers/DataRequirementHelpers.ts index 73514272..01d2c7cc 100644 --- a/src/helpers/DataRequirementHelpers.ts +++ b/src/helpers/DataRequirementHelpers.ts @@ -1,52 +1,27 @@ import { Extension } from 'fhir/r4'; import { CalculationOptions, DataTypeQuery, DRCalculationOutput } from '../types/Calculator'; import { GracefulError } from '../types/errors/GracefulError'; -import { - EqualsFilter, - InFilter, - DuringFilter, - AndFilter, - AnyFilter, - Filter, - NotNullFilter, - codeFilterQuery, - ValueFilter, - IsNullFilter -} from '../types/QueryFilterTypes'; +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 { ExtractedLibrary } from '../types/CQLTypes'; import * as Execution from '../execution/Execution'; import { UnexpectedResource } from '../types/errors/CustomErrors'; -import * as GapsInCareHelpers from '../gaps/GapsReportBuilder'; -import { parseQueryInfo } from '../gaps/QueryFilterParser'; -import * as RetrievesHelper from '../gaps/RetrievesFinder'; +import { + flattenFilters, + generateDetailedCodeFilter, + generateDetailedDateFilter, + generateDetailedValueFilter, + parseQueryInfo +} from './elm/QueryFilterParser'; +import * as RetrievesHelper from './elm/RetrievesHelper'; import { uniqBy } from 'lodash'; import { DateTime, Interval } from 'cql-execution'; import { parseTimeStringAsUTC } from '../execution/ValueSetHelper'; import * as MeasureBundleHelpers from './MeasureBundleHelpers'; const FHIR_QUERY_PATTERN_URL = 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-fhirQueryPattern'; -/** - * Take any nesting of base filters and AND filters and flatten into one list - * - * @param filter the root filter to flatten - * @returns a list of all filters used by this query at one level - */ -export function flattenFilters(filter: AnyFilter): AnyFilter[] { - if (filter.type !== 'and') { - return [filter]; - } else { - const a: AnyFilter[] = []; - (filter as AndFilter).children.forEach(c => { - a.push(...flattenFilters(c)); - }); - - return a; - } -} - /** * Returns a FHIR library containing data requirements, given a root library */ @@ -104,7 +79,7 @@ export async function getDataRequirements( results.dataRequirement = uniqBy( allRetrieves.map(retrieve => { const dr = generateDataRequirement(retrieve); - GapsInCareHelpers.addFiltersToDataRequirement(retrieve, dr, withErrors); + addFiltersToDataRequirement(retrieve, dr, withErrors); addFhirQueryPatternToDataRequirements(dr); return dr; }), @@ -124,109 +99,6 @@ export async function getDataRequirements( }; } -/** - * Map an EqualsFilter or InFilter into a FHIR DataRequirement codeFilter - * - * @param filter the filter to translate - * @returns codeFilter to be put on the DataRequirement - */ -export function generateDetailedCodeFilter( - filter: EqualsFilter | InFilter, - dataType?: string -): fhir4.DataRequirementCodeFilter | null { - const system: string | null = dataType ? codeLookup(dataType, filter.attribute) : null; - if (filter.type === 'equals') { - const equalsFilter = filter as EqualsFilter; - if (typeof equalsFilter.value === 'string') { - return { - path: equalsFilter.attribute, - code: [ - { - code: equalsFilter.value, - ...(system && { system: system }) - } - ] - }; - } - } else if (filter.type === 'in') { - const inFilter = filter as InFilter; - - if (inFilter.valueList?.every(v => typeof v === 'string')) { - return { - path: inFilter.attribute, - code: inFilter.valueList.map(v => ({ - code: v as string, - ...(system && { system: system }) - })) - }; - } else if (filter.valueCodingList) { - return { - path: filter.attribute, - code: filter.valueCodingList - }; - } - } - - return null; -} - -/** - * Map a during filter into a FHIR DataRequirement dateFilter - * - * @param filter the "during" filter to map - * @returns dateFilter for the dateFilter list of dataRequirement - */ -export function generateDetailedDateFilter(filter: DuringFilter): fhir4.DataRequirementDateFilter { - return { - path: filter.attribute, - valuePeriod: { start: filter.valuePeriod.start, end: filter.valuePeriod.end } - }; -} - -/** - * Map a filter into a FHIR DataRequirement valueFilter extension - * - * @param filter the filter to map - * @returns extension for the valueFilter list of dataRequirement - */ -export function generateDetailedValueFilter(filter: Filter): fhir4.Extension | GracefulError { - if (filter.type === 'notnull' || filter.type === 'isnull') { - const nullFilter = filter as NotNullFilter | IsNullFilter; - return { - url: 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-valueFilter', - extension: [ - { url: 'path', valueString: nullFilter.attribute }, - { url: 'comparator', valueCode: 'eq' }, - { url: 'value', valueString: nullFilter.type === 'notnull' ? 'not null' : 'null' } - ] - }; - } else if (filter.type === 'value') { - const valueFilter = filter as ValueFilter; - const valueExtension = { - url: 'value', - valueBoolean: valueFilter.valueBoolean, - valueInteger: valueFilter.valueInteger, - valueString: valueFilter.valueString, - valueQuantity: valueFilter.valueQuantity, - valueRange: valueFilter.valueRange, - valueRatio: valueFilter.valueRatio - }; - return { - url: 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-valueFilter', - extension: [ - { url: 'path', valueString: valueFilter.attribute }, - { url: 'comparator', valueCode: valueFilter.comparator }, - // Remove undefineds - JSON.parse(JSON.stringify(valueExtension)) - ] - }; - } else if (filter?.withError) { - return filter.withError; - } else { - return { message: `Detailed value filter is not yet supported for filter type ${filter.type}` } as GracefulError; - } -} - /** * Creates query string for the data requirement using either the code filter code or valueSet and * the specified endpoint, and adds a fhirQueryPattern extension to the data requirement that @@ -358,42 +230,6 @@ export function generateDataRequirement(retrieve: DataTypeQuery): fhir4.DataRequ return dataRequirement; } -/** - * Given a fhir dataType as a string and an attribute as a string, returns the url which outlines - * the code system used to define the valid inputs for the given attribute for the given dataType - * @param dataType - * @param attribute - * @returns string url for code system - */ -export function codeLookup(dataType: string, attribute: string): string | null { - const validDataTypes: string[] = ['Observation', 'Procedure', 'Encounter', 'MedicationRequest']; - - if (!validDataTypes.includes(dataType)) { - return null; - } else if (dataType === 'Observation' && attribute === 'status') { - return 'http://hl7.org/fhir/observation-status'; - } else if (dataType === 'Procedure' && attribute === 'status') { - return 'http://hl7.org/fhir/event-status'; - } else if (dataType === 'Encounter' && attribute === 'status') { - return 'http://hl7.org/fhir/encounter-status'; - } else if (dataType === 'MedicationRequest') { - switch (attribute) { - case 'status': - return 'http://hl7.org/fhir/CodeSystem/medicationrequest-status'; - - case 'intent': - return 'http://hl7.org/fhir/CodeSystem/medicationrequest-intent'; - - case 'priority': - return 'http://hl7.org/fhir/request-priority'; - - default: - return null; - } - } - return null; -} - /** * Extracts the measurement period information from either the options or effective period (in that order depending on presence) * and populates a parameters object including the extracted info to be passed into the parseQueryInfo function @@ -511,3 +347,67 @@ export function getFlattenedRelatedArtifacts( // unique the relatedArtifacts return uniqBy(relatedArtifacts, JSON.stringify); } + +/** + * + * @param q The query which contains the filters to add to the data requirement + * @param dataRequirement Data requirement to add date filters to + * @param withErrors Errors object which will eventually be returned to the user if populated + * @returns void, but populated the dataRequirement filters + */ +export function addFiltersToDataRequirement( + q: DataTypeQuery, + dataRequirement: fhir4.DataRequirement, + withErrors: GracefulError[] +) { + if (q.queryInfo) { + const relevantSource = q.queryInfo.sources.find(source => source.resourceType === q.dataType); + // if a source cannot be found that matches, exit the function + if (relevantSource) { + const detailedFilters = flattenFilters(q.queryInfo.filter); + + detailedFilters.forEach(df => { + // DuringFilter, etc. inherit from attribute filter (and have alias on them) + if (relevantSource.alias === (df as AttributeFilter).alias) { + if (df.type === 'equals' || df.type === 'in') { + const cf = generateDetailedCodeFilter(df as EqualsFilter | InFilter, q.dataType); + + if (cf !== null) { + if (dataRequirement.codeFilter) { + dataRequirement.codeFilter.push(cf); + } else { + dataRequirement.codeFilter = [cf]; + } + } + } else if (df.type === 'during') { + const dateFilter = generateDetailedDateFilter(df as DuringFilter); + if (dataRequirement.dateFilter) { + dataRequirement.dateFilter.push(dateFilter); + } else { + dataRequirement.dateFilter = [dateFilter]; + } + } else { + const valueFilter = generateDetailedValueFilter(df); + if (didEncounterDetailedValueFilterErrors(valueFilter)) { + withErrors.push(valueFilter); + } else if (valueFilter) { + if (dataRequirement.extension) { + dataRequirement.extension.push(valueFilter); + } else { + dataRequirement.extension = [valueFilter]; + } + } + } + } + }); + } + } +} + +function didEncounterDetailedValueFilterErrors(tbd: fhir4.Extension | GracefulError): tbd is GracefulError { + if ((tbd as GracefulError).message) { + return true; + } else { + return false; + } +} diff --git a/src/helpers/reportBuilderFactory.ts b/src/helpers/ReportBuilderFactory.ts similarity index 96% rename from src/helpers/reportBuilderFactory.ts rename to src/helpers/ReportBuilderFactory.ts index aeeb09ac..d844dc7b 100644 --- a/src/helpers/reportBuilderFactory.ts +++ b/src/helpers/ReportBuilderFactory.ts @@ -1,7 +1,7 @@ import { AbstractMeasureReportBuilder } from '../calculation/AbstractMeasureReportBuilder'; import { CompositeReportBuilder } from '../calculation/CompositeReportBuilder'; import MeasureReportBuilder from '../calculation/MeasureReportBuilder'; -import { extractCompositeMeasure, extractMeasureFromBundle } from '../helpers/MeasureBundleHelpers'; +import { extractCompositeMeasure, extractMeasureFromBundle } from './MeasureBundleHelpers'; import { CalculationOptions, PopulationGroupResult } from '../types/Calculator'; export function getReportBuilder( diff --git a/src/gaps/QueryFilterParser.ts b/src/helpers/elm/QueryFilterParser.ts similarity index 89% rename from src/gaps/QueryFilterParser.ts rename to src/helpers/elm/QueryFilterParser.ts index 98077de9..e6e1664f 100644 --- a/src/gaps/QueryFilterParser.ts +++ b/src/helpers/elm/QueryFilterParser.ts @@ -7,7 +7,7 @@ import { NamedTypeSpecifier, ListTypeSpecifier } from 'cql-execution'; -import { CQLPatient } from '../types/CQLPatient'; +import { CQLPatient } from '../../types/CQLPatient'; import { ELM, ELMEqual, @@ -43,7 +43,7 @@ import { ELMLessOrEqual, ELMRatio, ELMAliasRef -} from '../types/ELMTypes'; +} from '../../types/ELMTypes'; import { AndFilter, AnyFilter, @@ -61,12 +61,13 @@ import { ValueFilter, ValueFilterComparator, IsNullFilter, - QueryParserParams -} from '../types/QueryFilterTypes'; -import { findLibraryReference, findLibraryReferenceId } from '../helpers/elm/ELMDependencyHelpers'; -import { findClauseInLibrary } from '../helpers/elm/ELMHelpers'; -import { GracefulError, isOfTypeGracefulError } from '../types/errors/GracefulError'; -import { UnexpectedResource } from '../types/errors/CustomErrors'; + QueryParserParams, + Filter +} from '../../types/QueryFilterTypes'; +import { findLibraryReference, findLibraryReferenceId } from './ELMDependencyHelpers'; +import { findClauseInLibrary } from './ELMHelpers'; +import { GracefulError, isOfTypeGracefulError } from '../../types/errors/GracefulError'; +import { UnexpectedResource } from '../../types/errors/CustomErrors'; /** * Parse information about a query. This pulls out information about all sources in the query and attempts to parse @@ -1204,3 +1205,161 @@ export function interpretComparator( } return valueFilter; } + +/** + * Map an EqualsFilter or InFilter into a FHIR DataRequirement codeFilter + * + * @param filter the filter to translate + * @returns codeFilter to be put on the DataRequirement + */ +export function generateDetailedCodeFilter( + filter: EqualsFilter | InFilter, + dataType?: string +): fhir4.DataRequirementCodeFilter | null { + const system: string | null = dataType ? codeLookup(dataType, filter.attribute) : null; + if (filter.type === 'equals') { + const equalsFilter = filter as EqualsFilter; + if (typeof equalsFilter.value === 'string') { + return { + path: equalsFilter.attribute, + code: [ + { + code: equalsFilter.value, + ...(system && { system: system }) + } + ] + }; + } + } else if (filter.type === 'in') { + const inFilter = filter as InFilter; + + if (inFilter.valueList?.every(v => typeof v === 'string')) { + return { + path: inFilter.attribute, + code: inFilter.valueList.map(v => ({ + code: v as string, + ...(system && { system: system }) + })) + }; + } else if (filter.valueCodingList) { + return { + path: filter.attribute, + code: filter.valueCodingList + }; + } + } + + return null; +} + +/** + * Map a during filter into a FHIR DataRequirement dateFilter + * + * @param filter the "during" filter to map + * @returns dateFilter for the dateFilter list of dataRequirement + */ +export function generateDetailedDateFilter(filter: DuringFilter): fhir4.DataRequirementDateFilter { + return { + path: filter.attribute, + valuePeriod: { start: filter.valuePeriod.start, end: filter.valuePeriod.end } + }; +} + +/** + * Map a filter into a FHIR DataRequirement valueFilter extension + * + * @param filter the filter to map + * @returns extension for the valueFilter list of dataRequirement + */ +export function generateDetailedValueFilter(filter: Filter): fhir4.Extension | GracefulError { + if (filter.type === 'notnull' || filter.type === 'isnull') { + const nullFilter = filter as NotNullFilter | IsNullFilter; + return { + url: 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-valueFilter', + extension: [ + { url: 'path', valueString: nullFilter.attribute }, + { url: 'comparator', valueCode: 'eq' }, + { url: 'value', valueString: nullFilter.type === 'notnull' ? 'not null' : 'null' } + ] + }; + } else if (filter.type === 'value') { + const valueFilter = filter as ValueFilter; + const valueExtension = { + url: 'value', + valueBoolean: valueFilter.valueBoolean, + valueInteger: valueFilter.valueInteger, + valueString: valueFilter.valueString, + valueQuantity: valueFilter.valueQuantity, + valueRange: valueFilter.valueRange, + valueRatio: valueFilter.valueRatio + }; + return { + url: 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-valueFilter', + extension: [ + { url: 'path', valueString: valueFilter.attribute }, + { url: 'comparator', valueCode: valueFilter.comparator }, + // Remove undefineds + JSON.parse(JSON.stringify(valueExtension)) + ] + }; + } else if (filter?.withError) { + return filter.withError; + } else { + return { message: `Detailed value filter is not yet supported for filter type ${filter.type}` } as GracefulError; + } +} + +/** + * Given a fhir dataType as a string and an attribute as a string, returns the url which outlines + * the code system used to define the valid inputs for the given attribute for the given dataType + * @param dataType + * @param attribute + * @returns string url for code system + */ +export function codeLookup(dataType: string, attribute: string): string | null { + const validDataTypes: string[] = ['Observation', 'Procedure', 'Encounter', 'MedicationRequest']; + + if (!validDataTypes.includes(dataType)) { + return null; + } else if (dataType === 'Observation' && attribute === 'status') { + return 'http://hl7.org/fhir/observation-status'; + } else if (dataType === 'Procedure' && attribute === 'status') { + return 'http://hl7.org/fhir/event-status'; + } else if (dataType === 'Encounter' && attribute === 'status') { + return 'http://hl7.org/fhir/encounter-status'; + } else if (dataType === 'MedicationRequest') { + switch (attribute) { + case 'status': + return 'http://hl7.org/fhir/CodeSystem/medicationrequest-status'; + + case 'intent': + return 'http://hl7.org/fhir/CodeSystem/medicationrequest-intent'; + + case 'priority': + return 'http://hl7.org/fhir/request-priority'; + + default: + return null; + } + } + return null; +} + +/** + * Take any nesting of base filters and AND filters and flatten into one list + * + * @param filter the root filter to flatten + * @returns a list of all filters used by this query at one level + */ +export function flattenFilters(filter: AnyFilter): AnyFilter[] { + if (filter.type !== 'and') { + return [filter]; + } else { + const a: AnyFilter[] = []; + (filter as AndFilter).children.forEach(c => { + a.push(...flattenFilters(c)); + }); + + return a; + } +} diff --git a/src/gaps/RetrievesFinder.ts b/src/helpers/elm/RetrievesHelper.ts similarity index 95% rename from src/gaps/RetrievesFinder.ts rename to src/helpers/elm/RetrievesHelper.ts index 002b5a1b..2a3f0734 100644 --- a/src/gaps/RetrievesFinder.ts +++ b/src/helpers/elm/RetrievesHelper.ts @@ -9,12 +9,12 @@ import { ELMToList, ELMValueSetRef, ELMTuple -} from '../types/ELMTypes'; -import { DataTypeQuery, ExpressionStackEntry } from '../types/Calculator'; -import { findLibraryReference, findValueSetReference } from '../helpers/elm/ELMDependencyHelpers'; -import { findClauseInLibrary } from '../helpers/elm/ELMHelpers'; -import { GracefulError } from '../types/errors/GracefulError'; -import { UnexpectedResource } from '../types/errors/CustomErrors'; +} from '../../types/ELMTypes'; +import { DataTypeQuery, ExpressionStackEntry } from '../../types/Calculator'; +import { findLibraryReference, findValueSetReference } from './ELMDependencyHelpers'; +import { findClauseInLibrary } from './ELMHelpers'; +import { GracefulError } from '../../types/errors/GracefulError'; +import { UnexpectedResource } from '../../types/errors/CustomErrors'; /** * List of possible expressions that could be doing extra filtering on the result of a query diff --git a/src/index.ts b/src/index.ts index 36a1d1e3..c5a9b479 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ export { default as MeasureReportBuilder } from './calculation/MeasureReportBuil export * as ELMHelpers from './helpers/elm/ELMHelpers'; export * as ELMDependencyHelpers from './helpers/elm/ELMDependencyHelpers'; export * as MeasureBundleHelpers from './helpers/MeasureBundleHelpers'; -export * as RetrievesFinder from './gaps/RetrievesFinder'; +export * as RetrievesFinder from './helpers/elm/RetrievesHelper'; export { ValueSetResolver } from './execution/ValueSetResolver'; /** diff --git a/test/unit/ClauseResultsHelper.test.ts b/test/unit/ClauseResultsHelper.test.ts index 5c7a81f8..0345b2d7 100644 --- a/test/unit/ClauseResultsHelper.test.ts +++ b/test/unit/ClauseResultsHelper.test.ts @@ -1,4 +1,4 @@ -import * as ClauseResultsHelpers from '../../src/calculation/ClauseResultsHelpers'; +import * as ClauseResultsHelpers from '../../src/helpers/ClauseResultsHelpers'; import { ELM } from '../../src/types/ELMTypes'; import { getJSONFixture } from './helpers/testHelpers'; diff --git a/test/unit/DataRequirementHelpers.test.ts b/test/unit/DataRequirementHelpers.test.ts index 7eb503c7..21c01ce4 100644 --- a/test/unit/DataRequirementHelpers.test.ts +++ b/test/unit/DataRequirementHelpers.test.ts @@ -1,364 +1,11 @@ import * as DataRequirementHelpers from '../../src/helpers/DataRequirementHelpers'; -import { - AndFilter, - EqualsFilter, - DuringFilter, - InFilter, - NotNullFilter, - IsNullFilter, - UnknownFilter, - ValueFilter -} from '../../src/types/QueryFilterTypes'; import { CalculationOptions, DataTypeQuery } from '../../src/types/Calculator'; -import { GracefulError } from '../../src/types/errors/GracefulError'; import { DataRequirement } from 'fhir/r4'; import { DateTime, Interval } from 'cql-execution'; import moment from 'moment'; describe('DataRequirementHelpers', () => { - describe('Flatten Filters', () => { - test('should pass through standard equals filter', () => { - const equalsFilter: EqualsFilter = { - type: 'equals', - alias: 'R', - attribute: 'attr-0', - value: 'value-0' - }; - - const flattenedFilters = DataRequirementHelpers.flattenFilters(equalsFilter); - - expect(flattenedFilters).toHaveLength(1); - expect(flattenedFilters[0]).toEqual(equalsFilter); - }); - - test('should flatten AND filters', () => { - const equalsFilter0: EqualsFilter = { - type: 'equals', - alias: 'R', - attribute: 'attr-0', - value: 'value-0' - }; - - const equalsFilter1: EqualsFilter = { - type: 'equals', - alias: 'R', - attribute: 'attr-1', - value: 'value-1' - }; - - const duringFilter: DuringFilter = { - type: 'during', - alias: 'R', - attribute: 'attr-3', - valuePeriod: { - start: '2021-01-01', - end: '2021-12-01' - } - }; - - const filter: AndFilter = { - type: 'and', - children: [equalsFilter0, duringFilter, equalsFilter1] - }; - - const flattenedFilters = DataRequirementHelpers.flattenFilters(filter); - - expect(flattenedFilters).toHaveLength(3); - expect(flattenedFilters).toEqual( - expect.arrayContaining([ - expect.objectContaining({ ...equalsFilter0 }), - expect.objectContaining({ ...equalsFilter1 }), - expect.objectContaining({ ...duringFilter }) - ]) - ); - }); - }); - - describe('Code Filters', () => { - test('should return null for non equals or codeFilter', () => { - const fakeFilter: any = { - type: 'and' - }; - - expect(DataRequirementHelpers.generateDetailedCodeFilter(fakeFilter)).toBeNull(); - }); - - test('should return null for equals filter on non-string', () => { - const ef: EqualsFilter = { - type: 'equals', - value: 10, - attribute: 'attr-1', - alias: 'R' - }; - - expect(DataRequirementHelpers.generateDetailedCodeFilter(ef)).toBeNull(); - }); - test('equals filter should pull off attribute', () => { - const ef: EqualsFilter = { - type: 'equals', - alias: 'R', - attribute: 'attr-1', - value: 'value-1' - }; - - const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { - path: 'attr-1', - code: [ - { - code: 'value-1' - } - ] - }; - - expect(DataRequirementHelpers.generateDetailedCodeFilter(ef)).toEqual(expectedCodeFilter); - }); - - test('IN filter should pull off all of valueList', () => { - const inf: InFilter = { - type: 'in', - alias: 'R', - attribute: 'attr-1', - valueList: ['value-1', 'value-2', 'value-3'] - }; - - const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { - path: 'attr-1', - code: [ - { - code: 'value-1' - }, - { - code: 'value-2' - }, - { - code: 'value-3' - } - ] - }; - - expect(DataRequirementHelpers.generateDetailedCodeFilter(inf)).toEqual(expectedCodeFilter); - }); - - test('IN filter with non-string list should be ignored', () => { - const inf: InFilter = { - type: 'in', - alias: 'R', - attribute: 'attr-1', - valueList: [10] - }; - - expect(DataRequirementHelpers.generateDetailedCodeFilter(inf)).toBeNull(); - }); - - test('IN filter should pass through valueCodingList', () => { - const inf: InFilter = { - type: 'in', - alias: 'R', - attribute: 'attr-1', - valueCodingList: [ - { - system: 'system-1', - code: 'code-1', - display: 'display-code-1' - } - ] - }; - const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { - path: 'attr-1', - code: [ - { - system: 'system-1', - code: 'code-1', - display: 'display-code-1' - } - ] - }; - - expect(DataRequirementHelpers.generateDetailedCodeFilter(inf)).toEqual(expectedCodeFilter); - }); - - test('Equals filter should not add system attribute to output object for inappropriate dataType', () => { - const ef: EqualsFilter = { - type: 'equals', - alias: 'R', - attribute: 'status', - value: 'value1' - }; - - const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { - path: 'status', - code: [ - { - code: 'value1' - } - ] - }; - expect(DataRequirementHelpers.generateDetailedCodeFilter(ef, 'inappropriateDataType')).toEqual( - expectedCodeFilter - ); - }); - - test('Equals filter should add system attribute to output object', () => { - const ef: EqualsFilter = { - type: 'equals', - alias: 'R', - attribute: 'status', - value: 'value1' - }; - - const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { - path: 'status', - code: [ - { - code: 'value1', - system: 'http://hl7.org/fhir/encounter-status' - } - ] - }; - expect(DataRequirementHelpers.generateDetailedCodeFilter(ef, 'Encounter')).toEqual(expectedCodeFilter); - }); - - test('IN filter should add system attribute to output object', () => { - const inf: InFilter = { - type: 'in', - alias: 'R', - attribute: 'status', - valueList: ['value-1', 'value-2', 'value-3'] - }; - - const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { - path: 'status', - code: [ - { - code: 'value-1', - system: 'http://hl7.org/fhir/encounter-status' - }, - { - code: 'value-2', - system: 'http://hl7.org/fhir/encounter-status' - }, - { - code: 'value-3', - system: 'http://hl7.org/fhir/encounter-status' - } - ] - }; - expect(DataRequirementHelpers.generateDetailedCodeFilter(inf, 'Encounter')).toEqual(expectedCodeFilter); - }); - test('In filter should not add system attribute to output object for inappropriate dataType', () => { - const inf: InFilter = { - type: 'in', - alias: 'R', - attribute: 'status', - valueList: ['value1'] - }; - - const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { - path: 'status', - code: [ - { - code: 'value1' - } - ] - }; - expect(DataRequirementHelpers.generateDetailedCodeFilter(inf, 'inappropriateDataType')).toEqual( - expectedCodeFilter - ); - }); - }); - - describe('Date Filters', () => { - test('should pass through date filter', () => { - const df: DuringFilter = { - type: 'during', - alias: 'R', - attribute: 'attr-1', - valuePeriod: { - start: '2021-01-01', - end: '2021-12-31' - } - }; - - const expectedDateFilter: fhir4.DataRequirementDateFilter = { - path: 'attr-1', - valuePeriod: { - start: '2021-01-01', - end: '2021-12-31' - } - }; - - expect(DataRequirementHelpers.generateDetailedDateFilter(df)).toEqual(expectedDateFilter); - }); - }); - - describe('Value Filters', () => { - test('not null filter should create value filter', () => { - const nnf: NotNullFilter = { - type: 'notnull', - alias: 'R', - attribute: 'attr-1' - }; - - const expectedDetailFilter: fhir4.Extension = { - url: 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-valueFilter', - extension: [ - { url: 'path', valueString: 'attr-1' }, - { url: 'comparator', valueCode: 'eq' }, - { url: 'value', valueString: 'not null' } - ] - }; - - expect(DataRequirementHelpers.generateDetailedValueFilter(nnf)).toEqual(expectedDetailFilter); - }); - test('is null filter should create value filter', () => { - const inf: IsNullFilter = { - type: 'isnull', - alias: 'R', - attribute: 'attr-1' - }; - - const expectedDetailFilter: fhir4.Extension = { - url: 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-valueFilter', - extension: [ - { url: 'path', valueString: 'attr-1' }, - { url: 'comparator', valueCode: 'eq' }, - { url: 'value', valueString: 'null' } - ] - }; - - expect(DataRequirementHelpers.generateDetailedValueFilter(inf)).toEqual(expectedDetailFilter); - }); - test('filter of type value should create value filter', () => { - const valueFilter: ValueFilter = { - type: 'value', - attribute: 'attr-1', - comparator: 'gt', - valueBoolean: true - }; - const expectedDetailFilter: fhir4.Extension = { - url: 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-valueFilter', - extension: [ - { url: 'path', valueString: 'attr-1' }, - { url: 'comparator', valueCode: 'gt' }, - { url: 'value', valueBoolean: true } - ] - }; - expect(DataRequirementHelpers.generateDetailedValueFilter(valueFilter)).toEqual(expectedDetailFilter); - }); - - test('unknown filter should create a null for filter creation', () => { - const uf: UnknownFilter = { - type: 'unknown', - alias: 'R', - attribute: 'attr-1' - }; - const ge: GracefulError = { message: 'Detailed value filter is not yet supported for filter type unknown' }; - - expect(DataRequirementHelpers.generateDetailedValueFilter(uf)).toEqual(ge); - }); - }); - describe('generateDataRequirement', () => { test('can create DataRequirement with profile', () => { const dtq: DataTypeQuery = { @@ -429,51 +76,6 @@ describe('DataRequirementHelpers', () => { }); }); - describe('codeLookup', () => { - test('dataType is invalid', () => { - expect(DataRequirementHelpers.codeLookup('invalid', 'invalid')).toBeNull(); - }); - test('retrieves correct system url for dataType: MedicationRequest and attribute: status', () => { - expect(DataRequirementHelpers.codeLookup('MedicationRequest', 'status')).toEqual( - 'http://hl7.org/fhir/CodeSystem/medicationrequest-status' - ); - }); - test('retrieves correct system url for dataType: MedicationRequest and attribute: intent', () => { - expect(DataRequirementHelpers.codeLookup('MedicationRequest', 'intent')).toEqual( - 'http://hl7.org/fhir/CodeSystem/medicationrequest-intent' - ); - }); - test('retrieves correct system url for dataType: MedicationRequest and attribute: priority', () => { - expect(DataRequirementHelpers.codeLookup('MedicationRequest', 'priority')).toEqual( - 'http://hl7.org/fhir/request-priority' - ); - }); - test('retrieves correct system url for dataType: MedicationRequest and invalid attribute', () => { - expect(DataRequirementHelpers.codeLookup('MedicationRequest', 'nonsense')).toBeNull(); - }); - test('retrieves correct system url for dataType: Encounter and attribute: status', () => { - expect(DataRequirementHelpers.codeLookup('Encounter', 'status')).toEqual('http://hl7.org/fhir/encounter-status'); - }); - test('retrieves correct system url for dataType: Encounter and invalid attribute', () => { - expect(DataRequirementHelpers.codeLookup('Encounter', 'nonsense')).toBeNull(); - }); - - test('retrieves correct system url when dataType is Observation and attribute is status', () => { - expect(DataRequirementHelpers.codeLookup('Observation', 'status')).toEqual( - 'http://hl7.org/fhir/observation-status' - ); - }); - test('retrieves correct system url when dataType is Observation and attribute is invalid', () => { - expect(DataRequirementHelpers.codeLookup('Observation', 'nonsense')).toBeNull(); - }); - test('retrieves correct system url when dataType is Procedure and attribute is status', () => { - expect(DataRequirementHelpers.codeLookup('Procedure', 'status')).toEqual('http://hl7.org/fhir/event-status'); - }); - test('retrieves correct system url when dataType is Procedure and attribute is invalid', () => { - expect(DataRequirementHelpers.codeLookup('Procedure', 'nonsense')).toBeNull(); - }); - }); - describe('addFhirQueryPatternToDataRequirements', () => { test('add fhirQueryPattern extension with CodeFilter codes and valueSets', () => { const testDataReq: DataRequirement = { diff --git a/test/unit/GapsInCareHelpers.test.ts b/test/unit/GapsReportBuilder.test.ts similarity index 99% rename from test/unit/GapsInCareHelpers.test.ts rename to test/unit/GapsReportBuilder.test.ts index 8eb65bd7..fff5fd3c 100644 --- a/test/unit/GapsInCareHelpers.test.ts +++ b/test/unit/GapsReportBuilder.test.ts @@ -9,7 +9,7 @@ import { generateGuidanceResponses, generateReasonCode, hasDetailedReasonCode -} from '../../src/gaps/GapsReportBuilder'; +} from '../../src/calculation/GapsReportBuilder'; import { DataTypeQuery, DetailedPopulationGroupResult, diff --git a/test/unit/RetrievesFinder.test.ts b/test/unit/RetrievesFinder.test.ts index 3ad76af1..c76d0e17 100644 --- a/test/unit/RetrievesFinder.test.ts +++ b/test/unit/RetrievesFinder.test.ts @@ -1,5 +1,5 @@ import { getELMFixture } from './helpers/testHelpers'; -import { findRetrieves } from '../../src/gaps/RetrievesFinder'; +import { findRetrieves } from '../../src/helpers/elm/RetrievesHelper'; import { DataTypeQuery } from '../../src/types/Calculator'; import { GracefulError } from '../../src/types/errors/GracefulError'; diff --git a/test/unit/helpers/reportBuilderFactory.test.ts b/test/unit/helpers/ReportBuilderFactory.test.ts similarity index 95% rename from test/unit/helpers/reportBuilderFactory.test.ts rename to test/unit/helpers/ReportBuilderFactory.test.ts index eb89de86..ba9e67e7 100644 --- a/test/unit/helpers/reportBuilderFactory.test.ts +++ b/test/unit/helpers/ReportBuilderFactory.test.ts @@ -1,6 +1,6 @@ import MeasureReportBuilder from '../../../src/calculation/MeasureReportBuilder'; import { CompositeReportBuilder } from '../../../src/calculation/CompositeReportBuilder'; -import { getReportBuilder } from '../../../src/helpers/reportBuilderFactory'; +import { getReportBuilder } from '../../../src/helpers/ReportBuilderFactory'; import { getJSONFixture } from './testHelpers'; const simpleMeasure = getJSONFixture('measure/simple-measure.json') as fhir4.Measure; diff --git a/test/unit/queryFilters/parseQueryInfo.test.ts b/test/unit/queryFilters/QueryFilterParser.test.ts similarity index 60% rename from test/unit/queryFilters/parseQueryInfo.test.ts rename to test/unit/queryFilters/QueryFilterParser.test.ts index 87708f74..32b34473 100644 --- a/test/unit/queryFilters/parseQueryInfo.test.ts +++ b/test/unit/queryFilters/QueryFilterParser.test.ts @@ -1,11 +1,29 @@ import { getELMFixture } from '../helpers/testHelpers'; -import { parseQueryInfo } from '../../../src/gaps/QueryFilterParser'; +import { + parseQueryInfo, + generateDetailedCodeFilter, + generateDetailedDateFilter, + generateDetailedValueFilter, + codeLookup, + flattenFilters +} from '../../../src/helpers/elm/QueryFilterParser'; import { FHIRWrapper } from 'cql-exec-fhir'; import { DateTime, Interval } from 'cql-execution'; import { CQLPatient } from '../../../src/types/CQLPatient'; -import { QueryInfo, DuringFilter, AndFilter } from '../../../src/types/QueryFilterTypes'; +import { + QueryInfo, + DuringFilter, + AndFilter, + EqualsFilter, + InFilter, + NotNullFilter, + IsNullFilter, + ValueFilter, + UnknownFilter +} from '../../../src/types/QueryFilterTypes'; import { removeIntervalFromFilter } from '../helpers/queryFilterTestHelpers'; import { ELMLast } from '../../../src/types/ELMTypes'; +import { GracefulError } from '../../../src/types/errors/GracefulError'; const simpleQueryELM = getELMFixture('elm/queries/SimpleQueries.json'); const complexQueryELM = getELMFixture('elm/queries/ComplexQueries.json'); @@ -586,3 +604,382 @@ describe('Parse Query Info', () => { expect(queryInfo).toEqual(EXPECTED_INTERNAL_VALUE_COMPARISON_QUERY); }); }); + +describe('Code Filters', () => { + test('should return null for non equals or codeFilter', () => { + const fakeFilter: any = { + type: 'and' + }; + + expect(generateDetailedCodeFilter(fakeFilter)).toBeNull(); + }); + + test('should return null for equals filter on non-string', () => { + const ef: EqualsFilter = { + type: 'equals', + value: 10, + attribute: 'attr-1', + alias: 'R' + }; + + expect(generateDetailedCodeFilter(ef)).toBeNull(); + }); + test('equals filter should pull off attribute', () => { + const ef: EqualsFilter = { + type: 'equals', + alias: 'R', + attribute: 'attr-1', + value: 'value-1' + }; + + const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { + path: 'attr-1', + code: [ + { + code: 'value-1' + } + ] + }; + + expect(generateDetailedCodeFilter(ef)).toEqual(expectedCodeFilter); + }); + + test('IN filter should pull off all of valueList', () => { + const inf: InFilter = { + type: 'in', + alias: 'R', + attribute: 'attr-1', + valueList: ['value-1', 'value-2', 'value-3'] + }; + + const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { + path: 'attr-1', + code: [ + { + code: 'value-1' + }, + { + code: 'value-2' + }, + { + code: 'value-3' + } + ] + }; + + expect(generateDetailedCodeFilter(inf)).toEqual(expectedCodeFilter); + }); + + test('IN filter with non-string list should be ignored', () => { + const inf: InFilter = { + type: 'in', + alias: 'R', + attribute: 'attr-1', + valueList: [10] + }; + + expect(generateDetailedCodeFilter(inf)).toBeNull(); + }); + + test('IN filter should pass through valueCodingList', () => { + const inf: InFilter = { + type: 'in', + alias: 'R', + attribute: 'attr-1', + valueCodingList: [ + { + system: 'system-1', + code: 'code-1', + display: 'display-code-1' + } + ] + }; + const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { + path: 'attr-1', + code: [ + { + system: 'system-1', + code: 'code-1', + display: 'display-code-1' + } + ] + }; + + expect(generateDetailedCodeFilter(inf)).toEqual(expectedCodeFilter); + }); + + test('Equals filter should not add system attribute to output object for inappropriate dataType', () => { + const ef: EqualsFilter = { + type: 'equals', + alias: 'R', + attribute: 'status', + value: 'value1' + }; + + const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { + path: 'status', + code: [ + { + code: 'value1' + } + ] + }; + expect(generateDetailedCodeFilter(ef, 'inappropriateDataType')).toEqual(expectedCodeFilter); + }); + + test('Equals filter should add system attribute to output object', () => { + const ef: EqualsFilter = { + type: 'equals', + alias: 'R', + attribute: 'status', + value: 'value1' + }; + + const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { + path: 'status', + code: [ + { + code: 'value1', + system: 'http://hl7.org/fhir/encounter-status' + } + ] + }; + expect(generateDetailedCodeFilter(ef, 'Encounter')).toEqual(expectedCodeFilter); + }); + + test('IN filter should add system attribute to output object', () => { + const inf: InFilter = { + type: 'in', + alias: 'R', + attribute: 'status', + valueList: ['value-1', 'value-2', 'value-3'] + }; + + const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { + path: 'status', + code: [ + { + code: 'value-1', + system: 'http://hl7.org/fhir/encounter-status' + }, + { + code: 'value-2', + system: 'http://hl7.org/fhir/encounter-status' + }, + { + code: 'value-3', + system: 'http://hl7.org/fhir/encounter-status' + } + ] + }; + expect(generateDetailedCodeFilter(inf, 'Encounter')).toEqual(expectedCodeFilter); + }); + test('In filter should not add system attribute to output object for inappropriate dataType', () => { + const inf: InFilter = { + type: 'in', + alias: 'R', + attribute: 'status', + valueList: ['value1'] + }; + + const expectedCodeFilter: fhir4.DataRequirementCodeFilter = { + path: 'status', + code: [ + { + code: 'value1' + } + ] + }; + expect(generateDetailedCodeFilter(inf, 'inappropriateDataType')).toEqual(expectedCodeFilter); + }); +}); + +describe('Date Filters', () => { + test('should pass through date filter', () => { + const df: DuringFilter = { + type: 'during', + alias: 'R', + attribute: 'attr-1', + valuePeriod: { + start: '2021-01-01', + end: '2021-12-31' + } + }; + + const expectedDateFilter: fhir4.DataRequirementDateFilter = { + path: 'attr-1', + valuePeriod: { + start: '2021-01-01', + end: '2021-12-31' + } + }; + + expect(generateDetailedDateFilter(df)).toEqual(expectedDateFilter); + }); +}); + +describe('Value Filters', () => { + test('not null filter should create value filter', () => { + const nnf: NotNullFilter = { + type: 'notnull', + alias: 'R', + attribute: 'attr-1' + }; + + const expectedDetailFilter: fhir4.Extension = { + url: 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-valueFilter', + extension: [ + { url: 'path', valueString: 'attr-1' }, + { url: 'comparator', valueCode: 'eq' }, + { url: 'value', valueString: 'not null' } + ] + }; + + expect(generateDetailedValueFilter(nnf)).toEqual(expectedDetailFilter); + }); + test('is null filter should create value filter', () => { + const inf: IsNullFilter = { + type: 'isnull', + alias: 'R', + attribute: 'attr-1' + }; + + const expectedDetailFilter: fhir4.Extension = { + url: 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-valueFilter', + extension: [ + { url: 'path', valueString: 'attr-1' }, + { url: 'comparator', valueCode: 'eq' }, + { url: 'value', valueString: 'null' } + ] + }; + + expect(generateDetailedValueFilter(inf)).toEqual(expectedDetailFilter); + }); + test('filter of type value should create value filter', () => { + const valueFilter: ValueFilter = { + type: 'value', + attribute: 'attr-1', + comparator: 'gt', + valueBoolean: true + }; + const expectedDetailFilter: fhir4.Extension = { + url: 'http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-valueFilter', + extension: [ + { url: 'path', valueString: 'attr-1' }, + { url: 'comparator', valueCode: 'gt' }, + { url: 'value', valueBoolean: true } + ] + }; + expect(generateDetailedValueFilter(valueFilter)).toEqual(expectedDetailFilter); + }); + + test('unknown filter should create a null for filter creation', () => { + const uf: UnknownFilter = { + type: 'unknown', + alias: 'R', + attribute: 'attr-1' + }; + const ge: GracefulError = { message: 'Detailed value filter is not yet supported for filter type unknown' }; + + expect(generateDetailedValueFilter(uf)).toEqual(ge); + }); +}); + +describe('codeLookup', () => { + test('dataType is invalid', () => { + expect(codeLookup('invalid', 'invalid')).toBeNull(); + }); + test('retrieves correct system url for dataType: MedicationRequest and attribute: status', () => { + expect(codeLookup('MedicationRequest', 'status')).toEqual( + 'http://hl7.org/fhir/CodeSystem/medicationrequest-status' + ); + }); + test('retrieves correct system url for dataType: MedicationRequest and attribute: intent', () => { + expect(codeLookup('MedicationRequest', 'intent')).toEqual( + 'http://hl7.org/fhir/CodeSystem/medicationrequest-intent' + ); + }); + test('retrieves correct system url for dataType: MedicationRequest and attribute: priority', () => { + expect(codeLookup('MedicationRequest', 'priority')).toEqual('http://hl7.org/fhir/request-priority'); + }); + test('retrieves correct system url for dataType: MedicationRequest and invalid attribute', () => { + expect(codeLookup('MedicationRequest', 'nonsense')).toBeNull(); + }); + test('retrieves correct system url for dataType: Encounter and attribute: status', () => { + expect(codeLookup('Encounter', 'status')).toEqual('http://hl7.org/fhir/encounter-status'); + }); + test('retrieves correct system url for dataType: Encounter and invalid attribute', () => { + expect(codeLookup('Encounter', 'nonsense')).toBeNull(); + }); + + test('retrieves correct system url when dataType is Observation and attribute is status', () => { + expect(codeLookup('Observation', 'status')).toEqual('http://hl7.org/fhir/observation-status'); + }); + test('retrieves correct system url when dataType is Observation and attribute is invalid', () => { + expect(codeLookup('Observation', 'nonsense')).toBeNull(); + }); + test('retrieves correct system url when dataType is Procedure and attribute is status', () => { + expect(codeLookup('Procedure', 'status')).toEqual('http://hl7.org/fhir/event-status'); + }); + test('retrieves correct system url when dataType is Procedure and attribute is invalid', () => { + expect(codeLookup('Procedure', 'nonsense')).toBeNull(); + }); +}); + +describe('Flatten Filters', () => { + test('should pass through standard equals filter', () => { + const equalsFilter: EqualsFilter = { + type: 'equals', + alias: 'R', + attribute: 'attr-0', + value: 'value-0' + }; + + const flattenedFilters = flattenFilters(equalsFilter); + + expect(flattenedFilters).toHaveLength(1); + expect(flattenedFilters[0]).toEqual(equalsFilter); + }); + + test('should flatten AND filters', () => { + const equalsFilter0: EqualsFilter = { + type: 'equals', + alias: 'R', + attribute: 'attr-0', + value: 'value-0' + }; + + const equalsFilter1: EqualsFilter = { + type: 'equals', + alias: 'R', + attribute: 'attr-1', + value: 'value-1' + }; + + const duringFilter: DuringFilter = { + type: 'during', + alias: 'R', + attribute: 'attr-3', + valuePeriod: { + start: '2021-01-01', + end: '2021-12-01' + } + }; + + const filter: AndFilter = { + type: 'and', + children: [equalsFilter0, duringFilter, equalsFilter1] + }; + + const flattenedFilters = flattenFilters(filter); + + expect(flattenedFilters).toHaveLength(3); + expect(flattenedFilters).toEqual( + expect.arrayContaining([ + expect.objectContaining({ ...equalsFilter0 }), + expect.objectContaining({ ...equalsFilter1 }), + expect.objectContaining({ ...duringFilter }) + ]) + ); + }); +}); diff --git a/test/unit/queryFilters/interpretComparator.test.ts b/test/unit/queryFilters/interpretComparator.test.ts index 1e221c7d..87db20e9 100644 --- a/test/unit/queryFilters/interpretComparator.test.ts +++ b/test/unit/queryFilters/interpretComparator.test.ts @@ -8,7 +8,7 @@ import { ELMQuantity, ELMRatio } from '../../../src/types/ELMTypes'; -import * as QueryFilter from '../../../src/gaps/QueryFilterParser'; +import * as QueryFilter from '../../../src/helpers/elm/QueryFilterParser'; import { getELMFixture } from '../helpers/testHelpers'; const ValueQueries = getELMFixture('elm/queries/ValueQueries.json'); diff --git a/test/unit/queryFilters/interpretEquivalent.test.ts b/test/unit/queryFilters/interpretEquivalent.test.ts index c7266b41..8574c5ef 100644 --- a/test/unit/queryFilters/interpretEquivalent.test.ts +++ b/test/unit/queryFilters/interpretEquivalent.test.ts @@ -1,5 +1,5 @@ import { getELMFixture } from '../helpers/testHelpers'; -import * as QueryFilter from '../../../src/gaps/QueryFilterParser'; +import * as QueryFilter from '../../../src/helpers/elm/QueryFilterParser'; import { ELMEquivalent } from '../../../src/types/ELMTypes'; // to use as a library parameter for tests diff --git a/test/unit/queryFilters/interpretExpression.test.ts b/test/unit/queryFilters/interpretExpression.test.ts index 726ba5fb..513f23ae 100644 --- a/test/unit/queryFilters/interpretExpression.test.ts +++ b/test/unit/queryFilters/interpretExpression.test.ts @@ -1,7 +1,7 @@ import { FHIRWrapper } from 'cql-exec-fhir'; import { CQLPatient } from '../../../src/types/CQLPatient'; import { getELMFixture } from '../helpers/testHelpers'; -import * as QueryFilter from '../../../src/gaps/QueryFilterParser'; +import * as QueryFilter from '../../../src/helpers/elm/QueryFilterParser'; import { ELMFunctionRef } from '../../../src/types/ELMTypes'; // to use as a library parameter for tests diff --git a/test/unit/queryFilters/interpretFunctionRef.test.ts b/test/unit/queryFilters/interpretFunctionRef.test.ts index 8ed7cc66..bbf2d647 100644 --- a/test/unit/queryFilters/interpretFunctionRef.test.ts +++ b/test/unit/queryFilters/interpretFunctionRef.test.ts @@ -1,4 +1,4 @@ -import * as QueryFilter from '../../../src/gaps/QueryFilterParser'; +import * as QueryFilter from '../../../src/helpers/elm/QueryFilterParser'; import { ELMFunctionRef } from '../../../src/types/ELMTypes'; import { getELMFixture } from '../helpers/testHelpers'; diff --git a/test/unit/queryFilters/interpretGreaterOrEqual.test.ts b/test/unit/queryFilters/interpretGreaterOrEqual.test.ts index c9b5b00d..08a37f37 100644 --- a/test/unit/queryFilters/interpretGreaterOrEqual.test.ts +++ b/test/unit/queryFilters/interpretGreaterOrEqual.test.ts @@ -1,7 +1,7 @@ import { FHIRWrapper } from 'cql-exec-fhir'; import { CQLPatient } from '../../../src/types/CQLPatient'; import { getELMFixture } from '../helpers/testHelpers'; -import * as QueryFilter from '../../../src/gaps/QueryFilterParser'; +import * as QueryFilter from '../../../src/helpers/elm/QueryFilterParser'; import { ELMGreaterOrEqual } from '../../../src/types/ELMTypes'; import { DuringFilter, UnknownFilter } from '../../../src/types/QueryFilterTypes'; diff --git a/test/unit/queryFilters/interpretIn.test.ts b/test/unit/queryFilters/interpretIn.test.ts index f05c7469..75e1f116 100644 --- a/test/unit/queryFilters/interpretIn.test.ts +++ b/test/unit/queryFilters/interpretIn.test.ts @@ -1,6 +1,6 @@ import { getELMFixture } from '../helpers/testHelpers'; import { DateTime, Interval } from 'cql-execution'; -import * as QueryFilter from '../../../src/gaps/QueryFilterParser'; +import * as QueryFilter from '../../../src/helpers/elm/QueryFilterParser'; import { ELMIn } from '../../../src/types/ELMTypes'; import { DuringFilter } from '../../../src/types/QueryFilterTypes'; diff --git a/test/unit/queryFilters/interpretIncludedIn.test.ts b/test/unit/queryFilters/interpretIncludedIn.test.ts index f52239cf..9d7ed3f0 100644 --- a/test/unit/queryFilters/interpretIncludedIn.test.ts +++ b/test/unit/queryFilters/interpretIncludedIn.test.ts @@ -1,6 +1,6 @@ import { getELMFixture } from '../helpers/testHelpers'; import { DateTime, Interval } from 'cql-execution'; -import * as QueryFilter from '../../../src/gaps/QueryFilterParser'; +import * as QueryFilter from '../../../src/helpers/elm/QueryFilterParser'; import { ELMIncludedIn } from '../../../src/types/ELMTypes'; import { DuringFilter, UnknownFilter } from '../../../src/types/QueryFilterTypes'; diff --git a/test/unit/queryFilters/interpretIsNull.test.ts b/test/unit/queryFilters/interpretIsNull.test.ts index 4ce3d0b1..b46e2ffe 100644 --- a/test/unit/queryFilters/interpretIsNull.test.ts +++ b/test/unit/queryFilters/interpretIsNull.test.ts @@ -1,4 +1,4 @@ -import * as QueryFilter from '../../../src/gaps/QueryFilterParser'; +import * as QueryFilter from '../../../src/helpers/elm/QueryFilterParser'; import { ELMIsNull } from '../../../src/types/ELMTypes'; // Copied from SimpleQueries.json "Last Of ReferencedQueryIsNull" diff --git a/test/unit/queryFilters/interpretNot.test.ts b/test/unit/queryFilters/interpretNot.test.ts index 3304105f..66683ee0 100644 --- a/test/unit/queryFilters/interpretNot.test.ts +++ b/test/unit/queryFilters/interpretNot.test.ts @@ -1,4 +1,4 @@ -import * as QueryFilter from '../../../src/gaps/QueryFilterParser'; +import * as QueryFilter from '../../../src/helpers/elm/QueryFilterParser'; import { ELMNot } from '../../../src/types/ELMTypes'; import { UnknownFilter } from '../../../src/types/QueryFilterTypes';