@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/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, isDefined, mapEntries, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
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
- private readonly args?: TArgs,
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
- // TODO Operations can also have directives
700
- export class Operation {
701
- constructor(
702
- readonly schema: Schema,
703
- readonly rootKind: SchemaRootKind,
704
- readonly selectionSet: SelectionSet,
705
- readonly variableDefinitions: VariableDefinitions,
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
- optimize(fragments?: NamedFragments, minUsagesToOptimize: number = 2): Operation {
710
- assert(minUsagesToOptimize >= 1, `Expected 'minUsagesToOptimize' to be at least 1, but got ${minUsagesToOptimize}`)
711
- if (!fragments || fragments.isEmpty()) {
712
- return this;
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
- let optimizedSelection = this.selectionSet.optimize(fragments);
716
- if (optimizedSelection === this.selectionSet) {
717
- return this;
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
- const usages = new Map<string, number>();
721
- optimizedSelection.collectUsedFragmentNames(usages);
722
- for (const fragment of fragments.names()) {
723
- if (!usages.has(fragment)) {
724
- usages.set(fragment, 0);
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
- // We re-expand any fragments that is used less than our minimum. Optimizing all fragments to potentially
729
- // re-expand some is not entirely optimal, but it's simple and probably don't matter too much in practice
730
- // (we only call this optimization on the final computed query plan, so not a very hot path; plus in most
731
- // cases we won't even reach that point either because there is no fragment, or none will have been
732
- // optimized away so we'll exit above). We can optimize later if this show up in profiling though.
733
- //
734
- // Also note `toDeoptimize` will always contains the unused fragments, which will allow `expandFragments`
735
- // to remove them from the listed fragments in `optimizedSelection` (here again, this could make use call
736
- // `expandFragments` on _only_ unused fragments and that case could be dealt with more efficiently, but
737
- // probably not noticeable in practice so ...).
738
- const toDeoptimize = mapEntries(usages).filter(([_, count]) => count < minUsagesToOptimize).map(([name]) => name);
739
-
740
- if (toDeoptimize.length > 0) {
741
- const newFragments = optimizedSelection.fragments?.without(toDeoptimize);
742
- optimizedSelection = optimizedSelection.expandFragments(toDeoptimize, newFragments);
743
- // Expanding fragments could create some "inefficiencies" that we wouldn't have if we hadn't re-optimized
744
- // the fragments to de-optimize it later, so we do a final "trim" pass to remove those.
745
- optimizedSelection = optimizedSelection.trimUnsatisfiableBranches(optimizedSelection.parentType);
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
- return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, this.name);
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
- expandAllFragments(): Operation {
752
- const expandedSelections = this.selectionSet.expandAllFragments();
753
- if (expandedSelections === this.selectionSet) {
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
- expandedSelections,
879
+ newSelectionSet,
761
880
  this.variableDefinitions,
881
+ this.fragments,
762
882
  this.name
763
883
  );
764
884
  }
765
885
 
766
- trimUnsatisfiableBranches(): Operation {
767
- const trimmedSelections = this.selectionSet.trimUnsatisfiableBranches(this.selectionSet.parentType);
768
- if (trimmedSelections === this.selectionSet) {
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
- trimmedSelections,
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
- // If we have named fragments, we should be looking inside those and either expand those having @defer or,
790
- // probably better, replace them with a verison without @defer. But as we currently only call this method
791
- // after `expandAllFragments`, we'll implement this when/if we need it.
792
- assert(!this.selectionSet.fragments || this.selectionSet.fragments.isEmpty(), 'Removing @defer currently only work on "expanded" selections (no named fragments)');
793
- 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
- const updated = this.selectionSet.withNormalizedDefer(normalizer);
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
- private readonly selectionSetsAtTypesCache = new Map<string, SelectionSet>();
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.selectionSet.collectUsedFragmentNames(collector);
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 is if its type condition runtime types intersects with the
907
- * runtimes of the provided type.
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
- canApplyAtType(type: CompositeType): boolean {
912
- return sameType(type, this.typeCondition) || runtimeTypesIntersects(type, this.typeCondition);
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.canApplyAtType(type)` is `true` (and may crash if this is not true), and returns
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
- selectionSetAtType(type: CompositeType): SelectionSet {
934
- // First, if the candidate condition is an object or is the type passed, then there isn't any additional restriction to do.
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.selectionSet;
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
- // We should not call `trimUnsatisfiableBranches` where `type` is an abstract type (`interface` or `union`) as it currently could
940
- // create an invalid selection set (and throw down the line). In theory, when `type` is an abstract type, we could look at the
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
- let selectionSet = this.selectionSetsAtTypesCache.get(type.name);
952
- if (!selectionSet) {
953
- // Note that all we want is removing any top-level branches that don't apply due to the current type. There is no point
954
- // in going recursive however: any simplification due to `type` stops as soon as we traverse a field. And so we don't bother.
955
- selectionSet = this.selectionSet.trimUnsatisfiableBranches(type, { recursive: false });
956
- this.selectionSetsAtTypesCache.set(type.name, selectionSet);
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 selectionSet;
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.fragments.size === 0;
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
- maybeApplyingAtType(type: CompositeType): NamedFragmentDefinition[] {
991
- return this.fragments.values().filter(f => f.canApplyAtType(type));
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
- * This method:
1036
- * - expands all nested fragments,
1037
- * - applies the provided mapper to the selection set of the fragments,
1038
- * - and finally re-fragments the nested fragments.
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
- mapToExpandedSelectionSets(
1041
- mapper: (selectionSet: SelectionSet) => SelectionSet | undefined,
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 FragmentInfo = {
1045
- original: NamedFragmentDefinition,
1046
- mappedSelectionSet: SelectionSet,
1294
+ type FragmentDependencies = {
1295
+ fragment: NamedFragmentDefinition,
1047
1296
  dependsOn: string[],
1048
1297
  };
1049
- const fragmentsMap = new Map<string, FragmentInfo>();
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
- original: fragment,
1062
- mappedSelectionSet,
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 reoptimizedSelectionSet = info.mappedSelectionSet.optimize(mappedFragments);
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.mapToExpandedSelectionSets(
1085
- (s) => {
1086
- const rebasedType = schema.type(s.parentType.name);
1087
- try {
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
- (orig, newSelection) => new NamedFragmentDefinition(schema, orig.name, newSelection.parentType).setSelectionSet(newSelection),
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(): { path: string[], field: FieldSelection }[] {
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
- selection.collectUsedFragmentNames(collector);
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 optimized = wrapped.optimize(fragments);
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, fragments)
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
- // Handling the case where the selection may alreayd have some fragments adds complexity,
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
- expandAllFragments(): SelectionSet {
1287
- return this.lazyMap((selection) => selection.expandAllFragments(), { fragments: null });
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
- trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): SelectionSet {
1299
- return this.lazyMap((selection) => selection.trimUnsatisfiableBranches(parentType, options), { parentType });
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.withUpdatedFragments(newFragments);
1703
+ return this;
1337
1704
  }
1338
- return updatedSelections.toSelectionSet(options?.parentType ?? this.parentType, newFragments);
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 select from filtering out any selection that does not match the provided predicate.
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 will expand *ALL* fragments as the result of applying it's filtering. You should
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.filter(predicate));
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.filter((selection) => selection.selectionSet?.isEmpty() !== true);
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, this.fragments);
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
- private triviallyNestedSelectionsForKey(parentType: CompositeType, key: string): Selection[] {
1406
- const found: Selection[] = [];
1407
- for (const selection of this.selections()) {
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 otherSelections = this.triviallyNestedSelectionsForKey(this.parentType, key);
1514
- const mergedSelection = this.mergeSameKeySelections([thisSelection].concat(otherSelections).filter(isDefined));
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
- // Please note that this method assumes that `candidate.canApplyAtType(parentType) === true` but it is left to the caller to
1526
- // validate this (`canApplyAtType` is not free, and we want to avoid repeating it multiple times).
1527
- diffWithNamedFragmentIfContained(candidate: NamedFragmentDefinition, parentType: CompositeType): { contains: boolean, diff?: SelectionSet } {
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
- const otherSelections = that.triviallyNestedSelectionsForKey(this.parentType, key);
1576
- const allSelections = thatSelection ? [thatSelection].concat(otherSelections) : otherSelections;
1577
- if (allSelections.length === 0) {
1578
- // If it is a fragment spread, and we didn't find it in `that`, then we try to expand that
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
- const selectionDiff = allSelections.reduce<Selection | undefined>((prev, val) => prev?.minus(val), thisSelection);
1592
- if (selectionDiff) {
1593
- updated.add(selectionDiff);
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
- return updated.toSelectionSet(this.parentType, this.fragments);
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 && this.fragments && !this.fragments.isEmpty()
1702
- ? this.fragments.toString(indent) + "\n\n"
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, fragments);
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, fragments?: NamedFragments): SelectionSet {
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, fragments);
2304
+ return new SelectionSet(parentType, map);
2053
2305
  }
2054
2306
 
2055
- export function selectionSetOfElement(element: OperationElement, subSelection?: SelectionSet, fragments?: NamedFragments): SelectionSet {
2056
- return selectionSetOf(element.parentType, selectionOfElement(element, subSelection), fragments);
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 expandAllFragments(): TOwnType | readonly Selection[];
2383
+ abstract expandFragments(updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
2136
2384
 
2137
- abstract expandFragments(names: string[], updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
2385
+ abstract normalize(args: { parentType: CompositeType, recursive? : boolean }): TOwnType | SelectionSet | undefined;
2138
2386
 
2139
- abstract trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): TOwnType | SelectionSet | undefined;
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
- // Attempts to optimize the subselection of this field selection using named fragments `candidates` _assuming_ that
2154
- // those candidates do apply at `parentType` (that is, `candidates.every((c) => c.canApplyAtType(parentType))` is true,
2155
- // which is ensured by the fact that `tryOptimizeSubselectionWithFragments` calls this on a subset of the candidates
2156
- // returned by `maybeApplyingAtType`).
2157
- protected tryOptimizeSubselectionOnce(_: {
2158
- parentType: CompositeType,
2159
- subSelection: SelectionSet,
2160
- candidates: NamedFragmentDefinition[],
2161
- fragments: NamedFragments,
2162
- }): {
2163
- spread?: FragmentSpreadSelection,
2164
- optimizedSelection?: SelectionSet,
2165
- hasDiff?: boolean,
2166
- } {
2167
- // Field and inline fragment override this, but this should never be called for a spread.
2168
- assert(false, `UNSUPPORTED`);
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
- fragmentFilter,
2422
+ validator,
2423
+ canUseFullMatchingFragment,
2176
2424
  }: {
2177
2425
  parentType: CompositeType,
2178
2426
  subSelection: SelectionSet,
2179
2427
  fragments: NamedFragments,
2180
- fragmentFilter?: (f: NamedFragmentDefinition) => boolean,
2181
- }): SelectionSet | FragmentSpreadSelection {
2182
- let candidates = fragments.maybeApplyingAtType(parentType);
2183
- if (fragmentFilter) {
2184
- candidates = candidates.filter(fragmentFilter);
2185
- }
2186
- let shouldTryAgain: boolean;
2187
- do {
2188
- const { spread, optimizedSelection, hasDiff } = this.tryOptimizeSubselectionOnce({ parentType, subSelection, candidates, fragments });
2189
- if (optimizedSelection) {
2190
- subSelection = optimizedSelection;
2191
- } else if (spread) {
2192
- return spread;
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
- shouldTryAgain = !!spread && !!hasDiff;
2195
- if (shouldTryAgain) {
2196
- candidates = candidates.filter((c) => c !== spread?.namedFragment)
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
- } while (shouldTryAgain);
2199
- return subSelection;
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
- let optimizedSelection = this.selectionSet ? this.selectionSet.optimizeSelections(fragments) : undefined;
2235
- const fieldBaseType = this.element.baseType();
2236
- if (isCompositeType(fieldBaseType) && optimizedSelection) {
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: optimizedSelection,
2777
+ subSelection: this.selectionSet,
2240
2778
  fragments,
2241
- // We can never apply a fragments that has directives on it at the field level (but when those are expanded,
2242
- // their type condition would always be preserved due to said applied directives, so they will always
2243
- // be handled by `InlineFragmentSelection.optimize` anyway).
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
- assert(!(optimized instanceof FragmentSpreadSelection), 'tryOptimizeSubselectionOnce should never return only a spread');
2248
- optimizedSelection = optimized;
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
- : new FieldSelection(this.element, optimizedSelection);
2797
+ : this.withUpdatedSelectionSet(optimizedSelection);
2254
2798
  }
2255
2799
 
2256
- protected tryOptimizeSubselectionOnce({
2257
- parentType,
2258
- subSelection,
2259
- candidates,
2260
- fragments,
2261
- }: {
2262
- parentType: CompositeType,
2263
- subSelection: SelectionSet,
2264
- candidates: NamedFragmentDefinition[],
2265
- fragments: NamedFragments,
2266
- }): {
2267
- spread?: FragmentSpreadSelection,
2268
- optimizedSelection?: SelectionSet,
2269
- hasDiff?: boolean,
2270
- }{
2271
- let optimizedSelection = subSelection;
2272
- for (const candidate of candidates) {
2273
- const { contains, diff } = optimizedSelection.diffWithNamedFragmentIfContained(candidate, parentType);
2274
- if (contains) {
2275
- // We can optimize the selection with this fragment. The replaced sub-selection will be
2276
- // comprised of this new spread and the remaining `diff` if there is any.
2277
- const spread = new FragmentSpreadSelection(parentType, fragments, candidate, []);
2278
- optimizedSelection = diff
2279
- ? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
2280
- : selectionSetOf(parentType, spread);
2281
- return { spread, optimizedSelection, hasDiff: !!diff }
2282
- }
2283
- }
2284
- return {};
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.filter(predicate);
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
- expandAllFragments(): FieldSelection {
2386
- return this.mapToSelectionSet((s) => s.expandAllFragments());
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
- trimUnsatisfiableBranches(_: CompositeType, options?: { recursive? : boolean }): FieldSelection {
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 = this.element.baseType()
2395
- assert(isCompositeType(base), () => `Field ${this.element} should not have a sub-selection`);
2396
- const trimmed = (options?.recursive ?? true) ? this.mapToSelectionSet((s) => s.trimUnsatisfiableBranches(base)) : this;
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 (trimmed.selectionSet?.isEmpty()) {
2402
- return trimmed.withUpdatedSelectionSet(selectionSetOfElement(
2403
- new Field(
2404
- base.typenameField()!,
2405
- undefined,
2406
- [new Directive('include', { 'if': false })],
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 trimmed;
2932
+ return this.withUpdatedComponents(element, normalizedSubSelection);
2411
2933
  }
2412
2934
  }
2413
2935
 
2414
- expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FieldSelection {
2415
- return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
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): boolean {
2954
+ contains(that: Selection): ContainsResult {
2433
2955
  if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
2434
- return false;
2956
+ return ContainsResult.NOT_CONTAINED;
2435
2957
  }
2436
2958
 
2437
- if (!that.selectionSet) {
2438
- return true;
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
- return !!this.selectionSet && this.selectionSet.contains(that.selectionSet);
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
- filter(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
2992
+ filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
2474
2993
  // Note that we essentially expand all fragments as part of this.
2475
- const selectionSet = this.selectionSet;
2476
- const updatedSelectionSet = selectionSet.filter(predicate);
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): boolean;
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.optimizeSelections(fragments);
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
- protected tryOptimizeSubselectionOnce({
2602
- parentType,
2603
- subSelection,
2604
- candidates,
2605
- fragments,
2606
- }: {
2607
- parentType: CompositeType,
2608
- subSelection: SelectionSet,
2609
- candidates: NamedFragmentDefinition[],
2610
- fragments: NamedFragments,
2611
- }): {
2612
- spread?: FragmentSpreadSelection,
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
- // Returning a spread without a subselection will make the code "replace" this whole inline fragment
2641
- // by the spread, which is what we want. Do not that as we're replacing the whole inline fragment,
2642
- // we use `this.parentType` instead of `parentType` (the later being `this.element.typeCondition` basically).
2643
- return {
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
- const spread = new FragmentSpreadSelection(parentType, fragments, candidate, []);
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
- return {};
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
- trimUnsatisfiableBranches(currentType: CompositeType, options?: { recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
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
- // If the current type is an object, then we never need to keep the current fragment because:
2701
- // - either the fragment is also an object, but we've eliminated the case where the 2 types are the same,
2702
- // so this is just an unsatisfiable branch.
2703
- // - or it's not an object, but then the current type is more precise and no point in "casting" to a
2704
- // less precise interface/union. And if the current type is not even a valid runtime of said interface/union,
2705
- // then we should completely ignore the branch (or, since we're eliminating `thisCondition`, we would be
2706
- // building an invalid selection).
2707
- if (isObjectType(currentType)) {
2708
- if (isObjectType(thisCondition) || !possibleRuntimeTypes(thisCondition).includes(currentType)) {
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
- // As we preserve the current fragment, the rest is about recursing. If we don't recurse, we're done
2718
- if (!recursive) {
2719
- return this;
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
- // In all other cases, we first recurse on the sub-selection.
2723
- const trimmedSelectionSet = this.selectionSet.trimUnsatisfiableBranches(this.element.typeCondition ?? this.parentType);
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
- // First, could be that everything was unsatisfiable.
2726
- if (trimmedSelectionSet.isEmpty()) {
2727
- if (this.element.appliedDirectives.length === 0) {
2728
- return undefined;
2729
- } else {
2730
- return this.withUpdatedSelectionSet(selectionSetOfElement(
2731
- new Field(
2732
- (this.element.typeCondition ?? this.parentType).typenameField()!,
2733
- undefined,
2734
- [new Directive('include', { 'if': false })],
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(currentType), () => `Should not have got here if ${currentType} is an object type`);
2746
- const currentRuntimes = possibleRuntimeTypes(currentType);
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 trimmedSelectionSet.selections()) {
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 === trimmedSelectionSet.selections().length) {
2760
- return trimmedSelectionSet;
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
- trimmedSelectionSet.filter((s) => !liftableSelections.includes(s)),
3261
+ normalizedSelectionSet.filter((s) => !liftableSelections.includes(s)),
2770
3262
  ));
2771
- return newSet.toSelectionSet(this.parentType);
3263
+ return newSet.toSelectionSet(parentType);
2772
3264
  }
2773
3265
  }
2774
3266
 
2775
- return this.selectionSet === trimmedSelectionSet ? this : this.withUpdatedSelectionSet(trimmedSelectionSet);
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(names: string[], updatedFragments: NamedFragments | undefined): FragmentSelection {
2783
- return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
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): boolean {
2797
- return (that instanceof FragmentSelection)
2798
- && this.element.equals(that.element)
2799
- && this.selectionSet.contains(that.selectionSet);
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
- trimUnsatisfiableBranches(parentType: CompositeType): FragmentSelection {
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
- return this.rebaseOn(parentType);
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
- // We don't do anything else because fragment definition are validated when created.
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(_: NamedFragments): FragmentSelection {
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
- this.fragments,
2898
- this.namedFragment,
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
- expandAllFragments(): FragmentSelection | readonly Selection[] {
2910
- const expandedSubSelections = this.selectionSet.expandAllFragments();
2911
- return sameType(this.parentType, this.namedFragment.typeCondition) && this.element.appliedDirectives.length === 0
2912
- ? expandedSubSelections.selections()
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(names, updatedFragments);
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): boolean {
3446
+ contains(that: Selection): ContainsResult {
2957
3447
  if (this.equals(that)) {
2958
- return true;
3448
+ return ContainsResult.EQUAL;
2959
3449
  }
2960
3450
 
2961
- return (that instanceof FragmentSelection)
2962
- && this.element.equals(that.element)
2963
- && this.selectionSet.contains(that.selectionSet);
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: fragments.isEmpty() ? undefined : 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.selectionSet.fragments
3188
- ? operation.selectionSet.fragments?.toFragmentDefinitionNodes()
3680
+ const fragmentASTs: DefinitionNode[] = operation.fragments
3681
+ ? operation.fragments?.toFragmentDefinitionNodes()
3189
3682
  : [];
3190
3683
  return {
3191
3684
  kind: Kind.DOCUMENT,