fhirsmith 0.12.0 → 0.13.1
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 +323 -4
- package/README.md +1 -1
- package/config-template.json +13 -0
- package/library/languages.js +191 -34
- package/library/path-safety.js +51 -0
- package/library/request-token.js +56 -0
- package/package.json +2 -2
- package/packages/packages.js +17 -118
- package/publisher/publisher.js +60 -30
- package/publisher/validation.js +15 -3
- package/server.js +12 -3
- package/translations/Messages.properties +1 -0
- package/tx/README.md +21 -0
- package/tx/cs/cs-api.js +30 -1
- package/tx/cs/cs-cs.js +14 -1
- package/tx/cs/cs-icd11.js +844 -0
- package/tx/cs/cs-lang.js +331 -85
- package/tx/cs/cs-loinc.js +45 -4
- package/tx/cs/cs-mimetypes.js +492 -21
- package/tx/cs/cs-snomed.js +38 -4
- package/tx/cs/cs-ucum.js +33 -3
- package/tx/cs/icd11-expressions.js +243 -0
- package/tx/importers/export-icd11-codesystem.js +823 -0
- package/tx/importers/icd11-schema.md +285 -0
- package/tx/importers/import-icd11.js +861 -0
- package/tx/importers/import-loinc.module.js +14 -7
- package/tx/importers/import-sct.module.js +10 -1
- package/tx/library/codesystem.js +21 -27
- package/tx/library/nesting-limit.js +79 -0
- package/tx/library/operation-outcome.js +97 -3
- package/tx/library/valueset.js +16 -20
- package/tx/library.js +23 -1
- package/tx/ocl/cache/cache-utils.cjs +5 -3
- package/tx/ocl/cs-ocl.cjs +73 -17
- package/tx/ocl/jobs/background-queue.cjs +6 -3
- package/tx/ocl/mappers/concept-mapper.cjs +9 -1
- package/tx/ocl/shared/patches.cjs +48 -1
- package/tx/ocl/vs-ocl.cjs +15 -13
- package/tx/provider.js +1 -1
- package/tx/sct/expressions.js +194 -17
- package/tx/tx.fhir.org.yml +2 -1
- package/tx/tx.js +15 -42
- package/tx/workers/batch-validate.js +6 -16
- package/tx/workers/batch.js +16 -28
- package/tx/workers/cache-control.js +6 -12
- package/tx/workers/closure.js +3 -8
- package/tx/workers/compare.js +6 -34
- package/tx/workers/expand.js +36 -45
- package/tx/workers/lookup.js +21 -39
- package/tx/workers/read.js +6 -40
- package/tx/workers/search.js +2 -8
- package/tx/workers/subsumes.js +58 -41
- package/tx/workers/translate.js +11 -17
- package/tx/workers/validate.js +76 -77
- package/tx/workers/worker.js +70 -1
- package/tx/xml/xml-base.js +243 -76
- package/tx/xversion/xv-valueset.js +39 -1
package/tx/sct/expressions.js
CHANGED
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
* Licensed under BSD-3-Clause
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const { SnomedConceptList } = require('./structures');
|
|
10
|
+
|
|
9
11
|
const MAX_TERM_LENGTH = 1024;
|
|
12
|
+
// How deep concept-definition expansion may go while building a long normal form.
|
|
13
|
+
// Attribute values are themselves expanded, so a chain of definitions can descend a
|
|
14
|
+
// long way; in real data it is a handful of levels.
|
|
15
|
+
const MAX_NORMALISATION_DEPTH = 50;
|
|
16
|
+
|
|
10
17
|
const NO_REFERENCE = 0xFFFFFFFF;
|
|
11
18
|
|
|
12
19
|
// MRCM (machine readable concept model) reference sets and metadata concepts.
|
|
@@ -593,6 +600,15 @@ class SnomedExpressionParser {
|
|
|
593
600
|
|
|
594
601
|
this.ws();
|
|
595
602
|
result.code = this.conceptId();
|
|
603
|
+
// Resolve the attribute name the same way concept() resolves a focus concept. Without this
|
|
604
|
+
// the name keeps NO_REFERENCE, and SnomedConcept.matches() then compares by reference on one
|
|
605
|
+
// side and by code on the other, which makes refinement matching (and so subsumption between
|
|
606
|
+
// two refined expressions) asymmetric.
|
|
607
|
+
if (this.conceptList) {
|
|
608
|
+
const found = this.conceptList.findConcept(result.code);
|
|
609
|
+
this.rule(found.found, 'Concept "' + result.code + '" not valid');
|
|
610
|
+
result.reference = found.index;
|
|
611
|
+
}
|
|
596
612
|
this.ws();
|
|
597
613
|
|
|
598
614
|
if (this.gchar('|')) {
|
|
@@ -1035,7 +1051,12 @@ class SnomedExpressionServices {
|
|
|
1035
1051
|
}
|
|
1036
1052
|
|
|
1037
1053
|
const result = new SnomedExpressionParser().parse(source);
|
|
1038
|
-
|
|
1054
|
+
// Resolve concept ids to references, but do NOT run checkExpression: the MRCM rules it
|
|
1055
|
+
// applies are the postcoordination ones, and this is a concept's own stored definition -
|
|
1056
|
+
// precoordinated content, authored under MRCM content types that do not apply here. Running
|
|
1057
|
+
// them rejects the normal form of any concept using an attribute that is precoordinated-only
|
|
1058
|
+
// (e.g. 246075003 |Causative agent| on a disorder), which broke subsumption for expressions.
|
|
1059
|
+
this.resolveExpressionReferences(result);
|
|
1039
1060
|
return result;
|
|
1040
1061
|
}
|
|
1041
1062
|
}
|
|
@@ -1202,13 +1223,46 @@ class SnomedExpressionServices {
|
|
|
1202
1223
|
/**
|
|
1203
1224
|
* Normalize expression to normal form
|
|
1204
1225
|
*/
|
|
1205
|
-
|
|
1226
|
+
/**
|
|
1227
|
+
* Expand an expression to its long normal form, ready for structural comparison.
|
|
1228
|
+
*
|
|
1229
|
+
* A fully defined concept is replaced by its definition, because that definition is
|
|
1230
|
+
* both necessary and sufficient. A primitive concept is NOT replaced - its definition
|
|
1231
|
+
* is only necessary, and substituting it would make the expression look equivalent to
|
|
1232
|
+
* things it merely subsumes. But the primitive's defining attributes are still
|
|
1233
|
+
* entailed by it, so they are added alongside the concept rather than discarded:
|
|
1234
|
+
* `4846001 |Anicteric viral hepatitis|` is primitive and carries an inferred
|
|
1235
|
+
* `363698007 |Finding site| = 10200004 |Liver structure|`, so every instance of it
|
|
1236
|
+
* really is a disease with that finding site, and `64572001:{363698007=10200004}`
|
|
1237
|
+
* really does subsume it. Dropping those attributes - which is what this used to do -
|
|
1238
|
+
* lost that answer and reported not-subsumed, which is simply wrong.
|
|
1239
|
+
*
|
|
1240
|
+
* Keeping the concept itself in the focus is what stops the addition being unsound in
|
|
1241
|
+
* the other direction: the primitive stays as an atom that nothing else can match, so
|
|
1242
|
+
* an expression carrying the same attributes is never reported as equivalent to it.
|
|
1243
|
+
*
|
|
1244
|
+
* @param {SnomedExpression} exp
|
|
1245
|
+
* @param {number} [depth] - recursion guard, see MAX_NORMALISATION_DEPTH
|
|
1246
|
+
* @returns {SnomedExpression}
|
|
1247
|
+
*/
|
|
1248
|
+
normaliseExpression(exp, depth = 0) {
|
|
1249
|
+
if (depth > MAX_NORMALISATION_DEPTH) {
|
|
1250
|
+
// Expanding attribute values can descend a long way, and truncating silently is
|
|
1251
|
+
// not safe in both directions: dropping conditions from the subsuming side makes
|
|
1252
|
+
// it demand less and so subsume more, which would be a false positive. Fail
|
|
1253
|
+
// instead, and let the caller report that it could not answer.
|
|
1254
|
+
throw new Error(`Could not normalise the expression: it nests more than ${MAX_NORMALISATION_DEPTH}`
|
|
1255
|
+
+ ' levels deep once concept definitions are expanded');
|
|
1256
|
+
}
|
|
1206
1257
|
const work = new SnomedExpression();
|
|
1207
1258
|
|
|
1208
1259
|
// Process concepts
|
|
1209
1260
|
for (const concept of exp.concepts) {
|
|
1210
|
-
if (concept.reference === NO_REFERENCE
|
|
1261
|
+
if (concept.reference === NO_REFERENCE) {
|
|
1262
|
+
work.concepts.push(concept);
|
|
1263
|
+
} else if (this.isPrimitive(concept.reference)) {
|
|
1211
1264
|
work.concepts.push(concept);
|
|
1265
|
+
this.addDefiningAttributes(concept.reference, work, depth);
|
|
1212
1266
|
} else {
|
|
1213
1267
|
const ex = this.createNormalForm(concept.reference);
|
|
1214
1268
|
work.merge(ex);
|
|
@@ -1220,7 +1274,7 @@ class SnomedExpressionServices {
|
|
|
1220
1274
|
const refDst = new SnomedRefinement();
|
|
1221
1275
|
work.refinements.push(refDst);
|
|
1222
1276
|
refDst.name = refSrc.name;
|
|
1223
|
-
refDst.value = this.normaliseExpression(refSrc.value);
|
|
1277
|
+
refDst.value = this.normaliseExpression(refSrc.value, depth + 1);
|
|
1224
1278
|
}
|
|
1225
1279
|
|
|
1226
1280
|
// Process refinement groups
|
|
@@ -1232,7 +1286,7 @@ class SnomedExpressionServices {
|
|
|
1232
1286
|
const refDst = new SnomedRefinement();
|
|
1233
1287
|
grpDst.refinements.push(refDst);
|
|
1234
1288
|
refDst.name = refSrc.name;
|
|
1235
|
-
refDst.value = this.normaliseExpression(refSrc.value);
|
|
1289
|
+
refDst.value = this.normaliseExpression(refSrc.value, depth + 1);
|
|
1236
1290
|
}
|
|
1237
1291
|
}
|
|
1238
1292
|
|
|
@@ -1241,6 +1295,56 @@ class SnomedExpressionServices {
|
|
|
1241
1295
|
return work2.canonical();
|
|
1242
1296
|
}
|
|
1243
1297
|
|
|
1298
|
+
/**
|
|
1299
|
+
* Add a concept's own defining (non is-a) relationships to an expression, as grouped
|
|
1300
|
+
* and ungrouped refinements with their values normalised.
|
|
1301
|
+
*
|
|
1302
|
+
* Used for primitives, whose attributes are entailed but whose concept must stay in
|
|
1303
|
+
* the focus - see normaliseExpression. The stored normal form cannot be used for this:
|
|
1304
|
+
* it is built by createDefinedExpression, which stops at a primitive too, so the
|
|
1305
|
+
* cached normal form of a primitive is just the concept itself. Reading the
|
|
1306
|
+
* relationships here means existing caches keep working rather than needing a rebuild.
|
|
1307
|
+
*
|
|
1308
|
+
* @param {number} reference - the concept whose attributes to add
|
|
1309
|
+
* @param {SnomedExpression} work - expression to add them to
|
|
1310
|
+
* @param {number} depth - current normalisation depth
|
|
1311
|
+
*/
|
|
1312
|
+
addDefiningAttributes(reference, work, depth) {
|
|
1313
|
+
const groups = new Map();
|
|
1314
|
+
|
|
1315
|
+
for (const relIndex of this.getDefiningRelationships(reference)) {
|
|
1316
|
+
const rel = this.relationships.getRelationship(relIndex);
|
|
1317
|
+
|
|
1318
|
+
const ref = new SnomedRefinement();
|
|
1319
|
+
ref.name = new SnomedConcept(rel.relType);
|
|
1320
|
+
ref.name.code = this.getConceptId(rel.relType);
|
|
1321
|
+
|
|
1322
|
+
const value = new SnomedExpression();
|
|
1323
|
+
const target = new SnomedConcept(rel.target);
|
|
1324
|
+
target.code = this.getConceptId(rel.target);
|
|
1325
|
+
value.concepts.push(target);
|
|
1326
|
+
ref.value = this.normaliseExpression(value, depth + 1);
|
|
1327
|
+
|
|
1328
|
+
if (rel.group === 0) {
|
|
1329
|
+
if (!work.hasRefinement(ref)) {
|
|
1330
|
+
work.refinements.push(ref);
|
|
1331
|
+
}
|
|
1332
|
+
} else {
|
|
1333
|
+
const key = rel.group.toString();
|
|
1334
|
+
if (!groups.has(key)) {
|
|
1335
|
+
groups.set(key, new SnomedRefinementGroup());
|
|
1336
|
+
}
|
|
1337
|
+
groups.get(key).refinements.push(ref);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
for (const grp of groups.values()) {
|
|
1342
|
+
if (!work.hasRefinementGroup(grp)) {
|
|
1343
|
+
work.refinementGroups.push(grp);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1244
1348
|
/**
|
|
1245
1349
|
* Check if expression a subsumes expression b
|
|
1246
1350
|
*/
|
|
@@ -1266,10 +1370,20 @@ class SnomedExpressionServices {
|
|
|
1266
1370
|
}
|
|
1267
1371
|
}
|
|
1268
1372
|
|
|
1269
|
-
// Check refinement groups
|
|
1373
|
+
// Check refinement groups. Every group in e1 has to be satisfied by SOME group in e2 - try
|
|
1374
|
+
// them all rather than only the first one whose attribute names line up. Two normalisations
|
|
1375
|
+
// of the same expression can put equivalent groups in a different order, and when several
|
|
1376
|
+
// groups share the same attribute names (e.g. two morphology/site groups on one disorder)
|
|
1377
|
+
// picking the first match and giving up made subsumption depend on that order.
|
|
1270
1378
|
for (const r of e1.refinementGroups) {
|
|
1271
|
-
|
|
1272
|
-
|
|
1379
|
+
let ok = false;
|
|
1380
|
+
for (const rt of e2.refinementGroups) {
|
|
1381
|
+
if (this.subsumesGroup(r, rt)) {
|
|
1382
|
+
ok = true;
|
|
1383
|
+
break;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
if (!ok) {
|
|
1273
1387
|
return false;
|
|
1274
1388
|
}
|
|
1275
1389
|
}
|
|
@@ -1454,6 +1568,28 @@ class SnomedExpressionServices {
|
|
|
1454
1568
|
return null;
|
|
1455
1569
|
}
|
|
1456
1570
|
|
|
1571
|
+
/**
|
|
1572
|
+
* Resolve every concept id in an expression to its internal reference, without applying any
|
|
1573
|
+
* of the concept model rules. Used for expressions the server generated itself (normal forms),
|
|
1574
|
+
* where the ids are known good and the MRCM postcoordination rules do not apply
|
|
1575
|
+
* @param {SnomedExpression} expression
|
|
1576
|
+
*/
|
|
1577
|
+
resolveExpressionReferences(expression) {
|
|
1578
|
+
for (const concept of expression.concepts) {
|
|
1579
|
+
this.checkConcept(concept);
|
|
1580
|
+
}
|
|
1581
|
+
for (const refinement of expression.refinements) {
|
|
1582
|
+
this.checkConcept(refinement.name);
|
|
1583
|
+
this.resolveExpressionReferences(refinement.value);
|
|
1584
|
+
}
|
|
1585
|
+
for (const group of expression.refinementGroups) {
|
|
1586
|
+
for (const refinement of group.refinements) {
|
|
1587
|
+
this.checkConcept(refinement.name);
|
|
1588
|
+
this.resolveExpressionReferences(refinement.value);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1457
1593
|
/**
|
|
1458
1594
|
* Validate expression structure and concept references
|
|
1459
1595
|
*/
|
|
@@ -1467,6 +1603,7 @@ class SnomedExpressionServices {
|
|
|
1467
1603
|
this.checkRefinement(refinement);
|
|
1468
1604
|
this.checkRefinementDomain(expression, refinement);
|
|
1469
1605
|
this.checkRefinementRange(refinement);
|
|
1606
|
+
this.checkRefinementGrouped(refinement);
|
|
1470
1607
|
}
|
|
1471
1608
|
}
|
|
1472
1609
|
|
|
@@ -1493,7 +1630,8 @@ class SnomedExpressionServices {
|
|
|
1493
1630
|
* applied to a structure that cannot be lateralized, so it is enforced.
|
|
1494
1631
|
*
|
|
1495
1632
|
* Attributes with no rule in this edition's MRCM are not checked, and neither
|
|
1496
|
-
*
|
|
1633
|
+
* is cardinality (checkExpressionCardinality), grouping (checkRefinementGrouped)
|
|
1634
|
+
* or the range constraint (checkRefinementRange, which needs ECL).
|
|
1497
1635
|
*
|
|
1498
1636
|
* @param {SnomedExpression} expression - the expression the refinement is on
|
|
1499
1637
|
* @param {SnomedRefinement} refinement
|
|
@@ -1543,12 +1681,51 @@ class SnomedExpressionServices {
|
|
|
1543
1681
|
}
|
|
1544
1682
|
}
|
|
1545
1683
|
|
|
1684
|
+
/**
|
|
1685
|
+
* Check that an attribute the MRCM marks as grouped is actually in a relationship
|
|
1686
|
+
* group. This is only ever called for refinements that are NOT in a group.
|
|
1687
|
+
*
|
|
1688
|
+
* The MRCM attribute domain reference set carries a `grouped` flag per rule, and it
|
|
1689
|
+
* is a real constraint rather than a stylistic preference: `363698007 |Finding site|`
|
|
1690
|
+
* is grouped, so `40468003:363698007=10200004` is not a valid expression even though
|
|
1691
|
+
* every part of it is - it has to be written `40468003:{363698007=10200004}`. The
|
|
1692
|
+
* distinction matters because a group is what ties an attribute to the others that
|
|
1693
|
+
* apply with it, and an expression that omits the braces has not said which.
|
|
1694
|
+
*
|
|
1695
|
+
* SNOMED's postcoordination guide calls the ungrouped form Close To User Form and
|
|
1696
|
+
* defines a transformation ("Adding a Self-grouped Attribute") that converts it to
|
|
1697
|
+
* the Classifiable Form. That transformation is not implemented here, so the
|
|
1698
|
+
* ungrouped form is rejected rather than silently repaired: telling the caller their
|
|
1699
|
+
* expression is not classifiable is honest, quietly guessing what they meant is not.
|
|
1700
|
+
*
|
|
1701
|
+
* As elsewhere in this check, an attribute with no rule in this edition's MRCM is
|
|
1702
|
+
* left alone, and where several rules apply the strictest wins.
|
|
1703
|
+
*
|
|
1704
|
+
* @param {SnomedRefinement} refinement - a refinement outside any group
|
|
1705
|
+
*/
|
|
1706
|
+
checkRefinementGrouped(refinement) {
|
|
1707
|
+
const attribute = refinement.name ? refinement.name.reference : NO_REFERENCE;
|
|
1708
|
+
if (attribute === undefined || attribute === null || attribute === NO_REFERENCE) {
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
const rules = this.mrcmAttributeDomains().get(attribute);
|
|
1712
|
+
if (!rules || rules.length === 0) {
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
if (!rules.some(rule => rule.grouped)) {
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
const attrDesc = this.describeConceptForMessage(attribute);
|
|
1719
|
+
throw new Error(`The SNOMED CT concept model requires the attribute ${attrDesc} to be in a`
|
|
1720
|
+
+ ` relationship group, so it cannot be used ungrouped (write it as {${attrDesc} = ...})`);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1546
1723
|
/**
|
|
1547
1724
|
* The MRCM attribute domain reference set, indexed by attribute concept.
|
|
1548
1725
|
* Built once per edition and cached; an edition with no MRCM yields an empty
|
|
1549
1726
|
* map, which turns the domain check off rather than failing.
|
|
1550
1727
|
*
|
|
1551
|
-
* @returns {Map<number, Array<{domain: number, isRefset: boolean}>>}
|
|
1728
|
+
* @returns {Map<number, Array<{domain: number, isRefset: boolean, grouped: boolean, max: number, maxInGroup: number}>>}
|
|
1552
1729
|
*/
|
|
1553
1730
|
mrcmAttributeDomains() {
|
|
1554
1731
|
if (this._mrcmAttributeDomains) {
|
|
@@ -1602,6 +1779,9 @@ class SnomedExpressionServices {
|
|
|
1602
1779
|
list.push({
|
|
1603
1780
|
domain: domain,
|
|
1604
1781
|
isRefset: refSetConcepts.has(domain),
|
|
1782
|
+
// The grouped column is a plain 0/1 (type 4), not a reference into the string
|
|
1783
|
+
// table like the cardinalities beside it - reading it as one yields garbage.
|
|
1784
|
+
grouped: values[2] === 1,
|
|
1605
1785
|
// Only the maximums are enforced. A minimum ("1..1") says a concept in
|
|
1606
1786
|
// this domain must have the attribute in its own definition; it does not
|
|
1607
1787
|
// say a refinement of that concept has to restate it.
|
|
@@ -2160,9 +2340,7 @@ class SnomedExpressionServices {
|
|
|
2160
2340
|
// In SNOMED CT, primitive concepts are not fully defined by their relationships
|
|
2161
2341
|
try {
|
|
2162
2342
|
const concept = this.concepts.getConcept(reference);
|
|
2163
|
-
|
|
2164
|
-
// This may vary based on the specific implementation
|
|
2165
|
-
return (concept.flags & 1) !== 0;
|
|
2343
|
+
return (concept.flags & SnomedConceptList.MASK_CONCEPT_PRIMITIVE) !== 0;
|
|
2166
2344
|
} catch (error) {
|
|
2167
2345
|
// If we can't read the concept, assume it's primitive for safety
|
|
2168
2346
|
this.log.warn(`Warning: Could not check primitive status for concept ${reference}: ${error.message}`);
|
|
@@ -2487,10 +2665,9 @@ class SnomedExpressionServicesExtended extends SnomedExpressionServices {
|
|
|
2487
2665
|
isPrimitive(reference) {
|
|
2488
2666
|
try {
|
|
2489
2667
|
const concept = this.concepts.getConcept(reference);
|
|
2490
|
-
//
|
|
2491
|
-
//
|
|
2492
|
-
|
|
2493
|
-
return (concept.flags & 1) !== 0;
|
|
2668
|
+
// definitionStatusId 900000000000074008 (primitive) is recorded at import time as
|
|
2669
|
+
// MASK_CONCEPT_PRIMITIVE in the concept flags byte
|
|
2670
|
+
return (concept.flags & SnomedConceptList.MASK_CONCEPT_PRIMITIVE) !== 0;
|
|
2494
2671
|
} catch (error) {
|
|
2495
2672
|
// If we can't read the concept, assume it's primitive for safety
|
|
2496
2673
|
if (this.building) {
|
package/tx/tx.fhir.org.yml
CHANGED
|
@@ -26,7 +26,7 @@ sources:
|
|
|
26
26
|
- snomed:sct_us_20230301.cache
|
|
27
27
|
- snomed:sct_us_20240301.cache
|
|
28
28
|
- snomed:sct_us_20250901.cache
|
|
29
|
-
- snomed:
|
|
29
|
+
- snomed:sct_test_20250909b.cache
|
|
30
30
|
- cpt:CodeSystem-cpt.db|cpt-2023-fragment-0.1.db
|
|
31
31
|
- omop:omop_v20250227.db
|
|
32
32
|
- npm:hl7.terminology
|
|
@@ -37,6 +37,7 @@ sources:
|
|
|
37
37
|
- npm:us.cdc.phinvads
|
|
38
38
|
- npm:hl7.fhir.uv.sdc
|
|
39
39
|
- internal:vsac
|
|
40
|
+
- icd11:icd11-2026-01.db
|
|
40
41
|
|
|
41
42
|
ignored:
|
|
42
43
|
- urn:iso:std:iso:3166#20210120
|
package/tx/tx.js
CHANGED
|
@@ -51,6 +51,7 @@ const ClosureWorker = require("./workers/closure");
|
|
|
51
51
|
const {BundleXML} = require("./xml/bundle-xml");
|
|
52
52
|
const ConceptUsageTracker = require("./usage-tracker");
|
|
53
53
|
const ProblemFinder = require("./problems");
|
|
54
|
+
const { buildOperationOutcome } = require('./library/operation-outcome');
|
|
54
55
|
// const {writeFileSync} = require("fs");
|
|
55
56
|
|
|
56
57
|
class TXModule {
|
|
@@ -437,14 +438,7 @@ class TXModule {
|
|
|
437
438
|
}
|
|
438
439
|
} catch (e) {
|
|
439
440
|
this.log.error(`JSON parse error: ${e.message}`);
|
|
440
|
-
return res.status(400).json({
|
|
441
|
-
resourceType: 'OperationOutcome',
|
|
442
|
-
issue: [{
|
|
443
|
-
severity: 'error',
|
|
444
|
-
code: 'invalid',
|
|
445
|
-
diagnostics: `Invalid JSON: ${e.message}`
|
|
446
|
-
}]
|
|
447
|
-
});
|
|
441
|
+
return res.status(400).json(buildOperationOutcome('error', 'invalid', `Invalid JSON: ${e.message}`));
|
|
448
442
|
}
|
|
449
443
|
}
|
|
450
444
|
|
|
@@ -465,25 +459,11 @@ class TXModule {
|
|
|
465
459
|
req.body = this.convertXmlToResource(xmlStr);
|
|
466
460
|
} catch (e) {
|
|
467
461
|
this.log.error(`XML parse error: ${e.message}`);
|
|
468
|
-
return res.status(400).json({
|
|
469
|
-
resourceType: 'OperationOutcome',
|
|
470
|
-
issue: [{
|
|
471
|
-
severity: 'error',
|
|
472
|
-
code: 'invalid',
|
|
473
|
-
diagnostics: `Invalid XML: ${e.message}`
|
|
474
|
-
}]
|
|
475
|
-
});
|
|
462
|
+
return res.status(400).json(buildOperationOutcome('error', 'invalid', `Invalid XML: ${e.message}`));
|
|
476
463
|
}
|
|
477
464
|
}
|
|
478
465
|
} else if (contentType != 'application/x-www-form-urlencoded') {
|
|
479
|
-
return res.status(415).json({
|
|
480
|
-
resourceType: 'OperationOutcome',
|
|
481
|
-
issue: [{
|
|
482
|
-
severity: 'error',
|
|
483
|
-
code: 'invalid',
|
|
484
|
-
diagnostics: `Unsupported Media Type: ${contentType}`
|
|
485
|
-
}]
|
|
486
|
-
});
|
|
466
|
+
return res.status(415).json(buildOperationOutcome('error', 'invalid', `Unsupported Media Type: ${contentType}`));
|
|
487
467
|
}
|
|
488
468
|
|
|
489
469
|
if (req.body) {
|
|
@@ -1010,14 +990,7 @@ class TXModule {
|
|
|
1010
990
|
router.get('/', async (req, res) => {
|
|
1011
991
|
const start = Date.now();
|
|
1012
992
|
try {
|
|
1013
|
-
await res.json({
|
|
1014
|
-
resourceType: 'OperationOutcome',
|
|
1015
|
-
issue: [{
|
|
1016
|
-
severity: 'information',
|
|
1017
|
-
code: 'informational',
|
|
1018
|
-
diagnostics: `FHIR Terminology Server - FHIR v${req.txEndpoint.fhirVersion}`
|
|
1019
|
-
}]
|
|
1020
|
-
});
|
|
993
|
+
await res.json(buildOperationOutcome('information', 'informational', `FHIR Terminology Server - FHIR v${req.txEndpoint.fhirVersion}`));
|
|
1021
994
|
} finally {
|
|
1022
995
|
this.countRequest(endpointPath, 'home', Date.now() - start);
|
|
1023
996
|
}
|
|
@@ -1125,17 +1098,17 @@ class TXModule {
|
|
|
1125
1098
|
}
|
|
1126
1099
|
|
|
1127
1100
|
/**
|
|
1128
|
-
* Build an OperationOutcome
|
|
1101
|
+
* Build an OperationOutcome. Delegates to the shared builder so that every outcome this
|
|
1102
|
+
* server emits has details.text and a tx-issue-type coding: diagnostics is stripped by
|
|
1103
|
+
* the test harness, so nothing a caller needs may live there.
|
|
1104
|
+
* @param {string} severity - error, warning, information
|
|
1105
|
+
* @param {string} code - FHIR issue type
|
|
1106
|
+
* @param {string} message - the human readable account of the problem
|
|
1107
|
+
* @param {string} [txIssueType] - tx-issue-type; defaulted from code when not given
|
|
1108
|
+
* @returns {Object} OperationOutcome resource
|
|
1129
1109
|
*/
|
|
1130
|
-
operationOutcome(severity, code, message) {
|
|
1131
|
-
return
|
|
1132
|
-
resourceType: 'OperationOutcome',
|
|
1133
|
-
issue: [{
|
|
1134
|
-
severity,
|
|
1135
|
-
code,
|
|
1136
|
-
diagnostics: message
|
|
1137
|
-
}]
|
|
1138
|
-
};
|
|
1110
|
+
operationOutcome(severity, code, message, txIssueType = null) {
|
|
1111
|
+
return buildOperationOutcome(severity, code, message, txIssueType);
|
|
1139
1112
|
}
|
|
1140
1113
|
|
|
1141
1114
|
/**
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
|
|
15
15
|
const { TerminologyWorker } = require('./worker');
|
|
16
|
-
const {OperationOutcome, Issue} = require(
|
|
16
|
+
const { OperationOutcome, Issue, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
|
|
17
17
|
const {Parameters} = require("../library/parameters");
|
|
18
18
|
const {ValidateWorker} = require("./validate");
|
|
19
19
|
const {debugLog} = require("../operation-context");
|
|
@@ -137,7 +137,7 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
137
137
|
op.addIssue(error);
|
|
138
138
|
output.push({name: "validation", resource : op.jsonObj});
|
|
139
139
|
} else {
|
|
140
|
-
output.push({name: "validation", resource :
|
|
140
|
+
output.push({name: "validation", resource : outcomeFromError(error) } );
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
}
|
|
@@ -156,8 +156,7 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
156
156
|
oo.addIssue(error);
|
|
157
157
|
return res.status(error.statusCode || 500).json(oo.jsonObj);
|
|
158
158
|
}
|
|
159
|
-
return res.status(error.statusCode || 500).json(
|
|
160
|
-
'error', error.issueCode || 'exception', error.message));
|
|
159
|
+
return res.status(error.statusCode || 500).json(outcomeFromError(error));
|
|
161
160
|
}
|
|
162
161
|
}
|
|
163
162
|
|
|
@@ -218,18 +217,9 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
218
217
|
/**
|
|
219
218
|
* Build an OperationOutcome
|
|
220
219
|
*/
|
|
221
|
-
operationOutcome(severity, code, message) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
issue: [{
|
|
225
|
-
severity,
|
|
226
|
-
code,
|
|
227
|
-
details: {
|
|
228
|
-
text: message
|
|
229
|
-
},
|
|
230
|
-
diagnostics: message
|
|
231
|
-
}]
|
|
232
|
-
};
|
|
220
|
+
operationOutcome(severity, code, message, txIssueType = null) {
|
|
221
|
+
// the shared builder, so that every outcome has details.text and a tx-issue-type coding
|
|
222
|
+
return buildOperationOutcome(severity, code, message, txIssueType);
|
|
233
223
|
}
|
|
234
224
|
|
|
235
225
|
hasValueSet(parameter) {
|
package/tx/workers/batch.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
|
|
7
7
|
const { TerminologyWorker } = require('./worker');
|
|
8
|
-
const { Issue, OperationOutcome } = require('../library/operation-outcome');
|
|
8
|
+
const { Issue, OperationOutcome, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
|
|
9
9
|
const {debugLog} = require("../operation-context");
|
|
10
10
|
|
|
11
11
|
class BatchWorker extends TerminologyWorker {
|
|
@@ -47,8 +47,7 @@ class BatchWorker extends TerminologyWorker {
|
|
|
47
47
|
oo.addIssue(error);
|
|
48
48
|
return res.status(error.statusCode || 500).json(oo.jsonObj);
|
|
49
49
|
} else {
|
|
50
|
-
return res.status(error.statusCode || 500).json(
|
|
51
|
-
'error', error.issueCode || 'exception', error.message));
|
|
50
|
+
return res.status(error.statusCode || 500).json(outcomeFromError(error));
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
}
|
|
@@ -163,9 +162,8 @@ class BatchWorker extends TerminologyWorker {
|
|
|
163
162
|
this.log.error(error);
|
|
164
163
|
debugLog(error);
|
|
165
164
|
const statusCode = error.statusCode || 500;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return this.errorEntry(statusCode, issueCode, error.message);
|
|
165
|
+
// on a plain Error, issueCode is the FHIR issue type and txIssueType the tx-issue-type
|
|
166
|
+
return this.errorEntry(statusCode, error.issueCode || 'exception', error.message, error.txIssueType);
|
|
169
167
|
}
|
|
170
168
|
}
|
|
171
169
|
|
|
@@ -319,19 +317,13 @@ class BatchWorker extends TerminologyWorker {
|
|
|
319
317
|
/**
|
|
320
318
|
* Build an error response entry
|
|
321
319
|
* @param {number} statusCode - HTTP status code
|
|
322
|
-
* @param {string} issueCode - FHIR issue
|
|
320
|
+
* @param {string} issueCode - FHIR issue type
|
|
323
321
|
* @param {string} message - Error message
|
|
322
|
+
* @param {string} [txIssueType] - tx-issue-type; defaulted from issueCode when not given
|
|
324
323
|
* @returns {Object} Error entry
|
|
325
324
|
*/
|
|
326
|
-
errorEntry(statusCode, issueCode, message) {
|
|
327
|
-
const outcome =
|
|
328
|
-
resourceType: 'OperationOutcome',
|
|
329
|
-
issue: [{
|
|
330
|
-
severity: 'error',
|
|
331
|
-
code: issueCode,
|
|
332
|
-
diagnostics: message
|
|
333
|
-
}]
|
|
334
|
-
};
|
|
325
|
+
errorEntry(statusCode, issueCode, message, txIssueType = null) {
|
|
326
|
+
const outcome = buildOperationOutcome('error', issueCode, message, txIssueType);
|
|
335
327
|
|
|
336
328
|
return {
|
|
337
329
|
resource: outcome,
|
|
@@ -343,21 +335,17 @@ class BatchWorker extends TerminologyWorker {
|
|
|
343
335
|
}
|
|
344
336
|
|
|
345
337
|
/**
|
|
346
|
-
* Build an OperationOutcome
|
|
338
|
+
* Build an OperationOutcome. Delegates to the shared builder so that every outcome this
|
|
339
|
+
* server emits has details.text and a tx-issue-type coding: diagnostics is stripped by
|
|
340
|
+
* the test harness, so nothing a caller needs may live there.
|
|
347
341
|
* @param {string} severity - error, warning, information
|
|
348
|
-
* @param {string} code -
|
|
349
|
-
* @param {string} message -
|
|
342
|
+
* @param {string} code - FHIR issue type
|
|
343
|
+
* @param {string} message - the human readable account of the problem
|
|
344
|
+
* @param {string} [txIssueType] - tx-issue-type; defaulted from code when not given
|
|
350
345
|
* @returns {Object} OperationOutcome resource
|
|
351
346
|
*/
|
|
352
|
-
operationOutcome(severity, code, message) {
|
|
353
|
-
return
|
|
354
|
-
resourceType: 'OperationOutcome',
|
|
355
|
-
issue: [{
|
|
356
|
-
severity,
|
|
357
|
-
code,
|
|
358
|
-
diagnostics: message
|
|
359
|
-
}]
|
|
360
|
-
};
|
|
347
|
+
operationOutcome(severity, code, message, txIssueType = null) {
|
|
348
|
+
return buildOperationOutcome(severity, code, message, txIssueType);
|
|
361
349
|
}
|
|
362
350
|
}
|
|
363
351
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
const crypto = require('crypto');
|
|
17
17
|
const { TerminologyWorker, CACHE_ID_HEADER } = require('./worker');
|
|
18
18
|
const { Parameters } = require('../library/parameters');
|
|
19
|
-
const { Issue } = require('../library/operation-outcome');
|
|
19
|
+
const { Issue, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
|
|
20
20
|
const { debugLog } = require('../operation-context');
|
|
21
21
|
|
|
22
22
|
class CacheControlWorker extends TerminologyWorker {
|
|
@@ -71,8 +71,8 @@ class CacheControlWorker extends TerminologyWorker {
|
|
|
71
71
|
debugLog(error);
|
|
72
72
|
req.logInfo = this.usedSources.join('|') + ' - error' + (error.msgId ? ' ' + error.msgId : '');
|
|
73
73
|
const statusCode = error.statusCode || 500;
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
return res.status(statusCode).json(
|
|
75
|
+
outcomeFromError(error));
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -276,15 +276,9 @@ class CacheControlWorker extends TerminologyWorker {
|
|
|
276
276
|
* @param {string} message - Diagnostic message
|
|
277
277
|
* @returns {Object} OperationOutcome resource
|
|
278
278
|
*/
|
|
279
|
-
operationOutcome(severity, code, message) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
issue: [{
|
|
283
|
-
severity,
|
|
284
|
-
code,
|
|
285
|
-
details: { text: message }
|
|
286
|
-
}]
|
|
287
|
-
};
|
|
279
|
+
operationOutcome(severity, code, message, txIssueType = null) {
|
|
280
|
+
// the shared builder, so that every outcome has details.text and a tx-issue-type coding
|
|
281
|
+
return buildOperationOutcome(severity, code, message, txIssueType);
|
|
288
282
|
}
|
|
289
283
|
}
|
|
290
284
|
|
package/tx/workers/closure.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// POST /ConceptMap/$closure
|
|
6
6
|
//
|
|
7
7
|
|
|
8
|
+
const { buildOperationOutcome } = require('../library/operation-outcome');
|
|
9
|
+
|
|
8
10
|
class ClosureWorker {
|
|
9
11
|
/**
|
|
10
12
|
* Handle a $closure request
|
|
@@ -18,14 +20,7 @@ class ClosureWorker {
|
|
|
18
20
|
this.log.debug('ConceptMap $closure with params:', params);
|
|
19
21
|
|
|
20
22
|
// TODO: Implement closure logic using provider
|
|
21
|
-
res.status(501).json(
|
|
22
|
-
resourceType: 'OperationOutcome',
|
|
23
|
-
issue: [{
|
|
24
|
-
severity: 'error',
|
|
25
|
-
code: 'not-supported',
|
|
26
|
-
diagnostics: 'ConceptMap $closure operation not yet implemented'
|
|
27
|
-
}]
|
|
28
|
-
});
|
|
23
|
+
res.status(501).json(buildOperationOutcome('error', 'not-supported', 'ConceptMap $closure operation not yet implemented'));
|
|
29
24
|
}
|
|
30
25
|
}
|
|
31
26
|
|