@apollo/federation-internals 2.4.6 → 2.4.7
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/dist/operations.d.ts +39 -112
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +349 -304
- package/dist/operations.js.map +1 -1
- package/package.json +1 -1
- package/src/operations.ts +635 -516
package/src/operations.ts
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
import { isInterfaceObjectType } from "./federation";
|
|
52
52
|
import { ERRORS } from "./error";
|
|
53
53
|
import { isSubtype, sameType } from "./types";
|
|
54
|
-
import { assert,
|
|
54
|
+
import { assert, mapKeys, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
|
|
55
55
|
import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
|
|
56
56
|
import { v1 as uuidv1 } from 'uuid';
|
|
57
57
|
|
|
@@ -696,6 +696,164 @@ export type RootOperationPath = {
|
|
|
696
696
|
path: OperationPath
|
|
697
697
|
}
|
|
698
698
|
|
|
699
|
+
// Computes for every fragment, which other fragments use it (so the reverse of it's dependencies, the other fragment it uses).
|
|
700
|
+
function computeFragmentsDependents(fragments: NamedFragments): SetMultiMap<string, string> {
|
|
701
|
+
const reverseDeps = new SetMultiMap<string, string>();
|
|
702
|
+
for (const fragment of fragments.definitions()) {
|
|
703
|
+
for (const dependency of fragment.fragmentUsages().keys()) {
|
|
704
|
+
reverseDeps.add(dependency, fragment.name);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return reverseDeps;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function clearKeptFragments(
|
|
711
|
+
usages: Map<string, number>,
|
|
712
|
+
fragments: NamedFragments,
|
|
713
|
+
minUsagesToOptimize: number
|
|
714
|
+
) {
|
|
715
|
+
// `toCheck` will contain only fragments that we know we want to keep (but haven't handled/removed from `usages` yet).
|
|
716
|
+
let toCheck = Array.from(usages.entries()).filter(([_, count]) => count >= minUsagesToOptimize).map(([name, _]) => name);
|
|
717
|
+
while (toCheck.length > 0) {
|
|
718
|
+
const newToCheck = [];
|
|
719
|
+
for (const name of toCheck) {
|
|
720
|
+
// We "keep" that fragment so clear it.
|
|
721
|
+
usages.delete(name);
|
|
722
|
+
// But as it is used, bump the usage for every fragment it uses.
|
|
723
|
+
const ownUsages = fragments.get(name)!.fragmentUsages();
|
|
724
|
+
for (const [otherName, otherCount] of ownUsages.entries()) {
|
|
725
|
+
const prevCount = usages.get(otherName);
|
|
726
|
+
// We're interested in fragment not in `usages` anymore.
|
|
727
|
+
if (prevCount !== undefined) {
|
|
728
|
+
const newCount = prevCount + otherCount;
|
|
729
|
+
usages.set(otherName, newCount);
|
|
730
|
+
if (prevCount < minUsagesToOptimize && newCount >= minUsagesToOptimize) {
|
|
731
|
+
newToCheck.push(otherName);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
toCheck = newToCheck;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Checks, in `selectionSet`, which fragments (of `fragments`) are used at least `minUsagesToOptimize` times.
|
|
741
|
+
// Returns the updated set of fragments containing only the fragment definitions with usage above our threshold,
|
|
742
|
+
// and `undefined` or `null` if no such fragment meets said threshold. When this method returns `null`, it
|
|
743
|
+
// additionally means that no fragments are use at all in `selectionSet` (and so `undefined` means that
|
|
744
|
+
// "some" fragments are used in `selectionSet`, but just none of them is used at least `minUsagesToOptimize`
|
|
745
|
+
// times).
|
|
746
|
+
function computeFragmentsToKeep(
|
|
747
|
+
selectionSet: SelectionSet,
|
|
748
|
+
fragments: NamedFragments,
|
|
749
|
+
minUsagesToOptimize: number
|
|
750
|
+
): NamedFragments | undefined | null {
|
|
751
|
+
// We start by collecting the usages within the selection set.
|
|
752
|
+
const usages = new Map<string, number>();
|
|
753
|
+
selectionSet.collectUsedFragmentNames(usages);
|
|
754
|
+
|
|
755
|
+
// If we have no fragment in the selection set, then it's simple, we just don't keep any fragments.
|
|
756
|
+
if (usages.size === 0) {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// We're going to remove fragments from usages as we categorize them as kept or expanded, so we
|
|
761
|
+
// first ensure that it has entries for every fragment, default to 0.
|
|
762
|
+
for (const fragment of fragments.definitions()) {
|
|
763
|
+
if (usages.get(fragment.name) === undefined) {
|
|
764
|
+
usages.set(fragment.name, 0);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// At this point, `usages` contains the usages of fragments "in the selection". From that, we want
|
|
769
|
+
// to decide which fragment to "keep", and which to re-expand. But there is 2 subtlety:
|
|
770
|
+
// 1. when we decide to keep some fragment F, then we should could it's own usages of other fragments. That
|
|
771
|
+
// is, if a fragment G is use once in the selection, but also use once in a fragment F that we
|
|
772
|
+
// keep, then the usages for G is really 2 (but if F is unused, then we don't want to count
|
|
773
|
+
// it's usage of G for instance).
|
|
774
|
+
// 2. when we decide to expand a fragment, then this also impact the usages of other fragments it
|
|
775
|
+
// uses, as those gets "inlined" into the selection. But that also mean we have to be careful
|
|
776
|
+
// of the order in which we pick fragments to expand. Say we have:
|
|
777
|
+
// ```graphql
|
|
778
|
+
// query {
|
|
779
|
+
// ...F1
|
|
780
|
+
// }
|
|
781
|
+
//
|
|
782
|
+
// fragment F1 {
|
|
783
|
+
// a { ...F2 }
|
|
784
|
+
// b { ...F2 }
|
|
785
|
+
// }
|
|
786
|
+
//
|
|
787
|
+
// fragment F2 {
|
|
788
|
+
// // something
|
|
789
|
+
// }
|
|
790
|
+
// ```
|
|
791
|
+
// then at this point where we've only counted usages in the query selection, `usages` will be
|
|
792
|
+
// `{ F1: 1, F2: 0 }`. But we do not want to expand _both_ F1 and F2. Instead, we want to expand
|
|
793
|
+
// F1 first, and then realize that this increases F2 usages to 2, which means we stop there and keep F2.
|
|
794
|
+
// Generalizing this, it means we want to first pick up fragments to expand that are _not_ used by any
|
|
795
|
+
// other fragments that may be expanded.
|
|
796
|
+
const reverseDependencies = computeFragmentsDependents(fragments);
|
|
797
|
+
// We'll add to `toExpand` fragment we will definitively expand.
|
|
798
|
+
const toExpand = new Set<string>;
|
|
799
|
+
let shouldContinue = true;
|
|
800
|
+
while (shouldContinue) {
|
|
801
|
+
// We'll do an iteration, but if we make no progress, we won't continue (we don't want to loop forever).
|
|
802
|
+
shouldContinue = false;
|
|
803
|
+
clearKeptFragments(usages, fragments, minUsagesToOptimize);
|
|
804
|
+
for (const name of mapKeys(usages)) {
|
|
805
|
+
// Note that we modify `usages` as we iterate it, so 1) we use `mapKeys` above which copy into a list and 2)
|
|
806
|
+
// we get the `count` manually instead of relying on (possibly outdated) entries.
|
|
807
|
+
const count = usages.get(name)!;
|
|
808
|
+
// A unused fragment is not technically expanded, it is just removed and we can ignore for now (it's count
|
|
809
|
+
// count increase later but ...).
|
|
810
|
+
if (count === 0) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// If we find a fragment to keep, it means some fragment we expanded earlier in this iteration bump this
|
|
815
|
+
// one count. We unsure `shouldContinue` is set so `clearKeptFragments` is called again, but let that
|
|
816
|
+
// method deal with it otherwise.
|
|
817
|
+
if (count >= minUsagesToOptimize) {
|
|
818
|
+
shouldContinue = true;
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const fragmentsUsingName = reverseDependencies.get(name);
|
|
823
|
+
if (!fragmentsUsingName || [...fragmentsUsingName].every((fragName) => toExpand.has(fragName) || !usages.get(fragName))) {
|
|
824
|
+
// This fragment is not used enough, and is only used by fragments we keep, so we
|
|
825
|
+
// are guaranteed that expanding another fragment will not increase its usage. So
|
|
826
|
+
// we definitively expand it.
|
|
827
|
+
toExpand.add(name);
|
|
828
|
+
usages.delete(name);
|
|
829
|
+
|
|
830
|
+
// We've added to `toExpand`, so it's worth redoing another iteration
|
|
831
|
+
// after that to see if something changes.
|
|
832
|
+
shouldContinue = true;
|
|
833
|
+
|
|
834
|
+
// Now that we expand it, we should bump the usage for every fragment it uses.
|
|
835
|
+
const nameUsages = fragments.get(name)!.fragmentUsages();
|
|
836
|
+
for (const [otherName, otherCount] of nameUsages.entries()) {
|
|
837
|
+
const prev = usages.get(otherName);
|
|
838
|
+
// Note that if `otherName` is not part of usages, it means it's a fragment we
|
|
839
|
+
// already decided to keep/expand, so we just ignore it.
|
|
840
|
+
if (prev !== undefined) {
|
|
841
|
+
usages.set(otherName, prev + count * otherCount);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Finally, we know that to expand, which is `toExpand` plus whatever remains in `usage` (typically
|
|
849
|
+
// genuinely unused fragments).
|
|
850
|
+
for (const name of usages.keys()) {
|
|
851
|
+
toExpand.add(name);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return toExpand.size === 0 ? fragments : fragments.filter((f) => !toExpand.has(f.name));
|
|
855
|
+
}
|
|
856
|
+
|
|
699
857
|
// TODO Operations can also have directives
|
|
700
858
|
export class Operation {
|
|
701
859
|
constructor(
|
|
@@ -703,6 +861,7 @@ export class Operation {
|
|
|
703
861
|
readonly rootKind: SchemaRootKind,
|
|
704
862
|
readonly selectionSet: SelectionSet,
|
|
705
863
|
readonly variableDefinitions: VariableDefinitions,
|
|
864
|
+
readonly fragments?: NamedFragments,
|
|
706
865
|
readonly name?: string) {
|
|
707
866
|
}
|
|
708
867
|
|
|
@@ -717,39 +876,28 @@ export class Operation {
|
|
|
717
876
|
return this;
|
|
718
877
|
}
|
|
719
878
|
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
}
|
|
879
|
+
const finalFragments = computeFragmentsToKeep(optimizedSelection, fragments, minUsagesToOptimize);
|
|
880
|
+
if (finalFragments === null || finalFragments?.size === fragments.size) {
|
|
881
|
+
// This means either that there is no fragment usage whatsoever in `optimizedSelection`, or that
|
|
882
|
+
// we're keeping all fragments. In both cases, we need no additional work on `optimizedSelection`.
|
|
883
|
+
return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, finalFragments ?? undefined, this.name);
|
|
726
884
|
}
|
|
727
885
|
|
|
728
|
-
//
|
|
729
|
-
// re-expand some is not entirely optimal, but it's
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
|
|
734
|
-
// Also note `toDeoptimize` will always contains the unused fragments, which will allow `expandFragments`
|
|
735
|
-
// to remove them from the listed fragments in `optimizedSelection` (here again, this could make use call
|
|
736
|
-
// `expandFragments` on _only_ unused fragments and that case could be dealt with more efficiently, but
|
|
737
|
-
// probably not noticeable in practice so ...).
|
|
738
|
-
const toDeoptimize = mapEntries(usages).filter(([_, count]) => count < minUsagesToOptimize).map(([name]) => name);
|
|
886
|
+
// If we get here, it means some fragments need to be expanded, so we do so.
|
|
887
|
+
// Optimizing all fragments to potentially re-expand some is not entirely optimal, but it's unclear
|
|
888
|
+
// how to do otherwise, and it probably don't matter too much in practice (we only call this optimization
|
|
889
|
+
// on the final computed query plan, so not a very hot path; plus in most cases we won't even reach that
|
|
890
|
+
// point either because there is no fragment, or none will have been optimized away so we'll exit above).
|
|
891
|
+
optimizedSelection = optimizedSelection.expandFragments(finalFragments);
|
|
739
892
|
|
|
740
|
-
if
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
// the fragments to de-optimize it later, so we do a final "trim" pass to remove those.
|
|
745
|
-
optimizedSelection = optimizedSelection.trimUnsatisfiableBranches(optimizedSelection.parentType);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, this.name);
|
|
893
|
+
// Expanding fragments could create some "inefficiencies" that we wouldn't have if we hadn't re-optimized
|
|
894
|
+
// the fragments to de-optimize it later, so we do a final "trim" pass to remove those.
|
|
895
|
+
optimizedSelection = optimizedSelection.trimUnsatisfiableBranches(optimizedSelection.parentType);
|
|
896
|
+
return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, finalFragments, this.name);
|
|
749
897
|
}
|
|
750
898
|
|
|
751
899
|
expandAllFragments(): Operation {
|
|
752
|
-
const expandedSelections = this.selectionSet.
|
|
900
|
+
const expandedSelections = this.selectionSet.expandFragments();
|
|
753
901
|
if (expandedSelections === this.selectionSet) {
|
|
754
902
|
return this;
|
|
755
903
|
}
|
|
@@ -759,6 +907,7 @@ export class Operation {
|
|
|
759
907
|
this.rootKind,
|
|
760
908
|
expandedSelections,
|
|
761
909
|
this.variableDefinitions,
|
|
910
|
+
undefined,
|
|
762
911
|
this.name
|
|
763
912
|
);
|
|
764
913
|
}
|
|
@@ -774,6 +923,7 @@ export class Operation {
|
|
|
774
923
|
this.rootKind,
|
|
775
924
|
trimmedSelections,
|
|
776
925
|
this.variableDefinitions,
|
|
926
|
+
this.fragments,
|
|
777
927
|
this.name
|
|
778
928
|
);
|
|
779
929
|
}
|
|
@@ -786,14 +936,10 @@ export class Operation {
|
|
|
786
936
|
* applications are removed.
|
|
787
937
|
*/
|
|
788
938
|
withoutDefer(labelsToRemove?: Set<string>): Operation {
|
|
789
|
-
// If we have named fragments, we should be looking inside those and either expand those having @defer or,
|
|
790
|
-
// probably better, replace them with a verison without @defer. But as we currently only call this method
|
|
791
|
-
// after `expandAllFragments`, we'll implement this when/if we need it.
|
|
792
|
-
assert(!this.selectionSet.fragments || this.selectionSet.fragments.isEmpty(), 'Removing @defer currently only work on "expanded" selections (no named fragments)');
|
|
793
939
|
const updated = this.selectionSet.withoutDefer(labelsToRemove);
|
|
794
940
|
return updated == this.selectionSet
|
|
795
941
|
? this
|
|
796
|
-
: new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.name);
|
|
942
|
+
: new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.fragments, this.name);
|
|
797
943
|
}
|
|
798
944
|
|
|
799
945
|
/**
|
|
@@ -815,15 +961,12 @@ export class Operation {
|
|
|
815
961
|
assignedDeferLabels: Set<string>,
|
|
816
962
|
deferConditions: SetMultiMap<string, string>,
|
|
817
963
|
} {
|
|
818
|
-
// Similar comment than in `withoutDefer`
|
|
819
|
-
assert(!this.selectionSet.fragments || this.selectionSet.fragments.isEmpty(), 'Assigning @defer lables currently only work on "expanded" selections (no named fragments)');
|
|
820
|
-
|
|
821
964
|
const normalizer = new DeferNormalizer();
|
|
822
965
|
const { hasDefers, hasNonLabelledOrConditionalDefers } = normalizer.init(this.selectionSet);
|
|
823
966
|
let updatedOperation: Operation = this;
|
|
824
967
|
if (hasNonLabelledOrConditionalDefers) {
|
|
825
968
|
const updated = this.selectionSet.withNormalizedDefer(normalizer);
|
|
826
|
-
updatedOperation = new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.name);
|
|
969
|
+
updatedOperation = new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.fragments, this.name);
|
|
827
970
|
}
|
|
828
971
|
return {
|
|
829
972
|
operation: updatedOperation,
|
|
@@ -844,14 +987,20 @@ export class Operation {
|
|
|
844
987
|
}
|
|
845
988
|
|
|
846
989
|
toString(expandFragments: boolean = false, prettyPrint: boolean = true): string {
|
|
847
|
-
return this.selectionSet.toOperationString(this.rootKind, this.variableDefinitions, this.name, expandFragments, prettyPrint);
|
|
990
|
+
return this.selectionSet.toOperationString(this.rootKind, this.variableDefinitions, this.fragments, this.name, expandFragments, prettyPrint);
|
|
848
991
|
}
|
|
849
992
|
}
|
|
850
993
|
|
|
851
994
|
export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmentDefinition> {
|
|
852
995
|
private _selectionSet: SelectionSet | undefined;
|
|
853
996
|
|
|
854
|
-
|
|
997
|
+
// Lazily computed cache of the expanded selection set.
|
|
998
|
+
private _expandedSelectionSet: SelectionSet | undefined;
|
|
999
|
+
|
|
1000
|
+
private _fragmentUsages: Map<string, number> | undefined;
|
|
1001
|
+
private _includedFragmentNames: Set<string> | undefined;
|
|
1002
|
+
|
|
1003
|
+
private readonly expandedSelectionSetsAtTypesCache = new Map<string, SelectionSet>();
|
|
855
1004
|
|
|
856
1005
|
constructor(
|
|
857
1006
|
schema: Schema,
|
|
@@ -876,12 +1025,31 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
876
1025
|
return this._selectionSet;
|
|
877
1026
|
}
|
|
878
1027
|
|
|
1028
|
+
expandedSelectionSet(): SelectionSet {
|
|
1029
|
+
if (!this._expandedSelectionSet) {
|
|
1030
|
+
this._expandedSelectionSet = this.selectionSet.expandFragments().trimUnsatisfiableBranches(this.typeCondition);
|
|
1031
|
+
}
|
|
1032
|
+
return this._expandedSelectionSet;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
879
1035
|
withUpdatedSelectionSet(newSelectionSet: SelectionSet): NamedFragmentDefinition {
|
|
880
1036
|
return new NamedFragmentDefinition(this.schema(), this.name, this.typeCondition).setSelectionSet(newSelectionSet);
|
|
881
1037
|
}
|
|
882
1038
|
|
|
1039
|
+
fragmentUsages(): ReadonlyMap<string, number> {
|
|
1040
|
+
if (!this._fragmentUsages) {
|
|
1041
|
+
this._fragmentUsages = new Map();
|
|
1042
|
+
this.selectionSet.collectUsedFragmentNames(this._fragmentUsages);
|
|
1043
|
+
}
|
|
1044
|
+
return this._fragmentUsages;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
883
1047
|
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
884
|
-
this.
|
|
1048
|
+
const usages = this.fragmentUsages();
|
|
1049
|
+
for (const [name, count] of usages.entries()) {
|
|
1050
|
+
const prevCount = collector.get(name);
|
|
1051
|
+
collector.set(name, prevCount ? prevCount + count : count);
|
|
1052
|
+
}
|
|
885
1053
|
}
|
|
886
1054
|
|
|
887
1055
|
toFragmentDefinitionNode() : FragmentDefinitionNode {
|
|
@@ -903,13 +1071,32 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
903
1071
|
}
|
|
904
1072
|
|
|
905
1073
|
/**
|
|
906
|
-
* Whether this fragment may apply at the provided type, that is if
|
|
907
|
-
*
|
|
1074
|
+
* Whether this fragment may apply at the provided type, that is if either:
|
|
1075
|
+
* - its type condition is equal to the provided type.
|
|
1076
|
+
* - or the runtime types of the provided type include all of those of the fragment condition.
|
|
908
1077
|
*
|
|
909
1078
|
* @param type - the type at which we're looking at applying the fragment
|
|
910
1079
|
*/
|
|
911
1080
|
canApplyAtType(type: CompositeType): boolean {
|
|
912
|
-
|
|
1081
|
+
if (sameType(type, this.typeCondition)) {
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// No point computing runtime types if the condition is an object (it can never cover all of
|
|
1086
|
+
// the runtimes of `type` unless it's the same type, which is already covered).
|
|
1087
|
+
if (!isAbstractType(this.typeCondition)) {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const conditionRuntimes = possibleRuntimeTypes(this.typeCondition);
|
|
1092
|
+
const typeRuntimes = possibleRuntimeTypes(type);
|
|
1093
|
+
|
|
1094
|
+
// The fragment condition must be at least as general as the provided type (so that if we use the fragment
|
|
1095
|
+
// inside `type`, then it doesn't add restriction that weren't there without the fragment).
|
|
1096
|
+
// Note: the `length` test is technically redundant, but just avoid the more costly sub-set check if we
|
|
1097
|
+
// can cheaply show it's unnecessary.
|
|
1098
|
+
return conditionRuntimes.length >= typeRuntimes.length
|
|
1099
|
+
&& typeRuntimes.every((t1) => conditionRuntimes.some((t2) => sameType(t1, t2)));
|
|
913
1100
|
}
|
|
914
1101
|
|
|
915
1102
|
/**
|
|
@@ -930,10 +1117,12 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
930
1117
|
* then if the current type is `T1`, then all we care about matching for this fragment is the `... on T1` part, and this method gives
|
|
931
1118
|
* us that part.
|
|
932
1119
|
*/
|
|
933
|
-
|
|
1120
|
+
expandedSelectionSetAtType(type: CompositeType): SelectionSet {
|
|
1121
|
+
const expandedSelectionSet = this.expandedSelectionSet();
|
|
1122
|
+
|
|
934
1123
|
// First, if the candidate condition is an object or is the type passed, then there isn't any additional restriction to do.
|
|
935
1124
|
if (sameType(type, this.typeCondition) || isObjectType(this.typeCondition)) {
|
|
936
|
-
return
|
|
1125
|
+
return expandedSelectionSet;
|
|
937
1126
|
}
|
|
938
1127
|
|
|
939
1128
|
// We should not call `trimUnsatisfiableBranches` where `type` is an abstract type (`interface` or `union`) as it currently could
|
|
@@ -945,17 +1134,45 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
945
1134
|
// Concretely, this means that there may be corner cases where a named fragment could be reused but isn't, but waiting on finding
|
|
946
1135
|
// concrete examples where this matter to decide if it's worth the complexity.
|
|
947
1136
|
if (!isObjectType(type)) {
|
|
948
|
-
return
|
|
1137
|
+
return expandedSelectionSet;
|
|
949
1138
|
}
|
|
950
1139
|
|
|
951
|
-
let
|
|
952
|
-
if (!
|
|
1140
|
+
let selectionAtType = this.expandedSelectionSetsAtTypesCache.get(type.name);
|
|
1141
|
+
if (!selectionAtType) {
|
|
953
1142
|
// Note that all we want is removing any top-level branches that don't apply due to the current type. There is no point
|
|
954
1143
|
// in going recursive however: any simplification due to `type` stops as soon as we traverse a field. And so we don't bother.
|
|
955
|
-
|
|
956
|
-
this.
|
|
1144
|
+
selectionAtType = expandedSelectionSet.trimUnsatisfiableBranches(type, { recursive: false });
|
|
1145
|
+
this.expandedSelectionSetsAtTypesCache.set(type.name, selectionAtType);
|
|
957
1146
|
}
|
|
958
|
-
return
|
|
1147
|
+
return selectionAtType;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Whether this fragment fully includes `otherFragment`.
|
|
1152
|
+
* Note that this is slightly different from `this` "using" `otherFragment` in that this essentially checks
|
|
1153
|
+
* if the full selection set of `otherFragment` is contained by `this`, so this only look at "top-level" usages.
|
|
1154
|
+
*
|
|
1155
|
+
* Note that this is guaranteed to return `false` if passed `this` name.
|
|
1156
|
+
*/
|
|
1157
|
+
includes(otherFragment: string): boolean {
|
|
1158
|
+
if (this.name === otherFragment) {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (!this._includedFragmentNames) {
|
|
1163
|
+
this._includedFragmentNames = this.computeIncludedFragmentNames();
|
|
1164
|
+
}
|
|
1165
|
+
return this._includedFragmentNames.has(otherFragment);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
private computeIncludedFragmentNames(): Set<string> {
|
|
1169
|
+
const included = new Set<string>();
|
|
1170
|
+
for (const selection of this.selectionSet.selections()) {
|
|
1171
|
+
if (selection instanceof FragmentSpreadSelection) {
|
|
1172
|
+
included.add(selection.namedFragment.name);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
return included;
|
|
959
1176
|
}
|
|
960
1177
|
|
|
961
1178
|
toString(indent?: string): string {
|
|
@@ -967,7 +1184,11 @@ export class NamedFragments {
|
|
|
967
1184
|
private readonly fragments = new MapWithCachedArrays<string, NamedFragmentDefinition>();
|
|
968
1185
|
|
|
969
1186
|
isEmpty(): boolean {
|
|
970
|
-
return this.
|
|
1187
|
+
return this.size === 0;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
get size(): number {
|
|
1191
|
+
return this.fragments.size;
|
|
971
1192
|
}
|
|
972
1193
|
|
|
973
1194
|
names(): readonly string[] {
|
|
@@ -991,26 +1212,6 @@ export class NamedFragments {
|
|
|
991
1212
|
return this.fragments.values().filter(f => f.canApplyAtType(type));
|
|
992
1213
|
}
|
|
993
1214
|
|
|
994
|
-
without(names: string[]): NamedFragments | undefined {
|
|
995
|
-
if (!names.some(n => this.fragments.has(n))) {
|
|
996
|
-
return this;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
const newFragments = new NamedFragments();
|
|
1000
|
-
for (const fragment of this.fragments.values()) {
|
|
1001
|
-
if (!names.includes(fragment.name)) {
|
|
1002
|
-
// We want to keep that fragment. But that fragment might use a fragment we
|
|
1003
|
-
// remove, and if so, we need to expand that removed fragment.
|
|
1004
|
-
const updatedSelectionSet = fragment.selectionSet.expandFragments(names, newFragments);
|
|
1005
|
-
const newFragment = updatedSelectionSet === fragment.selectionSet
|
|
1006
|
-
? fragment
|
|
1007
|
-
: fragment.withUpdatedSelectionSet(updatedSelectionSet);
|
|
1008
|
-
newFragments.add(newFragment);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
return newFragments.isEmpty() ? undefined : newFragments;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
1215
|
get(name: string): NamedFragmentDefinition | undefined {
|
|
1015
1216
|
return this.fragments.get(name);
|
|
1016
1217
|
}
|
|
@@ -1032,47 +1233,44 @@ export class NamedFragments {
|
|
|
1032
1233
|
}
|
|
1033
1234
|
|
|
1034
1235
|
/**
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1236
|
+
* The mapper is called on every fragment definition (`fragment` argument), but in such a way that if a fragment A uses another fragment B,
|
|
1237
|
+
* then the mapper is guaranteed to be called on B _before_ being called on A. Further, the `newFragments` argument is a new `NamedFragments`
|
|
1238
|
+
* containing all the previously mapped definition (minus those for which the mapper returned `undefined`). So if A uses B (and the mapper
|
|
1239
|
+
* on B do not return undefined), then when mapper is called on A `newFragments` will have the mapped value for B.
|
|
1039
1240
|
*/
|
|
1040
|
-
|
|
1041
|
-
mapper: (
|
|
1042
|
-
recreateFct: (frag: NamedFragmentDefinition, newSelectionSet: SelectionSet) => NamedFragmentDefinition = (f, s) => f.withUpdatedSelectionSet(s),
|
|
1241
|
+
mapInDependencyOrder(
|
|
1242
|
+
mapper: (fragment: NamedFragmentDefinition, newFragments: NamedFragments) => NamedFragmentDefinition | undefined
|
|
1043
1243
|
): NamedFragments | undefined {
|
|
1044
|
-
type
|
|
1045
|
-
|
|
1046
|
-
mappedSelectionSet: SelectionSet,
|
|
1244
|
+
type FragmentDependencies = {
|
|
1245
|
+
fragment: NamedFragmentDefinition,
|
|
1047
1246
|
dependsOn: string[],
|
|
1048
1247
|
};
|
|
1049
|
-
const fragmentsMap = new Map<string,
|
|
1050
|
-
const removedFragments = new Set<string>();
|
|
1248
|
+
const fragmentsMap = new Map<string, FragmentDependencies>();
|
|
1051
1249
|
for (const fragment of this.definitions()) {
|
|
1052
|
-
const mappedSelectionSet = mapper(fragment.selectionSet.expandAllFragments().trimUnsatisfiableBranches(fragment.typeCondition));
|
|
1053
|
-
if (!mappedSelectionSet) {
|
|
1054
|
-
removedFragments.add(fragment.name);
|
|
1055
|
-
continue;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
const otherFragmentsUsages = new Map<string, number>();
|
|
1059
|
-
fragment.collectUsedFragmentNames(otherFragmentsUsages);
|
|
1060
1250
|
fragmentsMap.set(fragment.name, {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
dependsOn: Array.from(otherFragmentsUsages.keys()),
|
|
1251
|
+
fragment,
|
|
1252
|
+
dependsOn: Array.from(fragment.fragmentUsages().keys()),
|
|
1064
1253
|
});
|
|
1065
1254
|
}
|
|
1066
1255
|
|
|
1256
|
+
const removedFragments = new Set<string>();
|
|
1067
1257
|
const mappedFragments = new NamedFragments();
|
|
1068
1258
|
while (fragmentsMap.size > 0) {
|
|
1069
1259
|
for (const [name, info] of fragmentsMap) {
|
|
1070
1260
|
// Note that graphQL specifies that named fragments cannot have cycles (https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles)
|
|
1071
1261
|
// and so we're guaranteed that on every iteration, at least one element of the map is removed (so the `while` loop will terminate).
|
|
1072
1262
|
if (info.dependsOn.every((n) => mappedFragments.has(n) || removedFragments.has(n))) {
|
|
1073
|
-
const
|
|
1074
|
-
mappedFragments.add(recreateFct(info.original, reoptimizedSelectionSet));
|
|
1263
|
+
const mapped = mapper(info.fragment, mappedFragments);
|
|
1075
1264
|
fragmentsMap.delete(name);
|
|
1265
|
+
if (!mapped) {
|
|
1266
|
+
removedFragments.add(name);
|
|
1267
|
+
} else {
|
|
1268
|
+
mappedFragments.add(mapped);
|
|
1269
|
+
}
|
|
1270
|
+
// We just deleted from `fragmentsMap` so continuing our current `for` iteration is dangerous,
|
|
1271
|
+
// so we break to the `while` loop (besides, there is no reason why continuing the inner iteration
|
|
1272
|
+
// would be better than restarting it right away).
|
|
1273
|
+
break;
|
|
1076
1274
|
}
|
|
1077
1275
|
}
|
|
1078
1276
|
}
|
|
@@ -1080,20 +1278,58 @@ export class NamedFragments {
|
|
|
1080
1278
|
return mappedFragments.isEmpty() ? undefined : mappedFragments;
|
|
1081
1279
|
}
|
|
1082
1280
|
|
|
1281
|
+
/**
|
|
1282
|
+
* This method:
|
|
1283
|
+
* - expands all nested fragments,
|
|
1284
|
+
* - applies the provided mapper to the selection set of the fragments,
|
|
1285
|
+
* - and finally re-fragments the nested fragments.
|
|
1286
|
+
*/
|
|
1287
|
+
mapToExpandedSelectionSets(
|
|
1288
|
+
mapper: (selectionSet: SelectionSet) => SelectionSet | undefined,
|
|
1289
|
+
): NamedFragments | undefined {
|
|
1290
|
+
return this.mapInDependencyOrder((fragment, newFragments) => {
|
|
1291
|
+
const mappedSelectionSet = mapper(fragment.selectionSet.expandFragments().trimUnsatisfiableBranches(fragment.typeCondition));
|
|
1292
|
+
if (!mappedSelectionSet) {
|
|
1293
|
+
return undefined;
|
|
1294
|
+
}
|
|
1295
|
+
const reoptimizedSelectionSet = mappedSelectionSet.optimize(newFragments);
|
|
1296
|
+
return fragment.withUpdatedSelectionSet(reoptimizedSelectionSet);
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1083
1300
|
rebaseOn(schema: Schema): NamedFragments | undefined {
|
|
1084
|
-
return this.
|
|
1085
|
-
(
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
return rebasedType && isCompositeType(rebasedType) ? s.rebaseOn(rebasedType) : undefined;
|
|
1089
|
-
} catch (e) {
|
|
1090
|
-
// This means we cannot rebase this selection on the schema and thus cannot reuse that fragment on that
|
|
1091
|
-
// particular schema.
|
|
1301
|
+
return this.mapInDependencyOrder((fragment, newFragments) => {
|
|
1302
|
+
const rebasedType = schema.type(fragment.selectionSet.parentType.name);
|
|
1303
|
+
try {
|
|
1304
|
+
if (!rebasedType || !isCompositeType(rebasedType)) {
|
|
1092
1305
|
return undefined;
|
|
1093
1306
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1307
|
+
|
|
1308
|
+
const rebasedSelection = fragment.selectionSet.rebaseOn(rebasedType, newFragments);
|
|
1309
|
+
return new NamedFragmentDefinition(schema, fragment.name, rebasedType).setSelectionSet(rebasedSelection);
|
|
1310
|
+
} catch (e) {
|
|
1311
|
+
// This means we cannot rebase this selection on the schema and thus cannot reuse that fragment on that
|
|
1312
|
+
// particular schema.
|
|
1313
|
+
return undefined;
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
filter(predicate: (fragment: NamedFragmentDefinition) => boolean): NamedFragments | undefined {
|
|
1319
|
+
return this.mapInDependencyOrder((fragment, newFragments) => {
|
|
1320
|
+
if (predicate(fragment)) {
|
|
1321
|
+
// We want to keep that fragment. But that fragment might use a fragment we remove, and if so,
|
|
1322
|
+
// we need to expand that removed fragment. Note that because we're running in
|
|
1323
|
+
// dependency order, we know that `newFragments` will have every fragments that should be
|
|
1324
|
+
// kept/not expanded.
|
|
1325
|
+
const updatedSelectionSet = fragment.selectionSet.expandFragments(newFragments);
|
|
1326
|
+
return updatedSelectionSet === fragment.selectionSet
|
|
1327
|
+
? fragment
|
|
1328
|
+
: fragment.withUpdatedSelectionSet(updatedSelectionSet);
|
|
1329
|
+
} else {
|
|
1330
|
+
return undefined;
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1097
1333
|
}
|
|
1098
1334
|
|
|
1099
1335
|
validate(variableDefinitions: VariableDefinitions) {
|
|
@@ -1173,6 +1409,14 @@ class DeferNormalizer {
|
|
|
1173
1409
|
}
|
|
1174
1410
|
}
|
|
1175
1411
|
|
|
1412
|
+
export enum ContainsResult {
|
|
1413
|
+
// Note: enum values are numbers in the end, and 0 means false in JS, so we should keep `NOT_CONTAINED` first
|
|
1414
|
+
// so that using the result of `contains` as a boolean works.
|
|
1415
|
+
NOT_CONTAINED,
|
|
1416
|
+
STRICTLY_CONTAINED,
|
|
1417
|
+
EQUAL,
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1176
1420
|
export class SelectionSet {
|
|
1177
1421
|
private readonly _keyedSelections: Map<string, Selection>;
|
|
1178
1422
|
private readonly _selections: readonly Selection[];
|
|
@@ -1180,7 +1424,6 @@ export class SelectionSet {
|
|
|
1180
1424
|
constructor(
|
|
1181
1425
|
readonly parentType: CompositeType,
|
|
1182
1426
|
keyedSelections: Map<string, Selection> = new Map(),
|
|
1183
|
-
readonly fragments?: NamedFragments,
|
|
1184
1427
|
) {
|
|
1185
1428
|
this._keyedSelections = keyedSelections;
|
|
1186
1429
|
this._selections = mapValues(keyedSelections);
|
|
@@ -1234,7 +1477,7 @@ export class SelectionSet {
|
|
|
1234
1477
|
|
|
1235
1478
|
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
1236
1479
|
for (const selection of this.selections()) {
|
|
1237
|
-
|
|
1480
|
+
selection.collectUsedFragmentNames(collector);
|
|
1238
1481
|
}
|
|
1239
1482
|
}
|
|
1240
1483
|
|
|
@@ -1265,7 +1508,7 @@ export class SelectionSet {
|
|
|
1265
1508
|
// and in that case we return a singleton selection with just that. Otherwise, it's our wrapping inline fragment
|
|
1266
1509
|
// with the sub-selections optimized, and we just return that subselection.
|
|
1267
1510
|
return optimized instanceof FragmentSpreadSelection
|
|
1268
|
-
? selectionSetOf(this.parentType, optimized
|
|
1511
|
+
? selectionSetOf(this.parentType, optimized)
|
|
1269
1512
|
: optimized.selectionSet;
|
|
1270
1513
|
}
|
|
1271
1514
|
|
|
@@ -1273,26 +1516,11 @@ export class SelectionSet {
|
|
|
1273
1516
|
// may not match fragments that would apply at top-level, so you should usually use `optimize` instead (this exists mostly
|
|
1274
1517
|
// for the recursion).
|
|
1275
1518
|
optimizeSelections(fragments: NamedFragments): SelectionSet {
|
|
1276
|
-
|
|
1277
|
-
// not only because we need to deal with merging new and existing fragments, but also because
|
|
1278
|
-
// things get weird if some fragment names are in common to both. Since we currently only care
|
|
1279
|
-
// about this method when optimizing subgraph fetch selections and those are initially created
|
|
1280
|
-
// without any fragments, we don't bother handling this more complex case.
|
|
1281
|
-
assert(!this.fragments || this.fragments.isEmpty(), `Should not be called on selection that already has named fragments, but got ${this.fragments}`)
|
|
1282
|
-
|
|
1283
|
-
return this.lazyMap((selection) => selection.optimize(fragments), { fragments });
|
|
1519
|
+
return this.lazyMap((selection) => selection.optimize(fragments));
|
|
1284
1520
|
}
|
|
1285
1521
|
|
|
1286
|
-
|
|
1287
|
-
return this.lazyMap((selection) => selection.
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): SelectionSet {
|
|
1291
|
-
if (names.length === 0) {
|
|
1292
|
-
return this;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
return this.lazyMap((selection) => selection.expandFragments(names, updatedFragments), { fragments: updatedFragments ?? null });
|
|
1522
|
+
expandFragments(updatedFragments?: NamedFragments): SelectionSet {
|
|
1523
|
+
return this.lazyMap((selection) => selection.expandFragments(updatedFragments));
|
|
1296
1524
|
}
|
|
1297
1525
|
|
|
1298
1526
|
trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): SelectionSet {
|
|
@@ -1310,14 +1538,10 @@ export class SelectionSet {
|
|
|
1310
1538
|
lazyMap(
|
|
1311
1539
|
mapper: (selection: Selection) => Selection | readonly Selection[] | SelectionSet | undefined,
|
|
1312
1540
|
options?: {
|
|
1313
|
-
fragments?: NamedFragments | null,
|
|
1314
1541
|
parentType?: CompositeType,
|
|
1315
1542
|
}
|
|
1316
1543
|
): SelectionSet {
|
|
1317
1544
|
const selections = this.selections();
|
|
1318
|
-
const updatedFragments = options?.fragments;
|
|
1319
|
-
const newFragments = updatedFragments === undefined ? this.fragments : (updatedFragments ?? undefined);
|
|
1320
|
-
|
|
1321
1545
|
let updatedSelections: SelectionSetUpdates | undefined = undefined;
|
|
1322
1546
|
for (let i = 0; i < selections.length; i++) {
|
|
1323
1547
|
const selection = selections[i];
|
|
@@ -1333,22 +1557,16 @@ export class SelectionSet {
|
|
|
1333
1557
|
}
|
|
1334
1558
|
}
|
|
1335
1559
|
if (!updatedSelections) {
|
|
1336
|
-
return this
|
|
1560
|
+
return this;
|
|
1337
1561
|
}
|
|
1338
|
-
return updatedSelections.toSelectionSet(options?.parentType ?? this.parentType
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
private withUpdatedFragments(newFragments: NamedFragments | undefined): SelectionSet {
|
|
1342
|
-
return this.fragments === newFragments ? this : new SelectionSet(this.parentType, this._keyedSelections, newFragments);
|
|
1562
|
+
return updatedSelections.toSelectionSet(options?.parentType ?? this.parentType);
|
|
1343
1563
|
}
|
|
1344
1564
|
|
|
1345
1565
|
withoutDefer(labelsToRemove?: Set<string>): SelectionSet {
|
|
1346
|
-
assert(!this.fragments, 'Not yet supported');
|
|
1347
1566
|
return this.lazyMap((selection) => selection.withoutDefer(labelsToRemove));
|
|
1348
1567
|
}
|
|
1349
1568
|
|
|
1350
1569
|
withNormalizedDefer(normalizer: DeferNormalizer): SelectionSet {
|
|
1351
|
-
assert(!this.fragments, 'Not yet supported');
|
|
1352
1570
|
return this.lazyMap((selection) => selection.withNormalizedDefer(normalizer));
|
|
1353
1571
|
}
|
|
1354
1572
|
|
|
@@ -1371,17 +1589,17 @@ export class SelectionSet {
|
|
|
1371
1589
|
return updated.isEmpty() ? undefined : updated;
|
|
1372
1590
|
}
|
|
1373
1591
|
|
|
1374
|
-
rebaseOn(parentType: CompositeType): SelectionSet {
|
|
1592
|
+
rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): SelectionSet {
|
|
1375
1593
|
if (this.parentType === parentType) {
|
|
1376
1594
|
return this;
|
|
1377
1595
|
}
|
|
1378
1596
|
|
|
1379
1597
|
const newSelections = new Map<string, Selection>();
|
|
1380
1598
|
for (const selection of this.selections()) {
|
|
1381
|
-
newSelections.set(selection.key(), selection.rebaseOn(parentType));
|
|
1599
|
+
newSelections.set(selection.key(), selection.rebaseOn(parentType, fragments));
|
|
1382
1600
|
}
|
|
1383
1601
|
|
|
1384
|
-
return new SelectionSet(parentType, newSelections
|
|
1602
|
+
return new SelectionSet(parentType, newSelections);
|
|
1385
1603
|
}
|
|
1386
1604
|
|
|
1387
1605
|
equals(that: SelectionSet): boolean {
|
|
@@ -1402,130 +1620,34 @@ export class SelectionSet {
|
|
|
1402
1620
|
return true;
|
|
1403
1621
|
}
|
|
1404
1622
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
if (selection.isUnecessaryInlineFragment(parentType)) {
|
|
1409
|
-
const selectionForKey = selection.selectionSet._keyedSelections.get(key);
|
|
1410
|
-
if (selectionForKey) {
|
|
1411
|
-
found.push(selectionForKey);
|
|
1412
|
-
}
|
|
1413
|
-
for (const nestedSelection of selection.selectionSet.triviallyNestedSelectionsForKey(parentType, key)) {
|
|
1414
|
-
found.push(nestedSelection);
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
return found;
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
private mergeSameKeySelections(selections: Selection[]): Selection | undefined {
|
|
1422
|
-
if (selections.length === 0) {
|
|
1423
|
-
return undefined;
|
|
1424
|
-
}
|
|
1425
|
-
const first = selections[0];
|
|
1426
|
-
// We know that all the selections passed are for exactly the same element (same "key"). So if it is a
|
|
1427
|
-
// leaf field or a named fragment, then we know that even if we have more than 1 selection, all of them
|
|
1428
|
-
// are the exact same and we can just return the first one. Only if we have a composite field or an
|
|
1429
|
-
// inline fragment do we need to merge the underlying sub-selection (which may differ).
|
|
1430
|
-
if (!first.selectionSet || (first instanceof FragmentSpreadSelection) || selections.length === 1) {
|
|
1431
|
-
return first;
|
|
1432
|
-
}
|
|
1433
|
-
const mergedSubselections = new SelectionSetUpdates();
|
|
1434
|
-
for (const selection of selections) {
|
|
1435
|
-
mergedSubselections.add(selection.selectionSet!);
|
|
1623
|
+
contains(that: SelectionSet): ContainsResult {
|
|
1624
|
+
if (that._selections.length > this._selections.length) {
|
|
1625
|
+
return ContainsResult.NOT_CONTAINED;
|
|
1436
1626
|
}
|
|
1437
1627
|
|
|
1438
|
-
|
|
1439
|
-
// and we want to return a single selection with the merged selections. There is a subtlety regarding
|
|
1440
|
-
// the parent type of that merged selection however: we cannot safely rely on the parent type of any
|
|
1441
|
-
// of the individual selections, because this can be incorrect. Let's illustrate.
|
|
1442
|
-
// Consider that we have:
|
|
1443
|
-
// ```graphql
|
|
1444
|
-
// type Query {
|
|
1445
|
-
// a: A!
|
|
1446
|
-
// }
|
|
1447
|
-
//
|
|
1448
|
-
// interface IA1 {
|
|
1449
|
-
// b: IB1!
|
|
1450
|
-
// }
|
|
1451
|
-
//
|
|
1452
|
-
// interface IA2 {
|
|
1453
|
-
// b: IB2!
|
|
1454
|
-
// }
|
|
1455
|
-
//
|
|
1456
|
-
// type A implements IA1 & IA2 {
|
|
1457
|
-
// b: B!
|
|
1458
|
-
// }
|
|
1459
|
-
//
|
|
1460
|
-
// interface IB1 {
|
|
1461
|
-
// v1: Int!
|
|
1462
|
-
// }
|
|
1463
|
-
//
|
|
1464
|
-
// interface IB2 {
|
|
1465
|
-
// v2: Int!
|
|
1466
|
-
// }
|
|
1467
|
-
//
|
|
1468
|
-
// type B implements IB1 & IB2 {
|
|
1469
|
-
// v1: Int!
|
|
1470
|
-
// v2: Int!
|
|
1471
|
-
// }
|
|
1472
|
-
// ```
|
|
1473
|
-
// and suppose that we're trying to check if selection set:
|
|
1474
|
-
// maybeSuperset = { ... on IA1 { b { v1 } } ... on IA2 { b { v2 } } } // (parent type A)
|
|
1475
|
-
// contains selection set:
|
|
1476
|
-
// maybeSubset = { b { v1 v2 } } // (parent type A)
|
|
1477
|
-
//
|
|
1478
|
-
// In that case, the `contains` method below will call this function with the 2 sub-selections
|
|
1479
|
-
// from `maybeSuperset`, but with the unecessary interface fragment removed (reminder that the
|
|
1480
|
-
// parent type is `A`, so the "casts" into the interfaces are semantically useless).
|
|
1481
|
-
//
|
|
1482
|
-
// And so in that case, the argument to this method will be:
|
|
1483
|
-
// [ b { v1 } (parent type IA1), b { v2 } (parent type IA2) ]
|
|
1484
|
-
// but then, the sub-selection `{ v1 }` of the 1st value will have parent type IB1,
|
|
1485
|
-
// and the sub-selection `{ v2 }` of the 2nd value will have parent type IB2,
|
|
1486
|
-
// neither of which work for the merge sub-selection.
|
|
1487
|
-
//
|
|
1488
|
-
// Instead, we want to use as parent type the type of field `b` the parent type of `this`
|
|
1489
|
-
// (which is `maybeSupeset` in our example). Which means that we want to use type `B` for
|
|
1490
|
-
// the sub-selection, which is now guaranteed to work (or `maybeSupergerset` wouldn't have
|
|
1491
|
-
// been valid).
|
|
1492
|
-
//
|
|
1493
|
-
// Long story short, we get that type by rebasing any of the selection element (we use the
|
|
1494
|
-
// first as we have it) on `this.parentType`, which gives use the element we want, and we
|
|
1495
|
-
// use the type of that for the sub-selection.
|
|
1496
|
-
|
|
1497
|
-
if (first.kind === 'FieldSelection') {
|
|
1498
|
-
const rebasedField = first.element.rebaseOn(this.parentType);
|
|
1499
|
-
return new FieldSelection(rebasedField, mergedSubselections.toSelectionSet(rebasedField.baseType() as CompositeType));
|
|
1500
|
-
} else {
|
|
1501
|
-
const rebasedFragment = first.element.rebaseOn(this.parentType);
|
|
1502
|
-
return new InlineFragmentSelection(rebasedFragment, mergedSubselections.toSelectionSet(rebasedFragment.castedType()));
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
contains(that: SelectionSet): boolean {
|
|
1507
|
-
// Note that we cannot really rely on the number of selections in `this` and `that` to short-cut this method
|
|
1508
|
-
// due to the handling of "trivially nested selections". That is, `this` might have less top-level selections
|
|
1509
|
-
// than `that`, and yet contains a named fragment directly on the parent type that includes everything in `that`.
|
|
1510
|
-
|
|
1628
|
+
let isEqual = true;
|
|
1511
1629
|
for (const [key, thatSelection] of that._keyedSelections) {
|
|
1512
1630
|
const thisSelection = this._keyedSelections.get(key);
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
if (!(mergedSelection && mergedSelection.contains(thatSelection))
|
|
1517
|
-
&& !(thatSelection.isUnecessaryInlineFragment(this.parentType) && this.contains(thatSelection.selectionSet))
|
|
1518
|
-
) {
|
|
1519
|
-
return false
|
|
1631
|
+
const selectionResult = thisSelection?.contains(thatSelection);
|
|
1632
|
+
if (selectionResult === undefined || selectionResult === ContainsResult.NOT_CONTAINED) {
|
|
1633
|
+
return ContainsResult.NOT_CONTAINED;
|
|
1520
1634
|
}
|
|
1635
|
+
isEqual &&= selectionResult === ContainsResult.EQUAL;
|
|
1521
1636
|
}
|
|
1522
|
-
|
|
1637
|
+
|
|
1638
|
+
return isEqual && that._selections.length === this._selections.length
|
|
1639
|
+
? ContainsResult.EQUAL
|
|
1640
|
+
: ContainsResult.STRICTLY_CONTAINED;
|
|
1523
1641
|
}
|
|
1524
1642
|
|
|
1525
1643
|
// Please note that this method assumes that `candidate.canApplyAtType(parentType) === true` but it is left to the caller to
|
|
1526
1644
|
// validate this (`canApplyAtType` is not free, and we want to avoid repeating it multiple times).
|
|
1527
|
-
diffWithNamedFragmentIfContained(
|
|
1528
|
-
|
|
1645
|
+
diffWithNamedFragmentIfContained(
|
|
1646
|
+
candidate: NamedFragmentDefinition,
|
|
1647
|
+
parentType: CompositeType,
|
|
1648
|
+
fragments: NamedFragments,
|
|
1649
|
+
): { contains: boolean, diff?: SelectionSet } {
|
|
1650
|
+
const that = candidate.expandedSelectionSetAtType(parentType);
|
|
1529
1651
|
// It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
|
|
1530
1652
|
// `parentType` is empty, or contains only `__typename`. For instance, suppose we have
|
|
1531
1653
|
// a union `U = A | B | C`, and then a fragment:
|
|
@@ -1553,7 +1675,7 @@ export class SelectionSet {
|
|
|
1553
1675
|
// usually ok because `candidate` will also use those fragments, but one fragments that `candidate` can never be
|
|
1554
1676
|
// using is itself (the `contains` check is fine with this, but it's harder to deal in `minus`). So we expand
|
|
1555
1677
|
// the candidate we're currently looking at in "this" to avoid some issues.
|
|
1556
|
-
let updatedThis = this.expandFragments(
|
|
1678
|
+
let updatedThis = this.expandFragments(fragments.filter((f) => f.name !== candidate.name));
|
|
1557
1679
|
if (updatedThis !== this) {
|
|
1558
1680
|
updatedThis = updatedThis.trimUnsatisfiableBranches(parentType);
|
|
1559
1681
|
}
|
|
@@ -1572,29 +1694,16 @@ export class SelectionSet {
|
|
|
1572
1694
|
|
|
1573
1695
|
for (const [key, thisSelection] of this._keyedSelections) {
|
|
1574
1696
|
const thatSelection = that._keyedSelections.get(key);
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
// fragment and see if that result is entirely covered by `that`. If that is the case, then it means
|
|
1580
|
-
// `thisSelection` does not need to be in the returned "diff". If it's not entirely covered,
|
|
1581
|
-
// we just add the spread itself to the diff: even if some parts of it were covered by `that`,
|
|
1582
|
-
// keeping just the fragment is, in a sense, more condensed.
|
|
1583
|
-
if (thisSelection instanceof FragmentSpreadSelection) {
|
|
1584
|
-
const expanded = thisSelection.selectionSet.expandAllFragments().trimUnsatisfiableBranches(this.parentType);
|
|
1585
|
-
if (expanded.minus(that).isEmpty()) {
|
|
1586
|
-
continue;
|
|
1587
|
-
}
|
|
1697
|
+
if (thatSelection) {
|
|
1698
|
+
const remainder = thisSelection.minus(thatSelection);
|
|
1699
|
+
if (remainder) {
|
|
1700
|
+
updated.add(remainder);
|
|
1588
1701
|
}
|
|
1589
|
-
updated.add(thisSelection);
|
|
1590
1702
|
} else {
|
|
1591
|
-
|
|
1592
|
-
if (selectionDiff) {
|
|
1593
|
-
updated.add(selectionDiff);
|
|
1594
|
-
}
|
|
1703
|
+
updated.add(thisSelection);
|
|
1595
1704
|
}
|
|
1596
1705
|
}
|
|
1597
|
-
return updated.toSelectionSet(this.parentType
|
|
1706
|
+
return updated.toSelectionSet(this.parentType);
|
|
1598
1707
|
}
|
|
1599
1708
|
|
|
1600
1709
|
canRebaseOn(parentTypeToTest: CompositeType): boolean {
|
|
@@ -1693,13 +1802,14 @@ export class SelectionSet {
|
|
|
1693
1802
|
toOperationString(
|
|
1694
1803
|
rootKind: SchemaRootKind,
|
|
1695
1804
|
variableDefinitions: VariableDefinitions,
|
|
1805
|
+
fragments: NamedFragments | undefined,
|
|
1696
1806
|
operationName?: string,
|
|
1697
1807
|
expandFragments: boolean = false,
|
|
1698
1808
|
prettyPrint: boolean = true
|
|
1699
1809
|
): string {
|
|
1700
1810
|
const indent = prettyPrint ? '' : undefined;
|
|
1701
|
-
const fragmentsDefinitions = !expandFragments &&
|
|
1702
|
-
?
|
|
1811
|
+
const fragmentsDefinitions = !expandFragments && fragments && !fragments.isEmpty()
|
|
1812
|
+
? fragments.toString(indent) + "\n\n"
|
|
1703
1813
|
: "";
|
|
1704
1814
|
if (rootKind == "query" && !operationName && variableDefinitions.isEmpty()) {
|
|
1705
1815
|
return fragmentsDefinitions + this.toString(expandFragments, true, indent);
|
|
@@ -1889,7 +1999,7 @@ function makeSelection(parentType: CompositeType, updates: SelectionUpdate[], fr
|
|
|
1889
1999
|
|
|
1890
2000
|
// Optimize for the simple case of a single selection, as we don't have to do anything complex to merge the sub-selections.
|
|
1891
2001
|
if (updates.length === 1 && first instanceof AbstractSelection) {
|
|
1892
|
-
return first.rebaseOn(parentType);
|
|
2002
|
+
return first.rebaseOn(parentType, fragments);
|
|
1893
2003
|
}
|
|
1894
2004
|
|
|
1895
2005
|
const element = updateElement(first).rebaseOn(parentType);
|
|
@@ -1936,7 +2046,7 @@ function makeSelectionSet(parentType: CompositeType, keyedUpdates: MultiMap<stri
|
|
|
1936
2046
|
for (const [key, updates] of keyedUpdates.entries()) {
|
|
1937
2047
|
selections.set(key, makeSelection(parentType, updates, fragments));
|
|
1938
2048
|
}
|
|
1939
|
-
return new SelectionSet(parentType, selections
|
|
2049
|
+
return new SelectionSet(parentType, selections);
|
|
1940
2050
|
}
|
|
1941
2051
|
|
|
1942
2052
|
/**
|
|
@@ -2046,14 +2156,14 @@ export function allFieldDefinitionsInSelectionSet(selection: SelectionSet): Fiel
|
|
|
2046
2156
|
return allFields;
|
|
2047
2157
|
}
|
|
2048
2158
|
|
|
2049
|
-
export function selectionSetOf(parentType: CompositeType, selection: Selection
|
|
2159
|
+
export function selectionSetOf(parentType: CompositeType, selection: Selection): SelectionSet {
|
|
2050
2160
|
const map = new Map<string, Selection>()
|
|
2051
2161
|
map.set(selection.key(), selection);
|
|
2052
|
-
return new SelectionSet(parentType, map
|
|
2162
|
+
return new SelectionSet(parentType, map);
|
|
2053
2163
|
}
|
|
2054
2164
|
|
|
2055
|
-
export function selectionSetOfElement(element: OperationElement, subSelection?: SelectionSet
|
|
2056
|
-
return selectionSetOf(element.parentType, selectionOfElement(element, subSelection)
|
|
2165
|
+
export function selectionSetOfElement(element: OperationElement, subSelection?: SelectionSet): SelectionSet {
|
|
2166
|
+
return selectionSetOf(element.parentType, selectionOfElement(element, subSelection));
|
|
2057
2167
|
}
|
|
2058
2168
|
|
|
2059
2169
|
export function selectionOfElement(element: OperationElement, subSelection?: SelectionSet): Selection {
|
|
@@ -2081,7 +2191,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2081
2191
|
|
|
2082
2192
|
abstract validate(variableDefinitions: VariableDefinitions): void;
|
|
2083
2193
|
|
|
2084
|
-
abstract rebaseOn(parentType: CompositeType): TOwnType;
|
|
2194
|
+
abstract rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): TOwnType;
|
|
2085
2195
|
|
|
2086
2196
|
get parentType(): CompositeType {
|
|
2087
2197
|
return this.element.parentType;
|
|
@@ -2101,10 +2211,6 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2101
2211
|
this.selectionSet?.collectUsedFragmentNames(collector);
|
|
2102
2212
|
}
|
|
2103
2213
|
|
|
2104
|
-
namedFragments(): NamedFragments | undefined {
|
|
2105
|
-
return this.selectionSet?.fragments;
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
2214
|
abstract withUpdatedComponents(element: TElement, selectionSet: SelectionSet | TIsLeaf): TOwnType;
|
|
2109
2215
|
|
|
2110
2216
|
withUpdatedSelectionSet(selectionSet: SelectionSet | TIsLeaf): TOwnType {
|
|
@@ -2132,12 +2238,14 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2132
2238
|
|
|
2133
2239
|
abstract hasDefer(): boolean;
|
|
2134
2240
|
|
|
2135
|
-
abstract
|
|
2136
|
-
|
|
2137
|
-
abstract expandFragments(names: string[], updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
|
|
2241
|
+
abstract expandFragments(updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
|
|
2138
2242
|
|
|
2139
2243
|
abstract trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): TOwnType | SelectionSet | undefined;
|
|
2140
2244
|
|
|
2245
|
+
isFragmentSpread(): boolean {
|
|
2246
|
+
return false;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2141
2249
|
minus(that: Selection): TOwnType | undefined {
|
|
2142
2250
|
// If there is a subset, then we compute the diff of the subset and add that (if not empty).
|
|
2143
2251
|
// Otherwise, we have no diff.
|
|
@@ -2150,53 +2258,131 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2150
2258
|
return undefined;
|
|
2151
2259
|
}
|
|
2152
2260
|
|
|
2153
|
-
// Attempts to optimize the subselection of this field selection using named fragments `candidates` _assuming_ that
|
|
2154
|
-
// those candidates do apply at `parentType` (that is, `candidates.every((c) => c.canApplyAtType(parentType))` is true,
|
|
2155
|
-
// which is ensured by the fact that `tryOptimizeSubselectionWithFragments` calls this on a subset of the candidates
|
|
2156
|
-
// returned by `maybeApplyingAtType`).
|
|
2157
|
-
protected tryOptimizeSubselectionOnce(_: {
|
|
2158
|
-
parentType: CompositeType,
|
|
2159
|
-
subSelection: SelectionSet,
|
|
2160
|
-
candidates: NamedFragmentDefinition[],
|
|
2161
|
-
fragments: NamedFragments,
|
|
2162
|
-
}): {
|
|
2163
|
-
spread?: FragmentSpreadSelection,
|
|
2164
|
-
optimizedSelection?: SelectionSet,
|
|
2165
|
-
hasDiff?: boolean,
|
|
2166
|
-
} {
|
|
2167
|
-
// Field and inline fragment override this, but this should never be called for a spread.
|
|
2168
|
-
assert(false, `UNSUPPORTED`);
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
2261
|
protected tryOptimizeSubselectionWithFragments({
|
|
2172
2262
|
parentType,
|
|
2173
2263
|
subSelection,
|
|
2174
2264
|
fragments,
|
|
2175
|
-
|
|
2265
|
+
canUseFullMatchingFragment,
|
|
2176
2266
|
}: {
|
|
2177
2267
|
parentType: CompositeType,
|
|
2178
2268
|
subSelection: SelectionSet,
|
|
2179
2269
|
fragments: NamedFragments,
|
|
2180
|
-
|
|
2181
|
-
}): SelectionSet |
|
|
2270
|
+
canUseFullMatchingFragment: (match: NamedFragmentDefinition) => boolean,
|
|
2271
|
+
}): SelectionSet | NamedFragmentDefinition {
|
|
2182
2272
|
let candidates = fragments.maybeApplyingAtType(parentType);
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2273
|
+
|
|
2274
|
+
// First, we check which of the candidates do apply inside `subSelection`, if any.
|
|
2275
|
+
// If we find a candidate that applies to the whole `subSelection`, then we stop and only return
|
|
2276
|
+
// that one candidate. Otherwise, we cumulate in `applyingFragments` the list of fragments that
|
|
2277
|
+
// applies to a subset of `subSelection`.
|
|
2278
|
+
const applyingFragments: NamedFragmentDefinition[] = [];
|
|
2279
|
+
for (const candidate of candidates) {
|
|
2280
|
+
const fragmentSSet = candidate.expandedSelectionSetAtType(parentType);
|
|
2281
|
+
// It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
|
|
2282
|
+
// `parentType` is empty, or contains only `__typename`. For instance, suppose we have
|
|
2283
|
+
// a union `U = A | B | C`, and then a fragment:
|
|
2284
|
+
// ```graphql
|
|
2285
|
+
// fragment F on U {
|
|
2286
|
+
// ... on A {
|
|
2287
|
+
// x
|
|
2288
|
+
// }
|
|
2289
|
+
// ... on b {
|
|
2290
|
+
// y
|
|
2291
|
+
// }
|
|
2292
|
+
// }
|
|
2293
|
+
// ```
|
|
2294
|
+
// It is then possible to apply `F` when the parent type is `C`, but this ends up selecting
|
|
2295
|
+
// nothing at all.
|
|
2296
|
+
//
|
|
2297
|
+
// Using `F` in those cases is, while not 100% incorrect, at least not productive, and so we
|
|
2298
|
+
// skip it that case. This is essentially an optimisation.
|
|
2299
|
+
if (fragmentSSet.isEmpty() || (fragmentSSet.selections().length === 1 && fragmentSSet.selections()[0].isTypenameField())) {
|
|
2300
|
+
continue;
|
|
2193
2301
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2302
|
+
|
|
2303
|
+
const res = subSelection.contains(fragmentSSet);
|
|
2304
|
+
|
|
2305
|
+
if (res === ContainsResult.EQUAL) {
|
|
2306
|
+
if (canUseFullMatchingFragment(candidate)) {
|
|
2307
|
+
return candidate;
|
|
2308
|
+
}
|
|
2309
|
+
// If we're not going to replace the full thing, then same reasoning a below.
|
|
2310
|
+
if (candidate.appliedDirectives.length === 0) {
|
|
2311
|
+
applyingFragments.push(candidate);
|
|
2312
|
+
}
|
|
2313
|
+
// Note that if a fragment applies to only a subset of the subSelection, then we really only can use
|
|
2314
|
+
// it if that fragment is defined _without_ directives.
|
|
2315
|
+
} else if (res === ContainsResult.STRICTLY_CONTAINED && candidate.appliedDirectives.length === 0) {
|
|
2316
|
+
applyingFragments.push(candidate);
|
|
2197
2317
|
}
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
if (applyingFragments.length === 0) {
|
|
2321
|
+
return subSelection;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// We have found the list of fragments that applies to some subset of `subSelection`. In general, we
|
|
2325
|
+
// want ot now produce the selection set with spread for those fragments plus any selection that is not
|
|
2326
|
+
// covered by any of the fragments. For instance, suppose that `subselection` is `{ a b c d e }`
|
|
2327
|
+
// and we have found that `fragment F1 on X { a b c }` and `fragment F2 on X { c d }` applies, then
|
|
2328
|
+
// we will generate `{ ...F1 ...F2 e }`.
|
|
2329
|
+
//
|
|
2330
|
+
// In that example, `c` is covered by both fragments. And this is fine in this example as it is
|
|
2331
|
+
// worth using both fragments in general. A special case of this however is if a fragment is entirely
|
|
2332
|
+
// included into another. That is, consider that we now have `fragment F1 on X { a ...F2 }` and
|
|
2333
|
+
// `fragment F2 on X { b c }`. In that case, the code above would still match both `F1 and `F2`,
|
|
2334
|
+
// but as `F1` includes `F2` already, we really want to only use `F1`. So in practice, we filter
|
|
2335
|
+
// away any fragment spread that is known to be included in another one that applies.
|
|
2336
|
+
//
|
|
2337
|
+
// TODO: note that the logic used for this is theoretically a bit sub-optimial. That is, we only
|
|
2338
|
+
// check if one of the fragment happens to directly include a spread for another fragment at
|
|
2339
|
+
// top-level as in the example above. We do this because it is cheap to check and is likely the
|
|
2340
|
+
// most common case of this kind of inclusion. But in theory, we would have
|
|
2341
|
+
// `fragment F1 on X { a b c }` and `fragment F2 on X { b c }`, in which case `F2` is still
|
|
2342
|
+
// included in `F1`, but we'd have to work harder to figure this out and it's unclear it's
|
|
2343
|
+
// a good tradeoff. And while you could argue that it's on the user to define its fragments
|
|
2344
|
+
// a bit more optimally, it's actually a tad more complex because we're looking at fragments
|
|
2345
|
+
// in a particular context/parent type. Consider an interface `I` and:
|
|
2346
|
+
// ```graphql
|
|
2347
|
+
// fragment F3 on I {
|
|
2348
|
+
// ... on X {
|
|
2349
|
+
// a
|
|
2350
|
+
// }
|
|
2351
|
+
// ... on Y {
|
|
2352
|
+
// b
|
|
2353
|
+
// c
|
|
2354
|
+
// }
|
|
2355
|
+
// }
|
|
2356
|
+
//
|
|
2357
|
+
// fragment F4 on I {
|
|
2358
|
+
// ... on Y {
|
|
2359
|
+
// c
|
|
2360
|
+
// }
|
|
2361
|
+
// ... on Z {
|
|
2362
|
+
// d
|
|
2363
|
+
// }
|
|
2364
|
+
// }
|
|
2365
|
+
// ```
|
|
2366
|
+
// In that case, neither fragment include the other per-se. But what if we have sub-selection
|
|
2367
|
+
// `{ b c }` but where parent type is `Y`. In that case, both `F3` and `F4` applies, and in that
|
|
2368
|
+
// particular context, `F3` is fully included in `F4`. Long story short, we'll currently
|
|
2369
|
+
// return `{ ...F3 ...F4 }` in that case, but it would be technically better to return only `F4`.
|
|
2370
|
+
// However, this feels niche, and it might be costly to verify such inclusions, so not doing it
|
|
2371
|
+
// for now.
|
|
2372
|
+
const filteredApplyingFragments = applyingFragments.filter((f) => !applyingFragments.some((o) => o.includes(f.name)));
|
|
2373
|
+
|
|
2374
|
+
let notCoveredByFragments = subSelection;
|
|
2375
|
+
const optimized = new SelectionSetUpdates();
|
|
2376
|
+
// TODO: doing repeated calls to `minus` for every fragment is simple, but a `minusAll` method that
|
|
2377
|
+
// takes the fragment selections at once would be more efficient in pratice.
|
|
2378
|
+
for (const fragment of filteredApplyingFragments) {
|
|
2379
|
+
// Note: we call `expandedSelectionSetAType` twice in this method for the applying fragments, but
|
|
2380
|
+
// we know it's cached so rely on that fact.
|
|
2381
|
+
notCoveredByFragments = notCoveredByFragments.minus(fragment.expandedSelectionSetAtType(parentType));
|
|
2382
|
+
optimized.add(new FragmentSpreadSelection(parentType, fragments, fragment, []));
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
return optimized.add(notCoveredByFragments).toSelectionSet(parentType, fragments)
|
|
2200
2386
|
}
|
|
2201
2387
|
}
|
|
2202
2388
|
|
|
@@ -2231,57 +2417,36 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2231
2417
|
}
|
|
2232
2418
|
|
|
2233
2419
|
optimize(fragments: NamedFragments): Selection {
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2420
|
+
const fieldBaseType = baseType(this.element.definition.type!);
|
|
2421
|
+
if (!isCompositeType(fieldBaseType) || !this.selectionSet) {
|
|
2422
|
+
return this;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
// First, see if we can reuse fragments for the selection of this field.
|
|
2426
|
+
let optimizedSelection = this.selectionSet;
|
|
2427
|
+
if (isCompositeType(fieldBaseType) && this.selectionSet) {
|
|
2237
2428
|
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
2238
2429
|
parentType: fieldBaseType,
|
|
2239
|
-
subSelection:
|
|
2430
|
+
subSelection: this.selectionSet,
|
|
2240
2431
|
fragments,
|
|
2241
|
-
// We can never apply a fragments that has directives on it at the field level
|
|
2242
|
-
|
|
2243
|
-
// be handled by `InlineFragmentSelection.optimize` anyway).
|
|
2244
|
-
fragmentFilter: (f) => f.appliedDirectives.length === 0,
|
|
2432
|
+
// We can never apply a fragments that has directives on it at the field level.
|
|
2433
|
+
canUseFullMatchingFragment: (fragment) => fragment.appliedDirectives.length === 0,
|
|
2245
2434
|
});
|
|
2246
2435
|
|
|
2247
|
-
|
|
2248
|
-
|
|
2436
|
+
if (optimized instanceof NamedFragmentDefinition) {
|
|
2437
|
+
optimizedSelection = selectionSetOf(fieldBaseType, new FragmentSpreadSelection(fieldBaseType, fragments, optimized, []));
|
|
2438
|
+
} else {
|
|
2439
|
+
optimizedSelection = optimized;
|
|
2440
|
+
}
|
|
2249
2441
|
}
|
|
2250
2442
|
|
|
2443
|
+
// Then, recurse inside the field sub-selection (note that if we matched some fragments above,
|
|
2444
|
+
// this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
|
|
2445
|
+
optimizedSelection = optimizedSelection.optimize(fragments);
|
|
2446
|
+
|
|
2251
2447
|
return this.selectionSet === optimizedSelection
|
|
2252
2448
|
? this
|
|
2253
|
-
:
|
|
2254
|
-
}
|
|
2255
|
-
|
|
2256
|
-
protected tryOptimizeSubselectionOnce({
|
|
2257
|
-
parentType,
|
|
2258
|
-
subSelection,
|
|
2259
|
-
candidates,
|
|
2260
|
-
fragments,
|
|
2261
|
-
}: {
|
|
2262
|
-
parentType: CompositeType,
|
|
2263
|
-
subSelection: SelectionSet,
|
|
2264
|
-
candidates: NamedFragmentDefinition[],
|
|
2265
|
-
fragments: NamedFragments,
|
|
2266
|
-
}): {
|
|
2267
|
-
spread?: FragmentSpreadSelection,
|
|
2268
|
-
optimizedSelection?: SelectionSet,
|
|
2269
|
-
hasDiff?: boolean,
|
|
2270
|
-
}{
|
|
2271
|
-
let optimizedSelection = subSelection;
|
|
2272
|
-
for (const candidate of candidates) {
|
|
2273
|
-
const { contains, diff } = optimizedSelection.diffWithNamedFragmentIfContained(candidate, parentType);
|
|
2274
|
-
if (contains) {
|
|
2275
|
-
// We can optimize the selection with this fragment. The replaced sub-selection will be
|
|
2276
|
-
// comprised of this new spread and the remaining `diff` if there is any.
|
|
2277
|
-
const spread = new FragmentSpreadSelection(parentType, fragments, candidate, []);
|
|
2278
|
-
optimizedSelection = diff
|
|
2279
|
-
? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
|
|
2280
|
-
: selectionSetOf(parentType, spread);
|
|
2281
|
-
return { spread, optimizedSelection, hasDiff: !!diff }
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
return {};
|
|
2449
|
+
: this.withUpdatedSelectionSet(optimizedSelection);
|
|
2285
2450
|
}
|
|
2286
2451
|
|
|
2287
2452
|
filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
|
|
@@ -2315,7 +2480,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2315
2480
|
* Obviously, this operation will only succeed if this selection (both the field itself and its subselections)
|
|
2316
2481
|
* make sense from the provided parent type. If this is not the case, this method will throw.
|
|
2317
2482
|
*/
|
|
2318
|
-
rebaseOn(parentType: CompositeType): FieldSelection {
|
|
2483
|
+
rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FieldSelection {
|
|
2319
2484
|
if (this.element.parentType === parentType) {
|
|
2320
2485
|
return this;
|
|
2321
2486
|
}
|
|
@@ -2331,7 +2496,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2331
2496
|
}
|
|
2332
2497
|
|
|
2333
2498
|
validate(isCompositeType(rebasedBase), () => `Cannot rebase field selection ${this} on ${parentType}: rebased field base return type ${rebasedBase} is not composite`);
|
|
2334
|
-
return this.withUpdatedComponents(rebasedElement, this.selectionSet.rebaseOn(rebasedBase));
|
|
2499
|
+
return this.withUpdatedComponents(rebasedElement, this.selectionSet.rebaseOn(rebasedBase, fragments));
|
|
2335
2500
|
}
|
|
2336
2501
|
|
|
2337
2502
|
/**
|
|
@@ -2382,10 +2547,6 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2382
2547
|
return !!this.selectionSet?.hasDefer();
|
|
2383
2548
|
}
|
|
2384
2549
|
|
|
2385
|
-
expandAllFragments(): FieldSelection {
|
|
2386
|
-
return this.mapToSelectionSet((s) => s.expandAllFragments());
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
2550
|
trimUnsatisfiableBranches(_: CompositeType, options?: { recursive? : boolean }): FieldSelection {
|
|
2390
2551
|
if (!this.selectionSet) {
|
|
2391
2552
|
return this;
|
|
@@ -2411,8 +2572,8 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2411
2572
|
}
|
|
2412
2573
|
}
|
|
2413
2574
|
|
|
2414
|
-
expandFragments(
|
|
2415
|
-
return this.mapToSelectionSet((s) => s.expandFragments(
|
|
2575
|
+
expandFragments(updatedFragments?: NamedFragments): FieldSelection {
|
|
2576
|
+
return this.mapToSelectionSet((s) => s.expandFragments(updatedFragments));
|
|
2416
2577
|
}
|
|
2417
2578
|
|
|
2418
2579
|
equals(that: Selection): boolean {
|
|
@@ -2429,20 +2590,17 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2429
2590
|
return !!that.selectionSet && this.selectionSet.equals(that.selectionSet);
|
|
2430
2591
|
}
|
|
2431
2592
|
|
|
2432
|
-
contains(that: Selection):
|
|
2593
|
+
contains(that: Selection): ContainsResult {
|
|
2433
2594
|
if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
|
|
2434
|
-
return
|
|
2595
|
+
return ContainsResult.NOT_CONTAINED;
|
|
2435
2596
|
}
|
|
2436
2597
|
|
|
2437
|
-
if (!
|
|
2438
|
-
|
|
2598
|
+
if (!this.selectionSet) {
|
|
2599
|
+
assert(!that.selectionSet, '`this` and `that` have the same element, so if one does not have a sub-selection, neither should the other one')
|
|
2600
|
+
return ContainsResult.EQUAL;
|
|
2439
2601
|
}
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
isUnecessaryInlineFragment(_: CompositeType): this is InlineFragmentSelection {
|
|
2444
|
-
// Overridden by inline fragments
|
|
2445
|
-
return false;
|
|
2602
|
+
assert(that.selectionSet, '`this` and `that` have the same element, so if one has sub-selection, the other one should too')
|
|
2603
|
+
return this.selectionSet.contains(that.selectionSet);
|
|
2446
2604
|
}
|
|
2447
2605
|
|
|
2448
2606
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
@@ -2487,16 +2645,7 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
|
|
|
2487
2645
|
|
|
2488
2646
|
abstract equals(that: Selection): boolean;
|
|
2489
2647
|
|
|
2490
|
-
abstract contains(that: Selection):
|
|
2491
|
-
|
|
2492
|
-
isUnecessaryInlineFragment(parentType: CompositeType): boolean {
|
|
2493
|
-
return this.element.appliedDirectives.length === 0
|
|
2494
|
-
&& !!this.element.typeCondition
|
|
2495
|
-
&& (
|
|
2496
|
-
this.element.typeCondition.name === parentType.name
|
|
2497
|
-
|| (isObjectType(parentType) && possibleRuntimeTypes(this.element.typeCondition).some((t) => t.name === parentType.name))
|
|
2498
|
-
);
|
|
2499
|
-
}
|
|
2648
|
+
abstract contains(that: Selection): ContainsResult;
|
|
2500
2649
|
}
|
|
2501
2650
|
|
|
2502
2651
|
class InlineFragmentSelection extends FragmentSelection {
|
|
@@ -2530,7 +2679,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2530
2679
|
this.selectionSet.validate(variableDefinitions);
|
|
2531
2680
|
}
|
|
2532
2681
|
|
|
2533
|
-
rebaseOn(parentType: CompositeType): FragmentSelection {
|
|
2682
|
+
rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
|
|
2534
2683
|
if (this.parentType === parentType) {
|
|
2535
2684
|
return this;
|
|
2536
2685
|
}
|
|
@@ -2541,7 +2690,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2541
2690
|
return this.withUpdatedElement(rebasedFragment);
|
|
2542
2691
|
}
|
|
2543
2692
|
|
|
2544
|
-
return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType));
|
|
2693
|
+
return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType, fragments));
|
|
2545
2694
|
}
|
|
2546
2695
|
|
|
2547
2696
|
canAddTo(parentType: CompositeType): boolean {
|
|
@@ -2579,86 +2728,60 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2579
2728
|
}
|
|
2580
2729
|
|
|
2581
2730
|
optimize(fragments: NamedFragments): FragmentSelection {
|
|
2582
|
-
let optimizedSelection = this.selectionSet
|
|
2731
|
+
let optimizedSelection = this.selectionSet;
|
|
2732
|
+
|
|
2733
|
+
// First, see if we can reuse fragments for the selection of this field.
|
|
2583
2734
|
const typeCondition = this.element.typeCondition;
|
|
2584
2735
|
if (typeCondition) {
|
|
2585
2736
|
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
2586
2737
|
parentType: typeCondition,
|
|
2587
2738
|
subSelection: optimizedSelection,
|
|
2588
2739
|
fragments,
|
|
2740
|
+
canUseFullMatchingFragment: (fragment) => {
|
|
2741
|
+
// To be able to use a matching fragment, it needs to have either no directives, or if it has
|
|
2742
|
+
// some, then:
|
|
2743
|
+
// 1. all it's directives should also be on the current element.
|
|
2744
|
+
// 2. the directives of this element should be the fragment condition.
|
|
2745
|
+
// because if those 2 conditions are true, we cna replace the whole current inline fragment
|
|
2746
|
+
// with the match spread and directives will still match.
|
|
2747
|
+
return fragment.appliedDirectives.length === 0
|
|
2748
|
+
|| (
|
|
2749
|
+
sameType(typeCondition, fragment.typeCondition)
|
|
2750
|
+
&& fragment.appliedDirectives.every((d) => this.element.appliedDirectives.some((s) => sameDirectiveApplication(d, s)))
|
|
2751
|
+
);
|
|
2752
|
+
},
|
|
2589
2753
|
});
|
|
2590
|
-
if (optimized instanceof FragmentSpreadSelection) {
|
|
2591
|
-
// This means the whole inline fragment can be replaced by the spread.
|
|
2592
|
-
return optimized;
|
|
2593
|
-
}
|
|
2594
|
-
optimizedSelection = optimized;
|
|
2595
|
-
}
|
|
2596
|
-
return this.selectionSet === optimizedSelection
|
|
2597
|
-
? this
|
|
2598
|
-
: new InlineFragmentSelection(this.element, optimizedSelection);
|
|
2599
|
-
}
|
|
2600
2754
|
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
optimizedSelection?: SelectionSet,
|
|
2614
|
-
hasDiff?: boolean,
|
|
2615
|
-
}{
|
|
2616
|
-
let optimizedSelection = subSelection;
|
|
2617
|
-
for (const candidate of candidates) {
|
|
2618
|
-
const { contains, diff } = optimizedSelection.diffWithNamedFragmentIfContained(candidate, parentType);
|
|
2619
|
-
if (contains) {
|
|
2620
|
-
// The candidate selection is included in our sub-selection. One remaining thing to take into account
|
|
2621
|
-
// is applied directives: if the candidate has directives, then we can only use it if 1) there is
|
|
2622
|
-
// no `diff`, 2) the type condition of this fragment matches the candidate one and 3) the directives
|
|
2623
|
-
// in question are also on this very fragment. In that case, we can replace this whole inline fragment
|
|
2624
|
-
// by a spread of the candidate.
|
|
2625
|
-
if (!diff && sameType(this.element.typeCondition!, candidate.typeCondition)) {
|
|
2626
|
-
// We can potentially replace the whole fragment by the candidate; but as said above, still needs
|
|
2627
|
-
// to check the directives.
|
|
2628
|
-
let spreadDirectives: Directive<any>[] = this.element.appliedDirectives;
|
|
2629
|
-
if (candidate.appliedDirectives.length > 0) {
|
|
2630
|
-
const { isSubset, difference } = diffDirectives(this.element.appliedDirectives, candidate.appliedDirectives);
|
|
2631
|
-
if (!isSubset) {
|
|
2632
|
-
// While the candidate otherwise match, it has directives that are not on this element, so we
|
|
2633
|
-
// cannot reuse it.
|
|
2634
|
-
continue;
|
|
2635
|
-
}
|
|
2636
|
-
// Otherwise, any directives on this element that are not on the candidate should be kept and used
|
|
2637
|
-
// on the spread created.
|
|
2638
|
-
spreadDirectives = difference;
|
|
2755
|
+
if (optimized instanceof NamedFragmentDefinition) {
|
|
2756
|
+
// We're fully matching the sub-selection. If the fragment condition is also this element condition,
|
|
2757
|
+
// then we can replace the whole element by the spread (not just the sub-selection).
|
|
2758
|
+
if (sameType(typeCondition, optimized.typeCondition)) {
|
|
2759
|
+
// Note that `canUseFullMatchingFragment` above guarantees us that this element directives
|
|
2760
|
+
// are a superset of the fragment directives. But there can be additional directives, and in
|
|
2761
|
+
// that case they should be kept on the spread.
|
|
2762
|
+
let spreadDirectives = this.element.appliedDirectives;
|
|
2763
|
+
if (optimized.appliedDirectives) {
|
|
2764
|
+
spreadDirectives = spreadDirectives.filter(
|
|
2765
|
+
(s) => !optimized.appliedDirectives.some((d) => sameDirectiveApplication(d, s))
|
|
2766
|
+
);
|
|
2639
2767
|
}
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
// we
|
|
2643
|
-
|
|
2644
|
-
spread: new FragmentSpreadSelection(this.parentType, fragments, candidate, spreadDirectives),
|
|
2645
|
-
};
|
|
2646
|
-
}
|
|
2647
|
-
|
|
2648
|
-
// We're already dealt with the one case where we might be able to handle a candidate that has directives.
|
|
2649
|
-
if (candidate.appliedDirectives.length > 0) {
|
|
2650
|
-
continue;
|
|
2768
|
+
return new FragmentSpreadSelection(this.parentType, fragments, optimized, spreadDirectives);
|
|
2769
|
+
} else {
|
|
2770
|
+
// Otherwise, we keep this element and use a sub-selection with just the spread.
|
|
2771
|
+
optimizedSelection = selectionSetOf(typeCondition, new FragmentSpreadSelection(typeCondition, fragments, optimized, []));
|
|
2651
2772
|
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
optimizedSelection = diff
|
|
2655
|
-
? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
|
|
2656
|
-
: selectionSetOf(parentType, spread);
|
|
2657
|
-
|
|
2658
|
-
return { spread, optimizedSelection, hasDiff: !!diff };
|
|
2773
|
+
} else {
|
|
2774
|
+
optimizedSelection = optimized;
|
|
2659
2775
|
}
|
|
2660
2776
|
}
|
|
2661
|
-
|
|
2777
|
+
|
|
2778
|
+
// Then, recurse inside the field sub-selection (note that if we matched some fragments above,
|
|
2779
|
+
// this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
|
|
2780
|
+
optimizedSelection = optimizedSelection.optimizeSelections(fragments);
|
|
2781
|
+
|
|
2782
|
+
return this.selectionSet === optimizedSelection
|
|
2783
|
+
? this
|
|
2784
|
+
: new InlineFragmentSelection(this.element, optimizedSelection);
|
|
2662
2785
|
}
|
|
2663
2786
|
|
|
2664
2787
|
withoutDefer(labelsToRemove?: Set<string>): InlineFragmentSelection | SelectionSet {
|
|
@@ -2775,12 +2898,8 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2775
2898
|
return this.selectionSet === trimmedSelectionSet ? this : this.withUpdatedSelectionSet(trimmedSelectionSet);
|
|
2776
2899
|
}
|
|
2777
2900
|
|
|
2778
|
-
|
|
2779
|
-
return this.mapToSelectionSet((s) => s.
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
|
-
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FragmentSelection {
|
|
2783
|
-
return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
|
|
2901
|
+
expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection {
|
|
2902
|
+
return this.mapToSelectionSet((s) => s.expandFragments(updatedFragments));
|
|
2784
2903
|
}
|
|
2785
2904
|
|
|
2786
2905
|
equals(that: Selection): boolean {
|
|
@@ -2793,10 +2912,12 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2793
2912
|
&& this.selectionSet.equals(that.selectionSet);
|
|
2794
2913
|
}
|
|
2795
2914
|
|
|
2796
|
-
contains(that: Selection):
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2915
|
+
contains(that: Selection): ContainsResult {
|
|
2916
|
+
if (!(that instanceof FragmentSelection) || !this.element.equals(that.element)) {
|
|
2917
|
+
return ContainsResult.NOT_CONTAINED;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
return this.selectionSet.contains(that.selectionSet);
|
|
2800
2921
|
}
|
|
2801
2922
|
|
|
2802
2923
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
@@ -2804,14 +2925,6 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2804
2925
|
}
|
|
2805
2926
|
}
|
|
2806
2927
|
|
|
2807
|
-
function diffDirectives(superset: readonly Directive<any>[], maybeSubset: readonly Directive<any>[]): { isSubset: boolean, difference: Directive[] } {
|
|
2808
|
-
if (maybeSubset.every((d) => superset.some((s) => sameDirectiveApplication(d, s)))) {
|
|
2809
|
-
return { isSubset: true, difference: superset.filter((s) => !maybeSubset.some((d) => sameDirectiveApplication(d, s))) };
|
|
2810
|
-
} else {
|
|
2811
|
-
return { isSubset: false, difference: [] };
|
|
2812
|
-
}
|
|
2813
|
-
}
|
|
2814
|
-
|
|
2815
2928
|
class FragmentSpreadSelection extends FragmentSelection {
|
|
2816
2929
|
private computedKey: string | undefined;
|
|
2817
2930
|
|
|
@@ -2824,6 +2937,10 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2824
2937
|
super(new FragmentElement(sourceType, namedFragment.typeCondition, namedFragment.appliedDirectives.concat(spreadDirectives)));
|
|
2825
2938
|
}
|
|
2826
2939
|
|
|
2940
|
+
isFragmentSpread(): boolean {
|
|
2941
|
+
return true;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2827
2944
|
get selectionSet(): SelectionSet {
|
|
2828
2945
|
return this.namedFragment.selectionSet;
|
|
2829
2946
|
}
|
|
@@ -2842,11 +2959,8 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2842
2959
|
trimUnsatisfiableBranches(parentType: CompositeType): FragmentSelection {
|
|
2843
2960
|
// We must update the spread parent type if necessary since we're not going deeper,
|
|
2844
2961
|
// or we'll be fundamentally losing context.
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
namedFragments(): NamedFragments | undefined {
|
|
2849
|
-
return this.fragments;
|
|
2962
|
+
assert(parentType.schema() === this.parentType.schema(), 'Should not try to trim using a type from another schema');
|
|
2963
|
+
return this.rebaseOn(parentType, this.fragments);
|
|
2850
2964
|
}
|
|
2851
2965
|
|
|
2852
2966
|
validate(): void {
|
|
@@ -2879,7 +2993,7 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2879
2993
|
return this;
|
|
2880
2994
|
}
|
|
2881
2995
|
|
|
2882
|
-
rebaseOn(parentType: CompositeType): FragmentSelection {
|
|
2996
|
+
rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
|
|
2883
2997
|
// We preserve the parent type here, to make sure we don't lose context, but we actually don't
|
|
2884
2998
|
// want to expand the spread as that would compromise the code that optimize subgraph fetches to re-use named
|
|
2885
2999
|
// fragments.
|
|
@@ -2892,10 +3006,18 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2892
3006
|
if (this.parentType === parentType) {
|
|
2893
3007
|
return this;
|
|
2894
3008
|
}
|
|
3009
|
+
|
|
3010
|
+
// If we're rebasing on a _different_ schema, then we *must* have fragments, since reusing
|
|
3011
|
+
// `this.fragments` would be incorrect. If we're on the same schema though, we're happy to default
|
|
3012
|
+
// to `this.fragments`.
|
|
3013
|
+
assert(fragments || this.parentType.schema() === parentType.schema(), `Must provide fragments is rebasing on other schema`);
|
|
3014
|
+
const newFragments = fragments ?? this.fragments;
|
|
3015
|
+
const namedFragment = newFragments.get(this.namedFragment.name);
|
|
3016
|
+
assert(namedFragment, () => `Cannot rebase ${this} if it isn't part of the provided fragments`);
|
|
2895
3017
|
return new FragmentSpreadSelection(
|
|
2896
3018
|
parentType,
|
|
2897
|
-
|
|
2898
|
-
|
|
3019
|
+
newFragments,
|
|
3020
|
+
namedFragment,
|
|
2899
3021
|
this.spreadDirectives,
|
|
2900
3022
|
);
|
|
2901
3023
|
}
|
|
@@ -2906,26 +3028,20 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2906
3028
|
return true;
|
|
2907
3029
|
}
|
|
2908
3030
|
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
: new InlineFragmentSelection(this.element, expandedSubSelections);
|
|
2914
|
-
}
|
|
2915
|
-
|
|
2916
|
-
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FragmentSelection | readonly Selection[] {
|
|
2917
|
-
if (!names.includes(this.namedFragment.name)) {
|
|
3031
|
+
expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection | readonly Selection[] {
|
|
3032
|
+
// Note that this test will always fail if `updatedFragments` is `undefined`, making us expand everything.
|
|
3033
|
+
if (updatedFragments?.has(this.namedFragment.name)) {
|
|
3034
|
+
// This one is still there, it's not expanded.
|
|
2918
3035
|
return this;
|
|
2919
3036
|
}
|
|
2920
3037
|
|
|
2921
|
-
const expandedSubSelections = this.selectionSet.expandFragments(
|
|
3038
|
+
const expandedSubSelections = this.selectionSet.expandFragments(updatedFragments);
|
|
2922
3039
|
return sameType(this.parentType, this.namedFragment.typeCondition) && this.element.appliedDirectives.length === 0
|
|
2923
3040
|
? expandedSubSelections.selections()
|
|
2924
3041
|
: new InlineFragmentSelection(this.element, expandedSubSelections);
|
|
2925
3042
|
}
|
|
2926
3043
|
|
|
2927
3044
|
collectUsedFragmentNames(collector: Map<string, number>): void {
|
|
2928
|
-
this.selectionSet.collectUsedFragmentNames(collector);
|
|
2929
3045
|
const usageCount = collector.get(this.namedFragment.name);
|
|
2930
3046
|
collector.set(this.namedFragment.name, usageCount === undefined ? 1 : usageCount + 1);
|
|
2931
3047
|
}
|
|
@@ -2953,14 +3069,16 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2953
3069
|
&& sameDirectiveApplications(this.spreadDirectives, that.spreadDirectives);
|
|
2954
3070
|
}
|
|
2955
3071
|
|
|
2956
|
-
contains(that: Selection):
|
|
3072
|
+
contains(that: Selection): ContainsResult {
|
|
2957
3073
|
if (this.equals(that)) {
|
|
2958
|
-
return
|
|
3074
|
+
return ContainsResult.EQUAL;
|
|
2959
3075
|
}
|
|
2960
3076
|
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3077
|
+
if (!(that instanceof FragmentSelection) || !this.element.equals(that.element)) {
|
|
3078
|
+
return ContainsResult.NOT_CONTAINED;
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
return this.selectionSet.contains(that.selectionSet);
|
|
2964
3082
|
}
|
|
2965
3083
|
|
|
2966
3084
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
@@ -2985,7 +3103,6 @@ function selectionSetOfNode(
|
|
|
2985
3103
|
return selectionSetOf(
|
|
2986
3104
|
parentType,
|
|
2987
3105
|
selectionOfNode(parentType, node.selections[0], variableDefinitions, fragments, fieldAccessor),
|
|
2988
|
-
fragments,
|
|
2989
3106
|
);
|
|
2990
3107
|
}
|
|
2991
3108
|
|
|
@@ -3117,6 +3234,7 @@ function operationFromAST({
|
|
|
3117
3234
|
}) : Operation {
|
|
3118
3235
|
const rootType = schema.schemaDefinition.root(operation.operation);
|
|
3119
3236
|
validate(rootType, () => `The schema has no "${operation.operation}" root type defined`);
|
|
3237
|
+
const fragmentsIfAny = fragments.isEmpty() ? undefined : fragments;
|
|
3120
3238
|
return new Operation(
|
|
3121
3239
|
schema,
|
|
3122
3240
|
operation.operation,
|
|
@@ -3124,10 +3242,11 @@ function operationFromAST({
|
|
|
3124
3242
|
parentType: rootType.type,
|
|
3125
3243
|
source: operation.selectionSet,
|
|
3126
3244
|
variableDefinitions,
|
|
3127
|
-
fragments:
|
|
3245
|
+
fragments: fragmentsIfAny,
|
|
3128
3246
|
validate: validateInput,
|
|
3129
3247
|
}),
|
|
3130
3248
|
variableDefinitions,
|
|
3249
|
+
fragmentsIfAny,
|
|
3131
3250
|
operation.name?.value
|
|
3132
3251
|
);
|
|
3133
3252
|
}
|
|
@@ -3184,8 +3303,8 @@ export function operationToDocument(operation: Operation): DocumentNode {
|
|
|
3184
3303
|
selectionSet: operation.selectionSet.toSelectionSetNode(),
|
|
3185
3304
|
variableDefinitions: operation.variableDefinitions.toVariableDefinitionNodes(),
|
|
3186
3305
|
};
|
|
3187
|
-
const fragmentASTs: DefinitionNode[] = operation.
|
|
3188
|
-
? operation.
|
|
3306
|
+
const fragmentASTs: DefinitionNode[] = operation.fragments
|
|
3307
|
+
? operation.fragments?.toFragmentDefinitionNodes()
|
|
3189
3308
|
: [];
|
|
3190
3309
|
return {
|
|
3191
3310
|
kind: Kind.DOCUMENT,
|