fhirsmith 0.9.7 → 0.10.1
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 +42 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +123 -6
- package/packages/packages.js +104 -0
- package/publisher/publisher.js +191 -5
- package/registry/crawler.js +85 -6
- package/registry/registry.js +18 -1
- package/server.js +5 -0
- package/stats.js +26 -18
- package/translations/Messages.properties +1 -0
- package/tx/html/home-metrics.liquid +10 -10
- package/tx/library/codesystem.js +31 -0
- package/tx/library/conceptmap.js +24 -0
- package/tx/library/renderer.js +8 -8
- package/tx/library/valueset.js +46 -0
- package/tx/operation-context.js +109 -4
- package/tx/provider.js +3 -3
- package/tx/tx-html.js +27 -0
- package/tx/tx.js +80 -17
- package/tx/vs/vs-database.js +41 -8
- package/tx/vs/vs-package.js +3 -0
- package/tx/vs/vs-vsac.js +4 -0
- package/tx/workers/cache-control.js +186 -0
- package/tx/workers/{related.js → compare.js} +27 -27
- package/tx/workers/metadata.js +11 -6
- package/tx/workers/worker.js +133 -46
- package/tx/data/OperationDefinition-ValueSet-related.json +0 -133
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Cache Control Worker - Handles the $cache-control operation
|
|
3
|
+
//
|
|
4
|
+
// GET /$cache-control?mode=start - create a cache, return its (server-issued) id
|
|
5
|
+
// POST /$cache-control?mode=start - as above, optionally front-loading resources
|
|
6
|
+
// GET /$cache-control?mode=end - tell the server it can release the cache now
|
|
7
|
+
// POST /$cache-control?mode=end
|
|
8
|
+
// (mode=check is reserved for later - report whether a cache is still valid + stats)
|
|
9
|
+
//
|
|
10
|
+
// This is the explicit replacement for the implicit `cache-id` parameter protocol:
|
|
11
|
+
// the server owns the cache-id namespace, so it can authoritatively reject an
|
|
12
|
+
// unknown/expired cache later instead of failing obscurely deep inside a validation.
|
|
13
|
+
//
|
|
14
|
+
// NOTE: this is scaffolding. start()/end() currently parse the request into a
|
|
15
|
+
// Parameters resource (the same way validate.js does) but do not yet create or
|
|
16
|
+
// release anything. The behaviour is filled in by later steps.
|
|
17
|
+
//
|
|
18
|
+
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const { TerminologyWorker, CACHE_ID_HEADER } = require('./worker');
|
|
21
|
+
const { Parameters } = require('../library/parameters');
|
|
22
|
+
const { debugLog } = require('../operation-context');
|
|
23
|
+
|
|
24
|
+
class CacheControlWorker extends TerminologyWorker {
|
|
25
|
+
/**
|
|
26
|
+
* @param {OperationContext} opContext - Operation context
|
|
27
|
+
* @param {Logger} log - Logger instance
|
|
28
|
+
* @param {Provider} provider - Provider for code systems and resources
|
|
29
|
+
* @param {LanguageDefinitions} languages - Language definitions
|
|
30
|
+
* @param {I18nSupport} i18n - Internationalization support
|
|
31
|
+
*/
|
|
32
|
+
constructor(opContext, log, provider, languages, i18n) {
|
|
33
|
+
super(opContext, log, provider, languages, i18n);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
opName() {
|
|
37
|
+
return 'cache-control';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Not a value-set operation; the base class requires this to be implemented.
|
|
41
|
+
vsHandle() {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Express entry point for /$cache-control (GET and POST).
|
|
47
|
+
* Dispatches on the `mode` query parameter.
|
|
48
|
+
* @param {express.Request} req - Express request
|
|
49
|
+
* @param {express.Response} res - Express response
|
|
50
|
+
*/
|
|
51
|
+
async handle(req, res) {
|
|
52
|
+
try {
|
|
53
|
+
const mode = this.getMode(req);
|
|
54
|
+
// GET is accepted as well as POST, for consistency with the server's other
|
|
55
|
+
// operations (which are all browsable) and the convenience of poking
|
|
56
|
+
// $cache-control from a browser. The operation is declared affectsState=true
|
|
57
|
+
// in its OperationDefinition, so conformant clients POST; an accidental
|
|
58
|
+
// GET-created cache is an empty entry that self-expires.
|
|
59
|
+
switch (mode) {
|
|
60
|
+
case 'start':
|
|
61
|
+
return await this.start(req, res);
|
|
62
|
+
case 'end':
|
|
63
|
+
return await this.end(req, res);
|
|
64
|
+
default:
|
|
65
|
+
return res.status(400).json(this.operationOutcome('error', 'invalid',
|
|
66
|
+
`$cache-control requires a 'mode' of 'start' or 'end'` +
|
|
67
|
+
(mode ? ` (got '${mode}')` : ` (none supplied)`)));
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
this.log.error(error);
|
|
71
|
+
debugLog(error);
|
|
72
|
+
req.logInfo = this.usedSources.join('|') + ' - error' + (error.msgId ? ' ' + error.msgId : '');
|
|
73
|
+
const statusCode = error.statusCode || 500;
|
|
74
|
+
const issueCode = error.issueCode || 'exception';
|
|
75
|
+
return res.status(statusCode).json(this.operationOutcome('error', issueCode, error.message));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Determine the requested mode. The cache-control mode travels in the query
|
|
81
|
+
* string (?mode=start) so it survives even when a Parameters resource is POSTed
|
|
82
|
+
* as the body; a `mode` parameter in the body is accepted as a fallback.
|
|
83
|
+
* @param {express.Request} req
|
|
84
|
+
* @returns {string|null}
|
|
85
|
+
*/
|
|
86
|
+
getMode(req) {
|
|
87
|
+
if (req.query && req.query.mode) {
|
|
88
|
+
return String(req.query.mode);
|
|
89
|
+
}
|
|
90
|
+
if (req.body && req.body.resourceType === 'Parameters') {
|
|
91
|
+
const p = this.findParameter(req.body, 'mode');
|
|
92
|
+
if (p) {
|
|
93
|
+
return this.getParameterValue(p);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* mode=start: mint a server-issued cache-id, create the (per-endpoint) cache,
|
|
101
|
+
* front-load any supplied resources into it, and return the id in the body.
|
|
102
|
+
*
|
|
103
|
+
* The id is returned in the body rather than a header to keep it readable by
|
|
104
|
+
* browser clients without CORS expose-header configuration; subsequent calls
|
|
105
|
+
* carry it back as the `${CACHE_ID_HEADER}` request header.
|
|
106
|
+
*
|
|
107
|
+
* The cache is created even when no resources are front-loaded: an explicitly
|
|
108
|
+
* empty cache must still *exist* so the server can later tell "cache I issued,
|
|
109
|
+
* currently empty" from "cache-id I never issued". That's why this uses
|
|
110
|
+
* ResourceCache.set (which always creates the entry) rather than add (which
|
|
111
|
+
* ignores empty resource lists).
|
|
112
|
+
*
|
|
113
|
+
* @param {express.Request} req
|
|
114
|
+
* @param {express.Response} res
|
|
115
|
+
*/
|
|
116
|
+
async start(req, res) {
|
|
117
|
+
const cache = this.opContext.resourceCache;
|
|
118
|
+
if (!cache) {
|
|
119
|
+
return res.status(500).json(this.operationOutcome('error', 'exception',
|
|
120
|
+
'No resource cache is available on this endpoint'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const params = new Parameters(this.buildParameters(req));
|
|
124
|
+
const { txResources, primaryResources } = this.collectSuppliedResources(params.jsonObj);
|
|
125
|
+
const resources = txResources.concat(primaryResources);
|
|
126
|
+
|
|
127
|
+
const cacheId = crypto.randomUUID();
|
|
128
|
+
cache.set(cacheId, resources);
|
|
129
|
+
|
|
130
|
+
return res.status(200).json({
|
|
131
|
+
resourceType: 'Parameters',
|
|
132
|
+
parameter: [
|
|
133
|
+
{ name: 'cache-id', valueId: cacheId }
|
|
134
|
+
]
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* mode=end: release the cache named by the `${CACHE_ID_HEADER}` header so the
|
|
140
|
+
* server can reclaim it now rather than waiting for the idle timeout.
|
|
141
|
+
*
|
|
142
|
+
* Releasing is idempotent: ending an id the server doesn't have is not an error
|
|
143
|
+
* here (the authoritative "unknown cache" signal belongs on the validation path,
|
|
144
|
+
* a later step). A missing header is a client error, though.
|
|
145
|
+
*
|
|
146
|
+
* @param {express.Request} req
|
|
147
|
+
* @param {express.Response} res
|
|
148
|
+
*/
|
|
149
|
+
async end(req, res) {
|
|
150
|
+
const cache = this.opContext.resourceCache;
|
|
151
|
+
if (!cache) {
|
|
152
|
+
return res.status(500).json(this.operationOutcome('error', 'exception',
|
|
153
|
+
'No resource cache is available on this endpoint'));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const cacheId = req.headers[CACHE_ID_HEADER];
|
|
157
|
+
if (!cacheId) {
|
|
158
|
+
return res.status(400).json(this.operationOutcome('error', 'invalid',
|
|
159
|
+
`$cache-control mode=end requires the cache-id in the '${CACHE_ID_HEADER}' header`));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
cache.clear(cacheId);
|
|
163
|
+
|
|
164
|
+
return res.status(200).json({ resourceType: 'Parameters', parameter: [] });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Build an OperationOutcome.
|
|
169
|
+
* @param {string} severity - error, warning, information
|
|
170
|
+
* @param {string} code - Issue code
|
|
171
|
+
* @param {string} message - Diagnostic message
|
|
172
|
+
* @returns {Object} OperationOutcome resource
|
|
173
|
+
*/
|
|
174
|
+
operationOutcome(severity, code, message) {
|
|
175
|
+
return {
|
|
176
|
+
resourceType: 'OperationOutcome',
|
|
177
|
+
issue: [{
|
|
178
|
+
severity,
|
|
179
|
+
code,
|
|
180
|
+
details: { text: message }
|
|
181
|
+
}]
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { CacheControlWorker, CACHE_ID_HEADER };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
//
|
|
2
|
-
//
|
|
2
|
+
// Compare Worker - Handles ValueSet $compare operation
|
|
3
3
|
//
|
|
4
|
-
// GET /ValueSet/{id}/$
|
|
5
|
-
// GET /ValueSet/$
|
|
6
|
-
// POST /ValueSet/$
|
|
7
|
-
// POST /ValueSet/$
|
|
8
|
-
// POST /ValueSet/$
|
|
4
|
+
// GET /ValueSet/{id}/$compare
|
|
5
|
+
// GET /ValueSet/$compare?url=...&version=...
|
|
6
|
+
// POST /ValueSet/$compare (form body or Parameters with url)
|
|
7
|
+
// POST /ValueSet/$compare (body is ValueSet resource)
|
|
8
|
+
// POST /ValueSet/$compare (body is Parameters with valueSet parameter)
|
|
9
9
|
//
|
|
10
10
|
|
|
11
11
|
const { TerminologyWorker } = require('./worker');
|
|
@@ -18,7 +18,7 @@ const {SearchFilterText} = require("../library/designations");
|
|
|
18
18
|
const {ArrayMatcher} = require("../../library/utilities");
|
|
19
19
|
const {debugLog} = require("../operation-context");
|
|
20
20
|
|
|
21
|
-
class
|
|
21
|
+
class CompareWorker extends TerminologyWorker {
|
|
22
22
|
showLogic = false;
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -37,18 +37,18 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
37
37
|
* @returns {string}
|
|
38
38
|
*/
|
|
39
39
|
opName() {
|
|
40
|
-
return '
|
|
40
|
+
return 'compare';
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Handle a type-level $
|
|
45
|
-
* GET/POST /ValueSet/$
|
|
44
|
+
* Handle a type-level $compare request
|
|
45
|
+
* GET/POST /ValueSet/$compare
|
|
46
46
|
* @param {express.Request} req - Express request
|
|
47
47
|
* @param {express.Response} res - Express response
|
|
48
48
|
*/
|
|
49
49
|
async handle(req, res) {
|
|
50
50
|
try {
|
|
51
|
-
await this.
|
|
51
|
+
await this.handleTypeLevelCompare(req, res);
|
|
52
52
|
} catch (error) {
|
|
53
53
|
this.log.error(error);
|
|
54
54
|
debugLog(error);
|
|
@@ -76,14 +76,14 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* Handle an instance-level $
|
|
80
|
-
* GET/POST /ValueSet/{id}/$
|
|
79
|
+
* Handle an instance-level $compare request
|
|
80
|
+
* GET/POST /ValueSet/{id}/$compare
|
|
81
81
|
* @param {express.Request} req - Express request
|
|
82
82
|
* @param {express.Response} res - Express response
|
|
83
83
|
*/
|
|
84
84
|
async handleInstance(req, res) {
|
|
85
85
|
try {
|
|
86
|
-
await this.
|
|
86
|
+
await this.handleInstanceLevelCompare(req, res);
|
|
87
87
|
} catch (error) {
|
|
88
88
|
this.log.error(error);
|
|
89
89
|
debugLog(error);
|
|
@@ -105,11 +105,11 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
* Handle type-level $
|
|
108
|
+
* Handle type-level $compare: /ValueSet/$compare
|
|
109
109
|
* ValueSet identified by url, or provided directly in body
|
|
110
110
|
*/
|
|
111
|
-
async
|
|
112
|
-
this.deadCheck('
|
|
111
|
+
async handleTypeLevelCompare(req, res) {
|
|
112
|
+
this.deadCheck('compare-type-level');
|
|
113
113
|
|
|
114
114
|
let params = req.body;
|
|
115
115
|
this.addHttpParams(req, params);
|
|
@@ -121,16 +121,16 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
121
121
|
let thisVS = await this.readValueSet(res, "this", params, txp);
|
|
122
122
|
let otherVS = await this.readValueSet(res, "other", params, txp);
|
|
123
123
|
|
|
124
|
-
const result = await this.
|
|
124
|
+
const result = await this.doCompare(txp, thisVS, otherVS);
|
|
125
125
|
return res.json(result);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
* Handle instance-level
|
|
129
|
+
* Handle instance-level compare: /ValueSet/{id}/$compare
|
|
130
130
|
* ValueSet identified by resource ID
|
|
131
131
|
*/
|
|
132
|
-
async
|
|
133
|
-
this.deadCheck('
|
|
132
|
+
async handleInstanceLevelCompare(req, res) {
|
|
133
|
+
this.deadCheck('compare-instance-level');
|
|
134
134
|
|
|
135
135
|
let params = req.body;
|
|
136
136
|
this.addHttpParams(req, params);
|
|
@@ -147,7 +147,7 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
147
147
|
}
|
|
148
148
|
let otherVS = await this.readValueSet(res, "other", params, txp);
|
|
149
149
|
|
|
150
|
-
const result = await this.
|
|
150
|
+
const result = await this.doCompare(txp, thisVS, otherVS);
|
|
151
151
|
return res.json(result);
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -198,7 +198,7 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
async
|
|
201
|
+
async doCompare(txp, thisVS, otherVS) {
|
|
202
202
|
|
|
203
203
|
// ok, we have to compare the composes. we don't care about anything else
|
|
204
204
|
const thisC = thisVS.jsonObj.compose;
|
|
@@ -206,12 +206,12 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
206
206
|
if (!thisC) {
|
|
207
207
|
return this.makeOutcome("indeterminate", `The ValueSet ${thisVS.vurl} has no compose`);
|
|
208
208
|
}
|
|
209
|
-
Extensions.checkNoModifiers(thisC, '
|
|
209
|
+
Extensions.checkNoModifiers(thisC, 'CompareWorker.doCompare', 'compose', thisVS.vurl)
|
|
210
210
|
this.checkNoLockedDate(thisVS.vurl, thisC);
|
|
211
211
|
if (!otherC) {
|
|
212
212
|
return this.makeOutcome("indeterminate", `The ValueSet ${otherVS.vurl} has no compose`);
|
|
213
213
|
}
|
|
214
|
-
Extensions.checkNoModifiers(otherC, '
|
|
214
|
+
Extensions.checkNoModifiers(otherC, 'CompareWorker.doCompare', 'compose', otherVS.vurl)
|
|
215
215
|
this.checkNoLockedDate(otherVS.vurl, otherC);
|
|
216
216
|
|
|
217
217
|
let systems = new Map(); // tracks whether the comparison is version dependent or not
|
|
@@ -268,7 +268,7 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
268
268
|
} else if (status.left) {
|
|
269
269
|
outcome = this.makeOutcome("superset", `The valueSet ${thisVS.vurl} is a super-set of the valueSet ${otherVS.vurl}`);
|
|
270
270
|
} else {
|
|
271
|
-
outcome = this.makeOutcome("subset", `The valueSet ${thisVS.vurl} is a
|
|
271
|
+
outcome = this.makeOutcome("subset", `The valueSet ${thisVS.vurl} is a sub-set of the valueSet ${otherVS.vurl}`);
|
|
272
272
|
}
|
|
273
273
|
if (txp.diagnostics) {
|
|
274
274
|
outcome.parameter.push({name: 'performed-expansion', valueBoolean: exp ? true : false})
|
|
@@ -712,5 +712,5 @@ class RelatedWorker extends TerminologyWorker {
|
|
|
712
712
|
}
|
|
713
713
|
|
|
714
714
|
module.exports = {
|
|
715
|
-
|
|
715
|
+
CompareWorker
|
|
716
716
|
};
|
package/tx/workers/metadata.js
CHANGED
|
@@ -233,7 +233,7 @@ class MetadataHandler {
|
|
|
233
233
|
operation: [
|
|
234
234
|
{ name: 'expand', definition: 'http://hl7.org/fhir/OperationDefinition/ValueSet-expand' },
|
|
235
235
|
{ name: 'validate-code', definition: 'http://hl7.org/fhir/OperationDefinition/ValueSet-validate-code' },
|
|
236
|
-
{ name: '
|
|
236
|
+
{ name: 'compare', definition: 'http://hl7.org/fhir/tools/OperationDefinition/ValueSet-compare' }
|
|
237
237
|
]
|
|
238
238
|
},
|
|
239
239
|
{
|
|
@@ -266,7 +266,8 @@ class MetadataHandler {
|
|
|
266
266
|
{ name: 'validate-code', definition: 'http://hl7.org/fhir/OperationDefinition/Resource-validate-code' },
|
|
267
267
|
{ name: 'translate', definition: 'http://hl7.org/fhir/OperationDefinition/ConceptMap-translate' },
|
|
268
268
|
{ name: 'closure', definition: 'http://hl7.org/fhir/OperationDefinition/ConceptMap-closure' },
|
|
269
|
-
{ name: '
|
|
269
|
+
{ name: 'compare', definition: 'http://hl7.org/fhir/tools/OperationDefinition/ValueSet-compare' },
|
|
270
|
+
{ name: 'cache-control', definition: 'http://hl7.org/fhir/tools/OperationDefinition/cache-control' },
|
|
270
271
|
{ name: 'versions', definition: 'http://hl7.org/fhir/OperationDefinition/fhir-versions' }
|
|
271
272
|
]
|
|
272
273
|
}
|
|
@@ -395,10 +396,14 @@ class MetadataHandler {
|
|
|
395
396
|
buildExpansionCapabilities() {
|
|
396
397
|
return {
|
|
397
398
|
parameter: [
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
399
|
+
// NOTE: `cache-id` is deliberately not advertised as an expansion parameter.
|
|
400
|
+
// Caching is now discovered via the $cache-control operation in the
|
|
401
|
+
// CapabilityStatement, not by advertising a cache-id parameter here. The old
|
|
402
|
+
// advertisement implied the implicit "auto-create on first sight" protocol,
|
|
403
|
+
// which has been replaced: a cache must be created explicitly with
|
|
404
|
+
// $cache-control?mode=start before its cache-id can be used. Clients that
|
|
405
|
+
// only know the old mechanism will see no cache-id parameter and simply not
|
|
406
|
+
// cache (inlining each request), which is correct, just not optimised.
|
|
402
407
|
{
|
|
403
408
|
name: 'tx-resource',
|
|
404
409
|
documentation: 'Additional valuesets needed for evaluation e.g. value sets referred to from the import statement of the value set being expanded'
|
package/tx/workers/worker.js
CHANGED
|
@@ -8,6 +8,12 @@ const {Languages} = require("../../library/languages");
|
|
|
8
8
|
const {ConceptMap} = require("../library/conceptmap");
|
|
9
9
|
const {Renderer} = require("../library/renderer");
|
|
10
10
|
|
|
11
|
+
// The cache-id travels as an HTTP header (not an operation parameter) so proxies /
|
|
12
|
+
// load-balancers can act on it and the server can reject it before parsing the body.
|
|
13
|
+
// Defined here (the base worker) so both the worker and cache-control share one
|
|
14
|
+
// source of truth without a circular require. Express lower-cases header names.
|
|
15
|
+
const CACHE_ID_HEADER = 'x-cache-id';
|
|
16
|
+
|
|
11
17
|
/**
|
|
12
18
|
* Custom error for terminology setup issues
|
|
13
19
|
*/
|
|
@@ -121,7 +127,11 @@ class TerminologyWorker {
|
|
|
121
127
|
latest = i;
|
|
122
128
|
}
|
|
123
129
|
}
|
|
124
|
-
|
|
130
|
+
const found = matches[latest];
|
|
131
|
+
if (this.additionalResourcesCacheId && this.log) {
|
|
132
|
+
this.log.info(`cache-id '${this.additionalResourcesCacheId}': using cached ${found.resourceType} ${found.url}${found.version ? '|' + found.version : ''} for lookup of ${url}${version ? '|' + version : ''}`);
|
|
133
|
+
}
|
|
134
|
+
return found;
|
|
125
135
|
}
|
|
126
136
|
}
|
|
127
137
|
|
|
@@ -487,50 +497,75 @@ class TerminologyWorker {
|
|
|
487
497
|
* @returns {Object} Parameters resource
|
|
488
498
|
*/
|
|
489
499
|
buildParameters(req) {
|
|
500
|
+
let params;
|
|
501
|
+
|
|
490
502
|
// If POST with Parameters resource, use directly
|
|
491
503
|
if (req.method === 'POST' && req.body && req.body.resourceType === 'Parameters') {
|
|
492
|
-
|
|
493
|
-
}
|
|
494
|
-
if (req.method === 'POST' && req.body && req.body.resourceType) {
|
|
504
|
+
params = req.body;
|
|
505
|
+
} else if (req.method === 'POST' && req.body && req.body.resourceType) {
|
|
495
506
|
let langs = this.languages.parse(req.headers['accept-language']);
|
|
496
507
|
throw new Issue('error', 'invalid', null, 'Wrong_type_for_resource_expected', this.i18n.translate('Wrong_type_for_resource_expected', langs, ["Parameters", req.body.resourceType])).handleAsOO(400);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
};
|
|
508
|
+
} else {
|
|
509
|
+
// Convert query params or form body to Parameters
|
|
510
|
+
const source = req.method === 'POST' ? {...req.query, ...req.body} : req.query;
|
|
511
|
+
params = {
|
|
512
|
+
resourceType: 'Parameters',
|
|
513
|
+
parameter: []
|
|
514
|
+
};
|
|
505
515
|
|
|
506
|
-
|
|
507
|
-
|
|
516
|
+
for (const [name, value] of Object.entries(source)) {
|
|
517
|
+
if (value === undefined || value === null) continue;
|
|
508
518
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
519
|
+
if (Array.isArray(value)) {
|
|
520
|
+
// Repeating parameter
|
|
521
|
+
for (const v of value) {
|
|
522
|
+
params.parameter.push({name, valueString: String(v)});
|
|
523
|
+
}
|
|
524
|
+
} else if (typeof value === 'object') {
|
|
525
|
+
// Could be a resource or complex type - check resourceType
|
|
526
|
+
if (value.resourceType) {
|
|
527
|
+
params.parameter.push({name, resource: value});
|
|
528
|
+
} else {
|
|
529
|
+
// Assume it's a complex type like Coding or CodeableConcept
|
|
530
|
+
params.parameter.push(this.buildComplexParameter(name, value));
|
|
531
|
+
}
|
|
532
|
+
} else if (value == 'true') {
|
|
533
|
+
params.parameter.push({name, valueBoolean: true});
|
|
534
|
+
} else if (value == 'false') {
|
|
535
|
+
params.parameter.push({name, valueBoolean: false});
|
|
518
536
|
} else {
|
|
519
|
-
|
|
520
|
-
params.parameter.push(this.buildComplexParameter(name, value));
|
|
537
|
+
params.parameter.push({name, valueString: String(value)});
|
|
521
538
|
}
|
|
522
|
-
} else if (value == 'true') {
|
|
523
|
-
params.parameter.push({name, valueBoolean: true});
|
|
524
|
-
} else if (value == 'false') {
|
|
525
|
-
params.parameter.push({name, valueBoolean: false});
|
|
526
|
-
} else {
|
|
527
|
-
params.parameter.push({name, valueString: String(value)});
|
|
528
539
|
}
|
|
529
540
|
}
|
|
530
541
|
|
|
542
|
+
this.applyCacheIdHeader(req, params);
|
|
531
543
|
return params;
|
|
532
544
|
}
|
|
533
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Normalise the cache-id from the `${CACHE_ID_HEADER}` header into a `cache-id`
|
|
548
|
+
* parameter, so all downstream code can read the cache-id the same way whether
|
|
549
|
+
* the client sent it as a header (the going-forward mechanism) or, for now, still
|
|
550
|
+
* as a parameter. If a cache-id parameter is already present it is left as-is, so
|
|
551
|
+
* an explicit parameter wins and there's no surprising override.
|
|
552
|
+
* @param {express.Request} req
|
|
553
|
+
* @param {Object} params - Parameters resource (mutated in place)
|
|
554
|
+
*/
|
|
555
|
+
applyCacheIdHeader(req, params) {
|
|
556
|
+
const headerVal = req && req.headers ? req.headers[CACHE_ID_HEADER] : null;
|
|
557
|
+
if (!headerVal) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (!params.parameter) {
|
|
561
|
+
params.parameter = [];
|
|
562
|
+
}
|
|
563
|
+
if (params.parameter.some(p => p.name === 'cache-id')) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
params.parameter.push({ name: 'cache-id', valueId: String(headerVal) });
|
|
567
|
+
}
|
|
568
|
+
|
|
534
569
|
/**
|
|
535
570
|
* Build a parameter for complex types
|
|
536
571
|
*/
|
|
@@ -559,36 +594,87 @@ class TerminologyWorker {
|
|
|
559
594
|
// ========== Additional Resources Handling ==========
|
|
560
595
|
|
|
561
596
|
/**
|
|
562
|
-
*
|
|
597
|
+
* Collect the resources supplied inline in a Parameters resource: the
|
|
598
|
+
* `tx-resource` parameters, and the primary `valueSet`/`codeSystem` parameters.
|
|
599
|
+
*
|
|
600
|
+
* The primary resource is collected separately because the cache-id protocol
|
|
601
|
+
* needs it retained too: fhir-core sends the main ValueSet as `valueSet` (not
|
|
602
|
+
* `tx-resource`), so if it isn't cached, a later by-reference call can't resolve
|
|
603
|
+
* it ("value set ... could not be found"). A primary resource is only usable by
|
|
604
|
+
* reference if it has a url to key on, so url-less ones are skipped.
|
|
605
|
+
*
|
|
563
606
|
* @param {Object} params - Parameters resource
|
|
607
|
+
* @returns {{txResources: Array, primaryResources: Array}} wrapped resources
|
|
564
608
|
*/
|
|
565
|
-
|
|
566
|
-
if (!params || !params.parameter) return;
|
|
567
|
-
|
|
568
|
-
// Collect tx-resource parameters (resources provided inline)
|
|
609
|
+
collectSuppliedResources(params) {
|
|
569
610
|
const txResources = [];
|
|
611
|
+
const primaryResources = [];
|
|
612
|
+
if (!params || !params.parameter) {
|
|
613
|
+
return { txResources, primaryResources };
|
|
614
|
+
}
|
|
570
615
|
for (const param of params.parameter) {
|
|
571
|
-
this.deadCheck('
|
|
616
|
+
this.deadCheck('collectSuppliedResources');
|
|
572
617
|
if (param.name === 'tx-resource' && param.resource) {
|
|
573
|
-
|
|
618
|
+
const res = this.wrapRawResource(param.resource);
|
|
574
619
|
if (res) {
|
|
575
620
|
txResources.push(res);
|
|
576
621
|
}
|
|
622
|
+
} else if ((param.name === 'valueSet' || param.name === 'codeSystem') && param.resource && param.resource.url) {
|
|
623
|
+
const res = this.wrapRawResource(param.resource);
|
|
624
|
+
if (res) {
|
|
625
|
+
primaryResources.push(res);
|
|
626
|
+
}
|
|
577
627
|
}
|
|
578
628
|
}
|
|
629
|
+
return { txResources, primaryResources };
|
|
630
|
+
}
|
|
579
631
|
|
|
580
|
-
|
|
632
|
+
/**
|
|
633
|
+
* Set up additional resources from tx-resource parameters and cache
|
|
634
|
+
* @param {Object} params - Parameters resource
|
|
635
|
+
*/
|
|
636
|
+
setupAdditionalResources(params) {
|
|
637
|
+
if (!params || !params.parameter) return;
|
|
638
|
+
|
|
639
|
+
// Collect the resources supplied inline on this request (tx-resource plus the
|
|
640
|
+
// primary valueSet/codeSystem). See collectSuppliedResources for why the
|
|
641
|
+
// primary resource is included.
|
|
642
|
+
const { txResources, primaryResources } = this.collectSuppliedResources(params);
|
|
643
|
+
|
|
644
|
+
// Check for cache-id. An explicit cache-id *parameter* wins; otherwise fall
|
|
645
|
+
// back to the cache-id the middleware lifted off the X-Cache-Id header onto
|
|
646
|
+
// the operation context. This fallback is what makes the header work on the
|
|
647
|
+
// op paths that don't route their Parameters through buildParameters
|
|
648
|
+
// (expand, related, batch-validate) or that hand setupAdditionalResources a
|
|
649
|
+
// raw req.body (lookup) - previously those silently ignored a front-loaded
|
|
650
|
+
// cache and failed to resolve by-reference resources.
|
|
581
651
|
const cacheIdParam = this.findParameter(params, 'cache-id');
|
|
582
|
-
const cacheId = cacheIdParam ? this.getParameterValue(cacheIdParam) : null
|
|
652
|
+
const cacheId = (cacheIdParam ? this.getParameterValue(cacheIdParam) : null)
|
|
653
|
+
|| (this.opContext ? this.opContext.cacheId : null)
|
|
654
|
+
|| null;
|
|
583
655
|
|
|
584
656
|
if (cacheId && this.opContext.resourceCache) {
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
|
|
657
|
+
// The cache must already exist: caches are created explicitly via
|
|
658
|
+
// $cache-control?mode=start, which is the only thing that mints a cache-id.
|
|
659
|
+
// A cache-id the server doesn't know is an unambiguous, server-authoritative
|
|
660
|
+
// error condition (never created, or expired / released) - report it with a
|
|
661
|
+
// specific coded issue rather than silently auto-creating a fresh cache and
|
|
662
|
+
// then failing obscurely later when a by-reference resource can't be found.
|
|
663
|
+
if (!this.opContext.resourceCache.has(cacheId)) {
|
|
664
|
+
throw new Issue('error', 'not-found', null, 'CACHE_ID_UNKNOWN',
|
|
665
|
+
this.i18n.translate('CACHE_ID_UNKNOWN', this.opContext.langs, [cacheId]),
|
|
666
|
+
'cache-id-unknown', 404);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// The cache exists: merge any resources supplied on this request into it
|
|
670
|
+
// (incremental population is allowed), then expose the full cache contents.
|
|
671
|
+
const toCache = txResources.concat(primaryResources);
|
|
672
|
+
if (toCache.length > 0) {
|
|
673
|
+
this.opContext.resourceCache.add(cacheId, toCache);
|
|
588
674
|
}
|
|
589
675
|
|
|
590
|
-
// Set additional resources to all resources for this cache-id
|
|
591
676
|
this.additionalResources = this.opContext.resourceCache.get(cacheId);
|
|
677
|
+
this.additionalResourcesCacheId = cacheId;
|
|
592
678
|
} else {
|
|
593
679
|
// No cache-id, just use the tx-resources directly
|
|
594
680
|
this.additionalResources = txResources;
|
|
@@ -708,7 +794,7 @@ class TerminologyWorker {
|
|
|
708
794
|
// Check for various value types
|
|
709
795
|
const valueTypes = [
|
|
710
796
|
'valueString', 'valueCode', 'valueUri', 'valueCanonical', 'valueUrl',
|
|
711
|
-
'valueBoolean', 'valueInteger', 'valueDecimal',
|
|
797
|
+
'valueBoolean', 'valueInteger', 'valueDecimal', 'valueId',
|
|
712
798
|
'valueDateTime', 'valueDate', 'valueTime',
|
|
713
799
|
'valueCoding', 'valueCodeableConcept',
|
|
714
800
|
'valueIdentifier', 'valueQuantity'
|
|
@@ -934,5 +1020,6 @@ class TerminologyWorker {
|
|
|
934
1020
|
|
|
935
1021
|
module.exports = {
|
|
936
1022
|
TerminologyWorker,
|
|
937
|
-
TerminologySetupError
|
|
1023
|
+
TerminologySetupError,
|
|
1024
|
+
CACHE_ID_HEADER
|
|
938
1025
|
};
|