fhirsmith 0.16.0 → 0.17.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +40 -19
  3. package/config-template.json +25 -4
  4. package/extension-tracker/extension-tracker.js +111 -3
  5. package/extension-tracker/readme.md +3 -0
  6. package/library/html-server.js +16 -16
  7. package/library/html.js +9 -0
  8. package/library/narrative-check.js +203 -0
  9. package/library/npm-audit.js +42 -3
  10. package/package.json +2 -1
  11. package/packages/openapi.yaml +6 -3
  12. package/packages/package-crawler.js +189 -82
  13. package/packages/packages.js +33 -69
  14. package/packages/readme.md +1 -1
  15. package/packages/utilities/dedupe-versions.js +22 -8
  16. package/registry/crawler.js +8 -1
  17. package/registry/registry.js +18 -11
  18. package/release.sh +80 -0
  19. package/shl/shl.js +5 -3
  20. package/testing/readme.md +1 -1
  21. package/tx/cm/cm-database.js +62 -0
  22. package/tx/cm/cm-package.js +20 -3
  23. package/tx/cs/cs-cs.js +86 -18
  24. package/tx/cs/cs-loinc.js +32 -14
  25. package/tx/library/resource-ids.js +16 -1
  26. package/tx/library.js +9 -2
  27. package/tx/ocl/README.md +62 -6
  28. package/tx/ocl/cm-ocl.cjs +166 -44
  29. package/tx/ocl/cs-ocl.cjs +163 -13
  30. package/tx/ocl/resolve/public-access.js +143 -0
  31. package/tx/ocl/resolve/reference-resolver.js +458 -0
  32. package/tx/ocl/vs-ocl.cjs +167 -4
  33. package/tx/problems.js +7 -5
  34. package/tx/provider.js +5 -1
  35. package/tx/tx-html.js +46 -7
  36. package/tx/tx.js +22 -6
  37. package/tx/vs/vs-database.js +26 -0
  38. package/tx/vs/vs-package.js +42 -10
  39. package/tx/workers/expand.js +1 -1
  40. package/tx/workers/metadata.js +6 -1
  41. package/tx/workers/read.js +87 -5
  42. package/utilities/submit-test-report.js +1 -1
  43. package/xig/xig.js +88 -12
package/xig/xig.js CHANGED
@@ -19,12 +19,14 @@ const escape = require('escape-html');
19
19
 
20
20
  const Logger = require('../library/logger');
21
21
  const {describeCron} = require("../library/cron-utilities");
22
+ const {tokenMatches, tokenConfigured} = require('../library/request-token');
22
23
  const xigLog = Logger.getInstance().child({ module: 'xig' });
23
24
 
24
25
  const router = express.Router();
25
26
 
26
27
  // Configuration
27
- const XIG_DB_URL = 'http://fhir.org/guides/stats/xig.db';
28
+ // https: the database is trusted content (its HTML is shown as is), so it must come from fhir.org
29
+ const XIG_DB_URL = 'https://fhir.org/guides/stats/xig.db';
28
30
  const XIG_DB_PATH = folders.filePath('xig', 'xig.db');
29
31
  const TEMPLATE_PATH = path.join(__dirname, 'xig-template.html');
30
32
 
@@ -1718,6 +1720,11 @@ function downloadFile(url, destination, maxRedirects = 5) {
1718
1720
  }
1719
1721
  }
1720
1722
 
1723
+ // never be redirected off https
1724
+ if (new URL(redirectUrl).protocol !== 'https:') {
1725
+ reject(Object.assign(new Error(`Refused a redirect to ${redirectUrl}: the database is only downloaded over https`), { downloadMeta }));
1726
+ return;
1727
+ }
1721
1728
  attemptDownload(redirectUrl, redirectCount + 1);
1722
1729
  return;
1723
1730
  }
@@ -2203,6 +2210,23 @@ function getDatabaseAgeInfo() {
2203
2210
  };
2204
2211
  }
2205
2212
 
2213
+ // The form on /xig/stats that starts an update (POST /xig/update), when an updateToken is configured
2214
+ function buildUpdateForm() {
2215
+ if (!tokenConfigured(xigSettings.updateToken)) {
2216
+ return '<p class="text-muted">An update can be started from here when an updateToken is set in the xig configuration.</p>';
2217
+ }
2218
+ let html = '<h4>Update the database now</h4>';
2219
+ html += '<form method="POST" action="/xig/update" class="mb-4">';
2220
+ html += '<p>Downloads the current database from fhir.org (about 3 GB) and switches to it. ';
2221
+ html += updateInProgress ? '<strong>An update is running now.</strong>' : 'It takes several minutes.';
2222
+ html += '</p>';
2223
+ html += '<label for="xig-update-token">Update token</label> ';
2224
+ html += '<input type="password" id="xig-update-token" name="token" required autocomplete="off" maxlength="200"> ';
2225
+ html += '<button type="submit" class="btn btn-primary">Start the update</button>';
2226
+ html += '</form>';
2227
+ return html;
2228
+ }
2229
+
2206
2230
  function buildStatsTable(statsData) {
2207
2231
  let html = '<table class="table table-striped table-bordered">';
2208
2232
  html += '<thead class="table-dark">';
@@ -2429,16 +2453,25 @@ router.get('/', async (req, res) => {
2429
2453
  const title = 'FHIR Resources';
2430
2454
 
2431
2455
  // Parse query parameters
2456
+ // Each a string: a repeated parameter (?text=a&text=b) arrives as an array, which
2457
+ // the filters and SQL builders would fail on. Capped in length
2458
+ const qp = (name, dflt = '') => {
2459
+ let v = req.query[name];
2460
+ if (Array.isArray(v)) {
2461
+ v = v[0];
2462
+ }
2463
+ return typeof v === 'string' && v !== '' ? v.substring(0, 200) : dflt;
2464
+ };
2432
2465
  const queryParams = {
2433
- ver: req.query.ver || '',
2434
- auth: req.query.auth || '',
2435
- realm: req.query.realm || '',
2436
- type: req.query.type || '',
2437
- rt: req.query.rt || '',
2438
- text: req.query.text || '',
2439
- pkg: req.query.pkg || '',
2440
- onlyUsed: req.query.onlyUsed || '',
2441
- offset: req.query.offset || '0'
2466
+ ver: qp('ver'),
2467
+ auth: qp('auth'),
2468
+ realm: qp('realm'),
2469
+ type: qp('type'),
2470
+ rt: qp('rt'),
2471
+ text: qp('text'),
2472
+ pkg: qp('pkg'),
2473
+ onlyUsed: qp('onlyUsed'),
2474
+ offset: qp('offset', '0')
2442
2475
  };
2443
2476
 
2444
2477
  // Parse offset for pagination
@@ -2600,7 +2633,7 @@ router.get('/stats', async (req, res) => {
2600
2633
  introContent += `</div>`;
2601
2634
  }
2602
2635
 
2603
- const fullContent = introContent + content;
2636
+ const fullContent = introContent + content + buildUpdateForm();
2604
2637
 
2605
2638
  const stats = await gatherPageStatistics();
2606
2639
  stats.processingTime = Date.now() - startTime;
@@ -3125,8 +3158,44 @@ router.get('/cache', async (req, res) => {
3125
3158
  }
3126
3159
  });
3127
3160
 
3128
- router.post('/update', async (req, res) => {
3161
+ // POST /xig/update re-downloads the database (about 3 GB), so it needs the updateToken from
3162
+ // the xig config: in the x-update-token header, or (from the form on /xig/stats) in a
3163
+ // "token" form field. With no updateToken it is disabled.
3164
+ //
3165
+ // The form gets a page back straight away, while the update runs on (it takes minutes);
3166
+ // /xig/stats shows how it went. Other callers wait for the update and get JSON.
3167
+ router.post('/update', express.urlencoded({ extended: false, limit: '2kb' }), async (req, res) => {
3168
+ const fromForm = !!req.is('application/x-www-form-urlencoded');
3169
+ const page = (status, alert, message) => {
3170
+ const content = `<div class="alert alert-${alert}">${escape(message)}</div><p><a href="/xig/stats">Back to the status page</a></p>`;
3171
+ res.status(status).setHeader('Content-Type', 'text/html');
3172
+ res.send(renderPage('Update the XIG database', content));
3173
+ };
3174
+ const refuse = (message) => fromForm ? page(403, 'danger', message) : res.status(403).json({ status: 'ERROR', error: 'forbidden: ' + message });
3175
+
3129
3176
  try {
3177
+ if (!tokenConfigured(xigSettings.updateToken)) {
3178
+ refuse('no updateToken is configured, so this endpoint is disabled');
3179
+ return;
3180
+ }
3181
+ const supplied = req.headers['x-update-token'] ?? req.body?.token;
3182
+ if (!tokenMatches(xigSettings.updateToken, supplied)) {
3183
+ xigLog.warn('Rejected /xig/update: missing or invalid token (from ' + req.ip + ')');
3184
+ refuse('missing or invalid x-update-token');
3185
+ return;
3186
+ }
3187
+
3188
+ if (fromForm) {
3189
+ if (updateInProgress) {
3190
+ page(200, 'info', 'An update is already running. The status page shows how it is going.');
3191
+ return;
3192
+ }
3193
+ xigLog.info('Manual update triggered from the status page');
3194
+ updateXigDatabase().catch((err) => xigLog.error('XIG database update failed: ' + err.message));
3195
+ page(202, 'success', 'The update has started. It takes several minutes; the status page shows how it is going.');
3196
+ return;
3197
+ }
3198
+
3130
3199
  xigLog.info('Manual update triggered via API');
3131
3200
  await updateXigDatabase();
3132
3201
  res.json({
@@ -3143,10 +3212,16 @@ router.post('/update', async (req, res) => {
3143
3212
  });
3144
3213
 
3145
3214
  let globalStats;
3215
+ let xigSettings = {};
3216
+ // The xig section of the config (initializeXigModule; tests set it directly)
3217
+ function setXigSettings(xigConfig) {
3218
+ xigSettings = xigConfig || {};
3219
+ }
3146
3220
  // Initialize the XIG module
3147
3221
  async function initializeXigModule(stats, xigConfig) {
3148
3222
  try {
3149
3223
  globalStats = stats;
3224
+ setXigSettings(xigConfig);
3150
3225
  loadTemplate();
3151
3226
 
3152
3227
  await initializeDatabase();
@@ -3196,6 +3271,7 @@ function shutdown() {
3196
3271
  // Export everything
3197
3272
  module.exports = {
3198
3273
  router,
3274
+ setXigSettings,
3199
3275
  updateXigDatabase,
3200
3276
  getDatabaseInfo,
3201
3277
  shutdown,