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.
- package/CHANGELOG.md +41 -0
- package/README.md +4 -4
- package/package.json +1 -1
- package/packages/package-crawler.js +1 -1
- package/publisher/publisher.js +257 -218
- package/root-bare-template.html +0 -3
- package/server.js +27 -22
- package/shl/readme.md +27 -0
- package/shl/shl.js +2 -8
- package/tx/cs/cs-api.js +19 -4
- package/tx/cs/cs-base.js +13 -0
- package/tx/cs/cs-snomed.js +64 -3
- package/tx/library/codesystem.js +20 -0
- package/tx/library/designations.js +37 -1
- package/tx/provider.js +30 -0
- package/tx/workers/expand.js +19 -12
- package/tx/workers/lookup.js +19 -3
- package/tx/workers/worker.js +8 -2
- package/tx/xversion/xv-codesystem.js +3 -3
- package/tx/xversion/xv-parameters.js +2 -2
- package/tx/xversion/xv-valueset.js +1 -2
package/root-bare-template.html
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
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
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
const
|
|
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(
|
|
557
|
-
content += `<td style="background-color:${pctColor(
|
|
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
|
-
|
|
594
|
-
const
|
|
595
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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 =
|
|
400
|
-
const keyPath =
|
|
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
|
-
|
|
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 = {
|
package/tx/cs/cs-snomed.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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) {
|
package/tx/library/codesystem.js
CHANGED
|
@@ -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
|
package/tx/workers/expand.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|
package/tx/workers/lookup.js
CHANGED
|
@@ -149,14 +149,15 @@ class LookupWorker extends TerminologyWorker {
|
|
|
149
149
|
|
|
150
150
|
// check supplements
|
|
151
151
|
const used = new Set();
|
|
152
|
-
|
|
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
|