fhirsmith 0.5.3 → 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/CHANGELOG.md CHANGED
@@ -5,7 +5,22 @@ All notable changes to the Health Intersections Node Server will be documented i
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [v0.5.2] - 2026-02-24
8
+ ## [v0.5.4] - 2026-02-25
9
+
10
+ This version requires that you delete all package content from the terminology-cache directly
11
+ by hand before running this version.
12
+
13
+ ### Changed
14
+ - Improved Problem page
15
+ - Ignore system version in VSAC value sets
16
+ - Improve value set search
17
+ - better handling of code systems without a content property
18
+
19
+ ### Tx Conformance Statement
20
+
21
+ FHIRsmith 0.5.4 passed all 1382 HL7 terminology service tests (modes tx.fhir.org,omop,general,snomed, tests v1.9.0, runner v6.8.1)
22
+
23
+ ## [v0.5.3] - 2026-02-24
9
24
 
10
25
  ### Added
11
26
  - Page listing logical problems in terminology definitions
package/README.md CHANGED
@@ -238,7 +238,7 @@ GitHub Actions will automatically:
238
238
  git push origin main:XXXXXX
239
239
  ```
240
240
 
241
- do it via a PR
241
+ or do it via a PR
242
242
 
243
243
  4. Tag and push the release:
244
244
  ```bash
@@ -907,7 +907,7 @@ class PackageContentLoader {
907
907
  }
908
908
 
909
909
  id() {
910
- return this.package.name;
910
+ return this.package?.name;
911
911
  }
912
912
 
913
913
  version() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhirsmith",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
5
5
  "main": "server.js",
6
6
  "engines": {
@@ -116,4 +116,4 @@
116
116
  "url": "https://github.com/HealthIntersections/fhirsmith/issues"
117
117
  },
118
118
  "homepage": "https://github.com/HealthIntersections/fhirsmith#readme"
119
- }
119
+ }
@@ -3,7 +3,7 @@
3
3
  <form method="get" id="searchForm" action="{{ baseUrl }}/CodeSystem">
4
4
  <table class="grid" cellpadding="0" cellspacing="0">
5
5
  <tr>
6
- <td colspan="2">URL: <input type="text" name="url" size="40"/></td>
6
+ <td>URL: <input type="text" name="url" size="40"/></td>
7
7
  <td>Version: <input type="text" name="version"/></td>
8
8
  </tr>
9
9
  <tr>
@@ -11,10 +11,13 @@
11
11
  <td>Status: <select name="status" class="form-select"><option value="">(any status)</option>
12
12
  <option value="draft">draft</option><option value="active">active</option>
13
13
  <option value="retired">retired</option><option value="unknown">unknown</option></select></td>
14
+ </tr>
15
+ <tr>
14
16
  <td>Language: <input type="text" name="lang" size="10"/> (ietf code)</td>
17
+ <td>Source: <select name="source" class="form-select">{{ sourceOptions }}</select></td>
15
18
  </tr>
16
19
  <tr>
17
- <td colspan="2" title="CodeSystem - for supplements, value sets, and concept maps">System: <input type="text" name="system" size="40"/></td>
20
+ <td title="CodeSystem - for supplements, value sets, and concept maps">System: <input type="text" name="system" size="40"/></td>
18
21
  <td>CS Content: <select name="content-mode" class="form-select"><option value="">(any content)</option>
19
22
  <option value="not-present">not-present</option><option value="example">example</option>
20
23
  <option value="fragment">fragment</option><option value="complete">complete</option>
package/tx/problems.js CHANGED
@@ -1,3 +1,4 @@
1
+ const escape = require('escape-html');
1
2
 
2
3
  class ProblemFinder {
3
4
 
@@ -8,49 +9,70 @@ class ProblemFinder {
8
9
  async scanValueSets(provider) {
9
10
  let unknownVersions = {}; // system -> Set of versions not known to the server
10
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
+
11
18
  let list = await vsp.listAllValueSets();
12
19
  for (let url of list) {
13
20
  let vs = await vsp.fetchValueSet(url);
14
21
  if (vs && vs.jsonObj.compose) {
15
- await this.scanValueSet(vs.jsonObj.compose, unknownVersions, vs.jsonObj.status != 'retired');
22
+ await this.scanValueSet(vs.jsonObj.compose, sourceUnknownVersions);
16
23
  }
17
24
  }
18
25
  }
19
- // Filter to only versions the server doesn't know about
20
- for (const [system, vset] of Object.entries(unknownVersions)) {
21
- for (let v of [...vset]) {
22
- if (await provider.hasCsVersion(system, v)) {
23
- vset.delete(v);
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;
24
44
  }
25
- }
26
- if (vset.size === 0) {
27
- delete unknownVersions[system];
28
45
  }
29
46
  }
30
- return this.unknownVersionsHtml(unknownVersions);
47
+ return result;
31
48
  }
32
49
 
33
- unknownVersionsHtml(unknownVersions) {
50
+ async unknownVersionsHtml(unknownVersions, provider, source) {
34
51
  const entries = Object.entries(unknownVersions || {});
35
52
  if (entries.length === 0) {
36
53
  return '<p>No unknown system versions found.</p>';
37
54
  }
38
55
  entries.sort((a, b) => a[0].localeCompare(b[0]));
39
- let html = '<table class="grid"><thead><tr><th>System</th><th>Unknown Versions</th></tr></thead><tbody>';
56
+ let html = '<table class="grid"><thead><tr><th>System</th><th>Unknown Versions</th><th>Known Versions</th></tr></thead><tbody>';
40
57
  for (const [system, vset] of entries) {
41
- const versions = [...vset].sort((a, b) => a.localeCompare(b)).join('<br/>');
42
- html += `<tr><td>${system}</td><td>${versions}</td></tr>`;
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>`;
43
67
  }
44
68
  html += '</tbody></table>';
45
69
  return html;
46
70
  }
47
71
 
48
- async scanValueSet(compose, versions, active) {
72
+ async scanValueSet(compose, versions) {
49
73
  for (let inc of compose.include || []) {
50
- if (inc.system) {
51
- if (active && inc.version) {
52
- this.seeVersion(versions, inc.system, inc.version);
53
- }
74
+ if (inc.system && inc.version) {
75
+ this.seeVersion(versions, inc.system, inc.version);
54
76
  }
55
77
  }
56
78
  }
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();
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
+ };
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
 
package/tx/vs/vs-vsac.js CHANGED
@@ -51,6 +51,10 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
51
51
  });
52
52
  }
53
53
 
54
+ sourcePackage() {
55
+ return "vsac";
56
+ }
57
+
54
58
  /**
55
59
  * Initialize the provider - setup database and start refresh cycle
56
60
  * @returns {Promise<void>}
@@ -313,8 +317,20 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
313
317
  * @private
314
318
  */
315
319
  async _reloadMap() {
316
- const newMap = await this.database.loadAllValueSets("vsac");
317
-
320
+ const newMap = await this.database.loadAllValueSets(this.sourcePackage());
321
+ for (const vs of newMap.values()) {
322
+ if (vs.jsonObj.compose) {
323
+ for (const inc of vs.jsonObj.compose.include || []) {
324
+ if (inc.version) {
325
+ delete inc.version;
326
+ }
327
+ }for (const inc of vs.jsonObj.compose.exclude || []) {
328
+ if (inc.version) {
329
+ delete inc.version;
330
+ }
331
+ }
332
+ }
333
+ }
318
334
  // Atomic replacement of the map
319
335
  this.valueSetMap = newMap;
320
336
  }
@@ -12,7 +12,7 @@ const { TerminologyWorker } = require('./worker');
12
12
  const {TxParameters} = require("../params");
13
13
  const {Designations, SearchFilterText} = require("../library/designations");
14
14
  const {Extensions} = require("../library/extensions");
15
- const {getValuePrimitive, getValueName} = require("../../library/utilities");
15
+ const {getValuePrimitive, getValueName, validateParameter} = require("../../library/utilities");
16
16
  const {div} = require("../../library/html");
17
17
  const {Issue, OperationOutcome} = require("../library/operation-outcome");
18
18
  const crypto = require('crypto');
@@ -624,13 +624,15 @@ class ValueSetExpander {
624
624
  if (vsInfo && vsInfo.isSimple) {
625
625
  vsInfo.handleByCS = cs.handlesSelecting();
626
626
  }
627
- if (cs.contentMode() !== 'complete') {
627
+ if (!cs.contentMode()) {
628
+ throw new Issue('error', 'business-rule', null, null, 'The code system definition for ' + cset.system + ' has no content property, so this expansion cannot be performed', 'invalid');
629
+ } else if (cs.contentMode() !== 'complete') {
628
630
  if (cs.contentMode() === 'not-present') {
629
631
  throw new Issue('error', 'business-rule', null, null, 'The code system definition for ' + cset.system + ' has no content, so this expansion cannot be performed', 'invalid');
630
632
  } else if (cs.contentMode() === 'supplement') {
631
633
  throw new Issue('error', 'business-rule', null, null, 'The code system definition for ' + cset.system + ' defines a supplement, so this expansion cannot be performed', 'invalid');
632
634
  } else {
633
- this.addParamUri(cs.contentMode(), cs.system + '|' + cs.version);
635
+ this.addParamUri(exp, cs.contentMode(), cs.system + '|' + cs.version);
634
636
  Extensions.addString(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed",
635
637
  "This extension is based on a fragment of the code system " + cset.system);
636
638
  }
@@ -1437,6 +1439,9 @@ class ValueSetExpander {
1437
1439
  }
1438
1440
 
1439
1441
  addParamUri(exp, name, value) {
1442
+ validateParameter(name, 'name', String);
1443
+ validateParameter(value, 'value', String);
1444
+
1440
1445
  if (!this.hasParam(exp, name, value)) {
1441
1446
  if (!exp.parameter) {
1442
1447
  exp.parameter = [];
@@ -205,19 +205,25 @@ class SearchWorker extends TerminologyWorker {
205
205
  // Convert params object to array format expected by ValueSet providers
206
206
  // Exclude control params (_offset, _count, _elements, _sort)
207
207
  const searchParams = [];
208
+ let source = null;
208
209
  for (const [key, value] of Object.entries(params)) {
209
210
  if (!key.startsWith('_') && value && SearchWorker.ALLOWED_PARAMS.includes(key)) {
210
211
  searchParams.push({ name: key, value: value });
211
212
  }
213
+ if (key == 'source') {
214
+ source = value;
215
+ }
212
216
  }
213
217
 
214
218
  for (const vsp of this.provider.valueSetProviders) {
215
- this.deadCheck('searchValueSets-providers');
216
- const results = await vsp.searchValueSets(searchParams, elements);
217
- if (results && Array.isArray(results)) {
218
- for (const vs of results) {
219
- this.deadCheck('searchValueSets-results');
220
- allMatches.push(vs.jsonObj || vs);
219
+ if (!source || source == vsp.sourcePackage()) {
220
+ this.deadCheck('searchValueSets-providers');
221
+ const results = await vsp.searchValueSets(searchParams, elements);
222
+ if (results && Array.isArray(results)) {
223
+ for (const vs of results) {
224
+ this.deadCheck('searchValueSets-results');
225
+ allMatches.push(vs.jsonObj || vs);
226
+ }
221
227
  }
222
228
  }
223
229
  }