Skip to main content
Glama
references.ts5.69 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { TypedValue } from '@medplum/core'; import { badRequest, crawlTypedValueAsync, createReference, createStructureIssue, normalizeErrorString, OperationOutcomeError, parseSearchRequest, PropertyType, toTypedValue, } from '@medplum/core'; import type { OperationOutcomeIssue, Reference, Resource } from '@medplum/fhirtypes'; import { randomUUID } from 'node:crypto'; import type { Repository } from './repo'; import { getSystemRepo } from './repo'; /** * Exceptional, system-level references that should use systemRepo for validation * * Project.owner: * `User` reference which typically does not have meta.compartment specified * and is not technically in the project and thus would not be visible through * the non-system repo. * * Project.link.project: * The synthetic Project policy from `applyProjectAdminAccessPolicy` applies * a criteria that requires Project.id matches the admin's ProjectMembership.project * reference which will always fail for linked projects. */ const SYSTEM_REFERENCE_PATHS = ['Project.owner', 'Project.link.project', 'ProjectMembership.user']; async function validateReferences( repo: Repository, references: Record<string, Reference>, issues: OperationOutcomeIssue[] ): Promise<void> { const toValidate = Object.values(references); const paths = Object.keys(references); const validated = await repo.readReferences(toValidate); for (let i = 0; i < validated.length; i++) { const reference = validated[i]; if (reference instanceof Error) { const path = paths[i]; issues.push(createStructureIssue(path, `Invalid reference (${normalizeErrorString(reference)})`)); } } } function isCheckableReference(propertyValue: TypedValue | TypedValue[]): boolean { const valueType = Array.isArray(propertyValue) ? propertyValue[0].type : propertyValue.type; return valueType === PropertyType.Reference; } function shouldValidateReference(ref: Reference): boolean { return Boolean(ref.reference && !ref.reference.startsWith('%') && !ref.reference.startsWith('#')); } export async function validateResourceReferences<T extends Resource>(repo: Repository, resource: T): Promise<void> { const references: Record<string, Reference> = Object.create(null); const systemReferences: Record<string, Reference> = Object.create(null); await crawlTypedValueAsync( toTypedValue(resource), { async visitPropertyAsync(parent, _key, path, propertyValue, _schema) { if (!isCheckableReference(propertyValue) || parent.type === PropertyType.Meta) { return; } if (Array.isArray(propertyValue)) { for (let i = 0; i < propertyValue.length; i++) { const reference = propertyValue[i].value as Reference; if (!shouldValidateReference(reference)) { continue; } if (SYSTEM_REFERENCE_PATHS.includes(path)) { systemReferences[path + '[' + i + ']'] = reference; } else { references[path + '[' + i + ']'] = reference; } } } else if (shouldValidateReference(propertyValue.value)) { const reference = propertyValue.value as Reference; if (SYSTEM_REFERENCE_PATHS.includes(path)) { systemReferences[path] = reference; } else { references[path] = reference; } } }, }, { skipMissingProperties: true } ); const issues: OperationOutcomeIssue[] = []; await validateReferences(repo, references, issues); await validateReferences(getSystemRepo(), systemReferences, issues); if (issues.length > 0) { throw new OperationOutcomeError({ resourceType: 'OperationOutcome', id: randomUUID(), issue: issues, }); } } async function resolveReplacementReference( repo: Repository, reference: Reference | undefined, path: string ): Promise<Reference | undefined> { if (!reference?.reference?.includes?.('?')) { return undefined; } const searchCriteria = parseSearchRequest(reference.reference); searchCriteria.sortRules = undefined; searchCriteria.count = 2; const matches = await repo.searchResources(searchCriteria); if (matches.length !== 1) { throw new OperationOutcomeError( badRequest( `Conditional reference '${reference.reference}' ${matches.length ? 'matched multiple' : 'did not match any'} resources`, path ) ); } return createReference(matches[0]); } export async function replaceConditionalReferences<T extends Resource>(repo: Repository, resource: T): Promise<T> { await crawlTypedValueAsync( toTypedValue(resource), { async visitPropertyAsync(parent, key, path, propertyValue, _schema) { if (!isCheckableReference(propertyValue)) { return; } if (Array.isArray(propertyValue)) { for (let i = 0; i < propertyValue.length; i++) { const reference = propertyValue[i].value as Reference; const replacement = await resolveReplacementReference(repo, reference, path + '[' + i + ']'); if (replacement) { parent.value[key][i] = replacement; } } } else { const reference = propertyValue.value as Reference; const replacement = await resolveReplacementReference(repo, reference, path); if (replacement) { parent.value[key] = replacement; } } }, }, { skipMissingProperties: true } ); return resource; }

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