Skip to main content
Glama
searchparameter.ts9.07 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { SearchParameterDetails } from '@medplum/core'; import { capitalize, getSearchParameterDetails } from '@medplum/core'; import type { ResourceType, SearchParameter } from '@medplum/fhirtypes'; import { AddressTable } from './lookups/address'; import { CodingTable } from './lookups/coding'; import { ConceptMappingTable } from './lookups/conceptmapping'; import { HumanNameSearchParameterIds, HumanNameTable } from './lookups/humanname'; import type { LookupTable } from './lookups/lookuptable'; import { ReferenceTable } from './lookups/reference'; import { getTokenIndexType, TokenIndexTypes } from './tokens'; type Writeable<T> = { -readonly [P in keyof T]: T[P] }; export const SearchStrategies = { COLUMN: 'column', LOOKUP_TABLE: 'lookup-table', TOKEN_COLUMN: 'token-column', } as const; export interface ColumnSearchParameterImplementation extends SearchParameterDetails { readonly searchStrategy: typeof SearchStrategies.COLUMN; readonly columnName: string; } export interface LookupTableSearchParameterImplementation extends SearchParameterDetails { readonly searchStrategy: typeof SearchStrategies.LOOKUP_TABLE; readonly lookupTable: LookupTable; readonly sortColumnName: string | undefined; } export interface TokenColumnSearchParameterImplementation extends SearchParameterDetails { readonly searchStrategy: typeof SearchStrategies.TOKEN_COLUMN; readonly hasDedicatedColumns: boolean; readonly tokenColumnName: string; readonly sortColumnName: string; readonly textSearchColumnName: string; readonly caseInsensitive: boolean; readonly textSearch: boolean; } export type SearchParameterImplementation = | ColumnSearchParameterImplementation | LookupTableSearchParameterImplementation | TokenColumnSearchParameterImplementation; interface ResourceTypeSearchParameterInfo { searchParamsImplementations: Record<string, SearchParameterImplementation>; } type IndexedSearchParameters = { types: Record<string, ResourceTypeSearchParameterInfo>; }; export const globalSearchParameterRegistry: IndexedSearchParameters = { types: {} }; export function getSearchParameterImplementation( resourceType: string, searchParam: SearchParameter ): SearchParameterImplementation { let result: SearchParameterImplementation | undefined = globalSearchParameterRegistry.types[resourceType]?.searchParamsImplementations?.[searchParam.code as string]; if (!result) { result = buildSearchParameterImplementation(resourceType, searchParam); setSearchParameterImplementation(resourceType, searchParam.code, result); } return result; } function setSearchParameterImplementation( resourceType: string, code: string, implementation: SearchParameterImplementation ): void { let typeSchema = globalSearchParameterRegistry.types[resourceType]; if (!typeSchema) { typeSchema = { searchParamsImplementations: {} }; globalSearchParameterRegistry.types[resourceType] = typeSchema; } typeSchema.searchParamsImplementations[code] = implementation; } const ContainsSupportSearchParameterIds = [ 'individual-email', 'individual-phone', 'individual-telecom', 'NamingSystem-telecom', 'OrganizationAffiliation-email', 'OrganizationAffiliation-phone', 'OrganizationAffiliation-telecom', ]; function buildSearchParameterImplementation( resourceType: string, searchParam: SearchParameter ): SearchParameterImplementation { const code = searchParam.code; const impl = getSearchParameterDetails(resourceType, searchParam) as SearchParameterImplementation; if (!searchParam.base?.includes(resourceType as ResourceType)) { throw new Error(`SearchParameter.base does not include ${resourceType} for ${searchParam.id ?? searchParam.code}`); } const tokenIndexType = getTokenIndexType(searchParam, resourceType); if (tokenIndexType) { const writeable = impl as Writeable<TokenColumnSearchParameterImplementation>; writeable.searchStrategy = 'token-column'; const baseName = convertCodeToColumnName(code); if (hasDedicatedTokenColumns(searchParam, resourceType)) { writeable.hasDedicatedColumns = true; writeable.tokenColumnName = '__' + baseName; writeable.textSearchColumnName = '__' + baseName + 'Text'; } else { writeable.hasDedicatedColumns = false; writeable.tokenColumnName = '__sharedTokens'; writeable.textSearchColumnName = '__sharedTokensText'; } writeable.sortColumnName = '__' + baseName + 'Sort'; writeable.caseInsensitive = tokenIndexType === TokenIndexTypes.CASE_INSENSITIVE; writeable.textSearch = ContainsSupportSearchParameterIds.includes(searchParam.id as string); return impl; } const lookupTable = getLookupTable(resourceType, searchParam); if (lookupTable) { const writeable = impl as Writeable<LookupTableSearchParameterImplementation>; writeable.searchStrategy = 'lookup-table'; writeable.lookupTable = lookupTable; if (HumanNameSearchParameterIds.has(searchParam.id as string)) { writeable.sortColumnName = '__' + convertCodeToColumnName(code) + 'Sort'; } return impl; } const writeable = impl as Writeable<ColumnSearchParameterImplementation>; writeable.searchStrategy = 'column'; writeable.columnName = convertCodeToColumnName(code); return impl; } /** * Converts a hyphen-delimited code to camelCase string. * @param code - The search parameter code. * @returns The SQL column name. */ function convertCodeToColumnName(code: string): string { // hyphen is common in SearchParameter.code // colon is used in Medplum "derived" search parameters, see deriveIdentifierSearchParameter return code.split(/[-:]/).reduce((result, word, index) => result + (index ? capitalize(word) : word), ''); } /** * The lookup tables array includes a list of special tables for search indexing. */ export const lookupTables: LookupTable[] = [ new AddressTable(), new HumanNameTable(), new ReferenceTable(), new CodingTable(), new ConceptMappingTable(), ]; function getLookupTable(resourceType: string, searchParam: SearchParameter): LookupTable | undefined { for (const lookupTable of lookupTables) { if (lookupTable.isIndexed(searchParam, resourceType)) { return lookupTable; } } return undefined; } // This list of exceptions was constructed by analyzing the 15 resource types with the most token usage // and looking for the search parameters with zero or near-zero usage. // // The goal is to avoid creating dedicated token columns for search parameters that are rarely used // to keep the database schema smaller and have fewer indexes to be maintained // Specified as <ResourceType>|<SearchParameterCode> as the key instead of <SearchParameterId> // since not all search parameters have IDs; namely Medplum's derived referenced identifier search parameters const DedicatedTokenColumnsOverridesByResourceTypeAndCode: Record<string, boolean> = { // rarely used search parameters on resource types with high token usage 'AuditEvent|entity-type': false, 'AuditEvent|agent-role': false, 'AuditEvent|subtype': false, 'AuditEvent|_tag': false, 'Observation|component-data-absent-reason': false, 'Encounter|special-arrangement': false, 'ServiceRequest|body-site': false, 'Condition|body-site': false, 'Condition|evidence': false, 'DiagnosticReport|conclusion': false, 'DocumentReference|setting': false, 'DocumentReference|event': false, 'EvidenceVariable|context': false, 'EvidenceVariable|context-type': false, 'EvidenceVariable|jurisdiction': false, 'EvidenceVariable|topic': false, 'MedicationRequest|intended-performertype': false, 'ResearchStudy|category': false, 'ResearchStudy|classifier': false, 'ResearchStudy|focus': false, 'ResearchStudy|location': false, 'ResearchStudy|objective-type': false, 'ResearchStudy|region': false, 'Appointment|reason-code': false, // Overrides for more frequently used search parameters that would otherwise default to `false` 'Observation|patient:identifier': true, 'Observation|performer:identifier': true, 'Observation|subject:identifier': true, 'ServiceRequest|subject:identifier': true, 'ResearchStudy|eligibility:identifier': true, 'DiagnosticReport|result:identifier': true, }; function hasDedicatedTokenColumns(searchParam: SearchParameter, resourceType: string): boolean { if (searchParam.type !== 'token') { throw new Error( `hasDedicatedTokenColumns only supports token search parameters, but ${searchParam.id ?? searchParam.code} is ${searchParam.type}` ); } if (DedicatedTokenColumnsOverridesByResourceTypeAndCode[`${resourceType}|${searchParam.code}`] !== undefined) { return DedicatedTokenColumnsOverridesByResourceTypeAndCode[`${resourceType}|${searchParam.code}`]; } if (searchParam.code.endsWith(':identifier')) { return false; } if (searchParam.code === '_security') { return false; } return true; }

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