Skip to main content
Glama
observationUtils.ts14.1 kB
import { medplum, ensureAuthenticated } from '../config/medplumClient'; import { Observation, Patient, Practitioner, Encounter, Reference, Identifier, CodeableConcept, Period, Quantity, Ratio, SampledData, Attachment, Range, ObservationReferenceRange } from '@medplum/fhirtypes'; // Interface for creating an Observation export interface CreateObservationArgs { status: Observation['status']; code: CodeableConcept; subject?: Reference<Patient>; // Made optional since tests use subjectId subjectId?: string; // For convenience in tests - will be converted to subject reference encounter?: Reference<Encounter>; encounterId?: string; // For convenience in tests - will be converted to encounter reference effectiveDateTime?: string; effectivePeriod?: Period; issued?: string; performer?: Reference<Practitioner>[]; performerIds?: string[]; // For convenience in tests - will be converted to performer references valueQuantity?: Quantity; valueString?: string; valueBoolean?: boolean; valueCodeableConcept?: CodeableConcept; valueInteger?: number; valueRange?: Range; valueRatio?: Ratio; valueSampledData?: SampledData; valueTime?: string; valueDateTime?: string; valuePeriod?: Period; bodySite?: CodeableConcept; method?: CodeableConcept; component?: any[]; interpretation?: CodeableConcept[]; note?: string; referenceRange?: any[]; identifier?: { system?: string; value: string }; } // Interface for retrieving an Observation by ID export interface GetObservationByIdArgs { observationId: string; } // Interface for updating an Observation export interface UpdateObservationArgs { status?: Observation['status']; code?: CodeableConcept; subjectId?: string; encounterId?: string | null; effectiveDateTime?: string | null; effectivePeriod?: Period | null; issued?: string; performerIds?: string[] | null; valueQuantity?: Quantity; valueCodeableConcept?: CodeableConcept; valueString?: string; valueBoolean?: boolean; valueInteger?: number; valueRange?: Range; valueRatio?: Ratio; valueSampledData?: SampledData; valueTime?: string; valueDateTime?: string; valuePeriod?: Period; bodySite?: CodeableConcept | null; method?: CodeableConcept | null; referenceRange?: any[] | null; note?: string | null; interpretation?: CodeableConcept[] | null; identifier?: { system?: string; value: string } | null; } // Interface for searching Observations export interface ObservationSearchArgs { patientId?: string; code?: string; // Search by LOINC or other code system code codeSystem?: string; // Specify the system for the code encounterId?: string; date?: string; // Search by effectiveDateTime or effectivePeriod status?: Observation['status']; subject?: string; // FHIR style search param: Patient/XYZ performer?: string; // FHIR style search param: Practitioner/XYZ identifier?: string; // Search by identifier value (e.g. value or system|value) _lastUpdated?: string; // Search by last updated date } /** * Creates a new Observation resource. * @param args - The arguments for creating the observation. * @returns The created Observation resource. */ export async function createObservation(args: CreateObservationArgs): Promise<Observation> { await ensureAuthenticated(); // Handle subjectId conversion for convenience let subject = args.subject; if (args.subjectId && !subject) { subject = { reference: `Patient/${args.subjectId}` }; } // Handle encounterId conversion for convenience let encounter = args.encounter; if (args.encounterId && !encounter) { encounter = { reference: `Encounter/${args.encounterId}` }; } // Handle performerIds conversion for convenience let performer = args.performer; if (args.performerIds && !performer) { performer = args.performerIds.map(id => ({ reference: `Practitioner/${id}` })); } if (!subject?.reference) { throw new Error('Patient reference is required to create an observation.'); } if (!args.code || !args.code.coding || args.code.coding.length === 0) { throw new Error('Observation code with at least one coding is required.'); } if (!args.status) { throw new Error('Observation status is required.'); } if ( args.valueQuantity === undefined && args.valueCodeableConcept === undefined && args.valueString === undefined && args.valueBoolean === undefined && args.valueInteger === undefined && args.valueRange === undefined && args.valueRatio === undefined && args.valueSampledData === undefined && args.valueTime === undefined && args.valueDateTime === undefined && args.valuePeriod === undefined ) { throw new Error('At least one value field must be provided (valueQuantity, valueCodeableConcept, valueString, valueBoolean, valueInteger, valueRange, valueRatio, valueSampledData, valueTime, valueDateTime, or valuePeriod).'); } const observationResource: Observation = { resourceType: 'Observation', status: args.status, code: args.code, subject: subject, encounter: encounter, effectiveDateTime: args.effectiveDateTime, effectivePeriod: args.effectivePeriod, issued: args.issued || new Date().toISOString(), performer: performer, valueQuantity: args.valueQuantity, valueCodeableConcept: args.valueCodeableConcept, valueString: args.valueString, valueBoolean: args.valueBoolean, valueInteger: args.valueInteger, valueRange: args.valueRange, valueRatio: args.valueRatio, valueSampledData: args.valueSampledData, valueTime: args.valueTime, valueDateTime: args.valueDateTime, valuePeriod: args.valuePeriod, bodySite: args.bodySite, method: args.method, referenceRange: args.referenceRange, note: args.note ? [{ text: args.note }] : undefined, interpretation: args.interpretation, identifier: args.identifier ? [{ system: args.identifier.system, value: args.identifier.value }] : undefined, }; if (args.component) { observationResource.component = args.component; } return medplum.createResource<Observation>(observationResource); } /** * Retrieves an Observation by its ID. * @param args - The arguments containing the observation ID. * @returns The Observation resource, or null if not found. */ export async function getObservationById(args: GetObservationByIdArgs): Promise<Observation | null> { await ensureAuthenticated(); if (!args.observationId) { throw new Error('Observation ID is required to fetch an observation.'); } try { return await medplum.readResource('Observation', args.observationId); } catch (error: any) { if (error.outcome?.issue?.[0]?.code === 'not-found') { return null; } throw error; } } /** * Updates an existing Observation resource. * @param observationId - The ID of the observation to update. * @param updates - An object containing the fields to update. * @returns The updated Observation resource. */ export async function updateObservation(observationId: string, updates: UpdateObservationArgs): Promise<Observation> { await ensureAuthenticated(); if (!observationId) { throw new Error('Observation ID is required to update an observation.'); } if (!updates || Object.keys(updates).length === 0) { throw new Error('Updates object cannot be empty for updating an observation.'); } const existingObservation = await medplum.readResource('Observation', observationId); if (!existingObservation) { throw new Error(`Observation with ID ${observationId} not found.`); } const { note: noteInput, identifier: identifierInput, encounterId: encounterIdInput, performerIds: performerIdsInput, subjectId: subjectIdInput, // Though less common to update subject, handle if passed ...restOfUpdates // These are fields that are mostly 1:1 or simple null->undefined } = updates; const workingUpdates: Partial<Observation> = {}; // Handle fields from restOfUpdates, converting null to undefined for (const key in restOfUpdates) { if (Object.prototype.hasOwnProperty.call(restOfUpdates, key)) { const value = (restOfUpdates as any)[key]; if (value === null) { (workingUpdates as any)[key] = undefined; } else if (value !== undefined) { (workingUpdates as any)[key] = value; } } } // Handle specific conversions for note if (typeof noteInput === 'string') { workingUpdates.note = [{ text: noteInput }]; } else if (noteInput === null) { workingUpdates.note = undefined; } else if (noteInput !== undefined) { // If it was already Annotation[] workingUpdates.note = noteInput as any; // Cast if UpdateObservationArgs not updated } // Handle specific conversions for identifier if (identifierInput && typeof identifierInput === 'object') { workingUpdates.identifier = [identifierInput as Identifier]; } else if (identifierInput === null) { workingUpdates.identifier = undefined; } // Handle subjectId to subject reference if (typeof subjectIdInput === 'string') { workingUpdates.subject = { reference: `Patient/${subjectIdInput}` }; } else if (subjectIdInput === null) { // Allow clearing subject if necessary workingUpdates.subject = undefined; } // Handle encounterId to encounter reference if (typeof encounterIdInput === 'string') { workingUpdates.encounter = { reference: `Encounter/${encounterIdInput}` }; } else if (encounterIdInput === null) { workingUpdates.encounter = undefined; // Clear the encounter } // Handle performerIds to performer references if (Array.isArray(performerIdsInput)) { workingUpdates.performer = performerIdsInput.map(id => ({ reference: `Practitioner/${id}` })); } else if (performerIdsInput === null) { workingUpdates.performer = undefined; // Clear performers } // value[x] exclusivity logic const valueFields: (keyof Observation)[] = [ 'valueQuantity', 'valueCodeableConcept', 'valueString', 'valueBoolean', 'valueInteger', 'valueRange', 'valueRatio', 'valueSampledData', 'valueTime', 'valueDateTime', 'valuePeriod' ]; let valueKeyPresentInUpdates: keyof Observation | undefined; for (const key of valueFields) { // Check if the key (potentially a value[x] field) exists in the original `updates` object if ((updates as any)[key] !== undefined) { if (valueKeyPresentInUpdates) { // This indicates multiple value[x] fields were in the input `updates`. // The test "should throw error if updating with multiple value[x] types" // expects the Medplum server to reject this. So, we don't throw here, // but let Medplum handle it if workingUpdates still contains multiple. } valueKeyPresentInUpdates = key; } } // If a value[x] is being set in updates, ensure all other value[x] fields are cleared // from workingUpdates to ensure only one is sent to Medplum. if (valueKeyPresentInUpdates) { for (const key of valueFields) { if (key !== valueKeyPresentInUpdates) { (workingUpdates as any)[key] = undefined; } } } const updatedResource: Observation = { ...existingObservation, ...workingUpdates, resourceType: 'Observation', // Ensure resourceType is correctly maintained id: observationId, // Ensure ID is correctly maintained }; return medplum.updateResource(updatedResource); } // Helper function to clear other value[x] fields when one is set function clearOtherValues(resource: Observation, valueKeyToKeep: keyof Observation) { const valueKeysToClear: (keyof Observation)[] = [ 'valueQuantity', 'valueCodeableConcept', 'valueString', 'valueBoolean', 'valueInteger', 'valueRange', 'valueRatio', 'valueSampledData', 'valueTime', 'valueDateTime', 'valuePeriod' ]; valueKeysToClear.forEach(key => { if (key !== valueKeyToKeep) { delete resource[key]; } }); } /** * Searches for Observation resources based on specified criteria. * @param args - The search criteria. * @returns An array of matching Observation resources. */ export async function searchObservations(args: ObservationSearchArgs): Promise<Observation[]> { await ensureAuthenticated(); const searchCriteria: string[] = []; if (Object.keys(args).length === 0) { console.warn('Observation search called with no specific criteria. This might return a large number of results or be inefficient.'); } if (args.patientId) { searchCriteria.push(`subject=Patient/${args.patientId}`); } else if (args.subject) { searchCriteria.push(`subject=${args.subject}`); } if (args.code) { if (args.codeSystem) { searchCriteria.push(`code=${args.codeSystem}|${args.code}`); } else { searchCriteria.push(`code=${args.code}`); } } if (args.encounterId) { searchCriteria.push(`encounter=Encounter/${args.encounterId}`); } if (args.date) { // If no comparator is provided, default to 'eq' for exact date match const dateValue = args.date.match(/^(eq|ne|gt|lt|ge|le)/) ? args.date : `eq${args.date}`; searchCriteria.push(`date=${dateValue}`); } if (args.status) { searchCriteria.push(`status=${args.status}`); } if (args.performer) { searchCriteria.push(`performer=${args.performer}`); } if (args.identifier) { searchCriteria.push(`identifier=${args.identifier}`); } if (args._lastUpdated) { searchCriteria.push(`_lastUpdated=${args._lastUpdated}`); } if (searchCriteria.length === 0 && Object.keys(args).length > 0) { console.warn('Observation search arguments provided but did not map to any known search parameters:', args); return []; } if (searchCriteria.length > 0 || Object.keys(args).length === 0) { const queryString = searchCriteria.join('&'); // console.log(`Searching observations with query: ${queryString}`); // Optional: for debugging return medplum.searchResources('Observation', queryString); } else { return []; } }

Implementation Reference

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/rkirkendall/medplum-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server