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/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
|
-
//
|
|
75
|
-
|
|
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=
|
|
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,
|
|
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
|
-
*
|
|
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<
|
|
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
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 '
|
|
640
|
+
`SELECT 'event' AS kind,
|
|
597
641
|
url,
|
|
598
642
|
version,
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
718
|
+
detail = `Running... (started ${fmtFull(row.ts)})`;
|
|
646
719
|
colour = 'orange';
|
|
647
720
|
}
|
|
648
721
|
html += `<tr style="background:#f0f0f0">`;
|
|
649
|
-
html += `<td>${escape(
|
|
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(
|
|
656
|
-
html += `<td
|
|
657
|
-
html += `<td>${escape(row.url || '')}
|
|
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:
|
package/tx/workers/expand.js
CHANGED
|
@@ -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
|
-
|
|
708
|
-
this.
|
|
709
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
2076
|
-
params.limit =
|
|
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
|
};
|
package/tx/workers/related.js
CHANGED
|
@@ -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',
|
package/tx/workers/validate.js
CHANGED
|
@@ -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
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
async findValueSet(url, version = null) {
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
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)
|