Skip to main content
Glama
humanname.ts7.77 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { WithId } from '@medplum/core'; import { formatFamilyName, formatGivenName, formatHumanName } from '@medplum/core'; import type { HumanName, Patient, Person, Practitioner, RelatedPerson, Resource, ResourceType, SearchParameter, } from '@medplum/fhirtypes'; import type { Pool, PoolClient } from 'pg'; import { DeleteQuery } from '../sql'; import type { LookupTableRow } from './lookuptable'; import { LookupTable } from './lookuptable'; const resourceTypes = ['Patient', 'Person', 'Practitioner', 'RelatedPerson'] as const; const resourceTypeSet = new Set(resourceTypes); type HumanNameResourceType = (typeof resourceTypes)[number]; export type HumanNameResource = Patient | Person | Practitioner | RelatedPerson; export interface HumanNameTableRow extends LookupTableRow { name: string | undefined; given: string | undefined; family: string | undefined; } export const HumanNameSearchParameterIds = new Set<string>([ 'individual-given', 'individual-family', 'Patient-name', 'Person-name', 'Practitioner-name', 'RelatedPerson-name', ]); /** * The HumanNameTable class is used to index and search "name" properties on "Person" resources. * Each name is represented as a separate row in the "HumanName" table. */ export class HumanNameTable extends LookupTable { private static readonly knownParams: Set<string> = new Set<string>([ 'individual-given', 'individual-family', 'Patient-name', 'Person-name', 'Practitioner-name', 'RelatedPerson-name', ]); private static hasHumanName(resourceType: ResourceType): resourceType is HumanNameResourceType { return resourceTypeSet.has(resourceType as any); } protected readonly CONTAINS_SQL_OPERATOR: LookupTable['CONTAINS_SQL_OPERATOR'] = 'ILIKE'; /** * Returns the table name. * @returns The table name. */ getTableName(): string { return 'HumanName'; } /** * Returns the column name for the given search parameter. * @param code - The search parameter code. * @returns The column name. */ getColumnName(code: string): string { return code; } /** * Returns true if the search parameter is an HumanName parameter. * @param searchParam - The search parameter. * @returns True if the search parameter is an HumanName parameter. */ isIndexed(searchParam: SearchParameter): boolean { return HumanNameTable.knownParams.has(searchParam.id as string); } extractValues(result: HumanNameTableRow[], resource: WithId<Resource>): void { if (!HumanNameTable.hasHumanName(resource.resourceType)) { return; } const names: (HumanName | undefined | null)[] | undefined = (resource as HumanNameResource).name; if (!Array.isArray(names)) { return; } for (const name of names) { if (!name) { continue; } const extracted = { resourceId: resource.id, // logical OR coalesce to ensure that empty strings are inserted as NULL name: getNameString(name) || undefined, given: formatGivenName(name) || undefined, family: formatFamilyName(name) || undefined, }; if ( (extracted.name || extracted.given || extracted.family) && !result.some( (n) => n.resourceId === extracted.resourceId && n.name === extracted.name && n.given === extracted.given && n.family === extracted.family ) ) { result.push(extracted); } } } async batchIndexResources<T extends Resource>( client: PoolClient, resources: WithId<T>[], create: boolean, resourceBatchSize?: number ): Promise<void> { if (!resources[0] || !HumanNameTable.hasHumanName(resources[0].resourceType)) { return; } await super.batchIndexResources(client, resources, create, resourceBatchSize); } /** * Deletes the resource from the lookup table. * @param client - The database client. * @param resource - The resource to delete. */ async deleteValuesForResource(client: Pool | PoolClient, resource: Resource): Promise<void> { if (!HumanNameTable.hasHumanName(resource.resourceType)) { return; } const tableName = this.getTableName(); const resourceId = resource.id; await new DeleteQuery(tableName).where('resourceId', '=', resourceId).execute(client); } /** * Purges resources of the specified type that were last updated before the specified date. * This is only available to the system and super admin accounts. * @param client - The database client. * @param resourceType - The FHIR resource type. * @param before - The date before which resources should be purged. */ async purgeValuesBefore(client: Pool | PoolClient, resourceType: ResourceType, before: string): Promise<void> { if (!HumanNameTable.hasHumanName(resourceType)) { return; } await super.purgeValuesBefore(client, resourceType, before); } } /** * Returns a string representation of the human name for indexing. * * In previous versions, we simply used `formatHumanName(name)`. * * However, the FHIR spec indicates that the `text` field should be used for indexing. * * Quote: * * "The given name parts may contain whitespace, though generally they don't. * Initials may be used in place of the full name if that is all that is recorded. * Systems that operate across cultures should generally rely on the text form for * presentation and use the parts for index/search functionality. For this reason, * applications SHOULD populate the text element for future robustness." * * @param name - The input human name. * @returns A string representation of the human name. */ export function getNameString(name: HumanName): string { let result = formatHumanName(name); if (name.text) { // Add unique tokens from the text field const resultTokens = getTokens(result); const textTokens = getTokens(name.text); for (const token of textTokens) { if (!resultTokens.has(token)) { result += ' ' + token; resultTokens.add(token); } } } return result; } function getTokens(input: string): Set<string> { if (!input || typeof input !== 'string') { return new Set(); } // Convert to lowercase // Split on whitespace // Remove empty strings return new Set<string>(input.toLowerCase().split(/\s+/).filter(Boolean)); } export function getHumanNameSortValue( names: (HumanName | undefined | null)[] | undefined, searchParam: SearchParameter ): string | undefined { if (!Array.isArray(names)) { return undefined; } let result: string | undefined; let resultPrecedence: number = Infinity; for (const name of names) { if (!name) { continue; } let candidate: string | undefined; if (searchParam.code === 'given') { candidate = formatGivenName(name); } else if (searchParam.code === 'family') { candidate = formatFamilyName(name); } else { candidate = getNameString(name); } if (!candidate) { continue; } const candidatePrecedence = UsePrecedence[name.use ?? ''] ?? MissingUsePrecedence; if ( !result || candidatePrecedence < resultPrecedence || (candidatePrecedence === resultPrecedence && candidate.localeCompare(result) < 0) ) { result = candidate; resultPrecedence = candidatePrecedence; } } return result; } const MissingUsePrecedence = 3; const UsePrecedence = { usual: 1, official: 2, '': MissingUsePrecedence, temp: 4, nickname: 5, anonymous: 6, old: 7, maiden: 8, };

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