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
@@ -376,6 +376,8 @@ class MetadataHandler {
376
376
  }
377
377
  }
378
378
 
379
+ this.markDefaultVersions(provider, seenSystems);
380
+
379
381
  // Convert map to array and sort by URI
380
382
  const entries = Array.from(seenSystems.values());
381
383
  entries.sort((a, b) => (a.uri || '').localeCompare(b.uri || ''));
@@ -383,6 +385,39 @@ class MetadataHandler {
383
385
  return entries.length > 0 ? entries : undefined;
384
386
  }
385
387
 
388
+ /**
389
+ * Mark the version a request that names no version gets - the default - as isDefault. That's
390
+ * what getCodeSystemProvider() finds under the url alone: a native code system (factory) first,
391
+ * else the CodeSystem resource the provider keeps there (the most recent of the versions loaded,
392
+ * see Provider.addCodeSystem). A code system with only one version has that as its default,
393
+ * however it's held
394
+ */
395
+ markDefaultVersions(provider, seenSystems) {
396
+ for (const [url, entry] of seenSystems) {
397
+ if (!entry.version || entry.version.length === 0) {
398
+ continue;
399
+ }
400
+ let def = null;
401
+ const factory = provider && provider.codeSystemFactories ? provider.codeSystemFactories.get(url) : null;
402
+ if (factory) {
403
+ def = factory.version();
404
+ } else {
405
+ const cs = provider && provider.codeSystems ? provider.codeSystems.get(url) : null;
406
+ if (cs) {
407
+ def = cs.version || (cs.jsonObj && cs.jsonObj.version) || null;
408
+ }
409
+ }
410
+ if (!def && entry.version.length === 1) {
411
+ def = entry.version[0].code;
412
+ }
413
+ for (const v of entry.version) {
414
+ if (v.code === def) {
415
+ v.isDefault = true;
416
+ }
417
+ }
418
+ }
419
+ }
420
+
386
421
  /**
387
422
  * Add or update a code system entry
388
423
  * @param {Map} seenSystems - Map of URL to entry
@@ -7,6 +7,7 @@
7
7
 
8
8
  const { TerminologyWorker } = require('./worker');
9
9
  const {Utilities} = require("../../library/utilities");
10
+ const { VersionUtilities } = require("../../library/version-utilities");
10
11
  const {debugLog} = require("../operation-context");
11
12
  const {acceptsHtml} = require("../tx-html");
12
13
 
@@ -116,6 +117,9 @@ class SearchWorker extends TerminologyWorker {
116
117
 
117
118
  // Sort results
118
119
  matches = this.sortResults(matches, sort);
120
+ if (summary !== 'count') {
121
+ matches = await this.loadPage(matches, offset, count);
122
+ }
119
123
 
120
124
  // Build and return the bundle
121
125
  const bundle = this.buildSearchBundle(
@@ -142,7 +146,7 @@ class SearchWorker extends TerminologyWorker {
142
146
  const searchParams = {};
143
147
  for (const [key, value] of Object.entries(params)) {
144
148
  if (!key.startsWith('_') && value && SearchWorker.ALLOWED_PARAMS.includes(key)) {
145
- searchParams[key] = key == 'url' ? value : value.toLowerCase();
149
+ searchParams[key] = key == 'url' || key == 'version' ? value : value.toLowerCase();
146
150
  }
147
151
  }
148
152
 
@@ -266,6 +270,44 @@ class SearchWorker extends TerminologyWorker {
266
270
  return false;
267
271
  }
268
272
 
273
+ /**
274
+ * A provider that doesn't hold its resources in memory can return matches that only carry
275
+ * what a search is sorted by, and a load() that reads the resource (see VSACValueSetProvider):
276
+ * read the ones on this page. One that can't be read (it was rejected when it was parsed) is
277
+ * dropped, and the next match moves up onto the page; the total counts the dropped ones less.
278
+ * A rejected match on a page not asked for is still counted: rejected rows are rare, and
279
+ * finding out exactly which matches are good would mean reading all of them
280
+ */
281
+ async loadPage(matches, offset, count) {
282
+ let start = offset;
283
+ let end = Math.min(matches.length, offset + count);
284
+ while (start < end) {
285
+ this.deadCheck('search-load');
286
+ const page = [];
287
+ for (let i = start; i < end; i++) {
288
+ if (matches[i].load) {
289
+ page.push(i);
290
+ }
291
+ }
292
+ const loaded = await Promise.all(page.map(i => matches[i].load()));
293
+ let dropped = 0;
294
+ page.forEach((i, n) => {
295
+ matches[i] = loaded[n] || null;
296
+ if (!loaded[n]) {
297
+ dropped++;
298
+ }
299
+ });
300
+ if (dropped === 0) {
301
+ break;
302
+ }
303
+ // the matches after the page move up to fill it: read those
304
+ matches = matches.filter(m => m !== null);
305
+ start = end - dropped;
306
+ end = Math.min(matches.length, offset + count);
307
+ }
308
+ return matches;
309
+ }
310
+
269
311
  /**
270
312
  * Sort results by the specified field
271
313
  */
@@ -456,6 +498,11 @@ class SearchWorker extends TerminologyWorker {
456
498
  isMatch = false;
457
499
  break;
458
500
  }
501
+ } else if (param === 'version') { // version-aware matching
502
+ if (!VersionUtilities.versionSearchMatches(searchValue, json.version, VersionUtilities.versionAlgorithmOf(json))) {
503
+ isMatch = false;
504
+ break;
505
+ }
459
506
  } else if (param === 'url' || param === 'system') { // exact match
460
507
  const propValue = json.url;
461
508
  if (propValue !== searchValue) {
@@ -527,6 +574,11 @@ class SearchWorker extends TerminologyWorker {
527
574
  isMatch = false;
528
575
  break;
529
576
  }
577
+ } else if (param === 'version') { // version-aware matching
578
+ if (!VersionUtilities.versionSearchMatches(searchValue, json.version)) {
579
+ isMatch = false;
580
+ break;
581
+ }
530
582
  } else if (param === 'url' || param === 'system') { // exact match
531
583
  const propValue = json.url;
532
584
  if (propValue !== searchValue) {
@@ -0,0 +1,145 @@
1
+ const regexUtilities = require('../../library/regex-utilities');
2
+
3
+ /**
4
+ * A value set filter on a property that a supplement in use defines, and the code system
5
+ * itself doesn't (catalog-group = renal-diabetes on LOINC, with a supplement that gives
6
+ * LOINC codes a catalog-group). The supplement says which codes have which values, so the
7
+ * filter is evaluated from the supplement, the same way whatever the code system and
8
+ * whichever provider serves it - the provider never sees the filter.
9
+ *
10
+ * A filter that selects codes the supplement lists (=, in, regex, exists = true) can drive
11
+ * an expansion on its own; one that selects codes by what the supplement doesn't say
12
+ * (not-in, exists = false) can only narrow what other filters select.
13
+ */
14
+ class SupplementPropertyFilter {
15
+ /**
16
+ * @param {string} property
17
+ * @param {string} op
18
+ * @param {string} value
19
+ * @param {CodeSystem[]} supplements - the supplements in use that define the property
20
+ */
21
+ constructor(property, op, value, supplements) {
22
+ this.property = property;
23
+ this.op = op;
24
+ this.value = value;
25
+ this.values = new Map(); // code -> string[] (the property's values on that code)
26
+ for (const supplement of supplements) {
27
+ this.#scan(supplement.jsonObj.concept || []);
28
+ }
29
+ if (op === 'regex') {
30
+ this.regex = regexUtilities.compile('^(?:' + value + ')$'); // the whole value: a|b is ^(?:a|b)$, not ^a|b$
31
+ } else if (op === 'in' || op === 'not-in') {
32
+ this.list = value.split(',').map(s => s.trim());
33
+ }
34
+ }
35
+
36
+ #scan(concepts) {
37
+ for (const concept of concepts) {
38
+ for (const p of concept.property || []) {
39
+ if (p.code === this.property) {
40
+ const v = SupplementPropertyFilter.valueOf(p);
41
+ if (v !== null) {
42
+ if (!this.values.has(concept.code)) {
43
+ this.values.set(concept.code, []);
44
+ }
45
+ this.values.get(concept.code).push(v);
46
+ }
47
+ }
48
+ }
49
+ this.#scan(concept.concept || []);
50
+ }
51
+ }
52
+
53
+ /** the value of a CodeSystem.concept.property, as a string to compare with a filter value */
54
+ static valueOf(p) {
55
+ for (const [k, v] of Object.entries(p)) {
56
+ if (k.startsWith('value') && v !== undefined && v !== null) {
57
+ if (typeof v === 'object') {
58
+ return v.code !== undefined ? String(v.code) : null; // Coding
59
+ }
60
+ return String(v);
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+
66
+ static ops() {
67
+ return ['=', 'in', 'not-in', 'regex', 'exists'];
68
+ }
69
+
70
+ /** whether the codes this filter selects are all codes the supplement lists */
71
+ get enumerable() {
72
+ return this.op !== 'not-in' && !(this.op === 'exists' && this.value === 'false');
73
+ }
74
+
75
+ #valueMatches(v) {
76
+ switch (this.op) {
77
+ case '=': return v === this.value;
78
+ case 'in': return this.list.includes(v);
79
+ case 'regex': return this.regex.test(v);
80
+ default: return false;
81
+ }
82
+ }
83
+
84
+ /** whether the code passes this filter */
85
+ matches(code) {
86
+ const vals = this.values.get(code) || [];
87
+ switch (this.op) {
88
+ case 'exists': return (vals.length > 0) === (this.value === 'true');
89
+ case 'not-in': return !vals.some(v => this.list.includes(v));
90
+ default: return vals.some(v => this.#valueMatches(v));
91
+ }
92
+ }
93
+
94
+ /** the codes this filter selects (only for an enumerable filter), in the supplement's order */
95
+ codes() {
96
+ return [...this.values.keys()].filter(code => this.matches(code));
97
+ }
98
+
99
+ toString() {
100
+ return this.property + ' ' + this.op + ' ' + this.value;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * The supplement filter for a ValueSet filter on this code system, or null when the filter is
106
+ * the code system's own (it isn't on a property a supplement in use defines, or the code system
107
+ * does the filter itself, which takes precedence)
108
+ *
109
+ * @param {CodeSystemProvider} cs
110
+ * @param {Object} fc - the ValueSet.compose.include.filter
111
+ * @returns {Promise<SupplementPropertyFilter|null>}
112
+ */
113
+ async function supplementFilter(cs, fc) {
114
+ const supplements = (cs.supplements || []).filter(s => (s.jsonObj.property || []).some(p => p.code === fc.property));
115
+ if (supplements.length === 0 || !SupplementPropertyFilter.ops().includes(fc.op) || !fc.value) {
116
+ return null;
117
+ }
118
+ if (await cs.doesFilter(fc.property, fc.op, fc.value)) {
119
+ return null;
120
+ }
121
+ return new SupplementPropertyFilter(fc.property, fc.op, fc.value, supplements);
122
+ }
123
+
124
+ /**
125
+ * Split a ValueSet include's filters into the code system's own, and those on properties
126
+ * that supplements define
127
+ *
128
+ * @param {CodeSystemProvider} cs
129
+ * @param {Object[]} filters
130
+ * @returns {Promise<{base: Object[], supplement: SupplementPropertyFilter[]}>}
131
+ */
132
+ async function splitFilters(cs, filters) {
133
+ const result = { base: [], supplement: [] };
134
+ for (const fc of filters || []) {
135
+ const sf = await supplementFilter(cs, fc);
136
+ if (sf) {
137
+ result.supplement.push(sf);
138
+ } else {
139
+ result.base.push(fc);
140
+ }
141
+ }
142
+ return result;
143
+ }
144
+
145
+ module.exports = { SupplementPropertyFilter, supplementFilter, splitFilters };
@@ -30,6 +30,14 @@ function elementComment(em) {
30
30
  // checkTargetConcepts leaves it alone. A symbol key, so it never reaches the JSON output
31
31
  const TRUSTED_TARGET = Symbol('trustedTarget');
32
32
 
33
+ /** The source, for a message: quoted, unless it is already a description (see describeSourceSystem). */
34
+ function quoteFrom(from) {
35
+ if (!from) {
36
+ return "'(unspecified)'";
37
+ }
38
+ return from.startsWith("'") ? from : `'${from}'`;
39
+ }
40
+
33
41
  class TranslateWorker extends TerminologyWorker {
34
42
  /**
35
43
  * @param {OperationContext} opContext - Operation context
@@ -281,6 +289,9 @@ class TranslateWorker extends TerminologyWorker {
281
289
  }
282
290
  }
283
291
 
292
+ // the no-maps message names the source's version - which, for SNOMED CT, is the edition
293
+ this.sourceDescription = reverse ? null : await this.describeSourceSystem(coding, txp);
294
+
284
295
  // If no explicit concept map, we need to find one based on source/target
285
296
  if (conceptMaps.length == 0) {
286
297
  if (reverse) {
@@ -289,7 +300,24 @@ class TranslateWorker extends TerminologyWorker {
289
300
  } else {
290
301
  await this.findConceptMapsInAdditionalResources(conceptMaps, coding.system, sourceScope, targetScope, targetSystem);
291
302
  await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, coding.system, coding.version, sourceScope, targetScope, targetSystem, coding.code);
303
+ // targetScope with no targetSystem: the code systems (and versions) the value set draws
304
+ // on are the target systems - which is how implicit maps are found, since they belong
305
+ // to code systems, not value sets (a national SNOMED CT edition to the International
306
+ // Edition the value set is built on, say). What they produce is then held to the value
307
+ // set (see doTranslate)
308
+ if (targetScope && !targetSystem) {
309
+ for (const system of await this.systemsOfScope(targetScope)) {
310
+ // as the value set has it (system|version - what an implicit map into one SNOMED CT
311
+ // edition needs), and as the bare system (what a map's group.target usually is)
312
+ const targets = system.includes('|') ? [system, system.substring(0, system.indexOf('|'))] : [system];
313
+ for (const target of targets) {
314
+ await this.findConceptMapsInAdditionalResources(conceptMaps, coding.system, sourceScope, null, target);
315
+ await this.provider.findConceptMapForTranslation(this.opContext, conceptMaps, coding.system, coding.version, sourceScope, null, target, coding.code);
316
+ }
317
+ }
318
+ }
292
319
  }
320
+ conceptMaps = this.uniqueConceptMaps(conceptMaps);
293
321
  }
294
322
 
295
323
  // Whether the map was nominated or found, it only applies if the code is within its own
@@ -385,6 +413,7 @@ class TranslateWorker extends TerminologyWorker {
385
413
 
386
414
  let conceptMaps = [];
387
415
  conceptMaps.push(conceptMap);
416
+ this.sourceDescription = await this.describeSourceSystem(coding, txp);
388
417
 
389
418
  // Naming the map by id does not put an out-of-scope code into its scope, any more than
390
419
  // naming it by url does: the map either covers this code or it does not.
@@ -1032,6 +1061,138 @@ class TranslateWorker extends TerminologyWorker {
1032
1061
  return inScope;
1033
1062
  }
1034
1063
 
1064
+ /**
1065
+ * The source's code system and version, for messages: system|version as given, or - when
1066
+ * no version was given - the system, and the version the server takes it to be (the default
1067
+ * edition, for SNOMED CT). Which version it is decides which maps apply.
1068
+ * @returns {Promise<string>} quoted (see quoteFrom)
1069
+ */
1070
+ async describeSourceSystem(coding, params) {
1071
+ if (!coding || !coding.system) {
1072
+ return null;
1073
+ }
1074
+ if (coding.version) {
1075
+ return `'${coding.system}|${coding.version}'`;
1076
+ }
1077
+ let version = null;
1078
+ try {
1079
+ const cs = await this.findCodeSystem(coding.system, '', params, ['complete', 'fragment'], null, true);
1080
+ version = cs ? cs.version() : null;
1081
+ } catch (e) {
1082
+ version = null;
1083
+ }
1084
+ return version
1085
+ ? `'${coding.system}' (no version given, so '${coding.system}|${version}')`
1086
+ : `'${coding.system}' (no version given)`;
1087
+ }
1088
+
1089
+ /**
1090
+ * The code systems a value set draws on, as system|version when the include names a version:
1091
+ * the targets to look for maps to when targetScope is given without targetSystem. Value sets
1092
+ * it imports are looked into too (once each).
1093
+ * @returns {Promise<string[]>}
1094
+ */
1095
+ async systemsOfScope(canonical, seen = new Set()) {
1096
+ if (seen.has(canonical)) {
1097
+ return [];
1098
+ }
1099
+ seen.add(canonical);
1100
+ const vs = await this.resolveScopeValueSet(canonical, 'targetScope parameter');
1101
+ const systems = new Set();
1102
+ const compose = vs.jsonObj?.compose;
1103
+ for (const inc of Array.isArray(compose?.include) ? compose.include : []) {
1104
+ if (inc.system) {
1105
+ systems.add(inc.version ? `${inc.system}|${inc.version}` : inc.system);
1106
+ }
1107
+ for (const imported of Array.isArray(inc.valueSet) ? inc.valueSet : []) {
1108
+ try {
1109
+ for (const s of await this.systemsOfScope(imported, seen)) {
1110
+ systems.add(s);
1111
+ }
1112
+ } catch (e) {
1113
+ // an import that can't be found doesn't stop the others: the membership check
1114
+ // (doTranslate) will report it, if it matters
1115
+ }
1116
+ }
1117
+ }
1118
+ if (!compose) {
1119
+ for (const c of Array.isArray(vs.jsonObj?.expansion?.contains) ? vs.jsonObj.expansion.contains : []) {
1120
+ if (c.system) {
1121
+ systems.add(c.version ? `${c.system}|${c.version}` : c.system);
1122
+ }
1123
+ }
1124
+ }
1125
+ return [...systems];
1126
+ }
1127
+
1128
+ /** The candidate maps, each once (the same map can be found for more than one target). */
1129
+ uniqueConceptMaps(conceptMaps) {
1130
+ const seen = new Set();
1131
+ const result = [];
1132
+ for (const cm of conceptMaps) {
1133
+ const key = cm.url ? `${cm.url}|${cm.version || ''}` : cm;
1134
+ if (!seen.has(key)) {
1135
+ seen.add(key);
1136
+ result.push(cm);
1137
+ }
1138
+ }
1139
+ return result;
1140
+ }
1141
+
1142
+ /**
1143
+ * targetScope "limits the scope of the $translate operation to target codes that are members
1144
+ * of this value set": a match whose target concept isn't in it is dropped, and a message says
1145
+ * so. A target that can't be checked against it is kept, with a note saying it wasn't checked.
1146
+ *
1147
+ * A target the code system produced itself can name a version the server doesn't hold: an
1148
+ * edition to edition translation names the release of the International Edition that the
1149
+ * national edition was built on, and the server may have another release, or none on its own.
1150
+ * Then it is checked as a code of the edition (the version without its date - whichever
1151
+ * release of it the server has, and what an include that names the edition means), with a
1152
+ * note saying so; and if the server doesn't have the edition either, it is kept, unchecked.
1153
+ */
1154
+ async holdToTargetScope(result, targetScope, params) {
1155
+ const dropped = [];
1156
+ for (let i = result.length - 1; i >= 0; i--) {
1157
+ const match = result[i];
1158
+ if (match.name !== 'match' || !Array.isArray(match.part)) {
1159
+ continue;
1160
+ }
1161
+ const concept = match.part.find(p => p.name === 'concept')?.valueCoding;
1162
+ if (!concept || !concept.system || !concept.code) {
1163
+ continue;
1164
+ }
1165
+ let coding = concept;
1166
+ if (concept.version && !(await this.resolveCode(concept.system, concept.version, concept.code, params)).known) {
1167
+ const edition = concept.version.includes('/version/') ? concept.version.substring(0, concept.version.indexOf('/version/')) : null;
1168
+ if (edition && (await this.resolveCode(concept.system, edition, concept.code, params)).known) {
1169
+ coding = { system: concept.system, version: edition, code: concept.code };
1170
+ match.part.push({ name: 'note', valueString: `Checked against the target scope '${targetScope}' as a code of '${edition}': '${concept.version}' is not available on this server` });
1171
+ } else {
1172
+ match.part.push({ name: 'note', valueString: `'${concept.system}#${concept.code}' could not be checked against the target scope '${targetScope}': '${concept.version}' is not available on this server` });
1173
+ continue;
1174
+ }
1175
+ }
1176
+ let inScope;
1177
+ try {
1178
+ inScope = await this.codeInScope(targetScope, coding, params, 'targetScope parameter');
1179
+ } catch (e) {
1180
+ match.part.push({ name: 'note', valueString: `'${concept.system}#${concept.code}' could not be checked against the target scope '${targetScope}' (${e.message})` });
1181
+ continue;
1182
+ }
1183
+ if (!inScope) {
1184
+ const key = `${concept.system}#${concept.code}`;
1185
+ if (!dropped.includes(key)) {
1186
+ dropped.unshift(key);
1187
+ }
1188
+ result.splice(i, 1);
1189
+ }
1190
+ }
1191
+ if (dropped.length > 0) {
1192
+ result.push({ name: 'message', valueString: `${dropped.length === 1 ? 'A translation was' : `${dropped.length} translations were`} left out because ${dropped.length === 1 ? 'it is' : 'they are'} not in the target scope '${targetScope}': ${dropped.join(', ')}` });
1193
+ }
1194
+ }
1195
+
1035
1196
  /**
1036
1197
  * The answer when nothing can translate this code: a successful operation with a negative
1037
1198
  * result, not an error. Where any candidate map declared a scope, the message names the code,
@@ -1041,11 +1202,11 @@ class TranslateWorker extends TerminologyWorker {
1041
1202
  noMapsResponse(coding, sourceScope, targetScope, targetSystem, reverse, message, anyScoped) {
1042
1203
  let text = message;
1043
1204
  if (!text) {
1044
- const from = reverse ? (targetSystem || sourceScope) : coding.system;
1205
+ const from = reverse ? (targetSystem || sourceScope) : (this.sourceDescription || coding.system);
1045
1206
  const to = reverse ? coding.system : (targetSystem || targetScope);
1046
1207
  text = anyScoped
1047
- ? `No ConceptMap that translates from '${from || '(unspecified)'}' to '${to || '(unspecified)'}' has '${coding.system}#${coding.code}' in scope`
1048
- : `No ConceptMap is available to translate from '${from || '(unspecified)'}' to '${to || '(unspecified)'}'`;
1208
+ ? `No ConceptMap that translates from ${quoteFrom(from)} to '${to || '(unspecified)'}' has '${coding.system}#${coding.code}' in scope`
1209
+ : `No ConceptMap is available to translate from ${quoteFrom(from)} to '${to || '(unspecified)'}'`;
1049
1210
  }
1050
1211
  return {
1051
1212
  resourceType: 'Parameters',
@@ -1128,6 +1289,9 @@ class TranslateWorker extends TerminologyWorker {
1128
1289
  // says - but it is not a translation: "The value can only be true if at least one
1129
1290
  // returned match has a relationship other than 'not-related-to'" (tx-ecosystem translate-2b)
1130
1291
  await this.checkTargetConcepts(result, params);
1292
+ if (targetScope && !reverse) {
1293
+ await this.holdToTargetScope(result, targetScope, params);
1294
+ }
1131
1295
  const translated = added && result.some(p => p.name === 'match' && this.isPositiveMatch(p));
1132
1296
  result.push({
1133
1297
  name: 'result',
@@ -1177,7 +1341,8 @@ class TranslateWorker extends TerminologyWorker {
1177
1341
  if (!system || !code) {
1178
1342
  return { known: false };
1179
1343
  }
1180
- this.deadCheck('translate-resolve');
1344
+ // called per element: a turn for the event loop now and then, so a client that has gone is seen
1345
+ await this.checkAndYield('translate-resolve');
1181
1346
  let cs;
1182
1347
  try {
1183
1348
  cs = await this.findCodeSystem(system, version || '', params, ['complete', 'fragment'], null, true);