fhirsmith 0.4.2 → 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 +12 -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 +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-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/library/utilities.js
CHANGED
|
@@ -25,11 +25,42 @@ const Utilities = {
|
|
|
25
25
|
return isNaN(num) ? defaultValue : num;
|
|
26
26
|
},
|
|
27
27
|
parseFloatOrDefault(value, defaultValue) {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const num = parseFloat(value);
|
|
29
|
+
return isNaN(num) ? defaultValue : num;
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format the difference between two Date.now() timestamps for human reading
|
|
36
|
+
* @param {number} start - earlier timestamp (from Date.now())
|
|
37
|
+
* @param {number} end - later timestamp (from Date.now())
|
|
38
|
+
* @returns {string} formatted duration
|
|
39
|
+
*/
|
|
40
|
+
formatDuration(start, end) {
|
|
41
|
+
let ms = Math.abs(end - start);
|
|
42
|
+
|
|
43
|
+
if (ms < 1000) return `${ms}ms`;
|
|
44
|
+
|
|
45
|
+
const days = Math.floor(ms / 86400000);
|
|
46
|
+
ms %= 86400000;
|
|
47
|
+
const hours = Math.floor(ms / 3600000);
|
|
48
|
+
ms %= 3600000;
|
|
49
|
+
const minutes = Math.floor(ms / 60000);
|
|
50
|
+
ms %= 60000;
|
|
51
|
+
const seconds = Math.floor(ms / 1000);
|
|
52
|
+
ms %= 1000;
|
|
53
|
+
|
|
54
|
+
const parts = [];
|
|
55
|
+
if (days) parts.push(`${days}d`);
|
|
56
|
+
if (hours) parts.push(`${hours}h`);
|
|
57
|
+
if (minutes) parts.push(`${minutes}m`);
|
|
58
|
+
if (seconds || ms) {
|
|
59
|
+
parts.push(ms ? `${seconds}.${String(ms).padStart(3, '0')}s` : `${seconds}s`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return parts.join(' ');
|
|
63
|
+
}
|
|
33
64
|
|
|
34
65
|
};
|
|
35
66
|
|
|
@@ -172,25 +203,75 @@ function isAbsoluteUrl(s) {
|
|
|
172
203
|
}
|
|
173
204
|
|
|
174
205
|
/**
|
|
175
|
-
*
|
|
206
|
+
* This class takes two lists, and matches between the lists, producing three new lists:
|
|
207
|
+
* * items that are in both
|
|
208
|
+
* * items that only in left
|
|
209
|
+
* * items that are only in right
|
|
210
|
+
*
|
|
211
|
+
* You have to give it a match function that is called asynchronously
|
|
212
|
+
*
|
|
213
|
+
* examples of use:
|
|
214
|
+
*
|
|
215
|
+
* const matcher = new ArrayMatcher((l, r) =>
|
|
216
|
+
* this.filtersMatch(localstatus, cs, l, r)
|
|
217
|
+
* );
|
|
218
|
+
* await matcher.match(leftArray, rightArray);
|
|
219
|
+
*
|
|
220
|
+
* // Use the results
|
|
221
|
+
* for (const { left, right } of matcher.matched) { ... }
|
|
222
|
+
* for (const item of matcher.unmatchedLeft) { ... }
|
|
223
|
+
* for (const item of matcher.unmatchedRight) { ... }
|
|
224
|
+
*
|
|
225
|
+
* // or
|
|
226
|
+
* const matcher2 = new ArrayMatcher((l, r) =>
|
|
227
|
+
* this.compareProperties(system, version, l, r)
|
|
228
|
+
* );
|
|
229
|
+
* await matcher2.match(propsA, propsB);
|
|
230
|
+
*
|
|
176
231
|
*/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
232
|
+
class ArrayMatcher {
|
|
233
|
+
constructor(matchFn) {
|
|
234
|
+
this.matchFn = matchFn;
|
|
235
|
+
this.matched = [];
|
|
236
|
+
this.unmatchedLeft = [];
|
|
237
|
+
this.unmatchedRight = [];
|
|
183
238
|
}
|
|
184
239
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
240
|
+
/**
|
|
241
|
+
*
|
|
242
|
+
* @param left an array of items (or null/undefined)
|
|
243
|
+
* @param right an array of items (or null/undefined)
|
|
244
|
+
* @returns {Promise<ArrayMatcher>}
|
|
245
|
+
*/
|
|
246
|
+
async match(left, right) {
|
|
247
|
+
if (!left) {
|
|
248
|
+
left = [];
|
|
249
|
+
}
|
|
250
|
+
if (!right) {
|
|
251
|
+
right = [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
this.matched = [];
|
|
255
|
+
this.unmatchedRight = [...right];
|
|
256
|
+
|
|
257
|
+
for (const l of left) {
|
|
258
|
+
let idx = -1;
|
|
259
|
+
for (let i = 0; i < this.unmatchedRight.length; i++) {
|
|
260
|
+
if (await this.matchFn(l, this.unmatchedRight[i])) {
|
|
261
|
+
idx = i;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (idx !== -1) {
|
|
266
|
+
this.matched.push({ left: l, right: this.unmatchedRight[idx] });
|
|
267
|
+
this.unmatchedRight.splice(idx, 1);
|
|
268
|
+
} else {
|
|
269
|
+
this.unmatchedLeft.push(l);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
192
272
|
|
|
193
|
-
|
|
273
|
+
return this;
|
|
274
|
+
}
|
|
194
275
|
}
|
|
195
276
|
|
|
196
|
-
module.exports = { Utilities, validateParameter, validateOptionalParameter, validateArrayParameter, validateResource, strToBool, getValuePrimitive, getValueDT, getValueName, isAbsoluteUrl
|
|
277
|
+
module.exports = { Utilities, ArrayMatcher, validateParameter, validateOptionalParameter, validateArrayParameter, validateResource, strToBool, getValuePrimitive, getValueDT, getValueName, isAbsoluteUrl };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fhirsmith",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"engines": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"express-rate-limit": "^7.4.1",
|
|
45
45
|
"express-session": "^1.19.0",
|
|
46
46
|
"fast-xml-parser": "^5.3.4",
|
|
47
|
-
"fhir-validator-wrapper": "1.2.
|
|
47
|
+
"fhir-validator-wrapper": "1.2.2",
|
|
48
48
|
"fhirpath": "^4.8.3",
|
|
49
49
|
"fs-extra": "^11.3.3",
|
|
50
50
|
"inquirer": "^8.2.5",
|
|
@@ -13,9 +13,10 @@ const path = require('path');
|
|
|
13
13
|
class PackageCrawler {
|
|
14
14
|
log;
|
|
15
15
|
|
|
16
|
-
constructor(config, db) {
|
|
16
|
+
constructor(config, db, stats) {
|
|
17
17
|
this.config = config;
|
|
18
18
|
this.db = db;
|
|
19
|
+
this.stats = stats;
|
|
19
20
|
this.totalBytes = 0;
|
|
20
21
|
this.crawlerLog = {};
|
|
21
22
|
this.errors = '';
|
|
@@ -36,6 +37,7 @@ class PackageCrawler {
|
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
this.log.info('Running web crawler for packages using master URL: '+ this.config.masterUrl);
|
|
40
|
+
this.stats.task('Package Crawler', 'Running');
|
|
39
41
|
|
|
40
42
|
try {
|
|
41
43
|
// Fetch the master JSON file
|
|
@@ -54,6 +56,7 @@ class PackageCrawler {
|
|
|
54
56
|
this.log.info('Skipping feed with no URL: '+ feedConfig);
|
|
55
57
|
continue;
|
|
56
58
|
}
|
|
59
|
+
this.stats.task('Package Crawler', 'Running for '+feedConfig.url);
|
|
57
60
|
|
|
58
61
|
try {
|
|
59
62
|
await this.updateTheFeed(
|
|
@@ -76,6 +79,7 @@ class PackageCrawler {
|
|
|
76
79
|
this.log.info(`Web crawler completed successfully in ${runTime}ms`);
|
|
77
80
|
this.log.info(`Total bytes processed: ${this.totalBytes}`);
|
|
78
81
|
|
|
82
|
+
this.stats.task('Package Crawler', 'Complete');
|
|
79
83
|
return this.crawlerLog;
|
|
80
84
|
|
|
81
85
|
} catch (error) {
|
|
@@ -83,6 +87,7 @@ class PackageCrawler {
|
|
|
83
87
|
this.crawlerLog.runTime = `${runTime}ms`;
|
|
84
88
|
this.crawlerLog.fatalException = error.message;
|
|
85
89
|
this.crawlerLog.endTime = new Date().toISOString();
|
|
90
|
+
this.stats.task('Package Crawler', 'Error: '+error.message);
|
|
86
91
|
|
|
87
92
|
this.log.error('Web crawler failed: '+ error);
|
|
88
93
|
throw error;
|
package/packages/packages.js
CHANGED
|
@@ -12,9 +12,10 @@ const fs = require('fs');
|
|
|
12
12
|
const PackageCrawler = require('./package-crawler.js');
|
|
13
13
|
const htmlServer = require('../library/html-server');
|
|
14
14
|
const folders = require('../library/folder-setup');
|
|
15
|
-
|
|
15
|
+
const escape = require('escape-html');
|
|
16
16
|
const Logger = require('../library/logger');
|
|
17
17
|
const {validateParameter} = require("../library/utilities");
|
|
18
|
+
const {describeCron} = require("../library/cron-utilities");
|
|
18
19
|
const pckLog = Logger.getInstance().child({ module: 'packages' });
|
|
19
20
|
|
|
20
21
|
class PackagesModule {
|
|
@@ -125,24 +126,6 @@ class PackagesModule {
|
|
|
125
126
|
};
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
// Enhanced HTML escaping
|
|
129
|
-
escapeHtml(str) {
|
|
130
|
-
if (!str || typeof str !== 'string') return '';
|
|
131
|
-
|
|
132
|
-
const escapeMap = {
|
|
133
|
-
'&': '&',
|
|
134
|
-
'<': '<',
|
|
135
|
-
'>': '>',
|
|
136
|
-
'"': '"',
|
|
137
|
-
"'": ''',
|
|
138
|
-
'/': '/',
|
|
139
|
-
'`': '`',
|
|
140
|
-
'=': '='
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
return str.replace(/[&<>"'`=/]/g, (match) => escapeMap[match]);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
129
|
buildSecureQuery(baseQuery, conditions = []) {
|
|
147
130
|
let query = baseQuery;
|
|
148
131
|
const params = [];
|
|
@@ -572,7 +555,7 @@ class PackagesModule {
|
|
|
572
555
|
await this.ensureMirrorDirectory();
|
|
573
556
|
|
|
574
557
|
// Initialize the crawler
|
|
575
|
-
this.crawler = new PackageCrawler(this.config, this.db);
|
|
558
|
+
this.crawler = new PackageCrawler(this.config, this.db, this.stats);
|
|
576
559
|
|
|
577
560
|
// Start the hourly web crawler if enabled
|
|
578
561
|
if (config.crawler.enabled) {
|
|
@@ -804,6 +787,7 @@ class PackagesModule {
|
|
|
804
787
|
|
|
805
788
|
startCrawlerJob() {
|
|
806
789
|
if (this.config.crawler && this.config.crawler.schedule) {
|
|
790
|
+
this.stats.addTask("Package Crawler", describeCron(this.config.crawler.schedule));
|
|
807
791
|
this.crawlerJob = cron.schedule(this.config.crawler.schedule, async () => {
|
|
808
792
|
pckLog.info('Starting scheduled package crawler...');
|
|
809
793
|
try {
|
|
@@ -1445,12 +1429,12 @@ class PackagesModule {
|
|
|
1445
1429
|
|
|
1446
1430
|
for (const pkg of updates) {
|
|
1447
1431
|
table += '<tr>';
|
|
1448
|
-
table += `<td><a href="${
|
|
1449
|
-
table += `<td>${
|
|
1450
|
-
table += `<td>${
|
|
1451
|
-
table += `<td>${
|
|
1432
|
+
table += `<td><a href="${escape(pkg.url)}">${escape(pkg.name)}</a></td>`;
|
|
1433
|
+
table += `<td>${escape(pkg.version)}</td>`;
|
|
1434
|
+
table += `<td>${escape(pkg.fhirVersion)}</td>`;
|
|
1435
|
+
table += `<td>${escape(pkg.kind)}</td>`;
|
|
1452
1436
|
table += `<td>${new Date(pkg.date).toLocaleDateString()} ${new Date(pkg.date).toLocaleTimeString()}</td>`;
|
|
1453
|
-
table += `<td>${
|
|
1437
|
+
table += `<td>${escape(pkg.canonical || '')}</td>`;
|
|
1454
1438
|
table += '</tr>';
|
|
1455
1439
|
}
|
|
1456
1440
|
|
|
@@ -1899,12 +1883,12 @@ class PackagesModule {
|
|
|
1899
1883
|
|
|
1900
1884
|
for (const pv of sortedVersions) {
|
|
1901
1885
|
table += '<tr>';
|
|
1902
|
-
table += `<td title="${
|
|
1903
|
-
table += `<td>${
|
|
1904
|
-
table += `<td>${
|
|
1886
|
+
table += `<td title="${escape(pv.GUID)}"><strong>${escape(pv.Version)}</strong></td>`;
|
|
1887
|
+
table += `<td>${escape(this.interpretVersion(pv.FhirVersions))}</td>`;
|
|
1888
|
+
table += `<td>${escape(this.codeForKind(pv.Kind))}</td>`;
|
|
1905
1889
|
table += `<td>${new Date(pv.PubDate).toLocaleDateString()}</td>`;
|
|
1906
1890
|
table += `<td>${(pv.DownloadCount || 0).toLocaleString()}</td>`;
|
|
1907
|
-
table += `<td><a href="/packages/${
|
|
1891
|
+
table += `<td><a href="/packages/${escape(id)}/${escape(pv.Version)}" class="btn btn-sm btn-primary">Download</a></td>`;
|
|
1908
1892
|
table += '</tr>';
|
|
1909
1893
|
}
|
|
1910
1894
|
|
|
@@ -1949,7 +1933,7 @@ class PackagesModule {
|
|
|
1949
1933
|
let content = '<div class="row mb-4">';
|
|
1950
1934
|
content += '<div class="col-12">';
|
|
1951
1935
|
content += '<table class="grid">';
|
|
1952
|
-
content += `<tr><td>Package ID:</td><td>${
|
|
1936
|
+
content += `<tr><td>Package ID:</td><td>${escape(id)}</td></tr>`;
|
|
1953
1937
|
content += `<tr><td>Description</td><td>${vars.desc}</td></tr>`;
|
|
1954
1938
|
content += `<tr><td>Total Versions:</td><td>${vars.count}</td></tr>`;
|
|
1955
1939
|
content += `<tr><td>Total Downloads:</td><td>${vars.downloads}</td></tr>`;
|
|
@@ -1969,7 +1953,7 @@ class PackagesModule {
|
|
|
1969
1953
|
formatTextToHTML(text) {
|
|
1970
1954
|
if (!text) return '';
|
|
1971
1955
|
// Basic text to HTML formatting - convert newlines to <br>
|
|
1972
|
-
return
|
|
1956
|
+
return escape(text).replace(/\n/g, '<br>');
|
|
1973
1957
|
}
|
|
1974
1958
|
|
|
1975
1959
|
async serveSearch(req, res) {
|
|
@@ -2142,13 +2126,13 @@ class PackagesModule {
|
|
|
2142
2126
|
|
|
2143
2127
|
for (const pkg of results) {
|
|
2144
2128
|
table += '<tr>';
|
|
2145
|
-
table += `<td><a href="${
|
|
2146
|
-
table += `<td>${
|
|
2147
|
-
table += `<td>${
|
|
2148
|
-
table += `<td>${
|
|
2129
|
+
table += `<td><a href="${escape(pkg.url)}">${escape(pkg.name)}</a></td>`;
|
|
2130
|
+
table += `<td>${escape(pkg.version)} (<a href="/packages/${escape(pkg.name)}">all</a>)</td>`;
|
|
2131
|
+
table += `<td>${escape(pkg.fhirVersion)}</td>`;
|
|
2132
|
+
table += `<td>${escape(pkg.kind)}</td>`;
|
|
2149
2133
|
table += `<td>${pkg.date ? new Date(pkg.date).toLocaleDateString() : 'N/A'}</td>`;
|
|
2150
2134
|
table += `<td>${pkg.count ? pkg.count.toLocaleString() : 'N/A'}</td>`;
|
|
2151
|
-
table += `<td>${
|
|
2135
|
+
table += `<td>${escape(pkg.canonical || '')}</td>`;
|
|
2152
2136
|
table += '</tr>';
|
|
2153
2137
|
}
|
|
2154
2138
|
|
|
@@ -2202,22 +2186,22 @@ class PackagesModule {
|
|
|
2202
2186
|
|
|
2203
2187
|
content += '<tr>';
|
|
2204
2188
|
content += '<td>Id</td>';
|
|
2205
|
-
content += `<td><input type="text" name="name" value="${
|
|
2189
|
+
content += `<td><input type="text" name="name" value="${escape(vars.name)}"></td>`;
|
|
2206
2190
|
content += '</tr>';
|
|
2207
2191
|
|
|
2208
2192
|
content += '<tr>';
|
|
2209
2193
|
content += '<td>Depends On</td>';
|
|
2210
|
-
content += `<td><input type="text" name="dependson" value="${
|
|
2194
|
+
content += `<td><input type="text" name="dependson" value="${escape(vars.dependson)}"> <i>includes both direct and indirect dependencies</i></td>`;
|
|
2211
2195
|
content += '</tr>';
|
|
2212
2196
|
|
|
2213
2197
|
content += '<tr>';
|
|
2214
2198
|
content += '<td>Canonical (Package)</td>';
|
|
2215
|
-
content += `<td><input type="text" name="pkgcanonical" value="${
|
|
2199
|
+
content += `<td><input type="text" name="pkgcanonical" value="${escape(vars.canonicalPkg)}"></td>`;
|
|
2216
2200
|
content += '</tr>';
|
|
2217
2201
|
|
|
2218
2202
|
content += '<tr>';
|
|
2219
2203
|
content += '<td>Canonical (Resource)</td>';
|
|
2220
|
-
content += `<td><input type="text" name="canonical" value="${
|
|
2204
|
+
content += `<td><input type="text" name="canonical" value="${escape(vars.canonicalUrl)}"></td>`;
|
|
2221
2205
|
content += '</tr>';
|
|
2222
2206
|
|
|
2223
2207
|
content += '<tr>';
|
|
@@ -2368,10 +2352,10 @@ class PackagesModule {
|
|
|
2368
2352
|
<h1>FHIR Package Search</h1>
|
|
2369
2353
|
|
|
2370
2354
|
<form class="search-form" method="GET">
|
|
2371
|
-
<input type="text" name="name" placeholder="Package name" value="${
|
|
2372
|
-
<input type="text" name="dependson" placeholder="Depends on" value="${
|
|
2373
|
-
<input type="text" name="canonicalPkg" placeholder="Canonical package" value="${
|
|
2374
|
-
<input type="text" name="canonicalUrl" placeholder="Canonical URL" value="${
|
|
2355
|
+
<input type="text" name="name" placeholder="Package name" value="${escape(name)}">
|
|
2356
|
+
<input type="text" name="dependson" placeholder="Depends on" value="${escape(dependson)}">
|
|
2357
|
+
<input type="text" name="canonicalPkg" placeholder="Canonical package" value="${escape(canonicalPkg)}">
|
|
2358
|
+
<input type="text" name="canonicalUrl" placeholder="Canonical URL" value="${escape(canonicalUrl)}">
|
|
2375
2359
|
<select name="fhirVersion">
|
|
2376
2360
|
<option value="">Any FHIR version</option>
|
|
2377
2361
|
<option value="R2" ${fhirVersion === 'R2' ? 'selected' : ''}>R2</option>
|
|
@@ -2387,13 +2371,13 @@ class PackagesModule {
|
|
|
2387
2371
|
${results.map(pkg => `
|
|
2388
2372
|
<div class="package">
|
|
2389
2373
|
<div class="package-name">
|
|
2390
|
-
<a href="${pkg.url}">${
|
|
2374
|
+
<a href="${pkg.url}">${escape(pkg.name)}</a> v${escape(pkg.version)}
|
|
2391
2375
|
</div>
|
|
2392
2376
|
<div class="package-details">
|
|
2393
|
-
<strong>FHIR Version:</strong> ${
|
|
2394
|
-
<strong>Type:</strong> ${
|
|
2395
|
-
<strong>Canonical:</strong> ${
|
|
2396
|
-
${pkg.description ? `<strong>Description:</strong> ${
|
|
2377
|
+
<strong>FHIR Version:</strong> ${escape(pkg.fhirVersion)}<br>
|
|
2378
|
+
<strong>Type:</strong> ${escape(pkg.kind)}<br>
|
|
2379
|
+
<strong>Canonical:</strong> ${escape(pkg.canonical)}<br>
|
|
2380
|
+
${pkg.description ? `<strong>Description:</strong> ${escape(pkg.description)}<br>` : ''}
|
|
2397
2381
|
${pkg.date ? `<strong>Published:</strong> ${new Date(pkg.date).toLocaleDateString()}<br>` : ''}
|
|
2398
2382
|
${pkg.count ? `<strong>Downloads:</strong> ${pkg.count}<br>` : ''}
|
|
2399
2383
|
</div>
|
|
@@ -2593,14 +2577,14 @@ class PackagesModule {
|
|
|
2593
2577
|
for (const source of sourcePackages) {
|
|
2594
2578
|
const dependencies = brokenDependencies[source];
|
|
2595
2579
|
table += '<tr>';
|
|
2596
|
-
table += `<td>${
|
|
2580
|
+
table += `<td>${escape(source)}</td>`;
|
|
2597
2581
|
table += '<td>';
|
|
2598
2582
|
|
|
2599
2583
|
for (let i = 0; i < dependencies.length; i++) {
|
|
2600
2584
|
if (i > 0) {
|
|
2601
2585
|
table += ', ';
|
|
2602
2586
|
}
|
|
2603
|
-
table +=
|
|
2587
|
+
table += escape(dependencies[i]);
|
|
2604
2588
|
}
|
|
2605
2589
|
|
|
2606
2590
|
table += '</td>';
|
|
@@ -2656,7 +2640,7 @@ class PackagesModule {
|
|
|
2656
2640
|
buildLogPageContent(status, logData, summary) {
|
|
2657
2641
|
let content = '<div class="row mb-4">';
|
|
2658
2642
|
content += '<div class="col-12">';
|
|
2659
|
-
content += `<div class="alert ${this.crawlerRunning ? 'alert-info' : 'alert-secondary'}">${
|
|
2643
|
+
content += `<div class="alert ${this.crawlerRunning ? 'alert-info' : 'alert-secondary'}">${escape(status)}</div>`;
|
|
2660
2644
|
content += '</div>';
|
|
2661
2645
|
content += '</div>';
|
|
2662
2646
|
|
|
@@ -2752,7 +2736,7 @@ class PackagesModule {
|
|
|
2752
2736
|
output += 'No feeds processed.\n';
|
|
2753
2737
|
}
|
|
2754
2738
|
|
|
2755
|
-
return
|
|
2739
|
+
return escape(output);
|
|
2756
2740
|
}
|
|
2757
2741
|
|
|
2758
2742
|
getStatus() {
|
|
@@ -2825,7 +2809,7 @@ class PackagesModule {
|
|
|
2825
2809
|
if (this.lastRunTime) {
|
|
2826
2810
|
content += `<tr><td>Last Run</td><td>${new Date(this.lastRunTime).toLocaleString()}</td></tr>`;
|
|
2827
2811
|
}
|
|
2828
|
-
content += `<tr><td>Master URL</td><td><a href="${
|
|
2812
|
+
content += `<tr><td>Master URL</td><td><a href="${escape(this.config.masterUrl)}" target="_blank">${escape(this.config.masterUrl)}</a></td></tr>`;
|
|
2829
2813
|
content += '</table>';
|
|
2830
2814
|
content += '</div>';
|
|
2831
2815
|
content += '</div>';
|
package/publisher/publisher.js
CHANGED
|
@@ -5,6 +5,7 @@ const Database = require('sqlite3').Database;
|
|
|
5
5
|
const bcrypt = require('bcrypt');
|
|
6
6
|
const session = require('express-session');
|
|
7
7
|
const folders = require('../library/folder-setup');
|
|
8
|
+
const escape = require('escape-html');
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class PublisherModule {
|
|
@@ -1096,7 +1097,7 @@ class PublisherModule {
|
|
|
1096
1097
|
content += '<a href="/publisher/tasks/' + task.id + '/output" class="btn btn-sm btn-outline-info me-1">View Output</a>';
|
|
1097
1098
|
}
|
|
1098
1099
|
if (task.failure_reason) {
|
|
1099
|
-
content += '<span class="text-danger small me-1">' +
|
|
1100
|
+
content += '<span class="text-danger small me-1">' + escape(task.failure_reason) + '</span>';
|
|
1100
1101
|
}
|
|
1101
1102
|
}
|
|
1102
1103
|
|
|
@@ -1343,7 +1344,7 @@ class PublisherModule {
|
|
|
1343
1344
|
// Build log section
|
|
1344
1345
|
if (buildLog) {
|
|
1345
1346
|
content += '<h4>Build Log</h4>';
|
|
1346
|
-
content += '<div class="output-viewer">' +
|
|
1347
|
+
content += '<div class="output-viewer">' + escape(buildLog) + '</div>';
|
|
1347
1348
|
} else if (task.status === 'building') {
|
|
1348
1349
|
content += '<h4>Build Log</h4>';
|
|
1349
1350
|
content += '<p><em>Build in progress... Log will appear when available.</em></p>';
|
|
@@ -1424,28 +1425,28 @@ class PublisherModule {
|
|
|
1424
1425
|
|
|
1425
1426
|
const htmlServer = require('../library/html-server');
|
|
1426
1427
|
|
|
1427
|
-
let content = '<h3>Task History: #' + task.id + ' — ' +
|
|
1428
|
+
let content = '<h3>Task History: #' + task.id + ' — ' + escape(task.npm_package_id) + '#' + escape(task.version) + '</h3>';
|
|
1428
1429
|
|
|
1429
1430
|
// Task details summary card
|
|
1430
1431
|
content += '<div class="card mb-4"><div class="card-body">';
|
|
1431
1432
|
content += '<div class="row">';
|
|
1432
1433
|
content += '<div class="col-md-6">';
|
|
1433
1434
|
content += '<p><strong>Status:</strong> <span class="badge bg-' + this.getStatusColor(task.status) + '">' + task.status + '</span></p>';
|
|
1434
|
-
content += '<p><strong>Package:</strong> <code>' +
|
|
1435
|
-
content += '<p><strong>Version:</strong> ' +
|
|
1436
|
-
content += '<p><strong>Website:</strong> ' +
|
|
1435
|
+
content += '<p><strong>Package:</strong> <code>' + escape(task.npm_package_id) + '</code></p>';
|
|
1436
|
+
content += '<p><strong>Version:</strong> ' + escape(task.version) + '</p>';
|
|
1437
|
+
content += '<p><strong>Website:</strong> ' + escape(task.website_name) + '</p>';
|
|
1437
1438
|
content += '</div>';
|
|
1438
1439
|
content += '<div class="col-md-6">';
|
|
1439
|
-
content += '<p><strong>GitHub:</strong> ' +
|
|
1440
|
-
content += '<p><strong>Created by:</strong> ' +
|
|
1440
|
+
content += '<p><strong>GitHub:</strong> ' + escape(task.github_org) + '/' + escape(task.github_repo) + ' (' + escape(task.git_branch) + ')</p>';
|
|
1441
|
+
content += '<p><strong>Created by:</strong> ' + escape(task.user_name) + ' (' + escape(task.user_login) + ')</p>';
|
|
1441
1442
|
if (task.approved_by_name) {
|
|
1442
|
-
content += '<p><strong>Approved by:</strong> ' +
|
|
1443
|
+
content += '<p><strong>Approved by:</strong> ' + escape(task.approved_by_name) + '</p>';
|
|
1443
1444
|
}
|
|
1444
1445
|
if (task.local_folder) {
|
|
1445
|
-
content += '<p><strong>Local folder:</strong> <code>' +
|
|
1446
|
+
content += '<p><strong>Local folder:</strong> <code>' + escape(task.local_folder) + '</code></p>';
|
|
1446
1447
|
}
|
|
1447
1448
|
if (task.failure_reason) {
|
|
1448
|
-
content += '<p><strong>Failure reason:</strong> <span class="text-danger">' +
|
|
1449
|
+
content += '<p><strong>Failure reason:</strong> <span class="text-danger">' + escape(task.failure_reason) + '</span></p>';
|
|
1449
1450
|
}
|
|
1450
1451
|
content += '</div>';
|
|
1451
1452
|
content += '</div>';
|
|
@@ -1455,7 +1456,7 @@ class PublisherModule {
|
|
|
1455
1456
|
if (task.announcement) {
|
|
1456
1457
|
content += '<div class="card mb-4"><div class="card-body">';
|
|
1457
1458
|
content += '<h5>Announcement</h5>';
|
|
1458
|
-
content += '<pre class="mb-0" style="white-space: pre-wrap;">' +
|
|
1459
|
+
content += '<pre class="mb-0" style="white-space: pre-wrap;">' + escape(task.announcement) + '</pre>';
|
|
1459
1460
|
content += '</div></div>';
|
|
1460
1461
|
}
|
|
1461
1462
|
|
|
@@ -1464,7 +1465,7 @@ class PublisherModule {
|
|
|
1464
1465
|
|
|
1465
1466
|
// Status transition timestamps from the task record
|
|
1466
1467
|
if (task.queued_at) {
|
|
1467
|
-
events.push({ timestamp: task.queued_at, type: 'status', icon: '📋', label: 'Task queued', detail: 'Created by ' +
|
|
1468
|
+
events.push({ timestamp: task.queued_at, type: 'status', icon: '📋', label: 'Task queued', detail: 'Created by ' + escape(task.user_name), css: '' });
|
|
1468
1469
|
}
|
|
1469
1470
|
if (task.building_at) {
|
|
1470
1471
|
events.push({ timestamp: task.building_at, type: 'status', icon: '🔨', label: 'Draft build started', detail: '', css: '' });
|
|
@@ -1473,14 +1474,14 @@ class PublisherModule {
|
|
|
1473
1474
|
events.push({ timestamp: task.waiting_approval_at, type: 'status', icon: '⏳', label: 'Waiting for approval', detail: 'Draft build completed', css: '' });
|
|
1474
1475
|
}
|
|
1475
1476
|
if (task.publishing_at) {
|
|
1476
|
-
const approver = task.approved_by_name ? 'Approved by ' +
|
|
1477
|
+
const approver = task.approved_by_name ? 'Approved by ' + escape(task.approved_by_name) : '';
|
|
1477
1478
|
events.push({ timestamp: task.publishing_at, type: 'status', icon: '🚀', label: 'Publishing started', detail: approver, css: '' });
|
|
1478
1479
|
}
|
|
1479
1480
|
if (task.completed_at) {
|
|
1480
1481
|
events.push({ timestamp: task.completed_at, type: 'status', icon: '✅', label: 'Completed', detail: '', css: 'text-success' });
|
|
1481
1482
|
}
|
|
1482
1483
|
if (task.failed_at) {
|
|
1483
|
-
events.push({ timestamp: task.failed_at, type: 'status', icon: '❌', label: 'Failed', detail: task.failure_reason ?
|
|
1484
|
+
events.push({ timestamp: task.failed_at, type: 'status', icon: '❌', label: 'Failed', detail: task.failure_reason ? escape(task.failure_reason) : '', css: 'text-danger' });
|
|
1484
1485
|
}
|
|
1485
1486
|
|
|
1486
1487
|
// Task log entries
|
|
@@ -1489,7 +1490,7 @@ class PublisherModule {
|
|
|
1489
1490
|
timestamp: log.timestamp,
|
|
1490
1491
|
type: 'log',
|
|
1491
1492
|
icon: log.level === 'error' ? '🔴' : log.level === 'warn' ? '🟡' : '🔵',
|
|
1492
|
-
label:
|
|
1493
|
+
label: escape(log.message),
|
|
1493
1494
|
detail: '',
|
|
1494
1495
|
css: log.level === 'error' ? 'text-danger' : log.level === 'warn' ? 'text-warning' : 'text-muted'
|
|
1495
1496
|
});
|
|
@@ -1497,8 +1498,8 @@ class PublisherModule {
|
|
|
1497
1498
|
|
|
1498
1499
|
// User actions
|
|
1499
1500
|
for (const action of actions) {
|
|
1500
|
-
const who = action.user_name ?
|
|
1501
|
-
const ip = action.ip_address ? ' from ' +
|
|
1501
|
+
const who = action.user_name ? escape(action.user_name) + ' (' + escape(action.user_login) + ')' : 'Unknown';
|
|
1502
|
+
const ip = action.ip_address ? ' from ' + escape(action.ip_address) : '';
|
|
1502
1503
|
let actionLabel = action.action;
|
|
1503
1504
|
if (action.action === 'create_task') actionLabel = 'Created task';
|
|
1504
1505
|
else if (action.action === 'approve_task') actionLabel = 'Approved task';
|
|
@@ -1580,15 +1581,6 @@ class PublisherModule {
|
|
|
1580
1581
|
}
|
|
1581
1582
|
}
|
|
1582
1583
|
|
|
1583
|
-
escapeHtml(text) {
|
|
1584
|
-
return text
|
|
1585
|
-
.replace(/&/g, '&')
|
|
1586
|
-
.replace(/</g, '<')
|
|
1587
|
-
.replace(/>/g, '>')
|
|
1588
|
-
.replace(/"/g, '"')
|
|
1589
|
-
.replace(/'/g, ''');
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
1584
|
async renderWebsites(req, res) {
|
|
1593
1585
|
const start = Date.now();
|
|
1594
1586
|
try {
|
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
|
});
|