fhirsmith 0.9.1 → 0.9.3
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 +30 -0
- package/package.json +1 -1
- package/translations/Messages.properties +3 -1
- package/tx/cs/cs-api.js +2 -1
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +1 -1
- package/tx/cs/cs-cpt.js +1 -1
- package/tx/cs/cs-cs.js +1 -1
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-hgvs.js +1 -1
- package/tx/cs/cs-lang.js +1 -1
- package/tx/cs/cs-loinc.js +1 -1
- package/tx/cs/cs-ndc.js +1 -1
- package/tx/cs/cs-omop.js +1 -1
- package/tx/cs/cs-rxnorm.js +1 -1
- package/tx/cs/cs-snomed.js +603 -22
- package/tx/cs/cs-ucum.js +1 -1
- package/tx/importers/import-sct.module.js +167 -79
- package/tx/library.js +3 -0
- package/tx/sct/ecl.js +98 -49
- package/tx/tx.js +6 -2
- package/tx/vs/vs-database.js +213 -92
- package/tx/vs/vs-vsac.js +151 -55
- package/tx/workers/expand.js +42 -24
- package/tx/workers/related.js +1 -1
- package/tx/workers/validate.js +21 -24
- package/tx/workers/worker.js +16 -1
package/tx/sct/ecl.js
CHANGED
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
* Supports ECL v2.1 specification from SNOMED International
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const { SnomedFilterContext } = require('../cs/cs-snomed');
|
|
11
|
-
|
|
12
10
|
// ECL Token Types
|
|
13
11
|
const ECLTokenType = {
|
|
14
12
|
// Literals
|
|
@@ -18,15 +16,15 @@ const ECLTokenType = {
|
|
|
18
16
|
INTEGER: 'INTEGER',
|
|
19
17
|
DECIMAL: 'DECIMAL',
|
|
20
18
|
|
|
21
|
-
// Operators
|
|
22
|
-
CHILD_OF: 'CHILD_OF', //
|
|
23
|
-
CHILD_OR_SELF_OF: 'CHILD_OR_SELF_OF', //
|
|
24
|
-
DESCENDANT_OF: 'DESCENDANT_OF', //
|
|
25
|
-
DESCENDANT_OR_SELF_OF: 'DESCENDANT_OR_SELF_OF', //
|
|
26
|
-
PARENT_OF: 'PARENT_OF', //
|
|
27
|
-
PARENT_OR_SELF_OF: 'PARENT_OR_SELF_OF', //
|
|
28
|
-
ANCESTOR_OF: 'ANCESTOR_OF', //
|
|
29
|
-
ANCESTOR_OR_SELF_OF: 'ANCESTOR_OR_SELF_OF', //
|
|
19
|
+
// Operators (ECL 2.x spec)
|
|
20
|
+
CHILD_OF: 'CHILD_OF', // <! direct children only
|
|
21
|
+
CHILD_OR_SELF_OF: 'CHILD_OR_SELF_OF', // <<! self + direct children
|
|
22
|
+
DESCENDANT_OF: 'DESCENDANT_OF', // < all transitive descendants, no self
|
|
23
|
+
DESCENDANT_OR_SELF_OF: 'DESCENDANT_OR_SELF_OF', // << self + all transitive descendants
|
|
24
|
+
PARENT_OF: 'PARENT_OF', // >! direct parents only
|
|
25
|
+
PARENT_OR_SELF_OF: 'PARENT_OR_SELF_OF', // >>! self + direct parents
|
|
26
|
+
ANCESTOR_OF: 'ANCESTOR_OF', // > all transitive ancestors, no self
|
|
27
|
+
ANCESTOR_OR_SELF_OF: 'ANCESTOR_OR_SELF_OF', // >> self + all transitive ancestors
|
|
30
28
|
|
|
31
29
|
// Set operators
|
|
32
30
|
AND: 'AND',
|
|
@@ -268,29 +266,38 @@ class ECLLexer {
|
|
|
268
266
|
}
|
|
269
267
|
|
|
270
268
|
// Multi-character operators
|
|
269
|
+
// ECL 2.x hierarchy operators:
|
|
270
|
+
// < descendantOf (transitive, no self)
|
|
271
|
+
// << descendantOrSelfOf (transitive, with self)
|
|
272
|
+
// <! childOf (one step)
|
|
273
|
+
// <<! childOrSelfOf (one step + self)
|
|
274
|
+
// > ancestorOf (transitive, no self)
|
|
275
|
+
// >> ancestorOrSelfOf (transitive, with self)
|
|
276
|
+
// >! parentOf (one step)
|
|
277
|
+
// >>! parentOrSelfOf (one step + self)
|
|
271
278
|
if (this.current === '<') {
|
|
272
279
|
if (this.peek() === '<') {
|
|
273
280
|
if (this.peek(2) === '!') {
|
|
274
281
|
this.advance();
|
|
275
282
|
this.advance();
|
|
276
283
|
this.advance();
|
|
277
|
-
return { type: ECLTokenType.
|
|
284
|
+
return { type: ECLTokenType.CHILD_OR_SELF_OF, value: '<<!' };
|
|
278
285
|
} else {
|
|
279
286
|
this.advance();
|
|
280
287
|
this.advance();
|
|
281
|
-
return { type: ECLTokenType.
|
|
288
|
+
return { type: ECLTokenType.DESCENDANT_OR_SELF_OF, value: '<<' };
|
|
282
289
|
}
|
|
283
290
|
} else if (this.peek() === '!') {
|
|
284
291
|
this.advance();
|
|
285
292
|
this.advance();
|
|
286
|
-
return { type: ECLTokenType.
|
|
293
|
+
return { type: ECLTokenType.CHILD_OF, value: '<!' };
|
|
287
294
|
} else if (this.peek() === '=') {
|
|
288
295
|
this.advance();
|
|
289
296
|
this.advance();
|
|
290
297
|
return { type: ECLTokenType.LTE, value: '<=' };
|
|
291
298
|
} else {
|
|
292
299
|
this.advance();
|
|
293
|
-
return { type: ECLTokenType.
|
|
300
|
+
return { type: ECLTokenType.DESCENDANT_OF, value: '<' };
|
|
294
301
|
}
|
|
295
302
|
}
|
|
296
303
|
|
|
@@ -300,23 +307,23 @@ class ECLLexer {
|
|
|
300
307
|
this.advance();
|
|
301
308
|
this.advance();
|
|
302
309
|
this.advance();
|
|
303
|
-
return { type: ECLTokenType.
|
|
310
|
+
return { type: ECLTokenType.PARENT_OR_SELF_OF, value: '>>!' };
|
|
304
311
|
} else {
|
|
305
312
|
this.advance();
|
|
306
313
|
this.advance();
|
|
307
|
-
return { type: ECLTokenType.
|
|
314
|
+
return { type: ECLTokenType.ANCESTOR_OR_SELF_OF, value: '>>' };
|
|
308
315
|
}
|
|
309
316
|
} else if (this.peek() === '!') {
|
|
310
317
|
this.advance();
|
|
311
318
|
this.advance();
|
|
312
|
-
return { type: ECLTokenType.
|
|
319
|
+
return { type: ECLTokenType.PARENT_OF, value: '>!' };
|
|
313
320
|
} else if (this.peek() === '=') {
|
|
314
321
|
this.advance();
|
|
315
322
|
this.advance();
|
|
316
323
|
return { type: ECLTokenType.GTE, value: '>=' };
|
|
317
324
|
} else {
|
|
318
325
|
this.advance();
|
|
319
|
-
return { type: ECLTokenType.
|
|
326
|
+
return { type: ECLTokenType.ANCESTOR_OF, value: '>' };
|
|
320
327
|
}
|
|
321
328
|
}
|
|
322
329
|
|
|
@@ -350,9 +357,9 @@ class ECLLexer {
|
|
|
350
357
|
|
|
351
358
|
// Check if immediately followed by .digit (decimal number)
|
|
352
359
|
if (pos < this.input.length &&
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
360
|
+
this.input[pos] === '.' &&
|
|
361
|
+
pos + 1 < this.input.length &&
|
|
362
|
+
/\d/.test(this.input[pos + 1])) {
|
|
356
363
|
// This is a decimal number - parse it completely
|
|
357
364
|
const num = this.readNumber();
|
|
358
365
|
return { type: num.type, value: num.value };
|
|
@@ -471,8 +478,8 @@ class ECLParser {
|
|
|
471
478
|
const right = this.parseRefinedExpressionConstraint();
|
|
472
479
|
|
|
473
480
|
const nodeType = operator.type === ECLTokenType.AND ? ECLNodeType.CONJUNCTION :
|
|
474
|
-
|
|
475
|
-
|
|
481
|
+
operator.type === ECLTokenType.OR ? ECLNodeType.DISJUNCTION :
|
|
482
|
+
ECLNodeType.EXCLUSION;
|
|
476
483
|
|
|
477
484
|
left = {
|
|
478
485
|
type: ECLNodeType.COMPOUND_EXPRESSION_CONSTRAINT,
|
|
@@ -527,10 +534,10 @@ class ECLParser {
|
|
|
527
534
|
// Handle constraint operators
|
|
528
535
|
let operator = null;
|
|
529
536
|
if (this.match(
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
537
|
+
ECLTokenType.CHILD_OF, ECLTokenType.CHILD_OR_SELF_OF,
|
|
538
|
+
ECLTokenType.DESCENDANT_OF, ECLTokenType.DESCENDANT_OR_SELF_OF,
|
|
539
|
+
ECLTokenType.PARENT_OF, ECLTokenType.PARENT_OR_SELF_OF,
|
|
540
|
+
ECLTokenType.ANCESTOR_OF, ECLTokenType.ANCESTOR_OR_SELF_OF
|
|
534
541
|
)) {
|
|
535
542
|
operator = this.current;
|
|
536
543
|
this.advance();
|
|
@@ -681,8 +688,11 @@ class ECLParser {
|
|
|
681
688
|
operator: operator.type,
|
|
682
689
|
value
|
|
683
690
|
};
|
|
684
|
-
} else if (this.match(ECLTokenType.LT, ECLTokenType.LTE, ECLTokenType.
|
|
685
|
-
//
|
|
691
|
+
} else if (this.match(ECLTokenType.LT, ECLTokenType.LTE, ECLTokenType.DESCENDANT_OF, ECLTokenType.ANCESTOR_OF, ECLTokenType.GTE)) {
|
|
692
|
+
// In ECL, bare `<` and `>` are overloaded: they lex as hierarchy
|
|
693
|
+
// operators (DESCENDANT_OF / ANCESTOR_OF) but in an attribute comparison
|
|
694
|
+
// context they mean less-than / greater-than. Accept them here and map
|
|
695
|
+
// them to LT / GT so downstream consumers see a uniform shape.
|
|
686
696
|
const operator = this.current;
|
|
687
697
|
this.advance();
|
|
688
698
|
|
|
@@ -697,11 +707,11 @@ class ECLParser {
|
|
|
697
707
|
this.error('Expected numeric value after #');
|
|
698
708
|
}
|
|
699
709
|
|
|
700
|
-
// Map
|
|
710
|
+
// Map DESCENDANT_OF to LT and ANCESTOR_OF to GT for numeric comparisons
|
|
701
711
|
let operatorType = operator.type;
|
|
702
|
-
if (operator.type === ECLTokenType.
|
|
712
|
+
if (operator.type === ECLTokenType.DESCENDANT_OF) {
|
|
703
713
|
operatorType = ECLTokenType.LT;
|
|
704
|
-
} else if (operator.type === ECLTokenType.
|
|
714
|
+
} else if (operator.type === ECLTokenType.ANCESTOR_OF) {
|
|
705
715
|
operatorType = ECLTokenType.GT;
|
|
706
716
|
}
|
|
707
717
|
|
|
@@ -1030,6 +1040,8 @@ class ECLValidator {
|
|
|
1030
1040
|
}
|
|
1031
1041
|
|
|
1032
1042
|
async evaluateWildcard() {
|
|
1043
|
+
const { SnomedFilterContext } = require('../cs/cs-snomed');
|
|
1044
|
+
|
|
1033
1045
|
// Return all concepts - this would need optimization in practice
|
|
1034
1046
|
const filter = new SnomedFilterContext();
|
|
1035
1047
|
const allConcepts = [];
|
|
@@ -1046,49 +1058,86 @@ class ECLValidator {
|
|
|
1046
1058
|
}
|
|
1047
1059
|
|
|
1048
1060
|
async evaluateSubExpressionConstraint(node) {
|
|
1061
|
+
const { SnomedFilterContext } = require('../cs/cs-snomed');
|
|
1062
|
+
|
|
1049
1063
|
const baseFilter = await this.evaluateAST(node.focus);
|
|
1050
1064
|
|
|
1051
1065
|
if (!node.operator) {
|
|
1052
1066
|
return baseFilter;
|
|
1053
1067
|
}
|
|
1054
1068
|
|
|
1055
|
-
// Apply constraint operator
|
|
1056
|
-
|
|
1069
|
+
// Apply constraint operator — collect into a Set to deduplicate across
|
|
1070
|
+
// multi-concept base filters.
|
|
1071
|
+
const accumulated = new Set();
|
|
1057
1072
|
|
|
1058
1073
|
for (const conceptIndex of baseFilter.descendants || []) {
|
|
1059
1074
|
const conceptId = this.sct.concepts.getConceptId(conceptIndex);
|
|
1060
1075
|
|
|
1061
1076
|
let operatorFilter;
|
|
1062
1077
|
switch (node.operator) {
|
|
1063
|
-
|
|
1078
|
+
// ── Descendants ─────────────────────────────────────────────────────
|
|
1079
|
+
case ECLTokenType.DESCENDANT_OF: // < transitive, no self
|
|
1064
1080
|
operatorFilter = this.sct.filterIsA(conceptId, false);
|
|
1065
1081
|
break;
|
|
1066
|
-
case ECLTokenType.
|
|
1082
|
+
case ECLTokenType.DESCENDANT_OR_SELF_OF: // << transitive + self
|
|
1067
1083
|
operatorFilter = this.sct.filterIsA(conceptId, true);
|
|
1068
1084
|
break;
|
|
1069
|
-
case ECLTokenType.
|
|
1070
|
-
operatorFilter = this.sct.
|
|
1085
|
+
case ECLTokenType.CHILD_OF: // <! direct children only
|
|
1086
|
+
operatorFilter = this.sct.filterChildOf(conceptId);
|
|
1071
1087
|
break;
|
|
1072
|
-
case ECLTokenType.
|
|
1073
|
-
operatorFilter =
|
|
1088
|
+
case ECLTokenType.CHILD_OR_SELF_OF: { // <<! self + direct children
|
|
1089
|
+
operatorFilter = new SnomedFilterContext();
|
|
1090
|
+
const selfResult = this.sct.concepts.findConcept(conceptId);
|
|
1091
|
+
const children = selfResult.found ? this.sct.getConceptChildren(selfResult.index) : [];
|
|
1092
|
+
operatorFilter.descendants = selfResult.found ? [selfResult.index, ...children] : children;
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// ── Ancestors ───────────────────────────────────────────────────────
|
|
1097
|
+
case ECLTokenType.ANCESTOR_OF: // > transitive, no self
|
|
1098
|
+
operatorFilter = this.sct.filterGeneralizes(conceptId);
|
|
1099
|
+
break;
|
|
1100
|
+
case ECLTokenType.ANCESTOR_OR_SELF_OF: { // >> transitive + self
|
|
1101
|
+
operatorFilter = this.sct.filterGeneralizes(conceptId);
|
|
1102
|
+
const selfResult = this.sct.concepts.findConcept(conceptId);
|
|
1103
|
+
if (selfResult.found && !operatorFilter.descendants.includes(selfResult.index)) {
|
|
1104
|
+
operatorFilter.descendants.push(selfResult.index);
|
|
1105
|
+
}
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
case ECLTokenType.PARENT_OF: { // >! direct parents only
|
|
1109
|
+
operatorFilter = new SnomedFilterContext();
|
|
1110
|
+
const selfResult = this.sct.concepts.findConcept(conceptId);
|
|
1111
|
+
operatorFilter.descendants = selfResult.found
|
|
1112
|
+
? this.sct.getConceptParents(selfResult.index)
|
|
1113
|
+
: [];
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
case ECLTokenType.PARENT_OR_SELF_OF: { // >>! self + direct parents
|
|
1117
|
+
operatorFilter = new SnomedFilterContext();
|
|
1118
|
+
const selfResult = this.sct.concepts.findConcept(conceptId);
|
|
1119
|
+
const parents = selfResult.found ? this.sct.getConceptParents(selfResult.index) : [];
|
|
1120
|
+
operatorFilter.descendants = selfResult.found ? [selfResult.index, ...parents] : parents;
|
|
1074
1121
|
break;
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
case ECLTokenType.ANCESTOR_OF:
|
|
1078
|
-
case ECLTokenType.ANCESTOR_OR_SELF_OF:
|
|
1079
|
-
// These would require reverse hierarchy traversal
|
|
1080
|
-
throw new Error(`Operator ${node.operator} not yet implemented`);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1081
1124
|
default:
|
|
1082
1125
|
throw new Error(`Unknown constraint operator: ${node.operator}`);
|
|
1083
1126
|
}
|
|
1084
1127
|
|
|
1085
|
-
|
|
1128
|
+
for (const idx of operatorFilter.descendants || []) {
|
|
1129
|
+
accumulated.add(idx);
|
|
1130
|
+
}
|
|
1086
1131
|
}
|
|
1087
1132
|
|
|
1133
|
+
const results = new SnomedFilterContext();
|
|
1134
|
+
results.descendants = [...accumulated];
|
|
1088
1135
|
return results;
|
|
1089
1136
|
}
|
|
1090
1137
|
|
|
1091
1138
|
async evaluateCompoundExpression(node) {
|
|
1139
|
+
const { SnomedFilterContext } = require('../cs/cs-snomed');
|
|
1140
|
+
|
|
1092
1141
|
const leftFilter = await this.evaluateAST(node.left);
|
|
1093
1142
|
const rightFilter = await this.evaluateAST(node.right);
|
|
1094
1143
|
|
|
@@ -1327,7 +1376,7 @@ class ECLValidator {
|
|
|
1327
1376
|
this.validateSemanticAST(node.value, errors);
|
|
1328
1377
|
break;
|
|
1329
1378
|
|
|
1330
|
-
|
|
1379
|
+
// Basic nodes don't need semantic validation
|
|
1331
1380
|
case ECLNodeType.CONCEPT_REFERENCE:
|
|
1332
1381
|
case ECLNodeType.WILDCARD:
|
|
1333
1382
|
break;
|
package/tx/tx.js
CHANGED
|
@@ -20,7 +20,7 @@ const packageJson = require("../package.json");
|
|
|
20
20
|
// Import workers
|
|
21
21
|
const ReadWorker = require('./workers/read');
|
|
22
22
|
const SearchWorker = require('./workers/search');
|
|
23
|
-
const { ExpandWorker, INTERNAL_DEFAULT_LIMIT,
|
|
23
|
+
const { ExpandWorker, INTERNAL_DEFAULT_LIMIT, EXTERNAL_TEST_DEFAULT_LIMIT} = require('./workers/expand');
|
|
24
24
|
const { ValidateWorker } = require('./workers/validate');
|
|
25
25
|
const TranslateWorker = require('./workers/translate');
|
|
26
26
|
const LookupWorker = require('./workers/lookup');
|
|
@@ -1212,8 +1212,12 @@ class TXModule {
|
|
|
1212
1212
|
}
|
|
1213
1213
|
|
|
1214
1214
|
externalLimit(req) {
|
|
1215
|
+
let hdr = req.headers["x-too-costly-threshold"];
|
|
1216
|
+
if (hdr) {
|
|
1217
|
+
return parseInt(hdr);
|
|
1218
|
+
}
|
|
1215
1219
|
let isTest = req.header("User-Agent") == 'Tools/Java';
|
|
1216
|
-
if (this.config.internalLimit && !isTest) return this.config.externalLimit; else return
|
|
1220
|
+
if (this.config.internalLimit && !isTest) return this.config.externalLimit; else return EXTERNAL_TEST_DEFAULT_LIMIT;
|
|
1217
1221
|
}
|
|
1218
1222
|
|
|
1219
1223
|
}
|