Skip to main content
Glama
eval.ts8.34 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { Resource, ViewDefinition, ViewDefinitionSelect } from '@medplum/fhirtypes'; import { evalFhirPathTyped } from '../fhirpath/parse'; import { getTypedPropertyValue, toTypedValue } from '../fhirpath/utils'; import type { TypedValue } from '../types'; /** * Represents a "selection structure" in the SQL-on-FHIR specification. * * In practice, this can be a ViewDefinition or ViewDefinitionSelect. * * TypeScript does not like checks for properties that are not part of the type, so we use this interface instead. */ export interface SelectionStructure { forEach?: string; forEachOrNull?: string; column?: ViewDefinitionSelect['column']; select?: SelectionStructure[]; unionAll?: SelectionStructure[]; } /** * SQL on FHIR output row. */ export type OutputRow = Record<string, any>; /** * Evaluates a SQL-on-FHIR view on a set of FHIR resources. * @param view - The view definition. * @param resources - The array of FHIR resources. * @returns The output rows. */ export function evalSqlOnFhir(view: ViewDefinition, resources: Resource[]): OutputRow[] { const result = []; for (const resource of resources) { result.push(...processResource(view, resource)); } return result; } /** * Processes a FHIR resource with a ViewDefinition to emit all rows. * * This step emits all rows produced by a ViewDefinition on an input Resource, by setting up a recursive call. * * See: https://build.fhir.org/ig/FHIR/sql-on-fhir-v2/implementer_guidance.html#process-a-resource-entry-point * * @param v - The view definition. * @param r - The FHIR resource. * @returns The output rows. */ function processResource(v: ViewDefinition, r: Resource): OutputRow[] { if (!v.resource) { throw new Error('Resource type is required'); } if (v.resource !== r.resourceType) { return []; } const variables: Record<string, TypedValue> = {}; if (v.constant) { for (const c of v.constant) { const typedConstant = { type: 'ViewDefinitionConstant', value: c }; variables['%' + c.name] = getTypedPropertyValue(typedConstant, 'value') as TypedValue; } } const typedResource = toTypedValue(r); if (v.where) { for (const where of v.where) { const whereResult = evalFhirPathTyped(where.path, [typedResource], variables); if (whereResult.length !== 1) { return []; } if (whereResult[0].type !== 'boolean') { throw new Error('WHERE clause must evaluate to a boolean'); } if (!whereResult[0].value) { return []; } } } return process(v, typedResource, variables); } /** * Processes a selection structure and node to emit all rows. * * This step emits all rows for a given Selection Structure and Node. We first generate sets of * "partial rows" (i.e., sets of incomplete column bindings from the various clauses of V) and combine them to emit complete rows. * * This function is deliberately structured to match the pseudocode in the SQL-on-FHIR specification. * See: https://build.fhir.org/ig/FHIR/sql-on-fhir-v2/implementer_guidance.html#processs-n-recursive-step * * @param s - The selection structure. * @param n - The node (element) from a FHIR resource. * @param variables - The variables. * @returns An array of output rows. */ function process(s: SelectionStructure, n: TypedValue, variables: Record<string, TypedValue>): OutputRow[] { const result: OutputRow[] = []; // 1. Define a list of Nodes foci as let foci: TypedValue[]; if (s.forEach) { // If S.forEach is defined: fhirpath(S.forEach, N) foci = evalFhirPathTyped(s.forEach, [n], variables); } else if (s.forEachOrNull) { // Else if S.forEachOrNull is defined: fhirpath(S.forEachOrNull, N) foci = evalFhirPathTyped(s.forEachOrNull, [n], variables); } else { // Otherwise: [N] (a list with just the input node) foci = [n]; } // 2. For each element f of foci for (const f of foci) { // Initialize an empty list parts (each element of parts will be a list of partial rows) const parts: OutputRow[][] = []; // Process Columns: for (const col of s.column ?? []) { // For each Column col of S.column, define val as fhirpath(col.path, f) const val = evalFhirPathTyped(col.path, [f], variables); // Define b as a row whose column named col.name takes the value let b: OutputRow; if (val.length === 0) { // If val was the empty set: null b = { [col.name]: null }; } else if (col.collection) { // Else if col.collection is true: val b = { [col.name]: val.map((v) => v.value) }; } else if (val.length === 1) { // Else if val has a single element e: e b = { [col.name]: val[0].value }; } else { // Else: throw "Multiple values found but not expected for column" throw new Error('Multiple values found but not expected for column'); } // Append [b] to parts // (Note: append a list so the final element of parts is now a list containing the single row b).) parts.push([b]); } // Process Selects: // For each selection structure sel of S.select for (const sel of s.select ?? []) { // Define rows as the collection of all rows emitted by Process(sel, f) const rows = process(sel, f, variables); // Append rows to parts // (Note: do not append the elements but the whole list, so the final element of parts is now the list rows) parts.push(rows); } // Process UnionAlls: // Initialize urows as an empty list of rows // For each selection structure u of S.unionAll if (s.unionAll) { const urows: OutputRow[] = []; for (const u of s.unionAll) { // For each row r in Process(u, f) for (const r of process(u, f, variables)) { // Append r to urows urows.push(r); } } // Append urows to parts // (Note: do not append the elements but the whole list, so the final element of parts is now the list urows parts.push(urows); } // For every list of partial rows prows in the Cartesian product of parts // (Note: the Cartesian product is always between a Selection Structure and its direct children, not deeper descendants. // Because the process is recursive, rows generated by, for example, a .select[0].select[0].select[0] will eventually bubble up // to the top level, but the bubbling happens one level at a time.) result.push(...cartesianProduct(parts)); } // If foci is an empty list and S.forEachOrNull is defined if (foci.length === 0 && s.forEachOrNull) { // (Note: when this condition is met, no rows have been emitted so far) // Initialize a blank row r const r: OutputRow = {}; // For each Column c in ValidateColumns(V, []) for (const c of s.column ?? []) { // Bind the column c.name to null in the row r r[c.name] = null; } // Emit the row r result.push(r); } return result; } /** * Returns the Cartesian product of the given arrays. * * For example, if there are two sets of partial rows: * * [{"a": 1},{"a": 2}] with bindings for the variable a * [{"b": 3},{"b": 4}] with bindings for the variable b * * Then the Cartesian product of these sets consists of four complete rows: * * [ * {"a": 1, "b": 3}, * {"a": 1, "b": 4}, * {"a": 2, "b": 3}, * {"a": 2, "b": 4} * ] * * @param parts - The arrays to combine. * @returns The Cartesian product of the arrays. */ function cartesianProduct(parts: OutputRow[][]): OutputRow[] { if (parts.length === 0) { return []; } let temp = parts[0]; for (let i = 1; i < parts.length; i++) { temp = cartesianProductHelper(temp, parts[i]); } return temp; } function cartesianProductHelper(aArray: OutputRow[], bArray: OutputRow[]): OutputRow[] { const result = []; for (const a of aArray) { for (const b of bArray) { result.push(combinePartialRows(a, b)); } } return result; } function combinePartialRows(a: OutputRow, b: OutputRow): OutputRow { const result: OutputRow = {}; Object.assign(result, a); Object.assign(result, b); return result; }

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