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/tx.js CHANGED
@@ -36,6 +36,19 @@ const {ConceptMapXML} = require("./xml/conceptmap-xml");
36
36
  const {TxHtmlRenderer} = require("./tx-html");
37
37
  const {Renderer} = require("./library/renderer");
38
38
  const {OperationsWorker} = require("./workers/operations");
39
+ const {RelatedWorker} = require("./workers/related");
40
+ const {codeSystemFromR5} = require("./xversion/xv-codesystem");
41
+ const {operationOutcomeFromR5} = require("./xversion/xv-operationoutcome");
42
+ const {parametersFromR5} = require("./xversion/xv-parameters");
43
+ const {conceptMapFromR5} = require("./xversion/xv-conceptmap");
44
+ const {valueSetFromR5} = require("./xversion/xv-valueset");
45
+ const {terminologyCapabilitiesFromR5} = require("./xversion/xv-terminologyCapabilities");
46
+ const {capabilityStatementFromR5} = require("./xversion/xv-capabiliityStatement");
47
+ const {bundleFromR5} = require("./xversion/xv-bundle");
48
+ const {convertResourceToR5} = require("./xversion/xv-resource");
49
+ const ClosureWorker = require("./workers/closure");
50
+ const {BundleXML} = require("./xml/bundle-xml");
51
+ // const {writeFileSync} = require("fs");
39
52
 
40
53
  class TXModule {
41
54
  timers = [];
@@ -69,10 +82,48 @@ class TXModule {
69
82
  }
70
83
 
71
84
  acceptsXml(req) {
72
- const accept = req.headers.accept || '';
73
- return accept.includes('application/fhir+xml') || accept.includes('application/xml+fhir');
85
+ let _fmt = req.query._format || req.query.format || req.body?._format;
86
+ if (_fmt && typeof _fmt !== 'string') {
87
+ _fmt = null;
88
+ }
89
+ if (_fmt && _fmt == 'xml') {
90
+ return 'application/fhir+xml';
91
+ }
92
+ if (!_fmt) {
93
+ _fmt = req.headers.accept || '';
94
+ }
95
+ if (_fmt.includes('application/fhir+xml')) {
96
+ return 'application/fhir+xml';
97
+ } else if (_fmt.includes('application/xml+fhir')) {
98
+ return 'application/xml+fhir';
99
+ } else if (_fmt.includes('application/xml')) {
100
+ return 'application/xml';
101
+ } else {
102
+ return null;
103
+ }
74
104
  }
75
105
 
106
+ acceptsJson(req) {
107
+ let _fmt = req.query._format || req.query.format || req.body?._format;
108
+ if (_fmt && typeof _fmt !== 'string') {
109
+ _fmt = null;
110
+ }
111
+ if (_fmt && _fmt == 'json') {
112
+ return 'application/fhir+json';
113
+ }
114
+ if (!_fmt) {
115
+ _fmt = req.headers.accept || '';
116
+ }
117
+ if (_fmt.includes('application/fhir+json')) {
118
+ return 'application/fhir+json';
119
+ } else if (_fmt.includes('application/json+fhir')) {
120
+ return 'application/json+fhir';
121
+ } else if (_fmt.includes('application/json')) {
122
+ return 'application/json';
123
+ } else {
124
+ return 'application/fhir+json';
125
+ }
126
+ }
76
127
 
77
128
  /**
78
129
  * Initialize the TX module
@@ -103,9 +154,9 @@ class TXModule {
103
154
  }
104
155
 
105
156
  // Load language definitions
106
- const langPath = path.join(__dirname, 'data', 'lang.dat');
157
+ const langPath = path.join(__dirname, 'data');
107
158
  this.log.info(`Loading language definitions from: ${langPath}`);
108
- this.languages = await LanguageDefinitions.fromFile(langPath);
159
+ this.languages = await LanguageDefinitions.fromFiles(langPath);
109
160
  this.log.info('Language definitions loaded');
110
161
 
111
162
  // Initialize i18n support
@@ -131,7 +182,7 @@ class TXModule {
131
182
 
132
183
  // Load the library from YAML
133
184
  this.log.info(`Loading library from: ${config.librarySource}`);
134
- this.library = new Library(config.librarySource, this.log);
185
+ this.library = new Library(config.librarySource, config.vsacCfg, this.log, this.stats);
135
186
  this.log.info(`Load...`);
136
187
  await this.library.load();
137
188
  this.log.info('Library loaded successfully');
@@ -180,8 +231,8 @@ class TXModule {
180
231
  path: endpointPath,
181
232
  fhirVersion,
182
233
  context: context || null,
183
- resourceCache: new ResourceCache(),
184
- expansionCache: new ExpansionCache(expansionCacheSize, expansionCacheMemoryThreshold)
234
+ resourceCache: new ResourceCache(this.stats),
235
+ expansionCache: new ExpansionCache(this.stats, expansionCacheSize, expansionCacheMemoryThreshold)
185
236
  };
186
237
  // Create the provider once for this endpoint
187
238
  endpointInfo.provider = await this.library.cloneWithFhirVersion(fhirVersion, context, endpointPath);
@@ -190,6 +241,9 @@ class TXModule {
190
241
  // cacheTimeout is in minutes, default to 30 minutes
191
242
  const cacheTimeoutMs = cacheTimeoutMinutes * 60 * 1000;
192
243
  const pruneIntervalMs = 5 * 60 * 1000; // Run every 5 minutes
244
+ if (this.stats) {
245
+ this.stats.addTask("Client Cache", "5 min");
246
+ }
193
247
  this.timers.push(setInterval(() => {
194
248
  endpointInfo.resourceCache.prune(cacheTimeoutMs);
195
249
  }, pruneIntervalMs));
@@ -197,6 +251,9 @@ class TXModule {
197
251
 
198
252
  // Set up periodic memory pressure check for expansion cache (if threshold configured)
199
253
  if (expansionCacheMemoryThreshold > 0) {
254
+ if (this.stats) {
255
+ this.stats.addTask("Expansion Cache", "5 min");
256
+ }
200
257
  this.timers.push(setInterval(() => {
201
258
  if (endpointInfo.expansionCache.checkMemoryPressure()) {
202
259
  this.log.info(`Expansion cache memory pressure detected for ${endpointPath}, evicted oldest half`);
@@ -244,7 +301,9 @@ class TXModule {
244
301
  try {
245
302
  const duration = Date.now() - req.txStartTime;
246
303
  const isHtml = txhtml.acceptsHtml(req);
247
- const isXml = this.acceptsXml(req);
304
+ const xmlFmt = this.acceptsXml(req);
305
+ const jsonFmt = this.acceptsJson(req);
306
+ data = this.transformResourceForVersion(data, endpointInfo.fhirVersion);
248
307
 
249
308
  let responseSize;
250
309
  let result;
@@ -256,28 +315,31 @@ class TXModule {
256
315
  responseSize = Buffer.byteLength(html, 'utf8');
257
316
  res.setHeader('Content-Type', 'text/html');
258
317
  result = res.send(html);
259
- } else if (isXml) {
318
+ } else if (xmlFmt) {
260
319
  try {
261
320
  const xml = this.convertResourceToXml(data);
262
321
  responseSize = Buffer.byteLength(xml, 'utf8');
263
- res.setHeader('Content-Type', 'application/fhir+xml');
322
+ res.setHeader('Content-Type', xmlFmt);
264
323
  result = res.send(xml);
265
324
  } catch (err) {
266
325
  console.error(err);
267
326
  // Fall back to JSON if XML conversion not supported
268
327
  this.log.warn(`XML conversion failed for ${data.resourceType}: ${err.message}, falling back to JSON`);
328
+ res.setHeader('Content-Type', jsonFmt);
269
329
  const jsonStr = JSON.stringify(data);
270
330
  responseSize = Buffer.byteLength(jsonStr, 'utf8');
271
331
  result = originalJson(data);
272
332
  }
273
333
  } else {
274
334
  const jsonStr = JSON.stringify(data);
335
+ res.setHeader('Content-Type', jsonFmt);
336
+ this.checkProperJson(jsonStr);
275
337
  responseSize = Buffer.byteLength(jsonStr, 'utf8');
276
338
  result = originalJson(data);
277
339
  }
278
340
 
279
341
  // Log the request with request ID
280
- const format = isHtml ? 'html' : (isXml ? 'xml' : 'json');
342
+ const format = isHtml ? 'html' : (xmlFmt ? 'xml' : 'json');
281
343
  let li = req.logInfo ? "(" + req.logInfo + ")" : "";
282
344
  this.log.info(`[${requestId}] ${req.method} ${format} ${res.statusCode} ${duration}ms ${responseSize}: ${req.originalUrl} ${li})`);
283
345
 
@@ -364,11 +426,24 @@ class TXModule {
364
426
  });
365
427
  }
366
428
  }
429
+ } else if (contentType != 'application/x-www-form-urlencoded') {
430
+ return res.status(415).json({
431
+ resourceType: 'OperationOutcome',
432
+ issue: [{
433
+ severity: 'error',
434
+ code: 'invalid',
435
+ diagnostics: `Unsupported Media Type: ${contentType}`
436
+ }]
437
+ });
367
438
  }
368
439
 
440
+ if (req.body) {
441
+ req.body = convertResourceToR5(req.body, req.txEndpoint.fhirVersion);
442
+ }
369
443
  next();
370
444
  });
371
445
 
446
+ app.use(express.urlencoded({ extended: true }));
372
447
 
373
448
  // Set up routes
374
449
  this.setupRoutes(router);
@@ -490,6 +565,26 @@ class TXModule {
490
565
  }
491
566
  });
492
567
 
568
+ // ValueSet/$related(GET and POST)
569
+ router.get('/ValueSet/\\$related', async (req, res) => {
570
+ const start = Date.now();
571
+ try {
572
+ let worker = new RelatedWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
573
+ await worker.handle(req, res);
574
+ } finally {
575
+ this.countRequest('$related', Date.now() - start);
576
+ }
577
+ });
578
+ router.post('/ValueSet/\\$related', async (req, res) => {
579
+ const start = Date.now();
580
+ try {
581
+ let worker = new RelatedWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
582
+ await worker.handle(req, res);
583
+ } finally {
584
+ this.countRequest('$related', Date.now() - start);
585
+ }
586
+ });
587
+
493
588
  // ValueSet/$batch-validate-code (GET and POST)
494
589
  router.get('/ValueSet/\\$batch-validate-code', async (req, res) => {
495
590
  const start = Date.now();
@@ -554,7 +649,7 @@ class TXModule {
554
649
  router.get('/ConceptMap/\\$closure', async (req, res) => {
555
650
  const start = Date.now();
556
651
  try {
557
- let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
652
+ let worker = new ClosureWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
558
653
  await worker.handle(req, res, this.log);
559
654
  } finally {
560
655
  this.countRequest('$closure', Date.now() - start);
@@ -563,7 +658,7 @@ class TXModule {
563
658
  router.post('/ConceptMap/\\$closure', async (req, res) => {
564
659
  const start = Date.now();
565
660
  try {
566
- let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
661
+ let worker = new ClosureWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
567
662
  await worker.handle(req, res, this.log);
568
663
  } finally {
569
664
  this.countRequest('$closure', Date.now() - start);
@@ -653,6 +748,27 @@ class TXModule {
653
748
  }
654
749
  });
655
750
 
751
+
752
+ // ValueSet/[id]/$related
753
+ router.get('/ValueSet/:id/\\$related', async (req, res) => {
754
+ const start = Date.now();
755
+ try {
756
+ let worker = new RelatedWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
757
+ await worker.handleInstance(req, res, this.log);
758
+ } finally {
759
+ this.countRequest('$related', Date.now() - start);
760
+ }
761
+ });
762
+ router.post('/ValueSet/:id/\\$related', async (req, res) => {
763
+ const start = Date.now();
764
+ try {
765
+ let worker = new RelatedWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
766
+ await worker.handleInstance(req, res, this.log);
767
+ } finally {
768
+ this.countRequest('$related', Date.now() - start);
769
+ }
770
+ });
771
+
656
772
  // ValueSet/[id]/$expand
657
773
  router.get('/ValueSet/:id/\\$expand', async (req, res) => {
658
774
  const start = Date.now();
@@ -871,6 +987,7 @@ class TXModule {
871
987
  switch (res.resourceType) {
872
988
  case "CodeSystem" : return CodeSystemXML._jsonToXml(res);
873
989
  case "ValueSet" : return ValueSetXML.toXml(res);
990
+ case "Bundle" : return BundleXML.toXml(res, this.fhirVersion);
874
991
  case "CapabilityStatement" : return CapabilityStatementXML.toXml(res, "R5");
875
992
  case "TerminologyCapabilities" : return TerminologyCapabilitiesXML.toXml(res, "R5");
876
993
  case "Parameters": return ParametersXML.toXml(res, this.fhirVersion);
@@ -922,6 +1039,39 @@ class TXModule {
922
1039
  }
923
1040
  return count;
924
1041
  }
1042
+
1043
+ ec = 0;
1044
+
1045
+ checkProperJson() { // jsonStr) {
1046
+ // const errors = [];
1047
+ // if (jsonStr.includes("[]")) errors.push("Found [] in json");
1048
+ // if (jsonStr.includes('""')) errors.push('Found "" in json');
1049
+ //
1050
+ // if (errors.length > 0) {
1051
+ // this.ec++;
1052
+ // const filename = `/Users/grahamegrieve/temp/tx-err-log/err${this.ec}.json`;
1053
+ // writeFileSync(filename, jsonStr);
1054
+ // throw new Error(errors.join('; '));
1055
+ // }
1056
+ }
1057
+
1058
+ transformResourceForVersion(data, fhirVersion) {
1059
+ if (fhirVersion == "5.0" || !data.resourceType) {
1060
+ return data;
1061
+ }
1062
+ switch (data.resourceType) {
1063
+ case "CodeSystem": return codeSystemFromR5(data, fhirVersion);
1064
+ case "CapabilityStatement": return capabilityStatementFromR5(data, fhirVersion);
1065
+ case "TerminologyCapabilities": return terminologyCapabilitiesFromR5(data, fhirVersion);
1066
+ case "ValueSet": return valueSetFromR5(data, fhirVersion);
1067
+ case "ConceptMap": return conceptMapFromR5(data, fhirVersion);
1068
+ case "Parameters": return parametersFromR5(data, fhirVersion);
1069
+ case "OperationOutcome": return operationOutcomeFromR5(data, fhirVersion);
1070
+ case "Bundle": return bundleFromR5(data, fhirVersion);
1071
+ default: return data;
1072
+ }
1073
+ }
1074
+
925
1075
  }
926
1076
 
927
1077
  module.exports = TXModule;
@@ -2,7 +2,6 @@ const fs = require('fs').promises;
2
2
  const sqlite3 = require('sqlite3').verbose();
3
3
  const { VersionUtilities } = require('../../library/version-utilities');
4
4
  const ValueSet = require("../library/valueset");
5
- const row = require("../library/valueset");
6
5
 
7
6
  // Columns that can be returned directly without parsing JSON
8
7
  const INDEXED_COLUMNS = ['id', 'url', 'version', 'date', 'description', 'name', 'publisher', 'status', 'title'];
@@ -279,6 +278,37 @@ class ValueSetDatabase {
279
278
  });
280
279
  }
281
280
 
281
+ /**
282
+ * Just update the timestamp on the valueset
283
+ * @param {Object} valueSet - The ValueSet resource
284
+ * @returns {Promise<void>}
285
+ */
286
+ async seeValueSet(valueSet) {
287
+ if (!valueSet.url) {
288
+ throw new Error('ValueSet must have a url property');
289
+ }
290
+
291
+ const db = await this._getWriteConnection();
292
+
293
+ return new Promise((resolve, reject) => {
294
+ db.run(`
295
+ update valuesets
296
+ set last_seen = strftime('%s', 'now')
297
+ where url = ?
298
+ and version = ?
299
+ `, [
300
+ valueSet.url,
301
+ valueSet.version
302
+ ], (err) => {
303
+ if (err) {
304
+ reject(new Error(`Failed to update value Set: ${err.message}`));
305
+ return;
306
+ }
307
+ resolve();
308
+ });
309
+ });
310
+ }
311
+
282
312
  /**
283
313
  * Insert related records for a ValueSet
284
314
  * @param {sqlite3.Database} db - Database connection
@@ -380,22 +410,6 @@ class ValueSetDatabase {
380
410
  }
381
411
  }
382
412
 
383
- /**
384
- * Insert multiple ValueSets in a batch operation
385
- * @param {Array<Object>} valueSets - Array of ValueSet resources
386
- * @returns {Promise<void>}
387
- */
388
- async batchUpsertValueSets(valueSets) {
389
- if (valueSets.length === 0) {
390
- return;
391
- }
392
-
393
- // Process sequentially to avoid database locking
394
- for (const valueSet of valueSets) {
395
- await this.upsertValueSet(valueSet);
396
- }
397
- }
398
-
399
413
  /**
400
414
  * Load all ValueSets from the database
401
415
  * @returns {Promise<Map<string, Object>>} Map of all ValueSets keyed by various combinations
@@ -417,29 +431,8 @@ class ValueSetDatabase {
417
431
  for (const row of rows) {
418
432
  const valueSet = new ValueSet(JSON.parse(row.content));
419
433
  valueSet.sourcePackage = source;
420
-
421
434
  // Store by URL and id alone
422
- valueSetMap.set(row.url, valueSet);
423
- valueSetMap.set(row.id, valueSet);
424
-
425
- if (row.version) {
426
- // Store by url|version
427
- const versionKey = `${row.url}|${row.version}`;
428
- valueSetMap.set(versionKey, valueSet);
429
-
430
- // If version is semver, also store by url|major.minor
431
- try {
432
- if (VersionUtilities.isSemVer(row.version)) {
433
- const majorMinor = VersionUtilities.getMajMin(row.version);
434
- if (majorMinor) {
435
- const majorMinorKey = `${row.url}|${majorMinor}`;
436
- valueSetMap.set(majorMinorKey, valueSet);
437
- }
438
- }
439
- } catch (error) {
440
- // Ignore version parsing errors, just don't add major.minor key
441
- }
442
- }
435
+ this.addToMap(valueSetMap, row.id, row.url, row.version, valueSet);
443
436
  }
444
437
 
445
438
  resolve(valueSetMap);
@@ -450,6 +443,30 @@ class ValueSetDatabase {
450
443
  });
451
444
  }
452
445
 
446
+ addToMap(valueSetMap, id, url, version, valueSet) {
447
+ valueSetMap.set(url, valueSet);
448
+ valueSetMap.set(id, valueSet);
449
+
450
+ if (version) {
451
+ // Store by url|version
452
+ const versionKey = `${url}|${version}`;
453
+ valueSetMap.set(versionKey, valueSet);
454
+
455
+ // If version is semver, also store by url|major.minor
456
+ try {
457
+ if (VersionUtilities.isSemVer(version)) {
458
+ const majorMinor = VersionUtilities.getMajMin(version);
459
+ if (majorMinor) {
460
+ const majorMinorKey = `${url}|${majorMinor}`;
461
+ valueSetMap.set(majorMinorKey, valueSet);
462
+ }
463
+ }
464
+ } catch (error) {
465
+ // Ignore version parsing errors, just don't add major.minor key
466
+ }
467
+ }
468
+ }
469
+
453
470
  /**
454
471
  * Search for ValueSets based on criteria
455
472
  * @param {Array<{name: string, value: string}>} searchParams - Search criteria
@@ -71,10 +71,28 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
71
71
  }
72
72
 
73
73
  if (valueSets.length > 0) {
74
- await this.database.batchUpsertValueSets(valueSets);
74
+ await this.batchUpsertValueSets(valueSets);
75
75
  }
76
76
  }
77
77
 
78
+
79
+ /**
80
+ * Insert multiple ValueSets in a batch operation
81
+ * @param {Array<Object>} valueSets - Array of ValueSet resources
82
+ * @returns {Promise<void>}
83
+ */
84
+ async batchUpsertValueSets(valueSets) {
85
+ if (valueSets.length === 0) {
86
+ return;
87
+ }
88
+
89
+ // Process sequentially to avoid database locking
90
+ for (const valueSet of valueSets) {
91
+ await this.database.upsertValueSet(valueSet);
92
+ }
93
+ }
94
+
95
+
78
96
  /**
79
97
  * Fetches a value set by URL and version
80
98
  * @param {string} url - The canonical URL of the value set
@@ -107,7 +125,7 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
107
125
  }
108
126
 
109
127
  // Finally try just the URL
110
- if (this.valueSetMap.has(url)) {
128
+ if (!version && this.valueSetMap.has(url)) {
111
129
  return this.valueSetMap.get(url);
112
130
  }
113
131
 
@@ -325,6 +343,7 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
325
343
  // Get all current entries - we'll iterate and modify
326
344
  const entries = Array.from(this.valueSetMap.entries());
327
345
 
346
+ // eslint-disable-next-line no-unused-vars
328
347
  for (const [key, vs] of entries) {
329
348
  // Skip if we've already processed this ValueSet instance
330
349
  if (alreadyPrefixed.has(vs)) {