fhirsmith 0.9.2 → 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/cs/cs-ucum.js CHANGED
@@ -256,7 +256,7 @@ class UcumCodeSystemProvider extends BaseCSServices {
256
256
  // filterContext.filters.push(ucumFilter);
257
257
  }
258
258
 
259
- async filter(filterContext, prop, op, value) {
259
+ async filter(filterContext, forIteration, prop, op, value) {
260
260
  assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext');
261
261
  assert(prop != null && typeof prop === 'string', 'prop must be a non-null string');
262
262
  assert(op != null && typeof op === 'string', 'op must be a non-null string');
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
  }
package/tx/vs/vs-vsac.js CHANGED
@@ -136,7 +136,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
136
136
  console.log('Starting VSAC ValueSet refresh...');
137
137
 
138
138
  // This lists all the currently valid value sets by URL, but not the older versions
139
- let url = '/ValueSet?_offset=0&_count=100&_elements=id,url,version,status';
139
+ let url = '/ValueSet?_offset=0&_count=1000&_elements=id,url,version,status';
140
140
 
141
141
  let total = undefined;
142
142
  let count = 0;
@@ -669,7 +669,23 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
669
669
  );
670
670
  });
671
671
 
672
- const fmt = ts => ts
672
+ // ISO date (YYYY-MM-DD UTC) for grouping
673
+ const dayKey = ts => ts
674
+ ? new Date(ts * 1000).toISOString().substring(0, 10)
675
+ : '';
676
+ // Human-friendly day heading e.g. "Tuesday, 14 April 2026"
677
+ const dayLabel = ts => ts
678
+ ? new Date(ts * 1000).toLocaleDateString('en-GB', {
679
+ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
680
+ timeZone: 'UTC'
681
+ })
682
+ : '—';
683
+ // HH:MM:SS UTC within a day
684
+ const timeOnly = ts => ts
685
+ ? new Date(ts * 1000).toISOString().substring(11, 19) + ' UTC'
686
+ : '—';
687
+ // Full timestamp (used in "Running..." detail where context is needed)
688
+ const fmtFull = ts => ts
673
689
  ? new Date(ts * 1000).toISOString().replace('T', ' ').substring(0, 19) + ' UTC'
674
690
  : '—';
675
691
 
@@ -678,7 +694,16 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
678
694
  html += '<thead><tr><th>Time</th><th>Event</th><th>Detail</th></tr></thead>';
679
695
  html += '<tbody>';
680
696
 
697
+ let currentDay = null;
681
698
  for (const row of rows) {
699
+ const rowDay = dayKey(row.ts);
700
+ if (rowDay !== currentDay) {
701
+ currentDay = rowDay;
702
+ html += `<tr style="background:#d8d8d8">`;
703
+ html += `<td colspan="3"><strong>${escape(dayLabel(row.ts))}</strong></td>`;
704
+ html += `</tr>`;
705
+ }
706
+
682
707
  if (row.kind === 'run') {
683
708
  const duration = row.finished_at ? `${row.finished_at - row.ts}s` : 'in progress';
684
709
  let detail, colour;
@@ -690,11 +715,11 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
690
715
  detail = `Failed: ${escape(row.error_message || '')} (${duration})`;
691
716
  colour = 'red';
692
717
  } else {
693
- detail = `Running... (started ${fmt(row.ts)})`;
718
+ detail = `Running... (started ${fmtFull(row.ts)})`;
694
719
  colour = 'orange';
695
720
  }
696
721
  html += `<tr style="background:#f0f0f0">`;
697
- html += `<td>${escape(fmt(row.ts))}</td>`;
722
+ html += `<td>${escape(timeOnly(row.ts))}</td>`;
698
723
  html += `<td><strong style="color:${colour}">Sync run</strong></td>`;
699
724
  html += `<td>${detail}</td>`;
700
725
  html += `</tr>`;
@@ -703,15 +728,15 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
703
728
  let label, colour;
704
729
  switch (row.event_type) {
705
730
  case 'new':
706
- label = 'New value set';
731
+ label = 'New';
707
732
  colour = 'green';
708
733
  break;
709
734
  case 'updated':
710
- label = 'Updated value set';
735
+ label = 'Updated';
711
736
  colour = 'blue';
712
737
  break;
713
738
  case 'deleted':
714
- label = 'Deleted value set';
739
+ label = 'Deleted';
715
740
  colour = 'red';
716
741
  break;
717
742
  default:
@@ -719,9 +744,9 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
719
744
  colour = 'black';
720
745
  }
721
746
  html += `<tr>`;
722
- html += `<td>${escape(fmt(row.ts))}</td>`;
747
+ html += `<td>${escape(timeOnly(row.ts))}</td>`;
723
748
  html += `<td><span style="color:${colour}">${label}</span></td>`;
724
- html += `<td>${escape(row.url || '')}#${escape(row.version || '')}</td>`;
749
+ html += `<td>${escape(this.urlTail(row.url) || '')} v <a href="../ValueSet/${escape(this.urlTail(row.url) || '')}-${escape(row.version || '')}">${escape(row.version || '')}</a></td>`;
725
750
  html += `</tr>`;
726
751
  }
727
752
  }
@@ -733,6 +758,10 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
733
758
  id() {
734
759
  return "vsac";
735
760
  }
761
+
762
+ urlTail(url) {
763
+ return url ? url.substring(url.lastIndexOf('/') + 1) : '';
764
+ }
736
765
  }
737
766
 
738
767
  // Usage examples:
@@ -22,6 +22,7 @@ const {debugLog} = require("../operation-context");
22
22
 
23
23
  // Expansion limits (from Pascal constants)
24
24
  const EXTERNAL_DEFAULT_LIMIT = 1000;
25
+ const EXTERNAL_TEST_DEFAULT_LIMIT = 3000;
25
26
  const INTERNAL_DEFAULT_LIMIT = 10000;
26
27
  const EXPANSION_DEAD_TIME_SECS = 30;
27
28
  const CACHE_WHEN_DEBUGGING = false;
@@ -508,8 +509,8 @@ class ValueSetExpander {
508
509
  this.excluded.add(key);
509
510
  }
510
511
 
511
- async checkCanExpandValueSet(uri, version) {
512
- const vs = await this.worker.findValueSet(uri, version);
512
+ async checkCanExpandValueSet(uri, version, source) {
513
+ const vs = await this.worker.findValueSet(uri, version, source);
513
514
  if (vs == null) {
514
515
  if (!version && uri.includes('|')) {
515
516
  version = uri.substring(uri.indexOf('|') + 1);
@@ -525,9 +526,8 @@ class ValueSetExpander {
525
526
  }
526
527
  }
527
528
 
528
- async expandValueSet(uri, version, filter, notClosed) {
529
+ async expandValueSet(uri, version, vs, filter, notClosed) {
529
530
 
530
- let vs = await this.worker.findValueSet(uri, version);
531
531
  if (!vs) {
532
532
  if (version) {
533
533
  throw new Issue('error', 'not-found', null, 'VS_EXP_IMPORT_UNK_PINNED', this.worker.i18n.translate('VS_EXP_IMPORT_UNK_PINNED', this.params.httpLanguages, [uri, version]), "not-found", 422);
@@ -609,14 +609,14 @@ class ValueSetExpander {
609
609
  }
610
610
  }
611
611
 
612
- async checkSource(cset, exp, filter, srcURL, ts, vsInfo) {
612
+ async checkSource(cset, exp, filter, srcURL, ts, vsInfo , source) {
613
613
  this.worker.deadCheck('checkSource');
614
614
  Extensions.checkNoModifiers(cset, 'ValueSetExpander.checkSource', 'set', srcURL);
615
615
  let imp = false;
616
616
  for (const u of cset.valueSet || []) {
617
617
  this.worker.deadCheck('checkSource');
618
618
  const s = this.worker.pinValueSet(u);
619
- await this.checkCanExpandValueSet(s, '');
619
+ await this.checkCanExpandValueSet(s, '', source);
620
620
  imp = true;
621
621
  }
622
622
 
@@ -659,7 +659,7 @@ class ValueSetExpander {
659
659
 
660
660
  if (!cset.concept && !cset.filter) {
661
661
  if (cs.specialEnumeration()) {
662
- await this.checkCanExpandValueSet(cs.specialEnumeration(), '');
662
+ await this.checkCanExpandValueSet(cs.specialEnumeration(), '', null);
663
663
  } else if (filter.isNull) {
664
664
  if (cs.isNotClosed()) {
665
665
  if (cs.specialEnumeration()) {
@@ -704,9 +704,12 @@ class ValueSetExpander {
704
704
  this.worker.deadCheck('processCodes#2');
705
705
  const s = this.worker.pinValueSet(u);
706
706
  this.worker.opContext.log('import value set ' + s);
707
- const ivs = new ImportedValueSet(await this.expandValueSet(s, '', filter, notClosed));
708
- this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
709
- this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
707
+ let vs = await this.worker.findValueSet(s, '', vsSrc);
708
+ const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed));
709
+ this. checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
710
+ if (!vs.isContained) {
711
+ this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
712
+ }
710
713
  valueSets.push(ivs);
711
714
  }
712
715
  this.addToTotal(await this.importValueSet(valueSets[0].valueSet, expansion, valueSets, 1));
@@ -728,16 +731,20 @@ class ValueSetExpander {
728
731
  this.worker.deadCheck('processCodes#2');
729
732
  const s = this.worker.pinValueSet(u);
730
733
  this.worker.opContext.log('import value set ' + s);
731
- const ivs = new ImportedValueSet(await this.expandValueSet(s, '', filter, notClosed));
734
+ let vs = await this.worker.findValueSet(s, '', vsSrc);
735
+ const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed));
732
736
  this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
733
- this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
737
+ if (!vs.isContained) {
738
+ this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
739
+ }
734
740
  valueSets.push(ivs);
735
741
  }
736
742
 
737
743
  if (!cset.concept && !cset.filter) {
738
744
  if (cs.specialEnumeration() && filters.length === 0) {
739
745
  this.worker.opContext.log('import special value set ' + cs.specialEnumeration());
740
- const base = await this.expandValueSet(cs.specialEnumeration(), '', filter, notClosed);
746
+ let vs = await this.worker.findValueSet(cs.specialEnumeration(), '', null);
747
+ const base = await this.expandValueSet(cs.specialEnumeration(), '', vs, filter, notClosed);
741
748
  Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
742
749
  Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
743
750
  await this.importValueSet(base, expansion, valueSets, 0);
@@ -860,7 +867,7 @@ class ValueSetExpander {
860
867
  throw new Issue('error', 'invalid', path + ".filter[" + i + "]", 'UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.worker.i18n.translate('UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.params.httpLanguages, [cs.system(), fc.property, fc.op]), 'vs-invalid', 400);
861
868
  }
862
869
  Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl);
863
- await cs.filter(prep, fc.property, fc.op, fc.value);
870
+ await cs.filter(prep, i == 0, fc.property, fc.op, fc.value);
864
871
  }
865
872
 
866
873
  const fset = await cs.executeFilters(prep);
@@ -871,6 +878,7 @@ class ValueSetExpander {
871
878
  }
872
879
 
873
880
  this.worker.opContext.log('iterate filters');
881
+ this.addToTotal(0);
874
882
  const cds = new Designations(this.worker.i18n.languageDefinitions);
875
883
  while (await cs.filterMore(prep, fset[0])) {
876
884
  this.worker.deadCheck('processCodes#5');
@@ -937,9 +945,12 @@ class ValueSetExpander {
937
945
  for (const u of cset.valueSet) {
938
946
  const s = this.worker.pinValueSet(u);
939
947
  this.worker.deadCheck('processCodes#2');
940
- const ivs = new ImportedValueSet(await this.expandValueSet(s, '', filter, notClosed));
948
+ let vs = await this.worker.findValueSet(s, '', vsSrc);
949
+ const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed));
941
950
  this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
942
- this.addParamUri(expansion, 'used-valueset', ivs.valueSet.vurl);
951
+ if (!vs.isContained) {
952
+ this.addParamUri(expansion, 'used-valueset', ivs.valueSet.vurl);
953
+ }
943
954
  valueSets.push(ivs);
944
955
  }
945
956
  this.excludeValueSet(valueSets[0].valueSet, expansion, valueSets, 1);
@@ -959,9 +970,12 @@ class ValueSetExpander {
959
970
  this.worker.deadCheck('processCodes#3');
960
971
  const s = this.worker.pinValueSet(u);
961
972
  this.worker.opContext.log('import value set ' + s);
962
- const ivs = new ImportedValueSet(await this.expandValueSet(s, '', filter, notClosed));
973
+ let vs = await this.worker.findValueSet(s, '', vsSrc);
974
+ const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed));
963
975
  this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
964
- this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
976
+ if (!vs.isContained) {
977
+ this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
978
+ }
965
979
  valueSets.push(ivs);
966
980
  }
967
981
 
@@ -1039,10 +1053,12 @@ class ValueSetExpander {
1039
1053
  Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
1040
1054
  }
1041
1055
 
1056
+ let first = true;
1042
1057
  for (let fc of cset.filter) {
1043
1058
  this.worker.deadCheck('processCodes#4a');
1044
1059
  Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl);
1045
- await cs.filter(prep, fc.property, fc.op, fc.value);
1060
+ await cs.filter(prep, first, fc.property, fc.op, fc.value);
1061
+ first = false;
1046
1062
  }
1047
1063
 
1048
1064
  this.worker.opContext.log('iterate filters');
@@ -1150,12 +1166,12 @@ class ValueSetExpander {
1150
1166
  const ts = new Map();
1151
1167
  for (const c of source.jsonObj.compose.include || []) {
1152
1168
  this.worker.deadCheck('handleCompose#2');
1153
- await this.checkSource(c, expansion, filter, source.url, ts, vsInfo);
1169
+ await this.checkSource(c, expansion, filter, source.url, ts, vsInfo, source);
1154
1170
  }
1155
1171
  for (const c of source.jsonObj.compose.exclude || []) {
1156
1172
  this.worker.deadCheck('handleCompose#3');
1157
1173
  this.hasExclusions = true;
1158
- await this.checkSource(c, expansion, filter, source.url, ts, null);
1174
+ await this.checkSource(c, expansion, filter, source.url, ts, null, source);
1159
1175
  }
1160
1176
 
1161
1177
  this.worker.opContext.log('compose #2');
@@ -1215,6 +1231,7 @@ class ValueSetExpander {
1215
1231
  result.publisher = undefined;
1216
1232
  result.extension = undefined;
1217
1233
  result.text = undefined;
1234
+ result.contained = undefined;
1218
1235
  }
1219
1236
 
1220
1237
  for (let s of this.params.supplements) this.requiredSupplements.add(s);
@@ -1909,7 +1926,7 @@ class ExpandWorker extends TerminologyWorker {
1909
1926
  const url = this.getParameterValue(urlParam);
1910
1927
  const version = versionParam ? this.getParameterValue(versionParam) : null;
1911
1928
 
1912
- valueSet = await this.findValueSet(url, version);
1929
+ valueSet = await this.findValueSet(url, version, null);
1913
1930
  this.seeSourceVS(valueSet, url);
1914
1931
  if (!valueSet) {
1915
1932
  return res.status(422).json(this.operationOutcome('error', 'not-found',
@@ -2072,8 +2089,8 @@ class ExpandWorker extends TerminologyWorker {
2072
2089
 
2073
2090
  if (params.limit < -1) {
2074
2091
  params.limit = -1;
2075
- } else if (params.limit > EXTERNAL_DEFAULT_LIMIT) {
2076
- params.limit = EXTERNAL_DEFAULT_LIMIT; // can't ask for more than this externally, though you can internally
2092
+ } else if (params.limit > this.externalLimit) {
2093
+ params.limit = this.externalLimit; // can't ask for more than this externally, though you can internally
2077
2094
  }
2078
2095
 
2079
2096
  const filter = new SearchFilterText(params.filter);
@@ -2123,6 +2140,7 @@ module.exports = {
2123
2140
  EmptyFilterContext,
2124
2141
  EXTERNAL_DEFAULT_LIMIT,
2125
2142
  INTERNAL_DEFAULT_LIMIT,
2143
+ EXTERNAL_TEST_DEFAULT_LIMIT,
2126
2144
  TotalStatus,
2127
2145
  EXPANSION_DEAD_TIME_SECS
2128
2146
  };
@@ -188,7 +188,7 @@ class RelatedWorker extends TerminologyWorker {
188
188
  const url = this.getParameterValue(urlParam);
189
189
  const version = versionParam ? this.getParameterValue(versionParam) : null;
190
190
 
191
- let valueSet = await this.findValueSet(url, version);
191
+ let valueSet = await this.findValueSet(url, version, null);
192
192
  this.seeSourceVS(valueSet, url);
193
193
  if (!valueSet) {
194
194
  return res.status(404).json(this.operationOutcome('error', 'not-found',