fhirsmith 0.18.2 → 1.0.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 (203) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +149 -0
  3. package/CONFIGURATION.md +305 -0
  4. package/LICENSE +29 -0
  5. package/README.md +29 -122
  6. package/cluster.js +1220 -0
  7. package/cluster.md +707 -0
  8. package/config-template.json +77 -8
  9. package/extension-tracker/extension-tracker-template.html +5 -8
  10. package/extension-tracker/extension-tracker.js +0 -2
  11. package/folder/folder.js +7 -6
  12. package/ips/analysis.js +560 -0
  13. package/ips/ips-template.html +5 -8
  14. package/ips/ips.js +132 -59
  15. package/ips/nationality.js +120 -0
  16. package/ips/pipeline.js +74 -21
  17. package/ips/presenter.js +1 -1
  18. package/ips/readme.md +132 -25
  19. package/ips/render.js +466 -77
  20. package/ips/store.js +79 -22
  21. package/ips/structure.js +247 -0
  22. package/ips/terminology.js +1 -1
  23. package/library/body-limit.js +22 -0
  24. package/library/cache-generation.js +15 -0
  25. package/library/cgroup-memory.js +133 -1
  26. package/library/cluster-counts.js +121 -0
  27. package/library/cluster-events.js +153 -0
  28. package/library/cluster-metrics.js +193 -0
  29. package/library/cluster-status.js +468 -0
  30. package/library/config-reader.js +96 -0
  31. package/library/daily-report.js +100 -2
  32. package/library/html-server.js +27 -1
  33. package/library/i18nsupport.js +23 -1
  34. package/library/keep-alive.js +18 -0
  35. package/library/languages.js +17 -3
  36. package/library/logger.js +31 -0
  37. package/library/memory-guard.js +428 -0
  38. package/library/native-memory.js +52 -0
  39. package/library/npm-audit.js +51 -25
  40. package/library/openapi-doc.js +7 -5
  41. package/library/package-manager.js +8 -0
  42. package/library/role.js +197 -0
  43. package/library/server-credentials.js +169 -0
  44. package/library/sqlite-utilities.js +59 -0
  45. package/library/ssrf-guard.js +100 -0
  46. package/library/static-response-stats.js +16 -0
  47. package/library/version-utilities.js +50 -0
  48. package/library/worker-count.js +192 -0
  49. package/nginx.md +30 -11
  50. package/npmprojector/indexer.js +18 -10
  51. package/package.json +11 -8
  52. package/packages/feed-check-page.js +117 -0
  53. package/packages/feed-check.js +279 -0
  54. package/packages/npm-check.js +57 -3
  55. package/packages/npm-signing.js +219 -0
  56. package/packages/openapi.yaml +207 -1
  57. package/packages/package-crawler.js +322 -41
  58. package/packages/packages-template.html +6 -8
  59. package/packages/packages.js +604 -57
  60. package/packages/readme.md +113 -1
  61. package/publisher/publisher-template.html +2 -5
  62. package/publisher/publisher.js +121 -110
  63. package/publisher/readme.md +75 -0
  64. package/registry/crawler.js +84 -108
  65. package/registry/documentation/configuration.md +34 -0
  66. package/registry/registry-template.html +5 -8
  67. package/registry/registry.js +5 -4
  68. package/root-bare-template.html +7 -4
  69. package/root-template.html +5 -8
  70. package/server.js +398 -198
  71. package/shl/shl.js +38 -37
  72. package/static/a11y/a11y-check.js +832 -0
  73. package/static/a11y/axe.min.js +12 -0
  74. package/static/assets/css/bootstrap-fhir.css +15 -15
  75. package/static/assets/images/flags/au.svg +18 -0
  76. package/static/assets/images/flags/unknown.svg +11 -0
  77. package/static/assets/images/flags/us.svg +63 -0
  78. package/static/assets/images/icon_audit.png +0 -0
  79. package/static/assets/images/icon_clinical.png +0 -0
  80. package/static/assets/images/icon_document.png +0 -0
  81. package/static/assets/images/icon_external.png +0 -0
  82. package/static/assets/images/icon_medication.png +0 -0
  83. package/static/assets/images/icon_organization.png +0 -0
  84. package/static/assets/images/icon_other.png +0 -0
  85. package/static/assets/images/icon_person.png +0 -0
  86. package/static/assets/images/icon_section.png +0 -0
  87. package/stats-db.js +45 -0
  88. package/stats.js +197 -10
  89. package/testing/testing-template.html +5 -8
  90. package/token/nginx_passport_setup.md +1 -1
  91. package/token/security_guide.md +1 -1
  92. package/token/token-template.html +2 -5
  93. package/token/token.js +40 -36
  94. package/translations/Messages.properties +19 -0
  95. package/tx/README.md +154 -4
  96. package/tx/closure/closure-store.js +126 -3
  97. package/tx/cm/cm-database.js +69 -57
  98. package/tx/cm/cm-package.js +29 -1
  99. package/tx/cs/cs-api.js +284 -19
  100. package/tx/cs/cs-api.md +41 -4
  101. package/tx/cs/cs-areacode.js +1 -1
  102. package/tx/cs/cs-country.js +17 -5
  103. package/tx/cs/cs-cpt.js +33 -16
  104. package/tx/cs/cs-cs.js +148 -14
  105. package/tx/cs/cs-currency.js +1 -1
  106. package/tx/cs/cs-icd11.js +21 -13
  107. package/tx/cs/cs-icf.js +22 -3
  108. package/tx/cs/cs-lang.js +2 -2
  109. package/tx/cs/cs-loinc.js +185 -66
  110. package/tx/cs/cs-mimetypes.js +12 -2
  111. package/tx/cs/cs-ndc.js +40 -25
  112. package/tx/cs/cs-omop.js +54 -29
  113. package/tx/cs/cs-rxnorm.js +300 -196
  114. package/tx/cs/cs-snomed.js +608 -64
  115. package/tx/cs/cs-ucum.js +14 -4
  116. package/tx/cs/cs-unii.js +18 -14
  117. package/tx/cs/cs-virtual.js +210 -0
  118. package/tx/ecosystem-resolver.js +413 -0
  119. package/tx/html/cluster-dash-metrics.liquid +26 -0
  120. package/tx/html/cluster-home-metrics.liquid +31 -0
  121. package/tx/html/cluster-metrics-script.liquid +251 -0
  122. package/tx/html/codesystem-operations.liquid +3 -3
  123. package/tx/html/ecl-panel.liquid +1 -1
  124. package/tx/html/external-metrics.liquid +17 -0
  125. package/tx/html/operations-form.liquid +17 -17
  126. package/tx/html/search-form.liquid +11 -11
  127. package/tx/html/status-line.liquid +58 -0
  128. package/tx/html/status-panel.liquid +140 -0
  129. package/tx/html/status-tasks.liquid +25 -0
  130. package/tx/html/tx-template.html +2 -5
  131. package/tx/importers/atc-to-fhir.js +17 -16
  132. package/tx/importers/export-icd11-codesystem.js +3 -2
  133. package/tx/importers/import-icd11.js +4 -3
  134. package/tx/importers/import-loinc.module.js +118 -56
  135. package/tx/importers/import-ndc.module.js +56 -44
  136. package/tx/importers/import-rxnorm.module.js +118 -79
  137. package/tx/importers/import-sct.module.js +57 -58
  138. package/tx/importers/import-unii.module.js +37 -36
  139. package/tx/importers/subset-loinc.module.js +49 -48
  140. package/tx/importers/subset-rxnorm.module.js +29 -28
  141. package/tx/importers/terminal.js +18 -0
  142. package/tx/importers/tx-import-base.js +13 -12
  143. package/tx/importers/tx-import-settings.js +5 -4
  144. package/tx/importers/tx-import.js +53 -52
  145. package/tx/library/canonical-resource.js +49 -0
  146. package/tx/library/codesystem.js +94 -5
  147. package/tx/library/designations.js +45 -2
  148. package/tx/library/extensions.js +13 -7
  149. package/tx/library/implicit-valuesets.js +66 -0
  150. package/tx/library/renderer.js +77 -21
  151. package/tx/library/sqlite-utilities.js +2 -0
  152. package/tx/library/ucum-parsers.js +11 -1
  153. package/tx/library/wait-for-cache.js +43 -0
  154. package/tx/library.js +321 -22
  155. package/tx/openapi-operations.js +6 -4
  156. package/tx/operation-context.js +460 -99
  157. package/tx/params.js +25 -0
  158. package/tx/problems.js +3 -5
  159. package/tx/provider.js +278 -42
  160. package/tx/resource-cache-sqlite.js +1050 -0
  161. package/tx/sct/ecl.js +47 -6
  162. package/tx/sct/editions.js +39 -0
  163. package/tx/sct/expressions.js +10 -1
  164. package/tx/sct/load-config.js +107 -0
  165. package/tx/sct/mmap.js +109 -0
  166. package/tx/sct/structures.js +166 -13
  167. package/tx/sct/text-index.js +5 -1
  168. package/tx/syndication/syndication-feed.js +2 -2
  169. package/tx/tx-html.js +44 -8
  170. package/tx/tx.fhir.org.yml +8 -8
  171. package/tx/tx.js +282 -80
  172. package/tx/usage-tracker.js +39 -11
  173. package/tx/vs/vs-api.js +19 -0
  174. package/tx/vs/vs-database.js +386 -62
  175. package/tx/vs/vs-package.js +27 -4
  176. package/tx/vs/vs-vsac.js +648 -100
  177. package/tx/workers/batch-validate.js +37 -10
  178. package/tx/workers/batch.js +6 -1
  179. package/tx/workers/cache-control.js +15 -4
  180. package/tx/workers/closure.js +156 -13
  181. package/tx/workers/compare.js +8 -5
  182. package/tx/workers/expand.js +1173 -289
  183. package/tx/workers/metadata.js +35 -0
  184. package/tx/workers/search.js +53 -1
  185. package/tx/workers/supplement-filters.js +145 -0
  186. package/tx/workers/translate.js +169 -4
  187. package/tx/workers/validate.js +223 -119
  188. package/tx/workers/worker.js +123 -31
  189. package/tx/xversion/xv-valueset.js +7 -4
  190. package/utilities/submit-test-report.js +3 -1
  191. package/utilities/tx-stress-test.js +482 -0
  192. package/vcl/readme.md +37 -0
  193. package/vcl/vcl-parser.js +15 -0
  194. package/xig/readme.md +52 -0
  195. package/xig/xig-template.html +5 -8
  196. package/xig/xig.js +238 -422
  197. package/changelog-template.md +0 -22
  198. package/registry/test-runner.js +0 -237
  199. package/release.sh +0 -80
  200. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  201. package/tsconfig.eslint.json +0 -9
  202. package/tx/html/dash-metrics.liquid +0 -147
  203. package/tx/html/home-metrics.liquid +0 -225
@@ -12,6 +12,7 @@
12
12
  //
13
13
 
14
14
  const { TerminologyWorker, Unknown_Code_in_VersionSCT, SCTVersion } = require('./worker');
15
+ const { splitFilters, supplementFilter } = require('./supplement-filters');
15
16
  const {Languages, Language} = require("../../library/languages");
16
17
  const {Extensions} = require("../library/extensions");
17
18
  const {validateParameter, isAbsoluteUrl, validateOptionalParameter, getValuePrimitive} = require("../../library/utilities");
@@ -128,6 +129,11 @@ class ValueSetChecker {
128
129
  } catch (error) {
129
130
  this.worker.log.error(error);
130
131
  debugLog(error);
132
+ if (error instanceof Issue && error.statusCode >= 400 && error.statusCode < 500) {
133
+ // the value set can't be expanded (a code system it imports is unknown, it's too big...):
134
+ // that's the answer, not a server failure
135
+ throw error.handleAsOO(error.statusCode);
136
+ }
131
137
  throw new Error('Exception expanding value set in order to infer system: ' + error.message);
132
138
  }
133
139
  return result;
@@ -154,64 +160,64 @@ class ValueSetChecker {
154
160
  }
155
161
 
156
162
  async determineSystem(opContext, code, systems, op) {
157
- let result = '';
158
- let needDoExpansion = false;
159
- let mayNeedExpansion = false;
160
-
161
- for (let vsi of this.valueSet.jsonObj.compose.exclude || []) {
162
- if (vsi.valueSet || !vsi.system || vsi.filter) {
163
- needDoExpansion = true;
164
- }
163
+ if (!this.valueSet.jsonObj.compose) {
164
+ return await this.determineSystemFromExpansion(code, systems);
165
165
  }
166
- for (let vsi of this.valueSet.jsonObj.compose.include || []) {
167
- if (vsi.valueSet || !vsi.system) {
168
- needDoExpansion = true;
169
- }
170
- if (vsi.filter) {
171
- mayNeedExpansion = true;
172
- }
166
+ // Check the code against each include, and each value set they import, rather than expanding
167
+ // the value set: that costs as much as the value set is big (all of UCUM, say), and an
168
+ // expansion can be too big to produce at all
169
+ const scratch = new OperationOutcome(); // the reasons the code isn't in an include don't matter
170
+ for (const s of await this.systemsForCode(code, op, scratch)) {
171
+ systems.add(s);
173
172
  }
173
+ return systems.size === 1 ? [...systems][0] : '';
174
+ }
174
175
 
175
- if (needDoExpansion) {
176
- result = await this.determineSystemFromExpansion(code, systems);
177
- } else {
178
- for (let vsi of this.valueSet.jsonObj.compose.include) {
179
- this.worker.deadCheck('determineSystem');
180
- let cs = await this.worker.findCodeSystem(vsi.system, '', null, ['complete', 'fragment'], op,true, false, false, this.worker.requiredSupplements);
181
- if (cs === null) {
182
- return '';
176
+ /**
177
+ * The systems of the includes that have the code (in this value set, and the ones it imports), less
178
+ * those of this value set's excludes that leave it out
179
+ * @returns {Promise<Set<string>>}
180
+ */
181
+ async systemsForCode(code, op, scratch) {
182
+ const systems = new Set();
183
+ const check = async (vsi, role) => {
184
+ this.worker.deadCheck('determineSystem');
185
+ const v = this.worker.determineVersionBase(vsi.system, vsi.version, this.params);
186
+ const cs = await this.worker.findCodeSystem(vsi.system, v, this.params, ['complete', 'fragment'], op, true, false, false, this.worker.requiredSupplements);
187
+ if (cs === null) {
188
+ // The system can only be inferred from all of the value set: if an include names a code
189
+ // system that isn't known, the code might be in it, so the request can't be answered (422)
190
+ const url = vsi.version ? vsi.system + '|' + vsi.version : vsi.system;
191
+ throw new Issue('error', 'not-found', null, 'UNABLE_TO_INFER_CODESYSTEM_UNKNOWN_SYSTEM',
192
+ this.worker.i18n.translate('UNABLE_TO_INFER_CODESYSTEM_UNKNOWN_SYSTEM', this.params.HTTPLanguages, [code, this.valueSet.vurl, url]), 'not-found')
193
+ .handleAsOO(422);
194
+ }
195
+ return await this.checkConceptSet('', role, cs, vsi, code, new Designations(this.worker.i18n.languageDefinitions), this.valueSet, '',
196
+ {value: false}, {value: ''}, {value: ''}, scratch, null, []);
197
+ };
198
+ for (const vsi of this.valueSet.jsonObj.compose.include || []) {
199
+ if (vsi.system) {
200
+ // (an include that also imports value sets narrows this one: the validation that follows checks that)
201
+ if (await check(vsi, 'in')) {
202
+ systems.add(vsi.system);
183
203
  }
184
- if (vsi.concept) {
185
- for (let cc of vsi.concept) {
186
- this.worker.deadCheck('determineSystem#2');
187
- let match = cc.code === code;
188
- if (match) {
189
- systems.add(vsi.system);
190
- if (!result) {
191
- result = vsi.system;
192
- } else if (result !== vsi.system) {
193
- return '';
194
- }
195
- }
196
- }
197
- } else {
198
- let loc = await cs.locate(code);
199
- if (loc.context !== null) {
200
- systems.add(vsi.system);
201
- if (!result) {
202
- result = vsi.system;
203
- } else if (result !== vsi.system) {
204
- if (mayNeedExpansion) {
205
- result = await this.determineSystemFromExpansion(code, systems);
206
- } else {
207
- return '';
208
- }
204
+ } else {
205
+ for (const u of vsi.valueSet || []) {
206
+ const other = this.others.get(this.worker.pinValueSet(u));
207
+ if (other) {
208
+ for (const s of await other.systemsForCode(code, op, scratch)) {
209
+ systems.add(s);
209
210
  }
210
211
  }
211
212
  }
212
213
  }
213
214
  }
214
- return result;
215
+ for (const vsi of this.valueSet.jsonObj.compose.exclude || []) {
216
+ if (vsi.system && !(vsi.valueSet && vsi.valueSet.length) && await check(vsi, 'not in')) {
217
+ systems.delete(vsi.system);
218
+ }
219
+ }
220
+ return systems;
215
221
  }
216
222
 
217
223
  async determineVersion(path, system, versionVS, versionCoding, op, unknownSystems, messages) {
@@ -365,7 +371,10 @@ class ValueSetChecker {
365
371
 
366
372
  async prepareConceptSet(desc, cc, vs) {
367
373
  this.worker.deadCheck('prepareConceptSet');
368
- Extensions.checkNoModifiers(cc, 'ValueSetChecker.prepare', desc, vs.vurl);
374
+ Extensions.checkNoModifiers(cc, 'ValueSetChecker.prepare', 'ValueSet.compose.' + desc, vs.vurl);
375
+ for (const [ci, c] of (cc.concept || []).entries()) {
376
+ Extensions.checkNoModifiers(c, 'ValueSetChecker.prepare', 'ValueSet.compose.' + desc + '.concept[' + ci + ']', vs.vurl);
377
+ }
369
378
  this.worker.opContext.addNote(this.valueSet, 'Prepare ' + desc + ': "' + this.worker.renderer.displayValueSetInclude(cc) + '"', this.indentCount);
370
379
  if (cc.valueSet) {
371
380
  for (let u of cc.valueSet) {
@@ -395,12 +404,12 @@ class ValueSetChecker {
395
404
  let i = 0;
396
405
  for (let ccf of cc.filter || []) {
397
406
  this.worker.deadCheck('prepareConceptSet#2');
398
- Extensions.checkNoModifiers(ccf, 'ValueSetChecker.prepare', desc + '.filter', this.valueSet.vurl);
407
+ Extensions.checkNoModifiers(ccf, 'ValueSetChecker.prepare', 'ValueSet.compose.' + desc + '.filter[' + i + ']', this.valueSet.vurl);
399
408
  if (!ccf.value) {
400
409
  throw new Issue('error', 'invalid', "ValueSet.compose."+desc+".filter["+i+"]", 'UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE',
401
410
  this.worker.i18n.translate('UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.params.HTTPLanguages, [cs.system(), ccf.property, ccf.op]), "vs-invalid").handleAsOO(400);
402
411
  }
403
- if (!(ccf.property === 'concept' && ['is-a', 'descendent-of'].includes(ccf.op))) {
412
+ if (!(ccf.property === 'concept' && ['is-a', 'descendent-of'].includes(ccf.op)) && !(await supplementFilter(cs, ccf))) {
404
413
  if (!(await cs.doesFilter(ccf.property, ccf.op, ccf.value))) {
405
414
  throw new Issue('error', 'not-supported', "ValueSet.compose."+desc+".filter["+i+"]", 'FILTER_NOT_UNDERSTOOD', this.worker.i18n.translate('FILTER_NOT_UNDERSTOOD',
406
415
  this.params.HTTPLanguages, [ccf.property, ccf.op, ccf.value, this.valueSet.url, cs.system()]), "vs-invalid").handleAsOO(400);
@@ -716,6 +725,10 @@ class ValueSetChecker {
716
725
  determinedVersion = await this.pickApplicableVersion(vset, system, code, display);
717
726
  }
718
727
  }
728
+ // why the code wasn't in each include (a message from the code system - an error from
729
+ // another server, say). Only worth reporting if the code turns out not to be in the value
730
+ // set at all: if another include has it, they're beside the point
731
+ const reasons = [];
719
732
  const includes = [...(this.valueSet.jsonObj.compose.include || [])];
720
733
  includes.sort((a, b) => {
721
734
  if (a.system === b.system && a.version && b.version) {
@@ -770,7 +783,7 @@ class ValueSetChecker {
770
783
  contentMode.value = cs.contentMode();
771
784
 
772
785
  let msg = '';
773
- if ((system === '%%null%%' || cs.system() === system) && await this.checkConceptSet(path, 'in', cs, cc, code, displays, this.valueSet, msg, inactive, normalForm, vstatus, op, vcc, messages)) {
786
+ if ((system === '%%null%%' || cs.system() === system) && await this.checkConceptSet(path, 'in', cs, cc, code, displays, this.valueSet, msg, inactive, normalForm, vstatus, op, vcc, reasons)) {
774
787
  ver.value = cs.version();
775
788
  result = true;
776
789
  } else {
@@ -835,6 +848,13 @@ class ValueSetChecker {
835
848
  }
836
849
  }
837
850
  }
851
+ if (result !== true) {
852
+ for (const r of reasons) {
853
+ if (!messages.includes(r)) {
854
+ messages.push(r);
855
+ }
856
+ }
857
+ }
838
858
  } else if (Extensions.checkNoModifiers(this.valueSet.jsonObj.expansion, 'ValueSetChecker.prepare', 'ValueSet.expansion', this.valueSet.vurl)) {
839
859
  let ccc = this.valueSet.findContains(system, version, code);
840
860
  if (ccc === null) {
@@ -989,13 +1009,11 @@ class ValueSetChecker {
989
1009
  let i = 0;
990
1010
  let impliedSystem = { value: '' };
991
1011
  for (let c of code.coding || []) {
992
- if (this.worker.opContext.usageTracker) {
993
- this.worker.opContext.usageTracker.seeConcept(c.system, c.code);
994
- }
995
1012
  let vsImpliedVersion = this.findVSVersionForSystem(c.system);
996
1013
  const csd = await this.worker.findCodeSystem(c.system, vsImpliedVersion, this.params, ['complete', 'fragment'], false, true, false, false, this.worker.requiredSupplements);
997
1014
  this.worker.seeSourceProvider(csd, c.system);
998
- this.worker.deadCheck('check-b#1');
1015
+ // a turn for the event loop now and then, so a client that has gone is seen
1016
+ await this.worker.checkAndYield('check-b#1');
999
1017
  let path;
1000
1018
  if (issuePath === 'CodeableConcept') {
1001
1019
  path = addToPath(issuePath, 'coding[' + i + ']');
@@ -1021,6 +1039,15 @@ class ValueSetChecker {
1021
1039
  noCode = true;
1022
1040
  break;
1023
1041
  }
1042
+ // versionNeeded = true means a version must be specified whenever the code system is
1043
+ // referenced. That's a check on the coding itself: a version pinned by the value set or by
1044
+ // the parameters is specific to that context, and doesn't travel with the coding, so it
1045
+ // makes no difference here
1046
+ if (csd && !c.version && csd.versionNeeded()) {
1047
+ let m = this.worker.i18n.translate('CODESYSTEM_VERSION_NEEDED', this.params.HTTPLanguages, [csd.system()]);
1048
+ msg(m);
1049
+ op.addIssue(new Issue('warning', 'business-rule', addToPath(path, 'version'), 'CODESYSTEM_VERSION_NEEDED', m, 'invalid-data'));
1050
+ }
1024
1051
  list.clear();
1025
1052
  let ver = { value: '' };
1026
1053
  let contentMode = { value: null };
@@ -1031,6 +1058,10 @@ class ValueSetChecker {
1031
1058
  }
1032
1059
  if (v === true) {
1033
1060
  matchedCodings.add(i);
1061
+ // only codes that are valid: anything else is never in an expansion to count
1062
+ if (this.worker.opContext.usageTracker) {
1063
+ this.worker.opContext.usageTracker.seeConcept(c.system, c.code);
1064
+ }
1034
1065
  }
1035
1066
  let ws;
1036
1067
  if (impliedSystem.value) {
@@ -1093,13 +1124,9 @@ class ValueSetChecker {
1093
1124
  if (ver.value) {
1094
1125
  pver = ver.value;
1095
1126
  }
1096
- let pd = list.preferredDisplay(this.params.workingLanguages());
1097
- if (pd) {
1098
- pdisp = pd;
1099
- }
1100
- if (!pdisp) {
1101
- pdisp = list.preferredDisplay(null);
1102
- }
1127
+ // this coding's display, and no other: one from a coding before it that isn't in the
1128
+ // value set doesn't go with this code
1129
+ pdisp = list.preferredDisplay(this.params.workingLanguages()) || list.preferredDisplay(null);
1103
1130
  }
1104
1131
  } else if (!this.params.membershipOnly && ws) {
1105
1132
  if (!isAbsoluteUrl(ws)) {
@@ -1218,14 +1245,19 @@ class ValueSetChecker {
1218
1245
  vstatus.value = st;
1219
1246
  }
1220
1247
  }
1221
- let pd = list.preferredDisplay(this.params.workingLanguages());
1222
- if (pd) {
1223
- pdisp = pd;
1224
- }
1225
- if (!pdisp) {
1226
- pdisp = list.preferredDisplay(null);
1248
+ // this coding isn't in the value set: its display is only reported if no coding is
1249
+ // (the display goes with the code that's reported - the first coding that's in the
1250
+ // value set - and a coding after that one mustn't replace it)
1251
+ if (pcode === undefined) {
1252
+ let pd = list.preferredDisplay(this.params.workingLanguages());
1253
+ if (pd) {
1254
+ pdisp = pd;
1255
+ }
1256
+ if (!pdisp) {
1257
+ pdisp = list.preferredDisplay(null);
1258
+ }
1227
1259
  }
1228
- let severity = this.dispWarning();
1260
+ let severity = list.extensionCodeDisplay === 'referenced' ? 'warning' : this.dispWarning();
1229
1261
  if (c.display && list.designations.length > 0 && !list.hasDisplay(this.params.workingLanguages(), defLang.value, c.display, false, DisplayCheckingStyle.CASE_INSENSITIVE).found) {
1230
1262
  let baseMsg;
1231
1263
  if (list.hasDisplay(this.params.workingLanguages(), defLang.value, c.display, false, DisplayCheckingStyle.CASE_INSENSITIVE).difference === DisplayDifference.Normalized) {
@@ -1471,8 +1503,11 @@ class ValueSetChecker {
1471
1503
  let hd = list.hasDisplay(this.params.workingLanguages(), null, c.display, false, DisplayCheckingStyle.CASE_INSENSITIVE)
1472
1504
  if (!hd.found) {
1473
1505
  let baseMsg;
1474
- let severity = this.dispWarning();
1475
- if (list.userDefined) {
1506
+ // a display an extension-codes supplement that is not global gives a user-defined code is not
1507
+ // the only correct one, so another display is a warning. One from a global supplement is as if
1508
+ // the code system defined it
1509
+ let severity = list.extensionCodeDisplay === 'referenced' ? 'warning' : this.dispWarning();
1510
+ if (list.userDefined && !list.extensionCodeDisplay) {
1476
1511
  baseMsg = 'Display_Name_Not_Fixed_use_Supplement';
1477
1512
  severity = 'information';
1478
1513
  } else if (hd.difference === DisplayDifference.Normalized) {
@@ -1602,7 +1637,7 @@ class ValueSetChecker {
1602
1637
  * code systems with a classification code have two spellings, so for the rest this is
1603
1638
  * one call and false.
1604
1639
  */
1605
- async sameConceptDifferentForm(cs, code, enumerated) {
1640
+ async sameConceptDifferentForm(cs, code, enumerated, prefetched = null) {
1606
1641
  if (code === enumerated) {
1607
1642
  return false;
1608
1643
  }
@@ -1610,10 +1645,36 @@ class ValueSetChecker {
1610
1645
  if (!a.context || !(await cs.classificationCode(a.context))) {
1611
1646
  return false;
1612
1647
  }
1613
- const b = await cs.locate(enumerated);
1648
+ const b = await cs.locate(enumerated, prefetched);
1614
1649
  return !!b.context && (await cs.code(a.context)) === (await cs.code(b.context));
1615
1650
  }
1616
1651
 
1652
+ /**
1653
+ * The listed concept that is the code written in its other form (ICD-11's short code and
1654
+ * entity id), or null. sameConceptDifferentForm asks two things of each listed concept: does
1655
+ * the code have another form at all (locate the code, ask for its classification code), and
1656
+ * is this listed concept that form. The first depends only on the code, so it is asked once
1657
+ * here, and the listed concepts are compared only when the answer is yes.
1658
+ */
1659
+ async otherFormMatch(cs, code, concepts) {
1660
+ if (!concepts || concepts.length === 0 || concepts.some(cc => cc.code === code)) {
1661
+ return null;
1662
+ }
1663
+ const a = await cs.locate(code);
1664
+ if (!a.context || !(await cs.classificationCode(a.context))) {
1665
+ return null;
1666
+ }
1667
+ // each listed code is located next: a provider may read them together, and hand back what it read
1668
+ const prefetched = await cs.prefetchCodes(concepts.map(cc => cc.code), false);
1669
+ for (const cc of concepts) {
1670
+ this.worker.deadCheck('checkConceptSet#3');
1671
+ if (await this.sameConceptDifferentForm(cs, code, cc.code, prefetched)) {
1672
+ return cc;
1673
+ }
1674
+ }
1675
+ return null;
1676
+ }
1677
+
1617
1678
  async noteClassificationCode(cs, context, code, normalForm) {
1618
1679
  // the code system decides whether this spelling of the code earns a classificationCode
1619
1680
  // (ICD-11: when it was written with entity ids, in any order - not when it was written
@@ -1694,15 +1755,7 @@ class ValueSetChecker {
1694
1755
  } else if (this.params.activeOnly && await cs.isInactive(loc.context)) {
1695
1756
  this.worker.opContext.addNote(this.valueSet, 'Code "' + code + '" found in ' + this.worker.renderer.displayCoded(cs) + ' but is inactive', this.indentCount);
1696
1757
  result = false;
1697
- if (role !== 'not in') {
1698
- inactive.value = true;
1699
- inactive.path = path;
1700
- inactive.code = code;
1701
- vstatus.value = await cs.getStatus(loc.context);
1702
- let msg = this.worker.i18n.translate('STATUS_CODE_WARNING_CODE', this.params.HTTPLanguages, ['not active', code]);
1703
- messages.push(msg);
1704
- op.addIssue(new Issue('error', 'business-rule', addToPath(path, 'code'), 'STATUS_CODE_WARNING_CODE', msg, 'code-rule'));
1705
- }
1758
+ await this.#excludedByActiveOnly(cs, loc.context, code, path, role, op, messages, inactive, vstatus);
1706
1759
  } else {
1707
1760
  result = true;
1708
1761
  if (role !== 'not in') {
@@ -1730,15 +1783,7 @@ class ValueSetChecker {
1730
1783
  }
1731
1784
 
1732
1785
  // an enumerated concept may be written in the other form (ICD-11 short code vs uri)
1733
- let altMatch = null;
1734
- if (cset.concept && !cset.concept.some(cc => cc.code === code)) {
1735
- for (const cc of cset.concept) {
1736
- if (await this.sameConceptDifferentForm(cs, code, cc.code)) {
1737
- altMatch = cc;
1738
- break;
1739
- }
1740
- }
1741
- }
1786
+ const altMatch = await this.otherFormMatch(cs, code, cset.concept);
1742
1787
  for (let cc of cset.concept || []) {
1743
1788
  this.worker.deadCheck('checkConceptSet#1');
1744
1789
  let c = cc.code;
@@ -1761,6 +1806,10 @@ class ValueSetChecker {
1761
1806
  inactive.code = code;
1762
1807
  vstatus.value = await cs.getStatus(loc);
1763
1808
  }
1809
+ } else if (this.params.activeOnly && await cs.isInactive(loc)) {
1810
+ this.worker.opContext.addNote(this.valueSet, 'Code "' + code + '" found in ' + this.worker.renderer.displayCoded(cs) + ' but is inactive', this.indentCount);
1811
+ result = false;
1812
+ await this.#excludedByActiveOnly(cs, loc, code, path, role, op, messages, inactive, vstatus);
1764
1813
  } else {
1765
1814
  if (vcc !== null) {
1766
1815
  if (!vcc.coding) {vcc.coding = [];}
@@ -1792,29 +1841,43 @@ class ValueSetChecker {
1792
1841
 
1793
1842
  if (cset.filter) {
1794
1843
  let cfl = cset.filter;
1795
- let prep = await cs.getPrepContext(false);
1796
1844
  for (let fc of cfl) {
1797
- this.worker.deadCheck('checkConceptSet#2');
1798
1845
  if (!fc.value) {
1799
1846
  throw new Issue('error', 'invalid', null, 'UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.worker.i18n.translate('UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.params.HTTPLanguages, [cs.system(), fc.property, fc.op]));
1800
1847
  }
1801
- await cs.filter(prep, false, fc.property, fc.op, fc.value, (vs && vs.isCached) ? fc : null);
1802
- // if (f === null) {
1803
- // throw new Issue('error', 'not-supported', null, 'FILTER_NOT_UNDERSTOOD', this.worker.i18n.translate('FILTER_NOT_UNDERSTOOD', this.params.HTTPLanguages, [fc.property, fc.op, fc.value, vs.vurl, cs.system()]) + ' (2)', 'vs-invalid');
1804
- // }
1805
- }
1806
- let filters = await cs.executeFilters(prep);
1807
- if (!filters || filters.length == 0) {
1808
- throw new Error("executing filters failed for " + cs.name());
1809
- }
1810
- let loc = await cs.filterLocate(prep, filters[0], code);
1811
- if (loc != null && !(typeof loc === 'string') && filters.length > 1) {
1812
- for (let i = 1; i < filters.length; i++) {
1813
- if ((await cs.filterCheck(prep, filters[i], loc)) != true) { // a failing check may return a message, which is truthy
1814
- loc = null;
1815
- break;
1848
+ }
1849
+ // filters on properties that supplements in use define are evaluated from the supplements;
1850
+ // the code system does the rest
1851
+ const { base: baseFilters, supplement: suppFilters } = await splitFilters(cs, cfl);
1852
+ let loc;
1853
+ if (baseFilters.length > 0) {
1854
+ let prep = await cs.getPrepContext(false, this.params.requestedLanguages());
1855
+ for (let fc of baseFilters) {
1856
+ this.worker.deadCheck('checkConceptSet#2');
1857
+ await cs.filter(prep, false, fc.property, fc.op, fc.value, (vs && vs.isCached) ? fc : null);
1858
+ }
1859
+ let filters = await cs.executeFilters(prep);
1860
+ if (!filters || filters.length == 0) {
1861
+ throw new Error("executing filters failed for " + cs.name());
1862
+ }
1863
+ loc = await cs.filterLocate(prep, filters[0], code);
1864
+ if (loc != null && !(typeof loc === 'string') && filters.length > 1) {
1865
+ for (let i = 1; i < filters.length; i++) {
1866
+ if ((await cs.filterCheck(prep, filters[i], loc)) != true) { // a failing check may return a message, which is truthy
1867
+ loc = null;
1868
+ break;
1869
+ }
1816
1870
  }
1817
1871
  }
1872
+ } else {
1873
+ const located = await cs.locate(code);
1874
+ loc = located ? located.context : null;
1875
+ }
1876
+ if (loc != null && !(typeof loc === 'string') && suppFilters.length > 0) {
1877
+ const lcode = await cs.code(loc);
1878
+ if (!suppFilters.every(sf => sf.matches(lcode))) {
1879
+ loc = null;
1880
+ }
1818
1881
  }
1819
1882
  if (loc != null && !(typeof loc === 'string')) {
1820
1883
  await this.worker.listDisplaysFromCodeSystem(displays, cs, loc);
@@ -1832,6 +1895,10 @@ class ValueSetChecker {
1832
1895
  inactive.code = code;
1833
1896
  vstatus.value = await cs.getStatus(loc);
1834
1897
  }
1898
+ } else if (this.params.activeOnly && await cs.isInactive(loc)) {
1899
+ result = false;
1900
+ this.worker.opContext.addNote(this.valueSet, 'Filter ' + this.filterSummary(cset) + ': Code "' + code + '" found in ' + this.worker.renderer.displayCoded(cs) + ' but is inactive', this.indentCount);
1901
+ await this.#excludedByActiveOnly(cs, loc, code, path, role, op, messages, inactive, vstatus);
1835
1902
  } else {
1836
1903
  this.worker.opContext.addNote(this.valueSet, 'Filter ' + this.filterSummary(cset) + ': Code "' + code + '" found in ' + this.worker.renderer.displayCoded(cs), this.indentCount);
1837
1904
  if (vcc !== null) {
@@ -1850,6 +1917,13 @@ class ValueSetChecker {
1850
1917
  if (st) {
1851
1918
  vstatus.value = st;
1852
1919
  }
1920
+ } else if (!vstatus.value) {
1921
+ // an active code can still have a status worth reporting (deprecated), as it is
1922
+ // when the value set lists the code
1923
+ const st = await cs.getStatus(loc);
1924
+ if (st) {
1925
+ vstatus.value = st;
1926
+ }
1853
1927
  }
1854
1928
  }
1855
1929
  if (role !== 'not in') {
@@ -1860,7 +1934,10 @@ class ValueSetChecker {
1860
1934
  }
1861
1935
  } else if (loc != null) {
1862
1936
  this.worker.opContext.addNote(this.valueSet, 'Filter ' + this.filterSummary(cset) + ': Code "' + code + '" not found in ' + this.worker.renderer.displayCoded(cs)+ ": "+loc, this.indentCount);
1863
- messages.push(loc);
1937
+ // why it's not in an include (see check()); an exclude that doesn't have the code is no news
1938
+ if (messages && role !== 'not in') {
1939
+ messages.push(loc);
1940
+ }
1864
1941
  } else {
1865
1942
  this.worker.opContext.addNote(this.valueSet, 'Filter ' + this.filterSummary(cset) + ': Code "' + code + '" not found in ' + this.worker.renderer.displayCoded(cs), this.indentCount);
1866
1943
  }
@@ -1896,6 +1973,27 @@ class ValueSetChecker {
1896
1973
  }
1897
1974
 
1898
1975
 
1976
+ /**
1977
+ * activeOnly: the code is checked against the value set as expanded with activeOnly, which has no
1978
+ * inactive codes - whether the value set includes all of the code system, lists the code or
1979
+ * selects it with a filter - so an inactive code is not in it
1980
+ */
1981
+ async #excludedByActiveOnly(cs, loc, code, path, role, op, messages, inactive, vstatus) {
1982
+ if (role === 'not in') {
1983
+ return;
1984
+ }
1985
+ inactive.value = true;
1986
+ inactive.path = path;
1987
+ inactive.code = code;
1988
+ const st = await cs.getStatus(loc);
1989
+ if (st) {
1990
+ vstatus.value = st;
1991
+ }
1992
+ const msg = this.worker.i18n.translate('STATUS_CODE_WARNING_CODE', this.params.HTTPLanguages, ['not active', code]);
1993
+ messages.push(msg);
1994
+ op.addIssue(new Issue('error', 'business-rule', addToPath(path, 'code'), 'STATUS_CODE_WARNING_CODE', msg, 'code-rule'));
1995
+ }
1996
+
1899
1997
  excludeInactives() {
1900
1998
  return this.valueSet.jsonObj.compose && this.valueSet.jsonObj.compose.inactive != undefined && !this.valueSet.jsonObj.compose.inactive;
1901
1999
  }
@@ -2045,13 +2143,12 @@ class ValidateWorker extends TerminologyWorker {
2045
2143
  this.log.error(error);
2046
2144
  debugLog(error);
2047
2145
  if (error instanceof Issue) {
2048
- if (error.isHandleAsOO()) {
2049
- let oo = new OperationOutcome();
2050
- oo.addIssue(error);
2051
- return res.status(error.statusCode || 500).json(oo.jsonObj);
2052
- } else {
2053
- // this is actually handled in the inner method
2054
- }
2146
+ // Issues raised inside the validation are turned into a result by the inner method;
2147
+ // one that reaches here (e.g. cache-id-unknown from setupAdditionalResources, before
2148
+ // the inner try) must still be answered, or the request never completes.
2149
+ let oo = new OperationOutcome();
2150
+ oo.addIssue(error);
2151
+ return res.status(error.statusCode || 500).json(oo.jsonObj);
2055
2152
  } else {
2056
2153
  return res.status(error.statusCode || 500).json(outcomeFromError(error));
2057
2154
  }
@@ -2326,6 +2423,8 @@ class ValidateWorker extends TerminologyWorker {
2326
2423
 
2327
2424
  let csp = await this.provider.getCodeSystemProvider(this.opContext, url, version, supplements);
2328
2425
  if (csp) {
2426
+ // released at the end of the operation, like the providers findCodeSystem() returns
2427
+ this.opContext.registerProvider(csp);
2329
2428
  return csp;
2330
2429
  } else {
2331
2430
  let vs = await this.findValueSet(url, version, null);
@@ -2371,7 +2470,12 @@ class ValidateWorker extends TerminologyWorker {
2371
2470
  return fromAdditional;
2372
2471
  }
2373
2472
 
2374
- let vs = await this.provider.findValueSet(this.opContext, url, version);
2473
+ // an implicit value set in the generic form is drawn from the code system version the
2474
+ // request asks for (system-version and the like)
2475
+ const systemVersionFor = txParams && txParams.requestedSystemVersion
2476
+ ? (system) => txParams.requestedSystemVersion(system)
2477
+ : null;
2478
+ let vs = await this.provider.findValueSet(this.opContext, url, version, systemVersionFor);
2375
2479
  this.seeSourceVS(vs, url);
2376
2480
  if (vs == null) {
2377
2481
  throw new Issue('error', 'not-found', null, 'Unable_to_resolve_value_Set_', this.i18n.translate('Unable_to_resolve_value_Set_', params.HTTPLanguages, [url+(version ? "|"+version : "")]), 'not-found', 422);