fhirsmith 0.18.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +149 -0
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +132 -59
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +74 -21
- package/ips/presenter.js +1 -1
- package/ips/readme.md +132 -25
- package/ips/render.js +466 -77
- package/ips/store.js +79 -22
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/crawler.js +84 -108
- package/registry/documentation/configuration.md +34 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +5 -4
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +154 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +29 -1
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +148 -14
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +185 -66
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +608 -64
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +57 -58
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +278 -42
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/ecl.js +47 -6
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +166 -13
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +27 -4
- package/tx/vs/vs-vsac.js +648 -100
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- 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,
|
|
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.
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1649
|
-
|
|
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
|
|
1654
|
-
|
|
1655
|
-
|
|
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
|
-
|
|
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;
|