Skip to main content
Glama
SearchUtils.tsx18.5 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { Filter, InternalSchemaElement, SearchRequest } from '@medplum/core'; import { capitalize, DEFAULT_SEARCH_COUNT, evalFhirPathTyped, formatDateTime, Operator } from '@medplum/core'; import type { Resource, SearchParameter } from '@medplum/fhirtypes'; import type { JSX } from 'react'; import { MedplumLink } from '../MedplumLink/MedplumLink'; import { ResourcePropertyDisplay } from '../ResourcePropertyDisplay/ResourcePropertyDisplay'; import { getValueAndType } from '../ResourcePropertyDisplay/ResourcePropertyDisplay.utils'; import type { SearchControlField } from './SearchControlField'; const searchParamToOperators: Record<string, Operator[]> = { string: [Operator.EQUALS, Operator.NOT, Operator.CONTAINS, Operator.EXACT], fulltext: [Operator.EQUALS, Operator.NOT, Operator.CONTAINS, Operator.EXACT], token: [Operator.EQUALS, Operator.NOT], reference: [Operator.EQUALS, Operator.NOT], numeric: [ Operator.EQUALS, Operator.NOT_EQUALS, Operator.GREATER_THAN, Operator.LESS_THAN, Operator.GREATER_THAN_OR_EQUALS, Operator.LESS_THAN_OR_EQUALS, ], quantity: [ Operator.EQUALS, Operator.NOT_EQUALS, Operator.GREATER_THAN, Operator.LESS_THAN, Operator.GREATER_THAN_OR_EQUALS, Operator.LESS_THAN_OR_EQUALS, ], date: [ Operator.EQUALS, Operator.NOT_EQUALS, Operator.GREATER_THAN, Operator.LESS_THAN, Operator.GREATER_THAN_OR_EQUALS, Operator.LESS_THAN_OR_EQUALS, Operator.STARTS_AFTER, Operator.ENDS_BEFORE, Operator.APPROXIMATELY, ], datetime: [ Operator.EQUALS, Operator.NOT_EQUALS, Operator.GREATER_THAN, Operator.LESS_THAN, Operator.GREATER_THAN_OR_EQUALS, Operator.LESS_THAN_OR_EQUALS, Operator.STARTS_AFTER, Operator.ENDS_BEFORE, Operator.APPROXIMATELY, ], uri: [Operator.EQUALS, Operator.NOT, Operator.ABOVE, Operator.BELOW], }; const operatorNames: Record<Operator, string> = { eq: 'equals', ne: 'not equals', gt: 'greater than', lt: 'less than', ge: 'greater than or equals', le: 'less than or equals', sa: 'starts after', eb: 'ends before', ap: 'approximately', sw: 'starts with', contains: 'contains', exact: 'exact', text: 'text', not: 'not', above: 'above', below: 'below', in: 'in', 'not-in': 'not in', 'of-type': 'of type', missing: 'missing', present: 'present', identifier: 'identifier', iterate: 'iterate', }; /** * Sets the array of filters. * @param definition - The original search request. * @param filters - The new filters. * @returns The updated search request. */ export function setFilters(definition: SearchRequest, filters: Filter[]): SearchRequest { return { ...definition, filters: filters, offset: 0, name: undefined, }; } /** * Clears all of the filters. * @param definition - The original search request. * @returns The updated search request. */ export function clearFilters(definition: SearchRequest): SearchRequest { return setFilters(definition, []); } /** * Clears all of the filters on a certain field. * @param definition - The original search request. * @param code - The field key name to clear filters. * @returns The updated search request. */ export function clearFiltersOnField(definition: SearchRequest, code: string): SearchRequest { return setFilters( definition, (definition.filters ?? []).filter((f) => f.code !== code) ); } /** * Adds a filter. * @param definition - The original search request. * @param field - The field key name. * @param op - The operation key name. * @param value - The filter value. * @param opt_clear - Optional flag to clear filters on the field. * @returns The updated search request. */ export function addFilter( definition: SearchRequest, field: string, op: Operator, value?: string, opt_clear?: boolean ): SearchRequest { if (opt_clear) { definition = clearFiltersOnField(definition, field); } const nextFilters: Filter[] = []; if (definition.filters) { nextFilters.push(...definition.filters); } nextFilters.push({ code: field, operator: op, value: value ?? '' }); return setFilters(definition, nextFilters); } /** * Adds a field. * @param definition - The original search request. * @param field - The field key name. * @returns The updated search request. */ export function addField(definition: SearchRequest, field: string): SearchRequest { if (definition.fields?.includes(field)) { return definition; } const newFields = []; if (definition.fields) { newFields.push(...definition.fields); } newFields.push(field); return { ...definition, fields: newFields, name: undefined, }; } /** * Deletes a filter at the specified index. * @param definition - The original search request. * @param index - The filter index. * @returns The updated search request. */ export function deleteFilter(definition: SearchRequest, index: number): SearchRequest { if (!definition.filters) { return definition; } const newFilters = [...definition.filters]; newFilters.splice(index, 1); return { ...definition, filters: newFilters, name: undefined, }; } /** * Adds a filter that constrains the specified field to "yesterday". * @param definition - The original search request. * @param field - The field key name. * @returns The updated search request. */ export function addYesterdayFilter(definition: SearchRequest, field: string): SearchRequest { return addDayFilter(definition, field, -1); } /** * Adds a filter that constrains the specified field to "today". * @param definition - The original search request. * @param field - The field key name. * @returns The updated search request. */ export function addTodayFilter(definition: SearchRequest, field: string): SearchRequest { return addDayFilter(definition, field, 0); } /** * Adds a filter that constrains the specified field to "tomorrow". * @param definition - The original search request. * @param field - The field key name. * @returns The updated search request. */ export function addTomorrowFilter(definition: SearchRequest, field: string): SearchRequest { return addDayFilter(definition, field, 1); } /** * Adds a filter that constrains the specified field to a day. * The day is specified as a delta from the current day. * "Today" would be 0. * "Yesterday" would be -1. * "Tomorrow" would be 1. * @param definition - The original search request. * @param field - The field key name. * @param delta - The number of days from this day. * @returns The updated search request. */ function addDayFilter(definition: SearchRequest, field: string, delta: number): SearchRequest { const startTime = new Date(); startTime.setDate(startTime.getDate() + delta); startTime.setHours(0, 0, 0, 0); const endTime = new Date(startTime.getTime()); endTime.setDate(endTime.getDate() + 1); endTime.setTime(endTime.getTime() - 1); return addDateFilterBetween(definition, field, startTime, endTime); } /** * Adds a filter that constrains the specified field to "next 24 hours". * @param definition - The original search request. * @param field - The field key name. * @returns The updated search request. */ export function addNext24HoursFilter(definition: SearchRequest, field: string): SearchRequest { const now = new Date(); const endTime = new Date(now.getTime() + 24 * 60 * 60 * 1000); return addDateFilterBetween(definition, field, now, endTime); } /** * Adds a filter that constrains the specified field to "last month". * @param definition - The original search request. * @param field - The field key name. * @returns The updated search request. */ export function addLastMonthFilter(definition: SearchRequest, field: string): SearchRequest { return addMonthFilter(definition, field, -1); } /** * Adds a filter that constrains the specified field to "this month". * @param definition - The original search request. * @param field - The field key name. * @returns The updated search request. */ export function addThisMonthFilter(definition: SearchRequest, field: string): SearchRequest { return addMonthFilter(definition, field, 0); } /** * Adds a filter that constrains the specified field to "next month". * @param definition - The original search request. * @param field - The field key name. * @returns The updated search request. */ export function addNextMonthFilter(definition: SearchRequest, field: string): SearchRequest { return addMonthFilter(definition, field, 1); } /** * Adds a filter that constrains the specified field to a month. * The month is specified as a delta from the current month. * "This month" would be 0. * "Last month" would be -1. * "Next month" would be 1. * @param definition - The original search request. * @param field - The field key name. * @param delta - The number of months from this month. * @returns The updated search request. */ function addMonthFilter(definition: SearchRequest, field: string, delta: number): SearchRequest { const startTime = new Date(); startTime.setMonth(startTime.getMonth() + delta); startTime.setDate(1); startTime.setHours(0, 0, 0, 0); const endTime = new Date(startTime.getTime()); endTime.setMonth(endTime.getMonth() + 1); endTime.setDate(1); endTime.setHours(0, 0, 0, 0); endTime.setTime(endTime.getTime() - 1); return addDateFilterBetween(definition, field, startTime, endTime); } /** * Adds a filter that constrains the specified field to the year to date. * @param definition - The original search request. * @param field - The field key name. * @returns The updated search request. */ export function addYearToDateFilter(definition: SearchRequest, field: string): SearchRequest { const startTime = new Date(); startTime.setMonth(0); startTime.setDate(1); startTime.setHours(0, 0, 0, 0); const endTime = new Date(); return addDateFilterBetween(definition, field, startTime, endTime); } /** * Adds a filter for a date between two dates (inclusive of both dates). * @param definition - The original search request. * @param field - The field key name. * @param d1 - The start date. * @param d2 - The end date. * @returns The updated search request. */ export function addDateFilterBetween(definition: SearchRequest, field: string, d1: Date, d2: Date): SearchRequest { definition = clearFiltersOnField(definition, field); definition = addDateFilterImpl(definition, field, Operator.GREATER_THAN_OR_EQUALS, d1); definition = addDateFilterImpl(definition, field, Operator.LESS_THAN_OR_EQUALS, d2); return definition; } /** * Adds a filter for a date before a certain date/time. * @param definition - The original search request. * @param field - The field key name. * @param op - The date/time operation. * @param value - The date. * @returns The updated search request. */ function addDateFilterImpl(definition: SearchRequest, field: string, op: Operator, value: Date): SearchRequest { return addFilter(definition, field, op, value.toISOString()); } /** * Adds a filter that constrains the specified field to "missing". * @param definition - The original search request. * @param field - The field key name. * @param value - Optional boolean value. Default is true. * @returns The updated search request. */ export function addMissingFilter(definition: SearchRequest, field: string, value = true): SearchRequest { return addFilter(definition, field, Operator.MISSING, value.toString()); } /** * Sets the offset (starting at zero). * @param definition - The original search request. * @param offset - The offset number. * @returns The updated search request. */ export function setOffset(definition: SearchRequest, offset: number): SearchRequest { if (definition.offset === offset) { return definition; } return { ...definition, offset, name: undefined, }; } /** * Creates a new search request with the search offset at the specified page. * @param definition - The search definition. * @param page - The new page number * @returns The new search definition. */ export function setPage(definition: SearchRequest, page: number): SearchRequest { const count = definition.count ?? DEFAULT_SEARCH_COUNT; const newOffset = (page - 1) * count; return setOffset(definition, newOffset); } /** * Sorts the search by the specified key, and optional direction. * Direction defaults to ascending ('asc') if not specified. * @param definition - The original search request. * @param sort - The sort key. * @param desc - Optional descending flag. Default is false. * @returns The updated search request. */ export function setSort(definition: SearchRequest, sort: string, desc?: boolean): SearchRequest { if (sort === getSortField(definition) && desc !== undefined && desc === isSortDescending(definition)) { return definition; } return { ...definition, sortRules: [ { code: sort, descending: !!desc, }, ], name: undefined, }; } /** * Toggles the sort of the search by key. * If the search is already sorted by the key, reverses the direction. * If the search is not sorted by the key, sort in ascending order. * @param definition - The original search request. * @param key - The field key name. * @returns The updated search request. */ export function toggleSort(definition: SearchRequest, key: string): SearchRequest { let desc = false; if (getSortField(definition) === key) { desc = !isSortDescending(definition); } return setSort(definition, key, desc); } export function getSortField(definition: SearchRequest): string | undefined { const sortRules = definition.sortRules; if (!sortRules || sortRules.length === 0) { return undefined; } const field = sortRules[0].code; return field.startsWith('-') ? field.substr(1) : field; } export function isSortDescending(definition: SearchRequest): boolean { const sortRules = definition.sortRules; if (!sortRules || sortRules.length === 0) { return false; } return !!sortRules[0].descending; } /** * Returns a list of operators for a search parameter. * @param searchParam - The search parameter. * @returns The list of operators that can be used for the search parameter. */ export function getSearchOperators(searchParam: SearchParameter): Operator[] | undefined { return searchParamToOperators[searchParam.type as string]; } /** * Returns a string representing the operation. * @param op - The operation code. * @returns A display string for the operation. */ export function getOpString(op: Operator): string { return operatorNames[op] ?? ''; } /** * Returns a field display name. * @param key - The field key. * @returns The field display name. */ export function buildFieldNameString(key: string): string { let tmp = key; // If dot separated, only the last part if (tmp.includes('.')) { tmp = tmp.split('.').pop() as string; } // Special case for Version ID if (tmp === 'versionId') { return 'Version ID'; } // Remove choice of type tmp = tmp.replace('[x]', ''); // Convert camel case to space separated tmp = tmp.replaceAll(/([A-Z])/g, ' $1'); // Convert dashes and underscores to spaces tmp = tmp.replaceAll(/[-_]/g, ' '); // Normalize whitespace to single space character tmp = tmp.replaceAll(/\s+/g, ' '); // Trim tmp = tmp.trim(); // Special case for ID if (tmp.toLowerCase() === 'id') { return 'ID'; } // Capitalize the first letter of each word return tmp.split(/\s/).map(capitalize).join(' '); } /** * Returns a fragment to be displayed in the search table for the value. * @param resource - The parent resource. * @param field - The search code or FHIRPath expression. * @returns The fragment to display. */ export function renderValue(resource: Resource, field: SearchControlField): string | JSX.Element | null | undefined { const key = field.name; if (key === 'id') { return <MedplumLink to={`/${resource.resourceType}/${resource.id}`}>{resource.id}</MedplumLink>; } if (key === 'meta.versionId') { return resource.meta?.versionId; } if (key === '_lastUpdated') { return formatDateTime(resource.meta?.lastUpdated); } // Priority 1: InternalSchemaElement by exact match if (`${resource.resourceType}.${field.name}` === field.elementDefinition?.path) { return renderPropertyValue(resource, field.elementDefinition); } // Priority 2: SearchParameter by exact match if (field.searchParams?.length === 1 && field.name === field.searchParams[0].code) { return renderSearchParameterValue(resource, field.searchParams[0]); } // We don't know how to render this field definition return null; } /** * Returns a fragment to be displayed in the search table for a resource property. * @param resource - The parent resource. * @param elementDefinition - The property element definition. * @returns A React element or null. */ function renderPropertyValue(resource: Resource, elementDefinition: InternalSchemaElement): JSX.Element | null { const path = elementDefinition.path?.split('.')?.pop()?.replaceAll('[x]', '') ?? ''; const [value, propertyType] = getValueAndType({ type: resource.resourceType, value: resource }, path); if (!value) { return null; } return ( <ResourcePropertyDisplay path={elementDefinition.path} property={elementDefinition} propertyType={propertyType} value={value} maxWidth={200} ignoreMissingValues={true} link={false} /> ); } /** * Returns a fragment to be displayed in the search table for a search parameter. * @param resource - The parent resource. * @param searchParam - The search parameter. * @returns A React element or null. */ function renderSearchParameterValue(resource: Resource, searchParam: SearchParameter): JSX.Element | null { const value = evalFhirPathTyped(searchParam.expression as string, [{ type: resource.resourceType, value: resource }]); if (!value || value.length === 0) { return null; } return ( <> {value.map((v, index) => ( <ResourcePropertyDisplay key={`${index}-${value.length}`} propertyType={v.type} value={v.value} maxWidth={200} ignoreMissingValues={true} link={false} /> ))} </> ); }

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