fhirsmith 0.10.0 → 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 +48 -1
- 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 +235 -38
- package/tx/library.js +5 -1
- package/tx/operation-context.js +40 -3
- package/tx/provider.js +41 -12
- 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-database.js +41 -8
- package/tx/vs/vs-package.js +55 -0
- package/tx/vs/vs-vsac.js +4 -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
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
//
|
|
2
|
-
// Validate Worker - Handles $validate-code operations
|
|
2
|
+
// Batch Validate Worker - Handles $batch-validate-code operations
|
|
3
3
|
//
|
|
4
|
-
// GET /
|
|
5
|
-
// POST /CodeSystem/$validate-code
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
4
|
+
// GET/POST /ValueSet/$batch-validate-code - batch of ValueSet $validate-code
|
|
5
|
+
// GET/POST /CodeSystem/$batch-validate-code - batch of CodeSystem $validate-code
|
|
6
|
+
//
|
|
7
|
+
// A batch is a Parameters whose `validation` entries each carry a single
|
|
8
|
+
// $validate-code request (as a nested Parameters). Shared inputs may be supplied
|
|
9
|
+
// once at the top level ("globals") and are applied to every entry. Entries are
|
|
10
|
+
// dispatched per-shape (a url/valueSet entry validates against a value set; a
|
|
11
|
+
// system/codeSystem entry validates against a code system), so both batch routes
|
|
12
|
+
// share one implementation and even a mixed batch is handled correctly.
|
|
12
13
|
//
|
|
13
14
|
|
|
14
15
|
const { TerminologyWorker } = require('./worker');
|
|
@@ -30,12 +31,20 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
30
31
|
*/
|
|
31
32
|
constructor(opContext, log, provider, languages, i18n) {
|
|
32
33
|
super(opContext, log, provider, languages, i18n);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.
|
|
38
|
-
|
|
34
|
+
// "Global" parameters may be supplied once at the top level and are applied to
|
|
35
|
+
// every entry that doesn't override them. tx-resource is always shared. The
|
|
36
|
+
// primary differs by operation: a ValueSet batch shares url/valueSet, a
|
|
37
|
+
// CodeSystem batch shares system/codeSystem.
|
|
38
|
+
this.valueSetGlobalNames = new Set([
|
|
39
|
+
"tx-resource", "url", "valueSet", "lenient-display-validation",
|
|
40
|
+
"__Accept-Language", "__Content-Language"
|
|
41
|
+
]);
|
|
42
|
+
this.codeSystemGlobalNames = new Set([
|
|
43
|
+
"tx-resource", "system", "codeSystem", "lenient-display-validation",
|
|
44
|
+
"__Accept-Language", "__Content-Language"
|
|
45
|
+
]);
|
|
46
|
+
// Back-compat: `globalNames` was the (ValueSet) set before CodeSystem batches.
|
|
47
|
+
this.globalNames = this.valueSetGlobalNames;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
/**
|
|
@@ -46,18 +55,43 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
46
55
|
return 'batch-validate-code';
|
|
47
56
|
}
|
|
48
57
|
|
|
58
|
+
/** ValueSet/$batch-validate-code: shared globals are url/valueSet. */
|
|
49
59
|
async handleValueSet(req, res) {
|
|
60
|
+
return this.processBatch(req, res, this.valueSetGlobalNames);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** CodeSystem/$batch-validate-code: shared globals are system/codeSystem. */
|
|
64
|
+
async handleCodeSystem(req, res) {
|
|
65
|
+
return this.processBatch(req, res, this.codeSystemGlobalNames);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Run a batch of $validate-code requests. Two passes: frontLoadBatch() pools all
|
|
70
|
+
* supplied resources into an unsealed cache first (see that method); then each
|
|
71
|
+
* `validation` entry is evaluated, inheriting the top-level globals it does not
|
|
72
|
+
* override and dispatched by shape to the ValueSet or CodeSystem validator.
|
|
73
|
+
*
|
|
74
|
+
* @param {express.Request} req
|
|
75
|
+
* @param {express.Response} res
|
|
76
|
+
* @param {Set<string>} globalNames - which top-level params are shared globals
|
|
77
|
+
*/
|
|
78
|
+
async processBatch(req, res, globalNames) {
|
|
50
79
|
try {
|
|
51
80
|
let params = req.body;
|
|
52
81
|
this.addHttpParams(req, params);
|
|
53
82
|
|
|
54
83
|
let globalParams = [];
|
|
55
84
|
for (const p of params.parameter) {
|
|
56
|
-
if (
|
|
85
|
+
if (globalNames.has(p.name)) {
|
|
57
86
|
globalParams.push(p);
|
|
58
87
|
}
|
|
59
88
|
}
|
|
60
89
|
|
|
90
|
+
// Pass 1: front-load every resource the batch supplies into the (unsealed)
|
|
91
|
+
// session cache before any entry is evaluated. When this happens, pass 2
|
|
92
|
+
// below drops per-entry tx-resource processing (they're already cached).
|
|
93
|
+
const frontLoaded = this.frontLoadBatch(params);
|
|
94
|
+
|
|
61
95
|
let output = [];
|
|
62
96
|
|
|
63
97
|
for (const p of params.parameter) {
|
|
@@ -65,12 +99,26 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
65
99
|
let op = new Parameters();
|
|
66
100
|
op.jsonObj.parameter = [];
|
|
67
101
|
for (const gp of globalParams) {
|
|
102
|
+
if (gp.name == 'tx-resource') {
|
|
103
|
+
// Pass 2: when the batch was front-loaded into an unsealed cache, the
|
|
104
|
+
// tx-resources are already in the cache and every entry resolves them
|
|
105
|
+
// by reference - don't re-inject or re-process them per entry. Without
|
|
106
|
+
// front-loading (no cache, or a sealed cache) keep the original
|
|
107
|
+
// behaviour: the global tx-resources apply to every entry.
|
|
108
|
+
if (!frontLoaded) {
|
|
109
|
+
op.jsonObj.parameter.push(gp);
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
68
113
|
let exists = p.resource.parameter.find(pp => gp.name == pp.name);
|
|
69
|
-
if (
|
|
114
|
+
if (!exists) {
|
|
70
115
|
op.jsonObj.parameter.push(gp);
|
|
71
116
|
}
|
|
72
117
|
}
|
|
73
|
-
|
|
118
|
+
const entryParams = frontLoaded
|
|
119
|
+
? p.resource.parameter.filter(pp => pp.name !== 'tx-resource')
|
|
120
|
+
: p.resource.parameter;
|
|
121
|
+
op.jsonObj.parameter.push(...entryParams);
|
|
74
122
|
|
|
75
123
|
let worker = new ValidateWorker(this.opContext.copy(), this.log, this.provider, this.languages, this.i18n);
|
|
76
124
|
try {
|
|
@@ -100,11 +148,70 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
100
148
|
} catch (error) {
|
|
101
149
|
this.log.error(error);
|
|
102
150
|
debugLog(error);
|
|
151
|
+
// A batch-level failure (e.g. an unknown cache-id from pass 1) applies to the
|
|
152
|
+
// whole batch. Preserve a coded Issue (like cache-id-unknown) so the client
|
|
153
|
+
// gets the same coded OperationOutcome + status it would on a single op.
|
|
154
|
+
if (error instanceof Issue) {
|
|
155
|
+
const oo = new OperationOutcome();
|
|
156
|
+
oo.addIssue(error);
|
|
157
|
+
return res.status(error.statusCode || 500).json(oo.jsonObj);
|
|
158
|
+
}
|
|
103
159
|
return res.status(error.statusCode || 500).json(this.operationOutcome(
|
|
104
160
|
'error', error.issueCode || 'exception', error.message));
|
|
105
161
|
}
|
|
106
162
|
}
|
|
107
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Pass 1 of batch processing: front-load every resource the batch supplies
|
|
166
|
+
* (each `tx-resource`, plus each entry's primary `valueSet`/`codeSystem`) into
|
|
167
|
+
* the session cache before any entry is evaluated. This makes the batch
|
|
168
|
+
* order-independent - an entry may refer by url to a resource another entry
|
|
169
|
+
* supplied - and means a failing entry does not withhold the resources it
|
|
170
|
+
* carried, because population happens up front and as one step.
|
|
171
|
+
*
|
|
172
|
+
* Front-loading is only relevant for an *unsealed* cache: that is what grows, so
|
|
173
|
+
* that is how one entry's resources become visible to the others. A sealed cache
|
|
174
|
+
* does not grow (each entry stays self-contained), and with no cache there is
|
|
175
|
+
* nowhere to pool; in both cases this returns false and pass 2 keeps the original
|
|
176
|
+
* per-entry tx-resource handling.
|
|
177
|
+
*
|
|
178
|
+
* The unknown-cache-id check is done here, once, for the whole batch.
|
|
179
|
+
*
|
|
180
|
+
* @param {Object} params - the batch Parameters (req.body)
|
|
181
|
+
* @returns {boolean} true if resources were front-loaded into an unsealed cache
|
|
182
|
+
*/
|
|
183
|
+
frontLoadBatch(params) {
|
|
184
|
+
const cacheId = this.opContext ? this.opContext.cacheId : null;
|
|
185
|
+
const cache = this.opContext ? this.opContext.resourceCache : null;
|
|
186
|
+
if (!cacheId || !cache) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
if (!cache.has(cacheId)) {
|
|
190
|
+
throw new Issue('error', 'not-found', null, 'CACHE_ID_UNKNOWN',
|
|
191
|
+
this.i18n.translate('CACHE_ID_UNKNOWN', this.opContext.langs, [cacheId]),
|
|
192
|
+
'cache-id-unknown', 404);
|
|
193
|
+
}
|
|
194
|
+
if (cache.isSealed(cacheId)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
// Flatten the top-level params and every entry's params into one list, then let
|
|
198
|
+
// collectSuppliedResources pick out the tx-resource + primary valueSet/codeSystem.
|
|
199
|
+
const allParams = [];
|
|
200
|
+
for (const p of params.parameter) {
|
|
201
|
+
if (p.name === 'validation' && p.resource && Array.isArray(p.resource.parameter)) {
|
|
202
|
+
allParams.push(...p.resource.parameter);
|
|
203
|
+
} else {
|
|
204
|
+
allParams.push(p);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const { txResources, primaryResources } = this.collectSuppliedResources({ parameter: allParams });
|
|
208
|
+
const pool = txResources.concat(primaryResources);
|
|
209
|
+
if (pool.length > 0) {
|
|
210
|
+
cache.add(cacheId, pool);
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
108
215
|
/**
|
|
109
216
|
* Build an OperationOutcome
|
|
110
217
|
*/
|
|
@@ -124,17 +124,46 @@ class CacheControlWorker extends TerminologyWorker {
|
|
|
124
124
|
const { txResources, primaryResources } = this.collectSuppliedResources(params.jsonObj);
|
|
125
125
|
const resources = txResources.concat(primaryResources);
|
|
126
126
|
|
|
127
|
+
// `sealed` controls whether the cache may grow after creation. When sealed,
|
|
128
|
+
// the cache holds only the resources front-loaded here; when unsealed, later
|
|
129
|
+
// operations accumulate every resource they see into it (see
|
|
130
|
+
// setupAdditionalResources).
|
|
131
|
+
//
|
|
132
|
+
// NOTE: the protocol default is `true`, but the server default here is
|
|
133
|
+
// deliberately `false` during the transition: existing clients that rely on
|
|
134
|
+
// incremental population and do not yet send `sealed` must keep working.
|
|
135
|
+
// Flip this to default-true once all clients send an explicit value.
|
|
136
|
+
const sealed = this.readSealed(params.jsonObj);
|
|
137
|
+
|
|
127
138
|
const cacheId = crypto.randomUUID();
|
|
128
|
-
cache.set(cacheId, resources);
|
|
139
|
+
cache.set(cacheId, resources, sealed);
|
|
129
140
|
|
|
130
141
|
return res.status(200).json({
|
|
131
142
|
resourceType: 'Parameters',
|
|
132
143
|
parameter: [
|
|
133
|
-
{ name: 'cache-id', valueId: cacheId }
|
|
144
|
+
{ name: 'cache-id', valueId: cacheId },
|
|
145
|
+
{ name: 'sealed', valueBoolean: sealed }
|
|
134
146
|
]
|
|
135
147
|
});
|
|
136
148
|
}
|
|
137
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Read the `sealed` boolean from the start request's Parameters.
|
|
152
|
+
*
|
|
153
|
+
* Server-side default is FALSE (transitional — see start()): a cache is only
|
|
154
|
+
* sealed if the client explicitly asks for it. Accepts a real JSON boolean or
|
|
155
|
+
* the string "true"/"false" for robustness across clients.
|
|
156
|
+
*
|
|
157
|
+
* @param {Object} params - Parameters resource (jsonObj)
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
160
|
+
readSealed(params) {
|
|
161
|
+
const p = this.findParameter(params, 'sealed');
|
|
162
|
+
if (!p) return false;
|
|
163
|
+
const v = this.getParameterValue(p);
|
|
164
|
+
return v === true || v === 'true';
|
|
165
|
+
}
|
|
166
|
+
|
|
138
167
|
/**
|
|
139
168
|
* mode=end: release the cache named by the `${CACHE_ID_HEADER}` header so the
|
|
140
169
|
* server can reclaim it now rather than waiting for the idle timeout.
|
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
|
// }
|