Skip to main content
Glama
cds.ts8.79 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { Coding, Patient, Practitioner, PractitionerRole, RelatedPerson, Resource, ResourceType, } from '@medplum/fhirtypes'; import type { MedplumClient } from './client'; import { generateId } from './crypto'; import type { WithId } from './utils'; import { isObject, isString, splitN } from './utils'; /** * CDS Service definition. * See {@link https://cds-hooks.hl7.org/#response | CDS Hooks Discovery} for full details. */ export interface CdsService { readonly id: string; readonly hook: string; readonly title?: string; readonly description?: string; readonly usageRequirements?: string; readonly prefetch?: Record<string, string>; } /** * CDS Discovery Response definition. * See {@link https://cds-hooks.hl7.org/#discovery | CDS Hooks Discovery} for full details. */ export interface CdsDiscoveryResponse { readonly services: CdsService[]; } /** * CDS Request definition. * See {@link https://cds-hooks.hl7.org/#http-request-1 | Calling a CDS Service} for full details. */ export interface CdsRequest { readonly hook: string; readonly hookInstance: string; readonly context: Record<string, unknown>; readonly prefetch?: Record<string, unknown>; } /** * CDS FHIR Authorization definition. */ export interface CdsFhirAuthorization { readonly access_token: string; readonly token_type: string; readonly expires_in?: number; readonly scope?: string; readonly subject?: string; } /** * CDS Request with FHIR authorization. */ export interface CdsRequestWithAuth extends CdsRequest { readonly fhirServer: string; readonly fhirAuthorization: CdsFhirAuthorization; } /** * CDS Response definition. * See {@link https://cds-hooks.hl7.org/#cds-service-response | CDS Service Response} for full details. */ export interface CdsResponse { readonly cards: CdsCard[]; readonly systemActions?: CdsAction[]; } /** * CDS Card definition. * See {@link https://cds-hooks.hl7.org/#card-attributes | CDS Cards} for full details. */ export interface CdsCard { readonly uuid?: string; readonly summary: string; readonly detail?: string; readonly indicator: 'info' | 'warning' | 'hard-stop'; readonly source?: CdsSource; readonly suggestions?: CdsSuggestion[]; readonly links?: CdsLink[]; } /** * CDS Source definition. * See {@link https://cds-hooks.hl7.org/#source | CDS Source} for full details. */ export interface CdsSource { readonly label: string; readonly url?: string; readonly icon?: string; readonly topic?: Coding; } /** * CDS Suggestion definition. * See {@link https://cds-hooks.hl7.org/#suggestion | CDS Suggestions} for full details. */ export interface CdsSuggestion { readonly label: string; readonly uuid?: string; readonly isRecommended?: boolean; readonly actions: CdsAction[]; } /** * CDS Create Action. * See {@link https://cds-hooks.hl7.org/#actions | CDS Actions} for full details. */ export interface CdsCreateAction { readonly type: 'create'; readonly description: string; readonly resource: Resource; } /** * CDS Update Action. * See {@link https://cds-hooks.hl7.org/#actions | CDS Actions} for full details. */ export interface CdsUpdateAction { readonly type: 'update'; readonly description: string; readonly resource: Resource; } /** * CDS Delete Action. * See {@link https://cds-hooks.hl7.org/#actions | CDS Actions} for full details. */ export interface CdsDeleteAction { readonly type: 'delete'; readonly description: string; readonly resourceId: string; } /** * CDS Action. * See {@link https://cds-hooks.hl7.org/#actions | CDS Actions} for full details. */ export type CdsAction = CdsCreateAction | CdsUpdateAction | CdsDeleteAction; /** * CDS Link definition. * See {@link https://cds-hooks.hl7.org/#link | CDS Link} for full details. */ export interface CdsLink { readonly label: string; readonly url: string; readonly type: 'absolute' | 'smart' | 'relative'; readonly appContext?: string; readonly autolaunchable?: boolean; } export type CdsUserResource = WithId<Patient | Practitioner | PractitionerRole | RelatedPerson>; /** * Builds a CDS request. * @param medplum - The Medplum client. * @param user - The user resource. * @param service - The CDS service definition. * @param context - The CDS request context. * @returns The fully populated CDS request. */ export async function buildCdsRequest( medplum: MedplumClient, user: CdsUserResource, service: CdsService, context: Record<string, unknown> ): Promise<CdsRequest> { return { hook: service.hook, hookInstance: generateId(), context, prefetch: await buildPrefetch(medplum, user, service, context), }; } async function buildPrefetch( medplum: MedplumClient, user: CdsUserResource, service: CdsService, context: Record<string, unknown> ): Promise<Record<string, unknown>> { if (!service.prefetch) { return {}; } const prefetch: Record<string, unknown> = {}; for (const [key, query] of Object.entries(service.prefetch)) { const result = await evaluatePrefetch(medplum, user, context, query); prefetch[key] = result ?? null; } return prefetch; } function evaluatePrefetch( medplum: MedplumClient, user: CdsUserResource, context: Record<string, unknown>, query: string ): Promise<Resource | null | undefined> { query = replaceQueryVariables(user, context, query); if (query.includes('{{')) { return Promise.resolve(null); } const referenceMatch = /^(\w+)\/([0-9a-zA-Z-_]+)$/.exec(query); if (referenceMatch) { // Read request const [resourceType, id] = referenceMatch.slice(1); return medplum.readResource(resourceType as ResourceType, id); } // Search request const [resourceType, queryString] = splitN(query, '?', 2); return medplum.search(resourceType as ResourceType, queryString); } /** * See {@link https://cds-hooks.hl7.org/#prefetch-tokens-identifying-the-user | CDS Hooks Prefetch Tokens} for full details. */ const userProfileTokens = new Set([ 'userPractitionerId', 'userPractitionerRoleId', 'userPatientId', 'userRelatedPersonId', ]); /** * Replaces prefetch query variables with values from the context or user profile. * * A prefetch token is a placeholder in a prefetch template that is *replaced by information from the hook's context* * to construct the FHIR URL used to request the prefetch data. * * Prefetch tokens MUST be delimited by `{{` and `}}`, and MUST contain only the qualified path to a hook context field * or one of the following user identifiers: `userPractitionerId`, `userPractitionerRoleId`, `userPatientId`, or `userRelatedPersonId`. * * Note that the spec says: * * Individual hooks specify which of their `context` fields can be used as prefetch tokens. * Only root-level fields with a primitive value within the `context` object are eligible to be used as prefetch tokens. * For example, `{{context.medication.id}}` is not a valid prefetch token because it attempts to access the `id` field of the `medication` field. * * Unfortunately, many CDS Hooks services do not follow this rule. Therefore, this implementation allows access to nested fields. * * @param user - The user resource. * @param context - The CDS request context. * @param query - The prefetch query template. * @returns The prefetch query with variables replaced. */ export function replaceQueryVariables(user: CdsUserResource, context: Record<string, unknown>, query: string): string { return query.replaceAll(/\{\{([^}]+)\}\}/g, (substring, varName) => { varName = varName.trim(); if (userProfileTokens.has(varName)) { return user.id; } if (varName.startsWith('context.')) { const contextPath = varName.substring('context.'.length); const value = getNestedValue(context, contextPath); if (isString(value)) { return value; } if (typeof value === 'number' || typeof value === 'boolean') { return String(value); } } // No match; return the original substring return substring; }); } /** * Safely gets a nested property value from an object using a dot-notated path. * @param obj - The object to traverse * @param path - Dot-notated path like "draftOrders.ServiceRequest.id" * @returns The value at the path, or undefined if not found */ function getNestedValue(obj: Record<string, unknown>, path: string): unknown { const parts = path.split('.'); let current: unknown = obj; for (const part of parts) { if (current && isObject(current) && part in current) { current = current[part]; } else { return undefined; } } return current; }

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