@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 +109 -8
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +109 -8
- package/package.json +2 -2
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
178
|
-
|
|
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.
|
|
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.
|
|
25
|
+
"@fhir-dsl/utils": "0.34.0"
|
|
26
26
|
},
|
|
27
27
|
"author": "Abdelhadi Sabani",
|
|
28
28
|
"license": "MIT",
|