Skip to main content
Glama
search.test.ts185 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { Filter, OperationOutcomeError, SearchRequest, WithId } from '@medplum/core'; import { createReference, getReferenceString, getSearchParameter, LOINC, normalizeErrorString, Operator, parseSearchRequest, SNOMED, } from '@medplum/core'; import type { ActivityDefinition, AllergyIntolerance, Appointment, AuditEvent, Binary, Bundle, BundleEntry, CareTeam, Coding, Communication, Composition, Condition, DiagnosticReport, Encounter, EvidenceVariable, Goal, HealthcareService, Location, MeasureReport, Observation, Organization, Patient, PlanDefinition, Practitioner, Project, Provenance, Questionnaire, QuestionnaireResponse, ResearchStudy, Resource, RiskAssessment, SearchParameter, ServiceRequest, StructureDefinition, Task, } from '@medplum/fhirtypes'; import { randomUUID } from 'crypto'; import { initAppServices, shutdownApp } from '../app'; import { loadTestConfig } from '../config/loader'; import type { MedplumServerConfig } from '../config/types'; import { DatabaseMode } from '../database'; import { bundleContains, createTestProject, withTestContext } from '../test.setup'; import { getSystemRepo, Repository } from './repo'; import { clampEstimateCount } from './search'; import type { TokenColumnSearchParameterImplementation } from './searchparameter'; import { getSearchParameterImplementation } from './searchparameter'; import { SelectQuery } from './sql'; jest.mock('hibp'); const SUBSET_TAG: Coding = { system: 'http://hl7.org/fhir/v3/ObservationValue', code: 'SUBSETTED' }; describe('project-scoped Repository', () => { let config: MedplumServerConfig; let repo: Repository; beforeAll(async () => { config = await loadTestConfig(); await initAppServices(config); const { project } = await createTestProject(); repo = new Repository({ strictMode: true, projects: [project], currentProject: project, author: { reference: 'User/' + randomUUID() }, }); }); afterAll(async () => { await shutdownApp(); }); test('Search total', async () => withTestContext(async () => { await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], }); await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Bob'], family: 'Smith' }], }); const result1 = await repo.search({ resourceType: 'Patient', count: 1, }); expect(result1.total).toBeUndefined(); expect(result1.link?.length).toBe(3); const result2 = await repo.search({ resourceType: 'Patient', total: 'none', }); expect(result2.total).toBeUndefined(); const result3 = await repo.search({ resourceType: 'Patient', total: 'accurate', }); expect(result3.total).toBeDefined(); expect(typeof result3.total).toBe('number'); const result4 = await repo.search({ resourceType: 'Patient', total: 'estimate', }); expect(result4.total).toBeDefined(); expect(typeof result4.total).toBe('number'); })); test('Search count=0', async () => withTestContext(async () => { const result1 = await repo.search({ resourceType: 'Patient', count: 0, }); expect(result1.entry).toBeUndefined(); expect(result1.link).toBeDefined(); expect(result1.link?.length).toBe(1); })); test('Search offset max', async () => withTestContext(async () => { // Temporarily set a low maxSearchOffset const prevMax = config.maxSearchOffset; config.maxSearchOffset = 200; // Search under the limit, this should succeed const result1 = await repo.search({ resourceType: 'Patient', offset: 100, }); expect(result1.entry).toHaveLength(0); // Search over the limit, this should fail try { await repo.search({ resourceType: 'Patient', offset: 300, }); fail('Expected error'); } catch (err) { expect(normalizeErrorString(err)).toStrictEqual('Search offset exceeds maximum (got 300, max 200)'); } // Restore the maxSearchOffset config.maxSearchOffset = prevMax; })); test.each<[number | undefined, number, number | undefined, number]>([ // offset, estimateCount, rowCount, expected [undefined, 0, undefined, 0], // First page (offset = 0, count = 20) [0, 0, 0, 0], [0, 10, 0, 0], // 10 estimated rows, 0 actual => we know count is 0 [0, 0, 10, 10], // 0 estimated, 10 actual => 10 (estimate is too low) [0, 20, 20, 20], // 20 estimated, 20 actual => 20 (estimate accurate) [0, 1000, 21, 1000], // 1000 estimated, full page (21) returned => 1000 (estimate could be correct) // Second page (offset = 20, count = 20) [20, 20, 0, 20], // 20 estimated, empty page returned => 20 (estimate is correct) [20, 0, 0, 0], // 0 estimated, empty page returned => 20 (estimate is too low, but rowCount is 0) [20, 0, 1, 21], // rowCount = 1, estimate = 0 => 21 (estimate is too low) [20, 200, 0, 20], // rowCount = 0, estimate = 200 => 20 (estimate is too high) [20, 200, 1, 21], // rowCount = 1, estimate = 200 => 20 (estimate is too high) [20, 200, 21, 200], // rowCount = 21, estimate = 200 => 200 (estimate could be correct) ])( 'clampEstimateCount: offset = %p, %p estimated, %p returned => %p', (offset, estimateCount, rowCount, expected) => { expect(clampEstimateCount({ resourceType: 'Patient', offset }, estimateCount, rowCount)).toBe(expected); } ); test('Search _summary', () => withTestContext(async () => { const patient: Patient = { resourceType: 'Patient', meta: { profile: ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient'], tag: [{ system: 'http://example.com/', code: 'test' }], }, text: { status: 'generated', div: '<div xmlns="http://www.w3.org/1999/xhtml"></div>', }, identifier: [ { use: 'usual', type: { coding: [ { system: 'http://terminology.hl7.org/CodeSystem/v2-0203', code: 'MR', display: 'Medical Record Number', }, ], text: 'Medical Record Number', }, system: 'http://hospital.smarthealthit.org', value: '1032702', }, ], active: true, name: [ { use: 'old', family: 'Shaw', given: ['Amy', 'V.'], period: { start: '2016-12-06', end: '2020-07-22', }, }, { family: 'Baxter', given: ['Amy', 'V.'], suffix: ['PharmD'], period: { start: '2020-07-22', }, }, ], telecom: [ { system: 'phone', value: '555-555-5555', use: 'home', }, { system: 'email', value: 'amy.shaw@example.com', }, ], gender: 'female', birthDate: '1987-02-20', multipleBirthInteger: 2, address: [ { use: 'old', line: ['49 Meadow St'], city: 'Mounds', state: 'OK', postalCode: '74047', country: 'US', period: { start: '2016-12-06', end: '2020-07-22', }, }, { line: ['183 Mountain View St'], city: 'Mounds', state: 'OK', postalCode: '74048', country: 'US', period: { start: '2020-07-22', }, }, ], }; const resource = await repo.createResource(patient); // _summary=text const textResults = await repo.search({ resourceType: 'Patient', filters: [{ code: '_id', operator: Operator.EQUALS, value: resource.id }], summary: 'text', }); expect(textResults.entry).toHaveLength(1); const textResult = textResults.entry?.[0]?.resource as Resource; expect(textResult).toEqual<Partial<Patient>>({ resourceType: 'Patient', id: resource.id, meta: expect.objectContaining({ profile: ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient'], tag: [{ system: 'http://example.com/', code: 'test' }, SUBSET_TAG], }), text: { status: 'generated', div: '<div xmlns="http://www.w3.org/1999/xhtml"></div>', }, }); // _summary=data const dataResults = await repo.search({ resourceType: 'Patient', filters: [{ code: '_id', operator: Operator.EQUALS, value: resource.id }], summary: 'data', }); expect(dataResults.entry).toHaveLength(1); const dataResult = dataResults.entry?.[0]?.resource as Resource; const { text: _1, ...dataExpected } = resource; dataExpected.meta?.tag?.push(SUBSET_TAG); expect(dataResult).toEqual<Partial<Patient>>({ ...dataExpected }); // _summary=true const summaryResults = await repo.search({ resourceType: 'Patient', filters: [{ code: '_id', operator: Operator.EQUALS, value: resource.id }], summary: 'true', }); expect(summaryResults.entry).toHaveLength(1); const summaryResult = summaryResults.entry?.[0]?.resource as Resource; const { multipleBirthInteger: _2, text: _3, ...summaryResource } = resource; expect(summaryResult).toEqual<Partial<Patient>>(summaryResource); })); test('Search _elements', () => withTestContext(async () => { const patient: Patient = { resourceType: 'Patient', birthDate: '2000-01-01', _birthDate: { extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/patient-birthTime', valueDateTime: '2000-01-01T00:00:00.001Z', }, ], }, multipleBirthInteger: 2, deceasedBoolean: false, } as unknown as Patient; const resource = await repo.createResource(patient); const results = await repo.search({ resourceType: 'Patient', filters: [{ code: '_id', operator: Operator.EQUALS, value: resource.id }], fields: ['birthDate', 'deceased'], }); expect(results.entry).toHaveLength(1); const result = results.entry?.[0]?.resource as Resource; expect(result).toEqual<Partial<Patient>>({ resourceType: 'Patient', id: resource.id, meta: expect.objectContaining({ tag: [SUBSET_TAG], }), birthDate: resource.birthDate, _birthDate: (resource as any)._birthDate, deceasedBoolean: resource.deceasedBoolean, } as unknown as Patient); })); test('Search next link', () => withTestContext(async () => { const family = randomUUID(); for (let i = 0; i < 2; i++) { await repo.createResource({ resourceType: 'Patient', name: [{ family }], }); } const result1 = await repo.search({ resourceType: 'Patient', filters: [{ code: 'name', operator: Operator.EQUALS, value: family }], count: 1, }); expect(result1.entry).toHaveLength(1); expect(result1.link).toBeDefined(); expect(result1.link?.find((e) => e.relation === 'next')).toBeDefined(); const result2 = await repo.search({ resourceType: 'Patient', filters: [{ code: 'name', operator: Operator.EQUALS, value: family }], count: 2, }); expect(result2.entry).toHaveLength(2); expect(result2.link).toBeDefined(); expect(result2.link?.find((e) => e.relation === 'next')).toBeUndefined(); const result3 = await repo.search({ resourceType: 'Patient', filters: [{ code: 'name', operator: Operator.EQUALS, value: family }], count: 3, }); expect(result3.entry).toHaveLength(2); expect(result3.link).toBeDefined(); expect(result3.link?.find((e) => e.relation === 'next')).toBeUndefined(); })); test('Search previous link', () => withTestContext(async () => { const family = randomUUID(); for (let i = 0; i < 2; i++) { await repo.createResource({ resourceType: 'Patient', name: [{ family }], }); } const result1 = await repo.search({ resourceType: 'Patient', filters: [{ code: 'name', operator: Operator.EQUALS, value: family }], count: 1, offset: 1, }); expect(result1.entry).toHaveLength(1); expect(result1.link).toBeDefined(); expect(result1.link?.find((e) => e.relation === 'previous')).toBeDefined(); })); test('Search for Communications by Encounter', () => withTestContext(async () => { const patient1 = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], }); expect(patient1).toBeDefined(); const encounter1 = await repo.createResource<Encounter>({ resourceType: 'Encounter', status: 'in-progress', class: { code: 'HH', display: 'home health', }, subject: createReference(patient1 as Patient), }); expect(encounter1).toBeDefined(); const comm1 = await repo.createResource<Communication>({ resourceType: 'Communication', status: 'completed', encounter: createReference(encounter1 as Encounter), subject: createReference(patient1 as Patient), sender: createReference(patient1 as Patient), payload: [{ contentString: 'This is a test' }], }); expect(comm1).toBeDefined(); const patient2 = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Bob'], family: 'Jones' }], }); expect(patient2).toBeDefined(); const encounter2 = await repo.createResource<Encounter>({ resourceType: 'Encounter', status: 'in-progress', class: { code: 'HH', display: 'home health', }, subject: createReference(patient2 as Patient), }); expect(encounter2).toBeDefined(); const comm2 = await repo.createResource<Communication>({ resourceType: 'Communication', status: 'completed', encounter: createReference(encounter2), subject: createReference(patient2), sender: createReference(patient2), payload: [{ contentString: 'This is another test' }], }); expect(comm2).toBeDefined(); const searchResult = await repo.search({ resourceType: 'Communication', filters: [ { code: 'encounter', operator: Operator.EQUALS, value: getReferenceString(encounter1), }, ], }); expect(searchResult.entry?.length).toStrictEqual(1); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(comm1.id); })); test('Search for Communications by ServiceRequest', () => withTestContext(async () => { const patient1 = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], }); expect(patient1).toBeDefined(); const serviceRequest1 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', code: { text: 'text', }, subject: createReference(patient1 as Patient), }); expect(serviceRequest1).toBeDefined(); const comm1 = await repo.createResource<Communication>({ resourceType: 'Communication', status: 'completed', basedOn: [createReference(serviceRequest1 as ServiceRequest)], subject: createReference(patient1 as Patient), sender: createReference(patient1 as Patient), payload: [{ contentString: 'This is a test' }], }); expect(comm1).toBeDefined(); const patient2 = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Bob'], family: 'Jones' }], }); expect(patient2).toBeDefined(); const serviceRequest2 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', code: { text: 'test', }, subject: createReference(patient2 as Patient), }); expect(serviceRequest2).toBeDefined(); const comm2 = await repo.createResource<Communication>({ resourceType: 'Communication', status: 'completed', basedOn: [createReference(serviceRequest2 as ServiceRequest)], subject: createReference(patient2 as Patient), sender: createReference(patient2 as Patient), payload: [{ contentString: 'This is another test' }], }); expect(comm2).toBeDefined(); const searchResult = await repo.search({ resourceType: 'Communication', filters: [ { code: 'based-on', operator: Operator.EQUALS, value: getReferenceString(serviceRequest1), }, ], }); expect(searchResult.entry?.length).toStrictEqual(1); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(comm1.id); })); test('Search for QuestionnaireResponse by Questionnaire', () => withTestContext(async () => { const questionnaire = await repo.createResource<Questionnaire>({ resourceType: 'Questionnaire', url: 'https://example.com/yet-another-example-questionnaire', status: 'active', }); const response1 = await repo.createResource<QuestionnaireResponse>({ resourceType: 'QuestionnaireResponse', status: 'completed', questionnaire: questionnaire.url, }); await repo.createResource<QuestionnaireResponse>({ resourceType: 'QuestionnaireResponse', status: 'completed', questionnaire: 'https://example.com/a-different-example-questionnaire', }); const bundle = await repo.search({ resourceType: 'QuestionnaireResponse', filters: [ { code: 'questionnaire', operator: Operator.EQUALS, value: questionnaire.url as string, }, ], }); expect(bundle.entry?.length).toStrictEqual(1); expect(bundle.entry?.[0]?.resource?.id).toStrictEqual(response1.id); })); test('Search for token in array', async () => withTestContext(async () => { const bundle = await repo.search({ resourceType: 'SearchParameter', filters: [ { code: 'base', operator: Operator.EQUALS, value: 'Patient', }, ], count: 100, }); expect(bundle.entry?.find((e) => (e.resource as SearchParameter).code === 'name')).toBeDefined(); expect(bundle.entry?.find((e) => (e.resource as SearchParameter).code === 'email')).toBeDefined(); })); test('Search sort by Patient.id', async () => withTestContext(async () => { const bundle = await repo.search({ resourceType: 'Patient', sortRules: [{ code: '_id' }], }); expect(bundle).toBeDefined(); })); test('Search sort by Patient.meta.lastUpdated', async () => withTestContext(async () => { const bundle = await repo.search({ resourceType: 'Patient', sortRules: [{ code: '_lastUpdated' }], }); expect(bundle).toBeDefined(); })); test('Search sort by Patient.identifier', async () => withTestContext(async () => { const bundle = await repo.search({ resourceType: 'Patient', sortRules: [{ code: 'identifier' }], }); expect(bundle).toBeDefined(); })); test('Search sort by Patient.name', async () => withTestContext(async () => { const bundle = await repo.search({ resourceType: 'Patient', sortRules: [{ code: 'name' }], }); expect(bundle).toBeDefined(); })); test('Search sort by Patient.given', async () => withTestContext(async () => { const bundle = await repo.search({ resourceType: 'Patient', sortRules: [{ code: 'given' }], }); expect(bundle).toBeDefined(); })); test('Search sort by Patient.address', async () => withTestContext(async () => { const identifier = '13438374-1515-4da7-ab0d-6992a39bfed1'; await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], identifier: [{ system: 'https://www.example.com', value: identifier }], address: [{ line: ['1234 Fake Street'], city: 'Fake City', state: 'OK', postalCode: '99999', country: 'US' }], }); const bundle = await repo.search({ resourceType: 'Patient', sortRules: [{ code: 'address' }], filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier, }, ], }); const identifiers = bundle.entry?.map((e) => (e.resource as Patient).identifier?.[0]?.value); expect(identifiers).toStrictEqual([identifier]); })); test('Search sort by Patient.postalCode', async () => withTestContext(async () => { const identifier = '137f8a2f-4dff-4bb0-b971-f9f34d50a97c'; await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Hopper' }], identifier: [{ system: 'https://www.example.com', value: identifier }], address: [ { line: ['1234 Fake Street'], city: 'Fake City', state: 'OK', postalCode: '123456789', country: 'US' }, ], }); const bundle = await repo.search({ resourceType: 'Patient', sortRules: [{ code: 'address-postalcode' }], filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier, }, ], }); const identifiers = bundle.entry?.map((e) => (e.resource as Patient).identifier?.[0]?.value); expect(identifiers).toStrictEqual([identifier]); })); test('Search sort by Patient.telecom', async () => withTestContext(async () => { const bundle = await repo.search({ resourceType: 'Patient', sortRules: [{ code: 'telecom' }], }); expect(bundle).toBeDefined(); })); test('Search sort by Patient.email', async () => withTestContext(async () => { const bundle = await repo.search({ resourceType: 'Patient', sortRules: [{ code: 'email' }], }); expect(bundle).toBeDefined(); })); test('Search sort by Patient.birthDate', async () => withTestContext(async () => { const bundle = await repo.search({ resourceType: 'Patient', sortRules: [{ code: 'birthdate' }], }); expect(bundle).toBeDefined(); })); test('Filter and sort on same search parameter', () => withTestContext(async () => { await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Marge'], family: 'Simpson' }], }); await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Homer'], family: 'Simpson' }], }); const bundle = await repo.search({ resourceType: 'Patient', filters: [{ code: 'family', operator: Operator.EQUALS, value: 'Simpson' }], sortRules: [{ code: 'family' }], }); expect(bundle.entry).toBeDefined(); expect(bundle.entry?.length).toBeGreaterThanOrEqual(2); })); test('Search birthDate after delete', () => withTestContext(async () => { const family = randomUUID(); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family }], birthDate: '1971-02-02', }); const searchResult1 = await repo.search({ resourceType: 'Patient', filters: [ { code: 'family', operator: Operator.EQUALS, value: family, }, { code: 'birthdate', operator: Operator.EQUALS, value: '1971-02-02', }, ], }); expect(searchResult1.entry?.length).toStrictEqual(1); expect(searchResult1.entry?.[0]?.resource?.id).toStrictEqual(patient.id); await repo.deleteResource('Patient', patient.id); const searchResult2 = await repo.search({ resourceType: 'Patient', filters: [ { code: 'family', operator: Operator.EQUALS, value: family, }, { code: 'birthdate', operator: Operator.EQUALS, value: '1971-02-02', }, ], }); expect(searchResult2.entry?.length).toStrictEqual(0); })); test('Search identifier after delete', () => withTestContext(async () => { const identifier = randomUUID(); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], identifier: [{ system: 'https://www.example.com', value: identifier }], }); const searchResult1 = await repo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier, }, ], }); expect(searchResult1.entry?.length).toStrictEqual(1); expect(searchResult1.entry?.[0]?.resource?.id).toStrictEqual(patient.id); await repo.deleteResource('Patient', patient.id); const searchResult2 = await repo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: identifier, }, ], }); expect(searchResult2.entry?.length).toStrictEqual(0); })); test('String filter', async () => withTestContext(async () => { const bundle1 = await repo.search<StructureDefinition>({ resourceType: 'StructureDefinition', filters: [ { code: 'name', operator: Operator.EQUALS, value: 'Questionnaire', }, ], sortRules: [ { code: 'name', descending: false, }, ], }); expect(bundle1.entry?.map((e) => e.resource?.name)).toStrictEqual(['Questionnaire', 'QuestionnaireResponse']); const bundle2 = await repo.search({ resourceType: 'StructureDefinition', filters: [ { code: 'name', operator: Operator.EXACT, value: 'Questionnaire', }, ], }); expect(bundle2.entry?.length).toStrictEqual(1); expect((bundle2.entry?.[0]?.resource as StructureDefinition).name).toStrictEqual('Questionnaire'); })); test('String filter with escaped commas', async () => withTestContext(async () => { // Create a name with commas const name = randomUUID().replaceAll('-', ','); const location = await repo.createResource<Location>({ resourceType: 'Location', name, }); const bundle = await repo.search<Location>({ resourceType: 'Location', filters: [ { code: 'name', operator: Operator.EXACT, value: name.replaceAll(',', '\\,'), }, ], }); expect(bundle.entry?.length).toStrictEqual(1); expect(bundleContains(bundle, location)).toBeDefined(); })); test('Filter by _id', () => withTestContext(async () => { // Unique family name to isolate the test const family = randomUUID(); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family }], }); expect(patient).toBeDefined(); const searchResult1 = await repo.search({ resourceType: 'Patient', filters: [ { code: '_id', operator: Operator.EQUALS, value: patient.id, }, ], }); expect(searchResult1.entry?.length).toStrictEqual(1); expect(bundleContains(searchResult1 as Bundle, patient as Patient)).toBeDefined(); const searchResult2 = await repo.search({ resourceType: 'Patient', filters: [ { code: 'name', operator: Operator.EQUALS, value: family, }, { code: '_id', operator: Operator.NOT_EQUALS, value: patient.id, }, ], }); expect(searchResult2.entry?.length).toStrictEqual(0); })); test('Filter by chained _id', () => withTestContext(async () => { const organizationId = randomUUID(); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', managingOrganization: { reference: 'Organization/' + organizationId }, }); const searchResult1 = await repo.search(parseSearchRequest('Patient?organization._id=' + organizationId)); expect(searchResult1.entry?.length).toStrictEqual(1); expect(bundleContains(searchResult1 as Bundle, patient as Patient)).toBeDefined(); })); test('Reverse filter by chained _id', () => withTestContext(async () => { // Create Location const location = await repo.createResource<Location>({ resourceType: 'Location', }); // Create Patient const healthcareService = await repo.createResource<HealthcareService>({ resourceType: 'HealthcareService', location: [createReference(location)], }); // Search chain const searchResult = await repo.search( parseSearchRequest(`Location?_has:HealthcareService:location:_id=${healthcareService.id}`) ); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(location.id); })); test('Empty _id', async () => withTestContext(async () => { const searchResult1 = await repo.search({ resourceType: 'Patient', filters: [ { code: '_id', operator: Operator.EQUALS, value: '', }, ], }); expect(searchResult1.entry?.length).toStrictEqual(0); })); test('Non UUID _id', async () => withTestContext(async () => { const searchResult1 = await repo.search({ resourceType: 'Patient', filters: [ { code: '_id', operator: Operator.EQUALS, value: 'x', }, ], }); expect(searchResult1.entry?.length).toStrictEqual(0); })); test('Non UUID _compartment', async () => withTestContext(async () => { const searchResult1 = await repo.search({ resourceType: 'Patient', filters: [ { code: '_compartment', operator: Operator.EQUALS, value: 'x', }, ], }); expect(searchResult1.entry?.length).toStrictEqual(0); })); test('Reference string _compartment', () => withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient' }); const searchResult1 = await repo.search({ resourceType: 'Patient', filters: [ { code: '_compartment', operator: Operator.EQUALS, value: getReferenceString(patient), }, ], }); expect(searchResult1.entry?.length).toStrictEqual(1); expect(bundleContains(searchResult1 as Bundle, patient as Patient)).toBeDefined(); })); test.each([ ['xyz', false], ['1985', false], ['1985-11', false], ['1985-11-30', true], ['1985-11-30T05:05', true], ['1985-11-30 05:05', true], ['1985-11-30T05:05Z', true], ['1985-11-30 05:05Z', true], ['1985-11-30T05:05:55', true], ['1985-11-30 05:05:55', true], ['1985-11-30T05:05:55Z', true], ['1985-11-30T05:05:55.000Z', true], ['1985-11-30T05:05:55.000000000Z', true], ['1985-11-30T05:05:55+05:05', true], ['1985-11-30T05:05:55-13:30', true], ['1985-11-30T05:05:55-14:00', true], ['1985-11-30T05:05:55.000000000-14:00', true], ['1985-11-30T05:05:55-15:00', false], ])('Handle_lastUpdated value %s, isValid? %s', async (value, isValid) => withTestContext(async () => { const searchPromise = repo.search({ resourceType: 'Patient', filters: [ { code: '_lastUpdated', operator: Operator.LESS_THAN, value, }, ], }); if (isValid) { const searchResult = await searchPromise; expect(searchResult.entry?.length).toStrictEqual(0); } else { await expect(searchPromise).rejects.toThrow(`Invalid date value: ${value}`); } }) ); test('Handle non-string value', async () => withTestContext(async () => { try { await repo.search({ resourceType: 'Patient', filters: [ { code: '_id', operator: Operator.EQUALS, value: {} as unknown as string, }, ], }); fail('Expected error'); } catch (err) { expect(normalizeErrorString(err)).toStrictEqual('Search filter value must be a string'); } })); test('Handle string with null bytes', async () => withTestContext(async () => { try { await repo.search({ resourceType: 'Patient', filters: [ { code: '_id', operator: Operator.EQUALS, value: 'foo\x00bar', }, ], }); fail('Expected error'); } catch (err) { expect(normalizeErrorString(err)).toStrictEqual('Search filter value cannot contain null bytes'); } })); test('Filter by Coding', () => withTestContext(async () => { const auditEvents = [] as AuditEvent[]; for (let i = 0; i < 3; i++) { const resource = await repo.createResource<AuditEvent>({ resourceType: 'AuditEvent', recorded: new Date().toISOString(), type: { code: randomUUID(), }, agent: [ { who: { reference: 'Practitioner/' + randomUUID() }, requestor: true, }, ], source: { observer: { reference: 'Practitioner/' + randomUUID() }, }, }); auditEvents.push(resource); } for (let i = 0; i < 3; i++) { const bundle = await repo.search({ resourceType: 'AuditEvent', filters: [ { code: 'type', operator: Operator.EQUALS, value: auditEvents[i].type?.code as string, }, ], }); expect(bundle.entry?.length).toStrictEqual(1); expect(bundle.entry?.[0]?.resource?.id).toStrictEqual(auditEvents[i].id); } })); test('Filter by CodeableConcept', () => withTestContext(async () => { const x1 = randomUUID(); const x2 = randomUUID(); const x3 = randomUUID(); // Create test patient const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['John'], family: 'CodeableConcept' }], }); const serviceRequest1 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: createReference(patient), code: { coding: [{ code: x1 }] }, }); const serviceRequest2 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: createReference(patient), code: { coding: [{ code: x2 }] }, }); const serviceRequest3 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: createReference(patient), code: { coding: [{ code: x3 }] }, }); const bundle1 = await repo.search({ resourceType: 'ServiceRequest', filters: [ { code: 'code', operator: Operator.EQUALS, value: x1, }, ], }); expect(bundle1.entry?.length).toStrictEqual(1); expect(bundleContains(bundle1, serviceRequest1)).toBeDefined(); expect(bundleContains(bundle1, serviceRequest2)).toBeUndefined(); expect(bundleContains(bundle1, serviceRequest3)).toBeUndefined(); const bundle2 = await repo.search({ resourceType: 'ServiceRequest', filters: [ { code: 'code', operator: Operator.EQUALS, value: x2, }, ], }); expect(bundle2.entry?.length).toStrictEqual(1); expect(bundleContains(bundle2, serviceRequest1)).toBeUndefined(); expect(bundleContains(bundle2, serviceRequest2)).toBeDefined(); expect(bundleContains(bundle2, serviceRequest3)).toBeUndefined(); const bundle3 = await repo.search({ resourceType: 'ServiceRequest', filters: [ { code: 'code', operator: Operator.EQUALS, value: x3, }, ], }); expect(bundle3.entry?.length).toStrictEqual(1); expect(bundleContains(bundle3, serviceRequest1)).toBeUndefined(); expect(bundleContains(bundle3, serviceRequest2)).toBeUndefined(); expect(bundleContains(bundle3, serviceRequest3)).toBeDefined(); })); test('Filter by Quantity.value', () => withTestContext(async () => { const code = randomUUID(); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['John'], family: 'Quantity' }], }); const observation1 = await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', subject: createReference(patient), code: { coding: [{ code }] }, valueQuantity: { value: 1, unit: 'mg' }, }); const observation2 = await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', subject: createReference(patient), code: { coding: [{ code }] }, valueQuantity: { value: 5, unit: 'mg' }, }); const observation3 = await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', subject: createReference(patient), code: { coding: [{ code }] }, valueQuantity: { value: 10, unit: 'mg' }, }); const bundle1 = await repo.search<Observation>({ resourceType: 'Observation', filters: [{ code: 'code', operator: Operator.EQUALS, value: code }], sortRules: [{ code: 'value-quantity', descending: false }], }); expect(bundle1.entry?.length).toStrictEqual(3); expect(bundle1.entry?.[0]?.resource?.id).toStrictEqual(observation1.id); expect(bundle1.entry?.[1]?.resource?.id).toStrictEqual(observation2.id); expect(bundle1.entry?.[2]?.resource?.id).toStrictEqual(observation3.id); const bundle2 = await repo.search<Observation>({ resourceType: 'Observation', filters: [{ code: 'code', operator: Operator.EQUALS, value: code }], sortRules: [{ code: 'value-quantity', descending: true }], }); expect(bundle2.entry?.length).toStrictEqual(3); expect(bundle2.entry?.[0]?.resource?.id).toStrictEqual(observation3.id); expect(bundle2.entry?.[1]?.resource?.id).toStrictEqual(observation2.id); expect(bundle2.entry?.[2]?.resource?.id).toStrictEqual(observation1.id); const bundle3 = await repo.search<Observation>({ resourceType: 'Observation', filters: [ { code: 'code', operator: Operator.EQUALS, value: code }, { code: 'value-quantity', operator: Operator.GREATER_THAN, value: '8' }, ], }); expect(bundle3.entry?.length).toStrictEqual(1); expect(bundle3.entry?.[0]?.resource?.id).toStrictEqual(observation3.id); })); test('ServiceRequest.orderDetail search', () => withTestContext(async () => { const orderDetailText = randomUUID(); const orderDetailCode = randomUUID(); const serviceRequest = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/' + randomUUID(), }, code: { coding: [ { code: 'order-type', }, ], }, orderDetail: [ { text: orderDetailText, coding: [ { system: 'custom-order-system', code: orderDetailCode, }, ], }, ], }); const bundle1 = await repo.search({ resourceType: 'ServiceRequest', filters: [ { code: 'order-detail', operator: Operator.CONTAINS, value: orderDetailText, }, ], }); expect(bundle1.entry?.length).toStrictEqual(1); expect(bundle1.entry?.[0]?.resource?.id).toStrictEqual(serviceRequest.id); })); test('Comma separated value', () => withTestContext(async () => { const category = randomUUID(); const codes = [randomUUID(), randomUUID(), randomUUID()]; const serviceRequests = []; for (const code of codes) { const serviceRequest = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/' + randomUUID() }, category: [{ coding: [{ code: category }] }], code: { coding: [{ code: code }] }, }); serviceRequests.push(serviceRequest); } const bundle1 = await repo.search( parseSearchRequest('ServiceRequest', { category, code: `${codes[0]},${codes[1]}` }) ); expect(bundle1.entry?.length).toStrictEqual(2); expect(bundleContains(bundle1, serviceRequests[0])).toBeDefined(); expect(bundleContains(bundle1, serviceRequests[1])).toBeDefined(); })); test('Token not equals', () => withTestContext(async () => { const category = randomUUID(); const code1 = randomUUID(); const code2 = randomUUID(); const serviceRequest1 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/' + randomUUID() }, category: [{ coding: [{ code: category }] }], code: { coding: [{ code: code1 }] }, }); const serviceRequest2 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/' + randomUUID() }, category: [{ coding: [{ code: category }] }], code: { coding: [{ code: code2 }] }, }); const bundle1 = await repo.search(parseSearchRequest('ServiceRequest', { category, 'code:not': code1 })); expect(bundle1.entry?.length).toStrictEqual(1); expect(bundleContains(bundle1, serviceRequest1)).toBeUndefined(); expect(bundleContains(bundle1, serviceRequest2)).toBeDefined(); })); test('Token not equals matches missing value', () => withTestContext(async () => { const family = randomUUID(); const patient1 = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ family }], gender: 'male', }); const patient2 = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ family }], }); const bundle1 = await repo.search(parseSearchRequest('Patient', { name: family, 'gender:not': 'male' })); expect(bundle1.entry?.length).toStrictEqual(1); expect(bundleContains(bundle1, patient1)).toBeUndefined(); expect(bundleContains(bundle1, patient2)).toBeDefined(); })); test('Token array not equals', () => withTestContext(async () => { const category1 = randomUUID(); const category2 = randomUUID(); const code = randomUUID(); const serviceRequest1 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/' + randomUUID() }, category: [{ coding: [{ code: category1 }] }], code: { coding: [{ code }] }, }); const serviceRequest2 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/' + randomUUID() }, category: [{ coding: [{ code: category2 }] }], code: { coding: [{ code }] }, }); const bundle1 = await repo.search(parseSearchRequest('ServiceRequest', { code, 'category:not': category1 })); expect(bundle1.entry?.length).toStrictEqual(1); expect(bundleContains(bundle1, serviceRequest1)).toBeUndefined(); expect(bundleContains(bundle1, serviceRequest2)).toBeDefined(); })); test('Null token array not equals', () => withTestContext(async () => { const category1 = randomUUID(); const code = randomUUID(); const serviceRequest1 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/' + randomUUID() }, category: [{ coding: [{ code: category1 }] }], code: { coding: [{ code }] }, }); const serviceRequest2 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/' + randomUUID() }, code: { coding: [{ code }] }, }); const bundle1 = await repo.search(parseSearchRequest('ServiceRequest', { code, 'category:not': category1 })); expect(bundle1.entry?.length).toStrictEqual(1); expect(bundleContains(bundle1, serviceRequest1)).toBeUndefined(); expect(bundleContains(bundle1, serviceRequest2)).toBeDefined(); })); test('Missing', () => withTestContext(async () => { const code = randomUUID(); // Test both an array column (specimen) and a non-array column (encounter), // because the resulting SQL could be subtly different. const serviceRequest1 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', code: { coding: [{ code }] }, subject: { reference: 'Patient/' + randomUUID() }, specimen: [{ reference: 'Specimen/' + randomUUID() }], encounter: { reference: 'Encounter/' + randomUUID() }, }); const serviceRequest2 = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', code: { coding: [{ code }] }, subject: { reference: 'Patient/' + randomUUID() }, }); const bundle1 = await repo.search(parseSearchRequest('ServiceRequest', { code, 'specimen:missing': 'true' })); expect(bundle1.entry?.length).toStrictEqual(1); expect(bundleContains(bundle1, serviceRequest1)).toBeUndefined(); expect(bundleContains(bundle1, serviceRequest2)).toBeDefined(); const bundle2 = await repo.search(parseSearchRequest('ServiceRequest', { code, 'specimen:missing': 'false' })); expect(bundle2.entry?.length).toStrictEqual(1); expect(bundleContains(bundle2, serviceRequest1)).toBeDefined(); expect(bundleContains(bundle2, serviceRequest2)).toBeUndefined(); const bundle3 = await repo.search(parseSearchRequest('ServiceRequest', { code, 'encounter:missing': 'true' })); expect(bundle3.entry?.length).toStrictEqual(1); expect(bundleContains(bundle3, serviceRequest1)).toBeUndefined(); expect(bundleContains(bundle3, serviceRequest2)).toBeDefined(); const bundle4 = await repo.search(parseSearchRequest('ServiceRequest', { code, 'encounter:missing': 'false' })); expect(bundle4.entry?.length).toStrictEqual(1); expect(bundleContains(bundle4, serviceRequest1)).toBeDefined(); expect(bundleContains(bundle4, serviceRequest2)).toBeUndefined(); })); test('Missing with logical (identifier) references', () => withTestContext(async () => { const patientIdentifier = randomUUID(); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [ { system: 'http://example.com/guid', value: patientIdentifier, }, ], generalPractitioner: [ { identifier: { system: 'http://hl7.org/fhir/sid/us-npi', value: '9876543210', }, }, ], managingOrganization: { identifier: { system: 'http://hl7.org/fhir/sid/us-npi', value: '0123456789', }, }, }); // Test singlet reference column let results = await repo.searchResources( parseSearchRequest(`Patient?identifier=${patientIdentifier}&organization:missing=false`) ); expect(results).toHaveLength(1); expect(results[0]?.id).toStrictEqual(patient.id); // Test array reference column results = await repo.searchResources( parseSearchRequest(`Patient?identifier=${patientIdentifier}&general-practitioner:missing=false`) ); expect(results).toHaveLength(1); expect(results[0]?.id).toStrictEqual(patient.id); })); test('Starts after', () => withTestContext(async () => { // Create 2 appointments // One with a start date of 1 second ago // One with a start date of 2 seconds ago const code = randomUUID(); const now = new Date(); const nowMinus1Second = new Date(now.getTime() - 1000); const nowMinus2Seconds = new Date(now.getTime() - 2000); const nowMinus3Seconds = new Date(now.getTime() - 3000); const patient: Patient = { resourceType: 'Patient' }; const patientReference = createReference(patient); const appt1 = await repo.createResource<Appointment>({ resourceType: 'Appointment', status: 'booked', serviceType: [{ coding: [{ code }] }], participant: [{ status: 'accepted', actor: patientReference }], start: nowMinus1Second.toISOString(), end: now.toISOString(), }); expect(appt1).toBeDefined(); const appt2 = await repo.createResource<Appointment>({ resourceType: 'Appointment', status: 'booked', serviceType: [{ coding: [{ code }] }], participant: [{ status: 'accepted', actor: patientReference }], start: nowMinus2Seconds.toISOString(), end: now.toISOString(), }); expect(appt2).toBeDefined(); // Greater than (newer than) 2 seconds ago should only return appt 1 const searchResult1 = await repo.search({ resourceType: 'Appointment', filters: [ { code: 'service-type', operator: Operator.EQUALS, value: code, }, { code: 'date', operator: Operator.STARTS_AFTER, value: nowMinus2Seconds.toISOString(), }, ], }); expect(bundleContains(searchResult1 as Bundle, appt1 as Appointment)).toBeDefined(); expect(bundleContains(searchResult1 as Bundle, appt2 as Appointment)).toBeUndefined(); // Greater than (newer than) or equal to 2 seconds ago should return both appts const searchResult2 = await repo.search({ resourceType: 'Appointment', filters: [ { code: 'service-type', operator: Operator.EQUALS, value: code, }, { code: 'date', operator: Operator.GREATER_THAN_OR_EQUALS, value: nowMinus2Seconds.toISOString(), }, ], }); expect(bundleContains(searchResult2 as Bundle, appt1 as Appointment)).toBeDefined(); expect(bundleContains(searchResult2 as Bundle, appt2 as Appointment)).toBeDefined(); // Less than (older than) to 1 seconds ago should only return appt 2 const searchResult3 = await repo.search({ resourceType: 'Appointment', filters: [ { code: 'service-type', operator: Operator.EQUALS, value: code, }, { code: 'date', operator: Operator.STARTS_AFTER, value: nowMinus3Seconds.toISOString(), }, { code: 'date', operator: Operator.ENDS_BEFORE, value: nowMinus1Second.toISOString(), }, ], }); expect(bundleContains(searchResult3 as Bundle, appt1 as Appointment)).toBeUndefined(); expect(bundleContains(searchResult3 as Bundle, appt2 as Appointment)).toBeDefined(); // Less than (older than) or equal to 1 seconds ago should return both appts const searchResult4 = await repo.search({ resourceType: 'Appointment', filters: [ { code: 'service-type', operator: Operator.EQUALS, value: code, }, { code: 'date', operator: Operator.STARTS_AFTER, value: nowMinus3Seconds.toISOString(), }, { code: 'date', operator: Operator.LESS_THAN_OR_EQUALS, value: nowMinus1Second.toISOString(), }, ], }); expect(bundleContains(searchResult4 as Bundle, appt1 as Appointment)).toBeDefined(); expect(bundleContains(searchResult4 as Bundle, appt2 as Appointment)).toBeDefined(); })); test('Boolean search', () => withTestContext(async () => { const family = randomUUID(); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ family }], active: true, }); const searchResult = await repo.search({ resourceType: 'Patient', filters: [ { code: 'name', operator: Operator.EQUALS, value: family, }, { code: 'active', operator: Operator.EQUALS, value: 'true', }, ], }); expect(searchResult.entry).toHaveLength(1); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(patient.id); })); test('Boolean not equals matches missing value', () => withTestContext(async () => { const family = randomUUID(); // Create patient with active=true const activeTrue = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ family }], active: true, }); // Create patient with active=false const activeFalse = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ family }], active: false, }); // Create patient without active field (undefined/null) const activeUndefined = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ family }], }); // Search for active:not=false should return activeTrue and activeUndefined const searchResult = await repo.search({ resourceType: 'Patient', filters: [ { code: 'name', operator: Operator.EQUALS, value: family, }, { code: 'active', operator: Operator.NOT, value: 'false', }, ], }); expect(searchResult.entry?.length).toStrictEqual(2); expect(bundleContains(searchResult, activeTrue)).toBeDefined(); expect(bundleContains(searchResult, activeUndefined)).toBeDefined(); expect(bundleContains(searchResult, activeFalse)).toBeUndefined(); })); test('Not equals with comma separated values', () => withTestContext(async () => { // Create 3 service requests // All 3 have the same category for test isolation // Each have a different code const category = randomUUID(); const serviceRequests = []; for (let i = 0; i < 3; i++) { serviceRequests.push( await repo.createResource({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/' + randomUUID() }, category: [{ coding: [{ code: category }] }], code: { coding: [{ code: randomUUID() }] }, }) ); } // Search for service requests with category // and code "not equals" the first two separated by a comma const searchResult = await repo.search({ resourceType: 'ServiceRequest', filters: [ { code: 'category', operator: Operator.EQUALS, value: category, }, { code: 'code', operator: Operator.NOT_EQUALS, value: serviceRequests[0].code.coding[0].code + ',' + serviceRequests[1].code.coding[0].code, }, ], }); expect(searchResult.entry).toHaveLength(1); })); test('_id equals with comma separated values', () => withTestContext(async () => { // Create 3 service requests const serviceRequests = []; for (let i = 0; i < 3; i++) { serviceRequests.push( await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/' + randomUUID() }, code: { text: randomUUID() }, }) ); } // Search for service requests with _id equals the first two separated by a comma const searchResult = await repo.search({ resourceType: 'ServiceRequest', filters: [ { code: '_id', operator: Operator.EQUALS, value: serviceRequests[0].id + ',' + serviceRequests[1].id, }, ], }); expect(searchResult.entry).toHaveLength(2); })); test('Error on invalid search parameter', async () => withTestContext(async () => { try { await repo.search({ resourceType: 'ServiceRequest', filters: [ { code: 'basedOn', // should be "based-on" operator: Operator.EQUALS, value: 'ServiceRequest/123', }, ], }); } catch (err) { const outcome = (err as OperationOutcomeError).outcome; expect(outcome.issue?.[0]?.details?.text).toStrictEqual('Unknown search parameter: basedOn'); } })); test('Patient search without resource type', () => withTestContext(async () => { // Create Patient const patient = await repo.createResource<Patient>({ resourceType: 'Patient', }); // Create AllergyIntolerance const allergyIntolerance = await repo.createResource<AllergyIntolerance>({ resourceType: 'AllergyIntolerance', patient: createReference(patient), clinicalStatus: { text: 'active' }, }); // Search by patient const searchResult = await repo.search({ resourceType: 'AllergyIntolerance', filters: [ { code: 'patient', operator: Operator.EQUALS, value: patient.id, }, ], }); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(allergyIntolerance.id); })); test('Subject search without resource type', () => withTestContext(async () => { // Create Patient const patient = await repo.createResource<Patient>({ resourceType: 'Patient', }); // Create Observation const observation = await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { text: 'test' }, subject: createReference(patient), }); // Search by patient const searchResult = await repo.search({ resourceType: 'Observation', filters: [ { code: 'subject', operator: Operator.EQUALS, value: patient.id, }, ], }); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(observation.id); })); test('Chained search on array columns using reference tables', () => withTestContext(async () => { // Create Practitioner const pcp = await repo.createResource<Practitioner>({ resourceType: 'Practitioner', }); // Create Patient const patient = await repo.createResource<Patient>({ resourceType: 'Patient', generalPractitioner: [createReference(pcp)], }); // Create CareTeam const code = randomUUID(); const categorySystem = 'http://example.com/care-team-category'; await repo.createResource<CareTeam>({ resourceType: 'CareTeam', category: [ { coding: [ { system: categorySystem, code, display: 'Public health-focused care team', }, ], }, ], participant: [{ member: createReference(pcp) }], }); // Search chain const searchResult = await repo.search( parseSearchRequest( `Patient?general-practitioner:Practitioner._has:CareTeam:participant:category=${categorySystem}|${code}` ) ); expect(searchResult.entry?.[0]?.resource?.id).toStrictEqual(patient.id); })); test('Chained search on single columns using reference tables', () => withTestContext(async () => { const code = randomUUID(); // Create linked resources const patient = await repo.createResource<Patient>({ resourceType: 'Patient', }); const encounter = await repo.createResource<Encounter>({ resourceType: 'Encounter', status: 'finished', class: { system: 'http://example.com/appt-type', code }, }); const observation = await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { text: 'Throat culture' }, subject: createReference(patient), encounter: createReference(encounter), }); await repo.createResource<DiagnosticReport>({ resourceType: 'DiagnosticReport', status: 'final', code: { text: 'Strep test' }, encounter: createReference(encounter), result: [createReference(observation)], }); const result = await repo.search( parseSearchRequest(`Patient?_has:Observation:subject:encounter:Encounter.class=${code}`) ); expect(result.entry?.[0]?.resource?.id).toStrictEqual(patient.id); })); test('Chained search sort order', () => withTestContext(async () => { const identifier = randomUUID(); // Create linked resources const link1 = await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ value: identifier }], }); const link2 = await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ value: identifier }], }); const patientIds: string[] = []; for (let i = 1; i < 10; i++) { const patient = await repo.createResource<Patient>({ resourceType: 'Patient', birthDate: '1994-11-0' + i, link: [ { type: 'seealso', other: createReference(link1) }, { type: 'seealso', other: createReference(link2) }, ], }); patientIds.unshift(patient.id); } const result = await repo.search( parseSearchRequest(`Patient?link:Patient.identifier=${identifier}&_sort=-birthdate`) ); expect(result.entry?.map((e) => (e.resource as Patient).birthDate)).toEqual([ '1994-11-09', '1994-11-08', '1994-11-07', '1994-11-06', '1994-11-05', '1994-11-04', '1994-11-03', '1994-11-02', '1994-11-01', ]); expect(result.entry?.map((e) => e.resource?.id)).toEqual(patientIds); })); test('Chained search with negated filter', () => withTestContext(async () => { // Create linked resources const link1 = await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ value: randomUUID() }], }); const link2 = await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ value: randomUUID() }], }); const patientIds: string[] = []; for (let i = 1; i < 10; i++) { const middlePatient = await repo.createResource<Patient>({ resourceType: 'Patient', link: [ { type: 'seealso', other: createReference(link1) }, { type: 'seealso', other: createReference(link2) }, ], }); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', birthDate: '1994-11-0' + i, link: [{ type: 'seealso', other: createReference(middlePatient) }], }); patientIds.unshift(patient.id); } const result = await repo.search( parseSearchRequest(`Patient?link:Patient.link:Patient.identifier:not=${randomUUID()}&_sort=-birthdate`) ); expect(result.entry?.map((e) => (e.resource as Patient).birthDate)).toEqual([ '1994-11-09', '1994-11-08', '1994-11-07', '1994-11-06', '1994-11-05', '1994-11-04', '1994-11-03', '1994-11-02', '1994-11-01', ]); expect(result.entry?.map((e) => e.resource?.id)).toEqual(patientIds); })); test('Chained search on canonical reference', () => withTestContext(async () => { const url = 'http://example.com/' + randomUUID(); // Create linked resources const q = randomUUID(); const questionnaire = await repo.createResource<Questionnaire>({ resourceType: 'Questionnaire', status: 'unknown', url, identifier: [{ value: q }], }); const ev = randomUUID(); const evidenceVariable = await repo.createResource<EvidenceVariable>({ resourceType: 'EvidenceVariable', status: 'unknown', relatedArtifact: [{ type: 'derived-from', resource: url }], identifier: [{ value: ev }], }); const result = await repo.search( parseSearchRequest(`Questionnaire?_has:EvidenceVariable:derived-from:identifier=${ev}`) ); expect(result.entry).toHaveLength(1); expect(result.entry?.[0]?.resource?.id).toStrictEqual(questionnaire.id); const result2 = await repo.search( parseSearchRequest(`EvidenceVariable?derived-from:Questionnaire.identifier=${q}`) ); expect(result2.entry).toHaveLength(1); expect(result2.entry?.[0]?.resource?.id).toEqual(evidenceVariable.id); const study = await repo.createResource<ResearchStudy>({ resourceType: 'ResearchStudy', status: 'active', outcomeMeasure: [{ reference: createReference(evidenceVariable) }], }); const result3 = await repo.search( parseSearchRequest( `ResearchStudy?outcome-measure-reference:EvidenceVariable.derived-from:Questionnaire.identifier=${q}` ) ); expect(result3.entry).toHaveLength(1); expect(result3.entry?.[0]?.resource?.id).toEqual(study.id); })); test('Rejects too long chained search', () => withTestContext(async () => { await expect(() => repo.search( parseSearchRequest( `Patient?_has:Observation:subject:encounter:Encounter._has:DiagnosticReport:encounter:result.specimen.parent.collected=2023` ) ) ).rejects.toThrow(new Error('Search chains longer than three links are not currently supported')); })); test.each([ ['Patient?organization.invalid.name=Kaiser', 'Invalid search parameter in chain: Organization?invalid'], ['Patient?organization.invalid=true', 'Invalid search parameter at end of chain: Organization?invalid'], [ 'Patient?general-practitioner.qualification-period=2023', 'Unable to identify next resource type for search parameter: Patient?general-practitioner', ], ['Patient?_has:Observation:invalid:status=active', 'Invalid search parameter in chain: Observation?invalid'], [ 'Patient?_has:Observation:encounter:status=active', 'Invalid reverse chain link: search parameter Observation?encounter does not refer to Patient', ], ['Patient?_has:Observation:status=active', 'Invalid search chain: _has:Observation:status'], ])('Invalid chained search parameters: %s', (searchString: string, errorMsg: string) => { return withTestContext(async () => expect(repo.search(parseSearchRequest(searchString))).rejects.toStrictEqual(new Error(errorMsg)) ); }); test('Chained search with modifier', () => withTestContext(async () => { const code = randomUUID(); // Create linked resources const patient = await repo.createResource<Patient>({ resourceType: 'Patient', }); const encounter = await repo.createResource<Encounter>({ resourceType: 'Encounter', status: 'finished', class: { system: 'http://example.com/appt-type', code }, }); await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { text: 'Throat culture' }, subject: createReference(patient), encounter: createReference(encounter), }); const result = await repo.search( parseSearchRequest( `Patient?_has:Observation:subject:encounter:Encounter.class=${code}&_has:Observation:subject:encounter:Encounter.status:not=cancelled` ) ); expect(result.entry?.[0]?.resource?.id).toStrictEqual(patient.id); })); test('Chained search deduplication', () => withTestContext(async () => { const code = randomUUID(); const code2 = randomUUID(); // Create linked resources const patient = await repo.createResource<Patient>({ resourceType: 'Patient', }); await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { coding: [{ code }], text: 'Throat culture' }, subject: createReference(patient), }); await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { coding: [{ code: code2 }], text: 'Blood test' }, subject: createReference(patient), }); const result = await repo.search(parseSearchRequest(`Patient?_has:Observation:subject:code=${code},${code2}`)); expect(result.entry).toHaveLength(1); expect(result.entry?.[0]?.resource?.id).toStrictEqual(patient.id); })); test('Token search deduplication', () => withTestContext(async () => { const code = randomUUID(); const code2 = randomUUID(); // Create resource with multiple codes const observation = await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { text: 'Blood test' }, component: [{ code: { coding: [{ code }] } }, { code: { coding: [{ code: code2 }] } }], }); const result = await repo.search(parseSearchRequest(`Observation?component-code=${code},${code2}`)); expect(result.entry).toHaveLength(1); expect(result.entry?.[0]?.resource?.id).toStrictEqual(observation.id); })); test('Include references success', () => withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient' }); const order = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: createReference(patient), }); const bundle = await repo.search({ resourceType: 'ServiceRequest', include: [ { resourceType: 'ServiceRequest', searchParam: 'subject', }, ], total: 'accurate', filters: [{ code: '_id', operator: Operator.EQUALS, value: order.id }], }); expect(bundle.total).toStrictEqual(1); expect(bundleContains(bundle, order)).toMatchObject<BundleEntry>({ search: { mode: 'match' } }); expect(bundleContains(bundle, patient)).toMatchObject<BundleEntry>({ search: { mode: 'include' } }); })); test('Include canonical success', () => withTestContext(async () => { const canonicalURL = 'http://example.com/fhir/Questionnaire/PHQ-9/' + randomUUID(); const questionnaire = await repo.createResource<Questionnaire>({ resourceType: 'Questionnaire', status: 'active', url: canonicalURL, }); const response = await repo.createResource<QuestionnaireResponse>({ resourceType: 'QuestionnaireResponse', status: 'in-progress', questionnaire: canonicalURL, }); const bundle = await repo.search({ resourceType: 'QuestionnaireResponse', include: [ { resourceType: 'QuestionnaireResponse', searchParam: 'questionnaire', }, ], total: 'accurate', filters: [{ code: '_id', operator: Operator.EQUALS, value: response.id }], }); expect(bundle.total).toStrictEqual(1); expect(bundleContains(bundle, response)).toMatchObject<BundleEntry>({ search: { mode: 'match' } }); expect(bundleContains(bundle, questionnaire)).toMatchObject<BundleEntry>({ search: { mode: 'include' } }); })); test('Include PlanDefinition mixed types', () => withTestContext(async () => { const canonical = 'http://example.com/fhir/R4/ActivityDefinition/' + randomUUID(); const uri = 'http://example.com/fhir/R4/ActivityDefinition/' + randomUUID(); const plan = await repo.createResource<PlanDefinition>({ resourceType: 'PlanDefinition', status: 'active', action: [{ definitionCanonical: canonical }, { definitionUri: uri }], }); const activity1 = await repo.createResource<ActivityDefinition>({ resourceType: 'ActivityDefinition', status: 'active', url: canonical, }); const activity2 = await repo.createResource<ActivityDefinition>({ resourceType: 'ActivityDefinition', status: 'active', url: uri, }); const bundle = await repo.search({ resourceType: 'PlanDefinition', include: [ { resourceType: 'PlanDefinition', searchParam: 'definition', }, ], total: 'accurate', filters: [{ code: '_id', operator: Operator.EQUALS, value: plan.id }], }); expect(bundle.total).toStrictEqual(1); expect(bundleContains(bundle, plan)).toMatchObject<BundleEntry>({ search: { mode: 'match' } }); expect(bundleContains(bundle, activity1)).toMatchObject<BundleEntry>({ search: { mode: 'include' } }); expect(bundleContains(bundle, activity2)).toMatchObject<BundleEntry>({ search: { mode: 'include' } }); })); test('Include references invalid search param', async () => withTestContext(async () => { try { await repo.search({ resourceType: 'ServiceRequest', include: [ { resourceType: 'ServiceRequest', searchParam: 'xyz', }, ], }); } catch (err) { const outcome = (err as OperationOutcomeError).outcome; expect(outcome.issue?.[0]?.details?.text).toStrictEqual('Invalid include parameter: ServiceRequest:xyz'); } })); test('Reverse include Provenance', () => withTestContext(async () => { const family = randomUUID(); const practitioner1 = await repo.createResource<Practitioner>({ resourceType: 'Practitioner', name: [{ given: ['Homer'], family }], }); const practitioner2 = await repo.createResource<Practitioner>({ resourceType: 'Practitioner', name: [{ given: ['Marge'], family }], }); const searchRequest: SearchRequest = { resourceType: 'Practitioner', filters: [{ code: 'name', operator: Operator.EQUALS, value: family }], revInclude: [ { resourceType: 'Provenance', searchParam: 'target', }, ], }; const searchResult1 = await repo.search(searchRequest); expect(searchResult1.entry).toHaveLength(2); expect(bundleContains(searchResult1, practitioner1)).toBeTruthy(); expect(bundleContains(searchResult1, practitioner2)).toBeTruthy(); const provenance1 = await repo.createResource<Provenance>({ resourceType: 'Provenance', target: [createReference(practitioner1)], agent: [{ who: createReference(practitioner1) }], recorded: new Date().toISOString(), }); const provenance2 = await repo.createResource<Provenance>({ resourceType: 'Provenance', target: [createReference(practitioner2)], agent: [{ who: createReference(practitioner2) }], recorded: new Date().toISOString(), }); const searchResult2 = await repo.search(searchRequest); expect(searchResult2.entry).toHaveLength(4); expect(bundleContains(searchResult2, practitioner1)).toMatchObject<BundleEntry>({ search: { mode: 'match' }, }); expect(bundleContains(searchResult2, practitioner2)).toMatchObject<BundleEntry>({ search: { mode: 'match' }, }); expect(bundleContains(searchResult2, provenance1)).toMatchObject<BundleEntry>({ search: { mode: 'include' }, }); expect(bundleContains(searchResult2, provenance2)).toMatchObject<BundleEntry>({ search: { mode: 'include' }, }); })); test('Reverse include canonical', () => withTestContext(async () => { const canonicalURL = 'http://example.com/fhir/Questionnaire/PHQ-9/' + randomUUID(); const questionnaire = await repo.createResource<Questionnaire>({ resourceType: 'Questionnaire', status: 'active', url: canonicalURL, }); const response = await repo.createResource<QuestionnaireResponse>({ resourceType: 'QuestionnaireResponse', status: 'in-progress', questionnaire: canonicalURL, }); const bundle = await repo.search({ resourceType: 'Questionnaire', revInclude: [ { resourceType: 'QuestionnaireResponse', searchParam: 'questionnaire', }, ], total: 'accurate', filters: [{ code: '_id', operator: Operator.EQUALS, value: questionnaire.id }], }); expect(bundle.total).toStrictEqual(1); expect(bundleContains(bundle, questionnaire)).toMatchObject<BundleEntry>({ search: { mode: 'match' } }); expect(bundleContains(bundle, response)).toMatchObject<BundleEntry>({ search: { mode: 'include' } }); })); test('_include:iterate', () => withTestContext(async () => { /* Construct resources for the search to operate on. The test search query and resource graph it will act on are shown below. Query: /Patient?identifier=patient &_include=Patient:organization &_include:iterate=Patient:link &_include:iterate=Patient:general-practitioner ┌──────────────────────────────────┐ │patient │ └┬────────┬────────┬────────┬─────┬┘ ┌▽──────┐┌▽──────┐┌▽──────┐┌▽───┐┌▽────────────┐ │linked2││linked1││related││org1││practitioner1│ └┬──────┘└┬──────┘└───────┘└────┘└─────────────┘ │┌───────▽───────┐ ││linked3 │ │└┬────────────┬─┘ ┌▽─▽──────────┐┌▽─────┐ │practitioner2││org2 *│ └─────────────┘└──────┘ * omitted from search results This verifies the following behaviors of the :iterate modifier: 1. _include w/ :iterate recursively applies the same parameter (Patient:link) 2. _include w/ :iterate applies to resources from other _include parameters (Patient:general-practitioner) 3. _include w/o :iterate does not apply recursively (Patient:organization) 4. Resources which are included multiple times are deduplicated in the search results */ const rootPatientIdentifier = randomUUID(); const organization1 = await repo.createResource<Organization>({ resourceType: 'Organization', name: 'org1', }); const organization2 = await repo.createResource<Organization>({ resourceType: 'Organization', name: 'org2', }); const practitioner1 = await repo.createResource<Practitioner>({ resourceType: 'Practitioner' }); const practitioner2 = await repo.createResource<Practitioner>({ resourceType: 'Practitioner' }); const linked3 = await repo.createResource<Patient>({ resourceType: 'Patient', managingOrganization: { reference: `Organization/${organization2.id}` }, generalPractitioner: [ { reference: `Practitioner/${practitioner2.id}`, }, ], }); const linked1 = await repo.createResource<Patient>({ resourceType: 'Patient', link: [ { other: { reference: `Patient/${linked3.id}` }, type: 'replaces', }, ], }); const linked2 = await repo.createResource<Patient>({ resourceType: 'Patient', generalPractitioner: [ { reference: `Practitioner/${practitioner2.id}`, }, ], }); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [ { value: rootPatientIdentifier, }, ], link: [ { other: { reference: `Patient/${linked1.id}` }, type: 'replaces', }, { other: { reference: `Patient/${linked2.id}` }, type: 'replaces', }, ], managingOrganization: { reference: `Organization/${organization1.id}`, }, generalPractitioner: [ { reference: `Practitioner/${practitioner1.id}`, }, ], }); // Run the test search query const bundle = await repo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: rootPatientIdentifier, }, ], include: [ { resourceType: 'Patient', searchParam: 'organization' }, { resourceType: 'Patient', searchParam: 'link', modifier: Operator.ITERATE }, { resourceType: 'Patient', searchParam: 'general-practitioner', modifier: Operator.ITERATE }, ], }); const expected = [ `match:Patient/${patient.id}`, `include:Patient/${linked1.id}`, `include:Patient/${linked2.id}`, `include:Patient/${linked3.id}`, `include:Organization/${organization1.id}`, `include:Practitioner/${practitioner1.id}`, `include:Practitioner/${practitioner2.id}`, ].sort(); expect( bundle.entry?.map((e) => `${e.search?.mode}:${e.resource?.resourceType}/${e.resource?.id}`).sort() ).toStrictEqual(expected); })); test('_revinclude:iterate', () => withTestContext(async () => { /* Construct resources for the search to operate on. The test search query and resource graph it will act on are shown below. Query: /Patient?identifier=patient &_revinclude=Patient:link &_revinclude:iterate=Observation:subject &_revinclude:iterate=Observation:has-member ┌─────────┐┌────────────┐┌────────────┐ │linked3 *││observation3││observation4│ └┬────────┘└┬───────────┘└┬───────────┘ ┌▽──────┐┌──▽────┐┌───────▽────┐ │linked1││linked2││observation2│ └┬──────┘└┬──────┘└┬───────────┘ │ │┌───────▽─────────┐ │ ││observation1 │ │ │└┬────────────────┘ ┌▽────────▽─▽┐ │patient │ └────────────┘ * omitted from search results This verifies the following behaviors of the :iterate modifier: 1. _revinclude w/ :iterate recursively applies the same parameter (Observation:has-member) 2. _revinclude w/ :iterate applies to resources from other _revinclude parameters (Observation:subject) 3. _revinclude w/o :iterate does not apply recursively (Patient:link) */ const rootPatientIdentifier = randomUUID(); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [ { value: rootPatientIdentifier, }, ], }); const linked1 = await repo.createResource<Patient>({ resourceType: 'Patient', link: [ { other: { reference: `Patient/${patient.id}` }, type: 'replaced-by', }, ], }); const linked2 = await repo.createResource<Patient>({ resourceType: 'Patient', link: [ { other: { reference: `Patient/${patient.id}` }, type: 'replaced-by', }, ], }); await repo.createResource<Patient>({ resourceType: 'Patient', link: [ { other: { reference: `Patient/${linked1.id}` }, type: 'replaced-by', }, ], }); const baseObservation: Observation = { resourceType: 'Observation', status: 'final', code: { coding: [ { system: LOINC, code: 'fake', }, ], }, }; const observation1 = await repo.createResource<Observation>({ ...baseObservation, subject: { reference: `Patient/${patient.id}`, }, }); const observation2 = await repo.createResource<Observation>({ ...baseObservation, subject: { display: 'Alex J. Chalmers', }, hasMember: [ { reference: `Observation/${observation1.id}`, }, ], }); const observation3 = await repo.createResource<Observation>({ ...baseObservation, subject: { reference: `Patient/${linked2.id}`, }, }); const observation4 = await repo.createResource<Observation>({ ...baseObservation, subject: { display: 'Alex J. Chalmers', }, hasMember: [ { reference: `Observation/${observation2.id}`, }, ], }); // Run the test search query const bundle = await repo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: rootPatientIdentifier, }, ], revInclude: [ { resourceType: 'Patient', searchParam: 'link' }, { resourceType: 'Observation', searchParam: 'subject', modifier: Operator.ITERATE }, { resourceType: 'Observation', searchParam: 'has-member', modifier: Operator.ITERATE }, ], }); const expected = [ `match:Patient/${patient.id}`, `include:Patient/${linked1.id}`, `include:Patient/${linked2.id}`, `include:Observation/${observation1.id}`, `include:Observation/${observation2.id}`, `include:Observation/${observation3.id}`, `include:Observation/${observation4.id}`, ].sort(); expect( bundle.entry?.map((e) => `${e.search?.mode}:${e.resource?.resourceType}/${e.resource?.id}`).sort() ).toStrictEqual(expected); })); test('_include depth limit', () => withTestContext(async () => { const rootPatientIdentifier = randomUUID(); const linked6 = await repo.createResource<Patient>({ resourceType: 'Patient', }); const linked5 = await repo.createResource<Patient>({ resourceType: 'Patient', link: [ { other: { reference: `Patient/${linked6.id}` }, type: 'replaces', }, ], }); const linked4 = await repo.createResource<Patient>({ resourceType: 'Patient', link: [ { other: { reference: `Patient/${linked5.id}` }, type: 'replaces', }, ], }); const linked3 = await repo.createResource<Patient>({ resourceType: 'Patient', link: [ { other: { reference: `Patient/${linked4.id}` }, type: 'replaces', }, ], }); const linked2 = await repo.createResource<Patient>({ resourceType: 'Patient', link: [ { other: { reference: `Patient/${linked3.id}` }, type: 'replaces', }, ], }); const linked1 = await repo.createResource<Patient>({ resourceType: 'Patient', link: [ { other: { reference: `Patient/${linked2.id}` }, type: 'replaces', }, ], }); await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [ { value: rootPatientIdentifier, }, ], link: [ { other: { reference: `Patient/${linked1.id}` }, type: 'replaces', }, ], }); return expect( repo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: rootPatientIdentifier, }, ], include: [{ resourceType: 'Patient', searchParam: 'link', modifier: Operator.ITERATE }], }) ).rejects.toBeDefined(); })); test('_include on empty search results', () => withTestContext(async () => { return expect( repo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: randomUUID(), }, ], include: [{ resourceType: 'Patient', searchParam: 'link', modifier: Operator.ITERATE }], }) ).resolves.toMatchObject<Bundle>({ resourceType: 'Bundle', type: 'searchset', entry: [], total: undefined, }); })); test('_include on page boundary', () => withTestContext(async () => { const mrn = randomUUID(); const gp1 = await repo.createResource<Practitioner>({ resourceType: 'Practitioner', }); const patient1 = await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ value: mrn }], generalPractitioner: [createReference(gp1)], }); const gp2 = await repo.createResource<Practitioner>({ resourceType: 'Practitioner', }); const patient2 = await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ value: mrn }], generalPractitioner: [createReference(gp2)], }); const searchRequest: SearchRequest = { resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: mrn, }, ], sortRules: [{ code: '_lastUpdated' }], include: [{ resourceType: 'Patient', searchParam: 'general-practitioner' }], count: 1, }; await expect(repo.search(searchRequest)).resolves.toMatchObject<Bundle>({ resourceType: 'Bundle', type: 'searchset', entry: [ expect.objectContaining<BundleEntry>({ fullUrl: expect.stringContaining(getReferenceString(patient1)), search: { mode: 'match' }, }), expect.objectContaining<BundleEntry>({ fullUrl: expect.stringContaining(getReferenceString(gp1)), search: { mode: 'include' }, }), ], }); searchRequest.count = 2; await expect(repo.search(searchRequest)).resolves.toMatchObject<Bundle>({ resourceType: 'Bundle', type: 'searchset', entry: [ expect.objectContaining<BundleEntry>({ fullUrl: expect.stringContaining(getReferenceString(patient1)), search: { mode: 'match' }, }), expect.objectContaining<BundleEntry>({ fullUrl: expect.stringContaining(getReferenceString(patient2)), search: { mode: 'match' }, }), expect.objectContaining<BundleEntry>({ fullUrl: expect.stringContaining(getReferenceString(gp1)), search: { mode: 'include' }, }), expect.objectContaining<BundleEntry>({ fullUrl: expect.stringContaining(getReferenceString(gp2)), search: { mode: 'include' }, }), ], }); })); test('Include with invalid reference', () => withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient' }); const order = await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: { reference: `Patient/p_${patient.id}` }, // Invalid reference string }); const bundle = await repo.search({ resourceType: 'ServiceRequest', include: [ { resourceType: 'ServiceRequest', searchParam: 'subject', }, ], total: 'accurate', filters: [{ code: '_id', operator: Operator.EQUALS, value: order.id }], }); expect(bundle.total).toStrictEqual(1); expect(bundleContains(bundle, order)).toMatchObject<BundleEntry>({ search: { mode: 'match' } }); expect(bundleContains(bundle, patient)).toBeUndefined(); })); test('DiagnosticReport category with system', () => withTestContext(async () => { const code = randomUUID(); const dr = await repo.createResource<DiagnosticReport>({ resourceType: 'DiagnosticReport', status: 'final', code: { coding: [{ code }] }, category: [{ coding: [{ system: LOINC, code: 'LP217198-3' }] }], }); const bundle = await repo.search({ resourceType: 'DiagnosticReport', filters: [ { code: 'code', operator: Operator.EQUALS, value: code, }, { code: 'category', operator: Operator.EQUALS, value: `${LOINC}|LP217198-3`, }, ], count: 1, }); expect(bundleContains(bundle, dr)).toBeTruthy(); })); test('Encounter.period date search', () => withTestContext(async () => { const e = await repo.createResource<Encounter>({ resourceType: 'Encounter', identifier: [{ value: randomUUID() }], status: 'finished', class: { code: 'test' }, period: { start: '2020-02-01', end: '2020-02-02', }, }); const bundle = await repo.search({ resourceType: 'Encounter', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: e.identifier?.[0]?.value as string, }, { code: 'date', operator: Operator.GREATER_THAN, value: '2020-01-01', }, ], count: 1, }); expect(bundleContains(bundle, e)).toBeTruthy(); })); test('Encounter.period dateTime search', () => withTestContext(async () => { const e = await repo.createResource<Encounter>({ resourceType: 'Encounter', identifier: [{ value: randomUUID() }], status: 'finished', class: { code: 'test' }, period: { start: '2020-02-01T13:30:00Z', end: '2020-02-01T14:15:00Z', }, }); const bundle = await repo.search({ resourceType: 'Encounter', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: e.identifier?.[0]?.value as string, }, { code: 'date', operator: Operator.GREATER_THAN, value: '2020-02-01T12:00Z', }, ], count: 1, }); expect(bundleContains(bundle, e)).toBeTruthy(); })); test('Condition.code system search', () => withTestContext(async () => { const p = await repo.createResource({ resourceType: 'Patient', name: [{ family: randomUUID() }], }); const c1 = await repo.createResource<Condition>({ resourceType: 'Condition', subject: createReference(p), code: { coding: [{ system: SNOMED, code: '165002' }] }, }); const c2 = await repo.createResource<Condition>({ resourceType: 'Condition', subject: createReference(p), code: { coding: [{ system: 'https://example.com', code: 'test' }] }, }); const bundle = await repo.search({ resourceType: 'Condition', filters: [ { code: 'subject', operator: Operator.EQUALS, value: getReferenceString(p), }, { code: 'code', operator: Operator.EQUALS, value: `${SNOMED}|`, }, ], }); expect(bundle.entry?.length).toStrictEqual(1); expect(bundleContains(bundle, c1)).toBeTruthy(); expect(bundleContains(bundle, c2)).not.toBeTruthy(); })); test('Condition.code :not next URL', () => withTestContext(async () => { const p = await repo.createResource({ resourceType: 'Patient', name: [{ family: randomUUID() }], }); await repo.createResource<Condition>({ resourceType: 'Condition', subject: createReference(p), code: { coding: [{ system: SNOMED, code: '165002' }] }, }); await repo.createResource<Condition>({ resourceType: 'Condition', subject: createReference(p), code: { coding: [{ system: 'https://example.com', code: 'test' }] }, }); const bundle = await repo.search( parseSearchRequest(`https://x/Condition?subject=${getReferenceString(p)}&code:not=x&_count=1&_total=accurate`) ); expect(bundle.entry?.length).toStrictEqual(1); const nextUrl = bundle.link?.find((l) => l.relation === 'next')?.url; expect(nextUrl).toBeDefined(); expect(nextUrl).toContain('code:not=x'); })); test('Reference identifier search', () => withTestContext(async () => { const code = randomUUID(); const c1 = await repo.createResource<Condition>({ resourceType: 'Condition', code: { coding: [{ code }] }, subject: { identifier: { system: 'mrn', value: '123456' } }, }); const c2 = await repo.createResource<Condition>({ resourceType: 'Condition', code: { coding: [{ code }] }, subject: { identifier: { system: 'xyz', value: '123456' } }, }); // Search with system const bundle1 = await repo.search(parseSearchRequest(`Condition?code=${code}&subject:identifier=mrn|123456`)); expect(bundle1.entry?.length).toStrictEqual(1); expect(bundleContains(bundle1, c1)).toBeTruthy(); expect(bundleContains(bundle1, c2)).not.toBeTruthy(); // Search without system const bundle2 = await repo.search(parseSearchRequest(`Condition?code=${code}&subject:identifier=123456`)); expect(bundle2.entry?.length).toStrictEqual(2); expect(bundleContains(bundle2, c1)).toBeTruthy(); expect(bundleContains(bundle2, c2)).toBeTruthy(); // Search with count const bundle3 = await repo.search( parseSearchRequest(`Condition?code=${code}&subject:identifier=mrn|123456&_total=accurate`) ); expect(bundle3.entry?.length).toStrictEqual(1); expect(bundle3.total).toBe(1); expect(bundleContains(bundle3, c1)).toBeTruthy(); expect(bundleContains(bundle3, c2)).not.toBeTruthy(); })); test('Task patient identifier search', () => withTestContext(async () => { const identifier = randomUUID(); // Create a Task with a patient identifier reference _with_ Reference.type const task1 = await repo.createResource<Task>({ resourceType: 'Task', status: 'accepted', intent: 'order', for: { type: 'Patient', identifier: { system: 'mrn', value: identifier }, }, }); // Create a Task with a patient identifier reference _without_ Reference.type const task2 = await repo.createResource<Task>({ resourceType: 'Task', status: 'accepted', intent: 'order', for: { identifier: { system: 'mrn', value: identifier }, }, }); // Search by "subject" // This will include both Tasks, because the "subject" search parameter does not care about "type" const bundle1 = await repo.search( parseSearchRequest(`Task?subject:identifier=mrn|${identifier}&_total=accurate`) ); expect(bundle1.total).toStrictEqual(2); expect(bundle1.entry?.length).toStrictEqual(2); expect(bundleContains(bundle1, task1)).toBeTruthy(); expect(bundleContains(bundle1, task2)).toBeTruthy(); // Search by "patient" // This will only include the Task with the explicit "Patient" type, because the "patient" search parameter does care about "type" const bundle2 = await repo.search( parseSearchRequest(`Task?patient:identifier=mrn|${identifier}&_total=accurate`) ); expect(bundle2.total).toStrictEqual(1); expect(bundle2.entry?.length).toStrictEqual(1); expect(bundleContains(bundle2, task1)).toBeTruthy(); expect(bundleContains(bundle2, task2)).not.toBeTruthy(); })); describe('Resource meta search params', () => { let patientIdentifier: string; let patient: Patient; let identifierFilter: Filter; beforeAll(async () => { patientIdentifier = randomUUID(); patient = await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ system: 'http://example.com', value: patientIdentifier }], meta: { profile: ['http://example.com/fhir/a-patient-profile'], security: [{ system: 'http://hl7.org/fhir/v3/Confidentiality', code: 'N' }], source: 'http://example.org', tag: [{ system: 'http://hl7.org/fhir/v3/ObservationValue', code: 'SUBSETTED' }], }, }); identifierFilter = { code: 'identifier', operator: Operator.EQUALS, value: patientIdentifier, }; }); test('Search by dedicated token column search parameter, identifier', () => withTestContext(async () => { // make sure we're testing at least one token search parameter with dedicated columns const searchParam = getSearchParameter('Patient', 'identifier'); if (!searchParam) { throw new Error('Missing search parameter'); } const impl = getSearchParameterImplementation('Patient', searchParam); expect(impl.searchStrategy).toStrictEqual('token-column'); expect((impl as TokenColumnSearchParameterImplementation).hasDedicatedColumns).toStrictEqual(true); const bundle1 = await repo.search({ resourceType: 'Patient', filters: [ identifierFilter, { code: '_profile', operator: Operator.EQUALS, value: 'http://example.com/fhir/a-patient-profile', }, ], }); expect(bundleContains(bundle1, patient)).toBeTruthy(); })); test('Search by a shared token column search param, _security', () => withTestContext(async () => { // make sure we're testing at least one token search parameter with shared columns const searchParam = getSearchParameter('Patient', '_security'); if (!searchParam) { throw new Error('Missing search parameter'); } const impl = getSearchParameterImplementation('Patient', searchParam); expect(impl.searchStrategy).toStrictEqual('token-column'); expect((impl as TokenColumnSearchParameterImplementation).hasDedicatedColumns).toStrictEqual(false); const bundle2 = await repo.search({ resourceType: 'Patient', filters: [ identifierFilter, { code: '_security', operator: Operator.EQUALS, value: 'http://hl7.org/fhir/v3/Confidentiality|N', }, ], }); expect(bundleContains(bundle2, patient)).toBeTruthy(); })); test('Search by _source', () => withTestContext(async () => { const bundle3 = await repo.search({ resourceType: 'Patient', filters: [ identifierFilter, { code: '_source', operator: Operator.EQUALS, value: 'http://example.org', }, ], }); expect(bundleContains(bundle3, patient)).toBeTruthy(); })); test('Search by _tag', () => withTestContext(async () => { const bundle4 = await repo.search({ resourceType: 'Patient', filters: [ identifierFilter, { code: '_tag', operator: Operator.EQUALS, value: 'http://hl7.org/fhir/v3/ObservationValue|SUBSETTED', }, ], }); expect(bundleContains(bundle4, patient)).toBeTruthy(); })); }); test('Token :text search', () => withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient' }); const obs1 = await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { text: randomUUID() }, subject: createReference(patient), }); const obs2 = await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { coding: [{ display: randomUUID() }] }, subject: createReference(patient), }); const result1 = await repo.search({ resourceType: 'Observation', filters: [{ code: 'code', operator: Operator.TEXT, value: obs1.code?.text as string }], }); expect(result1.entry?.[0]?.resource?.id).toStrictEqual(obs1.id); const result2 = await repo.search({ resourceType: 'Observation', filters: [{ code: 'code', operator: Operator.TEXT, value: obs2.code?.coding?.[0]?.display as string }], }); expect(result2.entry?.[0]?.resource?.id).toStrictEqual(obs2.id); })); test('_filter search', () => withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient' }); const statuses: ('preliminary' | 'final')[] = ['preliminary', 'final']; const codes = ['123', '456']; const observations = []; for (const status of statuses) { for (const code of codes) { observations.push( await repo.createResource<Observation>({ resourceType: 'Observation', subject: createReference(patient), status, code: { coding: [{ code }] }, }) ); } } const result = await repo.search({ resourceType: 'Observation', filters: [ { code: 'subject', operator: Operator.EQUALS, value: getReferenceString(patient), }, { code: '_filter', operator: Operator.EQUALS, value: '(status eq preliminary and code eq 123) or (not (status eq preliminary) and code eq 456)', }, ], }); expect(result.entry).toHaveLength(2); })); test('_filter eq', () => withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Evelyn'] }], managingOrganization: { reference: 'Organization/' + randomUUID() }, }); const result1 = await repo.search({ resourceType: 'Patient', filters: [ { code: 'organization', operator: Operator.EQUALS, value: patient.managingOrganization?.reference as string, }, { code: '_filter', operator: Operator.EQUALS, value: 'given eq Eve', // eq with a prefix should NOT match }, ], }); expect(result1.entry).toHaveLength(0); const result2 = await repo.search({ resourceType: 'Patient', filters: [ { code: 'organization', operator: Operator.EQUALS, value: patient.managingOrganization?.reference as string, }, { code: '_filter', operator: Operator.EQUALS, value: 'given eq Evelyn', // eq with exact value should match }, ], }); expect(result2.entry).toHaveLength(1); })); test('_filter birthdate eq', () => withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Evelyn'] }], birthDate: '2000-01-01', managingOrganization: { reference: 'Organization/' + randomUUID() }, }); const result1 = await repo.search({ resourceType: 'Patient', filters: [ { code: 'organization', operator: Operator.EQUALS, value: patient.managingOrganization?.reference as string, }, { code: '_filter', operator: Operator.EQUALS, value: 'birthdate eq "2000-01-01"', }, ], }); expect(result1.entry).toHaveLength(1); })); test('_filter ne', () => withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Eve'] }], managingOrganization: { reference: 'Organization/' + randomUUID() }, }); const result = await repo.search({ resourceType: 'Patient', filters: [ { code: 'organization', operator: Operator.EQUALS, value: patient.managingOrganization?.reference as string, }, { code: '_filter', operator: Operator.EQUALS, value: 'given ne Eve', }, ], }); expect(result.entry).toHaveLength(0); })); test('_filter re', () => withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Eve'] }], managingOrganization: { reference: 'Organization/' + randomUUID() }, }); const result = await repo.search({ resourceType: 'Patient', filters: [ { code: '_filter', operator: Operator.EQUALS, value: 'organization re ' + patient.managingOrganization?.reference, }, ], }); expect(result.entry).toHaveLength(1); expect(result.entry?.[0]?.resource?.id).toStrictEqual(patient.id); })); test('_filter with chained search', () => withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient', birthDate: '2000-01-01', name: [{ given: ['Eve'] }], managingOrganization: { reference: 'Organization/' + randomUUID() }, }); await repo.createResource<Observation>({ resourceType: 'Observation', effectiveDateTime: '2000-01-01', status: 'final', code: { text: 'Born' }, subject: createReference(patient), }); const result = await repo.search({ resourceType: 'Patient', filters: [ { code: '_filter', operator: Operator.EQUALS, value: '_has:Observation:subject:date pr true', }, ], }); expect(result.entry).toHaveLength(1); expect(result.entry?.[0]?.resource?.id).toStrictEqual(patient.id); })); test('_filter sw', () => withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Evelyn', 'Dierdre'], family: 'Arachnae' }], }); const result = await repo.search({ resourceType: 'Patient', filters: [ { code: '_filter', operator: Operator.EQUALS, value: 'name eq Dier', }, ], }); expect(result.entry).toHaveLength(0); const result2 = await repo.search({ resourceType: 'Patient', filters: [ { code: '_filter', operator: Operator.EQUALS, value: 'name sw Dier', }, ], }); expect(result2.entry).toHaveLength(1); expect(result2.entry?.[0]?.resource?.id).toStrictEqual(patient.id); })); test('_filter with chained search', () => withTestContext(async () => { const mrn = randomUUID(); const npi = randomUUID(); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Eve'] }], identifier: [{ system: 'http://example.com/mrn', value: mrn }], }); const practitioner = await repo.createResource<Practitioner>({ resourceType: 'Practitioner', name: [{ given: ['Yves'] }], identifier: [{ system: 'http://example.com/npi', value: npi }], }); const observation1 = await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { text: 'Strep test', }, subject: createReference(patient), }); const observation2 = await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://example.com/obs', code: 'STRP' }], }, performer: [createReference(practitioner)], }); const result = await repo.search({ resourceType: 'Observation', filters: [ { code: '_filter', operator: Operator.EQUALS, value: `subject:Patient.identifier eq http://example.com/mrn|${mrn} or performer:Practitioner.identifier eq http://example.com/npi|${npi}`, }, ], }); expect(result.entry).toHaveLength(2); expect(result.entry?.map((e) => e.resource?.id)).toStrictEqual( expect.arrayContaining([observation1.id, observation2.id]) ); })); test('Lookup table exact match with comma disjunction', () => withTestContext(async () => { const family = randomUUID(); const p1 = await repo.createResource({ resourceType: 'Patient', name: [{ given: ['x'], family }] }); const p2 = await repo.createResource({ resourceType: 'Patient', name: [{ given: ['xx'], family }] }); const p3 = await repo.createResource({ resourceType: 'Patient', name: [{ given: ['y'], family }] }); const p4 = await repo.createResource({ resourceType: 'Patient', name: [{ given: ['yy'], family }] }); const bundle = await repo.search({ resourceType: 'Patient', total: 'accurate', filters: [ { code: 'given', operator: Operator.EXACT, value: 'x,y', }, { code: 'family', operator: Operator.EXACT, value: family, }, ], }); expect(bundle.entry?.length).toStrictEqual(2); expect(bundleContains(bundle, p1)).toBeTruthy(); expect(bundleContains(bundle, p2)).not.toBeTruthy(); expect(bundleContains(bundle, p3)).toBeTruthy(); expect(bundleContains(bundle, p4)).not.toBeTruthy(); expect(bundle.total).toStrictEqual(2); })); test('Duplicate rows from token lookup', () => withTestContext(async () => { const code = randomUUID(); const p = await repo.createResource({ resourceType: 'Patient' }); const s = await repo.createResource({ resourceType: 'ServiceRequest', subject: createReference(p), status: 'active', intent: 'order', category: [ { text: code, coding: [ { system: 'https://example.com/category', code, }, ], }, ], }); const bundle = await repo.search<ServiceRequest>({ resourceType: 'ServiceRequest', filters: [{ code: 'category', operator: Operator.EQUALS, value: code }], }); expect(bundle.entry?.length).toStrictEqual(1); expect(bundleContains(bundle, s)).toBeTruthy(); })); test('Filter task by due date', () => withTestContext(async () => { const code = randomUUID(); // Create 3 tasks // Mix of "no due date", using "start", and using "end" const task1 = await repo.createResource<Task>({ resourceType: 'Task', status: 'requested', intent: 'order', code: { coding: [{ code }] }, }); const task2 = await repo.createResource<Task>({ resourceType: 'Task', status: 'requested', intent: 'order', code: { coding: [{ code }] }, restriction: { period: { start: '2023-06-02T00:00:00.000Z' } }, }); const task3 = await repo.createResource<Task>({ resourceType: 'Task', status: 'requested', intent: 'order', code: { coding: [{ code }] }, restriction: { period: { end: '2023-06-03T00:00:00.000Z' } }, }); // Sort and filter by due date const bundle = await repo.search<Task>({ resourceType: 'Task', filters: [ { code: 'code', operator: Operator.EQUALS, value: code }, { code: 'due-date', operator: Operator.GREATER_THAN, value: '2023-06-01T00:00:00.000Z' }, ], sortRules: [{ code: 'due-date' }], }); expect(bundle.entry?.length).toStrictEqual(2); expect(bundle.entry?.[0]?.resource?.id).toStrictEqual(task2.id); expect(bundle.entry?.[1]?.resource?.id).toStrictEqual(task3.id); expect(bundleContains(bundle, task1)).not.toBeTruthy(); })); test('Get estimated count with filter on human name', async () => withTestContext(async () => { const result = await repo.search({ resourceType: 'Patient', total: 'estimate', filters: [ { code: 'name', operator: Operator.EQUALS, value: 'John', }, ], }); expect(result.total).toBeDefined(); expect(typeof result.total).toBe('number'); })); test('Organization by name', () => withTestContext(async () => { const org = await repo.createResource<Organization>({ resourceType: 'Organization', name: randomUUID(), }); const result = await repo.search({ resourceType: 'Organization', filters: [ { code: 'name', operator: Operator.EQUALS, value: `wrongname,${(org.name as string).slice(0, 5)}`, }, ], }); expect(result.entry?.length).toBe(1); })); test('Ambiguous search columns', () => withTestContext(async () => { const result = await repo.search({ resourceType: 'ProjectMembership', filters: [ { code: 'user:User.email', operator: Operator.EQUALS, value: randomUUID() + '@example.com', }, ], }); expect(result.entry?.length).toBe(0); })); test('Practitioner by email is case insensitive', () => withTestContext(async () => { const email = 'UPPER@EXAMPLE.COM'; const practitioner = await repo.createResource<Practitioner>({ resourceType: 'Practitioner', telecom: [{ system: 'email', value: email }], }); const result = await repo.search({ resourceType: 'Practitioner', filters: [ { code: 'telecom', operator: Operator.EQUALS, value: 'uPpeR@ExAmPlE.CoM', }, ], }); expect(result.entry?.length).toBe(1); expect(bundleContains(result, practitioner)).toBeDefined(); })); test('Practitioner by identifier is case sensitive', () => withTestContext(async () => { const value = 'MiXeD-cAsE'; const practitioner = await repo.createResource<Practitioner>({ resourceType: 'Practitioner', identifier: [{ system: 'http://example.com', value }], }); for (const [query, shouldFind] of [ [value, true], [value.toLocaleLowerCase(), false], [value.toLocaleUpperCase(), false], ] as [string, boolean][]) { const result = await repo.search({ resourceType: 'Practitioner', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: query, }, ], }); if (shouldFind) { expect(result.entry?.length).toBe(1); expect(bundleContains(result, practitioner)).toBeDefined(); } else { expect(result.entry?.length).toBe(0); } } })); test('Patient by name with stop word', () => withTestContext(async () => { const seed = randomUUID(); await repo.createResource<Patient>({ resourceType: 'Patient', name: [ { given: [seed + 'Justin', 'Wynn'], family: 'Sanders' + seed, }, ], }); const result = await repo.search({ resourceType: 'Patient', filters: [ { code: 'name', operator: Operator.CONTAINS, value: `${seed.slice(-3)}just`, }, ], }); expect(result.entry?.length).toBe(1); })); test('Sort by ID', () => withTestContext(async () => { const org = await repo.createResource<Organization>({ resourceType: 'Organization', name: 'org1' }); const managingOrganization = createReference(org); await repo.createResource<Patient>({ resourceType: 'Patient', managingOrganization }); await repo.createResource<Patient>({ resourceType: 'Patient', managingOrganization }); const result1 = await repo.search({ resourceType: 'Patient', filters: [{ code: 'organization', operator: Operator.EQUALS, value: getReferenceString(org) }], sortRules: [{ code: '_id', descending: false }], }); expect(result1.entry).toHaveLength(2); expect(result1.entry?.[0]?.resource?.id?.localeCompare(result1.entry?.[1]?.resource?.id as string)).toBe(-1); const result2 = await repo.search({ resourceType: 'Patient', filters: [{ code: 'organization', operator: Operator.EQUALS, value: getReferenceString(org) }], sortRules: [{ code: '_id', descending: true }], }); expect(result2.entry).toHaveLength(2); expect(result2.entry?.[0]?.resource?.id?.localeCompare(result2.entry?.[1]?.resource?.id as string)).toBe(1); })); test('Numeric parameter', () => withTestContext(async () => { const ident = randomUUID(); const riskAssessment: RiskAssessment = { resourceType: 'RiskAssessment', status: 'final', identifier: [{ value: ident }], subject: { reference: 'Patient/test', }, prediction: [ { outcome: { text: 'Breast Cancer' }, probabilityDecimal: 0.000168, whenRange: { high: { value: 53, unit: 'years' }, }, }, { outcome: { text: 'Breast Cancer' }, probabilityDecimal: 0.000368, whenRange: { low: { value: 54, unit: 'years' }, high: { value: 57, unit: 'years' }, }, }, { outcome: { text: 'Breast Cancer' }, probabilityDecimal: 0.000594, whenRange: { low: { value: 58, unit: 'years' }, high: { value: 62, unit: 'years' }, }, }, { outcome: { text: 'Breast Cancer' }, probabilityDecimal: 0.000838, whenRange: { low: { value: 63, unit: 'years' }, high: { value: 67, unit: 'years' }, }, }, ], }; await repo.createResource(riskAssessment); const result = await repo.search({ resourceType: 'RiskAssessment', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: ident }, { code: 'probability', operator: Operator.GREATER_THAN, value: '0.0005' }, ], }); expect(result.entry).toHaveLength(1); })); test('Disjunction with lookup tables', () => withTestContext(async () => { const n1 = randomUUID(); const n2 = randomUUID(); const p1 = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ family: n1 }], }); const p2 = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ family: n2 }], }); const result = await repo.search({ resourceType: 'Patient', filters: [{ code: '_filter', operator: Operator.EQUALS, value: `name co "${n1}" or name co "${n2}"` }], }); expect(result.entry).toHaveLength(2); expect(bundleContains(result, p1)).toBeDefined(); expect(bundleContains(result, p2)).toBeDefined(); })); test('Sort by unknown search parameter', async () => withTestContext(async () => { await expect( repo.search({ resourceType: 'Patient', sortRules: [{ code: 'xyz' }], }) ).rejects.toThrow(/^Unknown search parameter: xyz$/); })); test('Date range search', () => withTestContext(async () => { const ident = randomUUID(); const measureReport = await repo.createResource<MeasureReport>({ resourceType: 'MeasureReport', status: 'complete', type: 'individual', measure: 'http://example.com', identifier: [{ value: ident }], period: { start: '2020-01-01T12:00:00.000Z', end: '2020-01-15T12:00:00.000Z', }, }); expect(measureReport).toBeDefined(); async function searchByPeriod(operator: Operator, value: string): Promise<boolean> { const result = await repo.searchOne<MeasureReport>({ resourceType: 'MeasureReport', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: ident }, { code: 'period', operator, value }, ], }); return !!result; } expect(await searchByPeriod(Operator.EQUALS, '2019-12-31')).toBe(false); expect(await searchByPeriod(Operator.EQUALS, '2020-01-01')).toBe(true); expect(await searchByPeriod(Operator.EQUALS, '2020-01-02')).toBe(true); expect(await searchByPeriod(Operator.EQUALS, '2020-01-15')).toBe(true); expect(await searchByPeriod(Operator.EQUALS, '2020-01-16')).toBe(false); expect(await searchByPeriod(Operator.EQUALS, '2020-01-01T11:59:59.000Z')).toBe(false); expect(await searchByPeriod(Operator.EQUALS, '2020-01-01T12:00:00.000Z')).toBe(true); expect(await searchByPeriod(Operator.EQUALS, '2020-01-15T11:59:59.000Z')).toBe(true); expect(await searchByPeriod(Operator.EQUALS, '2020-01-15T12:00:00.000Z')).toBe(true); expect(await searchByPeriod(Operator.EQUALS, '2020-01-15T12:00:01.000Z')).toBe(false); expect(await searchByPeriod(Operator.NOT_EQUALS, '2019-12-31')).toBe(true); expect(await searchByPeriod(Operator.NOT_EQUALS, '2020-01-01')).toBe(false); expect(await searchByPeriod(Operator.NOT_EQUALS, '2020-01-02')).toBe(false); expect(await searchByPeriod(Operator.NOT_EQUALS, '2020-01-15')).toBe(false); expect(await searchByPeriod(Operator.NOT_EQUALS, '2020-01-16')).toBe(true); expect(await searchByPeriod(Operator.NOT_EQUALS, '2020-01-01T11:59:59.000Z')).toBe(true); expect(await searchByPeriod(Operator.NOT_EQUALS, '2020-01-01T12:00:00.000Z')).toBe(false); expect(await searchByPeriod(Operator.NOT_EQUALS, '2020-01-15T11:59:59.000Z')).toBe(false); expect(await searchByPeriod(Operator.NOT_EQUALS, '2020-01-15T12:00:00.000Z')).toBe(false); expect(await searchByPeriod(Operator.NOT_EQUALS, '2020-01-15T12:00:01.000Z')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN, '2019-12-31')).toBe(false); expect(await searchByPeriod(Operator.LESS_THAN, '2020-01-01')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN, '2020-01-02')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN, '2020-01-15')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN, '2020-01-16')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN, '2020-01-01T11:59:59.000Z')).toBe(false); expect(await searchByPeriod(Operator.LESS_THAN, '2020-01-01T12:00:00.000Z')).toBe(false); expect(await searchByPeriod(Operator.LESS_THAN, '2020-01-15T11:59:59.000Z')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN, '2020-01-15T12:00:00.000Z')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN, '2020-01-15T12:00:01.000Z')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN_OR_EQUALS, '2019-12-31')).toBe(false); expect(await searchByPeriod(Operator.LESS_THAN_OR_EQUALS, '2020-01-01')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN_OR_EQUALS, '2020-01-02')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN_OR_EQUALS, '2020-01-15')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN_OR_EQUALS, '2020-01-16')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN_OR_EQUALS, '2020-01-01T11:59:59.000Z')).toBe(false); expect(await searchByPeriod(Operator.LESS_THAN_OR_EQUALS, '2020-01-01T12:00:00.000Z')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN_OR_EQUALS, '2020-01-15T11:59:59.000Z')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN_OR_EQUALS, '2020-01-15T12:00:00.000Z')).toBe(true); expect(await searchByPeriod(Operator.LESS_THAN_OR_EQUALS, '2020-01-15T12:00:01.000Z')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN, '2019-12-31')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN, '2020-01-01')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN, '2020-01-02')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN, '2020-01-15')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN, '2020-01-16')).toBe(false); expect(await searchByPeriod(Operator.GREATER_THAN, '2020-01-01T11:59:59.000Z')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN, '2020-01-01T12:00:00.000Z')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN, '2020-01-15T11:59:59.000Z')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN, '2020-01-15T12:00:00.000Z')).toBe(false); expect(await searchByPeriod(Operator.GREATER_THAN, '2020-01-15T12:00:01.000Z')).toBe(false); expect(await searchByPeriod(Operator.GREATER_THAN_OR_EQUALS, '2019-12-31')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN_OR_EQUALS, '2020-01-01')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN_OR_EQUALS, '2020-01-02')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN_OR_EQUALS, '2020-01-15')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN_OR_EQUALS, '2020-01-16')).toBe(false); expect(await searchByPeriod(Operator.GREATER_THAN_OR_EQUALS, '2020-01-01T11:59:59.000Z')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN_OR_EQUALS, '2020-01-01T12:00:00.000Z')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN_OR_EQUALS, '2020-01-15T11:59:59.000Z')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN_OR_EQUALS, '2020-01-15T12:00:00.000Z')).toBe(true); expect(await searchByPeriod(Operator.GREATER_THAN_OR_EQUALS, '2020-01-15T12:00:01.000Z')).toBe(false); expect(await searchByPeriod(Operator.STARTS_AFTER, '2019-12-31')).toBe(true); expect(await searchByPeriod(Operator.STARTS_AFTER, '2020-01-01')).toBe(false); expect(await searchByPeriod(Operator.STARTS_AFTER, '2020-01-02')).toBe(false); expect(await searchByPeriod(Operator.STARTS_AFTER, '2020-01-15')).toBe(false); expect(await searchByPeriod(Operator.STARTS_AFTER, '2020-01-16')).toBe(false); expect(await searchByPeriod(Operator.STARTS_AFTER, '2020-01-01T11:59:59.000Z')).toBe(true); expect(await searchByPeriod(Operator.STARTS_AFTER, '2020-01-01T12:00:00.000Z')).toBe(false); expect(await searchByPeriod(Operator.STARTS_AFTER, '2020-01-15T11:59:59.000Z')).toBe(false); expect(await searchByPeriod(Operator.STARTS_AFTER, '2020-01-15T12:00:00.000Z')).toBe(false); expect(await searchByPeriod(Operator.STARTS_AFTER, '2020-01-15T12:00:01.000Z')).toBe(false); expect(await searchByPeriod(Operator.ENDS_BEFORE, '2019-12-31')).toBe(false); expect(await searchByPeriod(Operator.ENDS_BEFORE, '2020-01-01')).toBe(false); expect(await searchByPeriod(Operator.ENDS_BEFORE, '2020-01-02')).toBe(false); expect(await searchByPeriod(Operator.ENDS_BEFORE, '2020-01-15')).toBe(false); expect(await searchByPeriod(Operator.ENDS_BEFORE, '2020-01-16')).toBe(true); expect(await searchByPeriod(Operator.ENDS_BEFORE, '2020-01-01T11:59:59.000Z')).toBe(false); expect(await searchByPeriod(Operator.ENDS_BEFORE, '2020-01-01T12:00:00.000Z')).toBe(false); expect(await searchByPeriod(Operator.ENDS_BEFORE, '2020-01-15T11:59:59.000Z')).toBe(false); expect(await searchByPeriod(Operator.ENDS_BEFORE, '2020-01-15T12:00:00.000Z')).toBe(false); expect(await searchByPeriod(Operator.ENDS_BEFORE, '2020-01-15T12:00:01.000Z')).toBe(true); })); test('Multiple resource types with _type', async () => withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient' }); const obs = await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { text: 'test' }, subject: createReference(patient), }); const bundle = await repo.search({ resourceType: 'Patient', types: ['Patient', 'Observation'], filters: [{ code: '_compartment', operator: Operator.EQUALS, value: getReferenceString(patient) }], }); expect(bundle.entry?.length).toBe(2); expect(bundleContains(bundle, patient)).toBeTruthy(); expect(bundleContains(bundle, obs)).toBeTruthy(); })); test('Binary search not allowed', async () => withTestContext(async () => { await expect(repo.search<Binary>({ resourceType: 'Binary' })).rejects.toThrow( 'Cannot search on Binary resource type' ); })); describe('US Core Search Parameters', () => { test('USCoreCareTeamRole', async () => withTestContext(async () => { const careTeam = await repo.createResource<CareTeam>({ resourceType: 'CareTeam', participant: [ { member: { reference: 'Practitioner/123', }, role: [ { coding: [ { system: 'http://snomed.info/sct', code: '102262009', display: 'Chocolate (substance)', }, ], }, ], }, ], }); const bundle1 = await repo.search({ resourceType: 'CareTeam', filters: [{ code: 'role', operator: Operator.EQUALS, value: '102262009' }], }); expect(bundle1.entry?.length).toBe(1); expect(bundleContains(bundle1, careTeam)).toBeTruthy(); })); test('USCoreConditionAssertedDate', async () => withTestContext(async () => { const resource = await repo.createResource<Condition>({ resourceType: 'Condition', extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/condition-assertedDate', // valueDateTime: '2024-03-18T23:04:00.000Z', valueDateTime: '2000-03-18', }, ], subject: { reference: 'Patient/123', display: 'Matt Long', }, }); const oldDate = new Date(1999, 11, 30); const bundle1 = await repo.search({ resourceType: 'Condition', // filters: [{ code: 'asserted-date', operator: Operator.GREATER_THAN, value: oldDate.toISOString() }], filters: [{ code: 'asserted-date', operator: Operator.STARTS_AFTER, value: oldDate.toISOString() }], }); expect(bundle1.entry?.length).toBe(1); expect(bundleContains(bundle1, resource)).toBeTruthy(); })); test('USCoreEncounterDischargeDisposition', async () => withTestContext(async () => { const resource = await repo.createResource<Encounter>({ resourceType: 'Encounter', status: 'unknown', class: { system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', code: 'IMP', display: 'inpatient encounter', }, hospitalization: { dischargeDisposition: { coding: [ { system: 'http://www.nubc.org/patient-discharge', code: '01', display: 'Discharged to Home', }, ], }, }, }); const bundle1 = await repo.search({ resourceType: 'Encounter', filters: [{ code: 'discharge-disposition', operator: Operator.EQUALS, value: '01' }], }); expect(bundle1.entry?.length).toBe(1); expect(bundleContains(bundle1, resource)).toBeTruthy(); const bundle2 = await repo.search({ resourceType: 'Encounter', filters: [{ code: 'discharge-disposition', operator: Operator.EQUALS, value: '55' }], }); expect(bundle2.entry?.length).toBe(0); })); test('USCoreGoalDescription', async () => withTestContext(async () => { const resource = await repo.createResource<Goal>({ resourceType: 'Goal', lifecycleStatus: 'active', description: { coding: [ { system: 'http://snomed.info/sct', code: '406156006', display: 'In paid employment', }, ], text: 'This text is ignored in search.', }, subject: { reference: 'Patient/example', display: 'Amy Shaw', }, }); const bundle1 = await repo.search({ resourceType: 'Goal', filters: [{ code: 'description', operator: Operator.EQUALS, value: '406156006' }], }); expect(bundle1.entry?.length).toBe(1); expect(bundleContains(bundle1, resource)).toBeTruthy(); })); test('USCorePatient race, ethnicity, genderIdentity', async () => withTestContext(async () => { const resource = await repo.createResource<Patient>({ resourceType: 'Patient', extension: [ { extension: [ { url: 'ombCategory', valueCoding: { system: 'urn:oid:2.16.840.1.113883.6.238', code: '2106-3', display: 'White', }, }, { url: 'ombCategory', valueCoding: { system: 'urn:oid:2.16.840.1.113883.6.238', code: '1002-5', display: 'American Indian or Alaska Native', }, }, { url: 'ombCategory', valueCoding: { system: 'urn:oid:2.16.840.1.113883.6.238', code: '2028-9', display: 'Asian', }, }, { url: 'detailed', valueCoding: { system: 'urn:oid:2.16.840.1.113883.6.238', code: '1586-7', display: 'Shoshone', }, }, { url: 'detailed', valueCoding: { system: 'urn:oid:2.16.840.1.113883.6.238', code: '2036-2', display: 'Filipino', }, }, { url: 'text', valueString: 'Mixed', }, ], url: 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-race', }, { extension: [ { url: 'ombCategory', valueCoding: { system: 'urn:oid:2.16.840.1.113883.6.238', code: '2135-2', display: 'Hispanic or Latino', }, }, { url: 'detailed', valueCoding: { system: 'urn:oid:2.16.840.1.113883.6.238', code: '2184-0', display: 'Dominican', }, }, { url: 'detailed', valueCoding: { system: 'urn:oid:2.16.840.1.113883.6.238', code: '2148-5', display: 'Mexican', }, }, { url: 'text', valueString: 'Hispanic or Latino', }, ], url: 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity', }, { url: 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-genderIdentity', valueCodeableConcept: { coding: [ { system: 'http://terminology.hl7.org/CodeSystem/v3-NullFlavor', code: 'ASKU', display: 'asked but unknown', }, ], text: 'asked but unknown', }, }, ], }); // race const bundle1 = await repo.search({ resourceType: 'Patient', // ombCategory filters: [{ code: 'race', operator: Operator.EQUALS, value: '1002-5' }], }); expect(bundle1.entry?.length).toBe(1); expect(bundleContains(bundle1, resource)).toBeTruthy(); const bundle2 = await repo.search({ resourceType: 'Patient', // detailed filters: [{ code: 'race', operator: Operator.EQUALS, value: '1586-7' }], }); expect(bundle2.entry?.length).toBe(1); expect(bundleContains(bundle2, resource)).toBeTruthy(); // ethnicity const bundle3 = await repo.search({ resourceType: 'Patient', // ombCategory filters: [{ code: 'ethnicity', operator: Operator.EQUALS, value: '2135-2' }], }); expect(bundle3.entry?.length).toBe(1); expect(bundleContains(bundle3, resource)).toBeTruthy(); const bundle4 = await repo.search({ resourceType: 'Patient', // detailed filters: [{ code: 'ethnicity', operator: Operator.EQUALS, value: '2184-0' }], }); expect(bundle4.entry?.length).toBe(1); expect(bundleContains(bundle4, resource)).toBeTruthy(); // genderIdentity const bundle5 = await repo.search({ resourceType: 'Patient', filters: [{ code: 'gender-identity', operator: Operator.EQUALS, value: 'ASKU' }], }); expect(bundle5.entry?.length).toBe(1); expect(bundleContains(bundle5, resource)).toBeTruthy(); })); }); test('Reference search patterns', async () => withTestContext(async () => { const uuid = randomUUID(); const patient1 = await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ value: uuid }], }); const patient2 = await repo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ value: uuid }], link: [{ type: 'refer', other: createReference(patient1) }], }); const refStr = getReferenceString(patient1); const basicEqualsResult = await repo.search(parseSearchRequest(`Patient?identifier=${uuid}&link=${refStr}`)); expect(basicEqualsResult.entry).toHaveLength(1); expect(basicEqualsResult.entry?.[0]?.resource?.id).toStrictEqual(patient2.id); const notEqualsResult = await repo.search(parseSearchRequest(`Patient?identifier=${uuid}&link:not=${refStr}`)); expect(notEqualsResult.entry).toHaveLength(1); expect(notEqualsResult.entry?.[0]?.resource?.id).toStrictEqual(patient1.id); const filterEqualsResult = await repo.search( parseSearchRequest(`Patient?_filter=identifier eq "${uuid}" and link re "${refStr}"`) ); expect(filterEqualsResult.entry).toHaveLength(1); expect(filterEqualsResult.entry?.[0]?.resource?.id).toStrictEqual(patient2.id); const filterNotEqualsResult = await repo.search( parseSearchRequest(`Patient?_filter=identifier eq "${uuid}" and link ne "${refStr}"`) ); expect(filterNotEqualsResult.entry).toHaveLength(1); expect(filterNotEqualsResult.entry?.[0]?.resource?.id).toStrictEqual(patient1.id); })); test('Reference search with inline resource', async () => withTestContext(async () => { const date = new Date().toISOString(); // Test the Bundle-composition search parameter // "expression" : "Bundle.entry[0].resource as Composition" // This is a special case of inlined resource // This only works if the Composition is a "real" Composition, and exists in the database, // not a purely "virtual" Composition in the Bundle alone // Create a Composition const composition = await repo.createResource<Composition>({ resourceType: 'Composition', status: 'final', type: { text: 'test' }, date, author: [{ reference: 'Practitioner/example' }], title: 'Test Composition', }); // Create a Bundle with the Composition const bundle = await repo.createResource<Bundle>({ resourceType: 'Bundle', type: 'searchset', entry: [{ resource: composition }], }); // Search for the bundle const searchResult1 = await repo.search({ resourceType: 'Bundle', filters: [{ code: 'composition', operator: Operator.EQUALS, value: getReferenceString(composition) }], }); expect(searchResult1.entry).toHaveLength(1); expect(searchResult1.entry?.[0]?.resource?.id).toBe(bundle.id); // Chained search on the Composition const searchResult2 = await repo.search( parseSearchRequest(`Bundle?composition.date=${encodeURIComponent(date)}`) ); expect(searchResult2.entry).toHaveLength(1); expect(searchResult2.entry?.[0]?.resource?.id).toBe(bundle.id); })); describe('searchByReference', () => { async function createPatients(repo: Repository, count: number): Promise<WithId<Patient>[]> { const patients = []; for (let i = 0; i < count; i++) { patients.push(await repo.createResource<Patient>({ resourceType: 'Patient' })); } return patients; } async function createObservations( repo: Repository, count: number, { subject, hasMember, }: { subject?: Patient; hasMember?: Observation[]; } ): Promise<WithId<Observation>[]> { const resources = []; for (let i = 0; i < count; i++) { resources.push( await repo.createResource<Observation>({ resourceType: 'Observation', subject: subject ? createReference(subject) : undefined, hasMember: hasMember ? hasMember.map((o) => createReference(o)) : undefined, status: 'final', code: { text: 'code CodeableConcept.text' }, valueString: i.toString(), }) ); } return resources; } async function createServiceRequests( repo: Repository, count: number, patient: Patient ): Promise<WithId<ServiceRequest>[]> { const resources = []; for (let i = 0; i < count; i++) { resources.push( await repo.createResource<ServiceRequest>({ resourceType: 'ServiceRequest', subject: createReference(patient), status: 'active', intent: 'plan', }) ); } return resources; } function expectSearchByReferenceResults<Parent extends WithId<Resource>, Child extends WithId<Resource>>( parents: Parent[], childrenByParent: Child[][], count: number, results: Record<string, Child[]> ): void { // Each parent should be present in the results expect(Object.keys(results)).toHaveLength(parents.length); // All children are accounted for as well expect(childrenByParent).toHaveLength(parents.length); for (let i = 0; i < parents.length; i++) { const parent = parents[i]; const children = childrenByParent[i]; const result = results[getReferenceString(parent)]; expect(result).toBeDefined(); // `count` caps the number of children included in results expect(result).toHaveLength(Math.min(children.length, count)); // children returned should be a subset of the original, exhaustive, list of children expect(children.map((c) => c.id)).toEqual(expect.arrayContaining(result.map((r) => r.id))); } } test('Basic reference', async () => withTestContext(async () => { const patients = await createPatients(repo, 3); const observations = [ await createObservations(repo, 2, { subject: patients[0] }), await createObservations(repo, 0, { subject: patients[1] }), await createObservations(repo, 4, { subject: patients[2] }), ]; const count = 3; const result = await repo.searchByReference<Observation>( { resourceType: 'Observation', count }, 'subject', patients.map((p) => getReferenceString(p)) ); expectSearchByReferenceResults(patients, observations, count, result); })); test('Array reference column', async () => withTestContext(async () => { const parentObservations = await createObservations(repo, 3, {}); const childObservations = [ await createObservations(repo, 2, { hasMember: [parentObservations[0]] }), await createObservations(repo, 0, { hasMember: [parentObservations[1]] }), await createObservations(repo, 4, { hasMember: [parentObservations[2]] }), ]; const count = 3; const result = await repo.searchByReference<Observation>( { resourceType: 'Observation', count }, 'has-member', parentObservations.map((o) => getReferenceString(o)) ); expectSearchByReferenceResults(parentObservations, childObservations, count, result); })); test('Invalid reference field', async () => withTestContext(async () => { // 'code' is not a reference search parameter await expect(() => repo.searchByReference<Observation>({ resourceType: 'Observation', count: 1 }, 'code', []) ).rejects.toThrow('Invalid reference search parameter'); })); test('Reference with sort', async () => withTestContext(async () => { const patients = await createPatients(repo, 2); const patientObservations = [ await createObservations(repo, 3, { subject: patients[0] }), await createObservations(repo, 2, { subject: patients[1] }), ]; const count = 3; // descending const resultDesc = await repo.searchByReference<Observation>( { resourceType: 'Observation', count, sortRules: [{ code: 'value-string', descending: true }] }, 'subject', patients.map((p) => getReferenceString(p)) ); expectSearchByReferenceResults(patients, patientObservations, count, resultDesc); expect(resultDesc[getReferenceString(patients[0])].map((o) => o.valueString)).toStrictEqual(['2', '1', '0']); expect(resultDesc[getReferenceString(patients[1])].map((o) => o.valueString)).toStrictEqual(['1', '0']); // ascending const resultAsc = await repo.searchByReference<Observation>( { resourceType: 'Observation', count, sortRules: [{ code: 'value-string', descending: false }] }, 'subject', patients.map((p) => getReferenceString(p)) ); expectSearchByReferenceResults(patients, patientObservations, count, resultAsc); expect(resultAsc[getReferenceString(patients[0])].map((o) => o.valueString)).toStrictEqual(['0', '1', '2']); expect(resultAsc[getReferenceString(patients[1])].map((o) => o.valueString)).toStrictEqual(['0', '1']); })); test('narrowed fields', async () => withTestContext(async () => { const patients = await createPatients(repo, 1); const patientObservations = [await createObservations(repo, 1, { subject: patients[0] })]; const count = 1; const result = await repo.searchByReference<Observation>( { resourceType: 'Observation', count, fields: ['status'] }, 'subject', patients.map((p) => getReferenceString(p)) ); expectSearchByReferenceResults(patients, patientObservations, count, result); const observation = patientObservations[0][0]; const resultObservation = result[getReferenceString(patients[0])][0]; expect({ ...observation, meta: undefined }).toEqual({ resourceType: 'Observation', id: observation.id, status: observation.status, code: observation.code, method: observation.method, subject: expect.objectContaining({ reference: getReferenceString(patients[0]) }), valueString: observation.valueString, }); expect(resultObservation).toStrictEqual({ resourceType: 'Observation', id: observation.id, meta: expect.objectContaining({ tag: [SUBSET_TAG], }), status: observation.status, code: observation.code, // code is a mandatory field so is included even though not specified in fields }); })); test('Multiple types', async () => withTestContext(async () => { const patients = await createPatients(repo, 3); const patientObservations = [ await createObservations(repo, 0, { subject: patients[0] }), await createObservations(repo, 1, { subject: patients[1] }), await createObservations(repo, 3, { subject: patients[2] }), ]; const patientServiceRequests = [ await createServiceRequests(repo, 3, patients[0]), await createServiceRequests(repo, 1, patients[1]), await createServiceRequests(repo, 0, patients[2]), ]; const count = 2; const result = await repo.searchByReference( { resourceType: 'Observation', count, types: ['Observation', 'ServiceRequest'], fields: ['subject'] }, 'subject', patients.map((p) => getReferenceString(p)) ); const childrenByParent: WithId<Resource>[][] = []; for (let i = 0; i < patients.length; i++) { childrenByParent.push([...patientObservations[i], ...patientServiceRequests[i]]); } expectSearchByReferenceResults(patients, childrenByParent, count, result); // First patient has only ServiceRequests expect(result[getReferenceString(patients[0])].map((r) => r.resourceType)).toStrictEqual([ 'ServiceRequest', 'ServiceRequest', ]); // Second patient has one ServiceRequest and one Observation expect( result[getReferenceString(patients[1])].map((r) => r.resourceType).sort((a, b) => a.localeCompare(b)) ).toStrictEqual(['Observation', 'ServiceRequest']); // Third patient has only Observations expect(result[getReferenceString(patients[2])].map((r) => r.resourceType)).toStrictEqual([ 'Observation', 'Observation', ]); })); test('Error on cursor and offset', () => withTestContext(async () => { try { await repo.search({ resourceType: 'Patient', offset: 10, cursor: 'foo' }); fail('Expected error'); } catch (err) { expect(normalizeErrorString(err)).toBe('Cannot use both offset and cursor'); } })); test('Cursor pagination', () => withTestContext(async () => { const tasks: Task[] = []; for (let i = 0; i < 50; i++) { const task = await repo.createResource<Task>({ resourceType: 'Task', status: 'accepted', intent: 'order', code: { text: 'cursor_test' }, }); tasks.push(task); } let url = 'Task?code=cursor_test&_sort=_lastUpdated'; let count = 0; while (url) { const bundle = await repo.search(parseSearchRequest(url)); count += bundle.entry?.length ?? 0; const link = bundle.link?.find((l) => l.relation === 'next')?.url; if (link) { expect(link.includes('_cursor')).toBe(true); url = link; } else { url = ''; } } expect(count).toBe(50); })); test('Cursor pagination dedupes across page boundaries', () => withTestContext(async () => { const systemRepo = getSystemRepo(); const identifier = randomUUID(); const lastUpdated = new Date(); lastUpdated.setMilliseconds(0); const tasks: Task[] = []; for (let i = 0; i < 50; i++) { const task = await systemRepo.createResource<Task>({ resourceType: 'Task', status: 'accepted', intent: 'unknown', identifier: [{ value: identifier }], meta: { lastUpdated: lastUpdated.toISOString() }, }); tasks.push(task); if (i % 7 === 6) { lastUpdated.setMilliseconds(lastUpdated.getMilliseconds() + 33); } } let url = `Task?identifier=${identifier}&_sort=_lastUpdated`; const seenResources: string[] = []; while (url) { const bundle = await systemRepo.search(parseSearchRequest(url)); for (const entry of bundle.entry ?? []) { seenResources.push(entry.resource?.id as string); } const link = bundle.link?.find((l) => l.relation === 'next')?.url; if (link) { expect(link.includes('_cursor')).toBe(true); url = link; } else { url = ''; } } expect(seenResources.length).toBe(50); })); test('V1 cursor is not parsed as V2', () => withTestContext(async () => { const identifier = randomUUID(); const task = await repo.createResource<Task>({ resourceType: 'Task', status: 'accepted', intent: 'unknown', identifier: [{ value: identifier }], }); const nextInstant = new Date(task.meta?.lastUpdated as string).getTime(); const v1Cursor = `1-${nextInstant}-${task.id}`; const bundle = await repo.search( parseSearchRequest(`Task?identifier=${identifier}&_sort=_lastUpdated&_cursor=${v1Cursor}`) ); expect(bundleContains(bundle, task)).toBeDefined(); const v2Cursor = `2-${nextInstant}-${task.id}`; const bundle2 = await repo.search( parseSearchRequest(`Task?identifier=${identifier}&_sort=_lastUpdated&_cursor=${v2Cursor}`) ); expect(bundleContains(bundle2, task)).toBeUndefined(); })); }); describe('Quantity search', () => { beforeAll(async () => { for (const weight of [70, 75, 80, 85, 90]) { await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { coding: [{ code: '29463-7' }] }, valueQuantity: { value: weight, unit: 'kg', system: 'http://unitsofmeasure.org', code: 'kg', }, }); } }); test('Basic', async () => withTestContext(async () => { const result = await repo.search( parseSearchRequest<Observation>('Observation?code=29463-7&value-quantity=gt80') ); expect(result.entry).toHaveLength(2); expect(result.entry?.find((e) => e.resource?.valueQuantity?.value === 85)).toBeDefined(); expect(result.entry?.find((e) => e.resource?.valueQuantity?.value === 90)).toBeDefined(); })); test('With units', async () => withTestContext(async () => { const result = await repo.search( parseSearchRequest<Observation>('Observation?code=29463-7&value-quantity=gt80|http://unitsofmeasure.org|kg') ); expect(result.entry).toHaveLength(2); expect(result.entry?.find((e) => e.resource?.valueQuantity?.value === 85)).toBeDefined(); expect(result.entry?.find((e) => e.resource?.valueQuantity?.value === 90)).toBeDefined(); })); test('Approximately', async () => withTestContext(async () => { const result = await repo.search( parseSearchRequest<Observation>('Observation?code=29463-7&value-quantity=ap80|http://unitsofmeasure.org|kg') ); expect(result.entry).toHaveLength(3); expect(result.entry?.find((e) => e.resource?.valueQuantity?.value === 75)).toBeDefined(); expect(result.entry?.find((e) => e.resource?.valueQuantity?.value === 80)).toBeDefined(); expect(result.entry?.find((e) => e.resource?.valueQuantity?.value === 85)).toBeDefined(); })); }); test('Invalid canonical chained search link', async () => withTestContext(async () => { await expect( repo.search(parseSearchRequest('EvidenceVariable?derived-from:ResearchStudy.status=active')) ).rejects.toThrow('ResearchStudy cannot be chained via canonical reference (EvidenceVariable:derived-from)'); await expect( repo.search(parseSearchRequest('ResearchStudy?_has:EvidenceVariable:derived-from:_id=foo')) ).rejects.toThrow('ResearchStudy cannot be chained via canonical reference (EvidenceVariable:derived-from)'); })); describe('discourage sequential scans', () => { let querySpy: jest.SpyInstance; beforeEach(() => { querySpy = jest.spyOn(repo.getDatabaseClient(DatabaseMode.READER), 'query'); }); afterEach(() => { querySpy.mockRestore(); config.fhirSearchDiscourageSeqScan = undefined; config.fhirSearchMinLimit = undefined; }); test('config.fhirSearchDiscourageSeqScan', async () => { expect(config.fhirSearchDiscourageSeqScan).toBeUndefined(); await repo.search(parseSearchRequest('Patient?identifier=123&_count=1')); expect(querySpy).toHaveBeenCalledTimes(1); querySpy.mockClear(); config.fhirSearchDiscourageSeqScan = true; await repo.search(parseSearchRequest('Patient?identifier=123&_count=1')); expect(querySpy).toHaveBeenCalledTimes(3); expect(querySpy).toHaveBeenNthCalledWith(1, expect.stringContaining('SET enable_seqscan = off')); expect(querySpy).toHaveBeenNthCalledWith(2, expect.stringContaining('SELECT'), expect.anything()); expect(querySpy).toHaveBeenNthCalledWith(3, expect.stringContaining('RESET enable_seqscan')); querySpy.mockRestore(); }); test('config.fhirSearchMinLimit', async () => { expect(config.fhirSearchMinLimit).toBeUndefined(); await repo.search(parseSearchRequest('Patient?identifier=123&_count=1')); expect(querySpy).toHaveBeenCalledTimes(1); expect(querySpy).toHaveBeenNthCalledWith(1, expect.stringMatching(/LIMIT 2$/), expect.anything()); querySpy.mockClear(); config.fhirSearchMinLimit = 39; await repo.search(parseSearchRequest('Patient?identifier=123&_count=1')); expect(querySpy).toHaveBeenCalledTimes(1); expect(querySpy).toHaveBeenNthCalledWith(1, expect.stringMatching(/LIMIT 39$/), expect.anything()); querySpy.mockClear(); }); }); }); describe('systemRepo', () => { const systemRepo = getSystemRepo(); beforeAll(async () => { const config = await loadTestConfig(); await initAppServices(config); }); afterAll(async () => { await shutdownApp(); }); test('Filter by _project', () => withTestContext(async () => { const idValue = randomUUID(); const project1 = (await systemRepo.createResource<Project>({ resourceType: 'Project' })).id; const project2 = (await systemRepo.createResource<Project>({ resourceType: 'Project' })).id; const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ system: 'id', value: idValue }], meta: { project: project1, }, }); const patient2 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ system: 'id', value: idValue }], meta: { project: project2, }, }); const patient3 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', identifier: [{ system: 'id', value: idValue }], // no project }); const bundle = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: '_project', operator: Operator.EQUALS, value: project1, }, ], }); expect(bundle.entry?.length).toStrictEqual(1); expect(bundleContains(bundle as Bundle, patient1 as Patient)).toBeDefined(); expect(bundleContains(bundle as Bundle, patient2 as Patient)).toBeUndefined(); const missingBundle = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: idValue, }, { code: '_project', operator: Operator.MISSING, value: 'true', }, ], }); expect(missingBundle.entry?.length).toStrictEqual(1); expect(bundleContains(missingBundle, patient3)).toBeDefined(); const presentBundle = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value: idValue, }, { code: '_project', operator: Operator.PRESENT, value: 'true', }, ], }); expect(presentBundle.entry?.length).toStrictEqual(2); expect(bundleContains(presentBundle, patient1)).toBeDefined(); expect(bundleContains(presentBundle, patient2)).toBeDefined(); })); test('Filter by _lastUpdated', () => withTestContext(async () => { // Create 2 patients // One with a _lastUpdated of 1 second ago // One with a _lastUpdated of 2 seconds ago const family = randomUUID(); const now = new Date(); const nowMinus1Second = new Date(now.getTime() - 1000); const nowMinus2Seconds = new Date(now.getTime() - 2000); const nowMinus3Seconds = new Date(now.getTime() - 3000); const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family }], meta: { lastUpdated: nowMinus1Second.toISOString(), }, }); expect(patient1).toBeDefined(); const patient2 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family }], meta: { lastUpdated: nowMinus2Seconds.toISOString(), }, }); expect(patient2).toBeDefined(); // Greater than (newer than) 2 seconds ago should only return patient 1 const searchResult1 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'name', operator: Operator.EQUALS, value: family, }, { code: '_lastUpdated', operator: Operator.GREATER_THAN, value: nowMinus2Seconds.toISOString(), }, ], }); expect(bundleContains(searchResult1 as Bundle, patient1 as Patient)).toBeDefined(); expect(bundleContains(searchResult1 as Bundle, patient2 as Patient)).toBeUndefined(); // Greater than (newer than) or equal to 2 seconds ago should return both patients const searchResult2 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'name', operator: Operator.EQUALS, value: family, }, { code: '_lastUpdated', operator: Operator.GREATER_THAN_OR_EQUALS, value: nowMinus2Seconds.toISOString(), }, ], }); expect(bundleContains(searchResult2 as Bundle, patient1 as Patient)).toBeDefined(); expect(bundleContains(searchResult2 as Bundle, patient2 as Patient)).toBeDefined(); // Less than (older than) to 1 seconds ago should only return patient 2 const searchResult3 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'name', operator: Operator.EQUALS, value: family, }, { code: '_lastUpdated', operator: Operator.GREATER_THAN, value: nowMinus3Seconds.toISOString(), }, { code: '_lastUpdated', operator: Operator.LESS_THAN, value: nowMinus1Second.toISOString(), }, ], }); expect(bundleContains(searchResult3 as Bundle, patient1 as Patient)).toBeUndefined(); expect(bundleContains(searchResult3 as Bundle, patient2 as Patient)).toBeDefined(); // Less than (older than) or equal to 1 seconds ago should return both patients const searchResult4 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: 'name', operator: Operator.EQUALS, value: family, }, { code: '_lastUpdated', operator: Operator.GREATER_THAN, value: nowMinus3Seconds.toISOString(), }, { code: '_lastUpdated', operator: Operator.LESS_THAN_OR_EQUALS, value: nowMinus1Second.toISOString(), }, ], }); expect(bundleContains(searchResult4 as Bundle, patient1 as Patient)).toBeDefined(); expect(bundleContains(searchResult4 as Bundle, patient2 as Patient)).toBeDefined(); })); test('Sort by _lastUpdated', () => withTestContext(async () => { const project = (await systemRepo.createResource<Project>({ resourceType: 'Project' })).id; const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice1'], family: 'Smith1' }], meta: { lastUpdated: '2020-01-01T00:00:00.000Z', project, }, }); expect(patient1).toBeDefined(); const patient2 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice2'], family: 'Smith2' }], meta: { lastUpdated: '2020-01-02T00:00:00.000Z', project, }, }); expect(patient2).toBeDefined(); const bundle3 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: '_project', operator: Operator.EQUALS, value: project, }, ], sortRules: [ { code: '_lastUpdated', descending: false, }, ], }); expect(bundle3.entry?.length).toStrictEqual(2); expect(bundle3.entry?.[0]?.resource?.id).toStrictEqual(patient1.id); expect(bundle3.entry?.[1]?.resource?.id).toStrictEqual(patient2.id); const bundle4 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: '_project', operator: Operator.EQUALS, value: project, }, ], sortRules: [ { code: '_lastUpdated', descending: true, }, ], }); expect(bundle4.entry?.length).toStrictEqual(2); expect(bundle4.entry?.[0]?.resource?.id).toStrictEqual(patient2.id); expect(bundle4.entry?.[1]?.resource?.id).toStrictEqual(patient1.id); })); test('Should calculate count with join', () => withTestContext(async () => { const type = randomUUID(); const searchRequest = parseSearchRequest(`PractitionerRole?_total=accurate&organization.type=${type}`); await expect(systemRepo.search(searchRequest)).resolves.toMatchObject<Partial<Bundle>>({ type: 'searchset', total: 0, }); })); test('maxResourceVersion', () => withTestContext(async () => { const project: string = (await systemRepo.createResource<Project>({ resourceType: 'Project' })).id; const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', meta: { project }, }); const patient2 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', meta: { project }, }); const getVersionQuery = (id: string): SelectQuery => new SelectQuery('Patient').column('__version').where('id', '=', id); // patient1 at OLDER_VERSION, patient2 at Repository.VERSION const client = systemRepo.getDatabaseClient(DatabaseMode.WRITER); const OLDER_VERSION = Repository.VERSION - 1; await client.query('UPDATE "Patient" SET __version = $1 WHERE id = $2', [OLDER_VERSION, patient1.id]); expect((await getVersionQuery(patient1.id).execute(client))[0].__version).toStrictEqual(OLDER_VERSION); expect((await getVersionQuery(patient2.id).execute(client))[0].__version).toStrictEqual(Repository.VERSION); // without maxResourceVersion, both patients are returned const bundle1 = await systemRepo.search({ resourceType: 'Patient', filters: [ { code: '_project', operator: Operator.EQUALS, value: project, }, ], }); expect(bundle1.entry?.length).toStrictEqual(2); const ids = bundle1.entry?.map((e) => e.resource?.id); expect(ids).toContain(patient1.id); expect(ids).toContain(patient2.id); // with maxResourceVersion: OLDER_VERSION, expect only the outdated patient1 const bundle2 = await systemRepo.search( { resourceType: 'Patient', filters: [ { code: '_project', operator: Operator.EQUALS, value: project, }, ], }, { maxResourceVersion: OLDER_VERSION } ); expect(bundle2.entry?.length).toStrictEqual(1); expect(bundle2.entry?.[0]?.resource?.id).toStrictEqual(patient1.id); })); test('Search for deleted resources', () => withTestContext(async () => { const { repo } = await createTestProject({ withRepo: true }); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], }); await repo.deleteResource<Patient>('Patient', patient.id); const searchResult1 = await repo.search({ resourceType: 'Patient', }); expect(searchResult1.entry?.length).toBe(0); const searchResult2 = await repo.search({ resourceType: 'Patient', filters: [ { code: '_deleted', operator: Operator.EQUALS, value: 'true', }, ], }); expect(searchResult2.entry?.length).toBe(1); })); test('Reverse chained search with _filter', () => withTestContext(async () => { const { repo } = await createTestProject({ withRepo: true }); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], }); expect(patient).toBeDefined(); const obs = await repo.createResource<Observation>({ resourceType: 'Observation', status: 'final', subject: createReference(patient), code: { coding: [{ system: 'http://loinc.org', code: '3141-9' }] }, valueDateTime: '2020-01-02T03:04:05Z', }); expect(obs).toBeDefined(); const result1 = await repo.search( parseSearchRequest( `Patient?_has:Observation:subject:_filter=${encodeURIComponent('code eq "3141-9" and value-date eq "2020-01-02T03:04:05Z"')}` ) ); expect(result1.entry?.[0]?.resource?.id).toStrictEqual(patient.id); const result2 = await repo.search( parseSearchRequest(`Patient?_has:Observation:subject:_filter=${encodeURIComponent('code eq "xyz"')}`) ); expect(result2.entry?.length).toStrictEqual(0); })); describe('invalid operators and modifiers', () => { test.each([ // special search params ['Patient?_id:in=123', 'Invalid modifier'], ['Patient?_id:not-in=123', 'Invalid modifier'], ['Patient?_lastUpdated:in=2025-10-15', 'Invalid modifier'], ['Patient?_lastUpdated:not-in=2025-10-15', 'Invalid modifier'], ['Patient?_deleted:in=true', 'Invalid modifier'], ['Patient?_deleted:not-in=true', 'Invalid modifier'], ['Patient?_project:in=123', 'Invalid modifier'], ['Patient?_project:not-in=123', 'Invalid modifier'], ['Patient?_compartment:in=123', 'Invalid modifier'], ['Patient?_compartment:not-in=123', 'Invalid modifier'], // boolean ['Patient?active:in=true', 'Invalid modifier'], ['Patient?active:not-in=true', 'Invalid modifier'], // reference ['Patient?general-practitioner:in=123', 'Invalid modifier'], ['Patient?general-practitioner:not-in=123', 'Invalid modifier'], // token column ['Patient?identifier:in=123', 'Invalid modifier'], ['Patient?identifier:not-in=123', 'Invalid modifier'], // lookup table ['Patient?name:in=123', 'Invalid modifier'], ['Patient?name:not-in=123', 'Invalid modifier'], ])(':in and :not-in for %s', (searchString, expectedError) => withTestContext(async () => { await expect(async () => { const searchRequest = parseSearchRequest(searchString); await systemRepo.search(searchRequest); }).rejects.toThrow(expectedError); }) ); }); });

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