Skip to main content
Glama
terminology.ts9.21 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { WithId } from '@medplum/core'; import { badRequest, createReference, OperationOutcomeError, Operator, resolveId } from '@medplum/core'; import type { CodeSystem, CodeSystemProperty, ConceptMap, Reference, ValueSet, ValueSetComposeIncludeFilter, } from '@medplum/fhirtypes'; import type { Pool, PoolClient } from 'pg'; import { r4ProjectId } from '../../../constants'; import type { Repository } from '../../repo'; import { getSystemRepo } from '../../repo'; import { Column, Condition, Conjunction, Disjunction, SelectQuery, SqlFunction, Union } from '../../sql'; export const parentProperty = 'http://hl7.org/fhir/concept-properties#parent'; export const childProperty = 'http://hl7.org/fhir/concept-properties#child'; export const abstractProperty = 'http://hl7.org/fhir/concept-properties#notSelectable'; export type TerminologyResource = CodeSystem | ValueSet | ConceptMap; export async function findTerminologyResource<T extends TerminologyResource>( repo: Repository, resourceType: T['resourceType'], url: string, options?: { version?: string; ownProjectOnly?: boolean; } ): Promise<WithId<T>> { if (!url) { throw new OperationOutcomeError(badRequest(`${resourceType} not specified`)); } const project = repo.currentProject(); const versionDelim = url.lastIndexOf('|'); if (versionDelim > 0) { url = url.slice(0, versionDelim); options = { ...options, version: options?.version ?? url.slice(versionDelim + 1) }; } const filters = [{ code: 'url', operator: Operator.EQUALS, value: url }]; if (options?.version) { filters.push({ code: 'version', operator: Operator.EQUALS, value: options.version }); } const results = await repo.searchResources<T>({ resourceType, filters, sortRules: [ // Select highest version (by lexical sort -- no version is assumed to be "current") { code: 'version', descending: true }, // Break ties by selecting more recently-updated resource (lexically -- no date is assumed to be current) { code: 'date', descending: true }, ], }); if (!results.length) { throw new OperationOutcomeError(badRequest(`${resourceType} ${url} not found`)); } else if (results.length === 1 || !sameTerminologyResourceVersion(results[0], results[1])) { if (options?.ownProjectOnly) { const fullResource = await getSystemRepo().readReference(createReference(results[0])); if (fullResource.meta?.project === repo.currentProject()?.id) { return results[0]; } } else { return results[0]; } } else { const resourceReferences: Reference<T>[] = []; for (const resource of results) { resourceReferences.push(createReference(resource)); } const resources = await getSystemRepo().readReferences(resourceReferences); const projectResource = resources.find((r) => r instanceof Error || (project && r.meta?.project === project.id)); if (projectResource instanceof Error) { throw projectResource; } else if (projectResource) { return projectResource; } if (!options?.ownProjectOnly && project?.link) { for (const linkedProject of project.link) { const linkedResource = resources.find( (r) => !(r instanceof Error) && r.meta?.project === resolveId(linkedProject.project) ) as WithId<T> | undefined; if (linkedResource) { return linkedResource; } } } const baseResource = resources.find((r) => r instanceof Error || r.meta?.project === r4ProjectId); if (baseResource instanceof Error) { throw baseResource; } else if (baseResource) { return baseResource; } } throw new OperationOutcomeError(badRequest(`${resourceType} ${url} not found`)); } function sameTerminologyResourceVersion(a: TerminologyResource, b: TerminologyResource): boolean { return a.version === b.version && a.date === b.date; } export function selectCoding(systemId: string, ...code: string[]): SelectQuery { return new SelectQuery('Coding') .column('id') .column('code') .column('display') .column('synonymOf') .column('language') .where('system', '=', systemId) .where('code', 'IN', code); } export function addPropertyFilter( query: SelectQuery, condition: ValueSetComposeIncludeFilter, property: WithId<CodeSystemProperty> ): SelectQuery { const multiValue = condition.op.endsWith('in'); const values = multiValue ? condition.value.split(',') : condition.value; const propertyQuery = new SelectQuery('Coding_Property').whereExpr( new Conjunction([ new Condition(new Column(query.effectiveTableName, 'id'), '=', new Column('Coding_Property', 'coding')), new Condition(new Column('Coding_Property', 'property'), '=', property.id), new Condition('value', multiValue ? 'IN' : '=', values), ]) ); query.whereExpr(new SqlFunction('EXISTS', [propertyQuery])); return query; } export function findAncestor( base: SelectQuery, codeSystem: CodeSystem, property: WithId<CodeSystemProperty>, ancestorCode: string ): SelectQuery { const query = new SelectQuery('Coding').addColumns(base.columns).where('system', '=', codeSystem.id); const propertyTable = query.getNextJoinAlias(); query.join( 'INNER JOIN', 'Coding_Property', propertyTable, new Conjunction([ new Condition(new Column('Coding', 'id'), '=', new Column(propertyTable, 'target')), new Condition(new Column(propertyTable, 'property'), '=', property.id), ]) ); const recursiveCTE = 'cte_ancestors'; const recursiveTable = query.getNextJoinAlias(); query.join( 'INNER JOIN', recursiveCTE, recursiveTable, new Disjunction([ new Condition(new Column(propertyTable, 'coding'), '=', new Column(recursiveTable, 'id')), new Condition(new Column(propertyTable, 'coding'), '=', new Column(recursiveTable, 'synonymOf')), ]) ); return new SelectQuery(recursiveCTE) .addColumns(base.columns) .withRecursive(recursiveCTE, new Union(base, query)) .where('code', '=', ancestorCode) .limit(1); } export function getParentProperty(codeSystem: CodeSystem): CodeSystemProperty { if (codeSystem.hierarchyMeaning !== 'is-a') { throw new OperationOutcomeError( badRequest(`Invalid filter: CodeSystem ${codeSystem.url} does not have an is-a hierarchy`) ); } let property = codeSystem.property?.find((p) => p.uri === parentProperty); if (!property) { // Implicit parent property for hierarchical CodeSystems property = { code: codeSystem.hierarchyMeaning ?? 'parent', uri: parentProperty, type: 'code' }; } return property; } export async function resolveProperty( db: Pool | PoolClient, codeSystem: WithId<CodeSystem>, property: CodeSystemProperty ): Promise<WithId<CodeSystemProperty> | undefined> { const query = new SelectQuery('CodeSystem_Property') .column('id') .where('system', '=', codeSystem.id) .where('code', '=', property.code); const id: string | undefined = (await query.execute(db))[0]?.id; if (id) { property.id = id; return property as WithId<CodeSystemProperty>; } else { return undefined; } } /** * Extends a query to select descendants of a given coding. * @param query - The query to extend. * @param codeSystem - The CodeSystem to query within * @param property - The parent (is-a) property for the code system. * @param parentCode - The ancestor code, whose descendants are selected. * @returns The extended SELECT query. */ export function addDescendants( query: SelectQuery, codeSystem: CodeSystem, property: WithId<CodeSystemProperty>, parentCode: string ): SelectQuery { const base = new SelectQuery('Coding') .column('id') .column('code') .column('display') .column('synonymOf') .column('language') .where('system', '=', codeSystem.id) .where('code', '=', parentCode); const propertyTable = query.getNextJoinAlias(); const propertyJoinCondition = new Conjunction([ new Condition(new Column('Coding', 'id'), '=', new Column(propertyTable, 'coding')), ]); propertyJoinCondition.where(new Column(propertyTable, 'property'), '=', property.id); query.join('INNER JOIN', 'Coding_Property', propertyTable, propertyJoinCondition); const recursiveCTE = 'cte_descendants'; const recursiveTable = query.getNextJoinAlias(); query.join( 'INNER JOIN', recursiveCTE, recursiveTable, new Condition(new Column(propertyTable, 'target'), '=', new Column(recursiveTable, 'id')) ); // Move limit and offset to outer query const limit = query.limit_; query.limit(0); const offset = query.offset_; query.offset(0); return new SelectQuery(recursiveCTE) .addColumns(base.columns) .withRecursive(recursiveCTE, new Union(base, query)) .limit(limit) .offset(offset); } export function uniqueOn<T>(arr: T[], keyFn: (el: T) => string): T[] { const seen = Object.create(null); for (const el of arr) { const key = keyFn(el); seen[key] = el; } return Object.values(seen); }

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