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/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
|
-
|
|
73
|
-
|
|
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'
|
|
157
|
+
const langPath = path.join(__dirname, 'data');
|
|
107
158
|
this.log.info(`Loading language definitions from: ${langPath}`);
|
|
108
|
-
this.languages = await LanguageDefinitions.
|
|
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
|
|
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 (
|
|
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',
|
|
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' : (
|
|
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
|
|
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
|
|
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;
|
package/tx/vs/vs-database.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/tx/vs/vs-package.js
CHANGED
|
@@ -71,10 +71,28 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
if (valueSets.length > 0) {
|
|
74
|
-
await this.
|
|
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)) {
|