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
package/tx/params.js CHANGED
@@ -502,6 +502,19 @@ class TxParameters {
502
502
  return '';
503
503
  }
504
504
 
505
+ // The code system version the request's version rules ask for - force-system-version,
506
+ // else system-version, else check-system-version - or null when they name none
507
+ requestedSystemVersion(systemURI) {
508
+ const rules = this.rulesForSystem(systemURI).filter(t => !t.vs);
509
+ for (const mode of ['override', 'default', 'check']) {
510
+ const rule = rules.find(t => t.mode === mode);
511
+ if (rule) {
512
+ return rule.version;
513
+ }
514
+ }
515
+ return null;
516
+ }
517
+
505
518
  rulesForSystem(systemURI) {
506
519
  let result = [];
507
520
  for (let t of this.FVersionRules) {
@@ -521,6 +534,18 @@ class TxParameters {
521
534
  }
522
535
  }
523
536
 
537
+ // The languages the request named - displayLanguage, else the HTTP Accept-Language - or
538
+ // null when it named none (workingLanguages() falls back to a default; this does not)
539
+ requestedLanguages() {
540
+ if (this.FHasDisplayLanguages && this.FDisplayLanguages) {
541
+ return this.FDisplayLanguages;
542
+ }
543
+ if (this.FHasHTTPLanguages && this.FHTTPLanguages) {
544
+ return this.FHTTPLanguages;
545
+ }
546
+ return null;
547
+ }
548
+
524
549
  workingLanguages() {
525
550
  if (this.FDisplayLanguages) {
526
551
  return this.FDisplayLanguages;
package/tx/problems.js CHANGED
@@ -11,13 +11,11 @@ class ProblemFinder {
11
11
  unknownVersions[vsp.sourcePackage()] = sourceUnknownVersions;
12
12
  }
13
13
 
14
- let list = await vsp.listAllValueSets();
15
- for (let url of list) {
16
- let vs = await vsp.fetchValueSet(url);
17
- if (vs && vs.jsonObj.compose) {
14
+ await vsp.scanAllValueSets(async (vs) => {
15
+ if (vs.jsonObj.compose) {
18
16
  await this.scanValueSet(vs.jsonObj.compose, sourceUnknownVersions);
19
17
  }
20
- }
18
+ });
21
19
  }
22
20
  let result = '';
23
21
  for (let sp of provider.listValueSetSourceCodes()) {
package/tx/provider.js CHANGED
@@ -1,3 +1,4 @@
1
+ const { chooseImplicitValueSetFactory } = require('./library/implicit-valuesets');
1
2
  const { CodeSystem } = require("./library/codesystem");
2
3
  const { assertNoActiveContent } = require('../library/narrative-check');
3
4
  const {VersionUtilities} = require("../library/version-utilities");
@@ -11,6 +12,31 @@ const {PackageValueSetProvider} = require("./vs/vs-package");
11
12
  const ValueSet = require("./library/valueset");
12
13
  const {PackageConceptMapProvider} = require("./cm/cm-package");
13
14
  const { applyIdPrefix, CORE_PREFIX } = require("./library/resource-ids");
15
+ const { CodeSystemFactoryProvider } = require("./cs/cs-api");
16
+ const { VirtualCodeSystemProvider } = require("./cs/cs-virtual");
17
+
18
+
19
+ /**
20
+ * How many codes resolveCodes() locates at once. Enough to keep a database-backed code
21
+ * system busy without queueing a whole page's worth of queries at the same time.
22
+ */
23
+ const RESOLVE_CODES_CONCURRENCY = 8;
24
+
25
+ /**
26
+ * fn over items, at most `limit` at a time, with the results in the order of the items.
27
+ */
28
+ async function mapConcurrently(items, limit, fn) {
29
+ const results = new Array(items.length);
30
+ let next = 0;
31
+ const worker = async () => {
32
+ while (next < items.length) {
33
+ const i = next++;
34
+ results[i] = await fn(items[i], i);
35
+ }
36
+ };
37
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
38
+ return results;
39
+ }
14
40
 
15
41
  /**
16
42
  * This class holds what information is in context
@@ -23,6 +49,86 @@ const { applyIdPrefix, CORE_PREFIX } = require("./library/resource-ids");
23
49
  * - a list of concept map providers that can provide concept map instances
24
50
  *
25
51
  */
52
+ /**
53
+ * The provider's CodeSystem map (keys url and url|version), with an index of the CodeSystems it
54
+ * holds by the url they supplement, so that loadSupplements() looks only at those instead of
55
+ * every loaded CodeSystem on every code-system lookup.
56
+ *
57
+ * The index is part of the map, not a cache: set, delete and clear are the only ways to change a
58
+ * Map, and each updates the index in the same call. Provider.addCodeSystem and deleteCodeSystem
59
+ * are the only callers that change this map.
60
+ *
61
+ * supplementsOf(url) gives every value whose CodeSystem.isSupplementFor(url, version) can be
62
+ * true, in the map's own order, and possibly some for which it is false; loadSupplements still
63
+ * asks isSupplementFor of each. isSupplementFor is true only when `supplements` equals url or
64
+ * starts with url + '|', and then the part of `supplements` before any '|' is the part of url
65
+ * before any '|': that is the index key.
66
+ */
67
+ class CodeSystemMap extends Map {
68
+ constructor(entries) {
69
+ super();
70
+ this.supplementIndex = new Map(); // key used by supplementsOf -> Set of map keys
71
+ this.indexedUnder = new Map(); // map key -> its key in supplementIndex
72
+ this.position = new Map(); // map key -> insertion number, to keep the map's order
73
+ this.nextPosition = 0;
74
+ if (entries) {
75
+ for (const [k, v] of entries) this.set(k, v);
76
+ }
77
+ }
78
+
79
+ /** the index key for a url, or for a CodeSystem's supplements value: the part before any '|' */
80
+ static indexKey(url) {
81
+ return String(url).split('|')[0];
82
+ }
83
+
84
+ set(k, v) {
85
+ if (!super.has(k)) {
86
+ this.position.set(k, this.nextPosition++); // a Map keeps a replaced key where it was
87
+ }
88
+ this.#unindex(k);
89
+ super.set(k, v);
90
+ const supplements = v && v.jsonObj ? v.jsonObj.supplements : null;
91
+ if (supplements) {
92
+ const u = CodeSystemMap.indexKey(supplements);
93
+ let keys = this.supplementIndex.get(u);
94
+ if (!keys) this.supplementIndex.set(u, keys = new Set());
95
+ keys.add(k);
96
+ this.indexedUnder.set(k, u);
97
+ }
98
+ return this;
99
+ }
100
+
101
+ delete(k) {
102
+ this.#unindex(k);
103
+ this.position.delete(k);
104
+ return super.delete(k);
105
+ }
106
+
107
+ clear() {
108
+ this.supplementIndex.clear();
109
+ this.indexedUnder.clear();
110
+ this.position.clear();
111
+ super.clear();
112
+ }
113
+
114
+ // removes k from the index under the key it was indexed under when it was set
115
+ #unindex(k) {
116
+ const u = this.indexedUnder.get(k);
117
+ if (u === undefined) return;
118
+ this.indexedUnder.delete(k);
119
+ const keys = this.supplementIndex.get(u);
120
+ keys.delete(k);
121
+ if (keys.size === 0) this.supplementIndex.delete(u);
122
+ }
123
+
124
+ /** the values that may supplement url (any version), in the order values() gives them */
125
+ supplementsOf(url) {
126
+ const keys = this.supplementIndex.get(CodeSystemMap.indexKey(url));
127
+ if (!keys) return [];
128
+ return [...keys].sort((a, b) => this.position.get(a) - this.position.get(b)).map(k => super.get(k));
129
+ }
130
+ }
131
+
26
132
  class Provider {
27
133
  i18n;
28
134
  fhirVersion;
@@ -57,6 +163,10 @@ class Provider {
57
163
 
58
164
  baseUrl = null;
59
165
  path;
166
+ /**
167
+ * {EcosystemResolver|null} finds other servers for code systems that aren't held here (modules.tx.ecosystem)
168
+ */
169
+ ecosystem = null;
60
170
  cacheFolder = null;
61
171
  startTime = Date.now();
62
172
  startMemory = process.memoryUsage();
@@ -98,7 +208,7 @@ class Provider {
98
208
  factory = this.codeSystemFactories.get(vurlMM);
99
209
  }
100
210
  if (factory != null) {
101
- return factory.build(opContext, supplements);
211
+ return this.buildOnce(opContext, factory, supplements);
102
212
  }
103
213
  let cs = this.codeSystems.get(vurl);
104
214
  if (cs == null && vurlMM) {
@@ -107,7 +217,61 @@ class Provider {
107
217
  if (cs != null) {
108
218
  return this.createCodeSystemProvider(opContext, cs, supplements);
109
219
  }
110
- return null;
220
+ return await this.findInEcosystem(opContext, system, version, supplements);
221
+ }
222
+
223
+ /**
224
+ * A code system (and version) that isn't held here, provided by another server in the
225
+ * ecosystem, if modules.tx.ecosystem.use is true, and the request didn't come from
226
+ * another server doing the same thing (see EcosystemResolver)
227
+ *
228
+ * @returns {Promise<VirtualCodeSystemProvider|null>}
229
+ */
230
+ async findInEcosystem(opContext, system, version, supplements) {
231
+ if (!this.ecosystem || opContext.ecosystemForwarded) {
232
+ return null;
233
+ }
234
+ const server = await this.ecosystem.findServer(system, version, this.fhirVersion, opContext);
235
+ if (!server) {
236
+ return null;
237
+ }
238
+ // the registry's claims are broad (tx.fhir.org is authoritative for all of http://hl7.org/*),
239
+ // so the server has to say that it actually has the code system - and the version: the one
240
+ // asked for, or, for a version that is a url, the latest release of it (a SNOMED CT edition
241
+ // asked for without a release date). A partial semver is not completed this way
242
+ const versions = await this.ecosystem.listVersions(server, system, opContext);
243
+ if (versions == null) {
244
+ return null;
245
+ }
246
+ const codes = versions.map(v => v.code);
247
+ let actual = version;
248
+ if (!version) {
249
+ // no version asked for: it's the server's default version - the only version it has, or
250
+ // the one it says is the default
251
+ const defaults = versions.length === 1 ? versions : versions.filter(v => v.isDefault);
252
+ if (defaults.length !== 1) {
253
+ throw new TerminologyError(`Unable to use ${server} for ${system}: its TerminologyCapabilities doesn't say which version of it is the default`);
254
+ }
255
+ actual = defaults[0].code;
256
+ } else if (!codes.includes(version)) {
257
+ const matches = codes.filter(v => v.startsWith(version + '/')).sort();
258
+ if (matches.length === 0) {
259
+ return null;
260
+ }
261
+ actual = matches[matches.length - 1];
262
+ }
263
+ opContext.log('Using ' + server + ' for ' + system + (actual ? '|' + actual : ''));
264
+ return new VirtualCodeSystemProvider(opContext, supplements, server, system, actual, this.ecosystem);
265
+ }
266
+
267
+ /**
268
+ * factory.build(), once per operation, factory and set of supplements (see
269
+ * OperationContext.buildProviderOnce). The caller registers the provider for closing.
270
+ */
271
+ buildOnce(opContext, factory, supplements) {
272
+ // the factory is the code system and version; the supplements make a different provider
273
+ const variant = (supplements || []).map(cs => cs.vurl || cs.url).sort().join(' ');
274
+ return opContext.buildProviderOnce(factory, variant, () => factory.build(opContext, supplements));
111
275
  }
112
276
 
113
277
  loadSupplements(url, version, statedSupplements) {
@@ -115,9 +279,12 @@ class Provider {
115
279
  for (let csp of this.codeSystemFactories.values()) {
116
280
  csp.listSupplements(supplements, url, version, statedSupplements);
117
281
  }
118
- for (let cs of this.codeSystems.values()) {
282
+ // the index gives the same candidates in the same order as values(), less the ones that
283
+ // can't be supplements of url (see CodeSystemMap)
284
+ const candidates = this.codeSystems instanceof CodeSystemMap ? this.codeSystems.supplementsOf(url) : this.codeSystems.values();
285
+ for (let cs of candidates) {
119
286
  if (cs.isSupplementFor(url, version)) {
120
- if (cs.isLangPack() || this.isStatedSupplement(cs, statedSupplements)) {
287
+ if (cs.isGlobalSupplement() || this.isStatedSupplement(cs, statedSupplements)) {
121
288
  supplements.set(cs.vurl, cs);
122
289
  }
123
290
  }
@@ -161,9 +328,12 @@ class Provider {
161
328
  * share ids, and so does other content (hl7.terminology), so no endpoint can serve them on
162
329
  * their own ids (see tx/library/resource-ids.js)
163
330
  *
331
+ * @param {Object} [options]
332
+ * @param {boolean} [options.readOnly] - a tx-worker (cluster.md): the package's value set and
333
+ * concept map caches are the master's, opened read-only (see Library.packageCachesReadOnly)
164
334
  * @returns {Promise<PackageContentLoader|undefined>} the loaded package (mode "npm")
165
335
  */
166
- async loadNpm(packageManager, cacheFolder, details, isDefault, mode) {
336
+ async loadNpm(packageManager, cacheFolder, details, isDefault, mode, options = {}) {
167
337
  // Parse packageId and version from details (e.g., "hl7.terminology.r4#6.0.2")
168
338
  let packageId = details;
169
339
  let version = null;
@@ -210,11 +380,11 @@ class Provider {
210
380
  applyIdPrefix(cs, CORE_PREFIX);
211
381
  this.addCodeSystem(cs);
212
382
  }
213
- const vs = new PackageValueSetProvider(contentLoader);
383
+ const vs = new PackageValueSetProvider(contentLoader, { readOnly: !!options.readOnly });
214
384
  await vs.initialize();
215
385
  vs.assignIds(CORE_PREFIX);
216
386
  this.valueSetProviders.push(vs);
217
- const cm = new PackageConceptMapProvider(contentLoader);
387
+ const cm = new PackageConceptMapProvider(contentLoader, { readOnly: !!options.readOnly });
218
388
  await cm.initialize();
219
389
  cm.assignIds(CORE_PREFIX);
220
390
  this.conceptMapProviders.push(cm);
@@ -254,7 +424,7 @@ class Provider {
254
424
  return null;
255
425
  }
256
426
 
257
- async findValueSet(opContext, url, version) {
427
+ async findValueSet(opContext, url, version, systemVersionFor = null) {
258
428
  for (const vp of this.valueSetProviders) {
259
429
  if (opContext) opContext.deadCheck('findValueSet');
260
430
  let vs = await vp.fetchValueSet(url, version);
@@ -262,7 +432,7 @@ class Provider {
262
432
  return vs;
263
433
  }
264
434
  }
265
- let vs1 = await this.findKnownValueSet(url, version);
435
+ let vs1 = await this.findKnownValueSet(url, version, systemVersionFor);
266
436
  return vs1;
267
437
  }
268
438
 
@@ -325,7 +495,18 @@ class Provider {
325
495
  return result;
326
496
  }
327
497
 
328
- async findKnownValueSet(url, version) {
498
+ async findKnownValueSet(url, version, systemVersionFor = null) {
499
+ // A code system with several versions loaded (SNOMED CT editions) says which form of
500
+ // implicit value set url is: the generic form comes from the version the request asks
501
+ // for (systemVersionFor, from system-version and the like) or else the default version
502
+ // (the one registered under the bare system), the edition form from the latest version
503
+ // of that edition, and the version form from that version alone - not from whichever
504
+ // version happens to be asked first.
505
+ const chosen = chooseImplicitValueSetFactory(this.codeSystemFactories.values(), system => this.codeSystemFactories.get(system), url, systemVersionFor);
506
+ if (chosen) {
507
+ const vs = await chosen.buildKnownValueSet(url, version);
508
+ return vs != null ? new ValueSet(vs) : null;
509
+ }
329
510
  for (let csp of this.codeSystemFactories.values()) {
330
511
  let vs = await csp.buildKnownValueSet(url, version);
331
512
  if (vs != null) {
@@ -351,10 +532,17 @@ class Provider {
351
532
  const targetBase = targetSystem.split('|')[0];
352
533
  let uris = new Set();
353
534
  let asked = new Set(); // a factory is registered under several keys (system, system|version...)
535
+ // A source version that names a version without a date (a SNOMED CT edition URI, say)
536
+ // matches every version of it the server holds; it means the one it resolves to, as it
537
+ // does everywhere else - asking each would give the same translation once per release
538
+ const chosen = sourceVersion ? this.codeSystemFactories.get(sourceSystem + '|' + sourceVersion) : null;
354
539
  for (let csp of this.codeSystemFactories.values()) {
355
540
  if (asked.has(csp)) {
356
541
  continue;
357
542
  }
543
+ if (chosen && csp !== chosen && csp.system() === sourceSystem) {
544
+ continue;
545
+ }
358
546
  if (csp.system() === sourceSystem || csp.system() === targetBase || !uris.has(csp.system())) {
359
547
  uris.add(csp.system());
360
548
  asked.add(csp);
@@ -441,10 +629,31 @@ class Provider {
441
629
  }
442
630
 
443
631
  async resolveCode(opContext, system, version, code) {
632
+ validateParameter(code, "code", String);
633
+ return (await this.resolveCodes(opContext, system, version, [code]))[0];
634
+ }
635
+
636
+ /**
637
+ * resolveCode() for many codes of one code system: where each code lives (a link) and
638
+ * its display, or null, in the order of the codes.
639
+ *
640
+ * The code system is found and its provider built once for all of them, and the codes
641
+ * are located RESOLVE_CODES_CONCURRENCY at a time rather than one after another.
642
+ *
643
+ * @param {OperationContext} opContext
644
+ * @param {string} system
645
+ * @param {string|null} version
646
+ * @param {string[]} codes
647
+ * @returns {Promise<Array<{link: string, description: string}|null>>}
648
+ */
649
+ async resolveCodes(opContext, system, version, codes) {
444
650
  validateParameter(opContext, "opContext", OperationContext);
445
651
  validateParameter(system, "system", String);
446
652
  validateOptionalParameter(version, "version", String);
447
- validateParameter(code, "code", String);
653
+ validateArrayParameter(codes, "codes", String);
654
+ if (codes.length === 0) {
655
+ return [];
656
+ }
448
657
 
449
658
  if (system.includes("|")) {
450
659
  const url = system.substring(0, system.indexOf("|"));
@@ -464,41 +673,68 @@ class Provider {
464
673
  if (factory == null && vurlMM) {
465
674
  factory = this.codeSystemFactories.get(vurlMM);
466
675
  }
467
- if (factory != null) {
468
- const csp = await factory.build(opContext, []);
469
- opContext.registerProvider(csp);
470
- const c = csp ? csp.locate(code) : null;
471
- if (c) {
472
- if (factory.iteratable()) {
473
- return {
474
- link: this.path + "/CodeSystem/x-" + factory.id(),
475
- description: csp.display(c)
476
- }
477
- } else {
478
- const link = factory.codeLink(c);
479
- if (link) {
480
- return {
481
- link: link,
482
- description: csp.display(c)
483
- }
484
- }
485
- }
486
- }
487
- }
676
+ // A code system from a factory gives a code a link only if the factory is iteratable
677
+ // (US states) or has a codeLink() of its own. For the rest (RxNorm, LOINC, SNOMED CT,
678
+ // ...) what locate() finds would be thrown away, so they are not built or asked.
679
+ // That matters: RxNorm looks for a code it does not have in its archive table, which
680
+ // takes 36 ms a code, and 4,529 of Medication Brand Name's codes are not current.
681
+ const linksCodes = factory != null
682
+ && (factory.iteratable() || factory.codeLink !== CodeSystemFactoryProvider.prototype.codeLink);
683
+ const csp = linksCodes ? await this.resolveCodeProvider(opContext, factory) : null;
488
684
  let cs = this.codeSystems.get(vurl);
489
685
  if (cs == null && vurlMM) {
490
686
  cs = this.codeSystems.get(vurlMM);
491
687
  }
492
- if (cs != null) {
493
- const c = cs.codeMap.get(code);
494
- if (c) {
495
- return {
496
- link: this.path + "/CodeSystem/" + cs.id + '#' + code,
497
- description: c.display
498
- };
688
+
689
+ return mapConcurrently(codes, RESOLVE_CODES_CONCURRENCY, async (code) => {
690
+ if (csp) {
691
+ const located = await csp.locate(code);
692
+ const c = located ? located.context : null;
693
+ if (c) {
694
+ if (factory.iteratable()) {
695
+ return {
696
+ link: this.path + "/CodeSystem/x-" + factory.id(),
697
+ description: await csp.display(c)
698
+ }
699
+ } else {
700
+ const link = factory.codeLink(c);
701
+ if (link) {
702
+ return {
703
+ link: link,
704
+ description: await csp.display(c)
705
+ }
706
+ }
707
+ }
708
+ }
499
709
  }
500
- }
501
- return null;
710
+ if (cs != null) {
711
+ const c = cs.codeMap.get(code);
712
+ if (c) {
713
+ return {
714
+ link: this.path + "/CodeSystem/" + cs.id + '#' + code,
715
+ description: c.display
716
+ };
717
+ }
718
+ }
719
+ return null;
720
+ });
721
+ }
722
+
723
+ /**
724
+ * The provider resolveCode() uses for a code system factory in this operation: built
725
+ * the first time it is asked for (see buildOnce), registered for closing when the
726
+ * operation ends, and reused after that. A page asks resolveCode about every code it shows.
727
+ *
728
+ * @param {OperationContext} opContext
729
+ * @param {CodeSystemFactoryProvider} factory
730
+ * @returns {Promise<CodeSystemProvider|null>}
731
+ */
732
+ resolveCodeProvider(opContext, factory) {
733
+ // the same provider getCodeSystemProvider() builds for this code system with no supplements
734
+ return this.buildOnce(opContext, factory, []).then(p => {
735
+ opContext.registerProvider(p);
736
+ return p;
737
+ });
502
738
  }
503
739
 
504
740
  /**
@@ -632,27 +868,20 @@ class Provider {
632
868
  * moved from the FHIR core packages to terminology.hl7.org has been QA'd there since, and
633
869
  * its versions went backwards (coverage-selfpay is 4.0.1 in hl7.fhir.r4.core but 1.0.1 in
634
870
  * hl7.terminology), so version precedence can't decide between them. Instead, a code system
635
- * from an hl7.terminology package replaces every code system with the same url from a FHIR
636
- * core package - none of the core versions stay available.
871
+ * from an hl7.terminology package is always the default for its url - what a reference with
872
+ * no version gets - whatever versions the FHIR core packages have.
637
873
  *
638
- * Except: hl7.fhir.r4.core has the v2 tables 0006, 0360 and 0391 in two versions each,
639
- * with the version in the url (http://terminology.hl7.org/CodeSystem/v2-0006|2.1 - see
640
- * urlHadVersion in loadNpm). hl7.terminology doesn't publish those versions, so they stay
641
- * available by url|version; THO's code system is the default for the url.
874
+ * The core versions stay available by url|version, where THO doesn't provide that version
875
+ * itself: a reference that names its version (url|5.0.0) means the core one, and there's
876
+ * nothing else it can mean. They are never the default while THO provides the url (see also
877
+ * deleteCodeSystem).
642
878
  */
643
879
  addCodeSystem(cs) {
644
880
  if (Provider.#isTHOPackage(cs.sourcePackage)) {
645
- for (const [key, t] of [...this.codeSystems]) {
646
- if (t.url === cs.url && VersionUtilities.isCorePackage(t.sourcePackage)
647
- && (key === t.url || !t.urlHadVersion)) {
648
- this.codeSystems.delete(key);
649
- }
650
- }
651
- } else if (VersionUtilities.isCorePackage(cs.sourcePackage) && !cs.urlHadVersion) {
652
- // THO's code system is always the default for its url
881
+ // THO's code system takes over the default from a core one; the core versions stay
653
882
  const t = this.codeSystems.get(cs.url);
654
- if (t && Provider.#isTHOPackage(t.sourcePackage)) {
655
- return;
883
+ if (t && t.url === cs.url && VersionUtilities.isCorePackage(t.sourcePackage)) {
884
+ this.codeSystems.delete(cs.url);
656
885
  }
657
886
  }
658
887
  const existing = this.codeSystems.get(cs.url);
@@ -677,45 +906,63 @@ class Provider {
677
906
  * THO precedence for value sets and concept maps, as for code systems (see addCodeSystem):
678
907
  * once the library's providers have been added after this endpoint's core package, the core
679
908
  * package's value sets and concept maps whose urls an hl7.terminology package also provides
680
- * are left out. (No core value set or concept map has the version in its url.)
909
+ * are only available to a reference that names their version (and THO doesn't provide that
910
+ * version itself). They are never what a reference with no version gets, and they aren't
911
+ * read by id, found by search, or used for translation unless asked for.
681
912
  */
682
913
  applyTHOPrecedence() {
683
- const thoValueSets = new Set();
914
+ const thoValueSets = new Map();
684
915
  for (const vp of this.valueSetProviders) {
685
916
  if (vp instanceof PackageValueSetProvider && Provider.#isTHOPackage(vp.sourcePackage())) {
686
917
  for (const vs of vp.valueSetMap.values()) {
687
- thoValueSets.add(vs.url);
918
+ Provider.#addVersion(thoValueSets, vs);
688
919
  }
689
920
  }
690
921
  }
691
- const thoConceptMaps = new Set();
922
+ const thoConceptMaps = new Map();
692
923
  for (const cmp of this.conceptMapProviders) {
693
924
  if (cmp instanceof PackageConceptMapProvider && Provider.#isTHOPackage(cmp.sourcePackage())) {
694
925
  for (const cm of cmp.conceptMapMap.values()) {
695
- thoConceptMaps.add(cm.url);
926
+ Provider.#addVersion(thoConceptMaps, cm);
696
927
  }
697
928
  }
698
929
  }
699
930
  for (const vp of this.valueSetProviders) {
700
931
  if (vp instanceof PackageValueSetProvider && VersionUtilities.isCorePackage(vp.sourcePackage())) {
701
- vp.excludeUrls(thoValueSets);
932
+ vp.keepPinnedOnly(thoValueSets);
702
933
  }
703
934
  }
704
935
  for (const cmp of this.conceptMapProviders) {
705
936
  if (cmp instanceof PackageConceptMapProvider && VersionUtilities.isCorePackage(cmp.sourcePackage())) {
706
- cmp.excludeUrls(thoConceptMaps);
937
+ cmp.keepPinnedOnly(thoConceptMaps);
707
938
  }
708
939
  }
709
940
  }
710
941
 
942
+ static #addVersion(map, resource) {
943
+ if (!resource || !resource.url) {
944
+ return;
945
+ }
946
+ if (!map.has(resource.url)) {
947
+ map.set(resource.url, new Set());
948
+ }
949
+ if (resource.version) {
950
+ map.get(resource.url).add(resource.version);
951
+ }
952
+ }
953
+
711
954
  deleteCodeSystem(cs) {
712
955
  this.codeSystems.delete(cs.vurl);
713
956
  this.codeSystems.delete(cs.url);
714
957
  // If other versions of the SAME url survive, re-point the unversioned [url]
715
958
  // entry at the most-recent surviving version. Otherwise leave it deleted.
959
+ // THO precedence (see addCodeSystem): while a THO version survives, a core one
960
+ // can't become the default, however recent its (reset) version looks
961
+ const survivors = [...new Set(this.codeSystems.values())].filter(t => t.url === cs.url);
962
+ const tho = survivors.filter(t => Provider.#isTHOPackage(t.sourcePackage));
716
963
  let existing = null;
717
- for (let t of this.codeSystems.values()) {
718
- if (t.url === cs.url && (!existing || t.isMoreRecent(existing))) {
964
+ for (let t of (tho.length > 0 ? tho : survivors)) {
965
+ if (!existing || t.isMoreRecent(existing)) {
719
966
  existing = t;
720
967
  }
721
968
  }
@@ -726,4 +973,4 @@ class Provider {
726
973
 
727
974
  }
728
975
 
729
- module.exports = { Provider };
976
+ module.exports = { CodeSystemMap, Provider };