@fhir-dsl/terminology 0.32.0 → 0.34.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.cjs CHANGED
@@ -50,15 +50,80 @@ function flattenConcepts(concepts) {
50
50
  }
51
51
  return result;
52
52
  }
53
+ function buildNodes(concepts) {
54
+ const nodes = [];
55
+ const childrenOf = /* @__PURE__ */ new Map();
56
+ function visit(concept, implicitParent) {
57
+ if (!concept.code) {
58
+ for (const child of concept.concept ?? []) visit(child, implicitParent);
59
+ return;
60
+ }
61
+ const parents = /* @__PURE__ */ new Set();
62
+ if (implicitParent) parents.add(implicitParent);
63
+ for (const p of concept.property ?? []) {
64
+ if (p.code === "parent" && typeof p.valueCode === "string") parents.add(p.valueCode);
65
+ }
66
+ const properties = {};
67
+ for (const p of concept.property ?? []) {
68
+ if (!p.code || p.code === "parent") continue;
69
+ const v = p.valueCode ?? p.valueString ?? p.valueInteger ?? p.valueBoolean;
70
+ if (v !== void 0) properties[p.code] = v;
71
+ }
72
+ const designations = concept.designation?.filter((d) => typeof d.value === "string").map((d) => ({ language: d.language, use: d.use?.code, value: d.value })) ?? [];
73
+ for (const parent of parents) {
74
+ const list = childrenOf.get(parent) ?? [];
75
+ list.push(concept.code);
76
+ childrenOf.set(parent, list);
77
+ }
78
+ const node = { code: concept.code };
79
+ if (concept.display !== void 0) node.display = concept.display;
80
+ if (parents.size > 0) node.parents = [...parents];
81
+ if (Object.keys(properties).length > 0) node.properties = properties;
82
+ if (designations.length > 0) node.designations = designations;
83
+ nodes.push(node);
84
+ for (const child of concept.concept ?? []) visit(child, concept.code);
85
+ }
86
+ for (const top of concepts) visit(top, void 0);
87
+ return nodes.map((n) => {
88
+ const kids = childrenOf.get(n.code);
89
+ return kids?.length ? { ...n, children: kids } : n;
90
+ });
91
+ }
92
+ function makeIsA(nodes) {
93
+ const byCode = /* @__PURE__ */ new Map();
94
+ for (const n of nodes) byCode.set(n.code, n);
95
+ return (ancestor, descendant) => {
96
+ if (ancestor === descendant) return true;
97
+ const stack = [...byCode.get(descendant)?.parents ?? []];
98
+ const seen = /* @__PURE__ */ new Set();
99
+ while (stack.length) {
100
+ const cur = stack.pop();
101
+ if (cur === void 0 || seen.has(cur)) continue;
102
+ seen.add(cur);
103
+ if (cur === ancestor) return true;
104
+ const node = byCode.get(cur);
105
+ if (node?.parents?.length) stack.push(...node.parents);
106
+ }
107
+ return false;
108
+ };
109
+ }
53
110
  function parseCodeSystem(resource) {
54
111
  if (!isCodeSystem(resource)) return void 0;
55
112
  const content = normalizeContent(resource.content);
56
- return {
113
+ const hasConcepts = content === "complete" && resource.concept?.length;
114
+ const concepts = hasConcepts ? flattenConcepts(resource.concept) : [];
115
+ const nodes = hasConcepts ? buildNodes(resource.concept) : [];
116
+ const model = {
57
117
  url: resource.url ?? "",
58
118
  name: resource.name ?? "",
59
119
  content,
60
- concepts: content === "complete" && resource.concept?.length ? flattenConcepts(resource.concept) : []
120
+ concepts
61
121
  };
122
+ if (nodes.length > 0) {
123
+ model.nodes = nodes;
124
+ model.isA = makeIsA(nodes);
125
+ }
126
+ return model;
62
127
  }
63
128
  function normalizeContent(value) {
64
129
  switch (value) {
@@ -151,7 +216,8 @@ function resolveCompose(resource, codeSystemLookup) {
151
216
  } else if (include.filter?.length) {
152
217
  const cs = include.system ? codeSystemLookup.get(include.system) : void 0;
153
218
  if (cs && cs.content === "complete" && cs.concepts.length > 0) {
154
- const filtered = applyFilters(cs.concepts, include.filter);
219
+ const { codes: filtered, complete } = applyFilters(cs, include.filter);
220
+ if (!complete) isComplete = false;
155
221
  includedCodes.push(...filtered.map((c) => ({ ...c, system: include.system })));
156
222
  } else {
157
223
  isComplete = false;
@@ -202,14 +268,49 @@ function resolveCompose(resource, codeSystemLookup) {
202
268
  isComplete
203
269
  };
204
270
  }
205
- function applyFilters(concepts, filters) {
206
- let result = concepts;
271
+ function applyFilters(cs, filters) {
272
+ let result = cs.concepts;
273
+ let complete = true;
207
274
  for (const filter of filters) {
208
- if (filter.op === "=" && filter.property === "concept" && filter.value) {
209
- result = result.filter((c) => c.code === filter.value);
275
+ const op = filter.op;
276
+ const value = filter.value;
277
+ const prop = filter.property;
278
+ if (!op || !value || !prop) {
279
+ complete = false;
280
+ continue;
281
+ }
282
+ if (op === "=" && prop === "concept") {
283
+ result = result.filter((c) => c.code === value);
284
+ continue;
210
285
  }
286
+ if ((op === "is-a" || op === "isa") && prop === "concept") {
287
+ if (!cs.isA) {
288
+ complete = false;
289
+ continue;
290
+ }
291
+ result = result.filter((c) => cs.isA(value, c.code));
292
+ continue;
293
+ }
294
+ if (op === "descendent-of" && prop === "concept") {
295
+ if (!cs.isA) {
296
+ complete = false;
297
+ continue;
298
+ }
299
+ result = result.filter((c) => c.code !== value && cs.isA(value, c.code));
300
+ continue;
301
+ }
302
+ if (op === "regex") {
303
+ try {
304
+ const re = new RegExp(value);
305
+ result = result.filter((c) => re.test(prop === "display" ? c.display ?? "" : c.code));
306
+ } catch {
307
+ complete = false;
308
+ }
309
+ continue;
310
+ }
311
+ complete = false;
211
312
  }
212
- return result;
313
+ return { codes: result, complete };
213
314
  }
214
315
 
215
316
  // src/resolver.ts
package/dist/index.d.cts CHANGED
@@ -11,11 +11,43 @@ interface ResolvedValueSet {
11
11
  /** true if all includes were resolved offline; false if some were skipped */
12
12
  isComplete: boolean;
13
13
  }
14
+ /**
15
+ * Phase 3.2 — extended concept model.
16
+ *
17
+ * `parents` carries the codes inherited via `concept.property[code=parent]`
18
+ * (or implicit nesting under another concept). `children` is the inverse,
19
+ * derived after a full pass. Both are optional; older callers that only
20
+ * need `code`/`display` keep working.
21
+ */
22
+ interface ConceptNode {
23
+ code: string;
24
+ display?: string | undefined;
25
+ /** Hierarchy parents (direct). Empty for root concepts. */
26
+ parents?: readonly string[] | undefined;
27
+ /** Direct children (computed after the full code system loads). */
28
+ children?: readonly string[] | undefined;
29
+ /** Concept-level properties (`property[code]` → value). */
30
+ properties?: Readonly<Record<string, string | number | boolean>> | undefined;
31
+ /** Localized / alternate display strings. */
32
+ designations?: ReadonlyArray<{
33
+ language?: string | undefined;
34
+ use?: string | undefined;
35
+ value: string;
36
+ }> | undefined;
37
+ }
14
38
  interface CodeSystemModel {
15
39
  url: string;
16
40
  name: string;
17
41
  content: "complete" | "not-present" | "example" | "fragment" | "supplement";
18
42
  concepts: ResolvedCode[];
43
+ /** Phase 3.2: hierarchy + properties for each concept. Aligned with `concepts` by `.code`. */
44
+ nodes?: readonly ConceptNode[] | undefined;
45
+ /**
46
+ * Phase 3.2: returns true when `descendant` is `ancestor` or has it
47
+ * transitively in its `parents` chain. Implemented by the parser when
48
+ * a hierarchy is present.
49
+ */
50
+ isA?: ((ancestor: string, descendant: string) => boolean) | undefined;
19
51
  }
20
52
 
21
53
  declare function parseCodeSystem(resource: unknown): CodeSystemModel | undefined;
package/dist/index.d.ts CHANGED
@@ -11,11 +11,43 @@ interface ResolvedValueSet {
11
11
  /** true if all includes were resolved offline; false if some were skipped */
12
12
  isComplete: boolean;
13
13
  }
14
+ /**
15
+ * Phase 3.2 — extended concept model.
16
+ *
17
+ * `parents` carries the codes inherited via `concept.property[code=parent]`
18
+ * (or implicit nesting under another concept). `children` is the inverse,
19
+ * derived after a full pass. Both are optional; older callers that only
20
+ * need `code`/`display` keep working.
21
+ */
22
+ interface ConceptNode {
23
+ code: string;
24
+ display?: string | undefined;
25
+ /** Hierarchy parents (direct). Empty for root concepts. */
26
+ parents?: readonly string[] | undefined;
27
+ /** Direct children (computed after the full code system loads). */
28
+ children?: readonly string[] | undefined;
29
+ /** Concept-level properties (`property[code]` → value). */
30
+ properties?: Readonly<Record<string, string | number | boolean>> | undefined;
31
+ /** Localized / alternate display strings. */
32
+ designations?: ReadonlyArray<{
33
+ language?: string | undefined;
34
+ use?: string | undefined;
35
+ value: string;
36
+ }> | undefined;
37
+ }
14
38
  interface CodeSystemModel {
15
39
  url: string;
16
40
  name: string;
17
41
  content: "complete" | "not-present" | "example" | "fragment" | "supplement";
18
42
  concepts: ResolvedCode[];
43
+ /** Phase 3.2: hierarchy + properties for each concept. Aligned with `concepts` by `.code`. */
44
+ nodes?: readonly ConceptNode[] | undefined;
45
+ /**
46
+ * Phase 3.2: returns true when `descendant` is `ancestor` or has it
47
+ * transitively in its `parents` chain. Implemented by the parser when
48
+ * a hierarchy is present.
49
+ */
50
+ isA?: ((ancestor: string, descendant: string) => boolean) | undefined;
19
51
  }
20
52
 
21
53
  declare function parseCodeSystem(resource: unknown): CodeSystemModel | undefined;
package/dist/index.js CHANGED
@@ -19,15 +19,80 @@ function flattenConcepts(concepts) {
19
19
  }
20
20
  return result;
21
21
  }
22
+ function buildNodes(concepts) {
23
+ const nodes = [];
24
+ const childrenOf = /* @__PURE__ */ new Map();
25
+ function visit(concept, implicitParent) {
26
+ if (!concept.code) {
27
+ for (const child of concept.concept ?? []) visit(child, implicitParent);
28
+ return;
29
+ }
30
+ const parents = /* @__PURE__ */ new Set();
31
+ if (implicitParent) parents.add(implicitParent);
32
+ for (const p of concept.property ?? []) {
33
+ if (p.code === "parent" && typeof p.valueCode === "string") parents.add(p.valueCode);
34
+ }
35
+ const properties = {};
36
+ for (const p of concept.property ?? []) {
37
+ if (!p.code || p.code === "parent") continue;
38
+ const v = p.valueCode ?? p.valueString ?? p.valueInteger ?? p.valueBoolean;
39
+ if (v !== void 0) properties[p.code] = v;
40
+ }
41
+ const designations = concept.designation?.filter((d) => typeof d.value === "string").map((d) => ({ language: d.language, use: d.use?.code, value: d.value })) ?? [];
42
+ for (const parent of parents) {
43
+ const list = childrenOf.get(parent) ?? [];
44
+ list.push(concept.code);
45
+ childrenOf.set(parent, list);
46
+ }
47
+ const node = { code: concept.code };
48
+ if (concept.display !== void 0) node.display = concept.display;
49
+ if (parents.size > 0) node.parents = [...parents];
50
+ if (Object.keys(properties).length > 0) node.properties = properties;
51
+ if (designations.length > 0) node.designations = designations;
52
+ nodes.push(node);
53
+ for (const child of concept.concept ?? []) visit(child, concept.code);
54
+ }
55
+ for (const top of concepts) visit(top, void 0);
56
+ return nodes.map((n) => {
57
+ const kids = childrenOf.get(n.code);
58
+ return kids?.length ? { ...n, children: kids } : n;
59
+ });
60
+ }
61
+ function makeIsA(nodes) {
62
+ const byCode = /* @__PURE__ */ new Map();
63
+ for (const n of nodes) byCode.set(n.code, n);
64
+ return (ancestor, descendant) => {
65
+ if (ancestor === descendant) return true;
66
+ const stack = [...byCode.get(descendant)?.parents ?? []];
67
+ const seen = /* @__PURE__ */ new Set();
68
+ while (stack.length) {
69
+ const cur = stack.pop();
70
+ if (cur === void 0 || seen.has(cur)) continue;
71
+ seen.add(cur);
72
+ if (cur === ancestor) return true;
73
+ const node = byCode.get(cur);
74
+ if (node?.parents?.length) stack.push(...node.parents);
75
+ }
76
+ return false;
77
+ };
78
+ }
22
79
  function parseCodeSystem(resource) {
23
80
  if (!isCodeSystem(resource)) return void 0;
24
81
  const content = normalizeContent(resource.content);
25
- return {
82
+ const hasConcepts = content === "complete" && resource.concept?.length;
83
+ const concepts = hasConcepts ? flattenConcepts(resource.concept) : [];
84
+ const nodes = hasConcepts ? buildNodes(resource.concept) : [];
85
+ const model = {
26
86
  url: resource.url ?? "",
27
87
  name: resource.name ?? "",
28
88
  content,
29
- concepts: content === "complete" && resource.concept?.length ? flattenConcepts(resource.concept) : []
89
+ concepts
30
90
  };
91
+ if (nodes.length > 0) {
92
+ model.nodes = nodes;
93
+ model.isA = makeIsA(nodes);
94
+ }
95
+ return model;
31
96
  }
32
97
  function normalizeContent(value) {
33
98
  switch (value) {
@@ -120,7 +185,8 @@ function resolveCompose(resource, codeSystemLookup) {
120
185
  } else if (include.filter?.length) {
121
186
  const cs = include.system ? codeSystemLookup.get(include.system) : void 0;
122
187
  if (cs && cs.content === "complete" && cs.concepts.length > 0) {
123
- const filtered = applyFilters(cs.concepts, include.filter);
188
+ const { codes: filtered, complete } = applyFilters(cs, include.filter);
189
+ if (!complete) isComplete = false;
124
190
  includedCodes.push(...filtered.map((c) => ({ ...c, system: include.system })));
125
191
  } else {
126
192
  isComplete = false;
@@ -171,14 +237,49 @@ function resolveCompose(resource, codeSystemLookup) {
171
237
  isComplete
172
238
  };
173
239
  }
174
- function applyFilters(concepts, filters) {
175
- let result = concepts;
240
+ function applyFilters(cs, filters) {
241
+ let result = cs.concepts;
242
+ let complete = true;
176
243
  for (const filter of filters) {
177
- if (filter.op === "=" && filter.property === "concept" && filter.value) {
178
- result = result.filter((c) => c.code === filter.value);
244
+ const op = filter.op;
245
+ const value = filter.value;
246
+ const prop = filter.property;
247
+ if (!op || !value || !prop) {
248
+ complete = false;
249
+ continue;
250
+ }
251
+ if (op === "=" && prop === "concept") {
252
+ result = result.filter((c) => c.code === value);
253
+ continue;
179
254
  }
255
+ if ((op === "is-a" || op === "isa") && prop === "concept") {
256
+ if (!cs.isA) {
257
+ complete = false;
258
+ continue;
259
+ }
260
+ result = result.filter((c) => cs.isA(value, c.code));
261
+ continue;
262
+ }
263
+ if (op === "descendent-of" && prop === "concept") {
264
+ if (!cs.isA) {
265
+ complete = false;
266
+ continue;
267
+ }
268
+ result = result.filter((c) => c.code !== value && cs.isA(value, c.code));
269
+ continue;
270
+ }
271
+ if (op === "regex") {
272
+ try {
273
+ const re = new RegExp(value);
274
+ result = result.filter((c) => re.test(prop === "display" ? c.display ?? "" : c.code));
275
+ } catch {
276
+ complete = false;
277
+ }
278
+ continue;
279
+ }
280
+ complete = false;
180
281
  }
181
- return result;
282
+ return { codes: result, complete };
182
283
  }
183
284
 
184
285
  // src/resolver.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fhir-dsl/terminology",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "Compile-time FHIR terminology resolver for ValueSets and CodeSystems",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,7 +22,7 @@
22
22
  "dist"
23
23
  ],
24
24
  "dependencies": {
25
- "@fhir-dsl/utils": "0.32.0"
25
+ "@fhir-dsl/utils": "0.34.0"
26
26
  },
27
27
  "author": "Abdelhadi Sabani",
28
28
  "license": "MIT",