fhirsmith 0.16.1 → 0.18.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 +130 -0
- package/README.md +105 -30
- package/config-template.json +130 -4
- package/extension-tracker/extension-tracker.js +111 -3
- package/extension-tracker/readme.md +3 -0
- package/ips/ai-client.js +263 -0
- package/ips/composer.js +805 -0
- package/ips/editor-schema.js +289 -0
- package/ips/editor.js +290 -0
- package/ips/fhirpath-ips.js +397 -0
- package/ips/ips-codeable-concepts.md +1742 -0
- package/ips/ips-template.html +125 -0
- package/ips/ips-valuesets.js +159 -0
- package/ips/ips.js +1736 -0
- package/ips/membership.js +179 -0
- package/ips/pipeline.js +1012 -0
- package/ips/presenter.js +974 -0
- package/ips/profile-editor.js +578 -0
- package/ips/profiles.js +506 -0
- package/ips/readme.md +460 -0
- package/ips/render.js +1482 -0
- package/ips/search-params.js +83 -0
- package/ips/search.js +667 -0
- package/ips/share.js +178 -0
- package/ips/store.js +650 -0
- package/ips/terminology.js +395 -0
- package/ips/tx-client.js +202 -0
- package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
- package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
- package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
- package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
- package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
- package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
- package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
- package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
- package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
- package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
- package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
- package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
- package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-medication.json +50 -0
- package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
- package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
- package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
- package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
- package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
- package/ips/valuesets/ValueSet-ips-route.json +46 -0
- package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
- package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
- package/ips/valuesets/elements.json +44 -0
- package/library/daily-report.js +422 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/html.js +9 -0
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +222 -0
- package/library/npm-audit.js +25 -0
- package/library/package-manager.js +4 -2
- package/library/sqlite-session-store.js +29 -1
- package/package.json +8 -2
- package/packages/package-crawler.js +189 -82
- package/packages/packages.js +36 -63
- package/packages/utilities/dedupe-versions.js +22 -8
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +48 -1
- package/registry/documentation/api.md +162 -0
- package/registry/documentation/configuration.md +93 -0
- package/registry/documentation/documents.md +74 -0
- package/registry/documentation/internals.md +151 -0
- package/registry/documentation/readme.md +100 -0
- package/registry/documentation/security.md +199 -0
- package/registry/documents.js +309 -0
- package/registry/openapi.yaml +30 -0
- package/registry/registry-template.html +1 -1
- package/registry/registry.js +247 -54
- package/registry/uptime.js +154 -0
- package/release.sh +80 -0
- package/server.js +105 -44
- package/shl/shl.js +64 -9
- package/stats-db.js +93 -0
- package/stats.js +158 -1
- package/testing/readme.md +1 -1
- package/testing/search.js +2 -108
- package/testing/store.js +5 -0
- package/testing/testing.js +20 -0
- package/token/token-template.html +5 -4
- package/token/token.js +17 -10
- package/translations/Messages.properties +9 -0
- package/tx/README.md +52 -0
- package/tx/cm/cm-database.js +62 -0
- package/tx/cm/cm-package.js +20 -3
- package/tx/cs/cs-api.js +90 -14
- package/tx/cs/cs-cs.js +142 -19
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +418 -0
- package/tx/cs/cs-loinc.js +79 -39
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +918 -55
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- package/tx/library.js +107 -6
- 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/openapi-operations.js +4 -0
- package/tx/params.js +8 -1
- package/tx/problems.js +7 -5
- package/tx/provider.js +21 -5
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +37 -8
- package/tx/tx.fhir.org.yml +2 -3
- package/tx/tx.js +148 -5
- package/tx/vs/vs-database.js +26 -0
- package/tx/vs/vs-package.js +42 -10
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/expand.js +34 -7
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/metadata.js +6 -1
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +128 -6
- package/utilities/make-sct-test-extension.js +264 -0
- package/utilities/submit-test-report.js +1 -1
- package/xig/xig.js +120 -12
- package/gr.sh +0 -3
- package/registry/readme.md +0 -124
package/tx/tx.js
CHANGED
|
@@ -59,6 +59,8 @@ const ConceptUsageTracker = require("./usage-tracker");
|
|
|
59
59
|
const ProblemFinder = require("./problems");
|
|
60
60
|
const { buildOperationOutcome, outcomeFromError } = require('./library/operation-outcome');
|
|
61
61
|
const txOpenApi = require('./openapi');
|
|
62
|
+
const { SyndicationStore } = require('./syndication/syndication-store');
|
|
63
|
+
const syndicationFeed = require('./syndication/syndication-feed');
|
|
62
64
|
// const {writeFileSync} = require("fs");
|
|
63
65
|
|
|
64
66
|
class TXModule {
|
|
@@ -140,9 +142,14 @@ class TXModule {
|
|
|
140
142
|
* Initialize the TX module
|
|
141
143
|
* @param {Object} config - Module configuration
|
|
142
144
|
* @param {express.Application} app - Express application for registering endpoints
|
|
145
|
+
* @param {Object} [server] - server level settings: {hostName} (config.hostName)
|
|
143
146
|
*/
|
|
144
|
-
async initialize(config, app) {
|
|
147
|
+
async initialize(config, app, server = {}) {
|
|
145
148
|
this.config = config;
|
|
149
|
+
// the server's public host name, when it is configured as one (it is also used as a
|
|
150
|
+
// display name, so anything that isn't a host name is ignored)
|
|
151
|
+
this.publicHost = typeof server.hostName === 'string' && /^[A-Za-z0-9.-]+(:[0-9]{1,5})?$/.test(server.hostName)
|
|
152
|
+
? server.hostName : null;
|
|
146
153
|
// Initialize logger with config settings
|
|
147
154
|
this.log = Logger.getInstance().child({
|
|
148
155
|
module: 'tx',
|
|
@@ -166,6 +173,7 @@ class TXModule {
|
|
|
166
173
|
// Off unless the operator opts in. This gates the nav item as well as the route, so a
|
|
167
174
|
// server that does not publish its library source shows no link to it.
|
|
168
175
|
txHtml.setPublishLibrarySource(config.publishLibrarySource === true);
|
|
176
|
+
txHtml.setSyndicationFeed(!!(config.syndication && config.syndication.enabled));
|
|
169
177
|
|
|
170
178
|
// Validate config
|
|
171
179
|
if (!config.librarySource) {
|
|
@@ -211,7 +219,7 @@ class TXModule {
|
|
|
211
219
|
}
|
|
212
220
|
};
|
|
213
221
|
void prune(); // errors are logged inside
|
|
214
|
-
this.timers.push(setInterval(prune, 24 * 60 * 60 * 1000));
|
|
222
|
+
this.timers.push(setInterval(() => void prune(), 24 * 60 * 60 * 1000));
|
|
215
223
|
}
|
|
216
224
|
}
|
|
217
225
|
|
|
@@ -236,6 +244,17 @@ class TXModule {
|
|
|
236
244
|
// Load the library from YAML
|
|
237
245
|
this.log.info(`Loading library from: ${config.librarySource}`);
|
|
238
246
|
this.library = new Library(config.librarySource, config.vsacCfg, this.log, this.stats);
|
|
247
|
+
|
|
248
|
+
// The syndication feed: content is recorded in the store as the library loads it, from
|
|
249
|
+
// the time the administrator turns it on (see tx/syndication). One store serves every
|
|
250
|
+
// endpoint, since the library is shared. If it is turned on and the store can't be
|
|
251
|
+
// opened, that stops startup.
|
|
252
|
+
this.syndicationStore = null;
|
|
253
|
+
if (config.syndication && config.syndication.enabled) {
|
|
254
|
+
this.syndicationStore = new SyndicationStore(path.join(this.library.cacheFolder, 'syndication'), this.log).open();
|
|
255
|
+
this.library.syndication = this.syndicationStore;
|
|
256
|
+
this.log.info(`Syndication feed enabled: store in ${this.syndicationStore.folder}`);
|
|
257
|
+
}
|
|
239
258
|
this.log.info(`Load...`);
|
|
240
259
|
await this.library.load();
|
|
241
260
|
this.log.info('Library loaded successfully');
|
|
@@ -305,13 +324,14 @@ class TXModule {
|
|
|
305
324
|
if (this.stats) {
|
|
306
325
|
this.stats.addTask("Client Cache", "5 min");
|
|
307
326
|
}
|
|
308
|
-
|
|
327
|
+
const updateCodeSystemList = async () => {
|
|
309
328
|
try {
|
|
310
329
|
await endpointInfo.provider.updateCodeSystemList();
|
|
311
330
|
} catch (error) {
|
|
312
331
|
this.log.error(`Error updating CodeSystem list for ${endpointPath}: ${error.message}`);
|
|
313
332
|
}
|
|
314
|
-
}
|
|
333
|
+
};
|
|
334
|
+
this.timers.push(setInterval(() => void updateCodeSystemList(), 60 * 1000)); // errors are logged inside
|
|
315
335
|
this.log.info(`CodeSystem list update scheduled for ${endpointPath}`);
|
|
316
336
|
this.timers.push(setInterval(() => {
|
|
317
337
|
endpointInfo.resourceCache.prune(cacheTimeoutMs);
|
|
@@ -1132,6 +1152,10 @@ class TXModule {
|
|
|
1132
1152
|
});
|
|
1133
1153
|
}
|
|
1134
1154
|
|
|
1155
|
+
if (this.syndicationStore) {
|
|
1156
|
+
this.setupSyndicationRoutes(router, endpointPath);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1135
1159
|
// Metadata / CapabilityStatement
|
|
1136
1160
|
router.get('/metadata', async (req, res) => {
|
|
1137
1161
|
const start = Date.now();
|
|
@@ -1331,6 +1355,31 @@ class TXModule {
|
|
|
1331
1355
|
};
|
|
1332
1356
|
}
|
|
1333
1357
|
|
|
1358
|
+
/**
|
|
1359
|
+
* The paragraph for the server's daily report: how many of each operation
|
|
1360
|
+
* were performed, over all the endpoints.
|
|
1361
|
+
*/
|
|
1362
|
+
dailyReport({since, now}) {
|
|
1363
|
+
const {plural} = require('../library/daily-report');
|
|
1364
|
+
if (!this.stats) {
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
const counts = this.stats.countsSince(since, now);
|
|
1368
|
+
if (counts.total === 0) {
|
|
1369
|
+
return 'No requests were served.';
|
|
1370
|
+
}
|
|
1371
|
+
// the FHIR operations first, busiest first, then the rest (pages, metadata, reads)
|
|
1372
|
+
const names = Object.keys(counts.operations).sort((a, b) => {
|
|
1373
|
+
const opA = a.startsWith('$') ? 0 : 1;
|
|
1374
|
+
const opB = b.startsWith('$') ? 0 : 1;
|
|
1375
|
+
return opA - opB || counts.operations[b] - counts.operations[a] || a.localeCompare(b);
|
|
1376
|
+
});
|
|
1377
|
+
const list = names.map(n => `${n} ${counts.operations[n].toLocaleString('en-US')}`).join(', ');
|
|
1378
|
+
const endpoints = Object.keys(counts.endpoints).filter(e => e).length;
|
|
1379
|
+
const where = endpoints > 1 ? ` over ${plural(endpoints, 'endpoint')}` : '';
|
|
1380
|
+
return `${counts.total.toLocaleString('en-US')} requests were served${where}: ${list}.`;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1334
1383
|
/**
|
|
1335
1384
|
* Shutdown the module
|
|
1336
1385
|
*/
|
|
@@ -1340,6 +1389,10 @@ class TXModule {
|
|
|
1340
1389
|
clearInterval(timer);
|
|
1341
1390
|
}
|
|
1342
1391
|
this.timers = [];
|
|
1392
|
+
if (this.syndicationStore) {
|
|
1393
|
+
// let an induction in progress finish, so the store isn't left with a half-copied file
|
|
1394
|
+
await this.syndicationStore.whenIdle();
|
|
1395
|
+
}
|
|
1343
1396
|
// Clean up any resources if needed
|
|
1344
1397
|
await this.library.close();
|
|
1345
1398
|
if (this.closureStore) {
|
|
@@ -1349,6 +1402,93 @@ class TXModule {
|
|
|
1349
1402
|
this.log.info('TX module shut down');
|
|
1350
1403
|
}
|
|
1351
1404
|
|
|
1405
|
+
/**
|
|
1406
|
+
* The syndication feed for an endpoint (Terminology Syndication Feed IG, RFC 5005
|
|
1407
|
+
* archived feeds) - see tx/syndication/syndication-feed.js. Atom unless HTML is asked for.
|
|
1408
|
+
*/
|
|
1409
|
+
/**
|
|
1410
|
+
* The public url of an endpoint, for the links in its feed: from the configured host name
|
|
1411
|
+
* (config.hostName), so the links don't depend on what a client puts in the Host header.
|
|
1412
|
+
* Only without one does it fall back to the request (as for a local test server).
|
|
1413
|
+
*/
|
|
1414
|
+
syndicationBase(req, endpointPath) {
|
|
1415
|
+
if (this.publicHost) {
|
|
1416
|
+
return `https://${this.publicHost}${endpointPath}`;
|
|
1417
|
+
}
|
|
1418
|
+
return `${req.protocol}://${req.get('host')}${endpointPath}`;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
setupSyndicationRoutes(router, endpointPath) {
|
|
1422
|
+
const sendFeed = async (req, res, page) => {
|
|
1423
|
+
const start = Date.now();
|
|
1424
|
+
try {
|
|
1425
|
+
const params = {
|
|
1426
|
+
store: this.syndicationStore,
|
|
1427
|
+
endpointPath,
|
|
1428
|
+
fhirVersion: req.txEndpoint.fhirVersion,
|
|
1429
|
+
base: this.syndicationBase(req, endpointPath),
|
|
1430
|
+
page,
|
|
1431
|
+
title: this.config.title,
|
|
1432
|
+
serverVersion: packageJson.version
|
|
1433
|
+
};
|
|
1434
|
+
if (txHtml.acceptsHtml(req)) {
|
|
1435
|
+
const content = syndicationFeed.renderHtml(params);
|
|
1436
|
+
if (content == null) {
|
|
1437
|
+
res.status(404).json(this.operationOutcome('error', 'not-found', 'There is no such syndication archive'));
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
let txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid, this.languages, this.i18n, endpointPath);
|
|
1441
|
+
const title = page == null ? 'Syndication Feed' : `Syndication Feed (archive ${page})`;
|
|
1442
|
+
const html = await txhtml.renderPage(title, content, req.txEndpoint, req.txStartTime);
|
|
1443
|
+
res.setHeader('Content-Type', 'text/html');
|
|
1444
|
+
res.send(html);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
const xml = syndicationFeed.renderAtom(params);
|
|
1448
|
+
if (xml == null) {
|
|
1449
|
+
res.status(404).json(this.operationOutcome('error', 'not-found', 'There is no such syndication archive'));
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
// an archive never changes; the current document changes whenever content is added.
|
|
1453
|
+
// An archive is only cacheable forever when its links come from the configured host
|
|
1454
|
+
// name - links made from the request's Host header must not be cached for others
|
|
1455
|
+
res.setHeader('Cache-Control', page == null || !this.publicHost ? 'no-cache' : 'public, max-age=31536000, immutable');
|
|
1456
|
+
res.setHeader('Content-Type', 'application/atom+xml; charset=utf-8');
|
|
1457
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
1458
|
+
res.send(xml);
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
this.log.error(`Error rendering syndication feed: ${error.message}`);
|
|
1461
|
+
res.status(500).json(this.operationOutcome('error', 'exception', 'Unable to render the syndication feed'));
|
|
1462
|
+
} finally {
|
|
1463
|
+
this.countRequest(endpointPath, 'syndication', Date.now() - start);
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
router.get('/syndication', (req, res) => sendFeed(req, res, null));
|
|
1468
|
+
|
|
1469
|
+
router.get('/syndication/archive/:page', (req, res) => {
|
|
1470
|
+
const page = /^[1-9][0-9]{0,8}$/.test(req.params.page) ? Number(req.params.page) : -1;
|
|
1471
|
+
return sendFeed(req, res, page);
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
// not rate limited here: as for the rest of the tx module, nginx does that (see nginx.md)
|
|
1475
|
+
router.get('/syndication/files/:name', (req, res) => {
|
|
1476
|
+
const file = this.syndicationStore.filePath(req.params.name);
|
|
1477
|
+
if (!file) {
|
|
1478
|
+
res.status(404).json(this.operationOutcome('error', 'not-found', 'There is no such syndication file'));
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
// copies in the store never change
|
|
1482
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
1483
|
+
res.setHeader('Content-Type', 'application/fhir+json');
|
|
1484
|
+
// the file is whatever the source folder held: never let a browser sniff it as a page
|
|
1485
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
1486
|
+
// served from the store's folder by the store's own name for it (sendFile refuses
|
|
1487
|
+
// anything that would leave root)
|
|
1488
|
+
res.sendFile(path.basename(file), { root: this.syndicationStore.filesFolder });
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1352
1492
|
trimParameters(params) {
|
|
1353
1493
|
if (!params || !params.parameter) {
|
|
1354
1494
|
return params;
|
|
@@ -1514,8 +1654,11 @@ class TXModule {
|
|
|
1514
1654
|
if (hdr) {
|
|
1515
1655
|
return parseInt(hdr);
|
|
1516
1656
|
}
|
|
1657
|
+
// the tx-ecosystem test runner (Tools/Java) always gets the test limit, so every server
|
|
1658
|
+
// gives it the same answer. Otherwise the configured externalLimit - it used to be read
|
|
1659
|
+
// only when internalLimit was configured too, so setting it alone did nothing
|
|
1517
1660
|
let isTest = req.header("User-Agent") == 'Tools/Java';
|
|
1518
|
-
if (this.config.
|
|
1661
|
+
if (this.config.externalLimit && !isTest) return this.config.externalLimit; else return EXTERNAL_TEST_DEFAULT_LIMIT;
|
|
1519
1662
|
}
|
|
1520
1663
|
|
|
1521
1664
|
}
|
package/tx/vs/vs-database.js
CHANGED
|
@@ -4,6 +4,9 @@ const sqlite3 = require('sqlite3').verbose();
|
|
|
4
4
|
const { VersionUtilities } = require('../../library/version-utilities');
|
|
5
5
|
const ValueSet = require("../library/valueset");
|
|
6
6
|
|
|
7
|
+
// Settings key written once a package cache has been fully populated (see isComplete)
|
|
8
|
+
const CACHE_COMPLETE_KEY = 'cache-complete';
|
|
9
|
+
|
|
7
10
|
// Columns that can be returned directly without parsing JSON
|
|
8
11
|
const INDEXED_COLUMNS = ['id', 'url', 'version', 'date', 'description', 'name', 'publisher', 'status', 'title'];
|
|
9
12
|
|
|
@@ -485,6 +488,29 @@ class ValueSetDatabase {
|
|
|
485
488
|
});
|
|
486
489
|
}
|
|
487
490
|
|
|
491
|
+
/**
|
|
492
|
+
* True if this cache was fully populated: markComplete() was reached. A cache
|
|
493
|
+
* whose population was cut short (the process killed part way through) has no
|
|
494
|
+
* marker - and so does a file too damaged to open or read - so it reports false.
|
|
495
|
+
* @returns {Promise<boolean>}
|
|
496
|
+
*/
|
|
497
|
+
async isComplete() {
|
|
498
|
+
try {
|
|
499
|
+
return (await this.getSetting(CACHE_COMPLETE_KEY)) != null;
|
|
500
|
+
} catch (e) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Record that this cache has been fully populated. Called once, after the last
|
|
507
|
+
* value set has been written
|
|
508
|
+
* @returns {Promise<void>}
|
|
509
|
+
*/
|
|
510
|
+
async markComplete() {
|
|
511
|
+
await this.setSetting(CACHE_COMPLETE_KEY, new Date().toISOString());
|
|
512
|
+
}
|
|
513
|
+
|
|
488
514
|
/**
|
|
489
515
|
* Insert or update a single ValueSet in the database
|
|
490
516
|
* @param {Object} valueSet - The ValueSet resource
|
package/tx/vs/vs-package.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const { assertNoActiveContent } = require('../../library/narrative-check');
|
|
2
3
|
const fs = require('fs').promises;
|
|
3
4
|
const { AbstractValueSetProvider } = require('./vs-api');
|
|
4
5
|
const { PackageContentLoader } = require('../../library/package-manager');
|
|
@@ -21,7 +22,9 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
|
|
|
21
22
|
super();
|
|
22
23
|
validateParameter(packageLoader, "packageLoader", PackageContentLoader);
|
|
23
24
|
this.packageLoader = packageLoader;
|
|
24
|
-
|
|
25
|
+
// -2: caches from before value sets were checked for active content in their narrative
|
|
26
|
+
// are rebuilt (see initialize)
|
|
27
|
+
this.dbPath = path.join(packageLoader.packageFolder, '.valuesets-2.db');
|
|
25
28
|
this.database = new ValueSetDatabase(this.dbPath);
|
|
26
29
|
this.valueSetMap = new Map();
|
|
27
30
|
this.initialized = false;
|
|
@@ -45,12 +48,21 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
|
|
|
45
48
|
return;
|
|
46
49
|
}
|
|
47
50
|
await this.packageLoader.initialize();
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
await fs.rm(path.join(this.packageLoader.packageFolder, '.valuesets.db'), { force: true }); // the old cache
|
|
52
|
+
|
|
53
|
+
// A cache is only used once its population finished. One that was cut short
|
|
54
|
+
// (the process killed part way through) would otherwise be trusted forever,
|
|
55
|
+
// silently missing whatever hadn't been written yet
|
|
56
|
+
let dbExists = await this.database.exists();
|
|
57
|
+
if (dbExists && !(await this.database.isComplete())) {
|
|
58
|
+
require('../../library/logger').getInstance().child({ module: 'tx' })
|
|
59
|
+
.warn(`Package ${this.packageLoader.pid ? this.packageLoader.pid() : ''}: value set cache ${this.dbPath} is incomplete - rebuilding it`);
|
|
60
|
+
await this.#discardDatabase();
|
|
61
|
+
dbExists = false;
|
|
62
|
+
}
|
|
50
63
|
|
|
51
64
|
if (!dbExists) {
|
|
52
|
-
await this
|
|
53
|
-
await this._populateDatabase();
|
|
65
|
+
await this.#buildDatabase();
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
this.valueSetMap = await this.database.loadAllValueSets(this.sourcePackage());
|
|
@@ -60,11 +72,8 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
|
|
|
60
72
|
// package-manager.js): if any cached value set still carries a "system|version"
|
|
61
73
|
// include or a pipe in its url, rebuild this cache from the package.
|
|
62
74
|
if (dbExists && this.#hasPipeVersionedCanonicals()) {
|
|
63
|
-
await this
|
|
64
|
-
await
|
|
65
|
-
this.database = new ValueSetDatabase(this.dbPath);
|
|
66
|
-
await this.database.create();
|
|
67
|
-
await this._populateDatabase();
|
|
75
|
+
await this.#discardDatabase();
|
|
76
|
+
await this.#buildDatabase();
|
|
68
77
|
this.valueSetMap = await this.database.loadAllValueSets(this.sourcePackage());
|
|
69
78
|
}
|
|
70
79
|
|
|
@@ -84,6 +93,27 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
|
|
|
84
93
|
this.initialized = true;
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Create the cache and fill it from the package. The complete marker is written
|
|
98
|
+
* last, so a build that doesn't finish is detected (and redone) next time
|
|
99
|
+
* @private
|
|
100
|
+
*/
|
|
101
|
+
async #buildDatabase() {
|
|
102
|
+
await this.database.create();
|
|
103
|
+
await this._populateDatabase();
|
|
104
|
+
await this.database.markComplete();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Close and delete the cache file, leaving a fresh (not yet created) database
|
|
109
|
+
* @private
|
|
110
|
+
*/
|
|
111
|
+
async #discardDatabase() {
|
|
112
|
+
await this.database.close();
|
|
113
|
+
await fs.rm(this.dbPath, { force: true });
|
|
114
|
+
this.database = new ValueSetDatabase(this.dbPath);
|
|
115
|
+
}
|
|
116
|
+
|
|
87
117
|
/**
|
|
88
118
|
* True if any loaded value set still has a pipe-versioned canonical (url or
|
|
89
119
|
* compose include/exclude system) - the signature of a cache written before
|
|
@@ -134,6 +164,8 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
|
|
|
134
164
|
// don't store a value set the server can't use (e.g. one that contains a
|
|
135
165
|
// CodeSystem): it would be listed by search, and fail when read
|
|
136
166
|
checkContained(valueSet);
|
|
167
|
+
// its narrative is shown as-is in a page, so it must not carry script
|
|
168
|
+
assertNoActiveContent(valueSet);
|
|
137
169
|
} catch (e) {
|
|
138
170
|
require('../../library/logger').getInstance().child({ module: 'tx' })
|
|
139
171
|
.warn(`Package ${this.packageLoader.pid ? this.packageLoader.pid() : ''}: skipping ValueSet ${valueSet.url}: ${e.message}`);
|
package/tx/vs/vs-vsac.js
CHANGED
|
@@ -26,7 +26,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
26
26
|
* @param {string} config.apiKey - API key for VSAC authentication
|
|
27
27
|
* @param {string} config.cacheFolder - Local folder for cached database
|
|
28
28
|
* @param {number} [config.refreshIntervalHours=24] - Hours between refresh scans
|
|
29
|
-
* @param {string} [config.baseUrl='
|
|
29
|
+
* @param {string} [config.baseUrl='https://cts.nlm.nih.gov/fhir'] - Base URL for VSAC FHIR server
|
|
30
30
|
* @param {number} [config.timeoutMs=120000] - HTTP request timeout in milliseconds
|
|
31
31
|
* @param {string} [config.resyncPassword] - If set, enables the operator "resync a ValueSet"
|
|
32
32
|
* form on the /info page, gated by this password. If unset, the form is not offered.
|
|
@@ -43,7 +43,12 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
43
43
|
// Optional operator password for the on-demand resync form (see info()).
|
|
44
44
|
this.resyncPassword = config.resyncPassword || null;
|
|
45
45
|
this.cacheFolder = folders.ensureFilePath("terminology-cache/vsac");
|
|
46
|
-
|
|
46
|
+
// https: every request carries the API key. (VSAC redirects http to https, but only after
|
|
47
|
+
// the key has been sent in the clear)
|
|
48
|
+
this.baseUrl = (config.baseUrl || 'https://cts.nlm.nih.gov/fhir').replace(/\/+$/, '');
|
|
49
|
+
if (this.baseUrl.startsWith('http://')) {
|
|
50
|
+
console.warn(`VSAC: baseUrl ${this.baseUrl} is http, so the API key is sent unencrypted - use https`);
|
|
51
|
+
}
|
|
47
52
|
this.refreshIntervalHours = config.refreshIntervalHours || 24;
|
|
48
53
|
|
|
49
54
|
this.dbPath = path.join(this.cacheFolder, 'vsac-valuesets.db');
|
|
@@ -397,10 +402,24 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
397
402
|
return null;
|
|
398
403
|
}
|
|
399
404
|
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
s
|
|
403
|
-
|
|
405
|
+
// Followed only on the VSAC server itself, as a path under baseUrl: the request carries the
|
|
406
|
+
// API key, so a next link must not be able to send it anywhere else. The scheme is ignored -
|
|
407
|
+
// VSAC's links can say http - and the request goes out on baseUrl's
|
|
408
|
+
let next;
|
|
409
|
+
let base;
|
|
410
|
+
try {
|
|
411
|
+
base = new URL(this.baseUrl);
|
|
412
|
+
next = new URL(nextLink.url, this.baseUrl + '/');
|
|
413
|
+
} catch (e) {
|
|
414
|
+
console.warn(`VSAC: ignoring next link ${String(nextLink.url).substring(0, 200)}: ${e.message}`);
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
const basePath = base.pathname.replace(/\/+$/, '');
|
|
418
|
+
if (next.host !== base.host || !(next.pathname === basePath || next.pathname.startsWith(basePath + '/'))) {
|
|
419
|
+
console.warn(`VSAC: ignoring next link ${String(nextLink.url).substring(0, 200)}: it is not on ${this.baseUrl}`);
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
return next.pathname.substring(basePath.length) + next.search;
|
|
404
423
|
}
|
|
405
424
|
|
|
406
425
|
/**
|
package/tx/workers/expand.js
CHANGED
|
@@ -458,6 +458,19 @@ class ValueSetExpander {
|
|
|
458
458
|
}
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
// properties the code system always reports (ICD-11's classificationCode), unless
|
|
462
|
+
// the request asked for them anyway and the loops here add them
|
|
463
|
+
if (csProps != null && cs != null && !this.params.properties.includes('*')) {
|
|
464
|
+
for (const pn of cs.mandatoryExpansionProperties()) {
|
|
465
|
+
if (!this.params.properties.includes(pn)) {
|
|
466
|
+
for (const cp of csProps.filter(p => p.code === pn)) {
|
|
467
|
+
const vn = getValueName(cp);
|
|
468
|
+
this.defineProperty(expansion, n, this.getPropUrl(cs, pn, cp), pn, vn, cp[vn], true);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
461
474
|
for (const pn of this.params.properties) {
|
|
462
475
|
if (pn === '*') {
|
|
463
476
|
continue; // done above
|
|
@@ -836,7 +849,7 @@ class ValueSetExpander {
|
|
|
836
849
|
notClosed.value = true;
|
|
837
850
|
}
|
|
838
851
|
const prep = await cs.getPrepContext(true);
|
|
839
|
-
const ctxt = await cs.searchFilter(prep, filter,
|
|
852
|
+
const ctxt = await cs.searchFilter(prep, filter, true);
|
|
840
853
|
let set = await cs.executeFilters(prep);
|
|
841
854
|
if (this.count === 0 && valueSets.length === 0) {
|
|
842
855
|
// count=0 (count only): report the filtered size directly without
|
|
@@ -890,8 +903,9 @@ class ValueSetExpander {
|
|
|
890
903
|
const cctxt = await cs.locate(cc.code, this.allAltCodes);
|
|
891
904
|
if (cctxt && cctxt.context && (!this.params.activeOnly || !await cs.isInactive(cctxt.context)) && await this.passesFilters(cs, cctxt.context, prep, filters, 0)) {
|
|
892
905
|
let added;
|
|
906
|
+
const ecode = await cs.expansionCode(cctxt.context, cc.code);
|
|
893
907
|
if (this.noDetails) {
|
|
894
|
-
added = await this.includeCode(cs, null, cs.system(), cs.version(),
|
|
908
|
+
added = await this.includeCode(cs, null, cs.system(), cs.version(), ecode, await cs.isAbstract(cctxt.context), await cs.isInactive(cctxt.context), null, null, cds,
|
|
895
909
|
null, null, expansion, valueSets, null, null, null, null, excludeInactive, vsSrc.url);
|
|
896
910
|
} else {
|
|
897
911
|
await this.listDisplaysFromProvider(cds, cs, cctxt.context);
|
|
@@ -901,7 +915,7 @@ class ValueSetExpander {
|
|
|
901
915
|
if (!ov) {
|
|
902
916
|
ov = await cs.itemWeight(cctxt.context);
|
|
903
917
|
}
|
|
904
|
-
added = await this.includeCode(cs, null, cs.system(), cs.version(),
|
|
918
|
+
added = await this.includeCode(cs, null, cs.system(), cs.version(), ecode, await cs.isAbstract(cctxt.context), await cs.isInactive(cctxt.context), await cs.isDeprecated(cctxt.context), await cs.getStatus(cctxt.context), cds,
|
|
905
919
|
await cs.definition(cctxt.context), ov, expansion, valueSets, await cs.extensions(cctxt.context), cc.extension, await cs.properties(cctxt.context), null, excludeInactive, vsSrc.url);
|
|
906
920
|
}
|
|
907
921
|
if (added) {
|
|
@@ -939,16 +953,29 @@ class ValueSetExpander {
|
|
|
939
953
|
}
|
|
940
954
|
|
|
941
955
|
const fset = await cs.executeFilters(prep);
|
|
942
|
-
|
|
956
|
+
const filtersOpen = await cs.filtersNotClosed(prep);
|
|
957
|
+
if (filtersOpen) {
|
|
943
958
|
notClosed.value = true;
|
|
944
|
-
}
|
|
945
|
-
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// count=0 asks only for the total. When this include is the whole value set and its one
|
|
962
|
+
// filter set is the whole answer - nothing else to add, overlap, exclude or skip - the
|
|
963
|
+
// total is the set's size, and walking it would only end in VALUESET_TOO_COSTLY once
|
|
964
|
+
// the set is bigger than the limit. Anything less certain is counted by walking, as
|
|
965
|
+
// before: an earlier version added filterSize *and* walked, and double counted (#91, #93).
|
|
966
|
+
const countOnly = this.count === 0 && !filtersOpen && fset.length === 1 && filter.isNull
|
|
967
|
+
&& !excludeInactive && !this.params.activeOnly && !this.hasExclusions
|
|
968
|
+
&& !(cset.concept && cset.concept.length) && !(cset.valueSet && cset.valueSet.length)
|
|
969
|
+
&& (compose.include || []).length === 1 && (compose.exclude || []).length === 0;
|
|
970
|
+
if (countOnly) {
|
|
971
|
+
this.worker.opContext.log('count only: the filter size is the total');
|
|
972
|
+
this.addToTotal(await cs.filterSize(prep, fset[0]));
|
|
946
973
|
}
|
|
947
974
|
|
|
948
975
|
this.worker.opContext.log('iterate filters');
|
|
949
976
|
this.addToTotal(0);
|
|
950
977
|
const cds = new Designations(this.worker.i18n.languageDefinitions);
|
|
951
|
-
while (await cs.filterMore(prep, fset[0])) {
|
|
978
|
+
while (!countOnly && await cs.filterMore(prep, fset[0])) {
|
|
952
979
|
await this.worker.checkAndYield('processCodes#5');
|
|
953
980
|
const c = await cs.filterConcept(prep, fset[0]);
|
|
954
981
|
const ok = (!this.params.activeOnly || !await cs.isInactive(c)) && (await this.passesFilters(cs, c, prep, fset, 1));
|
package/tx/workers/lookup.js
CHANGED
|
@@ -15,6 +15,7 @@ const {Parameters} = require("../library/parameters");
|
|
|
15
15
|
const { Issue, OperationOutcome, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
|
|
16
16
|
const {debugLog} = require("../operation-context");
|
|
17
17
|
const {checkAddedParameters} = require("../library/parameter-check");
|
|
18
|
+
const { Languages } = require("../../library/languages");
|
|
18
19
|
|
|
19
20
|
class LookupWorker extends TerminologyWorker {
|
|
20
21
|
/**
|
|
@@ -204,7 +205,7 @@ class LookupWorker extends TerminologyWorker {
|
|
|
204
205
|
const supplements = this.loadSupplements(codeSystem.url, codeSystem.version, txp.supplements);
|
|
205
206
|
|
|
206
207
|
// Create a FhirCodeSystemProvider for this CodeSystem
|
|
207
|
-
const csProvider =
|
|
208
|
+
const csProvider = FhirCodeSystemProvider.create(this.opContext, codeSystem, supplements);
|
|
208
209
|
|
|
209
210
|
// Perform the lookup
|
|
210
211
|
const result = await this.doLookup(csProvider, code, txp);
|
|
@@ -247,6 +248,11 @@ class LookupWorker extends TerminologyWorker {
|
|
|
247
248
|
);
|
|
248
249
|
};
|
|
249
250
|
|
|
251
|
+
// Explicitly asked for: named in the property list, or '*'. An empty list is not asking for
|
|
252
|
+
// anything in particular (the server chooses), which is not the same thing for abstract and
|
|
253
|
+
// notSelectable - see below.
|
|
254
|
+
const asked = (name) => (params.properties || []).some(p => p === '*' || p.toLowerCase() === name.toLowerCase());
|
|
255
|
+
|
|
250
256
|
// Locate the code in the code system
|
|
251
257
|
const locateResult = await csProvider.locate(code);
|
|
252
258
|
|
|
@@ -271,7 +277,7 @@ class LookupWorker extends TerminologyWorker {
|
|
|
271
277
|
// name (required)
|
|
272
278
|
responseParams.push({
|
|
273
279
|
name: 'name',
|
|
274
|
-
valueString: csProvider.
|
|
280
|
+
valueString: csProvider.lookupName(params.workingLanguages())
|
|
275
281
|
});
|
|
276
282
|
responseParams.push({
|
|
277
283
|
name: 'code',
|
|
@@ -303,6 +309,32 @@ class LookupWorker extends TerminologyWorker {
|
|
|
303
309
|
valueString: disp || display || code
|
|
304
310
|
});
|
|
305
311
|
|
|
312
|
+
// A displayLanguage the code system has no display in, for this concept, still gets an
|
|
313
|
+
// answer - in the default language - but the client is told so: a display in a language it
|
|
314
|
+
// did not ask for, offered without comment, reads as an answer in the language it did.
|
|
315
|
+
// Only for an explicit displayLanguage: Accept-Language is a browser's standing preference,
|
|
316
|
+
// not a request about this code.
|
|
317
|
+
if (params.hasDisplayLanguages) {
|
|
318
|
+
// the languages actually asked for: displayLanguage is parsed with a wildcard appended,
|
|
319
|
+
// and the wildcard matches whatever display there is
|
|
320
|
+
const langs = new Languages(this.languages);
|
|
321
|
+
for (const l of params.workingLanguages()) {
|
|
322
|
+
if (l.code !== '*') langs.add(l);
|
|
323
|
+
}
|
|
324
|
+
const covered = langs.length === 0 || (designations.designations.length > 0
|
|
325
|
+
? designations.hasAnyDisplays(langs)
|
|
326
|
+
: csProvider.hasAnyDisplays(langs));
|
|
327
|
+
if (!covered) {
|
|
328
|
+
const used = (pd && pd.language) ? String(pd.language) : (csProvider.defLang() || 'en');
|
|
329
|
+
const csDesc = version ? csProvider.system() + '|' + version : csProvider.system();
|
|
330
|
+
responseParams.push({
|
|
331
|
+
name: 'warning',
|
|
332
|
+
valueString: this.i18n.translate('LOOKUP_DISPLAY_LANGUAGE_FALLBACK', params.HTTPLanguages,
|
|
333
|
+
[langs.asString(false), code, csDesc, used])
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
306
338
|
// definition (optional) - top-level parameter
|
|
307
339
|
if (hasProp('definition', true)) {
|
|
308
340
|
const definition = await csProvider.definition(ctxt);
|
|
@@ -314,9 +346,12 @@ class LookupWorker extends TerminologyWorker {
|
|
|
314
346
|
}
|
|
315
347
|
}
|
|
316
348
|
|
|
317
|
-
// abstract
|
|
318
|
-
|
|
319
|
-
|
|
349
|
+
// abstract and notSelectable (agreed for the tx-ecosystem tests, October 2026): a concept that
|
|
350
|
+
// is not selectable always says so, both ways, whatever properties were asked for - that is
|
|
351
|
+
// the fact a client most needs about it. A selectable one says neither unless asked.
|
|
352
|
+
// notSelectable is reconciled after the provider has added its properties, below.
|
|
353
|
+
const isAbstract = await csProvider.isAbstract(ctxt);
|
|
354
|
+
if (isAbstract || asked('abstract')) {
|
|
320
355
|
responseParams.push({
|
|
321
356
|
name: 'abstract',
|
|
322
357
|
valueBoolean: isAbstract
|
|
@@ -389,7 +424,7 @@ class LookupWorker extends TerminologyWorker {
|
|
|
389
424
|
// primitive belongs is a provider bug, and shipping it would produce a
|
|
390
425
|
// response that isn't valid FHIR (and, in XML, isn't even parseable).
|
|
391
426
|
const providerParamsFrom = responseParams.length;
|
|
392
|
-
await csProvider.extendLookup(ctxt, params.properties || [], responseParams);
|
|
427
|
+
await csProvider.extendLookup(ctxt, params.properties || [], responseParams, params);
|
|
393
428
|
let providerName;
|
|
394
429
|
try {
|
|
395
430
|
providerName = csProvider.system();
|
|
@@ -398,6 +433,26 @@ class LookupWorker extends TerminologyWorker {
|
|
|
398
433
|
}
|
|
399
434
|
checkAddedParameters(responseParams, providerParamsFrom, providerName);
|
|
400
435
|
|
|
436
|
+
// notSelectable, the property: present (true) for every concept that is not selectable;
|
|
437
|
+
// for a selectable one only when asked for (false), and taken out if a provider added it
|
|
438
|
+
// anyway
|
|
439
|
+
const isNotSelectable = (p) => p.name === 'property' &&
|
|
440
|
+
(p.part || []).some(x => x.name === 'code' && x.valueCode === 'notSelectable');
|
|
441
|
+
const nsAt = responseParams.findIndex(isNotSelectable);
|
|
442
|
+
if (isAbstract) {
|
|
443
|
+
if (nsAt < 0) {
|
|
444
|
+
responseParams.push({ name: 'property', part: [
|
|
445
|
+
{ name: 'code', valueCode: 'notSelectable' }, { name: 'value', valueBoolean: true }] });
|
|
446
|
+
}
|
|
447
|
+
} else if (asked('notSelectable')) {
|
|
448
|
+
if (nsAt < 0) {
|
|
449
|
+
responseParams.push({ name: 'property', part: [
|
|
450
|
+
{ name: 'code', valueCode: 'notSelectable' }, { name: 'value', valueBoolean: false }] });
|
|
451
|
+
}
|
|
452
|
+
} else if (nsAt >= 0) {
|
|
453
|
+
responseParams.splice(nsAt, 1);
|
|
454
|
+
}
|
|
455
|
+
|
|
401
456
|
if (reportedSupplements) {
|
|
402
457
|
for (const supplement of reportedSupplements) {
|
|
403
458
|
responseParams.push({
|
package/tx/workers/metadata.js
CHANGED
|
@@ -472,7 +472,12 @@ class MetadataHandler {
|
|
|
472
472
|
{ name: 'useSupplement' },
|
|
473
473
|
{ name: 'valueSet' },
|
|
474
474
|
{ name: 'valueSetVersion' }
|
|
475
|
-
]
|
|
475
|
+
],
|
|
476
|
+
textFilter: 'On code systems loaded from files and packages, a text filter ignores case, and accents on Latin, Greek and Cyrillic letters. ' +
|
|
477
|
+
'It matches concepts whose code, display, definition or a designation contains the text. ' +
|
|
478
|
+
'A filter of several words also matches a display or designation in which each word starts a word, ' +
|
|
479
|
+
'in any order; for this, punctuation and spaces only separate words. The closest matches come first. ' +
|
|
480
|
+
'Other code systems, such as SNOMED CT, LOINC and RxNorm, match text in their own way.'
|
|
476
481
|
};
|
|
477
482
|
}
|
|
478
483
|
|
package/tx/workers/subsumes.js
CHANGED
|
@@ -177,7 +177,7 @@ class SubsumesWorker extends TerminologyWorker {
|
|
|
177
177
|
const supplements = this.loadSupplements(codeSystem.url, codeSystem.version, txp.supplements);
|
|
178
178
|
|
|
179
179
|
// Create a FhirCodeSystemProvider for this CodeSystem
|
|
180
|
-
const csProvider =
|
|
180
|
+
const csProvider = FhirCodeSystemProvider.create(this.opContext, codeSystem, supplements);
|
|
181
181
|
|
|
182
182
|
// Get the codings
|
|
183
183
|
let codingA, codingB;
|