Skip to main content
Glama
sync-formulary.ts10.1 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { getCodeBySystem, normalizeErrorString } from '@medplum/core'; import type { BotEvent, MedplumClient, PatchOperation } from '@medplum/core'; import type { List, ListEntry, MedicationKnowledge } from '@medplum/fhirtypes'; import { NEUTRON_HEALTH, NEUTRON_HEALTH_TREATMENTS } from './constants'; import { handlePhotonAuth, photonGraphqlFetch } from './utils'; /** * This bot takes your formulary as an input and syncs it with your Catalog in your Photon project. It filters out all medications * that are already synced, then goes through the rest and adds them to your catalog in Photon if possible. Any medications that * were not able to be synced are added to an array that is returned to the user. * * @param medplum - Medplum Client to access your Medplum project * @param event - The Bot Event, a List of MedicationKnowledge resources * @returns An array of MedicationKnowledge resources that were not able to be synced */ export async function handler(medplum: MedplumClient, event: BotEvent<List>): Promise<MedicationKnowledge[]> { const formulary = event.input; // Get the Photon client ID and secret to ensure the user is authorized to read and write to the Photon API const photonClientId = event.secrets['PHOTON_CLIENT_ID']?.valueString; const photonClientSecret = event.secrets['PHOTON_CLIENT_SECRET']?.valueString; const photonAuthToken = await handlePhotonAuth(photonClientId, photonClientSecret); // Filter out already synced medications to avoid duplication const medications = formulary.entry?.filter((entry) => { if (entry.flag) { const synced = getCodeBySystem(entry.flag, NEUTRON_HEALTH); return !synced; } return true; }); if (!medications || medications.length === 0) { throw new Error('No medications to sync'); } // Get the catalog ID from Photon so your medicaitons can be added const catalogId = await getCatalogId(photonAuthToken); // This array will be used to store any medications that could not be synced const unaddedMedications: MedicationKnowledge[] = []; if (!catalogId) { throw new Error('No catalog found in Photon Health'); } // Loop over each medication and sync it to your Photon catalog for (const medication of medications) { // Get the full MedicationKnowledge resource and validate it const medReference = medication.item; const medicationKnowledge = await medplum.readReference(medReference); if (medicationKnowledge.resourceType !== 'MedicationKnowledge') { throw new Error('Invalid resource type in formulary'); } if (!medicationKnowledge.code) { throw new Error('Invalid MedicationKnowledge resource. No medication code provided'); } // Get the medication's RXCUI code let medicationCode = getCodeBySystem(medicationKnowledge.code, 'http://www.nlm.nih.gov/research/umls/rxnorm'); // If we cannot get the RXCUI, get the NDC code if (!medicationCode) { medicationCode = getCodeBySystem(medicationKnowledge.code, 'http://hl7.org/fhir/sid/ndc'); } // Get the medication from photon. If it is not in Photon, store it in the unadded medications array const photonMedicationId = await getPhotonMedication(photonAuthToken, medicationCode); if (!photonMedicationId) { unaddedMedications.push(medicationKnowledge); continue; } // Update the MedicationKnowledge to include the Photon treatment ID await addPhotonIdToMedicationKnowledge(photonMedicationId, medicationKnowledge, medplum); // If the medication is in Photon, sync it by adding it to your Photon catalog. await syncFormulary(catalogId, photonMedicationId, photonAuthToken); } // Update the formulary in Medplum await updateFormulary(medplum, formulary, unaddedMedications); // Return any medications that were not able to be synced return unaddedMedications; } /** * Adds the Photon treatment ID to the MedicationKnowledge as part of the medication code. It is added to the code as the * MedicationKnowledge does not have an identifier field. * * @param photonMedicationId - The Treatment ID of the medication in Photon * @param medicationKnowledge - The MedicationKnowledge resource being updated * @param medplum - Medplum Client to persist changes to the server */ export async function addPhotonIdToMedicationKnowledge( photonMedicationId: string, medicationKnowledge: MedicationKnowledge, medplum: MedplumClient ): Promise<void> { const medicationKnowledgeId = medicationKnowledge.id as string; const code = medicationKnowledge.code ?? { coding: [] }; code?.coding?.push({ system: NEUTRON_HEALTH_TREATMENTS, code: photonMedicationId }); const ops: PatchOperation[] = [ { op: 'test', path: '/meta/versionId', value: medicationKnowledge.meta?.versionId }, { op: 'add', path: '/code', value: code }, ]; try { await medplum.patchResource('MedicationKnowledge', medicationKnowledgeId, ops); } catch (err) { throw new Error(normalizeErrorString(err)); } } /** * This function goes through your formulary and updates the entries to flag them if they were synced. It skips any entries that were not able to be synced. * * @param medplum - The Medplum Client to update the formulary in your project * @param formulary - The List resource representing your formulary * @param medsToSkip - An array of MedicationKnowledge resources that should not be updated as they were not synced */ async function updateFormulary( medplum: MedplumClient, formulary: List, medsToSkip: MedicationKnowledge[] ): Promise<void> { const formularyId = formulary.id as string; const medications = formulary.entry; const updatedEntries: List['entry'] = []; if (!medications) { throw new Error('No medications in formulary.'); } for (const medication of medications) { if ( (medication.flag && getCodeBySystem(medication.flag, NEUTRON_HEALTH) === 'synced') || (await checkIfSkipped(medsToSkip, medication, medplum)) ) { updatedEntries.push(medication); } else { medication.flag = { coding: [{ system: NEUTRON_HEALTH, code: 'synced' }] }; updatedEntries.push(medication); } } try { const ops: PatchOperation[] = [{ path: '/entry', op: 'add', value: updatedEntries }]; await medplum.patchResource('List', formularyId, ops); } catch (err) { throw new Error(normalizeErrorString(err)); } } /** * This function takes a list entry from your formulary and checks it against the medications that could not be synced. It returns a boolean representing whether or not the resource should be omitted from your formulary update. * * @param medsToSkip - An array of MedicationKnowledge resources that should be skipped. * @param medicationEntry - A List entry of a MedicationKnowledge resource that is being checked to see if it should be skipped. * @param medplum - The Medplum Client used to get the full resource data so the check can be completed * @returns A boolean indicating if the given List entry should be skipped */ async function checkIfSkipped( medsToSkip: MedicationKnowledge[], medicationEntry: ListEntry, medplum: MedplumClient ): Promise<boolean> { const fullMedication = (await medplum.readReference(medicationEntry.item)) as MedicationKnowledge; const medicationCodeableConcept = fullMedication.code; if (!medicationCodeableConcept) { throw new Error('Medication has no code'); } const medicationCode = getCodeBySystem(medicationCodeableConcept, 'http://www.nlm.nih.gov/research/umls/rxnorm'); const shouldSkip = medsToSkip.find((med) => { if (!med.code) { return false; } return getCodeBySystem(med.code, 'http://www.nlm.nih.gov/research/umls/rxnorm') === medicationCode; }); return !!shouldSkip; } /** * This functiont takes a catalog and treatment ID to add a given treatment to your catalog in Photon * * @param catalogId - Your catalog's ID in Photon * @param treatmentId - The Photon ID of the treatment being synced * @param authToken - The Photon auth token allowing the bot to write to the Photon API */ async function syncFormulary(catalogId: string, treatmentId: string, authToken: string): Promise<void> { const query = ` mutation addToCatalog( $catalogId: ID!, $treatmentId: ID! ) { addToCatalog( catalogId: $catalogId, treatmentId: $treatmentId ) { id } } `; const variables = { catalogId, treatmentId }; const body = JSON.stringify({ query, variables }); await photonGraphqlFetch(body, authToken); } /** * This queries for your catalog's ID in Photon * * @param authToken - Photon auth token to authorize reading from the Photon API * @returns The id of your Photon catalog */ async function getCatalogId(authToken: string): Promise<string> { const query = ` query catalogs { catalogs { id } } `; const body = JSON.stringify({ query }); const result = await photonGraphqlFetch(body, authToken); return result.data.catalogs?.[0]?.id; } /** * This function queries Photon for a medication given an RXCUI code. * * @param authToken - Photon auth token to authorize the bot to read from the Photon API * @param code - The RXCUI code of the medication you are searching for * @returns The Photon ID of the medication with the given RXCUI code */ async function getPhotonMedication(authToken: string, code?: string): Promise<string | undefined> { if (!code) { return undefined; } const query = ` query medications( $filter: MedicationFilter, $after: ID, $first: Int ) { medications( filter: $filter, after: $after, first: $first ) { id } } `; const variables = { filter: { drug: { code } } }; const body = JSON.stringify({ query, variables }); const result = await photonGraphqlFetch(body, authToken); return result.data.medications?.[0]?.id; }

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