fhirsmith 0.4.2 → 0.5.0
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 +12 -0
- package/README.md +1 -1
- package/library/cron-utilities.js +136 -0
- package/library/html-server.js +13 -29
- package/library/html.js +3 -8
- package/library/languages.js +160 -37
- package/library/package-manager.js +48 -1
- package/library/utilities.js +100 -19
- package/package.json +2 -2
- package/packages/package-crawler.js +6 -1
- package/packages/packages.js +38 -54
- package/publisher/publisher.js +19 -27
- package/registry/api.js +11 -10
- package/registry/crawler.js +31 -29
- package/registry/model.js +5 -26
- package/registry/registry.js +32 -41
- package/server.js +53 -5
- package/shl/shl.js +0 -18
- package/static/assets/js/statuspage.js +1 -9
- package/stats.js +39 -1
- package/token/token.js +14 -9
- package/translations/Messages.properties +2 -1
- package/tx/README.md +17 -6
- package/tx/cs/cs-api.js +19 -1
- package/tx/cs/cs-base.js +77 -0
- package/tx/cs/cs-country.js +46 -0
- package/tx/cs/cs-cpt.js +9 -5
- package/tx/cs/cs-cs.js +27 -13
- package/tx/cs/cs-lang.js +60 -22
- package/tx/cs/cs-loinc.js +69 -98
- package/tx/cs/cs-mimetypes.js +4 -0
- package/tx/cs/cs-ndc.js +6 -0
- package/tx/cs/cs-omop.js +16 -15
- package/tx/cs/cs-rxnorm.js +23 -1
- package/tx/cs/cs-snomed.js +283 -40
- package/tx/cs/cs-ucum.js +90 -70
- package/tx/importers/import-sct.module.js +371 -35
- package/tx/importers/readme.md +117 -7
- package/tx/library/bundle.js +5 -0
- package/tx/library/capabilitystatement.js +3 -142
- package/tx/library/codesystem.js +19 -173
- package/tx/library/conceptmap.js +4 -218
- package/tx/library/designations.js +14 -1
- package/tx/library/extensions.js +7 -0
- package/tx/library/namingsystem.js +3 -89
- package/tx/library/operation-outcome.js +8 -3
- package/tx/library/parameters.js +3 -2
- package/tx/library/renderer.js +10 -6
- package/tx/library/terminologycapabilities.js +3 -243
- package/tx/library/valueset.js +3 -235
- package/tx/library.js +100 -13
- package/tx/operation-context.js +23 -4
- package/tx/params.js +35 -38
- package/tx/provider.js +6 -5
- package/tx/sct/expressions.js +12 -3
- package/tx/tx-html.js +80 -89
- package/tx/tx.fhir.org.yml +6 -5
- package/tx/tx.js +163 -13
- package/tx/vs/vs-database.js +56 -39
- package/tx/vs/vs-package.js +21 -2
- package/tx/vs/vs-vsac.js +175 -39
- package/tx/workers/batch-validate.js +2 -0
- package/tx/workers/batch.js +2 -0
- package/tx/workers/expand.js +132 -112
- package/tx/workers/lookup.js +33 -14
- package/tx/workers/metadata.js +2 -2
- package/tx/workers/read.js +3 -2
- package/tx/workers/related.js +574 -0
- package/tx/workers/search.js +46 -9
- package/tx/workers/subsumes.js +13 -3
- package/tx/workers/translate.js +7 -3
- package/tx/workers/validate.js +258 -285
- package/tx/workers/worker.js +43 -39
- package/tx/xml/bundle-xml.js +237 -0
- package/tx/xml/xml-base.js +215 -64
- package/tx/xversion/xv-bundle.js +71 -0
- package/tx/xversion/xv-capabiliityStatement.js +137 -0
- package/tx/xversion/xv-codesystem.js +169 -0
- package/tx/xversion/xv-conceptmap.js +224 -0
- package/tx/xversion/xv-namingsystem.js +88 -0
- package/tx/xversion/xv-operationoutcome.js +27 -0
- package/tx/xversion/xv-parameters.js +87 -0
- package/tx/xversion/xv-resource.js +45 -0
- package/tx/xversion/xv-terminologyCapabilities.js +214 -0
- package/tx/xversion/xv-valueset.js +234 -0
- package/utilities/dev-proxy-server.js +126 -0
- package/utilities/explode-results.js +58 -0
- package/utilities/split-by-system.js +198 -0
- package/utilities/vsac-cs-fetcher.js +0 -0
- package/{windows-install.js → utilities/windows-install.js} +2 -0
- package/vcl/vcl.js +0 -18
- package/xig/xig.js +108 -99
package/tx/vs/vs-vsac.js
CHANGED
|
@@ -3,6 +3,8 @@ const axios = require('axios');
|
|
|
3
3
|
const { AbstractValueSetProvider } = require('./vs-api');
|
|
4
4
|
const { ValueSetDatabase } = require('./vs-database');
|
|
5
5
|
const { VersionUtilities } = require('../../library/version-utilities');
|
|
6
|
+
const folders = require('../../library/folder-setup');
|
|
7
|
+
const ValueSet = require("../library/valueset");
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* VSAC (Value Set Authority Center) ValueSet provider
|
|
@@ -16,22 +18,20 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
16
18
|
* @param {number} [config.refreshIntervalHours=24] - Hours between refresh scans
|
|
17
19
|
* @param {string} [config.baseUrl='http://cts.nlm.nih.gov/fhir'] - Base URL for VSAC FHIR server
|
|
18
20
|
*/
|
|
19
|
-
constructor(config) {
|
|
21
|
+
constructor(config, stats) {
|
|
20
22
|
super();
|
|
23
|
+
this.stats = stats;
|
|
21
24
|
|
|
22
25
|
if (!config.apiKey) {
|
|
23
|
-
throw new Error('API key is required');
|
|
24
|
-
}
|
|
25
|
-
if (!config.cacheFolder) {
|
|
26
|
-
throw new Error('Cache folder is required');
|
|
26
|
+
throw new Error('VSAC API key is required');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
this.apiKey = config.apiKey;
|
|
30
|
-
this.cacheFolder =
|
|
30
|
+
this.cacheFolder = folders.ensureFilePath("terminology-cache/vsac");
|
|
31
31
|
this.baseUrl = config.baseUrl || 'http://cts.nlm.nih.gov/fhir';
|
|
32
32
|
this.refreshIntervalHours = config.refreshIntervalHours || 24;
|
|
33
33
|
|
|
34
|
-
this.dbPath = path.join(
|
|
34
|
+
this.dbPath = path.join(this.cacheFolder, 'vsac-valuesets.db');
|
|
35
35
|
this.database = new ValueSetDatabase(this.dbPath);
|
|
36
36
|
this.valueSetMap = new Map();
|
|
37
37
|
this.initialized = false;
|
|
@@ -60,16 +60,18 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
60
60
|
if (this.initialized) {
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
|
+
this.stats.addTask('VSAC Sync', `${this.refreshIntervalHours} hours`);
|
|
63
64
|
|
|
64
65
|
// Create database if it doesn't exist
|
|
65
66
|
if (!(await this.database.exists())) {
|
|
66
67
|
await this.database.create();
|
|
67
|
-
// Force initial refresh for new database
|
|
68
|
-
await this.refreshValueSets();
|
|
69
68
|
} else {
|
|
70
69
|
// Load existing data
|
|
71
70
|
await this._reloadMap();
|
|
72
71
|
}
|
|
72
|
+
if (this.valueSetMap.size == 0) {
|
|
73
|
+
await this.refreshValueSets();
|
|
74
|
+
}
|
|
73
75
|
|
|
74
76
|
// Start periodic refresh
|
|
75
77
|
this._startRefreshTimer();
|
|
@@ -110,67 +112,127 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
110
112
|
* @returns {Promise<void>}
|
|
111
113
|
*/
|
|
112
114
|
async refreshValueSets() {
|
|
115
|
+
this.stats.task('VSAC Sync', 'running');
|
|
113
116
|
if (this.isRefreshing) {
|
|
114
117
|
console.log('Refresh already in progress, skipping');
|
|
115
118
|
return;
|
|
116
119
|
}
|
|
120
|
+
this.queue = [];
|
|
117
121
|
|
|
118
122
|
this.isRefreshing = true;
|
|
119
|
-
const refreshStartTime = Math.floor(Date.now() / 1000);
|
|
120
123
|
|
|
121
124
|
try {
|
|
125
|
+
// phase 1: list all value sets
|
|
122
126
|
console.log('Starting VSAC ValueSet refresh...');
|
|
123
127
|
|
|
124
|
-
|
|
125
|
-
let url = '/ValueSet?_offset=0&_count=100';
|
|
128
|
+
// This lists all the currently valid value sets by URL, but not the older versions
|
|
129
|
+
let url = '/ValueSet?_offset=0&_count=100&_elements=id,url,version,status';
|
|
130
|
+
|
|
131
|
+
let total = undefined;
|
|
132
|
+
let count = 0;
|
|
133
|
+
let ncount = 0;
|
|
126
134
|
|
|
127
135
|
while (url) {
|
|
128
|
-
console.log(`
|
|
129
|
-
|
|
136
|
+
console.log(`Sync: ${count} of ${total} - ${ncount} new`);
|
|
137
|
+
this.stats.task('VSAC Sync', `Sync: ${count} of ${total} - ${ncount} new`);
|
|
130
138
|
|
|
131
|
-
|
|
132
|
-
// Extract ValueSets from bundle entries
|
|
133
|
-
const valueSets = bundle.entry
|
|
134
|
-
.filter(entry => entry.resource && entry.resource.resourceType === 'ValueSet')
|
|
135
|
-
.map(entry => entry.resource);
|
|
139
|
+
const bundle = await this._fetchBundle(url);
|
|
136
140
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
if (!total) {
|
|
142
|
+
total = bundle.total;
|
|
143
|
+
}
|
|
144
|
+
for (let be of bundle.entry || []) {
|
|
145
|
+
let vs = be.resource;
|
|
146
|
+
if (vs) {
|
|
147
|
+
count++;
|
|
148
|
+
// if we've seen this value set before, then we've got nothing new here.
|
|
149
|
+
if (!this.valueSetMap.has(vs.url+"|"+vs.version)) {
|
|
150
|
+
this.queue.push(vs.url);
|
|
151
|
+
ncount++;
|
|
152
|
+
}
|
|
141
153
|
}
|
|
142
154
|
}
|
|
143
|
-
|
|
144
155
|
// Find next link
|
|
145
156
|
url = this._getNextUrl(bundle);
|
|
146
157
|
|
|
147
158
|
// Safety check against infinite loops
|
|
148
|
-
if (
|
|
149
|
-
console.log(`Reached total count (${
|
|
159
|
+
if (count > total) {
|
|
160
|
+
console.log(`Reached total count (${total}), stopping`);
|
|
150
161
|
break;
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
this.lastRefresh = new Date();
|
|
166
|
+
console.log(`VSAC refresh phase 1 done. Total: ${count} with ${ncount} new items`);
|
|
167
|
+
this.stats.task('VSAC Sync', `VSAC refresh phase 1 done. Total: ${count} with ${ncount} new items`);
|
|
168
|
+
|
|
169
|
+
let tracking = { totalFetched: 0, totalNew: 0, count: 0, newCount : 0 };
|
|
170
|
+
// phase 2: query for history & content
|
|
171
|
+
this.requeue = [];
|
|
172
|
+
for (let q of this.queue) {
|
|
173
|
+
this.stats.task('VSAC History for '+q, `running (${tracking.totalFetched} fetched, ${tracking.totalNew} new)`);
|
|
174
|
+
try {
|
|
175
|
+
await this.processContentAndHistory(q, tracking, this.queue.length);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this.requeue.push(q)
|
|
178
|
+
console.log(error);
|
|
179
|
+
this.stats.task('VSAC Sync', error.message);
|
|
180
|
+
}
|
|
181
|
+
// `running (${totalFetched} fetched, ${totalNew} new)`)
|
|
182
|
+
tracking.count++;
|
|
183
|
+
}
|
|
184
|
+
console.log("Requeue");
|
|
185
|
+
for (let q of this.requeue) {
|
|
186
|
+
this.stats.task('VSAC History for '+q, `running (${tracking.totalFetched} fetched, ${tracking.totalNew} new)`);
|
|
187
|
+
try {
|
|
188
|
+
await this.processContentAndHistory(q, tracking, this.requeue.length);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.log(error);
|
|
191
|
+
this.stats.task('VSAC Sync', error.message);
|
|
192
|
+
}
|
|
193
|
+
// `running (${totalFetched} fetched, ${totalNew} new)`)
|
|
194
|
+
tracking.count++;
|
|
158
195
|
}
|
|
159
196
|
|
|
160
197
|
// Reload map with fresh data
|
|
161
198
|
await this._reloadMap();
|
|
162
|
-
|
|
163
|
-
this.lastRefresh = new Date();
|
|
164
|
-
console.log(`VSAC refresh completed. Total: ${totalFetched} ValueSets, Deleted: ${deletedCount}`);
|
|
165
|
-
|
|
199
|
+
console.log(`VSAC refresh completed. Total: ${tracking.totalFetched} ValueSets, Deleted: ${tracking.deletedCount}`);
|
|
166
200
|
} catch (error) {
|
|
167
201
|
console.log(error, 'Error during VSAC refresh:');
|
|
202
|
+
this.stats.task('VSAC Sync', `Error (${error.message})`);
|
|
168
203
|
throw error;
|
|
169
204
|
} finally {
|
|
170
205
|
this.isRefreshing = false;
|
|
171
206
|
}
|
|
172
207
|
}
|
|
173
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Insert multiple ValueSets in a batch operation
|
|
211
|
+
* @param {Array<Object>} valueSets - Array of ValueSet resources
|
|
212
|
+
* @returns {Promise<void>}
|
|
213
|
+
*/
|
|
214
|
+
async batchUpsertValueSets(valueSets) {
|
|
215
|
+
if (valueSets.length === 0) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let count = 0;
|
|
220
|
+
// Process sequentially to avoid database locking
|
|
221
|
+
for (const valueSet of valueSets) {
|
|
222
|
+
let key = valueSet.url+"|"+valueSet.version;
|
|
223
|
+
let vs = this.valueSetMap.get(key);
|
|
224
|
+
if (vs) {
|
|
225
|
+
// we've seen this before, and maybe fetched it's history, so just update
|
|
226
|
+
// the timestamp
|
|
227
|
+
await this.database.seeValueSet(valueSet);
|
|
228
|
+
} else {
|
|
229
|
+
await this.database.upsertValueSet(valueSet);
|
|
230
|
+
count++;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return count;
|
|
234
|
+
}
|
|
235
|
+
|
|
174
236
|
/**
|
|
175
237
|
* Fetch a FHIR Bundle from the server
|
|
176
238
|
* @param {string} url - Relative URL to fetch
|
|
@@ -184,7 +246,34 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
184
246
|
if (response.data && response.data.resourceType === 'Bundle') {
|
|
185
247
|
return response.data;
|
|
186
248
|
} else {
|
|
187
|
-
throw new Error('Response is not a FHIR Bundle');
|
|
249
|
+
throw new Error('VSAC Response is not a FHIR Bundle');
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error.response) {
|
|
253
|
+
throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
|
|
254
|
+
} else if (error.request) {
|
|
255
|
+
throw new Error('Network error: No response received');
|
|
256
|
+
} else {
|
|
257
|
+
throw new Error(`Request error: ${error.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Fetch a FHIR Bundle from the server
|
|
265
|
+
* @param {string} url - Relative URL to fetch
|
|
266
|
+
* @returns {Promise<Object>} FHIR Bundle
|
|
267
|
+
* @private
|
|
268
|
+
*/
|
|
269
|
+
async _fetchValueSet(id) {
|
|
270
|
+
try {
|
|
271
|
+
const response = await this.httpClient.get("/ValueSet/"+id);
|
|
272
|
+
|
|
273
|
+
if (response.data && response.data.resourceType === 'ValueSet') {
|
|
274
|
+
return response.data;
|
|
275
|
+
} else {
|
|
276
|
+
throw new Error('VSAC Response is not a FHIR ValueSet');
|
|
188
277
|
}
|
|
189
278
|
} catch (error) {
|
|
190
279
|
if (error.response) {
|
|
@@ -244,7 +333,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
244
333
|
// Try exact match first: url|version
|
|
245
334
|
let key = `${url}|${version}`;
|
|
246
335
|
if (this.valueSetMap.has(key)) {
|
|
247
|
-
return this.valueSetMap.get(key);
|
|
336
|
+
return await this.checkFullVS(this.valueSetMap.get(key));
|
|
248
337
|
}
|
|
249
338
|
|
|
250
339
|
// If version is semver, try url|major.minor
|
|
@@ -254,7 +343,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
254
343
|
if (majorMinor) {
|
|
255
344
|
key = `${url}|${majorMinor}`;
|
|
256
345
|
if (this.valueSetMap.has(key)) {
|
|
257
|
-
return this.valueSetMap.get(key);
|
|
346
|
+
return await this.checkFullVS(this.valueSetMap.get(key));
|
|
258
347
|
}
|
|
259
348
|
}
|
|
260
349
|
}
|
|
@@ -264,12 +353,15 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
264
353
|
|
|
265
354
|
// Finally try just the URL
|
|
266
355
|
if (this.valueSetMap.has(url)) {
|
|
267
|
-
return this.valueSetMap.get(url);
|
|
356
|
+
return await this.checkFullVS(this.valueSetMap.get(url));
|
|
268
357
|
}
|
|
269
358
|
|
|
270
|
-
|
|
359
|
+
return null;
|
|
271
360
|
}
|
|
272
361
|
|
|
362
|
+
async fetchValueSetById(id) {
|
|
363
|
+
return await this.checkFullVS(this.valueSetMap.get(id));
|
|
364
|
+
}
|
|
273
365
|
/**
|
|
274
366
|
* Searches for value sets based on criteria
|
|
275
367
|
* @param {Array<{name: string, value: string}>} searchParams - Search criteria
|
|
@@ -358,6 +450,50 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
|
|
|
358
450
|
await this.database.close();
|
|
359
451
|
}
|
|
360
452
|
|
|
453
|
+
// eslint-disable-next-line no-unused-vars
|
|
454
|
+
assignIds(ids) {
|
|
455
|
+
// nothing?
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// when we get a valueset from vsac via search, the compose is not
|
|
459
|
+
// populated. We don't load all the composes. Instead, when value sets
|
|
460
|
+
// are fetched, we check to see if we've got the compose, and if we
|
|
461
|
+
// haven't, then we fetch it and store it
|
|
462
|
+
async checkFullVS(vs) {
|
|
463
|
+
if (!vs) {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
if (vs.jsonObj.compose) {
|
|
467
|
+
return vs;
|
|
468
|
+
}
|
|
469
|
+
console.log('get a full copy for the ValueSet '+vs.url+'|'+vs.version);
|
|
470
|
+
let vsNew = await this._fetchValueSet(vs.id);
|
|
471
|
+
await this.database.upsertValueSet(vsNew);
|
|
472
|
+
this.database.addToMap(this.valueSetMap, vsNew.id, vsNew.url, vsNew.version, vsNew);
|
|
473
|
+
return new ValueSet(vsNew);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async processContentAndHistory(q, tracking, length) {
|
|
477
|
+
let url = `/ValueSet?url=${q}`;
|
|
478
|
+
const bundle = await this._fetchBundle(url);
|
|
479
|
+
|
|
480
|
+
let vcount = 0;
|
|
481
|
+
if (bundle.entry && bundle.entry.length > 0) {
|
|
482
|
+
// Extract ValueSets from bundle entries
|
|
483
|
+
const valueSets = bundle.entry
|
|
484
|
+
.filter(entry => entry.resource && entry.resource.resourceType === 'ValueSet')
|
|
485
|
+
.map(entry => entry.resource);
|
|
486
|
+
if (valueSets.length > 0) {
|
|
487
|
+
tracking.totalNew = tracking.totalNew + await this.batchUpsertValueSets(valueSets);
|
|
488
|
+
tracking.totalFetched += valueSets.length;
|
|
489
|
+
vcount = valueSets.length;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
let logMsg = `VSAC (${tracking.count} of ${length}) ${q}: ${vcount} versions`;
|
|
493
|
+
console.log(logMsg);
|
|
494
|
+
this.stats.task('VSAC Sync', logMsg);
|
|
495
|
+
|
|
496
|
+
}
|
|
361
497
|
}
|
|
362
498
|
|
|
363
499
|
// Usage examples:
|
|
@@ -82,6 +82,7 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
82
82
|
output.push({name: "validation", resource : p});
|
|
83
83
|
} catch (error) {
|
|
84
84
|
this.log.error(error);
|
|
85
|
+
this.debugLog(error);
|
|
85
86
|
if (error instanceof Issue) {
|
|
86
87
|
let op = new OperationOutcome();
|
|
87
88
|
op.addIssue(error);
|
|
@@ -97,6 +98,7 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
97
98
|
return res.json(result);
|
|
98
99
|
} catch (error) {
|
|
99
100
|
this.log.error(error);
|
|
101
|
+
this.debugLog(error);
|
|
100
102
|
return res.status(error.statusCode || 500).json(this.operationOutcome(
|
|
101
103
|
'error', error.issueCode || 'exception', error.message));
|
|
102
104
|
}
|
package/tx/workers/batch.js
CHANGED
|
@@ -40,6 +40,7 @@ class BatchWorker extends TerminologyWorker {
|
|
|
40
40
|
await this.handleBatch(req, res);
|
|
41
41
|
} catch (error) {
|
|
42
42
|
this.log.error(error);
|
|
43
|
+
this.debugLog(error);
|
|
43
44
|
if (error instanceof Issue) {
|
|
44
45
|
const oo = new OperationOutcome();
|
|
45
46
|
oo.addIssue(error);
|
|
@@ -159,6 +160,7 @@ class BatchWorker extends TerminologyWorker {
|
|
|
159
160
|
|
|
160
161
|
} catch (error) {
|
|
161
162
|
this.log.error(error);
|
|
163
|
+
this.debugLog(error);
|
|
162
164
|
const statusCode = error.statusCode || 500;
|
|
163
165
|
const issueCode = error.issueCode || 'exception';
|
|
164
166
|
|