fhirsmith 0.14.0 → 0.14.2
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 +64 -0
- package/README.md +3 -2
- package/config-template.json +20 -0
- package/library/fhir-openapi-schema.js +441 -0
- package/library/html-server.js +9 -3
- package/library/languages.js +15 -0
- package/library/npm-audit.js +265 -0
- package/library/openapi-doc.js +557 -0
- package/library/same-origin.js +62 -0
- package/library/utilities.js +21 -0
- package/nginx.md +17 -9
- package/package.json +10 -4
- package/packages/openapi.js +13 -0
- package/packages/openapi.yaml +603 -0
- package/packages/packages-template.html +4 -2
- package/packages/packages.js +286 -81
- package/packages/readme.md +120 -0
- package/publisher/publisher-template.html +1 -0
- package/publisher/publisher.js +95 -42
- package/registry/api.js +117 -41
- package/registry/crawler.js +24 -0
- package/registry/fhirsmith-releases.js +170 -0
- package/registry/model.js +10 -0
- package/registry/openapi.js +13 -0
- package/registry/openapi.yaml +311 -0
- package/registry/readme.md +107 -208
- package/registry/registry-template.html +4 -1
- package/registry/registry.js +183 -20
- package/server.js +25 -6
- package/testing/openapi-schemas.config.js +58 -0
- package/testing/openapi-schemas.json +1920 -0
- package/testing/openapi.js +15 -0
- package/testing/openapi.yaml +273 -0
- package/testing/readme.md +75 -1
- package/testing/render.js +270 -29
- package/testing/search.js +1 -1
- package/testing/store.js +152 -8
- package/testing/testing-template.html +2 -0
- package/testing/testing.js +307 -9
- package/translations/Messages.properties +1 -0
- package/tx/README.md +58 -0
- package/tx/cm/cm-database.js +10 -1
- package/tx/cm/cm-package.js +8 -1
- package/tx/cs/cs-rxnorm.js +23 -6
- package/tx/cs/cs-snomed.js +3 -72
- package/tx/html/tx-template.html +2 -0
- package/tx/library/canonical-resource.js +50 -1
- package/tx/library/codesystem.js +1 -0
- package/tx/library/conceptmap.js +1 -0
- package/tx/library/valueset.js +16 -0
- package/tx/library.js +17 -2
- package/tx/openapi-operations.js +363 -0
- package/tx/openapi-schemas.config.js +118 -0
- package/tx/openapi-schemas.json +7903 -0
- package/tx/openapi.js +289 -0
- package/tx/openapi.yaml +151 -0
- package/tx/operation-context.js +15 -0
- package/tx/params.js +3 -12
- package/tx/provider.js +10 -1
- package/tx/sct/editions.js +70 -0
- package/tx/tx-html.js +6 -0
- package/tx/tx.js +55 -0
- package/tx/vs/vs-database.js +14 -5
- package/tx/vs/vs-package.js +10 -0
- package/tx/workers/expand.js +37 -18
- package/tx/workers/metadata.js +0 -13
- package/tx/workers/translate.js +25 -1
- package/tx/workers/validate.js +74 -24
- package/tx/workers/worker.js +32 -43
- package/utilities/extract-schema-json.js +167 -0
- package/utilities/generate-openapi-schemas.js +49 -0
- package/utilities/submit-test-report.js +1 -1
- package/xig/xig.js +2 -4
package/tx/tx.js
CHANGED
|
@@ -54,6 +54,7 @@ const {BundleXML} = require("./xml/bundle-xml");
|
|
|
54
54
|
const ConceptUsageTracker = require("./usage-tracker");
|
|
55
55
|
const ProblemFinder = require("./problems");
|
|
56
56
|
const { buildOperationOutcome, outcomeFromError } = require('./library/operation-outcome');
|
|
57
|
+
const txOpenApi = require('./openapi');
|
|
57
58
|
// const {writeFileSync} = require("fs");
|
|
58
59
|
|
|
59
60
|
class TXModule {
|
|
@@ -521,6 +522,11 @@ class TXModule {
|
|
|
521
522
|
|
|
522
523
|
app.use(express.urlencoded({ extended: true }));
|
|
523
524
|
|
|
525
|
+
// The OpenAPI description, on the endpoints it describes (R5)
|
|
526
|
+
if (txOpenApi.describes(fhirVersion)) {
|
|
527
|
+
this.setupOpenApi(router, endpointInfo);
|
|
528
|
+
}
|
|
529
|
+
|
|
524
530
|
// Set up routes
|
|
525
531
|
this.setupRoutes(router, endpointInfo.path);
|
|
526
532
|
|
|
@@ -540,6 +546,55 @@ class TXModule {
|
|
|
540
546
|
this.log.info(`Endpoint ${endpointPath} registered`);
|
|
541
547
|
}
|
|
542
548
|
|
|
549
|
+
/**
|
|
550
|
+
* The OpenAPI description of an endpoint: /openapi.json, /openapi.yaml, and /openapi (an
|
|
551
|
+
* HTML reference for browsers, the JSON otherwise), and an RFC 8631 Link header on every
|
|
552
|
+
* response that points to them. These are sent directly, not through the res.json the
|
|
553
|
+
* endpoint wraps for FHIR resources.
|
|
554
|
+
*
|
|
555
|
+
* @param {express.Router} router
|
|
556
|
+
* @param {Object} endpointInfo
|
|
557
|
+
*/
|
|
558
|
+
setupOpenApi(router, endpointInfo) {
|
|
559
|
+
const doc = txOpenApi.forEndpoint(endpointInfo.path);
|
|
560
|
+
const sendJson = (res) => res.type('application/json').send(doc.getJson());
|
|
561
|
+
router.use((req, res, next) => {
|
|
562
|
+
res.setHeader('Link', `<${req.baseUrl}/openapi.json>; rel="service-desc", <${req.baseUrl}/openapi>; rel="service-doc"`);
|
|
563
|
+
next();
|
|
564
|
+
});
|
|
565
|
+
const count = (start) => this.countRequest(endpointInfo.path, 'openapi', Date.now() - start);
|
|
566
|
+
router.get('/openapi.json', (req, res) => {
|
|
567
|
+
const start = Date.now();
|
|
568
|
+
try {
|
|
569
|
+
sendJson(res);
|
|
570
|
+
} finally {
|
|
571
|
+
count(start);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
router.get('/openapi.yaml', (req, res) => {
|
|
575
|
+
const start = Date.now();
|
|
576
|
+
try {
|
|
577
|
+
res.type('application/yaml').send(doc.getYaml());
|
|
578
|
+
} finally {
|
|
579
|
+
count(start);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
router.get('/openapi', async (req, res) => {
|
|
583
|
+
const start = Date.now();
|
|
584
|
+
try {
|
|
585
|
+
if (!txHtml.acceptsHtml(req)) {
|
|
586
|
+
return sendJson(res);
|
|
587
|
+
}
|
|
588
|
+
const txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid, this.languages, this.i18n, endpointInfo.path);
|
|
589
|
+
const html = await txhtml.renderPage('API', doc.renderHtml(), endpointInfo, req.txStartTime);
|
|
590
|
+
res.setHeader('Content-Type', 'text/html');
|
|
591
|
+
res.send(html);
|
|
592
|
+
} finally {
|
|
593
|
+
count(start);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
543
598
|
/**
|
|
544
599
|
* Set up routes for an endpoint
|
|
545
600
|
* @param {express.Router} router - Express router
|
package/tx/vs/vs-database.js
CHANGED
|
@@ -743,7 +743,17 @@ class ValueSetDatabase {
|
|
|
743
743
|
const valueSetMap = new Map();
|
|
744
744
|
|
|
745
745
|
for (const row of rows) {
|
|
746
|
-
|
|
746
|
+
let valueSet;
|
|
747
|
+
try {
|
|
748
|
+
valueSet = new ValueSet(JSON.parse(row.content));
|
|
749
|
+
} catch (e) {
|
|
750
|
+
// one unacceptable value set (e.g. one containing a CodeSystem, which the
|
|
751
|
+
// server doesn't support) must not take the whole package's value sets with it
|
|
752
|
+
require('../../library/logger').getInstance().child({ module: 'tx' })
|
|
753
|
+
.warn(`${source || 'ValueSet database'}: skipping ValueSet ${row.url || row.id}${row.version ? '|' + row.version : ''}: ${e.message}`);
|
|
754
|
+
this.vsCount--;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
747
757
|
valueSet.sourcePackage = source;
|
|
748
758
|
// Attach the stored content hash so callers can detect changes
|
|
749
759
|
// without recomputing over the full JSON.
|
|
@@ -839,10 +849,9 @@ class ValueSetDatabase {
|
|
|
839
849
|
});
|
|
840
850
|
} else {
|
|
841
851
|
// Fall back to parsing JSON
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
});
|
|
852
|
+
// a value set the map doesn't have was rejected when the map was loaded
|
|
853
|
+
// (see loadAllValueSets), so it isn't a result
|
|
854
|
+
results = rows.map(row => map.get(row.id)).filter(vs => vs);
|
|
846
855
|
}
|
|
847
856
|
|
|
848
857
|
resolve(results);
|
package/tx/vs/vs-package.js
CHANGED
|
@@ -3,6 +3,7 @@ const fs = require('fs').promises;
|
|
|
3
3
|
const { AbstractValueSetProvider } = require('./vs-api');
|
|
4
4
|
const { PackageContentLoader } = require('../../library/package-manager');
|
|
5
5
|
const { ValueSetDatabase } = require('./vs-database');
|
|
6
|
+
const { checkContained } = require('../library/canonical-resource');
|
|
6
7
|
const { VersionUtilities } = require('../../library/version-utilities');
|
|
7
8
|
const {validateParameter} = require("../../library/utilities");
|
|
8
9
|
|
|
@@ -126,6 +127,15 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
|
|
|
126
127
|
for (const entry of valueSetEntries) {
|
|
127
128
|
const valueSet = await this.packageLoader.loadFile(entry);
|
|
128
129
|
if (valueSet.url) {
|
|
130
|
+
try {
|
|
131
|
+
// don't store a value set the server can't use (e.g. one that contains a
|
|
132
|
+
// CodeSystem): it would be listed by search, and fail when read
|
|
133
|
+
checkContained(valueSet);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
require('../../library/logger').getInstance().child({ module: 'tx' })
|
|
136
|
+
.warn(`Package ${this.packageLoader.pid ? this.packageLoader.pid() : ''}: skipping ValueSet ${valueSet.url}: ${e.message}`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
129
139
|
valueSets.push(valueSet);
|
|
130
140
|
}
|
|
131
141
|
}
|
package/tx/workers/expand.js
CHANGED
|
@@ -620,19 +620,23 @@ class ValueSetExpander {
|
|
|
620
620
|
return count;
|
|
621
621
|
}
|
|
622
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Exclude the codes of an (expanded) value set. Excludes are processed before includes,
|
|
625
|
+
* so the codes are recorded as excluded - as excludeCode does - and the includes then
|
|
626
|
+
* skip them. (This used to remove them from what had been included so far, which at
|
|
627
|
+
* that point is nothing, so excluding a value set excluded nothing.)
|
|
628
|
+
*/
|
|
623
629
|
excludeValueSet(vs, expansion, imports, offset) {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (idx >= 0) {
|
|
630
|
-
this.fullList.splice(idx, 1);
|
|
630
|
+
const walk = (list) => {
|
|
631
|
+
for (const c of list || []) {
|
|
632
|
+
this.worker.deadCheck('excludeValueSet');
|
|
633
|
+
if (c.code && this.passesImports(imports, c.system, c.code, offset)) {
|
|
634
|
+
this.excluded.add((this.doingVersion && !this.params.versionsMatch ? c.system + '|' + c.version : c.system) + '#' + c.code);
|
|
631
635
|
}
|
|
632
|
-
|
|
633
|
-
this.decTotal();
|
|
636
|
+
walk(c.contains);
|
|
634
637
|
}
|
|
635
|
-
}
|
|
638
|
+
};
|
|
639
|
+
walk(vs.expansion.contains);
|
|
636
640
|
}
|
|
637
641
|
|
|
638
642
|
async checkSource(cset, exp, filter, srcURL, ts, vsInfo , source) {
|
|
@@ -1012,8 +1016,10 @@ class ValueSetExpander {
|
|
|
1012
1016
|
let vs = await this.worker.findValueSet(s, '', vsSrc);
|
|
1013
1017
|
const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed));
|
|
1014
1018
|
this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
|
|
1015
|
-
|
|
1016
|
-
|
|
1019
|
+
// ivs.valueSet is the expansion (plain JSON), so it has no vurl - as for the
|
|
1020
|
+
// includes, build it
|
|
1021
|
+
if (!vs.isContained && this.worker.makeVurl(ivs.valueSet)) {
|
|
1022
|
+
this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
|
|
1017
1023
|
}
|
|
1018
1024
|
valueSets.push(ivs);
|
|
1019
1025
|
}
|
|
@@ -1314,13 +1320,22 @@ class ValueSetExpander {
|
|
|
1314
1320
|
}
|
|
1315
1321
|
|
|
1316
1322
|
async expand(source, filter, noCacheThisOne) {
|
|
1317
|
-
this.noCacheThisOne = noCacheThisOne;
|
|
1318
|
-
this.totalStatus = 'uninitialised';
|
|
1319
|
-
this.total = 0;
|
|
1320
|
-
|
|
1321
1323
|
Extensions.checkNoImplicitRules(source,'ValueSetExpander.Expand', 'ValueSet', source.vurl);
|
|
1322
1324
|
Extensions.checkNoModifiers(source,'ValueSetExpander.Expand', 'ValueSet', source.vurl);
|
|
1325
|
+
// circular reference detection: this value set is in the chain being processed until
|
|
1326
|
+
// its expansion is done
|
|
1323
1327
|
this.worker.seeValueSet(source, this.params);
|
|
1328
|
+
try {
|
|
1329
|
+
return await this.expandSeen(source, filter, noCacheThisOne);
|
|
1330
|
+
} finally {
|
|
1331
|
+
this.worker.unseeValueSet(source);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
async expandSeen(source, filter, noCacheThisOne) {
|
|
1336
|
+
this.noCacheThisOne = noCacheThisOne;
|
|
1337
|
+
this.totalStatus = 'uninitialised';
|
|
1338
|
+
this.total = 0;
|
|
1324
1339
|
this.valueSet = source;
|
|
1325
1340
|
|
|
1326
1341
|
const result = structuredClone(source.jsonObj);
|
|
@@ -1348,6 +1363,10 @@ class ValueSetExpander {
|
|
|
1348
1363
|
if (result.expansion) {
|
|
1349
1364
|
return result; // just return the expansion
|
|
1350
1365
|
}
|
|
1366
|
+
if (!source.jsonObj.compose) {
|
|
1367
|
+
throw new Issue('error', 'invalid', null, 'VALUESET_NO_COMPOSE',
|
|
1368
|
+
this.worker.i18n.translate('VALUESET_NO_COMPOSE', this.params.httpLanguages, [source.contextKey || source.vurlOrMsg]), 'vs-invalid', 422);
|
|
1369
|
+
}
|
|
1351
1370
|
|
|
1352
1371
|
if (this.params.generateNarrative && !this.noDetails) {
|
|
1353
1372
|
div_ = div();
|
|
@@ -1385,9 +1404,9 @@ class ValueSetExpander {
|
|
|
1385
1404
|
}
|
|
1386
1405
|
|
|
1387
1406
|
if (this.params.DisplayLanguages) {
|
|
1388
|
-
this.addParamCode(exp, 'displayLanguage', this.params.DisplayLanguages.
|
|
1407
|
+
this.addParamCode(exp, 'displayLanguage', this.params.DisplayLanguages.asParameter());
|
|
1389
1408
|
} else if (this.params.HTTPLanguages) {
|
|
1390
|
-
this.addParamCode(exp, 'displayLanguage', this.params.HTTPLanguages.
|
|
1409
|
+
this.addParamCode(exp, 'displayLanguage', this.params.HTTPLanguages.asParameter());
|
|
1391
1410
|
}
|
|
1392
1411
|
if (this.params.designations) {
|
|
1393
1412
|
for (const s of this.params.designations) {
|
package/tx/workers/metadata.js
CHANGED
|
@@ -437,7 +437,6 @@ class MetadataHandler {
|
|
|
437
437
|
// Parameters the $expand handler reads - see handleTypeLevelExpand in
|
|
438
438
|
// tx/workers/expand.js and TxParameters.seeParameter in tx/params.js.
|
|
439
439
|
// Keep this list in step with those when adding a parameter.
|
|
440
|
-
{ name: '_incomplete' },
|
|
441
440
|
{ name: 'abstract' },
|
|
442
441
|
{ name: 'activeOnly' },
|
|
443
442
|
{ name: 'check-system-version' },
|
|
@@ -453,20 +452,12 @@ class MetadataHandler {
|
|
|
453
452
|
{ name: 'filter' },
|
|
454
453
|
{ name: 'force-system-version' },
|
|
455
454
|
{ name: 'force-valueset-version' },
|
|
456
|
-
{ name: 'inactive' },
|
|
457
|
-
{ name: 'includeAlternateCodes' },
|
|
458
455
|
{ name: 'includeDefinition' },
|
|
459
456
|
{ name: 'includeDesignations' },
|
|
460
|
-
{ name: 'incomplete-ok' },
|
|
461
457
|
{
|
|
462
458
|
name: 'limit',
|
|
463
459
|
documentation: 'Upper bound on the size of the expansion; the operation fails as too-costly beyond it'
|
|
464
460
|
},
|
|
465
|
-
{ name: 'limitedExpansion' },
|
|
466
|
-
{
|
|
467
|
-
name: 'mode',
|
|
468
|
-
documentation: '=lenient-display-validation'
|
|
469
|
-
},
|
|
470
461
|
{ name: 'no-cache' },
|
|
471
462
|
{ name: 'offset' },
|
|
472
463
|
{ name: 'profile' },
|
|
@@ -479,10 +470,6 @@ class MetadataHandler {
|
|
|
479
470
|
{ name: 'url' },
|
|
480
471
|
{ name: 'useSupplement' },
|
|
481
472
|
{ name: 'valueSet' },
|
|
482
|
-
{
|
|
483
|
-
name: 'valueSetMode',
|
|
484
|
-
documentation: '= CHECK_MEMBERSHIP_ONLY | NO_MEMBERSHIP_CHECK'
|
|
485
|
-
},
|
|
486
473
|
{ name: 'valueSetVersion' }
|
|
487
474
|
]
|
|
488
475
|
};
|
package/tx/workers/translate.js
CHANGED
|
@@ -1085,9 +1085,14 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
1085
1085
|
added = await this.translateUsingGroupsForwards(cm, coding, targetScope, targetSystem, params, result) || added;
|
|
1086
1086
|
}
|
|
1087
1087
|
}
|
|
1088
|
+
// A map can say, explicitly, that a concept is NOT related to a target (not-related-to,
|
|
1089
|
+
// or R4's unmatched/disjoint). That is still reported as a match - it is what the map
|
|
1090
|
+
// says - but it is not a translation: "The value can only be true if at least one
|
|
1091
|
+
// returned match has a relationship other than 'not-related-to'" (tx-ecosystem translate-2b)
|
|
1092
|
+
const translated = added && result.some(p => p.name === 'match' && this.isPositiveMatch(p));
|
|
1088
1093
|
result.push({
|
|
1089
1094
|
name: 'result',
|
|
1090
|
-
valueBoolean:
|
|
1095
|
+
valueBoolean: translated
|
|
1091
1096
|
});
|
|
1092
1097
|
if (!added) {
|
|
1093
1098
|
result.push({
|
|
@@ -1120,6 +1125,25 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
1120
1125
|
};
|
|
1121
1126
|
}
|
|
1122
1127
|
|
|
1128
|
+
/**
|
|
1129
|
+
* Whether a match counts towards result=true. A noMap match says there is no mapping, and
|
|
1130
|
+
* an explicit negative relationship says the target is not related, so neither is a
|
|
1131
|
+
* translation. A match with no relationship otherwise (an unmapped fallback without one,
|
|
1132
|
+
* a code system supplied translation) still counts.
|
|
1133
|
+
*/
|
|
1134
|
+
isPositiveMatch(match) {
|
|
1135
|
+
const part = (name) => (match.part || []).find(p => p.name === name);
|
|
1136
|
+
if (part('noMap')?.valueBoolean === true) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
const rel = part('relationship')?.valueCode;
|
|
1140
|
+
const eq = part('equivalence')?.valueCode;
|
|
1141
|
+
if (eq === 'unmatched' || eq === 'disjoint') {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
return rel !== 'not-related-to';
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1123
1147
|
// eslint-disable-next-line no-unused-vars
|
|
1124
1148
|
isOkTarget(cm, vs) {
|
|
1125
1149
|
// if cm.target != null then
|
package/tx/workers/validate.js
CHANGED
|
@@ -285,7 +285,10 @@ class ValueSetChecker {
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
seeValueSet() {
|
|
288
|
-
this.
|
|
288
|
+
const key = this.valueSet.contextKey !== undefined ? this.valueSet.contextKey : this.valueSet.vurl;
|
|
289
|
+
if (key) {
|
|
290
|
+
this.worker.opContext.seeContext(key);
|
|
291
|
+
}
|
|
289
292
|
if (this.valueSet.jsonObj.compose && this.valueSet.jsonObj.compose.extension) {
|
|
290
293
|
for (let ext of this.valueSet.jsonObj.compose.extension) {
|
|
291
294
|
if (ext.url === 'http://hl7.org/fhir/StructureDefinition/valueset-expansion-parameter' || ext.url === 'http://hl7.org/fhir/tools/StructureDefinition/valueset-expansion-parameter') {
|
|
@@ -305,8 +308,22 @@ class ValueSetChecker {
|
|
|
305
308
|
async prepare() {
|
|
306
309
|
if (this.valueSet === null) {
|
|
307
310
|
throw new Issue('error', 'not-found', null, null, 'Error Error: vs = nil', null, 422);
|
|
308
|
-
}
|
|
309
|
-
|
|
311
|
+
}
|
|
312
|
+
// circular reference detection: this value set is in the chain being processed until
|
|
313
|
+
// it's prepared (which prepares everything it imports)
|
|
314
|
+
this.seeValueSet();
|
|
315
|
+
try {
|
|
316
|
+
await this.prepareSeen();
|
|
317
|
+
} finally {
|
|
318
|
+
const key = this.valueSet.contextKey !== undefined ? this.valueSet.contextKey : this.valueSet.vurl;
|
|
319
|
+
if (key) {
|
|
320
|
+
this.worker.opContext.unseeContext(key);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async prepareSeen() {
|
|
326
|
+
{
|
|
310
327
|
this.worker.opContext.addNote(this.valueSet, 'Analysing ' + this.valueSet.vurl + ' for validation purposes', this.indentCount);
|
|
311
328
|
if (this.indentCount === 0) {
|
|
312
329
|
this.worker.opContext.addNote(this.valueSet, 'Parameters: ' + this.params.summary(), this.indentCount);
|
|
@@ -569,9 +586,14 @@ class ValueSetChecker {
|
|
|
569
586
|
if (msg) {
|
|
570
587
|
op.addIssue(new Issue('information', 'informational', addToPath(path, 'code'), null, msg, 'process-note'));
|
|
571
588
|
}
|
|
572
|
-
inactive
|
|
573
|
-
inactive
|
|
574
|
-
|
|
589
|
+
// in a CodeableConcept a later coding must not clear the inactive status an earlier
|
|
590
|
+
// coding found, or the warning silently disappears when the inactive coding comes first
|
|
591
|
+
if (!inactive.value) {
|
|
592
|
+
inactive.value = await cs.isInactive(ctxt.context);
|
|
593
|
+
inactive.path = path;
|
|
594
|
+
inactive.code = code;
|
|
595
|
+
vstatus.value = await cs.getStatus(ctxt.context);
|
|
596
|
+
}
|
|
575
597
|
}
|
|
576
598
|
if (displays !== null) {
|
|
577
599
|
await this.worker.listDisplaysFromCodeSystem(displays, cs, ctxt.context);
|
|
@@ -925,6 +947,7 @@ class ValueSetChecker {
|
|
|
925
947
|
let pdisp;
|
|
926
948
|
let psys;
|
|
927
949
|
let pcode;
|
|
950
|
+
let ppath; // the path of the coding that is returned
|
|
928
951
|
let pver;
|
|
929
952
|
|
|
930
953
|
|
|
@@ -1063,6 +1086,7 @@ class ValueSetChecker {
|
|
|
1063
1086
|
if (pcode === undefined) {
|
|
1064
1087
|
psys = c.system;
|
|
1065
1088
|
pcode = c.code;
|
|
1089
|
+
ppath = path;
|
|
1066
1090
|
if (ver.value) {
|
|
1067
1091
|
pver = ver.value;
|
|
1068
1092
|
}
|
|
@@ -1088,7 +1112,11 @@ class ValueSetChecker {
|
|
|
1088
1112
|
mt.push(m);
|
|
1089
1113
|
op.addIssue(new Issue('error', 'invalid', p, 'Terminology_TX_System_Relative', m, 'invalid-data'));
|
|
1090
1114
|
}
|
|
1091
|
-
|
|
1115
|
+
// the code isn't in the value set, but it's still looked up in the code system to report on
|
|
1116
|
+
// it - and that has to be the version the value set selects when the coding doesn't say,
|
|
1117
|
+
// not the server default (tx-ecosystem sct validate-code-pc-none)
|
|
1118
|
+
let lookupVersion = c.version || vsImpliedVersion;
|
|
1119
|
+
let prov = await this.worker.findCodeSystem(ws, lookupVersion, this.params, ['complete', 'fragment'], op,true, true, false, this.worker.requiredSupplements);
|
|
1092
1120
|
if (prov === null) {
|
|
1093
1121
|
let vss = await this.worker.findValueSet(ws, '', null);
|
|
1094
1122
|
if (vss !== null) {
|
|
@@ -1098,7 +1126,7 @@ class ValueSetChecker {
|
|
|
1098
1126
|
op.addIssue(new Issue('error', 'invalid', addToPath(path, 'system'), 'Terminology_TX_System_ValueSet2', m, 'invalid-data'));
|
|
1099
1127
|
cause.value = 'invalid';
|
|
1100
1128
|
} else {
|
|
1101
|
-
let provS = await this.worker.findCodeSystem(ws,
|
|
1129
|
+
let provS = await this.worker.findCodeSystem(ws, lookupVersion, this.params, ['supplement'], op,true, true, false, this.worker.requiredSupplements);
|
|
1102
1130
|
if (provS !== null) {
|
|
1103
1131
|
vss = null;
|
|
1104
1132
|
let m = this.worker.i18n.translate('CODESYSTEM_CS_NO_SUPPLEMENT', this.params.HTTPLanguages, [provS.vurl()]);
|
|
@@ -1109,7 +1137,7 @@ class ValueSetChecker {
|
|
|
1109
1137
|
let prov2 = await this.worker.findCodeSystem(ws, '', this.params, ['complete', 'fragment'], op,true, true, false, this.worker.requiredSupplements);
|
|
1110
1138
|
let bAdd = true;
|
|
1111
1139
|
let m, mid, vn;
|
|
1112
|
-
if (prov2 === null && !
|
|
1140
|
+
if (prov2 === null && !lookupVersion) {
|
|
1113
1141
|
mid = 'UNKNOWN_CODESYSTEM';
|
|
1114
1142
|
m = this.worker.i18n.translate('UNKNOWN_CODESYSTEM', this.params.HTTPLanguages, [ws]);
|
|
1115
1143
|
bAdd = !unknownSystems.has(ws);
|
|
@@ -1123,9 +1151,9 @@ class ValueSetChecker {
|
|
|
1123
1151
|
vn = ws;
|
|
1124
1152
|
} else {
|
|
1125
1153
|
mid = 'UNKNOWN_CODESYSTEM_VERSION';
|
|
1126
|
-
vn = ws + '|' +
|
|
1154
|
+
vn = ws + '|' + lookupVersion;
|
|
1127
1155
|
}
|
|
1128
|
-
m = this.worker.i18n.translate(mid, this.params.HTTPLanguages, [ws,
|
|
1156
|
+
m = this.worker.i18n.translate(mid, this.params.HTTPLanguages, [ws, lookupVersion, this.worker.presentVersionList(vl)]);
|
|
1129
1157
|
bAdd = !unknownSystems.has(vn);
|
|
1130
1158
|
if (bAdd) {
|
|
1131
1159
|
unknownSystems.add(vn);
|
|
@@ -1181,6 +1209,7 @@ class ValueSetChecker {
|
|
|
1181
1209
|
if (!this.params.membershipOnly && !inactive.value && await prov.isInactive(ctxt.context)) {
|
|
1182
1210
|
inactive.value = true;
|
|
1183
1211
|
inactive.path = path;
|
|
1212
|
+
inactive.code = c.code;
|
|
1184
1213
|
const st = await prov.getStatus(ctxt.context);
|
|
1185
1214
|
if (st) {
|
|
1186
1215
|
vstatus.value = st;
|
|
@@ -1336,9 +1365,14 @@ class ValueSetChecker {
|
|
|
1336
1365
|
}
|
|
1337
1366
|
|
|
1338
1367
|
if (inactive.value) {
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1368
|
+
// the inactive and status parameters describe the code being returned. In a CodeableConcept
|
|
1369
|
+
// the inactive coding may be a different one (or none was returned at all): it still gets
|
|
1370
|
+
// its warning below, but the parameters are not claimed for the returned code
|
|
1371
|
+
if (mode !== 'codeableConcept' || (pcode && inactive.path === ppath)) {
|
|
1372
|
+
result.addParamBool('inactive', inactive.value);
|
|
1373
|
+
if (vstatus.value && vstatus.value !== 'inactive') {
|
|
1374
|
+
result.addParamCode('status', vstatus.value);
|
|
1375
|
+
}
|
|
1342
1376
|
}
|
|
1343
1377
|
let mpath = inactive.path;
|
|
1344
1378
|
if (!mpath) {
|
|
@@ -1350,7 +1384,9 @@ class ValueSetChecker {
|
|
|
1350
1384
|
mid1 = 'INACTIVE_CONCEPT_FOUND_ADD';
|
|
1351
1385
|
mm1 = 'inactive';
|
|
1352
1386
|
}
|
|
1353
|
-
|
|
1387
|
+
// report the code that is actually inactive - in a CodeableConcept that isn't necessarily
|
|
1388
|
+
// the first coding (tcode), which is what this message used to name
|
|
1389
|
+
let m = this.worker.i18n.translate(mid1, this.params.HTTPLanguages, [vstatus.value, inactive.code || tcode, mm1]);
|
|
1354
1390
|
msg(m);
|
|
1355
1391
|
op.addIssue(new Issue('warning', 'business-rule', mpath, 'INACTIVE_CONCEPT_FOUND', m, 'code-comment'));
|
|
1356
1392
|
} else if (vstatus.value && vstatus.value.toLowerCase() === 'deprecated') {
|
|
@@ -1544,6 +1580,7 @@ class ValueSetChecker {
|
|
|
1544
1580
|
if (!this.params.membershipOnly && role !== 'not in') {
|
|
1545
1581
|
inactive.value = true;
|
|
1546
1582
|
inactive.path = path;
|
|
1583
|
+
inactive.code = code;
|
|
1547
1584
|
if (inactive.value) {
|
|
1548
1585
|
vstatus.value = await cs.getStatus(loc.context);
|
|
1549
1586
|
}
|
|
@@ -1554,6 +1591,7 @@ class ValueSetChecker {
|
|
|
1554
1591
|
if (role !== 'not in') {
|
|
1555
1592
|
inactive.value = true;
|
|
1556
1593
|
inactive.path = path;
|
|
1594
|
+
inactive.code = code;
|
|
1557
1595
|
vstatus.value = await cs.getStatus(loc.context);
|
|
1558
1596
|
let msg = this.worker.i18n.translate('STATUS_CODE_WARNING_CODE', this.params.HTTPLanguages, ['not active', code]);
|
|
1559
1597
|
messages.push(msg);
|
|
@@ -1562,9 +1600,12 @@ class ValueSetChecker {
|
|
|
1562
1600
|
} else {
|
|
1563
1601
|
result = true;
|
|
1564
1602
|
if (role !== 'not in') {
|
|
1565
|
-
inactive.value
|
|
1566
|
-
|
|
1567
|
-
|
|
1603
|
+
if (!inactive.value) {
|
|
1604
|
+
inactive.value = await cs.isInactive(loc.context);
|
|
1605
|
+
inactive.path = path;
|
|
1606
|
+
inactive.code = code;
|
|
1607
|
+
vstatus.value = await cs.getStatus(loc.context);
|
|
1608
|
+
}
|
|
1568
1609
|
if (vcc !== null) {
|
|
1569
1610
|
if (!vcc.coding) {
|
|
1570
1611
|
vcc.coding = [];
|
|
@@ -1601,6 +1642,7 @@ class ValueSetChecker {
|
|
|
1601
1642
|
if (!this.params.membershipOnly) {
|
|
1602
1643
|
inactive.value = true;
|
|
1603
1644
|
inactive.path = path;
|
|
1645
|
+
inactive.code = code;
|
|
1604
1646
|
vstatus.value = await cs.getStatus(loc);
|
|
1605
1647
|
}
|
|
1606
1648
|
} else {
|
|
@@ -1614,9 +1656,12 @@ class ValueSetChecker {
|
|
|
1614
1656
|
} else if (Extensions.has(cc,'http://hl7.org/fhir/StructureDefinition/valueset-deprecated')) {
|
|
1615
1657
|
op.addIssue(new Issue('warning', 'business-rule', addToPath(path, 'code'), 'CONCEPT_DEPRECATED_IN_VALUESET', this.worker.i18n.translate('CONCEPT_DEPRECATED_IN_VALUESET', this.params.HTTPLanguages, [cs.system(), code, 'deprecated', vs.vurl]), 'code-comment'));
|
|
1616
1658
|
}
|
|
1617
|
-
inactive.value
|
|
1618
|
-
|
|
1619
|
-
|
|
1659
|
+
if (!inactive.value) {
|
|
1660
|
+
inactive.value = await cs.isInactive(loc);
|
|
1661
|
+
inactive.path = path;
|
|
1662
|
+
inactive.code = code;
|
|
1663
|
+
vstatus.value = await cs.getStatus(loc);
|
|
1664
|
+
}
|
|
1620
1665
|
result = true;
|
|
1621
1666
|
return result;
|
|
1622
1667
|
}
|
|
@@ -1665,6 +1710,7 @@ class ValueSetChecker {
|
|
|
1665
1710
|
if (!this.params.membershipOnly) {
|
|
1666
1711
|
inactive.value = true;
|
|
1667
1712
|
inactive.path = path;
|
|
1713
|
+
inactive.code = code;
|
|
1668
1714
|
vstatus.value = await cs.getStatus(loc);
|
|
1669
1715
|
}
|
|
1670
1716
|
} else {
|
|
@@ -1677,6 +1723,7 @@ class ValueSetChecker {
|
|
|
1677
1723
|
if (await cs.isInactive(loc)) {
|
|
1678
1724
|
inactive.value = true;
|
|
1679
1725
|
inactive.path = path;
|
|
1726
|
+
inactive.code = code;
|
|
1680
1727
|
// only replace a status we already have with a real one - a filter context
|
|
1681
1728
|
// doesn't always carry the status (SNOMED, LOINC), and the assembly reads
|
|
1682
1729
|
// vstatus for the message wording as well as for the status parameter
|
|
@@ -1714,9 +1761,12 @@ class ValueSetChecker {
|
|
|
1714
1761
|
}
|
|
1715
1762
|
} else {
|
|
1716
1763
|
result = true;
|
|
1717
|
-
inactive.value
|
|
1718
|
-
|
|
1719
|
-
|
|
1764
|
+
if (!inactive.value) {
|
|
1765
|
+
inactive.value = await cs.isInactive(loc.context);
|
|
1766
|
+
inactive.path = path;
|
|
1767
|
+
inactive.code = code;
|
|
1768
|
+
vstatus.value = await cs.getStatus(loc.context);
|
|
1769
|
+
}
|
|
1720
1770
|
await this.worker.listDisplaysFromCodeSystem(displays, cs, loc.context);
|
|
1721
1771
|
return result;
|
|
1722
1772
|
}
|
package/tx/workers/worker.js
CHANGED
|
@@ -7,6 +7,7 @@ const {Issue} = require("../library/operation-outcome");
|
|
|
7
7
|
const {Languages} = require("../../library/languages");
|
|
8
8
|
const {ConceptMap} = require("../library/conceptmap");
|
|
9
9
|
const {Renderer} = require("../library/renderer");
|
|
10
|
+
const {editionName} = require("../sct/editions");
|
|
10
11
|
|
|
11
12
|
// The cache-id travels as an HTTP header (not an operation parameter) so proxies /
|
|
12
13
|
// load-balancers can act on it and the server can reject it before parsing the body.
|
|
@@ -378,14 +379,22 @@ class TerminologyWorker {
|
|
|
378
379
|
return null;
|
|
379
380
|
}
|
|
380
381
|
if (url.startsWith("#")) {
|
|
382
|
+
// A local reference: per FHIR, #id is resolved against the contained resources of
|
|
383
|
+
// the resource being processed - which, when that is itself a contained value set,
|
|
384
|
+
// means its container's (contained resources are all siblings in one list)
|
|
381
385
|
if (source) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
386
|
+
const container = source.container || source;
|
|
387
|
+
const json = container.jsonObj || container;
|
|
388
|
+
for (const contained of json.contained || []) {
|
|
389
|
+
if (contained && contained.id === url.substring(1)) {
|
|
390
|
+
if (contained.resourceType !== 'ValueSet') {
|
|
391
|
+
// the resource wrappers already reject this; this is the second line of defence
|
|
392
|
+
throw new Issue('error', 'not-supported', null, 'CONTAINED_RESOURCE_NOT_SUPPORTED',
|
|
393
|
+
`The reference '${url}' is to a contained ${contained.resourceType}, not a ValueSet`, 'not-supported', 400);
|
|
394
|
+
}
|
|
387
395
|
const ret = this.wrapRawResource(contained);
|
|
388
396
|
ret.isContained = true;
|
|
397
|
+
ret.container = container.jsonObj ? container : this.wrapRawResource(container);
|
|
389
398
|
return ret;
|
|
390
399
|
}
|
|
391
400
|
}
|
|
@@ -476,11 +485,24 @@ class TerminologyWorker {
|
|
|
476
485
|
* Note: this is the caller's parameters, not this.params - the worker's own
|
|
477
486
|
* params is null when expanding an imported ValueSet
|
|
478
487
|
*/
|
|
488
|
+
/** The circular reference key seeValueSet tracks for a value set */
|
|
489
|
+
valueSetContextKey(vs) {
|
|
490
|
+
return vs.contextKey !== undefined ? vs.contextKey : (vs.url ? (vs.url + (vs.version ? '|' + vs.version : '')) : null);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Finished processing a value set: see OperationContext.unseeContext */
|
|
494
|
+
unseeValueSet(vs) {
|
|
495
|
+
const key = this.valueSetContextKey(vs);
|
|
496
|
+
if (key) {
|
|
497
|
+
this.opContext.unseeContext(key);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
479
501
|
seeValueSet(vs, params) {
|
|
480
502
|
// Build canonical URL from url and version
|
|
481
|
-
const
|
|
482
|
-
if (
|
|
483
|
-
this.opContext.seeContext(
|
|
503
|
+
const key = this.valueSetContextKey(vs);
|
|
504
|
+
if (key) {
|
|
505
|
+
this.opContext.seeContext(key);
|
|
484
506
|
}
|
|
485
507
|
// Check for expansion parameter extensions on compose
|
|
486
508
|
if (vs.jsonObj.compose && vs.jsonObj.compose.extension) {
|
|
@@ -1100,42 +1122,9 @@ function Unknown_Code_in_VersionSCT(url, version) {
|
|
|
1100
1122
|
function SCTVersion(url, ver) {
|
|
1101
1123
|
if (url !== 'http://snomed.info/sct' || !ver) {
|
|
1102
1124
|
return '';
|
|
1103
|
-
} else {
|
|
1104
|
-
let result = 'unknown';
|
|
1105
|
-
let s = ver.split('/');
|
|
1106
|
-
if (s.length >= 5) {
|
|
1107
|
-
if (s[4] === '900000000000207008') result = 'International Edition';
|
|
1108
|
-
else if (s[4] === '449081005') result = 'International Spanish Edition';
|
|
1109
|
-
else if (s[4] === '11000221109') result = 'Argentinian Edition';
|
|
1110
|
-
else if (s[4] === '32506021000036107') result = 'Australian Edition (with drug extension)';
|
|
1111
|
-
else if (s[4] === '11000234105') result = 'Austrian Edition';
|
|
1112
|
-
else if (s[4] === '11000172109') result = 'Belgian Edition';
|
|
1113
|
-
else if (s[4] === '20621000087109') result = 'Canadian English Edition';
|
|
1114
|
-
else if (s[4] === '20611000087101') result = 'Canadian Canadian French Edition';
|
|
1115
|
-
else if (s[4] === '11000279109') result = 'Czech Edition';
|
|
1116
|
-
else if (s[4] === '554471000005108') result = 'Danish Edition';
|
|
1117
|
-
else if (s[4] === '11000181102') result = 'Estonian Edition';
|
|
1118
|
-
else if (s[4] === '11000229106') result = 'Finnish Edition';
|
|
1119
|
-
else if (s[4] === '11000274103') result = 'German Edition';
|
|
1120
|
-
else if (s[4] === '1121000189102') result = 'Indian Edition';
|
|
1121
|
-
else if (s[4] === '827022005') result = 'IPS Terminology';
|
|
1122
|
-
else if (s[4] === '11000220105') result = 'Irish Edition';
|
|
1123
|
-
else if (s[4] === '11000146104') result = 'Netherlands Edition';
|
|
1124
|
-
else if (s[4] === '21000210109') result = 'New Zealand Edition';
|
|
1125
|
-
else if (s[4] === '51000202101') result = 'Norwegian Edition';
|
|
1126
|
-
else if (s[4] === '11000267109') result = 'Republic of Korea Edition (South Korea)';
|
|
1127
|
-
else if (s[4] === '900000001000122104') result = 'Spanish National Edition';
|
|
1128
|
-
else if (s[4] === '45991000052106') result = 'Swedish Edition';
|
|
1129
|
-
else if (s[4] === '2011000195101') result = 'Swiss Edition';
|
|
1130
|
-
else if (s[4] === '83821000000107') result = 'UK Edition';
|
|
1131
|
-
else if (s[4] === '999000021000000109') result = 'UK Clinical Edition';
|
|
1132
|
-
else if (s[4] === '5631000179106') result = 'Uruguay Edition';
|
|
1133
|
-
else if (s[4] === '21000325107') result = 'Chilean Edition';
|
|
1134
|
-
else if (s[4] === '731000124108') result = 'US Edition';
|
|
1135
|
-
else if (s[4] === '5991000124107') result = 'US Edition (with ICD-10-CM maps)';
|
|
1136
|
-
}
|
|
1137
|
-
return result;
|
|
1138
1125
|
}
|
|
1126
|
+
const s = ver.split('/');
|
|
1127
|
+
return (s.length >= 5 && editionName(s[4])) || 'unknown';
|
|
1139
1128
|
}
|
|
1140
1129
|
|
|
1141
1130
|
module.exports = {
|