@apollo/federation-internals 2.4.7 → 2.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/operations.ts CHANGED
@@ -47,10 +47,11 @@ import {
47
47
  Variables,
48
48
  isObjectType,
49
49
  NamedType,
50
+ isUnionType,
50
51
  } from "./definitions";
51
52
  import { isInterfaceObjectType } from "./federation";
52
53
  import { ERRORS } from "./error";
53
- import { isSubtype, sameType } from "./types";
54
+ import { isSubtype, sameType, typesCanBeMerged } from "./types";
54
55
  import { assert, mapKeys, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
55
56
  import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
56
57
  import { v1 as uuidv1 } from 'uuid';
@@ -84,7 +85,11 @@ abstract class AbstractOperationElement<T extends AbstractOperationElement<T>> e
84
85
 
85
86
  abstract asPathElement(): string | undefined;
86
87
 
87
- abstract rebaseOn(parentType: CompositeType): T;
88
+ abstract rebaseOn(args: { parentType: CompositeType, errorIfCannotRebase: boolean }): T | undefined;
89
+
90
+ rebaseOnOrError(parentType: CompositeType): T {
91
+ return this.rebaseOn({ parentType, errorIfCannotRebase: true })!;
92
+ }
88
93
 
89
94
  abstract withUpdatedDirectives(newDirectives: readonly Directive<any>[]): T;
90
95
 
@@ -115,7 +120,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
115
120
 
116
121
  constructor(
117
122
  readonly definition: FieldDefinition<CompositeType>,
118
- private readonly args?: TArgs,
123
+ readonly args?: TArgs,
119
124
  directives?: readonly Directive<any>[],
120
125
  readonly alias?: string,
121
126
  ) {
@@ -289,7 +294,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
289
294
  }
290
295
  }
291
296
 
292
- rebaseOn(parentType: CompositeType): Field<TArgs> {
297
+ rebaseOn({ parentType, errorIfCannotRebase }: { parentType: CompositeType, errorIfCannotRebase: boolean }): Field<TArgs> | undefined {
293
298
  const fieldParent = this.definition.parent;
294
299
  if (parentType === fieldParent) {
295
300
  return this;
@@ -299,12 +304,16 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
299
304
  return this.withUpdatedDefinition(parentType.typenameField()!);
300
305
  }
301
306
 
302
- validate(
303
- this.canRebaseOn(parentType),
304
- () => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${parentType}"`
305
- );
306
307
  const fieldDef = parentType.field(this.name);
307
- validate(fieldDef, () => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${parentType}" (that does not declare that field)`);
308
+ const canRebase = this.canRebaseOn(parentType) && fieldDef;
309
+ if (!canRebase) {
310
+ validate(
311
+ !errorIfCannotRebase,
312
+ () => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${parentType}"`
313
+ );
314
+ return undefined;
315
+ }
316
+
308
317
  return this.withUpdatedDefinition(fieldDef);
309
318
  }
310
319
 
@@ -465,7 +474,7 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
465
474
  return newFragment;
466
475
  }
467
476
 
468
- rebaseOn(parentType: CompositeType): FragmentElement {
477
+ rebaseOn({ parentType, errorIfCannotRebase }: { parentType: CompositeType, errorIfCannotRebase: boolean }): FragmentElement | undefined {
469
478
  const fragmentParent = this.parentType;
470
479
  const typeCondition = this.typeCondition;
471
480
  if (parentType === fragmentParent) {
@@ -476,10 +485,13 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
476
485
  // to update the source type of the fragment, but also "rebase" the condition to the selection set
477
486
  // schema.
478
487
  const { canRebase, rebasedCondition } = this.canRebaseOn(parentType);
479
- validate(
480
- canRebase,
481
- () => `Cannot add fragment of condition "${typeCondition}" (runtimes: [${possibleRuntimeTypes(typeCondition!)}]) to parent type "${parentType}" (runtimes: ${possibleRuntimeTypes(parentType)})`
482
- );
488
+ if (!canRebase) {
489
+ validate(
490
+ !errorIfCannotRebase,
491
+ () => `Cannot add fragment of condition "${typeCondition}" (runtimes: [${possibleRuntimeTypes(typeCondition!)}]) to parent type "${parentType}" (runtimes: ${possibleRuntimeTypes(parentType)})`
492
+ );
493
+ return undefined;
494
+ }
483
495
  return this.withUpdatedTypes(parentType, rebasedCondition);
484
496
  }
485
497
 
@@ -865,69 +877,104 @@ export class Operation {
865
877
  readonly name?: string) {
866
878
  }
867
879
 
868
- optimize(fragments?: NamedFragments, minUsagesToOptimize: number = 2): Operation {
869
- assert(minUsagesToOptimize >= 1, `Expected 'minUsagesToOptimize' to be at least 1, but got ${minUsagesToOptimize}`)
870
- if (!fragments || fragments.isEmpty()) {
871
- return this;
872
- }
873
-
874
- let optimizedSelection = this.selectionSet.optimize(fragments);
875
- if (optimizedSelection === this.selectionSet) {
876
- return this;
877
- }
878
-
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);
884
- }
885
-
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);
892
-
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);
897
- }
898
-
899
- expandAllFragments(): Operation {
900
- const expandedSelections = this.selectionSet.expandFragments();
901
- if (expandedSelections === this.selectionSet) {
880
+ // Returns a copy of this operation with the provided updated selection set.
881
+ // Note that this method assumes that the existing `this.fragments` is still appropriate.
882
+ private withUpdatedSelectionSet(newSelectionSet: SelectionSet): Operation {
883
+ if (this.selectionSet === newSelectionSet) {
902
884
  return this;
903
885
  }
904
886
 
905
887
  return new Operation(
906
888
  this.schema,
907
889
  this.rootKind,
908
- expandedSelections,
890
+ newSelectionSet,
909
891
  this.variableDefinitions,
910
- undefined,
892
+ this.fragments,
911
893
  this.name
912
894
  );
913
895
  }
914
896
 
915
- trimUnsatisfiableBranches(): Operation {
916
- const trimmedSelections = this.selectionSet.trimUnsatisfiableBranches(this.selectionSet.parentType);
917
- if (trimmedSelections === this.selectionSet) {
897
+ // Returns a copy of this operation with the provided updated selection set and fragments.
898
+ private withUpdatedSelectionSetAndFragments(newSelectionSet: SelectionSet, newFragments: NamedFragments | undefined): Operation {
899
+ if (this.selectionSet === newSelectionSet && newFragments === this.fragments) {
918
900
  return this;
919
901
  }
920
902
 
921
903
  return new Operation(
922
904
  this.schema,
923
905
  this.rootKind,
924
- trimmedSelections,
906
+ newSelectionSet,
925
907
  this.variableDefinitions,
926
- this.fragments,
908
+ newFragments,
927
909
  this.name
928
910
  );
929
911
  }
930
912
 
913
+ optimize(fragments?: NamedFragments, minUsagesToOptimize: number = 2): Operation {
914
+ assert(minUsagesToOptimize >= 1, `Expected 'minUsagesToOptimize' to be at least 1, but got ${minUsagesToOptimize}`)
915
+ if (!fragments || fragments.isEmpty()) {
916
+ return this;
917
+ }
918
+
919
+ let optimizedSelection = this.selectionSet.optimize(fragments);
920
+ if (optimizedSelection === this.selectionSet) {
921
+ return this;
922
+ }
923
+
924
+ let finalFragments = computeFragmentsToKeep(optimizedSelection, fragments, minUsagesToOptimize);
925
+
926
+ // If there is fragment usages and we're not keeping all fragments, we need to expand fragments.
927
+ if (finalFragments !== null && finalFragments?.size !== fragments.size) {
928
+ // Note that optimizing all fragments to potentially re-expand some is not entirely optimal, but it's unclear
929
+ // how to do otherwise, and it probably don't matter too much in practice (we only call this optimization
930
+ // on the final computed query plan, so not a very hot path; plus in most cases we won't even reach that
931
+ // point either because there is no fragment, or none will have been optimized away so we'll exit above).
932
+ optimizedSelection = optimizedSelection.expandFragments(finalFragments);
933
+
934
+ // Expanding fragments could create some "inefficiencies" that we wouldn't have if we hadn't re-optimized
935
+ // the fragments to de-optimize it later, so we do a final "normalize" pass to remove those.
936
+ optimizedSelection = optimizedSelection.normalize({ parentType: optimizedSelection.parentType });
937
+
938
+ // And if we've expanded some fragments but kept others, then it's not 100% impossible that some
939
+ // fragment was used multiple times in some expanded fragment(s), but that post-expansion all of
940
+ // it's usages are "dead" branches that are removed by the final `normalize`. In that case though,
941
+ // we need to ensure we don't include the now-unused fragment in the final list of fragments.
942
+ // TODO: remark that the same reasoning could leave a single instance of a fragment usage, so if
943
+ // we really really want to never have less than `minUsagesToOptimize`, we could do some loop of
944
+ // `expand then normalize` unless all fragments are provably used enough. We don't bother, because
945
+ // leaving this is not a huge deal and it's not worth the complexity, but it could be that we can
946
+ // refactor all this later to avoid this case without additional complexity.
947
+ if (finalFragments) {
948
+ // Note that removing a fragment might lead to another fragment being unused, so we need to iterate
949
+ // until there is nothing more to remove, or we're out of fragments.
950
+ let beforeRemoval: NamedFragments;
951
+ do {
952
+ beforeRemoval = finalFragments;
953
+ const usages = new Map<string, number>();
954
+ // Collecting all usages, both in the selection and within other fragments.
955
+ optimizedSelection.collectUsedFragmentNames(usages);
956
+ finalFragments.collectUsedFragmentNames(usages);
957
+ finalFragments = finalFragments.filter((f) => (usages.get(f.name) ?? 0) > 0);
958
+ } while (finalFragments && finalFragments.size < beforeRemoval.size);
959
+ }
960
+ }
961
+
962
+ return this.withUpdatedSelectionSetAndFragments(optimizedSelection, finalFragments ?? undefined);
963
+ }
964
+
965
+ expandAllFragments(): Operation {
966
+ // We clear up the fragments since we've expanded all.
967
+ // Also note that expanding fragment usually generate unecessary fragments/inefficient selections, so it
968
+ // basically always make sense to normalize afterwards. Besides, fragment reuse (done by `optimize`) rely
969
+ // on the fact that its input is normalized to work properly, so all the more reason to do it here.
970
+ const expanded = this.selectionSet.expandFragments();
971
+ return this.withUpdatedSelectionSetAndFragments(expanded.normalize({ parentType: expanded.parentType }), undefined);
972
+ }
973
+
974
+ normalize(): Operation {
975
+ return this.withUpdatedSelectionSet(this.selectionSet.normalize({ parentType: this.selectionSet.parentType }));
976
+ }
977
+
931
978
  /**
932
979
  * Returns this operation but potentially modified so all/some of the @defer applications have been removed.
933
980
  *
@@ -936,10 +983,7 @@ export class Operation {
936
983
  * applications are removed.
937
984
  */
938
985
  withoutDefer(labelsToRemove?: Set<string>): Operation {
939
- const updated = this.selectionSet.withoutDefer(labelsToRemove);
940
- return updated == this.selectionSet
941
- ? this
942
- : new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.fragments, this.name);
986
+ return this.withUpdatedSelectionSet(this.selectionSet.withoutDefer(labelsToRemove));
943
987
  }
944
988
 
945
989
  /**
@@ -965,8 +1009,7 @@ export class Operation {
965
1009
  const { hasDefers, hasNonLabelledOrConditionalDefers } = normalizer.init(this.selectionSet);
966
1010
  let updatedOperation: Operation = this;
967
1011
  if (hasNonLabelledOrConditionalDefers) {
968
- const updated = this.selectionSet.withNormalizedDefer(normalizer);
969
- updatedOperation = new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.fragments, this.name);
1012
+ updatedOperation = this.withUpdatedSelectionSet(this.selectionSet.withNormalizedDefer(normalizer));
970
1013
  }
971
1014
  return {
972
1015
  operation: updatedOperation,
@@ -991,6 +1034,8 @@ export class Operation {
991
1034
  }
992
1035
  }
993
1036
 
1037
+ export type FragmentRestrictionAtType = { selectionSet: SelectionSet, validator?: FieldsConflictValidator };
1038
+
994
1039
  export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmentDefinition> {
995
1040
  private _selectionSet: SelectionSet | undefined;
996
1041
 
@@ -1000,7 +1045,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1000
1045
  private _fragmentUsages: Map<string, number> | undefined;
1001
1046
  private _includedFragmentNames: Set<string> | undefined;
1002
1047
 
1003
- private readonly expandedSelectionSetsAtTypesCache = new Map<string, SelectionSet>();
1048
+ private readonly expandedSelectionSetsAtTypesCache = new Map<string, FragmentRestrictionAtType>();
1004
1049
 
1005
1050
  constructor(
1006
1051
  schema: Schema,
@@ -1027,7 +1072,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1027
1072
 
1028
1073
  expandedSelectionSet(): SelectionSet {
1029
1074
  if (!this._expandedSelectionSet) {
1030
- this._expandedSelectionSet = this.selectionSet.expandFragments().trimUnsatisfiableBranches(this.typeCondition);
1075
+ this._expandedSelectionSet = this.selectionSet.expandFragments().normalize({ parentType: this.typeCondition });
1031
1076
  }
1032
1077
  return this._expandedSelectionSet;
1033
1078
  }
@@ -1071,13 +1116,31 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1071
1116
  }
1072
1117
 
1073
1118
  /**
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.
1119
+ * Whether this fragment may apply _directly_ at the provided type, meaning that the fragment sub-selection
1120
+ * (_without_ the fragment condition, hence the "directly") can be normalized at `type` and this without
1121
+ * "widening" the runtime types to types that do not intersect the fragment condition.
1122
+ *
1123
+ * For that to be true, we need one of this to be true:
1124
+ * 1. the runtime types of the fragment condition must be at least as general as those of the provided `type`.
1125
+ * Otherwise, putting it at `type` without its condition would "generalize" more than the fragment meant to (and
1126
+ * so we'd "widen" the runtime types more than what the query meant to.
1127
+ * 2. either `type` and `this.typeCondition` are equal, or `type` is an object or `this.typeCondition` is a union
1128
+ * The idea is that, assuming our 1st point, then:
1129
+ * - if both are equal, things works trivially.
1130
+ * - if `type` is an object, `this.typeCondition` is either the same object, or a union/interface for which
1131
+ * type is a valid runtime. In all case, anything valid on `this.typeCondition` would apply to `type` too.
1132
+ * - if `this.typeCondition` is a union, then it's selection can only have fragments at top-level
1133
+ * (no fields save for `__typename`), and normalising is always fine with top-level fragments.
1134
+ * But in any other case, both types must be abstract (if `this.typeCondition` is an object, the 1st condition
1135
+ * imply `type` can only be the same type) and we're in one of:
1136
+ * - `type` and `this.typeCondition` are both different interfaces (that intersect but are different).
1137
+ * - `type` is aunion and `this.typeCondition` an interface.
1138
+ * And in both cases, since `this.typeCondition` is an interface, the fragment selection set may have field selections
1139
+ * on that interface, and those fields may not be valid for `type`.
1077
1140
  *
1078
1141
  * @param type - the type at which we're looking at applying the fragment
1079
1142
  */
1080
- canApplyAtType(type: CompositeType): boolean {
1143
+ canApplyDirectlyAtType(type: CompositeType): boolean {
1081
1144
  if (sameType(type, this.typeCondition)) {
1082
1145
  return true;
1083
1146
  }
@@ -1090,17 +1153,20 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1090
1153
 
1091
1154
  const conditionRuntimes = possibleRuntimeTypes(this.typeCondition);
1092
1155
  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).
1156
+ // The fragment condition must be at least as general as the provided type (in other words, all of the
1157
+ // runtimes of `type` must be in `conditionRuntimes`).
1096
1158
  // Note: the `length` test is technically redundant, but just avoid the more costly sub-set check if we
1097
1159
  // can cheaply show it's unnecessary.
1098
- return conditionRuntimes.length >= typeRuntimes.length
1099
- && typeRuntimes.every((t1) => conditionRuntimes.some((t2) => sameType(t1, t2)));
1160
+ if (conditionRuntimes.length < typeRuntimes.length
1161
+ || !typeRuntimes.every((t1) => conditionRuntimes.some((t2) => sameType(t1, t2)))) {
1162
+ return false;
1163
+ }
1164
+
1165
+ return isObjectType(type) || isUnionType(this.typeCondition);
1100
1166
  }
1101
1167
 
1102
1168
  /**
1103
- * This methods *assumes* that `this.canApplyAtType(type)` is `true` (and may crash if this is not true), and returns
1169
+ * This methods *assumes* that `this.canApplyDirectlyAtType(type)` is `true` (and may crash if this is not true), and returns
1104
1170
  * a version fo this named fragment selection set that corresponds to the "expansion" of this named fragment at `type`
1105
1171
  *
1106
1172
  * The overall idea here is that if we have an interface I with 2 implementations T1 and T2, and we have a fragment like:
@@ -1117,34 +1183,35 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1117
1183
  * 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
1118
1184
  * us that part.
1119
1185
  */
1120
- expandedSelectionSetAtType(type: CompositeType): SelectionSet {
1121
- const expandedSelectionSet = this.expandedSelectionSet();
1122
-
1123
- // First, if the candidate condition is an object or is the type passed, then there isn't any additional restriction to do.
1186
+ expandedSelectionSetAtType(type: CompositeType): FragmentRestrictionAtType {
1187
+ // First, if the candidate condition is an object or is the type passed, then there isn't any restriction to do.
1124
1188
  if (sameType(type, this.typeCondition) || isObjectType(this.typeCondition)) {
1125
- return expandedSelectionSet;
1189
+ return { selectionSet: this.expandedSelectionSet() };
1126
1190
  }
1127
1191
 
1128
- // We should not call `trimUnsatisfiableBranches` where `type` is an abstract type (`interface` or `union`) as it currently could
1129
- // create an invalid selection set (and throw down the line). In theory, when `type` is an abstract type, we could look at the
1130
- // intersection of its runtime types with those of `this.typeCondition`, call `trimUnsatisfiableBranches` for each of the resulting
1131
- // object types, and merge all those selection sets, and this "may" result in a smaller selection at times. This is a bit complex
1132
- // and costly to do however, so we just return the selection unchanged for now, which is always valid but simply may not be absolutely
1133
- // optimal.
1134
- // Concretely, this means that there may be corner cases where a named fragment could be reused but isn't, but waiting on finding
1135
- // concrete examples where this matter to decide if it's worth the complexity.
1136
- if (!isObjectType(type)) {
1137
- return expandedSelectionSet;
1192
+ let cached = this.expandedSelectionSetsAtTypesCache.get(type.name);
1193
+ if (!cached) {
1194
+ cached = this.computeExpandedSelectionSetAtType(type);
1195
+ this.expandedSelectionSetsAtTypesCache.set(type.name, cached);
1138
1196
  }
1197
+ return cached;
1198
+ }
1139
1199
 
1140
- let selectionAtType = this.expandedSelectionSetsAtTypesCache.get(type.name);
1141
- if (!selectionAtType) {
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
1143
- // in going recursive however: any simplification due to `type` stops as soon as we traverse a field. And so we don't bother.
1144
- selectionAtType = expandedSelectionSet.trimUnsatisfiableBranches(type, { recursive: false });
1145
- this.expandedSelectionSetsAtTypesCache.set(type.name, selectionAtType);
1146
- }
1147
- return selectionAtType;
1200
+ private computeExpandedSelectionSetAtType(type: CompositeType): FragmentRestrictionAtType {
1201
+ const expandedSelectionSet = this.expandedSelectionSet();
1202
+ // Note that what we want is get any simplification coming from normalizing at `type`, but any such simplication
1203
+ // stops as soon as we traverse a field, so no point in being recursive.
1204
+ const selectionSet = expandedSelectionSet.normalize({ parentType: type, recursive: false });
1205
+
1206
+ // Note that `trimmed` is the difference of 2 selections that may not have been normalized on the same parent type,
1207
+ // so in practice, it is possible that `trimmed` contains some of the selections that `selectionSet` contains, but
1208
+ // that they have been simplified in `selectionSet` in such a way that the `minus` call does not see it. However,
1209
+ // it is not trivial to deal with this, and it is fine given that we use trimmed to create the validator because
1210
+ // we know the non-trimmed parts cannot create field conflict issues so we're trying to build a smaller validator,
1211
+ // but it's ok if trimmed is not as small as it theoretically can be.
1212
+ const trimmed = expandedSelectionSet.minus(selectionSet);
1213
+ const validator = trimmed.isEmpty() ? undefined : FieldsConflictValidator.build(trimmed);
1214
+ return { selectionSet, validator };
1148
1215
  }
1149
1216
 
1150
1217
  /**
@@ -1176,10 +1243,11 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1176
1243
  }
1177
1244
 
1178
1245
  toString(indent?: string): string {
1179
- return (indent ?? '') + `fragment ${this.name} on ${this.typeCondition}${this.appliedDirectivesToString()} ${this.selectionSet.toString(false, true, indent)}`;
1246
+ return `fragment ${this.name} on ${this.typeCondition}${this.appliedDirectivesToString()} ${this.selectionSet.toString(false, true, indent)}`;
1180
1247
  }
1181
1248
  }
1182
1249
 
1250
+
1183
1251
  export class NamedFragments {
1184
1252
  private readonly fragments = new MapWithCachedArrays<string, NamedFragmentDefinition>();
1185
1253
 
@@ -1208,8 +1276,8 @@ export class NamedFragments {
1208
1276
  }
1209
1277
  }
1210
1278
 
1211
- maybeApplyingAtType(type: CompositeType): NamedFragmentDefinition[] {
1212
- return this.fragments.values().filter(f => f.canApplyAtType(type));
1279
+ maybeApplyingDirectlyAtType(type: CompositeType): NamedFragmentDefinition[] {
1280
+ return this.fragments.values().filter(f => f.canApplyDirectlyAtType(type));
1213
1281
  }
1214
1282
 
1215
1283
  get(name: string): NamedFragmentDefinition | undefined {
@@ -1224,6 +1292,15 @@ export class NamedFragments {
1224
1292
  return this.fragments.values();
1225
1293
  }
1226
1294
 
1295
+ /**
1296
+ * Collect the usages of fragments that are used within the selection of other fragments.
1297
+ */
1298
+ collectUsedFragmentNames(collector: Map<string, number>) {
1299
+ for (const fragment of this.definitions()) {
1300
+ fragment.collectUsedFragmentNames(collector);
1301
+ }
1302
+ }
1303
+
1227
1304
  map(mapper: (def: NamedFragmentDefinition) => NamedFragmentDefinition): NamedFragments {
1228
1305
  const mapped = new NamedFragments();
1229
1306
  for (const def of this.fragments.values()) {
@@ -1288,7 +1365,7 @@ export class NamedFragments {
1288
1365
  mapper: (selectionSet: SelectionSet) => SelectionSet | undefined,
1289
1366
  ): NamedFragments | undefined {
1290
1367
  return this.mapInDependencyOrder((fragment, newFragments) => {
1291
- const mappedSelectionSet = mapper(fragment.selectionSet.expandFragments().trimUnsatisfiableBranches(fragment.typeCondition));
1368
+ const mappedSelectionSet = mapper(fragment.selectionSet.expandFragments().normalize({ parentType: fragment.typeCondition }));
1292
1369
  if (!mappedSelectionSet) {
1293
1370
  return undefined;
1294
1371
  }
@@ -1297,21 +1374,39 @@ export class NamedFragments {
1297
1374
  });
1298
1375
  }
1299
1376
 
1377
+ // When we rebase named fragments on a subgraph schema, only a subset of what the fragment handles may belong
1378
+ // to that particular subgraph. And there are a few sub-cases where that subset is such that we basically need or
1379
+ // want to consider to ignore the fragment for that subgraph, and that is when:
1380
+ // 1. the subset that apply is actually empty. The fragment wouldn't be valid in this case anyway.
1381
+ // 2. the subset is a single leaf field: in that case, using the one field directly is just shorter than using
1382
+ // the fragment, so we consider the fragment don't really apply to that subgraph. Technically, using the
1383
+ // fragment could still be of value if the fragment name is a lot smaller than the one field name, but it's
1384
+ // enough of a niche case that we ignore it. Note in particular that one sub-case of this rule that is likely
1385
+ // to be common is when the subset ends up being just `__typename`: this would basically mean the fragment
1386
+ // don't really apply to the subgraph, and that this will ensure this is the case.
1387
+ private selectionSetIsWorthUsing(selectionSet: SelectionSet): boolean {
1388
+ const selections = selectionSet.selections();
1389
+ if (selections.length === 0) {
1390
+ return false;
1391
+ }
1392
+ if (selections.length === 1) {
1393
+ const s = selections[0];
1394
+ return !(s.kind === 'FieldSelection' && s.element.isLeafField());
1395
+ }
1396
+ return true;
1397
+ }
1398
+
1300
1399
  rebaseOn(schema: Schema): NamedFragments | undefined {
1301
1400
  return this.mapInDependencyOrder((fragment, newFragments) => {
1302
1401
  const rebasedType = schema.type(fragment.selectionSet.parentType.name);
1303
- try {
1304
- if (!rebasedType || !isCompositeType(rebasedType)) {
1305
- return undefined;
1306
- }
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.
1402
+ if (!rebasedType || !isCompositeType(rebasedType)) {
1313
1403
  return undefined;
1314
1404
  }
1405
+
1406
+ const rebasedSelection = fragment.selectionSet.rebaseOn({ parentType: rebasedType, fragments: newFragments, errorIfCannotRebase: false });
1407
+ return this.selectionSetIsWorthUsing(rebasedSelection)
1408
+ ? new NamedFragmentDefinition(schema, fragment.name, rebasedType).setSelectionSet(rebasedSelection)
1409
+ : undefined;;
1315
1410
  });
1316
1411
  }
1317
1412
 
@@ -1417,6 +1512,8 @@ export enum ContainsResult {
1417
1512
  EQUAL,
1418
1513
  }
1419
1514
 
1515
+ export type CollectedFieldsInSet = { path: string[], field: FieldSelection }[];
1516
+
1420
1517
  export class SelectionSet {
1421
1518
  private readonly _keyedSelections: Map<string, Selection>;
1422
1519
  private readonly _selections: readonly Selection[];
@@ -1447,7 +1544,21 @@ export class SelectionSet {
1447
1544
  return this._keyedSelections.has(typenameFieldName);
1448
1545
  }
1449
1546
 
1450
- fieldsInSet(): { path: string[], field: FieldSelection }[] {
1547
+ withoutTopLevelTypenameField(): SelectionSet {
1548
+ if (!this.hasTopLevelTypenameField) {
1549
+ return this;
1550
+ }
1551
+
1552
+ const newKeyedSelections = new Map<string, Selection>();
1553
+ for (const [key, selection] of this._keyedSelections) {
1554
+ if (key !== typenameFieldName) {
1555
+ newKeyedSelections.set(key, selection);
1556
+ }
1557
+ }
1558
+ return new SelectionSet(this.parentType, newKeyedSelections);
1559
+ }
1560
+
1561
+ fieldsInSet(): CollectedFieldsInSet {
1451
1562
  const fields = new Array<{ path: string[], field: FieldSelection }>();
1452
1563
  for (const selection of this.selections()) {
1453
1564
  if (selection.kind === 'FieldSelection') {
@@ -1463,6 +1574,22 @@ export class SelectionSet {
1463
1574
  return fields;
1464
1575
  }
1465
1576
 
1577
+ fieldsByResponseName(): MultiMap<string, FieldSelection> {
1578
+ const byResponseName = new MultiMap<string, FieldSelection>();
1579
+ this.collectFieldsByResponseName(byResponseName);
1580
+ return byResponseName;
1581
+ }
1582
+
1583
+ private collectFieldsByResponseName(collector: MultiMap<string, FieldSelection>) {
1584
+ for (const selection of this.selections()) {
1585
+ if (selection.kind === 'FieldSelection') {
1586
+ collector.add(selection.element.responseName(), selection);
1587
+ } else {
1588
+ selection.selectionSet.collectFieldsByResponseName(collector);
1589
+ }
1590
+ }
1591
+ }
1592
+
1466
1593
  usedVariables(): Variables {
1467
1594
  const collector = new VariableCollector();
1468
1595
  this.collectVariables(collector);
@@ -1502,7 +1629,8 @@ export class SelectionSet {
1502
1629
  // With that, `optimizeSelections` will correctly match on the `on Query` fragment; after which
1503
1630
  // we can unpack the final result.
1504
1631
  const wrapped = new InlineFragmentSelection(new FragmentElement(this.parentType, this.parentType), this);
1505
- const optimized = wrapped.optimize(fragments);
1632
+ const validator = FieldsConflictMultiBranchValidator.ofInitial(FieldsConflictValidator.build(this));
1633
+ const optimized = wrapped.optimize(fragments, validator);
1506
1634
 
1507
1635
  // Now, it's possible we matched a full fragment, in which case `optimized` will be just the named fragment,
1508
1636
  // and in that case we return a singleton selection with just that. Otherwise, it's our wrapping inline fragment
@@ -1515,16 +1643,92 @@ export class SelectionSet {
1515
1643
  // Tries to match fragments inside each selections of this selection set, and this recursively. However, note that this
1516
1644
  // may not match fragments that would apply at top-level, so you should usually use `optimize` instead (this exists mostly
1517
1645
  // for the recursion).
1518
- optimizeSelections(fragments: NamedFragments): SelectionSet {
1519
- return this.lazyMap((selection) => selection.optimize(fragments));
1646
+ optimizeSelections(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): SelectionSet {
1647
+ return this.lazyMap((selection) => selection.optimize(fragments, validator));
1520
1648
  }
1521
1649
 
1522
1650
  expandFragments(updatedFragments?: NamedFragments): SelectionSet {
1523
1651
  return this.lazyMap((selection) => selection.expandFragments(updatedFragments));
1524
1652
  }
1525
1653
 
1526
- trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): SelectionSet {
1527
- return this.lazyMap((selection) => selection.trimUnsatisfiableBranches(parentType, options), { parentType });
1654
+ /**
1655
+ * Applies some normalization rules to this selection set in the context of the provided `parentType`.
1656
+ *
1657
+ * Normalization mostly removes unecessary/redundant inline fragments, so that for instance, with
1658
+ * schema:
1659
+ * ```graphql
1660
+ * type Query {
1661
+ * t1: T1
1662
+ * i: I
1663
+ * }
1664
+ *
1665
+ * interface I {
1666
+ * id: ID!
1667
+ * }
1668
+ *
1669
+ * type T1 implements I {
1670
+ * id: ID!
1671
+ * v1: Int
1672
+ * }
1673
+ *
1674
+ * type T2 implements I {
1675
+ * id: ID!
1676
+ * v2: Int
1677
+ * }
1678
+ * ```
1679
+ *
1680
+ * ```
1681
+ * normalize({
1682
+ * t1 {
1683
+ * ... on I {
1684
+ * id
1685
+ * }
1686
+ * }
1687
+ * i {
1688
+ * ... on T1 {
1689
+ * ... on I {
1690
+ * ... on T1 {
1691
+ * v1
1692
+ * }
1693
+ * ... on T2 {
1694
+ * v2
1695
+ * }
1696
+ * }
1697
+ * }
1698
+ * ... on T2 {
1699
+ * ... on I {
1700
+ * id
1701
+ * }
1702
+ * }
1703
+ * }
1704
+ * }) === {
1705
+ * t1 {
1706
+ * id
1707
+ * }
1708
+ * i {
1709
+ * ... on T1 {
1710
+ * v1
1711
+ * }
1712
+ * ... on T2 {
1713
+ * id
1714
+ * }
1715
+ * }
1716
+ * }
1717
+ * ```
1718
+ *
1719
+ * For this operation to be valid (to not throw), `parentType` must be such that every field selection in
1720
+ * this selection set is such that the field parent type intersects `parentType` (there is no limitation
1721
+ * on the fragment selections, though any fragment selections whose condition do not intersects `parentType`
1722
+ * will be discarded). Note that `this.normalize(this.parentType)` is always valid and useful, but it is
1723
+ * also possible to pass a `parentType` that is more "restrictive" than the selection current parent type
1724
+ * (as long as the top-level fields of this selection set can be rebased on that type).
1725
+ *
1726
+ * Passing the option `recursive == false` makes the normalization only apply at the top-level, removing
1727
+ * any unecessary top-level inline fragments, possibly multiple layers of them, but we never recurse
1728
+ * inside the sub-selection of an selection that is not removed by the normalization.
1729
+ */
1730
+ normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): SelectionSet {
1731
+ return this.lazyMap((selection) => selection.normalize({ parentType, recursive }), { parentType });
1528
1732
  }
1529
1733
 
1530
1734
  /**
@@ -1575,28 +1779,48 @@ export class SelectionSet {
1575
1779
  }
1576
1780
 
1577
1781
  /**
1578
- * Returns the selection select from filtering out any selection that does not match the provided predicate.
1782
+ * Returns the selection set resulting from filtering out any of the top-level selection that does not match the provided predicate.
1579
1783
  *
1580
- * Please that this method will expand *ALL* fragments as the result of applying it's filtering. You should
1581
- * call `optimize` on the result if you want to re-apply some fragments.
1784
+ * Please that this method does not recurse within sub-selections.
1582
1785
  */
1583
1786
  filter(predicate: (selection: Selection) => boolean): SelectionSet {
1584
- return this.lazyMap((selection) => selection.filter(predicate));
1787
+ return this.lazyMap((selection) => predicate(selection) ? selection : undefined);
1788
+ }
1789
+
1790
+ /**
1791
+ * Returns the selection set resulting from "recursively" filtering any selection that does not match the provided predicate.
1792
+ * This method calls `predicate` on every selection of the selection set, not just top-level ones, and apply a "depth-first"
1793
+ * strategy, meaning that when the predicate is call on a given selection, the it is guaranteed that filtering has happened
1794
+ * on all the selections of its sub-selection.
1795
+ */
1796
+ filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): SelectionSet {
1797
+ return this.lazyMap((selection) => selection.filterRecursiveDepthFirst(predicate));
1585
1798
  }
1586
1799
 
1587
1800
  withoutEmptyBranches(): SelectionSet | undefined {
1588
- const updated = this.filter((selection) => selection.selectionSet?.isEmpty() !== true);
1801
+ const updated = this.filterRecursiveDepthFirst((selection) => selection.selectionSet?.isEmpty() !== true);
1589
1802
  return updated.isEmpty() ? undefined : updated;
1590
1803
  }
1591
1804
 
1592
- rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): SelectionSet {
1805
+ rebaseOn({
1806
+ parentType,
1807
+ fragments,
1808
+ errorIfCannotRebase,
1809
+ }: {
1810
+ parentType: CompositeType,
1811
+ fragments: NamedFragments | undefined
1812
+ errorIfCannotRebase: boolean,
1813
+ }): SelectionSet {
1593
1814
  if (this.parentType === parentType) {
1594
1815
  return this;
1595
1816
  }
1596
1817
 
1597
1818
  const newSelections = new Map<string, Selection>();
1598
1819
  for (const selection of this.selections()) {
1599
- newSelections.set(selection.key(), selection.rebaseOn(parentType, fragments));
1820
+ const rebasedSelection = selection.rebaseOn({ parentType, fragments, errorIfCannotRebase });
1821
+ if (rebasedSelection) {
1822
+ newSelections.set(selection.key(), rebasedSelection);
1823
+ }
1600
1824
  }
1601
1825
 
1602
1826
  return new SelectionSet(parentType, newSelections);
@@ -1620,15 +1844,25 @@ export class SelectionSet {
1620
1844
  return true;
1621
1845
  }
1622
1846
 
1623
- contains(that: SelectionSet): ContainsResult {
1847
+ contains(that: SelectionSet, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
1848
+ const ignoreMissingTypename = options?.ignoreMissingTypename ?? false;
1624
1849
  if (that._selections.length > this._selections.length) {
1625
- return ContainsResult.NOT_CONTAINED;
1850
+ // If `that` has more selections but we're ignoring missing __typename, then in the case where
1851
+ // `that` has a __typename but `this` does not, then we need the length of `that` to be at
1852
+ // least 2 more than that of `this` to be able to conclude there is no contains.
1853
+ if (!ignoreMissingTypename || that._selections.length > this._selections.length + 1 || this.hasTopLevelTypenameField() || !that.hasTopLevelTypenameField()) {
1854
+ return ContainsResult.NOT_CONTAINED;
1855
+ }
1626
1856
  }
1627
1857
 
1628
1858
  let isEqual = true;
1629
1859
  for (const [key, thatSelection] of that._keyedSelections) {
1860
+ if (key === typenameFieldName && ignoreMissingTypename) {
1861
+ continue;
1862
+ }
1863
+
1630
1864
  const thisSelection = this._keyedSelections.get(key);
1631
- const selectionResult = thisSelection?.contains(thatSelection);
1865
+ const selectionResult = thisSelection?.contains(thatSelection, options);
1632
1866
  if (selectionResult === undefined || selectionResult === ContainsResult.NOT_CONTAINED) {
1633
1867
  return ContainsResult.NOT_CONTAINED;
1634
1868
  }
@@ -1640,49 +1874,9 @@ export class SelectionSet {
1640
1874
  : ContainsResult.STRICTLY_CONTAINED;
1641
1875
  }
1642
1876
 
1643
- // Please note that this method assumes that `candidate.canApplyAtType(parentType) === true` but it is left to the caller to
1644
- // validate this (`canApplyAtType` is not free, and we want to avoid repeating it multiple times).
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
-
1673
- if (this.contains(that)) {
1674
- // One subtlety here is that at "this" sub-selections may already have been optimized with some fragments. It's
1675
- // usually ok because `candidate` will also use those fragments, but one fragments that `candidate` can never be
1676
- // using is itself (the `contains` check is fine with this, but it's harder to deal in `minus`). So we expand
1677
- // the candidate we're currently looking at in "this" to avoid some issues.
1678
- let updatedThis = this.expandFragments(fragments.filter((f) => f.name !== candidate.name));
1679
- if (updatedThis !== this) {
1680
- updatedThis = updatedThis.trimUnsatisfiableBranches(parentType);
1681
- }
1682
- const diff = updatedThis.minus(that);
1683
- return { contains: true, diff: diff.isEmpty() ? undefined : diff };
1684
- }
1685
- return { contains: false };
1877
+ containsTopLevelField(field: Field): boolean {
1878
+ const selection = this._keyedSelections.get(field.key());
1879
+ return !!selection && selection.element.equals(field);
1686
1880
  }
1687
1881
 
1688
1882
  /**
@@ -1706,6 +1900,28 @@ export class SelectionSet {
1706
1900
  return updated.toSelectionSet(this.parentType);
1707
1901
  }
1708
1902
 
1903
+ intersectionWith(that: SelectionSet): SelectionSet {
1904
+ if (this.isEmpty()) {
1905
+ return this;
1906
+ }
1907
+ if (that.isEmpty()) {
1908
+ return that;
1909
+ }
1910
+
1911
+ const intersection = new SelectionSetUpdates();
1912
+ for (const [key, thisSelection] of this._keyedSelections) {
1913
+ const thatSelection = that._keyedSelections.get(key);
1914
+ if (thatSelection) {
1915
+ const selection = thisSelection.intersectionWith(thatSelection);
1916
+ if (selection) {
1917
+ intersection.add(selection);
1918
+ }
1919
+ }
1920
+ }
1921
+
1922
+ return intersection.toSelectionSet(this.parentType);
1923
+ }
1924
+
1709
1925
  canRebaseOn(parentTypeToTest: CompositeType): boolean {
1710
1926
  return this.selections().every((selection) => selection.canAddTo(parentTypeToTest));
1711
1927
  }
@@ -1923,6 +2139,19 @@ export class SelectionSetUpdates {
1923
2139
  toSelectionSet(parentType: CompositeType, fragments?: NamedFragments): SelectionSet {
1924
2140
  return makeSelectionSet(parentType, this.keyedUpdates, fragments);
1925
2141
  }
2142
+
2143
+ toString() {
2144
+ return '{\n'
2145
+ + [...this.keyedUpdates.entries()].map(([k, updates]) => {
2146
+ const updStr = updates.map((upd) =>
2147
+ upd instanceof AbstractSelection
2148
+ ? upd.toString()
2149
+ : `${upd.path} -> ${upd.selections}`
2150
+ );
2151
+ return ` - ${k}: ${updStr}`;
2152
+ }).join('\n')
2153
+ +'\n\}'
2154
+ }
1926
2155
  }
1927
2156
 
1928
2157
  function addToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, selections: Selection | SelectionSet | readonly Selection[]) {
@@ -1999,10 +2228,10 @@ function makeSelection(parentType: CompositeType, updates: SelectionUpdate[], fr
1999
2228
 
2000
2229
  // Optimize for the simple case of a single selection, as we don't have to do anything complex to merge the sub-selections.
2001
2230
  if (updates.length === 1 && first instanceof AbstractSelection) {
2002
- return first.rebaseOn(parentType, fragments);
2231
+ return first.rebaseOnOrError({ parentType, fragments });
2003
2232
  }
2004
2233
 
2005
- const element = updateElement(first).rebaseOn(parentType);
2234
+ const element = updateElement(first).rebaseOnOrError(parentType);
2006
2235
  const subSelectionParentType = element.kind === 'Field' ? element.baseType() : element.castedType();
2007
2236
  if (!isCompositeType(subSelectionParentType)) {
2008
2237
  // This is a leaf, so all updates should correspond ot the same field and we just use the first.
@@ -2185,13 +2414,17 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2185
2414
 
2186
2415
  abstract key(): string;
2187
2416
 
2188
- abstract optimize(fragments: NamedFragments): Selection;
2417
+ abstract optimize(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): Selection;
2189
2418
 
2190
2419
  abstract toSelectionNode(): SelectionNode;
2191
2420
 
2192
2421
  abstract validate(variableDefinitions: VariableDefinitions): void;
2193
2422
 
2194
- abstract rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): TOwnType;
2423
+ abstract rebaseOn(args: { parentType: CompositeType, fragments: NamedFragments | undefined, errorIfCannotRebase: boolean}): TOwnType | undefined;
2424
+
2425
+ rebaseOnOrError({ parentType, fragments }: { parentType: CompositeType, fragments: NamedFragments | undefined }): TOwnType {
2426
+ return this.rebaseOn({ parentType, fragments, errorIfCannotRebase: true})!;
2427
+ }
2195
2428
 
2196
2429
  get parentType(): CompositeType {
2197
2430
  return this.element.parentType;
@@ -2240,7 +2473,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2240
2473
 
2241
2474
  abstract expandFragments(updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
2242
2475
 
2243
- abstract trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): TOwnType | SelectionSet | undefined;
2476
+ abstract normalize(args: { parentType: CompositeType, recursive? : boolean }): TOwnType | SelectionSet | undefined;
2244
2477
 
2245
2478
  isFragmentSpread(): boolean {
2246
2479
  return false;
@@ -2258,26 +2491,51 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2258
2491
  return undefined;
2259
2492
  }
2260
2493
 
2494
+ intersectionWith(that: Selection): TOwnType | undefined {
2495
+ // If there is a subset, then we compute the intersection add that (if not empty).
2496
+ // Otherwise, the intersection is this element.
2497
+ if (this.selectionSet && that.selectionSet) {
2498
+ const subSelectionSetIntersection = this.selectionSet.intersectionWith(that.selectionSet);
2499
+ if (subSelectionSetIntersection.isEmpty()) {
2500
+ return undefined;
2501
+ } else {
2502
+ return this.withUpdatedSelectionSet(subSelectionSetIntersection);
2503
+ }
2504
+ } else {
2505
+ return this.us();
2506
+ }
2507
+ }
2508
+
2261
2509
  protected tryOptimizeSubselectionWithFragments({
2262
2510
  parentType,
2263
2511
  subSelection,
2264
2512
  fragments,
2513
+ validator,
2265
2514
  canUseFullMatchingFragment,
2266
2515
  }: {
2267
2516
  parentType: CompositeType,
2268
2517
  subSelection: SelectionSet,
2269
2518
  fragments: NamedFragments,
2519
+ validator: FieldsConflictMultiBranchValidator,
2270
2520
  canUseFullMatchingFragment: (match: NamedFragmentDefinition) => boolean,
2271
2521
  }): SelectionSet | NamedFragmentDefinition {
2272
- let candidates = fragments.maybeApplyingAtType(parentType);
2522
+ // We limit to fragments whose selection could be applied "directly" at `parentType`, meaning without taking the fragment condition
2523
+ // into account. The idea being that if the fragment condition would be needed inside `parentType`, then that condition will not
2524
+ // have been "normalized away" and so we want for this very call to be called on the fragment whose type _is_ the fragment condition (at
2525
+ // which point, this `maybeApplyingDirectlyAtType` method will apply.
2526
+ // Also note that this is because we have this restriction that calling `expandedSelectionSetAtType` is ok.
2527
+ let candidates = fragments.maybeApplyingDirectlyAtType(parentType);
2528
+ if (candidates.length === 0) {
2529
+ return subSelection;
2530
+ }
2273
2531
 
2274
2532
  // First, we check which of the candidates do apply inside `subSelection`, if any.
2275
2533
  // If we find a candidate that applies to the whole `subSelection`, then we stop and only return
2276
2534
  // that one candidate. Otherwise, we cumulate in `applyingFragments` the list of fragments that
2277
2535
  // applies to a subset of `subSelection`.
2278
- const applyingFragments: NamedFragmentDefinition[] = [];
2536
+ const applyingFragments: { fragment: NamedFragmentDefinition, atType: FragmentRestrictionAtType }[] = [];
2279
2537
  for (const candidate of candidates) {
2280
- const fragmentSSet = candidate.expandedSelectionSetAtType(parentType);
2538
+ let atType = candidate.expandedSelectionSetAtType(parentType);
2281
2539
  // It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
2282
2540
  // `parentType` is empty, or contains only `__typename`. For instance, suppose we have
2283
2541
  // a union `U = A | B | C`, and then a fragment:
@@ -2296,24 +2554,39 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2296
2554
  //
2297
2555
  // Using `F` in those cases is, while not 100% incorrect, at least not productive, and so we
2298
2556
  // skip it that case. This is essentially an optimisation.
2299
- if (fragmentSSet.isEmpty() || (fragmentSSet.selections().length === 1 && fragmentSSet.selections()[0].isTypenameField())) {
2557
+ if (atType.selectionSet.isEmpty() || (atType.selectionSet.selections().length === 1 && atType.selectionSet.selections()[0].isTypenameField())) {
2300
2558
  continue;
2301
2559
  }
2302
2560
 
2303
- const res = subSelection.contains(fragmentSSet);
2561
+ // As we check inclusion, we ignore the case where the fragment queries __typename but the subSelection does not.
2562
+ // The rational is that querying `__typename` unecessarily is mostly harmless (it always works and it's super cheap)
2563
+ // so we don't want to not use a fragment just to save querying a `__typename` in a few cases. But the underlying
2564
+ // context of why this matters is that the query planner always requests __typename for abstract type, and will do
2565
+ // so in fragments too, but we can have a field that _does_ return an abstract type within a fragment, but that
2566
+ // _does not_ end up returning an abstract type when applied in a "more specific" context (think a fragment on
2567
+ // an interface I1 where a inside field returns another interface I2, but applied in the context of a implementation
2568
+ // type of I1 where that particular field returns an implementation of I2 rather than I2 directly; we would have
2569
+ // added __typename to the fragment (because it's all interfaces), but the selection itself, which only deals
2570
+ // with object type, may not have __typename requested; using the fragment might still be a good idea, and
2571
+ // querying __typename needlessly is a very small price to pay for that).
2572
+ const res = subSelection.contains(atType.selectionSet, { ignoreMissingTypename: true });
2304
2573
 
2305
2574
  if (res === ContainsResult.EQUAL) {
2306
2575
  if (canUseFullMatchingFragment(candidate)) {
2576
+ if (!validator.checkCanReuseFragmentAndTrackIt(atType)) {
2577
+ // We cannot use it at all, so no point in adding to `applyingFragments`.
2578
+ continue;
2579
+ }
2307
2580
  return candidate;
2308
2581
  }
2309
2582
  // If we're not going to replace the full thing, then same reasoning a below.
2310
2583
  if (candidate.appliedDirectives.length === 0) {
2311
- applyingFragments.push(candidate);
2584
+ applyingFragments.push({ fragment: candidate, atType});
2312
2585
  }
2313
2586
  // Note that if a fragment applies to only a subset of the subSelection, then we really only can use
2314
2587
  // it if that fragment is defined _without_ directives.
2315
2588
  } else if (res === ContainsResult.STRICTLY_CONTAINED && candidate.appliedDirectives.length === 0) {
2316
- applyingFragments.push(candidate);
2589
+ applyingFragments.push({ fragment: candidate, atType });
2317
2590
  }
2318
2591
  }
2319
2592
 
@@ -2322,7 +2595,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2322
2595
  }
2323
2596
 
2324
2597
  // 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
2598
+ // want to now produce the selection set with spread for those fragments plus any selection that is not
2326
2599
  // covered by any of the fragments. For instance, suppose that `subselection` is `{ a b c d e }`
2327
2600
  // and we have found that `fragment F1 on X { a b c }` and `fragment F2 on X { c d }` applies, then
2328
2601
  // we will generate `{ ...F1 ...F2 e }`.
@@ -2369,16 +2642,16 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2369
2642
  // return `{ ...F3 ...F4 }` in that case, but it would be technically better to return only `F4`.
2370
2643
  // However, this feels niche, and it might be costly to verify such inclusions, so not doing it
2371
2644
  // for now.
2372
- const filteredApplyingFragments = applyingFragments.filter((f) => !applyingFragments.some((o) => o.includes(f.name)));
2645
+ const filteredApplyingFragments = applyingFragments.filter(({ fragment }) => !applyingFragments.some((o) => o.fragment.includes(fragment.name)))
2373
2646
 
2374
2647
  let notCoveredByFragments = subSelection;
2375
2648
  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));
2649
+ for (const { fragment, atType} of filteredApplyingFragments) {
2650
+ if (!validator.checkCanReuseFragmentAndTrackIt(atType)) {
2651
+ continue;
2652
+ }
2653
+ const notCovered = subSelection.minus(atType.selectionSet);
2654
+ notCoveredByFragments = notCoveredByFragments.intersectionWith(notCovered);
2382
2655
  optimized.add(new FragmentSpreadSelection(parentType, fragments, fragment, []));
2383
2656
  }
2384
2657
 
@@ -2386,6 +2659,201 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2386
2659
  }
2387
2660
  }
2388
2661
 
2662
+ class FieldsConflictMultiBranchValidator {
2663
+ private usedSpreadTrimmedPartAtLevel?: FieldsConflictValidator[];
2664
+
2665
+ constructor(
2666
+ private readonly validators: FieldsConflictValidator[],
2667
+ ) {
2668
+ }
2669
+
2670
+ static ofInitial(validator: FieldsConflictValidator): FieldsConflictMultiBranchValidator {
2671
+ return new FieldsConflictMultiBranchValidator([validator]);
2672
+ }
2673
+
2674
+ forField(field: Field): FieldsConflictMultiBranchValidator {
2675
+ const forAllBranches = this.validators.flatMap((vs) => vs.forField(field));
2676
+ // As this is called on (non-leaf) field from the same query on which we have build the initial validators, we
2677
+ // should find at least one validator.
2678
+ assert(forAllBranches.length > 0, `Shoud have found at least one validator for ${field}`);
2679
+ return new FieldsConflictMultiBranchValidator(forAllBranches);
2680
+ }
2681
+
2682
+ // At this point, we known that the fragment, restricted to the current parent type, matches a subset of the
2683
+ // sub-selection. However, there is still one case we we cannot use it that we need to check, and this is
2684
+ // if using the fragment would create a field "conflict" (in the sense of the graphQL spec
2685
+ // [`FieldsInSetCanMerge`](https://spec.graphql.org/draft/#FieldsInSetCanMerge())) and thus create an
2686
+ // invalid selection. To be clear, `atType.selectionSet` cannot create a conflict, since it is a subset
2687
+ // of `subSelection` and `subSelection` is valid. *But* there may be some part of the fragment that
2688
+ // is not `atType.selectionSet` due to being "dead branches" for type `parentType`. And while those
2689
+ // branches _are_ "dead" as far as execution goes, the `FieldsInSetCanMerge` validation does not take
2690
+ // this into account (it's 1st step says "including visiting fragments and inline fragments" but has
2691
+ // no logic regarding ignoring any fragment that may not apply due to the intersection of runtimes
2692
+ // between multiple fragment being empty).
2693
+ checkCanReuseFragmentAndTrackIt(fragment: FragmentRestrictionAtType): boolean {
2694
+ // No validator means that everything in the fragment selection was part of the selection we're optimizing
2695
+ // away (by using the fragment), and we know the original selection was ok, so nothing to check.
2696
+ const validator = fragment.validator;
2697
+ if (!validator) {
2698
+ return true;
2699
+ }
2700
+
2701
+ if (!this.validators.every((v) => v.doMergeWith(validator))) {
2702
+ return false;
2703
+ }
2704
+
2705
+ // We need to make sure the trimmed parts of `fragment` merges with the rest of the selection,
2706
+ // but also that it merge with any of the trimmed parts of any fragment we have added already.
2707
+ // Note: this last condition means that if 2 fragment conflict on their "trimmed" parts,
2708
+ // then the choice of which is used can be based on the fragment ordering and selection order,
2709
+ // which may not be optimal. This feels niche enough that we keep it simple for now, but we
2710
+ // can revisit this decision if we run into real cases that justify it (but making it optimal
2711
+ // would be a involved in general, as in theory you could have complex dependencies of fragments
2712
+ // that conflict, even cycles, and you need to take the size of fragments into account to know
2713
+ // what's best; and even then, this could even depend on overall usage, as it can be better to
2714
+ // reuse a fragment that is used in other places, than to use one for which it's the only usage.
2715
+ // Adding to all that the fact that conflict can happen in sibling branches).
2716
+ if (this.usedSpreadTrimmedPartAtLevel) {
2717
+ if (!this.usedSpreadTrimmedPartAtLevel.every((t) => validator.doMergeWith(t))) {
2718
+ return false;
2719
+ }
2720
+ } else {
2721
+ this.usedSpreadTrimmedPartAtLevel = [];
2722
+ }
2723
+
2724
+ // We're good, but track the fragment
2725
+ this.usedSpreadTrimmedPartAtLevel.push(validator);
2726
+ return true;
2727
+ }
2728
+ }
2729
+
2730
+ class FieldsConflictValidator {
2731
+ private constructor(
2732
+ private readonly byResponseName: Map<string, Map<Field, FieldsConflictValidator | null>>,
2733
+ ) {
2734
+ }
2735
+
2736
+ static build(s: SelectionSet): FieldsConflictValidator {
2737
+ return FieldsConflictValidator.forLevel(s.fieldsInSet());
2738
+ }
2739
+
2740
+ private static forLevel(level: CollectedFieldsInSet): FieldsConflictValidator {
2741
+ const atLevel = new Map<string, Map<Field, CollectedFieldsInSet | null>>();
2742
+
2743
+ for (const { field } of level) {
2744
+ const responseName = field.element.responseName();
2745
+ let atResponseName = atLevel.get(responseName);
2746
+ if (!atResponseName) {
2747
+ atResponseName = new Map<Field, CollectedFieldsInSet>();
2748
+ atLevel.set(responseName, atResponseName);
2749
+ }
2750
+ if (field.selectionSet) {
2751
+ // It's unlikely that we've seen the same `field.element` as we don't particularly "intern" `Field` object (so even if the exact same field
2752
+ // is used in 2 parts of a selection set, it will probably be a different `Field` object), so the `get` below will probably mostly return `undefined`,
2753
+ // but it wouldn't be incorrect to re-use a `Field` object multiple side, so no reason not to handle that correctly.
2754
+ let forField = atResponseName.get(field.element) ?? [];
2755
+ atResponseName.set(field.element, forField.concat(field.selectionSet.fieldsInSet()));
2756
+ } else {
2757
+ // Note that whether a `FieldSelection` has `selectionSet` or not is entirely determined by whether the field type is a composite type
2758
+ // or not, so even if we've seen a previous version of `field.element` before, we know it's guarantee to have had no `selectionSet`.
2759
+ // So the `set` below may overwrite a previous entry, but it would be a `null` so no harm done.
2760
+ atResponseName.set(field.element, null);
2761
+ }
2762
+ }
2763
+
2764
+ const byResponseName = new Map<string, Map<Field, FieldsConflictValidator | null>>();
2765
+ for (const [name, level] of atLevel.entries()) {
2766
+ const atResponseName = new Map<Field, FieldsConflictValidator | null>();
2767
+ for (const [field, collectedFields] of level) {
2768
+ const validator = collectedFields ? FieldsConflictValidator.forLevel(collectedFields) : null;
2769
+ atResponseName.set(field, validator);
2770
+ }
2771
+ byResponseName.set(name, atResponseName);
2772
+ }
2773
+ return new FieldsConflictValidator(byResponseName);
2774
+ }
2775
+
2776
+ forField(field: Field): FieldsConflictValidator[] {
2777
+ const byResponseName = this.byResponseName.get(field.responseName());
2778
+ if (!byResponseName) {
2779
+ return [];
2780
+ }
2781
+ return mapValues(byResponseName).filter((v): v is FieldsConflictValidator => !!v);
2782
+ }
2783
+
2784
+ doMergeWith(that: FieldsConflictValidator): boolean {
2785
+ for (const [responseName, thisFields] of this.byResponseName.entries()) {
2786
+ const thatFields = that.byResponseName.get(responseName);
2787
+ if (!thatFields) {
2788
+ continue;
2789
+ }
2790
+
2791
+ // We're basically checking [FieldInSetCanMerge](https://spec.graphql.org/draft/#FieldsInSetCanMerge()),
2792
+ // but from 2 set of fields (`thisFields` and `thatFields`) of the same response that we know individually
2793
+ // merge already.
2794
+ for (const [thisField, thisValidator] of thisFields.entries()) {
2795
+ for (const [thatField, thatValidator] of thatFields.entries()) {
2796
+ // The `SameResponseShape` test that all fields must pass.
2797
+ if (!typesCanBeMerged(thisField.definition.type!, thatField.definition.type!)) {
2798
+ return false;
2799
+ }
2800
+
2801
+ const p1 = thisField.parentType;
2802
+ const p2 = thatField.parentType;
2803
+ if (sameType(p1, p2) || !isObjectType(p1) || !isObjectType(p2)) {
2804
+ // Additional checks of `FieldsInSetCanMerge` when same parent type or one isn't object
2805
+ if (thisField.name !== thatField.name
2806
+ || !argumentsEquals(thisField.args ?? {}, thatField.args ?? {})
2807
+ || (thisValidator && thatValidator && !thisValidator.doMergeWith(thatValidator))
2808
+ ) {
2809
+ return false;
2810
+ }
2811
+ } else {
2812
+ // Otherwise, the sub-selection must pass [SameResponseShape](https://spec.graphql.org/draft/#SameResponseShape()).
2813
+ if (thisValidator && thatValidator && !thisValidator.hasSameResponseShapeThan(thatValidator)) {
2814
+ return false;
2815
+ }
2816
+ }
2817
+ }
2818
+ }
2819
+ }
2820
+ return true;
2821
+ }
2822
+
2823
+ hasSameResponseShapeThan(that: FieldsConflictValidator): boolean {
2824
+ for (const [responseName, thisFields] of this.byResponseName.entries()) {
2825
+ const thatFields = that.byResponseName.get(responseName);
2826
+ if (!thatFields) {
2827
+ continue;
2828
+ }
2829
+
2830
+ for (const [thisField, thisValidator] of thisFields.entries()) {
2831
+ for (const [thatField, thatValidator] of thatFields.entries()) {
2832
+ if (!typesCanBeMerged(thisField.definition.type!, thatField.definition.type!)
2833
+ || (thisValidator && thatValidator && !thisValidator.hasSameResponseShapeThan(thatValidator))) {
2834
+ return false;
2835
+ }
2836
+ }
2837
+ }
2838
+ }
2839
+ return true;
2840
+ }
2841
+
2842
+ toString(indent: string = ''): string {
2843
+ // For debugging/testing ...
2844
+ return '{\n'
2845
+ + [...this.byResponseName.entries()].map(([name, byFields]) => {
2846
+ const innerIndent = indent + ' ';
2847
+ return `${innerIndent}${name}: [\n`
2848
+ + [...byFields.entries()]
2849
+ .map(([field, next]) => `${innerIndent} ${field.parentType}.${field}${next ? next.toString(innerIndent + ' '): ''}`)
2850
+ .join('\n')
2851
+ + `\n${innerIndent}]`;
2852
+ }).join('\n')
2853
+ + `\n${indent}}`
2854
+ }
2855
+ }
2856
+
2389
2857
  export class FieldSelection extends AbstractSelection<Field<any>, undefined, FieldSelection> {
2390
2858
  readonly kind = 'FieldSelection' as const;
2391
2859
 
@@ -2409,6 +2877,9 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2409
2877
  }
2410
2878
 
2411
2879
  withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
2880
+ if (this.element === field && this.selectionSet === selectionSet) {
2881
+ return this;
2882
+ }
2412
2883
  return new FieldSelection(field, selectionSet);
2413
2884
  }
2414
2885
 
@@ -2416,45 +2887,46 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2416
2887
  return this.element.key();
2417
2888
  }
2418
2889
 
2419
- optimize(fragments: NamedFragments): Selection {
2890
+ optimize(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): Selection {
2420
2891
  const fieldBaseType = baseType(this.element.definition.type!);
2421
2892
  if (!isCompositeType(fieldBaseType) || !this.selectionSet) {
2422
2893
  return this;
2423
2894
  }
2424
2895
 
2896
+ const fieldValidator = validator.forField(this.element);
2897
+
2425
2898
  // First, see if we can reuse fragments for the selection of this field.
2426
- let optimizedSelection = this.selectionSet;
2427
- if (isCompositeType(fieldBaseType) && this.selectionSet) {
2428
- const optimized = this.tryOptimizeSubselectionWithFragments({
2429
- parentType: fieldBaseType,
2430
- subSelection: this.selectionSet,
2431
- fragments,
2432
- // We can never apply a fragments that has directives on it at the field level.
2433
- canUseFullMatchingFragment: (fragment) => fragment.appliedDirectives.length === 0,
2434
- });
2899
+ const optimized = this.tryOptimizeSubselectionWithFragments({
2900
+ parentType: fieldBaseType,
2901
+ subSelection: this.selectionSet,
2902
+ fragments,
2903
+ validator: fieldValidator,
2904
+ // We can never apply a fragments that has directives on it at the field level.
2905
+ canUseFullMatchingFragment: (fragment) => fragment.appliedDirectives.length === 0,
2906
+ });
2435
2907
 
2436
- if (optimized instanceof NamedFragmentDefinition) {
2437
- optimizedSelection = selectionSetOf(fieldBaseType, new FragmentSpreadSelection(fieldBaseType, fragments, optimized, []));
2438
- } else {
2439
- optimizedSelection = optimized;
2440
- }
2908
+ let optimizedSelection;
2909
+ if (optimized instanceof NamedFragmentDefinition) {
2910
+ optimizedSelection = selectionSetOf(fieldBaseType, new FragmentSpreadSelection(fieldBaseType, fragments, optimized, []));
2911
+ } else {
2912
+ optimizedSelection = optimized;
2441
2913
  }
2442
2914
 
2443
2915
  // Then, recurse inside the field sub-selection (note that if we matched some fragments above,
2444
2916
  // this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
2445
- optimizedSelection = optimizedSelection.optimize(fragments);
2917
+ optimizedSelection = optimizedSelection.optimizeSelections(fragments, fieldValidator);
2446
2918
 
2447
2919
  return this.selectionSet === optimizedSelection
2448
2920
  ? this
2449
2921
  : this.withUpdatedSelectionSet(optimizedSelection);
2450
2922
  }
2451
2923
 
2452
- filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
2924
+ filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
2453
2925
  if (!this.selectionSet) {
2454
2926
  return predicate(this) ? this : undefined;
2455
2927
  }
2456
2928
 
2457
- const updatedSelectionSet = this.selectionSet.filter(predicate);
2929
+ const updatedSelectionSet = this.selectionSet.filterRecursiveDepthFirst(predicate);
2458
2930
  const thisWithFilteredSelectionSet = this.selectionSet === updatedSelectionSet
2459
2931
  ? this
2460
2932
  : new FieldSelection(this.element, updatedSelectionSet);
@@ -2480,12 +2952,24 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2480
2952
  * Obviously, this operation will only succeed if this selection (both the field itself and its subselections)
2481
2953
  * make sense from the provided parent type. If this is not the case, this method will throw.
2482
2954
  */
2483
- rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FieldSelection {
2955
+ rebaseOn({
2956
+ parentType,
2957
+ fragments,
2958
+ errorIfCannotRebase,
2959
+ }: {
2960
+ parentType: CompositeType,
2961
+ fragments: NamedFragments | undefined,
2962
+ errorIfCannotRebase: boolean,
2963
+ }): FieldSelection | undefined {
2484
2964
  if (this.element.parentType === parentType) {
2485
2965
  return this;
2486
2966
  }
2487
2967
 
2488
- const rebasedElement = this.element.rebaseOn(parentType);
2968
+ const rebasedElement = this.element.rebaseOn({ parentType, errorIfCannotRebase });
2969
+ if (!rebasedElement) {
2970
+ return undefined;
2971
+ }
2972
+
2489
2973
  if (!this.selectionSet) {
2490
2974
  return this.withUpdatedElement(rebasedElement);
2491
2975
  }
@@ -2496,7 +2980,8 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2496
2980
  }
2497
2981
 
2498
2982
  validate(isCompositeType(rebasedBase), () => `Cannot rebase field selection ${this} on ${parentType}: rebased field base return type ${rebasedBase} is not composite`);
2499
- return this.withUpdatedComponents(rebasedElement, this.selectionSet.rebaseOn(rebasedBase, fragments));
2983
+ const rebasedSelectionSet = this.selectionSet.rebaseOn({ parentType: rebasedBase, fragments, errorIfCannotRebase });
2984
+ return rebasedSelectionSet.isEmpty() ? undefined : this.withUpdatedComponents(rebasedElement, rebasedSelectionSet);
2500
2985
  }
2501
2986
 
2502
2987
  /**
@@ -2547,28 +3032,41 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2547
3032
  return !!this.selectionSet?.hasDefer();
2548
3033
  }
2549
3034
 
2550
- trimUnsatisfiableBranches(_: CompositeType, options?: { recursive? : boolean }): FieldSelection {
3035
+ normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FieldSelection {
3036
+ // This could be an interface field, and if we're normalizing on one of the implementation of that
3037
+ // interface, we want to make sure we use the field of the implementation, as it may in particular
3038
+ // have a more specific type which should propagate to the recursive call to normalize.
3039
+
3040
+ const definition = parentType === this.parentType
3041
+ ? this.element.definition
3042
+ : parentType.field(this.element.name);
3043
+ assert(definition, `Cannot normalize ${this.element} at ${parentType} which does not have that field`)
3044
+
3045
+ const element = this.element.definition === definition ? this.element : this.element.withUpdatedDefinition(definition);
2551
3046
  if (!this.selectionSet) {
2552
- return this;
3047
+ return this.withUpdatedElement(element);
2553
3048
  }
2554
3049
 
2555
- const base = this.element.baseType()
2556
- assert(isCompositeType(base), () => `Field ${this.element} should not have a sub-selection`);
2557
- const trimmed = (options?.recursive ?? true) ? this.mapToSelectionSet((s) => s.trimUnsatisfiableBranches(base)) : this;
3050
+ const base = element.baseType();
3051
+ assert(isCompositeType(base), () => `Field ${element} should not have a sub-selection`);
3052
+ const normalizedSubSelection = (recursive ?? true) ? this.selectionSet.normalize({ parentType: base }) : this.selectionSet;
2558
3053
  // In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
2559
3054
  // sub-selection is empty. Which suggest something may be wrong with this part of the query
2560
3055
  // intent, but the query was valid while keeping an empty sub-selection isn't. So in that
2561
3056
  // case, we just add some "non-included" __typename field just to keep the query valid.
2562
- if (trimmed.selectionSet?.isEmpty()) {
2563
- return trimmed.withUpdatedSelectionSet(selectionSetOfElement(
2564
- new Field(
2565
- base.typenameField()!,
2566
- undefined,
2567
- [new Directive('include', { 'if': false })],
3057
+ if (normalizedSubSelection?.isEmpty()) {
3058
+ return this.withUpdatedComponents(
3059
+ element,
3060
+ selectionSetOfElement(
3061
+ new Field(
3062
+ base.typenameField()!,
3063
+ undefined,
3064
+ [new Directive('include', { 'if': false })],
3065
+ )
2568
3066
  )
2569
- ));
3067
+ );
2570
3068
  } else {
2571
- return trimmed;
3069
+ return this.withUpdatedComponents(element, normalizedSubSelection);
2572
3070
  }
2573
3071
  }
2574
3072
 
@@ -2590,7 +3088,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2590
3088
  return !!that.selectionSet && this.selectionSet.equals(that.selectionSet);
2591
3089
  }
2592
3090
 
2593
- contains(that: Selection): ContainsResult {
3091
+ contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
2594
3092
  if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
2595
3093
  return ContainsResult.NOT_CONTAINED;
2596
3094
  }
@@ -2600,7 +3098,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2600
3098
  return ContainsResult.EQUAL;
2601
3099
  }
2602
3100
  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);
3101
+ return this.selectionSet.contains(that.selectionSet, options);
2604
3102
  }
2605
3103
 
2606
3104
  toString(expandFragments: boolean = true, indent?: string): string {
@@ -2628,11 +3126,10 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
2628
3126
  }
2629
3127
  }
2630
3128
 
2631
- filter(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
3129
+ filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
2632
3130
  // Note that we essentially expand all fragments as part of this.
2633
- const selectionSet = this.selectionSet;
2634
- const updatedSelectionSet = selectionSet.filter(predicate);
2635
- const thisWithFilteredSelectionSet = updatedSelectionSet === selectionSet
3131
+ const updatedSelectionSet = this.selectionSet.filterRecursiveDepthFirst(predicate);
3132
+ const thisWithFilteredSelectionSet = updatedSelectionSet === this.selectionSet
2636
3133
  ? this
2637
3134
  : new InlineFragmentSelection(this.element, updatedSelectionSet);
2638
3135
 
@@ -2645,7 +3142,27 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
2645
3142
 
2646
3143
  abstract equals(that: Selection): boolean;
2647
3144
 
2648
- abstract contains(that: Selection): ContainsResult;
3145
+ abstract contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult;
3146
+
3147
+ normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
3148
+ const thisCondition = this.element.typeCondition;
3149
+
3150
+ // This method assumes by contract that `parentType` runtimes intersects `this.parentType`'s, but `parentType`
3151
+ // runtimes may be a subset. So first check if the selection should not be discarded on that account (that
3152
+ // is, we should not keep the selection if its condition runtimes don't intersect at all with those of
3153
+ // `parentType` as that would ultimately make an invalid selection set).
3154
+ if (thisCondition && parentType !== this.parentType) {
3155
+ const conditionRuntimes = possibleRuntimeTypes(thisCondition);
3156
+ const typeRuntimes = possibleRuntimeTypes(parentType);
3157
+ if (!conditionRuntimes.some((t) => typeRuntimes.includes(t))) {
3158
+ return undefined;
3159
+ }
3160
+ }
3161
+
3162
+ return this.normalizeKnowingItIntersects({ parentType, recursive });
3163
+ }
3164
+
3165
+ protected abstract normalizeKnowingItIntersects({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined;
2649
3166
  }
2650
3167
 
2651
3168
  class InlineFragmentSelection extends FragmentSelection {
@@ -2665,6 +3182,9 @@ class InlineFragmentSelection extends FragmentSelection {
2665
3182
  }
2666
3183
 
2667
3184
  withUpdatedComponents(fragment: FragmentElement, selectionSet: SelectionSet): InlineFragmentSelection {
3185
+ if (fragment === this.element && selectionSet === this.selectionSet) {
3186
+ return this;
3187
+ }
2668
3188
  return new InlineFragmentSelection(fragment, selectionSet);
2669
3189
  }
2670
3190
 
@@ -2679,18 +3199,31 @@ class InlineFragmentSelection extends FragmentSelection {
2679
3199
  this.selectionSet.validate(variableDefinitions);
2680
3200
  }
2681
3201
 
2682
- rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
3202
+ rebaseOn({
3203
+ parentType,
3204
+ fragments,
3205
+ errorIfCannotRebase,
3206
+ }: {
3207
+ parentType: CompositeType,
3208
+ fragments: NamedFragments | undefined,
3209
+ errorIfCannotRebase: boolean,
3210
+ }): FragmentSelection | undefined {
2683
3211
  if (this.parentType === parentType) {
2684
3212
  return this;
2685
3213
  }
2686
3214
 
2687
- const rebasedFragment = this.element.rebaseOn(parentType);
3215
+ const rebasedFragment = this.element.rebaseOn({ parentType, errorIfCannotRebase });
3216
+ if (!rebasedFragment) {
3217
+ return undefined;
3218
+ }
3219
+
2688
3220
  const rebasedCastedType = rebasedFragment.castedType();
2689
3221
  if (rebasedCastedType === this.selectionSet.parentType) {
2690
3222
  return this.withUpdatedElement(rebasedFragment);
2691
3223
  }
2692
3224
 
2693
- return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType, fragments));
3225
+ const rebasedSelectionSet = this.selectionSet.rebaseOn({ parentType: rebasedCastedType, fragments, errorIfCannotRebase });
3226
+ return rebasedSelectionSet.isEmpty() ? undefined : this.withUpdatedComponents(rebasedFragment, rebasedSelectionSet);
2694
3227
  }
2695
3228
 
2696
3229
  canAddTo(parentType: CompositeType): boolean {
@@ -2727,7 +3260,7 @@ class InlineFragmentSelection extends FragmentSelection {
2727
3260
  };
2728
3261
  }
2729
3262
 
2730
- optimize(fragments: NamedFragments): FragmentSelection {
3263
+ optimize(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): FragmentSelection {
2731
3264
  let optimizedSelection = this.selectionSet;
2732
3265
 
2733
3266
  // First, see if we can reuse fragments for the selection of this field.
@@ -2737,12 +3270,13 @@ class InlineFragmentSelection extends FragmentSelection {
2737
3270
  parentType: typeCondition,
2738
3271
  subSelection: optimizedSelection,
2739
3272
  fragments,
3273
+ validator,
2740
3274
  canUseFullMatchingFragment: (fragment) => {
2741
3275
  // To be able to use a matching fragment, it needs to have either no directives, or if it has
2742
3276
  // some, then:
2743
3277
  // 1. all it's directives should also be on the current element.
2744
3278
  // 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
3279
+ // because if those 2 conditions are true, we can replace the whole current inline fragment
2746
3280
  // with the match spread and directives will still match.
2747
3281
  return fragment.appliedDirectives.length === 0
2748
3282
  || (
@@ -2777,7 +3311,7 @@ class InlineFragmentSelection extends FragmentSelection {
2777
3311
 
2778
3312
  // Then, recurse inside the field sub-selection (note that if we matched some fragments above,
2779
3313
  // this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
2780
- optimizedSelection = optimizedSelection.optimizeSelections(fragments);
3314
+ optimizedSelection = optimizedSelection.optimizeSelections(fragments, validator);
2781
3315
 
2782
3316
  return this.selectionSet === optimizedSelection
2783
3317
  ? this
@@ -2809,55 +3343,49 @@ class InlineFragmentSelection extends FragmentSelection {
2809
3343
  : this.withUpdatedComponents(newElement, newSelection);
2810
3344
  }
2811
3345
 
2812
- trimUnsatisfiableBranches(currentType: CompositeType, options?: { recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
2813
- const recursive = options?.recursive ?? true;
2814
-
3346
+ protected normalizeKnowingItIntersects({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
2815
3347
  const thisCondition = this.element.typeCondition;
2816
- // Note that if the condition has directives, we preserve the fragment no matter what.
3348
+
3349
+ // We know the condition is "valid", but it may not be useful. That said, if the condition has directives,
3350
+ // we preserve the fragment no matter what.
2817
3351
  if (this.element.appliedDirectives.length === 0) {
2818
- if (!thisCondition || currentType === this.element.typeCondition) {
2819
- const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType, options);
2820
- return trimmed.isEmpty() ? undefined : trimmed;
3352
+ // There is a number of cases where a fragment is not useful:
3353
+ // 1. if there is not conditions (remember it also has no directives).
3354
+ // 2. if it's the same type as the current type: it's not restricting types further.
3355
+ // 3. if the current type is an object more generally: because in that case too the condition
3356
+ // cannot be restricting things further (it's typically a less precise interface/union).
3357
+ if (!thisCondition || parentType === this.element.typeCondition || isObjectType(parentType)) {
3358
+ const normalized = this.selectionSet.normalize({ parentType, recursive });
3359
+ return normalized.isEmpty() ? undefined : normalized;
2821
3360
  }
3361
+ }
2822
3362
 
2823
- // If the current type is an object, then we never need to keep the current fragment because:
2824
- // - either the fragment is also an object, but we've eliminated the case where the 2 types are the same,
2825
- // so this is just an unsatisfiable branch.
2826
- // - or it's not an object, but then the current type is more precise and no point in "casting" to a
2827
- // less precise interface/union. And if the current type is not even a valid runtime of said interface/union,
2828
- // then we should completely ignore the branch (or, since we're eliminating `thisCondition`, we would be
2829
- // building an invalid selection).
2830
- if (isObjectType(currentType)) {
2831
- if (isObjectType(thisCondition) || !possibleRuntimeTypes(thisCondition).includes(currentType)) {
3363
+ // We preserve the current fragment, so we only recurse within the sub-selection if we're asked to be recusive.
3364
+ // (note that even if we're not recursive, we may still have some "lifting" to do)
3365
+ let normalizedSelectionSet: SelectionSet;
3366
+ if (recursive ?? true) {
3367
+ normalizedSelectionSet = this.selectionSet.normalize({ parentType: thisCondition ?? parentType });
3368
+
3369
+ // It could be that everything was unsatisfiable.
3370
+ if (normalizedSelectionSet.isEmpty()) {
3371
+ if (this.element.appliedDirectives.length === 0) {
2832
3372
  return undefined;
2833
3373
  } else {
2834
- const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType, options);
2835
- return trimmed.isEmpty() ? undefined : trimmed;
3374
+ return this.withUpdatedComponents(
3375
+ // We should be able to rebase, or there is a bug, so error if that is the case.
3376
+ this.element.rebaseOnOrError(parentType),
3377
+ selectionSetOfElement(
3378
+ new Field(
3379
+ (this.element.typeCondition ?? parentType).typenameField()!,
3380
+ undefined,
3381
+ [new Directive('include', { 'if': false })],
3382
+ )
3383
+ )
3384
+ );
2836
3385
  }
2837
3386
  }
2838
- }
2839
-
2840
- // As we preserve the current fragment, the rest is about recursing. If we don't recurse, we're done
2841
- if (!recursive) {
2842
- return this;
2843
- }
2844
-
2845
- // In all other cases, we first recurse on the sub-selection.
2846
- const trimmedSelectionSet = this.selectionSet.trimUnsatisfiableBranches(this.element.typeCondition ?? this.parentType);
2847
-
2848
- // First, could be that everything was unsatisfiable.
2849
- if (trimmedSelectionSet.isEmpty()) {
2850
- if (this.element.appliedDirectives.length === 0) {
2851
- return undefined;
2852
- } else {
2853
- return this.withUpdatedSelectionSet(selectionSetOfElement(
2854
- new Field(
2855
- (this.element.typeCondition ?? this.parentType).typenameField()!,
2856
- undefined,
2857
- [new Directive('include', { 'if': false })],
2858
- )
2859
- ));
2860
- }
3387
+ } else {
3388
+ normalizedSelectionSet = this.selectionSet;
2861
3389
  }
2862
3390
 
2863
3391
  // Second, we check if some of the sub-selection fragments can be "lifted" outside of this fragment. This can happen if:
@@ -2865,10 +3393,10 @@ class InlineFragmentSelection extends FragmentSelection {
2865
3393
  // 2. the sub-fragment is an object type,
2866
3394
  // 3. the sub-fragment type is a valid runtime of the current type.
2867
3395
  if (this.element.appliedDirectives.length === 0 && isAbstractType(thisCondition!)) {
2868
- assert(!isObjectType(currentType), () => `Should not have got here if ${currentType} is an object type`);
2869
- const currentRuntimes = possibleRuntimeTypes(currentType);
3396
+ assert(!isObjectType(parentType), () => `Should not have got here if ${parentType} is an object type`);
3397
+ const currentRuntimes = possibleRuntimeTypes(parentType);
2870
3398
  const liftableSelections: Selection[] = [];
2871
- for (const selection of trimmedSelectionSet.selections()) {
3399
+ for (const selection of normalizedSelectionSet.selections()) {
2872
3400
  if (selection.kind === 'FragmentSelection'
2873
3401
  && selection.element.typeCondition
2874
3402
  && isObjectType(selection.element.typeCondition)
@@ -2879,8 +3407,8 @@ class InlineFragmentSelection extends FragmentSelection {
2879
3407
  }
2880
3408
 
2881
3409
  // If we can lift all selections, then that just mean we can get rid of the current fragment altogether
2882
- if (liftableSelections.length === trimmedSelectionSet.selections().length) {
2883
- return trimmedSelectionSet;
3410
+ if (liftableSelections.length === normalizedSelectionSet.selections().length) {
3411
+ return normalizedSelectionSet;
2884
3412
  }
2885
3413
 
2886
3414
  // Otherwise, if there is "liftable" selections, we must return a set comprised of those lifted selection,
@@ -2889,13 +3417,15 @@ class InlineFragmentSelection extends FragmentSelection {
2889
3417
  const newSet = new SelectionSetUpdates();
2890
3418
  newSet.add(liftableSelections);
2891
3419
  newSet.add(this.withUpdatedSelectionSet(
2892
- trimmedSelectionSet.filter((s) => !liftableSelections.includes(s)),
3420
+ normalizedSelectionSet.filter((s) => !liftableSelections.includes(s)),
2893
3421
  ));
2894
- return newSet.toSelectionSet(this.parentType);
3422
+ return newSet.toSelectionSet(parentType);
2895
3423
  }
2896
3424
  }
2897
3425
 
2898
- return this.selectionSet === trimmedSelectionSet ? this : this.withUpdatedSelectionSet(trimmedSelectionSet);
3426
+ return this.parentType === parentType && this.selectionSet === normalizedSelectionSet
3427
+ ? this
3428
+ : this.withUpdatedComponents(this.element.rebaseOnOrError(parentType), normalizedSelectionSet);
2899
3429
  }
2900
3430
 
2901
3431
  expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection {
@@ -2912,12 +3442,12 @@ class InlineFragmentSelection extends FragmentSelection {
2912
3442
  && this.selectionSet.equals(that.selectionSet);
2913
3443
  }
2914
3444
 
2915
- contains(that: Selection): ContainsResult {
3445
+ contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
2916
3446
  if (!(that instanceof FragmentSelection) || !this.element.equals(that.element)) {
2917
3447
  return ContainsResult.NOT_CONTAINED;
2918
3448
  }
2919
3449
 
2920
- return this.selectionSet.contains(that.selectionSet);
3450
+ return this.selectionSet.contains(that.selectionSet, options);
2921
3451
  }
2922
3452
 
2923
3453
  toString(expandFragments: boolean = true, indent?: string): string {
@@ -2956,17 +3486,20 @@ class FragmentSpreadSelection extends FragmentSelection {
2956
3486
  assert(false, `Unsupported`);
2957
3487
  }
2958
3488
 
2959
- trimUnsatisfiableBranches(parentType: CompositeType): FragmentSelection {
3489
+ normalizeKnowingItIntersects({ parentType }: { parentType: CompositeType }): FragmentSelection {
2960
3490
  // We must update the spread parent type if necessary since we're not going deeper,
2961
3491
  // 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);
3492
+ assert(parentType.schema() === this.parentType.schema(), 'Should not try to normalize using a type from another schema');
3493
+ return this.rebaseOnOrError({ parentType, fragments: this.fragments });
2964
3494
  }
2965
3495
 
2966
3496
  validate(): void {
2967
3497
  this.validateDeferAndStream();
2968
3498
 
2969
- // We don't do anything else because fragment definition are validated when created.
3499
+ validate(
3500
+ runtimeTypesIntersects(this.parentType, this.namedFragment.typeCondition),
3501
+ () => `Fragment "${this.namedFragment.name}" cannot be spread inside type ${this.parentType} as the runtime types do not intersect ${this.namedFragment.typeCondition}`
3502
+ );
2970
3503
  }
2971
3504
 
2972
3505
  toSelectionNode(): FragmentSpreadNode {
@@ -2989,11 +3522,19 @@ class FragmentSpreadSelection extends FragmentSelection {
2989
3522
  };
2990
3523
  }
2991
3524
 
2992
- optimize(_: NamedFragments): FragmentSelection {
3525
+ optimize(_1: NamedFragments, _2: FieldsConflictMultiBranchValidator): FragmentSelection {
2993
3526
  return this;
2994
3527
  }
2995
3528
 
2996
- rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
3529
+ rebaseOn({
3530
+ parentType,
3531
+ fragments,
3532
+ errorIfCannotRebase,
3533
+ }: {
3534
+ parentType: CompositeType,
3535
+ fragments: NamedFragments | undefined,
3536
+ errorIfCannotRebase: boolean,
3537
+ }): FragmentSelection | undefined {
2997
3538
  // We preserve the parent type here, to make sure we don't lose context, but we actually don't
2998
3539
  // want to expand the spread as that would compromise the code that optimize subgraph fetches to re-use named
2999
3540
  // fragments.
@@ -3013,7 +3554,14 @@ class FragmentSpreadSelection extends FragmentSelection {
3013
3554
  assert(fragments || this.parentType.schema() === parentType.schema(), `Must provide fragments is rebasing on other schema`);
3014
3555
  const newFragments = fragments ?? this.fragments;
3015
3556
  const namedFragment = newFragments.get(this.namedFragment.name);
3016
- assert(namedFragment, () => `Cannot rebase ${this} if it isn't part of the provided fragments`);
3557
+ // If we're rebasing on another schema (think a subgraph), then named fragments will have been rebased on that, and some
3558
+ // of them may not contain anything that is on that subgraph, in which case they will not have been included at all.
3559
+ // If so, then as long as we're not ask to error if we cannot rebase, then we're happy to skip that spread (since again,
3560
+ // it expands to nothing that apply on the schema).
3561
+ if (!namedFragment) {
3562
+ validate(!errorIfCannotRebase, () => `Cannot rebase ${this.toString(false)} if it isn't part of the provided fragments`);
3563
+ return undefined;
3564
+ }
3017
3565
  return new FragmentSpreadSelection(
3018
3566
  parentType,
3019
3567
  newFragments,
@@ -3069,7 +3617,7 @@ class FragmentSpreadSelection extends FragmentSelection {
3069
3617
  && sameDirectiveApplications(this.spreadDirectives, that.spreadDirectives);
3070
3618
  }
3071
3619
 
3072
- contains(that: Selection): ContainsResult {
3620
+ contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
3073
3621
  if (this.equals(that)) {
3074
3622
  return ContainsResult.EQUAL;
3075
3623
  }
@@ -3078,7 +3626,7 @@ class FragmentSpreadSelection extends FragmentSelection {
3078
3626
  return ContainsResult.NOT_CONTAINED;
3079
3627
  }
3080
3628
 
3081
- return this.selectionSet.contains(that.selectionSet);
3629
+ return this.selectionSet.contains(that.selectionSet, options);
3082
3630
  }
3083
3631
 
3084
3632
  toString(expandFragments: boolean = true, indent?: string): string {