fhirsmith 0.18.1 → 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 (214) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +190 -9
  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 +331 -33
  15. package/ips/jurisdictions.js +65 -0
  16. package/ips/nationality.js +120 -0
  17. package/ips/pipeline.js +131 -17
  18. package/ips/presenter.js +1 -1
  19. package/ips/profile-editor.js +283 -4
  20. package/ips/readme.md +170 -20
  21. package/ips/render.js +769 -109
  22. package/ips/scripts.js +46 -0
  23. package/ips/store.js +123 -11
  24. package/ips/structure.js +247 -0
  25. package/ips/terminology.js +1 -1
  26. package/library/body-limit.js +22 -0
  27. package/library/cache-generation.js +15 -0
  28. package/library/cgroup-memory.js +133 -1
  29. package/library/cluster-counts.js +121 -0
  30. package/library/cluster-events.js +153 -0
  31. package/library/cluster-metrics.js +193 -0
  32. package/library/cluster-status.js +468 -0
  33. package/library/config-reader.js +96 -0
  34. package/library/daily-report.js +100 -2
  35. package/library/html-server.js +27 -1
  36. package/library/i18nsupport.js +23 -1
  37. package/library/keep-alive.js +18 -0
  38. package/library/languages.js +17 -3
  39. package/library/logger.js +31 -0
  40. package/library/memory-guard.js +428 -0
  41. package/library/native-memory.js +52 -0
  42. package/library/npm-audit.js +51 -25
  43. package/library/openapi-doc.js +7 -5
  44. package/library/package-manager.js +8 -0
  45. package/library/role.js +197 -0
  46. package/library/server-credentials.js +169 -0
  47. package/library/sqlite-utilities.js +59 -0
  48. package/library/ssrf-guard.js +100 -0
  49. package/library/static-response-stats.js +16 -0
  50. package/library/version-utilities.js +50 -0
  51. package/library/worker-count.js +192 -0
  52. package/nginx.md +30 -11
  53. package/npmprojector/indexer.js +18 -10
  54. package/package.json +11 -8
  55. package/packages/feed-check-page.js +117 -0
  56. package/packages/feed-check.js +279 -0
  57. package/packages/npm-check.js +57 -3
  58. package/packages/npm-signing.js +219 -0
  59. package/packages/openapi.yaml +207 -1
  60. package/packages/package-crawler.js +322 -41
  61. package/packages/packages-template.html +6 -8
  62. package/packages/packages.js +604 -57
  63. package/packages/readme.md +113 -1
  64. package/publisher/publisher-template.html +2 -5
  65. package/publisher/publisher.js +121 -110
  66. package/publisher/readme.md +75 -0
  67. package/registry/api.js +179 -1
  68. package/registry/crawler.js +86 -108
  69. package/registry/documentation/api.md +68 -2
  70. package/registry/documentation/configuration.md +34 -0
  71. package/registry/documentation/internals.md +4 -1
  72. package/registry/documentation/readme.md +3 -2
  73. package/registry/model.js +8 -0
  74. package/registry/openapi.yaml +84 -0
  75. package/registry/registry-template.html +5 -8
  76. package/registry/registry.js +58 -6
  77. package/root-bare-template.html +7 -4
  78. package/root-template.html +5 -8
  79. package/server.js +398 -198
  80. package/shl/shl.js +38 -37
  81. package/static/a11y/a11y-check.js +832 -0
  82. package/static/a11y/axe.min.js +12 -0
  83. package/static/assets/css/bootstrap-fhir.css +15 -15
  84. package/static/assets/images/flags/au.svg +18 -0
  85. package/static/assets/images/flags/unknown.svg +11 -0
  86. package/static/assets/images/flags/us.svg +63 -0
  87. package/static/assets/images/icon_audit.png +0 -0
  88. package/static/assets/images/icon_clinical.png +0 -0
  89. package/static/assets/images/icon_document.png +0 -0
  90. package/static/assets/images/icon_external.png +0 -0
  91. package/static/assets/images/icon_medication.png +0 -0
  92. package/static/assets/images/icon_organization.png +0 -0
  93. package/static/assets/images/icon_other.png +0 -0
  94. package/static/assets/images/icon_person.png +0 -0
  95. package/static/assets/images/icon_section.png +0 -0
  96. package/stats-db.js +45 -0
  97. package/stats.js +197 -10
  98. package/testing/testing-template.html +5 -8
  99. package/token/nginx_passport_setup.md +1 -1
  100. package/token/security_guide.md +1 -1
  101. package/token/token-template.html +2 -5
  102. package/token/token.js +40 -36
  103. package/translations/Messages.properties +19 -0
  104. package/tx/README.md +158 -4
  105. package/tx/closure/closure-store.js +126 -3
  106. package/tx/cm/cm-database.js +69 -57
  107. package/tx/cm/cm-package.js +50 -16
  108. package/tx/cs/cs-api.js +284 -19
  109. package/tx/cs/cs-api.md +41 -4
  110. package/tx/cs/cs-areacode.js +1 -1
  111. package/tx/cs/cs-country.js +17 -5
  112. package/tx/cs/cs-cpt.js +33 -16
  113. package/tx/cs/cs-cs.js +199 -24
  114. package/tx/cs/cs-currency.js +1 -1
  115. package/tx/cs/cs-icd11.js +21 -13
  116. package/tx/cs/cs-icf.js +22 -3
  117. package/tx/cs/cs-lang.js +2 -2
  118. package/tx/cs/cs-loinc.js +193 -67
  119. package/tx/cs/cs-mimetypes.js +12 -2
  120. package/tx/cs/cs-ndc.js +40 -25
  121. package/tx/cs/cs-omop.js +54 -29
  122. package/tx/cs/cs-rxnorm.js +300 -196
  123. package/tx/cs/cs-snomed.js +861 -95
  124. package/tx/cs/cs-ucum.js +14 -4
  125. package/tx/cs/cs-unii.js +18 -14
  126. package/tx/cs/cs-virtual.js +210 -0
  127. package/tx/ecosystem-resolver.js +413 -0
  128. package/tx/html/cluster-dash-metrics.liquid +26 -0
  129. package/tx/html/cluster-home-metrics.liquid +31 -0
  130. package/tx/html/cluster-metrics-script.liquid +251 -0
  131. package/tx/html/codesystem-operations.liquid +3 -3
  132. package/tx/html/ecl-panel.liquid +1 -1
  133. package/tx/html/external-metrics.liquid +17 -0
  134. package/tx/html/operations-form.liquid +17 -17
  135. package/tx/html/search-form.liquid +11 -11
  136. package/tx/html/status-line.liquid +58 -0
  137. package/tx/html/status-panel.liquid +140 -0
  138. package/tx/html/status-tasks.liquid +25 -0
  139. package/tx/html/tx-template.html +2 -5
  140. package/tx/importers/atc-to-fhir.js +17 -16
  141. package/tx/importers/export-icd11-codesystem.js +3 -2
  142. package/tx/importers/import-icd11.js +4 -3
  143. package/tx/importers/import-loinc.module.js +118 -56
  144. package/tx/importers/import-ndc.module.js +56 -44
  145. package/tx/importers/import-rxnorm.module.js +118 -79
  146. package/tx/importers/import-sct.module.js +125 -61
  147. package/tx/importers/import-unii.module.js +37 -36
  148. package/tx/importers/subset-loinc.module.js +49 -48
  149. package/tx/importers/subset-rxnorm.module.js +29 -28
  150. package/tx/importers/terminal.js +18 -0
  151. package/tx/importers/tx-import-base.js +13 -12
  152. package/tx/importers/tx-import-settings.js +5 -4
  153. package/tx/importers/tx-import.js +53 -52
  154. package/tx/library/canonical-resource.js +49 -0
  155. package/tx/library/codesystem.js +94 -5
  156. package/tx/library/designations.js +45 -2
  157. package/tx/library/extensions.js +13 -7
  158. package/tx/library/implicit-valuesets.js +66 -0
  159. package/tx/library/renderer.js +77 -21
  160. package/tx/library/sqlite-utilities.js +2 -0
  161. package/tx/library/tho-precedence.js +40 -0
  162. package/tx/library/ucum-parsers.js +11 -1
  163. package/tx/library/wait-for-cache.js +43 -0
  164. package/tx/library.js +321 -22
  165. package/tx/openapi-operations.js +6 -4
  166. package/tx/operation-context.js +460 -99
  167. package/tx/params.js +25 -0
  168. package/tx/problems.js +3 -5
  169. package/tx/provider.js +314 -67
  170. package/tx/resource-cache-sqlite.js +1050 -0
  171. package/tx/sct/documentation.md +45 -21
  172. package/tx/sct/ecl.js +81 -17
  173. package/tx/sct/editions.js +39 -0
  174. package/tx/sct/expressions.js +10 -1
  175. package/tx/sct/load-config.js +107 -0
  176. package/tx/sct/mmap.js +109 -0
  177. package/tx/sct/structures.js +175 -15
  178. package/tx/sct/text-index.js +5 -1
  179. package/tx/syndication/syndication-feed.js +2 -2
  180. package/tx/tx-html.js +44 -8
  181. package/tx/tx.fhir.org.yml +8 -8
  182. package/tx/tx.js +282 -80
  183. package/tx/usage-tracker.js +39 -11
  184. package/tx/vs/vs-api.js +19 -0
  185. package/tx/vs/vs-database.js +386 -62
  186. package/tx/vs/vs-package.js +44 -17
  187. package/tx/vs/vs-vsac.js +649 -101
  188. package/tx/workers/batch-validate.js +37 -10
  189. package/tx/workers/batch.js +6 -1
  190. package/tx/workers/cache-control.js +15 -4
  191. package/tx/workers/closure.js +156 -13
  192. package/tx/workers/compare.js +8 -5
  193. package/tx/workers/expand.js +1173 -289
  194. package/tx/workers/metadata.js +35 -0
  195. package/tx/workers/search.js +53 -1
  196. package/tx/workers/supplement-filters.js +145 -0
  197. package/tx/workers/translate.js +169 -4
  198. package/tx/workers/validate.js +223 -119
  199. package/tx/workers/worker.js +123 -31
  200. package/tx/xversion/xv-valueset.js +7 -4
  201. package/utilities/submit-test-report.js +3 -1
  202. package/utilities/tx-stress-test.js +482 -0
  203. package/vcl/readme.md +37 -0
  204. package/vcl/vcl-parser.js +15 -0
  205. package/xig/readme.md +52 -0
  206. package/xig/xig-template.html +5 -8
  207. package/xig/xig.js +238 -422
  208. package/changelog-template.md +0 -22
  209. package/registry/test-runner.js +0 -237
  210. package/release.sh +0 -80
  211. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  212. package/tsconfig.eslint.json +0 -9
  213. package/tx/html/dash-metrics.liquid +0 -147
  214. package/tx/html/home-metrics.liquid +0 -225
@@ -8,6 +8,8 @@ const { VersionUtilities } = require('../../library/version-utilities');
8
8
  const {validateParameter} = require("../../library/utilities");
9
9
  const {ConceptMap} = require("../library/conceptmap");
10
10
  const { applyIdPrefix, stripIdPrefix } = require('../library/resource-ids');
11
+ const { keepForPinnedReference } = require('../library/tho-precedence');
12
+ const { waitForCache } = require('../library/wait-for-cache');
11
13
 
12
14
  /**
13
15
  * Package-based ConceptMap provider using shared database layer
@@ -17,14 +19,19 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
17
19
 
18
20
  /**
19
21
  * @param {PackageContentLoader} packageLoader - Path to the extracted package folder
22
+ * @param {Object} [options]
23
+ * @param {boolean} [options.readOnly] - a tx-worker (cluster.md): the cache is the master's; it
24
+ * is only read (ConceptMapDatabase reads with OPEN_READONLY), waited for until the master has
25
+ * finished it, and never built or changed
20
26
  */
21
- constructor(packageLoader) {
27
+ constructor(packageLoader, options = {}) {
22
28
  super();
23
29
  validateParameter(packageLoader, "packageLoader", PackageContentLoader);
24
30
  this.packageLoader = packageLoader;
25
31
  // -2: caches from before concept maps were checked for active content in their
26
32
  // narrative are rebuilt (see initialize)
27
33
  this.dbPath = path.join(packageLoader.packageFolder, '.conceptmaps-2.db');
34
+ this.readOnly = !!options.readOnly;
28
35
  this.database = new ConceptMapDatabase(this.dbPath);
29
36
  this.conceptMapMap = new Map();
30
37
  this.initialized = false;
@@ -40,6 +47,14 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
40
47
  return;
41
48
  }
42
49
  await this.packageLoader.initialize();
50
+ if (this.readOnly) {
51
+ await waitForCache(async () => (await this.database.exists()) && (await this.database.isComplete()),
52
+ `Package ${this.packageLoader.pid ? this.packageLoader.pid() : ''}: concept map cache ${this.dbPath}`);
53
+ this.conceptMapMap = await this.database.loadAllConceptMaps();
54
+ this.#applyIdPrefix();
55
+ this.initialized = true;
56
+ return;
57
+ }
43
58
  await fs.rm(path.join(this.packageLoader.packageFolder, '.conceptmaps.db'), { force: true }); // the old cache
44
59
 
45
60
  // A cache is only used once its population finished. One that was cut short
@@ -169,7 +184,7 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
169
184
 
170
185
  // Only process each ConceptMap once (use URL to deduplicate)
171
186
  const vsUrl = json.url;
172
- if (seen.has(vsUrl)) {
187
+ if (seen.has(vsUrl) || this.#pinnedOnly.has(vs)) {
173
188
  continue;
174
189
  }
175
190
 
@@ -193,6 +208,20 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
193
208
  isMatch = false;
194
209
  break;
195
210
  }
211
+ } else if (param === 'url') {
212
+ // a left match, on the url as asked for (not lower cased)
213
+ const asked = searchParams.find(sp => sp.name === 'url').value;
214
+ if (!(json.url || '').startsWith(asked)) {
215
+ isMatch = false;
216
+ break;
217
+ }
218
+ } else if (param === 'version') {
219
+ // version-aware matching, on the version as asked for (not lower cased)
220
+ const asked = searchParams.find(sp => sp.name === 'version').value;
221
+ if (!VersionUtilities.versionSearchMatches(asked, json.version, VersionUtilities.versionAlgorithmOf(json))) {
222
+ isMatch = false;
223
+ break;
224
+ }
196
225
  } else if (param === 'jurisdiction') {
197
226
  // Special handling for jurisdiction - array of CodeableConcept
198
227
  if (!this._matchJurisdiction(json.jurisdiction, searchValue)) {
@@ -330,35 +359,39 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
330
359
  return this.packageLoader.id();
331
360
  }
332
361
 
333
- // the concept maps left out by excludeUrls, by their own (unprefixed) id
362
+ // the concept maps that keepPinnedOnly restricted, by their own (unprefixed) id, and themselves
334
363
  #excludedIds = new Set();
364
+ #pinnedOnly = new Set();
335
365
 
336
366
  /**
337
- * Leave out the concept maps with any of these urls: they can't be fetched, they aren't
338
- * found by search, and they aren't used for translation. The FHIR core package's concept
339
- * maps are left out where hl7.terminology provides the same url (see
340
- * Provider.applyTHOPrecedence). Must be called once the provider is initialized - the
341
- * database is shared, so it isn't changed.
367
+ * The concept maps with these urls are hl7.terminology's to provide (see
368
+ * Provider.applyTHOPrecedence): from this (core) package they are only available to a
369
+ * reference that names their version, where THO doesn't provide that version itself. They
370
+ * aren't the default for their url, can't be read by id, aren't found by search, and aren't
371
+ * used for translation unless asked for. Must be called once the provider is initialized -
372
+ * the database is shared, so it isn't changed.
342
373
  *
343
- * @param {Set<string>} urls
374
+ * @param {Map<string, Set<string>>} thoVersions - url -> the versions hl7.terminology provides
344
375
  */
345
- excludeUrls(urls) {
346
- const excluded = new Set();
376
+ keepPinnedOnly(thoVersions) {
377
+ const affected = new Set();
347
378
  for (const cm of this.conceptMapMap.values()) {
348
- if (cm && urls.has(cm.url)) {
349
- excluded.add(cm);
379
+ if (cm && thoVersions.has(cm.url)) {
380
+ affected.add(cm);
350
381
  }
351
382
  }
352
383
  for (const [key, cm] of [...this.conceptMapMap]) {
353
- if (excluded.has(cm)) {
384
+ if (affected.has(cm) && !keepForPinnedReference(key, cm, thoVersions.get(cm.url))) {
354
385
  this.conceptMapMap.delete(key);
355
386
  }
356
387
  }
357
- for (const cm of excluded) {
388
+ for (const cm of affected) {
389
+ this.#pinnedOnly.add(cm);
358
390
  this.#excludedIds.add(stripIdPrefix(cm.id, this.spaceId) ?? cm.id);
359
391
  }
360
392
  }
361
393
 
394
+
362
395
  prefixIds() {
363
396
  this.#applyIdPrefix();
364
397
  }
@@ -371,7 +404,8 @@ class PackageConceptMapProvider extends AbstractConceptMapProvider {
371
404
 
372
405
  async findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem) {
373
406
  for (let cm of this.conceptMapMap.values()) {
374
- if (cm.providesTranslation(sourceSystem, sourceScope, targetScope, targetSystem)) {
407
+ // a concept map that THO also provides is only used when it's asked for by version
408
+ if (!this.#pinnedOnly.has(cm) && cm.providesTranslation(sourceSystem, sourceScope, targetScope, targetSystem)) {
375
409
  conceptMaps.push(cm);
376
410
  }
377
411
  }
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) {