@apollo/federation-internals 2.4.5 → 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
@@ -46,11 +46,12 @@ import {
46
46
  isLeafType,
47
47
  Variables,
48
48
  isObjectType,
49
+ NamedType,
49
50
  } from "./definitions";
50
51
  import { isInterfaceObjectType } from "./federation";
51
52
  import { ERRORS } from "./error";
52
53
  import { isSubtype, sameType } from "./types";
53
- import { assert, isDefined, mapEntries, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
54
+ import { assert, mapKeys, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
54
55
  import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
55
56
  import { v1 as uuidv1 } from 'uuid';
56
57
 
@@ -152,7 +153,11 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
152
153
  }
153
154
 
154
155
  isLeafField(): boolean {
155
- return isLeafType(baseType(this.definition.type!));
156
+ return isLeafType(this.baseType());
157
+ }
158
+
159
+ baseType(): NamedType {
160
+ return baseType(this.definition.type!);
156
161
  }
157
162
 
158
163
  withUpdatedDefinition(newDefinition: FieldDefinition<any>): Field<TArgs> {
@@ -674,7 +679,7 @@ export function concatOperationPaths(head: OperationPath, tail: OperationPath):
674
679
 
675
680
  function isUselessFollowupElement(first: OperationElement, followup: OperationElement, conditionals: Directive<any, any>[]): boolean {
676
681
  const typeOfFirst = first.kind === 'Field'
677
- ? baseType(first.definition.type!)
682
+ ? first.baseType()
678
683
  : first.typeCondition;
679
684
 
680
685
  // The followup is useless if it's a fragment (with no directives we would want to preserve) whose type
@@ -691,6 +696,164 @@ export type RootOperationPath = {
691
696
  path: OperationPath
692
697
  }
693
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
+
694
857
  // TODO Operations can also have directives
695
858
  export class Operation {
696
859
  constructor(
@@ -698,6 +861,7 @@ export class Operation {
698
861
  readonly rootKind: SchemaRootKind,
699
862
  readonly selectionSet: SelectionSet,
700
863
  readonly variableDefinitions: VariableDefinitions,
864
+ readonly fragments?: NamedFragments,
701
865
  readonly name?: string) {
702
866
  }
703
867
 
@@ -712,39 +876,28 @@ export class Operation {
712
876
  return this;
713
877
  }
714
878
 
715
- const usages = new Map<string, number>();
716
- optimizedSelection.collectUsedFragmentNames(usages);
717
- for (const fragment of fragments.names()) {
718
- if (!usages.has(fragment)) {
719
- usages.set(fragment, 0);
720
- }
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);
721
884
  }
722
885
 
723
- // We re-expand any fragments that is used less than our minimum. Optimizing all fragments to potentially
724
- // re-expand some is not entirely optimal, but it's simple and probably don't matter too much in practice
725
- // (we only call this optimization on the final computed query plan, so not a very hot path; plus in most
726
- // cases we won't even reach that point either because there is no fragment, or none will have been
727
- // optimized away so we'll exit above). We can optimize later if this show up in profiling though.
728
- //
729
- // Also note `toDeoptimize` will always contains the unused fragments, which will allow `expandFragments`
730
- // to remove them from the listed fragments in `optimizedSelection` (here again, this could make use call
731
- // `expandFragments` on _only_ unused fragments and that case could be dealt with more efficiently, but
732
- // probably not noticeable in practice so ...).
733
- 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);
734
892
 
735
- if (toDeoptimize.length > 0) {
736
- const newFragments = optimizedSelection.fragments?.without(toDeoptimize);
737
- optimizedSelection = optimizedSelection.expandFragments(toDeoptimize, newFragments);
738
- // Expanding fragments could create some "inefficiencies" that we wouldn't have if we hadn't re-optimized
739
- // the fragments to de-optimize it later, so we do a final "trim" pass to remove those.
740
- optimizedSelection = optimizedSelection.trimUnsatisfiableBranches(optimizedSelection.parentType);
741
- }
742
-
743
- 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);
744
897
  }
745
898
 
746
899
  expandAllFragments(): Operation {
747
- const expandedSelections = this.selectionSet.expandAllFragments();
900
+ const expandedSelections = this.selectionSet.expandFragments();
748
901
  if (expandedSelections === this.selectionSet) {
749
902
  return this;
750
903
  }
@@ -754,6 +907,7 @@ export class Operation {
754
907
  this.rootKind,
755
908
  expandedSelections,
756
909
  this.variableDefinitions,
910
+ undefined,
757
911
  this.name
758
912
  );
759
913
  }
@@ -769,6 +923,7 @@ export class Operation {
769
923
  this.rootKind,
770
924
  trimmedSelections,
771
925
  this.variableDefinitions,
926
+ this.fragments,
772
927
  this.name
773
928
  );
774
929
  }
@@ -781,14 +936,10 @@ export class Operation {
781
936
  * applications are removed.
782
937
  */
783
938
  withoutDefer(labelsToRemove?: Set<string>): Operation {
784
- // If we have named fragments, we should be looking inside those and either expand those having @defer or,
785
- // probably better, replace them with a verison without @defer. But as we currently only call this method
786
- // after `expandAllFragments`, we'll implement this when/if we need it.
787
- assert(!this.selectionSet.fragments || this.selectionSet.fragments.isEmpty(), 'Removing @defer currently only work on "expanded" selections (no named fragments)');
788
939
  const updated = this.selectionSet.withoutDefer(labelsToRemove);
789
940
  return updated == this.selectionSet
790
941
  ? this
791
- : 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);
792
943
  }
793
944
 
794
945
  /**
@@ -810,15 +961,12 @@ export class Operation {
810
961
  assignedDeferLabels: Set<string>,
811
962
  deferConditions: SetMultiMap<string, string>,
812
963
  } {
813
- // Similar comment than in `withoutDefer`
814
- assert(!this.selectionSet.fragments || this.selectionSet.fragments.isEmpty(), 'Assigning @defer lables currently only work on "expanded" selections (no named fragments)');
815
-
816
964
  const normalizer = new DeferNormalizer();
817
965
  const { hasDefers, hasNonLabelledOrConditionalDefers } = normalizer.init(this.selectionSet);
818
966
  let updatedOperation: Operation = this;
819
967
  if (hasNonLabelledOrConditionalDefers) {
820
968
  const updated = this.selectionSet.withNormalizedDefer(normalizer);
821
- 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);
822
970
  }
823
971
  return {
824
972
  operation: updatedOperation,
@@ -839,14 +987,20 @@ export class Operation {
839
987
  }
840
988
 
841
989
  toString(expandFragments: boolean = false, prettyPrint: boolean = true): string {
842
- 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);
843
991
  }
844
992
  }
845
993
 
846
994
  export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmentDefinition> {
847
995
  private _selectionSet: SelectionSet | undefined;
848
996
 
849
- 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>();
850
1004
 
851
1005
  constructor(
852
1006
  schema: Schema,
@@ -871,12 +1025,31 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
871
1025
  return this._selectionSet;
872
1026
  }
873
1027
 
1028
+ expandedSelectionSet(): SelectionSet {
1029
+ if (!this._expandedSelectionSet) {
1030
+ this._expandedSelectionSet = this.selectionSet.expandFragments().trimUnsatisfiableBranches(this.typeCondition);
1031
+ }
1032
+ return this._expandedSelectionSet;
1033
+ }
1034
+
874
1035
  withUpdatedSelectionSet(newSelectionSet: SelectionSet): NamedFragmentDefinition {
875
1036
  return new NamedFragmentDefinition(this.schema(), this.name, this.typeCondition).setSelectionSet(newSelectionSet);
876
1037
  }
877
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
+
878
1047
  collectUsedFragmentNames(collector: Map<string, number>) {
879
- 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
+ }
880
1053
  }
881
1054
 
882
1055
  toFragmentDefinitionNode() : FragmentDefinitionNode {
@@ -898,13 +1071,32 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
898
1071
  }
899
1072
 
900
1073
  /**
901
- * Whether this fragment may apply at the provided type, that is if its type condition runtime types intersects with the
902
- * 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.
903
1077
  *
904
1078
  * @param type - the type at which we're looking at applying the fragment
905
1079
  */
906
1080
  canApplyAtType(type: CompositeType): boolean {
907
- 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)));
908
1100
  }
909
1101
 
910
1102
  /**
@@ -925,10 +1117,12 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
925
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
926
1118
  * us that part.
927
1119
  */
928
- selectionSetAtType(type: CompositeType): SelectionSet {
1120
+ expandedSelectionSetAtType(type: CompositeType): SelectionSet {
1121
+ const expandedSelectionSet = this.expandedSelectionSet();
1122
+
929
1123
  // First, if the candidate condition is an object or is the type passed, then there isn't any additional restriction to do.
930
1124
  if (sameType(type, this.typeCondition) || isObjectType(this.typeCondition)) {
931
- return this.selectionSet;
1125
+ return expandedSelectionSet;
932
1126
  }
933
1127
 
934
1128
  // We should not call `trimUnsatisfiableBranches` where `type` is an abstract type (`interface` or `union`) as it currently could
@@ -940,17 +1134,45 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
940
1134
  // Concretely, this means that there may be corner cases where a named fragment could be reused but isn't, but waiting on finding
941
1135
  // concrete examples where this matter to decide if it's worth the complexity.
942
1136
  if (!isObjectType(type)) {
943
- return this.selectionSet;
1137
+ return expandedSelectionSet;
944
1138
  }
945
1139
 
946
- let selectionSet = this.selectionSetsAtTypesCache.get(type.name);
947
- if (!selectionSet) {
1140
+ let selectionAtType = this.expandedSelectionSetsAtTypesCache.get(type.name);
1141
+ if (!selectionAtType) {
948
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
949
1143
  // in going recursive however: any simplification due to `type` stops as soon as we traverse a field. And so we don't bother.
950
- selectionSet = this.selectionSet.trimUnsatisfiableBranches(type, { recursive: false });
951
- this.selectionSetsAtTypesCache.set(type.name, selectionSet);
1144
+ selectionAtType = expandedSelectionSet.trimUnsatisfiableBranches(type, { recursive: false });
1145
+ this.expandedSelectionSetsAtTypesCache.set(type.name, selectionAtType);
952
1146
  }
953
- 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;
954
1176
  }
955
1177
 
956
1178
  toString(indent?: string): string {
@@ -962,7 +1184,11 @@ export class NamedFragments {
962
1184
  private readonly fragments = new MapWithCachedArrays<string, NamedFragmentDefinition>();
963
1185
 
964
1186
  isEmpty(): boolean {
965
- return this.fragments.size === 0;
1187
+ return this.size === 0;
1188
+ }
1189
+
1190
+ get size(): number {
1191
+ return this.fragments.size;
966
1192
  }
967
1193
 
968
1194
  names(): readonly string[] {
@@ -986,26 +1212,6 @@ export class NamedFragments {
986
1212
  return this.fragments.values().filter(f => f.canApplyAtType(type));
987
1213
  }
988
1214
 
989
- without(names: string[]): NamedFragments | undefined {
990
- if (!names.some(n => this.fragments.has(n))) {
991
- return this;
992
- }
993
-
994
- const newFragments = new NamedFragments();
995
- for (const fragment of this.fragments.values()) {
996
- if (!names.includes(fragment.name)) {
997
- // We want to keep that fragment. But that fragment might use a fragment we
998
- // remove, and if so, we need to expand that removed fragment.
999
- const updatedSelectionSet = fragment.selectionSet.expandFragments(names, newFragments);
1000
- const newFragment = updatedSelectionSet === fragment.selectionSet
1001
- ? fragment
1002
- : fragment.withUpdatedSelectionSet(updatedSelectionSet);
1003
- newFragments.add(newFragment);
1004
- }
1005
- }
1006
- return newFragments.isEmpty() ? undefined : newFragments;
1007
- }
1008
-
1009
1215
  get(name: string): NamedFragmentDefinition | undefined {
1010
1216
  return this.fragments.get(name);
1011
1217
  }
@@ -1027,48 +1233,44 @@ export class NamedFragments {
1027
1233
  }
1028
1234
 
1029
1235
  /**
1030
- * This method:
1031
- * - expands all nested fragments,
1032
- * - applies the provided mapper to the selection set of the fragments,
1033
- * - 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.
1034
1240
  */
1035
- mapToExpandedSelectionSets(
1036
- mapper: (selectionSet: SelectionSet) => SelectionSet | undefined,
1037
- recreateFct: (frag: NamedFragmentDefinition, newSelectionSet: SelectionSet) => NamedFragmentDefinition = (f, s) => f.withUpdatedSelectionSet(s),
1241
+ mapInDependencyOrder(
1242
+ mapper: (fragment: NamedFragmentDefinition, newFragments: NamedFragments) => NamedFragmentDefinition | undefined
1038
1243
  ): NamedFragments | undefined {
1039
- type FragmentInfo = {
1040
- original: NamedFragmentDefinition,
1041
- mappedSelectionSet: SelectionSet,
1244
+ type FragmentDependencies = {
1245
+ fragment: NamedFragmentDefinition,
1042
1246
  dependsOn: string[],
1043
1247
  };
1044
- const fragmentsMap = new Map<string, FragmentInfo>();
1045
-
1046
- const removedFragments = new Set<string>();
1248
+ const fragmentsMap = new Map<string, FragmentDependencies>();
1047
1249
  for (const fragment of this.definitions()) {
1048
- const mappedSelectionSet = mapper(fragment.selectionSet.expandAllFragments().trimUnsatisfiableBranches(fragment.typeCondition));
1049
- if (!mappedSelectionSet) {
1050
- removedFragments.add(fragment.name);
1051
- continue;
1052
- }
1053
-
1054
- const otherFragmentsUsages = new Map<string, number>();
1055
- fragment.collectUsedFragmentNames(otherFragmentsUsages);
1056
1250
  fragmentsMap.set(fragment.name, {
1057
- original: fragment,
1058
- mappedSelectionSet,
1059
- dependsOn: Array.from(otherFragmentsUsages.keys()),
1251
+ fragment,
1252
+ dependsOn: Array.from(fragment.fragmentUsages().keys()),
1060
1253
  });
1061
1254
  }
1062
1255
 
1256
+ const removedFragments = new Set<string>();
1063
1257
  const mappedFragments = new NamedFragments();
1064
1258
  while (fragmentsMap.size > 0) {
1065
1259
  for (const [name, info] of fragmentsMap) {
1066
1260
  // Note that graphQL specifies that named fragments cannot have cycles (https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles)
1067
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).
1068
1262
  if (info.dependsOn.every((n) => mappedFragments.has(n) || removedFragments.has(n))) {
1069
- const reoptimizedSelectionSet = info.mappedSelectionSet.optimize(mappedFragments);
1070
- mappedFragments.add(recreateFct(info.original, reoptimizedSelectionSet));
1263
+ const mapped = mapper(info.fragment, mappedFragments);
1071
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;
1072
1274
  }
1073
1275
  }
1074
1276
  }
@@ -1076,20 +1278,58 @@ export class NamedFragments {
1076
1278
  return mappedFragments.isEmpty() ? undefined : mappedFragments;
1077
1279
  }
1078
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
+
1079
1300
  rebaseOn(schema: Schema): NamedFragments | undefined {
1080
- return this.mapToExpandedSelectionSets(
1081
- (s) => {
1082
- const rebasedType = schema.type(s.parentType.name);
1083
- try {
1084
- return rebasedType && isCompositeType(rebasedType) ? s.rebaseOn(rebasedType) : undefined;
1085
- } catch (e) {
1086
- // This means we cannot rebase this selection on the schema and thus cannot reuse that fragment on that
1087
- // particular schema.
1301
+ return this.mapInDependencyOrder((fragment, newFragments) => {
1302
+ const rebasedType = schema.type(fragment.selectionSet.parentType.name);
1303
+ try {
1304
+ if (!rebasedType || !isCompositeType(rebasedType)) {
1088
1305
  return undefined;
1089
1306
  }
1090
- },
1091
- (orig, newSelection) => new NamedFragmentDefinition(schema, orig.name, newSelection.parentType).setSelectionSet(newSelection),
1092
- );
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
+ });
1093
1333
  }
1094
1334
 
1095
1335
  validate(variableDefinitions: VariableDefinitions) {
@@ -1169,6 +1409,14 @@ class DeferNormalizer {
1169
1409
  }
1170
1410
  }
1171
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
+
1172
1420
  export class SelectionSet {
1173
1421
  private readonly _keyedSelections: Map<string, Selection>;
1174
1422
  private readonly _selections: readonly Selection[];
@@ -1176,7 +1424,6 @@ export class SelectionSet {
1176
1424
  constructor(
1177
1425
  readonly parentType: CompositeType,
1178
1426
  keyedSelections: Map<string, Selection> = new Map(),
1179
- readonly fragments?: NamedFragments,
1180
1427
  ) {
1181
1428
  this._keyedSelections = keyedSelections;
1182
1429
  this._selections = mapValues(keyedSelections);
@@ -1230,7 +1477,7 @@ export class SelectionSet {
1230
1477
 
1231
1478
  collectUsedFragmentNames(collector: Map<string, number>) {
1232
1479
  for (const selection of this.selections()) {
1233
- selection.collectUsedFragmentNames(collector);
1480
+ selection.collectUsedFragmentNames(collector);
1234
1481
  }
1235
1482
  }
1236
1483
 
@@ -1261,7 +1508,7 @@ export class SelectionSet {
1261
1508
  // and in that case we return a singleton selection with just that. Otherwise, it's our wrapping inline fragment
1262
1509
  // with the sub-selections optimized, and we just return that subselection.
1263
1510
  return optimized instanceof FragmentSpreadSelection
1264
- ? selectionSetOf(this.parentType, optimized, fragments)
1511
+ ? selectionSetOf(this.parentType, optimized)
1265
1512
  : optimized.selectionSet;
1266
1513
  }
1267
1514
 
@@ -1269,26 +1516,11 @@ export class SelectionSet {
1269
1516
  // may not match fragments that would apply at top-level, so you should usually use `optimize` instead (this exists mostly
1270
1517
  // for the recursion).
1271
1518
  optimizeSelections(fragments: NamedFragments): SelectionSet {
1272
- // Handling the case where the selection may alreayd have some fragments adds complexity,
1273
- // not only because we need to deal with merging new and existing fragments, but also because
1274
- // things get weird if some fragment names are in common to both. Since we currently only care
1275
- // about this method when optimizing subgraph fetch selections and those are initially created
1276
- // without any fragments, we don't bother handling this more complex case.
1277
- assert(!this.fragments || this.fragments.isEmpty(), `Should not be called on selection that already has named fragments, but got ${this.fragments}`)
1278
-
1279
- return this.lazyMap((selection) => selection.optimize(fragments), { fragments });
1519
+ return this.lazyMap((selection) => selection.optimize(fragments));
1280
1520
  }
1281
1521
 
1282
- expandAllFragments(): SelectionSet {
1283
- return this.lazyMap((selection) => selection.expandAllFragments(), { fragments: null });
1284
- }
1285
-
1286
- expandFragments(names: string[], updatedFragments: NamedFragments | undefined): SelectionSet {
1287
- if (names.length === 0) {
1288
- return this;
1289
- }
1290
-
1291
- return this.lazyMap((selection) => selection.expandFragments(names, updatedFragments), { fragments: updatedFragments ?? null });
1522
+ expandFragments(updatedFragments?: NamedFragments): SelectionSet {
1523
+ return this.lazyMap((selection) => selection.expandFragments(updatedFragments));
1292
1524
  }
1293
1525
 
1294
1526
  trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): SelectionSet {
@@ -1306,14 +1538,10 @@ export class SelectionSet {
1306
1538
  lazyMap(
1307
1539
  mapper: (selection: Selection) => Selection | readonly Selection[] | SelectionSet | undefined,
1308
1540
  options?: {
1309
- fragments?: NamedFragments | null,
1310
1541
  parentType?: CompositeType,
1311
1542
  }
1312
1543
  ): SelectionSet {
1313
1544
  const selections = this.selections();
1314
- const updatedFragments = options?.fragments;
1315
- const newFragments = updatedFragments === undefined ? this.fragments : (updatedFragments ?? undefined);
1316
-
1317
1545
  let updatedSelections: SelectionSetUpdates | undefined = undefined;
1318
1546
  for (let i = 0; i < selections.length; i++) {
1319
1547
  const selection = selections[i];
@@ -1329,22 +1557,16 @@ export class SelectionSet {
1329
1557
  }
1330
1558
  }
1331
1559
  if (!updatedSelections) {
1332
- return this.withUpdatedFragments(newFragments);
1560
+ return this;
1333
1561
  }
1334
- return updatedSelections.toSelectionSet(options?.parentType ?? this.parentType, newFragments);
1335
- }
1336
-
1337
- private withUpdatedFragments(newFragments: NamedFragments | undefined): SelectionSet {
1338
- return this.fragments === newFragments ? this : new SelectionSet(this.parentType, this._keyedSelections, newFragments);
1562
+ return updatedSelections.toSelectionSet(options?.parentType ?? this.parentType);
1339
1563
  }
1340
1564
 
1341
1565
  withoutDefer(labelsToRemove?: Set<string>): SelectionSet {
1342
- assert(!this.fragments, 'Not yet supported');
1343
1566
  return this.lazyMap((selection) => selection.withoutDefer(labelsToRemove));
1344
1567
  }
1345
1568
 
1346
1569
  withNormalizedDefer(normalizer: DeferNormalizer): SelectionSet {
1347
- assert(!this.fragments, 'Not yet supported');
1348
1570
  return this.lazyMap((selection) => selection.withNormalizedDefer(normalizer));
1349
1571
  }
1350
1572
 
@@ -1367,17 +1589,17 @@ export class SelectionSet {
1367
1589
  return updated.isEmpty() ? undefined : updated;
1368
1590
  }
1369
1591
 
1370
- rebaseOn(parentType: CompositeType): SelectionSet {
1592
+ rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): SelectionSet {
1371
1593
  if (this.parentType === parentType) {
1372
1594
  return this;
1373
1595
  }
1374
1596
 
1375
1597
  const newSelections = new Map<string, Selection>();
1376
1598
  for (const selection of this.selections()) {
1377
- newSelections.set(selection.key(), selection.rebaseOn(parentType));
1599
+ newSelections.set(selection.key(), selection.rebaseOn(parentType, fragments));
1378
1600
  }
1379
1601
 
1380
- return new SelectionSet(parentType, newSelections, this.fragments);
1602
+ return new SelectionSet(parentType, newSelections);
1381
1603
  }
1382
1604
 
1383
1605
  equals(that: SelectionSet): boolean {
@@ -1398,70 +1620,62 @@ export class SelectionSet {
1398
1620
  return true;
1399
1621
  }
1400
1622
 
1401
- private triviallyNestedSelectionsForKey(parentType: CompositeType, key: string): Selection[] {
1402
- const found: Selection[] = [];
1403
- for (const selection of this.selections()) {
1404
- if (selection.isUnecessaryInlineFragment(parentType)) {
1405
- const selectionForKey = selection.selectionSet._keyedSelections.get(key);
1406
- if (selectionForKey) {
1407
- found.push(selectionForKey);
1408
- }
1409
- for (const nestedSelection of selection.selectionSet.triviallyNestedSelectionsForKey(parentType, key)) {
1410
- found.push(nestedSelection);
1411
- }
1412
- }
1413
- }
1414
- return found;
1415
- }
1416
-
1417
- private mergeSameKeySelections(selections: Selection[]): Selection | undefined {
1418
- if (selections.length === 0) {
1419
- return undefined;
1420
- }
1421
- const first = selections[0];
1422
- // We know that all the selections passed are for exactly the same element (same "key"). So if it is a
1423
- // leaf field or a named fragment, then we know that even if we have more than 1 selection, all of them
1424
- // are the exact same and we can just return the first one. Only if we have a composite field or an
1425
- // inline fragment do we need to merge the underlying sub-selection (which may differ).
1426
- if (!first.selectionSet || (first instanceof FragmentSpreadSelection) || selections.length === 1) {
1427
- return first;
1623
+ contains(that: SelectionSet): ContainsResult {
1624
+ if (that._selections.length > this._selections.length) {
1625
+ return ContainsResult.NOT_CONTAINED;
1428
1626
  }
1429
- const mergedSubselections = new SelectionSetUpdates();
1430
- for (const selection of selections) {
1431
- mergedSubselections.add(selection.selectionSet!);
1432
- }
1433
- return first.withUpdatedSelectionSet(mergedSubselections.toSelectionSet(first.selectionSet.parentType));
1434
- }
1435
-
1436
- contains(that: SelectionSet): boolean {
1437
- // Note that we cannot really rely on the number of selections in `this` and `that` to short-cut this method
1438
- // due to the handling of "trivially nested selections". That is, `this` might have less top-level selections
1439
- // than `that`, and yet contains a named fragment directly on the parent type that includes everything in `that`.
1440
1627
 
1628
+ let isEqual = true;
1441
1629
  for (const [key, thatSelection] of that._keyedSelections) {
1442
1630
  const thisSelection = this._keyedSelections.get(key);
1443
- const otherSelections = this.triviallyNestedSelectionsForKey(this.parentType, key);
1444
- const mergedSelection = this.mergeSameKeySelections([thisSelection].concat(otherSelections).filter(isDefined));
1445
-
1446
- if (!(mergedSelection && mergedSelection.contains(thatSelection))
1447
- && !(thatSelection.isUnecessaryInlineFragment(this.parentType) && this.contains(thatSelection.selectionSet))
1448
- ) {
1449
- return false
1631
+ const selectionResult = thisSelection?.contains(thatSelection);
1632
+ if (selectionResult === undefined || selectionResult === ContainsResult.NOT_CONTAINED) {
1633
+ return ContainsResult.NOT_CONTAINED;
1450
1634
  }
1635
+ isEqual &&= selectionResult === ContainsResult.EQUAL;
1451
1636
  }
1452
- return true;
1637
+
1638
+ return isEqual && that._selections.length === this._selections.length
1639
+ ? ContainsResult.EQUAL
1640
+ : ContainsResult.STRICTLY_CONTAINED;
1453
1641
  }
1454
1642
 
1455
1643
  // Please note that this method assumes that `candidate.canApplyAtType(parentType) === true` but it is left to the caller to
1456
1644
  // validate this (`canApplyAtType` is not free, and we want to avoid repeating it multiple times).
1457
- diffWithNamedFragmentIfContained(candidate: NamedFragmentDefinition, parentType: CompositeType): { contains: boolean, diff?: SelectionSet } {
1458
- 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);
1651
+ // It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
1652
+ // `parentType` is empty, or contains only `__typename`. For instance, suppose we have
1653
+ // a union `U = A | B | C`, and then a fragment:
1654
+ // ```graphql
1655
+ // fragment F on U {
1656
+ // ... on A {
1657
+ // x
1658
+ // }
1659
+ // ... on b {
1660
+ // y
1661
+ // }
1662
+ // }
1663
+ // ```
1664
+ // It is then possible to apply `F` when the parent type is `C`, but this ends up selecting
1665
+ // nothing at all.
1666
+ //
1667
+ // Returning `contains: true` in those cases is, while not 100% incorrect, at least not productive,
1668
+ // and so we skip right away in that case. This is essentially an optimisation.
1669
+ if (that.isEmpty() || (that.selections().length === 1 && that.selections()[0].isTypenameField())) {
1670
+ return { contains: false };
1671
+ }
1672
+
1459
1673
  if (this.contains(that)) {
1460
1674
  // One subtlety here is that at "this" sub-selections may already have been optimized with some fragments. It's
1461
1675
  // usually ok because `candidate` will also use those fragments, but one fragments that `candidate` can never be
1462
1676
  // using is itself (the `contains` check is fine with this, but it's harder to deal in `minus`). So we expand
1463
1677
  // the candidate we're currently looking at in "this" to avoid some issues.
1464
- let updatedThis = this.expandFragments([candidate.name], this.fragments);
1678
+ let updatedThis = this.expandFragments(fragments.filter((f) => f.name !== candidate.name));
1465
1679
  if (updatedThis !== this) {
1466
1680
  updatedThis = updatedThis.trimUnsatisfiableBranches(parentType);
1467
1681
  }
@@ -1480,18 +1694,16 @@ export class SelectionSet {
1480
1694
 
1481
1695
  for (const [key, thisSelection] of this._keyedSelections) {
1482
1696
  const thatSelection = that._keyedSelections.get(key);
1483
- const otherSelections = that.triviallyNestedSelectionsForKey(this.parentType, key);
1484
- const allSelections = thatSelection ? [thatSelection].concat(otherSelections) : otherSelections;
1485
- if (allSelections.length === 0) {
1486
- updated.add(thisSelection);
1487
- } else {
1488
- const selectionDiff = allSelections.reduce<Selection | undefined>((prev, val) => prev?.minus(val), thisSelection);
1489
- if (selectionDiff) {
1490
- updated.add(selectionDiff);
1697
+ if (thatSelection) {
1698
+ const remainder = thisSelection.minus(thatSelection);
1699
+ if (remainder) {
1700
+ updated.add(remainder);
1491
1701
  }
1702
+ } else {
1703
+ updated.add(thisSelection);
1492
1704
  }
1493
1705
  }
1494
- return updated.toSelectionSet(this.parentType, this.fragments);
1706
+ return updated.toSelectionSet(this.parentType);
1495
1707
  }
1496
1708
 
1497
1709
  canRebaseOn(parentTypeToTest: CompositeType): boolean {
@@ -1590,13 +1802,14 @@ export class SelectionSet {
1590
1802
  toOperationString(
1591
1803
  rootKind: SchemaRootKind,
1592
1804
  variableDefinitions: VariableDefinitions,
1805
+ fragments: NamedFragments | undefined,
1593
1806
  operationName?: string,
1594
1807
  expandFragments: boolean = false,
1595
1808
  prettyPrint: boolean = true
1596
1809
  ): string {
1597
1810
  const indent = prettyPrint ? '' : undefined;
1598
- const fragmentsDefinitions = !expandFragments && this.fragments && !this.fragments.isEmpty()
1599
- ? this.fragments.toString(indent) + "\n\n"
1811
+ const fragmentsDefinitions = !expandFragments && fragments && !fragments.isEmpty()
1812
+ ? fragments.toString(indent) + "\n\n"
1600
1813
  : "";
1601
1814
  if (rootKind == "query" && !operationName && variableDefinitions.isEmpty()) {
1602
1815
  return fragmentsDefinitions + this.toString(expandFragments, true, indent);
@@ -1786,11 +1999,11 @@ function makeSelection(parentType: CompositeType, updates: SelectionUpdate[], fr
1786
1999
 
1787
2000
  // Optimize for the simple case of a single selection, as we don't have to do anything complex to merge the sub-selections.
1788
2001
  if (updates.length === 1 && first instanceof AbstractSelection) {
1789
- return first.rebaseOn(parentType);
2002
+ return first.rebaseOn(parentType, fragments);
1790
2003
  }
1791
2004
 
1792
2005
  const element = updateElement(first).rebaseOn(parentType);
1793
- const subSelectionParentType = element.kind === 'Field' ? baseType(element.definition.type!) : element.castedType();
2006
+ const subSelectionParentType = element.kind === 'Field' ? element.baseType() : element.castedType();
1794
2007
  if (!isCompositeType(subSelectionParentType)) {
1795
2008
  // This is a leaf, so all updates should correspond ot the same field and we just use the first.
1796
2009
  return selectionOfElement(element);
@@ -1833,7 +2046,7 @@ function makeSelectionSet(parentType: CompositeType, keyedUpdates: MultiMap<stri
1833
2046
  for (const [key, updates] of keyedUpdates.entries()) {
1834
2047
  selections.set(key, makeSelection(parentType, updates, fragments));
1835
2048
  }
1836
- return new SelectionSet(parentType, selections, fragments);
2049
+ return new SelectionSet(parentType, selections);
1837
2050
  }
1838
2051
 
1839
2052
  /**
@@ -1943,14 +2156,14 @@ export function allFieldDefinitionsInSelectionSet(selection: SelectionSet): Fiel
1943
2156
  return allFields;
1944
2157
  }
1945
2158
 
1946
- export function selectionSetOf(parentType: CompositeType, selection: Selection, fragments?: NamedFragments): SelectionSet {
2159
+ export function selectionSetOf(parentType: CompositeType, selection: Selection): SelectionSet {
1947
2160
  const map = new Map<string, Selection>()
1948
2161
  map.set(selection.key(), selection);
1949
- return new SelectionSet(parentType, map, fragments);
2162
+ return new SelectionSet(parentType, map);
1950
2163
  }
1951
2164
 
1952
- export function selectionSetOfElement(element: OperationElement, subSelection?: SelectionSet, fragments?: NamedFragments): SelectionSet {
1953
- 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));
1954
2167
  }
1955
2168
 
1956
2169
  export function selectionOfElement(element: OperationElement, subSelection?: SelectionSet): Selection {
@@ -1978,12 +2191,17 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
1978
2191
 
1979
2192
  abstract validate(variableDefinitions: VariableDefinitions): void;
1980
2193
 
1981
- abstract rebaseOn(parentType: CompositeType): TOwnType;
2194
+ abstract rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): TOwnType;
1982
2195
 
1983
2196
  get parentType(): CompositeType {
1984
2197
  return this.element.parentType;
1985
2198
  }
1986
2199
 
2200
+ isTypenameField(): boolean {
2201
+ // Overridden where appropriate
2202
+ return false;
2203
+ }
2204
+
1987
2205
  collectVariables(collector: VariableCollector) {
1988
2206
  this.element.collectVariables(collector);
1989
2207
  this.selectionSet?.collectVariables(collector)
@@ -1993,10 +2211,6 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
1993
2211
  this.selectionSet?.collectUsedFragmentNames(collector);
1994
2212
  }
1995
2213
 
1996
- namedFragments(): NamedFragments | undefined {
1997
- return this.selectionSet?.fragments;
1998
- }
1999
-
2000
2214
  abstract withUpdatedComponents(element: TElement, selectionSet: SelectionSet | TIsLeaf): TOwnType;
2001
2215
 
2002
2216
  withUpdatedSelectionSet(selectionSet: SelectionSet | TIsLeaf): TOwnType {
@@ -2024,12 +2238,14 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2024
2238
 
2025
2239
  abstract hasDefer(): boolean;
2026
2240
 
2027
- abstract expandAllFragments(): TOwnType | readonly Selection[];
2028
-
2029
- abstract expandFragments(names: string[], updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
2241
+ abstract expandFragments(updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
2030
2242
 
2031
2243
  abstract trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): TOwnType | SelectionSet | undefined;
2032
2244
 
2245
+ isFragmentSpread(): boolean {
2246
+ return false;
2247
+ }
2248
+
2033
2249
  minus(that: Selection): TOwnType | undefined {
2034
2250
  // If there is a subset, then we compute the diff of the subset and add that (if not empty).
2035
2251
  // Otherwise, we have no diff.
@@ -2042,53 +2258,131 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2042
2258
  return undefined;
2043
2259
  }
2044
2260
 
2045
- // Attempts to optimize the subselection of this field selection using named fragments `candidates` _assuming_ that
2046
- // those candidates do apply at `parentType` (that is, `candidates.every((c) => c.canApplyAtType(parentType))` is true,
2047
- // which is ensured by the fact that `tryOptimizeSubselectionWithFragments` calls this on a subset of the candidates
2048
- // returned by `maybeApplyingAtType`).
2049
- protected tryOptimizeSubselectionOnce(_: {
2050
- parentType: CompositeType,
2051
- subSelection: SelectionSet,
2052
- candidates: NamedFragmentDefinition[],
2053
- fragments: NamedFragments,
2054
- }): {
2055
- spread?: FragmentSpreadSelection,
2056
- optimizedSelection?: SelectionSet,
2057
- hasDiff?: boolean,
2058
- } {
2059
- // Field and inline fragment override this, but this should never be called for a spread.
2060
- assert(false, `UNSUPPORTED`);
2061
- }
2062
-
2063
2261
  protected tryOptimizeSubselectionWithFragments({
2064
2262
  parentType,
2065
2263
  subSelection,
2066
2264
  fragments,
2067
- fragmentFilter,
2265
+ canUseFullMatchingFragment,
2068
2266
  }: {
2069
2267
  parentType: CompositeType,
2070
2268
  subSelection: SelectionSet,
2071
2269
  fragments: NamedFragments,
2072
- fragmentFilter?: (f: NamedFragmentDefinition) => boolean,
2073
- }): SelectionSet | FragmentSpreadSelection {
2270
+ canUseFullMatchingFragment: (match: NamedFragmentDefinition) => boolean,
2271
+ }): SelectionSet | NamedFragmentDefinition {
2074
2272
  let candidates = fragments.maybeApplyingAtType(parentType);
2075
- if (fragmentFilter) {
2076
- candidates = candidates.filter(fragmentFilter);
2077
- }
2078
- let shouldTryAgain: boolean;
2079
- do {
2080
- const { spread, optimizedSelection, hasDiff } = this.tryOptimizeSubselectionOnce({ parentType, subSelection, candidates, fragments });
2081
- if (optimizedSelection) {
2082
- subSelection = optimizedSelection;
2083
- } else if (spread) {
2084
- 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;
2085
2301
  }
2086
- shouldTryAgain = !!spread && !!hasDiff;
2087
- if (shouldTryAgain) {
2088
- 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);
2089
2317
  }
2090
- } while (shouldTryAgain);
2091
- 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)
2092
2386
  }
2093
2387
  }
2094
2388
 
@@ -2110,6 +2404,10 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2110
2404
  return this;
2111
2405
  }
2112
2406
 
2407
+ isTypenameField(): boolean {
2408
+ return this.element.definition.name === typenameFieldName;
2409
+ }
2410
+
2113
2411
  withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
2114
2412
  return new FieldSelection(field, selectionSet);
2115
2413
  }
@@ -2119,57 +2417,36 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2119
2417
  }
2120
2418
 
2121
2419
  optimize(fragments: NamedFragments): Selection {
2122
- let optimizedSelection = this.selectionSet ? this.selectionSet.optimizeSelections(fragments) : undefined;
2123
2420
  const fieldBaseType = baseType(this.element.definition.type!);
2124
- if (isCompositeType(fieldBaseType) && optimizedSelection) {
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) {
2125
2428
  const optimized = this.tryOptimizeSubselectionWithFragments({
2126
2429
  parentType: fieldBaseType,
2127
- subSelection: optimizedSelection,
2430
+ subSelection: this.selectionSet,
2128
2431
  fragments,
2129
- // We can never apply a fragments that has directives on it at the field level (but when those are expanded,
2130
- // their type condition would always be preserved due to said applied directives, so they will always
2131
- // be handled by `InlineFragmentSelection.optimize` anyway).
2132
- 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,
2133
2434
  });
2134
2435
 
2135
- assert(!(optimized instanceof FragmentSpreadSelection), 'tryOptimizeSubselectionOnce should never return only a spread');
2136
- optimizedSelection = optimized;
2436
+ if (optimized instanceof NamedFragmentDefinition) {
2437
+ optimizedSelection = selectionSetOf(fieldBaseType, new FragmentSpreadSelection(fieldBaseType, fragments, optimized, []));
2438
+ } else {
2439
+ optimizedSelection = optimized;
2440
+ }
2137
2441
  }
2138
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
+
2139
2447
  return this.selectionSet === optimizedSelection
2140
2448
  ? this
2141
- : new FieldSelection(this.element, optimizedSelection);
2142
- }
2143
-
2144
- protected tryOptimizeSubselectionOnce({
2145
- parentType,
2146
- subSelection,
2147
- candidates,
2148
- fragments,
2149
- }: {
2150
- parentType: CompositeType,
2151
- subSelection: SelectionSet,
2152
- candidates: NamedFragmentDefinition[],
2153
- fragments: NamedFragments,
2154
- }): {
2155
- spread?: FragmentSpreadSelection,
2156
- optimizedSelection?: SelectionSet,
2157
- hasDiff?: boolean,
2158
- }{
2159
- let optimizedSelection = subSelection;
2160
- for (const candidate of candidates) {
2161
- const { contains, diff } = optimizedSelection.diffWithNamedFragmentIfContained(candidate, parentType);
2162
- if (contains) {
2163
- // We can optimize the selection with this fragment. The replaced sub-selection will be
2164
- // comprised of this new spread and the remaining `diff` if there is any.
2165
- const spread = new FragmentSpreadSelection(parentType, fragments, candidate, []);
2166
- optimizedSelection = diff
2167
- ? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
2168
- : selectionSetOf(parentType, spread);
2169
- return { spread, optimizedSelection, hasDiff: !!diff }
2170
- }
2171
- }
2172
- return {};
2449
+ : this.withUpdatedSelectionSet(optimizedSelection);
2173
2450
  }
2174
2451
 
2175
2452
  filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
@@ -2203,7 +2480,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2203
2480
  * Obviously, this operation will only succeed if this selection (both the field itself and its subselections)
2204
2481
  * make sense from the provided parent type. If this is not the case, this method will throw.
2205
2482
  */
2206
- rebaseOn(parentType: CompositeType): FieldSelection {
2483
+ rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FieldSelection {
2207
2484
  if (this.element.parentType === parentType) {
2208
2485
  return this;
2209
2486
  }
@@ -2213,13 +2490,13 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2213
2490
  return this.withUpdatedElement(rebasedElement);
2214
2491
  }
2215
2492
 
2216
- const rebasedBase = baseType(rebasedElement.definition.type!);
2493
+ const rebasedBase = rebasedElement.baseType();
2217
2494
  if (rebasedBase === this.selectionSet.parentType) {
2218
2495
  return this.withUpdatedElement(rebasedElement);
2219
2496
  }
2220
2497
 
2221
2498
  validate(isCompositeType(rebasedBase), () => `Cannot rebase field selection ${this} on ${parentType}: rebased field base return type ${rebasedBase} is not composite`);
2222
- return this.withUpdatedComponents(rebasedElement, this.selectionSet.rebaseOn(rebasedBase));
2499
+ return this.withUpdatedComponents(rebasedElement, this.selectionSet.rebaseOn(rebasedBase, fragments));
2223
2500
  }
2224
2501
 
2225
2502
  /**
@@ -2270,16 +2547,12 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2270
2547
  return !!this.selectionSet?.hasDefer();
2271
2548
  }
2272
2549
 
2273
- expandAllFragments(): FieldSelection {
2274
- return this.mapToSelectionSet((s) => s.expandAllFragments());
2275
- }
2276
-
2277
2550
  trimUnsatisfiableBranches(_: CompositeType, options?: { recursive? : boolean }): FieldSelection {
2278
2551
  if (!this.selectionSet) {
2279
2552
  return this;
2280
2553
  }
2281
2554
 
2282
- const base = baseType(this.element.definition.type!)
2555
+ const base = this.element.baseType()
2283
2556
  assert(isCompositeType(base), () => `Field ${this.element} should not have a sub-selection`);
2284
2557
  const trimmed = (options?.recursive ?? true) ? this.mapToSelectionSet((s) => s.trimUnsatisfiableBranches(base)) : this;
2285
2558
  // In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
@@ -2299,8 +2572,8 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2299
2572
  }
2300
2573
  }
2301
2574
 
2302
- expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FieldSelection {
2303
- return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
2575
+ expandFragments(updatedFragments?: NamedFragments): FieldSelection {
2576
+ return this.mapToSelectionSet((s) => s.expandFragments(updatedFragments));
2304
2577
  }
2305
2578
 
2306
2579
  equals(that: Selection): boolean {
@@ -2317,20 +2590,17 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2317
2590
  return !!that.selectionSet && this.selectionSet.equals(that.selectionSet);
2318
2591
  }
2319
2592
 
2320
- contains(that: Selection): boolean {
2593
+ contains(that: Selection): ContainsResult {
2321
2594
  if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
2322
- return false;
2595
+ return ContainsResult.NOT_CONTAINED;
2323
2596
  }
2324
2597
 
2325
- if (!that.selectionSet) {
2326
- 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;
2327
2601
  }
2328
- return !!this.selectionSet && this.selectionSet.contains(that.selectionSet);
2329
- }
2330
-
2331
- isUnecessaryInlineFragment(_: CompositeType): this is InlineFragmentSelection {
2332
- // Overridden by inline fragments
2333
- 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);
2334
2604
  }
2335
2605
 
2336
2606
  toString(expandFragments: boolean = true, indent?: string): string {
@@ -2375,17 +2645,7 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
2375
2645
 
2376
2646
  abstract equals(that: Selection): boolean;
2377
2647
 
2378
- abstract contains(that: Selection): boolean;
2379
-
2380
- isUnecessaryInlineFragment(parentType: CompositeType): boolean {
2381
- return this.element.appliedDirectives.length === 0
2382
- && !!this.element.typeCondition
2383
- && (
2384
- this.element.typeCondition.name === parentType.name
2385
- || (isObjectType(parentType) && possibleRuntimeTypes(this.element.typeCondition).some((t) => t.name === parentType.name))
2386
- );
2387
- }
2388
-
2648
+ abstract contains(that: Selection): ContainsResult;
2389
2649
  }
2390
2650
 
2391
2651
  class InlineFragmentSelection extends FragmentSelection {
@@ -2419,7 +2679,7 @@ class InlineFragmentSelection extends FragmentSelection {
2419
2679
  this.selectionSet.validate(variableDefinitions);
2420
2680
  }
2421
2681
 
2422
- rebaseOn(parentType: CompositeType): FragmentSelection {
2682
+ rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
2423
2683
  if (this.parentType === parentType) {
2424
2684
  return this;
2425
2685
  }
@@ -2430,7 +2690,7 @@ class InlineFragmentSelection extends FragmentSelection {
2430
2690
  return this.withUpdatedElement(rebasedFragment);
2431
2691
  }
2432
2692
 
2433
- return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType));
2693
+ return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType, fragments));
2434
2694
  }
2435
2695
 
2436
2696
  canAddTo(parentType: CompositeType): boolean {
@@ -2468,86 +2728,60 @@ class InlineFragmentSelection extends FragmentSelection {
2468
2728
  }
2469
2729
 
2470
2730
  optimize(fragments: NamedFragments): FragmentSelection {
2471
- 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.
2472
2734
  const typeCondition = this.element.typeCondition;
2473
2735
  if (typeCondition) {
2474
2736
  const optimized = this.tryOptimizeSubselectionWithFragments({
2475
2737
  parentType: typeCondition,
2476
2738
  subSelection: optimizedSelection,
2477
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
+ },
2478
2753
  });
2479
- if (optimized instanceof FragmentSpreadSelection) {
2480
- // This means the whole inline fragment can be replaced by the spread.
2481
- return optimized;
2482
- }
2483
- optimizedSelection = optimized;
2484
- }
2485
- return this.selectionSet === optimizedSelection
2486
- ? this
2487
- : new InlineFragmentSelection(this.element, optimizedSelection);
2488
- }
2489
2754
 
2490
- protected tryOptimizeSubselectionOnce({
2491
- parentType,
2492
- subSelection,
2493
- candidates,
2494
- fragments,
2495
- }: {
2496
- parentType: CompositeType,
2497
- subSelection: SelectionSet,
2498
- candidates: NamedFragmentDefinition[],
2499
- fragments: NamedFragments,
2500
- }): {
2501
- spread?: FragmentSpreadSelection,
2502
- optimizedSelection?: SelectionSet,
2503
- hasDiff?: boolean,
2504
- }{
2505
- let optimizedSelection = subSelection;
2506
- for (const candidate of candidates) {
2507
- const { contains, diff } = optimizedSelection.diffWithNamedFragmentIfContained(candidate, parentType);
2508
- if (contains) {
2509
- // The candidate selection is included in our sub-selection. One remaining thing to take into account
2510
- // is applied directives: if the candidate has directives, then we can only use it if 1) there is
2511
- // no `diff`, 2) the type condition of this fragment matches the candidate one and 3) the directives
2512
- // in question are also on this very fragment. In that case, we can replace this whole inline fragment
2513
- // by a spread of the candidate.
2514
- if (!diff && sameType(this.element.typeCondition!, candidate.typeCondition)) {
2515
- // We can potentially replace the whole fragment by the candidate; but as said above, still needs
2516
- // to check the directives.
2517
- let spreadDirectives: Directive<any>[] = this.element.appliedDirectives;
2518
- if (candidate.appliedDirectives.length > 0) {
2519
- const { isSubset, difference } = diffDirectives(this.element.appliedDirectives, candidate.appliedDirectives);
2520
- if (!isSubset) {
2521
- // While the candidate otherwise match, it has directives that are not on this element, so we
2522
- // cannot reuse it.
2523
- continue;
2524
- }
2525
- // Otherwise, any directives on this element that are not on the candidate should be kept and used
2526
- // on the spread created.
2527
- 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
+ );
2528
2767
  }
2529
- // Returning a spread without a subselection will make the code "replace" this whole inline fragment
2530
- // by the spread, which is what we want. Do not that as we're replacing the whole inline fragment,
2531
- // we use `this.parentType` instead of `parentType` (the later being `this.element.typeCondition` basically).
2532
- return {
2533
- spread: new FragmentSpreadSelection(this.parentType, fragments, candidate, spreadDirectives),
2534
- };
2535
- }
2536
-
2537
- // We're already dealt with the one case where we might be able to handle a candidate that has directives.
2538
- if (candidate.appliedDirectives.length > 0) {
2539
- 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, []));
2540
2772
  }
2541
-
2542
- const spread = new FragmentSpreadSelection(parentType, fragments, candidate, []);
2543
- optimizedSelection = diff
2544
- ? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
2545
- : selectionSetOf(parentType, spread);
2546
-
2547
- return { spread, optimizedSelection, hasDiff: !!diff };
2773
+ } else {
2774
+ optimizedSelection = optimized;
2548
2775
  }
2549
2776
  }
2550
- 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);
2551
2785
  }
2552
2786
 
2553
2787
  withoutDefer(labelsToRemove?: Set<string>): InlineFragmentSelection | SelectionSet {
@@ -2597,7 +2831,7 @@ class InlineFragmentSelection extends FragmentSelection {
2597
2831
  if (isObjectType(thisCondition) || !possibleRuntimeTypes(thisCondition).includes(currentType)) {
2598
2832
  return undefined;
2599
2833
  } else {
2600
- const trimmed =this.selectionSet.trimUnsatisfiableBranches(currentType, options);
2834
+ const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType, options);
2601
2835
  return trimmed.isEmpty() ? undefined : trimmed;
2602
2836
  }
2603
2837
  }
@@ -2664,12 +2898,8 @@ class InlineFragmentSelection extends FragmentSelection {
2664
2898
  return this.selectionSet === trimmedSelectionSet ? this : this.withUpdatedSelectionSet(trimmedSelectionSet);
2665
2899
  }
2666
2900
 
2667
- expandAllFragments(): FragmentSelection {
2668
- return this.mapToSelectionSet((s) => s.expandAllFragments());
2669
- }
2670
-
2671
- expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FragmentSelection {
2672
- return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
2901
+ expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection {
2902
+ return this.mapToSelectionSet((s) => s.expandFragments(updatedFragments));
2673
2903
  }
2674
2904
 
2675
2905
  equals(that: Selection): boolean {
@@ -2682,10 +2912,12 @@ class InlineFragmentSelection extends FragmentSelection {
2682
2912
  && this.selectionSet.equals(that.selectionSet);
2683
2913
  }
2684
2914
 
2685
- contains(that: Selection): boolean {
2686
- return (that instanceof FragmentSelection)
2687
- && this.element.equals(that.element)
2688
- && 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);
2689
2921
  }
2690
2922
 
2691
2923
  toString(expandFragments: boolean = true, indent?: string): string {
@@ -2693,14 +2925,6 @@ class InlineFragmentSelection extends FragmentSelection {
2693
2925
  }
2694
2926
  }
2695
2927
 
2696
- function diffDirectives(superset: readonly Directive<any>[], maybeSubset: readonly Directive<any>[]): { isSubset: boolean, difference: Directive[] } {
2697
- if (maybeSubset.every((d) => superset.some((s) => sameDirectiveApplication(d, s)))) {
2698
- return { isSubset: true, difference: superset.filter((s) => !maybeSubset.some((d) => sameDirectiveApplication(d, s))) };
2699
- } else {
2700
- return { isSubset: false, difference: [] };
2701
- }
2702
- }
2703
-
2704
2928
  class FragmentSpreadSelection extends FragmentSelection {
2705
2929
  private computedKey: string | undefined;
2706
2930
 
@@ -2713,6 +2937,10 @@ class FragmentSpreadSelection extends FragmentSelection {
2713
2937
  super(new FragmentElement(sourceType, namedFragment.typeCondition, namedFragment.appliedDirectives.concat(spreadDirectives)));
2714
2938
  }
2715
2939
 
2940
+ isFragmentSpread(): boolean {
2941
+ return true;
2942
+ }
2943
+
2716
2944
  get selectionSet(): SelectionSet {
2717
2945
  return this.namedFragment.selectionSet;
2718
2946
  }
@@ -2728,12 +2956,11 @@ class FragmentSpreadSelection extends FragmentSelection {
2728
2956
  assert(false, `Unsupported`);
2729
2957
  }
2730
2958
 
2731
- trimUnsatisfiableBranches(_: CompositeType): FragmentSelection {
2732
- return this;
2733
- }
2734
-
2735
- namedFragments(): NamedFragments | undefined {
2736
- return this.fragments;
2959
+ trimUnsatisfiableBranches(parentType: CompositeType): FragmentSelection {
2960
+ // We must update the spread parent type if necessary since we're not going deeper,
2961
+ // or we'll be fundamentally losing context.
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);
2737
2964
  }
2738
2965
 
2739
2966
  validate(): void {
@@ -2766,41 +2993,55 @@ class FragmentSpreadSelection extends FragmentSelection {
2766
2993
  return this;
2767
2994
  }
2768
2995
 
2769
- rebaseOn(_parentType: CompositeType): FragmentSelection {
2770
- // This is a little bit iffy, because the fragment could link to a schema (typically the supergraph API one)
2771
- // that is different from the one of `_selectionSet` (say, a subgraph fetch selection in which we're trying to
2772
- // reuse a user fragment). But in practice, we expand all fragments when we do query planning and only re-add
2773
- // fragments back at the very end, so this should be fine. Importantly, we don't want this method to mistakenly
2774
- // expand the spread, as that would compromise the code that optimize subgraph fetches to re-use named
2996
+ rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
2997
+ // We preserve the parent type here, to make sure we don't lose context, but we actually don't
2998
+ // want to expand the spread as that would compromise the code that optimize subgraph fetches to re-use named
2775
2999
  // fragments.
2776
- return this;
3000
+ //
3001
+ // This is a little bit iffy, because the fragment may not apply at this parent type, but we
3002
+ // currently leave it to the caller to ensure this is not a mistake. But most of the
3003
+ // QP code works on selections with fully expanded fragments, so this code (and that of `canAddTo`
3004
+ // on come into play in the code for reusing fragments, and that code calls those methods
3005
+ // appropriately.
3006
+ if (this.parentType === parentType) {
3007
+ return this;
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`);
3017
+ return new FragmentSpreadSelection(
3018
+ parentType,
3019
+ newFragments,
3020
+ namedFragment,
3021
+ this.spreadDirectives,
3022
+ );
2777
3023
  }
2778
3024
 
2779
3025
  canAddTo(_: CompositeType): boolean {
2780
- // Mimicking the logic of `rebaseOn`.
3026
+ // Since `rebaseOn` never fail, we copy the logic here and always return `true`. But as
3027
+ // mentioned in `rebaseOn`, this leave it a bit to the caller to know what he is doing.
2781
3028
  return true;
2782
3029
  }
2783
3030
 
2784
- expandAllFragments(): FragmentSelection | readonly Selection[] {
2785
- const expandedSubSelections = this.selectionSet.expandAllFragments();
2786
- return sameType(this.parentType, this.namedFragment.typeCondition) && this.element.appliedDirectives.length === 0
2787
- ? expandedSubSelections.selections()
2788
- : new InlineFragmentSelection(this.element, expandedSubSelections);
2789
- }
2790
-
2791
- expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FragmentSelection | readonly Selection[] {
2792
- 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.
2793
3035
  return this;
2794
3036
  }
2795
3037
 
2796
- const expandedSubSelections = this.selectionSet.expandFragments(names, updatedFragments);
3038
+ const expandedSubSelections = this.selectionSet.expandFragments(updatedFragments);
2797
3039
  return sameType(this.parentType, this.namedFragment.typeCondition) && this.element.appliedDirectives.length === 0
2798
3040
  ? expandedSubSelections.selections()
2799
3041
  : new InlineFragmentSelection(this.element, expandedSubSelections);
2800
3042
  }
2801
3043
 
2802
3044
  collectUsedFragmentNames(collector: Map<string, number>): void {
2803
- this.selectionSet.collectUsedFragmentNames(collector);
2804
3045
  const usageCount = collector.get(this.namedFragment.name);
2805
3046
  collector.set(this.namedFragment.name, usageCount === undefined ? 1 : usageCount + 1);
2806
3047
  }
@@ -2828,14 +3069,16 @@ class FragmentSpreadSelection extends FragmentSelection {
2828
3069
  && sameDirectiveApplications(this.spreadDirectives, that.spreadDirectives);
2829
3070
  }
2830
3071
 
2831
- contains(that: Selection): boolean {
3072
+ contains(that: Selection): ContainsResult {
2832
3073
  if (this.equals(that)) {
2833
- return true;
3074
+ return ContainsResult.EQUAL;
2834
3075
  }
2835
3076
 
2836
- return (that instanceof FragmentSelection)
2837
- && this.element.equals(that.element)
2838
- && 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);
2839
3082
  }
2840
3083
 
2841
3084
  toString(expandFragments: boolean = true, indent?: string): string {
@@ -2860,7 +3103,6 @@ function selectionSetOfNode(
2860
3103
  return selectionSetOf(
2861
3104
  parentType,
2862
3105
  selectionOfNode(parentType, node.selections[0], variableDefinitions, fragments, fieldAccessor),
2863
- fragments,
2864
3106
  );
2865
3107
  }
2866
3108
 
@@ -2992,6 +3234,7 @@ function operationFromAST({
2992
3234
  }) : Operation {
2993
3235
  const rootType = schema.schemaDefinition.root(operation.operation);
2994
3236
  validate(rootType, () => `The schema has no "${operation.operation}" root type defined`);
3237
+ const fragmentsIfAny = fragments.isEmpty() ? undefined : fragments;
2995
3238
  return new Operation(
2996
3239
  schema,
2997
3240
  operation.operation,
@@ -2999,10 +3242,11 @@ function operationFromAST({
2999
3242
  parentType: rootType.type,
3000
3243
  source: operation.selectionSet,
3001
3244
  variableDefinitions,
3002
- fragments: fragments.isEmpty() ? undefined : fragments,
3245
+ fragments: fragmentsIfAny,
3003
3246
  validate: validateInput,
3004
3247
  }),
3005
3248
  variableDefinitions,
3249
+ fragmentsIfAny,
3006
3250
  operation.name?.value
3007
3251
  );
3008
3252
  }
@@ -3059,8 +3303,8 @@ export function operationToDocument(operation: Operation): DocumentNode {
3059
3303
  selectionSet: operation.selectionSet.toSelectionSetNode(),
3060
3304
  variableDefinitions: operation.variableDefinitions.toVariableDefinitionNodes(),
3061
3305
  };
3062
- const fragmentASTs: DefinitionNode[] = operation.selectionSet.fragments
3063
- ? operation.selectionSet.fragments?.toFragmentDefinitionNodes()
3306
+ const fragmentASTs: DefinitionNode[] = operation.fragments
3307
+ ? operation.fragments?.toFragmentDefinitionNodes()
3064
3308
  : [];
3065
3309
  return {
3066
3310
  kind: Kind.DOCUMENT,