Skip to main content
Glama
bundle.test.ts20.3 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { Bundle, BundleEntry, DiagnosticReport, Observation, Patient, Resource, Specimen, } from '@medplum/fhirtypes'; import { convertContainedResourcesToBundle, convertToTransactionBundle, findResourceInBundle, reorderBundle, } from './bundle'; import { getDataType } from './typeschema/types'; import { deepClone, isUUID } from './utils'; let jsonFile: any; function createResourceWithReference<T extends Resource>( resourceType: T['resourceType'], fullUrl: string, referenceField?: Partial<T> ): BundleEntry { let resource = { resourceType, id: fullUrl.split(':').pop() } as T; if (referenceField) { resource = { ...resource, ...referenceField }; } return { fullUrl, resource, request: { method: 'POST', url: resourceType }, }; } describe('Bundle tests', () => { beforeEach(() => { jest .spyOn(global.Math, 'random') .mockReturnValueOnce(0.1) .mockReturnValueOnce(0.2) .mockReturnValueOnce(0.3) .mockReturnValueOnce(0.4) .mockReturnValueOnce(0.5) .mockReturnValueOnce(0.6) .mockReturnValueOnce(0.7) .mockReturnValueOnce(0.8) .mockReturnValueOnce(0.9) .mockReturnValueOnce(0.0); }); afterEach(() => { jest.spyOn(global.Math, 'random').mockRestore(); }); describe('FHIR Bundle Download', () => { beforeEach(() => { jsonFile = { entry: [ { fullUrl: 'medplum.com', resource: { meta: {}, id: '123', resourceType: 'Patient', }, }, { fullUrl: 'app.medplum.com/123', resource: { meta: { id: '123', }, id: '456', resourceType: 'Patient', }, }, ], }; }); test('create a FHIR bundle from JSON File', () => { const transactionBundle = convertToTransactionBundle(jsonFile); const firstEntry = transactionBundle.entry?.[0]; expect(firstEntry?.request?.url).toStrictEqual('Patient'); }); }); describe('convertToTransactionBundle', () => { test('reorders a 2-element bundle', () => { const inputBundle: Bundle = { resourceType: 'Bundle', type: 'transaction', entry: [ createResourceWithReference('DiagnosticReport', 'urn:uuid:3d8b6e96-6de4-48c1-b7ff-e2c26c924620', { subject: { reference: 'urn:uuid:70653c8f-95e1-4b4e-84e8-8d64c15e4a13' }, }), createResourceWithReference('Patient', 'urn:uuid:70653c8f-95e1-4b4e-84e8-8d64c15e4a13'), ], }; const reorderedBundle = convertToTransactionBundle(inputBundle); expect(reorderedBundle?.entry?.map((e) => e.resource?.resourceType)).toStrictEqual([ 'Patient', 'DiagnosticReport', ]); }); test('reorders a bundle with a cycle', () => { const inputBundle: Bundle = { resourceType: 'Bundle', type: 'transaction', entry: [ createResourceWithReference('ServiceRequest', 'urn:uuid:c3d8f926-1f10-41b5-bd20-1d3d6e1f63b5', { subject: { reference: 'urn:uuid:b3e7d3f5-f7c0-41c3-b1c2-8b39e271b2c8' }, }), createResourceWithReference('Specimen', 'urn:uuid:b3e7d3f5-f7c0-41c3-b1c2-8b39e271b2c8', { request: [{ reference: 'urn:uuid:c3d8f926-1f10-41b5-bd20-1d3d6e1f63b5' }], }), ], }; const reorderedBundle = convertToTransactionBundle(inputBundle); expect(reorderedBundle?.entry?.map((e) => e.resource?.resourceType)).toStrictEqual([ 'ServiceRequest', 'Specimen', 'ServiceRequest', 'Specimen', ]); expect(reorderedBundle?.entry?.map((e) => e.request?.method)).toStrictEqual(['POST', 'POST', 'PUT', 'PUT']); }); test('Reorders Lab bundle', () => { const inputBundle: Bundle = { resourceType: 'Bundle', type: 'transaction', entry: [ createResourceWithReference('Patient', 'urn:uuid:ca760a2b-3f5d-4c85-9087-b8b6422970a8'), createResourceWithReference('ServiceRequest', 'urn:uuid:76cdff91-2a4d-4c57-8922-2f2ea17f6756', { subject: { reference: 'urn:uuid:ca760a2b-3f5d-4c85-9087-b8b6422970a8' }, }), createResourceWithReference('DiagnosticReport', 'urn:uuid:9e1fe992-1e45-4a0e-8dae-cbb8490f449e', { subject: { reference: 'urn:uuid:ca760a2b-3f5d-4c85-9087-b8b6422970a8' }, basedOn: [{ reference: 'urn:uuid:76cdff91-2a4d-4c57-8922-2f2ea17f6756' }], result: [{ reference: 'urn:uuid:e2d7f292-1e1d-4d5c-9f3a-fae792856f71' }], }), createResourceWithReference('Observation', 'urn:uuid:e2d7f292-1e1d-4d5c-9f3a-fae792856f71', { subject: { reference: 'urn:uuid:ca760a2b-3f5d-4c85-9087-b8b6422970a8' }, }), ], }; const reorderedBundle = convertToTransactionBundle(inputBundle); expect(reorderedBundle.entry?.map((e) => e.resource?.resourceType)).toStrictEqual([ 'Patient', 'Observation', 'ServiceRequest', 'DiagnosticReport', ]); }); test('Cancer Pathology Example', () => { const bundle: Bundle = { resourceType: 'Bundle', id: 'us-pathology-content-bundle-example', type: 'collection', entry: [ { fullUrl: 'http://hl7.org/fhir/us/cancer-reporting/Specimen/adrenal-example', resource: { resourceType: 'Specimen', id: 'adrenal-example', subject: { reference: 'Patient/JoelAlexPatient' }, }, }, { fullUrl: 'http://hl7.org/fhir/us/cancer-reporting/Patient/JoelAlexPatient', resource: { resourceType: 'Patient', id: 'JoelAlexPatient', name: [ { family: 'Joel', given: ['Alex'], }, ], gender: 'male', }, }, ], }; const transaction = convertToTransactionBundle(bundle); expect(transaction.entry?.map((e) => e.resource?.resourceType)).toMatchObject(['Patient', 'Specimen']); const specimen = transaction.entry?.find((e) => e.resource?.resourceType === 'Specimen')?.resource as Specimen; expect(isUUID(specimen?.subject?.reference?.split(':')[2] ?? '')).toBeTruthy(); }); test('Ignore unrecognized references', () => { const inputBundle: Bundle = { resourceType: 'Bundle', type: 'searchset', entry: [ { fullUrl: 'https://example.com/Specimen/xyz', resource: { resourceType: 'Specimen', subject: { reference: 'Patient/xyz' }, }, }, ], }; const result = convertToTransactionBundle(inputBundle); expect(result).toMatchObject({ resourceType: 'Bundle', type: 'transaction', entry: [ { resource: { resourceType: 'Specimen', subject: { reference: 'Patient/xyz' }, }, }, ], }); }); test('Remove empty resource.meta', () => { const patient: Patient = { resourceType: 'Patient', meta: { project: '11111111-2222-3333-4444-555555555555', }, active: true, }; const inputBundle: Bundle = { resourceType: 'Bundle', type: 'searchset', entry: [ { fullUrl: 'https://example.com/Patient/00000000-0000-0000-0000-000000000000', resource: patient, }, ], }; const result = convertToTransactionBundle(inputBundle); // meta.project will be removed // so meta will be empty // and therefore should be removed expect(result?.entry?.[0]?.resource?.meta).toBeUndefined(); }); test('Preserve resource.meta', () => { const patient: Patient = { resourceType: 'Patient', meta: { account: { reference: 'Organization/33333333-3333-3333-3333-333333333333', display: 'Organization #3', }, accounts: [ { reference: 'Organization/33333333-3333-3333-3333-333333333333', display: 'Organization #3', }, ], author: { reference: 'Practitioner/22222222-2222-2222-2222-222222222222', display: 'Doctor', }, onBehalfOf: { reference: 'Practitioner/22222222-2222-2222-2222-222222222222', display: 'Doctor', }, compartment: [ { reference: 'Project/11111111-2222-3333-4444-555555555555', }, { reference: 'Patient/00000000-0000-0000-0000-000000000000', }, ], extension: [{ url: 'https://example.com/Extension/meta-1', valueBoolean: true }], id: 'some-id', lastUpdated: '2024-02-14T21:47:11.777Z', profile: ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient'], project: '11111111-2222-3333-4444-555555555555', security: [{ system: 'http://hl7.org/fhir/v3/Confidentiality', code: 'N' }], source: 'https://example.com/source', tag: [{ system: 'http://hl7.org/fhir/v3/ObservationValue', code: 'SUBSETTED' }], versionId: '55555555-5555-5555-5555-555555555555', }, active: true, }; const expected = deepClone(patient); const meta = expected.meta; if (meta === undefined) { fail('Expected meta to be defined'); } const removedKeys = ['project', 'versionId', 'lastUpdated', 'compartment', 'author']; for (const key of Object.keys(getDataType('Meta').elements)) { // make sure every possible element is defined in the test expect((meta as any)[key]).toBeDefined(); if (removedKeys.includes(key)) { delete (meta as any)[key]; } } const inputBundle: Bundle = { resourceType: 'Bundle', type: 'searchset', entry: [ { fullUrl: 'https://example.com/Patient/00000000-0000-0000-0000-000000000000', resource: patient, }, ], }; const result = convertToTransactionBundle(inputBundle); expect(result?.entry?.[0]?.resource).toStrictEqual(expected); }); }); describe('convertContainedResourcesToBundle', () => { test('Simple resource', () => { const input: Patient = { resourceType: 'Patient' }; const result = convertContainedResourcesToBundle(input); expect(result).toMatchObject({ resourceType: 'Bundle', type: 'transaction', entry: [ { fullUrl: expect.stringMatching(/^urn:uuid:[a-f0-9-]{36}$/), request: { method: 'POST', url: 'Patient' }, resource: { resourceType: 'Patient', }, }, ], }); }); test('Contained observations', () => { const input: DiagnosticReport = { resourceType: 'DiagnosticReport', status: 'final', code: { text: 'test' }, contained: [ { resourceType: 'Observation', id: '123', status: 'final', code: { text: 'test' }, hasMember: [{ reference: '#456' }], }, { resourceType: 'Observation', id: '456', status: 'final', code: { text: 'test' }, }, ], result: [{ reference: '#123' }], }; const result = convertContainedResourcesToBundle(input); expect(result).toMatchObject({ resourceType: 'Bundle', type: 'transaction', entry: [ { fullUrl: expect.stringMatching(/^urn:uuid:[a-f0-9-]{36}$/), request: { method: 'POST', url: 'Observation' }, resource: { resourceType: 'Observation', }, }, { fullUrl: expect.stringMatching(/^urn:uuid:[a-f0-9-]{36}$/), request: { method: 'POST', url: 'Observation' }, resource: { resourceType: 'Observation', hasMember: [ { reference: expect.stringMatching(/^urn:uuid:[a-f0-9-]{36}$/), }, ], }, }, { fullUrl: expect.stringMatching(/^urn:uuid:[a-f0-9-]{36}$/), request: { method: 'POST', url: 'DiagnosticReport' }, resource: { resourceType: 'DiagnosticReport', result: [ { reference: expect.stringMatching(/^urn:uuid:[a-f0-9-]{36}$/), }, ], }, }, ], }); }); }); describe('reorderBundle', () => { test('Preserves entries without fullUrl and appends them at the end', () => { const inputBundle: Bundle = { resourceType: 'Bundle', type: 'transaction', entry: [ // Entry with fullUrl that references Patient (should be reordered) createResourceWithReference('DiagnosticReport', 'urn:uuid:3d8b6e96-6de4-48c1-b7ff-e2c26c924620', { subject: { reference: 'urn:uuid:70653c8f-95e1-4b4e-84e8-8d64c15e4a13' }, }), // Entry with fullUrl (should come first after reordering) createResourceWithReference('Patient', 'urn:uuid:70653c8f-95e1-4b4e-84e8-8d64c15e4a13'), // Entry without fullUrl (PATCH operation - should be preserved and appended at end) { request: { method: 'PATCH', url: 'Patient/70653c8f-95e1-4b4e-84e8-8d64c15e4a13', }, resource: { resourceType: 'Binary', contentType: 'application/json-patch+json', data: 'W3sib3AiOiJyZXBsYWNlIiwicGF0aCI6Ii9hY3RpdmUiLCJ2YWx1ZSI6dHJ1ZX1d', }, }, // Another entry without fullUrl (should also be preserved) { request: { method: 'PATCH', url: 'Observation/123', }, resource: { resourceType: 'Binary', contentType: 'application/json-patch+json', data: 'W3sib3AiOiJhZGQiLCJwYXRoIjoiL3N0YXR1cyIsInZhbHVlIjoiZmluYWwifV0=', }, }, { fullUrl: 'urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890', request: { method: 'POST', url: 'Observation' }, resource: { resourceType: 'Observation', status: 'final', code: { text: 'test' }, subject: { reference: 'urn:uuid:70653c8f-95e1-4b4e-84e8-8d64c15e4a13' }, derivedFrom: [{ reference: 'urn:uuid:3d8b6e96-6de4-48c1-b7ff-e2c26c924620' }], }, }, ], }; const reorderedBundle = reorderBundle(inputBundle); // Entries with fullUrl should be reordered (Patient before DiagnosticReport) expect(reorderedBundle?.entry?.length).toBe(5); expect(reorderedBundle?.entry?.[0]?.resource?.resourceType).toBe('Patient'); expect(reorderedBundle?.entry?.[0]?.fullUrl).toBe('urn:uuid:70653c8f-95e1-4b4e-84e8-8d64c15e4a13'); expect(reorderedBundle?.entry?.[1]?.resource?.resourceType).toBe('DiagnosticReport'); expect(reorderedBundle?.entry?.[1]?.fullUrl).toBe('urn:uuid:3d8b6e96-6de4-48c1-b7ff-e2c26c924620'); expect(reorderedBundle?.entry?.[2]?.resource?.resourceType).toBe('Observation'); expect(reorderedBundle?.entry?.[2]?.fullUrl).toBe('urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890'); // Entries without fullUrl should be preserved and appended at the end expect(reorderedBundle?.entry?.[3]?.fullUrl).toBeUndefined(); expect(reorderedBundle?.entry?.[3]?.request?.method).toBe('PATCH'); expect(reorderedBundle?.entry?.[3]?.request?.url).toBe('Patient/70653c8f-95e1-4b4e-84e8-8d64c15e4a13'); expect(reorderedBundle?.entry?.[3]?.resource?.resourceType).toBe('Binary'); expect(reorderedBundle?.entry?.[4]?.fullUrl).toBeUndefined(); expect(reorderedBundle?.entry?.[4]?.request?.method).toBe('PATCH'); expect(reorderedBundle?.entry?.[4]?.request?.url).toBe('Observation/123'); expect(reorderedBundle?.entry?.[4]?.resource?.resourceType).toBe('Binary'); }); }); describe('findResourceById', () => { test('Single resource', () => { const patient1: Patient = { resourceType: 'Patient', id: '123', name: [{ given: ['John'], family: 'Doe' }], }; const bundle: Bundle = { resourceType: 'Bundle', type: 'transaction', entry: [{ resource: patient1 }], }; const result = findResourceInBundle(bundle, 'Patient', '123'); expect(result).toMatchObject({ resourceType: 'Patient', id: '123', }); }); test('No matches', () => { const patient1: Patient = { resourceType: 'Patient', id: '123', name: [{ given: ['John'], family: 'Doe' }], }; const bundle: Bundle = { resourceType: 'Bundle', type: 'transaction', entry: [{ resource: patient1 }], }; const result = findResourceInBundle(bundle, 'Observation', '123'); expect(result).toBeUndefined(); }); test('Two resources, same type', () => { const patient1: Patient = { resourceType: 'Patient', id: '123', name: [{ given: ['John'], family: 'Doe' }], }; const patient2: Patient = { resourceType: 'Patient', id: '456', name: [{ given: ['Jane'], family: 'Doe' }], }; const bundle: Bundle = { resourceType: 'Bundle', type: 'transaction', entry: [{ resource: patient1 }, { resource: patient2 }], }; const result = findResourceInBundle(bundle, 'Patient', '123'); expect(result).toMatchObject({ resourceType: 'Patient', id: '123', }); }); test('Two resources, different types', () => { const patient1: Patient = { resourceType: 'Patient', id: '123', name: [{ given: ['John'], family: 'Doe' }], }; const obs: Observation = { resourceType: 'Observation', status: 'preliminary', code: { coding: [ { system: 'http://loinc.org', code: '9372-2', display: 'test title', }, ], text: 'test title', }, }; const bundle: Bundle = { resourceType: 'Bundle', type: 'transaction', entry: [{ resource: patient1 }, { resource: obs }], }; const result = findResourceInBundle(bundle, 'Patient', '123'); expect(result).toMatchObject({ resourceType: 'Patient', id: '123', }); }); test('Two resources, different types, same id', () => { const patient1: Patient = { resourceType: 'Patient', id: '123', name: [{ given: ['John'], family: 'Doe' }], }; const obs: Observation = { resourceType: 'Observation', id: '123', status: 'preliminary', code: { coding: [ { system: 'http://loinc.org', code: '9372-2', display: 'test title', }, ], text: 'test title', }, }; const bundle: Bundle = { resourceType: 'Bundle', type: 'transaction', entry: [{ resource: patient1 }, { resource: obs }], }; const result = findResourceInBundle(bundle, 'Patient', '123'); expect(result).toMatchObject({ resourceType: 'Patient', id: '123', }); }); }); });

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