npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@rethinkhealth/hl7v2-util-query

v0.4.2

Published

Query HL7v2 AST nodes by HL7 location paths

Readme

HL7v2 Query Utility

A tiny helper for reading HL7v2 ASTs with familiar canonical paths.

Features

  • Five verbsparse, select, selectAll, value, matches
  • Canonical paths – same syntax you see in HL7 specs
  • Deep traversal – walks nested groups automatically
  • Zero fuss – no options, no surprises, just results

Installation

npm install @rethinkhealth/hl7v2-util-query

Quick Start

import { select, selectAll, value, matches, parse } from '@rethinkhealth/hl7v2-util-query';
import { parseHL7v2 } from '@rethinkhealth/hl7v2-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: get all matching elements
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');
console.log(parsed);

API

parse(path: string): PathParts

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

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

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.

parse memoizes up to 1,000 unique paths using an LRU cache so repeated lookups stay fast without leaking memory. Long-running processes can call clearParseCache() to release cached entries (or inspect getParseCacheSize() if diagnostics are needed).

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

Returns all AST nodes (segments or groups) that match the path. Useful when a message contains multiple segments/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 will walk 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. More semantically clear than checking for null.

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

// Use in conditionals
if (matches(ast, 'PID-5')) {
  const name = value(ast, 'PID-5.1.1');
}

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 in diagnostics or tests.

Path Cheatsheet

  • 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], find assumes [1].

Types

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

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 obx = select(ast, 'OBX');
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 units = value(node, 'OBX-6')?.value;
  console.log(`${val} ${units}`);
}

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');

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

Path Format & Design Philosophy

Groups & Segments: Structure-Based Selection

Paths select whatever exists in the AST - segments OR groups:

// Groups are selectable!
const orderGroup = select(ast, 'ORDER');  
// Returns the ORDER group if it exists

// Segments are selectable!
const pidSegment = select(ast, 'PID');
// Returns the PID segment if it exists

// Groups also serve as navigation
select(ast, 'ORDER-ORC');         // Navigate through ORDER to ORC segment
select(ast, 'ORDER-TIMING-TQ1');  // Navigate through nested groups

// Field access definitively indicates segment access
select(ast, 'MSH-3');             // MSH must be a segment (has field access)
select(ast, 'ORDER-ORC-1');       // ORC must be a segment (has field access)

How It Works

  1. No field access (NAME): Returns segment OR group, whichever exists

    • Type: Segment | Group
    • Tries segments first, then groups
  2. With field access (NAME-N): Must be a segment

    • Type: Field | Component | Subcomponent
    • Field numbers indicate you're accessing segment internals
  3. AST as Source of Truth: The actual message structure determines what's returned

Why This Approach?

  • Maximum flexibility: Works with any valid HL7v2 names (standard 3-char segments, longer group names, custom segments)
  • Structure indicates intent: Field access (-N) tells us it's definitively a segment
  • Runtime correctness: AST determines what exists, not parsing rules
  • Type safety: TypeScript infers Segment | Group vs specific segment children

Practical Examples

// 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

// If both segment and group have same name, segment is prioritized
// (This is rare in practice since groups use descriptive names like ORDER, PATIENT)

Error Messages

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

Contributing

We welcome contributions! Please see our Contributing Guide for more details.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Code of Conduct

To ensure a welcoming and positive environment, we have a Code of Conduct that all contributors and participants are expected to adhere to.

License

Copyright 2025 Rethink Health, SUARL. All rights reserved.

This program is licensed to you under the terms of the MIT License. This program is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file for details.