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-api.js CHANGED
@@ -224,6 +224,17 @@ class CodeSystemProvider {
224
224
  return false;
225
225
  }
226
226
 
227
+ /**
228
+ * Why an expansion of all of the code system is not closed even though every code it defines
229
+ * is in it - e.g. it reserves code spaces for users to define their own codes
230
+ * (codesystem-reservedCodeSpace), and those codes can't be listed - or null when it is closed
231
+ * (as far as this is concerned: see isNotClosed() for grammars)
232
+ * @returns {string|null}
233
+ */
234
+ openCodeSpaceReason() {
235
+ return null;
236
+ }
237
+
227
238
  /**
228
239
  * returns false if the code system cannot be iterated / enumerated in any
229
240
  * meaningful sense - as distinct from isNotClosed(), which marks a code system
@@ -268,6 +279,96 @@ class CodeSystemProvider {
268
279
  }
269
280
  }
270
281
 
282
+ /**
283
+ * The language a text filter ($expand filter) searches. A text filter matches a code's
284
+ * displays and designations in one language only (the same rule as for the SNOMED CT display
285
+ * filter): the first language the request names (displayLanguage, else Accept-Language) that
286
+ * the code system - or a supplement in use - has displays in, else the code system's default
287
+ * language. Matching is at the language subtag level: de-DE searches all German.
288
+ *
289
+ * @param {Languages|null} languages - what the request asked for
290
+ * @returns {Language}
291
+ */
292
+ searchLanguage(languages) {
293
+ for (const lang of (languages && languages.languages) || []) {
294
+ if (!lang || !lang.language || lang.code === '*') {
295
+ continue;
296
+ }
297
+ const one = new Languages();
298
+ one.add(lang);
299
+ if (this.hasAnyDisplays(one) || this._hasSupplementText(lang)) {
300
+ return lang;
301
+ }
302
+ }
303
+ return new Language(this.defLang());
304
+ }
305
+
306
+ /**
307
+ * Whether a supplement in use has any display or designation (of any use) in the language
308
+ *
309
+ * @param {Language} lang
310
+ * @returns {boolean}
311
+ */
312
+ _hasSupplementText(lang) {
313
+ for (const supplement of this.supplements || []) {
314
+ const suppLang = supplement.jsonObj.language || this.defLang();
315
+ for (const concept of supplement.getAllConcepts()) {
316
+ if (concept.display && CodeSystemProvider.inSearchLanguage(suppLang, lang)) {
317
+ return true;
318
+ }
319
+ if ((concept.designation || []).some(d => d.value && CodeSystemProvider.inSearchLanguage(d.language, lang, suppLang))) {
320
+ return true;
321
+ }
322
+ }
323
+ }
324
+ return false;
325
+ }
326
+
327
+ /**
328
+ * Whether text in language `code` (a tag; empty means `fallback`) is in the search language
329
+ *
330
+ * @param {string|Language|null} code
331
+ * @param {Language} searchLang - from searchLanguage()
332
+ * @param {string} [fallback] - the language of text that doesn't say
333
+ * @returns {boolean}
334
+ */
335
+ static inSearchLanguage(code, searchLang, fallback = 'en') {
336
+ let lang = code instanceof Language ? code : (code ? new Language(code) : null);
337
+ if (!lang || !lang.language) {
338
+ lang = new Language(fallback || 'en');
339
+ }
340
+ return lang.language === searchLang.language;
341
+ }
342
+
343
+ /**
344
+ * The codes whose displays or designations in a supplement in use, in the search language,
345
+ * pass a text filter - a supplement's designations are searched like the code system's own,
346
+ * so every word of the filter must match (as LOINC's and SNOMED CT's own text search need)
347
+ *
348
+ * @param {SearchFilterText} filter
349
+ * @param {Language} searchLang - from searchLanguage()
350
+ * @returns {Set<string>}
351
+ */
352
+ _supplementSearchCodes(filter, searchLang) {
353
+ const codes = new Set();
354
+ for (const supplement of this.supplements || []) {
355
+ const suppLang = supplement.jsonObj.language || this.defLang();
356
+ for (const concept of supplement.getAllConcepts()) {
357
+ if (concept.display && CodeSystemProvider.inSearchLanguage(suppLang, searchLang) && filter.passesAll(concept.display)) {
358
+ codes.add(concept.code);
359
+ continue;
360
+ }
361
+ for (const d of concept.designation || []) {
362
+ if (d.value && CodeSystemProvider.inSearchLanguage(d.language, searchLang, suppLang) && filter.passesAll(d.value)) {
363
+ codes.add(concept.code);
364
+ break;
365
+ }
366
+ }
367
+ }
368
+ }
369
+ return codes;
370
+ }
371
+
271
372
  _hasAnySupplementDisplays(languages) {
272
373
  // Check if any supplements have displays in the requested languages
273
374
  if (this.supplements) {
@@ -326,11 +427,12 @@ class CodeSystemProvider {
326
427
  }
327
428
 
328
429
  /**
329
- * @param {boolean} langPacks - whether to include language packs
430
+ * @param {boolean} globals - whether to include global supplements (which $expand only reports
431
+ * when they contribute something)
330
432
  * @returns {string[]} all supplements in scope
331
433
  */
332
- listSupplements(langPacks) {
333
- return this.supplements ? this.supplements.filter(s => langPacks || !s.isLangPack()).map(s => s.vurl) : [];
434
+ listSupplements(globals) {
435
+ return this.supplements ? this.supplements.filter(s => globals || !s.isGlobalSupplement()).map(s => s.vurl) : [];
334
436
  }
335
437
 
336
438
  /**
@@ -377,6 +479,22 @@ class CodeSystemProvider {
377
479
  * @param {String} code
378
480
  * @returns {string} the best display given the languages in the operation context
379
481
  */
482
+ /**
483
+ * @param {string} code - a code users define (one in the code system's reserved code space)
484
+ * @returns {string|null} the display an extension-codes supplement gives it, if one does
485
+ */
486
+ _extensionCodeDisplay(code) {
487
+ for (const supplement of this.supplements || []) {
488
+ if (supplement.isExtensionCodes()) {
489
+ const concept = supplement.getConceptByCode(code);
490
+ if (concept && concept.display) {
491
+ return concept.display;
492
+ }
493
+ }
494
+ }
495
+ return null;
496
+ }
497
+
380
498
  _displayFromSupplements(code) {
381
499
  assert(typeof code === 'string', 'code must be string');
382
500
  if (this.supplements) {
@@ -499,13 +617,24 @@ class CodeSystemProvider {
499
617
  */
500
618
  async designations(code, displays, significantOnly = false) { return null; }
501
619
 
502
- _listSupplementDesignations(code, displays) {
620
+ /**
621
+ * @param {string} code - the code
622
+ * @param {ConceptDesignations} displays - the designations being built for the code
623
+ * @param {boolean} extensionCode - true if the code is one users define (it is in the code system's
624
+ * reserved code space), so that an extension-codes supplement's display is the code's display. The
625
+ * designations record where it came from (displays.extensionCodeDisplay): 'global' if a global
626
+ * supplement gave it - as if the code system defined it - otherwise 'referenced'
627
+ */
628
+ _listSupplementDesignations(code, displays, extensionCode = false) {
503
629
  assert(typeof code === 'string', 'code must be string');
504
630
 
505
631
  if (this.supplements) {
506
632
  for (const supplement of this.supplements) {
507
633
  const concept= supplement.getConceptByCode(code);
508
634
  if (concept) {
635
+ if (extensionCode && concept.display && supplement.isExtensionCodes()) {
636
+ displays.extensionCodeDisplay = supplement.isGlobalSupplement() || displays.extensionCodeDisplay === 'global' ? 'global' : 'referenced';
637
+ }
509
638
  if (concept.display) {
510
639
  // sometimes the display is just repeated from the base code system
511
640
  if (!displays.hasAnyDisplay(concept.display)) {
@@ -531,11 +660,50 @@ class CodeSystemProvider {
531
660
  async extensions(code) { return null; }
532
661
 
533
662
  /**
534
-
663
+ * The properties of a concept, in CodeSystem.concept.property form ({code, value[x]}).
664
+ * $expand reports them on expansion.contains, so a provider that has properties must
665
+ * override this, not just extendLookup - otherwise $lookup returns them and $expand never does
666
+ *
535
667
  * @param {string | CodeSystemProviderContext} code
668
+ * @param {string[]} [names] the properties the caller wants ('*' for all of them). A provider
669
+ * that has to compute its properties should compute only these (none when the list is
670
+ * empty: an expansion that asks for no properties must not pay for them); one that holds
671
+ * them in memory may return the lot, and the caller picks what it asked for
536
672
  * @returns {CodeSystem.concept.property[]} list of properties (may be empty)
537
673
  */
538
- async properties(code) { return []; }
674
+ // eslint-disable-next-line no-unused-vars
675
+ async properties(code, names) { return []; }
676
+
677
+ /**
678
+ * properties() for a provider whose properties are only built by extendLookup: run it for
679
+ * just the named properties, and turn the $lookup property parameters it adds into
680
+ * CodeSystem.concept.property form. Lookup-only parts (subproperty, description, language,
681
+ * code-display...) are dropped; designations are not properties
682
+ *
683
+ * @param {string | CodeSystemProviderContext} context
684
+ * @param {string[]} names - the properties wanted ('*' for all); none means none
685
+ * @returns {Promise<Object[]>}
686
+ */
687
+ async _propertiesFromLookup(context, names) {
688
+ if (!names || names.length === 0) {
689
+ return [];
690
+ }
691
+ const params = [];
692
+ await this.extendLookup(context, names, params);
693
+ const result = [];
694
+ for (const p of params) {
695
+ if (p.name !== 'property' || !Array.isArray(p.part)) {
696
+ continue;
697
+ }
698
+ const code = p.part.find(x => x.name === 'code')?.valueCode;
699
+ const value = p.part.find(x => x.name === 'value');
700
+ const vn = value ? Object.keys(value).find(k => k.startsWith('value')) : null;
701
+ if (code && vn && value[vn] !== undefined && value[vn] !== null) {
702
+ result.push({ code, [vn]: value[vn] });
703
+ }
704
+ }
705
+ return result;
706
+ }
539
707
 
540
708
  /**
541
709
 
@@ -559,9 +727,12 @@ class CodeSystemProvider {
559
727
  /**
560
728
 
561
729
  * @param {string } code
730
+ * @param {*} [prefetched] - a handle that prefetchCodes() returned for a list holding this code, or
731
+ * null. A provider uses it only if it is one of its own; otherwise it looks the code up as usual
562
732
  * @returns {{context : CodeSystemProviderContext, message : String} the result of looking for the code
563
733
  */
564
- async locate(code) { throw new Error("Must override"); }
734
+ // eslint-disable-next-line no-unused-vars
735
+ async locate(code, prefetched = null) { throw new Error("Must override"); }
565
736
 
566
737
  /**
567
738
 
@@ -605,6 +776,27 @@ class CodeSystemProvider {
605
776
  */
606
777
  async subsumesTest(codeA, codeB) { return 'not-subsumed'; }
607
778
 
779
+ /**
780
+ * The codes a code sits under: every ancestor in the hierarchy subsumesTest() answers from,
781
+ * not just the parents, and not the code itself. Optional: $closure uses it, when it is given,
782
+ * to relate many concepts to one another from one call per concept instead of one
783
+ * subsumesTest() per pair. The default returns null (not supported), and $closure then calls
784
+ * subsumesTest() for each pair, as it always has.
785
+ *
786
+ * A provider that implements it must agree with its own subsumesTest(): for two different
787
+ * codes a and b that this method answers for, subsumesTest(a, b) is 'subsumes' exactly when a
788
+ * is in ancestorsOf(b), 'subsumed-by' exactly when b is in ancestorsOf(a) (and a is not in
789
+ * ancestorsOf(b)), and never 'equivalent'. The codes are given in the form code() returns them.
790
+ * For any code where that can't be promised - an unknown code, a code not in the form code()
791
+ * gives (so another code may be the same concept), a postcoordinated expression - return null,
792
+ * and the pairs that involve it are tested with subsumesTest().
793
+ *
794
+ * @param {string | CodeSystemProviderContext} code
795
+ * @returns {Promise<Set<string>|null>} the ancestors' codes, or null when not supported for this code
796
+ */
797
+ // eslint-disable-next-line no-unused-vars
798
+ async ancestorsOf(code) { return null; }
799
+
608
800
  /**
609
801
 
610
802
  * @param {CodeSystemProviderContext} ctxt the context to add properties for
@@ -636,21 +828,67 @@ class CodeSystemProvider {
636
828
  }
637
829
 
638
830
  /**
639
- * Process a set of includes and excludes for the code system
831
+ * Select the codes of a whole compose in one call (only called when handlesSelecting() is true).
832
+ *
833
+ * expand.js hands over a compose that selects from this code system alone (one system|version),
834
+ * imports no value sets, has no text filter, and lists no concepts (an enumerated concept
835
+ * carries displays and extensions from the value set, so those go through the usual path,
836
+ * where prefetchCodes() applies). Anything else this provider doesn't support - an
837
+ * unknown filter, say - it declines by returning null, and the compose goes through the
838
+ * usual include/exclude path (getPrepContext/filter/executeFilters, locate) instead. A compose
839
+ * with a filter on a property that a supplement in use defines is never handed over: the usual
840
+ * path evaluates that filter from the supplement (tx/workers/supplement-filters.js).
841
+ *
842
+ * The provider applies offset and count: the sets it returns hold just the requested page of
843
+ * the selection, in expansion order. The worker adds every code in them to the expansion
844
+ * (with displays, properties etc. from this provider, as for a filter set), and takes the
845
+ * total from the result.
846
+ *
847
+ * @param {TxParameters} params: information from the request that the user made, to help optimise loading (activeOnly, properties, designations...)
848
+ * @param {Object[]} includes - the compose's includes. Each names this system (and version), with or without filters
849
+ * @param {Object[]} excludes - the compose's excludes, in the same form
850
+ * @param {boolean} excludeInactive: whether the value set leaves out inactive codes (compose.inactive = false)
851
+ * @param {int} offset the index of the first code to return, -1 when the request gave no offset (start at 0)
852
+ * @param {int} count the most codes to return, -1 when the request gave no count (all of them); 0 asks for the total only
853
+ * @param {Object} [context] what the selection is for. `context.valueSet` is `{ url, version }` of the
854
+ * value set whose compose this is (either may be undefined for a value set given inline without
855
+ * them). A provider may ignore it.
640
856
  *
641
- * @param {TxParameters} params: information from the request that the user made, to help optimise loading
642
- * @param {Object[]} includes - a list of includes from the code system. Each include may contain just the system(+version), concepts and/or filters (but won't contain value sets)
643
- * @param {Object[]} excludes - a list of excludes from the code system. Each include may contain just the system(+version), concepts and/or filters (but won't contain value sets)
644
- * @param {boolean} excludeInactive: whether the server will use inactive codes or not
645
- * @param {int} offset if handlesOffset() and !iterate, and if the value set is a simple one that only uses this provider, then this is the applicable offset. -1 if not applicable
646
- * @param {int} count if handlesOffset() and !iterate, and if the value set is a simple one that only uses this provider, then this is the applicable count. -1 if not applicable
647
- * @returns {FilterConceptSet[]} filter sets. In general, it wouldn't make sense to return more than one, but providers can do if they want to. See futher comments on executeFilters
648
- */
649
- processSelection(params, includes, excludes, excludeInactive, offset, count) {
857
+ * A provider that won't produce a selection because it would cost too much may throw the standard
858
+ * too-costly Issue, in the form expand.js uses when it refuses a value set (Issue from
859
+ * tx/library/operation-outcome.js; vurl is url|version, or url when there is no version):
860
+ *
861
+ * throw new Issue('error', 'too-costly', null, 'VALUESET_TOO_COSTLY',
862
+ * this.opContext.i18n.translate('VALUESET_TOO_COSTLY', params.httpLanguages, [vurl, '>' + limit]),
863
+ * null, 422).withDiagnostics(this.opContext.diagnostics());
864
+ *
865
+ * @returns {Promise<{sets: FilterConceptSet[], total: number, notClosed?: boolean}|null>} null to decline.
866
+ * Otherwise `sets` (in general one; iterated with filterMore/filterConcept) hold the page,
867
+ * `total` is the number of codes in the whole selection before paging, or -1 when the
868
+ * provider hasn't counted it (the expansion then has no total), and `notClosed` is true
869
+ * when the selection is open-ended (a grammar-based code system), so the expansion is
870
+ * marked unclosed
871
+ */
872
+ processSelection(params, includes, excludes, excludeInactive, offset, count, context = {}) {
650
873
  // well, you only need to override if handlesSelecting=true, but that's the only time this will be called
651
874
  throw new Error("Must override");
652
875
  }
653
876
 
877
+ /**
878
+ * Before the worker locates each code of a list one at a time: the concepts a value set include
879
+ * or exclude lists ($expand), or that validate-code compares a code with. A provider that
880
+ * answers locate() from a database may read the codes together here and return a handle to
881
+ * what it read: an object of its own, opaque to the worker. The worker passes the handle back
882
+ * as locate(code, prefetched) for each code of that list, and drops it when the loop ends. The
883
+ * provider keeps no state of its own for this: two lists in progress have two handles.
884
+ *
885
+ * @param {String[]} codes - the codes, in the order the worker will locate them (it may stop early)
886
+ * @param {boolean} withDesignations - whether the worker will also list each code's displays and designations
887
+ * @returns {Promise<*>} the handle, or null: this provider does no prefetching (the default)
888
+ */
889
+ // eslint-disable-next-line no-unused-vars
890
+ async prefetchCodes(codes, withDesignations) { return null; }
891
+
654
892
  /**
655
893
  * returns true if a filter is supported
656
894
  *
@@ -666,10 +904,17 @@ class CodeSystemProvider {
666
904
  * it's only use is to be passed back to the CodeSystem provider so it can make use of it to organise the filter process
667
905
  *
668
906
  * @param {boolean} iterate true if the conceptSets that result from this will be iterated, and false if they'll be used to locate a single code
907
+ * @param {Languages|null} languages the languages the request named (TxParameters.requestedLanguages()), or null if it named none;
908
+ * for filters whose meaning depends on language, such as a regex on the display
669
909
  * @returns {FilterExecutionContext} filter
670
910
  *
671
911
  **/
672
- async getPrepContext(iterate) { return new FilterExecutionContext(iterate); }
912
+ // eslint-disable-next-line no-unused-vars
913
+ async getPrepContext(iterate, languages = null) {
914
+ const result = new FilterExecutionContext(iterate);
915
+ result.languages = languages; // what the request asked for (a text filter searches one language)
916
+ return result;
917
+ }
673
918
 
674
919
 
675
920
  /**
@@ -872,6 +1117,18 @@ class CodeSystemProvider {
872
1117
  return null;
873
1118
  }
874
1119
 
1120
+ /**
1121
+ * For a provider with a sqlite connection in this.db: close it, once. The handle is kept:
1122
+ * sqlite still runs the queries already queued (and any their callbacks issue), and fails
1123
+ * later ones with an error. Setting it to null made such a callback throw, outside any promise.
1124
+ */
1125
+ closeDb() {
1126
+ if (this.db && !this.dbClosed) {
1127
+ this.dbClosed = true;
1128
+ this.db.close();
1129
+ }
1130
+ }
1131
+
875
1132
  /**
876
1133
  * a record of observed usages of codes from this code system
877
1134
  * - a map of code and object which has count, an integer count
@@ -1059,7 +1316,15 @@ class CodeSystemFactoryProvider {
1059
1316
  throw new Error("Must override");
1060
1317
  }
1061
1318
 
1062
- codeLink(code) {
1319
+ /**
1320
+ * A link for a code, usually to the code system's own web site, for pages that list codes
1321
+ * (a ValueSet's includes). Override it to give codes from this code system a link; the
1322
+ * default gives none, and then resolveCode() doesn't look the code up at all.
1323
+ *
1324
+ * @param {*} context - what locate() found for the code (the provider's own context)
1325
+ * @returns {string|undefined} the URL, or undefined for no link
1326
+ */
1327
+ codeLink(context) {
1063
1328
  return undefined;
1064
1329
  }
1065
1330
 
package/tx/cs/cs-api.md CHANGED
@@ -63,6 +63,7 @@ These methods describe the code system as a whole. All are synchronous.
63
63
  | `sourcePackage()` | `null` | The NPM package that contributed this code system, if known. |
64
64
  | `propertyDefinitions()` | `null` | The set of defined properties; override to expose them. |
65
65
  | `isNotClosed()` | `false` | Return `true` for grammar-based systems (e.g. UCUM) that cannot be fully enumerated. |
66
+ | `openCodeSpaceReason()` | `null` | Why an expansion of all of the code system isn't closed although it lists every code the system defines - e.g. the system reserves code spaces for user-defined codes (`codesystem-reservedCodeSpace`). The expansion is marked unclosed with this as the reason. |
66
67
  | `isCaseSensitive()` | `true` | Whether code comparisons are case-sensitive. |
67
68
  | `hasParents()` | `false` | Whether the code system has a concept hierarchy. |
68
69
  | `specialEnumeration()` | `null` | Override to nominate a substitute enumeration (used by UCUM). |
@@ -109,6 +110,7 @@ All are `async`.
109
110
  | `sameConcept(a, b)` | `false` | Returns `true` if `a` and `b` refer to the same concept (e.g. different expression forms). |
110
111
  | `isDisplay(designation)` | `false` | Called for designations not already marked with a standard display use code; return `true` to treat the designation as a display. |
111
112
  | `subsumesTest(codeA, codeB)` | `'not-subsumed'` | Returns one of `equivalent`, `subsumes`, `subsumed-by`, or `not-subsumed`. |
113
+ | `ancestorsOf(code)` | `null` | Optional. A `Set` of the codes this code sits under (all ancestors, not itself), in the form `code()` returns them. `$closure` uses it to relate concepts with one call per concept instead of one `subsumesTest()` per pair. It must agree with `subsumesTest()`: for different codes a and b it answers for, a is in `ancestorsOf(b)` exactly when `subsumesTest(a, b)` is `subsumes`, and the two are never `equivalent`. Return `null` for any code where that can't be promised (unknown, not in `code()` form, an expression); pairs involving it are tested with `subsumesTest()`. |
112
114
  | `extendLookup(ctxt, props, params)` | — | Called during `$lookup`; add any additional properties to `params`. |
113
115
 
114
116
  ---
@@ -190,9 +192,43 @@ For large code systems where a value set selects codes from only that system, th
190
192
  hand the entire include/exclude list to the provider in one call rather than using the filter
191
193
  workflow above.
192
194
 
193
- Override `handlesSelecting()` to return `true` and then implement `processSelection()`.
194
- The method receives the full lists of includes and excludes, along with pagination parameters
195
- if applicable, and returns `FilterConceptSet[]`.
195
+ Override `handlesSelecting()` to return `true` and then implement
196
+ `processSelection(params, includes, excludes, excludeInactive, offset, count, context)`.
197
+
198
+ - **When it is called.** `$expand` hands over a compose that selects from this code system
199
+ alone (one system|version), imports no value sets, has no text filter and lists no concepts.
200
+ Value sets that list concepts take the usual path: `locate`, with a `prefetchCodes` handle (see Lookup).
201
+ - **Declining.** Return `null` for anything the provider doesn't support (an unknown filter,
202
+ say). The compose then goes through the filter workflow above, as for any other provider.
203
+ - **Paging.** The provider applies `offset` (-1: none given, start at 0) and `count`
204
+ (-1: none given, return all; 0: the total only). The sets it returns hold just that page,
205
+ in expansion order.
206
+ - **Result.** `{ sets, total, notClosed }`: `sets` is a `FilterConceptSet[]` (in general one),
207
+ iterated with `filterMore`/`filterConcept`; `total` is the size of the whole selection
208
+ before paging, or -1 if the provider hasn't counted it (the expansion then has no total);
209
+ `notClosed` marks an open-ended selection (a grammar-based code system).
210
+
211
+ - **Context.** The optional last argument, `context`, says what the selection is for:
212
+ `context.valueSet` is `{ url, version }` of the value set whose compose this is (either may be
213
+ undefined for a value set given inline without them). A provider may ignore it.
214
+ - **Too costly.** A provider that won't produce a selection because it would cost too much may
215
+ throw the standard too-costly `Issue` (tx/library/operation-outcome.js), in the form `$expand`
216
+ uses when it refuses a value set. Here `vurl` is `url|version`, or `url` when there is no
217
+ version:
218
+
219
+ ```js
220
+ throw new Issue('error', 'too-costly', null, 'VALUESET_TOO_COSTLY',
221
+ this.opContext.i18n.translate('VALUESET_TOO_COSTLY', params.httpLanguages, [vurl, '>' + limit]),
222
+ null, 422).withDiagnostics(this.opContext.diagnostics());
223
+ ```
224
+
225
+ The response is a 422 with a `too-costly` issue, as for a value set `$expand` refuses itself.
226
+ - **Over the limit.** When an include takes the whole system and the reported `total` is over the
227
+ request's limit, `$expand` refuses as it does on the usual path, unless the requested page
228
+ fits inside the limit.
229
+
230
+ The default `handlesSelecting()` returns `false`, so a provider that doesn't override it is
231
+ never asked.
196
232
 
197
233
  ---
198
234
 
@@ -202,8 +238,9 @@ Lookup supports the `$lookup` and `$validate-code` operations.
202
238
 
203
239
  | Method | Description |
204
240
  |---|---|
205
- | `locate(code)` | Look up a code by string. Returns `{ context, message }`. If found, `context` is a `CodeSystemProviderContext` and `message` is `null`. If not found, `context` is `null` and `message` explains why. **Must override.** |
241
+ | `locate(code, prefetched)` | Look up a code by string. `prefetched` is a handle from `prefetchCodes` for a list holding the code, or `null`; ignore it unless it is one of yours. Returns `{ context, message }`. If found, `context` is a `CodeSystemProviderContext` and `message` is `null`. If not found, `context` is `null` and `message` explains why. **Must override.** |
206
242
  | `locateIsA(code)` | Look up a code within the scope of a parent concept. Required only if `hasParents()` returns `true`. |
243
+ | `prefetchCodes(codes, withDesignations)` | Called before the worker locates each code of a list (the concepts a value set include or exclude lists, in `$expand` and `$validate-code`). A database-backed provider may read them together and return a handle to what it read; the worker passes it back as `locate(code, prefetched)` for each code of that list, then drops it. No state on the provider. Default: returns `null` (no prefetching). |
207
244
  | `extendLookup(ctxt, props, params)` | Called during `$lookup` to allow the provider to add extra properties to the response `Parameters`. The `props` array lists the property names requested by the client. |
208
245
 
209
246
  The server calls `locate()` first to confirm the code exists, then uses the returned context
@@ -238,7 +238,7 @@ class AreaCodeServices extends CodeSystemProvider {
238
238
  return concept;
239
239
  }
240
240
  }
241
- return `Code '${code}' not found in filter set`;
241
+ return null;
242
242
  }
243
243
 
244
244
  async filterCheck(filterContext, set, concept) {
@@ -73,6 +73,14 @@ class CountryCodeServices extends CodeSystemProvider {
73
73
  if (!ctxt) {
74
74
  return null;
75
75
  }
76
+ // a user-assigned code's display is a placeholder ('User-assigned'): an extension-codes
77
+ // supplement's display for it comes first
78
+ if (ctxt.userDefined) {
79
+ const ext = this._extensionCodeDisplay(ctxt.code);
80
+ if (ext) {
81
+ return ext;
82
+ }
83
+ }
76
84
  if (ctxt.display && this.opContext.langs.isEnglishOrNothing()) {
77
85
  return ctxt.display;
78
86
  }
@@ -116,11 +124,15 @@ class CountryCodeServices extends CodeSystemProvider {
116
124
  const ctxt = await this.#ensureContext(code);
117
125
  if (ctxt != null) {
118
126
  displays.userDefined = ctxt.userDefined;
119
- displays.addDesignation( true, 'active', 'en', CodeSystem.makeUseForDisplay(), ctxt.display);
127
+ // when an extension-codes supplement gives a user-assigned code a display, the placeholder
128
+ // ('User-assigned') isn't one of its displays
129
+ if (!(ctxt.userDefined && this._extensionCodeDisplay(ctxt.code))) {
130
+ displays.addDesignation( true, 'active', 'en', CodeSystem.makeUseForDisplay(), ctxt.display);
131
+ }
120
132
  if (ctxt.french) {
121
133
  displays.addDesignation(true, 'active', 'fr', CodeSystem.makeUseForDisplay(), ctxt.french);
122
134
  }
123
- this._listSupplementDesignations(ctxt.code, displays);
135
+ this._listSupplementDesignations(ctxt.code, displays, ctxt.userDefined);
124
136
  }
125
137
  }
126
138
 
@@ -199,8 +211,8 @@ class CountryCodeServices extends CodeSystemProvider {
199
211
  const result = new CountryCodeConceptFilter();
200
212
 
201
213
  try {
202
- // Create regex with anchors to match the Pascal implementation (^value$)
203
- const regex = regexUtilities.compile('^' + value + '$');
214
+ // anchored to the whole code
215
+ const regex = regexUtilities.compile('^(?:' + value + ')$'); // the whole value: a|b is ^(?:a|b)$, not ^a|b$
204
216
 
205
217
  for (const concept of this.codes) {
206
218
  if (regex.test(concept.code)) {
@@ -266,7 +278,7 @@ class CountryCodeServices extends CodeSystemProvider {
266
278
  if (concept && set.list.includes(concept)) {
267
279
  return concept;
268
280
  }
269
- return `Code '${code}' not found in filter set`;
281
+ return null;
270
282
  }
271
283
 
272
284
  async filterCheck(filterContext, set, concept) {