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/vs/vs-vsac.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const path = require('path');
2
+ const crypto = require('crypto');
2
3
  const axios = require('axios');
3
4
  const { AbstractValueSetProvider } = require('./vs-api');
4
5
  const { ValueSetDatabase } = require('./vs-database');
@@ -11,6 +12,8 @@ const {debugLog} = require("../operation-context");
11
12
  * Fetches and caches ValueSets from the NLM VSAC FHIR server
12
13
  */
13
14
  class VSACValueSetProvider extends AbstractValueSetProvider {
15
+ SYNC_AT_START_UP = false;
16
+
14
17
  /**
15
18
  * @param {Object} config - Configuration object
16
19
  * @param {string} config.apiKey - API key for VSAC authentication
@@ -71,12 +74,11 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
71
74
  if (!(await this.database.exists())) {
72
75
  await this.database.create();
73
76
  } else {
74
- // Ensure schema is up to date (e.g. date_first_seen column added after initial deploy)
75
- await this.database._migrateIfNeeded(await this.database._getWriteConnection());
76
- // Load existing data
77
+ // Schema migrations are applied lazily by the database layer on first
78
+ // connection. Just load existing data.
77
79
  await this._reloadMap();
78
80
  }
79
- if (this.valueSetMap.size == 0) {
81
+ if (this.SYNC_AT_START_UP || this.valueSetMap.size == 0) {
80
82
  await this.refreshValueSets();
81
83
  }
82
84
  // Start periodic refresh
@@ -134,7 +136,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
134
136
  console.log('Starting VSAC ValueSet refresh...');
135
137
 
136
138
  // This lists all the currently valid value sets by URL, but not the older versions
137
- 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';
138
140
 
139
141
  let total = undefined;
140
142
  let count = 0;
@@ -182,11 +184,11 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
182
184
  // deduplicate the queue
183
185
  this.queue = [...new Set(this.queue)];
184
186
 
185
- let tracking = { totalFetched: 0, totalNew: 0, count: 0, newCount : 0 };
187
+ let tracking = { totalFetched: 0, totalNew: 0, totalUpdated: 0, count: 0, newCount : 0 };
186
188
  // phase 2: query for history & content
187
189
  this.requeue = [];
188
190
  for (let q of this.queue) {
189
- this.stats.task('VSAC History for '+q, `running (${tracking.totalFetched} fetched, ${tracking.totalNew} new)`);
191
+ this.stats.task('VSAC History for '+q, `running (${tracking.totalFetched} fetched, ${tracking.totalNew} new, ${tracking.totalUpdated} updated)`);
190
192
  try {
191
193
  await this.processContentAndHistory(q, tracking, this.queue.length);
192
194
  } catch (error) {
@@ -194,29 +196,27 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
194
196
  debugLog(error);
195
197
  this.stats.task('VSAC Sync', error.message);
196
198
  }
197
- // `running (${totalFetched} fetched, ${totalNew} new)`)
198
199
  tracking.count++;
199
200
  }
200
201
  console.log("Requeue");
201
202
  for (let q of this.requeue) {
202
- this.stats.task('VSAC History for '+q, `running (${tracking.totalFetched} fetched, ${tracking.totalNew} new)`);
203
+ this.stats.task('VSAC History for '+q, `running (${tracking.totalFetched} fetched, ${tracking.totalNew} new, ${tracking.totalUpdated} updated)`);
203
204
  try {
204
205
  await this.processContentAndHistory(q, tracking, this.requeue.length);
205
206
  } catch (error) {
206
207
  debugLog(error);
207
208
  this.stats.task('VSAC Sync', error.message);
208
209
  }
209
- // `running (${totalFetched} fetched, ${totalNew} new)`)
210
210
  tracking.count++;
211
211
  }
212
212
 
213
213
  // Reload map with fresh data
214
214
  await this._reloadMap();
215
- let msg = `VSAC refresh completed. Total: ${tracking.totalFetched} ValueSets, Deleted: ${tracking.deletedCount}`;
215
+ let msg = `VSAC refresh completed. Total: ${tracking.totalFetched} ValueSets, New: ${tracking.totalNew}, Updated: ${tracking.totalUpdated}`;
216
216
  this.stats.taskDone('VSAC Sync', msg);
217
217
  console.log(msg);
218
218
 
219
- await this.database.finishRun(runId, tracking.totalFetched, tracking.totalNew);
219
+ await this.database.finishRun(runId, tracking.totalFetched, tracking.totalNew, tracking.totalUpdated);
220
220
  } catch (error) {
221
221
  debugLog(error, 'Error during VSAC refresh:');
222
222
  this.stats.taskError('VSAC Sync', `Error (${error.message})`);
@@ -228,30 +228,71 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
228
228
  }
229
229
 
230
230
  /**
231
- * Insert multiple ValueSets in a batch operation
231
+ * Compute a SHA-256 hash of the ValueSet content for change detection.
232
+ * @param {Object} vs - The ValueSet resource (plain JSON object)
233
+ * @returns {string} hex-encoded SHA-256
234
+ * @private
235
+ */
236
+ _hashValueSet(vs) {
237
+ return crypto.createHash('sha256').update(JSON.stringify(vs)).digest('hex');
238
+ }
239
+
240
+ /**
241
+ * Insert multiple ValueSets in a batch operation.
242
+ * For each value set: if url|version is already known, compare content hashes.
243
+ * - hash unchanged -> touch last_seen only (seeValueSet)
244
+ * - hash changed -> upsert and record an 'updated' event
245
+ * - not seen before -> upsert and record a 'new' event
232
246
  * @param {Array<Object>} valueSets - Array of ValueSet resources
233
- * @returns {Promise<void>}
247
+ * @returns {Promise<{newCount: number, updatedCount: number}>}
234
248
  */
235
249
  async batchUpsertValueSets(valueSets) {
236
250
  if (valueSets.length === 0) {
237
- return;
251
+ return { newCount: 0, updatedCount: 0 };
238
252
  }
239
253
 
240
- let count = 0;
254
+ let newCount = 0;
255
+ let updatedCount = 0;
256
+
241
257
  // Process sequentially to avoid database locking
242
258
  for (const valueSet of valueSets) {
243
- let key = valueSet.url+"|"+valueSet.version;
244
- let vs = this.valueSetMap.get(key);
245
- if (vs) {
246
- // we've seen this before, and maybe fetched it's history, so just update
247
- // the timestamp
248
- await this.database.seeValueSet(valueSet);
259
+ const key = valueSet.url+"|"+valueSet.version;
260
+ const existing = this.valueSetMap.get(key);
261
+ const newHash = this._hashValueSet(valueSet);
262
+
263
+ if (existing) {
264
+ // We've seen this url|version before. Decide whether the content
265
+ // has actually changed by comparing hashes.
266
+ //
267
+ // Note: _reloadMap() mutates the in-memory jsonObj (strips inc.version
268
+ // from compose.include/exclude), so we cannot reliably recompute a
269
+ // hash from existing.jsonObj — it would not match the hash of the
270
+ // original unmutated JSON we stored. For rows predating this feature
271
+ // (content_hash NULL), we defer update detection until the next cycle:
272
+ // the upsert below runs only when hashes differ, so on the *next*
273
+ // sync after migration we'll have a proper baseline.
274
+ if (existing.contentHash && existing.contentHash === newHash) {
275
+ // No change - just touch last_seen
276
+ await this.database.seeValueSet(valueSet);
277
+ } else if (!existing.contentHash) {
278
+ // Legacy row without a stored hash - backfill the hash silently
279
+ // without emitting a spurious 'updated' event. We do a lightweight
280
+ // touch + hash update rather than a full upsert+event.
281
+ await this.database.seeValueSet(valueSet);
282
+ await this.database.setContentHash(valueSet.id, newHash);
283
+ } else {
284
+ // Content has changed - treat as update
285
+ await this.database.upsertValueSet(valueSet, newHash);
286
+ await this.database.recordEvent('updated', valueSet.url, valueSet.version);
287
+ updatedCount++;
288
+ }
249
289
  } else {
250
- await this.database.upsertValueSet(valueSet);
251
- count++;
290
+ await this.database.upsertValueSet(valueSet, newHash);
291
+ await this.database.recordEvent('new', valueSet.url, valueSet.version);
292
+ newCount++;
252
293
  }
253
294
  }
254
- return count;
295
+ return { newCount, updatedCount };
255
296
  }
256
297
 
257
298
  /**
@@ -511,18 +552,21 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
511
552
  const bundle = await this._fetchBundle(url);
512
553
 
513
554
  let vcount = 0;
555
+ let perRun = { newCount: 0, updatedCount: 0 };
514
556
  if (bundle.entry && bundle.entry.length > 0) {
515
557
  // Extract ValueSets from bundle entries
516
558
  const valueSets = bundle.entry
517
559
  .filter(entry => entry.resource && entry.resource.resourceType === 'ValueSet')
518
560
  .map(entry => entry.resource);
519
561
  if (valueSets.length > 0) {
520
- tracking.totalNew = tracking.totalNew + await this.batchUpsertValueSets(valueSets);
562
+ perRun = await this.batchUpsertValueSets(valueSets);
563
+ tracking.totalNew += perRun.newCount;
564
+ tracking.totalUpdated += perRun.updatedCount;
521
565
  tracking.totalFetched += valueSets.length;
522
566
  vcount = valueSets.length;
523
567
  }
524
568
  }
525
- let logMsg = `VSAC (${tracking.count} of ${length}) ${q}: ${vcount} versions`;
569
+ let logMsg = `VSAC (${tracking.count} of ${length}) ${q}: ${vcount} versions (${perRun.newCount} new, ${perRun.updatedCount} updated)`;
526
570
  console.log(logMsg);
527
571
  this.stats.task('VSAC Sync', logMsg);
528
572
  }
@@ -593,36 +637,55 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
593
637
 
594
638
  const rows = await new Promise((resolve, reject) => {
595
639
  db.all(
596
- `SELECT 'vs' AS kind,
640
+ `SELECT 'event' AS kind,
597
641
  url,
598
642
  version,
599
- date_first_seen AS ts,
600
- NULL AS status,
601
- NULL AS error_message,
602
- NULL AS finished_at,
603
- NULL AS total_fetched,
604
- NULL AS total_new
605
- FROM valuesets
606
- WHERE date_first_seen > 0
643
+ timestamp AS ts,
644
+ event_type,
645
+ NULL AS status,
646
+ NULL AS error_message,
647
+ NULL AS finished_at,
648
+ NULL AS total_fetched,
649
+ NULL AS total_new,
650
+ NULL AS total_updated
651
+ FROM vsac_events
607
652
  UNION ALL
608
- SELECT 'run' AS kind,
609
- NULL,
610
- NULL,
611
- started_at AS ts,
612
- status,
613
- error_message,
614
- finished_at,
615
- total_fetched,
616
- total_new
617
- FROM vsac_runs
618
- ORDER BY ts DESC
619
- LIMIT 200`,
653
+ SELECT 'run' AS kind,
654
+ NULL,
655
+ NULL,
656
+ started_at AS ts,
657
+ NULL AS event_type,
658
+ status,
659
+ error_message,
660
+ finished_at,
661
+ total_fetched,
662
+ total_new,
663
+ total_updated
664
+ FROM vsac_runs
665
+ ORDER BY ts DESC
666
+ LIMIT 200`,
620
667
  [],
621
668
  (err, rows) => err ? reject(err) : resolve(rows)
622
669
  );
623
670
  });
624
671
 
625
- 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
626
689
  ? new Date(ts * 1000).toISOString().replace('T', ' ').substring(0, 19) + ' UTC'
627
690
  : '—';
628
691
 
@@ -631,30 +694,59 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
631
694
  html += '<thead><tr><th>Time</th><th>Event</th><th>Detail</th></tr></thead>';
632
695
  html += '<tbody>';
633
696
 
697
+ let currentDay = null;
634
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
+
635
707
  if (row.kind === 'run') {
636
708
  const duration = row.finished_at ? `${row.finished_at - row.ts}s` : 'in progress';
637
709
  let detail, colour;
638
710
  if (row.status === 'ok') {
639
- detail = `${row.total_fetched} fetched, ${row.total_new} new, ${duration}`;
711
+ const updated = row.total_updated != null ? `, ${row.total_updated} updated` : '';
712
+ detail = `${row.total_fetched} fetched, ${row.total_new} new${updated}, ${duration}`;
640
713
  colour = 'green';
641
714
  } else if (row.status === 'error') {
642
715
  detail = `Failed: ${escape(row.error_message || '')} (${duration})`;
643
716
  colour = 'red';
644
717
  } else {
645
- detail = `Running... (started ${fmt(row.ts)})`;
718
+ detail = `Running... (started ${fmtFull(row.ts)})`;
646
719
  colour = 'orange';
647
720
  }
648
721
  html += `<tr style="background:#f0f0f0">`;
649
- html += `<td>${escape(fmt(row.ts))}</td>`;
722
+ html += `<td>${escape(timeOnly(row.ts))}</td>`;
650
723
  html += `<td><strong style="color:${colour}">Sync run</strong></td>`;
651
724
  html += `<td>${detail}</td>`;
652
725
  html += `</tr>`;
653
726
  } else {
727
+ // Event row: 'new', 'updated', or 'deleted'
728
+ let label, colour;
729
+ switch (row.event_type) {
730
+ case 'new':
731
+ label = 'New';
732
+ colour = 'green';
733
+ break;
734
+ case 'updated':
735
+ label = 'Updated';
736
+ colour = 'blue';
737
+ break;
738
+ case 'deleted':
739
+ label = 'Deleted';
740
+ colour = 'red';
741
+ break;
742
+ default:
743
+ label = escape(row.event_type || 'Event');
744
+ colour = 'black';
745
+ }
654
746
  html += `<tr>`;
655
- html += `<td>${escape(fmt(row.ts))}</td>`;
656
- html += `<td>New value set</td>`;
657
- html += `<td>${escape(row.url || '')}#${escape(row.version || '')}</td>`;
747
+ html += `<td>${escape(timeOnly(row.ts))}</td>`;
748
+ html += `<td><span style="color:${colour}">${label}</span></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>`;
658
750
  html += `</tr>`;
659
751
  }
660
752
  }
@@ -666,6 +758,10 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
666
758
  id() {
667
759
  return "vsac";
668
760
  }
761
+
762
+ urlTail(url) {
763
+ return url ? url.substring(url.lastIndexOf('/') + 1) : '';
764
+ }
669
765
  }
670
766
 
671
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',
@@ -351,7 +351,7 @@ class ValueSetChecker {
351
351
  let s = this.worker.pinValueSet(u);
352
352
  this.worker.deadCheck('prepareConceptSet');
353
353
  if (!this.others.has(s)) {
354
- let other = await this.worker.findValueSet(s, '');
354
+ let other = await this.worker.findValueSet(s, '', vs);
355
355
  if (other === null) {
356
356
  throw new Issue('error', 'not-found', null, 'Unable_to_resolve_value_Set_', this.worker.i18n.translate('Unable_to_resolve_value_Set_', this.params.HTTPLanguages, [s]), 'not-found', 422);
357
357
  }
@@ -472,7 +472,7 @@ class ValueSetChecker {
472
472
  this.worker.opContext.addNote(this.valueSet, 'Didn\'t find CodeSystem "' + this.worker.renderer.displayCoded(system, version) + '"', this.indentCount);
473
473
  result = null;
474
474
  cause.value = 'not-found';
475
- let vss = await this.worker.findValueSet(system, '');
475
+ let vss = await this.worker.findValueSet(system, '', null);
476
476
  if (vss !== null) {
477
477
  vss = null;
478
478
  let msg = this.worker.i18n.translate('Terminology_TX_System_ValueSet2', this.params.HTTPLanguages, [system]);
@@ -1154,7 +1154,7 @@ class ValueSetChecker {
1154
1154
  }
1155
1155
  let prov = await this.worker.findCodeSystem(ws, c.version, this.params, ['complete', 'fragment'], op,true, true, false, this.worker.requiredSupplements);
1156
1156
  if (prov === null) {
1157
- let vss = await this.worker.findValueSet(ws, '');
1157
+ let vss = await this.worker.findValueSet(ws, '', null);
1158
1158
  if (vss !== null) {
1159
1159
  vss = null;
1160
1160
  let m = this.worker.i18n.translate('Terminology_TX_System_ValueSet2', this.params.HTTPLanguages, [ws]);
@@ -1538,9 +1538,6 @@ class ValueSetChecker {
1538
1538
 
1539
1539
  async checkConceptSet(path, role, cs, cset, code, displays, vs, message, inactive, normalForm, vstatus, op, vcc, messages) {
1540
1540
  this.worker.opContext.addNote(vs, 'check code ' + role + ' ' + this.worker.renderer.displayValueSetInclude(cset) + ' at ' + path, this.indentCount);
1541
- if (role !== 'not in') {
1542
- inactive.value = false;
1543
- }
1544
1541
  let result = false;
1545
1542
  if (!cset.concept && !cset.filter) {
1546
1543
  let loc = await cs.locate(code);
@@ -1683,7 +1680,7 @@ class ValueSetChecker {
1683
1680
  if (!fc.value) {
1684
1681
  throw new Issue('error', 'invalid', null, '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]));
1685
1682
  }
1686
- await cs.filter(prep, fc.property, fc.op, fc.value);
1683
+ await cs.filter(prep, false, fc.property, fc.op, fc.value);
1687
1684
  // if (f === null) {
1688
1685
  // throw new Issue('error', 'not-supported', null, 'FILTER_NOT_UNDERSTOOD', this.worker.i18n.translate('FILTER_NOT_UNDERSTOOD', this.params.HTTPLanguages, [fc.property, fc.op, fc.value, vs.vurl, cs.system()]) + ' (2)', 'vs-invalid');
1689
1686
  // }
@@ -2232,7 +2229,7 @@ class ValidateWorker extends TerminologyWorker {
2232
2229
  if (csp) {
2233
2230
  return csp;
2234
2231
  } else {
2235
- let vs = await this.findValueSet(url, version);
2232
+ let vs = await this.findValueSet(url, version, null);
2236
2233
  if (vs) {
2237
2234
  let msg = this.i18n.translate('Terminology_TX_System_ValueSet2', txParams.HTTPLanguages, [url]);
2238
2235
  throw new Issue('error', 'invalid', path, 'Terminology_TX_System_ValueSet2', msg, 'invalid-data');
@@ -2448,22 +2445,22 @@ class ValidateWorker extends TerminologyWorker {
2448
2445
  return defaultValue;
2449
2446
  }
2450
2447
 
2451
- /**
2452
- * Find a ValueSet by URL
2453
- * @param {string} url - ValueSet URL
2454
- * @param {string} [version] - ValueSet version
2455
- * @returns {Object|null} ValueSet resource or null
2456
- */
2457
- async findValueSet(url, version = null) {
2458
- // First check additional resources
2459
- const found = this.findInAdditionalResources(url, version || '', 'ValueSet', false);
2460
- if (found) {
2461
- return found;
2462
- }
2463
-
2464
- // Then check provider
2465
- return await this.provider.findValueSet(this.opContext, url, version);
2466
- }
2448
+ // /**
2449
+ // * Find a ValueSet by URL
2450
+ // * @param {string} url - ValueSet URL
2451
+ // * @param {string} [version] - ValueSet version
2452
+ // * @returns {Object|null} ValueSet resource or null
2453
+ // */
2454
+ // async findValueSet(url, version = null) {
2455
+ // // First check additional resources
2456
+ // const found = this.findInAdditionalResources(url, version || '', 'ValueSet', false);
2457
+ // if (found) {
2458
+ // return found;
2459
+ // }
2460
+ //
2461
+ // // Then check provider
2462
+ // return await this.provider.findValueSet(this.opContext, url, version);
2463
+ // }
2467
2464
 
2468
2465
  /**
2469
2466
  * Get display text for a code (stub implementation for doValidationCS)