@glion/util-query 0.15.1 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +60 -127
  2. package/package.json +12 -8
package/README.md CHANGED
@@ -1,21 +1,18 @@
1
- # HL7v2 Query Utility
1
+ # @glion/util-query
2
2
 
3
- A tiny helper for reading HL7v2 ASTs with familiar canonical paths.
3
+ Reads HL7v2 ASTs using canonical paths like `PID-5[1].2.1` or `ORDER-ORC-1`.
4
4
 
5
- ## Features
5
+ ## What it does
6
6
 
7
- - **Six verbs** – `parse`, `select`, `selectAll`, `value`, `matches`, `format`
8
- - **Canonical paths** – same syntax you see in HL7 specs
9
- - **Deep traversal** – walks nested groups automatically
10
- - **Zero fuss** – no options, no surprises, just results
7
+ 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.
11
8
 
12
- ## Installation
9
+ ## Install
13
10
 
14
11
  ```bash
15
12
  npm install @glion/util-query
16
13
  ```
17
14
 
18
- ## Quick Start
15
+ ## Use
19
16
 
20
17
  ```typescript
21
18
  import {
@@ -44,7 +41,7 @@ if (pidSegment?.node.type === "segment") {
44
41
  console.log(`PID has ${pidSegment.node.children.length - 1} fields`);
45
42
  }
46
43
 
47
- // Multiple nodes: get all matching elements
44
+ // Multiple nodes
48
45
  const allObservations = selectAll(ast, "OBX");
49
46
  for (const { node } of allObservations) {
50
47
  console.log(`Found observation: ${node.type}`);
@@ -57,14 +54,13 @@ if (matches(ast, "PID-5")) {
57
54
 
58
55
  // Path parsing
59
56
  const parsed = parse("ORDERS[2]-OBX-5[1].2.1");
60
- console.log(parsed);
61
57
  ```
62
58
 
63
59
  ## API
64
60
 
65
61
  ### `parse(path: string): PathParts`
66
62
 
67
- Breaks a canonical HL7 path into structured pieces. Helpful for tooling and diagnostics.
63
+ Breaks a canonical HL7 path into structured pieces. Useful for tooling and diagnostics.
68
64
 
69
65
  ```typescript
70
66
  parse("PID-5[1].2.1");
@@ -77,6 +73,8 @@ parse("PID-5[1].2.1");
77
73
  // }
78
74
  ```
79
75
 
76
+ `parse` memoizes up to 1,000 unique paths using an LRU cache so repeated lookups stay fast without leaking memory.
77
+
80
78
  ### `select<Path>(root: Root, path: Path): { node: InferNodeType<Path>; ancestors: Nodes[] } | null`
81
79
 
82
80
  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.
@@ -91,11 +89,9 @@ if (result) {
91
89
 
92
90
  The `ancestors` array follows the [`unist-util-visit-parents`](https://github.com/syntax-tree/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.
93
91
 
94
- `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).
95
-
96
92
  ### `selectAll<Path>(root: Root, path: Path): Array<{ node: InferNodeType<Path>; ancestors: Nodes[] }>`
97
93
 
98
- Returns all AST nodes (segments or groups) that match the path. Useful when a message contains multiple segments/groups of the same type.
94
+ Returns all AST nodes that match the path. Useful when a message contains multiple segments or groups of the same type.
99
95
 
100
96
  ```typescript
101
97
  // Get all OBX segments
@@ -119,7 +115,7 @@ for (const { node } of values) {
119
115
 
120
116
  ### `value(root: Root, path: string): { value: string; node: Nodes; ancestors: Nodes[] } | null`
121
117
 
122
- 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.
118
+ 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.
123
119
 
124
120
  ```typescript
125
121
  const result = value(ast, "PID-3[1].1.1");
@@ -132,14 +128,13 @@ if (result) {
132
128
 
133
129
  ### `matches(root: Root, path: string): boolean`
134
130
 
135
- Returns `true` when the path points to an existing node, otherwise `false`. More semantically clear than checking for `null`.
131
+ Returns `true` when the path points to an existing node, otherwise `false`.
136
132
 
137
133
  ```typescript
138
134
  if (!matches(ast, "OBX-5")) {
139
135
  throw new Error("Missing observation value");
140
136
  }
141
137
 
142
- // Use in conditionals
143
138
  if (matches(ast, "PID-5")) {
144
139
  const name = value(ast, "PID-5.1.1");
145
140
  }
@@ -171,20 +166,9 @@ Clears the memoized parse results. Useful for long-running services that want to
171
166
 
172
167
  ### `getParseCacheSize(): number`
173
168
 
174
- Returns the current number of cached path entries so you can monitor cache pressure in diagnostics or tests.
175
-
176
- ## Path Cheatsheet
177
-
178
- - `SEGMENT` – segment (e.g. `PID`)
179
- - `SEGMENT-FIELD` – field (e.g. `PID-5`)
180
- - `SEGMENT-FIELD[REP]` – field repetition (e.g. `PID-5[2]`)
181
- - `SEGMENT-FIELD[REP].COMP` – component (e.g. `PID-5[1].2`)
182
- - `SEGMENT-FIELD[REP].COMP.SUB` – subcomponent (e.g. `PID-5[1].2.1`)
183
- - `GROUP[N]-...` – qualify with group name and optional repetition (e.g. `ORDERS[2]-OBX-5`)
184
-
185
- Repetition indexes are **1-based**. If you omit `[n]`, `find` assumes `[1]`.
169
+ Returns the current number of cached path entries so you can monitor cache pressure.
186
170
 
187
- ## Types
171
+ ### Types
188
172
 
189
173
  ```typescript
190
174
  type PathParts = {
@@ -197,9 +181,42 @@ type PathParts = {
197
181
  };
198
182
  ```
199
183
 
200
- ## Common Recipes
184
+ ## Path format
201
185
 
202
- ### Patient Demographics
186
+ Canonical path grammar:
187
+
188
+ - `SEGMENT` — segment (e.g., `PID`)
189
+ - `SEGMENT-FIELD` — field (e.g., `PID-5`)
190
+ - `SEGMENT-FIELD[REP]` — field repetition (e.g., `PID-5[2]`)
191
+ - `SEGMENT-FIELD[REP].COMP` — component (e.g., `PID-5[1].2`)
192
+ - `SEGMENT-FIELD[REP].COMP.SUB` — subcomponent (e.g., `PID-5[1].2.1`)
193
+ - `GROUP[N]-...` — qualify with group name and optional repetition (e.g., `ORDERS[2]-OBX-5`)
194
+
195
+ Repetition indexes are **1-based**. If you omit `[n]`, `select`/`value`/`matches` assume `[1]`.
196
+
197
+ 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`).
198
+
199
+ ```typescript
200
+ // Selecting groups directly
201
+ const orderGroup = select(ast, "ORDER");
202
+ if (orderGroup?.node.type === "group") {
203
+ console.log("Found ORDER group");
204
+ }
205
+
206
+ // Selecting segments
207
+ const pid = select(ast, "PID");
208
+ if (pid?.node.type === "segment") {
209
+ console.log("Found PID segment");
210
+ }
211
+
212
+ // Navigation through groups to segments
213
+ value(ast, "ORDER-ORC-1"); // Order control
214
+ value(ast, "ORDER-TIMING-TQ1-1"); // Timing quantity
215
+ ```
216
+
217
+ ## Common recipes
218
+
219
+ ### Patient demographics
203
220
 
204
221
  ```typescript
205
222
  const lastName = value(ast, "PID-5[1].1.1")?.value;
@@ -212,7 +229,6 @@ const dob = value(ast, "PID-7[1].1.1")?.value;
212
229
 
213
230
  ```typescript
214
231
  // Single observation
215
- const obx = select(ast, "OBX");
216
232
  const obsValue = value(ast, "OBX-5[1].1.1")?.value;
217
233
  const units = value(ast, "OBX-6[1].1.1")?.value;
218
234
  const refRange = value(ast, "OBX-7[1].1.1")?.value;
@@ -221,8 +237,8 @@ const refRange = value(ast, "OBX-7[1].1.1")?.value;
221
237
  const allOBX = selectAll(ast, "OBX");
222
238
  for (const { node } of allOBX) {
223
239
  const val = value(node, "OBX-5")?.value;
224
- const units = value(node, "OBX-6")?.value;
225
- console.log(`${val} ${units}`);
240
+ const u = value(node, "OBX-6")?.value;
241
+ console.log(`${val} ${u}`);
226
242
  }
227
243
  ```
228
244
 
@@ -232,104 +248,21 @@ for (const { node } of allOBX) {
232
248
  if (!matches(ast, "MSH")) throw new Error("Missing message header");
233
249
  if (!matches(ast, "PID-3[1].1.1")) throw new Error("Missing patient ID");
234
250
 
235
- // Conditional processing
236
251
  if (matches(ast, "OBX")) {
237
252
  const observations = selectAll(ast, "OBX");
238
253
  console.log(`Found ${observations.length} observations`);
239
254
  }
240
255
  ```
241
256
 
242
- ## Path Format & Design Philosophy
243
-
244
- ### Groups & Segments: Structure-Based Selection
245
-
246
- **Paths select whatever exists in the AST - segments OR groups:**
247
-
248
- ```typescript
249
- // Groups are selectable!
250
- const orderGroup = select(ast, "ORDER");
251
- // Returns the ORDER group if it exists
252
-
253
- // Segments are selectable!
254
- const pidSegment = select(ast, "PID");
255
- // Returns the PID segment if it exists
256
-
257
- // Groups also serve as navigation
258
- select(ast, "ORDER-ORC"); // Navigate through ORDER to ORC segment
259
- select(ast, "ORDER-TIMING-TQ1"); // Navigate through nested groups
260
-
261
- // Field access definitively indicates segment access
262
- select(ast, "MSH-3"); // MSH must be a segment (has field access)
263
- select(ast, "ORDER-ORC-1"); // ORC must be a segment (has field access)
264
- ```
265
-
266
- ### How It Works
267
-
268
- 1. **No field access (`NAME`)**: Returns segment OR group, whichever exists
269
- - Type: `Segment | Group`
270
- - Tries segments first, then groups
271
- 2. **With field access (`NAME-N`)**: Must be a segment
272
- - Type: `Field | Component | Subcomponent`
273
- - Field numbers indicate you're accessing segment internals
274
-
275
- 3. **AST as Source of Truth**: The actual message structure determines what's returned
276
-
277
- ### Why This Approach?
278
-
279
- - ✅ **Maximum flexibility**: Works with any valid HL7v2 names (standard 3-char segments, longer group names, custom segments)
280
- - ✅ **Structure indicates intent**: Field access (`-N`) tells us it's definitively a segment
281
- - ✅ **Runtime correctness**: AST determines what exists, not parsing rules
282
- - ✅ **Type safety**: TypeScript infers `Segment | Group` vs specific segment children
283
-
284
- ### Practical Examples
285
-
286
- ```typescript
287
- // Selecting groups directly
288
- const orderGroup = select(ast, "ORDER");
289
- if (orderGroup?.node.type === "group") {
290
- console.log("Found ORDER group");
291
- }
292
-
293
- // Selecting segments
294
- const pid = select(ast, "PID");
295
- if (pid?.node.type === "segment") {
296
- console.log("Found PID segment");
297
- }
298
-
299
- // Navigation through groups to segments
300
- value(ast, "ORDER-ORC-1"); // ✅ Order control
301
- value(ast, "ORDER-TIMING-TQ1-1"); // ✅ Timing quantity
302
-
303
- // If both segment and group have same name, segment is prioritized
304
- // (This is rare in practice since groups use descriptive names like ORDER, PATIENT)
305
- ```
306
-
307
- ## Error Messages
308
-
309
- - Invalid formats throw with guidance, e.g. `"Invalid HL7 path format: \"PID-\""`
310
- - Out-of-range indexes complain, e.g. `"Field number must be ≥1, got: 0"`
311
- - Whitespace is rejected to avoid accidental typos
312
-
313
- ## Contributing
314
-
315
- We welcome contributions! Please see our [Contributing Guide][github-contributing] for more details.
316
-
317
- 1. Fork the repository
318
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
319
- 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
320
- 4. Push to the branch (`git push origin feature/amazing-feature`)
321
- 5. Open a Pull Request
322
-
323
- ## Code of Conduct
324
-
325
- To ensure a welcoming and positive environment, we have a [Code of Conduct][github-code-of-conduct] that all contributors and participants are expected to adhere to.
257
+ ## Error messages
326
258
 
327
- ## License
259
+ - Invalid formats throw with guidance, e.g. `"Invalid HL7 path format: \"PID-\""`.
260
+ - Out-of-range indexes throw, e.g. `"Field number must be ≥1, got: 0"`.
261
+ - Whitespace is rejected to catch accidental typos.
328
262
 
329
- Copyright 2025 Rethink Health, SUARL. All rights reserved.
263
+ ## Part of Glion
330
264
 
331
- This program is licensed to you under the terms of the [MIT License](https://opensource.org/licenses/MIT). This program is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the [LICENSE][github-license] file for details.
265
+ `@glion/util-query` is part of **[Glion]**, the application framework for HL7v2. See the [Glion README] for the full package catalog and architecture.
332
266
 
333
- [github-code-of-conduct]: https://github.com/rethinkhealth/glion/blob/main/CODE_OF_CONDUCT.md
334
- [github-license]: https://github.com/rethinkhealth/glion/blob/main/LICENSE
335
- [github-contributing]: https://github.com/rethinkhealth/glion/blob/main/CONTRIBUTING.md
267
+ [Glion]: https://github.com/rethinkhealth/glion#readme
268
+ [Glion README]: https://github.com/rethinkhealth/glion#readme
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glion/util-query",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "description": "Query HL7v2 AST nodes by HL7 location paths",
5
5
  "keywords": [
6
6
  "health",
@@ -33,20 +33,22 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@glion/ast": "0.15.1",
37
- "@glion/utils": "0.15.1"
36
+ "@glion/ast": "0.15.2",
37
+ "@glion/utils": "0.15.2"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^25.5.0",
41
41
  "@types/unist": "^3.0.3",
42
42
  "@vitest/coverage-v8": "4.1.2",
43
+ "publint": "0.3.18",
43
44
  "tsdown": "0.21.7",
44
45
  "typescript": "^5.9.3",
45
46
  "vitest": "4.1.2",
46
- "@glion/parser": "0.15.1",
47
- "@glion/testing": "0.15.1",
48
- "@glion/builder": "0.15.1",
49
- "@glion/tsconfig": "0.15.1"
47
+ "@glion/builder": "0.15.2",
48
+ "@glion/check-readme": "0.15.2",
49
+ "@glion/parser": "0.15.2",
50
+ "@glion/tsconfig": "0.15.2",
51
+ "@glion/testing": "0.15.2"
50
52
  },
51
53
  "engines": {
52
54
  "node": ">=18"
@@ -55,8 +57,10 @@
55
57
  "scripts": {
56
58
  "build": "tsdown && tsc --emitDeclarationOnly",
57
59
  "check-types": "tsc --noEmit",
60
+ "check:readme": "glion-check-readme",
58
61
  "test": "vitest run",
59
62
  "test:coverage": "vitest run --coverage",
60
- "test:watch": "vitest"
63
+ "test:watch": "vitest",
64
+ "publint": "publint --strict"
61
65
  }
62
66
  }