Skip to content

Query utility

Reads HL7v2 ASTs using canonical paths like PID-5[1].2.1 or ORDER-ORC-1.

Reads HL7v2 ASTs using canonical paths like PID-5[1].2.1 or ORDER-ORC-1.

What it does

Provides six path-based helpers — parse, select, selectAll, value, matches, format — that operate on HL7v2 AST trees using the same path syntax found in HL7 specs. Paths can address segments (PID), groups (ORDER), nested groups and segments (ORDER-TIMING-TQ1), field repetitions (PID-5[2]), components (PID-5[1].2), and subcomponents (PID-5[1].2.1). Path parsing is memoized in an LRU cache so repeated lookups stay fast.

Install

npm install @glion/util-query

Use

import {
  select,
  selectAll,
  value,
  matches,
  format,
  parse,
} from "@glion/util-query";
import { parseHL7v2 } from "@glion/parser";

const message = `MSH|^~\\&|MyApp|MyFacility|ReceivingApp|ReceivingFacility|20231201120000||ADT^A01|12345|P|2.5
PID|1||123456789^^^MRN||Smith^John^Michael||19800101|M|||123 Main St^^Anytown^ST^12345
OBX|1||8675-3^Glucose^LN||120|mg/dL|70-105|H|||F`;

const ast = parseHL7v2(message);

// Values: drills to subcomponents automatically when the path is unique
const lastName = value(ast, "PID-5[1].1.1"); // "Smith"
const sendingApp = value(ast, "MSH-3[1].1.1"); // "MyApp"

// Single node: grab the first matching AST node
const pidSegment = select(ast, "PID");
if (pidSegment?.node.type === "segment") {
  console.log(`PID has ${pidSegment.node.children.length - 1} fields`);
}

// Multiple nodes
const allObservations = selectAll(ast, "OBX");
for (const { node } of allObservations) {
  console.log(`Found observation: ${node.type}`);
}

// Existence check
if (matches(ast, "PID-5")) {
  console.log("Patient has a name field");
}

// Path parsing
const parsed = parse("ORDERS[2]-OBX-5[1].2.1");

API

parse(path: string): PathParts

Breaks a canonical HL7 path into structured pieces. Useful for tooling and diagnostics.

parse("PID-5[1].2.1");
// {
//   segment: { name: 'PID' },
//   field: 5,
//   repetition: 1,
//   component: 2,
//   subcomponent: 1
// }

parse memoizes up to 1,000 unique paths using an LRU cache so repeated lookups stay fast without leaking memory.

select<Path>(root: Root, path: Path): { node: InferNodeType<Path>; ancestors: Nodes[] } | null

Returns the first AST node addressed by the path, along with its ancestor chain, or null when it cannot be found. The return type is automatically inferred from the path string.

const result = select(ast, "PID-5[1].2");
if (result) {
  result.node; // Type: Component
  result.ancestors; // [Root, Segment, Field, FieldRepetition]
}

The ancestors array follows the unist-util-visit-parents convention: it starts at the root node, ends with the immediate parent, and never includes the node itself. You can rely on ancestors[ancestors.length - 1] being the direct parent.

selectAll<Path>(root: Root, path: Path): Array<{ node: InferNodeType<Path>; ancestors: Nodes[] }>

Returns all AST nodes that match the path. Useful when a message contains multiple segments or groups of the same type.

// Get all OBX segments
const observations = selectAll(ast, "OBX");
for (const { node } of observations) {
  console.log(node.type); // 'segment'
}

// Get all ORDER groups
const orders = selectAll(ast, "ORDER");
for (const { node } of orders) {
  console.log(node.type); // 'group'
}

// Get all observation values
const values = selectAll(ast, "OBX-5");
for (const { node } of values) {
  console.log(node.type); // 'field'
}

value(root: Root, path: string): { value: string; node: Nodes; ancestors: Nodes[] } | null

Returns the string value stored at the path. If the node is not a subcomponent, it walks through single-child layers (field → field repetition → component → subcomponent) automatically.

const result = value(ast, "PID-3[1].1.1");
if (result) {
  console.log(result.value); // "123456789"
  console.log(result.node.type); // "subcomponent"
  console.log(result.ancestors); // [Root, Segment, Field, ...]
}

matches(root: Root, path: string): boolean

Returns true when the path points to an existing node, otherwise false.

if (!matches(ast, "OBX-5")) {
  throw new Error("Missing observation value");
}

if (matches(ast, "PID-5")) {
  const name = value(ast, "PID-5.1.1");
}

format(node: Nodes, ancestors: Nodes[]): string | null

The inverse of select. Given a node and its ancestor chain (as returned by visit or select), produces the canonical path string that would select that node. Returns null if the chain contains no segment or group.

// With select
const result = select(ast, "PID-5.2");
if (result) {
  format(result.node, result.ancestors); // "PID-5.2"
}

// With visit
visit(ast, "field", (node, ancestors) => {
  const path = format(node, ancestors); // "PID-5", "OBX-1", etc.
  console.log(`Visiting ${path}`);
});

// Round-trip: format(select(root, path)) === path

clearParseCache(): void

Clears the memoized parse results. Useful for long-running services that want to release memory during idle periods or before reloading configuration.

getParseCacheSize(): number

Returns the current number of cached path entries so you can monitor cache pressure.

Types

type PathParts = {
  groups?: Array<{ name: string; repetition?: number }>;
  segment: { name: string; repetition?: number };
  field?: number;
  repetition?: number;
  component?: number;
  subcomponent?: number;
};

Path format

Canonical path grammar:

  • SEGMENT — segment (e.g., PID)
  • SEGMENT-FIELD — field (e.g., PID-5)
  • SEGMENT-FIELD[REP] — field repetition (e.g., PID-5[2])
  • SEGMENT-FIELD[REP].COMP — component (e.g., PID-5[1].2)
  • SEGMENT-FIELD[REP].COMP.SUB — subcomponent (e.g., PID-5[1].2.1)
  • GROUP[N]-... — qualify with group name and optional repetition (e.g., ORDERS[2]-OBX-5)

Repetition indexes are 1-based. If you omit [n], select/value/matches assume [1].

Paths select whatever exists in the AST — segments or groups. A bare name (NAME) returns whichever exists, trying segments first; adding a field suffix (NAME-N) forces segment access because field numbers only apply to segments. Groups can also appear as navigation prefixes (ORDER-ORC, ORDER-TIMING-TQ1-1).

// Selecting groups directly
const orderGroup = select(ast, "ORDER");
if (orderGroup?.node.type === "group") {
  console.log("Found ORDER group");
}

// Selecting segments
const pid = select(ast, "PID");
if (pid?.node.type === "segment") {
  console.log("Found PID segment");
}

// Navigation through groups to segments
value(ast, "ORDER-ORC-1"); // Order control
value(ast, "ORDER-TIMING-TQ1-1"); // Timing quantity

Common recipes

Patient demographics

const lastName = value(ast, "PID-5[1].1.1")?.value;
const firstName = value(ast, "PID-5[1].2.1")?.value;
const middleName = value(ast, "PID-5[1].3.1")?.value;
const dob = value(ast, "PID-7[1].1.1")?.value;

Observations

// Single observation
const obsValue = value(ast, "OBX-5[1].1.1")?.value;
const units = value(ast, "OBX-6[1].1.1")?.value;
const refRange = value(ast, "OBX-7[1].1.1")?.value;

// Multiple observations
const allOBX = selectAll(ast, "OBX");
for (const { node } of allOBX) {
  const val = value(node, "OBX-5")?.value;
  const u = value(node, "OBX-6")?.value;
  console.log(`${val} ${u}`);
}

Validation

if (!matches(ast, "MSH")) throw new Error("Missing message header");
if (!matches(ast, "PID-3[1].1.1")) throw new Error("Missing patient ID");

if (matches(ast, "OBX")) {
  const observations = selectAll(ast, "OBX");
  console.log(`Found ${observations.length} observations`);
}

Error messages

  • Invalid formats throw with guidance, e.g. "Invalid HL7 path format: \"PID-\"".
  • Out-of-range indexes throw, e.g. "Field number must be ≥1, got: 0".
  • Whitespace is rejected to catch accidental typos.