fhirsmith 0.7.6 → 0.8.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 +26 -0
- package/library/languages.js +10 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +2 -2
- package/publisher/publisher.js +1 -1
- package/registry/registry.js +2 -2
- package/root-bare-template.html +1 -2
- package/server.js +100 -70
- package/stats.js +37 -6
- package/tx/cs/cs-loinc.js +14 -2
- package/tx/cs/cs-rxnorm.js +14 -10
- package/tx/cs/cs-snomed.js +166 -5
- package/tx/html/dash-metrics.liquid +147 -0
- package/tx/importers/import-rxnorm.module.js +4 -30
- package/tx/library/canonical-resource.js +8 -0
- package/tx/library/conceptmap.js +3 -1
- package/tx/library/designations.js +4 -8
- package/tx/library/renderer.js +9 -9
- package/tx/ocl/cm-ocl.cjs +185 -65
- package/tx/ocl/cs-ocl.cjs +69 -50
- package/tx/ocl/jobs/background-queue.cjs +0 -8
- package/tx/ocl/mappers/concept-mapper.cjs +13 -3
- package/tx/ocl/shared/patches.cjs +1 -0
- package/tx/ocl/vs-ocl.cjs +137 -157
- package/tx/operation-context.js +3 -3
- package/tx/provider.js +2 -2
- package/tx/sct/structures.js +5 -0
- package/tx/tx.fhir.org.yml +1 -1
- package/tx/vs/vs-database.js +107 -23
- package/tx/vs/vs-vsac.js +66 -19
- package/tx/workers/search.js +2 -1
- package/tx/workers/translate.js +39 -14
- package/tx/workers/validate.js +3 -3
- package/xig/xig.js +171 -9
package/xig/xig.js
CHANGED
|
@@ -966,6 +966,129 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
|
|
|
966
966
|
}
|
|
967
967
|
}
|
|
968
968
|
|
|
969
|
+
async function fetchResourceRows(queryParams, offset = 0, limit = 200) {
|
|
970
|
+
const { query: resourceQuery, params: qp } = buildSecureResourceQuery(queryParams, offset, limit);
|
|
971
|
+
return new Promise((resolve, reject) => {
|
|
972
|
+
xigDb.all(resourceQuery, qp, (err, rows) => {
|
|
973
|
+
if (err) reject(err);
|
|
974
|
+
else resolve(rows || []);
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function rowToObject(row, queryParams) {
|
|
980
|
+
const { ver, realm, auth, type, rt } = queryParams;
|
|
981
|
+
const packageObj = getPackage(row.PackageKey);
|
|
982
|
+
|
|
983
|
+
const obj = {
|
|
984
|
+
package: packageObj ? packageObj.Id : `Package ${row.PackageKey}`,
|
|
985
|
+
packageWeb: packageObj?.Web || null,
|
|
986
|
+
resourceType: row.ResourceType,
|
|
987
|
+
id: row.Id,
|
|
988
|
+
url: row.Url || null,
|
|
989
|
+
name: row.Name || null,
|
|
990
|
+
title: row.Title || null,
|
|
991
|
+
status: row.StandardsStatus || row.Status || null,
|
|
992
|
+
fmm: row.FMM || null,
|
|
993
|
+
wg: row.WG || null,
|
|
994
|
+
date: formatDate(row.Date),
|
|
995
|
+
realm: row.Realm || null,
|
|
996
|
+
authority: row.Authority || null,
|
|
997
|
+
versions: showVersion(row),
|
|
998
|
+
usageCount: row.UsageCount || 0
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
// Type-specific fields
|
|
1002
|
+
switch (type) {
|
|
1003
|
+
case 'cs':
|
|
1004
|
+
obj.content = row.Supplements ? `Suppl: ${row.Supplements}` : (row.Content || null);
|
|
1005
|
+
break;
|
|
1006
|
+
case 'rp':
|
|
1007
|
+
case 'dp':
|
|
1008
|
+
obj.type = row.Type || null;
|
|
1009
|
+
break;
|
|
1010
|
+
case 'ext': {
|
|
1011
|
+
const parts = (row.Details || '').split('|');
|
|
1012
|
+
obj.context = parts[0] || null;
|
|
1013
|
+
obj.modifier = parts[1] || null;
|
|
1014
|
+
obj.extensionType = parts[2] || null;
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
case 'vs':
|
|
1018
|
+
case 'cm':
|
|
1019
|
+
obj.sources = (row.Details || '').replace(/,/g, ' ') || null;
|
|
1020
|
+
break;
|
|
1021
|
+
case 'lm': {
|
|
1022
|
+
const packageCanonical = packageObj ? packageObj.Canonical : '';
|
|
1023
|
+
obj.type = (row.Type || '').replace(packageCanonical + 'StructureDefinition/', '');
|
|
1024
|
+
break;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
return obj;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async function buildResourceJson(queryParams) {
|
|
1032
|
+
if (!xigDb) return [];
|
|
1033
|
+
const rows = await fetchResourceRows(queryParams, 0, 1000000);
|
|
1034
|
+
return rows.map(row => rowToObject(row, queryParams));
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async function buildResourceCsv(queryParams) {
|
|
1038
|
+
if (!xigDb) return '';
|
|
1039
|
+
const { type, ver, realm, auth, rt } = queryParams;
|
|
1040
|
+
|
|
1041
|
+
// Build headers
|
|
1042
|
+
const headers = ['Package'];
|
|
1043
|
+
if (!ver || ver === '') headers.push('Versions');
|
|
1044
|
+
headers.push('ResourceType', 'Id', 'Name/Title', 'Status', 'FMM', 'WG', 'Date');
|
|
1045
|
+
if (!realm || realm === '') headers.push('Realm');
|
|
1046
|
+
if (!auth || auth === '') headers.push('Authority');
|
|
1047
|
+
|
|
1048
|
+
switch (type) {
|
|
1049
|
+
case 'cs': headers.push('Content'); break;
|
|
1050
|
+
case 'rp': if (!rt || rt === '') headers.push('Resource'); break;
|
|
1051
|
+
case 'dp': if (!rt || rt === '') headers.push('DataType'); break;
|
|
1052
|
+
case 'ext': headers.push('Context', 'Modifier', 'Type'); break;
|
|
1053
|
+
case 'vs': case 'cm': headers.push('Sources'); break;
|
|
1054
|
+
case 'lm': headers.push('Type'); break;
|
|
1055
|
+
}
|
|
1056
|
+
headers.push('UsageCount');
|
|
1057
|
+
|
|
1058
|
+
const csvEscape = v => {
|
|
1059
|
+
if (v === null || v === undefined) return '';
|
|
1060
|
+
const s = String(v);
|
|
1061
|
+
return s.includes(',') || s.includes('"') || s.includes('\n')
|
|
1062
|
+
? `"${s.replace(/"/g, '""')}"` : s;
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
const rows = await fetchResourceRows(queryParams, 0, 1000000);
|
|
1066
|
+
const lines = [headers.join(',')];
|
|
1067
|
+
|
|
1068
|
+
for (const row of rows) {
|
|
1069
|
+
const obj = rowToObject(row, queryParams);
|
|
1070
|
+
const cells = [obj.package];
|
|
1071
|
+
if (!ver || ver === '') cells.push(obj.versions);
|
|
1072
|
+
cells.push(obj.resourceType, obj.id, obj.title || obj.name || '', obj.status || '',
|
|
1073
|
+
obj.fmm || '', obj.wg || '', obj.date || '');
|
|
1074
|
+
if (!realm || realm === '') cells.push(obj.realm || '');
|
|
1075
|
+
if (!auth || auth === '') cells.push(obj.authority || '');
|
|
1076
|
+
|
|
1077
|
+
switch (type) {
|
|
1078
|
+
case 'cs': cells.push(obj.content || ''); break;
|
|
1079
|
+
case 'rp': case 'dp': if (!rt || rt === '') cells.push(obj.type || ''); break;
|
|
1080
|
+
case 'ext': cells.push(obj.context || '', obj.modifier || '', obj.extensionType || ''); break;
|
|
1081
|
+
case 'vs': case 'cm': cells.push(obj.sources || ''); break;
|
|
1082
|
+
case 'lm': cells.push(obj.type || ''); break;
|
|
1083
|
+
}
|
|
1084
|
+
cells.push(obj.usageCount);
|
|
1085
|
+
|
|
1086
|
+
lines.push(cells.map(csvEscape).join(','));
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return lines.join('\r\n');
|
|
1090
|
+
}
|
|
1091
|
+
|
|
969
1092
|
// Summary Statistics Functions
|
|
970
1093
|
|
|
971
1094
|
async function buildSummaryStats(queryParams, baseUrl) {
|
|
@@ -1214,7 +1337,7 @@ function buildAdditionalForm(queryParams) {
|
|
|
1214
1337
|
if (Object.keys(txSources).length > 0) {
|
|
1215
1338
|
// Convert txSources map to "code=display" format
|
|
1216
1339
|
const sourceOptions = Object.keys(txSources).map(code => `${code}=${txSources[code]}`);
|
|
1217
|
-
html += 'Source: ' + makeSelect(rt, sourceOptions) + ' ';
|
|
1340
|
+
html += 'Source: ' + makeSelect(rt, sourceOptions, 'rt') + ' ';
|
|
1218
1341
|
html += '<br/>';
|
|
1219
1342
|
}
|
|
1220
1343
|
break;
|
|
@@ -1225,7 +1348,7 @@ function buildAdditionalForm(queryParams) {
|
|
|
1225
1348
|
if (Object.keys(txSourcesCM).length > 0) {
|
|
1226
1349
|
// Convert txSources map to "code=display" format
|
|
1227
1350
|
const sourceOptionsCM = Object.keys(txSourcesCM).map(code => `${code}=${txSourcesCM[code]}`);
|
|
1228
|
-
html += 'Source: ' + makeSelect(rt, sourceOptionsCM) + ' ';
|
|
1351
|
+
html += 'Source: ' + makeSelect(rt, sourceOptionsCM, 'source') + ' ';
|
|
1229
1352
|
html += '<br/>';
|
|
1230
1353
|
}
|
|
1231
1354
|
break;
|
|
@@ -1503,6 +1626,9 @@ function hasCachedValue(tableName, value) {
|
|
|
1503
1626
|
if (cache instanceof Set) {
|
|
1504
1627
|
return cache.has(value);
|
|
1505
1628
|
}
|
|
1629
|
+
if (cache instanceof Map) {
|
|
1630
|
+
return cache.has(value);
|
|
1631
|
+
}
|
|
1506
1632
|
return false;
|
|
1507
1633
|
}
|
|
1508
1634
|
|
|
@@ -1600,7 +1726,7 @@ function downloadFile(url, destination, maxRedirects = 5) {
|
|
|
1600
1726
|
|
|
1601
1727
|
if (response.statusCode !== 200) {
|
|
1602
1728
|
if (globalStats) {
|
|
1603
|
-
globalStats.
|
|
1729
|
+
globalStats.taskError('XIG Download', `Download failed: ${response.statusCode}`)
|
|
1604
1730
|
}
|
|
1605
1731
|
reject(Object.assign(
|
|
1606
1732
|
new Error(`Download failed with HTTP ${response.statusCode}`),
|
|
@@ -1613,7 +1739,7 @@ function downloadFile(url, destination, maxRedirects = 5) {
|
|
|
1613
1739
|
const maxSize = 10 * 1024 * 1024 * 1024; // 10GB limit
|
|
1614
1740
|
if (downloadMeta.contentLength && downloadMeta.contentLength > maxSize) {
|
|
1615
1741
|
if (globalStats) {
|
|
1616
|
-
globalStats.
|
|
1742
|
+
globalStats.taskError('XIG Download', `Download failed: too large`)
|
|
1617
1743
|
}
|
|
1618
1744
|
reject(Object.assign(new Error('File too large'), { downloadMeta }));
|
|
1619
1745
|
return;
|
|
@@ -1626,7 +1752,7 @@ function downloadFile(url, destination, maxRedirects = 5) {
|
|
|
1626
1752
|
if (downloadMeta.downloadedBytes > maxSize) {
|
|
1627
1753
|
request.destroy();
|
|
1628
1754
|
if (globalStats) {
|
|
1629
|
-
globalStats.
|
|
1755
|
+
globalStats.taskError('XIG Download', `Download failed: file too large`);
|
|
1630
1756
|
}
|
|
1631
1757
|
fs.unlink(destination, () => {}); // Clean up
|
|
1632
1758
|
reject(Object.assign(new Error('File too large'), { downloadMeta }));
|
|
@@ -1641,14 +1767,14 @@ function downloadFile(url, destination, maxRedirects = 5) {
|
|
|
1641
1767
|
downloadMeta.durationMs = Date.now() - downloadMeta.startTime;
|
|
1642
1768
|
xigLog.info(`Download completed successfully. Downloaded ${downloadMeta.downloadedBytes} bytes to ${destination}`);
|
|
1643
1769
|
if (globalStats) {
|
|
1644
|
-
globalStats.
|
|
1770
|
+
globalStats.taskDone('XIG Download', `Downloaded ${downloadMeta.downloadedBytes} bytes to ${destination}`);
|
|
1645
1771
|
}
|
|
1646
1772
|
resolve(downloadMeta);
|
|
1647
1773
|
});
|
|
1648
1774
|
|
|
1649
1775
|
fileStream.on('error', (err) => {
|
|
1650
1776
|
if (globalStats) {
|
|
1651
|
-
globalStats.
|
|
1777
|
+
globalStats.taskError('XIG Download', `Download failed`);
|
|
1652
1778
|
}
|
|
1653
1779
|
fs.unlink(destination, () => {}); // Delete partial file
|
|
1654
1780
|
reject(Object.assign(err, { downloadMeta }));
|
|
@@ -1657,7 +1783,7 @@ function downloadFile(url, destination, maxRedirects = 5) {
|
|
|
1657
1783
|
|
|
1658
1784
|
request.on('error', (err) => {
|
|
1659
1785
|
if (globalStats) {
|
|
1660
|
-
globalStats.
|
|
1786
|
+
globalStats.taskError('XIG Download', `Download Error`);
|
|
1661
1787
|
}
|
|
1662
1788
|
reject(Object.assign(err, { downloadMeta }));
|
|
1663
1789
|
});
|
|
@@ -1665,7 +1791,7 @@ function downloadFile(url, destination, maxRedirects = 5) {
|
|
|
1665
1791
|
request.setTimeout(300000, () => { // 5 minutes timeout
|
|
1666
1792
|
request.destroy();
|
|
1667
1793
|
if (globalStats) {
|
|
1668
|
-
globalStats.
|
|
1794
|
+
globalStats.taskError('XIG Download', `Download Timeout`);
|
|
1669
1795
|
}
|
|
1670
1796
|
reject(Object.assign(new Error('Download timeout after 5 minutes'), { downloadMeta }));
|
|
1671
1797
|
});
|
|
@@ -2258,6 +2384,16 @@ function getDatabaseInfo() {
|
|
|
2258
2384
|
});
|
|
2259
2385
|
}
|
|
2260
2386
|
|
|
2387
|
+
function getRequestedFormat(req) {
|
|
2388
|
+
const fmt = req.query._fmt;
|
|
2389
|
+
if (fmt === 'json') return 'json';
|
|
2390
|
+
if (fmt === 'csv') return 'csv';
|
|
2391
|
+
const accept = req.headers['accept'] || '';
|
|
2392
|
+
if (accept.includes('application/json')) return 'json';
|
|
2393
|
+
if (accept.includes('text/csv')) return 'csv';
|
|
2394
|
+
return 'html';
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2261
2397
|
// Routes
|
|
2262
2398
|
router.get('/:packagePid/:resourceType/:resourceId', async (req, res) => {
|
|
2263
2399
|
const start = Date.now();
|
|
@@ -2310,6 +2446,22 @@ router.get('/', async (req, res) => {
|
|
|
2310
2446
|
// Parse offset for pagination
|
|
2311
2447
|
const offset = parseInt(queryParams.offset) || 0;
|
|
2312
2448
|
|
|
2449
|
+
// ── Format negotiation ──────────────────────────────────────
|
|
2450
|
+
const fmt = getRequestedFormat(req);
|
|
2451
|
+
|
|
2452
|
+
if (fmt === 'json') {
|
|
2453
|
+
const data = await buildResourceJson(queryParams, offset);
|
|
2454
|
+
res.setHeader('Content-Type', 'application/json');
|
|
2455
|
+
return res.send(JSON.stringify(data, null, 2));
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
if (fmt === 'csv') {
|
|
2459
|
+
const csv = await buildResourceCsv(queryParams, offset);
|
|
2460
|
+
res.setHeader('Content-Type', 'text/csv');
|
|
2461
|
+
res.setHeader('Content-Disposition', 'attachment; filename="xig-resources.csv"');
|
|
2462
|
+
return res.send(csv);
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2313
2465
|
// Build control panel
|
|
2314
2466
|
const controlPanel = buildControlPanel('/xig', queryParams);
|
|
2315
2467
|
|
|
@@ -2345,6 +2497,16 @@ router.get('/', async (req, res) => {
|
|
|
2345
2497
|
} else {
|
|
2346
2498
|
countParagraph += `${resourceCount.toLocaleString()} resources`;
|
|
2347
2499
|
}
|
|
2500
|
+
const downloadParams = { ...queryParams };
|
|
2501
|
+
delete downloadParams.offset;
|
|
2502
|
+
const downloadQs = Object.keys(downloadParams)
|
|
2503
|
+
.filter(key => downloadParams[key] && downloadParams[key] !== '')
|
|
2504
|
+
.map(key => `${key}=${encodeURIComponent(downloadParams[key])}`)
|
|
2505
|
+
.join('&');
|
|
2506
|
+
const downloadBase = '/xig' + (downloadQs ? '?' + downloadQs + '&' : '?');
|
|
2507
|
+
countParagraph += ` (<a href="${downloadBase}_fmt=json">JSON</a>`;
|
|
2508
|
+
countParagraph += ` | <a href="${downloadBase}_fmt=csv">CSV</a>)`;
|
|
2509
|
+
|
|
2348
2510
|
countParagraph += '</p>';
|
|
2349
2511
|
|
|
2350
2512
|
// Build additional form
|