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/xig/xig.js
CHANGED
|
@@ -15,8 +15,10 @@ const { EventEmitter } = require('events');
|
|
|
15
15
|
const zlib = require('zlib');
|
|
16
16
|
const htmlServer = require('../library/html-server');
|
|
17
17
|
const folders = require('../library/folder-setup');
|
|
18
|
+
const escape = require('escape-html');
|
|
18
19
|
|
|
19
20
|
const Logger = require('../library/logger');
|
|
21
|
+
const {describeCron} = require("../library/cron-utilities");
|
|
20
22
|
const xigLog = Logger.getInstance().child({ module: 'xig' });
|
|
21
23
|
|
|
22
24
|
const router = express.Router();
|
|
@@ -69,26 +71,6 @@ function getUpdateHistory() {
|
|
|
69
71
|
return updateHistory;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
// Enhanced HTML escaping
|
|
73
|
-
function escapeHtml(text) {
|
|
74
|
-
if (typeof text !== 'string') {
|
|
75
|
-
return String(text);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const map = {
|
|
79
|
-
'&': '&',
|
|
80
|
-
'<': '<',
|
|
81
|
-
'>': '>',
|
|
82
|
-
'"': '"',
|
|
83
|
-
"'": ''',
|
|
84
|
-
'/': '/',
|
|
85
|
-
'`': '`',
|
|
86
|
-
'=': '='
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
return text.replace(/[&<>"'`=/]/g, function(m) { return map[m]; });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
74
|
// URL validation for external requests
|
|
93
75
|
function validateExternalUrl(url) {
|
|
94
76
|
try {
|
|
@@ -421,13 +403,13 @@ function buildContentHtml(contentData) {
|
|
|
421
403
|
let html = '';
|
|
422
404
|
|
|
423
405
|
if (contentData.message) {
|
|
424
|
-
html += `<p>${
|
|
406
|
+
html += `<p>${escape(contentData.message)}</p>`;
|
|
425
407
|
}
|
|
426
408
|
|
|
427
409
|
if (contentData.data && Array.isArray(contentData.data)) {
|
|
428
410
|
html += '<ul>';
|
|
429
411
|
contentData.data.forEach(item => {
|
|
430
|
-
html += `<li>${
|
|
412
|
+
html += `<li>${escape(item)}</li>`;
|
|
431
413
|
});
|
|
432
414
|
html += '</ul>';
|
|
433
415
|
}
|
|
@@ -437,7 +419,7 @@ function buildContentHtml(contentData) {
|
|
|
437
419
|
if (contentData.table.headers) {
|
|
438
420
|
html += '<thead><tr>';
|
|
439
421
|
contentData.table.headers.forEach(header => {
|
|
440
|
-
html += `<th>${
|
|
422
|
+
html += `<th>${escape(header)}</th>`;
|
|
441
423
|
});
|
|
442
424
|
html += '</tr></thead>';
|
|
443
425
|
}
|
|
@@ -446,7 +428,7 @@ function buildContentHtml(contentData) {
|
|
|
446
428
|
contentData.table.rows.forEach(row => {
|
|
447
429
|
html += '<tr>';
|
|
448
430
|
row.forEach(cell => {
|
|
449
|
-
html += `<td>${
|
|
431
|
+
html += `<td>${escape(cell)}</td>`;
|
|
450
432
|
});
|
|
451
433
|
html += '</tr>';
|
|
452
434
|
});
|
|
@@ -712,9 +694,9 @@ function renderExtension(details) {
|
|
|
712
694
|
const modifier = parts[1] || '';
|
|
713
695
|
const type = parts[2] || '';
|
|
714
696
|
|
|
715
|
-
return `<td>${
|
|
697
|
+
return `<td>${escape(context)}</td><td>${escape(modifier)}</td><td>${escape(type)}</td>`;
|
|
716
698
|
} catch (error) {
|
|
717
|
-
return `<td colspan="3">${
|
|
699
|
+
return `<td colspan="3">${escape(details)}</td>`;
|
|
718
700
|
}
|
|
719
701
|
}
|
|
720
702
|
|
|
@@ -841,16 +823,16 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
|
|
|
841
823
|
// Package column
|
|
842
824
|
const packageObj = getPackage(row.PackageKey);
|
|
843
825
|
if (packageObj && packageObj.Web) {
|
|
844
|
-
parts.push(`<td><a href="${
|
|
826
|
+
parts.push(`<td><a href="${escape(packageObj.Web)}" target="_blank">${escape(packageObj.Id)}</a></td>`);
|
|
845
827
|
} else if (packageObj) {
|
|
846
|
-
parts.push(`<td>${
|
|
828
|
+
parts.push(`<td>${escape(packageObj.Id)}</td>`);
|
|
847
829
|
} else {
|
|
848
|
-
parts.push(`<td>Package ${
|
|
830
|
+
parts.push(`<td>Package ${escape(String(row.PackageKey))}</td>`);
|
|
849
831
|
}
|
|
850
832
|
|
|
851
833
|
// Version column (if not filtered)
|
|
852
834
|
if (!ver || ver === '') {
|
|
853
|
-
parts.push(`<td>${
|
|
835
|
+
parts.push(`<td>${escape(showVersion(row))}</td>`);
|
|
854
836
|
}
|
|
855
837
|
|
|
856
838
|
// Identity column with complex link logic
|
|
@@ -864,53 +846,53 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
|
|
|
864
846
|
}
|
|
865
847
|
|
|
866
848
|
const identityText = (row.ResourceType + '/').replace(resourceTypePrefix, '') + row.Id;
|
|
867
|
-
parts.push(`<td><a href="${identityLink}">${
|
|
849
|
+
parts.push(`<td><a href="${identityLink}">${escape(identityText)}</a></td>`);
|
|
868
850
|
|
|
869
851
|
// Name/Title column
|
|
870
852
|
const displayName = row.Title || row.Name || '';
|
|
871
|
-
parts.push(`<td>${
|
|
853
|
+
parts.push(`<td>${escape(displayName)}</td>`);
|
|
872
854
|
|
|
873
855
|
// Status column
|
|
874
856
|
if (row.StandardsStatus) {
|
|
875
|
-
parts.push(`<td>${
|
|
857
|
+
parts.push(`<td>${escape(row.StandardsStatus || '')}</td>`);
|
|
876
858
|
} else {
|
|
877
|
-
parts.push(`<td>${
|
|
859
|
+
parts.push(`<td>${escape(row.Status || '')}</td>`);
|
|
878
860
|
}
|
|
879
861
|
|
|
880
862
|
// FMM/WG Columns
|
|
881
|
-
parts.push(`<td>${
|
|
882
|
-
parts.push(`<td>${
|
|
863
|
+
parts.push(`<td>${escape(row.FMM || '')}</td>`);
|
|
864
|
+
parts.push(`<td>${escape(row.WG || '')}</td>`);
|
|
883
865
|
|
|
884
866
|
// Date column
|
|
885
867
|
parts.push(`<td>${formatDate(row.Date)}</td>`);
|
|
886
868
|
|
|
887
869
|
// Realm column (if not filtered)
|
|
888
870
|
if (!realm || realm === '') {
|
|
889
|
-
parts.push(`<td>${
|
|
871
|
+
parts.push(`<td>${escape(row.Realm || '')}</td>`);
|
|
890
872
|
}
|
|
891
873
|
|
|
892
874
|
// Authority column (if not filtered)
|
|
893
875
|
if (!auth || auth === '') {
|
|
894
|
-
parts.push(`<td>${
|
|
876
|
+
parts.push(`<td>${escape(row.Authority || '')}</td>`);
|
|
895
877
|
}
|
|
896
878
|
|
|
897
879
|
// Type-specific columns
|
|
898
880
|
switch (type) {
|
|
899
881
|
case 'cs': // CodeSystem
|
|
900
882
|
if (row.Supplements && row.Supplements !== '') {
|
|
901
|
-
parts.push(`<td>Suppl: ${
|
|
883
|
+
parts.push(`<td>Suppl: ${escape(row.Supplements)}</td>`);
|
|
902
884
|
} else {
|
|
903
|
-
parts.push(`<td>${
|
|
885
|
+
parts.push(`<td>${escape(row.Content || '')}</td>`);
|
|
904
886
|
}
|
|
905
887
|
break;
|
|
906
888
|
case 'rp': // Resource Profiles
|
|
907
889
|
if (!rt || rt === '') {
|
|
908
|
-
parts.push(`<td>${
|
|
890
|
+
parts.push(`<td>${escape(row.Type || '')}</td>`);
|
|
909
891
|
}
|
|
910
892
|
break;
|
|
911
893
|
case 'dp': // Datatype Profiles
|
|
912
894
|
if (!rt || rt === '') {
|
|
913
|
-
parts.push(`<td>${
|
|
895
|
+
parts.push(`<td>${escape(row.Type || '')}</td>`);
|
|
914
896
|
}
|
|
915
897
|
break;
|
|
916
898
|
case 'ext': // Extensions
|
|
@@ -919,13 +901,13 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
|
|
|
919
901
|
case 'vs': // ValueSets
|
|
920
902
|
case 'cm': { // ConceptMaps
|
|
921
903
|
const details = (row.Details || '').replace(/,/g, ' ');
|
|
922
|
-
parts.push(`<td>${
|
|
904
|
+
parts.push(`<td>${escape(details)}</td>`);
|
|
923
905
|
break;
|
|
924
906
|
}
|
|
925
907
|
case 'lm': { // Logical Models
|
|
926
908
|
const packageCanonical = packageObj ? packageObj.Canonical : '';
|
|
927
909
|
const typeText = (row.Type || '').replace(packageCanonical + 'StructureDefinition/', '');
|
|
928
|
-
parts.push(`<td>${
|
|
910
|
+
parts.push(`<td>${escape(typeText)}</td>`);
|
|
929
911
|
break;
|
|
930
912
|
}
|
|
931
913
|
}
|
|
@@ -940,7 +922,7 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
|
|
|
940
922
|
|
|
941
923
|
} catch (error) {
|
|
942
924
|
xigLog.error(`Error building resource table: ${error.message}`);
|
|
943
|
-
return `<p class="text-danger">Error loading resource list: ${
|
|
925
|
+
return `<p class="text-danger">Error loading resource list: ${escape(error.message)}</p>`;
|
|
944
926
|
}
|
|
945
927
|
}
|
|
946
928
|
|
|
@@ -980,9 +962,9 @@ async function buildSummaryStats(queryParams, baseUrl) {
|
|
|
980
962
|
});
|
|
981
963
|
|
|
982
964
|
const linkUrl = buildVersionLinkUrl(baseUrl, queryParams, version);
|
|
983
|
-
html += `<li><a href="${linkUrl}">${
|
|
965
|
+
html += `<li><a href="${linkUrl}">${escape(version)}</a>: ${count.toLocaleString()}</li>`;
|
|
984
966
|
} catch (error) {
|
|
985
|
-
html += `<li>${
|
|
967
|
+
html += `<li>${escape(version)}: Error</li>`;
|
|
986
968
|
}
|
|
987
969
|
}
|
|
988
970
|
html += '</ul>';
|
|
@@ -1015,7 +997,7 @@ async function buildSummaryStats(queryParams, baseUrl) {
|
|
|
1015
997
|
html += `<li>none: ${count.toLocaleString()}</li>`;
|
|
1016
998
|
} else {
|
|
1017
999
|
const linkUrl = buildAuthorityLinkUrl(baseUrl, queryParams, authority);
|
|
1018
|
-
html += `<li><a href="${linkUrl}">${
|
|
1000
|
+
html += `<li><a href="${linkUrl}">${escape(authority)}</a>: ${count.toLocaleString()}</li>`;
|
|
1019
1001
|
}
|
|
1020
1002
|
});
|
|
1021
1003
|
html += '</ul>';
|
|
@@ -1051,7 +1033,7 @@ async function buildSummaryStats(queryParams, baseUrl) {
|
|
|
1051
1033
|
c++;
|
|
1052
1034
|
} else {
|
|
1053
1035
|
const linkUrl = buildRealmLinkUrl(baseUrl, queryParams, realmCode);
|
|
1054
|
-
html += `<li><a href="${linkUrl}">${
|
|
1036
|
+
html += `<li><a href="${linkUrl}">${escape(realmCode)}</a>: ${count.toLocaleString()}</li>`;
|
|
1055
1037
|
}
|
|
1056
1038
|
});
|
|
1057
1039
|
if (c > 0) {
|
|
@@ -1064,7 +1046,7 @@ async function buildSummaryStats(queryParams, baseUrl) {
|
|
|
1064
1046
|
} catch (error) {
|
|
1065
1047
|
console.error(error);
|
|
1066
1048
|
xigLog.error(`Error building summary stats: ${error.message}`);
|
|
1067
|
-
html += `<p class="text-warning">Error loading summary statistics: ${
|
|
1049
|
+
html += `<p class="text-warning">Error loading summary statistics: ${escape(error.message)}</p>`;
|
|
1068
1050
|
}
|
|
1069
1051
|
|
|
1070
1052
|
return html;
|
|
@@ -1123,9 +1105,9 @@ function makeSelect(selectedValue, optionsList, name = 'rt') {
|
|
|
1123
1105
|
}
|
|
1124
1106
|
|
|
1125
1107
|
if (selectedValue === code) {
|
|
1126
|
-
html += `<option value="${
|
|
1108
|
+
html += `<option value="${escape(code)}" selected="true">${escape(display)}</option>`;
|
|
1127
1109
|
} else {
|
|
1128
|
-
html += `<option value="${
|
|
1110
|
+
html += `<option value="${escape(code)}">${escape(display)}</option>`;
|
|
1129
1111
|
}
|
|
1130
1112
|
});
|
|
1131
1113
|
|
|
@@ -1140,13 +1122,13 @@ function buildAdditionalForm(queryParams) {
|
|
|
1140
1122
|
|
|
1141
1123
|
// Add hidden inputs to preserve current filter state
|
|
1142
1124
|
if (ver && ver !== '') {
|
|
1143
|
-
html += `<input type="hidden" name="ver" value="${
|
|
1125
|
+
html += `<input type="hidden" name="ver" value="${escape(ver)}"/>`;
|
|
1144
1126
|
}
|
|
1145
1127
|
if (realm && realm !== '') {
|
|
1146
|
-
html += `<input type="hidden" name="realm" value="${
|
|
1128
|
+
html += `<input type="hidden" name="realm" value="${escape(realm)}"/>`;
|
|
1147
1129
|
}
|
|
1148
1130
|
if (auth && auth !== '') {
|
|
1149
|
-
html += `<input type="hidden" name="auth" value="${
|
|
1131
|
+
html += `<input type="hidden" name="auth" value="${escape(auth)}"/>`;
|
|
1150
1132
|
}
|
|
1151
1133
|
|
|
1152
1134
|
// Add type-specific fields
|
|
@@ -1214,7 +1196,7 @@ function buildAdditionalForm(queryParams) {
|
|
|
1214
1196
|
}
|
|
1215
1197
|
|
|
1216
1198
|
// Add text search field
|
|
1217
|
-
html += `Text: <input type="text" name="text" value="${
|
|
1199
|
+
html += `Text: <input type="text" name="text" value="${escape(text || '')}" class="" style="width: 200px;"/> `;
|
|
1218
1200
|
|
|
1219
1201
|
// Add submit button
|
|
1220
1202
|
html += '<input type="submit" value="Search" style="color:rgb(89, 137, 241)"/>';
|
|
@@ -1275,7 +1257,7 @@ function buildPageHeading(queryParams) {
|
|
|
1275
1257
|
default:
|
|
1276
1258
|
// No type selected or unknown type
|
|
1277
1259
|
if (rt && rt !== '') {
|
|
1278
|
-
heading += `Resources - ${
|
|
1260
|
+
heading += `Resources - ${escape(rt)}`;
|
|
1279
1261
|
} else {
|
|
1280
1262
|
heading += 'Resources - All Kinds';
|
|
1281
1263
|
}
|
|
@@ -1284,15 +1266,15 @@ function buildPageHeading(queryParams) {
|
|
|
1284
1266
|
|
|
1285
1267
|
// Add additional qualifiers
|
|
1286
1268
|
if (realm && realm !== '') {
|
|
1287
|
-
heading += `, Realm ${
|
|
1269
|
+
heading += `, Realm ${escape(realm.toUpperCase())}`;
|
|
1288
1270
|
}
|
|
1289
1271
|
|
|
1290
1272
|
if (auth && auth !== '') {
|
|
1291
|
-
heading += `, Authority ${
|
|
1273
|
+
heading += `, Authority ${escape(capitalizeFirst(auth))}`;
|
|
1292
1274
|
}
|
|
1293
1275
|
|
|
1294
1276
|
if (ver && ver !== '') {
|
|
1295
|
-
heading += `, Version ${
|
|
1277
|
+
heading += `, Version ${escape(ver)}`;
|
|
1296
1278
|
}
|
|
1297
1279
|
|
|
1298
1280
|
heading += '</h2>';
|
|
@@ -1329,10 +1311,10 @@ function buildVersionBar(baseUrl, currentParams) {
|
|
|
1329
1311
|
const versions = getCachedSet('versions');
|
|
1330
1312
|
versions.forEach(version => {
|
|
1331
1313
|
if (version === ver) {
|
|
1332
|
-
html += ` | <b>${
|
|
1314
|
+
html += ` | <b>${escape(version)}</b>`;
|
|
1333
1315
|
} else {
|
|
1334
1316
|
const separator = baseUrlWithoutVer.includes('?') ? '&' : '?';
|
|
1335
|
-
html += ` | <a href="${baseUrlWithoutVer}${separator}ver=${encodeURIComponent(version)}">${
|
|
1317
|
+
html += ` | <a href="${baseUrlWithoutVer}${separator}ver=${encodeURIComponent(version)}">${escape(version)}</a>`;
|
|
1336
1318
|
}
|
|
1337
1319
|
});
|
|
1338
1320
|
|
|
@@ -1356,10 +1338,10 @@ function buildAuthorityBar(baseUrl, currentParams) {
|
|
|
1356
1338
|
const authorities = getCachedSet('authorities');
|
|
1357
1339
|
authorities.forEach(authority => {
|
|
1358
1340
|
if (authority === auth) {
|
|
1359
|
-
html += ` | <b>${
|
|
1341
|
+
html += ` | <b>${escape(authority)}</b>`;
|
|
1360
1342
|
} else {
|
|
1361
1343
|
const separator = baseUrlWithoutAuth.includes('?') ? '&' : '?';
|
|
1362
|
-
html += ` | <a href="${baseUrlWithoutAuth}${separator}auth=${encodeURIComponent(authority)}">${
|
|
1344
|
+
html += ` | <a href="${baseUrlWithoutAuth}${separator}auth=${encodeURIComponent(authority)}">${escape(authority)}</a>`;
|
|
1363
1345
|
}
|
|
1364
1346
|
});
|
|
1365
1347
|
|
|
@@ -1383,10 +1365,10 @@ function buildRealmBar(baseUrl, currentParams) {
|
|
|
1383
1365
|
const realms = getCachedSet('realms');
|
|
1384
1366
|
realms.forEach(realmCode => {
|
|
1385
1367
|
if (realmCode === realm) {
|
|
1386
|
-
html += ` | <b>${
|
|
1368
|
+
html += ` | <b>${escape(realmCode)}</b>`;
|
|
1387
1369
|
} else {
|
|
1388
1370
|
const separator = baseUrlWithoutRealm.includes('?') ? '&' : '?';
|
|
1389
|
-
html += ` | <a href="${baseUrlWithoutRealm}${separator}realm=${encodeURIComponent(realmCode)}">${
|
|
1371
|
+
html += ` | <a href="${baseUrlWithoutRealm}${separator}realm=${encodeURIComponent(realmCode)}">${escape(realmCode)}</a>`;
|
|
1390
1372
|
}
|
|
1391
1373
|
});
|
|
1392
1374
|
|
|
@@ -1411,10 +1393,10 @@ function buildTypeBar(baseUrl, currentParams) {
|
|
|
1411
1393
|
if (typesMap instanceof Map) {
|
|
1412
1394
|
typesMap.forEach((display, code) => {
|
|
1413
1395
|
if (code === type) {
|
|
1414
|
-
html += ` | <b>${
|
|
1396
|
+
html += ` | <b>${escape(display)}</b>`;
|
|
1415
1397
|
} else {
|
|
1416
1398
|
const separator = baseUrlWithoutType.includes('?') ? '&' : '?';
|
|
1417
|
-
html += ` | <a href="${baseUrlWithoutType}${separator}type=${encodeURIComponent(code)}">${
|
|
1399
|
+
html += ` | <a href="${baseUrlWithoutType}${separator}type=${encodeURIComponent(code)}">${escape(display)}</a>`;
|
|
1418
1400
|
}
|
|
1419
1401
|
});
|
|
1420
1402
|
}
|
|
@@ -1519,6 +1501,9 @@ function getMetadata(key) {
|
|
|
1519
1501
|
function downloadFile(url, destination, maxRedirects = 5) {
|
|
1520
1502
|
return new Promise((resolve, reject) => {
|
|
1521
1503
|
xigLog.info(`Starting download from ${url}`);
|
|
1504
|
+
if (globalStats) {
|
|
1505
|
+
globalStats.task('XIG Download', `Downloading from ${url}`)
|
|
1506
|
+
}
|
|
1522
1507
|
const downloadMeta = {
|
|
1523
1508
|
url: url,
|
|
1524
1509
|
finalUrl: url,
|
|
@@ -1564,6 +1549,9 @@ function downloadFile(url, destination, maxRedirects = 5) {
|
|
|
1564
1549
|
}
|
|
1565
1550
|
|
|
1566
1551
|
if (response.statusCode !== 200) {
|
|
1552
|
+
if (globalStats) {
|
|
1553
|
+
globalStats.task('XIG Download', `Download failed: ${response.statusCode}`)
|
|
1554
|
+
}
|
|
1567
1555
|
reject(Object.assign(
|
|
1568
1556
|
new Error(`Download failed with HTTP ${response.statusCode}`),
|
|
1569
1557
|
{ downloadMeta }
|
|
@@ -1574,6 +1562,9 @@ function downloadFile(url, destination, maxRedirects = 5) {
|
|
|
1574
1562
|
// Check content length
|
|
1575
1563
|
const maxSize = 10 * 1024 * 1024 * 1024; // 10GB limit
|
|
1576
1564
|
if (downloadMeta.contentLength && downloadMeta.contentLength > maxSize) {
|
|
1565
|
+
if (globalStats) {
|
|
1566
|
+
globalStats.task('XIG Download', `Download failed: too large`)
|
|
1567
|
+
}
|
|
1577
1568
|
reject(Object.assign(new Error('File too large'), { downloadMeta }));
|
|
1578
1569
|
return;
|
|
1579
1570
|
}
|
|
@@ -1584,6 +1575,9 @@ function downloadFile(url, destination, maxRedirects = 5) {
|
|
|
1584
1575
|
downloadMeta.downloadedBytes += chunk.length;
|
|
1585
1576
|
if (downloadMeta.downloadedBytes > maxSize) {
|
|
1586
1577
|
request.destroy();
|
|
1578
|
+
if (globalStats) {
|
|
1579
|
+
globalStats.task('XIG Download', `Download failed: file too large`);
|
|
1580
|
+
}
|
|
1587
1581
|
fs.unlink(destination, () => {}); // Clean up
|
|
1588
1582
|
reject(Object.assign(new Error('File too large'), { downloadMeta }));
|
|
1589
1583
|
return;
|
|
@@ -1596,21 +1590,33 @@ function downloadFile(url, destination, maxRedirects = 5) {
|
|
|
1596
1590
|
fileStream.close();
|
|
1597
1591
|
downloadMeta.durationMs = Date.now() - downloadMeta.startTime;
|
|
1598
1592
|
xigLog.info(`Download completed successfully. Downloaded ${downloadMeta.downloadedBytes} bytes to ${destination}`);
|
|
1593
|
+
if (globalStats) {
|
|
1594
|
+
globalStats.task('XIG Download', `Downloaded ${downloadMeta.downloadedBytes} bytes to ${destination}`);
|
|
1595
|
+
}
|
|
1599
1596
|
resolve(downloadMeta);
|
|
1600
1597
|
});
|
|
1601
1598
|
|
|
1602
1599
|
fileStream.on('error', (err) => {
|
|
1600
|
+
if (globalStats) {
|
|
1601
|
+
globalStats.task('XIG Download', `Download failed`);
|
|
1602
|
+
}
|
|
1603
1603
|
fs.unlink(destination, () => {}); // Delete partial file
|
|
1604
1604
|
reject(Object.assign(err, { downloadMeta }));
|
|
1605
1605
|
});
|
|
1606
1606
|
});
|
|
1607
1607
|
|
|
1608
1608
|
request.on('error', (err) => {
|
|
1609
|
+
if (globalStats) {
|
|
1610
|
+
globalStats.task('XIG Download', `Download Error`);
|
|
1611
|
+
}
|
|
1609
1612
|
reject(Object.assign(err, { downloadMeta }));
|
|
1610
1613
|
});
|
|
1611
1614
|
|
|
1612
1615
|
request.setTimeout(300000, () => { // 5 minutes timeout
|
|
1613
1616
|
request.destroy();
|
|
1617
|
+
if (globalStats) {
|
|
1618
|
+
globalStats.task('XIG Download', `Download Timeout`);
|
|
1619
|
+
}
|
|
1614
1620
|
reject(Object.assign(new Error('Download timeout after 5 minutes'), { downloadMeta }));
|
|
1615
1621
|
});
|
|
1616
1622
|
|
|
@@ -2037,7 +2043,7 @@ function buildStatsTable(statsData) {
|
|
|
2037
2043
|
Object.keys(statsData.cache.tables).forEach(tableName => {
|
|
2038
2044
|
const tableInfo = statsData.cache.tables[tableName];
|
|
2039
2045
|
html += `<tr>`;
|
|
2040
|
-
html += `<td>Cache: ${
|
|
2046
|
+
html += `<td>Cache: ${escape(tableName)}</td>`;
|
|
2041
2047
|
html += `<td>${tableInfo.size.toLocaleString()}</td>`;
|
|
2042
2048
|
html += `<td>${tableInfo.type}</td>`;
|
|
2043
2049
|
html += `</tr>`;
|
|
@@ -2058,12 +2064,12 @@ function buildStatsTable(statsData) {
|
|
|
2058
2064
|
html += `<tr>`;
|
|
2059
2065
|
html += `<td>Database File</td>`;
|
|
2060
2066
|
html += `<td>${(statsData.database.fileSize / 1024 / 1024).toFixed(2)} MB</td>`;
|
|
2061
|
-
html += `<td>${
|
|
2067
|
+
html += `<td>${escape(XIG_DB_PATH)}</td>`;
|
|
2062
2068
|
html += `</tr>`;
|
|
2063
2069
|
|
|
2064
2070
|
html += `<tr>`;
|
|
2065
2071
|
html += `<td>Download Source</td>`;
|
|
2066
|
-
html += `<td colspan="2"><code>${
|
|
2072
|
+
html += `<td colspan="2"><code>${escape(XIG_DB_URL)}</code></td>`;
|
|
2067
2073
|
html += `</tr>`;
|
|
2068
2074
|
|
|
2069
2075
|
html += `<tr>`;
|
|
@@ -2114,13 +2120,13 @@ function buildStatsTable(statsData) {
|
|
|
2114
2120
|
detail += ` (HTTP ${entry.downloadMeta.httpStatus})`;
|
|
2115
2121
|
}
|
|
2116
2122
|
} else if (entry.status === 'failed') {
|
|
2117
|
-
detail =
|
|
2123
|
+
detail = escape(entry.error || 'Unknown error');
|
|
2118
2124
|
if (entry.downloadMeta) {
|
|
2119
2125
|
if (entry.downloadMeta.httpStatus) {
|
|
2120
2126
|
detail += ` (HTTP ${entry.downloadMeta.httpStatus})`;
|
|
2121
2127
|
}
|
|
2122
2128
|
if (entry.downloadMeta.finalUrl !== entry.sourceUrl) {
|
|
2123
|
-
detail += `<br>Redirected to: <code>${
|
|
2129
|
+
detail += `<br>Redirected to: <code>${escape(entry.downloadMeta.finalUrl)}</code>`;
|
|
2124
2130
|
}
|
|
2125
2131
|
if (entry.downloadMeta.downloadedBytes > 0) {
|
|
2126
2132
|
detail += `<br>Partial download: ${(entry.downloadMeta.downloadedBytes / 1024 / 1024).toFixed(1)} MB`;
|
|
@@ -2283,7 +2289,7 @@ router.get('/', async (req, res) => {
|
|
|
2283
2289
|
// Build resource count paragraph
|
|
2284
2290
|
let countParagraph = '<p>';
|
|
2285
2291
|
if (countError) {
|
|
2286
|
-
countParagraph += `<span class="text-warning">Unable to get resource count: ${
|
|
2292
|
+
countParagraph += `<span class="text-warning">Unable to get resource count: ${escape(countError)}</span>`;
|
|
2287
2293
|
} else {
|
|
2288
2294
|
countParagraph += `${resourceCount.toLocaleString()} resources`;
|
|
2289
2295
|
}
|
|
@@ -2356,7 +2362,7 @@ router.get('/stats', async (req, res) => {
|
|
|
2356
2362
|
if (lastAttempt) {
|
|
2357
2363
|
if (lastAttempt.status === 'failed') {
|
|
2358
2364
|
introContent += `<br><strong>Last update attempt failed</strong> at ${new Date(lastAttempt.timestamp).toLocaleString()}: `;
|
|
2359
|
-
introContent += `${
|
|
2365
|
+
introContent += `${escape(lastAttempt.error || 'Unknown error')}`;
|
|
2360
2366
|
if (lastAttempt.downloadMeta && lastAttempt.downloadMeta.httpStatus) {
|
|
2361
2367
|
introContent += ` (HTTP ${lastAttempt.downloadMeta.httpStatus})`;
|
|
2362
2368
|
}
|
|
@@ -2372,7 +2378,7 @@ router.get('/stats', async (req, res) => {
|
|
|
2372
2378
|
// DB is fresh but last attempt failed — still worth showing
|
|
2373
2379
|
introContent += `<div class="alert alert-warning">`;
|
|
2374
2380
|
introContent += `<strong>Last update attempt failed</strong> at ${new Date(lastAttempt.timestamp).toLocaleString()}: `;
|
|
2375
|
-
introContent += `${
|
|
2381
|
+
introContent += `${escape(lastAttempt.error || 'Unknown error')}`;
|
|
2376
2382
|
introContent += `</div>`;
|
|
2377
2383
|
}
|
|
2378
2384
|
|
|
@@ -2419,7 +2425,7 @@ router.get('/resource/:packagePid/:resourceType/:resourceId', async (req, res) =
|
|
|
2419
2425
|
const packageObj = getPackageByPid(dbPackagePid);
|
|
2420
2426
|
if (!packageObj) {
|
|
2421
2427
|
return res.status(404).send(renderPage('Resource Not Found',
|
|
2422
|
-
`<div class="alert alert-danger">Unknown Package: ${
|
|
2428
|
+
`<div class="alert alert-danger">Unknown Package: ${escape(packagePid)}</div>`));
|
|
2423
2429
|
}
|
|
2424
2430
|
|
|
2425
2431
|
// Get resource details
|
|
@@ -2437,7 +2443,7 @@ router.get('/resource/:packagePid/:resourceType/:resourceId', async (req, res) =
|
|
|
2437
2443
|
|
|
2438
2444
|
if (!resourceData) {
|
|
2439
2445
|
return res.status(404).send(renderPage('Resource Not Found',
|
|
2440
|
-
`<div class="alert alert-danger">Unknown Resource: ${
|
|
2446
|
+
`<div class="alert alert-danger">Unknown Resource: ${escape(resourceType)}/${escape(resourceId)} in package ${escape(packagePid)}</div>`));
|
|
2441
2447
|
}
|
|
2442
2448
|
|
|
2443
2449
|
// Build the resource detail page
|
|
@@ -2491,7 +2497,7 @@ async function buildResourceDetailPage(packageObj, resourceData, secure = false)
|
|
|
2491
2497
|
|
|
2492
2498
|
} catch (error) {
|
|
2493
2499
|
xigLog.error(`Error building resource detail content: ${error.message}`);
|
|
2494
|
-
html += `<div class="alert alert-warning">Error loading some content: ${
|
|
2500
|
+
html += `<div class="alert alert-warning">Error loading some content: ${escape(error.message)}</div>`;
|
|
2495
2501
|
}
|
|
2496
2502
|
|
|
2497
2503
|
return html;
|
|
@@ -2503,28 +2509,28 @@ async function buildResourceMetadataTable(packageObj, resourceData) {
|
|
|
2503
2509
|
|
|
2504
2510
|
// Package
|
|
2505
2511
|
if (packageObj && packageObj.Web) {
|
|
2506
|
-
html += `<tr><td><strong>Package</strong></td><td><a href="${
|
|
2512
|
+
html += `<tr><td><strong>Package</strong></td><td><a href="${escape(packageObj.Web)}" target="_blank">${escape(packageObj.Id)}</a></td></tr>`;
|
|
2507
2513
|
} else if (packageObj) {
|
|
2508
|
-
html += `<tr><td><strong>Package</strong></td><td>${
|
|
2514
|
+
html += `<tr><td><strong>Package</strong></td><td>${escape(packageObj.Id)}</td></tr>`;
|
|
2509
2515
|
}
|
|
2510
2516
|
|
|
2511
2517
|
// Type
|
|
2512
|
-
html += `<tr><td><strong>Resource Type</strong></td><td>${
|
|
2518
|
+
html += `<tr><td><strong>Resource Type</strong></td><td>${escape(resourceData.ResourceType)}</td></tr>`;
|
|
2513
2519
|
|
|
2514
2520
|
// Id
|
|
2515
|
-
html += `<tr><td><strong>Id</strong></td><td>${
|
|
2521
|
+
html += `<tr><td><strong>Id</strong></td><td>${escape(resourceData.Id)}</td></tr>`;
|
|
2516
2522
|
|
|
2517
2523
|
// FHIR Versions
|
|
2518
2524
|
const versions = showVersion(resourceData);
|
|
2519
2525
|
if (versions.includes(',')) {
|
|
2520
|
-
html += `<tr><td><strong>FHIR Versions</strong></td><td>${
|
|
2526
|
+
html += `<tr><td><strong>FHIR Versions</strong></td><td>${escape(versions)}</td></tr>`;
|
|
2521
2527
|
} else {
|
|
2522
|
-
html += `<tr><td><strong>FHIR Version</strong></td><td>${
|
|
2528
|
+
html += `<tr><td><strong>FHIR Version</strong></td><td>${escape(versions)}</td></tr>`;
|
|
2523
2529
|
}
|
|
2524
2530
|
|
|
2525
2531
|
// Source
|
|
2526
2532
|
if (resourceData.Web) {
|
|
2527
|
-
html += `<tr><td><strong>Source</strong></td><td><a href="${
|
|
2533
|
+
html += `<tr><td><strong>Source</strong></td><td><a href="${escape(resourceData.Web)}" target="_blank">${escape(resourceData.Web)}</a></td></tr>`;
|
|
2528
2534
|
}
|
|
2529
2535
|
|
|
2530
2536
|
// Add all other non-empty fields
|
|
@@ -2555,7 +2561,7 @@ async function buildResourceMetadataTable(packageObj, resourceData) {
|
|
|
2555
2561
|
const expValue = value === '1' ? 'True' : 'False';
|
|
2556
2562
|
html += `<tr><td><strong>${field.label}</strong></td><td>${expValue}</td></tr>`;
|
|
2557
2563
|
} else {
|
|
2558
|
-
html += `<tr><td><strong>${field.label}</strong></td><td>${
|
|
2564
|
+
html += `<tr><td><strong>${field.label}</strong></td><td>${escape(value)}</td></tr>`;
|
|
2559
2565
|
}
|
|
2560
2566
|
}
|
|
2561
2567
|
});
|
|
@@ -2619,7 +2625,7 @@ async function buildResourceDependencies(resourceData, secure = false) {
|
|
|
2619
2625
|
html += await buildExtensionExamplesSection(resourceData.Url);
|
|
2620
2626
|
}
|
|
2621
2627
|
} catch (error) {
|
|
2622
|
-
html += `<div class="alert alert-warning">Error loading dependencies: ${
|
|
2628
|
+
html += `<div class="alert alert-warning">Error loading dependencies: ${escape(error.message)}</div>`;
|
|
2623
2629
|
}
|
|
2624
2630
|
|
|
2625
2631
|
return html;
|
|
@@ -2662,8 +2668,8 @@ async function buildExtensionExamplesSection(resourceUrl) {
|
|
|
2662
2668
|
const versionName = example.Version ? (versionMap[example.Version] || example.Version.toString()) : '';
|
|
2663
2669
|
|
|
2664
2670
|
html += '<tr>';
|
|
2665
|
-
html += `<td><a href="${
|
|
2666
|
-
html += `<td>${
|
|
2671
|
+
html += `<td><a href="${escape(example.Url || '')}">${escape(example.Name || '')}</a></td>`;
|
|
2672
|
+
html += `<td>${escape(versionName)}</td>`;
|
|
2667
2673
|
html += '</tr>';
|
|
2668
2674
|
});
|
|
2669
2675
|
|
|
@@ -2673,7 +2679,7 @@ async function buildExtensionExamplesSection(resourceUrl) {
|
|
|
2673
2679
|
|
|
2674
2680
|
} catch (error) {
|
|
2675
2681
|
xigLog.error(`Error loading extension examples: ${error.message}`);
|
|
2676
|
-
html += `<div class="alert alert-warning">Error loading extension examples: ${
|
|
2682
|
+
html += `<div class="alert alert-warning">Error loading extension examples: ${escape(error.message)}</div>`;
|
|
2677
2683
|
}
|
|
2678
2684
|
|
|
2679
2685
|
return html;
|
|
@@ -2690,7 +2696,7 @@ function buildDependencyTable(dependencies) {
|
|
|
2690
2696
|
}
|
|
2691
2697
|
currentType = dep.ResourceType;
|
|
2692
2698
|
html += '<table class="table table-bordered">';
|
|
2693
|
-
html += `<tr style="background-color: #eeeeee"><td colspan="2"><strong>${
|
|
2699
|
+
html += `<tr style="background-color: #eeeeee"><td colspan="2"><strong>${escape(currentType)}</strong></td></tr>`;
|
|
2694
2700
|
}
|
|
2695
2701
|
|
|
2696
2702
|
html += '<tr>';
|
|
@@ -2708,15 +2714,15 @@ function buildDependencyTable(dependencies) {
|
|
|
2708
2714
|
const parts = displayUrl.split('/');
|
|
2709
2715
|
displayUrl = parts[parts.length - 1];
|
|
2710
2716
|
}
|
|
2711
|
-
html += `<td><a href="${resourceUrl}">${
|
|
2717
|
+
html += `<td><a href="${resourceUrl}">${escape(displayUrl)}</a></td>`;
|
|
2712
2718
|
} else {
|
|
2713
2719
|
const displayId = dep.ResourceType + '/' + dep.Id;
|
|
2714
|
-
html += `<td><a href="${resourceUrl}">${
|
|
2720
|
+
html += `<td><a href="${resourceUrl}">${escape(displayId)}</a></td>`;
|
|
2715
2721
|
}
|
|
2716
2722
|
|
|
2717
2723
|
// Title or Name
|
|
2718
2724
|
const displayName = dep.Title || dep.Name || '';
|
|
2719
|
-
html += `<td>${
|
|
2725
|
+
html += `<td>${escape(displayName)}</td>`;
|
|
2720
2726
|
|
|
2721
2727
|
html += '</tr>';
|
|
2722
2728
|
});
|
|
@@ -2784,7 +2790,7 @@ async function buildResourceNarrative(resourceKey, packageObj) {
|
|
|
2784
2790
|
|
|
2785
2791
|
} catch (error) {
|
|
2786
2792
|
xigLog.error(`Error loading narrative: ${error.message}`);
|
|
2787
|
-
html += `<div class="alert alert-warning">Error loading narrative: ${
|
|
2793
|
+
html += `<div class="alert alert-warning">Error loading narrative: ${escape(error.message)}</div>`;
|
|
2788
2794
|
}
|
|
2789
2795
|
|
|
2790
2796
|
return html;
|
|
@@ -2833,12 +2839,12 @@ async function buildResourceSource(resourceKey) {
|
|
|
2833
2839
|
const formattedJson = JSON.stringify(jsonData, null, 2);
|
|
2834
2840
|
|
|
2835
2841
|
html += '<pre>';
|
|
2836
|
-
html +=
|
|
2842
|
+
html += escape(formattedJson);
|
|
2837
2843
|
html += '</pre>';
|
|
2838
2844
|
|
|
2839
2845
|
} catch (error) {
|
|
2840
2846
|
xigLog.error(`Error loading source: ${error.message}`);
|
|
2841
|
-
html += `<div class="alert alert-warning">Error loading source: ${
|
|
2847
|
+
html += `<div class="alert alert-warning">Error loading source: ${escape(error.message)}</div>`;
|
|
2842
2848
|
}
|
|
2843
2849
|
|
|
2844
2850
|
return html;
|
|
@@ -2937,6 +2943,9 @@ async function initializeXigModule(stats) {
|
|
|
2937
2943
|
}, 5000);
|
|
2938
2944
|
}
|
|
2939
2945
|
|
|
2946
|
+
if (globalStats) {
|
|
2947
|
+
globalStats.addTask('XIG Download', describeCron(this.config.crawler.schedule));
|
|
2948
|
+
}
|
|
2940
2949
|
// Check if auto-update is enabled
|
|
2941
2950
|
// Note: This assumes we're called only when XIG is enabled
|
|
2942
2951
|
cron.schedule('0 2 * * *', () => {
|
|
@@ -2988,7 +2997,7 @@ module.exports = {
|
|
|
2988
2997
|
// Template functions
|
|
2989
2998
|
renderPage,
|
|
2990
2999
|
buildContentHtml,
|
|
2991
|
-
|
|
3000
|
+
escape,
|
|
2992
3001
|
loadTemplate,
|
|
2993
3002
|
|
|
2994
3003
|
// Control panel functions
|