@apollo/federation-internals 2.4.6 → 2.4.8
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/coreSpec.js +1 -1
- package/dist/coreSpec.js.map +1 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +3 -0
- package/dist/definitions.js.map +1 -1
- package/dist/federationSpec.js +2 -2
- package/dist/federationSpec.js.map +1 -1
- package/dist/operations.d.ts +85 -132
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +634 -383
- package/dist/operations.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +14 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/definitions.ts +4 -0
- package/src/operations.ts +1170 -677
- package/src/types.ts +22 -1
package/src/operations.ts
CHANGED
|
@@ -47,11 +47,12 @@ import {
|
|
|
47
47
|
Variables,
|
|
48
48
|
isObjectType,
|
|
49
49
|
NamedType,
|
|
50
|
+
isUnionType,
|
|
50
51
|
} from "./definitions";
|
|
51
52
|
import { isInterfaceObjectType } from "./federation";
|
|
52
53
|
import { ERRORS } from "./error";
|
|
53
|
-
import { isSubtype, sameType } from "./types";
|
|
54
|
-
import { assert,
|
|
54
|
+
import { isSubtype, sameType, typesCanBeMerged } from "./types";
|
|
55
|
+
import { assert, mapKeys, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
|
|
55
56
|
import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
|
|
56
57
|
import { v1 as uuidv1 } from 'uuid';
|
|
57
58
|
|
|
@@ -115,7 +116,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
115
116
|
|
|
116
117
|
constructor(
|
|
117
118
|
readonly definition: FieldDefinition<CompositeType>,
|
|
118
|
-
|
|
119
|
+
readonly args?: TArgs,
|
|
119
120
|
directives?: readonly Directive<any>[],
|
|
120
121
|
readonly alias?: string,
|
|
121
122
|
) {
|
|
@@ -696,88 +697,265 @@ export type RootOperationPath = {
|
|
|
696
697
|
path: OperationPath
|
|
697
698
|
}
|
|
698
699
|
|
|
699
|
-
//
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
readonly name?: string) {
|
|
700
|
+
// Computes for every fragment, which other fragments use it (so the reverse of it's dependencies, the other fragment it uses).
|
|
701
|
+
function computeFragmentsDependents(fragments: NamedFragments): SetMultiMap<string, string> {
|
|
702
|
+
const reverseDeps = new SetMultiMap<string, string>();
|
|
703
|
+
for (const fragment of fragments.definitions()) {
|
|
704
|
+
for (const dependency of fragment.fragmentUsages().keys()) {
|
|
705
|
+
reverseDeps.add(dependency, fragment.name);
|
|
706
|
+
}
|
|
707
707
|
}
|
|
708
|
+
return reverseDeps;
|
|
709
|
+
}
|
|
708
710
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
711
|
+
function clearKeptFragments(
|
|
712
|
+
usages: Map<string, number>,
|
|
713
|
+
fragments: NamedFragments,
|
|
714
|
+
minUsagesToOptimize: number
|
|
715
|
+
) {
|
|
716
|
+
// `toCheck` will contain only fragments that we know we want to keep (but haven't handled/removed from `usages` yet).
|
|
717
|
+
let toCheck = Array.from(usages.entries()).filter(([_, count]) => count >= minUsagesToOptimize).map(([name, _]) => name);
|
|
718
|
+
while (toCheck.length > 0) {
|
|
719
|
+
const newToCheck = [];
|
|
720
|
+
for (const name of toCheck) {
|
|
721
|
+
// We "keep" that fragment so clear it.
|
|
722
|
+
usages.delete(name);
|
|
723
|
+
// But as it is used, bump the usage for every fragment it uses.
|
|
724
|
+
const ownUsages = fragments.get(name)!.fragmentUsages();
|
|
725
|
+
for (const [otherName, otherCount] of ownUsages.entries()) {
|
|
726
|
+
const prevCount = usages.get(otherName);
|
|
727
|
+
// We're interested in fragment not in `usages` anymore.
|
|
728
|
+
if (prevCount !== undefined) {
|
|
729
|
+
const newCount = prevCount + otherCount;
|
|
730
|
+
usages.set(otherName, newCount);
|
|
731
|
+
if (prevCount < minUsagesToOptimize && newCount >= minUsagesToOptimize) {
|
|
732
|
+
newToCheck.push(otherName);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
713
736
|
}
|
|
737
|
+
toCheck = newToCheck;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
714
740
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
741
|
+
// Checks, in `selectionSet`, which fragments (of `fragments`) are used at least `minUsagesToOptimize` times.
|
|
742
|
+
// Returns the updated set of fragments containing only the fragment definitions with usage above our threshold,
|
|
743
|
+
// and `undefined` or `null` if no such fragment meets said threshold. When this method returns `null`, it
|
|
744
|
+
// additionally means that no fragments are use at all in `selectionSet` (and so `undefined` means that
|
|
745
|
+
// "some" fragments are used in `selectionSet`, but just none of them is used at least `minUsagesToOptimize`
|
|
746
|
+
// times).
|
|
747
|
+
function computeFragmentsToKeep(
|
|
748
|
+
selectionSet: SelectionSet,
|
|
749
|
+
fragments: NamedFragments,
|
|
750
|
+
minUsagesToOptimize: number
|
|
751
|
+
): NamedFragments | undefined | null {
|
|
752
|
+
// We start by collecting the usages within the selection set.
|
|
753
|
+
const usages = new Map<string, number>();
|
|
754
|
+
selectionSet.collectUsedFragmentNames(usages);
|
|
755
|
+
|
|
756
|
+
// If we have no fragment in the selection set, then it's simple, we just don't keep any fragments.
|
|
757
|
+
if (usages.size === 0) {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// We're going to remove fragments from usages as we categorize them as kept or expanded, so we
|
|
762
|
+
// first ensure that it has entries for every fragment, default to 0.
|
|
763
|
+
for (const fragment of fragments.definitions()) {
|
|
764
|
+
if (usages.get(fragment.name) === undefined) {
|
|
765
|
+
usages.set(fragment.name, 0);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// At this point, `usages` contains the usages of fragments "in the selection". From that, we want
|
|
770
|
+
// to decide which fragment to "keep", and which to re-expand. But there is 2 subtlety:
|
|
771
|
+
// 1. when we decide to keep some fragment F, then we should could it's own usages of other fragments. That
|
|
772
|
+
// is, if a fragment G is use once in the selection, but also use once in a fragment F that we
|
|
773
|
+
// keep, then the usages for G is really 2 (but if F is unused, then we don't want to count
|
|
774
|
+
// it's usage of G for instance).
|
|
775
|
+
// 2. when we decide to expand a fragment, then this also impact the usages of other fragments it
|
|
776
|
+
// uses, as those gets "inlined" into the selection. But that also mean we have to be careful
|
|
777
|
+
// of the order in which we pick fragments to expand. Say we have:
|
|
778
|
+
// ```graphql
|
|
779
|
+
// query {
|
|
780
|
+
// ...F1
|
|
781
|
+
// }
|
|
782
|
+
//
|
|
783
|
+
// fragment F1 {
|
|
784
|
+
// a { ...F2 }
|
|
785
|
+
// b { ...F2 }
|
|
786
|
+
// }
|
|
787
|
+
//
|
|
788
|
+
// fragment F2 {
|
|
789
|
+
// // something
|
|
790
|
+
// }
|
|
791
|
+
// ```
|
|
792
|
+
// then at this point where we've only counted usages in the query selection, `usages` will be
|
|
793
|
+
// `{ F1: 1, F2: 0 }`. But we do not want to expand _both_ F1 and F2. Instead, we want to expand
|
|
794
|
+
// F1 first, and then realize that this increases F2 usages to 2, which means we stop there and keep F2.
|
|
795
|
+
// Generalizing this, it means we want to first pick up fragments to expand that are _not_ used by any
|
|
796
|
+
// other fragments that may be expanded.
|
|
797
|
+
const reverseDependencies = computeFragmentsDependents(fragments);
|
|
798
|
+
// We'll add to `toExpand` fragment we will definitively expand.
|
|
799
|
+
const toExpand = new Set<string>;
|
|
800
|
+
let shouldContinue = true;
|
|
801
|
+
while (shouldContinue) {
|
|
802
|
+
// We'll do an iteration, but if we make no progress, we won't continue (we don't want to loop forever).
|
|
803
|
+
shouldContinue = false;
|
|
804
|
+
clearKeptFragments(usages, fragments, minUsagesToOptimize);
|
|
805
|
+
for (const name of mapKeys(usages)) {
|
|
806
|
+
// Note that we modify `usages` as we iterate it, so 1) we use `mapKeys` above which copy into a list and 2)
|
|
807
|
+
// we get the `count` manually instead of relying on (possibly outdated) entries.
|
|
808
|
+
const count = usages.get(name)!;
|
|
809
|
+
// A unused fragment is not technically expanded, it is just removed and we can ignore for now (it's count
|
|
810
|
+
// count increase later but ...).
|
|
811
|
+
if (count === 0) {
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
719
814
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (
|
|
724
|
-
|
|
815
|
+
// If we find a fragment to keep, it means some fragment we expanded earlier in this iteration bump this
|
|
816
|
+
// one count. We unsure `shouldContinue` is set so `clearKeptFragments` is called again, but let that
|
|
817
|
+
// method deal with it otherwise.
|
|
818
|
+
if (count >= minUsagesToOptimize) {
|
|
819
|
+
shouldContinue = true;
|
|
820
|
+
break;
|
|
725
821
|
}
|
|
726
|
-
}
|
|
727
822
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
823
|
+
const fragmentsUsingName = reverseDependencies.get(name);
|
|
824
|
+
if (!fragmentsUsingName || [...fragmentsUsingName].every((fragName) => toExpand.has(fragName) || !usages.get(fragName))) {
|
|
825
|
+
// This fragment is not used enough, and is only used by fragments we keep, so we
|
|
826
|
+
// are guaranteed that expanding another fragment will not increase its usage. So
|
|
827
|
+
// we definitively expand it.
|
|
828
|
+
toExpand.add(name);
|
|
829
|
+
usages.delete(name);
|
|
830
|
+
|
|
831
|
+
// We've added to `toExpand`, so it's worth redoing another iteration
|
|
832
|
+
// after that to see if something changes.
|
|
833
|
+
shouldContinue = true;
|
|
834
|
+
|
|
835
|
+
// Now that we expand it, we should bump the usage for every fragment it uses.
|
|
836
|
+
const nameUsages = fragments.get(name)!.fragmentUsages();
|
|
837
|
+
for (const [otherName, otherCount] of nameUsages.entries()) {
|
|
838
|
+
const prev = usages.get(otherName);
|
|
839
|
+
// Note that if `otherName` is not part of usages, it means it's a fragment we
|
|
840
|
+
// already decided to keep/expand, so we just ignore it.
|
|
841
|
+
if (prev !== undefined) {
|
|
842
|
+
usages.set(otherName, prev + count * otherCount);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
746
846
|
}
|
|
847
|
+
}
|
|
747
848
|
|
|
748
|
-
|
|
849
|
+
// Finally, we know that to expand, which is `toExpand` plus whatever remains in `usage` (typically
|
|
850
|
+
// genuinely unused fragments).
|
|
851
|
+
for (const name of usages.keys()) {
|
|
852
|
+
toExpand.add(name);
|
|
749
853
|
}
|
|
750
854
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
855
|
+
return toExpand.size === 0 ? fragments : fragments.filter((f) => !toExpand.has(f.name));
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// TODO Operations can also have directives
|
|
859
|
+
export class Operation {
|
|
860
|
+
constructor(
|
|
861
|
+
readonly schema: Schema,
|
|
862
|
+
readonly rootKind: SchemaRootKind,
|
|
863
|
+
readonly selectionSet: SelectionSet,
|
|
864
|
+
readonly variableDefinitions: VariableDefinitions,
|
|
865
|
+
readonly fragments?: NamedFragments,
|
|
866
|
+
readonly name?: string) {
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Returns a copy of this operation with the provided updated selection set.
|
|
870
|
+
// Note that this method assumes that the existing `this.fragments` is still appropriate.
|
|
871
|
+
private withUpdatedSelectionSet(newSelectionSet: SelectionSet): Operation {
|
|
872
|
+
if (this.selectionSet === newSelectionSet) {
|
|
754
873
|
return this;
|
|
755
874
|
}
|
|
756
875
|
|
|
757
876
|
return new Operation(
|
|
758
877
|
this.schema,
|
|
759
878
|
this.rootKind,
|
|
760
|
-
|
|
879
|
+
newSelectionSet,
|
|
761
880
|
this.variableDefinitions,
|
|
881
|
+
this.fragments,
|
|
762
882
|
this.name
|
|
763
883
|
);
|
|
764
884
|
}
|
|
765
885
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (
|
|
886
|
+
// Returns a copy of this operation with the provided updated selection set and fragments.
|
|
887
|
+
private withUpdatedSelectionSetAndFragments(newSelectionSet: SelectionSet, newFragments: NamedFragments | undefined): Operation {
|
|
888
|
+
if (this.selectionSet === newSelectionSet && newFragments === this.fragments) {
|
|
769
889
|
return this;
|
|
770
890
|
}
|
|
771
891
|
|
|
772
892
|
return new Operation(
|
|
773
893
|
this.schema,
|
|
774
894
|
this.rootKind,
|
|
775
|
-
|
|
895
|
+
newSelectionSet,
|
|
776
896
|
this.variableDefinitions,
|
|
897
|
+
newFragments,
|
|
777
898
|
this.name
|
|
778
899
|
);
|
|
779
900
|
}
|
|
780
901
|
|
|
902
|
+
optimize(fragments?: NamedFragments, minUsagesToOptimize: number = 2): Operation {
|
|
903
|
+
assert(minUsagesToOptimize >= 1, `Expected 'minUsagesToOptimize' to be at least 1, but got ${minUsagesToOptimize}`)
|
|
904
|
+
if (!fragments || fragments.isEmpty()) {
|
|
905
|
+
return this;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
let optimizedSelection = this.selectionSet.optimize(fragments);
|
|
909
|
+
if (optimizedSelection === this.selectionSet) {
|
|
910
|
+
return this;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
let finalFragments = computeFragmentsToKeep(optimizedSelection, fragments, minUsagesToOptimize);
|
|
914
|
+
|
|
915
|
+
// If there is fragment usages and we're not keeping all fragments, we need to expand fragments.
|
|
916
|
+
if (finalFragments !== null && finalFragments?.size !== fragments.size) {
|
|
917
|
+
// Note that optimizing all fragments to potentially re-expand some is not entirely optimal, but it's unclear
|
|
918
|
+
// how to do otherwise, and it probably don't matter too much in practice (we only call this optimization
|
|
919
|
+
// on the final computed query plan, so not a very hot path; plus in most cases we won't even reach that
|
|
920
|
+
// point either because there is no fragment, or none will have been optimized away so we'll exit above).
|
|
921
|
+
optimizedSelection = optimizedSelection.expandFragments(finalFragments);
|
|
922
|
+
|
|
923
|
+
// Expanding fragments could create some "inefficiencies" that we wouldn't have if we hadn't re-optimized
|
|
924
|
+
// the fragments to de-optimize it later, so we do a final "normalize" pass to remove those.
|
|
925
|
+
optimizedSelection = optimizedSelection.normalize({ parentType: optimizedSelection.parentType });
|
|
926
|
+
|
|
927
|
+
// And if we've expanded some fragments but kept others, then it's not 100% impossible that some
|
|
928
|
+
// fragment was used multiple times in some expanded fragment(s), but that post-expansion all of
|
|
929
|
+
// it's usages are "dead" branches that are removed by the final `normalize`. In that case though,
|
|
930
|
+
// we need to ensure we don't include the now-unused fragment in the final list of fragments.
|
|
931
|
+
// TODO: remark that the same reasoning could leave a single instance of a fragment usage, so if
|
|
932
|
+
// we really really want to never have less than `minUsagesToOptimize`, we could do some loop of
|
|
933
|
+
// `expand then normalize` unless all fragments are provably used enough. We don't bother, because
|
|
934
|
+
// leaving this is not a huge deal and it's not worth the complexity, but it could be that we can
|
|
935
|
+
// refactor all this later to avoid this case without additional complexity.
|
|
936
|
+
if (finalFragments) {
|
|
937
|
+
const usages = new Map<string, number>();
|
|
938
|
+
optimizedSelection.collectUsedFragmentNames(usages);
|
|
939
|
+
finalFragments = finalFragments.filter((f) => (usages.get(f.name) ?? 0) > 0);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return this.withUpdatedSelectionSetAndFragments(optimizedSelection, finalFragments ?? undefined);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
expandAllFragments(): Operation {
|
|
947
|
+
// We clear up the fragments since we've expanded all.
|
|
948
|
+
// Also note that expanding fragment usually generate unecessary fragments/inefficient selections, so it
|
|
949
|
+
// basically always make sense to normalize afterwards. Besides, fragment reuse (done by `optimize`) rely
|
|
950
|
+
// on the fact that its input is normalized to work properly, so all the more reason to do it here.
|
|
951
|
+
const expanded = this.selectionSet.expandFragments();
|
|
952
|
+
return this.withUpdatedSelectionSetAndFragments(expanded.normalize({ parentType: expanded.parentType }), undefined);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
normalize(): Operation {
|
|
956
|
+
return this.withUpdatedSelectionSet(this.selectionSet.normalize({ parentType: this.selectionSet.parentType }));
|
|
957
|
+
}
|
|
958
|
+
|
|
781
959
|
/**
|
|
782
960
|
* Returns this operation but potentially modified so all/some of the @defer applications have been removed.
|
|
783
961
|
*
|
|
@@ -786,14 +964,7 @@ export class Operation {
|
|
|
786
964
|
* applications are removed.
|
|
787
965
|
*/
|
|
788
966
|
withoutDefer(labelsToRemove?: Set<string>): Operation {
|
|
789
|
-
|
|
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
|
-
const updated = this.selectionSet.withoutDefer(labelsToRemove);
|
|
794
|
-
return updated == this.selectionSet
|
|
795
|
-
? this
|
|
796
|
-
: new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.name);
|
|
967
|
+
return this.withUpdatedSelectionSet(this.selectionSet.withoutDefer(labelsToRemove));
|
|
797
968
|
}
|
|
798
969
|
|
|
799
970
|
/**
|
|
@@ -815,15 +986,11 @@ export class Operation {
|
|
|
815
986
|
assignedDeferLabels: Set<string>,
|
|
816
987
|
deferConditions: SetMultiMap<string, string>,
|
|
817
988
|
} {
|
|
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
989
|
const normalizer = new DeferNormalizer();
|
|
822
990
|
const { hasDefers, hasNonLabelledOrConditionalDefers } = normalizer.init(this.selectionSet);
|
|
823
991
|
let updatedOperation: Operation = this;
|
|
824
992
|
if (hasNonLabelledOrConditionalDefers) {
|
|
825
|
-
|
|
826
|
-
updatedOperation = new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.name);
|
|
993
|
+
updatedOperation = this.withUpdatedSelectionSet(this.selectionSet.withNormalizedDefer(normalizer));
|
|
827
994
|
}
|
|
828
995
|
return {
|
|
829
996
|
operation: updatedOperation,
|
|
@@ -844,14 +1011,22 @@ export class Operation {
|
|
|
844
1011
|
}
|
|
845
1012
|
|
|
846
1013
|
toString(expandFragments: boolean = false, prettyPrint: boolean = true): string {
|
|
847
|
-
return this.selectionSet.toOperationString(this.rootKind, this.variableDefinitions, this.name, expandFragments, prettyPrint);
|
|
1014
|
+
return this.selectionSet.toOperationString(this.rootKind, this.variableDefinitions, this.fragments, this.name, expandFragments, prettyPrint);
|
|
848
1015
|
}
|
|
849
1016
|
}
|
|
850
1017
|
|
|
1018
|
+
export type FragmentRestrictionAtType = { selectionSet: SelectionSet, validator?: FieldsConflictValidator };
|
|
1019
|
+
|
|
851
1020
|
export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmentDefinition> {
|
|
852
1021
|
private _selectionSet: SelectionSet | undefined;
|
|
853
1022
|
|
|
854
|
-
|
|
1023
|
+
// Lazily computed cache of the expanded selection set.
|
|
1024
|
+
private _expandedSelectionSet: SelectionSet | undefined;
|
|
1025
|
+
|
|
1026
|
+
private _fragmentUsages: Map<string, number> | undefined;
|
|
1027
|
+
private _includedFragmentNames: Set<string> | undefined;
|
|
1028
|
+
|
|
1029
|
+
private readonly expandedSelectionSetsAtTypesCache = new Map<string, FragmentRestrictionAtType>();
|
|
855
1030
|
|
|
856
1031
|
constructor(
|
|
857
1032
|
schema: Schema,
|
|
@@ -876,12 +1051,31 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
876
1051
|
return this._selectionSet;
|
|
877
1052
|
}
|
|
878
1053
|
|
|
1054
|
+
expandedSelectionSet(): SelectionSet {
|
|
1055
|
+
if (!this._expandedSelectionSet) {
|
|
1056
|
+
this._expandedSelectionSet = this.selectionSet.expandFragments().normalize({ parentType: this.typeCondition });
|
|
1057
|
+
}
|
|
1058
|
+
return this._expandedSelectionSet;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
879
1061
|
withUpdatedSelectionSet(newSelectionSet: SelectionSet): NamedFragmentDefinition {
|
|
880
1062
|
return new NamedFragmentDefinition(this.schema(), this.name, this.typeCondition).setSelectionSet(newSelectionSet);
|
|
881
1063
|
}
|
|
882
1064
|
|
|
1065
|
+
fragmentUsages(): ReadonlyMap<string, number> {
|
|
1066
|
+
if (!this._fragmentUsages) {
|
|
1067
|
+
this._fragmentUsages = new Map();
|
|
1068
|
+
this.selectionSet.collectUsedFragmentNames(this._fragmentUsages);
|
|
1069
|
+
}
|
|
1070
|
+
return this._fragmentUsages;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
883
1073
|
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
884
|
-
this.
|
|
1074
|
+
const usages = this.fragmentUsages();
|
|
1075
|
+
for (const [name, count] of usages.entries()) {
|
|
1076
|
+
const prevCount = collector.get(name);
|
|
1077
|
+
collector.set(name, prevCount ? prevCount + count : count);
|
|
1078
|
+
}
|
|
885
1079
|
}
|
|
886
1080
|
|
|
887
1081
|
toFragmentDefinitionNode() : FragmentDefinitionNode {
|
|
@@ -903,17 +1097,58 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
903
1097
|
}
|
|
904
1098
|
|
|
905
1099
|
/**
|
|
906
|
-
* Whether this fragment may apply at the provided type, that
|
|
907
|
-
*
|
|
1100
|
+
* Whether this fragment may apply _directly_ at the provided type, meaning that the fragment sub-selection
|
|
1101
|
+
* could be put directly inside a `... on type {}` inline fragment (_without_ re-adding the fragment condition
|
|
1102
|
+
* that is), and both be valid and not "lose context".
|
|
1103
|
+
*
|
|
1104
|
+
* For that to be true, we need one of this to be true:
|
|
1105
|
+
* 1. the runtime types of the fragment condition must be at least as general as those of the provided `type`.
|
|
1106
|
+
* Otherwise, putting it at `type` without its condition would "generalize" more than fragment meant to (and
|
|
1107
|
+
* so we'd "lose context"
|
|
1108
|
+
* 2. either `type` and `this.typeCondition` are equal, or `type` is an object or `this.typeCondition` is a union
|
|
1109
|
+
* The idea is that, assuming our 1st point, then:
|
|
1110
|
+
* - if both are equal, things works trivially.
|
|
1111
|
+
* - if `type` is an object, `this.typeCondition` is either the same object, or a union/interface for which
|
|
1112
|
+
* type is a valid runtime. In all case, anything valid on `this.typeCondition` would apply to `type` too.
|
|
1113
|
+
* - if `this.typeCondition` is a union, then it's selection can only have fragments on object types at top-level
|
|
1114
|
+
* (save for `__typename`), and all those selection will work at `type` too.
|
|
1115
|
+
* But in any other case, both types must be abstract (if `this.typeCondition` is an object, the 1st condition
|
|
1116
|
+
* imply `type` can only be the same type) and we're in one of:
|
|
1117
|
+
* - `type` and `this.typeCondition` are both different interfaces (that intersect but are different).
|
|
1118
|
+
* - `type` is aunion and `this.typeCondition` an interface.
|
|
1119
|
+
* And in both cases, the selection of the fragment may selection an interface that is not valid at `type` (if `type`
|
|
1120
|
+
* is a union because a direct field is always wrong, and if `type` is another interface because that interface may
|
|
1121
|
+
* not have that particular field).
|
|
908
1122
|
*
|
|
909
1123
|
* @param type - the type at which we're looking at applying the fragment
|
|
910
1124
|
*/
|
|
911
|
-
|
|
912
|
-
|
|
1125
|
+
canApplyDirectlyAtType(type: CompositeType): boolean {
|
|
1126
|
+
if (sameType(type, this.typeCondition)) {
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// No point computing runtime types if the condition is an object (it can never cover all of
|
|
1131
|
+
// the runtimes of `type` unless it's the same type, which is already covered).
|
|
1132
|
+
if (!isAbstractType(this.typeCondition)) {
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const conditionRuntimes = possibleRuntimeTypes(this.typeCondition);
|
|
1137
|
+
const typeRuntimes = possibleRuntimeTypes(type);
|
|
1138
|
+
// The fragment condition must be at least as general as the provided type (in other words, all of the
|
|
1139
|
+
// runtimes of `type` must be in `conditionRuntimes`).
|
|
1140
|
+
// Note: the `length` test is technically redundant, but just avoid the more costly sub-set check if we
|
|
1141
|
+
// can cheaply show it's unnecessary.
|
|
1142
|
+
if (conditionRuntimes.length < typeRuntimes.length
|
|
1143
|
+
|| !typeRuntimes.every((t1) => conditionRuntimes.some((t2) => sameType(t1, t2)))) {
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return isObjectType(type) || isUnionType(this.typeCondition);
|
|
913
1148
|
}
|
|
914
1149
|
|
|
915
1150
|
/**
|
|
916
|
-
* This methods *assumes* that `this.
|
|
1151
|
+
* This methods *assumes* that `this.canApplyDirectlyAtType(type)` is `true` (and may crash if this is not true), and returns
|
|
917
1152
|
* a version fo this named fragment selection set that corresponds to the "expansion" of this named fragment at `type`
|
|
918
1153
|
*
|
|
919
1154
|
* The overall idea here is that if we have an interface I with 2 implementations T1 and T2, and we have a fragment like:
|
|
@@ -930,32 +1165,63 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
930
1165
|
* 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
1166
|
* us that part.
|
|
932
1167
|
*/
|
|
933
|
-
|
|
934
|
-
// First, if the candidate condition is an object or is the type passed, then there isn't any
|
|
1168
|
+
expandedSelectionSetAtType(type: CompositeType): FragmentRestrictionAtType {
|
|
1169
|
+
// First, if the candidate condition is an object or is the type passed, then there isn't any restriction to do.
|
|
935
1170
|
if (sameType(type, this.typeCondition) || isObjectType(this.typeCondition)) {
|
|
936
|
-
return this.
|
|
1171
|
+
return { selectionSet: this.expandedSelectionSet() };
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
let cached = this.expandedSelectionSetsAtTypesCache.get(type.name);
|
|
1175
|
+
if (!cached) {
|
|
1176
|
+
cached = this.computeExpandedSelectionSetAtType(type);
|
|
1177
|
+
this.expandedSelectionSetsAtTypesCache.set(type.name, cached);
|
|
1178
|
+
}
|
|
1179
|
+
return cached;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
private computeExpandedSelectionSetAtType(type: CompositeType): FragmentRestrictionAtType {
|
|
1183
|
+
const expandedSelectionSet = this.expandedSelectionSet();
|
|
1184
|
+
// Note that what we want is get any simplification coming from normalizing at `type`, but any such simplication
|
|
1185
|
+
// stops as soon as we traverse a field, so no point in being recursive.
|
|
1186
|
+
const selectionSet = expandedSelectionSet.normalize({ parentType: type, recursive: false });
|
|
1187
|
+
|
|
1188
|
+
// Note that `trimmed` is the difference of 2 selections that may not have been normalized on the same parent type,
|
|
1189
|
+
// so in practice, it is possible that `trimmed` contains some of the selections that `selectionSet` contains, but
|
|
1190
|
+
// that they have been simplified in `selectionSet` in such a way that the `minus` call does not see it. However,
|
|
1191
|
+
// it is not trivial to deal with this, and it is fine given that we use trimmed to create the validator because
|
|
1192
|
+
// we know the non-trimmed parts cannot create field conflict issues so we're trying to build a smaller validator,
|
|
1193
|
+
// but it's ok if trimmed is not as small as it theoretically can be.
|
|
1194
|
+
const trimmed = expandedSelectionSet.minus(selectionSet);
|
|
1195
|
+
const validator = trimmed.isEmpty() ? undefined : FieldsConflictValidator.build(trimmed);
|
|
1196
|
+
return { selectionSet, validator };
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Whether this fragment fully includes `otherFragment`.
|
|
1201
|
+
* Note that this is slightly different from `this` "using" `otherFragment` in that this essentially checks
|
|
1202
|
+
* if the full selection set of `otherFragment` is contained by `this`, so this only look at "top-level" usages.
|
|
1203
|
+
*
|
|
1204
|
+
* Note that this is guaranteed to return `false` if passed `this` name.
|
|
1205
|
+
*/
|
|
1206
|
+
includes(otherFragment: string): boolean {
|
|
1207
|
+
if (this.name === otherFragment) {
|
|
1208
|
+
return false;
|
|
937
1209
|
}
|
|
938
1210
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
// intersection of its runtime types with those of `this.typeCondition`, call `trimUnsatisfiableBranches` for each of the resulting
|
|
942
|
-
// object types, and merge all those selection sets, and this "may" result in a smaller selection at times. This is a bit complex
|
|
943
|
-
// and costly to do however, so we just return the selection unchanged for now, which is always valid but simply may not be absolutely
|
|
944
|
-
// optimal.
|
|
945
|
-
// Concretely, this means that there may be corner cases where a named fragment could be reused but isn't, but waiting on finding
|
|
946
|
-
// concrete examples where this matter to decide if it's worth the complexity.
|
|
947
|
-
if (!isObjectType(type)) {
|
|
948
|
-
return this.selectionSet;
|
|
1211
|
+
if (!this._includedFragmentNames) {
|
|
1212
|
+
this._includedFragmentNames = this.computeIncludedFragmentNames();
|
|
949
1213
|
}
|
|
1214
|
+
return this._includedFragmentNames.has(otherFragment);
|
|
1215
|
+
}
|
|
950
1216
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1217
|
+
private computeIncludedFragmentNames(): Set<string> {
|
|
1218
|
+
const included = new Set<string>();
|
|
1219
|
+
for (const selection of this.selectionSet.selections()) {
|
|
1220
|
+
if (selection instanceof FragmentSpreadSelection) {
|
|
1221
|
+
included.add(selection.namedFragment.name);
|
|
1222
|
+
}
|
|
957
1223
|
}
|
|
958
|
-
return
|
|
1224
|
+
return included;
|
|
959
1225
|
}
|
|
960
1226
|
|
|
961
1227
|
toString(indent?: string): string {
|
|
@@ -963,11 +1229,16 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
963
1229
|
}
|
|
964
1230
|
}
|
|
965
1231
|
|
|
1232
|
+
|
|
966
1233
|
export class NamedFragments {
|
|
967
1234
|
private readonly fragments = new MapWithCachedArrays<string, NamedFragmentDefinition>();
|
|
968
1235
|
|
|
969
1236
|
isEmpty(): boolean {
|
|
970
|
-
return this.
|
|
1237
|
+
return this.size === 0;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
get size(): number {
|
|
1241
|
+
return this.fragments.size;
|
|
971
1242
|
}
|
|
972
1243
|
|
|
973
1244
|
names(): readonly string[] {
|
|
@@ -987,28 +1258,8 @@ export class NamedFragments {
|
|
|
987
1258
|
}
|
|
988
1259
|
}
|
|
989
1260
|
|
|
990
|
-
|
|
991
|
-
return this.fragments.values().filter(f => f.
|
|
992
|
-
}
|
|
993
|
-
|
|
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;
|
|
1261
|
+
maybeApplyingDirectlyAtType(type: CompositeType): NamedFragmentDefinition[] {
|
|
1262
|
+
return this.fragments.values().filter(f => f.canApplyDirectlyAtType(type));
|
|
1012
1263
|
}
|
|
1013
1264
|
|
|
1014
1265
|
get(name: string): NamedFragmentDefinition | undefined {
|
|
@@ -1032,47 +1283,44 @@ export class NamedFragments {
|
|
|
1032
1283
|
}
|
|
1033
1284
|
|
|
1034
1285
|
/**
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1286
|
+
* The mapper is called on every fragment definition (`fragment` argument), but in such a way that if a fragment A uses another fragment B,
|
|
1287
|
+
* then the mapper is guaranteed to be called on B _before_ being called on A. Further, the `newFragments` argument is a new `NamedFragments`
|
|
1288
|
+
* containing all the previously mapped definition (minus those for which the mapper returned `undefined`). So if A uses B (and the mapper
|
|
1289
|
+
* on B do not return undefined), then when mapper is called on A `newFragments` will have the mapped value for B.
|
|
1039
1290
|
*/
|
|
1040
|
-
|
|
1041
|
-
mapper: (
|
|
1042
|
-
recreateFct: (frag: NamedFragmentDefinition, newSelectionSet: SelectionSet) => NamedFragmentDefinition = (f, s) => f.withUpdatedSelectionSet(s),
|
|
1291
|
+
mapInDependencyOrder(
|
|
1292
|
+
mapper: (fragment: NamedFragmentDefinition, newFragments: NamedFragments) => NamedFragmentDefinition | undefined
|
|
1043
1293
|
): NamedFragments | undefined {
|
|
1044
|
-
type
|
|
1045
|
-
|
|
1046
|
-
mappedSelectionSet: SelectionSet,
|
|
1294
|
+
type FragmentDependencies = {
|
|
1295
|
+
fragment: NamedFragmentDefinition,
|
|
1047
1296
|
dependsOn: string[],
|
|
1048
1297
|
};
|
|
1049
|
-
const fragmentsMap = new Map<string,
|
|
1050
|
-
const removedFragments = new Set<string>();
|
|
1298
|
+
const fragmentsMap = new Map<string, FragmentDependencies>();
|
|
1051
1299
|
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
1300
|
fragmentsMap.set(fragment.name, {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
dependsOn: Array.from(otherFragmentsUsages.keys()),
|
|
1301
|
+
fragment,
|
|
1302
|
+
dependsOn: Array.from(fragment.fragmentUsages().keys()),
|
|
1064
1303
|
});
|
|
1065
1304
|
}
|
|
1066
1305
|
|
|
1306
|
+
const removedFragments = new Set<string>();
|
|
1067
1307
|
const mappedFragments = new NamedFragments();
|
|
1068
1308
|
while (fragmentsMap.size > 0) {
|
|
1069
1309
|
for (const [name, info] of fragmentsMap) {
|
|
1070
1310
|
// Note that graphQL specifies that named fragments cannot have cycles (https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles)
|
|
1071
1311
|
// 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
1312
|
if (info.dependsOn.every((n) => mappedFragments.has(n) || removedFragments.has(n))) {
|
|
1073
|
-
const
|
|
1074
|
-
mappedFragments.add(recreateFct(info.original, reoptimizedSelectionSet));
|
|
1313
|
+
const mapped = mapper(info.fragment, mappedFragments);
|
|
1075
1314
|
fragmentsMap.delete(name);
|
|
1315
|
+
if (!mapped) {
|
|
1316
|
+
removedFragments.add(name);
|
|
1317
|
+
} else {
|
|
1318
|
+
mappedFragments.add(mapped);
|
|
1319
|
+
}
|
|
1320
|
+
// We just deleted from `fragmentsMap` so continuing our current `for` iteration is dangerous,
|
|
1321
|
+
// so we break to the `while` loop (besides, there is no reason why continuing the inner iteration
|
|
1322
|
+
// would be better than restarting it right away).
|
|
1323
|
+
break;
|
|
1076
1324
|
}
|
|
1077
1325
|
}
|
|
1078
1326
|
}
|
|
@@ -1080,20 +1328,58 @@ export class NamedFragments {
|
|
|
1080
1328
|
return mappedFragments.isEmpty() ? undefined : mappedFragments;
|
|
1081
1329
|
}
|
|
1082
1330
|
|
|
1331
|
+
/**
|
|
1332
|
+
* This method:
|
|
1333
|
+
* - expands all nested fragments,
|
|
1334
|
+
* - applies the provided mapper to the selection set of the fragments,
|
|
1335
|
+
* - and finally re-fragments the nested fragments.
|
|
1336
|
+
*/
|
|
1337
|
+
mapToExpandedSelectionSets(
|
|
1338
|
+
mapper: (selectionSet: SelectionSet) => SelectionSet | undefined,
|
|
1339
|
+
): NamedFragments | undefined {
|
|
1340
|
+
return this.mapInDependencyOrder((fragment, newFragments) => {
|
|
1341
|
+
const mappedSelectionSet = mapper(fragment.selectionSet.expandFragments().normalize({ parentType: fragment.typeCondition }));
|
|
1342
|
+
if (!mappedSelectionSet) {
|
|
1343
|
+
return undefined;
|
|
1344
|
+
}
|
|
1345
|
+
const reoptimizedSelectionSet = mappedSelectionSet.optimize(newFragments);
|
|
1346
|
+
return fragment.withUpdatedSelectionSet(reoptimizedSelectionSet);
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1083
1350
|
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.
|
|
1351
|
+
return this.mapInDependencyOrder((fragment, newFragments) => {
|
|
1352
|
+
const rebasedType = schema.type(fragment.selectionSet.parentType.name);
|
|
1353
|
+
try {
|
|
1354
|
+
if (!rebasedType || !isCompositeType(rebasedType)) {
|
|
1092
1355
|
return undefined;
|
|
1093
1356
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1357
|
+
|
|
1358
|
+
const rebasedSelection = fragment.selectionSet.rebaseOn(rebasedType, newFragments);
|
|
1359
|
+
return new NamedFragmentDefinition(schema, fragment.name, rebasedType).setSelectionSet(rebasedSelection);
|
|
1360
|
+
} catch (e) {
|
|
1361
|
+
// This means we cannot rebase this selection on the schema and thus cannot reuse that fragment on that
|
|
1362
|
+
// particular schema.
|
|
1363
|
+
return undefined;
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
filter(predicate: (fragment: NamedFragmentDefinition) => boolean): NamedFragments | undefined {
|
|
1369
|
+
return this.mapInDependencyOrder((fragment, newFragments) => {
|
|
1370
|
+
if (predicate(fragment)) {
|
|
1371
|
+
// We want to keep that fragment. But that fragment might use a fragment we remove, and if so,
|
|
1372
|
+
// we need to expand that removed fragment. Note that because we're running in
|
|
1373
|
+
// dependency order, we know that `newFragments` will have every fragments that should be
|
|
1374
|
+
// kept/not expanded.
|
|
1375
|
+
const updatedSelectionSet = fragment.selectionSet.expandFragments(newFragments);
|
|
1376
|
+
return updatedSelectionSet === fragment.selectionSet
|
|
1377
|
+
? fragment
|
|
1378
|
+
: fragment.withUpdatedSelectionSet(updatedSelectionSet);
|
|
1379
|
+
} else {
|
|
1380
|
+
return undefined;
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1097
1383
|
}
|
|
1098
1384
|
|
|
1099
1385
|
validate(variableDefinitions: VariableDefinitions) {
|
|
@@ -1173,6 +1459,16 @@ class DeferNormalizer {
|
|
|
1173
1459
|
}
|
|
1174
1460
|
}
|
|
1175
1461
|
|
|
1462
|
+
export enum ContainsResult {
|
|
1463
|
+
// Note: enum values are numbers in the end, and 0 means false in JS, so we should keep `NOT_CONTAINED` first
|
|
1464
|
+
// so that using the result of `contains` as a boolean works.
|
|
1465
|
+
NOT_CONTAINED,
|
|
1466
|
+
STRICTLY_CONTAINED,
|
|
1467
|
+
EQUAL,
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
export type CollectedFieldsInSet = { path: string[], field: FieldSelection }[];
|
|
1471
|
+
|
|
1176
1472
|
export class SelectionSet {
|
|
1177
1473
|
private readonly _keyedSelections: Map<string, Selection>;
|
|
1178
1474
|
private readonly _selections: readonly Selection[];
|
|
@@ -1180,7 +1476,6 @@ export class SelectionSet {
|
|
|
1180
1476
|
constructor(
|
|
1181
1477
|
readonly parentType: CompositeType,
|
|
1182
1478
|
keyedSelections: Map<string, Selection> = new Map(),
|
|
1183
|
-
readonly fragments?: NamedFragments,
|
|
1184
1479
|
) {
|
|
1185
1480
|
this._keyedSelections = keyedSelections;
|
|
1186
1481
|
this._selections = mapValues(keyedSelections);
|
|
@@ -1204,7 +1499,7 @@ export class SelectionSet {
|
|
|
1204
1499
|
return this._keyedSelections.has(typenameFieldName);
|
|
1205
1500
|
}
|
|
1206
1501
|
|
|
1207
|
-
fieldsInSet():
|
|
1502
|
+
fieldsInSet(): CollectedFieldsInSet {
|
|
1208
1503
|
const fields = new Array<{ path: string[], field: FieldSelection }>();
|
|
1209
1504
|
for (const selection of this.selections()) {
|
|
1210
1505
|
if (selection.kind === 'FieldSelection') {
|
|
@@ -1220,6 +1515,22 @@ export class SelectionSet {
|
|
|
1220
1515
|
return fields;
|
|
1221
1516
|
}
|
|
1222
1517
|
|
|
1518
|
+
fieldsByResponseName(): MultiMap<string, FieldSelection> {
|
|
1519
|
+
const byResponseName = new MultiMap<string, FieldSelection>();
|
|
1520
|
+
this.collectFieldsByResponseName(byResponseName);
|
|
1521
|
+
return byResponseName;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
private collectFieldsByResponseName(collector: MultiMap<string, FieldSelection>) {
|
|
1525
|
+
for (const selection of this.selections()) {
|
|
1526
|
+
if (selection.kind === 'FieldSelection') {
|
|
1527
|
+
collector.add(selection.element.responseName(), selection);
|
|
1528
|
+
} else {
|
|
1529
|
+
selection.selectionSet.collectFieldsByResponseName(collector);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1223
1534
|
usedVariables(): Variables {
|
|
1224
1535
|
const collector = new VariableCollector();
|
|
1225
1536
|
this.collectVariables(collector);
|
|
@@ -1234,7 +1545,7 @@ export class SelectionSet {
|
|
|
1234
1545
|
|
|
1235
1546
|
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
1236
1547
|
for (const selection of this.selections()) {
|
|
1237
|
-
|
|
1548
|
+
selection.collectUsedFragmentNames(collector);
|
|
1238
1549
|
}
|
|
1239
1550
|
}
|
|
1240
1551
|
|
|
@@ -1259,44 +1570,104 @@ export class SelectionSet {
|
|
|
1259
1570
|
// With that, `optimizeSelections` will correctly match on the `on Query` fragment; after which
|
|
1260
1571
|
// we can unpack the final result.
|
|
1261
1572
|
const wrapped = new InlineFragmentSelection(new FragmentElement(this.parentType, this.parentType), this);
|
|
1262
|
-
const
|
|
1573
|
+
const validator = FieldsConflictValidator.build(this);
|
|
1574
|
+
const optimized = wrapped.optimize(fragments, validator);
|
|
1263
1575
|
|
|
1264
1576
|
// Now, it's possible we matched a full fragment, in which case `optimized` will be just the named fragment,
|
|
1265
1577
|
// and in that case we return a singleton selection with just that. Otherwise, it's our wrapping inline fragment
|
|
1266
1578
|
// with the sub-selections optimized, and we just return that subselection.
|
|
1267
1579
|
return optimized instanceof FragmentSpreadSelection
|
|
1268
|
-
? selectionSetOf(this.parentType, optimized
|
|
1580
|
+
? selectionSetOf(this.parentType, optimized)
|
|
1269
1581
|
: optimized.selectionSet;
|
|
1270
1582
|
}
|
|
1271
1583
|
|
|
1272
1584
|
// Tries to match fragments inside each selections of this selection set, and this recursively. However, note that this
|
|
1273
1585
|
// may not match fragments that would apply at top-level, so you should usually use `optimize` instead (this exists mostly
|
|
1274
1586
|
// for the recursion).
|
|
1275
|
-
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 });
|
|
1587
|
+
optimizeSelections(fragments: NamedFragments, validator: FieldsConflictValidator): SelectionSet {
|
|
1588
|
+
return this.lazyMap((selection) => selection.optimize(fragments, validator));
|
|
1284
1589
|
}
|
|
1285
1590
|
|
|
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 });
|
|
1591
|
+
expandFragments(updatedFragments?: NamedFragments): SelectionSet {
|
|
1592
|
+
return this.lazyMap((selection) => selection.expandFragments(updatedFragments));
|
|
1296
1593
|
}
|
|
1297
1594
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1595
|
+
/**
|
|
1596
|
+
* Applies some normalization rules to this selection set in the context of the provided `parentType`.
|
|
1597
|
+
*
|
|
1598
|
+
* Normalization mostly removes unecessary/redundant inline fragments, so that for instance, with
|
|
1599
|
+
* schema:
|
|
1600
|
+
* ```graphql
|
|
1601
|
+
* type Query {
|
|
1602
|
+
* t1: T1
|
|
1603
|
+
* i: I
|
|
1604
|
+
* }
|
|
1605
|
+
*
|
|
1606
|
+
* interface I {
|
|
1607
|
+
* id: ID!
|
|
1608
|
+
* }
|
|
1609
|
+
*
|
|
1610
|
+
* type T1 implements I {
|
|
1611
|
+
* id: ID!
|
|
1612
|
+
* v1: Int
|
|
1613
|
+
* }
|
|
1614
|
+
*
|
|
1615
|
+
* type T2 implements I {
|
|
1616
|
+
* id: ID!
|
|
1617
|
+
* v2: Int
|
|
1618
|
+
* }
|
|
1619
|
+
* ```
|
|
1620
|
+
*
|
|
1621
|
+
* ```
|
|
1622
|
+
* normalize({
|
|
1623
|
+
* t1 {
|
|
1624
|
+
* ... on I {
|
|
1625
|
+
* id
|
|
1626
|
+
* }
|
|
1627
|
+
* }
|
|
1628
|
+
* i {
|
|
1629
|
+
* ... on T1 {
|
|
1630
|
+
* ... on I {
|
|
1631
|
+
* ... on T1 {
|
|
1632
|
+
* v1
|
|
1633
|
+
* }
|
|
1634
|
+
* ... on T2 {
|
|
1635
|
+
* v2
|
|
1636
|
+
* }
|
|
1637
|
+
* }
|
|
1638
|
+
* }
|
|
1639
|
+
* ... on T2 {
|
|
1640
|
+
* ... on I {
|
|
1641
|
+
* id
|
|
1642
|
+
* }
|
|
1643
|
+
* }
|
|
1644
|
+
* }
|
|
1645
|
+
* }) === {
|
|
1646
|
+
* t1 {
|
|
1647
|
+
* id
|
|
1648
|
+
* }
|
|
1649
|
+
* i {
|
|
1650
|
+
* ... on T1 {
|
|
1651
|
+
* v1
|
|
1652
|
+
* }
|
|
1653
|
+
* ... on T2 {
|
|
1654
|
+
* id
|
|
1655
|
+
* }
|
|
1656
|
+
* }
|
|
1657
|
+
* }
|
|
1658
|
+
* ```
|
|
1659
|
+
*
|
|
1660
|
+
* For this operation to be valid (to not throw), `parentType` must be such this selection set would
|
|
1661
|
+
* be valid as a subselection of an inline fragment `... on parentType { <this selection set> }` (and
|
|
1662
|
+
* so `this.normalize(this.parentType)` is always valid and useful, but it is possible to pass a `parentType`
|
|
1663
|
+
* that is more "restrictive" than the selection current parent type).
|
|
1664
|
+
*
|
|
1665
|
+
* Passing the option `recursive == false` makes the normalization only apply at the top-level, removing
|
|
1666
|
+
* any unecessary top-level inline fragments, possibly multiple layers of them, but we never recurse
|
|
1667
|
+
* inside the sub-selection of an selection that is not removed by the normalization.
|
|
1668
|
+
*/
|
|
1669
|
+
normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): SelectionSet {
|
|
1670
|
+
return this.lazyMap((selection) => selection.normalize({ parentType, recursive }), { parentType });
|
|
1300
1671
|
}
|
|
1301
1672
|
|
|
1302
1673
|
/**
|
|
@@ -1310,14 +1681,10 @@ export class SelectionSet {
|
|
|
1310
1681
|
lazyMap(
|
|
1311
1682
|
mapper: (selection: Selection) => Selection | readonly Selection[] | SelectionSet | undefined,
|
|
1312
1683
|
options?: {
|
|
1313
|
-
fragments?: NamedFragments | null,
|
|
1314
1684
|
parentType?: CompositeType,
|
|
1315
1685
|
}
|
|
1316
1686
|
): SelectionSet {
|
|
1317
1687
|
const selections = this.selections();
|
|
1318
|
-
const updatedFragments = options?.fragments;
|
|
1319
|
-
const newFragments = updatedFragments === undefined ? this.fragments : (updatedFragments ?? undefined);
|
|
1320
|
-
|
|
1321
1688
|
let updatedSelections: SelectionSetUpdates | undefined = undefined;
|
|
1322
1689
|
for (let i = 0; i < selections.length; i++) {
|
|
1323
1690
|
const selection = selections[i];
|
|
@@ -1333,22 +1700,16 @@ export class SelectionSet {
|
|
|
1333
1700
|
}
|
|
1334
1701
|
}
|
|
1335
1702
|
if (!updatedSelections) {
|
|
1336
|
-
return this
|
|
1703
|
+
return this;
|
|
1337
1704
|
}
|
|
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);
|
|
1705
|
+
return updatedSelections.toSelectionSet(options?.parentType ?? this.parentType);
|
|
1343
1706
|
}
|
|
1344
1707
|
|
|
1345
1708
|
withoutDefer(labelsToRemove?: Set<string>): SelectionSet {
|
|
1346
|
-
assert(!this.fragments, 'Not yet supported');
|
|
1347
1709
|
return this.lazyMap((selection) => selection.withoutDefer(labelsToRemove));
|
|
1348
1710
|
}
|
|
1349
1711
|
|
|
1350
1712
|
withNormalizedDefer(normalizer: DeferNormalizer): SelectionSet {
|
|
1351
|
-
assert(!this.fragments, 'Not yet supported');
|
|
1352
1713
|
return this.lazyMap((selection) => selection.withNormalizedDefer(normalizer));
|
|
1353
1714
|
}
|
|
1354
1715
|
|
|
@@ -1357,31 +1718,40 @@ export class SelectionSet {
|
|
|
1357
1718
|
}
|
|
1358
1719
|
|
|
1359
1720
|
/**
|
|
1360
|
-
* Returns the selection
|
|
1721
|
+
* Returns the selection set resulting from filtering out any of the top-level selection that does not match the provided predicate.
|
|
1361
1722
|
*
|
|
1362
|
-
* Please that this method
|
|
1363
|
-
* call `optimize` on the result if you want to re-apply some fragments.
|
|
1723
|
+
* Please that this method does not recurse within sub-selections.
|
|
1364
1724
|
*/
|
|
1365
1725
|
filter(predicate: (selection: Selection) => boolean): SelectionSet {
|
|
1366
|
-
return this.lazyMap((selection) => selection
|
|
1726
|
+
return this.lazyMap((selection) => predicate(selection) ? selection : undefined);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/**
|
|
1730
|
+
* Returns the selection set resulting from "recursively" filtering any selection that does not match the provided predicate.
|
|
1731
|
+
* This method calls `predicate` on every selection of the selection set, not just top-level ones, and apply a "depth-first"
|
|
1732
|
+
* strategy, meaning that when the predicate is call on a given selection, the it is guaranteed that filtering has happened
|
|
1733
|
+
* on all the selections of its sub-selection.
|
|
1734
|
+
*/
|
|
1735
|
+
filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): SelectionSet {
|
|
1736
|
+
return this.lazyMap((selection) => selection.filterRecursiveDepthFirst(predicate));
|
|
1367
1737
|
}
|
|
1368
1738
|
|
|
1369
1739
|
withoutEmptyBranches(): SelectionSet | undefined {
|
|
1370
|
-
const updated = this.
|
|
1740
|
+
const updated = this.filterRecursiveDepthFirst((selection) => selection.selectionSet?.isEmpty() !== true);
|
|
1371
1741
|
return updated.isEmpty() ? undefined : updated;
|
|
1372
1742
|
}
|
|
1373
1743
|
|
|
1374
|
-
rebaseOn(parentType: CompositeType): SelectionSet {
|
|
1744
|
+
rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): SelectionSet {
|
|
1375
1745
|
if (this.parentType === parentType) {
|
|
1376
1746
|
return this;
|
|
1377
1747
|
}
|
|
1378
1748
|
|
|
1379
1749
|
const newSelections = new Map<string, Selection>();
|
|
1380
1750
|
for (const selection of this.selections()) {
|
|
1381
|
-
newSelections.set(selection.key(), selection.rebaseOn(parentType));
|
|
1751
|
+
newSelections.set(selection.key(), selection.rebaseOn(parentType, fragments));
|
|
1382
1752
|
}
|
|
1383
1753
|
|
|
1384
|
-
return new SelectionSet(parentType, newSelections
|
|
1754
|
+
return new SelectionSet(parentType, newSelections);
|
|
1385
1755
|
}
|
|
1386
1756
|
|
|
1387
1757
|
equals(that: SelectionSet): boolean {
|
|
@@ -1402,165 +1772,24 @@ export class SelectionSet {
|
|
|
1402
1772
|
return true;
|
|
1403
1773
|
}
|
|
1404
1774
|
|
|
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
|
-
}
|
|
1775
|
+
contains(that: SelectionSet): ContainsResult {
|
|
1776
|
+
if (that._selections.length > this._selections.length) {
|
|
1777
|
+
return ContainsResult.NOT_CONTAINED;
|
|
1417
1778
|
}
|
|
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!);
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
// We know all the `selections` are basically for the same element (same field or same inline fragment),
|
|
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
1779
|
|
|
1780
|
+
let isEqual = true;
|
|
1511
1781
|
for (const [key, thatSelection] of that._keyedSelections) {
|
|
1512
1782
|
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
|
|
1783
|
+
const selectionResult = thisSelection?.contains(thatSelection);
|
|
1784
|
+
if (selectionResult === undefined || selectionResult === ContainsResult.NOT_CONTAINED) {
|
|
1785
|
+
return ContainsResult.NOT_CONTAINED;
|
|
1520
1786
|
}
|
|
1787
|
+
isEqual &&= selectionResult === ContainsResult.EQUAL;
|
|
1521
1788
|
}
|
|
1522
|
-
return true;
|
|
1523
|
-
}
|
|
1524
1789
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
const that = candidate.selectionSetAtType(parentType);
|
|
1529
|
-
// It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
|
|
1530
|
-
// `parentType` is empty, or contains only `__typename`. For instance, suppose we have
|
|
1531
|
-
// a union `U = A | B | C`, and then a fragment:
|
|
1532
|
-
// ```graphql
|
|
1533
|
-
// fragment F on U {
|
|
1534
|
-
// ... on A {
|
|
1535
|
-
// x
|
|
1536
|
-
// }
|
|
1537
|
-
// ... on b {
|
|
1538
|
-
// y
|
|
1539
|
-
// }
|
|
1540
|
-
// }
|
|
1541
|
-
// ```
|
|
1542
|
-
// It is then possible to apply `F` when the parent type is `C`, but this ends up selecting
|
|
1543
|
-
// nothing at all.
|
|
1544
|
-
//
|
|
1545
|
-
// Returning `contains: true` in those cases is, while not 100% incorrect, at least not productive,
|
|
1546
|
-
// and so we skip right away in that case. This is essentially an optimisation.
|
|
1547
|
-
if (that.isEmpty() || (that.selections().length === 1 && that.selections()[0].isTypenameField())) {
|
|
1548
|
-
return { contains: false };
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
if (this.contains(that)) {
|
|
1552
|
-
// One subtlety here is that at "this" sub-selections may already have been optimized with some fragments. It's
|
|
1553
|
-
// usually ok because `candidate` will also use those fragments, but one fragments that `candidate` can never be
|
|
1554
|
-
// using is itself (the `contains` check is fine with this, but it's harder to deal in `minus`). So we expand
|
|
1555
|
-
// the candidate we're currently looking at in "this" to avoid some issues.
|
|
1556
|
-
let updatedThis = this.expandFragments([candidate.name], this.fragments);
|
|
1557
|
-
if (updatedThis !== this) {
|
|
1558
|
-
updatedThis = updatedThis.trimUnsatisfiableBranches(parentType);
|
|
1559
|
-
}
|
|
1560
|
-
const diff = updatedThis.minus(that);
|
|
1561
|
-
return { contains: true, diff: diff.isEmpty() ? undefined : diff };
|
|
1562
|
-
}
|
|
1563
|
-
return { contains: false };
|
|
1790
|
+
return isEqual && that._selections.length === this._selections.length
|
|
1791
|
+
? ContainsResult.EQUAL
|
|
1792
|
+
: ContainsResult.STRICTLY_CONTAINED;
|
|
1564
1793
|
}
|
|
1565
1794
|
|
|
1566
1795
|
/**
|
|
@@ -1572,29 +1801,38 @@ export class SelectionSet {
|
|
|
1572
1801
|
|
|
1573
1802
|
for (const [key, thisSelection] of this._keyedSelections) {
|
|
1574
1803
|
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
|
-
}
|
|
1804
|
+
if (thatSelection) {
|
|
1805
|
+
const remainder = thisSelection.minus(thatSelection);
|
|
1806
|
+
if (remainder) {
|
|
1807
|
+
updated.add(remainder);
|
|
1588
1808
|
}
|
|
1589
|
-
updated.add(thisSelection);
|
|
1590
1809
|
} else {
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1810
|
+
updated.add(thisSelection);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return updated.toSelectionSet(this.parentType);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
intersectionWith(that: SelectionSet): SelectionSet {
|
|
1817
|
+
if (this.isEmpty()) {
|
|
1818
|
+
return this;
|
|
1819
|
+
}
|
|
1820
|
+
if (that.isEmpty()) {
|
|
1821
|
+
return that;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
const intersection = new SelectionSetUpdates();
|
|
1825
|
+
for (const [key, thisSelection] of this._keyedSelections) {
|
|
1826
|
+
const thatSelection = that._keyedSelections.get(key);
|
|
1827
|
+
if (thatSelection) {
|
|
1828
|
+
const selection = thisSelection.intersectionWith(thatSelection);
|
|
1829
|
+
if (selection) {
|
|
1830
|
+
intersection.add(selection);
|
|
1594
1831
|
}
|
|
1595
1832
|
}
|
|
1596
1833
|
}
|
|
1597
|
-
|
|
1834
|
+
|
|
1835
|
+
return intersection.toSelectionSet(this.parentType);
|
|
1598
1836
|
}
|
|
1599
1837
|
|
|
1600
1838
|
canRebaseOn(parentTypeToTest: CompositeType): boolean {
|
|
@@ -1693,13 +1931,14 @@ export class SelectionSet {
|
|
|
1693
1931
|
toOperationString(
|
|
1694
1932
|
rootKind: SchemaRootKind,
|
|
1695
1933
|
variableDefinitions: VariableDefinitions,
|
|
1934
|
+
fragments: NamedFragments | undefined,
|
|
1696
1935
|
operationName?: string,
|
|
1697
1936
|
expandFragments: boolean = false,
|
|
1698
1937
|
prettyPrint: boolean = true
|
|
1699
1938
|
): string {
|
|
1700
1939
|
const indent = prettyPrint ? '' : undefined;
|
|
1701
|
-
const fragmentsDefinitions = !expandFragments &&
|
|
1702
|
-
?
|
|
1940
|
+
const fragmentsDefinitions = !expandFragments && fragments && !fragments.isEmpty()
|
|
1941
|
+
? fragments.toString(indent) + "\n\n"
|
|
1703
1942
|
: "";
|
|
1704
1943
|
if (rootKind == "query" && !operationName && variableDefinitions.isEmpty()) {
|
|
1705
1944
|
return fragmentsDefinitions + this.toString(expandFragments, true, indent);
|
|
@@ -1813,6 +2052,19 @@ export class SelectionSetUpdates {
|
|
|
1813
2052
|
toSelectionSet(parentType: CompositeType, fragments?: NamedFragments): SelectionSet {
|
|
1814
2053
|
return makeSelectionSet(parentType, this.keyedUpdates, fragments);
|
|
1815
2054
|
}
|
|
2055
|
+
|
|
2056
|
+
toString() {
|
|
2057
|
+
return '{\n'
|
|
2058
|
+
+ [...this.keyedUpdates.entries()].map(([k, updates]) => {
|
|
2059
|
+
const updStr = updates.map((upd) =>
|
|
2060
|
+
upd instanceof AbstractSelection
|
|
2061
|
+
? upd.toString()
|
|
2062
|
+
: `${upd.path} -> ${upd.selections}`
|
|
2063
|
+
);
|
|
2064
|
+
return ` - ${k}: ${updStr}`;
|
|
2065
|
+
}).join('\n')
|
|
2066
|
+
+'\n\}'
|
|
2067
|
+
}
|
|
1816
2068
|
}
|
|
1817
2069
|
|
|
1818
2070
|
function addToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, selections: Selection | SelectionSet | readonly Selection[]) {
|
|
@@ -1889,7 +2141,7 @@ function makeSelection(parentType: CompositeType, updates: SelectionUpdate[], fr
|
|
|
1889
2141
|
|
|
1890
2142
|
// Optimize for the simple case of a single selection, as we don't have to do anything complex to merge the sub-selections.
|
|
1891
2143
|
if (updates.length === 1 && first instanceof AbstractSelection) {
|
|
1892
|
-
return first.rebaseOn(parentType);
|
|
2144
|
+
return first.rebaseOn(parentType, fragments);
|
|
1893
2145
|
}
|
|
1894
2146
|
|
|
1895
2147
|
const element = updateElement(first).rebaseOn(parentType);
|
|
@@ -1936,7 +2188,7 @@ function makeSelectionSet(parentType: CompositeType, keyedUpdates: MultiMap<stri
|
|
|
1936
2188
|
for (const [key, updates] of keyedUpdates.entries()) {
|
|
1937
2189
|
selections.set(key, makeSelection(parentType, updates, fragments));
|
|
1938
2190
|
}
|
|
1939
|
-
return new SelectionSet(parentType, selections
|
|
2191
|
+
return new SelectionSet(parentType, selections);
|
|
1940
2192
|
}
|
|
1941
2193
|
|
|
1942
2194
|
/**
|
|
@@ -2046,14 +2298,14 @@ export function allFieldDefinitionsInSelectionSet(selection: SelectionSet): Fiel
|
|
|
2046
2298
|
return allFields;
|
|
2047
2299
|
}
|
|
2048
2300
|
|
|
2049
|
-
export function selectionSetOf(parentType: CompositeType, selection: Selection
|
|
2301
|
+
export function selectionSetOf(parentType: CompositeType, selection: Selection): SelectionSet {
|
|
2050
2302
|
const map = new Map<string, Selection>()
|
|
2051
2303
|
map.set(selection.key(), selection);
|
|
2052
|
-
return new SelectionSet(parentType, map
|
|
2304
|
+
return new SelectionSet(parentType, map);
|
|
2053
2305
|
}
|
|
2054
2306
|
|
|
2055
|
-
export function selectionSetOfElement(element: OperationElement, subSelection?: SelectionSet
|
|
2056
|
-
return selectionSetOf(element.parentType, selectionOfElement(element, subSelection)
|
|
2307
|
+
export function selectionSetOfElement(element: OperationElement, subSelection?: SelectionSet): SelectionSet {
|
|
2308
|
+
return selectionSetOf(element.parentType, selectionOfElement(element, subSelection));
|
|
2057
2309
|
}
|
|
2058
2310
|
|
|
2059
2311
|
export function selectionOfElement(element: OperationElement, subSelection?: SelectionSet): Selection {
|
|
@@ -2075,13 +2327,13 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2075
2327
|
|
|
2076
2328
|
abstract key(): string;
|
|
2077
2329
|
|
|
2078
|
-
abstract optimize(fragments: NamedFragments): Selection;
|
|
2330
|
+
abstract optimize(fragments: NamedFragments, validator: FieldsConflictValidator): Selection;
|
|
2079
2331
|
|
|
2080
2332
|
abstract toSelectionNode(): SelectionNode;
|
|
2081
2333
|
|
|
2082
2334
|
abstract validate(variableDefinitions: VariableDefinitions): void;
|
|
2083
2335
|
|
|
2084
|
-
abstract rebaseOn(parentType: CompositeType): TOwnType;
|
|
2336
|
+
abstract rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): TOwnType;
|
|
2085
2337
|
|
|
2086
2338
|
get parentType(): CompositeType {
|
|
2087
2339
|
return this.element.parentType;
|
|
@@ -2101,10 +2353,6 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2101
2353
|
this.selectionSet?.collectUsedFragmentNames(collector);
|
|
2102
2354
|
}
|
|
2103
2355
|
|
|
2104
|
-
namedFragments(): NamedFragments | undefined {
|
|
2105
|
-
return this.selectionSet?.fragments;
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
2356
|
abstract withUpdatedComponents(element: TElement, selectionSet: SelectionSet | TIsLeaf): TOwnType;
|
|
2109
2357
|
|
|
2110
2358
|
withUpdatedSelectionSet(selectionSet: SelectionSet | TIsLeaf): TOwnType {
|
|
@@ -2132,11 +2380,13 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2132
2380
|
|
|
2133
2381
|
abstract hasDefer(): boolean;
|
|
2134
2382
|
|
|
2135
|
-
abstract
|
|
2383
|
+
abstract expandFragments(updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
|
|
2136
2384
|
|
|
2137
|
-
abstract
|
|
2385
|
+
abstract normalize(args: { parentType: CompositeType, recursive? : boolean }): TOwnType | SelectionSet | undefined;
|
|
2138
2386
|
|
|
2139
|
-
|
|
2387
|
+
isFragmentSpread(): boolean {
|
|
2388
|
+
return false;
|
|
2389
|
+
}
|
|
2140
2390
|
|
|
2141
2391
|
minus(that: Selection): TOwnType | undefined {
|
|
2142
2392
|
// If there is a subset, then we compute the diff of the subset and add that (if not empty).
|
|
@@ -2150,53 +2400,331 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2150
2400
|
return undefined;
|
|
2151
2401
|
}
|
|
2152
2402
|
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
} {
|
|
2167
|
-
// Field and inline fragment override this, but this should never be called for a spread.
|
|
2168
|
-
assert(false, `UNSUPPORTED`);
|
|
2403
|
+
intersectionWith(that: Selection): TOwnType | undefined {
|
|
2404
|
+
// If there is a subset, then we compute the intersection add that (if not empty).
|
|
2405
|
+
// Otherwise, the intersection is this element.
|
|
2406
|
+
if (this.selectionSet && that.selectionSet) {
|
|
2407
|
+
const subSelectionSetIntersection = this.selectionSet.intersectionWith(that.selectionSet);
|
|
2408
|
+
if (subSelectionSetIntersection.isEmpty()) {
|
|
2409
|
+
return undefined;
|
|
2410
|
+
} else {
|
|
2411
|
+
return this.withUpdatedSelectionSet(subSelectionSetIntersection);
|
|
2412
|
+
}
|
|
2413
|
+
} else {
|
|
2414
|
+
return this.us();
|
|
2415
|
+
}
|
|
2169
2416
|
}
|
|
2170
2417
|
|
|
2171
2418
|
protected tryOptimizeSubselectionWithFragments({
|
|
2172
2419
|
parentType,
|
|
2173
2420
|
subSelection,
|
|
2174
2421
|
fragments,
|
|
2175
|
-
|
|
2422
|
+
validator,
|
|
2423
|
+
canUseFullMatchingFragment,
|
|
2176
2424
|
}: {
|
|
2177
2425
|
parentType: CompositeType,
|
|
2178
2426
|
subSelection: SelectionSet,
|
|
2179
2427
|
fragments: NamedFragments,
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2428
|
+
validator: FieldsConflictValidator,
|
|
2429
|
+
canUseFullMatchingFragment: (match: NamedFragmentDefinition) => boolean,
|
|
2430
|
+
}): SelectionSet | NamedFragmentDefinition {
|
|
2431
|
+
// We limit to fragments whose selection could be applied "directly" at `parentType`, meaning without taking the fragment condition
|
|
2432
|
+
// into account. The idea being that if the fragment condition would be needed inside `parentType`, then that condition will not
|
|
2433
|
+
// have been "normalized away" and so we want for this very call to be called on the fragment whose type _is_ the fragment condition (at
|
|
2434
|
+
// which point, this `maybeApplyingDirectlyAtType` method will apply.
|
|
2435
|
+
// Also note that this is because we have this restriction that calling `expandedSelectionSetAtType` is ok.
|
|
2436
|
+
let candidates = fragments.maybeApplyingDirectlyAtType(parentType);
|
|
2437
|
+
if (candidates.length === 0) {
|
|
2438
|
+
return subSelection;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
// First, we check which of the candidates do apply inside `subSelection`, if any.
|
|
2442
|
+
// If we find a candidate that applies to the whole `subSelection`, then we stop and only return
|
|
2443
|
+
// that one candidate. Otherwise, we cumulate in `applyingFragments` the list of fragments that
|
|
2444
|
+
// applies to a subset of `subSelection`.
|
|
2445
|
+
const applyingFragments: { fragment: NamedFragmentDefinition, atType: FragmentRestrictionAtType }[] = [];
|
|
2446
|
+
for (const candidate of candidates) {
|
|
2447
|
+
const atType = candidate.expandedSelectionSetAtType(parentType);
|
|
2448
|
+
const selectionSetAtType = atType.selectionSet;
|
|
2449
|
+
// It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
|
|
2450
|
+
// `parentType` is empty, or contains only `__typename`. For instance, suppose we have
|
|
2451
|
+
// a union `U = A | B | C`, and then a fragment:
|
|
2452
|
+
// ```graphql
|
|
2453
|
+
// fragment F on U {
|
|
2454
|
+
// ... on A {
|
|
2455
|
+
// x
|
|
2456
|
+
// }
|
|
2457
|
+
// ... on b {
|
|
2458
|
+
// y
|
|
2459
|
+
// }
|
|
2460
|
+
// }
|
|
2461
|
+
// ```
|
|
2462
|
+
// It is then possible to apply `F` when the parent type is `C`, but this ends up selecting
|
|
2463
|
+
// nothing at all.
|
|
2464
|
+
//
|
|
2465
|
+
// Using `F` in those cases is, while not 100% incorrect, at least not productive, and so we
|
|
2466
|
+
// skip it that case. This is essentially an optimisation.
|
|
2467
|
+
if (selectionSetAtType.isEmpty() || (selectionSetAtType.selections().length === 1 && selectionSetAtType.selections()[0].isTypenameField())) {
|
|
2468
|
+
continue;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
const res = subSelection.contains(selectionSetAtType);
|
|
2472
|
+
|
|
2473
|
+
if (res === ContainsResult.EQUAL) {
|
|
2474
|
+
if (canUseFullMatchingFragment(candidate)) {
|
|
2475
|
+
if (!validator.checkCanReuseFragmentAndTrackIt(atType)) {
|
|
2476
|
+
// We cannot use it at all, so no point in adding to `applyingFragments`.
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
return candidate;
|
|
2480
|
+
}
|
|
2481
|
+
// If we're not going to replace the full thing, then same reasoning a below.
|
|
2482
|
+
if (candidate.appliedDirectives.length === 0) {
|
|
2483
|
+
applyingFragments.push({ fragment: candidate, atType});
|
|
2484
|
+
}
|
|
2485
|
+
// Note that if a fragment applies to only a subset of the subSelection, then we really only can use
|
|
2486
|
+
// it if that fragment is defined _without_ directives.
|
|
2487
|
+
} else if (res === ContainsResult.STRICTLY_CONTAINED && candidate.appliedDirectives.length === 0) {
|
|
2488
|
+
applyingFragments.push({ fragment: candidate, atType });
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
if (applyingFragments.length === 0) {
|
|
2493
|
+
return subSelection;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
// We have found the list of fragments that applies to some subset of `subSelection`. In general, we
|
|
2497
|
+
// want to now produce the selection set with spread for those fragments plus any selection that is not
|
|
2498
|
+
// covered by any of the fragments. For instance, suppose that `subselection` is `{ a b c d e }`
|
|
2499
|
+
// and we have found that `fragment F1 on X { a b c }` and `fragment F2 on X { c d }` applies, then
|
|
2500
|
+
// we will generate `{ ...F1 ...F2 e }`.
|
|
2501
|
+
//
|
|
2502
|
+
// In that example, `c` is covered by both fragments. And this is fine in this example as it is
|
|
2503
|
+
// worth using both fragments in general. A special case of this however is if a fragment is entirely
|
|
2504
|
+
// included into another. That is, consider that we now have `fragment F1 on X { a ...F2 }` and
|
|
2505
|
+
// `fragment F2 on X { b c }`. In that case, the code above would still match both `F1 and `F2`,
|
|
2506
|
+
// but as `F1` includes `F2` already, we really want to only use `F1`. So in practice, we filter
|
|
2507
|
+
// away any fragment spread that is known to be included in another one that applies.
|
|
2508
|
+
//
|
|
2509
|
+
// TODO: note that the logic used for this is theoretically a bit sub-optimial. That is, we only
|
|
2510
|
+
// check if one of the fragment happens to directly include a spread for another fragment at
|
|
2511
|
+
// top-level as in the example above. We do this because it is cheap to check and is likely the
|
|
2512
|
+
// most common case of this kind of inclusion. But in theory, we would have
|
|
2513
|
+
// `fragment F1 on X { a b c }` and `fragment F2 on X { b c }`, in which case `F2` is still
|
|
2514
|
+
// included in `F1`, but we'd have to work harder to figure this out and it's unclear it's
|
|
2515
|
+
// a good tradeoff. And while you could argue that it's on the user to define its fragments
|
|
2516
|
+
// a bit more optimally, it's actually a tad more complex because we're looking at fragments
|
|
2517
|
+
// in a particular context/parent type. Consider an interface `I` and:
|
|
2518
|
+
// ```graphql
|
|
2519
|
+
// fragment F3 on I {
|
|
2520
|
+
// ... on X {
|
|
2521
|
+
// a
|
|
2522
|
+
// }
|
|
2523
|
+
// ... on Y {
|
|
2524
|
+
// b
|
|
2525
|
+
// c
|
|
2526
|
+
// }
|
|
2527
|
+
// }
|
|
2528
|
+
//
|
|
2529
|
+
// fragment F4 on I {
|
|
2530
|
+
// ... on Y {
|
|
2531
|
+
// c
|
|
2532
|
+
// }
|
|
2533
|
+
// ... on Z {
|
|
2534
|
+
// d
|
|
2535
|
+
// }
|
|
2536
|
+
// }
|
|
2537
|
+
// ```
|
|
2538
|
+
// In that case, neither fragment include the other per-se. But what if we have sub-selection
|
|
2539
|
+
// `{ b c }` but where parent type is `Y`. In that case, both `F3` and `F4` applies, and in that
|
|
2540
|
+
// particular context, `F3` is fully included in `F4`. Long story short, we'll currently
|
|
2541
|
+
// return `{ ...F3 ...F4 }` in that case, but it would be technically better to return only `F4`.
|
|
2542
|
+
// However, this feels niche, and it might be costly to verify such inclusions, so not doing it
|
|
2543
|
+
// for now.
|
|
2544
|
+
const filteredApplyingFragments = applyingFragments.filter(({ fragment }) => !applyingFragments.some((o) => o.fragment.includes(fragment.name)))
|
|
2545
|
+
|
|
2546
|
+
let notCoveredByFragments = subSelection;
|
|
2547
|
+
const optimized = new SelectionSetUpdates();
|
|
2548
|
+
for (const { fragment, atType} of filteredApplyingFragments) {
|
|
2549
|
+
if (!validator.checkCanReuseFragmentAndTrackIt(atType)) {
|
|
2550
|
+
continue;
|
|
2193
2551
|
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2552
|
+
const notCovered = subSelection.minus(atType.selectionSet);
|
|
2553
|
+
notCoveredByFragments = notCoveredByFragments.intersectionWith(notCovered);
|
|
2554
|
+
optimized.add(new FragmentSpreadSelection(parentType, fragments, fragment, []));
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
return optimized.add(notCoveredByFragments).toSelectionSet(parentType, fragments)
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
class FieldsConflictValidator {
|
|
2562
|
+
private usedSpreadTrimmedPartAtLevel?: FieldsConflictValidator[];
|
|
2563
|
+
|
|
2564
|
+
private constructor(
|
|
2565
|
+
private readonly byResponseName: Map<string, Map<Field, FieldsConflictValidator | null>>,
|
|
2566
|
+
) {
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
static build(s: SelectionSet): FieldsConflictValidator {
|
|
2570
|
+
return FieldsConflictValidator.forLevel(s.fieldsInSet());
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
private static forLevel(level: CollectedFieldsInSet): FieldsConflictValidator {
|
|
2574
|
+
const atLevel = new Map<string, Map<Field, CollectedFieldsInSet | null>>();
|
|
2575
|
+
|
|
2576
|
+
for (const { field } of level) {
|
|
2577
|
+
const responseName = field.element.responseName();
|
|
2578
|
+
let atResponseName = atLevel.get(responseName);
|
|
2579
|
+
if (!atResponseName) {
|
|
2580
|
+
atResponseName = new Map<Field, CollectedFieldsInSet>();
|
|
2581
|
+
atLevel.set(responseName, atResponseName);
|
|
2197
2582
|
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2583
|
+
if (field.selectionSet) {
|
|
2584
|
+
let forField = atResponseName.get(field.element) ?? [];
|
|
2585
|
+
atResponseName.set(field.element, forField.concat(field.selectionSet.fieldsInSet()));
|
|
2586
|
+
} else {
|
|
2587
|
+
atResponseName.set(field.element, null);
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
const byResponseName = new Map<string, Map<Field, FieldsConflictValidator | null>>();
|
|
2592
|
+
for (const [name, level] of atLevel.entries()) {
|
|
2593
|
+
const atResponseName = new Map<Field, FieldsConflictValidator | null>();
|
|
2594
|
+
for (const [field, collectedFields] of level) {
|
|
2595
|
+
const validator = collectedFields ? FieldsConflictValidator.forLevel(collectedFields) : null;
|
|
2596
|
+
atResponseName.set(field, validator);
|
|
2597
|
+
}
|
|
2598
|
+
byResponseName.set(name, atResponseName);
|
|
2599
|
+
}
|
|
2600
|
+
return new FieldsConflictValidator(byResponseName);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
forField(field: Field): FieldsConflictValidator {
|
|
2604
|
+
const validator = this.byResponseName.get(field.responseName())?.get(field);
|
|
2605
|
+
// This should be called on validator built on the exact selection set from field this `field` is coming, so
|
|
2606
|
+
// we should find it or the code is buggy.
|
|
2607
|
+
assert(validator, () => `Should have found validator for ${field}`);
|
|
2608
|
+
return validator;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// At this point, we known that the fragment, restricted to the current parent type, matches a subset of the
|
|
2612
|
+
// sub-selection. However, there is still one case we we cannot use it that we need to check, and this is
|
|
2613
|
+
// if using the fragment would create a field "conflict" (in the sense of the graphQL spec
|
|
2614
|
+
// [`FieldsInSetCanMerge`](https://spec.graphql.org/draft/#FieldsInSetCanMerge())) and thus create an
|
|
2615
|
+
// invalid selection. To be clear, `atType.selectionSet` cannot create a conflict, since it is a subset
|
|
2616
|
+
// of `subSelection` and `subSelection` is valid. *But* there may be some part of the fragment that
|
|
2617
|
+
// is not `atType.selectionSet` due to being "dead branches" for type `parentType`. And while those
|
|
2618
|
+
// branches _are_ "dead" as far as execution goes, the `FieldsInSetCanMerge` validation does not take
|
|
2619
|
+
// this into account (it's 1st step says "including visiting fragments and inline fragments" but has
|
|
2620
|
+
// no logic regarding ignoring any fragment that may not apply due to the intersection of runtimes
|
|
2621
|
+
// between multiple fragment being empty).
|
|
2622
|
+
checkCanReuseFragmentAndTrackIt(fragment: FragmentRestrictionAtType): boolean {
|
|
2623
|
+
// No validator means that everything in the fragment selection was part of the selection we're optimizing
|
|
2624
|
+
// away (by using the fragment), and we know the original selection was ok, so nothing to check.
|
|
2625
|
+
const validator = fragment.validator;
|
|
2626
|
+
if (!validator) {
|
|
2627
|
+
return true;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
if (!this.doMergeWith(validator)) {
|
|
2631
|
+
return false;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
// We need to make sure the trimmed parts of `fragment` merges with the rest of the selection,
|
|
2635
|
+
// but also that it merge with any of the trimmed parts of any fragment we have added already.
|
|
2636
|
+
// Note: this last condition means that if 2 fragment conflict on their "trimmed" parts,
|
|
2637
|
+
// then the choice of which is used can be based on the fragment ordering and selection order,
|
|
2638
|
+
// which may not be optimal. This feels niche enough that we keep it simple for now, but we
|
|
2639
|
+
// can revisit this decision if we run into real cases that justify it (but making it optimal
|
|
2640
|
+
// would be a involved in general, as in theory you could have complex dependencies of fragments
|
|
2641
|
+
// that conflict, even cycles, and you need to take the size of fragments into account to know
|
|
2642
|
+
// what's best; and even then, this could even depend on overall usage, as it can be better to
|
|
2643
|
+
// reuse a fragment that is used in other places, than to use one for which it's the only usage.
|
|
2644
|
+
// Adding to all that the fact that conflict can happen in sibling branches).
|
|
2645
|
+
if (this.usedSpreadTrimmedPartAtLevel) {
|
|
2646
|
+
if (!this.usedSpreadTrimmedPartAtLevel.every((t) => validator.doMergeWith(t))) {
|
|
2647
|
+
return false;
|
|
2648
|
+
}
|
|
2649
|
+
} else {
|
|
2650
|
+
this.usedSpreadTrimmedPartAtLevel = [];
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
// We're good, but track the fragment
|
|
2654
|
+
this.usedSpreadTrimmedPartAtLevel.push(validator);
|
|
2655
|
+
return true;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
doMergeWith(that: FieldsConflictValidator): boolean {
|
|
2659
|
+
for (const [responseName, thisFields] of this.byResponseName.entries()) {
|
|
2660
|
+
const thatFields = that.byResponseName.get(responseName);
|
|
2661
|
+
if (!thatFields) {
|
|
2662
|
+
continue;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
// We're basically checking [FieldInSetCanMerge](https://spec.graphql.org/draft/#FieldsInSetCanMerge()),
|
|
2666
|
+
// but from 2 set of fields (`thisFields` and `thatFields`) of the same response that we know individually
|
|
2667
|
+
// merge already.
|
|
2668
|
+
for (const [thisField, thisValidator] of thisFields.entries()) {
|
|
2669
|
+
for (const [thatField, thatValidator] of thatFields.entries()) {
|
|
2670
|
+
// The `SameResponseShape` test that all fields must pass.
|
|
2671
|
+
if (!typesCanBeMerged(thisField.definition.type!, thatField.definition.type!)) {
|
|
2672
|
+
return false;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
const p1 = thisField.parentType;
|
|
2676
|
+
const p2 = thatField.parentType;
|
|
2677
|
+
if (sameType(p1, p2) || !isObjectType(p1) || !isObjectType(p2)) {
|
|
2678
|
+
// Additional checks of `FieldsInSetCanMerge` when same parent type or one isn't object
|
|
2679
|
+
if (thisField.name !== thatField.name
|
|
2680
|
+
|| !argumentsEquals(thisField.args ?? {}, thatField.args ?? {})
|
|
2681
|
+
|| (thisValidator && thatValidator && !thisValidator.doMergeWith(thatValidator))
|
|
2682
|
+
) {
|
|
2683
|
+
return false;
|
|
2684
|
+
}
|
|
2685
|
+
} else {
|
|
2686
|
+
// Otherwise, the sub-selection must pass [SameResponseShape](https://spec.graphql.org/draft/#SameResponseShape()).
|
|
2687
|
+
if (thisValidator && thatValidator && !thisValidator.hasSameResponseShapeThan(thatValidator)) {
|
|
2688
|
+
return false;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
return true;
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
hasSameResponseShapeThan(that: FieldsConflictValidator): boolean {
|
|
2698
|
+
for (const [responseName, thisFields] of this.byResponseName.entries()) {
|
|
2699
|
+
const thatFields = that.byResponseName.get(responseName);
|
|
2700
|
+
if (!thatFields) {
|
|
2701
|
+
continue;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
for (const [thisField, thisValidator] of thisFields.entries()) {
|
|
2705
|
+
for (const [thatField, thatValidator] of thatFields.entries()) {
|
|
2706
|
+
if (!typesCanBeMerged(thisField.definition.type!, thatField.definition.type!)
|
|
2707
|
+
|| (thisValidator && thatValidator && !thisValidator.hasSameResponseShapeThan(thatValidator))) {
|
|
2708
|
+
return false;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
return true;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
toString(indent: string = ''): string {
|
|
2717
|
+
// For debugging/testing ...
|
|
2718
|
+
return '{\n'
|
|
2719
|
+
+ [...this.byResponseName.entries()].map(([name, byFields]) => {
|
|
2720
|
+
const innerIndent = indent + ' ';
|
|
2721
|
+
return `${innerIndent}${name}: [\n`
|
|
2722
|
+
+ [...byFields.entries()]
|
|
2723
|
+
.map(([field, next]) => `${innerIndent} ${field.parentType}.${field}${next ? next.toString(innerIndent + ' '): ''}`)
|
|
2724
|
+
.join('\n')
|
|
2725
|
+
+ `\n${innerIndent}]`;
|
|
2726
|
+
}).join('\n')
|
|
2727
|
+
+ `\n${indent}}`
|
|
2200
2728
|
}
|
|
2201
2729
|
}
|
|
2202
2730
|
|
|
@@ -2223,6 +2751,9 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2223
2751
|
}
|
|
2224
2752
|
|
|
2225
2753
|
withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
|
|
2754
|
+
if (this.element === field && this.selectionSet === selectionSet) {
|
|
2755
|
+
return this;
|
|
2756
|
+
}
|
|
2226
2757
|
return new FieldSelection(field, selectionSet);
|
|
2227
2758
|
}
|
|
2228
2759
|
|
|
@@ -2230,66 +2761,48 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2230
2761
|
return this.element.key();
|
|
2231
2762
|
}
|
|
2232
2763
|
|
|
2233
|
-
optimize(fragments: NamedFragments): Selection {
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2764
|
+
optimize(fragments: NamedFragments, validator: FieldsConflictValidator): Selection {
|
|
2765
|
+
const fieldBaseType = baseType(this.element.definition.type!);
|
|
2766
|
+
if (!isCompositeType(fieldBaseType) || !this.selectionSet) {
|
|
2767
|
+
return this;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
const fieldValidator = validator.forField(this.element);
|
|
2771
|
+
|
|
2772
|
+
// First, see if we can reuse fragments for the selection of this field.
|
|
2773
|
+
let optimizedSelection = this.selectionSet;
|
|
2774
|
+
if (isCompositeType(fieldBaseType) && this.selectionSet) {
|
|
2237
2775
|
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
2238
2776
|
parentType: fieldBaseType,
|
|
2239
|
-
subSelection:
|
|
2777
|
+
subSelection: this.selectionSet,
|
|
2240
2778
|
fragments,
|
|
2241
|
-
|
|
2242
|
-
//
|
|
2243
|
-
|
|
2244
|
-
fragmentFilter: (f) => f.appliedDirectives.length === 0,
|
|
2779
|
+
validator: fieldValidator,
|
|
2780
|
+
// We can never apply a fragments that has directives on it at the field level.
|
|
2781
|
+
canUseFullMatchingFragment: (fragment) => fragment.appliedDirectives.length === 0,
|
|
2245
2782
|
});
|
|
2246
2783
|
|
|
2247
|
-
|
|
2248
|
-
|
|
2784
|
+
if (optimized instanceof NamedFragmentDefinition) {
|
|
2785
|
+
optimizedSelection = selectionSetOf(fieldBaseType, new FragmentSpreadSelection(fieldBaseType, fragments, optimized, []));
|
|
2786
|
+
} else {
|
|
2787
|
+
optimizedSelection = optimized;
|
|
2788
|
+
}
|
|
2249
2789
|
}
|
|
2250
2790
|
|
|
2791
|
+
// Then, recurse inside the field sub-selection (note that if we matched some fragments above,
|
|
2792
|
+
// this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
|
|
2793
|
+
optimizedSelection = optimizedSelection.optimizeSelections(fragments, fieldValidator);
|
|
2794
|
+
|
|
2251
2795
|
return this.selectionSet === optimizedSelection
|
|
2252
2796
|
? this
|
|
2253
|
-
:
|
|
2797
|
+
: this.withUpdatedSelectionSet(optimizedSelection);
|
|
2254
2798
|
}
|
|
2255
2799
|
|
|
2256
|
-
|
|
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 {};
|
|
2285
|
-
}
|
|
2286
|
-
|
|
2287
|
-
filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
|
|
2800
|
+
filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
|
|
2288
2801
|
if (!this.selectionSet) {
|
|
2289
2802
|
return predicate(this) ? this : undefined;
|
|
2290
2803
|
}
|
|
2291
2804
|
|
|
2292
|
-
const updatedSelectionSet = this.selectionSet.
|
|
2805
|
+
const updatedSelectionSet = this.selectionSet.filterRecursiveDepthFirst(predicate);
|
|
2293
2806
|
const thisWithFilteredSelectionSet = this.selectionSet === updatedSelectionSet
|
|
2294
2807
|
? this
|
|
2295
2808
|
: new FieldSelection(this.element, updatedSelectionSet);
|
|
@@ -2315,7 +2828,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2315
2828
|
* Obviously, this operation will only succeed if this selection (both the field itself and its subselections)
|
|
2316
2829
|
* make sense from the provided parent type. If this is not the case, this method will throw.
|
|
2317
2830
|
*/
|
|
2318
|
-
rebaseOn(parentType: CompositeType): FieldSelection {
|
|
2831
|
+
rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FieldSelection {
|
|
2319
2832
|
if (this.element.parentType === parentType) {
|
|
2320
2833
|
return this;
|
|
2321
2834
|
}
|
|
@@ -2331,7 +2844,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2331
2844
|
}
|
|
2332
2845
|
|
|
2333
2846
|
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));
|
|
2847
|
+
return this.withUpdatedComponents(rebasedElement, this.selectionSet.rebaseOn(rebasedBase, fragments));
|
|
2335
2848
|
}
|
|
2336
2849
|
|
|
2337
2850
|
/**
|
|
@@ -2382,37 +2895,46 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2382
2895
|
return !!this.selectionSet?.hasDefer();
|
|
2383
2896
|
}
|
|
2384
2897
|
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2898
|
+
normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FieldSelection {
|
|
2899
|
+
// This could be an interface field, and if we're normalizing on one of the implementation of that
|
|
2900
|
+
// interface, we want to make sure we use the field of the implementation, as it may in particular
|
|
2901
|
+
// have a more specific type which should propagate to the recursive call to normalize.
|
|
2902
|
+
|
|
2903
|
+
const definition = parentType === this.parentType
|
|
2904
|
+
? this.element.definition
|
|
2905
|
+
: parentType.field(this.element.name);
|
|
2906
|
+
assert(definition, `Cannot normalize ${this.element} at ${parentType} which does not have that field`)
|
|
2388
2907
|
|
|
2389
|
-
|
|
2908
|
+
const element = this.element.definition === definition ? this.element : this.element.withUpdatedDefinition(definition);
|
|
2390
2909
|
if (!this.selectionSet) {
|
|
2391
|
-
return this;
|
|
2910
|
+
return this.withUpdatedElement(element);
|
|
2392
2911
|
}
|
|
2393
2912
|
|
|
2394
|
-
const base =
|
|
2395
|
-
assert(isCompositeType(base), () => `Field ${
|
|
2396
|
-
const
|
|
2913
|
+
const base = element.baseType();
|
|
2914
|
+
assert(isCompositeType(base), () => `Field ${element} should not have a sub-selection`);
|
|
2915
|
+
const normalizedSubSelection = (recursive ?? true) ? this.selectionSet.normalize({ parentType: base }) : this.selectionSet;
|
|
2397
2916
|
// In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
|
|
2398
2917
|
// sub-selection is empty. Which suggest something may be wrong with this part of the query
|
|
2399
2918
|
// intent, but the query was valid while keeping an empty sub-selection isn't. So in that
|
|
2400
2919
|
// case, we just add some "non-included" __typename field just to keep the query valid.
|
|
2401
|
-
if (
|
|
2402
|
-
return
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2920
|
+
if (normalizedSubSelection?.isEmpty()) {
|
|
2921
|
+
return this.withUpdatedComponents(
|
|
2922
|
+
element,
|
|
2923
|
+
selectionSetOfElement(
|
|
2924
|
+
new Field(
|
|
2925
|
+
base.typenameField()!,
|
|
2926
|
+
undefined,
|
|
2927
|
+
[new Directive('include', { 'if': false })],
|
|
2928
|
+
)
|
|
2407
2929
|
)
|
|
2408
|
-
)
|
|
2930
|
+
);
|
|
2409
2931
|
} else {
|
|
2410
|
-
return
|
|
2932
|
+
return this.withUpdatedComponents(element, normalizedSubSelection);
|
|
2411
2933
|
}
|
|
2412
2934
|
}
|
|
2413
2935
|
|
|
2414
|
-
expandFragments(
|
|
2415
|
-
return this.mapToSelectionSet((s) => s.expandFragments(
|
|
2936
|
+
expandFragments(updatedFragments?: NamedFragments): FieldSelection {
|
|
2937
|
+
return this.mapToSelectionSet((s) => s.expandFragments(updatedFragments));
|
|
2416
2938
|
}
|
|
2417
2939
|
|
|
2418
2940
|
equals(that: Selection): boolean {
|
|
@@ -2429,20 +2951,17 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2429
2951
|
return !!that.selectionSet && this.selectionSet.equals(that.selectionSet);
|
|
2430
2952
|
}
|
|
2431
2953
|
|
|
2432
|
-
contains(that: Selection):
|
|
2954
|
+
contains(that: Selection): ContainsResult {
|
|
2433
2955
|
if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
|
|
2434
|
-
return
|
|
2956
|
+
return ContainsResult.NOT_CONTAINED;
|
|
2435
2957
|
}
|
|
2436
2958
|
|
|
2437
|
-
if (!
|
|
2438
|
-
|
|
2959
|
+
if (!this.selectionSet) {
|
|
2960
|
+
assert(!that.selectionSet, '`this` and `that` have the same element, so if one does not have a sub-selection, neither should the other one')
|
|
2961
|
+
return ContainsResult.EQUAL;
|
|
2439
2962
|
}
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
isUnecessaryInlineFragment(_: CompositeType): this is InlineFragmentSelection {
|
|
2444
|
-
// Overridden by inline fragments
|
|
2445
|
-
return false;
|
|
2963
|
+
assert(that.selectionSet, '`this` and `that` have the same element, so if one has sub-selection, the other one should too')
|
|
2964
|
+
return this.selectionSet.contains(that.selectionSet);
|
|
2446
2965
|
}
|
|
2447
2966
|
|
|
2448
2967
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
@@ -2470,11 +2989,10 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
|
|
|
2470
2989
|
}
|
|
2471
2990
|
}
|
|
2472
2991
|
|
|
2473
|
-
|
|
2992
|
+
filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
|
|
2474
2993
|
// Note that we essentially expand all fragments as part of this.
|
|
2475
|
-
const
|
|
2476
|
-
const
|
|
2477
|
-
const thisWithFilteredSelectionSet = updatedSelectionSet === selectionSet
|
|
2994
|
+
const updatedSelectionSet = this.selectionSet.filterRecursiveDepthFirst(predicate);
|
|
2995
|
+
const thisWithFilteredSelectionSet = updatedSelectionSet === this.selectionSet
|
|
2478
2996
|
? this
|
|
2479
2997
|
: new InlineFragmentSelection(this.element, updatedSelectionSet);
|
|
2480
2998
|
|
|
@@ -2487,16 +3005,7 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
|
|
|
2487
3005
|
|
|
2488
3006
|
abstract equals(that: Selection): boolean;
|
|
2489
3007
|
|
|
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
|
-
}
|
|
3008
|
+
abstract contains(that: Selection): ContainsResult;
|
|
2500
3009
|
}
|
|
2501
3010
|
|
|
2502
3011
|
class InlineFragmentSelection extends FragmentSelection {
|
|
@@ -2516,6 +3025,9 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2516
3025
|
}
|
|
2517
3026
|
|
|
2518
3027
|
withUpdatedComponents(fragment: FragmentElement, selectionSet: SelectionSet): InlineFragmentSelection {
|
|
3028
|
+
if (fragment === this.element && selectionSet === this.selectionSet) {
|
|
3029
|
+
return this;
|
|
3030
|
+
}
|
|
2519
3031
|
return new InlineFragmentSelection(fragment, selectionSet);
|
|
2520
3032
|
}
|
|
2521
3033
|
|
|
@@ -2530,7 +3042,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2530
3042
|
this.selectionSet.validate(variableDefinitions);
|
|
2531
3043
|
}
|
|
2532
3044
|
|
|
2533
|
-
rebaseOn(parentType: CompositeType): FragmentSelection {
|
|
3045
|
+
rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
|
|
2534
3046
|
if (this.parentType === parentType) {
|
|
2535
3047
|
return this;
|
|
2536
3048
|
}
|
|
@@ -2541,7 +3053,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2541
3053
|
return this.withUpdatedElement(rebasedFragment);
|
|
2542
3054
|
}
|
|
2543
3055
|
|
|
2544
|
-
return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType));
|
|
3056
|
+
return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType, fragments));
|
|
2545
3057
|
}
|
|
2546
3058
|
|
|
2547
3059
|
canAddTo(parentType: CompositeType): boolean {
|
|
@@ -2578,87 +3090,62 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2578
3090
|
};
|
|
2579
3091
|
}
|
|
2580
3092
|
|
|
2581
|
-
optimize(fragments: NamedFragments): FragmentSelection {
|
|
2582
|
-
let optimizedSelection = this.selectionSet
|
|
3093
|
+
optimize(fragments: NamedFragments, validator: FieldsConflictValidator): FragmentSelection {
|
|
3094
|
+
let optimizedSelection = this.selectionSet;
|
|
3095
|
+
|
|
3096
|
+
// First, see if we can reuse fragments for the selection of this field.
|
|
2583
3097
|
const typeCondition = this.element.typeCondition;
|
|
2584
3098
|
if (typeCondition) {
|
|
2585
3099
|
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
2586
3100
|
parentType: typeCondition,
|
|
2587
3101
|
subSelection: optimizedSelection,
|
|
2588
3102
|
fragments,
|
|
3103
|
+
validator,
|
|
3104
|
+
canUseFullMatchingFragment: (fragment) => {
|
|
3105
|
+
// To be able to use a matching fragment, it needs to have either no directives, or if it has
|
|
3106
|
+
// some, then:
|
|
3107
|
+
// 1. all it's directives should also be on the current element.
|
|
3108
|
+
// 2. the directives of this element should be the fragment condition.
|
|
3109
|
+
// because if those 2 conditions are true, we can replace the whole current inline fragment
|
|
3110
|
+
// with the match spread and directives will still match.
|
|
3111
|
+
return fragment.appliedDirectives.length === 0
|
|
3112
|
+
|| (
|
|
3113
|
+
sameType(typeCondition, fragment.typeCondition)
|
|
3114
|
+
&& fragment.appliedDirectives.every((d) => this.element.appliedDirectives.some((s) => sameDirectiveApplication(d, s)))
|
|
3115
|
+
);
|
|
3116
|
+
},
|
|
2589
3117
|
});
|
|
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
3118
|
|
|
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;
|
|
3119
|
+
if (optimized instanceof NamedFragmentDefinition) {
|
|
3120
|
+
// We're fully matching the sub-selection. If the fragment condition is also this element condition,
|
|
3121
|
+
// then we can replace the whole element by the spread (not just the sub-selection).
|
|
3122
|
+
if (sameType(typeCondition, optimized.typeCondition)) {
|
|
3123
|
+
// Note that `canUseFullMatchingFragment` above guarantees us that this element directives
|
|
3124
|
+
// are a superset of the fragment directives. But there can be additional directives, and in
|
|
3125
|
+
// that case they should be kept on the spread.
|
|
3126
|
+
let spreadDirectives = this.element.appliedDirectives;
|
|
3127
|
+
if (optimized.appliedDirectives) {
|
|
3128
|
+
spreadDirectives = spreadDirectives.filter(
|
|
3129
|
+
(s) => !optimized.appliedDirectives.some((d) => sameDirectiveApplication(d, s))
|
|
3130
|
+
);
|
|
2639
3131
|
}
|
|
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;
|
|
3132
|
+
return new FragmentSpreadSelection(this.parentType, fragments, optimized, spreadDirectives);
|
|
3133
|
+
} else {
|
|
3134
|
+
// Otherwise, we keep this element and use a sub-selection with just the spread.
|
|
3135
|
+
optimizedSelection = selectionSetOf(typeCondition, new FragmentSpreadSelection(typeCondition, fragments, optimized, []));
|
|
2651
3136
|
}
|
|
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 };
|
|
3137
|
+
} else {
|
|
3138
|
+
optimizedSelection = optimized;
|
|
2659
3139
|
}
|
|
2660
3140
|
}
|
|
2661
|
-
|
|
3141
|
+
|
|
3142
|
+
// Then, recurse inside the field sub-selection (note that if we matched some fragments above,
|
|
3143
|
+
// this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
|
|
3144
|
+
optimizedSelection = optimizedSelection.optimizeSelections(fragments, validator);
|
|
3145
|
+
|
|
3146
|
+
return this.selectionSet === optimizedSelection
|
|
3147
|
+
? this
|
|
3148
|
+
: new InlineFragmentSelection(this.element, optimizedSelection);
|
|
2662
3149
|
}
|
|
2663
3150
|
|
|
2664
3151
|
withoutDefer(labelsToRemove?: Set<string>): InlineFragmentSelection | SelectionSet {
|
|
@@ -2686,55 +3173,60 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2686
3173
|
: this.withUpdatedComponents(newElement, newSelection);
|
|
2687
3174
|
}
|
|
2688
3175
|
|
|
2689
|
-
|
|
2690
|
-
const recursive = options?.recursive ?? true;
|
|
2691
|
-
|
|
3176
|
+
normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
|
|
2692
3177
|
const thisCondition = this.element.typeCondition;
|
|
2693
|
-
// Note that if the condition has directives, we preserve the fragment no matter what.
|
|
2694
|
-
if (this.element.appliedDirectives.length === 0) {
|
|
2695
|
-
if (!thisCondition || currentType === this.element.typeCondition) {
|
|
2696
|
-
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType, options);
|
|
2697
|
-
return trimmed.isEmpty() ? undefined : trimmed;
|
|
2698
|
-
}
|
|
2699
3178
|
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
if (
|
|
2708
|
-
|
|
2709
|
-
return undefined;
|
|
2710
|
-
} else {
|
|
2711
|
-
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType, options);
|
|
2712
|
-
return trimmed.isEmpty() ? undefined : trimmed;
|
|
2713
|
-
}
|
|
3179
|
+
// This method assumes by contract that `parentType` runtimes intersects `this.parentType`'s, but `parentType`
|
|
3180
|
+
// runtimes may be a subset. So first check if the selection should not be discarded on that account (that
|
|
3181
|
+
// is, we should not keep the selection if its condition runtimes don't intersect at all with those of
|
|
3182
|
+
// `parentType` as that would ultimately make an invalid selection set).
|
|
3183
|
+
if (thisCondition && parentType !== this.parentType) {
|
|
3184
|
+
const conditionRuntimes = possibleRuntimeTypes(thisCondition);
|
|
3185
|
+
const typeRuntimes = possibleRuntimeTypes(parentType);
|
|
3186
|
+
if (!conditionRuntimes.some((t) => typeRuntimes.includes(t))) {
|
|
3187
|
+
return undefined;
|
|
2714
3188
|
}
|
|
2715
3189
|
}
|
|
2716
3190
|
|
|
2717
|
-
//
|
|
2718
|
-
|
|
2719
|
-
|
|
3191
|
+
// We know the condition is "valid", but it may not be useful. That said, if the condition has directives,
|
|
3192
|
+
// we preserve the fragment no matter what.
|
|
3193
|
+
if (this.element.appliedDirectives.length === 0) {
|
|
3194
|
+
// There is a number of cases where a fragment is not useful:
|
|
3195
|
+
// 1. if there is not conditions (remember it also has no directives).
|
|
3196
|
+
// 2. if it's the same type as the current type: it's not restricting types further.
|
|
3197
|
+
// 3. if the current type is an object more generally: because in that case too the condition
|
|
3198
|
+
// cannot be restricting things further (it's typically a less precise interface/union).
|
|
3199
|
+
if (!thisCondition || parentType === this.element.typeCondition || isObjectType(parentType)) {
|
|
3200
|
+
const normalized = this.selectionSet.normalize({ parentType, recursive });
|
|
3201
|
+
return normalized.isEmpty() ? undefined : normalized;
|
|
3202
|
+
}
|
|
2720
3203
|
}
|
|
2721
3204
|
|
|
2722
|
-
//
|
|
2723
|
-
|
|
3205
|
+
// We preserve the current fragment, so we only recurse within the sub-selection if we're asked to be recusive.
|
|
3206
|
+
// (note that even if we're not recursive, we may still have some "lifting" to do)
|
|
3207
|
+
let normalizedSelectionSet: SelectionSet;
|
|
3208
|
+
if (recursive ?? true) {
|
|
3209
|
+
normalizedSelectionSet = this.selectionSet.normalize({ parentType: thisCondition ?? parentType });
|
|
2724
3210
|
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
(
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
3211
|
+
// It could be that everything was unsatisfiable.
|
|
3212
|
+
if (normalizedSelectionSet.isEmpty()) {
|
|
3213
|
+
if (this.element.appliedDirectives.length === 0) {
|
|
3214
|
+
return undefined;
|
|
3215
|
+
} else {
|
|
3216
|
+
return this.withUpdatedComponents(
|
|
3217
|
+
this.element.rebaseOn(parentType),
|
|
3218
|
+
selectionSetOfElement(
|
|
3219
|
+
new Field(
|
|
3220
|
+
(this.element.typeCondition ?? parentType).typenameField()!,
|
|
3221
|
+
undefined,
|
|
3222
|
+
[new Directive('include', { 'if': false })],
|
|
3223
|
+
)
|
|
3224
|
+
)
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
2737
3227
|
}
|
|
3228
|
+
} else {
|
|
3229
|
+
normalizedSelectionSet = this.selectionSet;
|
|
2738
3230
|
}
|
|
2739
3231
|
|
|
2740
3232
|
// Second, we check if some of the sub-selection fragments can be "lifted" outside of this fragment. This can happen if:
|
|
@@ -2742,10 +3234,10 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2742
3234
|
// 2. the sub-fragment is an object type,
|
|
2743
3235
|
// 3. the sub-fragment type is a valid runtime of the current type.
|
|
2744
3236
|
if (this.element.appliedDirectives.length === 0 && isAbstractType(thisCondition!)) {
|
|
2745
|
-
assert(!isObjectType(
|
|
2746
|
-
const currentRuntimes = possibleRuntimeTypes(
|
|
3237
|
+
assert(!isObjectType(parentType), () => `Should not have got here if ${parentType} is an object type`);
|
|
3238
|
+
const currentRuntimes = possibleRuntimeTypes(parentType);
|
|
2747
3239
|
const liftableSelections: Selection[] = [];
|
|
2748
|
-
for (const selection of
|
|
3240
|
+
for (const selection of normalizedSelectionSet.selections()) {
|
|
2749
3241
|
if (selection.kind === 'FragmentSelection'
|
|
2750
3242
|
&& selection.element.typeCondition
|
|
2751
3243
|
&& isObjectType(selection.element.typeCondition)
|
|
@@ -2756,8 +3248,8 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2756
3248
|
}
|
|
2757
3249
|
|
|
2758
3250
|
// If we can lift all selections, then that just mean we can get rid of the current fragment altogether
|
|
2759
|
-
if (liftableSelections.length ===
|
|
2760
|
-
return
|
|
3251
|
+
if (liftableSelections.length === normalizedSelectionSet.selections().length) {
|
|
3252
|
+
return normalizedSelectionSet;
|
|
2761
3253
|
}
|
|
2762
3254
|
|
|
2763
3255
|
// Otherwise, if there is "liftable" selections, we must return a set comprised of those lifted selection,
|
|
@@ -2766,21 +3258,19 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2766
3258
|
const newSet = new SelectionSetUpdates();
|
|
2767
3259
|
newSet.add(liftableSelections);
|
|
2768
3260
|
newSet.add(this.withUpdatedSelectionSet(
|
|
2769
|
-
|
|
3261
|
+
normalizedSelectionSet.filter((s) => !liftableSelections.includes(s)),
|
|
2770
3262
|
));
|
|
2771
|
-
return newSet.toSelectionSet(
|
|
3263
|
+
return newSet.toSelectionSet(parentType);
|
|
2772
3264
|
}
|
|
2773
3265
|
}
|
|
2774
3266
|
|
|
2775
|
-
return this.
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
expandAllFragments(): FragmentSelection {
|
|
2779
|
-
return this.mapToSelectionSet((s) => s.expandAllFragments());
|
|
3267
|
+
return this.parentType === parentType && this.selectionSet === normalizedSelectionSet
|
|
3268
|
+
? this
|
|
3269
|
+
: this.withUpdatedComponents(this.element.rebaseOn(parentType), normalizedSelectionSet);
|
|
2780
3270
|
}
|
|
2781
3271
|
|
|
2782
|
-
expandFragments(
|
|
2783
|
-
return this.mapToSelectionSet((s) => s.expandFragments(
|
|
3272
|
+
expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection {
|
|
3273
|
+
return this.mapToSelectionSet((s) => s.expandFragments(updatedFragments));
|
|
2784
3274
|
}
|
|
2785
3275
|
|
|
2786
3276
|
equals(that: Selection): boolean {
|
|
@@ -2793,10 +3283,12 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2793
3283
|
&& this.selectionSet.equals(that.selectionSet);
|
|
2794
3284
|
}
|
|
2795
3285
|
|
|
2796
|
-
contains(that: Selection):
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
3286
|
+
contains(that: Selection): ContainsResult {
|
|
3287
|
+
if (!(that instanceof FragmentSelection) || !this.element.equals(that.element)) {
|
|
3288
|
+
return ContainsResult.NOT_CONTAINED;
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
return this.selectionSet.contains(that.selectionSet);
|
|
2800
3292
|
}
|
|
2801
3293
|
|
|
2802
3294
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
@@ -2804,14 +3296,6 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2804
3296
|
}
|
|
2805
3297
|
}
|
|
2806
3298
|
|
|
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
3299
|
class FragmentSpreadSelection extends FragmentSelection {
|
|
2816
3300
|
private computedKey: string | undefined;
|
|
2817
3301
|
|
|
@@ -2824,6 +3308,10 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2824
3308
|
super(new FragmentElement(sourceType, namedFragment.typeCondition, namedFragment.appliedDirectives.concat(spreadDirectives)));
|
|
2825
3309
|
}
|
|
2826
3310
|
|
|
3311
|
+
isFragmentSpread(): boolean {
|
|
3312
|
+
return true;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
2827
3315
|
get selectionSet(): SelectionSet {
|
|
2828
3316
|
return this.namedFragment.selectionSet;
|
|
2829
3317
|
}
|
|
@@ -2839,20 +3327,20 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2839
3327
|
assert(false, `Unsupported`);
|
|
2840
3328
|
}
|
|
2841
3329
|
|
|
2842
|
-
|
|
3330
|
+
normalize({ parentType }: { parentType: CompositeType }): FragmentSelection {
|
|
2843
3331
|
// We must update the spread parent type if necessary since we're not going deeper,
|
|
2844
3332
|
// or we'll be fundamentally losing context.
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
namedFragments(): NamedFragments | undefined {
|
|
2849
|
-
return this.fragments;
|
|
3333
|
+
assert(parentType.schema() === this.parentType.schema(), 'Should not try to normalize using a type from another schema');
|
|
3334
|
+
return this.rebaseOn(parentType, this.fragments);
|
|
2850
3335
|
}
|
|
2851
3336
|
|
|
2852
3337
|
validate(): void {
|
|
2853
3338
|
this.validateDeferAndStream();
|
|
2854
3339
|
|
|
2855
|
-
|
|
3340
|
+
validate(
|
|
3341
|
+
runtimeTypesIntersects(this.parentType, this.namedFragment.typeCondition),
|
|
3342
|
+
() => `Fragment "${this.namedFragment.name}" cannot be spread inside type ${this.parentType} as the runtime types do not intersect ${this.namedFragment.typeCondition}`
|
|
3343
|
+
);
|
|
2856
3344
|
}
|
|
2857
3345
|
|
|
2858
3346
|
toSelectionNode(): FragmentSpreadNode {
|
|
@@ -2875,11 +3363,11 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2875
3363
|
};
|
|
2876
3364
|
}
|
|
2877
3365
|
|
|
2878
|
-
optimize(
|
|
3366
|
+
optimize(_1: NamedFragments, _2: FieldsConflictValidator): FragmentSelection {
|
|
2879
3367
|
return this;
|
|
2880
3368
|
}
|
|
2881
3369
|
|
|
2882
|
-
rebaseOn(parentType: CompositeType): FragmentSelection {
|
|
3370
|
+
rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
|
|
2883
3371
|
// We preserve the parent type here, to make sure we don't lose context, but we actually don't
|
|
2884
3372
|
// want to expand the spread as that would compromise the code that optimize subgraph fetches to re-use named
|
|
2885
3373
|
// fragments.
|
|
@@ -2892,10 +3380,18 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2892
3380
|
if (this.parentType === parentType) {
|
|
2893
3381
|
return this;
|
|
2894
3382
|
}
|
|
3383
|
+
|
|
3384
|
+
// If we're rebasing on a _different_ schema, then we *must* have fragments, since reusing
|
|
3385
|
+
// `this.fragments` would be incorrect. If we're on the same schema though, we're happy to default
|
|
3386
|
+
// to `this.fragments`.
|
|
3387
|
+
assert(fragments || this.parentType.schema() === parentType.schema(), `Must provide fragments is rebasing on other schema`);
|
|
3388
|
+
const newFragments = fragments ?? this.fragments;
|
|
3389
|
+
const namedFragment = newFragments.get(this.namedFragment.name);
|
|
3390
|
+
assert(namedFragment, () => `Cannot rebase ${this} if it isn't part of the provided fragments`);
|
|
2895
3391
|
return new FragmentSpreadSelection(
|
|
2896
3392
|
parentType,
|
|
2897
|
-
|
|
2898
|
-
|
|
3393
|
+
newFragments,
|
|
3394
|
+
namedFragment,
|
|
2899
3395
|
this.spreadDirectives,
|
|
2900
3396
|
);
|
|
2901
3397
|
}
|
|
@@ -2906,26 +3402,20 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2906
3402
|
return true;
|
|
2907
3403
|
}
|
|
2908
3404
|
|
|
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)) {
|
|
3405
|
+
expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection | readonly Selection[] {
|
|
3406
|
+
// Note that this test will always fail if `updatedFragments` is `undefined`, making us expand everything.
|
|
3407
|
+
if (updatedFragments?.has(this.namedFragment.name)) {
|
|
3408
|
+
// This one is still there, it's not expanded.
|
|
2918
3409
|
return this;
|
|
2919
3410
|
}
|
|
2920
3411
|
|
|
2921
|
-
const expandedSubSelections = this.selectionSet.expandFragments(
|
|
3412
|
+
const expandedSubSelections = this.selectionSet.expandFragments(updatedFragments);
|
|
2922
3413
|
return sameType(this.parentType, this.namedFragment.typeCondition) && this.element.appliedDirectives.length === 0
|
|
2923
3414
|
? expandedSubSelections.selections()
|
|
2924
3415
|
: new InlineFragmentSelection(this.element, expandedSubSelections);
|
|
2925
3416
|
}
|
|
2926
3417
|
|
|
2927
3418
|
collectUsedFragmentNames(collector: Map<string, number>): void {
|
|
2928
|
-
this.selectionSet.collectUsedFragmentNames(collector);
|
|
2929
3419
|
const usageCount = collector.get(this.namedFragment.name);
|
|
2930
3420
|
collector.set(this.namedFragment.name, usageCount === undefined ? 1 : usageCount + 1);
|
|
2931
3421
|
}
|
|
@@ -2953,14 +3443,16 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2953
3443
|
&& sameDirectiveApplications(this.spreadDirectives, that.spreadDirectives);
|
|
2954
3444
|
}
|
|
2955
3445
|
|
|
2956
|
-
contains(that: Selection):
|
|
3446
|
+
contains(that: Selection): ContainsResult {
|
|
2957
3447
|
if (this.equals(that)) {
|
|
2958
|
-
return
|
|
3448
|
+
return ContainsResult.EQUAL;
|
|
2959
3449
|
}
|
|
2960
3450
|
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3451
|
+
if (!(that instanceof FragmentSelection) || !this.element.equals(that.element)) {
|
|
3452
|
+
return ContainsResult.NOT_CONTAINED;
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
return this.selectionSet.contains(that.selectionSet);
|
|
2964
3456
|
}
|
|
2965
3457
|
|
|
2966
3458
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
@@ -2985,7 +3477,6 @@ function selectionSetOfNode(
|
|
|
2985
3477
|
return selectionSetOf(
|
|
2986
3478
|
parentType,
|
|
2987
3479
|
selectionOfNode(parentType, node.selections[0], variableDefinitions, fragments, fieldAccessor),
|
|
2988
|
-
fragments,
|
|
2989
3480
|
);
|
|
2990
3481
|
}
|
|
2991
3482
|
|
|
@@ -3117,6 +3608,7 @@ function operationFromAST({
|
|
|
3117
3608
|
}) : Operation {
|
|
3118
3609
|
const rootType = schema.schemaDefinition.root(operation.operation);
|
|
3119
3610
|
validate(rootType, () => `The schema has no "${operation.operation}" root type defined`);
|
|
3611
|
+
const fragmentsIfAny = fragments.isEmpty() ? undefined : fragments;
|
|
3120
3612
|
return new Operation(
|
|
3121
3613
|
schema,
|
|
3122
3614
|
operation.operation,
|
|
@@ -3124,10 +3616,11 @@ function operationFromAST({
|
|
|
3124
3616
|
parentType: rootType.type,
|
|
3125
3617
|
source: operation.selectionSet,
|
|
3126
3618
|
variableDefinitions,
|
|
3127
|
-
fragments:
|
|
3619
|
+
fragments: fragmentsIfAny,
|
|
3128
3620
|
validate: validateInput,
|
|
3129
3621
|
}),
|
|
3130
3622
|
variableDefinitions,
|
|
3623
|
+
fragmentsIfAny,
|
|
3131
3624
|
operation.name?.value
|
|
3132
3625
|
);
|
|
3133
3626
|
}
|
|
@@ -3184,8 +3677,8 @@ export function operationToDocument(operation: Operation): DocumentNode {
|
|
|
3184
3677
|
selectionSet: operation.selectionSet.toSelectionSetNode(),
|
|
3185
3678
|
variableDefinitions: operation.variableDefinitions.toVariableDefinitionNodes(),
|
|
3186
3679
|
};
|
|
3187
|
-
const fragmentASTs: DefinitionNode[] = operation.
|
|
3188
|
-
? operation.
|
|
3680
|
+
const fragmentASTs: DefinitionNode[] = operation.fragments
|
|
3681
|
+
? operation.fragments?.toFragmentDefinitionNodes()
|
|
3189
3682
|
: [];
|
|
3190
3683
|
return {
|
|
3191
3684
|
kind: Kind.DOCUMENT,
|