Skip to main content
Glama
tokens.test.ts44.8 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { WithId } from '@medplum/core'; import { createReference, getReferenceString, getSearchParameter, Operator, SNOMED } from '@medplum/core'; import type { Bundle, Condition, Identifier, MedicationRequest, Patient, Reference, SearchParameter, ServiceRequest, SpecimenDefinition, } from '@medplum/fhirtypes'; import { randomUUID } from 'crypto'; import { initAppServices, shutdownApp } from '../app'; import { loadTestConfig } from '../config/loader'; import { bundleContains, createTestProject, withTestContext } from '../test.setup'; import { getSystemRepo, Repository } from './repo'; import type { TokenColumnSearchParameterImplementation } from './searchparameter'; import { getSearchParameterImplementation } from './searchparameter'; import { loadStructureDefinitions } from './structure'; import { TokenQueryOperators } from './token-column'; import type { Token } from './tokens'; import { buildTokensForSearchParameter } from './tokens'; const systemRepo = getSystemRepo(); beforeAll(async () => { const config = await loadTestConfig(); await initAppServices(config); }); afterAll(async () => { await shutdownApp(); }); test('Identifier', () => withTestContext(async () => { const identifier = randomUUID(); const text = 'this is some text'; const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], identifier: [{ system: 'https://www.example.com', value: identifier, type: { text } }], }); const searchResult = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier, }, ], }); expect(searchResult.entry?.length).toStrictEqual(1); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(patient.id); const goodTextResult = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier, }, { code: 'identifier', operator: Operator.TEXT, value: text, }, ], }); expect(goodTextResult.entry?.length).toStrictEqual(1); expect(goodTextResult.entry?.[0]?.resource?.id).toStrictEqual(patient.id); const badTextResult = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.TEXT, // :text on the identifier value itself should not match value: identifier, }, ], }); expect(badTextResult.entry?.length).toStrictEqual(0); })); test.each(TokenQueryOperators)('%s with empty value does not throw errors', async (operator) => { const search = systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: operator, value: '', }, ], }); await expect(search).resolves; }); test('Multiple identifiers', () => withTestContext(async () => { const identifier = randomUUID(); const other = randomUUID(); const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], identifier: [ { system: 'https://www.example.com', value: identifier }, { system: 'https://www.example.com', value: identifier }, { system: 'other', value: other }, ], }); const searchResult = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier, }, ], }); expect(searchResult.entry?.length).toStrictEqual(1); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(patient.id); const searchResult2 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: other, }, ], }); expect(searchResult2.entry?.length).toStrictEqual(1); expect(searchResult2.entry?.[0]?.resource?.id).toStrictEqual(patient.id); })); test('Update identifier', () => withTestContext(async () => { const identifier1 = randomUUID(); const identifier2 = randomUUID(); const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], identifier: [{ system: 'https://www.example.com', value: identifier1 }], }); const bundle2 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier1, }, ], }); expect(bundle2.entry?.length).toStrictEqual(1); expect(bundle2.entry?.[0]?.resource?.id).toStrictEqual(patient1.id); const bundle3 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier2, }, ], }); expect(bundle3.entry?.length).toStrictEqual(0); await systemRepo.updateResource<Patient>({ ...patient1, identifier: [{ system: 'https://www.example.com', value: identifier2 }], }); const bundle5 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier1, }, ], }); expect(bundle5.entry?.length).toStrictEqual(0); const bundle6 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier2, }, ], }); expect(bundle6.entry?.length).toStrictEqual(1); expect(bundle6.entry?.[0]?.resource?.id).toStrictEqual(patient1.id); })); test('Implicit exact', () => withTestContext(async () => { const identifier = randomUUID(); const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], identifier: [{ system: 'https://www.example.com', value: identifier }], }); const patient2 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Jones' }], identifier: [{ system: 'https://www.example.com', value: identifier + 'xyz' }], }); const searchResult1 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier, }, ], }); expect(searchResult1.entry?.length).toStrictEqual(1); expect(bundleContains(searchResult1, patient1)).toBeDefined(); expect(bundleContains(searchResult1, patient2)).toBeUndefined(); })); test('Explicit exact', () => withTestContext(async () => { const identifier = randomUUID(); const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], identifier: [{ system: 'https://www.example.com', value: identifier }], }); const patient2 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Jones' }], identifier: [{ system: 'https://www.example.com', value: identifier + 'xyz' }], }); const searchResult1 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EXACT, value: identifier, }, ], }); expect(searchResult1.entry?.length).toStrictEqual(1); expect(bundleContains(searchResult1, patient1)).toBeDefined(); expect(bundleContains(searchResult1, patient2)).toBeUndefined(); })); describe(':contains', () => { const baseIdentifierValue = randomUUID(); const identifier = randomUUID(); let patient1: Patient; const baseFilters = [ { code: 'identifier', operator: Operator.EQUALS, value: baseIdentifierValue, }, ]; beforeAll(async () => { const baseIdentifier = { system: 'https://hi.com', value: baseIdentifierValue }; patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], telecom: [ { system: 'email', value: 'prefix-email-postfix' }, { system: 'phone', value: 'prefix-phone-postfix' }, { system: 'pager', value: 'prefix-pager-postfix' }, ], identifier: [baseIdentifier, { system: 'https://www.example.com', value: identifier }], }); }); test('prefix on Patient-identifier', () => withTestContext(async () => { const identifierResult = await systemRepo.search({ resourceType: 'Patient', filters: [ ...baseFilters, { code: 'identifier', operator: Operator.CONTAINS, value: identifier.slice(0, 5), }, ], }); expect(identifierResult.entry?.length).toStrictEqual(0); })); test('infix on Patient-identifier', () => withTestContext(async () => { const identifierResult = await systemRepo.search({ resourceType: 'Patient', filters: [ ...baseFilters, { code: 'identifier', operator: Operator.CONTAINS, value: identifier.slice(5, 10), }, ], }); expect(identifierResult.entry?.length).toStrictEqual(0); })); test.each(['pager', 'PAGER', 'PaGeR'])('infix on Patient-telecom', (value) => withTestContext(async () => { const result = await systemRepo.search({ resourceType: 'Patient', filters: [ ...baseFilters, { code: 'telecom', operator: Operator.CONTAINS, value, }, ], }); expect(result.entry?.length).toStrictEqual(1); expect(bundleContains(result, patient1)).toBeDefined(); }) ); test('infix on Patient-email', () => withTestContext(async () => { const result = await systemRepo.search({ resourceType: 'Patient', filters: [ ...baseFilters, { code: 'email', operator: Operator.CONTAINS, value: 'email', }, ], }); expect(result.entry?.length).toStrictEqual(1); expect(bundleContains(result, patient1)).toBeDefined(); })); test('infix on Patient-phone', () => withTestContext(async () => { const result = await systemRepo.search({ resourceType: 'Patient', filters: [ ...baseFilters, { code: 'phone', operator: Operator.CONTAINS, value: 'phone', }, ], }); expect(result.entry?.length).toStrictEqual(1); expect(bundleContains(result, patient1)).toBeDefined(); })); }); test('Not equals', () => withTestContext(async () => { const identifier = randomUUID(); const name = randomUUID(); const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: name }], identifier: [{ system: 'https://www.example.com', value: identifier }], }); const patient2 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: name }], identifier: [{ system: 'https://www.example.com', value: identifier + 'xyz' }], }); const patient3 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: name }], // no identifiers should match NOT_EQUALS }); const searchResult1 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.NOT_EQUALS, value: identifier, }, { code: 'name', operator: Operator.EQUALS, value: name, }, ], }); expect(searchResult1.entry?.length).toStrictEqual(2); expect(bundleContains(searchResult1, patient1)).toBeUndefined(); expect(bundleContains(searchResult1, patient2)).toBeDefined(); expect(bundleContains(searchResult1, patient3)).toBeDefined(); })); test('Search comma separated identifier exact', () => withTestContext(async () => { const identifier1 = randomUUID(); const identifier2 = randomUUID(); const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], identifier: [{ system: 'https://www.example.com', value: identifier1 }], }); const patient2 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Jones' }], identifier: [{ system: 'https://www.example.com', value: identifier2 }], }); const searchResult1 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EXACT, value: `${identifier1},${identifier2}`, }, ], }); expect(searchResult1.entry?.length).toStrictEqual(2); expect(bundleContains(searchResult1, patient1)).toBeDefined(); expect(bundleContains(searchResult1, patient2)).toBeDefined(); })); test('Search on system', () => withTestContext(async () => { const system1 = 'https://foo.com'; const system2 = 'https://bar.com'; const identifier = randomUUID(); const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], identifier: [{ system: system1, value: identifier }], }); const patient2 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Jones' }], identifier: [{ system: system2, value: identifier }], }); const searchResult1 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EXACT, value: system1 + '|' + identifier, }, ], }); expect(searchResult1.entry?.length).toStrictEqual(1); expect(bundleContains(searchResult1, patient1)).toBeDefined(); expect(bundleContains(searchResult1, patient2)).toBeUndefined(); const searchResult2 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EXACT, value: system2 + '|' + identifier, }, ], }); expect(searchResult2.entry?.length).toStrictEqual(1); expect(bundleContains(searchResult2, patient1)).toBeUndefined(); expect(bundleContains(searchResult2, patient2)).toBeDefined(); })); test('Non-array identifier', () => withTestContext(async () => { const identifier = randomUUID(); const resource = await systemRepo.createResource<SpecimenDefinition>({ resourceType: 'SpecimenDefinition', identifier: { system: 'https://www.example.com', value: identifier }, }); const searchResult = await systemRepo.search({ resourceType: 'SpecimenDefinition', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier, }, ], }); expect(searchResult.entry?.length).toStrictEqual(1); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(resource.id); })); test('Leading whitespace', () => withTestContext(async () => { const identifier = randomUUID(); const resource = await systemRepo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ system: 'https://www.example.com', value: ' ' + identifier }], }); const searchResult = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier, }, ], }); expect(searchResult.entry?.length).toStrictEqual(1); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(resource.id); })); test('CodeableConcept text', () => withTestContext(async () => { const sr1 = await systemRepo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', intent: 'order', status: 'active', subject: { reference: 'Patient/1' }, category: [{ text: randomUUID() }], }); const sr2 = await systemRepo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', intent: 'order', status: 'active', subject: { reference: 'Patient/1' }, category: [{ text: randomUUID() }], }); const searchResult1 = await systemRepo.search({ resourceType: 'ServiceRequest', filters: [ { code: 'category', operator: Operator.EQUALS, value: sr1.category?.[0]?.text as string, }, ], }); expect(searchResult1.entry?.length).toStrictEqual(1); expect(bundleContains(searchResult1, sr1)).toBeDefined(); expect(bundleContains(searchResult1, sr2)).toBeUndefined(); })); test('Identifier value with pipe', () => withTestContext(async () => { const system = randomUUID(); const base = randomUUID(); const id1 = base + '|1'; const id2 = base + '|2'; const p1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], identifier: [{ system, value: id1 }], }); await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], identifier: [{ system, value: id2 }], }); const r1 = await systemRepo.search({ resourceType: 'Patient', filters: [{ code: 'identifier', operator: Operator.EQUALS, value: `${system}|${id1}` }], }); expect(r1.entry?.length).toStrictEqual(1); expect(r1.entry?.[0]?.resource?.id).toStrictEqual(p1.id); })); describe('Missing/present', () => { const family = randomUUID(); const email = randomUUID(); const identifier = randomUUID(); let p1: Patient; let p2: Patient; beforeAll(async () => { p1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', meta: { security: [{ code: '123' }], }, name: [{ family }], telecom: [{ system: 'email', value: email + 'abc' }], }); p2 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ family }], telecom: [{ system: 'email', value: email + 'xyz' }], identifier: [{ system: 'https://www.example.com', value: identifier }], }); }); test.each([ ['identifier', Operator.MISSING, 'true', true, false, true], ['identifier', Operator.MISSING, 'false', false, true, true], ['identifier', Operator.PRESENT, 'true', false, true, true], ['identifier', Operator.PRESENT, 'false', true, false, true], ['telecom', Operator.MISSING, 'true', false, false, true], ['telecom', Operator.MISSING, 'false', true, true, true], ['telecom', Operator.PRESENT, 'true', true, true, true], ['telecom', Operator.PRESENT, 'false', false, false, true], ['_security', Operator.MISSING, 'true', false, true, false], ['_security', Operator.MISSING, 'false', true, false, false], ['_security', Operator.PRESENT, 'true', true, false, false], ['_security', Operator.PRESENT, 'false', false, true, false], ])('%s :%s %s', (code, operator, value, expected1, expected2, expectedHasDedicated) => withTestContext(async () => { const searchParam = getSearchParameter('Patient', code) as SearchParameter; const impl = getSearchParameterImplementation('Patient', searchParam); expect(impl.searchStrategy).toStrictEqual('token-column'); expect((impl as TokenColumnSearchParameterImplementation).hasDedicatedColumns).toStrictEqual( expectedHasDedicated ); const res = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'name', operator: Operator.EQUALS, value: family }, { code, operator, value }, ], }); expect(Boolean(bundleContains(res, p1))).toBe(expected1); expect(Boolean(bundleContains(res, p2))).toBe(expected2); }) ); }); test.each([[Operator.IN], [Operator.NOT_IN]])('Condition.code :%s search', (operator) => withTestContext(async () => { // ValueSet: http://hl7.org/fhir/ValueSet/condition-code // compose includes codes from http://snomed.info/sct // but does not include codes from https://example.com const p = await systemRepo.createResource({ resourceType: 'Patient', name: [{ family: randomUUID() }], }); await systemRepo.createResource<Condition>({ resourceType: 'Condition', subject: createReference(p), code: { coding: [{ system: SNOMED, code: '165002' }] }, }); await systemRepo.createResource<Condition>({ resourceType: 'Condition', subject: createReference(p), code: { coding: [{ system: 'https://example.com', code: 'test' }] }, }); await expect( systemRepo.search({ resourceType: 'Condition', filters: [ { code: 'subject', operator: Operator.EQUALS, value: getReferenceString(p), }, { code: 'code', operator: operator, value: 'http://hl7.org/fhir/ValueSet/condition-code', }, ], }) ).rejects.toThrow('Invalid modifier'); }) ); describe('Non-strict mode', () => { let nonStrictRepo: Repository; let repo: Repository; let patient: Patient; const sys1 = 'http://sys.one'; const val1 = 'ABCDEFGHIJ'; beforeAll(async () => { const { project } = await createTestProject(); nonStrictRepo = new Repository({ strictMode: false, projects: [project], author: { reference: 'User/' + randomUUID() }, }); repo = new Repository({ strictMode: true, projects: [project], author: { reference: 'User/' + randomUUID() }, }); patient = await nonStrictRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Henry'] }], }); }); test('Handle malformed system', () => withTestContext(async () => { const subject = createReference(patient); await nonStrictRepo.createResource<Condition>({ resourceType: 'Condition', subject, code: { coding: [ { system: [sys1] as unknown as string, // Force malformed data code: val1, }, ], }, }); const res = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'code', operator: Operator.EQUALS, value: sys1 + '|', }, ], }); //TODO what is actually the expected behavior here? expect(toSortedIdentifierValues(res)).toStrictEqual([]); })); test('Handle malformed value', () => withTestContext(async () => { const subject = createReference(patient); await nonStrictRepo.createResource<Condition>({ resourceType: 'Condition', subject, code: { coding: [ { system: 'https://example.com', code: ['test1'] as unknown as string, // Force malformed data }, ], }, }); const res = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'code', operator: Operator.EQUALS, value: val1, }, ], }); //TODO what is actually the expected behavior here? expect(toSortedIdentifierValues(res)).toStrictEqual([undefined]); })); }); describe('Condition.code token queries', () => { let repo: Repository; let patient: WithId<Patient>; let subject: Reference<Patient> & { reference: string }; const conditionNames = [ 'noCodeNoCat', 'noCodeCatOne', 'codeOneNoCat', 'codeOneCatOne', 'codeOneCatTwo', 'codeOneWithoutSystemNoCat', 'codeTwoWithoutSystemCatTwo', ] as const; type Conditions = (typeof conditionNames)[number]; const cond: Record<Conditions, Condition & { id: string }> = {} as Record<Conditions, Condition & { id: string }>; const sys1 = 'http://sys.one'; const sys2 = 'http://sys.two'; const val1 = 'ABCDEFGHIJ'; const val2 = '0123456789'; const disp1 = 'The Quick Brown Fox'; beforeAll(async () => { const { project } = await createTestProject(); repo = new Repository({ strictMode: true, projects: [project], author: { reference: 'User/' + randomUUID() }, }); patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Henry'] }] }); subject = createReference(patient); const partial = cond as Record<string, Condition>; partial.noCodeNoCat = await repo.createResource<Condition>({ resourceType: 'Condition', subject, identifier: [{ value: 'noCodeNoCat' }], }); partial.noCodeCatOne = await repo.createResource<Condition>({ resourceType: 'Condition', subject, identifier: [{ value: 'noCodeCatOne' }], category: [{ coding: [{ system: sys1, code: val1, display: disp1 }] }], }); partial.codeOneNoCat = await repo.createResource<Condition>({ resourceType: 'Condition', subject, identifier: [{ value: 'codeOneNoCat' }], code: { coding: [{ system: sys1, code: val1, display: disp1 }] }, }); partial.codeOneCatOne = await repo.createResource<Condition>({ resourceType: 'Condition', subject, identifier: [{ value: 'codeOneCatOne' }], code: { coding: [{ system: sys1, code: val1 }] }, category: [{ coding: [{ system: sys1, code: val1 }] }], }); partial.codeOneCatTwo = await repo.createResource<Condition>({ resourceType: 'Condition', subject, identifier: [{ value: 'codeOneCatTwo' }], code: { coding: [{ system: sys1, code: val1 }] }, category: [{ coding: [{ system: sys2, code: val2 }] }], }); partial.codeOneWithoutSystemNoCat = await repo.createResource<Condition>({ resourceType: 'Condition', subject, identifier: [{ value: 'codeOneWithoutSystemNoCat' }], code: { coding: [{ code: val1 }] }, }); partial.codeTwoWithoutSystemCatTwo = await repo.createResource<Condition>({ resourceType: 'Condition', subject, identifier: [{ value: 'codeTwoWithoutSystemCatTwo' }], code: { coding: [{ code: val2 }] }, category: [{ coding: [{ system: sys2, code: val2 }] }], }); for (const name of conditionNames) { if (!cond[name]?.id) { throw new Error('Condition "' + name + '" not created'); } expect(cond[name].identifier?.[0].value).toEqual(name); } expect(Object.keys(cond)).toHaveLength(conditionNames.length); }); test('Sort by identifier', () => withTestContext(async () => { const system = randomUUID(); await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'None' }], identifier: [{ system }], }); await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'AAA' }], identifier: [{ system, value: 'AAA' }], }); await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'ZZZ' }], identifier: [{ system, value: 'ZZZ' }], }); const ascending = await repo.search<Patient>({ resourceType: 'Patient', filters: [{ code: 'identifier', operator: Operator.EQUALS, value: system + '|' }], sortRules: [{ code: 'identifier', descending: false }], }); expect(ascending.entry?.map((e) => e.resource?.name?.[0]?.family)).toStrictEqual(['AAA', 'ZZZ', 'None']); const descending = await repo.search<Patient>({ resourceType: 'Patient', filters: [{ code: 'identifier', operator: Operator.EQUALS, value: system + '|' }], sortRules: [{ code: 'identifier', descending: true }], }); expect(descending.entry?.map((e) => e.resource?.name?.[0]?.family)).toStrictEqual(['None', 'ZZZ', 'AAA']); })); test('Sort by identifier with multiple values', () => withTestContext(async () => { const system = randomUUID(); await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'First' }], identifier: [ { system, value: 'AAA' }, { system, value: 'ZZZ' }, ], }); await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Second' }], identifier: [{ system, value: 'LLL' }], }); const ascending = await repo.search<Patient>({ resourceType: 'Patient', filters: [{ code: 'identifier', operator: Operator.EQUALS, value: system + '|' }], sortRules: [{ code: 'identifier', descending: false }], }); const descending = await repo.search<Patient>({ resourceType: 'Patient', filters: [{ code: 'identifier', operator: Operator.EQUALS, value: system + '|' }], sortRules: [{ code: 'identifier', descending: true }], }); // Ideally ASC and DESC would have the same sort order since // ascending should use "AAA" and descending should use "ZZZ", // but a simpler sort implementation is used expect(ascending.entry?.map((e) => e.resource?.name?.[0]?.family)).toStrictEqual(['First', 'Second']); expect(descending.entry?.map((e) => e.resource?.name?.[0]?.family)).toStrictEqual(['Second', 'First']); })); test.each<[string, Conditions[]]>([ [sys1, ['codeOneNoCat', 'codeOneCatOne', 'codeOneCatTwo']], [sys2, []], [val1, []], // incorrectly passing val as a system ])('code by system %s', async (value, expected) => { const res = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'code', operator: Operator.EQUALS, value: value + '|', }, ], }); expect(toSortedIdentifierValues(res)).toStrictEqual(toSorted(expected)); }); test.each<[string, Conditions[]]>([ [sys1, []], // incorrectly passing sys as a value [val1, ['codeOneNoCat', 'codeOneCatOne', 'codeOneCatTwo', 'codeOneWithoutSystemNoCat']], [val2, ['codeTwoWithoutSystemCatTwo']], ])('code by value %s', async (value, expected) => { const resEquals = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'code', operator: Operator.EQUALS, value, }, ], }); expect(toSortedIdentifierValues(resEquals)).toStrictEqual(toSorted(expected)); const resContains = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'code', operator: Operator.CONTAINS, value, }, ], }); expect(toSortedIdentifierValues(resContains)).toStrictEqual([]); }); test.each<[string, string, Conditions[]]>([ [sys1, val1, ['codeOneNoCat', 'codeOneCatOne', 'codeOneCatTwo']], [sys1, val2, []], [sys2, val1, []], [sys2, val2, []], ])('code by system and value %s|%s', async (system, value, expected) => { const res = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'code', operator: Operator.EQUALS, value: system + '|' + value, }, ], }); expect(toSortedIdentifierValues(res)).toStrictEqual(toSorted(expected)); }); test.each<[string, Conditions[]]>([ [val1, ['codeOneWithoutSystemNoCat']], [val2, ['codeTwoWithoutSystemCatTwo']], ])('code by missing system and value %s', async (value, expected) => { const res = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'code', operator: Operator.EQUALS, value: '|' + value, }, ], }); expect(toSortedIdentifierValues(res)).toStrictEqual(expected); }); test.each<[string, string, Conditions[]]>([ [val1, val1, ['codeOneCatOne']], [val1, val2, ['codeOneCatTwo']], [val2, val1, []], [sys1 + '|', val2, ['codeOneCatTwo']], ['|' + val1, val1, []], ['|' + val2, sys2 + '|' + val2, ['codeTwoWithoutSystemCatTwo']], ])('code and category by code=%s category=%s', async (codeValue, categoryValue, expected) => { const res = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'code', operator: Operator.EQUALS, value: codeValue, }, { code: 'category', operator: Operator.EQUALS, value: categoryValue, }, ], }); expect(toSortedIdentifierValues(res)).toStrictEqual(expected); }); test.each<string>([val1, val1.slice(0, 3)])('code :contains beginning of value %s', async (value) => { const resContains = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'code', operator: Operator.CONTAINS, value, }, ], }); expect(toSortedIdentifierValues(resContains)).toStrictEqual([]); }); test.each<[string]>([[val1.slice(1, 3)], [val2.slice(1, 3)]])(`code :contains middle of value %s`, async (value) => { const resContains = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'code', operator: Operator.CONTAINS, value, }, ], }); // Token columns don't support :contains on `code` // Token lookup tables don't support infix queries expect(toSortedIdentifierValues(resContains).length).toBe(0); }); test.each<[string, Conditions[]]>([ [disp1, ['codeOneNoCat']], [disp1.split(' ').slice(0, 2).join(' '), ['codeOneNoCat']], [disp1.split(' ').slice(1, 3).join(' '), ['codeOneNoCat']], [disp1.split(' ').slice(-2).join(' '), ['codeOneNoCat']], ])('code :text %s', async (value, expected) => { const resContains = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'code', operator: Operator.TEXT, value, }, ], }); expect(toSortedIdentifierValues(resContains)).toStrictEqual(expected); }); describe('code :text search for special chars', () => { const identifier = randomUUID(); let condWithSpecialChars: WithId<Condition>; beforeAll(async () => { condWithSpecialChars = await repo.createResource<Condition>({ resourceType: 'Condition', code: { coding: [ { system: sys1, code: 'some-value', display: '.^$*+?()[]{}\\|hello|', }, ], }, subject, identifier: [ { system: sys1, value: identifier, }, ], }); }); // same behavior between token columns and lookup tables test.each<[string, boolean]>([ ['.', true], ['^', true], ['$', true], ['*', true], ['+', true], ['?', true], ['(', true], [')', true], ['[', true], [']', true], ['{', true], ['}', true], ['\\', true], ['()', true], ['[]', true], ['{}', true], ['.^', true], ['.^$*+', true], ['$', true], ['+?', true], [']{', true], ['hello', true], ['||', false], ])(':text search for %s should match? %s', async (query, shouldBeFound) => { const res = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'subject', operator: Operator.EQUALS, value: subject.reference, }, { code: 'identifier', operator: Operator.EQUALS, value: identifier, }, { code: 'code', operator: Operator.TEXT, value: query, }, ], }); if (shouldBeFound) { expect(res.entry?.length).toBe(1); expect(res.entry?.[0].resource?.id).toBe(condWithSpecialChars.id); } else { expect(res.entry?.length).toBe(0); } }); // differing behavior between token columns and lookup tables test.each<[string, boolean]>([ ['|', true], ['|hello|', true], ['.()', false], ])(':text search for %s should match? %s', async (query, shouldBeFound) => { const res = await repo.search<Condition>({ resourceType: 'Condition', filters: [ { code: 'subject', operator: Operator.EQUALS, value: subject.reference, }, { code: 'identifier', operator: Operator.EQUALS, value: identifier, }, { code: 'code', operator: Operator.TEXT, value: query, }, ], }); if (shouldBeFound) { expect(res.entry?.length).toBe(1); expect(res.entry?.[0].resource?.id).toBe(condWithSpecialChars.id); } else { expect(res.entry?.length).toBe(0); } }); }); }); describe('MedicationRequest.code legacy behavior', () => { let repo: Repository; let patient: WithId<Patient>; let mr1: WithId<MedicationRequest>; let mr2: WithId<MedicationRequest>; const sys1 = 'http://sys.one'; const val1 = 'ABCDEFGHIJ'; const val2 = 'ZZZZZZZZZZ'; beforeAll(async () => { const { project } = await createTestProject(); repo = new Repository({ strictMode: true, projects: [project], author: { reference: 'User/' + randomUUID() }, }); patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Henry'] }] }); mr1 = await repo.createResource<MedicationRequest>({ resourceType: 'MedicationRequest', status: 'active', intent: 'order', subject: createReference(patient), medicationCodeableConcept: { coding: [{ system: sys1, code: val1 }] }, }); mr2 = await repo.createResource<MedicationRequest>({ resourceType: 'MedicationRequest', status: 'active', intent: 'order', subject: createReference(patient), medicationCodeableConcept: { coding: [{ system: sys1, code: val2 }] }, }); }); test('Formerly "legacy" column implementation search params use `token-column` strategy', async () => { const searchParam = getSearchParameter('MedicationRequest', 'code'); if (!searchParam) { throw new Error('Could not find MedicationRequest-code search parameter'); } const impl = getSearchParameterImplementation('MedicationRequest', searchParam); expect(impl.searchStrategy).toBe('token-column'); }); test('Search by system only works when using token columns', async () => { const searchResult = await repo.search<MedicationRequest>({ resourceType: 'MedicationRequest', filters: [ { code: 'code', operator: Operator.EQUALS, value: sys1 + '|', }, ], }); expect(searchResult.entry?.length).toStrictEqual(2); expect(searchResult.entry?.map((e) => e.resource?.id)).toContain(mr1.id); expect(searchResult.entry?.map((e) => e.resource?.id)).toContain(mr2.id); }); test('Search by value always works', async () => { const searchResult = await repo.search({ resourceType: 'MedicationRequest', filters: [ { code: 'code', operator: Operator.EQUALS, value: val1, }, ], }); expect(searchResult.entry?.length).toStrictEqual(1); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(mr1.id); }); test('Search by system and value only works when using token columns', async () => { const searchResult = await repo.search({ resourceType: 'MedicationRequest', filters: [ { code: 'code', operator: Operator.EQUALS, value: sys1 + '|' + val1, }, ], }); expect(searchResult.entry?.length).toStrictEqual(1); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(mr1.id); }); test('Sort by code always works', async () => { const ascResult = await repo.search<MedicationRequest>({ resourceType: 'MedicationRequest', filters: [ { code: 'patient', operator: Operator.EQUALS, value: patient.id, }, ], sortRules: [{ code: 'code', descending: false }], }); expect(ascResult.entry?.length).toStrictEqual(2); expect(ascResult.entry?.map((e) => e.resource?.id)).toContain(mr1.id); expect(ascResult.entry?.map((e) => e.resource?.id)).toContain(mr2.id); expect(ascResult.entry?.map((e) => e.resource?.medicationCodeableConcept?.coding?.[0]?.code)).toStrictEqual([ 'ABCDEFGHIJ', 'ZZZZZZZZZZ', ]); const descResult = await repo.search<MedicationRequest>({ resourceType: 'MedicationRequest', filters: [ { code: 'patient', operator: Operator.EQUALS, value: patient.id, }, ], sortRules: [{ code: 'code', descending: true }], }); expect(descResult.entry?.length).toStrictEqual(2); expect(descResult.entry?.map((e) => e.resource?.id)).toContain(mr1.id); expect(descResult.entry?.map((e) => e.resource?.id)).toContain(mr2.id); expect(descResult.entry?.map((e) => e.resource?.medicationCodeableConcept?.coding?.[0]?.code)).toStrictEqual([ 'ZZZZZZZZZZ', 'ABCDEFGHIJ', ]); }); }); describe('buildTokens', () => { beforeAll(() => { loadStructureDefinitions(); }); test('empty resource', () => { const identifierSearchParam = getSearchParameter('Patient', 'identifier'); if (!identifierSearchParam) { throw new Error('Could not find identifier search parameter'); } const r1: WithId<Patient> = { resourceType: 'Patient', id: '1', identifier: undefined, }; const result1: Token[] = []; buildTokensForSearchParameter(result1, r1, identifierSearchParam); expect(result1).toStrictEqual([]); const validIdentifiers: Identifier[] = [ {}, { system: 'http://example.com', value: '123' }, { system: 'http://example.com', value: '123' }, { system: 'http://example.com', value: '456' }, { use: 'official' }, ]; const r2: WithId<Patient> = { resourceType: 'Patient', id: '2', identifier: [null, undefined, ...validIdentifiers] as unknown as Identifier[], }; const result2: Token[] = []; buildTokensForSearchParameter(result2, r2, identifierSearchParam); expect(result2).toStrictEqual([ { code: 'identifier', system: 'http://example.com', value: '123' }, { code: 'identifier', system: 'http://example.com', value: '456' }, ]); }); }); function toIdentifierValues(bundle: Bundle<Condition | Patient>): string[] { return bundle.entry?.map((e) => e.resource?.identifier?.[0].value as string) ?? []; } function toSortedIdentifierValues(bundle: Bundle<Condition | Patient>): string[] { return toSorted(toIdentifierValues(bundle)); } // Array.prototype.toSorted isn't available in Node 18, so write our own function toSorted<T extends string>(array: T[]): T[] { const newArray = Array.from(array); newArray.sort((a, b) => a.localeCompare(b)); return newArray; }

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