From 412518daf01382c4f8f1a7e3c25ec62500317ddd Mon Sep 17 00:00:00 2001 From: Nestor Carvantes Date: Fri, 16 Apr 2021 00:27:12 -0700 Subject: [PATCH 1/3] feat: support number and quantity type parameters --- src/QueryBuilder/index.ts | 8 +- .../typeQueries/common/number.test.ts | 48 +++++ src/QueryBuilder/typeQueries/common/number.ts | 46 +++++ .../typeQueries/common/prefixRange.ts | 124 ++++++++++++ src/QueryBuilder/typeQueries/dateQuery.ts | 81 +------- .../typeQueries/numberQuery.test.ts | 85 +++++++++ src/QueryBuilder/typeQueries/numberQuery.ts | 44 +++++ .../typeQueries/quantityQuery.test.ts | 177 ++++++++++++++++++ src/QueryBuilder/typeQueries/quantityQuery.ts | 86 +++++++++ 9 files changed, 619 insertions(+), 80 deletions(-) create mode 100644 src/QueryBuilder/typeQueries/common/number.test.ts create mode 100644 src/QueryBuilder/typeQueries/common/number.ts create mode 100644 src/QueryBuilder/typeQueries/common/prefixRange.ts create mode 100644 src/QueryBuilder/typeQueries/numberQuery.test.ts create mode 100644 src/QueryBuilder/typeQueries/numberQuery.ts create mode 100644 src/QueryBuilder/typeQueries/quantityQuery.test.ts create mode 100644 src/QueryBuilder/typeQueries/quantityQuery.ts diff --git a/src/QueryBuilder/index.ts b/src/QueryBuilder/index.ts index 628571a..4cf91e7 100644 --- a/src/QueryBuilder/index.ts +++ b/src/QueryBuilder/index.ts @@ -9,6 +9,8 @@ import { CompiledSearchParam, FHIRSearchParametersRegistry, SearchParam } from ' import { stringQuery } from './typeQueries/stringQuery'; import { dateQuery } from './typeQueries/dateQuery'; import { tokenQuery } from './typeQueries/tokenQuery'; +import { numberQuery } from './typeQueries/numberQuery'; +import { quantityQuery } from './typeQueries/quantityQuery'; function typeQueryWithConditions( searchParam: SearchParam, @@ -26,9 +28,13 @@ function typeQueryWithConditions( case 'token': typeQuery = tokenQuery(compiledSearchParam, searchValue); break; - case 'composite': case 'number': + typeQuery = numberQuery(compiledSearchParam, searchValue); + break; case 'quantity': + typeQuery = quantityQuery(compiledSearchParam, searchValue); + break; + case 'composite': case 'reference': case 'special': case 'uri': diff --git a/src/QueryBuilder/typeQueries/common/number.test.ts b/src/QueryBuilder/typeQueries/common/number.test.ts new file mode 100644 index 0000000..7385768 --- /dev/null +++ b/src/QueryBuilder/typeQueries/common/number.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import each from 'jest-each'; +import { InvalidSearchParameterError } from 'fhir-works-on-aws-interface'; +import { parseNumber } from './number'; + +describe('parseNumber', () => { + describe('valid inputs', () => { + each([ + ['-100', 5e-1], + ['100', 5e-1], + ['10.0', 5e-2], + ['1e2', 5e1], + ['1.0e2', 5], + ['1.0E2', 5], + ['5.4', 5e-2], + ['5.40e-3', 5e-6], + ['5.4e-3', 5e-5], + // Below cases are not truly scientific notation but we allow them since they are valid numbers. + ['10e1', 5], + ['100e0', 5e-1], + ['54.0e-4', 5e-6], + ]).test('%s', (string: string, delta: number) => { + const result = parseNumber(string); + expect(result.number).toBeCloseTo(Number(string), 7); + expect(result.implicitRange.end).toBeCloseTo(Number(string) + delta, 8); + expect(result.implicitRange.start).toBeCloseTo(Number(string) - delta, 8); + }); + }); + + describe('invalid inputs', () => { + each([ + ['not a number at all'], + [' 100 '], + ['1.2.3'], + ['1,2'], + ['+-1'], + ['1+1'], + ['1.3e12xx'], + ['1.3a12'], + ]).test('%s', (string: string) => { + expect(() => parseNumber(string)).toThrow(InvalidSearchParameterError); + }); + }); +}); diff --git a/src/QueryBuilder/typeQueries/common/number.ts b/src/QueryBuilder/typeQueries/common/number.ts new file mode 100644 index 0000000..96d8d50 --- /dev/null +++ b/src/QueryBuilder/typeQueries/common/number.ts @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + * + */ + +import { InvalidSearchParameterError } from 'fhir-works-on-aws-interface'; + +interface FhirNumber { + number: number; + implicitRange: { + start: number; + end: number; + }; +} + +const NUMBER_REGEX = /^(?[+-])?(?\d+)(\.(?\d+))?([eE](?[+-]?\d+))?$/; + +// eslint-disable-next-line import/prefer-default-export +export const parseNumber = (numberString: string): FhirNumber => { + const match = numberString.match(NUMBER_REGEX); + if (match === null) { + throw new InvalidSearchParameterError(`Invalid number in search parameter: ${numberString}`); + } + + const { decimals = '', exp } = match.groups!; + + let significantFiguresDeltaExp = 0; + // FHIR considers ALL written digits to be significant figures + // e.g. 100 has 3 significant figures (this is contrary to the more common definition where trailing zeroes are NOT significant figures) + // See: https://www.hl7.org/fhir/search.html#number + if (exp !== undefined) { + significantFiguresDeltaExp = Number(exp) - (decimals.length + 1); + } else { + significantFiguresDeltaExp = -(decimals.length + 1); + } + + const numberValue = Number(numberString); + return { + number: numberValue, + implicitRange: { + start: numberValue - 5 * 10 ** significantFiguresDeltaExp, + end: numberValue + 5 * 10 ** significantFiguresDeltaExp, + }, + }; +}; diff --git a/src/QueryBuilder/typeQueries/common/prefixRange.ts b/src/QueryBuilder/typeQueries/common/prefixRange.ts new file mode 100644 index 0000000..24ca164 --- /dev/null +++ b/src/QueryBuilder/typeQueries/common/prefixRange.ts @@ -0,0 +1,124 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InvalidSearchParameterError } from 'fhir-works-on-aws-interface'; + +interface Range { + start: Date | number; + end: Date | number; +} + +interface NumberRange { + start: number; + end: number; +} + +interface DateRange { + start: Date; + end: Date; +} + +const prefixRange = (prefix: string, range: Range, path: string): any => { + const { start, end } = range; + + // See https://www.hl7.org/fhir/search.html#prefix + if (prefix !== 'ne') { + let elasticSearchRange; + switch (prefix) { + case 'eq': // equal + elasticSearchRange = { + gte: start, + lte: end, + }; + break; + case 'lt': // less than + elasticSearchRange = { + lt: end, + }; + break; + case 'le': // less or equal + elasticSearchRange = { + lte: end, + }; + break; + case 'gt': // greater than + elasticSearchRange = { + gt: start, + }; + break; + case 'ge': // greater or equal + elasticSearchRange = { + gte: start, + }; + break; + case 'sa': // starts after + elasticSearchRange = { + gt: end, + }; + break; + case 'eb': // ends before + elasticSearchRange = { + lt: start, + }; + break; + case 'ap': // approximately + throw new InvalidSearchParameterError('Unsupported prefix: ap'); + default: + // this should never happen + throw new Error(`unknown search prefix: ${prefix}`); + } + + return { + range: { + [path]: elasticSearchRange, + }, + }; + } + + // ne prefix is the only case that requires a bool query; + const neQuery = { + bool: { + should: [ + { + range: { + [path]: { + gt: end, + }, + }, + }, + { + range: { + [path]: { + lt: start, + }, + }, + }, + ], + }, + }; + + return neQuery; +}; + +export const prefixRangeNumber = (prefix: string, number: number, implicitRange: NumberRange, path: string): any => { + if (prefix === 'eq' || prefix === 'ne') { + return prefixRange(prefix, implicitRange, path); + } + // When a comparison prefix in the set lgt, lt, ge, le, sa & eb is provided, the implicit precision of the number is ignored, + // and they are treated as if they have arbitrarily high precision + // https://www.hl7.org/fhir/search.html#number + return prefixRange( + prefix, + { + start: number, + end: number, + }, + path, + ); +}; + +export const prefixRangeDate = (prefix: string, range: DateRange, path: string): any => { + return prefixRange(prefix, range, path); +}; diff --git a/src/QueryBuilder/typeQueries/dateQuery.ts b/src/QueryBuilder/typeQueries/dateQuery.ts index 8de270c..c0ab2f2 100644 --- a/src/QueryBuilder/typeQueries/dateQuery.ts +++ b/src/QueryBuilder/typeQueries/dateQuery.ts @@ -10,6 +10,7 @@ import lastDayOfMonth from 'date-fns/lastDayOfMonth'; import set from 'date-fns/set'; import { InvalidSearchParameterError } from 'fhir-works-on-aws-interface'; import { CompiledSearchParam } from '../../FHIRSearchParametersRegistry'; +import { prefixRangeDate } from './common/prefixRange'; interface DateSearchParameter { prefix: string; @@ -69,83 +70,5 @@ export const parseDateSearchParam = (param: string): DateSearchParameter => { // eslint-disable-next-line import/prefer-default-export export const dateQuery = (compiledSearchParam: CompiledSearchParam, value: string): any => { const { prefix, range } = parseDateSearchParam(value); - const { start, end } = range; - - // See https://www.hl7.org/fhir/search.html#prefix - if (prefix !== 'ne') { - let elasticSearchRange; - switch (prefix) { - case 'eq': // equal - elasticSearchRange = { - gte: start, - lte: end, - }; - break; - case 'lt': // less than - elasticSearchRange = { - lt: end, - }; - break; - case 'le': // less or equal - elasticSearchRange = { - lte: end, - }; - break; - case 'gt': // greater than - elasticSearchRange = { - gt: start, - }; - break; - case 'ge': // greater or equal - elasticSearchRange = { - gte: start, - }; - break; - case 'sa': // starts after - elasticSearchRange = { - gt: end, - }; - break; - case 'eb': // ends before - elasticSearchRange = { - lt: start, - }; - break; - case 'ap': // approximately - throw new InvalidSearchParameterError('Unsupported prefix: ap'); - default: - // this should never happen - throw new Error(`unknown search prefix: ${prefix}`); - } - - return { - range: { - [compiledSearchParam.path]: elasticSearchRange, - }, - }; - } - - // ne prefix is the only case that requires a bool query; - const neQuery = { - bool: { - should: [ - { - range: { - [compiledSearchParam.path]: { - gt: end, - }, - }, - }, - { - range: { - [compiledSearchParam.path]: { - lt: start, - }, - }, - }, - ], - }, - }; - - return neQuery; + return prefixRangeDate(prefix, range, compiledSearchParam.path); }; diff --git a/src/QueryBuilder/typeQueries/numberQuery.test.ts b/src/QueryBuilder/typeQueries/numberQuery.test.ts new file mode 100644 index 0000000..659f723 --- /dev/null +++ b/src/QueryBuilder/typeQueries/numberQuery.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import each from 'jest-each'; +import { InvalidSearchParameterError } from 'fhir-works-on-aws-interface'; +import { numberQuery } from './numberQuery'; +import { FHIRSearchParametersRegistry } from '../../FHIRSearchParametersRegistry'; + +const fhirSearchParametersRegistry = new FHIRSearchParametersRegistry('4.0.1'); +const factorOverrideParam = fhirSearchParametersRegistry.getSearchParameter('ChargeItem', 'factor-override')! + .compiled[0]; + +describe('numberQuery', () => { + describe('valid inputs', () => { + test('10', () => { + expect(numberQuery(factorOverrideParam, '10')).toMatchInlineSnapshot(` + Object { + "range": Object { + "factorOverride": Object { + "gte": 9.5, + "lte": 10.5, + }, + }, + } + `); + }); + test('lt10', () => { + expect(numberQuery(factorOverrideParam, 'lt10')).toMatchInlineSnapshot(` + Object { + "range": Object { + "factorOverride": Object { + "lt": 10, + }, + }, + } + `); + }); + test('10.57', () => { + expect(numberQuery(factorOverrideParam, '10.57')).toMatchInlineSnapshot(` + Object { + "range": Object { + "factorOverride": Object { + "gte": 10.565, + "lte": 10.575000000000001, + }, + }, + } + `); + }); + test('-8.2', () => { + expect(numberQuery(factorOverrideParam, '-8.2')).toMatchInlineSnapshot(` + Object { + "range": Object { + "factorOverride": Object { + "gte": -8.25, + "lte": -8.149999999999999, + }, + }, + } + `); + }); + test('ge8e-1', () => { + expect(numberQuery(factorOverrideParam, '8e-1')).toMatchInlineSnapshot(` + Object { + "range": Object { + "factorOverride": Object { + "gte": 0.75, + "lte": 0.8500000000000001, + }, + }, + } + `); + }); + }); + describe('invalid inputs', () => { + each([['This is not a number at all'], ['badPrefix100'], ['100someSuffix'], ['100|system|code']]).test( + '%s', + param => { + expect(() => numberQuery(factorOverrideParam, param)).toThrow(InvalidSearchParameterError); + }, + ); + }); +}); diff --git a/src/QueryBuilder/typeQueries/numberQuery.ts b/src/QueryBuilder/typeQueries/numberQuery.ts new file mode 100644 index 0000000..edeac21 --- /dev/null +++ b/src/QueryBuilder/typeQueries/numberQuery.ts @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { InvalidSearchParameterError } from 'fhir-works-on-aws-interface'; +import { CompiledSearchParam } from '../../FHIRSearchParametersRegistry'; +import { parseNumber } from './common/number'; +import { prefixRangeNumber } from './common/prefixRange'; + +interface NumberSearchParameter { + prefix: string; + number: number; + implicitRange: { + start: number; + end: number; + }; +} + +const NUMBER_SEARCH_PARAM_REGEX = /^(?eq|ne|lt|gt|ge|le|sa|eb|ap)?(?[\d.+-eE]+)$/; + +export const parseNumberSearchParam = (param: string): NumberSearchParameter => { + const match = param.match(NUMBER_SEARCH_PARAM_REGEX); + if (match === null) { + throw new InvalidSearchParameterError(`Invalid number search parameter: ${param}`); + } + + const { numberString } = match.groups!; + + // If no prefix is present, the prefix eq is assumed. + // https://www.hl7.org/fhir/search.html#prefix + const prefix = match.groups!.prefix ?? 'eq'; + + const fhirNumber = parseNumber(numberString); + return { + prefix, + ...fhirNumber, + }; +}; + +export const numberQuery = (compiledSearchParam: CompiledSearchParam, value: string): any => { + const { prefix, implicitRange, number } = parseNumberSearchParam(value); + return prefixRangeNumber(prefix, number, implicitRange, compiledSearchParam.path); +}; diff --git a/src/QueryBuilder/typeQueries/quantityQuery.test.ts b/src/QueryBuilder/typeQueries/quantityQuery.test.ts new file mode 100644 index 0000000..5ca807a --- /dev/null +++ b/src/QueryBuilder/typeQueries/quantityQuery.test.ts @@ -0,0 +1,177 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import each from 'jest-each'; +import { InvalidSearchParameterError } from 'fhir-works-on-aws-interface'; +import { FHIRSearchParametersRegistry } from '../../FHIRSearchParametersRegistry'; +import { quantityQuery } from './quantityQuery'; + +const fhirSearchParametersRegistry = new FHIRSearchParametersRegistry('4.0.1'); +const quantityParam = fhirSearchParametersRegistry.getSearchParameter('Observation', 'value-quantity')!.compiled[0]; + +describe('quantityQuery', () => { + describe('valid inputs', () => { + test('5.4|http://unitsofmeasure.org|mg', () => { + expect(quantityQuery(quantityParam, '5.4|http://unitsofmeasure.org|mg')).toMatchInlineSnapshot(` + Object { + "bool": Object { + "must": Array [ + Object { + "range": Object { + "valueQuantity.value": Object { + "gte": 5.3500000000000005, + "lte": 5.45, + }, + }, + }, + Object { + "multi_match": Object { + "fields": Array [ + "valueQuantity.code.keyword", + ], + "lenient": true, + "query": "mg", + }, + }, + Object { + "multi_match": Object { + "fields": Array [ + "valueQuantity.system.keyword", + ], + "lenient": true, + "query": "http://unitsofmeasure.org", + }, + }, + ], + }, + } + `); + }); + test('5.40e-3|http://unitsofmeasure.org|g', () => { + expect(quantityQuery(quantityParam, '5.40e-3|http://unitsofmeasure.org|g')).toMatchInlineSnapshot(` + Object { + "bool": Object { + "must": Array [ + Object { + "range": Object { + "valueQuantity.value": Object { + "gte": 0.0053950000000000005, + "lte": 0.005405, + }, + }, + }, + Object { + "multi_match": Object { + "fields": Array [ + "valueQuantity.code.keyword", + ], + "lenient": true, + "query": "g", + }, + }, + Object { + "multi_match": Object { + "fields": Array [ + "valueQuantity.system.keyword", + ], + "lenient": true, + "query": "http://unitsofmeasure.org", + }, + }, + ], + }, + } + `); + }); + test('5.4||mg', () => { + expect(quantityQuery(quantityParam, '5.4||mg')).toMatchInlineSnapshot(` + Object { + "bool": Object { + "must": Array [ + Object { + "range": Object { + "valueQuantity.value": Object { + "gte": 5.3500000000000005, + "lte": 5.45, + }, + }, + }, + Object { + "multi_match": Object { + "fields": Array [ + "valueQuantity.code.keyword", + "valueQuantity.unit.keyword", + ], + "lenient": true, + "query": "mg", + }, + }, + ], + }, + } + `); + }); + test('5.4', () => { + expect(quantityQuery(quantityParam, '5.4')).toMatchInlineSnapshot(` + Object { + "range": Object { + "valueQuantity.value": Object { + "gte": 5.3500000000000005, + "lte": 5.45, + }, + }, + } + `); + }); + test('le5.4|http://unitsofmeasure.org|mg', () => { + expect(quantityQuery(quantityParam, 'le5.4|http://unitsofmeasure.org|mg')).toMatchInlineSnapshot(` + Object { + "bool": Object { + "must": Array [ + Object { + "range": Object { + "valueQuantity.value": Object { + "lte": 5.4, + }, + }, + }, + Object { + "multi_match": Object { + "fields": Array [ + "valueQuantity.code.keyword", + ], + "lenient": true, + "query": "mg", + }, + }, + Object { + "multi_match": Object { + "fields": Array [ + "valueQuantity.system.keyword", + ], + "lenient": true, + "query": "http://unitsofmeasure.org", + }, + }, + ], + }, + } + `); + }); + }); + + describe('invalid inputs', () => { + each([ + ['This is not a quantity at all'], + ['badPrefix100'], + ['100someSuffix'], + ['100|a|b|c'], + ['100xxx|system|code'], + ['100e-2x|system|code'], + ]).test('%s', param => { + expect(() => quantityQuery(quantityParam, param)).toThrow(InvalidSearchParameterError); + }); + }); +}); diff --git a/src/QueryBuilder/typeQueries/quantityQuery.ts b/src/QueryBuilder/typeQueries/quantityQuery.ts new file mode 100644 index 0000000..aa6153e --- /dev/null +++ b/src/QueryBuilder/typeQueries/quantityQuery.ts @@ -0,0 +1,86 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { isEmpty } from 'lodash'; +import { InvalidSearchParameterError } from 'fhir-works-on-aws-interface'; +import { CompiledSearchParam } from '../../FHIRSearchParametersRegistry'; +import { parseNumber } from './common/number'; +import { prefixRangeNumber } from './common/prefixRange'; + +interface QuantitySearchParameter { + prefix: string; + system: string; + code: string; + number: number; + implicitRange: { + start: number; + end: number; + }; +} + +const QUANTITY_SEARCH_PARAM_REGEX = /^(?eq|ne|lt|gt|ge|le|sa|eb|ap)?(?[\d.+-eE]+)(\|(?[^|\s]*)\|(?[^|\s]*))?$/; + +export const parseQuantitySearchParam = (param: string): QuantitySearchParameter => { + const match = param.match(QUANTITY_SEARCH_PARAM_REGEX); + if (match === null) { + throw new InvalidSearchParameterError(`Invalid quantity search parameter: ${param}`); + } + + const { numberString, system = '', code = '' } = match.groups!; + + // If no prefix is present, the prefix eq is assumed. + // https://www.hl7.org/fhir/search.html#prefix + const prefix = match.groups!.prefix ?? 'eq'; + + const fhirNumber = parseNumber(numberString); + return { + prefix, + system, + code, + ...fhirNumber, + }; +}; + +export const quantityQuery = (compiledSearchParam: CompiledSearchParam, value: string): any => { + const { prefix, implicitRange, number, system, code } = parseQuantitySearchParam(value); + const queries = [prefixRangeNumber(prefix, number, implicitRange, `${compiledSearchParam.path}.value`)]; + + if (!isEmpty(system) && !isEmpty(code)) { + queries.push({ + multi_match: { + fields: [`${compiledSearchParam.path}.code.keyword`], + query: code, + lenient: true, + }, + }); + + queries.push({ + multi_match: { + fields: [`${compiledSearchParam.path}.system.keyword`], + query: system, + lenient: true, + }, + }); + } else if (!isEmpty(code)) { + // when there is no system, search either the code (code) or the stated human unit (unit) + // https://www.hl7.org/fhir/search.html#quantity + queries.push({ + multi_match: { + fields: [`${compiledSearchParam.path}.code.keyword`, `${compiledSearchParam.path}.unit.keyword`], + query: code, + lenient: true, + }, + }); + } + + if (queries.length === 1) { + return queries[0]; + } + return { + bool: { + must: queries, + }, + }; +}; From 0ead589428260df90ac5fd9b0e0bec39a63eef02 Mon Sep 17 00:00:00 2001 From: Nestor Carvantes Date: Fri, 16 Apr 2021 16:02:18 -0700 Subject: [PATCH 2/3] add more number tests --- src/QueryBuilder/typeQueries/common/number.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/QueryBuilder/typeQueries/common/number.test.ts b/src/QueryBuilder/typeQueries/common/number.test.ts index 7385768..0fee8c5 100644 --- a/src/QueryBuilder/typeQueries/common/number.test.ts +++ b/src/QueryBuilder/typeQueries/common/number.test.ts @@ -12,8 +12,13 @@ describe('parseNumber', () => { each([ ['-100', 5e-1], ['100', 5e-1], + ['+100', 5e-1], ['10.0', 5e-2], + ['+10.0', 5e-2], + ['-10.0', 5e-2], ['1e2', 5e1], + ['+1e2', 5e1], + ['-1e2', 5e1], ['1.0e2', 5], ['1.0E2', 5], ['5.4', 5e-2], From b99e6cb8605615f5480f557753cc031d9dae1507 Mon Sep 17 00:00:00 2001 From: Nestor Carvantes Date: Fri, 16 Apr 2021 16:04:47 -0700 Subject: [PATCH 3/3] minor refactor in prefixes --- .../typeQueries/common/prefixRange.ts | 138 +++++++++--------- 1 file changed, 67 insertions(+), 71 deletions(-) diff --git a/src/QueryBuilder/typeQueries/common/prefixRange.ts b/src/QueryBuilder/typeQueries/common/prefixRange.ts index 24ca164..3f13ed6 100644 --- a/src/QueryBuilder/typeQueries/common/prefixRange.ts +++ b/src/QueryBuilder/typeQueries/common/prefixRange.ts @@ -23,85 +23,81 @@ interface DateRange { const prefixRange = (prefix: string, range: Range, path: string): any => { const { start, end } = range; - // See https://www.hl7.org/fhir/search.html#prefix - if (prefix !== 'ne') { - let elasticSearchRange; - switch (prefix) { - case 'eq': // equal - elasticSearchRange = { - gte: start, - lte: end, - }; - break; - case 'lt': // less than - elasticSearchRange = { - lt: end, - }; - break; - case 'le': // less or equal - elasticSearchRange = { - lte: end, - }; - break; - case 'gt': // greater than - elasticSearchRange = { - gt: start, - }; - break; - case 'ge': // greater or equal - elasticSearchRange = { - gte: start, - }; - break; - case 'sa': // starts after - elasticSearchRange = { - gt: end, - }; - break; - case 'eb': // ends before - elasticSearchRange = { - lt: start, - }; - break; - case 'ap': // approximately - throw new InvalidSearchParameterError('Unsupported prefix: ap'); - default: - // this should never happen - throw new Error(`unknown search prefix: ${prefix}`); - } - + // not equal + if (prefix === 'ne') { return { - range: { - [path]: elasticSearchRange, - }, - }; - } - - // ne prefix is the only case that requires a bool query; - const neQuery = { - bool: { - should: [ - { - range: { - [path]: { - gt: end, + bool: { + should: [ + { + range: { + [path]: { + gt: end, + }, }, }, - }, - { - range: { - [path]: { - lt: start, + { + range: { + [path]: { + lt: start, + }, }, }, - }, - ], + ], + }, + }; + } + + // See https://www.hl7.org/fhir/search.html#prefix + let elasticSearchRange; + switch (prefix) { + case 'eq': // equal + elasticSearchRange = { + gte: start, + lte: end, + }; + break; + case 'lt': // less than + elasticSearchRange = { + lt: end, + }; + break; + case 'le': // less or equal + elasticSearchRange = { + lte: end, + }; + break; + case 'gt': // greater than + elasticSearchRange = { + gt: start, + }; + break; + case 'ge': // greater or equal + elasticSearchRange = { + gte: start, + }; + break; + case 'sa': // starts after + elasticSearchRange = { + gt: end, + }; + break; + case 'eb': // ends before + elasticSearchRange = { + lt: start, + }; + break; + case 'ap': // approximately + throw new InvalidSearchParameterError('Unsupported prefix: ap'); + default: + // this should never happen + throw new Error(`unknown search prefix: ${prefix}`); + } + return { + range: { + [path]: elasticSearchRange, }, }; - - return neQuery; }; - export const prefixRangeNumber = (prefix: string, number: number, implicitRange: NumberRange, path: string): any => { if (prefix === 'eq' || prefix === 'ne') { return prefixRange(prefix, implicitRange, path);