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/tx.js CHANGED
@@ -14,7 +14,7 @@ const path = require('path');
14
14
  const fs = require('fs');
15
15
  const Logger = require('../library/logger');
16
16
  const { Library } = require('./library');
17
- const { OperationContext, ResourceCache, ExpansionCache, debugLog} = require('./operation-context');
17
+ const { OperationContext, ResourceCache, ExpansionCache, ExpansionCacheBudget, debugLog} = require('./operation-context');
18
18
  const { LanguageDefinitions } = require('../library/languages');
19
19
  const { I18nSupport } = require('../library/i18nsupport');
20
20
  const { CodeSystemXML } = require('./xml/codesystem-xml');
@@ -25,7 +25,7 @@ const packageJson = require("../package.json");
25
25
  // Import workers
26
26
  const ReadWorker = require('./workers/read');
27
27
  const SearchWorker = require('./workers/search');
28
- const { ExpandWorker, INTERNAL_DEFAULT_LIMIT, EXTERNAL_TEST_DEFAULT_LIMIT} = require('./workers/expand');
28
+ const { ExpandWorker, INTERNAL_DEFAULT_LIMIT, EXTERNAL_DEFAULT_LIMIT, WALK_LIMIT_FACTOR } = require('./workers/expand');
29
29
  const { ValidateWorker } = require('./workers/validate');
30
30
  const TranslateWorker = require('./workers/translate');
31
31
  const LookupWorker = require('./workers/lookup');
@@ -61,6 +61,7 @@ const { buildOperationOutcome, outcomeFromError } = require('./library/operation
61
61
  const txOpenApi = require('./openapi');
62
62
  const { SyndicationStore } = require('./syndication/syndication-store');
63
63
  const syndicationFeed = require('./syndication/syndication-feed');
64
+ const { EcosystemResolver, FORWARDED_HEADER } = require('./ecosystem-resolver');
64
65
  // const {writeFileSync} = require("fs");
65
66
 
66
67
  class TXModule {
@@ -138,6 +139,62 @@ class TXModule {
138
139
  }
139
140
  }
140
141
 
142
+ /**
143
+ * The memory limits in effect (see "Configuration Options" in tx/README.md): each from the tx setting
144
+ * of the same name, or its default when left out (or null); 0 = no limit (Infinity).
145
+ * @param {Object} config - Module configuration
146
+ * @returns {{cacheMaxConcepts: number, cacheMaxTotalConcepts: number, usageTrackerMaxCodes: number,
147
+ * usageTrackerMaxCodeLength: number, languageTagCacheSize: number}}
148
+ */
149
+ static resourceLimits(config) {
150
+ const limit = (name, dflt) => {
151
+ let n = Number(config[name] ?? dflt);
152
+ if (!Number.isFinite(n) || n < 0) {
153
+ n = dflt; // not a number: the default, not silently no limit
154
+ }
155
+ return n === 0 ? Infinity : n;
156
+ };
157
+ return {
158
+ cacheMaxConcepts: limit('cacheMaxConcepts', ResourceCache.MAX_CONCEPTS_PER_CACHE),
159
+ cacheMaxTotalConcepts: limit('cacheMaxTotalConcepts', ResourceCache.MAX_TOTAL_CONCEPTS),
160
+ usageTrackerMaxCodes: limit('usageTrackerMaxCodes', ConceptUsageTracker.MAX_CODES),
161
+ usageTrackerMaxCodeLength: limit('usageTrackerMaxCodeLength', ConceptUsageTracker.MAX_CODE_LENGTH),
162
+ languageTagCacheSize: limit('languageTagCacheSize', LanguageDefinitions.MAX_PARSED)
163
+ };
164
+ }
165
+
166
+ /**
167
+ * The expansion limits, from the configuration or the defaults: external (the most codes an
168
+ * $expand returns; externalLimit, default 10000), internal (the most an expansion builds, an
169
+ * imported value set's for one; internalLimit, default 100000) and walk (the furthest a paged
170
+ * $expand walks to reach its page; walkLimit, default 10 x internal). Each, when set, must be a
171
+ * positive whole number, and external <= internal <= walk: anything else stops startup
172
+ * @returns {{external: number, internal: number, walk: number}}
173
+ */
174
+ static expansionLimits(config = {}) {
175
+ const read = (name, dflt) => {
176
+ const v = config[name];
177
+ if (v === undefined || v === null) {
178
+ return dflt;
179
+ }
180
+ const n = typeof v === 'number' || (typeof v === 'string' && /^\s*\d+\s*$/.test(v)) ? Number(v) : NaN;
181
+ if (!Number.isInteger(n) || n <= 0) {
182
+ throw new Error(`modules.tx.${name} must be a whole number above 0, not '${v}'`);
183
+ }
184
+ return n;
185
+ };
186
+ const internal = read('internalLimit', INTERNAL_DEFAULT_LIMIT);
187
+ const external = read('externalLimit', Math.min(EXTERNAL_DEFAULT_LIMIT, internal));
188
+ const walk = read('walkLimit', internal * WALK_LIMIT_FACTOR);
189
+ if (external > internal) {
190
+ throw new Error(`modules.tx.externalLimit (${external}) can't be more than internalLimit (${internal}): an expansion can't return more codes than it builds`);
191
+ }
192
+ if (internal > walk) {
193
+ throw new Error(`modules.tx.walkLimit (${walk}) can't be less than internalLimit (${internal})`);
194
+ }
195
+ return { external, internal, walk };
196
+ }
197
+
141
198
  /**
142
199
  * Initialize the TX module
143
200
  * @param {Object} config - Module configuration
@@ -156,9 +213,15 @@ class TXModule {
156
213
  consoleErrors: config.consoleErrors,
157
214
  telnetErrors: config.telnetErrors
158
215
  });
159
- this.usageTracker = new ConceptUsageTracker();
216
+ this.limits = TXModule.resourceLimits(config);
217
+ this.expansionLimitValues = TXModule.expansionLimits(config);
218
+ this.usageTracker = new ConceptUsageTracker(this.limits.usageTrackerMaxCodes, this.limits.usageTrackerMaxCodeLength);
160
219
 
161
220
  this.log.info('Initializing TX module');
221
+ this.log.info(`Limits: operationTimeLimit=${config.operationTimeLimit ?? 20}s, cacheTimeout=${config.cacheTimeout || 30}min, ` +
222
+ Object.entries(this.limits).map(([name, value]) => `${name}=${value === Infinity ? 'unlimited' : value}`).join(', '));
223
+ this.log.info(`Expansion limits: externalLimit=${this.expansionLimitValues.external}, internalLimit=${this.expansionLimitValues.internal}, ` +
224
+ `walkLimit=${this.expansionLimitValues.walk}`);
162
225
 
163
226
  // Optional instance code, prefixed onto every cache-id this server issues so a
164
227
  // proxy in front of several instances can route by it (see nginx.md). Checked
@@ -188,6 +251,7 @@ class TXModule {
188
251
  const langPath = path.join(__dirname, 'data');
189
252
  this.log.info(`Loading language definitions from: ${langPath}`);
190
253
  this.languages = await LanguageDefinitions.fromFiles(langPath);
254
+ this.languages.maxParsed = this.limits.languageTagCacheSize;
191
255
  this.log.info('Language definitions loaded');
192
256
 
193
257
  // Initialize i18n support
@@ -202,12 +266,19 @@ class TXModule {
202
266
  // the database can't be opened, that stops startup - silently running without it
203
267
  // would be worse.
204
268
  this.closureStore = null;
205
- if (config.closure && config.closure.enabled) {
206
- this.closureStore = new ClosureStore(config.closure, this.log);
269
+ // a tx-worker (cluster.md) opens neither the closure tables nor the syndication store:
270
+ // the master owns them, and $closure and the feed are routed to it - unless
271
+ // server.cluster.sharedClosure is on (set only with a role), when every process
272
+ // answers $closure on the one database (see closure-store.js)
273
+ const worker = config.clusterRole === 'tx-worker';
274
+ const sharedClosure = config.sharedClosure === true;
275
+ if (config.closure && config.closure.enabled && (!worker || sharedClosure)) {
276
+ this.closureStore = new ClosureStore(config.closure, this.log, { shared: sharedClosure });
207
277
  this.closureStore.open();
208
- this.log.info(`$closure enabled: tables in ${this.closureStore.path}`);
278
+ this.log.info(`$closure enabled: tables in ${this.closureStore.path}${sharedClosure ? ', shared with the other processes' : ''}`);
209
279
  const days = config.closure.retentionDays;
210
- if (days) {
280
+ // with the tables shared, the master alone drops the stale ones
281
+ if (days && !worker) {
211
282
  const prune = async () => {
212
283
  try {
213
284
  const dropped = await this.closureStore.pruneUnused(days);
@@ -225,7 +296,7 @@ class TXModule {
225
296
 
226
297
  // Initialize metadata handler with config
227
298
  this.metadataHandler = new MetadataHandler({
228
- closure: this.closureStore != null,
299
+ closure: this.closureStore != null || (worker && !!(config.closure && config.closure.enabled)),
229
300
  baseUrl: config.baseUrl,
230
301
  serverVersion: packageJson.version,
231
302
  txVersion: packageJson.txVersion,
@@ -243,14 +314,31 @@ class TXModule {
243
314
 
244
315
  // Load the library from YAML
245
316
  this.log.info(`Loading library from: ${config.librarySource}`);
246
- this.library = new Library(config.librarySource, config.vsacCfg, this.log, this.stats);
317
+ this.library = new Library(config.librarySource, config.vsacCfg, this.log, this.stats, config.snomed);
318
+ if (config.pruneTerminologyCache !== undefined && typeof config.pruneTerminologyCache !== 'boolean') {
319
+ throw new Error(`modules.tx.pruneTerminologyCache must be true or false, not '${config.pruneTerminologyCache}'`);
320
+ }
321
+ // delete the .db and .cache files and package folders in terminology-cache that aren't used
322
+ // (once the endpoints are set up: see below)
323
+ this.library.pruneTerminologyCacheFiles = config.pruneTerminologyCache === true;
324
+ if (config.clusterRole) {
325
+ // the folder:/file: source caches are the master's, under one name every process shares
326
+ this.library.localSourceTag = 'master';
327
+ // the master builds the package caches; a worker only reads them
328
+ this.library.packageCachesReadOnly = config.clusterRole === 'tx-worker';
329
+ // the master refreshes the IANA media types registry; a worker reads its copy
330
+ this.library.mimeTypesReadOnly = config.clusterRole === 'tx-worker';
331
+ // the LOINC, RxNorm, NDC, UNII, CPT and OMOP databases are the master's; a worker opens
332
+ // them read-only
333
+ this.library.terminologyDatabasesReadOnly = config.clusterRole === 'tx-worker';
334
+ }
247
335
 
248
336
  // The syndication feed: content is recorded in the store as the library loads it, from
249
337
  // the time the administrator turns it on (see tx/syndication). One store serves every
250
338
  // endpoint, since the library is shared. If it is turned on and the store can't be
251
339
  // opened, that stops startup.
252
340
  this.syndicationStore = null;
253
- if (config.syndication && config.syndication.enabled) {
341
+ if (config.syndication && config.syndication.enabled && !worker) {
254
342
  this.syndicationStore = new SyndicationStore(path.join(this.library.cacheFolder, 'syndication'), this.log).open();
255
343
  this.library.syndication = this.syndicationStore;
256
344
  this.log.info(`Syndication feed enabled: store in ${this.syndicationStore.folder}`);
@@ -263,6 +351,9 @@ class TXModule {
263
351
  for (const endpoint of config.endpoints) {
264
352
  await this.setupEndpoint(endpoint, app);
265
353
  }
354
+ // everything the library and the endpoints use is loaded: with pruneTerminologyCache, the
355
+ // rest of terminology-cache can go
356
+ await this.library.pruneUnusedCaches();
266
357
 
267
358
  this.log.info(`TX module initialized with ${config.endpoints.length} endpoint(s)`);
268
359
 
@@ -300,17 +391,38 @@ class TXModule {
300
391
  const cacheTimeoutMinutes = this.config.cacheTimeout || 30;
301
392
  const expansionCacheSize = this.config.expansionCacheSize || 1000;
302
393
  const expansionCacheMemoryThreshold = this.config.expansionCacheMemoryThreshold || 0;
394
+ // one budget of codes for the expansion caches of all the endpoints
395
+ if (!this.expansionCacheBudget) {
396
+ this.expansionCacheBudget = new ExpansionCacheBudget(this.config.expansionCacheMaxCodes || 0);
397
+ this.log.info(`Expansion caches: at most ${this.expansionCacheBudget.maxCodes} codes in all` +
398
+ (this.config.expansionCacheMaxCodes ? '' : ' (a quarter of the heap limit, at 1 KB a code)'));
399
+ }
303
400
 
304
401
  // Store endpoint info for provider creation
305
402
  const endpointInfo = {
306
403
  path: endpointPath,
307
404
  fhirVersion,
308
405
  context: context || null,
309
- resourceCache: new ResourceCache(this.stats),
310
- expansionCache: new ExpansionCache(this.stats, expansionCacheSize, expansionCacheMemoryThreshold)
406
+ // server.cluster.sharedCache (cluster.md, "Options"; set only with a role): one SQLite
407
+ // file that every process shares. Otherwise this process's memory, as ever
408
+ resourceCache: this.config.sharedCache ? this.createSharedResourceCache(endpointPath) : new ResourceCache(this.stats),
409
+ expansionCache: new ExpansionCache(this.stats, expansionCacheSize, expansionCacheMemoryThreshold, this.expansionCacheBudget)
311
410
  };
312
411
  // Create the provider once for this endpoint
313
412
  endpointInfo.provider = await this.library.cloneWithFhirVersion(fhirVersion, context, endpointPath);
413
+ for (const old of ['ecosystemResolution', 'ecosystemExcludedServers']) {
414
+ if (this.config[old] !== undefined) {
415
+ throw new Error(`modules.tx.${old} has been replaced by modules.tx.ecosystem (see tx/README.md)`);
416
+ }
417
+ }
418
+ const ecosystem = EcosystemResolver.checkConfig(this.config.ecosystem);
419
+ if (ecosystem) {
420
+ if (!this.ecosystemResolver) {
421
+ this.ecosystemResolver = new EcosystemResolver(ecosystem);
422
+ this.log.info(`Code systems not held here are looked for in the ecosystem (${this.ecosystemResolver.registry})`);
423
+ }
424
+ endpointInfo.provider.ecosystem = this.ecosystemResolver;
425
+ }
314
426
 
315
427
  // Set up periodic pruning of the resource cache
316
428
  // cacheTimeout is in minutes, default to 30 minutes
@@ -321,6 +433,7 @@ class TXModule {
321
433
  // than guessing at a number it has no way to see.
322
434
  endpointInfo.resourceCache.setIdleTimeout(cacheTimeoutMs);
323
435
  endpointInfo.resourceCache.setInstanceCode(this.config.instanceCode);
436
+ endpointInfo.resourceCache.setLimits(this.limits.cacheMaxConcepts, this.limits.cacheMaxTotalConcepts);
324
437
  if (this.stats) {
325
438
  this.stats.addTask("Client Cache", "5 min");
326
439
  }
@@ -382,6 +495,9 @@ class TXModule {
382
495
  // others read req.body / query directly). An explicit cache-id parameter
383
496
  // on the request still wins (see setupAdditionalResources).
384
497
  opContext.cacheId = req.get('X-Cache-Id') || null;
498
+ // a request another server sent on while resolving a code system through the ecosystem
499
+ // isn't resolved through the ecosystem again (see EcosystemResolver)
500
+ opContext.ecosystemForwarded = !!req.get(FORWARDED_HEADER);
385
501
 
386
502
  // Attach everything to request
387
503
  req.txProvider = endpointInfo.provider;
@@ -465,7 +581,8 @@ class TXModule {
465
581
  res.setHeader('Content-Type', jsonFmt);
466
582
  this.checkProperJson(jsonStr);
467
583
  responseSize = Buffer.byteLength(jsonStr, 'utf8');
468
- result = originalJson(data);
584
+ // send the string built above: res.json(data) would serialise the resource again
585
+ result = res.send(jsonStr);
469
586
  }
470
587
 
471
588
  // Log the request with request ID
@@ -494,6 +611,12 @@ class TXModule {
494
611
  router.use((req, res, next) => {
495
612
  const contentType = req.get('Content-Type') || '';
496
613
 
614
+ // The body was bigger than the server accepts (server.maxBodyMB; see below)
615
+ if (req.bodyTooLarge) {
616
+ return res.status(413).json(buildOperationOutcome('error', 'too-costly',
617
+ `The request body is bigger than this server accepts (${req.bodyTooLarge.limit} bytes)`));
618
+ }
619
+
497
620
  // Only process POST/PUT
498
621
  if (req.method !== 'POST' && req.method !== 'PUT') {
499
622
  return next();
@@ -568,6 +691,17 @@ class TXModule {
568
691
  next();
569
692
  });
570
693
 
694
+ // A body over the size limit is refused by the body parsers, which run before this
695
+ // router, and Express would answer with its HTML error page. Note it and carry on, so
696
+ // the router answers with an OperationOutcome, in the format the client asked for
697
+ app.use(endpointPath, (err, req, res, next) => {
698
+ if (err.type !== 'entity.too.large') {
699
+ return next(err);
700
+ }
701
+ req.bodyTooLarge = err;
702
+ next();
703
+ });
704
+
571
705
  // Register the router with the app
572
706
  app.use(endpointPath, router);
573
707
  this.routers.set(endpointPath, router);
@@ -645,7 +779,7 @@ class TXModule {
645
779
  let worker = new LookupWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
646
780
  await worker.handle(req, res);
647
781
  } finally {
648
- this.countRequest(endpointPath, '$lookup', Date.now() - start);
782
+ this.countRequest(endpointPath, '$lookup', Date.now() - start, req);
649
783
  }
650
784
  });
651
785
  router.post('/CodeSystem/\\$lookup', async (req, res) => {
@@ -654,7 +788,7 @@ class TXModule {
654
788
  let worker = new LookupWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
655
789
  await worker.handle(req, res);
656
790
  } finally {
657
- this.countRequest(endpointPath, '$lookup', Date.now() - start);
791
+ this.countRequest(endpointPath, '$lookup', Date.now() - start, req);
658
792
  }
659
793
  });
660
794
 
@@ -665,7 +799,7 @@ class TXModule {
665
799
  let worker = new SubsumesWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
666
800
  await worker.handle(req, res);
667
801
  } finally {
668
- this.countRequest(endpointPath, '$subsumes', Date.now() - start);
802
+ this.countRequest(endpointPath, '$subsumes', Date.now() - start, req);
669
803
  }
670
804
  });
671
805
  router.post('/CodeSystem/\\$subsumes', async (req, res) => {
@@ -674,7 +808,7 @@ class TXModule {
674
808
  let worker = new SubsumesWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
675
809
  await worker.handle(req, res);
676
810
  } finally {
677
- this.countRequest(endpointPath, '$subsumes', Date.now() - start);
811
+ this.countRequest(endpointPath, '$subsumes', Date.now() - start, req);
678
812
  }
679
813
  });
680
814
 
@@ -685,7 +819,7 @@ class TXModule {
685
819
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
686
820
  await worker.handleCodeSystem(req, res);
687
821
  } finally {
688
- this.countRequest(endpointPath, '$validate', Date.now() - start);
822
+ this.countRequest(endpointPath, '$validate', Date.now() - start, req);
689
823
  }
690
824
  });
691
825
  router.post('/CodeSystem/\\$validate-code', async (req, res) => {
@@ -694,7 +828,7 @@ class TXModule {
694
828
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
695
829
  await worker.handleCodeSystem(req, res);
696
830
  } finally {
697
- this.countRequest(endpointPath, '$validate', Date.now() - start);
831
+ this.countRequest(endpointPath, '$validate', Date.now() - start, req);
698
832
  }
699
833
  });
700
834
 
@@ -705,7 +839,7 @@ class TXModule {
705
839
  let worker = new BatchValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
706
840
  await worker.handleCodeSystem(req, res);
707
841
  } finally {
708
- this.countRequest(endpointPath, '$batch', Date.now() - start);
842
+ this.countRequest(endpointPath, '$batch', Date.now() - start, req);
709
843
  }
710
844
  });
711
845
  router.post('/CodeSystem/\\$batch-validate-code', async (req, res) => {
@@ -714,7 +848,7 @@ class TXModule {
714
848
  let worker = new BatchValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
715
849
  await worker.handleCodeSystem(req, res);
716
850
  } finally {
717
- this.countRequest(endpointPath, '$batch', Date.now() - start);
851
+ this.countRequest(endpointPath, '$batch', Date.now() - start, req);
718
852
  }
719
853
  });
720
854
  // ValueSet/$validate-code (GET and POST)
@@ -724,7 +858,7 @@ class TXModule {
724
858
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
725
859
  await worker.handleValueSet(req, res);
726
860
  } finally {
727
- this.countRequest(endpointPath, '$validate', Date.now() - start);
861
+ this.countRequest(endpointPath, '$validate', Date.now() - start, req);
728
862
  }
729
863
  });
730
864
  router.post('/ValueSet/\\$validate-code', async (req, res) => {
@@ -733,7 +867,7 @@ class TXModule {
733
867
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
734
868
  await worker.handleValueSet(req, res);
735
869
  } finally {
736
- this.countRequest(endpointPath, '$validate', Date.now() - start);
870
+ this.countRequest(endpointPath, '$validate', Date.now() - start, req);
737
871
  }
738
872
  });
739
873
 
@@ -744,7 +878,7 @@ class TXModule {
744
878
  let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
745
879
  await worker.handle(req, res);
746
880
  } finally {
747
- this.countRequest(endpointPath, '$compare', Date.now() - start);
881
+ this.countRequest(endpointPath, '$compare', Date.now() - start, req);
748
882
  }
749
883
  });
750
884
  router.post('/ValueSet/\\$compare', async (req, res) => {
@@ -753,7 +887,7 @@ class TXModule {
753
887
  let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
754
888
  await worker.handle(req, res);
755
889
  } finally {
756
- this.countRequest(endpointPath, '$compare', Date.now() - start);
890
+ this.countRequest(endpointPath, '$compare', Date.now() - start, req);
757
891
  }
758
892
  });
759
893
 
@@ -764,7 +898,7 @@ class TXModule {
764
898
  let worker = new BatchValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
765
899
  await worker.handleValueSet(req, res);
766
900
  } finally {
767
- this.countRequest(endpointPath, '$batch', Date.now() - start);
901
+ this.countRequest(endpointPath, '$batch', Date.now() - start, req);
768
902
  }
769
903
  });
770
904
  router.post('/ValueSet/\\$batch-validate-code', async (req, res) => {
@@ -773,7 +907,7 @@ class TXModule {
773
907
  let worker = new BatchValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
774
908
  await worker.handleValueSet(req, res);
775
909
  } finally {
776
- this.countRequest(endpointPath, 'validate', Date.now() - start);
910
+ this.countRequest(endpointPath, 'validate', Date.now() - start, req);
777
911
  }
778
912
  });
779
913
 
@@ -781,19 +915,19 @@ class TXModule {
781
915
  router.get('/ValueSet/\\$expand', async (req, res) => {
782
916
  const start = Date.now();
783
917
  try {
784
- let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
918
+ let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req), this.walkLimit(req));
785
919
  await worker.handle(req, res, this.log);
786
920
  } finally {
787
- this.countRequest(endpointPath, '$expand', Date.now() - start);
921
+ this.countRequest(endpointPath, '$expand', Date.now() - start, req);
788
922
  }
789
923
  });
790
924
  router.post('/ValueSet/\\$expand', async (req, res) => {
791
925
  const start = Date.now();
792
926
  try {
793
- let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
927
+ let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req), this.walkLimit(req));
794
928
  await worker.handle(req, res, this.log);
795
929
  } finally {
796
- this.countRequest(endpointPath, '$expand', Date.now() - start);
930
+ this.countRequest(endpointPath, '$expand', Date.now() - start, req);
797
931
  }
798
932
  });
799
933
 
@@ -804,7 +938,7 @@ class TXModule {
804
938
  let worker = new CacheControlWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
805
939
  await worker.handle(req, res, this.log);
806
940
  } finally {
807
- this.countRequest(endpointPath, '$cache-control', Date.now() - start);
941
+ this.countRequest(endpointPath, '$cache-control', Date.now() - start, req);
808
942
  }
809
943
  });
810
944
  router.post('/\\$cache-control', async (req, res) => {
@@ -813,7 +947,7 @@ class TXModule {
813
947
  let worker = new CacheControlWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
814
948
  await worker.handle(req, res, this.log);
815
949
  } finally {
816
- this.countRequest(endpointPath, '$cache-control', Date.now() - start);
950
+ this.countRequest(endpointPath, '$cache-control', Date.now() - start, req);
817
951
  }
818
952
  });
819
953
 
@@ -824,7 +958,7 @@ class TXModule {
824
958
  let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
825
959
  await worker.handle(req, res, this.log);
826
960
  } finally {
827
- this.countRequest(endpointPath, '$translate', Date.now() - start);
961
+ this.countRequest(endpointPath, '$translate', Date.now() - start, req);
828
962
  }
829
963
  });
830
964
  router.post('/ConceptMap/\\$translate', async (req, res) => {
@@ -833,7 +967,7 @@ class TXModule {
833
967
  let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
834
968
  await worker.handle(req, res, this.log);
835
969
  } finally {
836
- this.countRequest(endpointPath, '$translate', Date.now() - start);
970
+ this.countRequest(endpointPath, '$translate', Date.now() - start, req);
837
971
  }
838
972
  });
839
973
 
@@ -845,7 +979,7 @@ class TXModule {
845
979
  let worker = new ClosureWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.closureStore, this.config.closure);
846
980
  await worker.handle(req, res, this.log);
847
981
  } finally {
848
- this.countRequest(endpointPath, '$closure', Date.now() - start);
982
+ this.countRequest(endpointPath, '$closure', Date.now() - start, req);
849
983
  }
850
984
  });
851
985
  router.post('/\\$closure', async (req, res) => {
@@ -854,7 +988,7 @@ class TXModule {
854
988
  let worker = new ClosureWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.closureStore, this.config.closure);
855
989
  await worker.handle(req, res, this.log);
856
990
  } finally {
857
- this.countRequest(endpointPath, '$closure', Date.now() - start);
991
+ this.countRequest(endpointPath, '$closure', Date.now() - start, req);
858
992
  }
859
993
  });
860
994
 
@@ -867,7 +1001,7 @@ class TXModule {
867
1001
  let worker = new LookupWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
868
1002
  await worker.handleInstance(req, res);
869
1003
  } finally {
870
- this.countRequest(endpointPath, '$lookup', Date.now() - start);
1004
+ this.countRequest(endpointPath, '$lookup', Date.now() - start, req);
871
1005
  }
872
1006
  });
873
1007
  router.post('/CodeSystem/:id/\\$lookup', async (req, res) => {
@@ -876,7 +1010,7 @@ class TXModule {
876
1010
  let worker = new LookupWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
877
1011
  await worker.handleInstance(req, res);
878
1012
  } finally {
879
- this.countRequest(endpointPath, '$lookup', Date.now() - start);
1013
+ this.countRequest(endpointPath, '$lookup', Date.now() - start, req);
880
1014
  }
881
1015
  });
882
1016
 
@@ -887,7 +1021,7 @@ class TXModule {
887
1021
  let worker = new SubsumesWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
888
1022
  await worker.handleInstance(req, res);
889
1023
  } finally {
890
- this.countRequest(endpointPath, '$subsumes', Date.now() - start);
1024
+ this.countRequest(endpointPath, '$subsumes', Date.now() - start, req);
891
1025
  }
892
1026
  });
893
1027
  router.post('/CodeSystem/:id/\\$subsumes', async (req, res) => {
@@ -896,7 +1030,7 @@ class TXModule {
896
1030
  let worker = new SubsumesWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
897
1031
  await worker.handleInstance(req, res);
898
1032
  } finally {
899
- this.countRequest(endpointPath, '$subsumes', Date.now() - start);
1033
+ this.countRequest(endpointPath, '$subsumes', Date.now() - start, req);
900
1034
  }
901
1035
  });
902
1036
 
@@ -907,7 +1041,7 @@ class TXModule {
907
1041
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
908
1042
  await worker.handleCodeSystemInstance(req, res, this.log);
909
1043
  } finally {
910
- this.countRequest(endpointPath, '$validate', Date.now() - start);
1044
+ this.countRequest(endpointPath, '$validate', Date.now() - start, req);
911
1045
  }
912
1046
  });
913
1047
  router.post('/CodeSystem/:id/\\$validate-code', async (req, res) => {
@@ -916,7 +1050,7 @@ class TXModule {
916
1050
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
917
1051
  await worker.handleCodeSystemInstance(req, res, this.log);
918
1052
  } finally {
919
- this.countRequest(endpointPath, '$validate', Date.now() - start);
1053
+ this.countRequest(endpointPath, '$validate', Date.now() - start, req);
920
1054
  }
921
1055
 
922
1056
  });
@@ -928,7 +1062,7 @@ class TXModule {
928
1062
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
929
1063
  await worker.handleValueSetInstance(req, res, this.log);
930
1064
  } finally {
931
- this.countRequest(endpointPath, '$validate', Date.now() - start);
1065
+ this.countRequest(endpointPath, '$validate', Date.now() - start, req);
932
1066
  }
933
1067
  });
934
1068
  router.post('/ValueSet/:id/\\$validate-code', async (req, res) => {
@@ -937,7 +1071,7 @@ class TXModule {
937
1071
  let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
938
1072
  await worker.handleValueSetInstance(req, res, this.log);
939
1073
  } finally {
940
- this.countRequest(endpointPath, '$validate', Date.now() - start);
1074
+ this.countRequest(endpointPath, '$validate', Date.now() - start, req);
941
1075
  }
942
1076
  });
943
1077
 
@@ -949,7 +1083,7 @@ class TXModule {
949
1083
  let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
950
1084
  await worker.handleInstance(req, res, this.log);
951
1085
  } finally {
952
- this.countRequest(endpointPath, '$compare', Date.now() - start);
1086
+ this.countRequest(endpointPath, '$compare', Date.now() - start, req);
953
1087
  }
954
1088
  });
955
1089
  router.post('/ValueSet/:id/\\$compare', async (req, res) => {
@@ -958,7 +1092,7 @@ class TXModule {
958
1092
  let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
959
1093
  await worker.handleInstance(req, res, this.log);
960
1094
  } finally {
961
- this.countRequest(endpointPath, '$compare', Date.now() - start);
1095
+ this.countRequest(endpointPath, '$compare', Date.now() - start, req);
962
1096
  }
963
1097
  });
964
1098
 
@@ -966,19 +1100,19 @@ class TXModule {
966
1100
  router.get('/ValueSet/:id/\\$expand', async (req, res) => {
967
1101
  const start = Date.now();
968
1102
  try {
969
- let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
1103
+ let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req), this.walkLimit(req));
970
1104
  await worker.handleInstance(req, res, this.log);
971
1105
  } finally {
972
- this.countRequest(endpointPath, '$expand', Date.now() - start);
1106
+ this.countRequest(endpointPath, '$expand', Date.now() - start, req);
973
1107
  }
974
1108
  });
975
1109
  router.post('/ValueSet/:id/\\$expand', async (req, res) => {
976
1110
  const start = Date.now();
977
1111
  try {
978
- let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
1112
+ let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req), this.walkLimit(req));
979
1113
  await worker.handleInstance(req, res, this.log);
980
1114
  } finally {
981
- this.countRequest(endpointPath, '$expand', Date.now() - start);
1115
+ this.countRequest(endpointPath, '$expand', Date.now() - start, req);
982
1116
  }
983
1117
  });
984
1118
 
@@ -989,7 +1123,7 @@ class TXModule {
989
1123
  let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
990
1124
  await worker.handleInstance(req, res, this.log);
991
1125
  } finally {
992
- this.countRequest(endpointPath, '$translate', Date.now() - start);
1126
+ this.countRequest(endpointPath, '$translate', Date.now() - start, req);
993
1127
  }
994
1128
  });
995
1129
  router.post('/ConceptMap/:id/\\$translate', async (req, res) => {
@@ -998,7 +1132,7 @@ class TXModule {
998
1132
  let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
999
1133
  await worker.handleInstance(req, res, this.log);
1000
1134
  } finally {
1001
- this.countRequest(endpointPath, '$translate', Date.now() - start);
1135
+ this.countRequest(endpointPath, '$translate', Date.now() - start, req);
1002
1136
  }
1003
1137
  });
1004
1138
 
@@ -1020,7 +1154,7 @@ class TXModule {
1020
1154
  let worker = new ReadWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
1021
1155
  await worker.handle(req, res, resourceType, (req, res, resourceType, id, matches) => this.renderIdChoices(req, res, resourceType, id, matches));
1022
1156
  } finally {
1023
- this.countRequest(endpointPath, 'read', Date.now() - start);
1157
+ this.countRequest(endpointPath, 'read', Date.now() - start, req);
1024
1158
  }
1025
1159
  });
1026
1160
  }
@@ -1033,7 +1167,7 @@ class TXModule {
1033
1167
  let worker = new SearchWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
1034
1168
  await worker.handle(req, res, resourceType);
1035
1169
  } finally {
1036
- this.countRequest(endpointPath, 'search', Date.now() - start);
1170
+ this.countRequest(endpointPath, 'search', Date.now() - start, req);
1037
1171
  }
1038
1172
  });
1039
1173
  router.post(`/${resourceType}/_search`, async (req, res) => {
@@ -1042,7 +1176,7 @@ class TXModule {
1042
1176
  let worker = new SearchWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
1043
1177
  await worker.handle(req, res, resourceType);
1044
1178
  } finally {
1045
- this.countRequest(endpointPath, 'search', Date.now() - start);
1179
+ this.countRequest(endpointPath, 'search', Date.now() - start, req);
1046
1180
  }
1047
1181
  });
1048
1182
  }
@@ -1068,7 +1202,7 @@ class TXModule {
1068
1202
  // anything else falls through to the not-found fallback rather than hanging
1069
1203
  return next();
1070
1204
  } finally {
1071
- this.countRequest(endpointPath, '$read', Date.now() - start);
1205
+ this.countRequest(endpointPath, '$read', Date.now() - start, req);
1072
1206
  }
1073
1207
  });
1074
1208
  }
@@ -1079,7 +1213,7 @@ class TXModule {
1079
1213
  let worker = new OperationsWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
1080
1214
  await worker.handle(req, res);
1081
1215
  } finally {
1082
- this.countRequest(endpointPath, '$op', Date.now() - start);
1216
+ this.countRequest(endpointPath, '$op', Date.now() - start, req);
1083
1217
  }
1084
1218
  });
1085
1219
 
@@ -1095,7 +1229,7 @@ class TXModule {
1095
1229
  res.setHeader('Content-Type', 'text/html');
1096
1230
  res.send(html);
1097
1231
  } finally {
1098
- this.countRequest(endpointPath, 'ecl', Date.now() - start);
1232
+ this.countRequest(endpointPath, 'ecl', Date.now() - start, req);
1099
1233
  }
1100
1234
  });
1101
1235
 
@@ -1109,7 +1243,7 @@ class TXModule {
1109
1243
  res.setHeader('Content-Type', 'text/html');
1110
1244
  res.send(html);
1111
1245
  } finally {
1112
- this.countRequest(endpointPath, 'problems', Date.now() - start);
1246
+ this.countRequest(endpointPath, 'problems', Date.now() - start, req);
1113
1247
  }
1114
1248
  });
1115
1249
 
@@ -1147,7 +1281,7 @@ class TXModule {
1147
1281
  res.send(source);
1148
1282
  }
1149
1283
  } finally {
1150
- this.countRequest(endpointPath, 'library', Date.now() - start);
1284
+ this.countRequest(endpointPath, 'library', Date.now() - start, req);
1151
1285
  }
1152
1286
  });
1153
1287
  }
@@ -1167,7 +1301,7 @@ class TXModule {
1167
1301
  res.status(500).json(this.operationOutcome('error', 'exception', error.message));
1168
1302
  }
1169
1303
  } finally {
1170
- this.countRequest(endpointPath, 'metadata', Date.now() - start);
1304
+ this.countRequest(endpointPath, 'metadata', Date.now() - start, req);
1171
1305
  }
1172
1306
  });
1173
1307
 
@@ -1182,7 +1316,7 @@ class TXModule {
1182
1316
  res.status(500).json(this.operationOutcome('error', 'exception', error.message));
1183
1317
  }
1184
1318
  } finally {
1185
- this.countRequest(endpointPath, '$versions', Date.now() - start);
1319
+ this.countRequest(endpointPath, '$versions', Date.now() - start, req);
1186
1320
  }
1187
1321
  });
1188
1322
 
@@ -1192,7 +1326,7 @@ class TXModule {
1192
1326
  try {
1193
1327
  await res.json(buildOperationOutcome('information', 'informational', `FHIR Terminology Server - FHIR v${req.txEndpoint.fhirVersion}`));
1194
1328
  } finally {
1195
- this.countRequest(endpointPath, 'home', Date.now() - start);
1329
+ this.countRequest(endpointPath, 'home', Date.now() - start, req);
1196
1330
  }
1197
1331
  });
1198
1332
 
@@ -1217,7 +1351,7 @@ class TXModule {
1217
1351
  this.log.error(`Error rendering info page for ${req.params.id}: ${error.message}`);
1218
1352
  res.status(500).send('Internal server error');
1219
1353
  } finally {
1220
- this.countRequest(endpointPath, 'info', Date.now() - start);
1354
+ this.countRequest(endpointPath, 'info', Date.now() - start, req);
1221
1355
  }
1222
1356
  };
1223
1357
  router.get('/info/:id', infoHandler);
@@ -1236,7 +1370,7 @@ class TXModule {
1236
1370
  res.status(404).json(this.operationOutcome('error', 'not-found',
1237
1371
  `Unknown ${what} on this server: ${req.method} ${req.baseUrl}${req.path}`, 'not-found'));
1238
1372
  } finally {
1239
- this.countRequest(endpointPath, 'unknown', Date.now() - start);
1373
+ this.countRequest(endpointPath, 'unknown', Date.now() - start, req);
1240
1374
  }
1241
1375
  });
1242
1376
 
@@ -1380,6 +1514,39 @@ class TXModule {
1380
1514
  return `${counts.total.toLocaleString('en-US')} requests were served${where}: ${list}.`;
1381
1515
  }
1382
1516
 
1517
+ /**
1518
+ * An endpoint's $cache-control cache in the shared SQLite file (databases/resource-cache.db),
1519
+ * for server.cluster.sharedCache. Endpoints share the file but not caches.
1520
+ * @param {string} endpointPath - e.g. /r4
1521
+ * @returns {SqliteResourceCache}
1522
+ */
1523
+ createSharedResourceCache(endpointPath) {
1524
+ const { SqliteResourceCache } = require('./resource-cache-sqlite');
1525
+ const { newCacheGeneration } = require('../library/cache-generation');
1526
+ const folders = require('../library/folder-setup');
1527
+ const file = path.join(folders.databasesDir(), 'resource-cache.db');
1528
+ // from server.cluster.sharedCacheMaxMB, by applyRole (library/role.js, where the default
1529
+ // is); 0: no cap
1530
+ const maxBytes = this.config.sharedCacheMaxBytes ?? 0;
1531
+ if (this.sharedCacheGeneration === undefined) {
1532
+ // Caches don't survive a server restart: each time the master starts (under the
1533
+ // launcher or by hand) it begins a new generation, and every live cache becomes a
1534
+ // "lost to a server restart" tombstone. A tx-worker joins the current generation,
1535
+ // so one restarting while the master carries on keeps every cache. One token for
1536
+ // all of this process's endpoints
1537
+ this.sharedCacheGeneration = this.config.clusterRole === 'master' ? newCacheGeneration() : null;
1538
+ }
1539
+ this.log.info(`Resource cache for ${endpointPath}: shared with the other processes, in ${file}, ` +
1540
+ (maxBytes ? `client resources capped at ${+(maxBytes / 2 ** 20).toFixed(1)} MB for every endpoint together` : 'no size cap') +
1541
+ ' (server.cluster.sharedCacheMaxMB)');
1542
+ const cache = new SqliteResourceCache(this.stats, { path: file, scope: endpointPath, maxBytes,
1543
+ generation: this.sharedCacheGeneration });
1544
+ if (cache.lostToRestart) {
1545
+ this.log.info(`Resource cache: ${cache.lostToRestart} cache(s) from before this start are now tombstones (lost to a server restart)`);
1546
+ }
1547
+ return cache;
1548
+ }
1549
+
1383
1550
  /**
1384
1551
  * Shutdown the module
1385
1552
  */
@@ -1389,6 +1556,13 @@ class TXModule {
1389
1556
  clearInterval(timer);
1390
1557
  }
1391
1558
  this.timers = [];
1559
+ if (this.config && this.config.sharedCache) {
1560
+ for (const ep of this.endpoints || []) {
1561
+ if (ep.resourceCache) {
1562
+ ep.resourceCache.close();
1563
+ }
1564
+ }
1565
+ }
1392
1566
  if (this.syndicationStore) {
1393
1567
  // let an induction in progress finish, so the store isn't left with a half-copied file
1394
1568
  await this.syndicationStore.whenIdle();
@@ -1460,7 +1634,7 @@ class TXModule {
1460
1634
  this.log.error(`Error rendering syndication feed: ${error.message}`);
1461
1635
  res.status(500).json(this.operationOutcome('error', 'exception', 'Unable to render the syndication feed'));
1462
1636
  } finally {
1463
- this.countRequest(endpointPath, 'syndication', Date.now() - start);
1637
+ this.countRequest(endpointPath, 'syndication', Date.now() - start, req);
1464
1638
  }
1465
1639
  };
1466
1640
 
@@ -1543,9 +1717,20 @@ class TXModule {
1543
1717
  return data;
1544
1718
  }
1545
1719
 
1546
- countRequest(endpoint, name, tat) {
1720
+ /**
1721
+ * Count a request. One that had to ask other servers (ecosystem resolution: the operation
1722
+ * context counts the requests it sent) is counted as an external request: those take
1723
+ * seconds, and are graphed separately (ServerStats.countExternalRequest)
1724
+ *
1725
+ * @param {Object} [req] - the request, for its operation context
1726
+ */
1727
+ countRequest(endpoint, name, tat, req = null) {
1547
1728
  if (this.stats) {
1548
- this.stats.countRequest(name, tat, endpoint);
1729
+ if (req && req.txOpContext && req.txOpContext.externalRequests > 0) {
1730
+ this.stats.countExternalRequest(name, tat, endpoint);
1731
+ } else {
1732
+ this.stats.countRequest(name, tat, endpoint);
1733
+ }
1549
1734
  }
1550
1735
  }
1551
1736
 
@@ -1644,23 +1829,40 @@ class TXModule {
1644
1829
  }
1645
1830
  }
1646
1831
 
1832
+ // the configured expansion limits, checked at startup (see expansionLimits). Every client gets
1833
+ // the same: the tx-ecosystem test runner (User-Agent Tools/Java) used to be held to the
1834
+ // defaults, and now asks for a lower limit where a test depends on one (x-too-costly-threshold)
1835
+ currentExpansionLimits() {
1836
+ return this.expansionLimitValues || TXModule.expansionLimits(this.config || {});
1837
+ }
1838
+
1839
+ // eslint-disable-next-line no-unused-vars
1647
1840
  internalLimit(req) {
1648
- let isTest = req.header("User-Agent") == 'Tools/Java';
1649
- if (this.config.internalLimit && !isTest) return this.config.internalLimit; else return INTERNAL_DEFAULT_LIMIT;
1841
+ return this.currentExpansionLimits().internal;
1842
+ }
1843
+
1844
+ // How far a paged expansion (count and offset) may walk to reach its page: offset + count. Only
1845
+ // the page is built, the codes before it are counted, so this can be well past internalLimit
1846
+ // eslint-disable-next-line no-unused-vars
1847
+ walkLimit(req) {
1848
+ return this.currentExpansionLimits().walk;
1650
1849
  }
1651
1850
 
1652
1851
  externalLimit(req) {
1653
- let hdr = req.headers["x-too-costly-threshold"];
1654
- if (hdr) {
1655
- return parseInt(hdr);
1852
+ let limit = this.currentExpansionLimits().external;
1853
+ // x-too-costly-threshold sets the limit for this request - mostly so test cases can ask for
1854
+ // a lower one. It can't be more than the internal limit, and a value that isn't a positive
1855
+ // whole number is ignored: it never means "no limit"
1856
+ const hdr = req.headers["x-too-costly-threshold"];
1857
+ if (hdr !== undefined) {
1858
+ const n = Number(hdr);
1859
+ if (Number.isInteger(n) && n > 0) {
1860
+ limit = Math.min(n, this.internalLimit(req));
1861
+ }
1656
1862
  }
1657
- // the tx-ecosystem test runner (Tools/Java) always gets the test limit, so every server
1658
- // gives it the same answer. Otherwise the configured externalLimit - it used to be read
1659
- // only when internalLimit was configured too, so setting it alone did nothing
1660
- let isTest = req.header("User-Agent") == 'Tools/Java';
1661
- if (this.config.externalLimit && !isTest) return this.config.externalLimit; else return EXTERNAL_TEST_DEFAULT_LIMIT;
1863
+ return limit;
1662
1864
  }
1663
1865
 
1664
1866
  }
1665
1867
 
1666
- module.exports = TXModule;
1868
+ module.exports = TXModule;