@glion/util-query 0.14.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Rethink Health
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,335 @@
1
+ # HL7v2 Query Utility
2
+
3
+ A tiny helper for reading HL7v2 ASTs with familiar canonical paths.
4
+
5
+ ## Features
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
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @glion/util-query
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```typescript
21
+ import {
22
+ select,
23
+ selectAll,
24
+ value,
25
+ matches,
26
+ format,
27
+ parse,
28
+ } from "@glion/util-query";
29
+ import { parseHL7v2 } from "@glion/parser";
30
+
31
+ const message = `MSH|^~\\&|MyApp|MyFacility|ReceivingApp|ReceivingFacility|20231201120000||ADT^A01|12345|P|2.5
32
+ PID|1||123456789^^^MRN||Smith^John^Michael||19800101|M|||123 Main St^^Anytown^ST^12345
33
+ OBX|1||8675-3^Glucose^LN||120|mg/dL|70-105|H|||F`;
34
+
35
+ const ast = parseHL7v2(message);
36
+
37
+ // Values: drills to subcomponents automatically when the path is unique
38
+ const lastName = value(ast, "PID-5[1].1.1"); // "Smith"
39
+ const sendingApp = value(ast, "MSH-3[1].1.1"); // "MyApp"
40
+
41
+ // Single node: grab the first matching AST node
42
+ const pidSegment = select(ast, "PID");
43
+ if (pidSegment?.node.type === "segment") {
44
+ console.log(`PID has ${pidSegment.node.children.length - 1} fields`);
45
+ }
46
+
47
+ // Multiple nodes: get all matching elements
48
+ const allObservations = selectAll(ast, "OBX");
49
+ for (const { node } of allObservations) {
50
+ console.log(`Found observation: ${node.type}`);
51
+ }
52
+
53
+ // Existence check
54
+ if (matches(ast, "PID-5")) {
55
+ console.log("Patient has a name field");
56
+ }
57
+
58
+ // Path parsing
59
+ const parsed = parse("ORDERS[2]-OBX-5[1].2.1");
60
+ console.log(parsed);
61
+ ```
62
+
63
+ ## API
64
+
65
+ ### `parse(path: string): PathParts`
66
+
67
+ Breaks a canonical HL7 path into structured pieces. Helpful for tooling and diagnostics.
68
+
69
+ ```typescript
70
+ parse("PID-5[1].2.1");
71
+ // {
72
+ // segment: { name: 'PID' },
73
+ // field: 5,
74
+ // repetition: 1,
75
+ // component: 2,
76
+ // subcomponent: 1
77
+ // }
78
+ ```
79
+
80
+ ### `select<Path>(root: Root, path: Path): { node: InferNodeType<Path>; ancestors: Nodes[] } | null`
81
+
82
+ 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.
83
+
84
+ ```typescript
85
+ const result = select(ast, "PID-5[1].2");
86
+ if (result) {
87
+ result.node; // Type: Component
88
+ result.ancestors; // [Root, Segment, Field, FieldRepetition]
89
+ }
90
+ ```
91
+
92
+ 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
+
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
+ ### `selectAll<Path>(root: Root, path: Path): Array<{ node: InferNodeType<Path>; ancestors: Nodes[] }>`
97
+
98
+ Returns all AST nodes (segments or groups) that match the path. Useful when a message contains multiple segments/groups of the same type.
99
+
100
+ ```typescript
101
+ // Get all OBX segments
102
+ const observations = selectAll(ast, "OBX");
103
+ for (const { node } of observations) {
104
+ console.log(node.type); // 'segment'
105
+ }
106
+
107
+ // Get all ORDER groups
108
+ const orders = selectAll(ast, "ORDER");
109
+ for (const { node } of orders) {
110
+ console.log(node.type); // 'group'
111
+ }
112
+
113
+ // Get all observation values
114
+ const values = selectAll(ast, "OBX-5");
115
+ for (const { node } of values) {
116
+ console.log(node.type); // 'field'
117
+ }
118
+ ```
119
+
120
+ ### `value(root: Root, path: string): { value: string; node: Nodes; ancestors: Nodes[] } | null`
121
+
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.
123
+
124
+ ```typescript
125
+ const result = value(ast, "PID-3[1].1.1");
126
+ if (result) {
127
+ console.log(result.value); // "123456789"
128
+ console.log(result.node.type); // "subcomponent"
129
+ console.log(result.ancestors); // [Root, Segment, Field, ...]
130
+ }
131
+ ```
132
+
133
+ ### `matches(root: Root, path: string): boolean`
134
+
135
+ Returns `true` when the path points to an existing node, otherwise `false`. More semantically clear than checking for `null`.
136
+
137
+ ```typescript
138
+ if (!matches(ast, "OBX-5")) {
139
+ throw new Error("Missing observation value");
140
+ }
141
+
142
+ // Use in conditionals
143
+ if (matches(ast, "PID-5")) {
144
+ const name = value(ast, "PID-5.1.1");
145
+ }
146
+ ```
147
+
148
+ ### `format(node: Nodes, ancestors: Nodes[]): string | null`
149
+
150
+ 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.
151
+
152
+ ```typescript
153
+ // With select
154
+ const result = select(ast, "PID-5.2");
155
+ if (result) {
156
+ format(result.node, result.ancestors); // "PID-5.2"
157
+ }
158
+
159
+ // With visit
160
+ visit(ast, "field", (node, ancestors) => {
161
+ const path = format(node, ancestors); // "PID-5", "OBX-1", etc.
162
+ console.log(`Visiting ${path}`);
163
+ });
164
+
165
+ // Round-trip: format(select(root, path)) === path
166
+ ```
167
+
168
+ ### `clearParseCache(): void`
169
+
170
+ Clears the memoized parse results. Useful for long-running services that want to release memory during idle periods or before reloading configuration.
171
+
172
+ ### `getParseCacheSize(): number`
173
+
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]`.
186
+
187
+ ## Types
188
+
189
+ ```typescript
190
+ type PathParts = {
191
+ groups?: Array<{ name: string; repetition?: number }>;
192
+ segment: { name: string; repetition?: number };
193
+ field?: number;
194
+ repetition?: number;
195
+ component?: number;
196
+ subcomponent?: number;
197
+ };
198
+ ```
199
+
200
+ ## Common Recipes
201
+
202
+ ### Patient Demographics
203
+
204
+ ```typescript
205
+ const lastName = value(ast, "PID-5[1].1.1")?.value;
206
+ const firstName = value(ast, "PID-5[1].2.1")?.value;
207
+ const middleName = value(ast, "PID-5[1].3.1")?.value;
208
+ const dob = value(ast, "PID-7[1].1.1")?.value;
209
+ ```
210
+
211
+ ### Observations
212
+
213
+ ```typescript
214
+ // Single observation
215
+ const obx = select(ast, "OBX");
216
+ const obsValue = value(ast, "OBX-5[1].1.1")?.value;
217
+ const units = value(ast, "OBX-6[1].1.1")?.value;
218
+ const refRange = value(ast, "OBX-7[1].1.1")?.value;
219
+
220
+ // Multiple observations
221
+ const allOBX = selectAll(ast, "OBX");
222
+ for (const { node } of allOBX) {
223
+ const val = value(node, "OBX-5")?.value;
224
+ const units = value(node, "OBX-6")?.value;
225
+ console.log(`${val} ${units}`);
226
+ }
227
+ ```
228
+
229
+ ### Validation
230
+
231
+ ```typescript
232
+ if (!matches(ast, "MSH")) throw new Error("Missing message header");
233
+ if (!matches(ast, "PID-3[1].1.1")) throw new Error("Missing patient ID");
234
+
235
+ // Conditional processing
236
+ if (matches(ast, "OBX")) {
237
+ const observations = selectAll(ast, "OBX");
238
+ console.log(`Found ${observations.length} observations`);
239
+ }
240
+ ```
241
+
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.
326
+
327
+ ## License
328
+
329
+ Copyright 2025 Rethink Health, SUARL. All rights reserved.
330
+
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.
332
+
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
@@ -0,0 +1,15 @@
1
+ import type { Delimiters, Root } from "@glion/ast";
2
+ /**
3
+ * Extract delimiters from a Root AST by reading MSH-1 and MSH-2.
4
+ *
5
+ * MSH-1 contains the field separator character (e.g., `|`).
6
+ * MSH-2 contains up to 4 encoding characters: component, repetition,
7
+ * escape, and subcomponent (e.g., `^~\&`).
8
+ *
9
+ * Falls back to DEFAULT_DELIMITERS for any values that cannot be derived.
10
+ *
11
+ * @param root - The Root AST node to extract delimiters from.
12
+ * @returns The resolved Delimiters object.
13
+ */
14
+ export declare function delimiters(root: Root): Delimiters;
15
+ //# sourceMappingURL=delimiters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delimiters.d.ts","sourceRoot":"","sources":["../src/delimiters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAanD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,CAsBjD"}
@@ -0,0 +1,26 @@
1
+ import type { Nodes } from "@glion/ast";
2
+ /**
3
+ * Format a node and its ancestor chain into a canonical HL7v2 path string.
4
+ *
5
+ * This is the inverse of `select`: given a node and the ancestors
6
+ * returned by `visit` or `select`, it produces the path string that
7
+ * would select that node.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * visit(tree, "field", (node, ancestors) => {
12
+ * format(node, ancestors); // "PID-5"
13
+ * });
14
+ *
15
+ * const result = select(root, "PID-5.2");
16
+ * format(result.node, result.ancestors); // "PID-5.2"
17
+ * ```;
18
+ *
19
+ * @param node - The target AST node
20
+ * @param ancestors - Ancestor chain from root to parent (as provided by
21
+ * visit/select)
22
+ * @returns Canonical path string (e.g., "PID-5.2", "ORDER-ORC-1"), or `null` if the
23
+ * ancestor chain contains no segment or group
24
+ */
25
+ export declare function format(node: Nodes, ancestors: Nodes[]): string | null;
26
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,wBAAgB,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,MAAM,GAAG,IAAI,CAmDrE"}
@@ -0,0 +1,9 @@
1
+ export { delimiters } from "./delimiters";
2
+ export { format } from "./format";
3
+ export { matches } from "./matches";
4
+ export { parse } from "./parse";
5
+ export { select, selectAll } from "./select";
6
+ export { set } from "./set";
7
+ export type { GroupLocator, InferNodeType, PathParts, SegmentLocator, } from "./types";
8
+ export { value } from "./value";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,YAAY,EACV,YAAY,EACZ,aAAa,EACb,SAAS,EACT,cAAc,GACf,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC"}