fhirsmith 0.16.1 → 0.18.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 +130 -0
- package/README.md +105 -30
- package/config-template.json +130 -4
- package/extension-tracker/extension-tracker.js +111 -3
- package/extension-tracker/readme.md +3 -0
- package/ips/ai-client.js +263 -0
- package/ips/composer.js +805 -0
- package/ips/editor-schema.js +289 -0
- package/ips/editor.js +290 -0
- package/ips/fhirpath-ips.js +397 -0
- package/ips/ips-codeable-concepts.md +1742 -0
- package/ips/ips-template.html +125 -0
- package/ips/ips-valuesets.js +159 -0
- package/ips/ips.js +1736 -0
- package/ips/membership.js +179 -0
- package/ips/pipeline.js +1012 -0
- package/ips/presenter.js +974 -0
- package/ips/profile-editor.js +578 -0
- package/ips/profiles.js +506 -0
- package/ips/readme.md +460 -0
- package/ips/render.js +1482 -0
- package/ips/search-params.js +83 -0
- package/ips/search.js +667 -0
- package/ips/share.js +178 -0
- package/ips/store.js +650 -0
- package/ips/terminology.js +395 -0
- package/ips/tx-client.js +202 -0
- package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
- package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
- package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
- package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
- package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
- package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
- package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
- package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
- package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
- package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
- package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
- package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
- package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-medication.json +50 -0
- package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
- package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
- package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
- package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
- package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
- package/ips/valuesets/ValueSet-ips-route.json +46 -0
- package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
- package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
- package/ips/valuesets/elements.json +44 -0
- package/library/daily-report.js +422 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/html.js +9 -0
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +222 -0
- package/library/npm-audit.js +25 -0
- package/library/package-manager.js +4 -2
- package/library/sqlite-session-store.js +29 -1
- package/package.json +8 -2
- package/packages/package-crawler.js +189 -82
- package/packages/packages.js +36 -63
- package/packages/utilities/dedupe-versions.js +22 -8
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +48 -1
- package/registry/documentation/api.md +162 -0
- package/registry/documentation/configuration.md +93 -0
- package/registry/documentation/documents.md +74 -0
- package/registry/documentation/internals.md +151 -0
- package/registry/documentation/readme.md +100 -0
- package/registry/documentation/security.md +199 -0
- package/registry/documents.js +309 -0
- package/registry/openapi.yaml +30 -0
- package/registry/registry-template.html +1 -1
- package/registry/registry.js +247 -54
- package/registry/uptime.js +154 -0
- package/release.sh +80 -0
- package/server.js +105 -44
- package/shl/shl.js +64 -9
- package/stats-db.js +93 -0
- package/stats.js +158 -1
- package/testing/readme.md +1 -1
- package/testing/search.js +2 -108
- package/testing/store.js +5 -0
- package/testing/testing.js +20 -0
- package/token/token-template.html +5 -4
- package/token/token.js +17 -10
- package/translations/Messages.properties +9 -0
- package/tx/README.md +52 -0
- package/tx/cm/cm-database.js +62 -0
- package/tx/cm/cm-package.js +20 -3
- package/tx/cs/cs-api.js +90 -14
- package/tx/cs/cs-cs.js +142 -19
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +418 -0
- package/tx/cs/cs-loinc.js +79 -39
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +918 -55
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/library.js +107 -6
- package/tx/ocl/README.md +62 -6
- package/tx/ocl/cm-ocl.cjs +166 -44
- package/tx/ocl/cs-ocl.cjs +163 -13
- package/tx/ocl/resolve/public-access.js +143 -0
- package/tx/ocl/resolve/reference-resolver.js +458 -0
- package/tx/ocl/vs-ocl.cjs +167 -4
- package/tx/openapi-operations.js +4 -0
- package/tx/params.js +8 -1
- package/tx/problems.js +7 -5
- package/tx/provider.js +21 -5
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +37 -8
- package/tx/tx.fhir.org.yml +2 -3
- package/tx/tx.js +148 -5
- package/tx/vs/vs-database.js +26 -0
- package/tx/vs/vs-package.js +42 -10
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/expand.js +34 -7
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/metadata.js +6 -1
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +128 -6
- package/utilities/make-sct-test-extension.js +264 -0
- package/utilities/submit-test-report.js +1 -1
- package/xig/xig.js +120 -12
- package/gr.sh +0 -3
- package/registry/readme.md +0 -124
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
<a href="/tx-reg/resolve" style="color: gold">Resolve</a> |
|
|
63
63
|
<a href="/tx-reg/software" style="color: gold">Software</a> |
|
|
64
64
|
<a href="/tx-reg/log" style="color: gold">Crawler Log</a> |
|
|
65
|
-
<a href="/tx-reg/openapi" style="color: gold">API</a>
|
|
65
|
+
<a href="/tx-reg/openapi" style="color: gold">API</a>[%account%]
|
|
66
66
|
</div> <!-- /container -->
|
|
67
67
|
</div> <!-- /container -->
|
|
68
68
|
</div>
|
package/registry/registry.js
CHANGED
|
@@ -11,6 +11,8 @@ const folders = require('../library/folder-setup');
|
|
|
11
11
|
const escape = require('escape-html');
|
|
12
12
|
const registryOpenApi = require('./openapi');
|
|
13
13
|
const { FhirsmithReleases, isFhirsmith, describeAge } = require('./fhirsmith-releases');
|
|
14
|
+
const { formatUptime } = require('./uptime');
|
|
15
|
+
const { RegistryAuth } = require('./auth');
|
|
14
16
|
|
|
15
17
|
// The query parameters of the public API. These are the contract published in openapi.yaml,
|
|
16
18
|
// and tests/registry/openapi.test.js checks that the two agree - so change both together.
|
|
@@ -19,6 +21,41 @@ const { FhirsmithReleases, isFhirsmith, describeAge } = require('./fhirsmith-rel
|
|
|
19
21
|
const DISCOVERY_PARAMS = ['registry', 'server', 'fhirVersion', 'url', 'authoritativeOnly', 'language'];
|
|
20
22
|
const RESOLVE_PARAMS = ['fhirVersion', 'url', 'version', 'valueSet', 'authoritativeOnly', 'language', 'usage'];
|
|
21
23
|
|
|
24
|
+
// A link to a URL from a registry list or a crawled server: those are other people's
|
|
25
|
+
// content, so only an http(s) URL is linked, and it is escaped
|
|
26
|
+
function safeHref(url) {
|
|
27
|
+
return typeof url === 'string' && /^https?:\/\//i.test(url.trim()) ? escape(url) : '#';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The base URL that a server's endpoints share: http://tx.fhir.org for http://tx.fhir.org/r4
|
|
32
|
+
* and http://tx.fhir.org/r5. One endpoint is its own base
|
|
33
|
+
*/
|
|
34
|
+
function commonBaseUrl(urls) {
|
|
35
|
+
const list = urls.filter(u => u);
|
|
36
|
+
if (list.length === 0) {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
if (list.length === 1) {
|
|
40
|
+
return list[0];
|
|
41
|
+
}
|
|
42
|
+
let prefix = list[0];
|
|
43
|
+
for (const url of list.slice(1)) {
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < prefix.length && i < url.length && prefix[i] === url[i]) {
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
prefix = prefix.substring(0, i);
|
|
49
|
+
}
|
|
50
|
+
// back up to a path boundary, unless the prefix already is one
|
|
51
|
+
if (!list.every(u => u === prefix || u.startsWith(prefix + '/'))) {
|
|
52
|
+
prefix = prefix.substring(0, prefix.lastIndexOf('/'));
|
|
53
|
+
}
|
|
54
|
+
prefix = prefix.replace(/\/+$/, '');
|
|
55
|
+
// less than scheme + host is not a base URL at all
|
|
56
|
+
return /^https?:\/\/[^/]+/.test(prefix) ? prefix : list[0];
|
|
57
|
+
}
|
|
58
|
+
|
|
22
59
|
class RegistryModule {
|
|
23
60
|
constructor(stats) {
|
|
24
61
|
this.router = express.Router();
|
|
@@ -35,6 +72,8 @@ class RegistryModule {
|
|
|
35
72
|
this.currentData = null;
|
|
36
73
|
this.dataLock = false;
|
|
37
74
|
this.stats = stats;
|
|
75
|
+
// open until initialize() reads modules.registry.security
|
|
76
|
+
this.auth = new RegistryAuth({}, { logger: this.logger, renderPage: () => {} });
|
|
38
77
|
}
|
|
39
78
|
|
|
40
79
|
/**
|
|
@@ -50,7 +89,10 @@ class RegistryModule {
|
|
|
50
89
|
masterUrl: config.masterUrl || 'https://fhir.github.io/ig-registry/tx-servers.json',
|
|
51
90
|
timeout: config.timeout || 30000,
|
|
52
91
|
userAgent: config.userAgent || 'FHIRRegistryServer/1.0',
|
|
53
|
-
apiKeys: config.apiKeys || {}
|
|
92
|
+
apiKeys: config.apiKeys || {},
|
|
93
|
+
maxResponseMB: config.maxResponseMB,
|
|
94
|
+
uptimeWindowDays: config.uptimeWindowDays,
|
|
95
|
+
uptimeRetentionDays: config.uptimeRetentionDays
|
|
54
96
|
};
|
|
55
97
|
|
|
56
98
|
this.crawler = new RegistryCrawler(crawlerConfig, this.stats);
|
|
@@ -69,6 +111,17 @@ class RegistryModule {
|
|
|
69
111
|
// Load saved data if available
|
|
70
112
|
await this.loadSavedData();
|
|
71
113
|
|
|
114
|
+
// Securing the API (see auth.js). With no modules.registry.security, the API is open
|
|
115
|
+
this.auth = new RegistryAuth(config.security, {
|
|
116
|
+
logger: this.logger,
|
|
117
|
+
renderPage: (req, res, title, content, status) => {
|
|
118
|
+
res.status(status || 200);
|
|
119
|
+
res.setHeader('Content-Type', 'text/html');
|
|
120
|
+
res.send(this.renderPage('registry', title, content, this.api.getStatistics(), req));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
await this.auth.initialize();
|
|
124
|
+
|
|
72
125
|
// Set up routes
|
|
73
126
|
this.setupRoutes();
|
|
74
127
|
|
|
@@ -106,6 +159,13 @@ class RegistryModule {
|
|
|
106
159
|
} catch (error) {
|
|
107
160
|
this.logger.info('No saved registry data found, will fetch fresh data');
|
|
108
161
|
}
|
|
162
|
+
try {
|
|
163
|
+
const fs = require('fs').promises;
|
|
164
|
+
const uptimePath = folders.ensureFilePath('registry', 'registry-uptime.json');
|
|
165
|
+
this.crawler.loadUptime(JSON.parse(await fs.readFile(uptimePath, 'utf8')));
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.logger.info('No saved uptime history found');
|
|
168
|
+
}
|
|
109
169
|
}
|
|
110
170
|
|
|
111
171
|
/**
|
|
@@ -118,6 +178,8 @@ class RegistryModule {
|
|
|
118
178
|
|
|
119
179
|
const data = this.crawler.saveData();
|
|
120
180
|
await fs.writeFile(dataPath, JSON.stringify(data, null, 2));
|
|
181
|
+
const uptimePath = folders.ensureFilePath('registry', 'registry-uptime.json');
|
|
182
|
+
await fs.writeFile(uptimePath, JSON.stringify(this.crawler.saveUptime()));
|
|
121
183
|
this.logger.debug('Saved registry data to disk');
|
|
122
184
|
} catch (error) {
|
|
123
185
|
this.logger.error('Failed to save registry data:', error);
|
|
@@ -248,11 +310,23 @@ class RegistryModule {
|
|
|
248
310
|
next();
|
|
249
311
|
});
|
|
250
312
|
|
|
313
|
+
// The JSON API is what security protects; the HTML pages stay open
|
|
314
|
+
const guard = this.auth.apiGuard();
|
|
315
|
+
const apiGuard = (req, res, next) => {
|
|
316
|
+
const acceptsHtml = req.headers.accept && req.headers.accept.includes('text/html');
|
|
317
|
+
return acceptsHtml ? next() : guard(req, res, next);
|
|
318
|
+
};
|
|
319
|
+
// failed authentications are limited (successful ones aren't counted)
|
|
320
|
+
const apiLimiter = this.auth.apiLimiter();
|
|
321
|
+
// the session, where there are users, so the pages know who is logged in (it makes none)
|
|
322
|
+
const session = this.auth.optionalSession();
|
|
323
|
+
this.auth.setupRoutes(this.router);
|
|
324
|
+
|
|
251
325
|
// Main registry page
|
|
252
|
-
this.router.get('/', this.handleMainPage.bind(this));
|
|
253
|
-
this.router.get('/resolve', this.handleResolveEndpoint.bind(this));
|
|
254
|
-
this.router.get('/log', this.handleLogEndpoint.bind(this));
|
|
255
|
-
this.router.get('/software', this.handleSoftwarePage.bind(this));
|
|
326
|
+
this.router.get('/', apiLimiter, apiGuard, session, this.handleMainPage.bind(this));
|
|
327
|
+
this.router.get('/resolve', apiLimiter, apiGuard, session, this.handleResolveEndpoint.bind(this));
|
|
328
|
+
this.router.get('/log', session, this.handleLogEndpoint.bind(this));
|
|
329
|
+
this.router.get('/software', session, this.handleSoftwarePage.bind(this));
|
|
256
330
|
|
|
257
331
|
// OpenAPI description of this API: /openapi.json, /openapi.yaml, and /openapi (an HTML
|
|
258
332
|
// reference for browsers, the JSON otherwise)
|
|
@@ -273,8 +347,8 @@ class RegistryModule {
|
|
|
273
347
|
if (!htmlServer.hasTemplate('registry')) {
|
|
274
348
|
htmlServer.loadTemplate('registry', path.join(__dirname, 'registry-template.html'));
|
|
275
349
|
}
|
|
276
|
-
const html =
|
|
277
|
-
registryOpenApi.renderHtml(), this.api ? this.api.getStatistics() : {});
|
|
350
|
+
const html = this.renderPage('registry', 'Terminology Server Registry API',
|
|
351
|
+
registryOpenApi.renderHtml(), this.api ? this.api.getStatistics() : {}, req);
|
|
278
352
|
res.setHeader('Content-Type', 'text/html');
|
|
279
353
|
res.send(html);
|
|
280
354
|
} catch (error) {
|
|
@@ -284,6 +358,18 @@ class RegistryModule {
|
|
|
284
358
|
});
|
|
285
359
|
}
|
|
286
360
|
|
|
361
|
+
/**
|
|
362
|
+
* The registry's pages: html-server's template, plus the login/account link in the
|
|
363
|
+
* navbar (pages rendered without a request show the login link)
|
|
364
|
+
*/
|
|
365
|
+
renderPage(template, title, content, stats, req) {
|
|
366
|
+
if (template === 'registry' && !htmlServer.hasTemplate('registry')) {
|
|
367
|
+
htmlServer.loadTemplate('registry', path.join(__dirname, 'registry-template.html'));
|
|
368
|
+
}
|
|
369
|
+
const html = htmlServer.renderPage(template, title, content, stats);
|
|
370
|
+
return html.replace('[%account%]', () => this.auth ? this.auth.navLinks(req || {}) : '');
|
|
371
|
+
}
|
|
372
|
+
|
|
287
373
|
/**
|
|
288
374
|
* Render HTML page for code system or value set query
|
|
289
375
|
* Combines functionality from sendHtmlCS and sendHtmlVS
|
|
@@ -334,14 +420,15 @@ class RegistryModule {
|
|
|
334
420
|
htmlServer.loadTemplate('registry', templatePath);
|
|
335
421
|
}
|
|
336
422
|
|
|
337
|
-
return
|
|
423
|
+
return this.renderPage(
|
|
338
424
|
'registry',
|
|
339
425
|
'FHIR Terminology Server Registry',
|
|
340
426
|
this.buildHtmlContent(),
|
|
341
427
|
{
|
|
342
428
|
...this.api.getStatistics(),
|
|
343
429
|
templateVars: templateVars
|
|
344
|
-
}
|
|
430
|
+
},
|
|
431
|
+
req
|
|
345
432
|
);
|
|
346
433
|
} catch (error) {
|
|
347
434
|
this.logger.error('Error rendering page:', error);
|
|
@@ -451,11 +538,11 @@ class RegistryModule {
|
|
|
451
538
|
stats.crawlInProgress = this.crawlInProgress;
|
|
452
539
|
stats.lastCrawl = this.lastCrawlTime;
|
|
453
540
|
|
|
454
|
-
const html =
|
|
541
|
+
const html = this.renderPage(
|
|
455
542
|
'registry',
|
|
456
543
|
'FHIR Terminology Server Registry',
|
|
457
544
|
content,
|
|
458
|
-
stats
|
|
545
|
+
stats, req
|
|
459
546
|
);
|
|
460
547
|
|
|
461
548
|
res.setHeader('Content-Type', 'text/html');
|
|
@@ -484,7 +571,7 @@ class RegistryModule {
|
|
|
484
571
|
const content = this.buildSoftwareContent();
|
|
485
572
|
const stats = this.api.getStatistics();
|
|
486
573
|
stats.processingTime = Date.now() - startTime;
|
|
487
|
-
const html =
|
|
574
|
+
const html = this.renderPage('registry', 'Terminology Server Software', content, stats, req);
|
|
488
575
|
res.setHeader('Content-Type', 'text/html');
|
|
489
576
|
res.send(html);
|
|
490
577
|
} catch (error) {
|
|
@@ -502,24 +589,8 @@ class RegistryModule {
|
|
|
502
589
|
'<p>The initial crawl is in progress. Please refresh in a moment.</p></div>';
|
|
503
590
|
}
|
|
504
591
|
|
|
505
|
-
const rows =
|
|
506
|
-
|
|
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));
|
|
592
|
+
const rows = this.softwareRows(data);
|
|
593
|
+
const uptime = this.crawler ? this.crawler.uptime : null;
|
|
523
594
|
|
|
524
595
|
let html = '<h3>Terminology Server Software</h3>';
|
|
525
596
|
const latest = this.releases ? this.releases.latest() : null;
|
|
@@ -532,29 +603,102 @@ class RegistryModule {
|
|
|
532
603
|
|
|
533
604
|
html += '<table class="grid">';
|
|
534
605
|
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>';
|
|
606
|
+
'<th>Released</th><th>Age</th><th>Uptime</th></tr></thead><tbody>';
|
|
536
607
|
for (const row of rows) {
|
|
537
608
|
html += '<tr>';
|
|
538
609
|
html += `<td>${escape(row.name)}</td>`;
|
|
539
|
-
html += `<td><a href="${
|
|
540
|
-
|
|
541
|
-
html += ` <span class="text-danger" title="${escape(row.error)}">(unreachable)</span>`;
|
|
542
|
-
}
|
|
610
|
+
html += `<td><a href="${safeHref(row.url)}" target="_blank" rel="noopener">${escape(row.url)}</a>`;
|
|
611
|
+
html += this._renderUnreachable(row);
|
|
543
612
|
html += '</td>';
|
|
544
|
-
html += `<td>${escape(row.
|
|
613
|
+
html += `<td>${escape(row.fhirVersions.join(', '))}</td>`;
|
|
545
614
|
html += `<td>${row.software ? escape(row.software.replace('Reference Server', 'HealthIntersections')) : '<i>unknown</i>'}</td>`;
|
|
546
615
|
html += `<td>${row.softwareVersion ? escape(row.softwareVersion) : '<i>unknown</i>'}</td>`;
|
|
547
616
|
html += this._renderReleaseCells(row, now);
|
|
617
|
+
html += this._renderUptimeCell(uptime ? uptime.uptime(row.endpoints.map(e => e.url), now) : null);
|
|
548
618
|
html += '</tr>';
|
|
549
619
|
}
|
|
550
620
|
html += '</tbody></table>';
|
|
551
621
|
|
|
622
|
+
html += '<p class="text-muted"><small>';
|
|
623
|
+
if (uptime) {
|
|
624
|
+
const days = Math.round(uptime.windowMs / (24 * 60 * 60 * 1000));
|
|
625
|
+
html += `Uptime is the share of this registry's scans over the last ${days} days that the server answered ` +
|
|
626
|
+
'(every scan probes each of its endpoints; a server with one of two endpoints down is half down for that scan). ' +
|
|
627
|
+
'Scans the registry itself missed are not counted. ';
|
|
628
|
+
}
|
|
629
|
+
html += 'Endpoints of one server that run the same software are shown as one row. ';
|
|
552
630
|
if (this.releases && this.releases.source !== 'none') {
|
|
553
|
-
html +=
|
|
631
|
+
html += `FHIRsmith release dates from ${escape(this.releases.source)}.`;
|
|
554
632
|
}
|
|
633
|
+
html += '</small></p>';
|
|
555
634
|
return html;
|
|
556
635
|
}
|
|
557
636
|
|
|
637
|
+
/**
|
|
638
|
+
* The rows of the software page: one per server, or per software a server runs, since
|
|
639
|
+
* a server's endpoints (its FHIR versions) are nearly always the same software
|
|
640
|
+
* @returns {Array<{name, url, fhirVersions, software, softwareVersion, softwareReleaseDate, endpoints}>}
|
|
641
|
+
*/
|
|
642
|
+
softwareRows(data) {
|
|
643
|
+
const rows = [];
|
|
644
|
+
for (const registry of data.registries) {
|
|
645
|
+
for (const server of registry.servers || []) {
|
|
646
|
+
const groups = new Map(); // software|version -> row
|
|
647
|
+
for (const version of server.versions || []) {
|
|
648
|
+
const software = version.software && version.software !== 'unknown' ? version.software : '';
|
|
649
|
+
const softwareVersion = version.softwareVersion || '';
|
|
650
|
+
const key = `${software}|${softwareVersion}`;
|
|
651
|
+
let row = groups.get(key);
|
|
652
|
+
if (!row) {
|
|
653
|
+
row = {
|
|
654
|
+
name: server.name || server.code || '',
|
|
655
|
+
url: '',
|
|
656
|
+
fhirVersions: [],
|
|
657
|
+
software,
|
|
658
|
+
softwareVersion,
|
|
659
|
+
softwareReleaseDate: version.softwareReleaseDate || '',
|
|
660
|
+
endpoints: []
|
|
661
|
+
};
|
|
662
|
+
groups.set(key, row);
|
|
663
|
+
rows.push(row);
|
|
664
|
+
}
|
|
665
|
+
row.fhirVersions.push(version.version || '');
|
|
666
|
+
row.endpoints.push({ url: version.address || '', fhirVersion: version.version || '', error: version.error || '' });
|
|
667
|
+
if (!row.softwareReleaseDate && version.softwareReleaseDate) {
|
|
668
|
+
row.softwareReleaseDate = version.softwareReleaseDate;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
for (const row of groups.values()) {
|
|
672
|
+
row.url = commonBaseUrl(row.endpoints.map(e => e.url));
|
|
673
|
+
row.fhirVersions.sort();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
rows.sort((a, b) => a.name.localeCompare(b.name) || a.url.localeCompare(b.url));
|
|
678
|
+
return rows;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
_renderUnreachable(row) {
|
|
682
|
+
const down = row.endpoints.filter(e => e.error);
|
|
683
|
+
if (down.length === 0) {
|
|
684
|
+
return '';
|
|
685
|
+
}
|
|
686
|
+
const title = down.map(e => `${e.fhirVersion}: ${e.error}`).join('\n');
|
|
687
|
+
const what = down.length === row.endpoints.length ? 'unreachable' :
|
|
688
|
+
`${down.map(e => e.fhirVersion).join(', ')} unreachable`;
|
|
689
|
+
return ` <span class="text-danger" title="${escape(title)}">(${escape(what)})</span>`;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
_renderUptimeCell(u) {
|
|
693
|
+
if (!u) {
|
|
694
|
+
return '<td></td>';
|
|
695
|
+
}
|
|
696
|
+
const cls = u.failures === 0 ? 'text-success' : u.percent >= 99 ? '' : u.percent >= 95 ? 'text-warning' : 'text-danger';
|
|
697
|
+
const since = new Date(u.since).toISOString().substring(0, 10);
|
|
698
|
+
const title = `${u.probes - u.failures} of ${u.probes} probes answered since ${since}`;
|
|
699
|
+
return `<td><span class="${cls}" title="${escape(title)}">${formatUptime(u)}</span></td>`;
|
|
700
|
+
}
|
|
701
|
+
|
|
558
702
|
_renderReleaseCells(row, now) {
|
|
559
703
|
if (!isFhirsmith(row.software) || !this.releases) {
|
|
560
704
|
return this._renderReportedReleaseCells(row, now);
|
|
@@ -682,7 +826,7 @@ class RegistryModule {
|
|
|
682
826
|
|
|
683
827
|
for (const server of serverVersions) {
|
|
684
828
|
html += '<tr>';
|
|
685
|
-
html += `<td><a href="${server.serverUrl}" target="_blank">${escape(server.serverUrl)}</a></td>`;
|
|
829
|
+
html += `<td><a href="${safeHref(server.serverUrl)}" target="_blank">${escape(server.serverUrl)}</a></td>`;
|
|
686
830
|
html += `<td>${escape(server.software.replace("Reference Server", "HealthIntersections"))}</td>`;
|
|
687
831
|
html += `<td>${escape(server.authority.replace("Published by", ""))}</td>`;
|
|
688
832
|
html += `<td>${escape(server.version)}</td>`;
|
|
@@ -1032,7 +1176,7 @@ class RegistryModule {
|
|
|
1032
1176
|
const rowspan = cs.servers.length;
|
|
1033
1177
|
html += '<tr>';
|
|
1034
1178
|
html += `<td rowspan="${rowspan}">${this._highlightWildcard(escape(formattedMask))}</td>`;
|
|
1035
|
-
html += `<td><a href="${
|
|
1179
|
+
html += `<td><a href="${safeHref(cs.servers[0].url)}" target="_blank">${escape(cs.servers[0].url)}</a>${this._renderLanguageScope(cs.servers[0])}</td>`;
|
|
1036
1180
|
|
|
1037
1181
|
// Format versions as R3/R4/R5
|
|
1038
1182
|
const formattedVersions = cs.servers[0].versions.map(v => this._formatFhirVersion(v));
|
|
@@ -1042,7 +1186,7 @@ class RegistryModule {
|
|
|
1042
1186
|
// Additional rows for this code system (if any)
|
|
1043
1187
|
for (let i = 1; i < cs.servers.length; i++) {
|
|
1044
1188
|
html += '<tr>';
|
|
1045
|
-
html += `<td><a href="${
|
|
1189
|
+
html += `<td><a href="${safeHref(cs.servers[i].url)}" target="_blank">${escape(cs.servers[i].url)}</a>${this._renderLanguageScope(cs.servers[i])}</td>`;
|
|
1046
1190
|
|
|
1047
1191
|
// Format versions as R3/R4/R5
|
|
1048
1192
|
const formattedVersions = cs.servers[i].versions.map(v => this._formatFhirVersion(v));
|
|
@@ -1088,7 +1232,7 @@ class RegistryModule {
|
|
|
1088
1232
|
const rowspan = vs.servers.length;
|
|
1089
1233
|
html += '<tr>';
|
|
1090
1234
|
html += `<td rowspan="${rowspan}">${this._highlightWildcard(escape(vs.mask))}</td>`;
|
|
1091
|
-
html += `<td><a href="${
|
|
1235
|
+
html += `<td><a href="${safeHref(vs.servers[0].url)}" target="_blank">${escape(vs.servers[0].url)}</a>${this._renderLanguageScope(vs.servers[0])}</td>`;
|
|
1092
1236
|
|
|
1093
1237
|
// Format versions as R3/R4/R5
|
|
1094
1238
|
const formattedVersions = vs.servers[0].versions.map(v => this._formatFhirVersion(v));
|
|
@@ -1098,7 +1242,7 @@ class RegistryModule {
|
|
|
1098
1242
|
// Additional rows for this value set (if any)
|
|
1099
1243
|
for (let i = 1; i < vs.servers.length; i++) {
|
|
1100
1244
|
html += '<tr>';
|
|
1101
|
-
html += `<td><a href="${
|
|
1245
|
+
html += `<td><a href="${safeHref(vs.servers[i].url)}" target="_blank">${escape(vs.servers[i].url)}</a>${this._renderLanguageScope(vs.servers[i])}</td>`;
|
|
1102
1246
|
|
|
1103
1247
|
// Format versions as R3/R4/R5
|
|
1104
1248
|
const formattedVersions = vs.servers[i].versions.map(v => this._formatFhirVersion(v));
|
|
@@ -1133,6 +1277,49 @@ class RegistryModule {
|
|
|
1133
1277
|
};
|
|
1134
1278
|
}
|
|
1135
1279
|
|
|
1280
|
+
/**
|
|
1281
|
+
* The paragraph for the server's daily report: how many client requests the
|
|
1282
|
+
* API answered, how many servers are registered, and which of them (if any)
|
|
1283
|
+
* answered none of the registry's scans in the period.
|
|
1284
|
+
*/
|
|
1285
|
+
dailyReport({since, now}) {
|
|
1286
|
+
const {plural} = require('../library/daily-report');
|
|
1287
|
+
const counts = this.stats.countsSince(since, now);
|
|
1288
|
+
const api = counts.operations.resolve || 0;
|
|
1289
|
+
const sentences = [`The API answered ${plural(api, 'client request')} (${plural(counts.total, 'request')} in all, including the web pages).`];
|
|
1290
|
+
const data = this.api ? this.api.getData() : null;
|
|
1291
|
+
if (!data || !data.registries) {
|
|
1292
|
+
sentences.push('The registry data is not available (no crawl has completed).');
|
|
1293
|
+
return sentences.join(' ');
|
|
1294
|
+
}
|
|
1295
|
+
let servers = 0;
|
|
1296
|
+
let endpoints = 0;
|
|
1297
|
+
const down = [];
|
|
1298
|
+
const uptime = this.crawler ? this.crawler.uptime : null;
|
|
1299
|
+
for (const registry of data.registries) {
|
|
1300
|
+
for (const server of registry.servers || []) {
|
|
1301
|
+
servers++;
|
|
1302
|
+
const addresses = (server.versions || []).map(v => v.address || v.url).filter(Boolean);
|
|
1303
|
+
endpoints += addresses.length;
|
|
1304
|
+
if (uptime && addresses.length > 0) {
|
|
1305
|
+
const u = uptime.uptime(addresses, now, since);
|
|
1306
|
+
if (u && u.failures === u.probes) {
|
|
1307
|
+
down.push(server.name || server.code || addresses[0]);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
sentences.push(`${plural(servers, 'server')} ${servers === 1 ? 'is' : 'are'} registered, with ${plural(endpoints, 'endpoint')}.`);
|
|
1313
|
+
if (!uptime) {
|
|
1314
|
+
sentences.push('No scan history is kept, so availability is unknown.');
|
|
1315
|
+
} else if (down.length === 0) {
|
|
1316
|
+
sentences.push('Every server answered at least one scan during the period.');
|
|
1317
|
+
} else {
|
|
1318
|
+
sentences.push(`${down.length === 1 ? 'This server' : 'These servers'} did not answer any scan during the period: ${down.join(', ')}.`);
|
|
1319
|
+
}
|
|
1320
|
+
return sentences.join(' ');
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1136
1323
|
/**
|
|
1137
1324
|
* Shutdown the module
|
|
1138
1325
|
*/
|
|
@@ -1148,6 +1335,9 @@ class RegistryModule {
|
|
|
1148
1335
|
if (this.crawler) {
|
|
1149
1336
|
this.crawler.shutdown();
|
|
1150
1337
|
}
|
|
1338
|
+
if (this.auth) {
|
|
1339
|
+
this.auth.shutdown();
|
|
1340
|
+
}
|
|
1151
1341
|
// Save current data
|
|
1152
1342
|
if (this.crawler && this.currentData) {
|
|
1153
1343
|
await this.saveData();
|
|
@@ -1232,11 +1422,11 @@ class RegistryModule {
|
|
|
1232
1422
|
const stats = this.api.getStatistics();
|
|
1233
1423
|
stats.processingTime = Date.now() - startTime;
|
|
1234
1424
|
|
|
1235
|
-
const html =
|
|
1425
|
+
const html = this.renderPage(
|
|
1236
1426
|
'registry',
|
|
1237
1427
|
'FHIR Terminology Server Resolver',
|
|
1238
1428
|
content,
|
|
1239
|
-
stats
|
|
1429
|
+
stats, req
|
|
1240
1430
|
);
|
|
1241
1431
|
|
|
1242
1432
|
res.setHeader('Content-Type', 'text/html');
|
|
@@ -1287,18 +1477,20 @@ class RegistryModule {
|
|
|
1287
1477
|
const stats = this.api.getStatistics();
|
|
1288
1478
|
stats.processingTime = Date.now() - startTime;
|
|
1289
1479
|
|
|
1290
|
-
const html =
|
|
1480
|
+
const html = this.renderPage(
|
|
1291
1481
|
'registry',
|
|
1292
1482
|
'FHIR Terminology Server Resolution Results',
|
|
1293
1483
|
content,
|
|
1294
|
-
stats
|
|
1484
|
+
stats, req
|
|
1295
1485
|
);
|
|
1296
1486
|
|
|
1297
1487
|
res.setHeader('Content-Type', 'text/html');
|
|
1298
1488
|
return res.send(html);
|
|
1299
1489
|
} catch (error) {
|
|
1300
1490
|
this.logger.error('Error rendering resolve result page:', error);
|
|
1301
|
-
//
|
|
1491
|
+
// not the JSON instead: this request was let through as a page, without a token
|
|
1492
|
+
res.status(500).setHeader('Content-Type', 'text/html');
|
|
1493
|
+
return res.send('<!DOCTYPE html><html><body><p>The resolution page could not be shown (see the server log).</p></body></html>');
|
|
1302
1494
|
}
|
|
1303
1495
|
}
|
|
1304
1496
|
res.json(result);
|
|
@@ -1322,7 +1514,7 @@ class RegistryModule {
|
|
|
1322
1514
|
html += '<div class="card-body">';
|
|
1323
1515
|
html += `<p><strong>FHIR Version:</strong> ${escape(fhirVersion)}</p>`;
|
|
1324
1516
|
html += `<p><strong>Resource URL:</strong> ${escape(resourceUrl)}</p>`;
|
|
1325
|
-
html += `<p><strong>Registry URL:</strong> <a href="${result['registry-url']}" target="_blank">${escape(result['registry-url'])}</a></p>`;
|
|
1517
|
+
html += `<p><strong>Registry URL:</strong> <a href="${safeHref(result['registry-url'])}" target="_blank">${escape(result['registry-url'])}</a></p>`;
|
|
1326
1518
|
if (usage) {
|
|
1327
1519
|
html += `<p><strong>Usage:</strong> ${escape(usage)}</p>`;
|
|
1328
1520
|
}
|
|
@@ -1355,7 +1547,7 @@ class RegistryModule {
|
|
|
1355
1547
|
result.authoritative.forEach(server => {
|
|
1356
1548
|
html += '<tr>';
|
|
1357
1549
|
html += `<td>${escape(server['server-name'])}</td>`;
|
|
1358
|
-
html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
|
|
1550
|
+
html += `<td><a href="${safeHref(server.url)}" target="_blank">${escape(server.url)}</a></td>`;
|
|
1359
1551
|
html += `<td>${this.renderSecurityTags(server)}</td>`;
|
|
1360
1552
|
html += `<td>${server.languages ? escape(server.languages.join(', ')) : ''}</td>`;
|
|
1361
1553
|
html += `<td>${server.access_info ? escape(server.access_info) : ''}</td>`;
|
|
@@ -1395,7 +1587,7 @@ class RegistryModule {
|
|
|
1395
1587
|
result.candidates.forEach(server => {
|
|
1396
1588
|
html += '<tr>';
|
|
1397
1589
|
html += `<td>${escape(server['server-name'])}</td>`;
|
|
1398
|
-
html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
|
|
1590
|
+
html += `<td><a href="${safeHref(server.url)}" target="_blank">${escape(server.url)}</a></td>`;
|
|
1399
1591
|
html += `<td>${this.renderSecurityTags(server)}</td>`;
|
|
1400
1592
|
if (server['language-support'] === 'unknown') {
|
|
1401
1593
|
html += '<td><span class="text-muted" title="This server hosts the code system, but the ecosystem does not know whether it can supply the requested language">unknown</span></td>';
|
|
@@ -1559,18 +1751,18 @@ class RegistryModule {
|
|
|
1559
1751
|
const stats = this.api.getStatistics();
|
|
1560
1752
|
stats.processingTime = Date.now() - startTime;
|
|
1561
1753
|
|
|
1562
|
-
const html =
|
|
1754
|
+
const html = this.renderPage(
|
|
1563
1755
|
'registry',
|
|
1564
1756
|
'FHIR Terminology Server Registry - Logs',
|
|
1565
1757
|
content,
|
|
1566
|
-
stats
|
|
1758
|
+
stats, req
|
|
1567
1759
|
);
|
|
1568
1760
|
|
|
1569
1761
|
res.setHeader('Content-Type', 'text/html');
|
|
1570
1762
|
res.send(html);
|
|
1571
1763
|
} catch (error) {
|
|
1572
1764
|
this.logger.error('Error rendering log page:', error);
|
|
1573
|
-
res.status(500).send(`<pre>Error rendering log page: ${error.message}</pre>`);
|
|
1765
|
+
res.status(500).send(`<pre>Error rendering log page: ${escape(error.message)}</pre>`);
|
|
1574
1766
|
}
|
|
1575
1767
|
} else {
|
|
1576
1768
|
// Return JSON logs
|
|
@@ -1642,4 +1834,5 @@ class RegistryModule {
|
|
|
1642
1834
|
RegistryModule.DISCOVERY_PARAMS = DISCOVERY_PARAMS;
|
|
1643
1835
|
RegistryModule.RESOLVE_PARAMS = RESOLVE_PARAMS;
|
|
1644
1836
|
|
|
1645
|
-
module.exports = RegistryModule;
|
|
1837
|
+
module.exports = RegistryModule;
|
|
1838
|
+
module.exports.commonBaseUrl = commonBaseUrl;
|