Skip to main content
Glama
export.test.ts8.88 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { ContentType } from '@medplum/core'; import type { BulkDataExportOutput, Observation } from '@medplum/fhirtypes'; import express from 'express'; import request from 'supertest'; import { initApp, shutdownApp } from '../../app'; import { loadTestConfig } from '../../config/loader'; import type { FileSystemStorage } from '../../storage/filesystem'; import { getBinaryStorage } from '../../storage/loader'; import { createTestProject, initTestAuth, waitForAsyncJob, withTestContext } from '../../test.setup'; import { getSystemRepo } from '../repo'; import { exportResourceType, exportResources } from './export'; import { BulkExporter } from './utils/bulkexporter'; describe('Export', () => { const app = express(); const systemRepo = getSystemRepo(); beforeAll(async () => { const config = await loadTestConfig(); await initApp(app, config); }); afterAll(async () => { await shutdownApp(); }); test('Success', async () => { const accessToken = await initTestAuth({ membership: { admin: true } }); expect(accessToken).toBeDefined(); const res1 = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], address: [{ use: 'home', line: ['123 Main St'], city: 'Anywhere', state: 'CA', postalCode: '90210' }], telecom: [ { system: 'phone', value: '555-555-5555' }, { system: 'email', value: 'alice@example.com' }, ], }); expect(res1.status).toBe(201); // Create observation const res2 = await request(app) .post(`/fhir/R4/Observation`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Observation', status: 'final', code: { text: 'test' }, subject: { reference: `Patient/${res1.body.id}` }, }); expect(res2.status).toBe(201); // Start the export const initRes = await request(app) .post('/fhir/R4/$export') .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .set('X-Medplum', 'extended') .send({}); expect(initRes.status).toBe(202); expect(initRes.headers['content-location']).toBeDefined(); // Check the export status const contentLocation = new URL(initRes.headers['content-location']); await waitForAsyncJob(initRes.headers['content-location'], app, accessToken); const statusRes = await request(app) .get(contentLocation.pathname) .set('Authorization', 'Bearer ' + accessToken); expect(statusRes.status).toBe(200); const resBody = statusRes.body; const output = resBody?.output as BulkDataExportOutput[]; expect( Object.values(output) .map((ex) => ex.type) .sort() ).toStrictEqual(['ClientApplication', 'Observation', 'Patient', 'Project', 'ProjectMembership']); // Get the export content const outputLocation = new URL(output.find((o) => o.type === 'Observation')?.url as string); const outputContent = (getBinaryStorage() as FileSystemStorage).readFileByUrlForTests(outputLocation); // Output format is "ndjson", new line delimited JSON // However, we only expect one Observation, so we can parse it as JSON const resourceJSON = outputContent.trim().split('\n'); expect(resourceJSON).toHaveLength(1); expect(JSON.parse(resourceJSON[0])?.subject?.reference).toStrictEqual(`Patient/${res1.body.id}`); }); test('System Export Accepted with GET', async () => { const accessToken = await initTestAuth(); // Start the export const initRes = await request(app) .get('/fhir/R4/$export') .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .set('X-Medplum', 'extended') .send({}); expect(initRes.status).toBe(202); expect(initRes.headers['content-location']).toBeDefined(); await waitForAsyncJob(initRes.headers['content-location'], app, accessToken); }); test('Patient Export Accepted with GET', async () => { const accessToken = await initTestAuth(); // Start the export const initRes = await request(app) .get('/fhir/R4/Patient/$export') .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .set('X-Medplum', 'extended') .send({}); expect(initRes.status).toBe(202); expect(initRes.headers['content-location']).toBeDefined(); await waitForAsyncJob(initRes.headers['content-location'], app, accessToken); }); test('exportResourceType iterating through paginated search results', async () => withTestContext(async () => { await systemRepo.createResource<Observation>({ resourceType: 'Observation', status: 'preliminary', subject: { reference: 'Patient/123' }, code: { text: 'patient observation 1', }, }); await systemRepo.createResource<Observation>({ resourceType: 'Observation', status: 'preliminary', subject: { reference: 'Patient/123' }, code: { text: 'patient observation 2', }, }); const exporter = new BulkExporter(systemRepo); const exportWriteResourceSpy = jest.spyOn(exporter, 'writeResource'); await exporter.start('http://example.com'); const { project } = await createTestProject(); expect(project).toBeDefined(); await exportResourceType(exporter, 'Observation', 1); const bulkDataExport = await exporter.close(project); expect(bulkDataExport.status).toBe('completed'); expect(exportWriteResourceSpy).toHaveBeenCalled(); })); test('closeWriter removes only specified resource type from tracking', async () => withTestContext(async () => { const exporter = new BulkExporter(systemRepo); await exporter.start('http://example.com'); // Create and write multiple resource types const patient = await systemRepo.createResource({ resourceType: 'Patient', name: [{ given: ['Test'], family: 'Patient' }], }); const observation = await systemRepo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { text: 'test' }, subject: { reference: `Patient/${patient.id}` }, }); await exporter.writeResource(patient); await exporter.writeResource(observation); // Verify both resource types are tracked expect(exporter.resourceSets.size).toBe(2); expect(exporter.resourceSets.has('Patient')).toBe(true); expect(exporter.resourceSets.has('Observation')).toBe(true); expect(exporter.resourceSets.get('Patient')?.has(`Patient/${patient.id}`)).toBe(true); expect(exporter.resourceSets.get('Observation')?.has(`Observation/${observation.id}`)).toBe(true); // Close writer for Observation (which clears tracking for that type) await exporter.closeWriter('Observation'); // Verify only Observation was removed from tracking expect(exporter.resourceSets.size).toBe(1); expect(exporter.resourceSets.has('Patient')).toBe(true); expect(exporter.resourceSets.has('Observation')).toBe(false); expect(exporter.resourceSets.get('Patient')?.has(`Patient/${patient.id}`)).toBe(true); const { project } = await createTestProject(); await exporter.close(project); })); test('closeWriter called for each resource type during export', async () => withTestContext(async () => { // Create test resources await systemRepo.createResource({ resourceType: 'Patient', name: [{ given: ['Test'], family: 'Patient' }], }); await systemRepo.createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { text: 'test' }, }); const exporter = new BulkExporter(systemRepo); const closeWriterSpy = jest.spyOn(exporter, 'closeWriter'); await exporter.start('http://example.com'); const { project } = await createTestProject(); // Export only Patient and Observation types await exportResources(exporter, project, ['Patient', 'Observation'], 'System'); // Verify closeWriter was called for each exported resource type expect(closeWriterSpy).toHaveBeenCalledWith('Patient'); expect(closeWriterSpy).toHaveBeenCalledWith('Observation'); // Verify that tracking was cleared (resourceSets should be empty after close) expect(exporter.resourceSets.size).toBe(0); })); });

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