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.
@@ -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
- // Related Worker - Handles ValueSet $related operation
2
+ // Compare Worker - Handles ValueSet $compare operation
3
3
  //
4
- // GET /ValueSet/{id}/$related
5
- // GET /ValueSet/$related?url=...&version=...
6
- // POST /ValueSet/$related (form body or Parameters with url)
7
- // POST /ValueSet/$related (body is ValueSet resource)
8
- // POST /ValueSet/$related (body is Parameters with valueSet parameter)
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 RelatedWorker extends TerminologyWorker {
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 'related';
40
+ return 'compare';
41
41
  }
42
42
 
43
43
  /**
44
- * Handle a type-level $related request
45
- * GET/POST /ValueSet/$related
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.handleTypeLevelRelated(req, res);
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 $related request
80
- * GET/POST /ValueSet/{id}/$related
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.handleInstanceLevelRelated(req, res);
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 $related: /ValueSet/$related
108
+ * Handle type-level $compare: /ValueSet/$compare
109
109
  * ValueSet identified by url, or provided directly in body
110
110
  */
111
- async handleTypeLevelRelated(req, res) {
112
- this.deadCheck('related-type-level');
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.doRelated(txp, thisVS, otherVS);
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 related: /ValueSet/{id}/$related
129
+ * Handle instance-level compare: /ValueSet/{id}/$compare
130
130
  * ValueSet identified by resource ID
131
131
  */
132
- async handleInstanceLevelRelated(req, res) {
133
- this.deadCheck('related-instance-level');
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.doRelated(txp, thisVS, otherVS);
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 doRelated(txp, thisVS, otherVS) {
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, 'RelatedWorker.doRelated', 'compose', thisVS.vurl)
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, 'RelatedWorker.doRelated', 'compose', otherVS.vurl)
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 seb-set of the valueSet ${otherVS.vurl}`);
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
- RelatedWorker
715
+ CompareWorker
716
716
  };
@@ -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: 'related', definition: 'https://raw.githubusercontent.com/HealthIntersections/FHIRsmith/refs/heads/main/tx/data/OperationDefinition-ValueSet-related.json' }
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: 'related', definition: 'https://raw.githubusercontent.com/HealthIntersections/FHIRsmith/refs/heads/main/tx/data/OperationDefinition-ValueSet-related.json' },
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
- name: 'cache-id',
400
- documentation: 'This server supports caching terminology resources between calls. Clients only need to send value sets and codesystems once; thereafter they are automatically in scope for calls with the same cache-id. The cache is retained for 30 min from last call'
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'
@@ -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
- return matches[latest];
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
- return req.body;
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
- // Convert query params or form body to Parameters
500
- const source = req.method === 'POST' ? {...req.query, ...req.body} : req.query;
501
- const params = {
502
- resourceType: 'Parameters',
503
- parameter: []
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
- for (const [name, value] of Object.entries(source)) {
507
- if (value === undefined || value === null) continue;
516
+ for (const [name, value] of Object.entries(source)) {
517
+ if (value === undefined || value === null) continue;
508
518
 
509
- if (Array.isArray(value)) {
510
- // Repeating parameter
511
- for (const v of value) {
512
- params.parameter.push({name, valueString: String(v)});
513
- }
514
- } else if (typeof value === 'object') {
515
- // Could be a resource or complex type - check resourceType
516
- if (value.resourceType) {
517
- params.parameter.push({name, resource: value});
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
- // Assume it's a complex type like Coding or CodeableConcept
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
- * Set up additional resources from tx-resource parameters and cache
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
- setupAdditionalResources(params) {
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('setupAdditionalResources');
616
+ this.deadCheck('collectSuppliedResources');
572
617
  if (param.name === 'tx-resource' && param.resource) {
573
- let res = this.wrapRawResource(param.resource);
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
- // Check for cache-id
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
- // Merge tx-resources with cached resources
586
- if (txResources.length > 0) {
587
- this.opContext.resourceCache.add(cacheId, txResources);
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
  };