fhirsmith 0.14.1 → 0.15.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 +58 -0
- package/README.md +3 -2
- package/config-template.json +9 -0
- package/library/changelog.js +57 -0
- package/library/fhir-openapi-schema.js +441 -0
- package/library/html-server.js +9 -3
- package/library/npm-audit.js +265 -0
- package/library/openapi-doc.js +557 -0
- package/library/utilities.js +21 -0
- 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/registry/api.js +117 -41
- package/registry/crawler.js +41 -0
- package/registry/fhirsmith-releases.js +167 -0
- package/registry/model.js +13 -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 +198 -19
- package/server.js +19 -5
- 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 +43 -1
- package/testing/search.js +1 -1
- package/testing/testing-template.html +2 -0
- package/testing/testing.js +24 -0
- package/translations/Messages.properties +1 -0
- package/tx/README.md +78 -4
- package/tx/cm/cm-api.js +25 -10
- package/tx/cm/cm-database.js +10 -5
- package/tx/cm/cm-package.js +61 -15
- package/tx/cs/cs-provider-api.js +22 -7
- package/tx/cs/cs-provider-list.js +10 -9
- package/tx/cs/cs-rxnorm.js +23 -6
- 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/resource-ids.js +107 -0
- package/tx/library/valueset.js +16 -0
- package/tx/library.js +113 -26
- package/tx/ocl/cm-ocl.cjs +23 -15
- package/tx/ocl/cs-ocl.cjs +9 -10
- package/tx/ocl/vs-ocl.cjs +17 -17
- 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 +90 -14
- package/tx/tx-html.js +6 -0
- package/tx/tx.fhir.org.yml +16 -8
- package/tx/tx.js +62 -1
- package/tx/vs/vs-api.js +25 -10
- package/tx/vs/vs-database.js +14 -9
- package/tx/vs/vs-package.js +58 -31
- package/tx/vs/vs-vsac.js +20 -5
- package/tx/workers/expand.js +35 -16
- package/tx/workers/metadata.js +2 -14
- package/tx/workers/read.js +3 -18
- package/tx/workers/validate.js +65 -19
- package/tx/workers/worker.js +29 -8
- package/utilities/extract-schema-json.js +167 -0
- package/utilities/generate-openapi-schemas.js +49 -0
- 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,132 @@ 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
|
+
softwareReleaseDate: version.softwareReleaseDate || '',
|
|
516
|
+
error: version.error,
|
|
517
|
+
lastSuccess: version.lastSuccess
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
rows.sort((a, b) => a.name.localeCompare(b.name) || a.url.localeCompare(b.url));
|
|
523
|
+
|
|
524
|
+
let html = '<h3>Terminology Server Software</h3>';
|
|
525
|
+
const latest = this.releases ? this.releases.latest() : null;
|
|
526
|
+
if (latest) {
|
|
527
|
+
html += `<p>The current FHIRsmith release is <b>v${escape(latest.version)}</b>, released ` +
|
|
528
|
+
`${escape(latest.date.toISOString().substring(0, 10))}. ` +
|
|
529
|
+
'Servers running older FHIRsmith releases are missing security fixes, both in FHIRsmith ' +
|
|
530
|
+
'and in the libraries it depends on, and should be upgraded.</p>';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
html += '<table class="grid">';
|
|
534
|
+
html += '<thead><tr><th>Server</th><th>URL</th><th>FHIR</th><th>Software</th><th>Version</th>' +
|
|
535
|
+
'<th>Released</th><th>Age</th></tr></thead><tbody>';
|
|
536
|
+
for (const row of rows) {
|
|
537
|
+
html += '<tr>';
|
|
538
|
+
html += `<td>${escape(row.name)}</td>`;
|
|
539
|
+
html += `<td><a href="${escape(row.url)}" target="_blank" rel="noopener">${escape(row.url)}</a>`;
|
|
540
|
+
if (row.error) {
|
|
541
|
+
html += ` <span class="text-danger" title="${escape(row.error)}">(unreachable)</span>`;
|
|
542
|
+
}
|
|
543
|
+
html += '</td>';
|
|
544
|
+
html += `<td>${escape(row.fhirVersion)}</td>`;
|
|
545
|
+
html += `<td>${row.software ? escape(row.software.replace('Reference Server', 'HealthIntersections')) : '<i>unknown</i>'}</td>`;
|
|
546
|
+
html += `<td>${row.softwareVersion ? escape(row.softwareVersion) : '<i>unknown</i>'}</td>`;
|
|
547
|
+
html += this._renderReleaseCells(row, now);
|
|
548
|
+
html += '</tr>';
|
|
549
|
+
}
|
|
550
|
+
html += '</tbody></table>';
|
|
551
|
+
|
|
552
|
+
if (this.releases && this.releases.source !== 'none') {
|
|
553
|
+
html += `<p class="text-muted"><small>FHIRsmith release dates from ${escape(this.releases.source)}.</small></p>`;
|
|
554
|
+
}
|
|
555
|
+
return html;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
_renderReleaseCells(row, now) {
|
|
559
|
+
if (!isFhirsmith(row.software) || !this.releases) {
|
|
560
|
+
return this._renderReportedReleaseCells(row, now);
|
|
561
|
+
}
|
|
562
|
+
const d = this.releases.describe(row.softwareVersion, now);
|
|
563
|
+
const behind = d.behind ? ` (${d.behind} release${d.behind === 1 ? '' : 's'} behind)` : '';
|
|
564
|
+
switch (d.status) {
|
|
565
|
+
case 'current':
|
|
566
|
+
return `<td>${d.release.date.toISOString().substring(0, 10)}</td>` +
|
|
567
|
+
`<td><span class="text-success">${describeAge(d.ageDays)} - current release</span></td>`;
|
|
568
|
+
case 'outdated': {
|
|
569
|
+
// more than 3 months, or more than 3 releases, out of date is flagged
|
|
570
|
+
const cls = d.ageDays > 90 || d.behind > 3 ? 'text-danger' : 'text-warning';
|
|
571
|
+
return `<td>${d.release.date.toISOString().substring(0, 10)}</td>` +
|
|
572
|
+
`<td><span class="${cls}">${describeAge(d.ageDays)}${behind}</span></td>`;
|
|
573
|
+
}
|
|
574
|
+
case 'dev':
|
|
575
|
+
return `<td>${this._reportedReleaseDay(row)}</td><td>development build` +
|
|
576
|
+
(d.release ? ` after v${escape(d.release.version)}` : '') + behind + '</td>';
|
|
577
|
+
default:
|
|
578
|
+
return this._renderReportedReleaseCells(row, now);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// what the server says about its release (CapabilityStatement.software.releaseDate) -
|
|
583
|
+
// the only source for software other than FHIRsmith
|
|
584
|
+
_reportedReleaseDay(row) {
|
|
585
|
+
// the day as the server gives it - converting to UTC can move it a day
|
|
586
|
+
const m = /^(\d{4}-\d{2}-\d{2})/.exec(row.softwareReleaseDate || '');
|
|
587
|
+
return m ? m[1] : '';
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
_renderReportedReleaseCells(row, now) {
|
|
591
|
+
const day = this._reportedReleaseDay(row);
|
|
592
|
+
if (!day) {
|
|
593
|
+
return '<td></td><td></td>';
|
|
594
|
+
}
|
|
595
|
+
const days = Math.max(0, Math.floor((now - new Date(row.softwareReleaseDate).getTime()) / (24 * 60 * 60 * 1000)));
|
|
596
|
+
return `<td>${day}</td><td>${describeAge(days)}</td>`;
|
|
597
|
+
}
|
|
598
|
+
|
|
419
599
|
/**
|
|
420
600
|
* Build HTML content for main page
|
|
421
601
|
*/
|
|
@@ -1086,10 +1266,6 @@ class RegistryModule {
|
|
|
1086
1266
|
this.logger.info(`Resolved CodeSystem ${url} for FHIR ${fhirVersion} (usage=${usage}, language=${language || 'none'}): ${matches}`);
|
|
1087
1267
|
}
|
|
1088
1268
|
|
|
1089
|
-
// If only authoritative servers are requested, filter results
|
|
1090
|
-
if (authoritativeOnly === 'true' && result) {
|
|
1091
|
-
result.candidates = [];
|
|
1092
|
-
}
|
|
1093
1269
|
if (acceptsHtml) {
|
|
1094
1270
|
try {
|
|
1095
1271
|
const startTime = Date.now();
|
|
@@ -1456,4 +1632,7 @@ class RegistryModule {
|
|
|
1456
1632
|
}
|
|
1457
1633
|
}
|
|
1458
1634
|
|
|
1635
|
+
RegistryModule.DISCOVERY_PARAMS = DISCOVERY_PARAMS;
|
|
1636
|
+
RegistryModule.RESOLVE_PARAMS = RESOLVE_PARAMS;
|
|
1637
|
+
|
|
1459
1638
|
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;
|
|
@@ -110,10 +111,13 @@ app.use(cors(config.server.cors));
|
|
|
110
111
|
const modules = {};
|
|
111
112
|
|
|
112
113
|
let stats = null;
|
|
114
|
+
// periodic npm advisory check of the installed packages - see library/npm-audit.js
|
|
115
|
+
let npmAudit = null;
|
|
113
116
|
|
|
114
117
|
// Initialize modules based on configuration
|
|
115
118
|
async function initializeModules() {
|
|
116
119
|
stats = new ServerStats(config.stats, serverLog);
|
|
120
|
+
npmAudit = new NpmAudit(config.npmAudit || {}, Logger.getInstance().child({ module: 'npm-audit' }), __dirname, stats);
|
|
117
121
|
|
|
118
122
|
// Initialize SHL module
|
|
119
123
|
if (config.modules?.shl?.enabled) {
|
|
@@ -305,7 +309,8 @@ async function loadTemplates() {
|
|
|
305
309
|
|
|
306
310
|
async function buildRootPageContent() {
|
|
307
311
|
stats.requestCount++;
|
|
308
|
-
let content =
|
|
312
|
+
let content = npmAudit ? npmAudit.renderBanner() : '';
|
|
313
|
+
content += '<div class="row mb-4">';
|
|
309
314
|
content += '<div class="col-12">';
|
|
310
315
|
|
|
311
316
|
content += '<h3>Available Modules</h3>';
|
|
@@ -314,7 +319,7 @@ async function buildRootPageContent() {
|
|
|
314
319
|
// Check which modules are enabled and add them to the list
|
|
315
320
|
if (config.modules.packages.enabled) {
|
|
316
321
|
content += '<li class="list-group-item">';
|
|
317
|
-
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>)';
|
|
318
323
|
content += '</li>';
|
|
319
324
|
}
|
|
320
325
|
|
|
@@ -339,7 +344,7 @@ async function buildRootPageContent() {
|
|
|
339
344
|
if (config.modules.registry && config.modules.registry.enabled) {
|
|
340
345
|
content += '<li class="list-group-item">';
|
|
341
346
|
content += '<a href="/tx-reg" class="text-decoration-none">Terminology Server Registry</a>: ';
|
|
342
|
-
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>)';
|
|
343
348
|
content += '</li>';
|
|
344
349
|
}
|
|
345
350
|
|
|
@@ -374,7 +379,7 @@ async function buildRootPageContent() {
|
|
|
374
379
|
if (config.modules?.testing?.enabled) {
|
|
375
380
|
content += '<li class="list-group-item">';
|
|
376
381
|
content += '<a href="/testing" class="text-decoration-none">Test Reports</a>: ';
|
|
377
|
-
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>)';
|
|
378
383
|
content += '</li>';
|
|
379
384
|
}
|
|
380
385
|
|
|
@@ -402,7 +407,9 @@ async function buildRootPageContent() {
|
|
|
402
407
|
if (config.modules.tx.endpoints && config.modules.tx.endpoints.length > 0) {
|
|
403
408
|
content += '<ul class="mt-2 mb-0">';
|
|
404
409
|
for (const endpoint of config.modules.tx.endpoints) {
|
|
405
|
-
|
|
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>`;
|
|
406
413
|
}
|
|
407
414
|
content += '</ul>';
|
|
408
415
|
}
|
|
@@ -658,6 +665,9 @@ app.get('/dashboard', async (req, res) => {
|
|
|
658
665
|
startTime: stats.startTime
|
|
659
666
|
});
|
|
660
667
|
content += stats.taskDetails();
|
|
668
|
+
if (npmAudit) {
|
|
669
|
+
content += npmAudit.renderDashboard();
|
|
670
|
+
}
|
|
661
671
|
content += `<p>Data: ${folders.dataDir()}</p>`;
|
|
662
672
|
|
|
663
673
|
content = '<div class="row mb-4"><div class="col-12">' + content + '</div></div>';
|
|
@@ -790,6 +800,7 @@ async function startServer() {
|
|
|
790
800
|
stats.markStarted();
|
|
791
801
|
serverLog.info(`=== Server running on http://localhost:${PORT} ===`);
|
|
792
802
|
});
|
|
803
|
+
npmAudit.start();
|
|
793
804
|
if (modules.packages && config.modules.packages.enabled) {
|
|
794
805
|
modules.packages.startInitialCrawler();
|
|
795
806
|
}
|
|
@@ -817,6 +828,9 @@ async function startServer() {
|
|
|
817
828
|
// Graceful shutdown
|
|
818
829
|
process.on('SIGINT', async () => {
|
|
819
830
|
serverLog.info('\nShutting down server...');
|
|
831
|
+
if (npmAudit) {
|
|
832
|
+
npmAudit.stop();
|
|
833
|
+
}
|
|
820
834
|
|
|
821
835
|
// Shutdown all modules
|
|
822
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
|
+
};
|