fhirsmith 0.8.2 → 0.8.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.
@@ -9,9 +9,6 @@
9
9
  <meta content="http://hl7.org/fhir" name="author"/>
10
10
  <meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" />
11
11
 
12
- <link rel="stylesheet" href="/fhir.css"/>
13
-
14
-
15
12
  <!-- Bootstrap core CSS -->
16
13
  <link rel="stylesheet" href="/assets/css/bootstrap.css"/>
17
14
  <link rel="stylesheet" href="/assets/css/bootstrap-fhir.css"/>
package/server.js CHANGED
@@ -11,6 +11,7 @@ const express = require('express');
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
13
  const os = require('os');
14
+ const v8 = require('v8');
14
15
  const folders = require('./library/folder-setup'); // <-- ADD: load early
15
16
  const { statSync, readdirSync } = require('fs');
16
17
  const escape = require('escape-html');
@@ -41,8 +42,8 @@ serverLog.info(`Data directory: ${folders.dataDir()}`);
41
42
  serverLog.info(`========================================`);
42
43
 
43
44
  const activeModules = config.modules ? Object.keys(config.modules)
44
- .filter(mod => config.modules[mod].enabled)
45
- .join(', ') : [];
45
+ .filter(mod => config.modules[mod].enabled)
46
+ .join(', ') : [];
46
47
  serverLog.info(`Loaded Configuration. Active modules = ${activeModules}`);
47
48
 
48
49
  // Import modules
@@ -541,20 +542,21 @@ app.get('/dashboard', async (req, res) => {
541
542
 
542
543
  // Memory usage
543
544
  const memUsage = process.memoryUsage();
544
- const heapUsedPCT = (memUsage.heapUsed * 100) / memUsage.heapTotal;
545
- const freeMemMB = (os.freemem() / 1024 / 1024).toFixed(0);
546
- const totalMemMB = (os.totalmem() / 1024 / 1024).toFixed(0);
547
- const usedMemPCT = 100 - ((freeMemMB * 100) / totalMemMB);
545
+ const heapStats = v8.getHeapStatistics();
546
+ const nodeMemPCT = (memUsage.heapUsed * 100) / heapStats.heap_size_limit; // % of Node.js memory limit used
547
+ const totalMemBytes = os.totalmem();
548
+ const freeMemBytes = os.freemem();
549
+ const sysMemPCT = ((totalMemBytes - freeMemBytes) * 100) / totalMemBytes; // % of system memory used
548
550
  const fstats = fs.statfsSync(folders.logsDir());
549
- const diskPCT = (fstats.bavail * 100) / fstats.blocks;
551
+ const diskPCT = 100 - ((fstats.bavail * 100) / fstats.blocks); // % of disk used
550
552
 
551
553
  let content = '<style>table.grid{margin-bottom:10px;border:1px solid black;margin-right:auto}table.grid th,table.grid td{border:1px solid silver;padding:3px 7px 2px;font-size:12px;line-height:1.4em;font-family:verdana;vertical-align:top}table.grid th{font-weight:bold}table.grid td{font-weight:normal}</style>';
552
554
  content += '<table class="grid">';
553
555
  content += '<tr>';
554
556
  content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
555
557
  content += `<td><strong>Request Count:</strong> ${stats.requestCount} (static: ${stats.staticRequestCount})</td>`;
556
- content += `<td style="background-color:${pctColor(usedMemPCT)}"><strong>Memory:</strong> ${usedMemPCT.toFixed(0)}%</td>`;
557
- content += `<td style="background-color:${pctColor(heapUsedPCT)}"><strong>Heap:</strong> ${heapUsedPCT.toFixed(0)}%</td>`;
558
+ content += `<td style="background-color:${pctColor(nodeMemPCT)}"><strong>Node Memory:</strong> ${nodeMemPCT.toFixed(0)}%</td>`;
559
+ content += `<td style="background-color:${pctColor(sysMemPCT)}"><strong>System Memory:</strong> ${sysMemPCT.toFixed(0)}%</td>`;
558
560
  content += `<td style="background-color:${pctColor(diskPCT)}"><strong>Disk:</strong> ${diskPCT.toFixed(0)}%</td>`;
559
561
  content += '</tr>';
560
562
  content += '</table>';
@@ -590,9 +592,12 @@ app.get('/dashboard', async (req, res) => {
590
592
  });
591
593
 
592
594
  function pctColor(pct) {
593
- const r = Math.round(pct * 2.55);
594
- const g = Math.round((100 - pct) * 2.55);
595
- return `rgb(${r}, ${g}, 100)`; // the 100 keeps it pastel/light
595
+ // Gradient from green (#deffe0) at 0% to red (#ffd3d1) at 100%
596
+ const t = Math.max(0, Math.min(100, pct)) / 100;
597
+ const r = Math.round(222 + 33 * t); // 222 -> 255
598
+ const g = Math.round(255 - 44 * t); // 255 -> 211
599
+ const b = Math.round(224 - 15 * t); // 224 -> 209
600
+ return `rgb(${r}, ${g}, ${b})`;
596
601
  }
597
602
 
598
603
  // Health check endpoint
@@ -659,10 +664,10 @@ function getLogStats() {
659
664
  const limitInfo = `${maxFiles} files × ${maxSize} each`;
660
665
 
661
666
  return '<tr>'
662
- + `<td><strong>Existing Logs:</strong> ${files.length} (${sizeMB} MB)</td>`
663
- + `<td><strong>Retention Policy:</strong> ${limitInfo}</td>`
664
- + diskInfo
665
- + '</tr>';
667
+ + `<td><strong>Existing Logs:</strong> ${files.length} (${sizeMB} MB)</td>`
668
+ + `<td><strong>Retention Policy:</strong> ${limitInfo}</td>`
669
+ + diskInfo
670
+ + '</tr>';
666
671
  } catch (e) {
667
672
  return `<tr><td colspan="3"><strong>Logs:</strong> unable to read (${e.message})</td></tr>`;
668
673
  }
@@ -793,12 +798,12 @@ async function serveFhirsmithHome(req, res) {
793
798
  endpoints: {
794
799
  health: '/health',
795
800
  ...Object.fromEntries(
796
- Object.keys(enabledModules)
797
- .filter(m => m !== 'tx')
798
- .map(m => [
799
- m,
800
- m === 'vcl' ? '/VCL' : `/${m}`
801
- ])
801
+ Object.keys(enabledModules)
802
+ .filter(m => m !== 'tx')
803
+ .map(m => [
804
+ m,
805
+ m === 'vcl' ? '/VCL' : `/${m}`
806
+ ])
802
807
  ),
803
808
  // Add TX endpoints separately
804
809
  ...(enabledModules.tx ? {
package/shl/readme.md ADDED
@@ -0,0 +1,27 @@
1
+ To generate a certificate:
2
+
3
+ ```shell
4
+ # Generate EC P-256 private key
5
+ openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
6
+
7
+ # Extract the public key / self-signed cert
8
+ openssl req -new -x509 -key private-key.pem -out public-key.pem -days 3650 \
9
+ -subj "/CN=fhirsmith-shl"
10
+
11
+ node -e "
12
+ const crypto = require('crypto');
13
+ const fs = require('fs');
14
+ const key = crypto.createPrivateKey(fs.readFileSync('private-key.pem'));
15
+ const jwk = key.export({format:'jwk'});
16
+ const thumbprint = crypto.createHash('sha256')
17
+ .update(JSON.stringify({crv:jwk.crv,kty:jwk.kty,x:jwk.x,y:jwk.y}))
18
+ .digest('base64url');
19
+ console.log('kid: '+thumbprint);
20
+ "
21
+
22
+ ```
23
+
24
+ Config:
25
+
26
+ All files will be in {data}/shl. the file paths in the config are relative to
27
+ that location
package/shl/shl.js CHANGED
@@ -396,14 +396,8 @@ class SHLModule {
396
396
 
397
397
  loadCertificates() {
398
398
  try {
399
- const certPath = path.resolve(__dirname, this.config.certificates.certFile);
400
- const keyPath = path.resolve(__dirname, this.config.certificates.keyFile);
401
-
402
- // Validate paths to prevent directory traversal
403
- if (!certPath.startsWith(path.resolve(__dirname)) ||
404
- !keyPath.startsWith(path.resolve(__dirname))) {
405
- throw new Error('Certificate paths outside allowed directory');
406
- }
399
+ const certPath = folders.filePath('shl', this.config.certificates.certFile);
400
+ const keyPath = folders.filePath('shl', this.config.certificates.keyFile);
407
401
 
408
402
  const certPem = fs.readFileSync(certPath, 'utf8');
409
403
  const keyPem = fs.readFileSync(keyPath, 'utf8');
package/tx/cs/cs-api.js CHANGED
@@ -221,10 +221,11 @@ class CodeSystemProvider {
221
221
  }
222
222
 
223
223
  /**
224
+ * @param {boolean} langPacks - whether to include language packs
224
225
  * @returns {string[]} all supplements in scope
225
226
  */
226
- listSupplements() {
227
- return this.supplements ? this.supplements.map(s => s.vurl) : [];
227
+ listSupplements(langPacks) {
228
+ return this.supplements ? this.supplements.filter(s => langPacks || !s.isLangPack()).map(s => s.vurl) : [];
228
229
  }
229
230
 
230
231
  /**
@@ -385,12 +386,15 @@ class CodeSystemProvider {
385
386
  const concept= supplement.getConceptByCode(code);
386
387
  if (concept) {
387
388
  if (concept.display) {
388
- displays.addDesignation(true, 'active', supplement.jsonObj.language, CodeSystem.makeUseForDisplay(), concept.display);
389
+ // sometimes the display is just repeated from the base code system
390
+ if (!displays.hasAnyDisplay(concept.display)) {
391
+ displays.addDesignation(true, 'active', supplement.jsonObj.language, CodeSystem.makeUseForDisplay(), concept.display).supplement = supplement;
392
+ }
389
393
  }
390
394
  if (concept.designation) {
391
395
  for (const d of concept.designation) {
392
396
  let status = Extensions.readString(d, "http://hl7.org/fhir/StructureDefinition/structuredefinition-standards-status");
393
- displays.addDesignation(false, status || 'active', d.language, d.use, d.value, d.extension?.length > 0 ? d.extension : []);
397
+ displays.addDesignation(false, status || 'active', d.language, d.use, d.value, d.extension?.length > 0 ? d.extension : []).supplement = supplement;
394
398
  }
395
399
  }
396
400
  }
@@ -854,6 +858,17 @@ class CodeSystemFactoryProvider {
854
858
  return [];
855
859
  }
856
860
 
861
+ /**
862
+ *
863
+ * @param supplements - the list of supplements to populate - fill with any supplements matching url(+version)
864
+ * @param url - url of code system
865
+ * @param version - version of codesystem
866
+ * @param {Map} statedSupplements - return language packs and supplements that are listed in stated supplements, by versioned URL
867
+ * @returns {Promise<void>}
868
+ */
869
+ async listSupplements(supplements, url, version, statedSupplements) {
870
+ // do nothing
871
+ }
857
872
  /**
858
873
  * see comments for registerSupplements()
859
874
  *
package/tx/cs/cs-base.js CHANGED
@@ -41,6 +41,19 @@ class BaseCSServices extends CodeSystemProvider {
41
41
  return property;
42
42
  }
43
43
 
44
+ _addDateTimeProperty(params, type, name, value) {
45
+
46
+ const property = {
47
+ name: type,
48
+ part: [
49
+ {name: 'code', valueCode: name},
50
+ {name: 'value', valueDateTime: value}
51
+ ]
52
+ };
53
+ params.push(property);
54
+ return property;
55
+ }
56
+
44
57
  _addStringProperty(params, type, name, value, language = null) {
45
58
 
46
59
  const property = {
@@ -355,6 +355,52 @@ class SnomedServices {
355
355
  return result;
356
356
  }
357
357
 
358
+ filterChildOf(id = true) {
359
+ const result = new SnomedFilterContext();
360
+ const conceptResult = this.concepts.findConcept(id);
361
+
362
+ if (!conceptResult.found) {
363
+ throw new Error(`The SNOMED CT Concept ${id} is not known`);
364
+ }
365
+
366
+ const descendants = this.getConceptChildren(conceptResult.index);
367
+
368
+ result.descendants = descendants;
369
+
370
+ return result;
371
+ }
372
+
373
+
374
+ filterGeneralizes(id = true) {
375
+ const result = new SnomedFilterContext();
376
+ const conceptResult = this.concepts.findConcept(id);
377
+
378
+ if (!conceptResult.found) {
379
+ throw new Error(`The SNOMED CT Concept ${id} is not known`);
380
+ }
381
+
382
+ let ancestors = new Set();
383
+ let parents = this.getConceptParents(conceptResult.index);
384
+ let isNew = true;
385
+ while (isNew) {
386
+ isNew = false;
387
+ let np = [];
388
+ for (let parent of parents) {
389
+ if (!ancestors.has(parent)) {
390
+ isNew = true;
391
+ ancestors.add(parent);
392
+ np.push(...this.getConceptParents(parent));
393
+ }
394
+ }
395
+ parents = np;
396
+ }
397
+
398
+ result.descendants = [...ancestors];
399
+
400
+ return result;
401
+ }
402
+
403
+
358
404
  filterIn(id) {
359
405
  const result = new SnomedFilterContext();
360
406
  const conceptResult = this.concepts.findConcept(id);
@@ -775,6 +821,13 @@ class SnomedProvider extends BaseCSServices {
775
821
  const ctxt = await this.#ensureContext(context);
776
822
  if (ctxt) {
777
823
  if (!(ctxt instanceof SnomedExpressionContext) || ctxt.expression?.concepts.length == 1) {
824
+ const time = this.sct.concepts.getConcept(ctxt.getReference()).effectiveTime;
825
+ const pascalEpoch = new Date(1899, 11, 30);
826
+ const date = new Date(pascalEpoch.getTime() + time * 86400000);
827
+ const dateStr = date.toISOString().slice(0, 10);
828
+ this._addDateTimeProperty(params, 'property', 'effectiveTime', dateStr);
829
+
830
+
778
831
  const parents = this.sct.getConceptParents(ctxt.getReference());
779
832
  for (let parentRef of parents) {
780
833
  const code = this.sct.getConceptId(parentRef);
@@ -792,7 +845,7 @@ class SnomedProvider extends BaseCSServices {
792
845
  const moduleId = this.sct.concepts.getModuleId(ctxt.getReference());
793
846
  if (moduleId) {
794
847
  const code = this.sct.getConceptId(moduleId);
795
- this._addCodeProperty(params, 'property', 'module', code, null, null);
848
+ this._addCodeProperty(params, 'property', 'module', code, null, this.sct.getDisplayName(moduleId));
796
849
  }
797
850
 
798
851
  const relationships = this.sct.getConceptRelationships(ctxt.getReference());
@@ -838,7 +891,7 @@ class SnomedProvider extends BaseCSServices {
838
891
  async doesFilter(prop, op, value) {
839
892
  if (prop === 'concept') {
840
893
  const id = this.sct.stringToIdOrZero(value);
841
- if (id !== 0n && ['=', 'is-a', 'descendent-of', 'in'].includes(op)) {
894
+ if (id !== 0n && ['=', 'is-a', 'descendent-of', 'in', 'generalizes', 'child-of'].includes(op)) {
842
895
  return this.sct.conceptExists(value);
843
896
  }
844
897
  }
@@ -891,6 +944,14 @@ class SnomedProvider extends BaseCSServices {
891
944
  filterContext.filters.push(this.sct.filterIsA(id, false));
892
945
  return null;
893
946
  }
947
+ case 'child-of': {
948
+ filterContext.filters.push(this.sct.filterChildOf(id));
949
+ return null;
950
+ }
951
+ case 'generalizes': {
952
+ filterContext.filters.push(this.sct.filterGeneralizes(id, false));
953
+ return null;
954
+ }
894
955
  case 'in': {
895
956
  filterContext.filters.push(this.sct.filterIn(id));
896
957
  return null;
@@ -1121,7 +1182,7 @@ class SnomedProvider extends BaseCSServices {
1121
1182
  if (set.propProp || set.propValue) {
1122
1183
  for (let i = 0; i < this.sct.concepts.count(); i++) {
1123
1184
  let concept = this.sct.concepts.getConceptByCount(i);
1124
- const relationships = this.sct.getConceptRelationships(concept.getReference());
1185
+ const relationships = this.sct.getConceptRelationships(concept.index);
1125
1186
  for (let relationshipRef of relationships) {
1126
1187
  const relationship = this.sct.relationships.getRelationship(relationshipRef);
1127
1188
  if (set.propProp === relationship.relType && set.propValue === relationship.target) {
@@ -2,6 +2,7 @@ const { Language } = require("../../library/languages");
2
2
  const {CanonicalResource} = require("./canonical-resource");
3
3
  const {codeSystemFromR5, codeSystemToR5} = require("../xversion/xv-codesystem");
4
4
  const {getValuePrimitive} = require("../../library/utilities");
5
+ const {VersionUtilities} = require("../../library/version-utilities");
5
6
 
6
7
  const CodeSystemContentMode = Object.freeze({
7
8
  Complete: 'complete',
@@ -614,12 +615,31 @@ class CodeSystem extends CanonicalResource {
614
615
  }
615
616
 
616
617
  isLangPack() {
618
+ // todo: this is a temporary work around until fhir.tx.support.r4#0.37.0 is released that properly marks this as a language pack
619
+ if (this.jsonObj.url === 'https://terminology.dhp.uz/fhir/CodeSystem/loinc-supplement-uz') {
620
+ return true;
621
+ }
617
622
  return (this.jsonObj.extension || []).find(x => x.url == 'http://hl7.org/fhir/StructureDefinition/codesystem-supplement-type' && getValuePrimitive(x) == 'lang-pack');
618
623
  }
619
624
 
620
625
  isPropUri(code, uri) {
621
626
  return (this.jsonObj.property || []).find(x => x.code == code && x.uri == uri);
622
627
  }
628
+
629
+ isSupplementFor(url, version) {
630
+ if (this.jsonObj.content !== 'supplement') {
631
+ return false;
632
+ }
633
+ let suppU = this.jsonObj.supplements;
634
+ if (suppU == url) {
635
+ return true;
636
+ }
637
+ if (suppU.startsWith(url + '|')) {
638
+ let suppV = suppU.substring(suppU.indexOf('|') + 1);
639
+ return VersionUtilities.versionMatchesByAlgorithm(suppV, version, VersionUtilities.guessVersionAlgorithmFromVersion(version));
640
+ }
641
+ return false;
642
+ }
623
643
  }
624
644
 
625
645
  module.exports = { CodeSystem, CodeSystemContentMode };
@@ -404,6 +404,15 @@ class Designations {
404
404
  // }
405
405
  }
406
406
 
407
+ hasAnyDisplay(value) {
408
+ for (let designation of this.designations) {
409
+ if (designation.value === value) {
410
+ return true;
411
+ }
412
+ }
413
+ return false;
414
+ }
415
+
407
416
  /**
408
417
  * Check if a display value exists with specified matching criteria
409
418
  */
@@ -551,7 +560,7 @@ class Designations {
551
560
  /**
552
561
  * Find the preferred designation for given language preferences
553
562
  */
554
- preferredDesignation(langList = null) {
563
+ preferredDesignation(langList = null, supplements = null) {
555
564
  if (this.designations.length === 0) {
556
565
  return null;
557
566
  }
@@ -560,24 +569,39 @@ class Designations {
560
569
  // No language list, prefer base designations
561
570
  for (const cd of this.designations) {
562
571
  if (this._isPreferred(cd) && cd.isActive()) {
572
+ if (supplements && cd.source) {
573
+ supplements.add(cd.source);
574
+ }
563
575
  return cd;
564
576
  }
565
577
  }
566
578
  for (const cd of this.designations) {
567
579
  if (this.isDisplay(cd) && cd.isActive()) {
580
+ if (supplements && cd.source) {
581
+ supplements.add(cd.source);
582
+ }
568
583
  return cd;
569
584
  }
570
585
  }
571
586
  for (const cd of this.designations) {
572
587
  if (this.isDisplay(cd)) {
588
+ if (supplements && cd.source) {
589
+ supplements.add(cd.source);
590
+ }
573
591
  return cd;
574
592
  }
575
593
  }
576
594
  for (const cd of this.designations) {
577
595
  if (this._isPreferred(cd)) {
596
+ if (supplements && cd.source) {
597
+ supplements.add(cd.source);
598
+ }
578
599
  return cd;
579
600
  }
580
601
  }
602
+ if (supplements && this.designations[0].source) {
603
+ supplements.add(this.designations[0].source);
604
+ }
581
605
  return this.designations[0];
582
606
  }
583
607
 
@@ -589,16 +613,25 @@ class Designations {
589
613
  for (const matchType of matchTypes) {
590
614
  for (const cd of this.designations) {
591
615
  if (this._langMatches(lang, cd.language, matchType) && this.isDisplay(cd)) {
616
+ if (supplements && cd.source) {
617
+ supplements.add(cd.source);
618
+ }
592
619
  return cd;
593
620
  }
594
621
  }
595
622
  for (const cd of this.designations) {
596
623
  if (this._langMatches(lang, cd.language, matchType) && this._isPreferred(cd)) {
624
+ if (supplements && cd.source) {
625
+ supplements.add(cd.source);
626
+ }
597
627
  return cd;
598
628
  }
599
629
  }
600
630
  for (const cd of this.designations) {
601
631
  if (this._langMatches(lang, cd.language, matchType)) {
632
+ if (supplements && cd.source) {
633
+ supplements.add(cd.source);
634
+ }
602
635
  return cd;
603
636
  }
604
637
  }
@@ -606,6 +639,9 @@ class Designations {
606
639
  }
607
640
  for (const cd of this.designations) {
608
641
  if (!cd.language && this.isDisplay(cd)) {
642
+ if (supplements && cd.source) {
643
+ supplements.add(cd.source);
644
+ }
609
645
  return cd;
610
646
  }
611
647
  }
package/tx/provider.js CHANGED
@@ -107,6 +107,36 @@ class Provider {
107
107
  return null;
108
108
  }
109
109
 
110
+ loadSupplements(url, version, statedSupplements) {
111
+ let supplements = new Map();
112
+ for (let csp of this.codeSystemFactories.values()) {
113
+ csp.listSupplements(supplements, url, version, statedSupplements);
114
+ }
115
+ for (let cs of this.codeSystems.values()) {
116
+ if (cs.isSupplementFor(url, version)) {
117
+ if (cs.isLangPack() || this.isStatedSupplement(cs, statedSupplements)) {
118
+ supplements.set(cs.vurl, cs);
119
+ }
120
+ }
121
+ }
122
+ return [...supplements.values()];
123
+ }
124
+
125
+ isStatedSupplement(cs, statedSupplements) {
126
+ if (statedSupplements == null) {
127
+ return false;
128
+ }
129
+ for (let suppU of statedSupplements) {
130
+ if (suppU === cs.url) {
131
+ return true;
132
+ }
133
+ if (suppU.startsWith(cs.url+"|")) {
134
+ let suppV = suppU.substring(suppU.indexOf('|') + 1);
135
+ return VersionUtilities.versionMatchesByAlgorithm(suppV, cs.version, VersionUtilities.guessVersionAlgorithmFromVersion(cs.version));
136
+ }
137
+ }
138
+ return false;
139
+ }
110
140
  /**
111
141
  * Create a code system provider from a CodeSystem resource
112
142
  * @param {OperationContext} opContext - The code system resource
@@ -204,6 +204,7 @@ class ValueSetExpander {
204
204
  hasExclusions = false;
205
205
  requiredSupplements = new Set();
206
206
  usedSupplements = new Set();
207
+ reportedSupplements = new Set();
207
208
  internalLimit = INTERNAL_DEFAULT_LIMIT;
208
209
  externalLimit = EXTERNAL_DEFAULT_LIMIT;
209
210
 
@@ -339,9 +340,9 @@ class ValueSetExpander {
339
340
  const s = this.canonical(system, version);
340
341
  this.addParamUri(expansion, 'used-codesystem', s);
341
342
  if (cs != null) {
342
- const ts = cs.listSupplements();
343
+ const ts = cs.listSupplements(false);
343
344
  for (const vs of ts) {
344
- this.addParamUri(expansion, 'used-supplement', vs);
345
+ this.reportedSupplements.add(vs);
345
346
  }
346
347
  }
347
348
  }
@@ -415,7 +416,7 @@ class ValueSetExpander {
415
416
  }
416
417
 
417
418
  // display and designations
418
- const pref = displays.preferredDesignation(this.params.workingLanguages());
419
+ const pref = displays.preferredDesignation(this.params.workingLanguages(), this.reportedSupplements);
419
420
  if (pref && pref.value) {
420
421
  n.display = pref.value;
421
422
  }
@@ -426,6 +427,9 @@ class ValueSetExpander {
426
427
  if (!n.designation) {
427
428
  n.designation = [];
428
429
  }
430
+ if (t.source) {
431
+ this.reportedSupplements.add(t.source);
432
+ }
429
433
  n.designation.push(t.asObject());
430
434
  }
431
435
  }
@@ -490,9 +494,9 @@ class ValueSetExpander {
490
494
  const s = this.canonical(system, version);
491
495
  this.addParamUri(expansion, 'used-codesystem', s);
492
496
  if (cs) {
493
- const ts= cs.listSupplements();
497
+ const ts= cs.listSupplements(false);
494
498
  for (const vs of ts) {
495
- this.addParamUri(expansion, 'used-supplement', vs);
499
+ this.reportedSupplements.add(vs);
496
500
  }
497
501
  }
498
502
  }
@@ -813,7 +817,7 @@ class ValueSetExpander {
813
817
  this.worker.opContext.log('iterate concepts done');
814
818
  }
815
819
 
816
- if (cset.filter) {
820
+ if (cset.filter && cset.filter.length > 0) {
817
821
  this.worker.opContext.log('prepare filters');
818
822
  const fcl = cset.filter;
819
823
  const prep = await cs.getPrepContext(true);
@@ -1040,10 +1044,9 @@ class ValueSetExpander {
1040
1044
  if (expansion) {
1041
1045
  const vs = this.canonical(await cs.system(), await cs.version());
1042
1046
  this.addParamUri(expansion, 'used-codesystem', vs);
1043
- const ts = cs.listSupplements();
1047
+ const ts = cs.listSupplements(false);
1044
1048
  for (const v of ts) {
1045
- this.worker.deadCheck('processCodeAndDescendants');
1046
- this.addParamUri(expansion, 'used-supplement', v);
1049
+ this.reportedSupplements.add(v);
1047
1050
  }
1048
1051
  }
1049
1052
 
@@ -1081,10 +1084,9 @@ class ValueSetExpander {
1081
1084
  if (expansion) {
1082
1085
  const vs = this.canonical(await cs.system(), await cs.version());
1083
1086
  this.addParamUri(expansion, 'used-codesystem', vs);
1084
- const ts= cs.listSupplements();
1087
+ const ts= cs.listSupplements(false);
1085
1088
  for (const v of ts) {
1086
- this.worker.deadCheck('processCodeAndDescendants');
1087
- this.addParamUri(expansion, 'used-supplement', v);
1089
+ this.reportedSupplements.add(v);
1088
1090
  }
1089
1091
  }
1090
1092
 
@@ -1303,6 +1305,11 @@ class ValueSetExpander {
1303
1305
  }
1304
1306
  }
1305
1307
 
1308
+ const ts = this.reportedSupplements;
1309
+ for (const v of ts) {
1310
+ this.addParamUri(exp, 'used-supplement', v);
1311
+ }
1312
+
1306
1313
  this.worker.opContext.log('finish up');
1307
1314
 
1308
1315
  let list;
@@ -149,14 +149,15 @@ class LookupWorker extends TerminologyWorker {
149
149
 
150
150
  // check supplements
151
151
  const used = new Set();
152
- this.checkSupplements(csProvider, null, txp.supplements, used);
152
+ const reported = new Set();
153
+ this.checkSupplements(csProvider, null, txp.supplements, used, reported);
153
154
  const unused = new Set([...txp.supplements].filter(s => !used.has(s)));
154
155
  if (unused.size > 0) {
155
156
  throw new Issue('error', 'not-found', null, 'VALUESET_SUPPLEMENT_MISSING', this.i18n.translatePlural(unused.size, 'VALUESET_SUPPLEMENT_MISSING', txp.HTTPLanguages, [[...unused].join(',')]), 'not-found').handleAsOO(400);
156
157
  }
157
158
 
158
159
  // Perform the lookup
159
- const result = await this.doLookup(csProvider, code, txp);
160
+ const result = await this.doLookup(csProvider, code, txp, reported);
160
161
  return res.status(200).json(result);
161
162
  } catch (error) {
162
163
  this.log.error(error);
@@ -242,9 +243,10 @@ class LookupWorker extends TerminologyWorker {
242
243
  * @param {CodeSystemProvider} csProvider - CodeSystem provider
243
244
  * @param {string} code - Code to look up
244
245
  * @param {Object} params - Parsed parameters
246
+ * @param {Set} reportedSupplements - Set of supplements that are to be reported
245
247
  * @returns {Object} Parameters resource with lookup result
246
248
  */
247
- async doLookup(csProvider, code, params) {
249
+ async doLookup(csProvider, code, params, reportedSupplements) {
248
250
  this.deadCheck('doLookup');
249
251
 
250
252
  await this.checkSupplements(csProvider, null, params.supplements);
@@ -352,6 +354,12 @@ class LookupWorker extends TerminologyWorker {
352
354
  this.deadCheck('doLookup-designations');
353
355
  const designationParts = [];
354
356
 
357
+ if (designation.supplement) {
358
+ designationParts.push({
359
+ name: 'source',
360
+ valueCanonical: designation.supplement.vurl
361
+ });
362
+ }
355
363
  if (designation.language) {
356
364
  designationParts.push({
357
365
  name: 'language',
@@ -382,6 +390,14 @@ class LookupWorker extends TerminologyWorker {
382
390
  // Let the provider add additional properties
383
391
  await csProvider.extendLookup(ctxt, params.properties || [], responseParams);
384
392
 
393
+ if (reportedSupplements) {
394
+ for (const supplement of reportedSupplements) {
395
+ responseParams.push({
396
+ name: 'used-supplement',
397
+ valueCanonical: supplement
398
+ });
399
+ }
400
+ }
385
401
  return {
386
402
  resourceType: 'Parameters',
387
403
  parameter: responseParams