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.
Files changed (121) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +80 -13
  3. package/config-template.json +106 -1
  4. package/ips/ai-client.js +263 -0
  5. package/ips/composer.js +805 -0
  6. package/ips/editor-schema.js +289 -0
  7. package/ips/editor.js +290 -0
  8. package/ips/fhirpath-ips.js +397 -0
  9. package/ips/ips-codeable-concepts.md +1742 -0
  10. package/ips/ips-template.html +125 -0
  11. package/ips/ips-valuesets.js +159 -0
  12. package/ips/ips.js +1736 -0
  13. package/ips/membership.js +179 -0
  14. package/ips/pipeline.js +1012 -0
  15. package/ips/presenter.js +974 -0
  16. package/ips/profile-editor.js +578 -0
  17. package/ips/profiles.js +506 -0
  18. package/ips/readme.md +460 -0
  19. package/ips/render.js +1482 -0
  20. package/ips/search-params.js +83 -0
  21. package/ips/search.js +667 -0
  22. package/ips/share.js +178 -0
  23. package/ips/store.js +650 -0
  24. package/ips/terminology.js +395 -0
  25. package/ips/tx-client.js +202 -0
  26. package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
  27. package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
  28. package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
  29. package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
  30. package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
  31. package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
  32. package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
  33. package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
  34. package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
  35. package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
  36. package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
  37. package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
  38. package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
  39. package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
  40. package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
  41. package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
  42. package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
  43. package/ips/valuesets/ValueSet-ips-medication.json +50 -0
  44. package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
  45. package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
  46. package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
  47. package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
  48. package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
  49. package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
  50. package/ips/valuesets/ValueSet-ips-route.json +46 -0
  51. package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
  52. package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
  53. package/ips/valuesets/elements.json +44 -0
  54. package/library/daily-report.js +422 -0
  55. package/library/fhir-search-values.js +138 -0
  56. package/library/html-server.js +32 -23
  57. package/library/mailer.js +77 -0
  58. package/library/narrative-check.js +21 -2
  59. package/library/npm-audit.js +25 -0
  60. package/library/package-manager.js +4 -2
  61. package/library/sqlite-session-store.js +29 -1
  62. package/package.json +7 -2
  63. package/packages/packages.js +34 -3
  64. package/publisher/publisher.js +29 -0
  65. package/registry/auth-store.js +366 -0
  66. package/registry/auth.js +872 -0
  67. package/registry/crawler.js +41 -1
  68. package/registry/documentation/api.md +162 -0
  69. package/registry/documentation/configuration.md +93 -0
  70. package/registry/documentation/documents.md +74 -0
  71. package/registry/documentation/internals.md +151 -0
  72. package/registry/documentation/readme.md +100 -0
  73. package/registry/documentation/security.md +199 -0
  74. package/registry/documents.js +309 -0
  75. package/registry/openapi.yaml +30 -0
  76. package/registry/registry-template.html +1 -1
  77. package/registry/registry.js +230 -44
  78. package/registry/uptime.js +154 -0
  79. package/server.js +105 -44
  80. package/shl/shl.js +64 -9
  81. package/stats-db.js +93 -0
  82. package/stats.js +158 -1
  83. package/testing/search.js +2 -108
  84. package/testing/store.js +5 -0
  85. package/testing/testing.js +20 -0
  86. package/token/token-template.html +5 -4
  87. package/token/token.js +17 -10
  88. package/translations/Messages.properties +9 -0
  89. package/tx/README.md +52 -0
  90. package/tx/cs/cs-api.js +90 -14
  91. package/tx/cs/cs-cs.js +56 -1
  92. package/tx/cs/cs-icd11.js +366 -112
  93. package/tx/cs/cs-icf.js +418 -0
  94. package/tx/cs/cs-loinc.js +47 -25
  95. package/tx/cs/cs-omop.js +10 -3
  96. package/tx/cs/cs-snomed.js +918 -55
  97. package/tx/cs/cs-ucum.js +6 -0
  98. package/tx/cs/icd11-expressions.js +191 -78
  99. package/tx/importers/export-icd11-codesystem.js +58 -12
  100. package/tx/importers/icd11-schema.md +9 -5
  101. package/tx/library.js +98 -4
  102. package/tx/openapi-operations.js +4 -0
  103. package/tx/params.js +8 -1
  104. package/tx/provider.js +16 -4
  105. package/tx/sct/documentation.md +251 -0
  106. package/tx/sct/ecl.js +758 -46
  107. package/tx/sct/expressions.js +113 -52
  108. package/tx/syndication/syndication-feed.js +218 -0
  109. package/tx/syndication/syndication-store.js +415 -0
  110. package/tx/tx-html.js +15 -1
  111. package/tx/tx.fhir.org.yml +2 -3
  112. package/tx/tx.js +144 -2
  113. package/tx/vs/vs-vsac.js +25 -6
  114. package/tx/workers/expand.js +33 -6
  115. package/tx/workers/lookup.js +61 -6
  116. package/tx/workers/subsumes.js +1 -1
  117. package/tx/workers/translate.js +166 -15
  118. package/tx/workers/validate.js +128 -6
  119. package/utilities/make-sct-test-extension.js +264 -0
  120. package/xig/xig.js +32 -0
  121. package/registry/readme.md +0 -124
@@ -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', 'exists', 'regex'].includes(op)) {
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', 'exists', 'regex'].includes(op)) {
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
- if (this.codes.has(value)) {
768
+ const rk = this.relationships.get(prop);
769
+ if (value === 'true') {
754
770
  sql = `SELECT DISTINCT SourceKey as Key FROM Relationships
755
- WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
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(DISTINCT SourceKey) FROM Relationships
759
- WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
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
- sql = `SELECT DISTINCT CodeKey as Key FROM Properties
826
- WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
827
- ORDER BY CodeKey ASC`;
828
- lsql = `SELECT COUNT(CodeKey) FROM Properties
829
- WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
830
- AND CodeKey = `;
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 `OMOP Concepts`;
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
- async findImplicitConceptMaps(conceptMaps, source, dest) {
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 `OMOP Concepts`;
1017
+ return 'OMOP_Concepts';
1011
1018
  }
1012
1019
 
1013
1020
  id() {