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
package/tx/cs/cs-cpt.js CHANGED
@@ -4,6 +4,7 @@ const { CodeSystem } = require('../library/codesystem');
4
4
  const { FilterExecutionContext, CodeSystemFactoryProvider } = require('./cs-api');
5
5
  const {validateArrayParameter} = require("../../library/utilities");
6
6
  const {BaseCSServices} = require("./cs-base");
7
+ const { guarded } = require('../library/sqlite-utilities');
7
8
 
8
9
  class CPTConceptDesignation {
9
10
  constructor(kind, value) {
@@ -123,10 +124,7 @@ class CPTServices extends BaseCSServices {
123
124
  }
124
125
 
125
126
  close() {
126
- if (this.db) {
127
- this.db.close();
128
- this.db = null;
129
- }
127
+ this.closeDb();
130
128
  }
131
129
 
132
130
  // Metadata methods
@@ -228,6 +226,15 @@ class CPTServices extends BaseCSServices {
228
226
  isNotClosed() {
229
227
  return true;
230
228
  }
229
+ /**
230
+ * The concept's properties for $expand: built by extendLookup, so that an expansion reports
231
+ * the same properties $lookup does (see CodeSystemProvider._propertiesFromLookup). Not the
232
+ * copyright, which extendLookup always adds: it is about the code system, not the concept
233
+ */
234
+ async properties(context, names) {
235
+ return (await this._propertiesFromLookup(context, names)).filter(p => p.code !== 'copyright');
236
+ }
237
+
231
238
  async extendLookup(ctxt, props, params) {
232
239
  validateArrayParameter(props, 'props', String);
233
240
  validateArrayParameter(params, 'params', Object);
@@ -571,7 +578,8 @@ class CPTServices extends BaseCSServices {
571
578
 
572
579
 
573
580
 
574
- // Subsumption testing - not implemented
581
+ // Subsumption: CPT codes have no subsumption relationships, so two codes that both exist are
582
+ // not-subsumed (the caller deals with equal codes: equivalent)
575
583
  async subsumesTest(codeA, codeB) {
576
584
  await this.#ensureContext(codeA);
577
585
  await this.#ensureContext(codeB);
@@ -585,9 +593,14 @@ class CPTServices extends BaseCSServices {
585
593
  }
586
594
 
587
595
  class CPTServicesFactory extends CodeSystemFactoryProvider {
588
- constructor(i18n, dbPath) {
596
+ /**
597
+ * @param {Object} [options] - {readOnly: true} in a tx-worker (cluster.md): the file is
598
+ * the master's; this process opens it read-only
599
+ */
600
+ constructor(i18n, dbPath, options = {}) {
589
601
  super(i18n);
590
602
  this.dbPath = dbPath;
603
+ this.readOnly = !!options.readOnly;
591
604
  this.uses = 0;
592
605
  this._loaded = false;
593
606
  this._sharedData = null;
@@ -613,8 +626,12 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
613
626
  }
614
627
  }
615
628
 
629
+ #open() {
630
+ return this.readOnly ? new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY) : new sqlite3.Database(this.dbPath);
631
+ }
632
+
616
633
  async load() {
617
- const db = new sqlite3.Database(this.dbPath);
634
+ const db = this.#open();
618
635
 
619
636
  try {
620
637
  this._sharedData = {
@@ -645,7 +662,7 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
645
662
 
646
663
  async #loadVersion(db) {
647
664
  return new Promise((resolve, reject) => {
648
- db.all('SELECT * FROM Information', (err, rows) => {
665
+ db.all('SELECT * FROM Information', guarded(reject, (err, rows) => {
649
666
  if (err) {
650
667
  reject(err);
651
668
  } else {
@@ -656,13 +673,13 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
656
673
  }
657
674
  resolve();
658
675
  }
659
- });
676
+ }));
660
677
  });
661
678
  }
662
679
 
663
680
  async #loadConcepts(db) {
664
681
  return new Promise((resolve, reject) => {
665
- db.all('SELECT * FROM Concepts', (err, rows) => {
682
+ db.all('SELECT * FROM Concepts', guarded(reject, (err, rows) => {
666
683
  if (err) {
667
684
  reject(err);
668
685
  } else {
@@ -680,13 +697,13 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
680
697
  }
681
698
  resolve();
682
699
  }
683
- });
700
+ }));
684
701
  });
685
702
  }
686
703
 
687
704
  async #loadProperties(db) {
688
705
  return new Promise((resolve, reject) => {
689
- db.all('SELECT * FROM Properties', (err, rows) => {
706
+ db.all('SELECT * FROM Properties', guarded(reject, (err, rows) => {
690
707
  if (err) {
691
708
  reject(err);
692
709
  } else {
@@ -698,13 +715,13 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
698
715
  }
699
716
  resolve();
700
717
  }
701
- });
718
+ }));
702
719
  });
703
720
  }
704
721
 
705
722
  async #loadDesignations(db) {
706
723
  return new Promise((resolve, reject) => {
707
- db.all('SELECT * FROM Designations', (err, rows) => {
724
+ db.all('SELECT * FROM Designations', guarded(reject, (err, rows) => {
708
725
  if (err) {
709
726
  reject(err);
710
727
  } else {
@@ -716,7 +733,7 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
716
733
  }
717
734
  resolve();
718
735
  }
719
- });
736
+ }));
720
737
  });
721
738
  }
722
739
 
@@ -729,7 +746,7 @@ class CPTServicesFactory extends CodeSystemFactoryProvider {
729
746
  this.recordUse();
730
747
 
731
748
  // Create fresh database connection for this provider instance
732
- const db = new sqlite3.Database(this.dbPath);
749
+ const db = this.#open();
733
750
 
734
751
  return new CPTServices(opContext, supplements, db, this._sharedData);
735
752
  }
package/tx/cs/cs-cs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const { CodeSystem} = require("../library/codesystem");
2
- const { CodeSystemFactoryProvider, FilterExecutionContext } = require( "./cs-api");
2
+ const { CodeSystemFactoryProvider, FilterExecutionContext, CodeSystemProvider } = require( "./cs-api");
3
3
  const { VersionUtilities } = require("../../library/version-utilities");
4
4
  const { Language } = require ("../../library/languages");
5
5
  const { validateOptionalParameter, getValuePrimitive, validateArrayParameter} = require("../../library/utilities");
@@ -56,7 +56,8 @@ function conceptSearchTexts(concept) {
56
56
  display: concept.display ? searchText(concept.display) : null,
57
57
  definition: concept.definition ? foldSearchText(concept.definition) : null,
58
58
  designations: Array.isArray(concept.designation)
59
- ? concept.designation.filter(designation => designation.value).map(designation => searchText(designation.value))
59
+ ? concept.designation.filter(designation => designation.value)
60
+ .map(designation => ({ ...searchText(designation.value), language: designation.language || null }))
60
61
  : []
61
62
  };
62
63
  SEARCH_TEXTS.set(concept, texts);
@@ -83,9 +84,16 @@ function wordsRatio(words, text) {
83
84
  * Context class for FHIR CodeSystem provider concepts
84
85
  */
85
86
  class FhirCodeSystemProviderContext {
86
- constructor(code, concept) {
87
+ /**
88
+ * @param {string} code - the code
89
+ * @param {Object} concept - its definition in the code system
90
+ * @param {boolean} reserved - true if the code system doesn't define the code, but it is in its
91
+ * reserved code space (so the concept is just the code, and supplements may define it)
92
+ */
93
+ constructor(code, concept, reserved = false) {
87
94
  this.code = code;
88
95
  this.concept = concept;
96
+ this.reserved = reserved;
89
97
  }
90
98
  }
91
99
 
@@ -190,6 +198,16 @@ function providerClassFor(url) {
190
198
  }
191
199
 
192
200
  class FhirCodeSystemProvider extends BaseCSServices {
201
+ /**
202
+ * A code system that reserves code spaces for users (codesystem-reservedCodeSpace) has codes that
203
+ * aren't listed in an expansion of all of it
204
+ * @returns {string|null}
205
+ */
206
+ openCodeSpaceReason() {
207
+ return this.codeSystem.reservedCodeSpaces().length > 0 ?
208
+ "The code system '" + this.system() + "' reserves code spaces for users to define their own codes, and those codes are not in the expansion" : null;
209
+ }
210
+
193
211
  /**
194
212
  * The provider for a CodeSystem resource: a FhirCodeSystemProvider, or the subclass
195
213
  * registered for its url. Always use this rather than the constructor.
@@ -464,6 +482,16 @@ class FhirCodeSystemProvider extends BaseCSServices {
464
482
  };
465
483
  }
466
484
 
485
+ // a code the code system doesn't define, in a code space it reserves for users to define their own
486
+ // codes, is a valid code. An extension-codes supplement in scope may define it (display etc); if
487
+ // none does, it has no display
488
+ if (this.codeSystem.isInReservedCodeSpace(code)) {
489
+ return {
490
+ context: new FhirCodeSystemProviderContext(code, { code: code }, true),
491
+ message: null
492
+ };
493
+ }
494
+
467
495
  return {
468
496
  context: null,
469
497
  message: undefined
@@ -511,6 +539,11 @@ class FhirCodeSystemProvider extends BaseCSServices {
511
539
  if (supplementDisplay) {
512
540
  return supplementDisplay;
513
541
  }
542
+ // a code in the reserved code space has the display an extension-codes supplement gives it,
543
+ // whatever languages were asked for - or, if none does, the code space's default display
544
+ if (ctxt.reserved) {
545
+ return this._extensionCodeDisplay(ctxt.code) || this.codeSystem.reservedCodeSpaceFor(ctxt.code).defaultDisplay;
546
+ }
514
547
 
515
548
  // Use language-aware display logic
516
549
  if (this.opContext.langs && !this.opContext.langs.isEnglishOrNothing()) {
@@ -774,8 +807,17 @@ class FhirCodeSystemProvider extends BaseCSServices {
774
807
  }
775
808
  }
776
809
 
810
+ // a code in the reserved code space that no extension-codes supplement in scope gives a display has
811
+ // its code space's default display ('User-assigned'). It is a placeholder, so (as for ISO 3166's
812
+ // user-assigned codes) another display is not wrong - see validate.js checkDisplays
813
+ if (ctxt.reserved && !this._extensionCodeDisplay(ctxt.code)) {
814
+ displays.userDefined = true;
815
+ const displayLang = this.defaultLanguage ? this.defaultLanguage.toString() : null;
816
+ displays.addDesignation(true, 'active', displayLang, CodeSystem.makeUseForDisplay(), this.codeSystem.reservedCodeSpaceFor(ctxt.code).defaultDisplay);
817
+ }
818
+
777
819
  // Add supplement designations
778
- this._listSupplementDesignations(ctxt.code, displays);
820
+ this._listSupplementDesignations(ctxt.code, displays, ctxt.reserved);
779
821
  }
780
822
 
781
823
  /**
@@ -1132,9 +1174,10 @@ class FhirCodeSystemProvider extends BaseCSServices {
1132
1174
  * @param {boolean} iterate - True if results will be iterated
1133
1175
  * @returns {FilterExecutionContext} Filter context
1134
1176
  */
1135
- async getPrepContext(iterate) {
1136
-
1137
- return new FilterExecutionContext(iterate);
1177
+ async getPrepContext(iterate, languages = null) {
1178
+ const result = new FilterExecutionContext(iterate);
1179
+ result.languages = languages; // what the request asked for (a text filter searches one language)
1180
+ return result;
1138
1181
  }
1139
1182
 
1140
1183
  /**
@@ -1161,7 +1204,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1161
1204
  }
1162
1205
  // Supported hierarchy filters
1163
1206
  if ((prop === 'concept' || prop === 'code') &&
1164
- ['is-a', 'descendent-of', 'is-not-a', 'child-of', 'in', '=', 'regex'].includes(op)) {
1207
+ ['is-a', 'descendent-of', 'descendent-leaf', 'is-not-a', 'child-of', 'in', 'not-in', '=', 'regex'].includes(op)) {
1165
1208
  return true;
1166
1209
  }
1167
1210
 
@@ -1304,13 +1347,16 @@ class FhirCodeSystemProvider extends BaseCSServices {
1304
1347
  const results = new FhirCodeSystemProviderFilterContext();
1305
1348
  const searchTerm = foldSearchText(filter.filter);
1306
1349
  const searchWords = searchTerm.split(/[^\p{L}\p{N}]+/u).filter(word => word !== '');
1350
+ // the display, definition and designations in one language only (see searchLanguage),
1351
+ // with the designations that supplements in use give in that language
1352
+ const lang = this.searchLanguage(filterContext.languages);
1307
1353
 
1308
1354
  // Search through all concepts
1309
1355
  const allConcepts = this.codeSystem.getAllConcepts();
1310
1356
 
1311
1357
  for (const concept of allConcepts) {
1312
1358
  this.opContext.deadCheck('cs:searchFilter');
1313
- const rating = this._calculateSearchRating(concept, searchTerm, searchWords);
1359
+ const rating = this._calculateSearchRating(concept, searchTerm, searchWords, lang);
1314
1360
  if (rating > 0) {
1315
1361
  results.add(concept, rating);
1316
1362
  }
@@ -1337,8 +1383,20 @@ class FhirCodeSystemProvider extends BaseCSServices {
1337
1383
  * @returns {number} Rating (0 = no match, higher = better match)
1338
1384
  * @private
1339
1385
  */
1340
- _calculateSearchRating(concept, searchTerm, searchWords) {
1341
- const texts = conceptSearchTexts(concept);
1386
+ _calculateSearchRating(concept, searchTerm, searchWords, lang = null) {
1387
+ const all = conceptSearchTexts(concept);
1388
+ let texts = all;
1389
+ if (lang) {
1390
+ const csLang = this.defLang();
1391
+ const inLang = CodeSystemProvider.inSearchLanguage(csLang, lang);
1392
+ texts = {
1393
+ code: all.code,
1394
+ display: inLang ? all.display : null,
1395
+ definition: inLang ? all.definition : null,
1396
+ designations: all.designations.filter(d => CodeSystemProvider.inSearchLanguage(d.language, lang, csLang))
1397
+ .concat(this.#supplementSearchTexts(concept.code, lang))
1398
+ };
1399
+ }
1342
1400
  const display = texts.display;
1343
1401
  let rating = 0;
1344
1402
 
@@ -1389,6 +1447,55 @@ class FhirCodeSystemProvider extends BaseCSServices {
1389
1447
  return rating;
1390
1448
  }
1391
1449
 
1450
+ /**
1451
+ * As for any code system (CodeSystemProvider.searchLanguage), but whether there is text in
1452
+ * a language is decided by what the code system actually has: its own language, a designation
1453
+ * in it, or a supplement's (hasAnyDisplays says yes to English whatever the code system's
1454
+ * language is)
1455
+ */
1456
+ searchLanguage(languages) {
1457
+ for (const lang of (languages && languages.languages) || []) {
1458
+ if (lang && lang.language && lang.code !== '*' && this.#hasSearchText(lang)) {
1459
+ return lang;
1460
+ }
1461
+ }
1462
+ return new Language(this.defLang());
1463
+ }
1464
+
1465
+ #hasSearchText(lang) {
1466
+ const csLang = this.defLang();
1467
+ if (CodeSystemProvider.inSearchLanguage(csLang, lang)) {
1468
+ return true;
1469
+ }
1470
+ for (const concept of this.codeSystem.getAllConcepts()) {
1471
+ if ((concept.designation || []).some(d => d.value && CodeSystemProvider.inSearchLanguage(d.language, lang, csLang))) {
1472
+ return true;
1473
+ }
1474
+ }
1475
+ return this._hasSupplementText(lang);
1476
+ }
1477
+
1478
+ // the displays and designations that supplements in use give a code, in the search language
1479
+ #supplementSearchTexts(code, lang) {
1480
+ const result = [];
1481
+ for (const supplement of this.supplements || []) {
1482
+ const concept = supplement.getConceptByCode(code);
1483
+ if (!concept) {
1484
+ continue;
1485
+ }
1486
+ const suppLang = supplement.jsonObj.language || this.defLang();
1487
+ if (concept.display && CodeSystemProvider.inSearchLanguage(suppLang, lang)) {
1488
+ result.push(searchText(concept.display));
1489
+ }
1490
+ for (const d of concept.designation || []) {
1491
+ if (d.value && CodeSystemProvider.inSearchLanguage(d.language, lang, suppLang)) {
1492
+ result.push(searchText(d.value));
1493
+ }
1494
+ }
1495
+ }
1496
+ return result;
1497
+ }
1498
+
1392
1499
  /**
1393
1500
  * Execute a value set filter
1394
1501
  * @param {FilterExecutionContext} filterContext - Filter context
@@ -1479,7 +1586,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1479
1586
  */
1480
1587
  _isCacheableFilter(prop, op) {
1481
1588
  if (prop === 'concept' || prop === 'code') {
1482
- return op === 'is-a' || op === 'descendent-of' || op === 'is-not-a' || op === 'regex';
1589
+ return op === 'is-a' || op === 'descendent-of' || op === 'descendent-leaf' || op === 'is-not-a' || op === 'regex';
1483
1590
  }
1484
1591
  if (prop === 'child' && op === 'exists') {
1485
1592
  return true;
@@ -1518,6 +1625,19 @@ class FhirCodeSystemProvider extends BaseCSServices {
1518
1625
  // Find all descendants of the specified code
1519
1626
  const includeRoot = (op === 'is-a');
1520
1627
  await this._addDescendants(results, value, includeRoot);
1628
+ } else if (op === 'descendent-leaf') {
1629
+ // R5: the descendants that have no children of their own
1630
+ if (this.codeSystem.getConceptByCode(value)) {
1631
+ for (const code of this.codeSystem.getDescendants(value)) {
1632
+ this.opContext.deadCheck('cs:conceptFilter:descendent-leaf');
1633
+ if (code !== value && this.codeSystem.getChildren(code).length === 0) {
1634
+ const concept = this.codeSystem.getConceptByCode(code);
1635
+ if (concept) {
1636
+ results.add(concept, 0);
1637
+ }
1638
+ }
1639
+ }
1640
+ }
1521
1641
  } else if (op === 'child-of') {
1522
1642
  // Find all descendants of the specified code
1523
1643
  await this._addChildren(results, value);
@@ -1547,6 +1667,20 @@ class FhirCodeSystemProvider extends BaseCSServices {
1547
1667
  }
1548
1668
  }
1549
1669
  }
1670
+ else if (op === 'not-in') {
1671
+ // Every code except the listed ones (comma separated) - their descendants are not excluded
1672
+ const excludeSet = new Set(value.split(',').map(c => c.trim()));
1673
+ const allCodes = this.codeSystem.getAllCodes();
1674
+ for (const code of allCodes) {
1675
+ this.opContext.deadCheck('cs:conceptFilter:not-in');
1676
+ if (!excludeSet.has(code)) {
1677
+ const concept = this.codeSystem.getConceptByCode(code);
1678
+ if (concept) {
1679
+ results.add(concept, 0);
1680
+ }
1681
+ }
1682
+ }
1683
+ }
1550
1684
  else if (op === '=') {
1551
1685
  // Exact match
1552
1686
  const concept = this.codeSystem.getConceptByCode(value);
@@ -1557,7 +1691,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1557
1691
  else if (op === 'regex') {
1558
1692
  // Regular expression match
1559
1693
  try {
1560
- const regex = regexUtilities.compile('^' + value + '$');
1694
+ const regex = regexUtilities.compile('^(?:' + value + ')$'); // the whole value: a|b is ^(?:a|b)$, not ^a|b$
1561
1695
  const allCodes = this.codeSystem.getAllCodes();
1562
1696
  for (const code of allCodes) {
1563
1697
  this.opContext.deadCheck('cs:conceptFilter:regex');
@@ -1705,7 +1839,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1705
1839
  }
1706
1840
  else if (op === 'regex') {
1707
1841
  try {
1708
- const regex = regexUtilities.compile('^' + value + '$');
1842
+ const regex = regexUtilities.compile('^(?:' + value + ')$'); // the whole value: a|b is ^(?:a|b)$, not ^a|b$
1709
1843
  return properties.some(p => regex.test(this._getPropertyValue(p)));
1710
1844
  } catch (error) {
1711
1845
  return false;
@@ -245,7 +245,7 @@ class Iso4217Services extends CodeSystemProvider {
245
245
  return concept;
246
246
  }
247
247
  }
248
- return `Code '${code}' not found in filter set`;
248
+ return null;
249
249
  }
250
250
 
251
251
  async filterCheck(filterContext, set, concept) {
package/tx/cs/cs-icd11.js CHANGED
@@ -22,6 +22,7 @@ const { ICD11Expression, parseExpression, axisLabel } = require('./icd11-express
22
22
  const { Issue } = require('../library/operation-outcome');
23
23
  const { DesignationUse } = require('../library/designations');
24
24
  const assert = require('assert');
25
+ const { guarded } = require('../library/sqlite-utilities');
25
26
 
26
27
  // WHO's own vocabulary for the kinds of text an entity carries, used as designation.use.
27
28
  const SCHEMA_NS = 'http://id.who.int/icd/schema';
@@ -100,10 +101,7 @@ class ICD11Services extends BaseCSServices {
100
101
  }
101
102
 
102
103
  close() {
103
- if (this.db) {
104
- this.db.close();
105
- this.db = null;
106
- }
104
+ this.closeDb();
107
105
  }
108
106
 
109
107
  // -- small promise wrappers, so no query below carries callback boilerplate -----------
@@ -116,7 +114,7 @@ class ICD11Services extends BaseCSServices {
116
114
 
117
115
  #all(sql, params = []) {
118
116
  return new Promise((resolve, reject) => {
119
- this.db.all(sql, params, (err, rows) => err ? reject(err) : resolve(rows || []));
117
+ this.db.all(sql, params, guarded(reject, (err, rows) => err ? reject(err) : resolve(rows || [])));
120
118
  });
121
119
  }
122
120
 
@@ -993,12 +991,22 @@ class ICD11ServicesFactory extends CodeSystemFactoryProvider {
993
991
 
994
992
  id() { return `icd11-${this.systemCode}`; }
995
993
 
994
+ /**
995
+ * The WHO browser page for the concept, as the ICD-API gives it (browserUrl, kept in the
996
+ * database: https://icd.who.int/browse/2026-01/mms/en#257068234). A postcoordinated
997
+ * expression links to its stem.
998
+ */
999
+ codeLink(context) {
1000
+ const concept = context && context.tokens ? context.tokens[0]?.concept : context;
1001
+ return concept && concept.browserUrl ? concept.browserUrl : undefined;
1002
+ }
1003
+
996
1004
  async load() {
997
1005
  if (this._loaded) return;
998
1006
  const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY);
999
1007
  try {
1000
1008
  const get = (sql, p) => new Promise((res, rej) =>
1001
- db.get(sql, p, (e, r) => e ? rej(e) : res(r)));
1009
+ db.get(sql, p, guarded(rej, (e, r) => e ? rej(e) : res(r))));
1002
1010
  const sys = await get('SELECT * FROM system WHERE code = ?', [this.systemCode]);
1003
1011
  if (!sys) {
1004
1012
  throw new Error(`ICD-11 database ${this.dbPath} has no '${this.systemCode}' code system`);
@@ -1007,7 +1015,7 @@ class ICD11ServicesFactory extends CodeSystemFactoryProvider {
1007
1015
  const langs = await get("SELECT value FROM meta WHERE key = 'languages'", []);
1008
1016
  // per-language titles; a database built before system_title existed has none
1009
1017
  const all = (sql, p) => new Promise((res, rej) =>
1010
- db.all(sql, p, (e, r) => e ? rej(e) : res(r || [])));
1018
+ db.all(sql, p, guarded(rej, (e, r) => e ? rej(e) : res(r || []))));
1011
1019
  const titles = {};
1012
1020
  const hasTitles = await get(
1013
1021
  "SELECT 1 x FROM sqlite_master WHERE type = 'table' AND name = 'system_title'", []);
@@ -1054,8 +1062,8 @@ class ICD11ServicesFactory extends CodeSystemFactoryProvider {
1054
1062
 
1055
1063
  const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY);
1056
1064
  try {
1057
- const get = (sql, p) => new Promise((res, rej) => db.get(sql, p, (e, r) => e ? rej(e) : res(r)));
1058
- const all = (sql, p) => new Promise((res, rej) => db.all(sql, p, (e, r) => e ? rej(e) : res(r || [])));
1065
+ const get = (sql, p) => new Promise((res, rej) => db.get(sql, p, guarded(rej, (e, r) => e ? rej(e) : res(r))));
1066
+ const all = (sql, p) => new Promise((res, rej) => db.all(sql, p, guarded(rej, (e, r) => e ? rej(e) : res(r || []))));
1059
1067
  const scale = await get(
1060
1068
  `SELECT ps.id, ps.axis, ps.axis_name, ps.required, ps.allow_multiple,
1061
1069
  c.entity_id, c.code AS stem_code, c.uri AS stem_uri
@@ -1100,14 +1108,14 @@ class ICD11ServicesFactory extends CodeSystemFactoryProvider {
1100
1108
  /** Which code systems a given database holds, so the library can register one each. */
1101
1109
  static listSystems(dbPath) {
1102
1110
  return new Promise((resolve, reject) => {
1103
- const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
1111
+ const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, guarded(reject, (err) => {
1104
1112
  if (err) { reject(new Error(`cannot open ICD-11 database ${dbPath}: ${err.message}`)); return; }
1105
- db.all('SELECT code, url, version, title FROM system ORDER BY code', (e, rows) => {
1113
+ db.all('SELECT code, url, version, title FROM system ORDER BY code', guarded(reject, (e, rows) => {
1106
1114
  db.close();
1107
1115
  if (e) reject(new Error(`${dbPath} is not an ICD-11 database: ${e.message}`));
1108
1116
  else resolve(rows || []);
1109
- });
1110
- });
1117
+ }));
1118
+ }));
1111
1119
  });
1112
1120
  }
1113
1121
  }
package/tx/cs/cs-icf.js CHANGED
@@ -195,9 +195,23 @@ class IcfCodeSystemProvider extends FhirCodeSystemProvider {
195
195
  return ctxt instanceof IcfQualifiedContext ? ctxt.qualifiedCode : super.code(ctxt);
196
196
  }
197
197
 
198
- /** The category's display, then the qualifiers' displays, leaving out any 8 (not specified). */
198
+ /**
199
+ * A qualified code's display is the one an extension-codes supplement in scope gives it, if one
200
+ * does. Otherwise it is generated: the category's display, then the qualifiers' displays, leaving
201
+ * out any 8 (not specified).
202
+ */
199
203
  async display(context) {
200
204
  const ctxt = await this.#resolve(context);
205
+ if (ctxt instanceof IcfQualifiedContext) {
206
+ const ext = this._extensionCodeDisplay(ctxt.qualifiedCode);
207
+ if (ext) {
208
+ return ext;
209
+ }
210
+ }
211
+ return this.#generatedDisplay(ctxt);
212
+ }
213
+
214
+ async #generatedDisplay(ctxt) {
201
215
  const display = await super.display(ctxt);
202
216
  if (!(ctxt instanceof IcfQualifiedContext)) {
203
217
  return display;
@@ -207,14 +221,19 @@ class IcfCodeSystemProvider extends FhirCodeSystemProvider {
207
221
  return parts.length > 0 ? display + ': ' + parts.join(', ') : display;
208
222
  }
209
223
 
210
- /** A qualified code has one designation: its display. The category's translations do not describe it. */
224
+ /**
225
+ * A qualified code's designations: the display an extension-codes supplement in scope gives it
226
+ * (first, so it is the preferred display), and its generated display, which stays a valid one. The
227
+ * category's translations do not describe it.
228
+ */
211
229
  async designations(context, displays) {
212
230
  const ctxt = await this.#resolve(context);
213
231
  if (!(ctxt instanceof IcfQualifiedContext)) {
214
232
  return super.designations(ctxt, displays);
215
233
  }
234
+ this._listSupplementDesignations(ctxt.qualifiedCode, displays, true);
216
235
  const lang = this.defaultLanguage ? this.defaultLanguage.toString() : null;
217
- displays.addDesignation(true, 'active', lang, CodeSystem.makeUseForDisplay(), await this.display(ctxt));
236
+ displays.addDesignation(true, 'active', lang, CodeSystem.makeUseForDisplay(), await this.#generatedDisplay(ctxt));
218
237
  }
219
238
 
220
239
  /**
package/tx/cs/cs-lang.js CHANGED
@@ -75,7 +75,7 @@ class IETFLanguageCodeFilter {
75
75
  }
76
76
 
77
77
  /** Why a code did not match, for the caller. @returns {string} */
78
- describe(code, _lang) {
78
+ describe(code) {
79
79
  const action = this.status ? 'does not contain' : 'contains';
80
80
  const requirement = this.status ? 'required' : 'not allowed';
81
81
  return `The language code ${code} ${action} a ${this.component}, and it is ${requirement}`;
@@ -101,7 +101,7 @@ class IETFLanguageEqualityFilter {
101
101
  return value.toLowerCase() === this.value.toLowerCase();
102
102
  }
103
103
 
104
- describe(code, _lang) {
104
+ describe(code) {
105
105
  return `The language code ${code} does not have a ${this.component} of '${this.value}'`;
106
106
  }
107
107
  }