Skip to main content
Glama
codesystemimport.ts8.46 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { WithId } from '@medplum/core'; import { OperationOutcomeError, allOk, badRequest, forbidden, normalizeOperationOutcome } from '@medplum/core'; import type { FhirRequest, FhirResponse } from '@medplum/fhir-router'; import type { CodeSystem, CodeSystemProperty, Coding, OperationDefinition, OperationDefinitionParameter, } from '@medplum/fhirtypes'; import type { PoolClient } from 'pg'; import { getAuthenticatedContext } from '../../context'; import { Condition, InsertQuery, SelectQuery } from '../sql'; import { buildOutputParameters, parseInputParameters } from './utils/parameters'; import { findTerminologyResource, parentProperty, selectCoding, uniqueOn } from './utils/terminology'; // Helper function to satisfy code duplication rules function makeCodeAttributeParameter( paramName: string, attributeParam: Omit<OperationDefinitionParameter, 'part' | 'use'> ): OperationDefinitionParameter { return { use: 'in', name: paramName, min: 0, max: '*', part: [ { use: 'in', name: 'code', type: 'code', min: 1, max: '1' }, { use: 'in', ...attributeParam }, { use: 'in', name: 'value', type: 'string', min: 1, max: '1' }, ], }; } const operation: OperationDefinition = { resourceType: 'OperationDefinition', name: 'codesystem-import', status: 'active', kind: 'operation', code: 'import', experimental: true, resource: ['CodeSystem'], system: false, type: true, instance: false, parameter: [ { use: 'in', name: 'system', type: 'uri', min: 0, max: '1' }, { use: 'in', name: 'concept', type: 'Coding', min: 0, max: '*' }, makeCodeAttributeParameter('property', { name: 'property', type: 'code', min: 1, max: '1', }), makeCodeAttributeParameter('designation', { name: 'language', type: 'code', min: 0, max: '1', }), { use: 'out', name: 'return', type: 'CodeSystem', min: 1, max: '1' }, ], }; export type ImportedProperty = { code: string; property: string; value: string; }; export type Designation = { code: string; language?: string; value: string; }; export type CodeSystemImportParameters = { system?: string; concept?: Coding[]; property?: ImportedProperty[]; designation?: Designation[]; }; /** * Handles a request to import codes and their properties into a CodeSystem. * * Endpoint - CodeSystem resource type * [fhir base]/CodeSystem/$import * * @param req - The FHIR request. * @returns The FHIR response. */ export async function codeSystemImportHandler(req: FhirRequest): Promise<FhirResponse> { const repo = getAuthenticatedContext().repo; const isSuperAdmin = repo.isSuperAdmin(); if (!repo.isProjectAdmin() && !isSuperAdmin) { return [forbidden]; } const params = parseInputParameters<CodeSystemImportParameters>(operation, req); let codeSystem: WithId<CodeSystem>; if (req.params.id) { codeSystem = await repo.readResource<CodeSystem>('CodeSystem', req.params.id); } else if (params.system) { codeSystem = await findTerminologyResource<CodeSystem>(repo, 'CodeSystem', params.system, { ownProjectOnly: !isSuperAdmin, }); } else { return [badRequest('No code system specified')]; } try { await repo.withTransaction(async (db) => { await importCodeSystem(db, codeSystem, params.concept, params.property, params.designation); }); } catch (err) { return [normalizeOperationOutcome(err)]; } return [allOk, buildOutputParameters(operation, codeSystem)]; } export async function importCodeSystem( db: PoolClient, codeSystem: WithId<CodeSystem>, concepts?: Coding[], properties?: ImportedProperty[], designations?: Designation[] ): Promise<void> { if (concepts?.length) { const rows = uniqueOn(concepts, (c) => c.code as string).map((c) => ({ system: codeSystem.id, code: c.code, display: c.display, isSynonym: false, })); const query = new InsertQuery('Coding', rows).mergeOnConflict( ['system', 'code'], new Condition('synonymOf', '=', null) ); await query.execute(db); } if (properties?.length) { await processProperties(properties, codeSystem, db); } if (designations?.length) { const lookupCodes = new Set<string>(designations.map((d) => d.code)); // Batch lookup all Codings with associated properties const codingIds = await selectCoding(codeSystem.id, ...lookupCodes).execute(db); const synonyms: Record<string, any>[] = []; for (const designation of designations) { // Add synonym row const sourceCodingId = codingIds.find((r) => r.code === designation.code)?.id; if (!sourceCodingId) { throw new OperationOutcomeError(badRequest(`Unknown code: ${codeSystem.url}|${designation.code}`)); } synonyms.push({ system: codeSystem.id, code: designation.code, display: designation.value, isSynonym: true, synonymOf: sourceCodingId, language: designation.language, }); } const query = new InsertQuery('Coding', synonyms).ignoreOnConflict(); await query.execute(db); } } async function processProperties( importedProperties: ImportedProperty[], codeSystem: WithId<CodeSystem>, db: PoolClient ): Promise<void> { const cache: Record<string, { id: number; property: CodeSystemProperty }> = Object.create(null); const lookupCodes = new Set<string>(); for (const imported of importedProperties) { const propertyCode = imported.property; const cacheKey = codeSystem.url + '|' + propertyCode; let { id: propId, property } = cache[cacheKey] ?? {}; if (!propId) { [propId, property] = await resolveProperty(codeSystem, propertyCode, db); cache[cacheKey] = { id: propId, property }; } lookupCodes.add(imported.code); if (property.type === 'code') { lookupCodes.add(imported.value); } } // Batch lookup all Codings with associated properties const codingIds = await selectCoding(codeSystem.id, ...lookupCodes).execute(db); const rows: Record<string, any>[] = []; const synonyms: Record<string, any>[] = []; for (const imported of importedProperties) { const sourceCodingId = codingIds.find((r) => r.code === imported.code)?.id; if (!sourceCodingId) { throw new OperationOutcomeError(badRequest(`Unknown code: ${codeSystem.url}|${imported.code}`)); } const { id: propId, property } = cache[`${codeSystem.url}|${imported.property}`] ?? {}; const targetCodingId = codingIds.find((r) => r.code === imported.value)?.id; rows.push({ coding: sourceCodingId, property: propId, value: imported.value, target: property.type === 'code' && targetCodingId ? targetCodingId : null, }); if (property.uri === 'http://hl7.org/fhir/concept-properties#synonym') { synonyms.push({ system: codeSystem.id, code: imported.code, display: imported.value, isSynonym: true, synonymOf: sourceCodingId, }); } } const query = new InsertQuery('Coding_Property', rows).ignoreOnConflict(); await query.execute(db); if (synonyms.length) { const query = new InsertQuery('Coding', synonyms).ignoreOnConflict(); await query.execute(db); } } async function resolveProperty( codeSystem: CodeSystem, code: string, db: PoolClient ): Promise<[number, CodeSystemProperty]> { let prop = codeSystem.property?.find((p) => p.code === code); if (!prop) { if (code === codeSystem.hierarchyMeaning || (code === 'parent' && !codeSystem.hierarchyMeaning)) { prop = { code, uri: parentProperty, type: 'code' }; } else { throw new OperationOutcomeError(badRequest(`Unknown property: ${code}`)); } } const knownProp = ( await new SelectQuery('CodeSystem_Property') .column('id') .where('system', '=', codeSystem.id) .where('code', '=', code) .execute(db) )[0]; if (knownProp) { return [knownProp.id, prop]; } const newProp = ( await new InsertQuery('CodeSystem_Property', [ { system: codeSystem.id, code, type: prop.type, uri: prop.uri, description: prop.description, }, ]) .returnColumn('id') .execute(db) )[0]; return [newProp.id, prop]; }

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