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.
@@ -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
  };
@@ -1,133 +0,0 @@
1
- {
2
- "resourceType": "OperationDefinition",
3
- "id": "ValueSet-related",
4
- "url": "http://hl7.org/fhir/OperationDefinition/ValueSet-related",
5
- "version": "5.0.0",
6
- "name": "ValueSetRelated",
7
- "title": "Value Set Related Determination",
8
- "status": "active",
9
- "kind": "operation",
10
- "experimental": false,
11
- "date": "2023-03-26T15:21:02+11:00",
12
- "publisher": "FHIRsmith",
13
- "description": "Determine the relationship between two value sets. Different versions of code systems are considered compatible unless versionNeeded = true for the code system",
14
- "jurisdiction": [
15
- {
16
- "coding": [
17
- {
18
- "system": "http://unstats.un.org/unsd/methods/m49/m49.htm",
19
- "code": "001",
20
- "display": "World"
21
- }
22
- ]
23
- }
24
- ],
25
- "affectsState": false,
26
- "code": "related",
27
- "comment": "An $expand will be performed internally if needed.",
28
- "resource": [
29
- "ValueSet"
30
- ],
31
- "system": false,
32
- "type": true,
33
- "instance": false,
34
- "parameter": [
35
- {
36
- "name": "thisUrl",
37
- "use": "in",
38
- "scope": [
39
- "type"
40
- ],
41
- "min": 0,
42
- "max": "1",
43
- "documentation": "Value set Canonical URL for the first value set of the pair (the base). The server must know the value set (e.g. it is provided as an attached resource, it is defined explicitly in the server's value sets, or it is defined implicitly by some code system known to the server",
44
- "type": "uri"
45
- },
46
- {
47
- "name": "otherUrl",
48
- "use": "in",
49
- "scope": [
50
- "type"
51
- ],
52
- "min": 0,
53
- "max": "1",
54
- "documentation": "Value set Canonical URL for the second value set of the pair (the one being compared to base). The server must know the value set (e.g. it is provided as an attached resource, it is defined explicitly in the server's value sets, or it is defined implicitly by some code system known to the server",
55
- "type": "uri"
56
- },
57
- {
58
- "name": "thisValueSet",
59
- "use": "in",
60
- "scope": [
61
- "type"
62
- ],
63
- "min": 0,
64
- "max": "1",
65
- "documentation": "The first value set is provided directly as part of the request. Servers may choose not to accept value sets in this fashion. This parameter is used when the client wants the server to expand a value set that is not stored on the server",
66
- "type": "ValueSet"
67
- },
68
- {
69
- "name": "otherValueSet",
70
- "use": "in",
71
- "scope": [
72
- "type"
73
- ],
74
- "min": 0,
75
- "max": "1",
76
- "documentation": "The other value set is provided directly as part of the request. Servers may choose not to accept value sets in this fashion. This parameter is used when the client wants the server to expand a value set that is not stored on the server",
77
- "type": "ValueSet"
78
- },
79
- {
80
- "name": "thisVersion",
81
- "use": "in",
82
- "scope": [
83
- "type"
84
- ],
85
- "min": 0,
86
- "max": "1",
87
- "documentation": "The identifier that is used to identify a specific version of the value set to be used when validating the code. This is an arbitrary value managed by the value set author and is not expected to be globally unique. For example, it might be a timestamp (e.g. yyyymmdd) if a managed version is not available.",
88
- "type": "string"
89
- },
90
- {
91
- "name": "otherVersion",
92
- "use": "in",
93
- "scope": [
94
- "type"
95
- ],
96
- "min": 0,
97
- "max": "1",
98
- "documentation": "The identifier that is used to identify a specific version of the value set to be used when validating the code. This is an arbitrary value managed by the value set author and is not expected to be globally unique. For example, it might be a timestamp (e.g. yyyymmdd) if a managed version is not available.",
99
- "type": "string"
100
- },
101
- {
102
- "name": "useSupplement",
103
- "use": "in",
104
- "min": 0,
105
- "max": "*",
106
- "documentation": "The supplement must be used when validating the code. Use of this parameter should result in $validate-code behaving the same way as if the supplements were included in the value set definition using the [http://hl7.org/fhir/StructureDefinition/valueset-supplement](http://hl7.org/fhir/extensions/StructureDefinition-valueset-supplement.html)",
107
- "type": "canonical"
108
- },
109
- {
110
- "name": "diagnostics",
111
- "use": "in",
112
- "min": 0,
113
- "max": "1",
114
- "documentation": "Whether to return information about the reasoning process"
115
- },
116
- {
117
- "name": "result",
118
- "use": "out",
119
- "min": 1,
120
- "max": "1",
121
- "documentation": "The relationship between the ValueSets. One of: same, superset, subset, overlapping, dsjoint, empty, and indeterminate",
122
- "type": "boolean"
123
- },
124
- {
125
- "name": "message",
126
- "use": "out",
127
- "min": 0,
128
- "max": "1",
129
- "documentation": "Explanation of the code, with reason if appropriate",
130
- "type": "string"
131
- }
132
- ]
133
- }