@apollo/federation-internals 2.4.6 → 2.4.7

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