@glion/util-query 0.25.0 → 0.27.0

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/dist/index.js CHANGED
@@ -1,103 +1,113 @@
1
1
  import { DEFAULT_DELIMITERS } from "@glion/utils";
2
- //#region src/parse.ts
2
+ //#region src/errors.ts
3
3
  /**
4
- * Regular expression for canonical HL7 paths.
5
- *
6
- * Format:
7
- * [GROUP[N]-]...[GROUP[N]-]NAME[N][-FIELD[REPETITION][.COMPONENT[.SUBCOMPONENT]]]
8
- *
9
- * **Design Philosophy**: Groups vs Segments are distinguished by POSITION and
10
- * USAGE, not name length:
11
- *
12
- * - **Navigation (Groups)**: Names appearing as prefixes (before the final name)
13
- * are treated as group navigation: `ORDER-ORC`, `ORDER-TIMING-TQ1`
14
- * - **Target (Segments)**: The final name in the path is the query target.
15
- * Whether it's a segment or group is determined by:
16
- *
17
- * 1. Field access (-.digit): Definitively a segment → `ORC-1`, `ORDER-ORC-1`
18
- * 2. No field access: Query the AST to see what exists → `ORC`, `ORDER`
4
+ * The typed failure for `@glion/util-query`. Branch on `code`; each code is
5
+ * one cause.
19
6
  *
20
- * This approach:
21
- *
22
- * - Makes no assumptions about name length (works with standard 3-char segments
23
- * AND edge cases)
24
- * - Lets the path structure indicate intent (field access = segment internals)
25
- * - Allows the AST to be the source of truth for what exists
26
- *
27
- * Examples:
28
- *
29
- * - `MSH-3` → MSH is segment (has field access)
30
- * - `ORDER-ORC` → ORDER is navigation, ORC is target (could be segment or nested
31
- * group)
32
- * - `ORDER-ORC-1` → ORDER is navigation, ORC is segment (has field access)
7
+ * @module
33
8
  */
34
- const PATH_REGEX = /^((?:[A-Z][A-Z0-9_]*(?:\[\d+\])?-)*)([A-Z][A-Z0-9_]*)(?:\[(\d+)\])?(?:-(\d+)(?:(?:\[(\d+)\])?(?:\.(\d+)(?:\.(\d+))?)?)?)?$/;
35
9
  /**
36
- * Cache for parsed paths to avoid re-parsing the same path multiple times.
37
- * No eviction — the realistic set of unique paths in an application is
38
- * small (50-100 entries) and the memory cost is negligible.
10
+ * Machine-readable codes for what a path got wrong:
11
+ *
12
+ * - `EMPTY_PATH` — the path is empty.
13
+ * - `INVALID_PATH` — the path does not follow the path grammar.
39
14
  */
40
- const parseCache = /* @__PURE__ */ new Map();
15
+ const QueryErrorCode = {
16
+ EMPTY_PATH: "EMPTY_PATH",
17
+ INVALID_PATH: "INVALID_PATH"
18
+ };
19
+ function sentence(code, path) {
20
+ switch (code) {
21
+ case "EMPTY_PATH": return "The path is empty; a path is `[GROUP[N].]...NAME[N][.FIELD[REPETITION][.COMPONENT[.SUBCOMPONENT]]]`, or `FIELD[REPETITION][.COMPONENT[.SUBCOMPONENT]]` read from a segment, with positions from 1, such as `PID.5.1` or `5.1`.";
22
+ case "INVALID_PATH": return `Path \`${path}\` does not follow the path grammar; a path is \`[GROUP[N].]...NAME[N][.FIELD[REPETITION][.COMPONENT[.SUBCOMPONENT]]]\`, or \`FIELD[REPETITION][.COMPONENT[.SUBCOMPONENT]]\` read from a segment, with positions from 1, such as \`PID.5.1\` or \`5.1\`.`;
23
+ }
24
+ }
25
+ /** A path that `@glion/util-query` cannot read. */
26
+ var QueryError = class extends Error {
27
+ name = "QueryError";
28
+ code;
29
+ path;
30
+ constructor(code, path) {
31
+ super(sentence(code, path));
32
+ this.code = code;
33
+ this.path = path;
34
+ }
35
+ };
36
+ //#endregion
37
+ //#region src/parse.ts
41
38
  /**
42
- * Pre-computed PathParts for frequently accessed MSH fields.
39
+ * Canonical HL7 path grammar, every step separated with `.`:
40
+ *
41
+ * [GROUP[N].]...NAME[N][.FIELD[REPETITION][.COMPONENT[.SUBCOMPONENT]]]
42
+ * FIELD[REPETITION][.COMPONENT[.SUBCOMPONENT]]
43
43
  *
44
- * Multiple plugins and lint rules read MSH-9 (message type) and MSH-12
45
- * (version) on every message. Skipping regex parsing and cache lookups
46
- * for these paths eliminates ~100ns per call.
44
+ * Names start with a letter and positions are numbers from 1, so the first
45
+ * numeric step is the field. The final name is the query target. Field
46
+ * access makes it a segment (`ORDER.ORC.1`); without it the AST decides
47
+ * whether it names a segment or a group (`ORDER.ORC`, `ORDER`). A path that
48
+ * starts with the field names no segment (`5.1`).
47
49
  */
48
- const HOT_PATHS = {
49
- "MSH-10": {
50
- field: 10,
51
- segment: { name: "MSH" }
52
- },
53
- "MSH-12": {
54
- field: 12,
55
- segment: { name: "MSH" }
56
- },
57
- "MSH-3": {
50
+ const PATH_REGEX = /^(?:([A-Z][A-Z0-9_]*(?:\[[1-9]\d*\])?(?:\.[A-Z][A-Z0-9_]*(?:\[[1-9]\d*\])?)*)(?:\.(?=[1-9])|$))?(?:([1-9]\d*)(?:\[([1-9]\d*)\])?(?:\.([1-9]\d*)(?:\.([1-9]\d*))?)?)?$/;
51
+ const NAVIGATION_STEP_REGEX = /([A-Z][A-Z0-9_]*)(?:\[(\d+)\])?/g;
52
+ /** Parts of the MSH paths that plugins and lint rules read on every message. */
53
+ const PRECOMPUTED_PATHS = new Map([
54
+ ["MSH.3", () => ({
58
55
  field: 3,
59
56
  segment: { name: "MSH" }
60
- },
61
- "MSH-4": {
57
+ })],
58
+ ["MSH.4", () => ({
62
59
  field: 4,
63
60
  segment: { name: "MSH" }
64
- },
65
- "MSH-5": {
61
+ })],
62
+ ["MSH.5", () => ({
66
63
  field: 5,
67
64
  segment: { name: "MSH" }
68
- },
69
- "MSH-6": {
65
+ })],
66
+ ["MSH.6", () => ({
70
67
  field: 6,
71
68
  segment: { name: "MSH" }
72
- },
73
- "MSH-9": {
69
+ })],
70
+ ["MSH.9", () => ({
74
71
  field: 9,
75
72
  segment: { name: "MSH" }
76
- },
77
- "MSH-9.1": {
73
+ })],
74
+ ["MSH.9.1", () => ({
78
75
  component: 1,
79
76
  field: 9,
80
77
  segment: { name: "MSH" }
81
- },
82
- "MSH-9.2": {
78
+ })],
79
+ ["MSH.9.2", () => ({
83
80
  component: 2,
84
81
  field: 9,
85
82
  segment: { name: "MSH" }
86
- },
87
- "MSH-9.3": {
83
+ })],
84
+ ["MSH.9.3", () => ({
88
85
  component: 3,
89
86
  field: 9,
90
87
  segment: { name: "MSH" }
91
- }
92
- };
88
+ })],
89
+ ["MSH.10", () => ({
90
+ field: 10,
91
+ segment: { name: "MSH" }
92
+ })],
93
+ ["MSH.12", () => ({
94
+ field: 12,
95
+ segment: { name: "MSH" }
96
+ })],
97
+ ["MSH.12.1", () => ({
98
+ component: 1,
99
+ field: 12,
100
+ segment: { name: "MSH" }
101
+ })]
102
+ ]);
93
103
  /**
94
104
  * Parse an HL7 path string into structured parts.
95
- * Results are memoized for performance. Common MSH paths use
96
- * pre-computed constants to skip parsing entirely.
105
+ *
106
+ * Returns a new object on every call.
97
107
  *
98
108
  * @example
99
109
  * ```typescript
100
- * parse("PID-5[1].2.1");
110
+ * parse("PID.5[1].2.1");
101
111
  * // {
102
112
  * // segment: { name: 'PID' },
103
113
  * // field: 5,
@@ -111,387 +121,195 @@ const HOT_PATHS = {
111
121
  * @returns Structured path components
112
122
  */
113
123
  function parse(path) {
114
- const hot = HOT_PATHS[path];
115
- if (hot) return hot;
116
- const cached = parseCache.get(path);
117
- if (cached) return cached;
118
- const result = parseImpl(path);
119
- parseCache.set(path, result);
120
- return result;
121
- }
122
- /**
123
- * Internal implementation of path parsing.
124
- */
125
- function parseImpl(path) {
126
- if (!path || typeof path !== "string") throw new Error(`Path must be a non-empty string, got: ${path}`);
127
- if (path.trim() !== path) throw new Error(`Path cannot contain leading/trailing spaces: "${path}"`);
124
+ const precomputed = PRECOMPUTED_PATHS.get(path);
125
+ if (precomputed) return precomputed();
126
+ if (path === "") throw new QueryError("EMPTY_PATH", path);
128
127
  const match = PATH_REGEX.exec(path);
129
- if (!match) throw new Error(`Invalid HL7 path format: "${path}". Expected [GROUP[N]-]...NAME[N][-FIELD[REPETITION][.COMPONENT[.SUBCOMPONENT]]].`);
130
- const [, groupsRaw, segmentName, segmentRep, fieldStr, repetitionStr, componentStr, subcomponentStr] = match;
131
- if (!segmentName) throw new Error(`Path must include a segment identifier: "${path}"`);
132
- const parts = { segment: { name: segmentName } };
133
- if (segmentRep) {
134
- const repetition = Number.parseInt(segmentRep, 10);
135
- if (repetition < 1) throw new Error(`Segment repetition must be ≥1, got: ${segmentRep} in "${path}"`);
136
- parts.segment.repetition = repetition;
137
- }
138
- if (groupsRaw) {
139
- const groups = [];
140
- for (const groupMatch of groupsRaw.matchAll(/([A-Z][A-Z0-9_]*)(?:\[(\d+)\])?-/g)) {
141
- const name = groupMatch[1];
142
- if (!name) continue;
143
- const repStr = groupMatch[2];
144
- const group = { name };
145
- if (repStr) {
146
- const repetition = Number.parseInt(repStr, 10);
147
- if (repetition < 1) throw new Error(`Group repetition must be ≥1, got: ${repStr} in "${path}"`);
148
- group.repetition = repetition;
149
- }
150
- groups.push(group);
151
- }
152
- if (groups.length > 0) parts.groups = groups;
153
- }
154
- if (fieldStr) {
155
- const field = Number.parseInt(fieldStr, 10);
156
- if (field < 1) throw new Error(`Field number must be ≥1, got: ${fieldStr}`);
157
- parts.field = field;
158
- }
159
- if (repetitionStr) {
160
- const repetition = Number.parseInt(repetitionStr, 10);
161
- if (repetition < 1) throw new Error(`Repetition number must be ≥1, got: ${repetitionStr}`);
162
- parts.repetition = repetition;
163
- }
164
- if (componentStr) {
165
- const component = Number.parseInt(componentStr, 10);
166
- if (component < 1) throw new Error(`Component number must be ≥1, got: ${componentStr}`);
167
- parts.component = component;
168
- }
169
- if (subcomponentStr) {
170
- const subcomponent = Number.parseInt(subcomponentStr, 10);
171
- if (subcomponent < 1) throw new Error(`Subcomponent number must be ≥1, got: ${subcomponentStr}`);
172
- parts.subcomponent = subcomponent;
173
- }
128
+ if (!match) throw new QueryError("INVALID_PATH", path);
129
+ const [, navigation = "", field, repetition, component, subcomponent] = match;
130
+ const steps = [];
131
+ for (const [, name, position] of navigation.matchAll(NAVIGATION_STEP_REGEX)) steps.push(position === void 0 ? { name } : {
132
+ name,
133
+ repetition: Number(position)
134
+ });
135
+ const parts = {};
136
+ const segment = steps.pop();
137
+ if (segment !== void 0) parts.segment = segment;
138
+ if (steps.length > 0) parts.groups = steps;
139
+ if (field !== void 0) parts.field = Number(field);
140
+ if (repetition !== void 0) parts.repetition = Number(repetition);
141
+ if (component !== void 0) parts.component = Number(component);
142
+ if (subcomponent !== void 0) parts.subcomponent = Number(subcomponent);
174
143
  return parts;
175
144
  }
176
145
  //#endregion
177
- //#region src/utils.ts
178
- function locateSegmentOrGroup(root, parts) {
179
- const ancestors = [root];
180
- const scope = followGroups(root, parts.groups ?? [], ancestors);
181
- if (!scope) return null;
182
- const targetName = parts.segment.name;
183
- const targetRepetition = parts.segment.repetition;
184
- const matchedSegments = collectSegments(scope, targetName, ancestors);
185
- if (matchedSegments.length > 0) {
186
- const match = matchedSegments[(targetRepetition ?? 1) - 1];
187
- if (match) return [match.segment, match.ancestors];
146
+ //#region src/resolve.ts
147
+ /** The name a path gives a node: a segment's name, or a group's ID. */
148
+ const keyOf = (node) => {
149
+ switch (node.type) {
150
+ case "root":
151
+ case "field":
152
+ case "field-repetition":
153
+ case "component":
154
+ case "subcomponent": return;
155
+ case "segment": return node.name;
156
+ case "group": return node.id;
188
157
  }
189
- const matchedGroups = collectGroups(scope, targetName, ancestors);
190
- if (matchedGroups.length > 0) {
191
- const match = matchedGroups[(targetRepetition ?? 1) - 1];
192
- if (match) return [match.group, match.ancestors];
193
- }
194
- return null;
195
- }
196
- function locateSegment(root, parts) {
197
- const ancestors = [root];
198
- const scope = followGroups(root, parts.groups ?? [], ancestors);
199
- if (!scope) return null;
200
- const matchedSegments = collectSegments(scope, parts.segment.name, ancestors);
201
- if (matchedSegments.length === 0) return null;
202
- const match = matchedSegments[(parts.segment.repetition ?? 1) - 1];
203
- if (!match) return null;
204
- return [match.segment, match.ancestors];
205
- }
206
- function followGroups(root, groups, ancestorChain) {
207
- let children = filterSegmentsAndGroups(root.children);
208
- for (const locator of groups) {
209
- const matchedGroups = children.filter((node) => node.type === "group" && node.id === locator.name);
210
- if (matchedGroups.length === 0) return null;
211
- const selected = matchedGroups[(locator.repetition ?? 1) - 1];
212
- if (!selected) return null;
213
- ancestorChain.push(selected);
214
- children = filterSegmentsAndGroups(selected.children);
158
+ };
159
+ /**
160
+ * The `position`th of `children` (1-based) when given; otherwise every one
161
+ * when `every` is set, or the first.
162
+ */
163
+ function pick(children, position, every) {
164
+ if (position !== void 0) {
165
+ const child = children[position - 1];
166
+ return child === void 0 ? [] : [child];
215
167
  }
216
- return children;
168
+ return every ? children : children.slice(0, 1);
217
169
  }
218
170
  /**
219
- * Collect all possible scopes when groups can match multiple instances.
220
- * Used by selectAll to find nodes across all matching groups.
171
+ * The children of `parent` a path step names `name`, segments and groups
172
+ * together: the `position`th (1-based) when given; otherwise every one when
173
+ * `every` is set, or the first.
221
174
  */
222
- function collectAllScopes(root, groups) {
223
- if (groups.length === 0) return [{
224
- ancestors: [root],
225
- scope: filterSegmentsAndGroups(root.children)
226
- }];
227
- const results = [];
228
- const firstLocator = groups[0];
229
- const remainingGroups = groups.slice(1);
230
- if (!firstLocator) return [{
231
- ancestors: [root],
232
- scope: filterSegmentsAndGroups(root.children)
233
- }];
234
- const matchingGroups = filterSegmentsAndGroups(root.children).filter((node) => node.type === "group" && node.id === firstLocator.name);
235
- if (firstLocator.repetition !== void 0) {
236
- const selected = matchingGroups[firstLocator.repetition - 1];
237
- if (!selected) return [];
238
- const ancestors = [root, selected];
239
- return collectAllScopesRecursive(selected.children, remainingGroups, ancestors);
240
- }
241
- for (const group of matchingGroups) {
242
- const ancestors = [root, group];
243
- const subScopes = collectAllScopesRecursive(group.children, remainingGroups, ancestors);
244
- results.push(...subScopes);
175
+ function stepChildren(parent, name, position, every) {
176
+ const result = [];
177
+ const target = position ?? 1;
178
+ let count = 0;
179
+ for (const child of parent.children) {
180
+ if (keyOf(child) !== name) continue;
181
+ if (every && position === void 0) {
182
+ result.push(child);
183
+ continue;
184
+ }
185
+ count += 1;
186
+ if (count === target) return [child];
245
187
  }
246
- return results;
188
+ return result;
247
189
  }
248
- function collectAllScopesRecursive(children, groups, ancestors) {
249
- if (groups.length === 0) return [{
250
- ancestors,
251
- scope: filterSegmentsAndGroups(children)
252
- }];
253
- const results = [];
254
- const firstLocator = groups[0];
255
- const remainingGroups = groups.slice(1);
256
- if (!firstLocator) return [{
257
- ancestors,
258
- scope: filterSegmentsAndGroups(children)
259
- }];
260
- const matchingGroups = filterSegmentsAndGroups(children).filter((node) => node.type === "group" && node.id === firstLocator.name);
261
- if (firstLocator.repetition !== void 0) {
262
- const selected = matchingGroups[firstLocator.repetition - 1];
263
- if (!selected) return [];
264
- const newAncestors = [...ancestors, selected];
265
- return collectAllScopesRecursive(selected.children, remainingGroups, newAncestors);
266
- }
267
- for (const group of matchingGroups) {
268
- const newAncestors = [...ancestors, group];
269
- const subScopes = collectAllScopesRecursive(group.children, remainingGroups, newAncestors);
270
- results.push(...subScopes);
271
- }
272
- return results;
190
+ /** The children of each scope that `step` names, with their ancestors. */
191
+ function advance(scopes, step, every) {
192
+ const next = [];
193
+ for (const scope of scopes) for (const node of stepChildren(scope.node, step.name, step.repetition, every)) next.push({
194
+ ancestors: [...scope.ancestors, scope.node],
195
+ node
196
+ });
197
+ return next;
273
198
  }
274
- function collectSegments(nodes, targetSegmentName, ancestors) {
275
- const result = [];
276
- for (const node of nodes) {
277
- if (node.type === "segment") {
278
- if (getSegmentName(node) === targetSegmentName) result.push({
279
- ancestors: [...ancestors],
280
- segment: node
199
+ /**
200
+ * The field, repetitions, components, or subcomponents `parts` addresses in
201
+ * `segment`.
202
+ */
203
+ function resolveField(segment, ancestors, fieldNumber, parts, every) {
204
+ const field = segment.children[fieldNumber - 1];
205
+ if (!field) return [];
206
+ const fieldAncestors = [...ancestors, segment];
207
+ if (parts.repetition === void 0 && parts.component === void 0) return [{
208
+ ancestors: fieldAncestors,
209
+ node: field
210
+ }];
211
+ const matches = [];
212
+ for (const repetition of pick(field.children ?? [], parts.repetition, every)) {
213
+ const repetitionAncestors = [...fieldAncestors, field];
214
+ if (parts.component === void 0) {
215
+ matches.push({
216
+ ancestors: repetitionAncestors,
217
+ node: repetition
281
218
  });
219
+ continue;
282
220
  }
283
- if (node.type === "group") {
284
- const nextAncestors = [...ancestors, node];
285
- result.push(...collectSegments(filterSegmentsAndGroups(node.children), targetSegmentName, nextAncestors));
221
+ const component = repetition.children?.[parts.component - 1];
222
+ if (!component) continue;
223
+ const componentAncestors = [...repetitionAncestors, repetition];
224
+ if (parts.subcomponent === void 0) {
225
+ matches.push({
226
+ ancestors: componentAncestors,
227
+ node: component
228
+ });
229
+ continue;
286
230
  }
287
- }
288
- return result;
289
- }
290
- function collectGroups(nodes, targetGroupId, ancestors) {
291
- const result = [];
292
- for (const node of nodes) {
293
- if (node.type === "group" && node.id === targetGroupId) result.push({
294
- ancestors: [...ancestors],
295
- group: node
231
+ const subcomponent = component.children?.[parts.subcomponent - 1];
232
+ if (subcomponent) matches.push({
233
+ ancestors: [...componentAncestors, component],
234
+ node: subcomponent
296
235
  });
297
- if (node.type === "group") {
298
- const nextAncestors = [...ancestors, node];
299
- result.push(...collectGroups(filterSegmentsAndGroups(node.children), targetGroupId, nextAncestors));
300
- }
301
236
  }
302
- return result;
303
- }
304
- function locateField(segment, fieldNum, parentAncestors) {
305
- const field = getFields(segment)[fieldNum - 1];
306
- if (!field) return null;
307
- return [field, [...parentAncestors, segment]];
308
- }
309
- function locateRepetition(field, repetition, parentAncestors) {
310
- if (repetition < 1) return null;
311
- const node = field.children?.[repetition - 1];
312
- if (!node) return null;
313
- return [node, [...parentAncestors, field]];
314
- }
315
- function locateComponent(repetition, componentNum, parentAncestors) {
316
- if (componentNum < 1) return null;
317
- const component = repetition.children?.[componentNum - 1];
318
- if (!component) return null;
319
- return [component, [...parentAncestors, repetition]];
320
- }
321
- function locateSubcomponent(component, subcomponentNum, parentAncestors) {
322
- if (subcomponentNum < 1) return null;
323
- const node = component.children?.[subcomponentNum - 1];
324
- if (!node) return null;
325
- return [node, [...parentAncestors, component]];
237
+ return matches;
326
238
  }
327
- function getSegmentName(segment) {
328
- return segment.name;
329
- }
330
- function getFields(segment) {
331
- return segment.children;
332
- }
333
- function filterSegmentsAndGroups(nodes) {
334
- return nodes.filter((node) => node.type === "segment" || node.type === "group");
239
+ /**
240
+ * The nodes `parts` addresses under `start`, in document order, each with its
241
+ * own ancestors array, which begins at `start`.
242
+ *
243
+ * Every step names a direct child of the node before it; the first step
244
+ * names a direct child of `start`. A step's `[n]` counts the children with
245
+ * that name, segments and groups together. A step without `[n]` matches the
246
+ * first such child, or every one when `every` is set; so does a field
247
+ * without a repetition when the path names a component.
248
+ */
249
+ function resolve(start, parts, every) {
250
+ let scopes = [{
251
+ ancestors: [],
252
+ node: start
253
+ }];
254
+ for (const group of parts.groups ?? []) scopes = advance(scopes, group, every);
255
+ if (parts.segment !== void 0) scopes = advance(scopes, parts.segment, every);
256
+ if (parts.field === void 0) return scopes;
257
+ const matches = [];
258
+ for (const { node, ancestors } of scopes) {
259
+ if (node.type !== "segment") continue;
260
+ for (const match of resolveField(node, ancestors, parts.field, parts, every)) matches.push(match);
261
+ }
262
+ return matches;
335
263
  }
336
264
  //#endregion
337
265
  //#region src/select.ts
338
266
  /**
339
- * Select the first node at the given path and return it with its ancestor
340
- * chain.
267
+ * The node `path` addresses under `node`, with its ancestors from `node`, or
268
+ * `null`.
341
269
  *
342
- * This function finds the first matching node for the given path. For paths
343
- * that can match multiple nodes (e.g., "PID" when multiple PID segments exist),
344
- * use {@link selectAll} to retrieve all matches.
270
+ * Every step of `path` names a direct child of the node before it; the first
271
+ * step names a direct child of `node`, so a path read from a segment starts
272
+ * with the field. A step without `[n]` names the first such child, so
273
+ * `select` returns `null` when that child does not lead to a match, even if a
274
+ * later one does; `selectAll` returns every match.
345
275
  *
346
276
  * @example
347
277
  * ```typescript
348
- * const result = select(root, "PID-5");
349
- * if (result) {
350
- * result.node; // Type: Field
351
- * result.ancestors; // [Root, Segment]
352
- * }
278
+ * select(root, "PID.5"); // { node: Field, ancestors: [Root, Segment] }
279
+ * select(root, "PATIENT_RESULT.PATIENT.PID.5");
280
+ * select(segment, "5.1"); // { node: Component, ancestors: [Segment, Field, FieldRepetition] }
353
281
  * ```;
354
282
  *
355
- * @param root - The root node to search from
356
- * @param path - The HL7 path string
357
- * @returns Object containing the node and its ancestors, or null if not found
283
+ * @param node - The message, group, or segment to read from.
284
+ * @param path - The path to the node.
285
+ * @returns The node and its ancestors, or `null` when nothing matches.
358
286
  */
359
- function select(root, path) {
360
- const parts = parse(path);
361
- if (parts.field === void 0) {
362
- const result = locateSegmentOrGroup(root, parts);
363
- if (!result) return null;
364
- return {
365
- ancestors: result[1],
366
- node: result[0]
367
- };
368
- }
369
- const segmentResult = locateSegment(root, parts);
370
- if (!segmentResult) return null;
371
- const [segment, segmentAncestors] = segmentResult;
372
- const fieldResult = locateField(segment, parts.field, segmentAncestors);
373
- if (!fieldResult) return null;
374
- const [field, fieldAncestors] = fieldResult;
375
- if (parts.component === void 0 && parts.subcomponent === void 0 && parts.repetition === void 0) return {
376
- ancestors: fieldAncestors,
377
- node: field
378
- };
379
- const repetitionResult = locateRepetition(field, parts.repetition ?? 1, fieldAncestors);
380
- if (!repetitionResult) return null;
381
- const [repetitionNode, repetitionAncestors] = repetitionResult;
382
- if (parts.component === void 0) return {
383
- ancestors: repetitionAncestors,
384
- node: repetitionNode
385
- };
386
- const componentResult = locateComponent(repetitionNode, parts.component, repetitionAncestors);
387
- if (!componentResult) return null;
388
- const [componentNode, componentAncestors] = componentResult;
389
- if (parts.subcomponent === void 0) return {
390
- ancestors: componentAncestors,
391
- node: componentNode
392
- };
393
- const subResult = locateSubcomponent(componentNode, parts.subcomponent, componentAncestors);
394
- if (!subResult) return null;
395
- return {
396
- ancestors: subResult[1],
397
- node: subResult[0]
398
- };
287
+ function select(node, path) {
288
+ const [match] = resolve(node, parse(path), false);
289
+ return match ?? null;
399
290
  }
400
291
  /**
401
- * Select all nodes that match the given path.
292
+ * Every node `path` addresses under `node`, with its ancestors from `node`, in
293
+ * document order.
402
294
  *
403
- * This function returns all matching nodes for paths that can match multiple
404
- * elements. For example, "PID" will return all PID segments in the message, and
405
- * "OBX-5" will return all observation values.
295
+ * Every step of `path` names a direct child of the node before it; the first
296
+ * step names a direct child of `node`, so a path read from a segment starts
297
+ * with the field. A step without `[n]` matches every such child, and so does
298
+ * a field without a repetition when `path` names a component.
406
299
  *
407
300
  * @example
408
301
  * ```typescript
409
- * // Get all OBX segments
410
- * const results = selectAll(root, "OBX");
411
- * for (const { node } of results) {
412
- * console.log(node.type); // 'segment'
413
- * }
414
- *
415
- * // Get all patient name fields
416
- * const names = selectAll(root, "PID-5");
302
+ * selectAll(root, "OBX.5"); // OBX-5 of every top-level OBX
303
+ * selectAll(root, "OBX.1[2]"); // the second repetition of OBX-1 in every OBX
304
+ * selectAll(segment, "3.1"); // the first component of every repetition of field 3
417
305
  * ```;
418
306
  *
419
- * @param root - The root node to search from
420
- * @param path - The HL7 path string (must not include repetition index)
421
- * @returns Array of objects containing nodes and their ancestor chains
307
+ * @param node - The message, group, or segment to read from.
308
+ * @param path - The path to the nodes.
309
+ * @returns The nodes and their ancestors; empty when nothing matches.
422
310
  */
423
- function selectAll(root, path) {
424
- const parts = parse(path);
425
- if (parts.segment.repetition !== void 0 || parts.repetition !== void 0) {
426
- const result = select(root, path);
427
- return result ? [result] : [];
428
- }
429
- const results = [];
430
- const scopes = collectAllScopes(root, parts.groups ?? []);
431
- const segments = [];
432
- const includeGroups = parts.field === void 0;
433
- const groups = [];
434
- for (const { scope, ancestors } of scopes) {
435
- const matchingSegments = collectSegments(scope, parts.segment.name, ancestors);
436
- segments.push(...matchingSegments);
437
- if (includeGroups) {
438
- const matchingGroups = collectGroups(scope, parts.segment.name, ancestors);
439
- for (const { group, ancestors: groupAncestors } of matchingGroups) groups.push({
440
- ancestors: groupAncestors,
441
- group
442
- });
443
- }
444
- }
445
- if (parts.field === void 0) {
446
- for (const { segment, ancestors } of segments) results.push({
447
- ancestors,
448
- node: segment
449
- });
450
- for (const { group, ancestors } of groups) results.push({
451
- ancestors,
452
- node: group
453
- });
454
- return results;
455
- }
456
- for (const { segment, ancestors: segmentAncestors } of segments) {
457
- const fieldResult = locateField(segment, parts.field, segmentAncestors);
458
- if (!fieldResult) continue;
459
- const [field, fieldAncestors] = fieldResult;
460
- if (parts.component === void 0 && parts.subcomponent === void 0 && parts.repetition === void 0) {
461
- results.push({
462
- ancestors: fieldAncestors,
463
- node: field
464
- });
465
- continue;
466
- }
467
- if (!field.children) continue;
468
- for (const repetitionNode of field.children) {
469
- const repetitionAncestors = [...fieldAncestors, field];
470
- if (parts.component === void 0) {
471
- results.push({
472
- ancestors: repetitionAncestors,
473
- node: repetitionNode
474
- });
475
- continue;
476
- }
477
- const componentResult = locateComponent(repetitionNode, parts.component, repetitionAncestors);
478
- if (!componentResult) continue;
479
- const [componentNode, componentAncestors] = componentResult;
480
- if (parts.subcomponent === void 0) {
481
- results.push({
482
- ancestors: componentAncestors,
483
- node: componentNode
484
- });
485
- continue;
486
- }
487
- const subResult = locateSubcomponent(componentNode, parts.subcomponent, componentAncestors);
488
- if (subResult) results.push({
489
- ancestors: subResult[1],
490
- node: subResult[0]
491
- });
492
- }
493
- }
494
- return results;
311
+ function selectAll(node, path) {
312
+ return resolve(node, parse(path), true);
495
313
  }
496
314
  //#endregion
497
315
  //#region src/value.ts
@@ -502,7 +320,7 @@ function selectAll(root, path) {
502
320
  * This is a convenience function that automatically traverses through container
503
321
  * nodes (Field → FieldRepetition → Component → Subcomponent) to extract the
504
322
  * primary string value. When a container has multiple children, the first child
505
- * is selected (e.g., `value(tree, "MSH-12")` returns the first component of a
323
+ * is selected (e.g., `value(tree, "MSH.12")` returns the first component of a
506
324
  * composite VID field).
507
325
  *
508
326
  * Nodes with no children return `value: null` to differentiate between empty
@@ -510,42 +328,51 @@ function selectAll(root, path) {
510
328
  *
511
329
  * @example
512
330
  * ```typescript
513
- * const result = value(root, "PID-5.1");
331
+ * const result = value(root, "PID.5.1");
514
332
  * if (result) {
515
333
  * console.log(result.value); // string | null
516
334
  * console.log(result.node.type); // 'subcomponent'
517
335
  * console.log(result.ancestors); // [Root, Segment, Field, ...]
518
336
  * }
337
+ *
338
+ * value(segment, "5.1"); // the same value, read from the PID segment
519
339
  * ```;
520
340
  *
521
- * @param root - The root node to search from
341
+ * @param node - The message, group, or segment to read from
522
342
  * @param path - The HL7 path string
523
- * @returns Object containing the value, node, and ancestors, or null if not
524
- * found
343
+ * @returns Object containing the value, node, and ancestors from `node`, or
344
+ * null if not found
525
345
  */
526
- function value(root, path) {
527
- const selectResult = select(root, path);
528
- if (!selectResult) return null;
529
- let { node } = selectResult;
530
- let { ancestors } = selectResult;
531
- while (node.type !== "subcomponent") {
532
- if (node.type === "segment" || node.type === "group" || node.type === "root") return null;
533
- if (!("children" in node && node.children)) return null;
534
- if (node.children.length === 0) return {
535
- ancestors: [...ancestors],
346
+ function value(node, path) {
347
+ const selected = select(node, path);
348
+ return selected ? drill(selected.node, selected.ancestors) : null;
349
+ }
350
+ /**
351
+ * The first subcomponent under `node`, with its value, or `node` with value
352
+ * `null` when a field, repetition, or component on the way has no children.
353
+ * `null` for a root, group, or segment.
354
+ */
355
+ function drill(node, ancestors) {
356
+ switch (node.type) {
357
+ case "root":
358
+ case "group":
359
+ case "segment": return null;
360
+ case "subcomponent": return {
361
+ ancestors,
536
362
  node,
537
- value: null
363
+ value: node.value
538
364
  };
539
- const next = node.children[0];
540
- if (!next) return null;
541
- ancestors = [...ancestors, node];
542
- node = next;
365
+ case "field":
366
+ case "field-repetition":
367
+ case "component": {
368
+ const first = node.children?.[0];
369
+ return first ? drill(first, [...ancestors, node]) : {
370
+ ancestors,
371
+ node,
372
+ value: null
373
+ };
374
+ }
543
375
  }
544
- return {
545
- ancestors,
546
- node,
547
- value: node.value
548
- };
549
376
  }
550
377
  //#endregion
551
378
  //#region src/delimiters.ts
@@ -570,9 +397,9 @@ const MSH2_KEYS = [
570
397
  */
571
398
  function delimiters(root) {
572
399
  const result = { ...DEFAULT_DELIMITERS };
573
- const msh1 = value(root, "MSH-1")?.value;
400
+ const msh1 = value(root, "MSH.1")?.value;
574
401
  if (msh1 && msh1.length === 1) result.field = msh1;
575
- const msh2 = value(root, "MSH-2")?.value;
402
+ const msh2 = value(root, "MSH.2")?.value;
576
403
  if (msh2) {
577
404
  const chars = [...msh2];
578
405
  for (const [i, key] of MSH2_KEYS.entries()) {
@@ -584,204 +411,99 @@ function delimiters(root) {
584
411
  }
585
412
  //#endregion
586
413
  //#region src/format.ts
587
- /** What a path names a node by: a segment's name, or a group's ID. */
588
- const keyOf = (node) => node.type === "group" ? node.id : node.name;
589
414
  /**
590
415
  * Format a node and its ancestor chain into a canonical HL7v2 path string.
591
416
  *
592
- * This is the inverse of `select`: given a node and the ancestors
593
- * returned by `visit` or `select`, it produces the path string that
594
- * would select that node.
417
+ * When `ancestors` starts at the root, `select(root, path)` returns `node`:
418
+ * the path names every group, and `[n]` counts the siblings with the same
419
+ * name, segments and groups together.
595
420
  *
596
421
  * @example
597
422
  * ```typescript
598
423
  * visit(tree, "field", (node, ancestors) => {
599
- * format(node, ancestors); // "PID-5"
424
+ * format(node, ancestors); // "PID.5"
600
425
  * });
601
426
  *
602
- * const result = select(root, "PID-5.2");
603
- * format(result.node, result.ancestors); // "PID-5.2"
427
+ * const result = select(root, "PID.5.2");
428
+ * format(result.node, result.ancestors); // "PID.5.2"
604
429
  * ```;
605
430
  *
606
431
  * @param node - The target AST node
607
432
  * @param ancestors - Ancestor chain from root to parent (as provided by
608
433
  * visit/select)
609
- * @returns Canonical path string (e.g., "PID-5.2", "ORDER-ORC-1"), or `null` if the
610
- * ancestor chain contains no segment or group
434
+ * @returns Canonical path string (e.g., "PID.5.2", "ORDER.ORC.1"), or `null` if the
435
+ * ancestor chain contains no segment or group, or an ancestor does not
436
+ * hold the next node in the chain
611
437
  */
612
438
  function format(node, ancestors) {
613
- let result = null;
614
439
  const chain = [...ancestors, node];
615
- for (const [i, current] of chain.entries()) {
616
- const parent = chain[i - 1];
440
+ const steps = [];
441
+ let positions = "";
442
+ for (const [index, current] of chain.entries()) {
443
+ const position = positionOf(current, chain[index - 1]);
444
+ if (position === 0) return null;
617
445
  switch (current.type) {
618
446
  case "root": break;
619
447
  case "group":
620
- case "segment": {
621
- const pos = positionOf(current, parent);
622
- result = (result ?? "") + keyOf(current);
623
- if (pos > 1) result += `[${pos}]`;
624
- if (current.type === "group" && current !== node) result += "-";
448
+ case "segment":
449
+ steps.push(position > 1 ? `${keyOf(current)}[${position}]` : `${keyOf(current)}`);
625
450
  break;
626
- }
627
- case "field":
628
- result += `-${positionOf(current, parent)}`;
629
- break;
630
- case "field-repetition": {
631
- const pos = positionOf(current, parent);
632
- if (pos > 1 || current === node) result += `[${pos}]`;
451
+ case "field-repetition":
452
+ if (position > 1 || current === node) positions += `[${position}]`;
633
453
  break;
634
- }
454
+ case "field":
635
455
  case "component":
636
- result += `.${positionOf(current, parent)}`;
637
- break;
638
456
  case "subcomponent":
639
- result += `.${positionOf(current, parent)}`;
457
+ positions += `.${position}`;
640
458
  break;
641
459
  }
642
460
  }
643
- return result;
461
+ return steps.length === 0 ? null : steps.join(".") + positions;
644
462
  }
645
463
  /**
646
- * Compute the 1-based position of a node among its parent's children.
647
- *
648
- * For segments and groups, counts only siblings with the same name (a
649
- * group's ID): the 2nd PID in [MSH, PID, OBX, PID] is PID[2], not PID[4].
650
- *
651
- * For positional nodes (fields, repetitions, components, subcomponents),
652
- * uses the array index directly.
464
+ * The 1-based position of `node` among the children of `parent`: for a
465
+ * segment or group, its `[n]` counted as `select` counts it; otherwise its
466
+ * index. 1 without a parent; 0 when `parent` does not hold `node`.
653
467
  */
654
468
  function positionOf(node, parent) {
655
- if (!parent || !("children" in parent)) return 1;
656
- const children = parent.children;
657
- if (node.type === "group" || node.type === "segment") {
658
- const key = keyOf(node);
659
- let count = 0;
660
- for (const child of children) {
661
- if (child === node) return count + 1;
662
- if (child.type === node.type && keyOf(child) === key) count++;
663
- }
664
- throw new Error("format(): node not found in parent's children array");
469
+ if (parent === void 0 || !("children" in parent)) return 1;
470
+ switch (node.type) {
471
+ case "group":
472
+ case "segment": return stepChildren(parent, keyOf(node), void 0, true).indexOf(node) + 1;
473
+ case "root":
474
+ case "field":
475
+ case "field-repetition":
476
+ case "component":
477
+ case "subcomponent": return parent.children.indexOf(node) + 1;
665
478
  }
666
- const idx = children.indexOf(node);
667
- if (idx === -1) throw new Error("format(): node not found in parent's children array");
668
- return idx + 1;
669
479
  }
670
480
  //#endregion
671
481
  //#region src/matches.ts
672
482
  /**
673
- * Check if a node exists at the given path.
483
+ * Whether `select(node, path)` finds a node.
674
484
  *
675
- * This is a convenience function equivalent to `select(root, path) !== null`,
676
- * but more semantically clear for existence checks.
485
+ * A step without `[n]` names the first such child, so `matches` is `false`
486
+ * when that child does not lead to a match, even if a later one does.
677
487
  *
678
488
  * @example
679
489
  * ```typescript
680
- * if (matches(root, "PID-5")) {
490
+ * if (matches(root, "PID.5")) {
681
491
  * console.log("Patient has a name field");
682
492
  * }
683
493
  *
684
- * if (!matches(root, "OBX-5")) {
494
+ * if (!matches(root, "OBX.5")) {
685
495
  * throw new Error("Missing observation value");
686
496
  * }
687
497
  * ```;
688
498
  *
689
- * @param root - The root node to search from
499
+ * @param node - The message, group, or segment to read from
690
500
  * @param path - The HL7 path string
691
501
  * @returns True if the path resolves to a node, false otherwise
692
502
  */
693
- function matches(root, path) {
694
- return select(root, path) !== null;
695
- }
696
- //#endregion
697
- //#region src/set.ts
698
- /**
699
- * Set a string value at the given HL7 path, creating all missing intermediate
700
- * nodes as needed (fields, repetitions, components, subcomponents).
701
- *
702
- * This is the write counterpart to {@link value}. It handles the full HL7v2
703
- * path syntax including group navigation and segment repetitions.
704
- *
705
- * Padding nodes are created with empty children arrays — they represent
706
- * structurally absent nodes, not empty values. Only the target node gets
707
- * a fully populated chain down to the subcomponent.
708
- *
709
- * @example
710
- * ```typescript
711
- * // Update existing value
712
- * set(tree, "MSH-12", "2.7.1");
713
- *
714
- * // Create missing MSH-9.3 (fills gaps with empty components)
715
- * set(tree, "MSH-9.3", "ADT_A01");
716
- *
717
- * // Set a specific repetition
718
- * set(tree, "PID-3[2]", "ID-002");
719
- *
720
- * // Set a subcomponent
721
- * set(tree, "PID-5.1.2", "suffix");
722
- * ```;
723
- *
724
- * @param root - The root node to modify
725
- * @param path - The HL7 path string (must include at least a field)
726
- * @param val - The string value to set
727
- * @throws When the path has no field specifier (segment-only paths)
728
- * @throws When the target segment does not exist in the tree
729
- */
730
- function set(root, path, val) {
731
- const parts = parse(path);
732
- if (parts.field === void 0) throw new Error(`Cannot set value on segment-only path: "${path}"`);
733
- const segmentResult = locateSegment(root, parts);
734
- if (!segmentResult) throw new Error(`Segment "${parts.segment.name}" not found in tree`);
735
- const [segment] = segmentResult;
736
- const fieldIndex = parts.field - 1;
737
- padArray(segment.children, fieldIndex + 1, stubField);
738
- const field = segment.children[fieldIndex];
739
- const repIndex = (parts.repetition ?? 1) - 1;
740
- padArray(field.children, repIndex + 1, stubRepetition);
741
- const repetition = field.children[repIndex];
742
- const compIndex = (parts.component ?? 1) - 1;
743
- padArray(repetition.children, compIndex + 1, stubComponent);
744
- const component = repetition.children[compIndex];
745
- const subIndex = (parts.subcomponent ?? 1) - 1;
746
- padArray(component.children, subIndex + 1, stubSubcomponent);
747
- component.children[subIndex].value = val;
748
- }
749
- /**
750
- * Pad an array to a target length using a factory function.
751
- */
752
- function padArray(arr, targetLength, factory) {
753
- while (arr.length < targetLength) arr.push(factory());
754
- }
755
- /**
756
- * Stub nodes for padding — created with empty children arrays to represent
757
- * structurally absent nodes. These are distinguishable from nodes with empty
758
- * values (which have a subcomponent with value "").
759
- */
760
- function stubField() {
761
- return {
762
- children: [],
763
- type: "field"
764
- };
765
- }
766
- function stubRepetition() {
767
- return {
768
- children: [],
769
- type: "field-repetition"
770
- };
771
- }
772
- function stubComponent() {
773
- return {
774
- children: [],
775
- type: "component"
776
- };
777
- }
778
- function stubSubcomponent() {
779
- return {
780
- type: "subcomponent",
781
- value: ""
782
- };
503
+ function matches(node, path) {
504
+ return select(node, path) !== null;
783
505
  }
784
506
  //#endregion
785
- export { delimiters, format, matches, parse, select, selectAll, set, value };
507
+ export { QueryError, QueryErrorCode, delimiters, format, matches, parse, select, selectAll, value };
786
508
 
787
509
  //# sourceMappingURL=index.js.map