Skip to main content
Glama
ccda-converter.ts5.4 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { createReference } from '@medplum/core'; import type { BotEvent, MedplumClient } from '@medplum/core'; import type { Media, Observation, Patient } from '@medplum/fhirtypes'; import { XMLParser } from 'fast-xml-parser'; // Helper function to safely access nested properties function getElementAtPath(obj: Record<string, any>, path: string): any { return path.split('.').reduce((acc, part) => acc?.[part], obj); } function convertCcdaToFhir(xmlContent: string): { patient: Patient; observations: Observation[]; } { const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_', parseAttributeValue: true, }); const parsedData = parser.parse(xmlContent); const clinicalDoc = parsedData.ClinicalDocument; // Extract patient information const patientRole = getElementAtPath(clinicalDoc, 'recordTarget.patientRole'); const patientEle = patientRole.patient; const name = patientEle.name; const addr = patientRole.addr; const birthTime = patientEle.birthTime['@_value']?.toString(); // Create FHIR Patient resource const patient: Patient = { resourceType: 'Patient', id: patientRole.id['@_extension'], name: [ { use: (name['@_use'] || 'official').toLowerCase(), family: name.family, given: Array.isArray(name.given) ? name.given : [name.given], suffix: name.suffix ? [name.suffix] : undefined, }, ], gender: patientEle.administrativeGenderCode['@_code'] === 'M' ? 'male' : 'female', birthDate: birthTime?.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3'), address: [ { use: (addr['@_use'] || 'home').toLowerCase(), line: [addr.streetAddressLine?.trim()], city: addr.city?.trim(), state: addr.state?.trim(), postalCode: addr.postalCode.toString()?.trim(), country: addr.country?.trim(), }, ], telecom: Array.isArray(patientRole.telecom) ? patientRole.telecom.map((tel: any) => ({ system: 'phone', value: tel['@_value']?.replace('tel:', ''), use: tel['@_use']?.toLowerCase(), })) : [ { system: 'phone', value: patientRole.telecom['@_value']?.replace('tel:', ''), use: patientRole.telecom['@_use']?.toLowerCase(), }, ], }; // Find vital signs section and extract observations const components = Array.isArray(clinicalDoc.component.structuredBody.component) ? clinicalDoc.component.structuredBody.component : [clinicalDoc.component.structuredBody.component]; const vitalSignsSection = components.find( (comp: any) => getElementAtPath(comp, 'section.code.@_code') === '8716-3' )?.section; const patientRef = createReference(patient); const observations = [] as Observation[]; if (vitalSignsSection?.entry) { const components = Array.isArray(vitalSignsSection.entry.organizer.component) ? vitalSignsSection.entry.organizer.component : [vitalSignsSection.entry.organizer.component]; for (const comp of components) { const obs = comp.observation; observations.push({ resourceType: 'Observation', id: obs.id['@_extension'], status: obs.statusCode['@_code'], code: { coding: [ { system: 'http://loinc.org', code: obs.code['@_code'], display: obs.code['@_displayName'], }, ], }, valueQuantity: { value: Number.parseFloat(obs.value['@_value']), unit: obs.value['@_unit'], system: 'http://unitsofmeasure.org', }, effectiveDateTime: obs.effectiveTime['@_value']?.toString()?.replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3'), subject: patientRef, }); } } return { patient, observations, }; } export async function handler(medplum: MedplumClient, event: BotEvent<Media>): Promise<any> { const { input } = event; // If the Media resource is not a C-CDA or doesn't have a content url, do not attempt to convert it if (input.content?.contentType !== 'application/cda+xml' || !input.content.url) { return; } // We get the raw XML by downloading it from the content.url const xml = await (await medplum.download(input.content.url)).text(); // We convert the raw XML to a patient and some observations using our function we created const { patient, observations } = convertCcdaToFhir(xml); const promises = [] as Promise<any>[]; // Read the subject of this Media const subject = input.subject ? await medplum.readReference(input.subject) : undefined; // If subject is present, we should update that subject with the fields we parsed from the patient section if (subject?.resourceType === 'Patient') { promises.push(medplum.updateResource({ ...subject, ...patient, id: subject.id })); } else { // Otherwise we just create this patient net-new promises.push(medplum.createResource(patient)); } // We go through each observation and create them as well for (const observation of observations) { promises.push(medplum.createResource(observation)); } // We wait for all of our promises to resolve before exiting await Promise.allSettled(promises); }

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