Skip to main content
Glama
epic-query-patient.ts9.08 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { createReference, getIdentifier, getReferenceString, MedplumClient, resolveId } from '@medplum/core'; import type { BotEvent } from '@medplum/core'; import type { Patient } from '@medplum/fhirtypes'; import { createPrivateKey, randomBytes } from 'crypto'; import { SignJWT } from 'jose'; import fetch from 'node-fetch'; /** * Handles the logic for the Epic FHIR integration bot. * * @param medplum - The Medplum client * @param event - The BotEvent object * * @returns The Patient resource in Medplum */ export async function handler(medplum: MedplumClient, event: BotEvent<Patient>): Promise<Patient> { const clientId = event.secrets['EPIC_CLIENT_ID']?.valueString; if (!clientId) { throw new Error('Missing EPIC_CLIENT_ID'); } // Handles unknown issue with newlines in private key const privateKeyString = event.secrets['EPIC_PRIVATE_KEY']?.valueString?.replaceAll('\\n', '\n'); if (!privateKeyString) { throw new Error('Missing EPIC_PRIVATE_KEY'); } // TODO: Add a try/catch block const privateKey = createPrivateKey(privateKeyString); const baseUrl = 'https://fhir.epic.com/interconnect-fhir-oauth/'; const tokenUrl = 'https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token'; const fhirUrlPath = 'api/FHIR/R4/'; // Construct Epic MedplumClient base const epic = new MedplumClient({ fetch, baseUrl, tokenUrl, fhirUrlPath, clientId, }); // Construct JWT assertion const jwt = await new SignJWT({}) .setProtectedHeader({ alg: 'RS384', typ: 'JWT' }) .setIssuer(clientId) .setSubject(clientId) .setAudience(tokenUrl) .setJti(randomBytes(16).toString('hex')) .setIssuedAt() .setExpirationTime('5m') .sign(privateKey); // Start the JWT assertion login await epic.startJwtAssertionLogin(jwt); const medplumPatient = event.input; const epicPatientId = getIdentifier(medplumPatient, 'http://open.epic.com/FHIR/StructureDefinition/patient-fhir-id'); if (!epicPatientId) { console.log('No Epic Patient ID found, creating patient in Epic'); // If no Epic patient ID exists, create the patient in Epic return createEpicPatient(medplum, epic, medplumPatient); } else { console.log('Epic Patient ID found, syncing data from Epic to Medplum'); // If an Epic patient ID exists, sync data from Epic to Medplum return syncEpicPatient(medplum, epic, medplumPatient, epicPatientId); } } /** * Creates a patient in Epic * * @param medplum - The Medplum client * @param epic - The Epic client * @param medplumPatient - The Patient resource in Medplum * * @returns The Patient resource in Medplum */ async function createEpicPatient( medplum: MedplumClient, epic: MedplumClient, medplumPatient: Patient ): Promise<Patient> { const ssnIdentifier = getIdentifier(medplumPatient, 'http://hl7.org/fhir/sid/us-ssn'); if (!ssnIdentifier) { throw new Error('SSN identifier is required to create a patient in Epic'); } // Destructure to omit id and meta before creating the resource in Epic const { id: _id, meta: _meta, ...patientToCreate } = medplumPatient; // Epic requires the SSN identifier to be in the OID system patientToCreate.identifier = patientToCreate.identifier?.map((id) => { if (id.system === 'http://hl7.org/fhir/sid/us-ssn') { return { use: 'usual', system: 'urn:oid:2.16.840.1.113883.4.1', value: id.value, }; } return id; }); await epic.createResource<Patient>(patientToCreate); const epicPatient = await epic.searchOne('Patient', { identifier: ssnIdentifier }); if (!epicPatient) { throw new Error('Failed to create patient in Epic'); } medplumPatient.identifier ??= []; medplumPatient.identifier.push({ system: 'http://open.epic.com/FHIR/StructureDefinition/patient-fhir-id', value: epicPatient.id, }); const updatedMedplumPatient = await medplum.updateResource(medplumPatient); return updatedMedplumPatient; } /** * Syncs data from an existing Epic patient to Medplum * * @param medplum - The Medplum client * @param epic - The Epic client * @param medplumPatient - The Patient resource in Medplum * @param epicPatientId - The ID of the patient in Epic * * @returns The Patient resource in Medplum */ async function syncEpicPatient( medplum: MedplumClient, epic: MedplumClient, medplumPatient: Patient, epicPatientId: string ): Promise<Patient> { // Note that Epic ID and Medplum ID are different so that is why id is being removed // when performing the upsert below. // Read the patient resource from Epic const epicPatient = await epic.readResource('Patient', epicPatientId); // Upsert referenced resources first to ensure they exist in Medplum. if (epicPatient.managingOrganization?.reference) { const [orgResourceType, orgId] = epicPatient.managingOrganization.reference.split('/'); if (orgResourceType === 'Organization' && orgId) { const epicOrganization = await epic.readResource('Organization', orgId); const npiIdentifier = getIdentifier(epicOrganization, 'http://hl7.org/fhir/sid/us-npi'); const medplumOrganization = await medplum.upsertResource( { ...epicOrganization, id: undefined }, { identifier: npiIdentifier, } ); epicPatient.managingOrganization = createReference(medplumOrganization); } } if (epicPatient.generalPractitioner) { await Promise.all( epicPatient.generalPractitioner.map(async (gp) => { if (!gp?.reference) { return; } const epicPractitioner = await epic.readResource('Practitioner', resolveId(gp) as string); const npiIdentifier = getIdentifier(epicPractitioner, 'http://hl7.org/fhir/sid/us-npi'); const medplumPractitioner = await medplum.upsertResource( { ...epicPractitioner, id: undefined }, { identifier: npiIdentifier } ); gp.reference = getReferenceString(medplumPractitioner); }) ); } // Update the patient resource in Medplum with the Epic patient data const updatedMedplumPatient = await medplum.updateResource({ ...epicPatient, ...medplumPatient }); // Create resources that relate to the Patient Profile (e.g. allergies, medications) const epicAllergies = await epic.searchResources('AllergyIntolerance', { patient: epicPatientId }); await Promise.all( epicAllergies.map(async (allergyIntolerance) => { const code = allergyIntolerance.code?.coding?.[0]; if (!code) { return; } await medplum.upsertResource( { ...allergyIntolerance, id: undefined, patient: createReference(updatedMedplumPatient) }, { patient: getReferenceString(updatedMedplumPatient), code: `${code.system}|${code.code}`, } ); }) ); const epicMedicationRequests = await epic.searchResources('MedicationRequest', { patient: epicPatientId }); await Promise.all( epicMedicationRequests.map(async (medicationRequest) => { if (!medicationRequest.medicationReference?.reference) { return; } const epicMedication = await epic.readResource( 'Medication', resolveId(medicationRequest.medicationReference) as string ); const medplumMedication = await medplum.upsertResource( { ...epicMedication, id: undefined }, { identifier: epicMedication.identifier?.[0].value } ); medicationRequest.medicationReference = createReference(medplumMedication); if (medicationRequest.requester) { const epicRequester = await epic.readResource('Practitioner', resolveId(medicationRequest.requester) as string); const npiIdentifier = getIdentifier(epicRequester, 'http://hl7.org/fhir/sid/us-npi'); const medplumRequester = await medplum.upsertResource( { ...epicRequester, id: undefined }, { identifier: npiIdentifier } ); medicationRequest.requester = createReference(medplumRequester); } if (medicationRequest.recorder) { const epicRecorder = await epic.readResource('Practitioner', resolveId(medicationRequest.recorder) as string); const npiIdentifier = getIdentifier(epicRecorder, 'http://hl7.org/fhir/sid/us-npi'); const medplumRecorder = await medplum.upsertResource( { ...epicRecorder, id: undefined }, { identifier: npiIdentifier } ); medicationRequest.recorder = createReference(medplumRecorder); } await medplum.upsertResource( { ...medicationRequest, id: undefined, encounter: undefined, // To simplify the demo subject: createReference(updatedMedplumPatient), }, { patient: getReferenceString(updatedMedplumPatient), identifier: medicationRequest.identifier?.[0].value, } ); }) ); return updatedMedplumPatient; }

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