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/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.task('XIG Download', `Download failed: ${response.statusCode}`)
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.task('XIG Download', `Download failed: too large`)
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.task('XIG Download', `Download failed: file too large`);
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.task('XIG Download', `Downloaded ${downloadMeta.downloadedBytes} bytes to ${destination}`);
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.task('XIG Download', `Download failed`);
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.task('XIG Download', `Download Error`);
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.task('XIG Download', `Download Timeout`);
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