fhirsmith 0.5.2 → 0.5.4

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/tx/params.js CHANGED
@@ -36,6 +36,7 @@ class TxParameters {
36
36
  validating = false;
37
37
  abstractOk = true; // note true!
38
38
  inferSystem = false;
39
+ sort = 'design';
39
40
 
40
41
  constructor(languages, i18n, validating) {
41
42
  validateParameter(languages, 'languages', LanguageDefinitions);
@@ -242,6 +243,10 @@ class TxParameters {
242
243
  if (getValuePrimitive(p) == true) this.inferSystem = true;
243
244
  break;
244
245
  }
246
+ case 'sort': {
247
+ this.sort = getValuePrimitive(p);
248
+ break;
249
+ }
245
250
  case "exclude-system": {
246
251
  throw new Issue('error', 'not-supported', null, null, "The parameter 'exclude-system' is not supported by this system", null, 400);
247
252
  }
@@ -524,7 +529,7 @@ class TxParameters {
524
529
  this.FUid + '|' + b(this.FMembershipOnly) + '|' + this.FProperties.join(',') + '|' +
525
530
  b(this.FActiveOnly) + b(this.FDisplayWarning) + b(this.FExcludeNested) + b(this.FGenerateNarrative) + b(this.FExcludeNotForUI) + b(this.FExcludePostCoordinated) +
526
531
  b(this.FIncludeDesignations) + b(this.FIncludeDefinition) + b(this.hasActiveOnly) + b(this.hasExcludeNested) + b(this.hasGenerateNarrative) +
527
- b(this.hasExcludeNotForUI) + b(this.hasExcludePostCoordinated) + b(this.hasIncludeDesignations) +
532
+ b(this.hasExcludeNotForUI) + b(this.hasExcludePostCoordinated) + b(this.hasIncludeDesignations) + this.sort+'|'+
528
533
  b(this.hasIncludeDefinition) + b(this.hasDefaultToLatestVersion) + b(this.hasDisplayWarning) + b(this.hasExcludeNotForUI) + b(this.hasMembershipOnly) + b(this.FDefaultToLatestVersion);
529
534
 
530
535
  if (this.hasHTTPLanguages) {
@@ -585,6 +590,7 @@ class TxParameters {
585
590
  this.hasDefaultToLatestVersion = other.hasDefaultToLatestVersion;
586
591
  this.hasMembershipOnly = other.hasMembershipOnly;
587
592
  this.hasDisplayWarning = other.hasDisplayWarning;
593
+ this.sort = other.sort;
588
594
 
589
595
  if (other.FProperties) {
590
596
  this.FProperties = [...other.FProperties];
package/tx/problems.js ADDED
@@ -0,0 +1,90 @@
1
+ const escape = require('escape-html');
2
+
3
+ class ProblemFinder {
4
+
5
+ constructor() {
6
+ this.map = new Map();
7
+ }
8
+
9
+ async scanValueSets(provider) {
10
+ let unknownVersions = {}; // system -> Set of versions not known to the server
11
+ for (let vsp of provider.valueSetProviders) {
12
+ let sourceUnknownVersions = unknownVersions[vsp.sourcePackage()];
13
+ if (!sourceUnknownVersions) {
14
+ sourceUnknownVersions = {};
15
+ unknownVersions[vsp.sourcePackage()] = sourceUnknownVersions;
16
+ }
17
+
18
+ let list = await vsp.listAllValueSets();
19
+ for (let url of list) {
20
+ let vs = await vsp.fetchValueSet(url);
21
+ if (vs && vs.jsonObj.compose) {
22
+ await this.scanValueSet(vs.jsonObj.compose, sourceUnknownVersions);
23
+ }
24
+ }
25
+ }
26
+ let result = '';
27
+ for (let sp of provider.listValueSetSourceCodes()) {
28
+ let sourceUnknownVersions = unknownVersions[sp];
29
+ if (sourceUnknownVersions) {
30
+ // Filter to only versions the server doesn't know about
31
+ for (const [system, vset] of Object.entries(sourceUnknownVersions)) {
32
+ for (let v of [...vset]) {
33
+ if (await provider.hasCsVersion(system, v)) {
34
+ vset.delete(v);
35
+ }
36
+ }
37
+ if (vset.size === 0) {
38
+ delete sourceUnknownVersions[system];
39
+ }
40
+ }
41
+ let list = await this.unknownVersionsHtml(sourceUnknownVersions, provider, sp);
42
+ if (list) {
43
+ result = result + `<h4>${sp}</h4>` + list;
44
+ }
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+
50
+ async unknownVersionsHtml(unknownVersions, provider, source) {
51
+ const entries = Object.entries(unknownVersions || {});
52
+ if (entries.length === 0) {
53
+ return '<p>No unknown system versions found.</p>';
54
+ }
55
+ entries.sort((a, b) => a[0].localeCompare(b[0]));
56
+ let html = '<table class="grid"><thead><tr><th>System</th><th>Unknown Versions</th><th>Known Versions</th></tr></thead><tbody>';
57
+ for (const [system, vset] of entries) {
58
+ const systemEsc = escape(system);
59
+ const versions = [...vset].sort((a, b) => a.localeCompare(b));
60
+ const versionRefs = [];
61
+ for (const v of versions) {
62
+ versionRefs.push(`<a href="ValueSet?system=${systemEsc}|${escape(v)}&source=${source}&_elements=url%2Cversion%2Cname%2Ctitle%2Cstatus%2Ccontent%2Cdate">${v}</a>`);
63
+ }
64
+ const knownVersions = [...await provider.listCodeSystemVersions(system)].join('<br/>');
65
+ html += `<tr><td><a href="CodeSystem?url=${systemEsc}&_elements=url%2Cversion%2Cname%2Ctitle%2Cstatus%2Ccontent%2Cdate">${system}</a></td>`+
66
+ `<td>${versionRefs.join('<br/>')}</td><td>${knownVersions}</td></tr>`;
67
+ }
68
+ html += '</tbody></table>';
69
+ return html;
70
+ }
71
+
72
+ async scanValueSet(compose, versions) {
73
+ for (let inc of compose.include || []) {
74
+ if (inc.system && inc.version) {
75
+ this.seeVersion(versions, inc.system, inc.version);
76
+ }
77
+ }
78
+ }
79
+
80
+ seeVersion(versions, system, version) {
81
+ let set = versions[system];
82
+ if (set == null) {
83
+ set = new Set();
84
+ versions[system] = set;
85
+ }
86
+ set.add(version);
87
+ }
88
+ }
89
+
90
+ module.exports = ProblemFinder;
package/tx/provider.js CHANGED
@@ -218,9 +218,16 @@ class Provider {
218
218
  await csp.findImplicitConceptMap(url, version);
219
219
  }
220
220
  }
221
-
222
221
  }
223
222
 
223
+ listValueSetSourceCodes() {
224
+ let result = [];
225
+ for (let vsp of this.valueSetProviders) {
226
+ result.push(vsp.sourcePackage());
227
+ }
228
+ result.sort((a, b) => {a.localeCompare(b)});
229
+ return result;
230
+ }
224
231
 
225
232
  async listCodeSystemVersions(url) {
226
233
  let result = new Set();
@@ -399,6 +406,20 @@ class Provider {
399
406
  return null;
400
407
  }
401
408
 
409
+ async hasCsVersion(system, version) {
410
+ for (let cs of this.codeSystems.values()) {
411
+ if (cs.url == system && cs.version == version) {
412
+ return true;
413
+ }
414
+ }
415
+ for (let cp of this.codeSystemFactories.values()) {
416
+ if (cp.system() == system && cp.version() == version) {
417
+ return true;
418
+ }
419
+ }
420
+ return false;
421
+ }
422
+
402
423
  }
403
424
 
404
425
  module.exports = { Provider };
package/tx/tx-html.js CHANGED
@@ -137,7 +137,7 @@ class TxHtmlRenderer {
137
137
 
138
138
  // eslint-disable-next-line no-unused-vars
139
139
  async buildSearchForm(req, mode, params) {
140
- const html = await this.liquid.renderFile('search-form', { baseUrl: escape(req.baseUrl) });
140
+ const html = await this.liquid.renderFile('search-form', { baseUrl: escape(req.baseUrl), sourceOptions : this.buildSourceOptions(req.txProvider) });
141
141
  return html;
142
142
  }
143
143
 
@@ -800,14 +800,128 @@ class TxHtmlRenderer {
800
800
  return firstEntry?.resourceType || 'Resource';
801
801
  }
802
802
 
803
+ /**
804
+ * Build a human-readable description of what this search bundle represents,
805
+ * by parsing the self link URL parameters.
806
+ */
807
+ describeSearchBundle(json) {
808
+ const selfLink = json.link?.find(l => l.relation === 'self')?.url || '';
809
+ if (!selfLink) return '';
810
+
811
+ let urlObj;
812
+ try {
813
+ urlObj = new URL(selfLink);
814
+ } catch {
815
+ return '';
816
+ }
817
+
818
+ // Extract resource type from path
819
+ const typeMatch = selfLink.match(/\/(CodeSystem|ValueSet|ConceptMap)\b/);
820
+ const resourceType = typeMatch ? typeMatch[1] : 'Resource';
821
+
822
+ // Human-friendly labels for search params
823
+ const PARAM_LABELS = {
824
+ 'url': 'URL',
825
+ 'version': 'Version',
826
+ 'name': 'Name',
827
+ 'title': 'Title',
828
+ 'status': 'Status',
829
+ 'publisher': 'Publisher',
830
+ 'description': 'Description',
831
+ 'identifier': 'Identifier',
832
+ 'jurisdiction': 'Jurisdiction',
833
+ 'date': 'Date',
834
+ 'text': 'Text',
835
+ 'system': 'System',
836
+ 'supplements': 'Supplements',
837
+ 'content-mode': 'Content mode',
838
+ 'source': 'Source'
839
+ };
840
+
841
+ const WORDS = {
842
+ 'url': 'contains',
843
+ 'version': 'contains',
844
+ 'name': 'contains',
845
+ 'title': 'contains',
846
+ 'status': 'is',
847
+ 'publisher': 'contains',
848
+ 'description': 'contains',
849
+ 'identifier': 'matches',
850
+ 'jurisdiction': 'contains',
851
+ 'date': 'matches',
852
+ 'text': 'contains',
853
+ 'system': 'matches',
854
+ 'supplements': 'matches',
855
+ 'content-mode': 'is',
856
+ 'source': 'is'
857
+ };
858
+
859
+ const CONTROL_PARAMS = new Set(['_offset', '_count', '_sort', '_summary', '_elements', '_total']);
860
+
861
+ // Collect filter criteria
862
+ const criteria = [];
863
+ for (const [key, value] of urlObj.searchParams) {
864
+ if (key != 'mode') {
865
+ if (CONTROL_PARAMS.has(key) || !value) continue;
866
+ const label = PARAM_LABELS[key] || key;
867
+ const word = WORDS[key] || "contains";
868
+ criteria.push(`<strong>${escape(label)}</strong> ${word} &ldquo;${escape(value)}&rdquo;`);
869
+ }
870
+ }
871
+
872
+ // Collect display/pagination context
873
+ const total = json.total;
874
+ const offset = parseInt(urlObj.searchParams.get('_offset') || '0');
875
+ const count = parseInt(urlObj.searchParams.get('_count') || '20');
876
+ const sort = urlObj.searchParams.get('_sort');
877
+ const summary = urlObj.searchParams.get('_summary');
878
+ const elementsParam = urlObj.searchParams.get('_elements');
879
+
880
+ // Build the description sentence
881
+ let desc = `Searching <strong>${escape(resourceType)}s</strong>`;
882
+
883
+ if (criteria.length > 0) {
884
+ desc += ' where ' + criteria.join(', ');
885
+ } else {
886
+ desc += ' (all)';
887
+ }
888
+
889
+ // Pagination context
890
+ if (typeof total === 'number') {
891
+ const from = Math.min(offset + 1, total);
892
+ const to = Math.min(offset + count, total);
893
+ if (total === 0) {
894
+ desc += ' — <strong>no results found</strong>';
895
+ } else {
896
+ desc += ` — showing <strong>${from}–${to}</strong> of <strong>${total}</strong>`;
897
+ }
898
+ }
899
+
900
+ // Sort
901
+ if (sort) {
902
+ desc += `, sorted by <strong>${escape(sort)}</strong>`;
903
+ }
904
+
905
+ // Summary mode
906
+ if (summary && summary !== 'false') {
907
+ desc += ` [summary: ${escape(summary)}]`;
908
+ }
909
+
910
+ // Elements
911
+ if (elementsParam) {
912
+ desc += ` [fields: ${escape(elementsParam)}]`;
913
+ }
914
+
915
+ return `<p class="search-description">${desc}</p>`;
916
+ }
917
+
803
918
  /**
804
919
  * Render search results as a table (when _elements is specified)
805
920
  */
806
921
  async renderSearchTable(json, elements, req) {
807
922
  const entries = json.entry || [];
808
- const total = json.total || 0;
809
923
 
810
- let html = `<p>Found ${total} result(s)</p>`;
924
+ let html = this.describeSearchBundle(json);
811
925
 
812
926
  // Pagination links
813
927
  html += this.renderPaginationLinks(json);
@@ -859,22 +973,12 @@ class TxHtmlRenderer {
859
973
  */
860
974
  async renderSearchSummary(json, req) {
861
975
  const entries = json.entry || [];
862
- const total = json.total || 0;
863
976
 
864
- let html = `<p>Found ${total} result(s)</p>`;
977
+ let html = this.describeSearchBundle(json);
865
978
 
866
979
  // Pagination links
867
980
  html += this.renderPaginationLinks(json);
868
981
 
869
- // Bundle summary
870
- html += '<div class="card mb-3">';
871
- html += '<div class="card-header">Bundle Summary</div>';
872
- html += '<div class="card-body">';
873
- html += `<p><strong>Type:</strong> ${escape(json.type)}</p>`;
874
- html += `<p><strong>Total:</strong> ${total}</p>`;
875
- html += '</div>';
876
- html += '</div>';
877
-
878
982
  // Each entry
879
983
  for (const entry of entries) {
880
984
  html += '<hr/>';
@@ -1010,7 +1114,7 @@ class TxHtmlRenderer {
1010
1114
  html += '<div class="narrative">(No Narrative)</div>';
1011
1115
  }
1012
1116
  if (json.text && json.text.div) {
1013
- // Collapsible JSON source
1117
+ // Collapsible JSON source
1014
1118
  html += '<div class="xhtml">';
1015
1119
  html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleOriginalNarrative('${resourceId}x')">`;
1016
1120
  html += 'Show Original Narrative</button>';
@@ -1047,8 +1151,17 @@ class TxHtmlRenderer {
1047
1151
  valueSetsJson: JSON.stringify(json.valueSets || [])
1048
1152
  });
1049
1153
  }
1154
+
1155
+ buildSourceOptions(provider) {
1156
+ let result = '';
1157
+ result += `<option value="internal">internal</option>`;
1158
+ for (let sp of provider.listValueSetSourceCodes()) {
1159
+ result += `<option value="${sp}">${sp}</option>`;
1160
+ }
1161
+ return result;
1162
+ }
1050
1163
  }
1051
1164
 
1052
1165
  module.exports = {
1053
- TxHtmlRenderer, loadTemplate
1054
- };
1166
+ TxHtmlRenderer, loadTemplate
1167
+ };
@@ -26,6 +26,7 @@ sources:
26
26
  - snomed:sct_nl_20240930.cache
27
27
  - snomed:sct_uk_20230412.cache
28
28
  - snomed:sct_us_20230301.cache
29
+ - snomed:sct_us_20240301.cache
29
30
  - snomed:sct_us_20250901.cache
30
31
  - snomed:sct_test_20250814.cache
31
32
  - cpt:CodeSystem-cpt.db|cpt-2023-fragment-0.1.db
package/tx/tx.js CHANGED
@@ -20,7 +20,7 @@ const packageJson = require("../package.json");
20
20
  // Import workers
21
21
  const ReadWorker = require('./workers/read');
22
22
  const SearchWorker = require('./workers/search');
23
- const { ExpandWorker } = require('./workers/expand');
23
+ const { ExpandWorker, INTERNAL_DEFAULT_LIMIT, EXTERNAL_DEFAULT_LIMIT} = require('./workers/expand');
24
24
  const { ValidateWorker } = require('./workers/validate');
25
25
  const TranslateWorker = require('./workers/translate');
26
26
  const LookupWorker = require('./workers/lookup');
@@ -48,6 +48,8 @@ const {bundleFromR5} = require("./xversion/xv-bundle");
48
48
  const {convertResourceToR5} = require("./xversion/xv-resource");
49
49
  const ClosureWorker = require("./workers/closure");
50
50
  const {BundleXML} = require("./xml/bundle-xml");
51
+ const ConceptUsageTracker = require("./usage-tracker");
52
+ const ProblemFinder = require("./problems");
51
53
  // const {writeFileSync} = require("fs");
52
54
 
53
55
  class TXModule {
@@ -138,6 +140,7 @@ class TXModule {
138
140
  consoleErrors: config.consoleErrors,
139
141
  telnetErrors: config.telnetErrors
140
142
  });
143
+ this.usageTracker = new ConceptUsageTracker();
141
144
 
142
145
  this.log.info('Initializing TX module');
143
146
 
@@ -280,6 +283,7 @@ class TXModule {
280
283
  acceptLanguage, this.i18n, requestId, 30,
281
284
  endpointInfo.resourceCache, endpointInfo.expansionCache
282
285
  );
286
+ opContext.usageTracker = this.usageTracker;
283
287
 
284
288
  // Attach everything to request
285
289
  req.txProvider = endpointInfo.provider;
@@ -448,6 +452,14 @@ class TXModule {
448
452
  // Set up routes
449
453
  this.setupRoutes(router);
450
454
 
455
+ // Redirect /r5 → /r5/
456
+ app.use((req, res, next) => {
457
+ if (req.path === endpointPath) {
458
+ return res.redirect(301, endpointPath + '/');
459
+ }
460
+ next();
461
+ });
462
+
451
463
  // Register the router with the app
452
464
  app.use(endpointPath, router);
453
465
  this.routers.set(endpointPath, router);
@@ -609,7 +621,7 @@ class TXModule {
609
621
  router.get('/ValueSet/\\$expand', async (req, res) => {
610
622
  const start = Date.now();
611
623
  try {
612
- let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
624
+ let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
613
625
  await worker.handle(req, res, this.log);
614
626
  } finally {
615
627
  this.countRequest('$expand', Date.now() - start);
@@ -618,7 +630,7 @@ class TXModule {
618
630
  router.post('/ValueSet/\\$expand', async (req, res) => {
619
631
  const start = Date.now();
620
632
  try {
621
- let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
633
+ let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
622
634
  await worker.handle(req, res, this.log);
623
635
  } finally {
624
636
  this.countRequest('$expand', Date.now() - start);
@@ -773,7 +785,7 @@ class TXModule {
773
785
  router.get('/ValueSet/:id/\\$expand', async (req, res) => {
774
786
  const start = Date.now();
775
787
  try {
776
- let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
788
+ let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
777
789
  await worker.handleInstance(req, res, this.log);
778
790
  } finally {
779
791
  this.countRequest('$expand', Date.now() - start);
@@ -782,7 +794,7 @@ class TXModule {
782
794
  router.post('/ValueSet/:id/\\$expand', async (req, res) => {
783
795
  const start = Date.now();
784
796
  try {
785
- let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
797
+ let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
786
798
  await worker.handleInstance(req, res, this.log);
787
799
  } finally {
788
800
  this.countRequest('$expand', Date.now() - start);
@@ -882,6 +894,20 @@ class TXModule {
882
894
  }
883
895
  });
884
896
 
897
+ router.get('/problems.html', async (req, res) => {
898
+ const start = Date.now();
899
+ try {
900
+ let txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid);
901
+ const problemFinder = new ProblemFinder();
902
+ const content = await problemFinder.scanValueSets(req.txProvider);
903
+ const html = await txhtml.renderPage('Problems', '<h3>ValueSet dependencies on unknown CodeSystem/Versions</h3>'+content, req.txEndpoint, req.txStartTime);
904
+ res.setHeader('Content-Type', 'text/html');
905
+ res.send(html);
906
+ } finally {
907
+ this.countRequest('problems', Date.now() - start);
908
+ }
909
+ });
910
+
885
911
  // Metadata / CapabilityStatement
886
912
  router.get('/metadata', async (req, res) => {
887
913
  const start = Date.now();
@@ -1072,6 +1098,16 @@ class TXModule {
1072
1098
  }
1073
1099
  }
1074
1100
 
1101
+ internalLimit(req) {
1102
+ let isTest = req.header("User-Agent") == 'Tools/Java';
1103
+ if (this.config.internalLimit && !isTest) return this.config.internalLimit; else return INTERNAL_DEFAULT_LIMIT;
1104
+ }
1105
+
1106
+ externalLimit(req) {
1107
+ let isTest = req.header("User-Agent") == 'Tools/Java';
1108
+ if (this.config.internalLimit && !isTest) return this.config.externalLimit; else return EXTERNAL_DEFAULT_LIMIT;
1109
+ }
1110
+
1075
1111
  }
1076
1112
 
1077
1113
  module.exports = TXModule;
@@ -0,0 +1,70 @@
1
+
2
+ class ConceptUsageTracker {
3
+
4
+ constructor() {
5
+ this.map = new Map();
6
+ }
7
+
8
+ async scanValueSets(library) {
9
+ let c = 0;
10
+ for (let vsp of library.valueSetProviders) {
11
+ let list = await vsp.listAllValueSets();
12
+ for (let url of list) {
13
+ let vs = await vsp.fetchValueSet(url);
14
+ if (vs && vs.jsonObj.compose) {
15
+ if (await this.scanValueSet(vs.jsonObj.compose)) {
16
+ c++;
17
+ }
18
+ }
19
+ }
20
+ }
21
+ return c;
22
+ }
23
+
24
+ async scanValueSet(compose, versions, active) {
25
+ let ok = false;
26
+ for (let inc of compose.include || []) {
27
+ if (inc.system) {
28
+ if (active && inc.version) {
29
+ this.seeVersion(versions, inc.system, inc.version);
30
+ }
31
+ for (let c of inc.concept || []) {
32
+ if (c.code) {
33
+ ok = true;
34
+ this.seeConcept(inc.system, c.code);
35
+ }
36
+ }
37
+ }
38
+ }
39
+ return ok;
40
+ }
41
+
42
+ seeConcept(system, code) {
43
+ let cs = this.map.get(system);
44
+ if (!cs) {
45
+ cs = new Map();
46
+ this.map.set(system, cs);
47
+ }
48
+ let ci = cs.get(code);
49
+ if (!ci) {
50
+ ci = { count : 0 }
51
+ cs.set(code, ci);
52
+ }
53
+ ci.count++;
54
+ }
55
+
56
+ usages(system) {
57
+ return this.map.get(system) || null;
58
+ }
59
+
60
+ seeVersion(versions, system, version) {
61
+ let set = versions[system];
62
+ if (set == null) {
63
+ set = new Set();
64
+ versions[system] = set;
65
+ }
66
+ set.add(version);
67
+ }
68
+ }
69
+
70
+ module.exports = ConceptUsageTracker;
package/tx/vs/vs-api.js CHANGED
@@ -8,6 +8,9 @@ class AbstractValueSetProvider {
8
8
  */
9
9
  spaceId;
10
10
 
11
+ code() {
12
+ throw new Error('code must be implemented by AbstractValueSetProvider subclass');
13
+ }
11
14
  /**
12
15
  * ensure that the ids on the value sets are unique, if they are
13
16
  * in the global namespace
@@ -37,7 +37,7 @@ class ValueSetDatabase {
37
37
  this._db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY, (err) => {
38
38
  if (err) {
39
39
  this._db = null;
40
- reject(new Error(`Failed to open database: ${err.message}`));
40
+ reject(new Error(`Failed to open database ${this.dbPath}: ${err.message}`));
41
41
  } else {
42
42
  resolve(this._db);
43
43
  }
@@ -122,7 +122,7 @@ class ValueSetDatabase {
122
122
  return new Promise((resolve, reject) => {
123
123
  const db = new sqlite3.Database(this.dbPath, (err) => {
124
124
  if (err) {
125
- reject(new Error(`Failed to create database: ${err.message}`));
125
+ reject(new Error(`Failed to create database ${this.dbPath}: ${err.message}`));
126
126
  return;
127
127
  }
128
128
 
@@ -177,6 +177,7 @@ class ValueSetDatabase {
177
177
  CREATE TABLE valueset_systems (
178
178
  valueset_id TEXT,
179
179
  system TEXT,
180
+ version TEXT,
180
181
  FOREIGN KEY (valueset_id) REFERENCES valuesets(url)
181
182
  )
182
183
  `);
@@ -193,7 +194,7 @@ class ValueSetDatabase {
193
194
  db.run('CREATE INDEX idx_identifiers_value ON valueset_identifiers(value)');
194
195
  db.run('CREATE INDEX idx_jurisdictions_system ON valueset_jurisdictions(system)');
195
196
  db.run('CREATE INDEX idx_jurisdictions_code ON valueset_jurisdictions(code)');
196
- db.run('CREATE INDEX idx_systems_system ON valueset_systems(system)');
197
+ db.run('CREATE INDEX idx_systems_system ON valueset_systems(system, version)');
197
198
 
198
199
  db.close((err) => {
199
200
  if (err) {
@@ -392,8 +393,8 @@ class ValueSetDatabase {
392
393
  pendingOperations++;
393
394
 
394
395
  db.run(`
395
- INSERT INTO valueset_systems (valueset_id, system) VALUES (?, ?)
396
- `, [valueSet.id, include.system], function(err) {
396
+ INSERT INTO valueset_systems (valueset_id, system, version) VALUES (?, ?, ?)
397
+ `, [valueSet.id, include.system, include.version], function(err) {
397
398
  if (err) {
398
399
  operationError(new Error(`Failed to insert system: ${err.message}`));
399
400
  } else {
@@ -735,8 +736,15 @@ class ValueSetDatabase {
735
736
 
736
737
  case 'system':
737
738
  joins.add('JOIN valueset_systems vs ON v.id = vs.valueset_id');
738
- conditions.push('vs.system = ?');
739
- params.push(value);
739
+ if (value.includes('|')) {
740
+ conditions.push('vs.system = ?');
741
+ params.push(value.substring(0, value.indexOf('|')));
742
+ conditions.push('vs.version = ?');
743
+ params.push(value.substring(value.indexOf('|')+1));
744
+ } else {
745
+ conditions.push('vs.system = ?');
746
+ params.push(value);
747
+ }
740
748
  break;
741
749
 
742
750
  default:
@@ -23,6 +23,11 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
23
23
  this.valueSetMap = new Map();
24
24
  this.initialized = false;
25
25
  this.count = 0;
26
+ this.sourcePackageCode = packageLoader.id();
27
+ }
28
+
29
+ sourcePackage() {
30
+ return this.sourcePackageCode;
26
31
  }
27
32
 
28
33
  /**
@@ -42,7 +47,7 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
42
47
  await this._populateDatabase();
43
48
  }
44
49
 
45
- this.valueSetMap = await this.database.loadAllValueSets(this.packageLoader.pid());
50
+ this.valueSetMap = await this.database.loadAllValueSets(this.sourcePackage());
46
51
  this.initialized = true;
47
52
  }
48
53