fhirsmith 0.3.0 → 0.5.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 +24 -0
- package/README.md +4 -2
- package/library/cron-utilities.js +136 -0
- package/library/folder-setup.js +6 -0
- package/library/html-server.js +13 -29
- package/library/html.js +3 -8
- package/library/languages.js +160 -37
- package/library/package-manager.js +48 -1
- package/library/utilities.js +100 -19
- package/package.json +2 -2
- package/packages/package-crawler.js +6 -1
- package/packages/packages.js +38 -54
- package/publisher/publisher.js +19 -27
- package/registry/api.js +11 -10
- package/registry/crawler.js +31 -29
- package/registry/model.js +5 -26
- package/registry/readme.md +1 -11
- package/registry/registry.js +32 -41
- package/server.js +53 -5
- package/shl/shl.js +0 -18
- package/static/assets/js/statuspage.js +1 -9
- package/stats.js +39 -1
- package/token/token.js +14 -9
- package/translations/Messages.properties +2 -1
- package/tx/README.md +17 -6
- package/tx/cs/cs-api.js +19 -1
- package/tx/cs/cs-base.js +77 -0
- package/tx/cs/cs-country.js +46 -0
- package/tx/cs/cs-cpt.js +9 -5
- package/tx/cs/cs-cs.js +27 -13
- package/tx/cs/cs-db.js +0 -13
- package/tx/cs/cs-lang.js +60 -22
- package/tx/cs/cs-loinc.js +69 -98
- package/tx/cs/cs-mimetypes.js +4 -0
- package/tx/cs/cs-ndc.js +6 -0
- package/tx/cs/cs-omop.js +16 -15
- package/tx/cs/cs-rxnorm.js +23 -1
- package/tx/cs/cs-snomed.js +283 -40
- package/tx/cs/cs-ucum.js +90 -70
- package/tx/importers/import-sct.module.js +371 -35
- package/tx/importers/readme.md +117 -7
- package/tx/library/bundle.js +5 -0
- package/tx/library/capabilitystatement.js +3 -142
- package/tx/library/codesystem.js +19 -173
- package/tx/library/conceptmap.js +4 -218
- package/tx/library/designations.js +14 -1
- package/tx/library/extensions.js +7 -0
- package/tx/library/namingsystem.js +3 -89
- package/tx/library/operation-outcome.js +8 -3
- package/tx/library/parameters.js +3 -2
- package/tx/library/renderer.js +10 -6
- package/tx/library/terminologycapabilities.js +3 -243
- package/tx/library/valueset.js +3 -235
- package/tx/library.js +100 -13
- package/tx/operation-context.js +23 -4
- package/tx/params.js +35 -38
- package/tx/provider.js +6 -5
- package/tx/sct/expressions.js +12 -3
- package/tx/tx-html.js +80 -89
- package/tx/tx.fhir.org.yml +6 -5
- package/tx/tx.js +163 -13
- package/tx/vs/vs-database.js +56 -39
- package/tx/vs/vs-package.js +21 -2
- package/tx/vs/vs-vsac.js +175 -39
- package/tx/workers/batch-validate.js +2 -0
- package/tx/workers/batch.js +2 -0
- package/tx/workers/expand.js +132 -112
- package/tx/workers/lookup.js +33 -14
- package/tx/workers/metadata.js +2 -2
- package/tx/workers/read.js +3 -2
- package/tx/workers/related.js +574 -0
- package/tx/workers/search.js +46 -9
- package/tx/workers/subsumes.js +13 -3
- package/tx/workers/translate.js +7 -3
- package/tx/workers/validate.js +258 -285
- package/tx/workers/worker.js +43 -39
- package/tx/xml/bundle-xml.js +237 -0
- package/tx/xml/xml-base.js +215 -64
- package/tx/xversion/xv-bundle.js +71 -0
- package/tx/xversion/xv-capabiliityStatement.js +137 -0
- package/tx/xversion/xv-codesystem.js +169 -0
- package/tx/xversion/xv-conceptmap.js +224 -0
- package/tx/xversion/xv-namingsystem.js +88 -0
- package/tx/xversion/xv-operationoutcome.js +27 -0
- package/tx/xversion/xv-parameters.js +87 -0
- package/tx/xversion/xv-resource.js +45 -0
- package/tx/xversion/xv-terminologyCapabilities.js +214 -0
- package/tx/xversion/xv-valueset.js +234 -0
- package/utilities/dev-proxy-server.js +126 -0
- package/utilities/explode-results.js +58 -0
- package/utilities/split-by-system.js +198 -0
- package/utilities/vsac-cs-fetcher.js +0 -0
- package/{windows-install.js → utilities/windows-install.js} +2 -0
- package/vcl/vcl.js +0 -18
- package/xig/xig.js +108 -99
- package/passwords.ini +0 -2
- package/registry/registry-data.json +0 -121015
- package/shl/private-key.pem +0 -5
- package/shl/public-key.pem +0 -18
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/tx/dev.fhir.org.yml +0 -14
- package/tx/fixtures/test-cases-setup.json +0 -18
- package/tx/fixtures/test-cases.yml +0 -16
package/registry/api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Enhanced registry-api.js with resolver and HTML rendering functions
|
|
2
2
|
|
|
3
3
|
const { ServerRegistryUtilities } = require('./model');
|
|
4
|
+
const escape = require('escape-html');
|
|
4
5
|
|
|
5
6
|
class RegistryAPI {
|
|
6
7
|
constructor(crawler) {
|
|
@@ -631,19 +632,19 @@ class RegistryAPI {
|
|
|
631
632
|
html += '<tr>\n';
|
|
632
633
|
|
|
633
634
|
if (!regCode) {
|
|
634
|
-
html += `<td><a href="${path}®istry=${row['registry-code']}">${
|
|
635
|
+
html += `<td><a href="${path}®istry=${row['registry-code']}">${escape(row['registry-name'])}</a></td>\n`;
|
|
635
636
|
}
|
|
636
637
|
if (!serverCode) {
|
|
637
|
-
html += `<td><a href="${path}&server=${row['server-code']}">${
|
|
638
|
+
html += `<td><a href="${path}&server=${row['server-code']}">${escape(row['server-name'])}</a></td>\n`;
|
|
638
639
|
}
|
|
639
640
|
if (!versionCode) {
|
|
640
641
|
html += `<td><a href="${path}&fhirVersion=${row.fhirVersion}">${row.fhirVersion}</a></td>\n`;
|
|
641
642
|
}
|
|
642
643
|
|
|
643
|
-
html += `<td><a href="${
|
|
644
|
+
html += `<td><a href="${escape(row.url)}">${escape(row.url)}</a></td>\n`;
|
|
644
645
|
|
|
645
646
|
if (row.error) {
|
|
646
|
-
html += `<td><span style="color: maroon">Error: ${
|
|
647
|
+
html += `<td><span style="color: maroon">Error: ${escape(row.error)}</span> Last OK ${this._formatDuration(row['last-success'])} ago</td>\n`;
|
|
647
648
|
} else {
|
|
648
649
|
html += `<td>Last OK ${this._formatDuration(row['last-success'])} ago</td>\n`;
|
|
649
650
|
}
|
|
@@ -673,20 +674,20 @@ class RegistryAPI {
|
|
|
673
674
|
const data = this.crawler.getData();
|
|
674
675
|
let html = '<table class="grid">';
|
|
675
676
|
|
|
676
|
-
html += `<tr><td width="130px"><img src="/assets/images/tx-registry-root.gif"> Registries</td><td>${data.address} (${
|
|
677
|
+
html += `<tr><td width="130px"><img src="/assets/images/tx-registry-root.gif"> Registries</td><td>${data.address} (${escape(data.outcome)})</td></tr>`;
|
|
677
678
|
|
|
678
679
|
data.registries.forEach(registry => {
|
|
679
680
|
if (registry.error) {
|
|
680
|
-
html += `<tr><td title="${
|
|
681
|
+
html += `<tr><td title="${escape(registry.name)}"> <img src="/assets/images/tx-registry.png"> ${registry.code}</td><td><a href="${escape(registry.address)}">${escape(registry.address)}</a>. Error: ${escape(registry.error)}</td></tr>`;
|
|
681
682
|
} else {
|
|
682
|
-
html += `<tr><td title="${
|
|
683
|
+
html += `<tr><td title="${escape(registry.name)}"> <img src="/assets/images/tx-registry.png"> ${registry.code}</td><td><a href="${escape(registry.address)}">${escape(registry.address)}</a></td></tr>`;
|
|
683
684
|
}
|
|
684
685
|
|
|
685
686
|
registry.servers.forEach(server => {
|
|
686
687
|
if (server.authCSList.length > 0 || server.authVSList.length > 0 || server.usageList.length > 0) {
|
|
687
|
-
html += `<tr><td title="${
|
|
688
|
+
html += `<tr><td title="${escape(server.name)}"> <img src="/assets/images/tx-server.png"> ${server.code}</td><td><a href="${escape(server.address)}">${escape(server.address)}</a>. ${server.description}</td></tr>`;
|
|
688
689
|
} else {
|
|
689
|
-
html += `<tr><td title="${
|
|
690
|
+
html += `<tr><td title="${escape(server.name)}"> <img src="/assets/images/tx-server.png"> ${server.code}</td><td><a href="${escape(server.address)}">${escape(server.address)}</a></td></tr>`;
|
|
690
691
|
}
|
|
691
692
|
|
|
692
693
|
server.versions.forEach(version => {
|
|
@@ -694,7 +695,7 @@ class RegistryAPI {
|
|
|
694
695
|
const versionParts = version.version.split('.');
|
|
695
696
|
const majorMinor = versionParts.slice(0, 2).join('.');
|
|
696
697
|
|
|
697
|
-
html += `<tr><td> <img src="/assets/images/tx-version.png"> v${majorMinor}</td><td><a href="${
|
|
698
|
+
html += `<tr><td> <img src="/assets/images/tx-version.png"> v${majorMinor}</td><td><a href="${escape(version.address)}">${escape(version.address)}</a>. Status: ${escape(version.details)}. ${version.codeSystems.length} CodeSystems, ${version.valueSets.length} ValueSets</td></tr>`;
|
|
698
699
|
});
|
|
699
700
|
});
|
|
700
701
|
});
|
package/registry/crawler.js
CHANGED
|
@@ -14,7 +14,7 @@ const MASTER_URL = 'https://fhir.github.io/ig-registry/tx-servers.json';
|
|
|
14
14
|
class RegistryCrawler {
|
|
15
15
|
log;
|
|
16
16
|
|
|
17
|
-
constructor(config = {}) {
|
|
17
|
+
constructor(config = {}, stats) {
|
|
18
18
|
this.config = {
|
|
19
19
|
timeout: config.timeout || 30000, // 30 seconds default
|
|
20
20
|
masterUrl: config.masterUrl || MASTER_URL,
|
|
@@ -22,7 +22,8 @@ class RegistryCrawler {
|
|
|
22
22
|
crawlInterval: config.crawlInterval || 5 * 60 * 1000, // 5 minutes default
|
|
23
23
|
apiKeys: config.apiKeys || {} // Map of server URL or code to API key
|
|
24
24
|
};
|
|
25
|
-
|
|
25
|
+
this.stats = stats;
|
|
26
|
+
|
|
26
27
|
this.currentData = new ServerRegistries();
|
|
27
28
|
this.crawlTimer = null;
|
|
28
29
|
this.isCrawling = false;
|
|
@@ -35,32 +36,32 @@ class RegistryCrawler {
|
|
|
35
36
|
this.log = logv;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
start() {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
stop() {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
39
|
+
// /**
|
|
40
|
+
// * Start the crawler with periodic updates
|
|
41
|
+
// */
|
|
42
|
+
// start() {
|
|
43
|
+
// if (this.crawlTimer) {
|
|
44
|
+
// return; // Already running
|
|
45
|
+
// }
|
|
46
|
+
//
|
|
47
|
+
// // Initial crawl
|
|
48
|
+
// this.crawl();
|
|
49
|
+
//
|
|
50
|
+
// // Set up periodic crawling
|
|
51
|
+
// this.crawlTimer = setInterval(() => {
|
|
52
|
+
// this.crawl();
|
|
53
|
+
// }, this.config.crawlInterval);
|
|
54
|
+
// }
|
|
55
|
+
//
|
|
56
|
+
// /**
|
|
57
|
+
// * Stop the crawler
|
|
58
|
+
// */
|
|
59
|
+
// stop() {
|
|
60
|
+
// if (this.crawlTimer) {
|
|
61
|
+
// clearInterval(this.crawlTimer);
|
|
62
|
+
// this.crawlTimer = null;
|
|
63
|
+
// }
|
|
64
|
+
// }
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* Main entry point - crawl the registry starting from the master URL
|
|
@@ -133,7 +134,8 @@ class RegistryCrawler {
|
|
|
133
134
|
registry.name = registryConfig.name;
|
|
134
135
|
registry.authority = registryConfig.authority || '';
|
|
135
136
|
registry.address = registryConfig.url;
|
|
136
|
-
|
|
137
|
+
this.stats.task('TxRegistry', 'Checking: '+registry.address);
|
|
138
|
+
|
|
137
139
|
if (!registry.name) {
|
|
138
140
|
this.addLogEntry('error', 'No name provided for registry', registryConfig.url);
|
|
139
141
|
return registry;
|
package/registry/model.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// registry-model.js
|
|
2
2
|
// Data model for terminology server registry
|
|
3
|
+
const escape = require('escape-html');
|
|
3
4
|
|
|
4
5
|
class ServerVersionInformation {
|
|
5
6
|
constructor() {
|
|
@@ -36,28 +37,17 @@ class ServerVersionInformation {
|
|
|
36
37
|
getCsListHtml() {
|
|
37
38
|
if (this.codeSystems.length === 0) return '<ul></ul>';
|
|
38
39
|
return '<ul>' + this.codeSystems.map(cs =>
|
|
39
|
-
`<li>${
|
|
40
|
+
`<li>${escape(cs)}</li>`
|
|
40
41
|
).join('') + '</ul>';
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
getVsListHtml() {
|
|
44
45
|
if (this.valueSets.length === 0) return '<ul></ul>';
|
|
45
46
|
return '<ul>' + this.valueSets.map(vs =>
|
|
46
|
-
`<li>${
|
|
47
|
+
`<li>${escape(vs)}</li>`
|
|
47
48
|
).join('') + '</ul>';
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
_escapeHtml(text) {
|
|
51
|
-
const map = {
|
|
52
|
-
'&': '&',
|
|
53
|
-
'<': '<',
|
|
54
|
-
'>': '>',
|
|
55
|
-
'"': '"',
|
|
56
|
-
"'": '''
|
|
57
|
-
};
|
|
58
|
-
return text.replace(/[&<>"']/g, m => map[m]);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
51
|
toJSON() {
|
|
62
52
|
return {
|
|
63
53
|
version: this.version,
|
|
@@ -151,7 +141,7 @@ class ServerInformation {
|
|
|
151
141
|
if (result) result += '. ';
|
|
152
142
|
result += 'Authoritative for the following CodeSystems: <ul>';
|
|
153
143
|
this.authCSList.forEach(cs => {
|
|
154
|
-
const escaped =
|
|
144
|
+
const escaped = escape(cs).replace(/\*/g, '<b>*</b>');
|
|
155
145
|
result += `<li>${escaped}</li>`;
|
|
156
146
|
});
|
|
157
147
|
result += '</ul>';
|
|
@@ -161,7 +151,7 @@ class ServerInformation {
|
|
|
161
151
|
if (result) result += '. ';
|
|
162
152
|
result += 'Authoritative for the following ValueSets: <ul>';
|
|
163
153
|
this.authVSList.forEach(vs => {
|
|
164
|
-
const escaped =
|
|
154
|
+
const escaped = escape(vs).replace(/\*/g, '<b>*</b>');
|
|
165
155
|
result += `<li>${escaped}</li>`;
|
|
166
156
|
});
|
|
167
157
|
result += '</ul>';
|
|
@@ -170,17 +160,6 @@ class ServerInformation {
|
|
|
170
160
|
return result;
|
|
171
161
|
}
|
|
172
162
|
|
|
173
|
-
_escapeHtml(text) {
|
|
174
|
-
const map = {
|
|
175
|
-
'&': '&',
|
|
176
|
-
'<': '<',
|
|
177
|
-
'>': '>',
|
|
178
|
-
'"': '"',
|
|
179
|
-
"'": '''
|
|
180
|
-
};
|
|
181
|
-
return text.replace(/[&<>"']/g, m => map[m]);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
163
|
toJSON() {
|
|
185
164
|
return {
|
|
186
165
|
code: this.code,
|
package/registry/readme.md
CHANGED
|
@@ -195,17 +195,7 @@ npm run test:jest
|
|
|
195
195
|
|
|
196
196
|
## Data Persistence
|
|
197
197
|
|
|
198
|
-
The crawler
|
|
199
|
-
|
|
200
|
-
```javascript
|
|
201
|
-
// Save current state
|
|
202
|
-
const data = crawler.saveData();
|
|
203
|
-
fs.writeFileSync('registry-data.json', JSON.stringify(data));
|
|
204
|
-
|
|
205
|
-
// Load saved state
|
|
206
|
-
const savedData = JSON.parse(fs.readFileSync('registry-data.json'));
|
|
207
|
-
crawler.loadData(savedData);
|
|
208
|
-
```
|
|
198
|
+
The crawler saves and loads its state in [data]/registry-data.json
|
|
209
199
|
|
|
210
200
|
## Development
|
|
211
201
|
|
package/registry/registry.js
CHANGED
|
@@ -8,6 +8,7 @@ const htmlServer = require('../library/html-server');
|
|
|
8
8
|
const Logger = require('../library/logger');
|
|
9
9
|
const regLog = Logger.getInstance().child({ module: 'registry' });
|
|
10
10
|
const folders = require('../library/folder-setup');
|
|
11
|
+
const escape = require('escape-html');
|
|
11
12
|
|
|
12
13
|
class RegistryModule {
|
|
13
14
|
constructor(stats) {
|
|
@@ -43,7 +44,7 @@ class RegistryModule {
|
|
|
43
44
|
apiKeys: config.apiKeys || {}
|
|
44
45
|
};
|
|
45
46
|
|
|
46
|
-
this.crawler = new RegistryCrawler(crawlerConfig);
|
|
47
|
+
this.crawler = new RegistryCrawler(crawlerConfig, this.stats);
|
|
47
48
|
this.crawler.useLog(regLog);
|
|
48
49
|
|
|
49
50
|
// Initialize API with crawler
|
|
@@ -119,6 +120,7 @@ class RegistryModule {
|
|
|
119
120
|
}, 5000);
|
|
120
121
|
|
|
121
122
|
// Set up periodic crawling
|
|
123
|
+
this.stats.addTask("TxRegistry", `${intervalMinutes} min`);
|
|
122
124
|
this.crawlInterval = setInterval(() => {
|
|
123
125
|
this.performCrawl();
|
|
124
126
|
}, intervalMs);
|
|
@@ -134,6 +136,7 @@ class RegistryModule {
|
|
|
134
136
|
this.logger.info('Crawl already in progress, skipping...');
|
|
135
137
|
return;
|
|
136
138
|
}
|
|
139
|
+
this.stats.task('TxRegistry', 'Crawling');
|
|
137
140
|
|
|
138
141
|
this.crawlInProgress = true;
|
|
139
142
|
this.logger.info('Starting registry crawl...');
|
|
@@ -160,9 +163,10 @@ class RegistryModule {
|
|
|
160
163
|
`Found ${newData.registries.length} registries, ` +
|
|
161
164
|
`${metadata.errors.length} errors, ` +
|
|
162
165
|
`downloaded ${this.crawler.formatBytes(metadata.totalBytes)}`);
|
|
163
|
-
|
|
166
|
+
this.stats.task('TxRegistry', 'Crawling Finished');
|
|
164
167
|
} catch (error) {
|
|
165
168
|
this.logger.error('Crawl failed:', error);
|
|
169
|
+
this.stats.task('TxRegistry', 'Crawling Error: '+error.message);
|
|
166
170
|
} finally {
|
|
167
171
|
this.crawlInProgress = false;
|
|
168
172
|
}
|
|
@@ -487,15 +491,15 @@ class RegistryModule {
|
|
|
487
491
|
|
|
488
492
|
for (const server of serverVersions) {
|
|
489
493
|
html += '<tr>';
|
|
490
|
-
html += `<td><a href="${server.serverUrl}" target="_blank">${
|
|
491
|
-
html += `<td>${
|
|
492
|
-
html += `<td>${
|
|
493
|
-
html += `<td>${
|
|
494
|
-
html += `<td>${
|
|
494
|
+
html += `<td><a href="${server.serverUrl}" target="_blank">${escape(server.serverUrl)}</a></td>`;
|
|
495
|
+
html += `<td>${escape(server.software.replace("Reference Server", "HealthIntersections"))}</td>`;
|
|
496
|
+
html += `<td>${escape(server.authority.replace("Published by", ""))}</td>`;
|
|
497
|
+
html += `<td>${escape(server.version)}</td>`;
|
|
498
|
+
html += `<td>${escape(server.security || '')}</td>`;
|
|
495
499
|
html += '<td>';
|
|
496
500
|
if (server.usage && server.usage.length > 0) {
|
|
497
501
|
const badges = server.usage.map(tag =>
|
|
498
|
-
(tag == 'public' ? '' : `<span class="badge badge-info mr-1">${
|
|
502
|
+
(tag == 'public' ? '' : `<span class="badge badge-info mr-1">${escape(tag)}</span>`)
|
|
499
503
|
);
|
|
500
504
|
html += badges.join(' ');
|
|
501
505
|
}
|
|
@@ -534,19 +538,6 @@ class RegistryModule {
|
|
|
534
538
|
return html;
|
|
535
539
|
}
|
|
536
540
|
|
|
537
|
-
/**
|
|
538
|
-
* Helper function to escape HTML special characters
|
|
539
|
-
*/
|
|
540
|
-
_escapeHtml(text) {
|
|
541
|
-
if (!text) return '';
|
|
542
|
-
return text
|
|
543
|
-
.replace(/&/g, '&')
|
|
544
|
-
.replace(/</g, '<')
|
|
545
|
-
.replace(/>/g, '>')
|
|
546
|
-
.replace(/"/g, '"')
|
|
547
|
-
.replace(/'/g, ''');
|
|
548
|
-
}
|
|
549
|
-
|
|
550
541
|
/**
|
|
551
542
|
* Gather information about authoritative code systems
|
|
552
543
|
* @returns {Array} Array of objects with code system information
|
|
@@ -821,8 +812,8 @@ class RegistryModule {
|
|
|
821
812
|
// First row for this code system
|
|
822
813
|
const rowspan = cs.servers.length;
|
|
823
814
|
html += '<tr>';
|
|
824
|
-
html += `<td rowspan="${rowspan}">${this._highlightWildcard(
|
|
825
|
-
html += `<td><a href="${
|
|
815
|
+
html += `<td rowspan="${rowspan}">${this._highlightWildcard(escape(formattedMask))}</td>`;
|
|
816
|
+
html += `<td><a href="${escape(cs.servers[0].url)}" target="_blank">${escape(cs.servers[0].url)}</a></td>`;
|
|
826
817
|
|
|
827
818
|
// Format versions as R3/R4/R5
|
|
828
819
|
const formattedVersions = cs.servers[0].versions.map(v => this._formatFhirVersion(v));
|
|
@@ -832,7 +823,7 @@ class RegistryModule {
|
|
|
832
823
|
// Additional rows for this code system (if any)
|
|
833
824
|
for (let i = 1; i < cs.servers.length; i++) {
|
|
834
825
|
html += '<tr>';
|
|
835
|
-
html += `<td><a href="${
|
|
826
|
+
html += `<td><a href="${escape(cs.servers[i].url)}" target="_blank">${escape(cs.servers[i].url)}</a></td>`;
|
|
836
827
|
|
|
837
828
|
// Format versions as R3/R4/R5
|
|
838
829
|
const formattedVersions = cs.servers[i].versions.map(v => this._formatFhirVersion(v));
|
|
@@ -877,8 +868,8 @@ class RegistryModule {
|
|
|
877
868
|
// First row for this value set
|
|
878
869
|
const rowspan = vs.servers.length;
|
|
879
870
|
html += '<tr>';
|
|
880
|
-
html += `<td rowspan="${rowspan}">${this._highlightWildcard(
|
|
881
|
-
html += `<td><a href="${
|
|
871
|
+
html += `<td rowspan="${rowspan}">${this._highlightWildcard(escape(vs.mask))}</td>`;
|
|
872
|
+
html += `<td><a href="${escape(vs.servers[0].url)}" target="_blank">${escape(vs.servers[0].url)}</a></td>`;
|
|
882
873
|
|
|
883
874
|
// Format versions as R3/R4/R5
|
|
884
875
|
const formattedVersions = vs.servers[0].versions.map(v => this._formatFhirVersion(v));
|
|
@@ -888,7 +879,7 @@ class RegistryModule {
|
|
|
888
879
|
// Additional rows for this value set (if any)
|
|
889
880
|
for (let i = 1; i < vs.servers.length; i++) {
|
|
890
881
|
html += '<tr>';
|
|
891
|
-
html += `<td><a href="${
|
|
882
|
+
html += `<td><a href="${escape(vs.servers[i].url)}" target="_blank">${escape(vs.servers[i].url)}</a></td>`;
|
|
892
883
|
|
|
893
884
|
// Format versions as R3/R4/R5
|
|
894
885
|
const formattedVersions = vs.servers[i].versions.map(v => this._formatFhirVersion(v));
|
|
@@ -1103,11 +1094,11 @@ class RegistryModule {
|
|
|
1103
1094
|
html += '<h2 class="card-title">Query Information</h2>';
|
|
1104
1095
|
html += '</div>';
|
|
1105
1096
|
html += '<div class="card-body">';
|
|
1106
|
-
html += `<p><strong>FHIR Version:</strong> ${
|
|
1107
|
-
html += `<p><strong>Resource URL:</strong> ${
|
|
1108
|
-
html += `<p><strong>Registry URL:</strong> <a href="${result['registry-url']}" target="_blank">${
|
|
1097
|
+
html += `<p><strong>FHIR Version:</strong> ${escape(fhirVersion)}</p>`;
|
|
1098
|
+
html += `<p><strong>Resource URL:</strong> ${escape(resourceUrl)}</p>`;
|
|
1099
|
+
html += `<p><strong>Registry URL:</strong> <a href="${result['registry-url']}" target="_blank">${escape(result['registry-url'])}</a></p>`;
|
|
1109
1100
|
if (usage) {
|
|
1110
|
-
html += `<p><strong>Usage:</strong> ${
|
|
1101
|
+
html += `<p><strong>Usage:</strong> ${escape(usage)}</p>`;
|
|
1111
1102
|
}
|
|
1112
1103
|
html += '</div>';
|
|
1113
1104
|
html += '</div>';
|
|
@@ -1133,10 +1124,10 @@ class RegistryModule {
|
|
|
1133
1124
|
|
|
1134
1125
|
result.authoritative.forEach(server => {
|
|
1135
1126
|
html += '<tr>';
|
|
1136
|
-
html += `<td>${
|
|
1137
|
-
html += `<td><a href="${server.url}" target="_blank">${
|
|
1127
|
+
html += `<td>${escape(server['server-name'])}</td>`;
|
|
1128
|
+
html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
|
|
1138
1129
|
html += `<td>${this.renderSecurityTags(server)}</td>`;
|
|
1139
|
-
html += `<td>${server.access_info ?
|
|
1130
|
+
html += `<td>${server.access_info ? escape(server.access_info) : ''}</td>`;
|
|
1140
1131
|
html += '</tr>';
|
|
1141
1132
|
});
|
|
1142
1133
|
|
|
@@ -1170,10 +1161,10 @@ class RegistryModule {
|
|
|
1170
1161
|
|
|
1171
1162
|
result.candidates.forEach(server => {
|
|
1172
1163
|
html += '<tr>';
|
|
1173
|
-
html += `<td>${
|
|
1174
|
-
html += `<td><a href="${server.url}" target="_blank">${
|
|
1164
|
+
html += `<td>${escape(server['server-name'])}</td>`;
|
|
1165
|
+
html += `<td><a href="${server.url}" target="_blank">${escape(server.url)}</a></td>`;
|
|
1175
1166
|
html += `<td>${this.renderSecurityTags(server)}</td>`;
|
|
1176
|
-
html += `<td>${server.access_info ?
|
|
1167
|
+
html += `<td>${server.access_info ? escape(server.access_info) : ''}</td>`;
|
|
1177
1168
|
html += '</tr>';
|
|
1178
1169
|
});
|
|
1179
1170
|
|
|
@@ -1230,7 +1221,7 @@ class RegistryModule {
|
|
|
1230
1221
|
html += '<p>';
|
|
1231
1222
|
html += '<label for="fhirVersion" class="form-label fw-bold">FHIR Version <span class="text-danger">*</span></label>';
|
|
1232
1223
|
html += `<input type="text" class="form-control" id="fhirVersion" name="fhirVersion" size="8"
|
|
1233
|
-
value="${
|
|
1224
|
+
value="${escape(fhirVersion)}" required>`;
|
|
1234
1225
|
html += '</p>';
|
|
1235
1226
|
html += '<p class="text-muted small">Examples: R4, 4.0.1, 5.0.0, etc.</p>';
|
|
1236
1227
|
|
|
@@ -1238,7 +1229,7 @@ class RegistryModule {
|
|
|
1238
1229
|
html += '<p>';
|
|
1239
1230
|
html += '<label for="url" class="form-label fw-bold">Code System URL</label>';
|
|
1240
1231
|
html += `<input type="url" class="form-control" id="url" name="url"
|
|
1241
|
-
value="${
|
|
1232
|
+
value="${escape(url)}">`;
|
|
1242
1233
|
html += '</p>';
|
|
1243
1234
|
html += '<p class="text-muted small">Example: http://loinc.org</p>';
|
|
1244
1235
|
|
|
@@ -1246,7 +1237,7 @@ class RegistryModule {
|
|
|
1246
1237
|
html += '<p>';
|
|
1247
1238
|
html += '<label for="valueSet" class="form-label fw-bold">Value Set URL</label>';
|
|
1248
1239
|
html += `<input type="url" class="form-control" id="valueSet" name="valueSet"
|
|
1249
|
-
value="${
|
|
1240
|
+
value="${escape(valueSet)}">`;
|
|
1250
1241
|
html += '</p>';
|
|
1251
1242
|
html += '<p class="text-muted small">Example: http://hl7.org/fhir/ValueSet/observation-codes</p>';
|
|
1252
1243
|
|
|
@@ -1382,7 +1373,7 @@ class RegistryModule {
|
|
|
1382
1373
|
}
|
|
1383
1374
|
|
|
1384
1375
|
// Format: [time] [LEVEL] message
|
|
1385
|
-
html += `<span style="color: #666;">[${timeDisplay}]</span> <span style="${levelStyle}">[${log.level.toUpperCase()}]</span> ${
|
|
1376
|
+
html += `<span style="color: #666;">[${timeDisplay}]</span> <span style="${levelStyle}">[${log.level.toUpperCase()}]</span> ${escape(log.message)}\n`;
|
|
1386
1377
|
});
|
|
1387
1378
|
}
|
|
1388
1379
|
|
package/server.js
CHANGED
|
@@ -11,6 +11,8 @@ const express = require('express');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const folders = require('./library/folder-setup'); // <-- ADD: load early
|
|
14
|
+
const { statSync, readdirSync } = require('fs');
|
|
15
|
+
const escape = require('escape-html');
|
|
14
16
|
|
|
15
17
|
// Load configuration BEFORE logger
|
|
16
18
|
let config;
|
|
@@ -46,7 +48,7 @@ const packageJson = require('./package.json');
|
|
|
46
48
|
const htmlServer = require('./library/html-server');
|
|
47
49
|
const ServerStats = require("./stats");
|
|
48
50
|
const {Liquid} = require("liquidjs");
|
|
49
|
-
|
|
51
|
+
|
|
50
52
|
htmlServer.useLog(serverLog);
|
|
51
53
|
|
|
52
54
|
const app = express();
|
|
@@ -182,7 +184,6 @@ async function initializeModules() {
|
|
|
182
184
|
modules.tx = new TXModule(stats);
|
|
183
185
|
await modules.tx.initialize(config.modules.tx, app);
|
|
184
186
|
} catch (error) {
|
|
185
|
-
console.log(error);
|
|
186
187
|
serverLog.error('Failed to initialize TX module:', error);
|
|
187
188
|
throw error;
|
|
188
189
|
}
|
|
@@ -333,7 +334,7 @@ async function buildRootPageContent() {
|
|
|
333
334
|
content += '<table class="grid">';
|
|
334
335
|
content += '<tr>';
|
|
335
336
|
content += `<td><strong>Module Count:</strong> ${mc}</td>`;
|
|
336
|
-
content += `<td><strong>Uptime:</strong> ${
|
|
337
|
+
content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
|
|
337
338
|
content += `<td><strong>Request Count:</strong> ${stats.requestCount}</td>`;
|
|
338
339
|
content += '</tr>';
|
|
339
340
|
content += '<tr>';
|
|
@@ -341,7 +342,7 @@ async function buildRootPageContent() {
|
|
|
341
342
|
content += `<td><strong>Heap Total:</strong> ${heapTotalMB} MB</td>`;
|
|
342
343
|
content += `<td><strong>Process Memory:</strong> ${rssMB} MB</td>`;
|
|
343
344
|
content += '</tr>';
|
|
344
|
-
|
|
345
|
+
content += getLogStats();
|
|
345
346
|
content += '</table>';
|
|
346
347
|
|
|
347
348
|
|
|
@@ -355,6 +356,7 @@ async function buildRootPageContent() {
|
|
|
355
356
|
historyJson: JSON.stringify(stats.history),
|
|
356
357
|
startTime: stats.startTime
|
|
357
358
|
});
|
|
359
|
+
content += stats.taskDetails();
|
|
358
360
|
|
|
359
361
|
content += '</div>';
|
|
360
362
|
return content;
|
|
@@ -388,7 +390,7 @@ app.get('/', async (req, res) => {
|
|
|
388
390
|
processingTime: Date.now() - startTime
|
|
389
391
|
};
|
|
390
392
|
|
|
391
|
-
const html = htmlServer.renderPage('root', config.hostName || 'FHIRsmith Server', content, stats);
|
|
393
|
+
const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
|
|
392
394
|
res.setHeader('Content-Type', 'text/html');
|
|
393
395
|
res.send(html);
|
|
394
396
|
} catch (error) {
|
|
@@ -473,6 +475,52 @@ app.get('/health', async (req, res) => {
|
|
|
473
475
|
res.json(healthStatus);
|
|
474
476
|
});
|
|
475
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Get log directory statistics: file count, total size, and disk space info
|
|
480
|
+
* @returns {string} HTML table row(s) with log stats
|
|
481
|
+
*/
|
|
482
|
+
function getLogStats() {
|
|
483
|
+
const logDir = folders.logsDir();
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const files = readdirSync(logDir).filter(f => f.endsWith('.log'));
|
|
487
|
+
let totalSize = 0;
|
|
488
|
+
for (const file of files) {
|
|
489
|
+
totalSize += statSync(path.join(logDir, file)).size;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const sizeMB = (totalSize / 1024 / 1024).toFixed(2);
|
|
493
|
+
|
|
494
|
+
let diskInfo = '';
|
|
495
|
+
try {
|
|
496
|
+
// statfs available in Node 18.15+
|
|
497
|
+
const stats = fs.statfsSync(logDir);
|
|
498
|
+
const blockSize = stats.bsize;
|
|
499
|
+
const freeSpace = stats.bavail * blockSize;
|
|
500
|
+
const totalSpace = stats.blocks * blockSize;
|
|
501
|
+
const freeGB = (freeSpace / 1024 / 1024 / 1024).toFixed(2);
|
|
502
|
+
const totalGB = (totalSpace / 1024 / 1024 / 1024).toFixed(2);
|
|
503
|
+
diskInfo = `<td><strong>Disk Space:</strong> ${freeGB} GB of ${totalGB} GB</td>`;
|
|
504
|
+
} catch {
|
|
505
|
+
diskInfo = '<td><strong>Disk Space:</strong> unavailable</td>';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Log rotation limit from logger config
|
|
509
|
+
const loggerOpts = Logger.getInstance().options;
|
|
510
|
+
const maxFiles = loggerOpts.file.maxFiles;
|
|
511
|
+
const maxSize = loggerOpts.file.maxSize;
|
|
512
|
+
const limitInfo = `${maxFiles} files × ${maxSize} each`;
|
|
513
|
+
|
|
514
|
+
return '<tr>'
|
|
515
|
+
+ `<td><strong>Existing Logs:</strong> ${files.length} (${sizeMB} MB)</td>`
|
|
516
|
+
+ `<td><strong>Retention Policy:</strong> ${limitInfo}</td>`
|
|
517
|
+
+ diskInfo
|
|
518
|
+
+ '</tr>';
|
|
519
|
+
} catch (e) {
|
|
520
|
+
return `<tr><td colspan="3"><strong>Logs:</strong> unable to read (${e.message})</td></tr>`;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
476
524
|
// Initialize everything
|
|
477
525
|
async function startServer() {
|
|
478
526
|
try {
|
package/shl/shl.js
CHANGED
|
@@ -222,24 +222,6 @@ class SHLModule {
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
-
// Enhanced HTML escaping
|
|
226
|
-
escapeHtml(str) {
|
|
227
|
-
if (!str || typeof str !== 'string') return '';
|
|
228
|
-
|
|
229
|
-
const escapeMap = {
|
|
230
|
-
'&': '&',
|
|
231
|
-
'<': '<',
|
|
232
|
-
'>': '>',
|
|
233
|
-
'"': '"',
|
|
234
|
-
"'": ''',
|
|
235
|
-
'/': '/',
|
|
236
|
-
'`': '`',
|
|
237
|
-
'=': '='
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
return str.replace(/[&<>"'`=/]/g, (match) => escapeMap[match]);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
225
|
// URL validation
|
|
244
226
|
validateExternalUrl(url) {
|
|
245
227
|
try {
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
<!--- admin -->
|
|
2
|
-
|
|
3
|
-
String.prototype.escapeHTML = function () {
|
|
4
|
-
return(
|
|
5
|
-
this.replace(/&/g,'&').
|
|
6
|
-
replace(/>/g,'>').
|
|
7
|
-
replace(/</g,'<').
|
|
8
|
-
replace(/"/g,'"')
|
|
9
|
-
);
|
|
10
|
-
};
|
|
2
|
+
const escape = require('escape-html');
|
|
11
3
|
|
|
12
4
|
function setCookie(c_name,value,exdays)
|
|
13
5
|
{
|