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/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.DESCENDANT_OR_SELF_OF, value: '<<!', };
284
+ return { type: ECLTokenType.CHILD_OR_SELF_OF, value: '<<!' };
278
285
  } else {
279
286
  this.advance();
280
287
  this.advance();
281
- return { type: ECLTokenType.CHILD_OR_SELF_OF, value: '<<' };
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.DESCENDANT_OF, value: '<!' };
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.CHILD_OF, value: '<' };
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.ANCESTOR_OR_SELF_OF, value: '>>!' };
310
+ return { type: ECLTokenType.PARENT_OR_SELF_OF, value: '>>!' };
304
311
  } else {
305
312
  this.advance();
306
313
  this.advance();
307
- return { type: ECLTokenType.PARENT_OR_SELF_OF, value: '>>' };
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.ANCESTOR_OF, value: '>!' };
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.PARENT_OF, value: '>' };
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
- this.input[pos] === '.' &&
354
- pos + 1 < this.input.length &&
355
- /\d/.test(this.input[pos + 1])) {
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
- operator.type === ECLTokenType.OR ? ECLNodeType.DISJUNCTION :
475
- ECLNodeType.EXCLUSION;
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
- ECLTokenType.CHILD_OF, ECLTokenType.CHILD_OR_SELF_OF,
531
- ECLTokenType.DESCENDANT_OF, ECLTokenType.DESCENDANT_OR_SELF_OF,
532
- ECLTokenType.PARENT_OF, ECLTokenType.PARENT_OR_SELF_OF,
533
- ECLTokenType.ANCESTOR_OF, ECLTokenType.ANCESTOR_OR_SELF_OF
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.CHILD_OF, ECLTokenType.PARENT_OF, ECLTokenType.GTE)) {
685
- // Note: CHILD_OF (<) is treated as LT and PARENT_OF (>) is treated as GT in numeric comparison context
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 CHILD_OF to LT and PARENT_OF to GT for numeric comparisons
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.CHILD_OF) {
712
+ if (operator.type === ECLTokenType.DESCENDANT_OF) {
703
713
  operatorType = ECLTokenType.LT;
704
- } else if (operator.type === ECLTokenType.PARENT_OF) {
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
- const results = new SnomedFilterContext();
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
- case ECLTokenType.CHILD_OF:
1078
+ // ── Descendants ─────────────────────────────────────────────────────
1079
+ case ECLTokenType.DESCENDANT_OF: // < transitive, no self
1064
1080
  operatorFilter = this.sct.filterIsA(conceptId, false);
1065
1081
  break;
1066
- case ECLTokenType.CHILD_OR_SELF_OF:
1082
+ case ECLTokenType.DESCENDANT_OR_SELF_OF: // << transitive + self
1067
1083
  operatorFilter = this.sct.filterIsA(conceptId, true);
1068
1084
  break;
1069
- case ECLTokenType.DESCENDANT_OF:
1070
- operatorFilter = this.sct.filterIsA(conceptId, false);
1085
+ case ECLTokenType.CHILD_OF: // <! direct children only
1086
+ operatorFilter = this.sct.filterChildOf(conceptId);
1071
1087
  break;
1072
- case ECLTokenType.DESCENDANT_OR_SELF_OF:
1073
- operatorFilter = this.sct.filterIsA(conceptId, true);
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
- case ECLTokenType.PARENT_OF:
1076
- case ECLTokenType.PARENT_OR_SELF_OF:
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
- results.descendants = [...(results.descendants || []), ...(operatorFilter.descendants || [])];
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
- // Basic nodes don't need semantic validation
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, EXTERNAL_DEFAULT_LIMIT} = require('./workers/expand');
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 EXTERNAL_DEFAULT_LIMIT;
1220
+ if (this.config.internalLimit && !isTest) return this.config.externalLimit; else return EXTERNAL_TEST_DEFAULT_LIMIT;
1217
1221
  }
1218
1222
 
1219
1223
  }