fhirsmith 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +305 -0
- package/README.md +1 -1
- package/config-template.json +13 -0
- package/library/languages.js +191 -34
- package/library/path-safety.js +51 -0
- package/library/request-token.js +56 -0
- package/package.json +2 -2
- package/packages/packages.js +17 -118
- package/publisher/publisher.js +60 -30
- package/publisher/validation.js +15 -3
- package/server.js +12 -3
- package/translations/Messages.properties +1 -0
- package/tx/README.md +21 -0
- package/tx/cs/cs-api.js +30 -1
- package/tx/cs/cs-cs.js +1 -1
- package/tx/cs/cs-icd11.js +805 -0
- package/tx/cs/cs-lang.js +331 -85
- package/tx/cs/cs-loinc.js +45 -4
- package/tx/cs/cs-mimetypes.js +492 -21
- package/tx/cs/cs-snomed.js +38 -4
- package/tx/cs/cs-ucum.js +33 -3
- package/tx/cs/icd11-expressions.js +243 -0
- package/tx/importers/export-icd11-codesystem.js +823 -0
- package/tx/importers/icd11-schema.md +285 -0
- package/tx/importers/import-icd11.js +861 -0
- package/tx/importers/import-loinc.module.js +14 -7
- package/tx/importers/import-sct.module.js +10 -1
- package/tx/library/codesystem.js +21 -27
- package/tx/library/nesting-limit.js +79 -0
- package/tx/library/operation-outcome.js +97 -3
- package/tx/library/valueset.js +16 -20
- package/tx/library.js +23 -1
- package/tx/ocl/cache/cache-utils.cjs +5 -3
- package/tx/ocl/cs-ocl.cjs +73 -17
- package/tx/ocl/jobs/background-queue.cjs +6 -3
- package/tx/ocl/mappers/concept-mapper.cjs +9 -1
- package/tx/ocl/shared/patches.cjs +48 -1
- package/tx/ocl/vs-ocl.cjs +15 -13
- package/tx/sct/expressions.js +194 -17
- package/tx/tx.fhir.org.yml +2 -1
- package/tx/tx.js +15 -42
- package/tx/workers/batch-validate.js +6 -16
- package/tx/workers/batch.js +16 -28
- package/tx/workers/cache-control.js +6 -12
- package/tx/workers/closure.js +3 -8
- package/tx/workers/compare.js +6 -34
- package/tx/workers/expand.js +36 -45
- package/tx/workers/lookup.js +21 -39
- package/tx/workers/read.js +6 -40
- package/tx/workers/search.js +2 -8
- package/tx/workers/subsumes.js +58 -41
- package/tx/workers/translate.js +11 -17
- package/tx/workers/validate.js +76 -77
- package/tx/workers/worker.js +70 -1
- package/tx/xml/xml-base.js +243 -76
- package/tx/xversion/xv-valueset.js +39 -1
package/tx/xml/xml-base.js
CHANGED
|
@@ -652,122 +652,289 @@ class FhirXmlBase {
|
|
|
652
652
|
* Simple XML parser for FHIR resources
|
|
653
653
|
* Parses XML string into {name, attributes, children} structure
|
|
654
654
|
*/
|
|
655
|
+
/**
|
|
656
|
+
* A deliberately small XML reader for FHIR resources. It is not a general XML processor: FHIR
|
|
657
|
+
* XML has no DTDs, no entities beyond the five predefined ones, and carries its values in
|
|
658
|
+
* attributes, so the grammar it needs is tiny.
|
|
659
|
+
*
|
|
660
|
+
* What it does have to be is total. Every input either produces a tree or throws - it must never
|
|
661
|
+
* loop, and it must never run away with memory or stack. This matters more than it looks: the
|
|
662
|
+
* server accepts application/fhir+xml on every endpoint, parsing is synchronous, and node is
|
|
663
|
+
* single threaded, so a parser that fails to terminate does not hang one request, it stops the
|
|
664
|
+
* process answering anything at all. The cooperative deadCheck cannot help - it needs an await
|
|
665
|
+
* point, and there isn't one inside a parse.
|
|
666
|
+
*
|
|
667
|
+
* Two rules keep that promise, and both are enforced rather than assumed:
|
|
668
|
+
*
|
|
669
|
+
* - no scan may fail silently. Every search for a delimiter goes through #seek(), which throws
|
|
670
|
+
* if the delimiter is absent. The bug this replaces was an indexOf() returning -1 whose result
|
|
671
|
+
* was assigned to the cursor, sending it back to zero and restarting the parse forever.
|
|
672
|
+
* - every loop must consume input. Each iteration asserts the cursor moved forward, so a future
|
|
673
|
+
* edit that forgets to advance it fails loudly on the first malformed document instead of
|
|
674
|
+
* spinning.
|
|
675
|
+
*/
|
|
676
|
+
|
|
677
|
+
// FHIR resources nest a couple of dozen deep; narrative markup can go further. This is well
|
|
678
|
+
// clear of anything real and well under the call stack, so deep input is a clean error rather
|
|
679
|
+
// than a RangeError from the recursion in _parseElement.
|
|
680
|
+
const MAX_ELEMENT_DEPTH = 1000;
|
|
681
|
+
|
|
655
682
|
class FhirXmlParser {
|
|
683
|
+
/**
|
|
684
|
+
* @param {string} xml - the document to read
|
|
685
|
+
*/
|
|
656
686
|
constructor(xml) {
|
|
687
|
+
if (typeof xml !== 'string') {
|
|
688
|
+
throw new Error('XML input must be a string');
|
|
689
|
+
}
|
|
657
690
|
this.xml = xml;
|
|
658
691
|
this.pos = 0;
|
|
692
|
+
this.depth = 0;
|
|
659
693
|
}
|
|
660
694
|
|
|
695
|
+
/**
|
|
696
|
+
* @returns {{name: string, attributes: Object, children: Array}} the root element
|
|
697
|
+
*/
|
|
661
698
|
parse() {
|
|
662
|
-
this
|
|
663
|
-
this
|
|
664
|
-
return
|
|
699
|
+
this.#parseProlog();
|
|
700
|
+
const root = this.#parseElement();
|
|
701
|
+
return root;
|
|
665
702
|
}
|
|
666
703
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
704
|
+
// ========== scanning primitives ==========
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Throw with the position, which is what makes a malformed document diagnosable
|
|
708
|
+
* @param {string} message
|
|
709
|
+
*/
|
|
710
|
+
#fail(message) {
|
|
711
|
+
throw new Error(`${message} at position ${this.pos}`);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* indexOf that cannot return -1. Every delimiter search goes through here
|
|
716
|
+
* @param {string} needle
|
|
717
|
+
* @param {string} what - what we were looking for, for the error message
|
|
718
|
+
* @returns {number}
|
|
719
|
+
*/
|
|
720
|
+
#seek(needle, what) {
|
|
721
|
+
const at = this.xml.indexOf(needle, this.pos);
|
|
722
|
+
if (at === -1) {
|
|
723
|
+
this.#fail(`Unterminated ${what}: no '${needle}' found`);
|
|
674
724
|
}
|
|
725
|
+
return at;
|
|
675
726
|
}
|
|
676
727
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
728
|
+
/**
|
|
729
|
+
* Move the cursor, checking it actually moved forward. Called at the end of every loop body
|
|
730
|
+
* @param {number} was - the cursor before the iteration
|
|
731
|
+
*/
|
|
732
|
+
#mustHaveAdvanced(was) {
|
|
733
|
+
if (this.pos <= was) {
|
|
734
|
+
// not reachable by any input we know of - it means a code path forgot to consume
|
|
735
|
+
this.#fail('Internal error: the XML parser stopped making progress');
|
|
680
736
|
}
|
|
681
737
|
}
|
|
682
738
|
|
|
683
|
-
|
|
684
|
-
this.
|
|
739
|
+
#atEnd() {
|
|
740
|
+
return this.pos >= this.xml.length;
|
|
741
|
+
}
|
|
685
742
|
|
|
686
|
-
|
|
687
|
-
|
|
743
|
+
#skipWhitespace() {
|
|
744
|
+
while (this.pos < this.xml.length && /\s/.test(this.xml[this.pos])) {
|
|
745
|
+
this.pos++;
|
|
688
746
|
}
|
|
689
|
-
|
|
747
|
+
}
|
|
690
748
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
this.pos += nameEnd;
|
|
749
|
+
#startsWith(text) {
|
|
750
|
+
return this.xml.startsWith(text, this.pos);
|
|
751
|
+
}
|
|
695
752
|
|
|
696
|
-
|
|
697
|
-
const attributes = {};
|
|
698
|
-
this._skipWhitespace();
|
|
753
|
+
// ========== document structure ==========
|
|
699
754
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
755
|
+
/**
|
|
756
|
+
* The XML declaration, and any comments or processing instructions before the root element.
|
|
757
|
+
* A DOCTYPE is refused outright: FHIR XML has no DTD, and accepting one is how a parser ends
|
|
758
|
+
* up doing entity expansion on someone else's behalf.
|
|
759
|
+
*/
|
|
760
|
+
#parseProlog() {
|
|
761
|
+
this.#skipWhitespace();
|
|
762
|
+
if (this.#startsWith('<?xml')) {
|
|
763
|
+
this.pos = this.#seek('?>', 'XML declaration') + 2;
|
|
764
|
+
}
|
|
765
|
+
for (;;) {
|
|
766
|
+
const was = this.pos;
|
|
767
|
+
this.#skipWhitespace();
|
|
768
|
+
if (this.#startsWith('<!--')) {
|
|
769
|
+
this.#skipComment();
|
|
770
|
+
} else if (this.#startsWith('<!DOCTYPE') || this.#startsWith('<!')) {
|
|
771
|
+
this.#fail('Document type declarations are not allowed in FHIR XML');
|
|
772
|
+
} else if (this.#startsWith('<?')) {
|
|
773
|
+
this.pos = this.#seek('?>', 'processing instruction') + 2;
|
|
774
|
+
} else {
|
|
775
|
+
return;
|
|
704
776
|
}
|
|
705
|
-
this
|
|
777
|
+
this.#mustHaveAdvanced(was);
|
|
706
778
|
}
|
|
779
|
+
}
|
|
707
780
|
|
|
708
|
-
|
|
781
|
+
#skipComment() {
|
|
782
|
+
this.pos = this.#seek('-->', 'comment') + 3;
|
|
783
|
+
}
|
|
709
784
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
785
|
+
/**
|
|
786
|
+
* One element and everything inside it.
|
|
787
|
+
* @returns {{name: string, attributes: Object, children: Array}}
|
|
788
|
+
*/
|
|
789
|
+
#parseElement() {
|
|
790
|
+
if (++this.depth > MAX_ELEMENT_DEPTH) {
|
|
791
|
+
this.depth--;
|
|
792
|
+
this.#fail(`XML is nested more than ${MAX_ELEMENT_DEPTH} elements deep`);
|
|
714
793
|
}
|
|
794
|
+
try {
|
|
795
|
+
return this.#parseElementInner();
|
|
796
|
+
} finally {
|
|
797
|
+
this.depth--;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
715
800
|
|
|
716
|
-
|
|
801
|
+
#parseElementInner() {
|
|
802
|
+
this.#skipWhitespace();
|
|
803
|
+
if (this.#atEnd() || this.xml[this.pos] !== '<') {
|
|
804
|
+
this.#fail("Expected '<'");
|
|
805
|
+
}
|
|
806
|
+
this.pos++;
|
|
717
807
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
this._skipWhitespace();
|
|
808
|
+
const name = this.#parseName('element name');
|
|
809
|
+
const attributes = this.#parseAttributes(name);
|
|
721
810
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
811
|
+
if (this.#startsWith('/>')) {
|
|
812
|
+
this.pos += 2;
|
|
813
|
+
return { name, attributes, children: [] };
|
|
814
|
+
}
|
|
815
|
+
if (this.#atEnd() || this.xml[this.pos] !== '>') {
|
|
816
|
+
this.#fail(`Expected '>' or '/>' to close the start tag of <${name}>`);
|
|
817
|
+
}
|
|
818
|
+
this.pos++;
|
|
728
819
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
if (this.xml.substring(this.pos, this.pos + 4) === '<!--') {
|
|
732
|
-
const commentEnd = this.xml.indexOf('-->', this.pos);
|
|
733
|
-
this.pos = commentEnd + 3;
|
|
734
|
-
continue;
|
|
735
|
-
}
|
|
820
|
+
return { name, attributes, children: this.#parseChildren(name) };
|
|
821
|
+
}
|
|
736
822
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
823
|
+
/**
|
|
824
|
+
* An element or attribute name: everything up to whitespace, '/', '>' or '='.
|
|
825
|
+
* @param {string} what - for the error message
|
|
826
|
+
* @returns {string}
|
|
827
|
+
*/
|
|
828
|
+
#parseName(what) {
|
|
829
|
+
const from = this.pos;
|
|
830
|
+
while (this.pos < this.xml.length && !/[\s/>=]/.test(this.xml[this.pos])) {
|
|
831
|
+
this.pos++;
|
|
743
832
|
}
|
|
744
|
-
|
|
745
|
-
|
|
833
|
+
if (this.pos === from) {
|
|
834
|
+
this.#fail(`Expected an ${what}`);
|
|
835
|
+
}
|
|
836
|
+
return this.xml.substring(from, this.pos);
|
|
746
837
|
}
|
|
747
838
|
|
|
748
|
-
|
|
749
|
-
|
|
839
|
+
/**
|
|
840
|
+
* @param {string} elementName - for error messages
|
|
841
|
+
* @returns {Object} name to value
|
|
842
|
+
*/
|
|
843
|
+
#parseAttributes(elementName) {
|
|
844
|
+
const attributes = {};
|
|
845
|
+
for (;;) {
|
|
846
|
+
const was = this.pos;
|
|
847
|
+
this.#skipWhitespace();
|
|
848
|
+
if (this.#atEnd()) {
|
|
849
|
+
this.#fail(`Unterminated start tag <${elementName}>`);
|
|
850
|
+
}
|
|
851
|
+
if (this.xml[this.pos] === '>' || this.#startsWith('/>')) {
|
|
852
|
+
return attributes;
|
|
853
|
+
}
|
|
854
|
+
if (this.xml[this.pos] === '/') {
|
|
855
|
+
this.#fail(`Expected '/>' in the start tag of <${elementName}>`);
|
|
856
|
+
}
|
|
750
857
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
858
|
+
const name = this.#parseName('attribute name');
|
|
859
|
+
this.#skipWhitespace();
|
|
860
|
+
if (this.#atEnd() || this.xml[this.pos] !== '=') {
|
|
861
|
+
this.#fail(`Expected '=' after the attribute '${name}' of <${elementName}>`);
|
|
862
|
+
}
|
|
863
|
+
this.pos++;
|
|
864
|
+
attributes[name] = this.#parseAttributeValue(name, elementName);
|
|
754
865
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
this.pos = eqPos + 1;
|
|
866
|
+
this.#mustHaveAdvanced(was);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
759
869
|
|
|
760
|
-
|
|
761
|
-
|
|
870
|
+
/**
|
|
871
|
+
* A quoted attribute value. XML has no unquoted form, and accepting one is what let a stray
|
|
872
|
+
* character send the cursor somewhere arbitrary
|
|
873
|
+
* @param {string} name
|
|
874
|
+
* @param {string} elementName
|
|
875
|
+
* @returns {string}
|
|
876
|
+
*/
|
|
877
|
+
#parseAttributeValue(name, elementName) {
|
|
878
|
+
this.#skipWhitespace();
|
|
762
879
|
const quote = this.xml[this.pos];
|
|
880
|
+
if (quote !== '"' && quote !== "'") {
|
|
881
|
+
this.#fail(`The value of the attribute '${name}' of <${elementName}> must be quoted`);
|
|
882
|
+
}
|
|
763
883
|
this.pos++;
|
|
884
|
+
const end = this.#seek(quote, `value of the attribute '${name}' of <${elementName}>`);
|
|
885
|
+
const value = FhirXmlBase.unescapeXml(this.xml.substring(this.pos, end));
|
|
886
|
+
this.pos = end + 1;
|
|
887
|
+
return value;
|
|
888
|
+
}
|
|
764
889
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
890
|
+
/**
|
|
891
|
+
* Everything between a start tag and its matching end tag. Text is skipped: FHIR carries its
|
|
892
|
+
* values in attributes, and this parser has never returned character data.
|
|
893
|
+
* @param {string} elementName
|
|
894
|
+
* @returns {Array} the child elements
|
|
895
|
+
*/
|
|
896
|
+
#parseChildren(elementName) {
|
|
897
|
+
const children = [];
|
|
898
|
+
for (;;) {
|
|
899
|
+
const was = this.pos;
|
|
900
|
+
this.#skipWhitespace();
|
|
901
|
+
if (this.#atEnd()) {
|
|
902
|
+
this.#fail(`Unclosed element <${elementName}>: expected </${elementName}>`);
|
|
903
|
+
}
|
|
769
904
|
|
|
770
|
-
|
|
905
|
+
if (this.#startsWith('</')) {
|
|
906
|
+
this.pos += 2;
|
|
907
|
+
const closing = this.#parseName('element name');
|
|
908
|
+
if (closing !== elementName) {
|
|
909
|
+
this.#fail(`Expected </${elementName}> but found </${closing}>`);
|
|
910
|
+
}
|
|
911
|
+
this.#skipWhitespace();
|
|
912
|
+
if (this.#atEnd() || this.xml[this.pos] !== '>') {
|
|
913
|
+
this.#fail(`Expected '>' to close </${closing}>`);
|
|
914
|
+
}
|
|
915
|
+
this.pos++;
|
|
916
|
+
return children;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (this.xml[this.pos] === '<') {
|
|
920
|
+
if (this.#startsWith('<!--')) {
|
|
921
|
+
this.#skipComment();
|
|
922
|
+
} else if (this.#startsWith('<![CDATA[')) {
|
|
923
|
+
this.pos = this.#seek(']]>', 'CDATA section') + 3;
|
|
924
|
+
} else if (this.#startsWith('<!')) {
|
|
925
|
+
this.#fail('Declarations are not allowed inside a FHIR XML element');
|
|
926
|
+
} else if (this.#startsWith('<?')) {
|
|
927
|
+
this.pos = this.#seek('?>', 'processing instruction') + 2;
|
|
928
|
+
} else {
|
|
929
|
+
children.push(this.#parseElement());
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
// character data - skipped, but it still has to be consumed
|
|
933
|
+
this.pos = this.#seek('<', `content of <${elementName}>`);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
this.#mustHaveAdvanced(was);
|
|
937
|
+
}
|
|
771
938
|
}
|
|
772
939
|
}
|
|
773
940
|
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
const {VersionUtilities} = require("../../library/version-utilities");
|
|
2
|
-
const {getValueName} = require("../../library/utilities");
|
|
2
|
+
const {getValueName, getValuePrimitive} = require("../../library/utilities");
|
|
3
3
|
const {Extensions} = require("../library/extensions");
|
|
4
4
|
|
|
5
|
+
// ValueSet.compose.property is R5 and up - it is how a client says which properties it wants
|
|
6
|
+
// back in an expansion. R4 and R3 have nowhere to put it, so it travels as a cross-version
|
|
7
|
+
// extension, the same one the Java convertors use. Dropping it instead means the expansion
|
|
8
|
+
// comes back without the properties that were asked for, which reads as a server fault.
|
|
9
|
+
const COMPOSE_PROPERTY_EXT = "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.property";
|
|
10
|
+
|
|
5
11
|
/**
|
|
6
12
|
* Converts input ValueSet to R5 format (modifies input object for performance)
|
|
7
13
|
* @param {Object} jsonObj - The input ValueSet object
|
|
@@ -20,6 +26,7 @@ function valueSetToR5(jsonObj, sourceVersion) {
|
|
|
20
26
|
for (const inc of jsonObj.compose?.exclude || []) {
|
|
21
27
|
valueSetIncludeToR5(inc);
|
|
22
28
|
}
|
|
29
|
+
composePropertyToR5(jsonObj.compose);
|
|
23
30
|
if (VersionUtilities.isR4Ver(sourceVersion)) {
|
|
24
31
|
return jsonObj; // No conversion needed
|
|
25
32
|
}
|
|
@@ -33,6 +40,28 @@ function valueSetToR5(jsonObj, sourceVersion) {
|
|
|
33
40
|
throw new Error(`Unsupported FHIR version: ${sourceVersion}`);
|
|
34
41
|
}
|
|
35
42
|
|
|
43
|
+
/** Read ValueSet.compose.property back out of the cross-version extension. */
|
|
44
|
+
function composePropertyToR5(compose) {
|
|
45
|
+
if (!compose) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const carried = Extensions.list(compose, COMPOSE_PROPERTY_EXT);
|
|
49
|
+
if (carried.length === 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
compose.property = compose.property || [];
|
|
53
|
+
for (const ext of carried) {
|
|
54
|
+
const value = getValuePrimitive(ext);
|
|
55
|
+
if (value !== undefined && value !== null) {
|
|
56
|
+
compose.property.push(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
compose.extension = compose.extension.filter(ext => ext.url !== COMPOSE_PROPERTY_EXT);
|
|
60
|
+
if (compose.extension.length === 0) {
|
|
61
|
+
delete compose.extension;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
36
65
|
function valueSetIncludeToR5(inc) {
|
|
37
66
|
for (const filter of inc.filter || []) {
|
|
38
67
|
if (filter._op) {
|
|
@@ -84,6 +113,15 @@ function valueSetR5ToR4(r5Obj) {
|
|
|
84
113
|
delete r5Obj.versionAlgorithmCoding;
|
|
85
114
|
}
|
|
86
115
|
|
|
116
|
+
// ValueSet.compose.property has no home in R4, so it goes into the extension
|
|
117
|
+
if (r5Obj.compose && Array.isArray(r5Obj.compose.property) && r5Obj.compose.property.length > 0) {
|
|
118
|
+
r5Obj.compose.extension = r5Obj.compose.extension || [];
|
|
119
|
+
for (const prop of r5Obj.compose.property) {
|
|
120
|
+
r5Obj.compose.extension.push({ url: COMPOSE_PROPERTY_EXT, valueString: prop });
|
|
121
|
+
}
|
|
122
|
+
delete r5Obj.compose.property;
|
|
123
|
+
}
|
|
124
|
+
|
|
87
125
|
// Filter out R5-only filter operators in compose
|
|
88
126
|
if (r5Obj.compose && r5Obj.compose.include) {
|
|
89
127
|
r5Obj.compose.include = r5Obj.compose.include.map(include => {
|