fhirsmith 0.14.0 → 0.14.2
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 +64 -0
- package/README.md +3 -2
- package/config-template.json +20 -0
- package/library/fhir-openapi-schema.js +441 -0
- package/library/html-server.js +9 -3
- package/library/languages.js +15 -0
- package/library/npm-audit.js +265 -0
- package/library/openapi-doc.js +557 -0
- package/library/same-origin.js +62 -0
- package/library/utilities.js +21 -0
- package/nginx.md +17 -9
- package/package.json +10 -4
- package/packages/openapi.js +13 -0
- package/packages/openapi.yaml +603 -0
- package/packages/packages-template.html +4 -2
- package/packages/packages.js +286 -81
- package/packages/readme.md +120 -0
- package/publisher/publisher-template.html +1 -0
- package/publisher/publisher.js +95 -42
- package/registry/api.js +117 -41
- package/registry/crawler.js +24 -0
- package/registry/fhirsmith-releases.js +170 -0
- package/registry/model.js +10 -0
- package/registry/openapi.js +13 -0
- package/registry/openapi.yaml +311 -0
- package/registry/readme.md +107 -208
- package/registry/registry-template.html +4 -1
- package/registry/registry.js +183 -20
- package/server.js +25 -6
- package/testing/openapi-schemas.config.js +58 -0
- package/testing/openapi-schemas.json +1920 -0
- package/testing/openapi.js +15 -0
- package/testing/openapi.yaml +273 -0
- package/testing/readme.md +75 -1
- package/testing/render.js +270 -29
- package/testing/search.js +1 -1
- package/testing/store.js +152 -8
- package/testing/testing-template.html +2 -0
- package/testing/testing.js +307 -9
- package/translations/Messages.properties +1 -0
- package/tx/README.md +58 -0
- package/tx/cm/cm-database.js +10 -1
- package/tx/cm/cm-package.js +8 -1
- package/tx/cs/cs-rxnorm.js +23 -6
- package/tx/cs/cs-snomed.js +3 -72
- package/tx/html/tx-template.html +2 -0
- package/tx/library/canonical-resource.js +50 -1
- package/tx/library/codesystem.js +1 -0
- package/tx/library/conceptmap.js +1 -0
- package/tx/library/valueset.js +16 -0
- package/tx/library.js +17 -2
- package/tx/openapi-operations.js +363 -0
- package/tx/openapi-schemas.config.js +118 -0
- package/tx/openapi-schemas.json +7903 -0
- package/tx/openapi.js +289 -0
- package/tx/openapi.yaml +151 -0
- package/tx/operation-context.js +15 -0
- package/tx/params.js +3 -12
- package/tx/provider.js +10 -1
- package/tx/sct/editions.js +70 -0
- package/tx/tx-html.js +6 -0
- package/tx/tx.js +55 -0
- package/tx/vs/vs-database.js +14 -5
- package/tx/vs/vs-package.js +10 -0
- package/tx/workers/expand.js +37 -18
- package/tx/workers/metadata.js +0 -13
- package/tx/workers/translate.js +25 -1
- package/tx/workers/validate.js +74 -24
- package/tx/workers/worker.js +32 -43
- package/utilities/extract-schema-json.js +167 -0
- package/utilities/generate-openapi-schemas.js +49 -0
- package/utilities/submit-test-report.js +1 -1
- package/xig/xig.js +2 -4
package/registry/registry.js
CHANGED
|
@@ -9,6 +9,15 @@ const Logger = require('../library/logger');
|
|
|
9
9
|
const regLog = Logger.getInstance().child({ module: 'registry' });
|
|
10
10
|
const folders = require('../library/folder-setup');
|
|
11
11
|
const escape = require('escape-html');
|
|
12
|
+
const registryOpenApi = require('./openapi');
|
|
13
|
+
const { FhirsmithReleases, isFhirsmith, describeAge } = require('./fhirsmith-releases');
|
|
14
|
+
|
|
15
|
+
// The query parameters of the public API. These are the contract published in openapi.yaml,
|
|
16
|
+
// and tests/registry/openapi.test.js checks that the two agree - so change both together.
|
|
17
|
+
// Parameters are read leniently (a repeated parameter takes its first value, and unknown
|
|
18
|
+
// parameters are ignored) because existing ecosystem clients rely on that.
|
|
19
|
+
const DISCOVERY_PARAMS = ['registry', 'server', 'fhirVersion', 'url', 'authoritativeOnly', 'language'];
|
|
20
|
+
const RESOLVE_PARAMS = ['fhirVersion', 'url', 'version', 'valueSet', 'authoritativeOnly', 'language', 'usage'];
|
|
12
21
|
|
|
13
22
|
class RegistryModule {
|
|
14
23
|
constructor(stats) {
|
|
@@ -50,6 +59,13 @@ class RegistryModule {
|
|
|
50
59
|
// Initialize API with crawler
|
|
51
60
|
this.api = new RegistryAPI(this.crawler);
|
|
52
61
|
|
|
62
|
+
// FHIRsmith release dates, for the software page. CHANGELOG.md until GitHub answers
|
|
63
|
+
this.releases = new FhirsmithReleases(crawlerConfig, this.logger);
|
|
64
|
+
this.releases.loadFromChangelog();
|
|
65
|
+
if (config.releasesUrl !== '') {
|
|
66
|
+
this.releases.refresh().catch(() => {});
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
// Load saved data if available
|
|
54
70
|
await this.loadSavedData();
|
|
55
71
|
|
|
@@ -145,6 +161,9 @@ class RegistryModule {
|
|
|
145
161
|
try {
|
|
146
162
|
// Perform the crawl
|
|
147
163
|
const newData = await this.crawler.crawl(this.config.masterUrl);
|
|
164
|
+
if (this.releases && this.config.releasesUrl !== '') {
|
|
165
|
+
await this.releases.refresh();
|
|
166
|
+
}
|
|
148
167
|
|
|
149
168
|
// Thread-safe update of current data
|
|
150
169
|
await this.updateData(() => {
|
|
@@ -210,6 +229,9 @@ class RegistryModule {
|
|
|
210
229
|
// Content Security Policy
|
|
211
230
|
res.setHeader('Content-Security-Policy', "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'");
|
|
212
231
|
|
|
232
|
+
// RFC 8631: where to find the machine-readable description of this API
|
|
233
|
+
res.setHeader('Link', `<${req.baseUrl}/openapi.json>; rel="service-desc", <${req.baseUrl}/openapi>; rel="service-doc"`);
|
|
234
|
+
|
|
213
235
|
next();
|
|
214
236
|
});
|
|
215
237
|
}
|
|
@@ -230,6 +252,36 @@ class RegistryModule {
|
|
|
230
252
|
this.router.get('/', this.handleMainPage.bind(this));
|
|
231
253
|
this.router.get('/resolve', this.handleResolveEndpoint.bind(this));
|
|
232
254
|
this.router.get('/log', this.handleLogEndpoint.bind(this));
|
|
255
|
+
this.router.get('/software', this.handleSoftwarePage.bind(this));
|
|
256
|
+
|
|
257
|
+
// OpenAPI description of this API: /openapi.json, /openapi.yaml, and /openapi (an HTML
|
|
258
|
+
// reference for browsers, the JSON otherwise)
|
|
259
|
+
this.router.get('/openapi.json', (req, res) => {
|
|
260
|
+
res.type('application/json').send(registryOpenApi.getJson());
|
|
261
|
+
});
|
|
262
|
+
this.router.get('/openapi.yaml', (req, res) => {
|
|
263
|
+
res.setHeader('Content-Type', 'application/yaml');
|
|
264
|
+
res.send(registryOpenApi.getYaml());
|
|
265
|
+
});
|
|
266
|
+
this.router.get('/openapi', (req, res) => {
|
|
267
|
+
const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
|
|
268
|
+
if (!acceptsHtml) {
|
|
269
|
+
res.type('application/json').send(registryOpenApi.getJson());
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
if (!htmlServer.hasTemplate('registry')) {
|
|
274
|
+
htmlServer.loadTemplate('registry', path.join(__dirname, 'registry-template.html'));
|
|
275
|
+
}
|
|
276
|
+
const html = htmlServer.renderPage('registry', 'Terminology Server Registry API',
|
|
277
|
+
registryOpenApi.renderHtml(), this.api ? this.api.getStatistics() : {});
|
|
278
|
+
res.setHeader('Content-Type', 'text/html');
|
|
279
|
+
res.send(html);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
this.logger.error('Error rendering OpenAPI page:', error);
|
|
282
|
+
res.status(500).send(`<html><body><h1>Error</h1><p>${escape(error.message)}</p></body></html>`);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
233
285
|
}
|
|
234
286
|
|
|
235
287
|
/**
|
|
@@ -357,21 +409,23 @@ class RegistryModule {
|
|
|
357
409
|
const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
|
|
358
410
|
|
|
359
411
|
if (!acceptsHtml) {
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
412
|
+
// The ecosystem Discovery API
|
|
413
|
+
const params = this._normalizeQueryParams(req.query);
|
|
414
|
+
if (params.url && !this._isValidUrl(params.url.split('|')[0])) {
|
|
415
|
+
return res.status(400).json({error: 'Invalid code system URL format'});
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const filters = {};
|
|
419
|
+
for (const name of DISCOVERY_PARAMS) {
|
|
420
|
+
if (params[name]) {
|
|
421
|
+
filters[name] = params[name];
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return res.json(this.api.discover(filters));
|
|
425
|
+
} catch (error) {
|
|
426
|
+
this.logger.error('Error in discovery:', error);
|
|
427
|
+
return res.status(500).json({error: error.message});
|
|
428
|
+
}
|
|
375
429
|
}
|
|
376
430
|
|
|
377
431
|
// Render HTML page
|
|
@@ -416,6 +470,114 @@ class RegistryModule {
|
|
|
416
470
|
}
|
|
417
471
|
}
|
|
418
472
|
|
|
473
|
+
/**
|
|
474
|
+
* The software page - what each registered server is running, and for FHIRsmith,
|
|
475
|
+
* how old that release is
|
|
476
|
+
*/
|
|
477
|
+
async handleSoftwarePage(req, res) {
|
|
478
|
+
const start = Date.now();
|
|
479
|
+
try {
|
|
480
|
+
if (!htmlServer.hasTemplate('registry')) {
|
|
481
|
+
htmlServer.loadTemplate('registry', path.join(__dirname, 'registry-template.html'));
|
|
482
|
+
}
|
|
483
|
+
const startTime = Date.now();
|
|
484
|
+
const content = this.buildSoftwareContent();
|
|
485
|
+
const stats = this.api.getStatistics();
|
|
486
|
+
stats.processingTime = Date.now() - startTime;
|
|
487
|
+
const html = htmlServer.renderPage('registry', 'Terminology Server Software', content, stats);
|
|
488
|
+
res.setHeader('Content-Type', 'text/html');
|
|
489
|
+
res.send(html);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
this.logger.error('Error rendering software page:', error);
|
|
492
|
+
res.status(500).send(`<html><body><h1>Error</h1><p>${escape(error.message)}</p></body></html>`);
|
|
493
|
+
} finally {
|
|
494
|
+
this.stats.countRequest('software', Date.now() - start);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
buildSoftwareContent(now = Date.now()) {
|
|
499
|
+
const data = this.api.getData();
|
|
500
|
+
if (!data || !data.registries) {
|
|
501
|
+
return '<div class="alert alert-info"><h4>Registry data not yet available</h4>' +
|
|
502
|
+
'<p>The initial crawl is in progress. Please refresh in a moment.</p></div>';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const rows = [];
|
|
506
|
+
for (const registry of data.registries) {
|
|
507
|
+
for (const server of registry.servers || []) {
|
|
508
|
+
for (const version of server.versions || []) {
|
|
509
|
+
rows.push({
|
|
510
|
+
name: server.name || server.code || '',
|
|
511
|
+
url: version.address || '',
|
|
512
|
+
fhirVersion: version.version || '',
|
|
513
|
+
software: version.software && version.software !== 'unknown' ? version.software : '',
|
|
514
|
+
softwareVersion: version.softwareVersion || '',
|
|
515
|
+
error: version.error,
|
|
516
|
+
lastSuccess: version.lastSuccess
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
rows.sort((a, b) => a.name.localeCompare(b.name) || a.url.localeCompare(b.url));
|
|
522
|
+
|
|
523
|
+
let html = '<h3>Terminology Server Software</h3>';
|
|
524
|
+
const latest = this.releases ? this.releases.latest() : null;
|
|
525
|
+
if (latest) {
|
|
526
|
+
html += `<p>The current FHIRsmith release is <b>v${escape(latest.version)}</b>, released ` +
|
|
527
|
+
`${escape(latest.date.toISOString().substring(0, 10))}. ` +
|
|
528
|
+
'Servers running older FHIRsmith releases are missing security fixes, both in FHIRsmith ' +
|
|
529
|
+
'and in the libraries it depends on, and should be upgraded.</p>';
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
html += '<table class="grid">';
|
|
533
|
+
html += '<thead><tr><th>Server</th><th>URL</th><th>FHIR</th><th>Software</th><th>Version</th>' +
|
|
534
|
+
'<th>Released</th><th>Age</th></tr></thead><tbody>';
|
|
535
|
+
for (const row of rows) {
|
|
536
|
+
html += '<tr>';
|
|
537
|
+
html += `<td>${escape(row.name)}</td>`;
|
|
538
|
+
html += `<td><a href="${escape(row.url)}" target="_blank" rel="noopener">${escape(row.url)}</a>`;
|
|
539
|
+
if (row.error) {
|
|
540
|
+
html += ` <span class="text-danger" title="${escape(row.error)}">(unreachable)</span>`;
|
|
541
|
+
}
|
|
542
|
+
html += '</td>';
|
|
543
|
+
html += `<td>${escape(row.fhirVersion)}</td>`;
|
|
544
|
+
html += `<td>${row.software ? escape(row.software.replace('Reference Server', 'HealthIntersections')) : '<i>unknown</i>'}</td>`;
|
|
545
|
+
html += `<td>${row.softwareVersion ? escape(row.softwareVersion) : '<i>unknown</i>'}</td>`;
|
|
546
|
+
html += this._renderReleaseCells(row, now);
|
|
547
|
+
html += '</tr>';
|
|
548
|
+
}
|
|
549
|
+
html += '</tbody></table>';
|
|
550
|
+
|
|
551
|
+
if (this.releases && this.releases.source !== 'none') {
|
|
552
|
+
html += `<p class="text-muted"><small>FHIRsmith release dates from ${escape(this.releases.source)}.</small></p>`;
|
|
553
|
+
}
|
|
554
|
+
return html;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
_renderReleaseCells(row, now) {
|
|
558
|
+
if (!isFhirsmith(row.software) || !this.releases) {
|
|
559
|
+
return '<td></td><td></td>';
|
|
560
|
+
}
|
|
561
|
+
const d = this.releases.describe(row.softwareVersion, now);
|
|
562
|
+
const behind = d.behind ? ` (${d.behind} release${d.behind === 1 ? '' : 's'} behind)` : '';
|
|
563
|
+
switch (d.status) {
|
|
564
|
+
case 'current':
|
|
565
|
+
return `<td>${d.release.date.toISOString().substring(0, 10)}</td>` +
|
|
566
|
+
`<td><span class="text-success">${describeAge(d.ageDays)} - current release</span></td>`;
|
|
567
|
+
case 'outdated': {
|
|
568
|
+
// more than 3 months, or more than 3 releases, out of date is flagged
|
|
569
|
+
const cls = d.ageDays > 90 || d.behind > 3 ? 'text-danger' : 'text-warning';
|
|
570
|
+
return `<td>${d.release.date.toISOString().substring(0, 10)}</td>` +
|
|
571
|
+
`<td><span class="${cls}">${describeAge(d.ageDays)}${behind}</span></td>`;
|
|
572
|
+
}
|
|
573
|
+
case 'dev':
|
|
574
|
+
return '<td></td><td>development build' +
|
|
575
|
+
(d.release ? ` after v${escape(d.release.version)}` : '') + behind + '</td>';
|
|
576
|
+
default:
|
|
577
|
+
return '<td></td><td><i>unknown</i></td>';
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
419
581
|
/**
|
|
420
582
|
* Build HTML content for main page
|
|
421
583
|
*/
|
|
@@ -721,7 +883,8 @@ class RegistryModule {
|
|
|
721
883
|
* @returns {string} Formatted URL with edition description
|
|
722
884
|
*/
|
|
723
885
|
_describeSnomedEdition(url) {
|
|
724
|
-
|
|
886
|
+
// the test edition is published under xsct (http://snomed.info/xsct/31000003106/...)
|
|
887
|
+
if (!url.startsWith('http://snomed.info/sct') && !url.startsWith('http://snomed.info/xsct')) {
|
|
725
888
|
return url;
|
|
726
889
|
}
|
|
727
890
|
|
|
@@ -745,6 +908,7 @@ class RegistryModule {
|
|
|
745
908
|
// Match edition code to description
|
|
746
909
|
switch (editionCode) {
|
|
747
910
|
case '900000000000207008': edition = 'Intl'; break;
|
|
911
|
+
case '31000003106': edition = 'Test'; break; // tx-ecosystem test edition (xsct)
|
|
748
912
|
case '731000124108': edition = 'US'; break;
|
|
749
913
|
case '32506021000036107': edition = 'AU'; break;
|
|
750
914
|
case '449081005': edition = 'ES/Intl'; break;
|
|
@@ -1084,10 +1248,6 @@ class RegistryModule {
|
|
|
1084
1248
|
this.logger.info(`Resolved CodeSystem ${url} for FHIR ${fhirVersion} (usage=${usage}, language=${language || 'none'}): ${matches}`);
|
|
1085
1249
|
}
|
|
1086
1250
|
|
|
1087
|
-
// If only authoritative servers are requested, filter results
|
|
1088
|
-
if (authoritativeOnly === 'true' && result) {
|
|
1089
|
-
result.candidates = [];
|
|
1090
|
-
}
|
|
1091
1251
|
if (acceptsHtml) {
|
|
1092
1252
|
try {
|
|
1093
1253
|
const startTime = Date.now();
|
|
@@ -1454,4 +1614,7 @@ class RegistryModule {
|
|
|
1454
1614
|
}
|
|
1455
1615
|
}
|
|
1456
1616
|
|
|
1617
|
+
RegistryModule.DISCOVERY_PARAMS = DISCOVERY_PARAMS;
|
|
1618
|
+
RegistryModule.RESOLVE_PARAMS = RESOLVE_PARAMS;
|
|
1619
|
+
|
|
1457
1620
|
module.exports = RegistryModule;
|
package/server.js
CHANGED
|
@@ -17,6 +17,7 @@ const { statSync, readdirSync } = require('fs');
|
|
|
17
17
|
const escape = require('escape-html');
|
|
18
18
|
const { resolveWithin } = require('./library/path-safety');
|
|
19
19
|
const { readCgroupMemoryLimit } = require('./library/cgroup-memory');
|
|
20
|
+
const { NpmAudit } = require('./library/npm-audit');
|
|
20
21
|
|
|
21
22
|
// Load configuration BEFORE logger
|
|
22
23
|
let config;
|
|
@@ -79,7 +80,12 @@ const app = express();
|
|
|
79
80
|
|
|
80
81
|
// Behind nginx (or any reverse proxy): honor X-Forwarded-* so req.protocol and
|
|
81
82
|
// req.hostname reflect what the client actually requested, not the loopback hop.
|
|
82
|
-
|
|
83
|
+
// Only proxies on the same host or a private network are trusted by default. `true` would
|
|
84
|
+
// trust every X-Forwarded-For hop, so anyone who could reach the port directly could name
|
|
85
|
+
// their own IP address and walk around every rate limit (express-rate-limit refuses to be
|
|
86
|
+
// quiet about it: ERR_ERL_PERMISSIVE_TRUST_PROXY). See nginx.md.
|
|
87
|
+
const DEFAULT_TRUST_PROXY = 'loopback, linklocal, uniquelocal';
|
|
88
|
+
app.set('trust proxy', config.server.trustProxy ?? DEFAULT_TRUST_PROXY);
|
|
83
89
|
|
|
84
90
|
const PORT = process.env.PORT || config.server.port || 3000;
|
|
85
91
|
|
|
@@ -105,10 +111,13 @@ app.use(cors(config.server.cors));
|
|
|
105
111
|
const modules = {};
|
|
106
112
|
|
|
107
113
|
let stats = null;
|
|
114
|
+
// periodic npm advisory check of the installed packages - see library/npm-audit.js
|
|
115
|
+
let npmAudit = null;
|
|
108
116
|
|
|
109
117
|
// Initialize modules based on configuration
|
|
110
118
|
async function initializeModules() {
|
|
111
119
|
stats = new ServerStats(config.stats, serverLog);
|
|
120
|
+
npmAudit = new NpmAudit(config.npmAudit || {}, Logger.getInstance().child({ module: 'npm-audit' }), __dirname, stats);
|
|
112
121
|
|
|
113
122
|
// Initialize SHL module
|
|
114
123
|
if (config.modules?.shl?.enabled) {
|
|
@@ -300,7 +309,8 @@ async function loadTemplates() {
|
|
|
300
309
|
|
|
301
310
|
async function buildRootPageContent() {
|
|
302
311
|
stats.requestCount++;
|
|
303
|
-
let content =
|
|
312
|
+
let content = npmAudit ? npmAudit.renderBanner() : '';
|
|
313
|
+
content += '<div class="row mb-4">';
|
|
304
314
|
content += '<div class="col-12">';
|
|
305
315
|
|
|
306
316
|
content += '<h3>Available Modules</h3>';
|
|
@@ -309,7 +319,7 @@ async function buildRootPageContent() {
|
|
|
309
319
|
// Check which modules are enabled and add them to the list
|
|
310
320
|
if (config.modules.packages.enabled) {
|
|
311
321
|
content += '<li class="list-group-item">';
|
|
312
|
-
content += '<a href="/packages" class="text-decoration-none">Package Server</a>: Browse and download FHIR Implementation Guide packages';
|
|
322
|
+
content += '<a href="/packages" class="text-decoration-none">Package Server</a>: Browse and download FHIR Implementation Guide packages (<a href="/packages/openapi">API</a>)';
|
|
313
323
|
content += '</li>';
|
|
314
324
|
}
|
|
315
325
|
|
|
@@ -334,7 +344,7 @@ async function buildRootPageContent() {
|
|
|
334
344
|
if (config.modules.registry && config.modules.registry.enabled) {
|
|
335
345
|
content += '<li class="list-group-item">';
|
|
336
346
|
content += '<a href="/tx-reg" class="text-decoration-none">Terminology Server Registry</a>: ';
|
|
337
|
-
content += 'Discover and query FHIR terminology servers for code system and value set support';
|
|
347
|
+
content += 'Discover and query FHIR terminology servers for code system and value set support (<a href="/tx-reg/openapi">API</a>)';
|
|
338
348
|
content += '</li>';
|
|
339
349
|
}
|
|
340
350
|
|
|
@@ -369,7 +379,7 @@ async function buildRootPageContent() {
|
|
|
369
379
|
if (config.modules?.testing?.enabled) {
|
|
370
380
|
content += '<li class="list-group-item">';
|
|
371
381
|
content += '<a href="/testing" class="text-decoration-none">Test Reports</a>: ';
|
|
372
|
-
content += 'TestReports submitted by TxTester and other test tools';
|
|
382
|
+
content += 'TestReports submitted by TxTester and other test tools (<a href="/testing/openapi">API</a>)';
|
|
373
383
|
content += '</li>';
|
|
374
384
|
}
|
|
375
385
|
|
|
@@ -397,7 +407,9 @@ async function buildRootPageContent() {
|
|
|
397
407
|
if (config.modules.tx.endpoints && config.modules.tx.endpoints.length > 0) {
|
|
398
408
|
content += '<ul class="mt-2 mb-0">';
|
|
399
409
|
for (const endpoint of config.modules.tx.endpoints) {
|
|
400
|
-
|
|
410
|
+
// the OpenAPI description is for the R5 endpoints
|
|
411
|
+
const api = String(endpoint.fhirVersion).startsWith('5') ? ` (<a href="${endpoint.path}/openapi">API</a>)` : '';
|
|
412
|
+
content += `<li><a href="${endpoint.path}" class="text-decoration-none">${endpoint.path}</a> (FHIR v${endpoint.fhirVersion}${endpoint.context ? ', context: ' + endpoint.context : ''})${api}</li>`;
|
|
401
413
|
}
|
|
402
414
|
content += '</ul>';
|
|
403
415
|
}
|
|
@@ -653,6 +665,9 @@ app.get('/dashboard', async (req, res) => {
|
|
|
653
665
|
startTime: stats.startTime
|
|
654
666
|
});
|
|
655
667
|
content += stats.taskDetails();
|
|
668
|
+
if (npmAudit) {
|
|
669
|
+
content += npmAudit.renderDashboard();
|
|
670
|
+
}
|
|
656
671
|
content += `<p>Data: ${folders.dataDir()}</p>`;
|
|
657
672
|
|
|
658
673
|
content = '<div class="row mb-4"><div class="col-12">' + content + '</div></div>';
|
|
@@ -785,6 +800,7 @@ async function startServer() {
|
|
|
785
800
|
stats.markStarted();
|
|
786
801
|
serverLog.info(`=== Server running on http://localhost:${PORT} ===`);
|
|
787
802
|
});
|
|
803
|
+
npmAudit.start();
|
|
788
804
|
if (modules.packages && config.modules.packages.enabled) {
|
|
789
805
|
modules.packages.startInitialCrawler();
|
|
790
806
|
}
|
|
@@ -812,6 +828,9 @@ async function startServer() {
|
|
|
812
828
|
// Graceful shutdown
|
|
813
829
|
process.on('SIGINT', async () => {
|
|
814
830
|
serverLog.info('\nShutting down server...');
|
|
831
|
+
if (npmAudit) {
|
|
832
|
+
npmAudit.stop();
|
|
833
|
+
}
|
|
815
834
|
|
|
816
835
|
// Shutdown all modules
|
|
817
836
|
for (const [moduleName, moduleInstance] of Object.entries(modules)) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// What utilities/generate-openapi-schemas.js generates for the testing module's OpenAPI
|
|
2
|
+
// description (into openapi-schemas.json, which the loader merges into openapi.yaml's
|
|
3
|
+
// components). Regenerate after changing this:
|
|
4
|
+
//
|
|
5
|
+
// node utilities/generate-openapi-schemas.js testing
|
|
6
|
+
//
|
|
7
|
+
// tests/testing/openapi.test.js fails if the generated file is out of date, and checks that
|
|
8
|
+
// the overlay requires everything validateReport() in testing.js requires.
|
|
9
|
+
|
|
10
|
+
const ref = (name) => ({ $ref: `#/components/schemas/${name}` });
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
package: 'hl7.fhir.r5.core#5.0.0',
|
|
14
|
+
roots: ['TestReport', 'OperationOutcome', 'Bundle'],
|
|
15
|
+
|
|
16
|
+
// reports may not contain resources (validateReport rejects them too)
|
|
17
|
+
prohibit: ['contained'],
|
|
18
|
+
|
|
19
|
+
overlay: {
|
|
20
|
+
TestReport: {
|
|
21
|
+
description: 'A TestReport, as this server accepts it: an R5 TestReport (or an R4 one - see testScript), ' +
|
|
22
|
+
'with name, tester, issued and at least one participant, and no contained resources.',
|
|
23
|
+
required: ['name', 'tester', 'issued', 'participant'],
|
|
24
|
+
properties: {
|
|
25
|
+
participant: { minItems: 1 },
|
|
26
|
+
testScript: {
|
|
27
|
+
$replace: true,
|
|
28
|
+
description: 'The TestScript that was run: a canonical (R5), or a Reference (R4)',
|
|
29
|
+
anyOf: [{ type: 'string' }, ref('Reference')]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// the search response: a Bundle of TestReports, plus an OperationOutcome entry
|
|
35
|
+
// (search.mode = outcome) when unknown parameters were ignored
|
|
36
|
+
TestReportSearchBundle: {
|
|
37
|
+
description: 'A searchset Bundle of TestReports',
|
|
38
|
+
allOf: [
|
|
39
|
+
ref('Bundle'),
|
|
40
|
+
{
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
type: { const: 'searchset' },
|
|
44
|
+
entry: {
|
|
45
|
+
type: 'array',
|
|
46
|
+
items: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
resource: { anyOf: [ref('TestReport'), ref('OperationOutcome')] }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|