Skip to main content
Glama
ir.ts8.4 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 // Search Immediate Representation (IR) module // // https://hl7.org/fhir/R4/search.html // // FHIR R4 has the following search parameter types: // 1. Number // 2. Date/DateTime // 3. String // 4. Token // 5. Reference // 6. Composite // 7. Quantity // 8. URI // 9. Special // // To make matters more complicated, we must consider that these search parameters can be applied // to many different underlying element types. // // To make our lives easier, we will use a simple Immediate Representation (IR) format to represent the search parameters. // All underlying element types will be mapped to the IR format for the corresponding search parameter type. import type { CodeableConcept, Coding, ContactPoint, Identifier, Period, Quantity } from '@medplum/fhirtypes'; import { isQuantity, toPeriod } from '../fhirpath/utils'; import { typedValueToString } from '../format'; import type { TypedValue } from '../types'; import { isReference, PropertyType } from '../types'; import { getReferenceString, isResourceWithId, isString } from '../utils'; export interface SearchableToken { readonly system: string | undefined; readonly value: string | undefined; } export function convertToSearchableNumbers(typedValues: TypedValue[]): [number | undefined, number | undefined][] { const result: [number | undefined, number | undefined][] = []; for (const typedValue of typedValues) { if (typedValue.type === PropertyType.Range) { result.push([typedValue.value?.low?.value, typedValue.value?.high?.value]); } else if (typeof typedValue.value === 'number') { result.push([typedValue.value, typedValue.value]); } } return result; } export function convertToSearchableDates(typedValues: TypedValue[]): Period[] { const result: Period[] = []; for (const typedValue of typedValues) { const period = toPeriod(typedValue.value); if (period) { result.push(period); } } return result; } export function convertToSearchableStrings(typedValues: TypedValue[]): string[] { const result = new Set<string>(); for (const typedValue of typedValues) { const str = typedValueToString(typedValue); if (str) { result.add(str); } } return Array.from(result); } export function convertToSearchableReferences(typedValues: TypedValue[]): string[] { const result = new Set<string>(); for (const typedValue of typedValues) { const { value } = typedValue; if (!value) { continue; } if (isString(value)) { // Handle "canonical" properties such as QuestionnaireResponse.questionnaire // This is a reference string that is not a FHIR reference result.add(value); } else if (isReference(value)) { // Handle normal "reference" properties result.add(value.reference); } else if (isResourceWithId(value)) { // Handle inline references result.add(getReferenceString(value)); } else if (typeof value.identifier === 'object') { // Handle logical (identifier-only) references by putting a placeholder in the column // NOTE(mattwiller 2023-11-01): This is done to enable searches using the :missing modifier; // actual identifier search matching is handled by the `<ResourceType>_Token` lookup tables result.add(`identifier:${value.identifier.system}|${value.identifier.value}`); } } return Array.from(result); } export function convertToSearchableQuantities(typedValues: TypedValue[]): Quantity[] { const result: Quantity[] = []; for (const typedValue of typedValues) { const { value } = typedValue; if (typeof value === 'number') { result.push({ value }); } else if (isQuantity(value)) { result.push(value); } } return result; } export function convertToSearchableUris(typedValues: TypedValue[]): string[] { const result = new Set<string>(); for (const typedValue of typedValues) { if (isString(typedValue.value)) { result.add(typedValue.value); } } return Array.from(result); } export interface TokensContext { caseInsensitive?: boolean; textSearchSystem?: string; } export function convertToSearchableTokens(typedValues: TypedValue[], context: TokensContext = {}): SearchableToken[] { const result: SearchableToken[] = []; for (const typedValue of typedValues) { buildTokens(context, result, typedValue); } return result; } /** * Builds a list of zero or more tokens for a search parameter and value. * @param context - The context for building tokens. * @param result - The result array where tokens will be added. * @param typedValue - A typed value to be indexed for the search parameter. */ function buildTokens(context: TokensContext, result: SearchableToken[], typedValue: TypedValue): void { const { type, value } = typedValue; switch (type) { case PropertyType.Identifier: buildIdentifierToken(result, context, value as Identifier); break; case PropertyType.CodeableConcept: buildCodeableConceptToken(result, context, value as CodeableConcept); break; case PropertyType.Coding: buildCodingToken(result, context, value as Coding); break; case PropertyType.ContactPoint: buildContactPointToken(result, context, value as ContactPoint); break; default: buildSimpleToken(result, context, undefined, value?.toString() as string | undefined); } } /** * Builds an identifier token. * @param result - The result array where tokens will be added. * @param context - Context for building tokens. * @param identifier - The Identifier object to be indexed. */ function buildIdentifierToken( result: SearchableToken[], context: TokensContext, identifier: Identifier | undefined ): void { if (identifier?.type?.text) { buildSimpleToken(result, context, context.textSearchSystem, identifier.type.text); } buildSimpleToken(result, context, identifier?.system, identifier?.value); } /** * Builds zero or more CodeableConcept tokens. * @param result - The result array where tokens will be added. * @param context - Context for building tokens. * @param codeableConcept - The CodeableConcept object to be indexed. */ function buildCodeableConceptToken( result: SearchableToken[], context: TokensContext, codeableConcept: CodeableConcept | undefined ): void { if (codeableConcept?.text) { buildSimpleToken(result, context, context.textSearchSystem, codeableConcept.text); } if (codeableConcept?.coding) { for (const coding of codeableConcept.coding) { buildCodingToken(result, context, coding); } } } /** * Builds a Coding token. * @param result - The result array where tokens will be added. * @param context - Context for building tokens. * @param coding - The Coding object to be indexed. */ function buildCodingToken(result: SearchableToken[], context: TokensContext, coding: Coding | undefined): void { if (coding) { if (coding.display) { buildSimpleToken(result, context, context.textSearchSystem, coding.display); } buildSimpleToken(result, context, coding.system, coding.code); } } /** * Builds a ContactPoint token. * @param result - The result array where tokens will be added. * @param context - Context for building tokens. * @param contactPoint - The ContactPoint object to be indexed. */ function buildContactPointToken( result: SearchableToken[], context: TokensContext, contactPoint: ContactPoint | undefined ): void { if (contactPoint) { buildSimpleToken(result, context, contactPoint.system, contactPoint.value?.toLocaleLowerCase()); } } /** * Builds a simple token. * @param result - The result array where tokens will be added. * @param context - Context for building tokens. * @param system - The token system. * @param value - The token value. */ function buildSimpleToken( result: SearchableToken[], context: TokensContext, system: string | undefined, value: string | undefined ): void { // Only add the token if there is a system or a value, and if it is not already in the list. if ((system || value) && !result.some((token) => token.system === system && token.value === value)) { result.push({ system, value: value && context.caseInsensitive ? value.toLocaleLowerCase() : value, }); } }

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