Skip to main content
Glama
resourcegraph.test.ts18.9 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { ContentType, createReference, normalizeErrorString } from '@medplum/core'; import type { ActivityDefinition, Bundle, DiagnosticReport, GraphDefinition, GraphDefinitionLink, Observation, ObservationDefinition, Organization, Patient, PlanDefinition, Questionnaire, Resource, ServiceRequest, SpecimenDefinition, } from '@medplum/fhirtypes'; import { randomUUID } from 'crypto'; import express from 'express'; import request from 'supertest'; import { initApp, shutdownApp } from '../../app'; import { loadTestConfig } from '../../config/loader'; import { initTestAuth } from '../../test.setup'; const app = express(); let defaultAccessToken: string; describe('Resource $graph', () => { beforeAll(async () => { const config = await loadTestConfig(); await initApp(app, config); defaultAccessToken = await initTestAuth({ project: { strictMode: false } }); }); afterAll(async () => { await shutdownApp(); }); test('Smoke Test', async () => { // 1. Create a GraphDefinition // 2. Create a Patient // 3. Create a ServiceRequest // 4. Run the $graph operation // 5. Verify the Bundle const graphName = 'example-smoke-test'; // 1. Create a GraphDefinition await createResource({ resourceType: 'GraphDefinition', status: 'active', name: graphName, start: 'ServiceRequest', link: [{ path: 'ServiceRequest.subject', target: [{ type: 'Patient' }] }], } as GraphDefinition); // 2. Create a Patient const patient = await createResource({ resourceType: 'Patient', name: [{ given: ['Graph'], family: 'Demo' }], } as Patient); // 3. Create a Service Request const serviceRequest = await createResource({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: createReference(patient), } as ServiceRequest); // 4. Apply the PlanDefinition to create the Task and RequestGroup const bundle = await getResourceGraph(serviceRequest, graphName); const resources = bundle.entry?.map((entry) => entry.resource); expect(resources).toHaveLength(2); expect(resources?.[0]).toMatchObject(serviceRequest); expect(resources?.[1]).toMatchObject(patient); }); describe('Error cases', () => { test('Invalid graph parameter', async () => { // Abuse the graph parameter, express will parse it as string array rather than string const graphName = 'g&graph=g'; const patient = await createResource({ resourceType: 'Patient', name: [{ given: ['Graph'], family: 'Demo' }], } as Patient); await getResourceGraph(patient, graphName, 400); }); test('Missing Graph Definition', async () => { const graphName = 'this-graph-doesnt-exist'; const patient = await createResource({ resourceType: 'Patient', name: [{ given: ['Graph'], family: 'Demo' }], } as Patient); await getResourceGraph(patient, graphName, 404); }); test('Missing Resource', async () => { const graphName = 'test-missing-resource'; await createResource<GraphDefinition>({ resourceType: 'GraphDefinition', status: 'active', name: graphName, start: 'PlanDefinition', link: [{ path: 'PlanDefinition.action.definition', target: [{ type: 'Questionnaire' }] }], }); await getResourceGraph({ resourceType: 'PlanDefinition', id: randomUUID() } as PlanDefinition, graphName, 404); }); test('Missing Target', async () => { const graphName = 'test-missing-target'; const patient = await createResource({ resourceType: 'Patient', name: [{ given: ['Graph'], family: 'Demo' }], } as Patient); await createResource<GraphDefinition>({ resourceType: 'GraphDefinition', status: 'active', name: graphName, start: 'Patient', link: [ { path: 'Patient.generalPractitioner', }, ], }); const bundle = await getResourceGraph(patient, graphName); expect(bundle.entry).toHaveLength(1); expect(bundle.entry?.[0]?.resource?.resourceType).toStrictEqual('Patient'); }); test('Malformed Target', async () => { const graphName = 'test-malformed-target'; const patient = await createResource({ resourceType: 'Patient', name: [{ given: ['Graph'], family: 'Demo' }], } as Patient); await createResource<GraphDefinition>({ resourceType: 'GraphDefinition', status: 'active', name: graphName, start: 'Patient', link: [ { target: [{ id: 'foo' }], } as GraphDefinitionLink, ], }); const outcome = await getResourceGraph(patient, graphName, 400); expect(normalizeErrorString(outcome)).toContain('Invalid link'); }); test('Invalid Start', async () => { const graphName = 'test-invalid-start'; const patient = await createResource({ resourceType: 'Patient', name: [{ given: ['Graph'], family: 'Demo' }], } as Patient); await createResource<GraphDefinition>({ resourceType: 'GraphDefinition', status: 'active', name: graphName, link: [ { path: 'Patient.generalPractitioner', target: [{ type: 'Practitioner' }], }, ], } as GraphDefinition); const outcome = await getResourceGraph(patient, graphName, 400); expect(normalizeErrorString(outcome)).toContain('Missing or incorrect `start` type'); }); test('Invalid link type', async () => { const graphName = 'test-invalid-link-type'; const patient = await createResource({ resourceType: 'Patient', name: [{ given: ['Graph'], family: 'Demo' }], } as Patient); await createResource<GraphDefinition>({ resourceType: 'GraphDefinition', status: 'active', name: graphName, start: 'Patient', link: [ { path: 'Patient.name', target: [{ type: 'Practitioner' }], }, ], }); const outcome = await getResourceGraph(patient, graphName, 400); expect(normalizeErrorString(outcome)).toContain('Invalid link path. Must return a path to a Reference type'); }); test('Missing ref', async () => { const graphName = 'test-missing-ref'; const graphDefinition = await createResource<GraphDefinition>({ resourceType: 'GraphDefinition', status: 'active', name: graphName, start: 'GraphDefinition', link: [{ target: [{ type: 'GraphDefinition', params: 'url=x' }] }], }); const outcome = await getResourceGraph(graphDefinition, graphName, 400); expect(normalizeErrorString(outcome)).toContain('Link target search params must include {ref}'); }); }); test('Canonical Link', async () => { const graphName = 'example-canonical'; await createResource({ resourceType: 'GraphDefinition', status: 'active', name: graphName, start: 'PlanDefinition', link: [{ path: 'PlanDefinition.action.definition', target: [{ type: 'Questionnaire' }] }], } as GraphDefinition); const q1 = await createResource({ resourceType: 'Questionnaire', status: 'active', name: 'Patient Registration', title: 'Patient Registration', url: 'http://example.com/PatientRegistration', } as Questionnaire); const q2 = await createResource({ resourceType: 'Questionnaire', status: 'active', name: 'Medical History', title: 'Medical History', url: 'http://example.com/MedicalHistory', } as Questionnaire); const q3 = await createResource({ resourceType: 'Questionnaire', status: 'active', name: 'Medical History', title: 'Medical History', url: 'http://example.com/MedicalHistory', } as Questionnaire); // 3. Create a PlanDefinition const planDefinition = await createResource({ resourceType: 'PlanDefinition', status: 'active', url: 'http://example.com/PlanDefinition', action: [ { definitionCanonical: 'http://example.com/PatientRegistration' }, { definitionCanonical: 'http://example.com/MedicalHistory' }, { definitionCanonical: 'http://example.com/MedicalHistory' }, ], } as PlanDefinition); // 4. Apply the PlanDefinition to create the Task and RequestGroup const bundle = await getResourceGraph(planDefinition, graphName); const resources = bundle.entry?.map((entry) => entry.resource); expect(resources).toHaveLength(4); expect(resources?.[0]).toMatchObject(planDefinition); expect(resources?.[1]).toMatchObject(q1); expect(resources?.[2]).toMatchObject(q2); expect(resources?.[3]).toMatchObject(q3); }); test('Two Levels Deep', async () => { const graphName = 'example-two-levels'; await createResource({ resourceType: 'GraphDefinition', status: 'active', name: graphName, start: 'PlanDefinition', link: [ { path: 'PlanDefinition.action.definition', target: [ { type: 'ActivityDefinition', link: [ { path: 'ActivityDefinition.observationResultRequirement', target: [{ type: 'ObservationDefinition' }], }, ], }, ], }, ], } as GraphDefinition); const obsDefs = await Promise.all( ['ACT', 'BUN', 'HEM'].map((code) => createResource<ObservationDefinition>({ resourceType: 'ObservationDefinition', code: { text: code } }) ) ); const a1 = await createResource({ resourceType: 'ActivityDefinition', status: 'active', name: 'ACT Test', title: 'ACT Test', url: 'http://example.com/ActTest', observationResultRequirement: [createReference(obsDefs[0])], } as ActivityDefinition); const a2 = await createResource({ resourceType: 'ActivityDefinition', status: 'active', name: 'BUN Panel', title: 'BUN Panel', url: 'http://example.com/BunPanel', observationResultRequirement: [createReference(obsDefs[1]), createReference(obsDefs[2])], } as ActivityDefinition); // 3. Create a PlanDefinition const planDefinition = await createResource({ resourceType: 'PlanDefinition', status: 'active', action: [ { definitionCanonical: 'http://example.com/ActTest' }, { definitionCanonical: 'http://example.com/BunPanel' }, ], } as PlanDefinition); // 4. Apply the PlanDefinition to create the Task and RequestGroup const bundle = await getResourceGraph(planDefinition, graphName); const resources = bundle.entry?.map((entry) => entry.resource); expect(resources).toHaveLength(6); expect(resources?.[0]).toMatchObject(planDefinition); expect(resources?.filter((e) => e?.resourceType === 'ActivityDefinition')).toMatchObject([a1, a2]); expect(resources?.filter((e) => e?.resourceType === 'ObservationDefinition')).toMatchObject(obsDefs); }); test('Parallel Links', async () => { const graphName = 'example-parallel-link'; await createResource({ resourceType: 'GraphDefinition', status: 'active', name: graphName, start: 'PlanDefinition', link: [ { path: 'PlanDefinition.action.definition', target: [ { type: 'ActivityDefinition', link: [ { path: 'ActivityDefinition.observationResultRequirement', target: [{ type: 'ObservationDefinition' }], }, { path: 'ActivityDefinition.specimenRequirement', target: [{ type: 'SpecimenDefinition' }], }, ], }, ], }, ], } as GraphDefinition); const obsDefs = await Promise.all( ['ACT', 'BUN', 'HEM'].map((code) => createResource<ObservationDefinition>({ resourceType: 'ObservationDefinition', code: { text: code } }) ) ); const specDefs = [ await createResource<SpecimenDefinition>({ resourceType: 'SpecimenDefinition', collection: [{ text: 'finger prick' }], }), await createResource<SpecimenDefinition>({ resourceType: 'SpecimenDefinition', collection: [{ text: 'saliva' }], }), ]; const a1 = await createResource({ resourceType: 'ActivityDefinition', status: 'active', name: 'ACT Test', title: 'ACT Test', url: 'http://example.com/ActTest-Parallel', observationResultRequirement: [createReference(obsDefs[0])], specimenRequirement: [createReference(specDefs[0])], } as ActivityDefinition); const a2 = await createResource({ resourceType: 'ActivityDefinition', status: 'active', name: 'BUN Panel', title: 'BUN Panel', url: 'http://example.com/BunPanel-Parallel', observationResultRequirement: [createReference(obsDefs[1]), createReference(obsDefs[2])], specimenRequirement: [createReference(specDefs[1])], } as ActivityDefinition); // 3. Create a PlanDefinition const planDefinition = await createResource({ resourceType: 'PlanDefinition', status: 'active', action: [{ definitionCanonical: a1.url }, { definitionCanonical: a2.url }], } as PlanDefinition); // 4. Apply the PlanDefinition to create the Task and RequestGroup const bundle = await getResourceGraph(planDefinition, graphName); const resources = bundle.entry?.map((entry) => entry.resource); expect(resources).toHaveLength(8); expect(resources?.[0]).toMatchObject(planDefinition); expect(resources?.filter((e) => e?.resourceType === 'ActivityDefinition')).toMatchObject([a1, a2]); expect(resources?.filter((e) => e?.resourceType === 'ObservationDefinition')).toMatchObject(obsDefs); expect(resources?.filter((e) => e?.resourceType === 'SpecimenDefinition')).toMatchObject(specDefs); }); test('Search Based Link', async () => { const graphName = 'example-search-based-link'; // 1. Create a GraphDefinition await createResource({ resourceType: 'GraphDefinition', status: 'active', name: graphName, start: 'ServiceRequest', link: [{ target: [{ type: 'DiagnosticReport', params: 'based-on={ref}' }], max: '*' }], } as GraphDefinition); const patient = await createResource({ resourceType: 'Patient', name: [{ given: ['Graph'], family: 'Demo' }], } as Patient); const serviceRequest = await createResource({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: createReference(patient), } as ServiceRequest); // 2. Create a DiagnosticReport const report = await createResource({ resourceType: 'DiagnosticReport', status: 'final', code: { text: 'foo' }, basedOn: [createReference(serviceRequest)], } as DiagnosticReport); // 4. Apply the PlanDefinition to create the Task and RequestGroup const bundle = await getResourceGraph(serviceRequest, graphName); const resources = bundle.entry?.map((entry) => entry.resource); expect(resources).toHaveLength(2); expect(resources?.[0]).toMatchObject(serviceRequest); expect(resources?.[1]).toMatchObject(report); }); test('Nested Search Links', async () => { const graphName = 'example-nested-search'; // 1. Create a GraphDefinition await createResource({ resourceType: 'GraphDefinition', status: 'active', name: graphName, start: 'ServiceRequest', link: [ { target: [ { type: 'Observation', params: 'based-on={ref}', link: [{ path: 'Observation.performer', target: [{ type: 'Organization' }] }], }, ], }, ], } as GraphDefinition); const patient = await createResource({ resourceType: 'Patient', name: [{ given: ['Graph'], family: 'Demo' }], } as Patient); const serviceRequest = await createResource({ resourceType: 'ServiceRequest', status: 'active', intent: 'order', subject: createReference(patient), } as ServiceRequest); const performer = await createResource({ resourceType: 'Organization', name: 'Foo Medical', } as Organization); const observations = await Promise.all( ['AAA', 'BBB', 'CCC'].map((code) => createResource<Observation>({ resourceType: 'Observation', status: 'final', code: { text: code }, performer: [createReference(performer)], basedOn: [createReference(serviceRequest)], }) ) ); // 4. Apply the PlanDefinition to create the Task and RequestGroup const bundle = await getResourceGraph(serviceRequest, graphName); const resources = bundle.entry?.map((entry) => entry.resource); expect(resources).toHaveLength(5); expect(resources?.[0]).toMatchObject(serviceRequest); expect(resources?.filter((res) => res?.resourceType === 'Observation')).toContainEqual(observations[0]); expect(resources?.filter((res) => res?.resourceType === 'Observation')).toContainEqual(observations[1]); expect(resources?.filter((res) => res?.resourceType === 'Observation')).toContainEqual(observations[2]); // All 3 observations have the same performer, so we should expect a single Organization entry expect(resources?.filter((res) => res?.resourceType === 'Organization')).toMatchObject([performer]); }); }); async function getResourceGraph( resource: Resource, graphName: string, expectedReturnCode = 200, token: string | undefined = undefined ): Promise<Bundle> { const url = `/fhir/R4/${resource?.resourceType}/${resource?.id}/$graph?graph=${graphName}`; const currentToken = token || defaultAccessToken; const res = await request(app) .get(url) .set('Authorization', 'Bearer ' + currentToken); expect(res.status).toBe(expectedReturnCode); return res.body as Bundle; } async function createResource<T extends Resource>(resource: T, token?: string): Promise<T> { const currentToken = token || defaultAccessToken; const res = await request(app) .post(`/fhir/R4/${resource.resourceType}`) .set('Authorization', 'Bearer ' + currentToken) .set('Content-Type', ContentType.FHIR_JSON) .send(resource); expect(res.status).toBe(201); return res.body; }

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