fhirsmith 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/changelog-template.md +22 -0
- package/library/logger.js +8 -1
- package/library/package-manager.js +40 -2
- package/npmprojector/watcher.js +1 -1
- package/package.json +3 -2
- package/packages/packages.js +28 -15
- package/publisher/publisher.js +8 -8
- package/registry/api.js +92 -31
- package/registry/crawler.js +15 -1
- package/registry/model.js +158 -8
- package/registry/readme.md +5 -13
- package/registry/registry.js +87 -46
- package/registry/test-runner.js +1 -1
- package/server.js +8 -1
- package/token/token.js +1 -1
- package/translations/Messages.properties +3 -1
- package/translations/rendering-phrases.properties +1227 -1218
- package/translations/rendering-phrases_ar.properties +1097 -1091
- package/translations/rendering-phrases_fr.properties +626 -624
- package/translations/rendering-phrases_nl.properties +976 -970
- package/translations/rendering-phrases_pt.properties +1026 -1020
- package/translations/rendering-phrases_ru.properties +1100 -1094
- package/tsconfig.eslint.json +9 -0
- package/tx/cs/cs-api.js +57 -4
- package/tx/cs/cs-cs.js +98 -25
- package/tx/cs/cs-hgvs.js +4 -0
- package/tx/cs/cs-lang.js +8 -0
- package/tx/cs/cs-mimetypes.js +4 -0
- package/tx/cs/cs-snomed.js +530 -75
- package/tx/cs/cs-uri.js +4 -0
- package/tx/data/snomed-subset.txt +40 -3
- package/tx/importers/import-sct.module.js +18 -0
- package/tx/library/designations.js +3 -5
- package/tx/library/renderer.js +227 -30
- package/tx/library.js +5 -1
- package/tx/operation-context.js +40 -3
- package/tx/provider.js +38 -9
- package/tx/sct/ecl.js +30 -11
- package/tx/sct/expressions.js +28 -1
- package/tx/sct/structures.js +5 -5
- package/tx/tx-html.js +24 -15
- package/tx/tx.fhir.org.yml +1 -4
- package/tx/vs/vs-package.js +52 -0
- package/tx/workers/batch-validate.js +125 -18
- package/tx/workers/cache-control.js +31 -2
- package/tx/workers/expand.js +50 -32
- package/tx/workers/metadata.js +27 -6
- package/tx/workers/search.js +7 -1
- package/tx/workers/validate.js +30 -18
- package/tx/workers/worker.js +30 -7
- package/tx/xversion/xv-valueset.js +5 -2
- package/xig/xig.js +3 -3
package/tx/workers/expand.js
CHANGED
|
@@ -220,7 +220,7 @@ class ValueSetExpander {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
async listDisplaysFromProvider(displays, cs, context) {
|
|
223
|
-
await cs.designations(context, displays);
|
|
223
|
+
await cs.designations(context, displays, true);
|
|
224
224
|
displays.source = cs;
|
|
225
225
|
}
|
|
226
226
|
|
|
@@ -661,15 +661,9 @@ class ValueSetExpander {
|
|
|
661
661
|
if (cs.specialEnumeration()) {
|
|
662
662
|
await this.checkCanExpandValueSet(cs.specialEnumeration(), '', null);
|
|
663
663
|
} else if (filter.isNull) {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
Extensions.addString(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
668
|
-
} else {
|
|
669
|
-
throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly', null, 422).withDiagnostics(this.worker.opContext.diagnostics());
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
if (!imp && this.limitCount > 0 && cs.totalCount > this.limitCount) {
|
|
664
|
+
// The unclosed marking for a grammar code system is added by
|
|
665
|
+
// processCodes (the actual expansion); checkSource only guards size.
|
|
666
|
+
if (!imp && this.count !== 0 && this.limitCount > 0 && cs.totalCount > this.limitCount) {
|
|
673
667
|
throw new Issue("error", "too-costly", null, 'VALUESET_TOO_COSTLY', this.worker.i18n.translate('VALUESET_TOO_COSTLY', this.params.httpLanguages, [srcURL, '>' + this.limitCount]), null, 422).withDiagnostics(this.worker.opContext.diagnostics());
|
|
674
668
|
}
|
|
675
669
|
}
|
|
@@ -746,45 +740,64 @@ class ValueSetExpander {
|
|
|
746
740
|
let vs = await this.worker.findValueSet(cs.specialEnumeration(), '', null);
|
|
747
741
|
const base = await this.expandValueSet(cs.specialEnumeration(), '', vs, filter, notClosed);
|
|
748
742
|
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
749
|
-
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason",
|
|
743
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
750
744
|
await this.importValueSet(base, expansion, valueSets, 0);
|
|
751
745
|
notClosed.value = true;
|
|
746
|
+
} else if (!cs.canBeExpanded()) {
|
|
747
|
+
throw new Issue("error", "not-supported", null, 'CODESYSTEM_NOT_ENUMERABLE', this.worker.i18n.translate('CODESYSTEM_NOT_ENUMERABLE', this.params.httpLanguages, [cs.system()]), null, 422).withDiagnostics(this.worker.opContext.diagnostics());
|
|
752
748
|
} else if (filter.isNull) {
|
|
753
749
|
this.worker.opContext.log('add whole code system');
|
|
754
750
|
if (cs.isNotClosed()) {
|
|
755
751
|
if (cs.specialEnumeration()) {
|
|
756
752
|
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
757
|
-
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason",
|
|
753
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
758
754
|
} else {
|
|
759
|
-
|
|
755
|
+
// Grammar code system with no special enumeration (e.g. SNOMED CT):
|
|
756
|
+
// enumerate the precoordinated concepts we can, and mark the
|
|
757
|
+
// expansion incomplete rather than refusing it.
|
|
758
|
+
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
759
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members");
|
|
760
760
|
}
|
|
761
761
|
notClosed.value = true;
|
|
762
762
|
}
|
|
763
763
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
764
|
+
if (this.count === 0 && filters.length === 0 && valueSets.length === 0) {
|
|
765
|
+
// count=0 (count only) with a single whole-code-system include and no
|
|
766
|
+
// additional filters: report the code count directly rather than
|
|
767
|
+
// enumerating - which for a poly-hierarchy (e.g. SNOMED) is both
|
|
768
|
+
// expensive and pointless when no codes are being materialised.
|
|
769
|
+
this.addToTotal(await cs.countAllCodes(excludeInactive));
|
|
770
|
+
} else {
|
|
771
|
+
const iter = await cs.iterator(null);
|
|
772
|
+
if (valueSets.length === 0 && this.limitCount > 0 && (iter && iter.total > this.limitCount) && this.offset < 0) {
|
|
773
|
+
throw new Issue("error", "too-costly", null, 'VALUESET_TOO_COSTLY', this.worker.i18n.translate('VALUESET_TOO_COSTLY', this.params.httpLanguages, [vsSrc.vurl, '>' + this.limitCount]), null, 422).withDiagnostics(this.worker.opContext.diagnostics());
|
|
767
774
|
|
|
768
|
-
}
|
|
769
|
-
let tcount = 0;
|
|
770
|
-
let c = await cs.nextContext(iter);
|
|
771
|
-
while (c) {
|
|
772
|
-
this.worker.deadCheck('processCodes#3a');
|
|
773
|
-
if (await this.passesFilters(cs, c, prep, filters, 0)) {
|
|
774
|
-
tcount += await this.includeCodeAndDescendants(cs, c, expansion, valueSets, null, excludeInactive, vsSrc.vurl);
|
|
775
775
|
}
|
|
776
|
-
|
|
776
|
+
let tcount = 0;
|
|
777
|
+
let c = await cs.nextContext(iter);
|
|
778
|
+
while (c) {
|
|
779
|
+
this.worker.deadCheck('processCodes#3a');
|
|
780
|
+
if (await this.passesFilters(cs, c, prep, filters, 0)) {
|
|
781
|
+
tcount += await this.includeCodeAndDescendants(cs, c, expansion, valueSets, null, excludeInactive, vsSrc.vurl);
|
|
782
|
+
}
|
|
783
|
+
c = await cs.nextContext(iter);
|
|
784
|
+
}
|
|
785
|
+
this.addToTotal(tcount);
|
|
777
786
|
}
|
|
778
|
-
this.addToTotal(tcount);
|
|
779
787
|
} else {
|
|
780
788
|
this.worker.opContext.log('prepare filters');
|
|
781
|
-
this.noTotal();
|
|
782
789
|
if (cs.isNotClosed(filter)) {
|
|
783
790
|
notClosed.value = true;
|
|
784
791
|
}
|
|
785
792
|
const prep = await cs.getPrepContext(true);
|
|
786
793
|
const ctxt = await cs.searchFilter(prep, filter, false);
|
|
787
794
|
let set = await cs.executeFilters(prep);
|
|
795
|
+
if (this.count === 0 && valueSets.length === 0) {
|
|
796
|
+
// count=0 (count only): report the filtered size directly without
|
|
797
|
+
// materialising the codes.
|
|
798
|
+
this.addToTotal(await cs.filterSize(ctxt, set[0]));
|
|
799
|
+
} else {
|
|
800
|
+
this.noTotal();
|
|
788
801
|
this.worker.opContext.log('iterate filters');
|
|
789
802
|
while (await cs.filterMore(ctxt, set[0])) {
|
|
790
803
|
this.worker.deadCheck('processCodes#4');
|
|
@@ -808,6 +821,7 @@ class ValueSetExpander {
|
|
|
808
821
|
}
|
|
809
822
|
}
|
|
810
823
|
this.worker.opContext.log('iterate filters done');
|
|
824
|
+
}
|
|
811
825
|
}
|
|
812
826
|
}
|
|
813
827
|
|
|
@@ -856,7 +870,7 @@ class ValueSetExpander {
|
|
|
856
870
|
|
|
857
871
|
if (cs.specialEnumeration()) {
|
|
858
872
|
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
859
|
-
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason",
|
|
873
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
860
874
|
notClosed.value = true;
|
|
861
875
|
}
|
|
862
876
|
|
|
@@ -867,7 +881,7 @@ class ValueSetExpander {
|
|
|
867
881
|
throw new Issue('error', 'invalid', path + ".filter[" + i + "]", 'UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.worker.i18n.translate('UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.params.httpLanguages, [cs.system(), fc.property, fc.op]), 'vs-invalid', 400);
|
|
868
882
|
}
|
|
869
883
|
Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl);
|
|
870
|
-
await cs.filter(prep, i == 0, fc.property, fc.op, fc.value);
|
|
884
|
+
await cs.filter(prep, i == 0, fc.property, fc.op, fc.value, vsSrc.isCached ? fc : null);
|
|
871
885
|
}
|
|
872
886
|
|
|
873
887
|
const fset = await cs.executeFilters(prep);
|
|
@@ -1003,9 +1017,13 @@ class ValueSetExpander {
|
|
|
1003
1017
|
if (cs.isNotClosed(filter)) {
|
|
1004
1018
|
if (cs.specialEnumeration()) {
|
|
1005
1019
|
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
1006
|
-
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason",
|
|
1020
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
1007
1021
|
} else {
|
|
1008
|
-
|
|
1022
|
+
// Grammar code system with no special enumeration (e.g. SNOMED CT):
|
|
1023
|
+
// enumerate the precoordinated concepts we can, and mark the
|
|
1024
|
+
// expansion incomplete rather than refusing it.
|
|
1025
|
+
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
1026
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members");
|
|
1009
1027
|
}
|
|
1010
1028
|
}
|
|
1011
1029
|
|
|
@@ -1053,14 +1071,14 @@ class ValueSetExpander {
|
|
|
1053
1071
|
if (cs.specialEnumeration()) {
|
|
1054
1072
|
await cs.specialFilter(prep, true);
|
|
1055
1073
|
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
1056
|
-
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason",
|
|
1074
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
1057
1075
|
}
|
|
1058
1076
|
|
|
1059
1077
|
let first = true;
|
|
1060
1078
|
for (let fc of cset.filter) {
|
|
1061
1079
|
this.worker.deadCheck('processCodes#4a');
|
|
1062
1080
|
Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl);
|
|
1063
|
-
await cs.filter(prep, first, fc.property, fc.op, fc.value);
|
|
1081
|
+
await cs.filter(prep, first, fc.property, fc.op, fc.value, vsSrc.isCached ? fc : null);
|
|
1064
1082
|
first = false;
|
|
1065
1083
|
}
|
|
1066
1084
|
|
package/tx/workers/metadata.js
CHANGED
|
@@ -36,16 +36,37 @@ class MetadataHandler {
|
|
|
36
36
|
|
|
37
37
|
if (mode === 'terminology') {
|
|
38
38
|
this.logInfo = 'termcaps';
|
|
39
|
-
const tc = new TerminologyCapabilities(await this.buildTerminologyCapabilities(endpoint, provider));
|
|
39
|
+
const tc = new TerminologyCapabilities(await this.buildTerminologyCapabilities(endpoint, provider, req));
|
|
40
40
|
return res.json(tc.jsonObj);
|
|
41
41
|
}
|
|
42
42
|
this.logInfo = 'metadata';
|
|
43
43
|
|
|
44
44
|
// Default: return CapabilityStatement
|
|
45
|
-
const cs = new CapabilityStatement(this.buildCapabilityStatement(endpoint, provider));
|
|
45
|
+
const cs = new CapabilityStatement(this.buildCapabilityStatement(endpoint, provider, req));
|
|
46
46
|
return res.json(cs.jsonObj);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the public base URL for this request.
|
|
51
|
+
* Derived from what the client actually asked for (scheme + host) plus the
|
|
52
|
+
* endpoint path, so the server echoes the host the client used rather than a
|
|
53
|
+
* fixed, hardcoded value. Falls back to config/host only when no request is
|
|
54
|
+
* available (e.g. unit tests calling the build methods directly).
|
|
55
|
+
* @param {express.Request} [req] - Express request
|
|
56
|
+
* @param {Object} endpoint - Endpoint info {path, ...}
|
|
57
|
+
* @returns {string} Base URL, e.g. https://tx.fhir.org/r4
|
|
58
|
+
*/
|
|
59
|
+
resolveBaseUrl(req, endpoint) {
|
|
60
|
+
if (!req) {
|
|
61
|
+
return this.config.baseUrl || `https://${this.host}${endpoint.path}`;
|
|
62
|
+
}
|
|
63
|
+
// Requires app.set('trust proxy', ...) and nginx forwarding X-Forwarded-Proto
|
|
64
|
+
// for the scheme to be correct behind the reverse proxy.
|
|
65
|
+
const proto = req.protocol || 'https';
|
|
66
|
+
const host = req.get('host') || this.host;
|
|
67
|
+
return `${proto}://${host}${endpoint.path}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
/**
|
|
50
71
|
* Handle GET /$versions request
|
|
51
72
|
* @param {express.Request} req - Express request (with txEndpoint attached)
|
|
@@ -106,11 +127,11 @@ class MetadataHandler {
|
|
|
106
127
|
* @param {Object} provider - Provider for code systems and resources
|
|
107
128
|
* @returns {Object} CapabilityStatement resource
|
|
108
129
|
*/
|
|
109
|
-
buildCapabilityStatement(endpoint) {
|
|
130
|
+
buildCapabilityStatement(endpoint, provider, req) {
|
|
110
131
|
|
|
111
132
|
const now = new Date().toISOString();
|
|
112
133
|
const fhirVersion = this.mapFhirVersion(endpoint.fhirVersion);
|
|
113
|
-
const baseUrl = this.
|
|
134
|
+
const baseUrl = this.resolveBaseUrl(req, endpoint);
|
|
114
135
|
const serverVersion = this.config.serverVersion;
|
|
115
136
|
|
|
116
137
|
return {
|
|
@@ -281,9 +302,9 @@ class MetadataHandler {
|
|
|
281
302
|
* @param {Object} provider - Provider for code systems and resources
|
|
282
303
|
* @returns {Object} TerminologyCapabilities resource
|
|
283
304
|
*/
|
|
284
|
-
async buildTerminologyCapabilities(endpoint, provider) {
|
|
305
|
+
async buildTerminologyCapabilities(endpoint, provider, req) {
|
|
285
306
|
const now = new Date().toISOString();
|
|
286
|
-
const baseUrl = this.
|
|
307
|
+
const baseUrl = this.resolveBaseUrl(req, endpoint);
|
|
287
308
|
const serverVersion = this.config.serverVersion || '1.0.0';
|
|
288
309
|
|
|
289
310
|
const tc = {
|
package/tx/workers/search.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const { TerminologyWorker } = require('./worker');
|
|
9
9
|
const {Utilities} = require("../../library/utilities");
|
|
10
10
|
const {debugLog} = require("../operation-context");
|
|
11
|
+
const {acceptsHtml} = require("../tx-html");
|
|
11
12
|
|
|
12
13
|
class SearchWorker extends TerminologyWorker {
|
|
13
14
|
/**
|
|
@@ -84,7 +85,12 @@ class SearchWorker extends TerminologyWorker {
|
|
|
84
85
|
break;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
// Default page size depends on the return format: browsers get small pages
|
|
89
|
+
// (20), while API clients (json/xml) get big ones (1000) - they just want
|
|
90
|
+
// the data, and small pages make crawlers (e.g. the tx registry) painfully
|
|
91
|
+
// slow. An explicit _count always wins, subject to the caps.
|
|
92
|
+
const defaultCount = acceptsHtml(req) ? 20 : 1000;
|
|
93
|
+
const count = summary === 'count' ? 0 : Math.min(elements ? 2000 : 200, params._count && Utilities.isInteger(params._count) ? parseInt(params._count) : defaultCount);
|
|
88
94
|
const sort = params._sort || "id";
|
|
89
95
|
|
|
90
96
|
// Get matching resources
|
package/tx/workers/validate.js
CHANGED
|
@@ -753,6 +753,7 @@ class ValueSetChecker {
|
|
|
753
753
|
messages.push(msg);
|
|
754
754
|
}
|
|
755
755
|
} else {
|
|
756
|
+
this.worker.opContext.addNote(this.valueSet, 'Include for ' + this.worker.renderer.displayCoded(cc.system, cc.version) + ' not considered for "' + this.worker.renderer.displayCoded(system, version) + '"', this.indentCount);
|
|
756
757
|
result = false;
|
|
757
758
|
}
|
|
758
759
|
for (let u of cc.valueSet || []) {
|
|
@@ -875,6 +876,7 @@ class ValueSetChecker {
|
|
|
875
876
|
}
|
|
876
877
|
}
|
|
877
878
|
} else {
|
|
879
|
+
this.worker.opContext.addNote(this.valueSet, 'ValueSet has neither compose nor expansion - cannot determine membership', this.indentCount);
|
|
878
880
|
result = false;
|
|
879
881
|
}
|
|
880
882
|
}
|
|
@@ -1126,17 +1128,23 @@ class ValueSetChecker {
|
|
|
1126
1128
|
} else if (c.display && list.designations.length > 0) {
|
|
1127
1129
|
await this.checkDisplays(list, defLang, c, msg, op, path);
|
|
1128
1130
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1131
|
+
// Report the FIRST matching coding, not the last: when a CodeableConcept
|
|
1132
|
+
// has several valid codings, the code/system/version/display echoed back
|
|
1133
|
+
// are those of the earliest coding that validated. Only capture once
|
|
1134
|
+
// (pcode is undefined until the first match).
|
|
1135
|
+
if (pcode === undefined) {
|
|
1136
|
+
psys = c.system;
|
|
1137
|
+
pcode = c.code;
|
|
1138
|
+
if (ver.value) {
|
|
1139
|
+
pver = ver.value;
|
|
1140
|
+
}
|
|
1141
|
+
let pd = list.preferredDisplay(this.params.workingLanguages());
|
|
1142
|
+
if (pd) {
|
|
1143
|
+
pdisp = pd;
|
|
1144
|
+
}
|
|
1145
|
+
if (!pdisp) {
|
|
1146
|
+
pdisp = list.preferredDisplay(null);
|
|
1147
|
+
}
|
|
1140
1148
|
}
|
|
1141
1149
|
} else if (!this.params.membershipOnly && ws) {
|
|
1142
1150
|
if (!isAbsoluteUrl(ws)) {
|
|
@@ -1203,7 +1211,7 @@ class ValueSetChecker {
|
|
|
1203
1211
|
}
|
|
1204
1212
|
}
|
|
1205
1213
|
} else {
|
|
1206
|
-
this.checkCanonicalStatusCS(path, op, prov, this.valueSet);
|
|
1214
|
+
await this.checkCanonicalStatusCS(path, op, prov, this.valueSet);
|
|
1207
1215
|
let ctxt = await prov.locate(c.code);
|
|
1208
1216
|
if (!ctxt.context) {
|
|
1209
1217
|
// message can never be populated in pascal?
|
|
@@ -1280,7 +1288,10 @@ class ValueSetChecker {
|
|
|
1280
1288
|
}
|
|
1281
1289
|
i++;
|
|
1282
1290
|
}
|
|
1283
|
-
|
|
1291
|
+
// for an internally defined value set (CodeSystem validation), the code-system-level
|
|
1292
|
+
// message is preferred - but if nothing produced an error, this is the only
|
|
1293
|
+
// explanation there will be, so it can't be suppressed
|
|
1294
|
+
if (ok === false && (!this.valueSet.jsonObj.internallyDefined || !op.hasErrors())) {
|
|
1284
1295
|
let mid, m, p;
|
|
1285
1296
|
if (mode === 'codeableConcept') {
|
|
1286
1297
|
mid = 'TX_GENERAL_CC_ERROR_MESSAGE';
|
|
@@ -1354,12 +1365,13 @@ class ValueSetChecker {
|
|
|
1354
1365
|
if (!mpath) {
|
|
1355
1366
|
mpath = 'code';
|
|
1356
1367
|
}
|
|
1368
|
+
let mid1 = "INACTIVE_CONCEPT_FOUND";
|
|
1369
|
+
let mm1 = "";
|
|
1357
1370
|
if (!['inactive', 'DISCOURAGED'].includes(vstatus.value)) {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
op.addIssue(new Issue('warning', 'business-rule', mpath, 'INACTIVE_CONCEPT_FOUND', m, 'code-comment'));
|
|
1371
|
+
mid1 = 'INACTIVE_CONCEPT_FOUND_ADD';
|
|
1372
|
+
mm1 = 'inactive';
|
|
1361
1373
|
}
|
|
1362
|
-
let m = this.worker.i18n.translate(
|
|
1374
|
+
let m = this.worker.i18n.translate(mid1, this.params.HTTPLanguages, [vstatus.value, tcode, mm1]);
|
|
1363
1375
|
msg(m);
|
|
1364
1376
|
op.addIssue(new Issue('warning', 'business-rule', mpath, 'INACTIVE_CONCEPT_FOUND', m, 'code-comment'));
|
|
1365
1377
|
} else if (vstatus.value && vstatus.value.toLowerCase() === 'deprecated') {
|
|
@@ -1688,7 +1700,7 @@ class ValueSetChecker {
|
|
|
1688
1700
|
if (!fc.value) {
|
|
1689
1701
|
throw new Issue('error', 'invalid', null, 'UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.worker.i18n.translate('UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.params.HTTPLanguages, [cs.system(), fc.property, fc.op]));
|
|
1690
1702
|
}
|
|
1691
|
-
await cs.filter(prep, false, fc.property, fc.op, fc.value);
|
|
1703
|
+
await cs.filter(prep, false, fc.property, fc.op, fc.value, (vs && vs.isCached) ? fc : null);
|
|
1692
1704
|
// if (f === null) {
|
|
1693
1705
|
// throw new Issue('error', 'not-supported', null, 'FILTER_NOT_UNDERSTOOD', this.worker.i18n.translate('FILTER_NOT_UNDERSTOOD', this.params.HTTPLanguages, [fc.property, fc.op, fc.value, vs.vurl, cs.system()]) + ' (2)', 'vs-invalid');
|
|
1694
1706
|
// }
|
package/tx/workers/worker.js
CHANGED
|
@@ -661,19 +661,42 @@ class TerminologyWorker {
|
|
|
661
661
|
// specific coded issue rather than silently auto-creating a fresh cache and
|
|
662
662
|
// then failing obscurely later when a by-reference resource can't be found.
|
|
663
663
|
if (!this.opContext.resourceCache.has(cacheId)) {
|
|
664
|
+
// A comma in the cache-id means the request carried multiple X-Cache-Id
|
|
665
|
+
// headers (Node joins duplicate headers with ", "): server-minted ids are
|
|
666
|
+
// single uuids. This happens when a client attaches a new cache-id header
|
|
667
|
+
// without clearing the old one (e.g. IG publisher builds where a second
|
|
668
|
+
// client context wrapped the same HTTP client) - report that specifically,
|
|
669
|
+
// because the generic "unknown cache" message sends people looking for an
|
|
670
|
+
// expiry problem that isn't there.
|
|
671
|
+
if (cacheId.includes(',')) {
|
|
672
|
+
throw new Issue('error', 'invalid', null, 'CACHE_ID_DUPLICATE',
|
|
673
|
+
`The request carried multiple ${CACHE_ID_HEADER} headers (combined value '${cacheId}'). ` +
|
|
674
|
+
'This is a client error: each request must carry exactly one cache-id. It usually means the client ' +
|
|
675
|
+
'started a second cache on a connection that already had one, without removing the first header.',
|
|
676
|
+
'cache-id-duplicate', 400);
|
|
677
|
+
}
|
|
664
678
|
throw new Issue('error', 'not-found', null, 'CACHE_ID_UNKNOWN',
|
|
665
679
|
this.i18n.translate('CACHE_ID_UNKNOWN', this.opContext.langs, [cacheId]),
|
|
666
680
|
'cache-id-unknown', 404);
|
|
667
681
|
}
|
|
668
682
|
|
|
669
|
-
// The cache exists
|
|
670
|
-
// (incremental population is
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
683
|
+
// The cache exists. If it is unsealed, merge any resources supplied on this
|
|
684
|
+
// request into it (incremental population). If it is sealed, the cache is
|
|
685
|
+
// fixed at what it was created with: resources supplied now are still used
|
|
686
|
+
// for this request (via additionalResources below) but are NOT added to the
|
|
687
|
+
// shared cache, so a sealed cache never grows.
|
|
688
|
+
if (!this.opContext.resourceCache.isSealed(cacheId)) {
|
|
689
|
+
const toCache = txResources.concat(primaryResources);
|
|
690
|
+
if (toCache.length > 0) {
|
|
691
|
+
this.opContext.resourceCache.add(cacheId, toCache);
|
|
692
|
+
}
|
|
693
|
+
this.additionalResources = this.opContext.resourceCache.get(cacheId);
|
|
694
|
+
} else {
|
|
695
|
+
// Sealed: expose the cache contents plus this request's own inline
|
|
696
|
+
// resources (used for this call only), without mutating the cache.
|
|
697
|
+
this.additionalResources = this.opContext.resourceCache.get(cacheId)
|
|
698
|
+
.concat(txResources, primaryResources);
|
|
674
699
|
}
|
|
675
|
-
|
|
676
|
-
this.additionalResources = this.opContext.resourceCache.get(cacheId);
|
|
677
700
|
this.additionalResourcesCacheId = cacheId;
|
|
678
701
|
} else {
|
|
679
702
|
// No cache-id, just use the tx-resources directly
|
|
@@ -131,9 +131,12 @@ function valueSetR5ToR4(r5Obj) {
|
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
133
|
delete exp.property;
|
|
134
|
-
convertContainsPropertyR5ToR4(exp.contains);
|
|
135
|
-
|
|
136
134
|
}
|
|
135
|
+
|
|
136
|
+
// ValueSet.expansion.contains.property is R5-only too, and must be converted
|
|
137
|
+
// whether or not the expansion declares expansion-level property definitions:
|
|
138
|
+
// a contained code can carry a property with no expansion.property present.
|
|
139
|
+
convertContainsPropertyR5ToR4(exp.contains);
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
return r5Obj;
|
package/xig/xig.js
CHANGED
|
@@ -977,7 +977,7 @@ async function fetchResourceRows(queryParams, offset = 0, limit = 200) {
|
|
|
977
977
|
}
|
|
978
978
|
|
|
979
979
|
function rowToObject(row, queryParams) {
|
|
980
|
-
const {
|
|
980
|
+
const { type } = queryParams;
|
|
981
981
|
const packageObj = getPackage(row.PackageKey);
|
|
982
982
|
|
|
983
983
|
const obj = {
|
|
@@ -3156,7 +3156,7 @@ async function initializeXigModule(stats, xigConfig) {
|
|
|
3156
3156
|
if (!fs.existsSync(XIG_DB_PATH)) {
|
|
3157
3157
|
xigLog.info('No existing XIG database found, triggering initial download');
|
|
3158
3158
|
setTimeout(() => {
|
|
3159
|
-
updateXigDatabase();
|
|
3159
|
+
updateXigDatabase().catch((err) => xigLog.error('XIG database update failed: ' + err.message));
|
|
3160
3160
|
}, 5000);
|
|
3161
3161
|
}
|
|
3162
3162
|
|
|
@@ -3167,7 +3167,7 @@ async function initializeXigModule(stats, xigConfig) {
|
|
|
3167
3167
|
// Note: This assumes we're called only when XIG is enabled
|
|
3168
3168
|
if (xigConfig?.autoUpdate !== false) {
|
|
3169
3169
|
cron.schedule('0 2 * * *', () => {
|
|
3170
|
-
updateXigDatabase();
|
|
3170
|
+
updateXigDatabase().catch((err) => xigLog.error('XIG database update failed: ' + err.message));
|
|
3171
3171
|
});
|
|
3172
3172
|
}
|
|
3173
3173
|
|