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.
Files changed (92) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/library/cron-utilities.js +136 -0
  4. package/library/html-server.js +13 -29
  5. package/library/html.js +3 -8
  6. package/library/languages.js +160 -37
  7. package/library/package-manager.js +48 -1
  8. package/library/utilities.js +100 -19
  9. package/package.json +2 -2
  10. package/packages/package-crawler.js +6 -1
  11. package/packages/packages.js +38 -54
  12. package/publisher/publisher.js +19 -27
  13. package/registry/api.js +11 -10
  14. package/registry/crawler.js +31 -29
  15. package/registry/model.js +5 -26
  16. package/registry/registry.js +32 -41
  17. package/server.js +53 -5
  18. package/shl/shl.js +0 -18
  19. package/static/assets/js/statuspage.js +1 -9
  20. package/stats.js +39 -1
  21. package/token/token.js +14 -9
  22. package/translations/Messages.properties +2 -1
  23. package/tx/README.md +17 -6
  24. package/tx/cs/cs-api.js +19 -1
  25. package/tx/cs/cs-base.js +77 -0
  26. package/tx/cs/cs-country.js +46 -0
  27. package/tx/cs/cs-cpt.js +9 -5
  28. package/tx/cs/cs-cs.js +27 -13
  29. package/tx/cs/cs-lang.js +60 -22
  30. package/tx/cs/cs-loinc.js +69 -98
  31. package/tx/cs/cs-mimetypes.js +4 -0
  32. package/tx/cs/cs-ndc.js +6 -0
  33. package/tx/cs/cs-omop.js +16 -15
  34. package/tx/cs/cs-rxnorm.js +23 -1
  35. package/tx/cs/cs-snomed.js +283 -40
  36. package/tx/cs/cs-ucum.js +90 -70
  37. package/tx/importers/import-sct.module.js +371 -35
  38. package/tx/importers/readme.md +117 -7
  39. package/tx/library/bundle.js +5 -0
  40. package/tx/library/capabilitystatement.js +3 -142
  41. package/tx/library/codesystem.js +19 -173
  42. package/tx/library/conceptmap.js +4 -218
  43. package/tx/library/designations.js +14 -1
  44. package/tx/library/extensions.js +7 -0
  45. package/tx/library/namingsystem.js +3 -89
  46. package/tx/library/operation-outcome.js +8 -3
  47. package/tx/library/parameters.js +3 -2
  48. package/tx/library/renderer.js +10 -6
  49. package/tx/library/terminologycapabilities.js +3 -243
  50. package/tx/library/valueset.js +3 -235
  51. package/tx/library.js +100 -13
  52. package/tx/operation-context.js +23 -4
  53. package/tx/params.js +35 -38
  54. package/tx/provider.js +6 -5
  55. package/tx/sct/expressions.js +12 -3
  56. package/tx/tx-html.js +80 -89
  57. package/tx/tx.fhir.org.yml +6 -5
  58. package/tx/tx.js +163 -13
  59. package/tx/vs/vs-database.js +56 -39
  60. package/tx/vs/vs-package.js +21 -2
  61. package/tx/vs/vs-vsac.js +175 -39
  62. package/tx/workers/batch-validate.js +2 -0
  63. package/tx/workers/batch.js +2 -0
  64. package/tx/workers/expand.js +132 -112
  65. package/tx/workers/lookup.js +33 -14
  66. package/tx/workers/metadata.js +2 -2
  67. package/tx/workers/read.js +3 -2
  68. package/tx/workers/related.js +574 -0
  69. package/tx/workers/search.js +46 -9
  70. package/tx/workers/subsumes.js +13 -3
  71. package/tx/workers/translate.js +7 -3
  72. package/tx/workers/validate.js +258 -285
  73. package/tx/workers/worker.js +43 -39
  74. package/tx/xml/bundle-xml.js +237 -0
  75. package/tx/xml/xml-base.js +215 -64
  76. package/tx/xversion/xv-bundle.js +71 -0
  77. package/tx/xversion/xv-capabiliityStatement.js +137 -0
  78. package/tx/xversion/xv-codesystem.js +169 -0
  79. package/tx/xversion/xv-conceptmap.js +224 -0
  80. package/tx/xversion/xv-namingsystem.js +88 -0
  81. package/tx/xversion/xv-operationoutcome.js +27 -0
  82. package/tx/xversion/xv-parameters.js +87 -0
  83. package/tx/xversion/xv-resource.js +45 -0
  84. package/tx/xversion/xv-terminologyCapabilities.js +214 -0
  85. package/tx/xversion/xv-valueset.js +234 -0
  86. package/utilities/dev-proxy-server.js +126 -0
  87. package/utilities/explode-results.js +58 -0
  88. package/utilities/split-by-system.js +198 -0
  89. package/utilities/vsac-cs-fetcher.js +0 -0
  90. package/{windows-install.js → utilities/windows-install.js} +2 -0
  91. package/vcl/vcl.js +0 -18
  92. 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 = config.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(config.cacheFolder, 'vsac-valuesets.db');
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
- let totalFetched = 0;
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(`Fetching page: ${url}`);
129
- const bundle = await this._fetchBundle(url);
136
+ console.log(`Sync: ${count} of ${total} - ${ncount} new`);
137
+ this.stats.task('VSAC Sync', `Sync: ${count} of ${total} - ${ncount} new`);
130
138
 
131
- if (bundle.entry && bundle.entry.length > 0) {
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
- if (valueSets.length > 0) {
138
- await this.database.batchUpsertValueSets(valueSets);
139
- totalFetched += valueSets.length;
140
- console.log(`Processed ${valueSets.length} ValueSets (total: ${totalFetched})`);
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 (bundle.total && totalFetched >= bundle.total) {
149
- console.log(`Reached total count (${bundle.total}), stopping`);
159
+ if (count > total) {
160
+ console.log(`Reached total count (${total}), stopping`);
150
161
  break;
151
162
  }
152
163
  }
153
164
 
154
- // Clean up old records
155
- const deletedCount = await this.database.deleteOldValueSets(refreshStartTime);
156
- if (deletedCount > 0) {
157
- console.log(`Deleted ${deletedCount} old ValueSets`);
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
- throw new Error(`Value set not found: ${url} version ${version}`);
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
  }
@@ -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