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.
- package/CHANGELOG.md +65 -0
- package/README.md +40 -19
- package/config-template.json +25 -4
- package/extension-tracker/extension-tracker.js +111 -3
- package/extension-tracker/readme.md +3 -0
- package/library/html-server.js +16 -16
- package/library/html.js +9 -0
- package/library/narrative-check.js +203 -0
- package/library/npm-audit.js +42 -3
- package/package.json +2 -1
- package/packages/openapi.yaml +6 -3
- package/packages/package-crawler.js +189 -82
- package/packages/packages.js +33 -69
- package/packages/readme.md +1 -1
- package/packages/utilities/dedupe-versions.js +22 -8
- package/registry/crawler.js +8 -1
- package/registry/registry.js +18 -11
- package/release.sh +80 -0
- package/shl/shl.js +5 -3
- package/testing/readme.md +1 -1
- package/tx/cm/cm-database.js +62 -0
- package/tx/cm/cm-package.js +20 -3
- package/tx/cs/cs-cs.js +86 -18
- package/tx/cs/cs-loinc.js +32 -14
- package/tx/library/resource-ids.js +16 -1
- package/tx/library.js +9 -2
- package/tx/ocl/README.md +62 -6
- package/tx/ocl/cm-ocl.cjs +166 -44
- package/tx/ocl/cs-ocl.cjs +163 -13
- package/tx/ocl/resolve/public-access.js +143 -0
- package/tx/ocl/resolve/reference-resolver.js +458 -0
- package/tx/ocl/vs-ocl.cjs +167 -4
- package/tx/problems.js +7 -5
- package/tx/provider.js +5 -1
- package/tx/tx-html.js +46 -7
- package/tx/tx.js +22 -6
- package/tx/vs/vs-database.js +26 -0
- package/tx/vs/vs-package.js +42 -10
- package/tx/workers/expand.js +1 -1
- package/tx/workers/metadata.js +6 -1
- package/tx/workers/read.js +87 -5
- package/utilities/submit-test-report.js +1 -1
- 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
|
-
|
|
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:
|
|
2434
|
-
auth:
|
|
2435
|
-
realm:
|
|
2436
|
-
type:
|
|
2437
|
-
rt:
|
|
2438
|
-
text:
|
|
2439
|
-
pkg:
|
|
2440
|
-
onlyUsed:
|
|
2441
|
-
offset:
|
|
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
|
-
|
|
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,
|