fhirsmith 0.9.7 → 0.10.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/tx/tx-html.js CHANGED
@@ -471,6 +471,33 @@ class TxHtmlRenderer {
471
471
  if (param.valueCode !== undefined) {
472
472
  return `<code>${escape(param.valueCode)}</code>`;
473
473
  }
474
+ if (param.valueId !== undefined) {
475
+ return escape(String(param.valueId));
476
+ }
477
+ if (param.valueOid !== undefined) {
478
+ return escape(String(param.valueOid));
479
+ }
480
+ if (param.valueUuid !== undefined) {
481
+ return escape(String(param.valueUuid));
482
+ }
483
+ if (param.valueMarkdown !== undefined) {
484
+ // Render markdown to HTML the same way the rest of the server does, using
485
+ // commonmark in safe mode (raw HTML in the markdown is escaped, so this is
486
+ // XSS-safe).
487
+ const commonmark = require('commonmark');
488
+ const reader = new commonmark.Parser();
489
+ const writer = new commonmark.HtmlRenderer({ safe: true });
490
+ return writer.render(reader.parse(String(param.valueMarkdown)));
491
+ }
492
+ if (param.valueInteger64 !== undefined) {
493
+ return escape(String(param.valueInteger64));
494
+ }
495
+ if (param.valuePositiveInt !== undefined) {
496
+ return escape(String(param.valuePositiveInt));
497
+ }
498
+ if (param.valueUnsignedInt !== undefined) {
499
+ return escape(String(param.valueUnsignedInt));
500
+ }
474
501
  if (param.valueDate !== undefined) {
475
502
  return escape(param.valueDate);
476
503
  }
package/tx/tx.js CHANGED
@@ -27,6 +27,7 @@ const LookupWorker = require('./workers/lookup');
27
27
  const SubsumesWorker = require('./workers/subsumes');
28
28
  const { MetadataHandler } = require('./workers/metadata');
29
29
  const { BatchValidateWorker } = require('./workers/batch-validate');
30
+ const { CacheControlWorker } = require('./workers/cache-control');
30
31
  const {CapabilityStatementXML} = require("./xml/capabilitystatement-xml");
31
32
  const {TerminologyCapabilitiesXML} = require("./xml/terminologycapabilities-xml");
32
33
  const {ParametersXML} = require("./xml/parameters-xml");
@@ -36,7 +37,7 @@ const {ConceptMapXML} = require("./xml/conceptmap-xml");
36
37
  const {TxHtmlRenderer} = require("./tx-html");
37
38
  const {Renderer} = require("./library/renderer");
38
39
  const {OperationsWorker} = require("./workers/operations");
39
- const {RelatedWorker} = require("./workers/related");
40
+ const {CompareWorker} = require("./workers/compare");
40
41
  const {codeSystemFromR5} = require("./xversion/xv-codesystem");
41
42
  const {operationOutcomeFromR5} = require("./xversion/xv-operationoutcome");
42
43
  const {parametersFromR5} = require("./xversion/xv-parameters");
@@ -296,6 +297,12 @@ class TXModule {
296
297
  endpointInfo.resourceCache, endpointInfo.expansionCache
297
298
  );
298
299
  opContext.usageTracker = this.usageTracker;
300
+ // Normalise the cache-id from the X-Cache-Id header onto the operation
301
+ // context once, here, so every worker honours a front-loaded cache no
302
+ // matter how it later assembles its Parameters (some use buildParameters,
303
+ // others read req.body / query directly). An explicit cache-id parameter
304
+ // on the request still wins (see setupAdditionalResources).
305
+ opContext.cacheId = req.get('X-Cache-Id') || null;
299
306
 
300
307
  // Attach everything to request
301
308
  req.txProvider = endpointInfo.provider;
@@ -595,23 +602,23 @@ class TXModule {
595
602
  }
596
603
  });
597
604
 
598
- // ValueSet/$related(GET and POST)
599
- router.get('/ValueSet/\\$related', async (req, res) => {
605
+ // ValueSet/$compare(GET and POST)
606
+ router.get('/ValueSet/\\$compare', async (req, res) => {
600
607
  const start = Date.now();
601
608
  try {
602
- let worker = new RelatedWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
609
+ let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
603
610
  await worker.handle(req, res);
604
611
  } finally {
605
- this.countRequest('$related', Date.now() - start);
612
+ this.countRequest('$compare', Date.now() - start);
606
613
  }
607
614
  });
608
- router.post('/ValueSet/\\$related', async (req, res) => {
615
+ router.post('/ValueSet/\\$compare', async (req, res) => {
609
616
  const start = Date.now();
610
617
  try {
611
- let worker = new RelatedWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
618
+ let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
612
619
  await worker.handle(req, res);
613
620
  } finally {
614
- this.countRequest('$related', Date.now() - start);
621
+ this.countRequest('$compare', Date.now() - start);
615
622
  }
616
623
  });
617
624
 
@@ -655,6 +662,26 @@ class TXModule {
655
662
  }
656
663
  });
657
664
 
665
+ // $cache-control (GET and POST) - base-level operation: mode=start|end
666
+ router.get('/\\$cache-control', async (req, res) => {
667
+ const start = Date.now();
668
+ try {
669
+ let worker = new CacheControlWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
670
+ await worker.handle(req, res, this.log);
671
+ } finally {
672
+ this.countRequest('$cache-control', Date.now() - start);
673
+ }
674
+ });
675
+ router.post('/\\$cache-control', async (req, res) => {
676
+ const start = Date.now();
677
+ try {
678
+ let worker = new CacheControlWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
679
+ await worker.handle(req, res, this.log);
680
+ } finally {
681
+ this.countRequest('$cache-control', Date.now() - start);
682
+ }
683
+ });
684
+
658
685
  // ConceptMap/$translate (GET and POST)
659
686
  router.get('/ConceptMap/\\$translate', async (req, res) => {
660
687
  const start = Date.now();
@@ -779,23 +806,23 @@ class TXModule {
779
806
  });
780
807
 
781
808
 
782
- // ValueSet/[id]/$related
783
- router.get('/ValueSet/:id/\\$related', async (req, res) => {
809
+ // ValueSet/[id]/$compare
810
+ router.get('/ValueSet/:id/\\$compare', async (req, res) => {
784
811
  const start = Date.now();
785
812
  try {
786
- let worker = new RelatedWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
813
+ let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
787
814
  await worker.handleInstance(req, res, this.log);
788
815
  } finally {
789
- this.countRequest('$related', Date.now() - start);
816
+ this.countRequest('$compare', Date.now() - start);
790
817
  }
791
818
  });
792
- router.post('/ValueSet/:id/\\$related', async (req, res) => {
819
+ router.post('/ValueSet/:id/\\$compare', async (req, res) => {
793
820
  const start = Date.now();
794
821
  try {
795
- let worker = new RelatedWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
822
+ let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
796
823
  await worker.handleInstance(req, res, this.log);
797
824
  } finally {
798
- this.countRequest('$related', Date.now() - start);
825
+ this.countRequest('$compare', Date.now() - start);
799
826
  }
800
827
  });
801
828
 
@@ -1177,10 +1204,46 @@ class TXModule {
1177
1204
  }
1178
1205
  }
1179
1206
 
1180
- cacheCount() {
1207
+ // Number of entries in the expansion cache, summed across endpoints.
1208
+ expansionItemCount() {
1209
+ let count = 0;
1210
+ for (let ep of this.endpoints) {
1211
+ count = count + ep.expansionCache.size();
1212
+ }
1213
+ return count;
1214
+ }
1215
+
1216
+ // Number of concepts held in the client (resource) cache, summed across endpoints.
1217
+ clientConceptCount() {
1218
+ let count = 0;
1219
+ for (let ep of this.endpoints) {
1220
+ count = count + ep.resourceCache.conceptCount();
1221
+ }
1222
+ return count;
1223
+ }
1224
+
1225
+ // Number of caches (cache-ids) currently held in the client cache, across endpoints.
1226
+ clientCacheCount() {
1227
+ let count = 0;
1228
+ for (let ep of this.endpoints) {
1229
+ count = count + ep.resourceCache.size();
1230
+ }
1231
+ return count;
1232
+ }
1233
+
1234
+ // High-water marks for the client cache, summed across endpoints.
1235
+ maxClientCacheCount() {
1236
+ let count = 0;
1237
+ for (let ep of this.endpoints) {
1238
+ count = count + ep.resourceCache.maxSize();
1239
+ }
1240
+ return count;
1241
+ }
1242
+
1243
+ maxClientConceptCount() {
1181
1244
  let count = 0;
1182
1245
  for (let ep of this.endpoints) {
1183
- count = count + ep.resourceCache.size() + ep.expansionCache.size();
1246
+ count = count + ep.resourceCache.maxConceptCount();
1184
1247
  }
1185
1248
  return count;
1186
1249
  }
@@ -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'