Skip to main content
Glama
utils.test.ts17.7 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { readJson } from '@medplum/definitions'; import type { Bundle, Period, Questionnaire } from '@medplum/fhirtypes'; import type { TypedValue } from '../types'; import { PropertyType } from '../types'; import type { InternalSchemaElement } from '../typeschema/types'; import { indexStructureDefinitionBundle } from '../typeschema/types'; import { fhirPathArrayEquals, fhirPathArrayEquivalent, fhirPathEquals, fhirPathEquivalent, fhirPathIs, getTypedPropertyValue, getTypedPropertyValueWithoutSchema, getTypedPropertyValueWithSchema, isDateString, isDateTimeString, toJsBoolean, toPeriod, toTypedValue, } from './utils'; const TYPED_TRUE = { type: PropertyType.boolean, value: true }; const TYPED_FALSE = { type: PropertyType.boolean, value: false }; const TYPED_1 = { type: PropertyType.integer, value: 1 }; const TYPED_2 = { type: PropertyType.integer, value: 2 }; const TYPED_CODING_MEDPLUM123 = { type: PropertyType.Coding, value: { code: 'MEDPLUM123' } }; const TYPED_CODING_MEDPLUM123_W_SYSTEM = { type: PropertyType.Coding, value: { code: 'MEDPLUM123', system: 'medplum-v123.456.789' }, }; const TYPED_CODING_NOT_MEDPLUM123 = { type: PropertyType.Coding, value: { code: 'NOT_MEDPLUM123' } }; describe('FHIRPath utils', () => { beforeAll(() => { indexStructureDefinitionBundle(readJson('fhir/r4/profiles-types.json') as Bundle); indexStructureDefinitionBundle(readJson('fhir/r4/profiles-resources.json') as Bundle); }); test('toJsBoolean', () => { expect(toJsBoolean([{ type: PropertyType.BackboneElement, value: undefined }])).toStrictEqual(false); expect(toJsBoolean([{ type: PropertyType.BackboneElement, value: null }])).toStrictEqual(false); expect(toJsBoolean([{ type: PropertyType.boolean, value: false }])).toStrictEqual(false); expect(toJsBoolean([{ type: PropertyType.boolean, value: true }])).toStrictEqual(true); expect(toJsBoolean([{ type: PropertyType.string, value: '' }])).toStrictEqual(false); expect(toJsBoolean([{ type: PropertyType.string, value: 'hi' }])).toStrictEqual(true); }); test('toTypedValue', () => { expect(toTypedValue(1)).toStrictEqual(TYPED_1); expect(toTypedValue(1.5)).toStrictEqual({ type: PropertyType.decimal, value: 1.5 }); expect(toTypedValue(true)).toStrictEqual(TYPED_TRUE); expect(toTypedValue(false)).toStrictEqual(TYPED_FALSE); expect(toTypedValue('xyz')).toStrictEqual({ type: PropertyType.string, value: 'xyz' }); expect(toTypedValue({ code: 'x' })).toStrictEqual({ type: PropertyType.Coding, value: { code: 'x' }, }); expect(toTypedValue({ coding: [{ code: 'y' }] })).toStrictEqual({ type: PropertyType.CodeableConcept, value: { coding: [{ code: 'y' }] }, }); expect(toTypedValue({ value: 123, unit: 'mg' })).toStrictEqual({ type: PropertyType.Quantity, value: { value: 123, unit: 'mg' }, }); }); test('fhirPathIs', () => { expect(fhirPathIs({ type: PropertyType.string, value: undefined }, 'string')).toStrictEqual(false); expect(fhirPathIs({ type: PropertyType.BackboneElement, value: {} }, 'Patient')).toStrictEqual(false); expect( fhirPathIs({ type: PropertyType.BackboneElement, value: { resourceType: 'Patient' } }, 'Patient') ).toStrictEqual(true); expect( fhirPathIs({ type: PropertyType.BackboneElement, value: { resourceType: 'Observation' } }, 'Patient') ).toStrictEqual(false); expect(fhirPathIs({ type: PropertyType.boolean, value: true }, 'Boolean')).toStrictEqual(true); expect(fhirPathIs({ type: PropertyType.boolean, value: false }, 'Boolean')).toStrictEqual(true); expect(fhirPathIs({ type: PropertyType.integer, value: 100 }, 'Boolean')).toStrictEqual(false); expect(fhirPathIs({ type: PropertyType.BackboneElement, value: {} }, 'Boolean')).toStrictEqual(false); }); test('fhirPathEquals', () => { expect(fhirPathEquals(TYPED_TRUE, TYPED_TRUE)).toStrictEqual([TYPED_TRUE]); expect(fhirPathEquals(TYPED_TRUE, TYPED_FALSE)).toStrictEqual([TYPED_FALSE]); expect(fhirPathEquals(TYPED_1, TYPED_1)).toStrictEqual([TYPED_TRUE]); expect(fhirPathEquals(TYPED_1, TYPED_2)).toStrictEqual([TYPED_FALSE]); expect(fhirPathEquals(TYPED_2, TYPED_1)).toStrictEqual([TYPED_FALSE]); }); test('fhirPathArrayEquals', () => { expect(fhirPathArrayEquals([TYPED_1], [TYPED_1])).toStrictEqual([TYPED_TRUE]); expect(fhirPathArrayEquals([TYPED_1], [TYPED_2])).toStrictEqual([TYPED_FALSE]); // Acceptable threshold expect(fhirPathArrayEquals([toTypedValue(1.0)], [toTypedValue(1.0001)])).toStrictEqual([TYPED_FALSE]); expect(fhirPathArrayEquals([toTypedValue(1.0)], [toTypedValue(1.5)])).toStrictEqual([TYPED_FALSE]); // Sort order does matter expect(fhirPathArrayEquals([TYPED_1, TYPED_2], [TYPED_2, TYPED_1])).toStrictEqual([TYPED_FALSE]); expect(fhirPathArrayEquals([TYPED_1, TYPED_2], [TYPED_1, TYPED_1])).toStrictEqual([TYPED_FALSE]); }); test('fhirPathEquivalent', () => { expect(fhirPathEquivalent(TYPED_TRUE, TYPED_TRUE)).toStrictEqual([TYPED_TRUE]); expect(fhirPathEquivalent(TYPED_TRUE, TYPED_FALSE)).toStrictEqual([TYPED_FALSE]); expect(fhirPathEquivalent(TYPED_1, TYPED_1)).toStrictEqual([TYPED_TRUE]); expect(fhirPathEquivalent(TYPED_1, TYPED_2)).toStrictEqual([TYPED_FALSE]); expect(fhirPathEquivalent(TYPED_2, TYPED_1)).toStrictEqual([TYPED_FALSE]); // Test `Coding` equivalence expect(fhirPathEquivalent(TYPED_CODING_MEDPLUM123, TYPED_CODING_MEDPLUM123)).toStrictEqual([TYPED_TRUE]); expect(fhirPathEquivalent(TYPED_CODING_MEDPLUM123, TYPED_CODING_MEDPLUM123_W_SYSTEM)).toStrictEqual([TYPED_FALSE]); expect(fhirPathEquivalent(TYPED_CODING_MEDPLUM123, TYPED_CODING_NOT_MEDPLUM123)).toStrictEqual([TYPED_FALSE]); expect(fhirPathEquivalent(TYPED_CODING_MEDPLUM123_W_SYSTEM, TYPED_CODING_MEDPLUM123_W_SYSTEM)).toStrictEqual([ TYPED_TRUE, ]); }); test('fhirPathArrayEquivalent', () => { expect(fhirPathArrayEquivalent([TYPED_1], [TYPED_1])).toStrictEqual([TYPED_TRUE]); expect(fhirPathArrayEquivalent([TYPED_1], [TYPED_2])).toStrictEqual([TYPED_FALSE]); // Acceptable threshold expect(fhirPathArrayEquivalent([toTypedValue(1.0)], [toTypedValue(1.0001)])).toStrictEqual([TYPED_TRUE]); expect(fhirPathArrayEquivalent([toTypedValue(1.0)], [toTypedValue(1.5)])).toStrictEqual([TYPED_FALSE]); // Sort order does not matter expect(fhirPathArrayEquivalent([TYPED_1, TYPED_2], [TYPED_2, TYPED_1])).toStrictEqual([TYPED_TRUE]); expect(fhirPathArrayEquivalent([TYPED_1, TYPED_2], [TYPED_1, TYPED_1])).toStrictEqual([TYPED_FALSE]); }); test('getTypedPropertyValue', () => { expect(getTypedPropertyValue({ type: '', value: undefined }, 'x')).toBeUndefined(); expect(getTypedPropertyValue({ type: '', value: null }, 'x')).toBeUndefined(); expect(getTypedPropertyValue({ type: 'x', value: {} }, 'x')).toBeUndefined(); expect(getTypedPropertyValue({ type: 'integer', value: 123 }, 'x')).toBeUndefined(); // Support missing schemas expect(getTypedPropertyValue({ type: 'Foo', value: { x: 1 } }, 'x')).toStrictEqual(TYPED_1); expect(getTypedPropertyValue({ type: 'Foo', value: { x: [1] } }, 'x')).toStrictEqual([TYPED_1]); expect(getTypedPropertyValue({ type: 'Foo', value: { valueInteger: 1 } }, 'value')).toStrictEqual(TYPED_1); // Only use valid property types expect( getTypedPropertyValue(toTypedValue({ resourceType: 'Patient', identifier: [{ value: 'foo' }] }), 'id') ).toBeUndefined(); expect(getTypedPropertyValue(toTypedValue({ resourceType: 'AccessPolicy' }), 'resource')).toBeUndefined(); // Silently ignore empty arrays expect( getTypedPropertyValue(toTypedValue({ resourceType: 'Patient', identifier: [] }), 'identifier') ).toBeUndefined(); expect(getTypedPropertyValue({ type: 'X', value: { x: [] } }, 'x')).toBeUndefined(); // Property path that is part of multi-type element in schema expect(getTypedPropertyValue({ type: 'Extension', value: { valueBoolean: true } }, 'valueBoolean')).toStrictEqual({ type: 'boolean', value: true, }); }); test('getTypedPropertyValueWithoutSchema', () => { expect(getTypedPropertyValueWithoutSchema({ type: '', value: undefined }, 'x')).toBeUndefined(); expect(getTypedPropertyValueWithoutSchema({ type: '', value: null }, 'x')).toBeUndefined(); expect(getTypedPropertyValueWithoutSchema({ type: 'x', value: {} }, 'x')).toBeUndefined(); expect(getTypedPropertyValueWithoutSchema({ type: 'integer', value: 123 }, 'x')).toBeUndefined(); // Support missing schemas expect(getTypedPropertyValueWithoutSchema({ type: 'Foo', value: { x: 1 } }, 'x')).toStrictEqual(TYPED_1); expect(getTypedPropertyValueWithoutSchema({ type: 'Foo', value: { x: [1] } }, 'x')).toStrictEqual([TYPED_1]); expect(getTypedPropertyValueWithoutSchema({ type: 'Foo', value: { valueInteger: 1 } }, 'value')).toStrictEqual( TYPED_1 ); expect(getTypedPropertyValueWithoutSchema({ type: 'Foo', value: { valueInteger: 1 } }, 'value[x]')).toStrictEqual( TYPED_1 ); // Only use valid property types expect( getTypedPropertyValueWithoutSchema( toTypedValue({ resourceType: 'Patient', identifier: [{ value: 'foo' }] }), 'id' ) ).toBeUndefined(); expect( getTypedPropertyValueWithoutSchema(toTypedValue({ resourceType: 'AccessPolicy' }), 'resource') ).toBeUndefined(); // Silently ignore empty arrays expect( getTypedPropertyValueWithoutSchema(toTypedValue({ resourceType: 'Patient', identifier: [] }), 'identifier') ).toBeUndefined(); expect(getTypedPropertyValueWithoutSchema({ type: 'X', value: { x: [] } }, 'x')).toBeUndefined(); // Property path that is part of multi-type element in schema expect( getTypedPropertyValueWithoutSchema({ type: 'Extension', value: { valueBoolean: true } }, 'valueBoolean') ).toStrictEqual({ type: 'boolean', value: true, }); }); test('Bundle entries', () => { const bundle: Bundle = { resourceType: 'Bundle', type: 'searchset', entry: [ { resource: { resourceType: 'Patient', identifier: [{ value: 'foo' }], }, }, ], }; const result1 = getTypedPropertyValue(toTypedValue(bundle), 'entry') as TypedValue[]; expect(result1).toHaveLength(1); const bundleEntry = result1[0]; expect(bundleEntry).toMatchObject({ type: 'BundleEntry', value: { resource: { resourceType: 'Patient', }, }, }); const patient = getTypedPropertyValue(bundleEntry, 'resource') as TypedValue; expect(patient).toMatchObject({ type: 'Patient', value: { resourceType: 'Patient', }, }); }); test('Content references', () => { const questionnaire: Questionnaire = { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: '1', type: 'group', item: [ { linkId: '1.1', type: 'display', }, ], }, ], }; const result1 = getTypedPropertyValue(toTypedValue(questionnaire), 'item') as TypedValue[]; expect(result1).toHaveLength(1); const item1 = result1[0]; expect(item1).toMatchObject({ type: 'QuestionnaireItem', value: { linkId: '1', type: 'group', }, }); const result2 = getTypedPropertyValue(item1, 'item') as TypedValue[]; expect(result2).toHaveLength(1); const item2 = result2[0]; expect(item2).toMatchObject({ type: 'QuestionnaireItem', value: { linkId: '1.1', type: 'display', }, }); }); test('getTypedPropertyValueWithSchema', () => { const typedValue: TypedValue = { type: 'Patient', value: { active: true } }; const path = 'active'; const goodElement: InternalSchemaElement = { description: '', path: 'Patient.active', min: 0, max: 0, type: [{ code: 'boolean' }], }; expect(getTypedPropertyValueWithSchema(typedValue, path, goodElement)).toStrictEqual({ type: 'boolean', value: true, }); const choiceOfTypeTypedValue: TypedValue = { type: 'Extension', value: { valueBoolean: true } }; const extensionValueX: InternalSchemaElement = { description: '', path: 'Extension.value[x]', min: 1, max: 1, type: [{ code: 'boolean' }], }; expect(getTypedPropertyValueWithSchema(choiceOfTypeTypedValue, 'value[x]', extensionValueX)).toStrictEqual({ type: 'boolean', value: true, }); expect(getTypedPropertyValueWithSchema(choiceOfTypeTypedValue, 'value', extensionValueX)).toStrictEqual({ type: 'boolean', value: true, }); }); test('getTypedPropertyValueWithSchema with primitive extensions', () => { const primitiveValue = 'Johnny'; const primitiveExtension = { url: 'http://example.com', valueBoolean: true }; const humanName = { given: ['John', primitiveValue], _given: [null, { extension: [primitiveExtension] }], }; const elementSchema: InternalSchemaElement = { description: '', path: 'HumanName.given', min: 0, max: 2, isArray: true, type: [{ code: 'string' }], }; // Extract elements with and without primitive extensions const results = getTypedPropertyValueWithSchema({ type: 'HumanName', value: humanName }, 'given', elementSchema); expect(results).toHaveLength(2); const [simple, extended] = results as TypedValue[]; expect(simple).toStrictEqual({ type: 'string', value: 'John' }); expect(extended).toStrictEqual({ type: 'string', value: expect.objectContaining(Object.assign('', primitiveValue, { extension: [primitiveExtension] })), }); // Check that values look correct when access "normally" expect(extended.value.valueOf()).toBe('Johnny'); expect(extended.value.extension).toStrictEqual([primitiveExtension]); // With primitive extensions, array values can be changed into a `String` wrapper type which has a typeof 'object'; // need to ensure the original input array values are not mutated as such expect(humanName.given.every((g) => typeof g === 'string')).toBe(true); // If extension only is specified, should still extract const results2 = getTypedPropertyValueWithSchema( { type: 'HumanName', value: { _given: [{ extension: [primitiveExtension] }] } }, 'given', elementSchema ); expect(results2).toHaveLength(1); const [extensionOnly] = results2 as TypedValue[]; expect(extensionOnly).toStrictEqual({ type: 'string', value: expect.objectContaining(Object.assign('', { extension: [primitiveExtension] })), }); }); test.each<[any, boolean]>([ [undefined, false], [null, false], ['', false], ['x', false], ['2020', true], ['2020-01', true], ['2020-01-01', true], ['2020-01-01T12:34:56Z', false], ['2020-01-01T12:34:56.789Z', false], ])('isDateString', (input, expected) => { expect(isDateString(input)).toBe(expected); }); test.each<[any, boolean]>([ [undefined, false], [null, false], ['', false], ['x', false], ['2020', true], ['2020-01', true], ['2020-01-01', true], ['2020-01-01T12:34:56Z', true], ['2020-01-01T12:34:56.7Z', true], ['2020-01-01T12:34:56.789Z', true], ['2020-01-01T12:34:56+01:30', true], ['2020-01-01T12:34:56.7+01:30', true], ['2020-01-01T12:34:56.789+01:30', true], ])('isDateTimeString(%p)', (input, expected) => { expect(isDateTimeString(input)).toBe(expected); }); test.each<[any, Period | undefined]>([ [undefined, undefined], [null, undefined], ['', undefined], ['x', undefined], [{}, undefined], ['2020-01-01', { start: '2020-01-01T00:00:00.000Z', end: '2020-01-01T23:59:59.999Z' }], ['2025-05-25T15:55:55Z', { start: '2025-05-25T15:55:55.000Z', end: '2025-05-25T15:55:55.999Z' }], ['2025-05-25T15:55:55.7Z', { start: '2025-05-25T15:55:55.700Z', end: '2025-05-25T15:55:55.799Z' }], ['2020-01-01T12:34:56.000Z', { start: '2020-01-01T12:34:56.000Z', end: '2020-01-01T12:34:56.000Z' }], ['2025-05-25T15:55:55+01:30', { start: '2025-05-25T14:25:55.000Z', end: '2025-05-25T14:25:55.999Z' }], ['2025-05-25T15:55:55.7+01:30', { start: '2025-05-25T14:25:55.700Z', end: '2025-05-25T14:25:55.799Z' }], ['2020-01-01T12:34:56.000+01:30', { start: '2020-01-01T11:04:56.000Z', end: '2020-01-01T11:04:56.000Z' }], [ { start: '2020-01-01T12:34:56.000Z', end: '2020-01-01T12:34:56.999Z' }, { start: '2020-01-01T12:34:56.000Z', end: '2020-01-01T12:34:56.999Z' }, ], // Normalize date strings with time zone offsets ['2020-01-01T12:34:56.000+01:00', { start: '2020-01-01T11:34:56.000Z', end: '2020-01-01T11:34:56.000Z' }], // Normalize periods with time zone offsets [ { start: '2020-01-01T12:34:56.000+01:00', end: '2020-01-01T12:34:56.999+01:00' }, { start: '2020-01-01T11:34:56.000Z', end: '2020-01-01T11:34:56.999Z' }, ], // Extend year to valid dates ['2020', { start: '2020-01-01T00:00:00.000Z', end: '2020-12-31T23:59:59.999Z' }], ])('toPeriod(%p)', (input, expected) => { if (expected) { expect(toPeriod(input)).toMatchObject(expected); } else { expect(toPeriod(input)).toBeUndefined(); } }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server