fhirsmith 0.13.3 → 0.14.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/CHANGELOG.md +47 -1
- package/README.md +48 -4
- package/config-template.json +70 -1
- package/library/cgroup-memory.js +113 -0
- package/library/folder-setup.js +8 -1
- package/nginx.md +391 -0
- package/package.json +3 -2
- package/publisher/git-remote.js +141 -0
- package/publisher/github-release.js +195 -0
- package/publisher/publisher.js +146 -14
- package/server.js +51 -14
- package/stats.js +19 -13
- package/testing/readme.md +112 -0
- package/testing/render.js +482 -0
- package/testing/search.js +378 -0
- package/testing/store.js +266 -0
- package/testing/testing-template.html +126 -0
- package/testing/testing.js +486 -0
- package/translations/Messages.properties +32 -0
- package/tx/closure/closure-store.js +396 -0
- package/tx/cs/cs-api.js +6 -1
- package/tx/cs/cs-rxnorm.js +35 -8
- package/tx/cs/cs-snomed.js +142 -27
- package/tx/html/codesystem-operations.liquid +177 -0
- package/tx/html/ecl-panel.liquid +93 -0
- package/tx/html/home-metrics.liquid +39 -76
- package/tx/html/tx-template.html +1 -0
- package/tx/library/conceptmap.js +22 -6
- package/tx/library/designations.js +12 -3
- package/tx/library/extensions.js +2 -2
- package/tx/library/operation-outcome.js +3 -1
- package/tx/library/renderer.js +42 -5
- package/tx/operation-context.js +111 -15
- package/tx/provider.js +85 -0
- package/tx/sct/expressions.js +128 -44
- package/tx/sct/text-index.js +419 -0
- package/tx/tx-html.js +426 -44
- package/tx/tx.js +117 -9
- package/tx/workers/cache-control.js +2 -2
- package/tx/workers/closure.js +483 -13
- package/tx/workers/expand.js +20 -0
- package/tx/workers/metadata.js +29 -4
- package/tx/workers/read.js +18 -3
- package/tx/workers/subsumes.js +18 -2
- package/tx/workers/translate.js +167 -20
- package/tx/workers/validate.js +61 -182
- package/tx/workers/worker.js +9 -0
- package/tx/xversion/xv-conceptmap.js +17 -8
- package/utilities/submit-test-report.js +150 -0
package/tx/tx.js
CHANGED
|
@@ -49,10 +49,11 @@ const {capabilityStatementFromR5} = require("./xversion/xv-capabiliityStatement"
|
|
|
49
49
|
const {bundleFromR5} = require("./xversion/xv-bundle");
|
|
50
50
|
const {convertResourceToR5} = require("./xversion/xv-resource");
|
|
51
51
|
const ClosureWorker = require("./workers/closure");
|
|
52
|
+
const { ClosureStore } = require("./closure/closure-store");
|
|
52
53
|
const {BundleXML} = require("./xml/bundle-xml");
|
|
53
54
|
const ConceptUsageTracker = require("./usage-tracker");
|
|
54
55
|
const ProblemFinder = require("./problems");
|
|
55
|
-
const { buildOperationOutcome } = require('./library/operation-outcome');
|
|
56
|
+
const { buildOperationOutcome, outcomeFromError } = require('./library/operation-outcome');
|
|
56
57
|
// const {writeFileSync} = require("fs");
|
|
57
58
|
|
|
58
59
|
class TXModule {
|
|
@@ -147,6 +148,13 @@ class TXModule {
|
|
|
147
148
|
|
|
148
149
|
this.log.info('Initializing TX module');
|
|
149
150
|
|
|
151
|
+
// Optional instance code, prefixed onto every cache-id this server issues so a
|
|
152
|
+
// proxy in front of several instances can route by it (see nginx.md). Checked
|
|
153
|
+
// here so a bad value stops startup rather than surfacing per endpoint.
|
|
154
|
+
if (ResourceCache.checkInstanceCode(config.instanceCode)) {
|
|
155
|
+
this.log.info(`Cache-ids will be issued with instance code '${config.instanceCode}'`);
|
|
156
|
+
}
|
|
157
|
+
|
|
150
158
|
// Load HTML template
|
|
151
159
|
txHtml.loadTemplate();
|
|
152
160
|
|
|
@@ -176,8 +184,35 @@ class TXModule {
|
|
|
176
184
|
await this.i18n.load();
|
|
177
185
|
this.log.info('I18n support initialized');
|
|
178
186
|
|
|
187
|
+
// $closure keeps its tables on disk, across sessions and restarts, so it only exists
|
|
188
|
+
// when the administrator turns it on and says where they go. If it is turned on and
|
|
189
|
+
// the database can't be opened, that stops startup - silently running without it
|
|
190
|
+
// would be worse.
|
|
191
|
+
this.closureStore = null;
|
|
192
|
+
if (config.closure && config.closure.enabled) {
|
|
193
|
+
this.closureStore = new ClosureStore(config.closure, this.log);
|
|
194
|
+
this.closureStore.open();
|
|
195
|
+
this.log.info(`$closure enabled: tables in ${this.closureStore.path}`);
|
|
196
|
+
const days = config.closure.retentionDays;
|
|
197
|
+
if (days) {
|
|
198
|
+
const prune = async () => {
|
|
199
|
+
try {
|
|
200
|
+
const dropped = await this.closureStore.pruneUnused(days);
|
|
201
|
+
if (dropped.length > 0) {
|
|
202
|
+
this.log.info(`closure: dropped ${dropped.length} table(s) unused for ${days} days`);
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
this.log.error(`closure: pruning failed: ${error.message}`);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
void prune(); // errors are logged inside
|
|
209
|
+
this.timers.push(setInterval(prune, 24 * 60 * 60 * 1000));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
179
213
|
// Initialize metadata handler with config
|
|
180
214
|
this.metadataHandler = new MetadataHandler({
|
|
215
|
+
closure: this.closureStore != null,
|
|
181
216
|
baseUrl: config.baseUrl,
|
|
182
217
|
serverVersion: packageJson.version,
|
|
183
218
|
txVersion: packageJson.txVersion,
|
|
@@ -259,6 +294,7 @@ class TXModule {
|
|
|
259
294
|
// can report it and a client can size its keepalive interval to this server rather
|
|
260
295
|
// than guessing at a number it has no way to see.
|
|
261
296
|
endpointInfo.resourceCache.setIdleTimeout(cacheTimeoutMs);
|
|
297
|
+
endpointInfo.resourceCache.setInstanceCode(this.config.instanceCode);
|
|
262
298
|
if (this.stats) {
|
|
263
299
|
this.stats.addTask("Client Cache", "5 min");
|
|
264
300
|
}
|
|
@@ -370,9 +406,15 @@ class TXModule {
|
|
|
370
406
|
let result;
|
|
371
407
|
|
|
372
408
|
if (isHtml) {
|
|
373
|
-
|
|
409
|
+
// _format=html/fragment: the rendering only, with no page around it, for a
|
|
410
|
+
// page that runs an operation and puts the result inside itself (the ECL
|
|
411
|
+
// panel does this). The rendering is the same either way, so an error comes
|
|
412
|
+
// back as the rendered OperationOutcome rather than as something the caller
|
|
413
|
+
// has to handle separately.
|
|
374
414
|
const content = await txhtml.render(data, req);
|
|
375
|
-
const
|
|
415
|
+
const fragment = (req.query._format || req.query.format) === 'html/fragment';
|
|
416
|
+
const html = fragment ? content
|
|
417
|
+
: await txhtml.renderPage(txhtml.buildTitle(data, req), content, req.txEndpoint, req.txStartTime);
|
|
376
418
|
responseSize = Buffer.byteLength(html, 'utf8');
|
|
377
419
|
res.setHeader('Content-Type', 'text/html');
|
|
378
420
|
result = res.send(html);
|
|
@@ -710,20 +752,21 @@ class TXModule {
|
|
|
710
752
|
}
|
|
711
753
|
});
|
|
712
754
|
|
|
713
|
-
//
|
|
714
|
-
|
|
755
|
+
// $closure (GET and POST). System level: the OperationDefinition is
|
|
756
|
+
// ConceptMap-closure, but it is declared system=true, type=false (#100).
|
|
757
|
+
router.get('/\\$closure', async (req, res) => {
|
|
715
758
|
const start = Date.now();
|
|
716
759
|
try {
|
|
717
|
-
let worker = new ClosureWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
760
|
+
let worker = new ClosureWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.closureStore, this.config.closure);
|
|
718
761
|
await worker.handle(req, res, this.log);
|
|
719
762
|
} finally {
|
|
720
763
|
this.countRequest(endpointPath, '$closure', Date.now() - start);
|
|
721
764
|
}
|
|
722
765
|
});
|
|
723
|
-
router.post('
|
|
766
|
+
router.post('/\\$closure', async (req, res) => {
|
|
724
767
|
const start = Date.now();
|
|
725
768
|
try {
|
|
726
|
-
let worker = new ClosureWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
769
|
+
let worker = new ClosureWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.closureStore, this.config.closure);
|
|
727
770
|
await worker.handle(req, res, this.log);
|
|
728
771
|
} finally {
|
|
729
772
|
this.countRequest(endpointPath, '$closure', Date.now() - start);
|
|
@@ -921,9 +964,15 @@ class TXModule {
|
|
|
921
964
|
|
|
922
965
|
// Unsupported methods
|
|
923
966
|
for (const resourceType of resourceTypes) {
|
|
924
|
-
router.all(`/${resourceType}/:id`, (req, res) => {
|
|
967
|
+
router.all(`/${resourceType}/:id`, (req, res, next) => {
|
|
925
968
|
const start = Date.now();
|
|
926
969
|
try {
|
|
970
|
+
// /ConceptMap/$closure etc: an operation this server doesn't have at this
|
|
971
|
+
// level, not a resource that refuses the method
|
|
972
|
+
if (req.params.id.startsWith('$')) {
|
|
973
|
+
return res.status(404).json(this.operationOutcome('error', 'not-found',
|
|
974
|
+
`Unknown operation on this server: ${req.method} ${req.baseUrl}${req.path}`, 'not-found'));
|
|
975
|
+
}
|
|
927
976
|
if (['PUT', 'POST', 'DELETE', 'PATCH'].includes(req.method)) {
|
|
928
977
|
return res.status(405).json(this.operationOutcome(
|
|
929
978
|
'error',
|
|
@@ -931,6 +980,8 @@ class TXModule {
|
|
|
931
980
|
`Method ${req.method} is not supported`
|
|
932
981
|
));
|
|
933
982
|
}
|
|
983
|
+
// anything else falls through to the not-found fallback rather than hanging
|
|
984
|
+
return next();
|
|
934
985
|
} finally {
|
|
935
986
|
this.countRequest(endpointPath, '$read', Date.now() - start);
|
|
936
987
|
}
|
|
@@ -947,6 +998,22 @@ class TXModule {
|
|
|
947
998
|
}
|
|
948
999
|
});
|
|
949
1000
|
|
|
1001
|
+
// The ECL panel: an expression, an edition, and the expansion it produces. ECL is
|
|
1002
|
+
// written by trial and error, so the page keeps the inputs and puts each result under
|
|
1003
|
+
// them rather than navigating away from what you just typed.
|
|
1004
|
+
router.get('/ecl', async (req, res) => {
|
|
1005
|
+
const start = Date.now();
|
|
1006
|
+
try {
|
|
1007
|
+
let txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid, this.languages, this.i18n, req.txEndpoint.path);
|
|
1008
|
+
const content = await txhtml.buildEclPage(req);
|
|
1009
|
+
const html = await txhtml.renderPage('ECL', content, req.txEndpoint, req.txStartTime);
|
|
1010
|
+
res.setHeader('Content-Type', 'text/html');
|
|
1011
|
+
res.send(html);
|
|
1012
|
+
} finally {
|
|
1013
|
+
this.countRequest(endpointPath, 'ecl', Date.now() - start);
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
|
|
950
1017
|
router.get('/problems.html', async (req, res) => {
|
|
951
1018
|
const start = Date.now();
|
|
952
1019
|
try {
|
|
@@ -1066,6 +1133,35 @@ class TXModule {
|
|
|
1066
1133
|
};
|
|
1067
1134
|
router.get('/info/:id', infoHandler);
|
|
1068
1135
|
router.post('/info/:id', infoHandler);
|
|
1136
|
+
|
|
1137
|
+
// ===== Fallbacks - keep these last =====
|
|
1138
|
+
|
|
1139
|
+
// Nothing above matched. Without this, Express answers with its own HTML
|
|
1140
|
+
// "Cannot POST /r4/$foo" page; a FHIR server must answer with an
|
|
1141
|
+
// OperationOutcome (#100). Goes through the wrapped res.json, so it is
|
|
1142
|
+
// rendered as JSON, XML or HTML to suit the client like everything else.
|
|
1143
|
+
router.use((req, res) => {
|
|
1144
|
+
const start = Date.now();
|
|
1145
|
+
try {
|
|
1146
|
+
const what = req.path.includes('$') ? 'operation' : 'path';
|
|
1147
|
+
res.status(404).json(this.operationOutcome('error', 'not-found',
|
|
1148
|
+
`Unknown ${what} on this server: ${req.method} ${req.baseUrl}${req.path}`, 'not-found'));
|
|
1149
|
+
} finally {
|
|
1150
|
+
this.countRequest(endpointPath, 'unknown', Date.now() - start);
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
// A handler threw or rejected without answering. Express 5 forwards those here;
|
|
1155
|
+
// its default handler would send an HTML error page.
|
|
1156
|
+
// eslint-disable-next-line no-unused-vars
|
|
1157
|
+
router.use((err, req, res, next) => {
|
|
1158
|
+
this.log.error(err);
|
|
1159
|
+
debugLog(err);
|
|
1160
|
+
if (res.headersSent) {
|
|
1161
|
+
return next(err);
|
|
1162
|
+
}
|
|
1163
|
+
res.status(err.statusCode || 500).json(outcomeFromError(err));
|
|
1164
|
+
});
|
|
1069
1165
|
}
|
|
1070
1166
|
|
|
1071
1167
|
/**
|
|
@@ -1181,6 +1277,10 @@ class TXModule {
|
|
|
1181
1277
|
this.timers = [];
|
|
1182
1278
|
// Clean up any resources if needed
|
|
1183
1279
|
await this.library.close();
|
|
1280
|
+
if (this.closureStore) {
|
|
1281
|
+
this.closureStore.close();
|
|
1282
|
+
this.closureStore = null;
|
|
1283
|
+
}
|
|
1184
1284
|
this.log.info('TX module shut down');
|
|
1185
1285
|
}
|
|
1186
1286
|
|
|
@@ -1271,6 +1371,14 @@ class TXModule {
|
|
|
1271
1371
|
return count;
|
|
1272
1372
|
}
|
|
1273
1373
|
|
|
1374
|
+
// $closure usage for the home page: null when closure isn't turned on
|
|
1375
|
+
closureStats() {
|
|
1376
|
+
if (!this.closureStore) {
|
|
1377
|
+
return null;
|
|
1378
|
+
}
|
|
1379
|
+
return { ...this.closureStore.totals(), retentionDays: this.config.closure.retentionDays || null };
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1274
1382
|
// High-water marks for the client cache, summed across endpoints.
|
|
1275
1383
|
maxClientCacheCount() {
|
|
1276
1384
|
let count = 0;
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
// unknown/expired cache later instead of failing obscurely deep inside a validation.
|
|
14
14
|
//
|
|
15
15
|
|
|
16
|
-
const crypto = require('crypto');
|
|
17
16
|
const { TerminologyWorker, CACHE_ID_HEADER } = require('./worker');
|
|
18
17
|
const { Parameters } = require('../library/parameters');
|
|
19
18
|
const { Issue, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
|
|
@@ -135,7 +134,8 @@ class CacheControlWorker extends TerminologyWorker {
|
|
|
135
134
|
// Flip this to default-true once all clients send an explicit value.
|
|
136
135
|
const sealed = this.readSealed(params.jsonObj);
|
|
137
136
|
|
|
138
|
-
|
|
137
|
+
// "<instanceCode>.<uuid>" when this server has an instance code, else a bare UUID
|
|
138
|
+
const cacheId = cache.newCacheId();
|
|
139
139
|
cache.set(cacheId, resources, sealed);
|
|
140
140
|
|
|
141
141
|
return res.status(200).json({
|