fhirsmith 0.17.0 → 0.18.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/CHANGELOG.md +83 -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 +422 -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 +7 -2
- 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 +9 -0
- package/tx/README.md +52 -0
- package/tx/cs/cs-api.js +90 -14
- package/tx/cs/cs-cs.js +56 -1
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +418 -0
- package/tx/cs/cs-loinc.js +47 -25
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +918 -55
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/library.js +98 -4
- package/tx/openapi-operations.js +4 -0
- package/tx/params.js +8 -1
- package/tx/provider.js +16 -4
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- 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 +2 -3
- package/tx/tx.js +144 -2
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/expand.js +33 -6
- 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 +128 -6
- 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,418 @@
|
|
|
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 not supported yet (barriers '.', facilitators '+')
|
|
16
|
+
* Each digit must be a value of its position's scale, which the code system carries as the
|
|
17
|
+
* concepts q1-n (extent of impairment), q2-n (nature), q3-n (localization) and q4-n (extent of
|
|
18
|
+
* problem), under the group concepts q1..q4. A position cannot be omitted; 8 (not specified) is
|
|
19
|
+
* what fills it, and 8 says nothing in the display. Each position is reported by $lookup as the
|
|
20
|
+
* property named in QUALIFIERS.
|
|
21
|
+
*
|
|
22
|
+
* Qualifiers are not mandatory at the code system level: a plain category is still a valid
|
|
23
|
+
* code. Qualified codes cannot be enumerated, so the code system is not closed.
|
|
24
|
+
*
|
|
25
|
+
* Value sets control qualifiers with filters:
|
|
26
|
+
* <qualifier property> exists true|false - the qualifier is required / not allowed. 8 (not
|
|
27
|
+
* specified) counts as absent, so it never satisfies exists = true, and always satisfies
|
|
28
|
+
* exists = false. A qualifier the code's component does not have is absent
|
|
29
|
+
* qualified = true|false - qualified codes only / no qualified codes at all (b1100.888
|
|
30
|
+
* is qualified, though it says nothing)
|
|
31
|
+
* These combine with the other filters in an include as usual. A qualified code is in any
|
|
32
|
+
* other filter's results when its category is.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const { CodeSystem } = require('../library/codesystem');
|
|
36
|
+
const { Issue } = require('../library/operation-outcome');
|
|
37
|
+
const { FhirCodeSystemProvider, FhirCodeSystemProviderContext, FhirCodeSystemProviderFilterContext } = require('./cs-cs');
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The qualifier positions of each component: the property that reports the qualifier, the
|
|
41
|
+
* scale (group concept) its values come from, and the label that introduces its value in the
|
|
42
|
+
* display (none for b and s, whose qualifiers say what they are).
|
|
43
|
+
*/
|
|
44
|
+
const QUALIFIERS = {
|
|
45
|
+
b: [
|
|
46
|
+
{ property: 'extent', scale: 'q1' }
|
|
47
|
+
],
|
|
48
|
+
s: [
|
|
49
|
+
{ property: 'extent', scale: 'q1' },
|
|
50
|
+
{ property: 'nature', scale: 'q2' },
|
|
51
|
+
{ property: 'localization', scale: 'q3' }
|
|
52
|
+
],
|
|
53
|
+
d: [
|
|
54
|
+
{ property: 'performance', scale: 'q4', label: 'performance' },
|
|
55
|
+
{ property: 'capacity', scale: 'q4', label: 'capacity' },
|
|
56
|
+
{ property: 'capacity-with-assistance', scale: 'q4', label: 'capacity with assistance' },
|
|
57
|
+
{ property: 'performance-without-assistance', scale: 'q4', label: 'performance without assistance' }
|
|
58
|
+
],
|
|
59
|
+
e: null // barriers and facilitators: not supported yet
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const COMPONENT_NAMES = { b: 'body functions', s: 'body structures', d: 'activities and participation', e: 'environmental factors' };
|
|
63
|
+
const COMPONENT_CODES = { b: 'a body functions (b) code', s: 'a body structures (s) code', d: 'an activities and participation (d) code' };
|
|
64
|
+
|
|
65
|
+
const QUALIFIER_PROPERTIES = new Set(Object.values(QUALIFIERS).filter(q => q).flatMap(q => q.map(p => p.property)));
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A category with qualifiers. code and concept are the category's, so whatever is inherited
|
|
69
|
+
* from FhirCodeSystemProvider describes the category; qualifiedCode is the code as a whole.
|
|
70
|
+
*/
|
|
71
|
+
class IcfQualifiedContext extends FhirCodeSystemProviderContext {
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} qualifiedCode - e.g. s7300.183
|
|
74
|
+
* @param {Object} concept - the category's concept
|
|
75
|
+
* @param {{position: number, digit: string, code: string, concept: Object, property: string}[]} qualifiers
|
|
76
|
+
*/
|
|
77
|
+
constructor(qualifiedCode, concept, qualifiers) {
|
|
78
|
+
super(concept.code, concept);
|
|
79
|
+
this.qualifiedCode = qualifiedCode;
|
|
80
|
+
this.qualifiers = qualifiers;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The result of a qualifier filter. test() decides membership for a located code, qualified or
|
|
86
|
+
* not. concepts (what iteration walks) holds only the plain categories that pass, since
|
|
87
|
+
* qualified codes cannot be listed; closesQualified marks the one filter (qualified = false)
|
|
88
|
+
* that admits no qualified code, so an include with it is closed again.
|
|
89
|
+
*/
|
|
90
|
+
class IcfQualifierFilter extends FhirCodeSystemProviderFilterContext {
|
|
91
|
+
constructor(test, closesQualified) {
|
|
92
|
+
super();
|
|
93
|
+
this.test = test;
|
|
94
|
+
this.closesQualified = closesQualified;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function propertyValue(concept, code) {
|
|
99
|
+
const p = (concept.property || []).find(p => p.code === code);
|
|
100
|
+
return p ? (p.valueCode ?? p.valueBoolean ?? p.valueString) : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class IcfCodeSystemProvider extends FhirCodeSystemProvider {
|
|
104
|
+
|
|
105
|
+
/** Qualified codes are a grammar: they cannot all be listed. */
|
|
106
|
+
isNotClosed() {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// -- locating -------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A code in the code system, or a selectable category followed by '.' and one to three
|
|
114
|
+
* qualifier digits. An unknown category is a plain miss (no message); a known category
|
|
115
|
+
* with bad qualifiers says what is wrong with them.
|
|
116
|
+
*/
|
|
117
|
+
async locate(code) {
|
|
118
|
+
const base = await super.locate(code);
|
|
119
|
+
if (base.context || typeof code !== 'string') {
|
|
120
|
+
return base;
|
|
121
|
+
}
|
|
122
|
+
const dot = code.indexOf('.');
|
|
123
|
+
if (dot <= 0) {
|
|
124
|
+
return base;
|
|
125
|
+
}
|
|
126
|
+
const stemCode = code.substring(0, dot);
|
|
127
|
+
const digits = code.substring(dot + 1);
|
|
128
|
+
const stem = this.codeSystem.getConceptByCode(stemCode);
|
|
129
|
+
if (!stem) {
|
|
130
|
+
return { context: null, message: undefined };
|
|
131
|
+
}
|
|
132
|
+
if (!this.#isQualifiable(stem)) {
|
|
133
|
+
return { context: null, message: `The ICF code '${code}' is not valid: qualifiers can only follow a selectable ICF category, and '${stemCode}' is not one` };
|
|
134
|
+
}
|
|
135
|
+
const component = stemCode[0];
|
|
136
|
+
const positions = QUALIFIERS[component];
|
|
137
|
+
if (!positions) {
|
|
138
|
+
return { context: null, message: `The ICF code '${code}' is not valid: qualifiers on ${COMPONENT_NAMES[component] || 'these'} codes are not supported` };
|
|
139
|
+
}
|
|
140
|
+
const names = positions.map(q => q.property).join(', ');
|
|
141
|
+
if (!new RegExp(`^[0-9]{1,${positions.length}}$`).test(digits)) {
|
|
142
|
+
return { context: null, message: positions.length === 1
|
|
143
|
+
? `The ICF code '${code}' is not valid: ${COMPONENT_CODES[component]} takes one qualifier digit after the '.' (${names})`
|
|
144
|
+
: `The ICF code '${code}' is not valid: ${COMPONENT_CODES[component]} takes 1 to ${positions.length} qualifier digits after the '.' (${names})` };
|
|
145
|
+
}
|
|
146
|
+
const qualifiers = [];
|
|
147
|
+
for (let i = 0; i < digits.length; i++) {
|
|
148
|
+
const position = positions[i];
|
|
149
|
+
const concept = this.codeSystem.getConceptByCode(position.scale + '-' + digits[i]);
|
|
150
|
+
if (!concept) {
|
|
151
|
+
return { context: null, message: `The ICF code '${code}' is not valid: '${digits[i]}' is not a valid value for qualifier ${i + 1} (${position.property})` };
|
|
152
|
+
}
|
|
153
|
+
qualifiers.push({ position: i + 1, digit: digits[i], code: concept.code, concept, property: position.property, label: position.label });
|
|
154
|
+
}
|
|
155
|
+
return { context: new IcfQualifiedContext(code, stem, qualifiers), message: null };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A selectable primary category: not a heading, and not a qualifier value. */
|
|
159
|
+
#isQualifiable(concept) {
|
|
160
|
+
const kind = propertyValue(concept, 'kind');
|
|
161
|
+
if (kind !== undefined && kind !== 'primary') {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return propertyValue(concept, 'notSelectable') !== true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async #resolve(context) {
|
|
168
|
+
if (typeof context === 'string') {
|
|
169
|
+
const r = await this.locate(context);
|
|
170
|
+
if (!r.context) {
|
|
171
|
+
throw new Error(r.message ? r.message : `Code '${context}' not found in CodeSystem '${this.system()}'`);
|
|
172
|
+
}
|
|
173
|
+
return r.context;
|
|
174
|
+
}
|
|
175
|
+
return context || null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// -- concept information --------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
async code(context) {
|
|
181
|
+
const ctxt = await this.#resolve(context);
|
|
182
|
+
return ctxt instanceof IcfQualifiedContext ? ctxt.qualifiedCode : super.code(ctxt);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The category's display, then the qualifiers' displays, leaving out any 8 (not specified). */
|
|
186
|
+
async display(context) {
|
|
187
|
+
const ctxt = await this.#resolve(context);
|
|
188
|
+
const display = await super.display(ctxt);
|
|
189
|
+
if (!(ctxt instanceof IcfQualifiedContext)) {
|
|
190
|
+
return display;
|
|
191
|
+
}
|
|
192
|
+
const parts = ctxt.qualifiers.filter(q => q.digit !== '8')
|
|
193
|
+
.map(q => (q.label ? q.label + ' ' : '') + (q.concept.display || '').trim());
|
|
194
|
+
return parts.length > 0 ? display + ': ' + parts.join(', ') : display;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** A qualified code has one designation: its display. The category's translations do not describe it. */
|
|
198
|
+
async designations(context, displays) {
|
|
199
|
+
const ctxt = await this.#resolve(context);
|
|
200
|
+
if (!(ctxt instanceof IcfQualifiedContext)) {
|
|
201
|
+
return super.designations(ctxt, displays);
|
|
202
|
+
}
|
|
203
|
+
const lang = this.defaultLanguage ? this.defaultLanguage.toString() : null;
|
|
204
|
+
displays.addDesignation(true, 'active', lang, CodeSystem.makeUseForDisplay(), await this.display(ctxt));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A qualified code has the category's properties, less parent (its parent is the category),
|
|
209
|
+
* plus one property per qualifier, named by the qualifier's scale (extent, nature, localization).
|
|
210
|
+
*/
|
|
211
|
+
async properties(context) {
|
|
212
|
+
const ctxt = await this.#resolve(context);
|
|
213
|
+
const props = await super.properties(ctxt);
|
|
214
|
+
if (!(ctxt instanceof IcfQualifiedContext)) {
|
|
215
|
+
return props;
|
|
216
|
+
}
|
|
217
|
+
const result = props.filter(p => p.code !== 'parent');
|
|
218
|
+
for (const q of ctxt.qualifiers) {
|
|
219
|
+
result.push({ code: q.property, valueCode: q.code, definition: this.getPropertyDefinition(this.codeSystem.jsonObj, q.property) });
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async parent(context) {
|
|
225
|
+
const ctxt = await this.#resolve(context);
|
|
226
|
+
return ctxt instanceof IcfQualifiedContext ? ctxt.code : super.parent(ctxt);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async sameConcept(a, b) {
|
|
230
|
+
const ca = await this.#resolve(a);
|
|
231
|
+
const cb = await this.#resolve(b);
|
|
232
|
+
if (!ca || !cb) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
return (await this.code(ca)) === (await this.code(cb));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** As for any concept, but a qualified code has no children. */
|
|
239
|
+
async extendLookup(ctxt, props, params) {
|
|
240
|
+
const start = params.length;
|
|
241
|
+
await super.extendLookup(ctxt, props, params);
|
|
242
|
+
if (ctxt instanceof IcfQualifiedContext) {
|
|
243
|
+
for (let i = params.length - 1; i >= start; i--) {
|
|
244
|
+
if (params[i].name === 'property' && params[i].part?.[0]?.valueCode === 'child') {
|
|
245
|
+
params.splice(i, 1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// -- hierarchy ------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Whether `general` subsumes `specific` or is it: a qualified code sits under its category,
|
|
255
|
+
* and so under everything the category does. Qualified codes are related only to themselves.
|
|
256
|
+
*/
|
|
257
|
+
#isA(specific, general) {
|
|
258
|
+
if (general instanceof IcfQualifiedContext) {
|
|
259
|
+
return specific instanceof IcfQualifiedContext && specific.qualifiedCode === general.qualifiedCode;
|
|
260
|
+
}
|
|
261
|
+
const code = specific.code; // the category, for a qualified code
|
|
262
|
+
if (code === general.code) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
return this.codeSystem.isDescendantOf(code, general.code);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async locateIsA(code, parent, disallowSelf = false) {
|
|
269
|
+
const codeResult = await this.locate(code);
|
|
270
|
+
if (!codeResult.context) {
|
|
271
|
+
return codeResult;
|
|
272
|
+
}
|
|
273
|
+
const parentResult = await this.locate(parent);
|
|
274
|
+
if (!(codeResult.context instanceof IcfQualifiedContext) && !(parentResult.context instanceof IcfQualifiedContext)) {
|
|
275
|
+
return super.locateIsA(code, parent, disallowSelf);
|
|
276
|
+
}
|
|
277
|
+
if (!parentResult.context) {
|
|
278
|
+
return { context: null, message: `Parent code '${parent}' not found in CodeSystem '${this.system()}'` };
|
|
279
|
+
}
|
|
280
|
+
if (code === parent) {
|
|
281
|
+
return disallowSelf ? { context: null, message: `Code '${code}' cannot be the same as its parent` } : codeResult;
|
|
282
|
+
}
|
|
283
|
+
if (this.#isA(codeResult.context, parentResult.context)) {
|
|
284
|
+
return codeResult;
|
|
285
|
+
}
|
|
286
|
+
return { context: null, message: `Code '${code}' is not a descendant of '${parent}'` };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async subsumesTest(codeA, codeB) {
|
|
290
|
+
const a = (await this.locate(codeA)).context;
|
|
291
|
+
if (!a) {
|
|
292
|
+
throw new Error(`Unknown Code "${codeA}"`);
|
|
293
|
+
}
|
|
294
|
+
const b = (await this.locate(codeB)).context;
|
|
295
|
+
if (!b) {
|
|
296
|
+
throw new Error(`Unknown Code "${codeB}"`);
|
|
297
|
+
}
|
|
298
|
+
if (!(a instanceof IcfQualifiedContext) && !(b instanceof IcfQualifiedContext)) {
|
|
299
|
+
return super.subsumesTest(codeA, codeB);
|
|
300
|
+
}
|
|
301
|
+
if ((await this.code(a)) === (await this.code(b))) {
|
|
302
|
+
return 'equivalent';
|
|
303
|
+
}
|
|
304
|
+
if (this.#isA(b, a)) {
|
|
305
|
+
return 'subsumes';
|
|
306
|
+
}
|
|
307
|
+
if (this.#isA(a, b)) {
|
|
308
|
+
return 'subsumed-by';
|
|
309
|
+
}
|
|
310
|
+
return 'not-subsumed';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** A qualified code has no children. */
|
|
314
|
+
async iterator(context) {
|
|
315
|
+
if (context) {
|
|
316
|
+
const ctxt = await this.#resolve(context);
|
|
317
|
+
if (ctxt instanceof IcfQualifiedContext) {
|
|
318
|
+
return { type: 'children', parentCode: ctxt.qualifiedCode, codes: [], current: 0, total: 0 };
|
|
319
|
+
}
|
|
320
|
+
return super.iterator(ctxt);
|
|
321
|
+
}
|
|
322
|
+
return super.iterator(context);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// -- filters --------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
#isQualifierFilter(prop, op) {
|
|
328
|
+
return (op === 'exists' && QUALIFIER_PROPERTIES.has(prop)) || (prop === 'qualified' && op === '=');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async doesFilter(prop, op, value) {
|
|
332
|
+
if (this.#isQualifierFilter(prop, op)) {
|
|
333
|
+
return value === 'true' || value === 'false';
|
|
334
|
+
}
|
|
335
|
+
return super.doesFilter(prop, op, value);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async filter(filterContext, forIteration, prop, op, value, fc) {
|
|
339
|
+
if (!this.#isQualifierFilter(prop, op)) {
|
|
340
|
+
return super.filter(filterContext, forIteration, prop, op, value, fc);
|
|
341
|
+
}
|
|
342
|
+
if (value !== 'true' && value !== 'false') {
|
|
343
|
+
throw new Issue('error', 'exception', null, 'FILTER_NOT_UNDERSTOOD',
|
|
344
|
+
this.opContext.i18n.translate('FILTER_NOT_UNDERSTOOD', this.opContext.langs, [prop, op, value]), 'vs-invalid', 422);
|
|
345
|
+
}
|
|
346
|
+
const want = value === 'true';
|
|
347
|
+
let test;
|
|
348
|
+
if (prop === 'qualified') {
|
|
349
|
+
test = (ctxt) => (ctxt instanceof IcfQualifiedContext) === want;
|
|
350
|
+
} else {
|
|
351
|
+
test = (ctxt) => {
|
|
352
|
+
const q = ctxt instanceof IcfQualifiedContext ? ctxt.qualifiers.find(q => q.property === prop) : undefined;
|
|
353
|
+
return (q !== undefined && q.digit !== '8') === want;
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const results = new IcfQualifierFilter(test, prop === 'qualified' && !want);
|
|
357
|
+
// the plain categories that pass: none for exists = true or qualified = true
|
|
358
|
+
for (const code of this.codeSystem.getAllCodes()) {
|
|
359
|
+
const concept = this.codeSystem.getConceptByCode(code);
|
|
360
|
+
if (concept && this.#isPrimary(concept) && test(new FhirCodeSystemProviderContext(code, concept))) {
|
|
361
|
+
results.add(concept);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (!filterContext.filters) {
|
|
365
|
+
filterContext.filters = [];
|
|
366
|
+
}
|
|
367
|
+
filterContext.filters.push(results);
|
|
368
|
+
return results;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** A primary ICF category (heading or not), as opposed to a qualifier value. */
|
|
372
|
+
#isPrimary(concept) {
|
|
373
|
+
const kind = propertyValue(concept, 'kind');
|
|
374
|
+
return kind === undefined || kind === 'primary';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Every include can admit qualified codes, unless it has a qualified = false filter. */
|
|
378
|
+
async filtersNotClosed(filterContext) {
|
|
379
|
+
return !(filterContext.filters || []).some(f => f instanceof IcfQualifierFilter && f.closesQualified);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async filterCheck(filterContext, set, concept) {
|
|
383
|
+
if (set instanceof IcfQualifierFilter && concept instanceof IcfQualifiedContext) {
|
|
384
|
+
return set.test(concept) ? true : `Code '${concept.qualifiedCode}' does not meet the qualifier rules`;
|
|
385
|
+
}
|
|
386
|
+
return super.filterCheck(filterContext, set, concept);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* A qualified code is in a filter's results when its category is: it is-a the category,
|
|
391
|
+
* and has the category's properties. (filterCheck needs nothing extra: it checks the
|
|
392
|
+
* context's concept, which is the category's.)
|
|
393
|
+
*/
|
|
394
|
+
async filterLocate(filterContext, set, code) {
|
|
395
|
+
if (set instanceof IcfQualifierFilter) {
|
|
396
|
+
const loc = await this.locate(code);
|
|
397
|
+
if (!loc.context) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
return (await this.filterCheck(filterContext, set, loc.context)) === true ? loc.context : null;
|
|
401
|
+
}
|
|
402
|
+
const found = await super.filterLocate(filterContext, set, code);
|
|
403
|
+
if (found) {
|
|
404
|
+
return found;
|
|
405
|
+
}
|
|
406
|
+
const loc = await this.locate(code);
|
|
407
|
+
if (!(loc.context instanceof IcfQualifiedContext)) {
|
|
408
|
+
return found;
|
|
409
|
+
}
|
|
410
|
+
const stem = await super.filterLocate(filterContext, set, loc.context.code);
|
|
411
|
+
return stem && typeof stem !== 'string' ? loc.context : null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
module.exports = {
|
|
416
|
+
IcfCodeSystemProvider,
|
|
417
|
+
IcfQualifiedContext
|
|
418
|
+
};
|
package/tx/cs/cs-loinc.js
CHANGED
|
@@ -149,6 +149,12 @@ class LoincServices extends BaseCSServices {
|
|
|
149
149
|
return 'LOINC';
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/** $lookup's display name: the code system's title. */
|
|
153
|
+
// eslint-disable-next-line no-unused-vars
|
|
154
|
+
lookupName(languages) {
|
|
155
|
+
return 'Logical Observation Identifier Names and Codes (LOINC)';
|
|
156
|
+
}
|
|
157
|
+
|
|
152
158
|
description() {
|
|
153
159
|
return 'LOINC';
|
|
154
160
|
}
|
|
@@ -607,12 +613,18 @@ class LoincServices extends BaseCSServices {
|
|
|
607
613
|
// Filter support
|
|
608
614
|
async doesFilter(prop, op, value) {
|
|
609
615
|
// Relationship filters
|
|
610
|
-
if (this.relationships.has(prop) && ['=', 'in', '
|
|
616
|
+
if (this.relationships.has(prop) && ['=', 'in', 'regex'].includes(op)) {
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
if (this.relationships.has(prop) && op === 'exists' && ['true', 'false'].includes(value)) {
|
|
611
620
|
return true;
|
|
612
621
|
}
|
|
613
622
|
|
|
614
623
|
// Property filters
|
|
615
|
-
if (this.propertyList.has(prop) && ['=', 'in', '
|
|
624
|
+
if (this.propertyList.has(prop) && ['=', 'in', 'regex'].includes(op)) {
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
if (this.propertyList.has(prop) && op === 'exists' && ['true', 'false'].includes(value)) {
|
|
616
628
|
return true;
|
|
617
629
|
}
|
|
618
630
|
|
|
@@ -748,26 +760,26 @@ class LoincServices extends BaseCSServices {
|
|
|
748
760
|
AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code IN (${codes}))
|
|
749
761
|
AND SourceKey = `;
|
|
750
762
|
}
|
|
751
|
-
// Relationship 'exists' filter
|
|
763
|
+
// Relationship 'exists' filter: value is true or false (doesFilter rejects anything else).
|
|
764
|
+
// true = codes that have at least one relationship of this type; false = every other code
|
|
765
|
+
// in the code system (Parts, AnswerLists and Answers included - combine with e.g.
|
|
766
|
+
// CLASSTYPE exists true to restrict to LOINC terms)
|
|
752
767
|
else if (this.relationships.has(prop) && op === 'exists') {
|
|
753
|
-
|
|
768
|
+
const rk = this.relationships.get(prop);
|
|
769
|
+
if (value === 'true') {
|
|
754
770
|
sql = `SELECT DISTINCT SourceKey as Key FROM Relationships
|
|
755
|
-
WHERE RelationshipTypeKey = ${
|
|
756
|
-
AND EXISTS (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
771
|
+
WHERE RelationshipTypeKey = ${rk}
|
|
757
772
|
ORDER BY SourceKey ASC`;
|
|
758
|
-
lsql = `SELECT COUNT(
|
|
759
|
-
WHERE RelationshipTypeKey = ${
|
|
760
|
-
AND EXISTS (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
|
|
761
|
-
AND SourceKey = `;
|
|
762
|
-
} else {
|
|
763
|
-
sql = `SELECT DISTINCT SourceKey as Key FROM Relationships
|
|
764
|
-
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
765
|
-
AND EXISTS (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
|
|
766
|
-
ORDER BY SourceKey ASC`;
|
|
767
|
-
lsql = `SELECT COUNT(DISTINCT SourceKey) FROM Relationships
|
|
768
|
-
WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
|
|
769
|
-
AND EXISTS (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
|
|
773
|
+
lsql = `SELECT COUNT(SourceKey) FROM Relationships
|
|
774
|
+
WHERE RelationshipTypeKey = ${rk}
|
|
770
775
|
AND SourceKey = `;
|
|
776
|
+
} else if (value === 'false') {
|
|
777
|
+
sql = `SELECT CodeKey as Key FROM Codes
|
|
778
|
+
WHERE NOT EXISTS (SELECT 1 FROM Relationships r WHERE r.RelationshipTypeKey = ${rk} AND r.SourceKey = Codes.CodeKey)
|
|
779
|
+
ORDER BY CodeKey ASC`;
|
|
780
|
+
lsql = `SELECT COUNT(CodeKey) FROM Codes
|
|
781
|
+
WHERE NOT EXISTS (SELECT 1 FROM Relationships r WHERE r.RelationshipTypeKey = ${rk} AND r.SourceKey = Codes.CodeKey)
|
|
782
|
+
AND CodeKey = `;
|
|
771
783
|
}
|
|
772
784
|
}
|
|
773
785
|
// Relationship regex filter
|
|
@@ -820,14 +832,24 @@ class LoincServices extends BaseCSServices {
|
|
|
820
832
|
AND PropertyValues.Value IN (${codes}) COLLATE NOCASE
|
|
821
833
|
AND CodeKey = `;
|
|
822
834
|
}
|
|
823
|
-
// Property 'exists' filter
|
|
835
|
+
// Property 'exists' filter: value is true or false, as for relationships above
|
|
824
836
|
else if (this.propertyList.has(prop) && op === 'exists') {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
837
|
+
const pk = this.propertyList.get(prop);
|
|
838
|
+
if (value === 'true') {
|
|
839
|
+
sql = `SELECT DISTINCT CodeKey as Key FROM Properties
|
|
840
|
+
WHERE Properties.PropertyTypeKey = ${pk}
|
|
841
|
+
ORDER BY CodeKey ASC`;
|
|
842
|
+
lsql = `SELECT COUNT(CodeKey) FROM Properties
|
|
843
|
+
WHERE Properties.PropertyTypeKey = ${pk}
|
|
844
|
+
AND CodeKey = `;
|
|
845
|
+
} else if (value === 'false') {
|
|
846
|
+
sql = `SELECT CodeKey as Key FROM Codes
|
|
847
|
+
WHERE NOT EXISTS (SELECT 1 FROM Properties p WHERE p.PropertyTypeKey = ${pk} AND p.CodeKey = Codes.CodeKey)
|
|
848
|
+
ORDER BY CodeKey ASC`;
|
|
849
|
+
lsql = `SELECT COUNT(CodeKey) FROM Codes
|
|
850
|
+
WHERE NOT EXISTS (SELECT 1 FROM Properties p WHERE p.PropertyTypeKey = ${pk} AND p.CodeKey = Codes.CodeKey)
|
|
851
|
+
AND CodeKey = `;
|
|
852
|
+
}
|
|
831
853
|
}
|
|
832
854
|
// Property regex filter
|
|
833
855
|
else if (this.propertyList.has(prop) && op === 'regex') {
|
package/tx/cs/cs-omop.js
CHANGED
|
@@ -151,7 +151,13 @@ class OMOPServices extends BaseCSServices {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
name() {
|
|
154
|
-
return
|
|
154
|
+
return 'OMOP_Concepts';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** $lookup's display name: the code system's title. */
|
|
158
|
+
// eslint-disable-next-line no-unused-vars
|
|
159
|
+
lookupName(languages) {
|
|
160
|
+
return 'OMOP Unified Vocabularies';
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
description() {
|
|
@@ -952,7 +958,8 @@ class OMOPServicesFactory extends CodeSystemFactoryProvider {
|
|
|
952
958
|
* @param version
|
|
953
959
|
* @returns {ConceptMap}
|
|
954
960
|
*/
|
|
955
|
-
|
|
961
|
+
// eslint-disable-next-line no-unused-vars
|
|
962
|
+
async findImplicitConceptMaps(conceptMaps, source, sourceVersion, dest) {
|
|
956
963
|
if (source == 'https://fhir-terminology.ohdsi.org') {
|
|
957
964
|
let key = this.#getVocabId(dest);
|
|
958
965
|
if (key) {
|
|
@@ -1007,7 +1014,7 @@ class OMOPServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1007
1014
|
|
|
1008
1015
|
|
|
1009
1016
|
name() {
|
|
1010
|
-
return
|
|
1017
|
+
return 'OMOP_Concepts';
|
|
1011
1018
|
}
|
|
1012
1019
|
|
|
1013
1020
|
id() {
|