@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.
- package/README.md +60 -127
- package/package.json +12 -8
package/README.md
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @glion/util-query
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Reads HL7v2 ASTs using canonical paths like `PID-5[1].2.1` or `ORDER-ORC-1`.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What it does
|
|
6
6
|
|
|
7
|
-
-
|
|
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
|
-
##
|
|
9
|
+
## Install
|
|
13
10
|
|
|
14
11
|
```bash
|
|
15
12
|
npm install @glion/util-query
|
|
16
13
|
```
|
|
17
14
|
|
|
18
|
-
##
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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`.
|
|
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
|
|
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
|
-
|
|
171
|
+
### Types
|
|
188
172
|
|
|
189
173
|
```typescript
|
|
190
174
|
type PathParts = {
|
|
@@ -197,9 +181,42 @@ type PathParts = {
|
|
|
197
181
|
};
|
|
198
182
|
```
|
|
199
183
|
|
|
200
|
-
##
|
|
184
|
+
## Path format
|
|
201
185
|
|
|
202
|
-
|
|
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
|
|
225
|
-
console.log(`${val} ${
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
263
|
+
## Part of Glion
|
|
330
264
|
|
|
331
|
-
|
|
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
|
-
[
|
|
334
|
-
[
|
|
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.
|
|
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.
|
|
37
|
-
"@glion/utils": "0.15.
|
|
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/
|
|
47
|
-
"@glion/
|
|
48
|
-
"@glion/
|
|
49
|
-
"@glion/tsconfig": "0.15.
|
|
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
|
}
|