Skip to main content
Glama
structuredefinitionexpandprofile.test.ts11.9 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { ContentType, HTTP_HL7_ORG } from '@medplum/core'; import { readJson } from '@medplum/definitions'; import type { Bundle, ElementDefinition, StructureDefinition, StructureDefinitionSnapshot } from '@medplum/fhirtypes'; import express from 'express'; import request from 'supertest'; import { initApp, shutdownApp } from '../../app'; import { loadTestConfig } from '../../config/loader'; import { initTestAuth } from '../../test.setup'; jest.mock('node-fetch'); const app = express(); describe('StructureDefinition $expand-profile', () => { let USCoreStructureDefinitions: StructureDefinition[]; let accessToken: string; async function createSDs(profileUrls: string[], accessToken: string): Promise<void> { for (const profileUrl of profileUrls) { const sd = USCoreStructureDefinitions.find((sd) => sd.url === profileUrl); if (!sd) { fail(`could not find structure definition for ${profileUrl}`); } const res = await request(app) .post(`/fhir/R4/StructureDefinition`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send(sd); expect(res.status).toStrictEqual(201); } } beforeAll(async () => { USCoreStructureDefinitions = readJson('fhir/r4/testing/uscore-v5.0.1-structuredefinitions.json'); const config = await loadTestConfig(); await initApp(app, config); }); beforeEach(async () => { // A new project per test since tests are dependent on SDs being within search scope or not. accessToken = await initTestAuth(); }); afterAll(async () => { await shutdownApp(); }); test('Success with nested profiles', async () => { const profileUrl = `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-patient`; const expectedProfiles = [ profileUrl, `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-race`, `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-ethnicity`, `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-birthsex`, `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-genderIdentity`, ]; await createSDs(expectedProfiles, accessToken); const res = await request(app) .post(`/fhir/R4/StructureDefinition/$expand-profile?url=${profileUrl}`) .set('Authorization', 'Bearer ' + accessToken) .send(); expect(res.status).toStrictEqual(200); expect(res.body.resourceType).toStrictEqual('Bundle'); const bundle = res.body as Bundle<StructureDefinition>; expect(bundle.entry?.length).toStrictEqual(expectedProfiles.length); for (const entry of bundle.entry || []) { expect(expectedProfiles.includes(entry.resource?.url ?? '')).toStrictEqual(true); } }); test('Success with missing nested profiles', async () => { const profileUrl = `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-patient`; // us-core-patient references several other profiles, but they are not in the database // so we expect to only get the profiles that are const expectedProfiles = [profileUrl, `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-race`]; await createSDs(expectedProfiles, accessToken); const res = await request(app) .post(`/fhir/R4/StructureDefinition/$expand-profile?url=${profileUrl}`) .set('Authorization', 'Bearer ' + accessToken) .send(); expect(res.status).toStrictEqual(200); expect(res.body.resourceType).toStrictEqual('Bundle'); const bundle = res.body as Bundle<StructureDefinition>; expect(bundle.entry?.length).toStrictEqual(expectedProfiles.length); for (const entry of bundle.entry || []) { expect(expectedProfiles.includes(entry.resource?.url ?? '')).toStrictEqual(true); } }); test('Profile not found', async () => { const profileUrl = `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-race`; // Note that nothing is created in the database, so we expect an empty bundle const res = await request(app) .post(`/fhir/R4/StructureDefinition/$expand-profile?url=${profileUrl}`) .set('Authorization', 'Bearer ' + accessToken) .send(); expect(res.status).toStrictEqual(400); }); test('Profile URL not specified', async () => { const res = await request(app) .post(`/fhir/R4/StructureDefinition/$expand-profile`) .set('Authorization', 'Bearer ' + accessToken) .send(); expect(res.status).toStrictEqual(400); }); test('Circuit breaker for deeply nested profiles', async () => { const extensionCount = 10; const sds = await createNestedStructureDefinitions(accessToken, extensionCount); const sdUrls = sds.map((sd) => sd.url); expect(sds.length).toBe(extensionCount + 1); const profileUrl = sds[0].url; const res = await request(app) .post(`/fhir/R4/StructureDefinition/$expand-profile?url=${profileUrl}`) .set('Authorization', 'Bearer ' + accessToken) .send(); expect(res.status).toStrictEqual(200); const bundle = res.body as Bundle<StructureDefinition>; expect(bundle.entry?.length).toStrictEqual(sds.length); for (const entry of bundle.entry || []) { expect(sdUrls.includes(entry.resource?.url ?? '')).toStrictEqual(true); } }); test('Circuit breaker for too deeply nested profiles', async () => { const extensionCount = 11; const sds = await createNestedStructureDefinitions(accessToken, extensionCount); const sdUrls = sds.map((sd) => sd.url); expect(sds.length).toBe(extensionCount + 1); const profileUrl = sds[0].url; const res = await request(app) .post(`/fhir/R4/StructureDefinition/$expand-profile?url=${profileUrl}`) .set('Authorization', 'Bearer ' + accessToken) .send(); expect(res.status).toStrictEqual(200); const bundle = res.body as Bundle<StructureDefinition>; expect(bundle.entry?.length).toStrictEqual(sds.length - 1); // -1 because the last extension was not recursed due to circuit breaker for (const sdUrl of sdUrls.slice(0, -1)) { expect(bundle.entry?.some((entry) => entry.resource?.url === sdUrl)).toStrictEqual(true); } const missingSdUrl = sdUrls.at(-1); expect(bundle.entry?.some((entry) => entry.resource?.url === missingSdUrl)).toStrictEqual(false); }); }); async function createNestedStructureDefinitions( accessToken: string, extensionDepthCount: number ): Promise<StructureDefinition[]> { if (extensionDepthCount < 1) { throw new Error('extensionDepthCount must be at least one'); } const sds: StructureDefinition[] = []; const sd: StructureDefinition = { resourceType: 'StructureDefinition', id: 'deeply-nested', url: 'http://hl7.org/fhir/StructureDefinition/deeply-nested-profile', name: 'DeeplyNestedProfile', experimental: true, date: '2024-02-07', description: '', kind: 'resource', abstract: false, status: 'draft', type: 'Patient', baseDefinition: 'http://hl7.org/fhir/StructureDefinition/Patient', derivation: 'constraint', snapshot: { element: [ { id: 'Patient', path: 'Patient', definition: '\\-', min: 0, max: '*', base: { path: 'Patient', min: 0, max: '*' }, isModifier: false, isSummary: false, }, { id: 'Patient.extension', path: 'Patient.extension', definition: '\\-', slicing: { discriminator: [{ type: 'value', path: 'url' }], ordered: false, rules: 'open' }, min: 0, max: '*', base: { path: 'DomainResource.extension', min: 0, max: '*' }, type: [{ code: 'Extension' }], isModifier: false, isSummary: false, }, { id: 'Patient.extension:someExtension', path: 'Patient.extension', definition: '\\-', sliceName: 'someExtension', min: 0, max: '1', base: { path: 'DomainResource.extension', min: 0, max: '*' }, type: [{ code: 'Extension', profile: [getExtensionUrl(0)] }], }, ], }, }; sds.push(sd); for (let i = 0; i < extensionDepthCount; i++) { const extension = createExtension( `nested-extension-${i}`, getExtensionUrl(i), i === extensionDepthCount - 1 ? undefined : getExtensionUrl(i + 1) ); sds.push(extension); } for (const sd of sds) { const res = await request(app) .post(`/fhir/R4/StructureDefinition`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send(sd); expect(res.status).toStrictEqual(201); } return sds; } function getExtensionUrl(index: number): string { return `${HTTP_HL7_ORG}/fhir/StructureDefinition/deeply-nested-extension-${index}`; } function createExtension(id: string, url: string, nestedExtensionUrl: string | undefined): StructureDefinition { let nestedExtensionElement: ElementDefinition | undefined; if (nestedExtensionUrl) { nestedExtensionElement = { id: 'Extension.extension:nestedExtension', path: 'Extension.extension', definition: '\\-', sliceName: 'nestedExtension', min: 0, max: '1', base: { path: 'Extension.extension', min: 0, max: '*' }, type: [{ code: 'Extension', profile: [nestedExtensionUrl] }], mustSupport: true, isModifier: false, isSummary: false, }; } const extension: StructureDefinition & { snapshot: StructureDefinitionSnapshot } = { resourceType: 'StructureDefinition', id, url, name: 'DeeplyNestedExtension', title: 'Deeply Nested Extension', status: 'draft', description: 'An arbitrarily deeply nested extension for testing purposes', fhirVersion: '4.0.1', kind: 'complex-type', abstract: false, type: 'Extension', baseDefinition: 'http://hl7.org/fhir/StructureDefinition/Extension', derivation: 'constraint', context: [{ type: 'element', expression: 'Element' }], snapshot: { element: [ { id: 'Extension', path: 'Extension', definition: '\\-', min: 0, max: '*', base: { path: 'Extension', min: 0, max: '*' }, isModifier: false, }, { id: 'Extension.id', path: 'Extension.id', definition: '\\-', min: 0, max: '1', base: { path: 'Extension.id', min: 0, max: '*' }, type: [ { extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type', valueUrl: 'string', }, ], code: 'http://hl7.org/fhirpath/System.String', }, ], isModifier: false, isSummary: false, }, { id: 'Extension.extension', path: 'Extension.extension', definition: '\\-', slicing: { discriminator: [ { type: 'value', path: 'url', }, ], description: 'Extensions are always sliced by (at least) url', rules: 'open', }, min: 0, max: '*', base: { path: 'Extension.extension', min: 0, max: '*' }, type: [ { code: 'Extension', }, ], isModifier: false, isSummary: false, }, ], }, }; if (nestedExtensionElement) { extension.snapshot.element.push(nestedExtensionElement); } return extension; }

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