fhirsmith 0.17.0 → 0.18.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/CHANGELOG.md +113 -0
- package/README.md +80 -13
- package/config-template.json +106 -1
- package/ips/ai-client.js +263 -0
- package/ips/composer.js +805 -0
- package/ips/editor-schema.js +289 -0
- package/ips/editor.js +290 -0
- package/ips/fhirpath-ips.js +397 -0
- package/ips/ips-codeable-concepts.md +1742 -0
- package/ips/ips-template.html +125 -0
- package/ips/ips-valuesets.js +159 -0
- package/ips/ips.js +1736 -0
- package/ips/membership.js +179 -0
- package/ips/pipeline.js +1012 -0
- package/ips/presenter.js +974 -0
- package/ips/profile-editor.js +578 -0
- package/ips/profiles.js +506 -0
- package/ips/readme.md +460 -0
- package/ips/render.js +1482 -0
- package/ips/search-params.js +83 -0
- package/ips/search.js +667 -0
- package/ips/share.js +178 -0
- package/ips/store.js +650 -0
- package/ips/terminology.js +395 -0
- package/ips/tx-client.js +202 -0
- package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
- package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
- package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
- package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
- package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
- package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
- package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
- package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
- package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
- package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
- package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
- package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
- package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-medication.json +50 -0
- package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
- package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
- package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
- package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
- package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
- package/ips/valuesets/ValueSet-ips-route.json +46 -0
- package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
- package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
- package/ips/valuesets/elements.json +44 -0
- package/library/daily-report.js +432 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +21 -2
- package/library/npm-audit.js +25 -0
- package/library/package-manager.js +4 -2
- package/library/sqlite-session-store.js +29 -1
- package/package.json +8 -3
- package/packages/packages.js +34 -3
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +41 -1
- package/registry/documentation/api.md +162 -0
- package/registry/documentation/configuration.md +93 -0
- package/registry/documentation/documents.md +74 -0
- package/registry/documentation/internals.md +151 -0
- package/registry/documentation/readme.md +100 -0
- package/registry/documentation/security.md +199 -0
- package/registry/documents.js +309 -0
- package/registry/openapi.yaml +30 -0
- package/registry/registry-template.html +1 -1
- package/registry/registry.js +230 -44
- package/registry/uptime.js +154 -0
- package/server.js +105 -44
- package/shl/shl.js +64 -9
- package/stats-db.js +93 -0
- package/stats.js +158 -1
- package/testing/search.js +2 -108
- package/testing/store.js +5 -0
- package/testing/testing.js +20 -0
- package/token/token-template.html +5 -4
- package/token/token.js +17 -10
- package/translations/Messages.properties +10 -0
- package/tx/README.md +52 -0
- package/tx/closure/closure-store.js +14 -0
- package/tx/cs/cs-api.js +92 -14
- package/tx/cs/cs-cs.js +64 -3
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +431 -0
- package/tx/cs/cs-loinc.js +296 -27
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +1210 -80
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/csv-records.js +35 -0
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/importers/import-loinc.module.js +177 -41
- package/tx/importers/import-sct.module.js +66 -1
- package/tx/importers/subset-loinc.module.js +25 -6
- package/tx/library/codesystem.js +4 -1
- package/tx/library/designations.js +23 -30
- package/tx/library.js +98 -4
- package/tx/openapi-operations.js +6 -1
- package/tx/params.js +8 -1
- package/tx/provider.js +24 -6
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/sct/structures.js +8 -2
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +15 -1
- package/tx/tx.fhir.org.yml +3 -4
- package/tx/tx.js +144 -2
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/closure.js +20 -0
- package/tx/workers/expand.js +35 -8
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +138 -6
- package/utilities/make-sct-loinc-subset.js +250 -0
- package/utilities/make-sct-test-extension.js +264 -0
- package/xig/xig.js +32 -0
- package/registry/readme.md +0 -124
package/tx/cs/cs-icf.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ICF (http://hl7.org/fhir/sid/icf).
|
|
3
|
+
*
|
|
4
|
+
* The content is an ordinary CodeSystem resource (fhir.tx.support has 2001a and 2017a), so
|
|
5
|
+
* this extends FhirCodeSystemProvider rather than being a factory with its own data:
|
|
6
|
+
* FhirCodeSystemProvider.create() picks this class by url.
|
|
7
|
+
*
|
|
8
|
+
* What ICF adds is a grammar: a category may be followed by '.' and qualifier digits, one per
|
|
9
|
+
* position, and what the positions are depends on the component - the first letter of the code
|
|
10
|
+
* (see QUALIFIERS):
|
|
11
|
+
* b body functions extent
|
|
12
|
+
* s body structures extent, nature, localization
|
|
13
|
+
* d activities/participation performance, capacity, then optionally capacity with
|
|
14
|
+
* assistance and performance without assistance
|
|
15
|
+
* e environmental factors one: a barrier after '.', or a facilitator after '+' (in
|
|
16
|
+
* place of the '.') - e310.2, e310+3
|
|
17
|
+
* Each digit must be a value of its position's scale, which the code system carries as the
|
|
18
|
+
* concepts q1-n (extent of impairment), q2-n (nature), q3-n (localization), q4-n (extent of
|
|
19
|
+
* problem), q5-n (barrier) and q6-n (facilitator, which has no 9), under the group concepts
|
|
20
|
+
* q1..q6. A position cannot be omitted; 8 (not specified) is
|
|
21
|
+
* what fills it, and 8 says nothing in the display. Each position is reported by $lookup as the
|
|
22
|
+
* property named in QUALIFIERS.
|
|
23
|
+
*
|
|
24
|
+
* Qualifiers are not mandatory at the code system level: a plain category is still a valid
|
|
25
|
+
* code. Qualified codes cannot be enumerated, so the code system is not closed.
|
|
26
|
+
*
|
|
27
|
+
* Value sets control qualifiers with filters:
|
|
28
|
+
* <qualifier property> exists true|false - the qualifier is required / not allowed. 8 (not
|
|
29
|
+
* specified) counts as absent, so it never satisfies exists = true, and always satisfies
|
|
30
|
+
* exists = false. A qualifier the code's component does not have is absent
|
|
31
|
+
* qualified = true|false - qualified codes only / no qualified codes at all (b1100.888
|
|
32
|
+
* is qualified, though it says nothing)
|
|
33
|
+
* These combine with the other filters in an include as usual. A qualified code is in any
|
|
34
|
+
* other filter's results when its category is.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const { CodeSystem } = require('../library/codesystem');
|
|
38
|
+
const { Issue } = require('../library/operation-outcome');
|
|
39
|
+
const { FhirCodeSystemProvider, FhirCodeSystemProviderContext, FhirCodeSystemProviderFilterContext } = require('./cs-cs');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The qualifier positions of each component, by the separator that introduces them: the
|
|
43
|
+
* property that reports the qualifier, the scale (group concept) its values come from, and the
|
|
44
|
+
* label that introduces its value in the display (none where the value says what it is).
|
|
45
|
+
* Only environmental factors take '+', which makes the qualifier a facilitator.
|
|
46
|
+
*/
|
|
47
|
+
const QUALIFIERS = {
|
|
48
|
+
b: {
|
|
49
|
+
'.': [{ property: 'extent', scale: 'q1' }]
|
|
50
|
+
},
|
|
51
|
+
s: {
|
|
52
|
+
'.': [
|
|
53
|
+
{ property: 'extent', scale: 'q1' },
|
|
54
|
+
{ property: 'nature', scale: 'q2' },
|
|
55
|
+
{ property: 'localization', scale: 'q3' }
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
d: {
|
|
59
|
+
'.': [
|
|
60
|
+
{ property: 'performance', scale: 'q4', label: 'performance' },
|
|
61
|
+
{ property: 'capacity', scale: 'q4', label: 'capacity' },
|
|
62
|
+
{ property: 'capacity-with-assistance', scale: 'q4', label: 'capacity with assistance' },
|
|
63
|
+
{ property: 'performance-without-assistance', scale: 'q4', label: 'performance without assistance' }
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
e: {
|
|
67
|
+
'.': [{ property: 'barrier', scale: 'q5' }],
|
|
68
|
+
'+': [{ property: 'facilitator', scale: 'q6' }]
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const COMPONENT_NAMES = { b: 'body functions', s: 'body structures', d: 'activities and participation', e: 'environmental factors' };
|
|
73
|
+
const COMPONENT_CODES = { b: 'a body functions (b) code', s: 'a body structures (s) code', d: 'an activities and participation (d) code', e: 'an environmental factors (e) code' };
|
|
74
|
+
|
|
75
|
+
const QUALIFIER_PROPERTIES = new Set(Object.values(QUALIFIERS).flatMap(c => Object.values(c)).flatMap(q => q.map(p => p.property)));
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A category with qualifiers. code and concept are the category's, so whatever is inherited
|
|
79
|
+
* from FhirCodeSystemProvider describes the category; qualifiedCode is the code as a whole.
|
|
80
|
+
*/
|
|
81
|
+
class IcfQualifiedContext extends FhirCodeSystemProviderContext {
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} qualifiedCode - e.g. s7300.183
|
|
84
|
+
* @param {Object} concept - the category's concept
|
|
85
|
+
* @param {{position: number, digit: string, code: string, concept: Object, property: string}[]} qualifiers
|
|
86
|
+
*/
|
|
87
|
+
constructor(qualifiedCode, concept, qualifiers) {
|
|
88
|
+
super(concept.code, concept);
|
|
89
|
+
this.qualifiedCode = qualifiedCode;
|
|
90
|
+
this.qualifiers = qualifiers;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The result of a qualifier filter. test() decides membership for a located code, qualified or
|
|
96
|
+
* not. concepts (what iteration walks) holds only the plain categories that pass, since
|
|
97
|
+
* qualified codes cannot be listed; closesQualified marks the one filter (qualified = false)
|
|
98
|
+
* that admits no qualified code, so an include with it is closed again.
|
|
99
|
+
*/
|
|
100
|
+
class IcfQualifierFilter extends FhirCodeSystemProviderFilterContext {
|
|
101
|
+
constructor(test, closesQualified) {
|
|
102
|
+
super();
|
|
103
|
+
this.test = test;
|
|
104
|
+
this.closesQualified = closesQualified;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function propertyValue(concept, code) {
|
|
109
|
+
const p = (concept.property || []).find(p => p.code === code);
|
|
110
|
+
return p ? (p.valueCode ?? p.valueBoolean ?? p.valueString) : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class IcfCodeSystemProvider extends FhirCodeSystemProvider {
|
|
114
|
+
|
|
115
|
+
/** Qualified codes are a grammar: they cannot all be listed. */
|
|
116
|
+
isNotClosed() {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// -- locating -------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A code in the code system, or a selectable category followed by '.' (or, for an
|
|
124
|
+
* environmental factor, '+') and its qualifier digits. An unknown category is a plain miss
|
|
125
|
+
* (no message); a known category with bad qualifiers says what is wrong with them.
|
|
126
|
+
*/
|
|
127
|
+
async locate(code) {
|
|
128
|
+
const base = await super.locate(code);
|
|
129
|
+
if (base.context || typeof code !== 'string') {
|
|
130
|
+
return base;
|
|
131
|
+
}
|
|
132
|
+
const sep = code.search(/[.+]/);
|
|
133
|
+
if (sep <= 0) {
|
|
134
|
+
return base;
|
|
135
|
+
}
|
|
136
|
+
const separator = code[sep];
|
|
137
|
+
const stemCode = code.substring(0, sep);
|
|
138
|
+
const digits = code.substring(sep + 1);
|
|
139
|
+
const stem = this.codeSystem.getConceptByCode(stemCode);
|
|
140
|
+
if (!stem) {
|
|
141
|
+
return { context: null, message: undefined };
|
|
142
|
+
}
|
|
143
|
+
if (!this.#isQualifiable(stem)) {
|
|
144
|
+
return { context: null, message: `The ICF code '${code}' is not valid: qualifiers can only follow a selectable ICF category, and '${stemCode}' is not one` };
|
|
145
|
+
}
|
|
146
|
+
const component = stemCode[0];
|
|
147
|
+
const positions = (QUALIFIERS[component] || {})[separator];
|
|
148
|
+
if (!positions) {
|
|
149
|
+
return { context: null, message: separator === '+'
|
|
150
|
+
? `The ICF code '${code}' is not valid: only an environmental factor (e) can be a facilitator, written with '+'`
|
|
151
|
+
: `The ICF code '${code}' is not valid: qualifiers on ${COMPONENT_NAMES[component] || 'these'} codes are not supported` };
|
|
152
|
+
}
|
|
153
|
+
const names = positions.map(q => q.property).join(', ');
|
|
154
|
+
if (!new RegExp(`^[0-9]{1,${positions.length}}$`).test(digits)) {
|
|
155
|
+
return { context: null, message: positions.length === 1
|
|
156
|
+
? `The ICF code '${code}' is not valid: ${COMPONENT_CODES[component]} takes one qualifier digit after the '${separator}' (${names})`
|
|
157
|
+
: `The ICF code '${code}' is not valid: ${COMPONENT_CODES[component]} takes 1 to ${positions.length} qualifier digits after the '${separator}' (${names})` };
|
|
158
|
+
}
|
|
159
|
+
const qualifiers = [];
|
|
160
|
+
for (let i = 0; i < digits.length; i++) {
|
|
161
|
+
const position = positions[i];
|
|
162
|
+
const concept = this.codeSystem.getConceptByCode(position.scale + '-' + digits[i]);
|
|
163
|
+
if (!concept) {
|
|
164
|
+
return { context: null, message: `The ICF code '${code}' is not valid: '${digits[i]}' is not a valid value for qualifier ${i + 1} (${position.property})` };
|
|
165
|
+
}
|
|
166
|
+
qualifiers.push({ position: i + 1, digit: digits[i], code: concept.code, concept, property: position.property, label: position.label });
|
|
167
|
+
}
|
|
168
|
+
return { context: new IcfQualifiedContext(code, stem, qualifiers), message: null };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A selectable primary category: not a heading, and not a qualifier value. */
|
|
172
|
+
#isQualifiable(concept) {
|
|
173
|
+
const kind = propertyValue(concept, 'kind');
|
|
174
|
+
if (kind !== undefined && kind !== 'primary') {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
return propertyValue(concept, 'notSelectable') !== true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async #resolve(context) {
|
|
181
|
+
if (typeof context === 'string') {
|
|
182
|
+
const r = await this.locate(context);
|
|
183
|
+
if (!r.context) {
|
|
184
|
+
throw new Error(r.message ? r.message : `Code '${context}' not found in CodeSystem '${this.system()}'`);
|
|
185
|
+
}
|
|
186
|
+
return r.context;
|
|
187
|
+
}
|
|
188
|
+
return context || null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// -- concept information --------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
async code(context) {
|
|
194
|
+
const ctxt = await this.#resolve(context);
|
|
195
|
+
return ctxt instanceof IcfQualifiedContext ? ctxt.qualifiedCode : super.code(ctxt);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The category's display, then the qualifiers' displays, leaving out any 8 (not specified). */
|
|
199
|
+
async display(context) {
|
|
200
|
+
const ctxt = await this.#resolve(context);
|
|
201
|
+
const display = await super.display(ctxt);
|
|
202
|
+
if (!(ctxt instanceof IcfQualifiedContext)) {
|
|
203
|
+
return display;
|
|
204
|
+
}
|
|
205
|
+
const parts = ctxt.qualifiers.filter(q => q.digit !== '8')
|
|
206
|
+
.map(q => (q.label ? q.label + ' ' : '') + (q.concept.display || '').trim());
|
|
207
|
+
return parts.length > 0 ? display + ': ' + parts.join(', ') : display;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A qualified code has one designation: its display. The category's translations do not describe it. */
|
|
211
|
+
async designations(context, displays) {
|
|
212
|
+
const ctxt = await this.#resolve(context);
|
|
213
|
+
if (!(ctxt instanceof IcfQualifiedContext)) {
|
|
214
|
+
return super.designations(ctxt, displays);
|
|
215
|
+
}
|
|
216
|
+
const lang = this.defaultLanguage ? this.defaultLanguage.toString() : null;
|
|
217
|
+
displays.addDesignation(true, 'active', lang, CodeSystem.makeUseForDisplay(), await this.display(ctxt));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A qualified code has the category's properties, less parent (its parent is the category),
|
|
222
|
+
* plus one property per qualifier, named by the qualifier's scale (extent, nature, localization).
|
|
223
|
+
*/
|
|
224
|
+
async properties(context) {
|
|
225
|
+
const ctxt = await this.#resolve(context);
|
|
226
|
+
const props = await super.properties(ctxt);
|
|
227
|
+
if (!(ctxt instanceof IcfQualifiedContext)) {
|
|
228
|
+
return props;
|
|
229
|
+
}
|
|
230
|
+
const result = props.filter(p => p.code !== 'parent');
|
|
231
|
+
for (const q of ctxt.qualifiers) {
|
|
232
|
+
result.push({ code: q.property, valueCode: q.code, definition: this.getPropertyDefinition(this.codeSystem.jsonObj, q.property) });
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async parent(context) {
|
|
238
|
+
const ctxt = await this.#resolve(context);
|
|
239
|
+
return ctxt instanceof IcfQualifiedContext ? ctxt.code : super.parent(ctxt);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async sameConcept(a, b) {
|
|
243
|
+
const ca = await this.#resolve(a);
|
|
244
|
+
const cb = await this.#resolve(b);
|
|
245
|
+
if (!ca || !cb) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
return (await this.code(ca)) === (await this.code(cb));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** As for any concept, but a qualified code has no children. */
|
|
252
|
+
async extendLookup(ctxt, props, params) {
|
|
253
|
+
const start = params.length;
|
|
254
|
+
await super.extendLookup(ctxt, props, params);
|
|
255
|
+
if (ctxt instanceof IcfQualifiedContext) {
|
|
256
|
+
for (let i = params.length - 1; i >= start; i--) {
|
|
257
|
+
if (params[i].name === 'property' && params[i].part?.[0]?.valueCode === 'child') {
|
|
258
|
+
params.splice(i, 1);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// -- hierarchy ------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Whether `general` subsumes `specific` or is it: a qualified code sits under its category,
|
|
268
|
+
* and so under everything the category does. Qualified codes are related only to themselves.
|
|
269
|
+
*/
|
|
270
|
+
#isA(specific, general) {
|
|
271
|
+
if (general instanceof IcfQualifiedContext) {
|
|
272
|
+
return specific instanceof IcfQualifiedContext && specific.qualifiedCode === general.qualifiedCode;
|
|
273
|
+
}
|
|
274
|
+
const code = specific.code; // the category, for a qualified code
|
|
275
|
+
if (code === general.code) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
return this.codeSystem.isDescendantOf(code, general.code);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async locateIsA(code, parent, disallowSelf = false) {
|
|
282
|
+
const codeResult = await this.locate(code);
|
|
283
|
+
if (!codeResult.context) {
|
|
284
|
+
return codeResult;
|
|
285
|
+
}
|
|
286
|
+
const parentResult = await this.locate(parent);
|
|
287
|
+
if (!(codeResult.context instanceof IcfQualifiedContext) && !(parentResult.context instanceof IcfQualifiedContext)) {
|
|
288
|
+
return super.locateIsA(code, parent, disallowSelf);
|
|
289
|
+
}
|
|
290
|
+
if (!parentResult.context) {
|
|
291
|
+
return { context: null, message: `Parent code '${parent}' not found in CodeSystem '${this.system()}'` };
|
|
292
|
+
}
|
|
293
|
+
if (code === parent) {
|
|
294
|
+
return disallowSelf ? { context: null, message: `Code '${code}' cannot be the same as its parent` } : codeResult;
|
|
295
|
+
}
|
|
296
|
+
if (this.#isA(codeResult.context, parentResult.context)) {
|
|
297
|
+
return codeResult;
|
|
298
|
+
}
|
|
299
|
+
return { context: null, message: `Code '${code}' is not a descendant of '${parent}'` };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async subsumesTest(codeA, codeB) {
|
|
303
|
+
const a = (await this.locate(codeA)).context;
|
|
304
|
+
if (!a) {
|
|
305
|
+
throw new Error(`Unknown Code "${codeA}"`);
|
|
306
|
+
}
|
|
307
|
+
const b = (await this.locate(codeB)).context;
|
|
308
|
+
if (!b) {
|
|
309
|
+
throw new Error(`Unknown Code "${codeB}"`);
|
|
310
|
+
}
|
|
311
|
+
if (!(a instanceof IcfQualifiedContext) && !(b instanceof IcfQualifiedContext)) {
|
|
312
|
+
return super.subsumesTest(codeA, codeB);
|
|
313
|
+
}
|
|
314
|
+
if ((await this.code(a)) === (await this.code(b))) {
|
|
315
|
+
return 'equivalent';
|
|
316
|
+
}
|
|
317
|
+
if (this.#isA(b, a)) {
|
|
318
|
+
return 'subsumes';
|
|
319
|
+
}
|
|
320
|
+
if (this.#isA(a, b)) {
|
|
321
|
+
return 'subsumed-by';
|
|
322
|
+
}
|
|
323
|
+
return 'not-subsumed';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** A qualified code has no children. */
|
|
327
|
+
async iterator(context) {
|
|
328
|
+
if (context) {
|
|
329
|
+
const ctxt = await this.#resolve(context);
|
|
330
|
+
if (ctxt instanceof IcfQualifiedContext) {
|
|
331
|
+
return { type: 'children', parentCode: ctxt.qualifiedCode, codes: [], current: 0, total: 0 };
|
|
332
|
+
}
|
|
333
|
+
return super.iterator(ctxt);
|
|
334
|
+
}
|
|
335
|
+
return super.iterator(context);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// -- filters --------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
#isQualifierFilter(prop, op) {
|
|
341
|
+
return (op === 'exists' && QUALIFIER_PROPERTIES.has(prop)) || (prop === 'qualified' && op === '=');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async doesFilter(prop, op, value) {
|
|
345
|
+
if (this.#isQualifierFilter(prop, op)) {
|
|
346
|
+
return value === 'true' || value === 'false';
|
|
347
|
+
}
|
|
348
|
+
return super.doesFilter(prop, op, value);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async filter(filterContext, forIteration, prop, op, value, fc) {
|
|
352
|
+
if (!this.#isQualifierFilter(prop, op)) {
|
|
353
|
+
return super.filter(filterContext, forIteration, prop, op, value, fc);
|
|
354
|
+
}
|
|
355
|
+
if (value !== 'true' && value !== 'false') {
|
|
356
|
+
throw new Issue('error', 'exception', null, 'FILTER_NOT_UNDERSTOOD',
|
|
357
|
+
this.opContext.i18n.translate('FILTER_NOT_UNDERSTOOD', this.opContext.langs, [prop, op, value]), 'vs-invalid', 422);
|
|
358
|
+
}
|
|
359
|
+
const want = value === 'true';
|
|
360
|
+
let test;
|
|
361
|
+
if (prop === 'qualified') {
|
|
362
|
+
test = (ctxt) => (ctxt instanceof IcfQualifiedContext) === want;
|
|
363
|
+
} else {
|
|
364
|
+
test = (ctxt) => {
|
|
365
|
+
const q = ctxt instanceof IcfQualifiedContext ? ctxt.qualifiers.find(q => q.property === prop) : undefined;
|
|
366
|
+
return (q !== undefined && q.digit !== '8') === want;
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const results = new IcfQualifierFilter(test, prop === 'qualified' && !want);
|
|
370
|
+
// the plain categories that pass: none for exists = true or qualified = true
|
|
371
|
+
for (const code of this.codeSystem.getAllCodes()) {
|
|
372
|
+
const concept = this.codeSystem.getConceptByCode(code);
|
|
373
|
+
if (concept && this.#isPrimary(concept) && test(new FhirCodeSystemProviderContext(code, concept))) {
|
|
374
|
+
results.add(concept);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (!filterContext.filters) {
|
|
378
|
+
filterContext.filters = [];
|
|
379
|
+
}
|
|
380
|
+
filterContext.filters.push(results);
|
|
381
|
+
return results;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** A primary ICF category (heading or not), as opposed to a qualifier value. */
|
|
385
|
+
#isPrimary(concept) {
|
|
386
|
+
const kind = propertyValue(concept, 'kind');
|
|
387
|
+
return kind === undefined || kind === 'primary';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Every include can admit qualified codes, unless it has a qualified = false filter. */
|
|
391
|
+
async filtersNotClosed(filterContext) {
|
|
392
|
+
return !(filterContext.filters || []).some(f => f instanceof IcfQualifierFilter && f.closesQualified);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async filterCheck(filterContext, set, concept) {
|
|
396
|
+
if (set instanceof IcfQualifierFilter && concept instanceof IcfQualifiedContext) {
|
|
397
|
+
return set.test(concept) ? true : `Code '${concept.qualifiedCode}' does not meet the qualifier rules`;
|
|
398
|
+
}
|
|
399
|
+
return super.filterCheck(filterContext, set, concept);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* A qualified code is in a filter's results when its category is: it is-a the category,
|
|
404
|
+
* and has the category's properties. (filterCheck needs nothing extra: it checks the
|
|
405
|
+
* context's concept, which is the category's.)
|
|
406
|
+
*/
|
|
407
|
+
async filterLocate(filterContext, set, code) {
|
|
408
|
+
if (set instanceof IcfQualifierFilter) {
|
|
409
|
+
const loc = await this.locate(code);
|
|
410
|
+
if (!loc.context) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
return (await this.filterCheck(filterContext, set, loc.context)) === true ? loc.context : null;
|
|
414
|
+
}
|
|
415
|
+
const found = await super.filterLocate(filterContext, set, code);
|
|
416
|
+
if (found) {
|
|
417
|
+
return found;
|
|
418
|
+
}
|
|
419
|
+
const loc = await this.locate(code);
|
|
420
|
+
if (!(loc.context instanceof IcfQualifiedContext)) {
|
|
421
|
+
return found;
|
|
422
|
+
}
|
|
423
|
+
const stem = await super.filterLocate(filterContext, set, loc.context.code);
|
|
424
|
+
return stem && typeof stem !== 'string' ? loc.context : null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
module.exports = {
|
|
429
|
+
IcfCodeSystemProvider,
|
|
430
|
+
IcfQualifiedContext
|
|
431
|
+
};
|