fhirsmith 0.4.2 → 0.5.1
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 +1 -1
- package/library/cron-utilities.js +136 -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/registry.js +32 -41
- package/server.js +89 -12
- 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-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 +241 -230
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/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
|
@@ -10,7 +10,10 @@ const express = require('express');
|
|
|
10
10
|
// const cors = require('cors');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
13
14
|
const folders = require('./library/folder-setup'); // <-- ADD: load early
|
|
15
|
+
const { statSync, readdirSync } = require('fs');
|
|
16
|
+
const escape = require('escape-html');
|
|
14
17
|
|
|
15
18
|
// Load configuration BEFORE logger
|
|
16
19
|
let config;
|
|
@@ -25,6 +28,17 @@ try {
|
|
|
25
28
|
|
|
26
29
|
const Logger = require('./library/logger');
|
|
27
30
|
const serverLog = Logger.getInstance().child({ module: 'server' });
|
|
31
|
+
const packageJson = require('./package.json');
|
|
32
|
+
|
|
33
|
+
// Startup banner
|
|
34
|
+
const totalMemGB = (os.totalmem() / 1024 / 1024 / 1024).toFixed(1);
|
|
35
|
+
const freeMemGB = (os.freemem() / 1024 / 1024 / 1024).toFixed(1);
|
|
36
|
+
serverLog.info(`========================================`);
|
|
37
|
+
serverLog.info(`FHIRsmith v${packageJson.version} starting (PID ${process.pid})`);
|
|
38
|
+
serverLog.info(`Node.js ${process.version} on ${os.type()} ${os.release()} (${os.arch()})`);
|
|
39
|
+
serverLog.info(`Memory: ${freeMemGB} GB free / ${totalMemGB} GB total`);
|
|
40
|
+
serverLog.info(`Data directory: ${folders.dataDir()}`);
|
|
41
|
+
serverLog.info(`========================================`);
|
|
28
42
|
|
|
29
43
|
const activeModules = config.modules ? Object.keys(config.modules)
|
|
30
44
|
.filter(mod => config.modules[mod].enabled)
|
|
@@ -41,12 +55,11 @@ const PublisherModule = require('./publisher/publisher.js');
|
|
|
41
55
|
const TokenModule = require('./token/token.js');
|
|
42
56
|
const NpmProjectorModule = require('./npmprojector/npmprojector.js');
|
|
43
57
|
const TXModule = require('./tx/tx.js');
|
|
44
|
-
const packageJson = require('./package.json');
|
|
45
58
|
|
|
46
59
|
const htmlServer = require('./library/html-server');
|
|
47
60
|
const ServerStats = require("./stats");
|
|
48
61
|
const {Liquid} = require("liquidjs");
|
|
49
|
-
|
|
62
|
+
|
|
50
63
|
htmlServer.useLog(serverLog);
|
|
51
64
|
|
|
52
65
|
const app = express();
|
|
@@ -79,6 +92,7 @@ async function initializeModules() {
|
|
|
79
92
|
// Initialize SHL module
|
|
80
93
|
if (config.modules?.shl?.enabled) {
|
|
81
94
|
try {
|
|
95
|
+
serverLog.info('Initializing module: shl...');
|
|
82
96
|
modules.shl = new SHLModule(stats);
|
|
83
97
|
await modules.shl.initialize(config.modules.shl);
|
|
84
98
|
app.use('/shl', modules.shl.router);
|
|
@@ -91,6 +105,7 @@ async function initializeModules() {
|
|
|
91
105
|
// Initialize VCL module
|
|
92
106
|
if (config.modules?.vcl?.enabled) {
|
|
93
107
|
try {
|
|
108
|
+
serverLog.info('Initializing module: vcl...');
|
|
94
109
|
modules.vcl = new VCLModule(stats);
|
|
95
110
|
await modules.vcl.initialize(config.modules.vcl);
|
|
96
111
|
app.use('/VCL', modules.vcl.router);
|
|
@@ -99,11 +114,12 @@ async function initializeModules() {
|
|
|
99
114
|
throw error;
|
|
100
115
|
}
|
|
101
116
|
}
|
|
102
|
-
|
|
117
|
+
|
|
103
118
|
// Initialize XIG module
|
|
104
119
|
if (config.modules?.xig?.enabled) {
|
|
105
120
|
try {
|
|
106
|
-
|
|
121
|
+
serverLog.info('Initializing module: xig...');
|
|
122
|
+
await xigModule.initializeXigModule(stats, config.modules.xig);
|
|
107
123
|
app.use('/xig', xigModule.router);
|
|
108
124
|
modules.xig = xigModule;
|
|
109
125
|
} catch (error) {
|
|
@@ -115,6 +131,7 @@ async function initializeModules() {
|
|
|
115
131
|
// Initialize Packages module
|
|
116
132
|
if (config.modules?.packages?.enabled) {
|
|
117
133
|
try {
|
|
134
|
+
serverLog.info('Initializing module: packages...');
|
|
118
135
|
modules.packages = new PackagesModule(stats);
|
|
119
136
|
await modules.packages.initialize(config.modules.packages);
|
|
120
137
|
app.use('/packages', modules.packages.router);
|
|
@@ -128,6 +145,7 @@ async function initializeModules() {
|
|
|
128
145
|
// Initialize Registry module
|
|
129
146
|
if (config.modules?.registry?.enabled) {
|
|
130
147
|
try {
|
|
148
|
+
serverLog.info('Initializing module: registry...');
|
|
131
149
|
modules.registry = new RegistryModule(stats);
|
|
132
150
|
await modules.registry.initialize(config.modules.registry);
|
|
133
151
|
app.use('/tx-reg', modules.registry.router);
|
|
@@ -140,6 +158,7 @@ async function initializeModules() {
|
|
|
140
158
|
// Initialize Publisher module
|
|
141
159
|
if (config.modules?.publisher?.enabled) {
|
|
142
160
|
try {
|
|
161
|
+
serverLog.info('Initializing module: publisher...');
|
|
143
162
|
modules.publisher = new PublisherModule(stats);
|
|
144
163
|
await modules.publisher.initialize(config.modules.publisher);
|
|
145
164
|
app.use('/publisher', modules.publisher.router);
|
|
@@ -152,6 +171,7 @@ async function initializeModules() {
|
|
|
152
171
|
// Initialize Token module
|
|
153
172
|
if (config.modules?.token?.enabled) {
|
|
154
173
|
try {
|
|
174
|
+
serverLog.info('Initializing module: token...');
|
|
155
175
|
modules.token = new TokenModule(stats);
|
|
156
176
|
await modules.token.initialize(config.modules.token);
|
|
157
177
|
app.use('/token', modules.token.router);
|
|
@@ -164,6 +184,7 @@ async function initializeModules() {
|
|
|
164
184
|
// Initialize NpmProjector module
|
|
165
185
|
if (config.modules?.npmprojector?.enabled) {
|
|
166
186
|
try {
|
|
187
|
+
serverLog.info('Initializing module: npmprojector...');
|
|
167
188
|
modules.npmprojector = new NpmProjectorModule(stats);
|
|
168
189
|
await modules.npmprojector.initialize(config.modules.npmprojector);
|
|
169
190
|
const basePath = NpmProjectorModule.getBasePath(config.modules.npmprojector);
|
|
@@ -179,10 +200,10 @@ async function initializeModules() {
|
|
|
179
200
|
// because it supports multiple endpoints at different paths
|
|
180
201
|
if (config.modules?.tx?.enabled) {
|
|
181
202
|
try {
|
|
203
|
+
serverLog.info('Initializing module: tx...');
|
|
182
204
|
modules.tx = new TXModule(stats);
|
|
183
205
|
await modules.tx.initialize(config.modules.tx, app);
|
|
184
206
|
} catch (error) {
|
|
185
|
-
console.log(error);
|
|
186
207
|
serverLog.error('Failed to initialize TX module:', error);
|
|
187
208
|
throw error;
|
|
188
209
|
}
|
|
@@ -333,7 +354,7 @@ async function buildRootPageContent() {
|
|
|
333
354
|
content += '<table class="grid">';
|
|
334
355
|
content += '<tr>';
|
|
335
356
|
content += `<td><strong>Module Count:</strong> ${mc}</td>`;
|
|
336
|
-
content += `<td><strong>Uptime:</strong> ${
|
|
357
|
+
content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
|
|
337
358
|
content += `<td><strong>Request Count:</strong> ${stats.requestCount}</td>`;
|
|
338
359
|
content += '</tr>';
|
|
339
360
|
content += '<tr>';
|
|
@@ -341,7 +362,7 @@ async function buildRootPageContent() {
|
|
|
341
362
|
content += `<td><strong>Heap Total:</strong> ${heapTotalMB} MB</td>`;
|
|
342
363
|
content += `<td><strong>Process Memory:</strong> ${rssMB} MB</td>`;
|
|
343
364
|
content += '</tr>';
|
|
344
|
-
|
|
365
|
+
content += getLogStats();
|
|
345
366
|
content += '</table>';
|
|
346
367
|
|
|
347
368
|
|
|
@@ -355,6 +376,7 @@ async function buildRootPageContent() {
|
|
|
355
376
|
historyJson: JSON.stringify(stats.history),
|
|
356
377
|
startTime: stats.startTime
|
|
357
378
|
});
|
|
379
|
+
content += stats.taskDetails();
|
|
358
380
|
|
|
359
381
|
content += '</div>';
|
|
360
382
|
return content;
|
|
@@ -363,6 +385,13 @@ async function buildRootPageContent() {
|
|
|
363
385
|
// eslint-disable-next-line no-unused-vars
|
|
364
386
|
process.on('unhandledRejection', (reason, promise) => {
|
|
365
387
|
console.error('Unhandled Rejection:', reason);
|
|
388
|
+
serverLog.error('Unhandled Rejection:', reason);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
process.on('uncaughtException', (error) => {
|
|
392
|
+
console.error('FATAL - Uncaught Exception:', error);
|
|
393
|
+
serverLog.error('FATAL - Uncaught Exception:', error);
|
|
394
|
+
process.exitCode = 1;
|
|
366
395
|
});
|
|
367
396
|
|
|
368
397
|
app.get('/', async (req, res) => {
|
|
@@ -380,7 +409,7 @@ app.get('/', async (req, res) => {
|
|
|
380
409
|
}
|
|
381
410
|
|
|
382
411
|
const content = await buildRootPageContent();
|
|
383
|
-
|
|
412
|
+
|
|
384
413
|
// Build basic stats for root page
|
|
385
414
|
const stats = {
|
|
386
415
|
version: packageJson.version,
|
|
@@ -388,7 +417,7 @@ app.get('/', async (req, res) => {
|
|
|
388
417
|
processingTime: Date.now() - startTime
|
|
389
418
|
};
|
|
390
419
|
|
|
391
|
-
const html = htmlServer.renderPage('root', config.hostName || 'FHIRsmith Server', content, stats);
|
|
420
|
+
const html = htmlServer.renderPage('root', escape(config.hostName) || 'FHIRsmith Server', content, stats);
|
|
392
421
|
res.setHeader('Content-Type', 'text/html');
|
|
393
422
|
res.send(html);
|
|
394
423
|
} catch (error) {
|
|
@@ -429,7 +458,7 @@ app.get('/', async (req, res) => {
|
|
|
429
458
|
Object.keys(enabledModules)
|
|
430
459
|
.filter(m => m !== 'tx')
|
|
431
460
|
.map(m => [
|
|
432
|
-
m,
|
|
461
|
+
m,
|
|
433
462
|
m === 'vcl' ? '/VCL' : `/${m}`
|
|
434
463
|
])
|
|
435
464
|
),
|
|
@@ -473,6 +502,52 @@ app.get('/health', async (req, res) => {
|
|
|
473
502
|
res.json(healthStatus);
|
|
474
503
|
});
|
|
475
504
|
|
|
505
|
+
/**
|
|
506
|
+
* Get log directory statistics: file count, total size, and disk space info
|
|
507
|
+
* @returns {string} HTML table row(s) with log stats
|
|
508
|
+
*/
|
|
509
|
+
function getLogStats() {
|
|
510
|
+
const logDir = folders.logsDir();
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const files = readdirSync(logDir).filter(f => f.endsWith('.log'));
|
|
514
|
+
let totalSize = 0;
|
|
515
|
+
for (const file of files) {
|
|
516
|
+
totalSize += statSync(path.join(logDir, file)).size;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const sizeMB = (totalSize / 1024 / 1024).toFixed(2);
|
|
520
|
+
|
|
521
|
+
let diskInfo = '';
|
|
522
|
+
try {
|
|
523
|
+
// statfs available in Node 18.15+
|
|
524
|
+
const stats = fs.statfsSync(logDir);
|
|
525
|
+
const blockSize = stats.bsize;
|
|
526
|
+
const freeSpace = stats.bavail * blockSize;
|
|
527
|
+
const totalSpace = stats.blocks * blockSize;
|
|
528
|
+
const freeGB = (freeSpace / 1024 / 1024 / 1024).toFixed(2);
|
|
529
|
+
const totalGB = (totalSpace / 1024 / 1024 / 1024).toFixed(2);
|
|
530
|
+
diskInfo = `<td><strong>Disk Space:</strong> ${freeGB} GB of ${totalGB} GB</td>`;
|
|
531
|
+
} catch {
|
|
532
|
+
diskInfo = '<td><strong>Disk Space:</strong> unavailable</td>';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Log rotation limit from logger config
|
|
536
|
+
const loggerOpts = Logger.getInstance().options;
|
|
537
|
+
const maxFiles = loggerOpts.file.maxFiles;
|
|
538
|
+
const maxSize = loggerOpts.file.maxSize;
|
|
539
|
+
const limitInfo = `${maxFiles} files × ${maxSize} each`;
|
|
540
|
+
|
|
541
|
+
return '<tr>'
|
|
542
|
+
+ `<td><strong>Existing Logs:</strong> ${files.length} (${sizeMB} MB)</td>`
|
|
543
|
+
+ `<td><strong>Retention Policy:</strong> ${limitInfo}</td>`
|
|
544
|
+
+ diskInfo
|
|
545
|
+
+ '</tr>';
|
|
546
|
+
} catch (e) {
|
|
547
|
+
return `<tr><td colspan="3"><strong>Logs:</strong> unable to read (${e.message})</td></tr>`;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
476
551
|
// Initialize everything
|
|
477
552
|
async function startServer() {
|
|
478
553
|
try {
|
|
@@ -494,8 +569,10 @@ async function startServer() {
|
|
|
494
569
|
modules.packages.startInitialCrawler();
|
|
495
570
|
}
|
|
496
571
|
} catch (error) {
|
|
497
|
-
|
|
498
|
-
|
|
572
|
+
console.error('FATAL - Failed to start server:', error);
|
|
573
|
+
serverLog.error('FATAL - Failed to start server:', error);
|
|
574
|
+
// Give the logger a moment to flush before exiting
|
|
575
|
+
setTimeout(() => process.exit(1), 500);
|
|
499
576
|
}
|
|
500
577
|
}
|
|
501
578
|
|
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
|
{
|