@apollo/federation-internals 2.4.7 → 2.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/operations.ts CHANGED
@@ -47,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';
@@ -115,7 +116,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
115
116
 
116
117
  constructor(
117
118
  readonly definition: FieldDefinition<CompositeType>,
118
- private readonly args?: TArgs,
119
+ readonly args?: TArgs,
119
120
  directives?: readonly Directive<any>[],
120
121
  readonly alias?: string,
121
122
  ) {
@@ -865,69 +866,96 @@ export class Operation {
865
866
  readonly name?: string) {
866
867
  }
867
868
 
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) {
869
+ // Returns a copy of this operation with the provided updated selection set.
870
+ // Note that this method assumes that the existing `this.fragments` is still appropriate.
871
+ private withUpdatedSelectionSet(newSelectionSet: SelectionSet): Operation {
872
+ if (this.selectionSet === newSelectionSet) {
902
873
  return this;
903
874
  }
904
875
 
905
876
  return new Operation(
906
877
  this.schema,
907
878
  this.rootKind,
908
- expandedSelections,
879
+ newSelectionSet,
909
880
  this.variableDefinitions,
910
- undefined,
881
+ this.fragments,
911
882
  this.name
912
883
  );
913
884
  }
914
885
 
915
- trimUnsatisfiableBranches(): Operation {
916
- const trimmedSelections = this.selectionSet.trimUnsatisfiableBranches(this.selectionSet.parentType);
917
- if (trimmedSelections === this.selectionSet) {
886
+ // Returns a copy of this operation with the provided updated selection set and fragments.
887
+ private withUpdatedSelectionSetAndFragments(newSelectionSet: SelectionSet, newFragments: NamedFragments | undefined): Operation {
888
+ if (this.selectionSet === newSelectionSet && newFragments === this.fragments) {
918
889
  return this;
919
890
  }
920
891
 
921
892
  return new Operation(
922
893
  this.schema,
923
894
  this.rootKind,
924
- trimmedSelections,
895
+ newSelectionSet,
925
896
  this.variableDefinitions,
926
- this.fragments,
897
+ newFragments,
927
898
  this.name
928
899
  );
929
900
  }
930
901
 
902
+ optimize(fragments?: NamedFragments, minUsagesToOptimize: number = 2): Operation {
903
+ assert(minUsagesToOptimize >= 1, `Expected 'minUsagesToOptimize' to be at least 1, but got ${minUsagesToOptimize}`)
904
+ if (!fragments || fragments.isEmpty()) {
905
+ return this;
906
+ }
907
+
908
+ let optimizedSelection = this.selectionSet.optimize(fragments);
909
+ if (optimizedSelection === this.selectionSet) {
910
+ return this;
911
+ }
912
+
913
+ let finalFragments = computeFragmentsToKeep(optimizedSelection, fragments, minUsagesToOptimize);
914
+
915
+ // If there is fragment usages and we're not keeping all fragments, we need to expand fragments.
916
+ if (finalFragments !== null && finalFragments?.size !== fragments.size) {
917
+ // Note that optimizing all fragments to potentially re-expand some is not entirely optimal, but it's unclear
918
+ // how to do otherwise, and it probably don't matter too much in practice (we only call this optimization
919
+ // on the final computed query plan, so not a very hot path; plus in most cases we won't even reach that
920
+ // point either because there is no fragment, or none will have been optimized away so we'll exit above).
921
+ optimizedSelection = optimizedSelection.expandFragments(finalFragments);
922
+
923
+ // Expanding fragments could create some "inefficiencies" that we wouldn't have if we hadn't re-optimized
924
+ // the fragments to de-optimize it later, so we do a final "normalize" pass to remove those.
925
+ optimizedSelection = optimizedSelection.normalize({ parentType: optimizedSelection.parentType });
926
+
927
+ // And if we've expanded some fragments but kept others, then it's not 100% impossible that some
928
+ // fragment was used multiple times in some expanded fragment(s), but that post-expansion all of
929
+ // it's usages are "dead" branches that are removed by the final `normalize`. In that case though,
930
+ // we need to ensure we don't include the now-unused fragment in the final list of fragments.
931
+ // TODO: remark that the same reasoning could leave a single instance of a fragment usage, so if
932
+ // we really really want to never have less than `minUsagesToOptimize`, we could do some loop of
933
+ // `expand then normalize` unless all fragments are provably used enough. We don't bother, because
934
+ // leaving this is not a huge deal and it's not worth the complexity, but it could be that we can
935
+ // refactor all this later to avoid this case without additional complexity.
936
+ if (finalFragments) {
937
+ const usages = new Map<string, number>();
938
+ optimizedSelection.collectUsedFragmentNames(usages);
939
+ finalFragments = finalFragments.filter((f) => (usages.get(f.name) ?? 0) > 0);
940
+ }
941
+ }
942
+
943
+ return this.withUpdatedSelectionSetAndFragments(optimizedSelection, finalFragments ?? undefined);
944
+ }
945
+
946
+ expandAllFragments(): Operation {
947
+ // We clear up the fragments since we've expanded all.
948
+ // Also note that expanding fragment usually generate unecessary fragments/inefficient selections, so it
949
+ // basically always make sense to normalize afterwards. Besides, fragment reuse (done by `optimize`) rely
950
+ // on the fact that its input is normalized to work properly, so all the more reason to do it here.
951
+ const expanded = this.selectionSet.expandFragments();
952
+ return this.withUpdatedSelectionSetAndFragments(expanded.normalize({ parentType: expanded.parentType }), undefined);
953
+ }
954
+
955
+ normalize(): Operation {
956
+ return this.withUpdatedSelectionSet(this.selectionSet.normalize({ parentType: this.selectionSet.parentType }));
957
+ }
958
+
931
959
  /**
932
960
  * Returns this operation but potentially modified so all/some of the @defer applications have been removed.
933
961
  *
@@ -936,10 +964,7 @@ export class Operation {
936
964
  * applications are removed.
937
965
  */
938
966
  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);
967
+ return this.withUpdatedSelectionSet(this.selectionSet.withoutDefer(labelsToRemove));
943
968
  }
944
969
 
945
970
  /**
@@ -965,8 +990,7 @@ export class Operation {
965
990
  const { hasDefers, hasNonLabelledOrConditionalDefers } = normalizer.init(this.selectionSet);
966
991
  let updatedOperation: Operation = this;
967
992
  if (hasNonLabelledOrConditionalDefers) {
968
- const updated = this.selectionSet.withNormalizedDefer(normalizer);
969
- updatedOperation = new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.fragments, this.name);
993
+ updatedOperation = this.withUpdatedSelectionSet(this.selectionSet.withNormalizedDefer(normalizer));
970
994
  }
971
995
  return {
972
996
  operation: updatedOperation,
@@ -991,6 +1015,8 @@ export class Operation {
991
1015
  }
992
1016
  }
993
1017
 
1018
+ export type FragmentRestrictionAtType = { selectionSet: SelectionSet, validator?: FieldsConflictValidator };
1019
+
994
1020
  export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmentDefinition> {
995
1021
  private _selectionSet: SelectionSet | undefined;
996
1022
 
@@ -1000,7 +1026,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1000
1026
  private _fragmentUsages: Map<string, number> | undefined;
1001
1027
  private _includedFragmentNames: Set<string> | undefined;
1002
1028
 
1003
- private readonly expandedSelectionSetsAtTypesCache = new Map<string, SelectionSet>();
1029
+ private readonly expandedSelectionSetsAtTypesCache = new Map<string, FragmentRestrictionAtType>();
1004
1030
 
1005
1031
  constructor(
1006
1032
  schema: Schema,
@@ -1027,7 +1053,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1027
1053
 
1028
1054
  expandedSelectionSet(): SelectionSet {
1029
1055
  if (!this._expandedSelectionSet) {
1030
- this._expandedSelectionSet = this.selectionSet.expandFragments().trimUnsatisfiableBranches(this.typeCondition);
1056
+ this._expandedSelectionSet = this.selectionSet.expandFragments().normalize({ parentType: this.typeCondition });
1031
1057
  }
1032
1058
  return this._expandedSelectionSet;
1033
1059
  }
@@ -1071,13 +1097,32 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1071
1097
  }
1072
1098
 
1073
1099
  /**
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.
1100
+ * Whether this fragment may apply _directly_ at the provided type, meaning that the fragment sub-selection
1101
+ * could be put directly inside a `... on type {}` inline fragment (_without_ re-adding the fragment condition
1102
+ * that is), and both be valid and not "lose context".
1103
+ *
1104
+ * For that to be true, we need one of this to be true:
1105
+ * 1. the runtime types of the fragment condition must be at least as general as those of the provided `type`.
1106
+ * Otherwise, putting it at `type` without its condition would "generalize" more than fragment meant to (and
1107
+ * so we'd "lose context"
1108
+ * 2. either `type` and `this.typeCondition` are equal, or `type` is an object or `this.typeCondition` is a union
1109
+ * The idea is that, assuming our 1st point, then:
1110
+ * - if both are equal, things works trivially.
1111
+ * - if `type` is an object, `this.typeCondition` is either the same object, or a union/interface for which
1112
+ * type is a valid runtime. In all case, anything valid on `this.typeCondition` would apply to `type` too.
1113
+ * - if `this.typeCondition` is a union, then it's selection can only have fragments on object types at top-level
1114
+ * (save for `__typename`), and all those selection will work at `type` too.
1115
+ * But in any other case, both types must be abstract (if `this.typeCondition` is an object, the 1st condition
1116
+ * imply `type` can only be the same type) and we're in one of:
1117
+ * - `type` and `this.typeCondition` are both different interfaces (that intersect but are different).
1118
+ * - `type` is aunion and `this.typeCondition` an interface.
1119
+ * And in both cases, the selection of the fragment may selection an interface that is not valid at `type` (if `type`
1120
+ * is a union because a direct field is always wrong, and if `type` is another interface because that interface may
1121
+ * not have that particular field).
1077
1122
  *
1078
1123
  * @param type - the type at which we're looking at applying the fragment
1079
1124
  */
1080
- canApplyAtType(type: CompositeType): boolean {
1125
+ canApplyDirectlyAtType(type: CompositeType): boolean {
1081
1126
  if (sameType(type, this.typeCondition)) {
1082
1127
  return true;
1083
1128
  }
@@ -1090,17 +1135,20 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1090
1135
 
1091
1136
  const conditionRuntimes = possibleRuntimeTypes(this.typeCondition);
1092
1137
  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).
1138
+ // The fragment condition must be at least as general as the provided type (in other words, all of the
1139
+ // runtimes of `type` must be in `conditionRuntimes`).
1096
1140
  // Note: the `length` test is technically redundant, but just avoid the more costly sub-set check if we
1097
1141
  // can cheaply show it's unnecessary.
1098
- return conditionRuntimes.length >= typeRuntimes.length
1099
- && typeRuntimes.every((t1) => conditionRuntimes.some((t2) => sameType(t1, t2)));
1142
+ if (conditionRuntimes.length < typeRuntimes.length
1143
+ || !typeRuntimes.every((t1) => conditionRuntimes.some((t2) => sameType(t1, t2)))) {
1144
+ return false;
1145
+ }
1146
+
1147
+ return isObjectType(type) || isUnionType(this.typeCondition);
1100
1148
  }
1101
1149
 
1102
1150
  /**
1103
- * This methods *assumes* that `this.canApplyAtType(type)` is `true` (and may crash if this is not true), and returns
1151
+ * This methods *assumes* that `this.canApplyDirectlyAtType(type)` is `true` (and may crash if this is not true), and returns
1104
1152
  * a version fo this named fragment selection set that corresponds to the "expansion" of this named fragment at `type`
1105
1153
  *
1106
1154
  * The overall idea here is that if we have an interface I with 2 implementations T1 and T2, and we have a fragment like:
@@ -1117,34 +1165,35 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1117
1165
  * then if the current type is `T1`, then all we care about matching for this fragment is the `... on T1` part, and this method gives
1118
1166
  * us that part.
1119
1167
  */
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.
1168
+ expandedSelectionSetAtType(type: CompositeType): FragmentRestrictionAtType {
1169
+ // First, if the candidate condition is an object or is the type passed, then there isn't any restriction to do.
1124
1170
  if (sameType(type, this.typeCondition) || isObjectType(this.typeCondition)) {
1125
- return expandedSelectionSet;
1171
+ return { selectionSet: this.expandedSelectionSet() };
1126
1172
  }
1127
1173
 
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;
1174
+ let cached = this.expandedSelectionSetsAtTypesCache.get(type.name);
1175
+ if (!cached) {
1176
+ cached = this.computeExpandedSelectionSetAtType(type);
1177
+ this.expandedSelectionSetsAtTypesCache.set(type.name, cached);
1138
1178
  }
1179
+ return cached;
1180
+ }
1139
1181
 
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;
1182
+ private computeExpandedSelectionSetAtType(type: CompositeType): FragmentRestrictionAtType {
1183
+ const expandedSelectionSet = this.expandedSelectionSet();
1184
+ // Note that what we want is get any simplification coming from normalizing at `type`, but any such simplication
1185
+ // stops as soon as we traverse a field, so no point in being recursive.
1186
+ const selectionSet = expandedSelectionSet.normalize({ parentType: type, recursive: false });
1187
+
1188
+ // Note that `trimmed` is the difference of 2 selections that may not have been normalized on the same parent type,
1189
+ // so in practice, it is possible that `trimmed` contains some of the selections that `selectionSet` contains, but
1190
+ // that they have been simplified in `selectionSet` in such a way that the `minus` call does not see it. However,
1191
+ // it is not trivial to deal with this, and it is fine given that we use trimmed to create the validator because
1192
+ // we know the non-trimmed parts cannot create field conflict issues so we're trying to build a smaller validator,
1193
+ // but it's ok if trimmed is not as small as it theoretically can be.
1194
+ const trimmed = expandedSelectionSet.minus(selectionSet);
1195
+ const validator = trimmed.isEmpty() ? undefined : FieldsConflictValidator.build(trimmed);
1196
+ return { selectionSet, validator };
1148
1197
  }
1149
1198
 
1150
1199
  /**
@@ -1180,6 +1229,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1180
1229
  }
1181
1230
  }
1182
1231
 
1232
+
1183
1233
  export class NamedFragments {
1184
1234
  private readonly fragments = new MapWithCachedArrays<string, NamedFragmentDefinition>();
1185
1235
 
@@ -1208,8 +1258,8 @@ export class NamedFragments {
1208
1258
  }
1209
1259
  }
1210
1260
 
1211
- maybeApplyingAtType(type: CompositeType): NamedFragmentDefinition[] {
1212
- return this.fragments.values().filter(f => f.canApplyAtType(type));
1261
+ maybeApplyingDirectlyAtType(type: CompositeType): NamedFragmentDefinition[] {
1262
+ return this.fragments.values().filter(f => f.canApplyDirectlyAtType(type));
1213
1263
  }
1214
1264
 
1215
1265
  get(name: string): NamedFragmentDefinition | undefined {
@@ -1288,7 +1338,7 @@ export class NamedFragments {
1288
1338
  mapper: (selectionSet: SelectionSet) => SelectionSet | undefined,
1289
1339
  ): NamedFragments | undefined {
1290
1340
  return this.mapInDependencyOrder((fragment, newFragments) => {
1291
- const mappedSelectionSet = mapper(fragment.selectionSet.expandFragments().trimUnsatisfiableBranches(fragment.typeCondition));
1341
+ const mappedSelectionSet = mapper(fragment.selectionSet.expandFragments().normalize({ parentType: fragment.typeCondition }));
1292
1342
  if (!mappedSelectionSet) {
1293
1343
  return undefined;
1294
1344
  }
@@ -1417,6 +1467,8 @@ export enum ContainsResult {
1417
1467
  EQUAL,
1418
1468
  }
1419
1469
 
1470
+ export type CollectedFieldsInSet = { path: string[], field: FieldSelection }[];
1471
+
1420
1472
  export class SelectionSet {
1421
1473
  private readonly _keyedSelections: Map<string, Selection>;
1422
1474
  private readonly _selections: readonly Selection[];
@@ -1447,7 +1499,7 @@ export class SelectionSet {
1447
1499
  return this._keyedSelections.has(typenameFieldName);
1448
1500
  }
1449
1501
 
1450
- fieldsInSet(): { path: string[], field: FieldSelection }[] {
1502
+ fieldsInSet(): CollectedFieldsInSet {
1451
1503
  const fields = new Array<{ path: string[], field: FieldSelection }>();
1452
1504
  for (const selection of this.selections()) {
1453
1505
  if (selection.kind === 'FieldSelection') {
@@ -1463,6 +1515,22 @@ export class SelectionSet {
1463
1515
  return fields;
1464
1516
  }
1465
1517
 
1518
+ fieldsByResponseName(): MultiMap<string, FieldSelection> {
1519
+ const byResponseName = new MultiMap<string, FieldSelection>();
1520
+ this.collectFieldsByResponseName(byResponseName);
1521
+ return byResponseName;
1522
+ }
1523
+
1524
+ private collectFieldsByResponseName(collector: MultiMap<string, FieldSelection>) {
1525
+ for (const selection of this.selections()) {
1526
+ if (selection.kind === 'FieldSelection') {
1527
+ collector.add(selection.element.responseName(), selection);
1528
+ } else {
1529
+ selection.selectionSet.collectFieldsByResponseName(collector);
1530
+ }
1531
+ }
1532
+ }
1533
+
1466
1534
  usedVariables(): Variables {
1467
1535
  const collector = new VariableCollector();
1468
1536
  this.collectVariables(collector);
@@ -1502,7 +1570,8 @@ export class SelectionSet {
1502
1570
  // With that, `optimizeSelections` will correctly match on the `on Query` fragment; after which
1503
1571
  // we can unpack the final result.
1504
1572
  const wrapped = new InlineFragmentSelection(new FragmentElement(this.parentType, this.parentType), this);
1505
- const optimized = wrapped.optimize(fragments);
1573
+ const validator = FieldsConflictValidator.build(this);
1574
+ const optimized = wrapped.optimize(fragments, validator);
1506
1575
 
1507
1576
  // Now, it's possible we matched a full fragment, in which case `optimized` will be just the named fragment,
1508
1577
  // and in that case we return a singleton selection with just that. Otherwise, it's our wrapping inline fragment
@@ -1515,16 +1584,90 @@ export class SelectionSet {
1515
1584
  // Tries to match fragments inside each selections of this selection set, and this recursively. However, note that this
1516
1585
  // may not match fragments that would apply at top-level, so you should usually use `optimize` instead (this exists mostly
1517
1586
  // for the recursion).
1518
- optimizeSelections(fragments: NamedFragments): SelectionSet {
1519
- return this.lazyMap((selection) => selection.optimize(fragments));
1587
+ optimizeSelections(fragments: NamedFragments, validator: FieldsConflictValidator): SelectionSet {
1588
+ return this.lazyMap((selection) => selection.optimize(fragments, validator));
1520
1589
  }
1521
1590
 
1522
1591
  expandFragments(updatedFragments?: NamedFragments): SelectionSet {
1523
1592
  return this.lazyMap((selection) => selection.expandFragments(updatedFragments));
1524
1593
  }
1525
1594
 
1526
- trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): SelectionSet {
1527
- return this.lazyMap((selection) => selection.trimUnsatisfiableBranches(parentType, options), { parentType });
1595
+ /**
1596
+ * Applies some normalization rules to this selection set in the context of the provided `parentType`.
1597
+ *
1598
+ * Normalization mostly removes unecessary/redundant inline fragments, so that for instance, with
1599
+ * schema:
1600
+ * ```graphql
1601
+ * type Query {
1602
+ * t1: T1
1603
+ * i: I
1604
+ * }
1605
+ *
1606
+ * interface I {
1607
+ * id: ID!
1608
+ * }
1609
+ *
1610
+ * type T1 implements I {
1611
+ * id: ID!
1612
+ * v1: Int
1613
+ * }
1614
+ *
1615
+ * type T2 implements I {
1616
+ * id: ID!
1617
+ * v2: Int
1618
+ * }
1619
+ * ```
1620
+ *
1621
+ * ```
1622
+ * normalize({
1623
+ * t1 {
1624
+ * ... on I {
1625
+ * id
1626
+ * }
1627
+ * }
1628
+ * i {
1629
+ * ... on T1 {
1630
+ * ... on I {
1631
+ * ... on T1 {
1632
+ * v1
1633
+ * }
1634
+ * ... on T2 {
1635
+ * v2
1636
+ * }
1637
+ * }
1638
+ * }
1639
+ * ... on T2 {
1640
+ * ... on I {
1641
+ * id
1642
+ * }
1643
+ * }
1644
+ * }
1645
+ * }) === {
1646
+ * t1 {
1647
+ * id
1648
+ * }
1649
+ * i {
1650
+ * ... on T1 {
1651
+ * v1
1652
+ * }
1653
+ * ... on T2 {
1654
+ * id
1655
+ * }
1656
+ * }
1657
+ * }
1658
+ * ```
1659
+ *
1660
+ * For this operation to be valid (to not throw), `parentType` must be such this selection set would
1661
+ * be valid as a subselection of an inline fragment `... on parentType { <this selection set> }` (and
1662
+ * so `this.normalize(this.parentType)` is always valid and useful, but it is possible to pass a `parentType`
1663
+ * that is more "restrictive" than the selection current parent type).
1664
+ *
1665
+ * Passing the option `recursive == false` makes the normalization only apply at the top-level, removing
1666
+ * any unecessary top-level inline fragments, possibly multiple layers of them, but we never recurse
1667
+ * inside the sub-selection of an selection that is not removed by the normalization.
1668
+ */
1669
+ normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): SelectionSet {
1670
+ return this.lazyMap((selection) => selection.normalize({ parentType, recursive }), { parentType });
1528
1671
  }
1529
1672
 
1530
1673
  /**
@@ -1575,17 +1718,26 @@ export class SelectionSet {
1575
1718
  }
1576
1719
 
1577
1720
  /**
1578
- * Returns the selection select from filtering out any selection that does not match the provided predicate.
1721
+ * Returns the selection set resulting from filtering out any of the top-level selection that does not match the provided predicate.
1579
1722
  *
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.
1723
+ * Please that this method does not recurse within sub-selections.
1582
1724
  */
1583
1725
  filter(predicate: (selection: Selection) => boolean): SelectionSet {
1584
- return this.lazyMap((selection) => selection.filter(predicate));
1726
+ return this.lazyMap((selection) => predicate(selection) ? selection : undefined);
1727
+ }
1728
+
1729
+ /**
1730
+ * Returns the selection set resulting from "recursively" filtering any selection that does not match the provided predicate.
1731
+ * This method calls `predicate` on every selection of the selection set, not just top-level ones, and apply a "depth-first"
1732
+ * strategy, meaning that when the predicate is call on a given selection, the it is guaranteed that filtering has happened
1733
+ * on all the selections of its sub-selection.
1734
+ */
1735
+ filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): SelectionSet {
1736
+ return this.lazyMap((selection) => selection.filterRecursiveDepthFirst(predicate));
1585
1737
  }
1586
1738
 
1587
1739
  withoutEmptyBranches(): SelectionSet | undefined {
1588
- const updated = this.filter((selection) => selection.selectionSet?.isEmpty() !== true);
1740
+ const updated = this.filterRecursiveDepthFirst((selection) => selection.selectionSet?.isEmpty() !== true);
1589
1741
  return updated.isEmpty() ? undefined : updated;
1590
1742
  }
1591
1743
 
@@ -1640,51 +1792,6 @@ export class SelectionSet {
1640
1792
  : ContainsResult.STRICTLY_CONTAINED;
1641
1793
  }
1642
1794
 
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 };
1686
- }
1687
-
1688
1795
  /**
1689
1796
  * Returns a selection set that correspond to this selection set but where any of the selections in the
1690
1797
  * provided selection set have been remove.
@@ -1706,6 +1813,28 @@ export class SelectionSet {
1706
1813
  return updated.toSelectionSet(this.parentType);
1707
1814
  }
1708
1815
 
1816
+ intersectionWith(that: SelectionSet): SelectionSet {
1817
+ if (this.isEmpty()) {
1818
+ return this;
1819
+ }
1820
+ if (that.isEmpty()) {
1821
+ return that;
1822
+ }
1823
+
1824
+ const intersection = new SelectionSetUpdates();
1825
+ for (const [key, thisSelection] of this._keyedSelections) {
1826
+ const thatSelection = that._keyedSelections.get(key);
1827
+ if (thatSelection) {
1828
+ const selection = thisSelection.intersectionWith(thatSelection);
1829
+ if (selection) {
1830
+ intersection.add(selection);
1831
+ }
1832
+ }
1833
+ }
1834
+
1835
+ return intersection.toSelectionSet(this.parentType);
1836
+ }
1837
+
1709
1838
  canRebaseOn(parentTypeToTest: CompositeType): boolean {
1710
1839
  return this.selections().every((selection) => selection.canAddTo(parentTypeToTest));
1711
1840
  }
@@ -1923,6 +2052,19 @@ export class SelectionSetUpdates {
1923
2052
  toSelectionSet(parentType: CompositeType, fragments?: NamedFragments): SelectionSet {
1924
2053
  return makeSelectionSet(parentType, this.keyedUpdates, fragments);
1925
2054
  }
2055
+
2056
+ toString() {
2057
+ return '{\n'
2058
+ + [...this.keyedUpdates.entries()].map(([k, updates]) => {
2059
+ const updStr = updates.map((upd) =>
2060
+ upd instanceof AbstractSelection
2061
+ ? upd.toString()
2062
+ : `${upd.path} -> ${upd.selections}`
2063
+ );
2064
+ return ` - ${k}: ${updStr}`;
2065
+ }).join('\n')
2066
+ +'\n\}'
2067
+ }
1926
2068
  }
1927
2069
 
1928
2070
  function addToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, selections: Selection | SelectionSet | readonly Selection[]) {
@@ -2185,7 +2327,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2185
2327
 
2186
2328
  abstract key(): string;
2187
2329
 
2188
- abstract optimize(fragments: NamedFragments): Selection;
2330
+ abstract optimize(fragments: NamedFragments, validator: FieldsConflictValidator): Selection;
2189
2331
 
2190
2332
  abstract toSelectionNode(): SelectionNode;
2191
2333
 
@@ -2240,7 +2382,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2240
2382
 
2241
2383
  abstract expandFragments(updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
2242
2384
 
2243
- abstract trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): TOwnType | SelectionSet | undefined;
2385
+ abstract normalize(args: { parentType: CompositeType, recursive? : boolean }): TOwnType | SelectionSet | undefined;
2244
2386
 
2245
2387
  isFragmentSpread(): boolean {
2246
2388
  return false;
@@ -2258,26 +2400,52 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2258
2400
  return undefined;
2259
2401
  }
2260
2402
 
2403
+ intersectionWith(that: Selection): TOwnType | undefined {
2404
+ // If there is a subset, then we compute the intersection add that (if not empty).
2405
+ // Otherwise, the intersection is this element.
2406
+ if (this.selectionSet && that.selectionSet) {
2407
+ const subSelectionSetIntersection = this.selectionSet.intersectionWith(that.selectionSet);
2408
+ if (subSelectionSetIntersection.isEmpty()) {
2409
+ return undefined;
2410
+ } else {
2411
+ return this.withUpdatedSelectionSet(subSelectionSetIntersection);
2412
+ }
2413
+ } else {
2414
+ return this.us();
2415
+ }
2416
+ }
2417
+
2261
2418
  protected tryOptimizeSubselectionWithFragments({
2262
2419
  parentType,
2263
2420
  subSelection,
2264
2421
  fragments,
2422
+ validator,
2265
2423
  canUseFullMatchingFragment,
2266
2424
  }: {
2267
2425
  parentType: CompositeType,
2268
2426
  subSelection: SelectionSet,
2269
2427
  fragments: NamedFragments,
2428
+ validator: FieldsConflictValidator,
2270
2429
  canUseFullMatchingFragment: (match: NamedFragmentDefinition) => boolean,
2271
2430
  }): SelectionSet | NamedFragmentDefinition {
2272
- let candidates = fragments.maybeApplyingAtType(parentType);
2431
+ // We limit to fragments whose selection could be applied "directly" at `parentType`, meaning without taking the fragment condition
2432
+ // into account. The idea being that if the fragment condition would be needed inside `parentType`, then that condition will not
2433
+ // have been "normalized away" and so we want for this very call to be called on the fragment whose type _is_ the fragment condition (at
2434
+ // which point, this `maybeApplyingDirectlyAtType` method will apply.
2435
+ // Also note that this is because we have this restriction that calling `expandedSelectionSetAtType` is ok.
2436
+ let candidates = fragments.maybeApplyingDirectlyAtType(parentType);
2437
+ if (candidates.length === 0) {
2438
+ return subSelection;
2439
+ }
2273
2440
 
2274
2441
  // First, we check which of the candidates do apply inside `subSelection`, if any.
2275
2442
  // If we find a candidate that applies to the whole `subSelection`, then we stop and only return
2276
2443
  // that one candidate. Otherwise, we cumulate in `applyingFragments` the list of fragments that
2277
2444
  // applies to a subset of `subSelection`.
2278
- const applyingFragments: NamedFragmentDefinition[] = [];
2445
+ const applyingFragments: { fragment: NamedFragmentDefinition, atType: FragmentRestrictionAtType }[] = [];
2279
2446
  for (const candidate of candidates) {
2280
- const fragmentSSet = candidate.expandedSelectionSetAtType(parentType);
2447
+ const atType = candidate.expandedSelectionSetAtType(parentType);
2448
+ const selectionSetAtType = atType.selectionSet;
2281
2449
  // It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
2282
2450
  // `parentType` is empty, or contains only `__typename`. For instance, suppose we have
2283
2451
  // a union `U = A | B | C`, and then a fragment:
@@ -2296,24 +2464,28 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2296
2464
  //
2297
2465
  // Using `F` in those cases is, while not 100% incorrect, at least not productive, and so we
2298
2466
  // skip it that case. This is essentially an optimisation.
2299
- if (fragmentSSet.isEmpty() || (fragmentSSet.selections().length === 1 && fragmentSSet.selections()[0].isTypenameField())) {
2467
+ if (selectionSetAtType.isEmpty() || (selectionSetAtType.selections().length === 1 && selectionSetAtType.selections()[0].isTypenameField())) {
2300
2468
  continue;
2301
2469
  }
2302
2470
 
2303
- const res = subSelection.contains(fragmentSSet);
2471
+ const res = subSelection.contains(selectionSetAtType);
2304
2472
 
2305
2473
  if (res === ContainsResult.EQUAL) {
2306
2474
  if (canUseFullMatchingFragment(candidate)) {
2475
+ if (!validator.checkCanReuseFragmentAndTrackIt(atType)) {
2476
+ // We cannot use it at all, so no point in adding to `applyingFragments`.
2477
+ continue;
2478
+ }
2307
2479
  return candidate;
2308
2480
  }
2309
2481
  // If we're not going to replace the full thing, then same reasoning a below.
2310
2482
  if (candidate.appliedDirectives.length === 0) {
2311
- applyingFragments.push(candidate);
2483
+ applyingFragments.push({ fragment: candidate, atType});
2312
2484
  }
2313
2485
  // Note that if a fragment applies to only a subset of the subSelection, then we really only can use
2314
2486
  // it if that fragment is defined _without_ directives.
2315
2487
  } else if (res === ContainsResult.STRICTLY_CONTAINED && candidate.appliedDirectives.length === 0) {
2316
- applyingFragments.push(candidate);
2488
+ applyingFragments.push({ fragment: candidate, atType });
2317
2489
  }
2318
2490
  }
2319
2491
 
@@ -2322,7 +2494,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2322
2494
  }
2323
2495
 
2324
2496
  // 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
2497
+ // want to now produce the selection set with spread for those fragments plus any selection that is not
2326
2498
  // covered by any of the fragments. For instance, suppose that `subselection` is `{ a b c d e }`
2327
2499
  // and we have found that `fragment F1 on X { a b c }` and `fragment F2 on X { c d }` applies, then
2328
2500
  // we will generate `{ ...F1 ...F2 e }`.
@@ -2369,16 +2541,16 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2369
2541
  // return `{ ...F3 ...F4 }` in that case, but it would be technically better to return only `F4`.
2370
2542
  // However, this feels niche, and it might be costly to verify such inclusions, so not doing it
2371
2543
  // for now.
2372
- const filteredApplyingFragments = applyingFragments.filter((f) => !applyingFragments.some((o) => o.includes(f.name)));
2544
+ const filteredApplyingFragments = applyingFragments.filter(({ fragment }) => !applyingFragments.some((o) => o.fragment.includes(fragment.name)))
2373
2545
 
2374
2546
  let notCoveredByFragments = subSelection;
2375
2547
  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));
2548
+ for (const { fragment, atType} of filteredApplyingFragments) {
2549
+ if (!validator.checkCanReuseFragmentAndTrackIt(atType)) {
2550
+ continue;
2551
+ }
2552
+ const notCovered = subSelection.minus(atType.selectionSet);
2553
+ notCoveredByFragments = notCoveredByFragments.intersectionWith(notCovered);
2382
2554
  optimized.add(new FragmentSpreadSelection(parentType, fragments, fragment, []));
2383
2555
  }
2384
2556
 
@@ -2386,6 +2558,176 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2386
2558
  }
2387
2559
  }
2388
2560
 
2561
+ class FieldsConflictValidator {
2562
+ private usedSpreadTrimmedPartAtLevel?: FieldsConflictValidator[];
2563
+
2564
+ private constructor(
2565
+ private readonly byResponseName: Map<string, Map<Field, FieldsConflictValidator | null>>,
2566
+ ) {
2567
+ }
2568
+
2569
+ static build(s: SelectionSet): FieldsConflictValidator {
2570
+ return FieldsConflictValidator.forLevel(s.fieldsInSet());
2571
+ }
2572
+
2573
+ private static forLevel(level: CollectedFieldsInSet): FieldsConflictValidator {
2574
+ const atLevel = new Map<string, Map<Field, CollectedFieldsInSet | null>>();
2575
+
2576
+ for (const { field } of level) {
2577
+ const responseName = field.element.responseName();
2578
+ let atResponseName = atLevel.get(responseName);
2579
+ if (!atResponseName) {
2580
+ atResponseName = new Map<Field, CollectedFieldsInSet>();
2581
+ atLevel.set(responseName, atResponseName);
2582
+ }
2583
+ if (field.selectionSet) {
2584
+ let forField = atResponseName.get(field.element) ?? [];
2585
+ atResponseName.set(field.element, forField.concat(field.selectionSet.fieldsInSet()));
2586
+ } else {
2587
+ atResponseName.set(field.element, null);
2588
+ }
2589
+ }
2590
+
2591
+ const byResponseName = new Map<string, Map<Field, FieldsConflictValidator | null>>();
2592
+ for (const [name, level] of atLevel.entries()) {
2593
+ const atResponseName = new Map<Field, FieldsConflictValidator | null>();
2594
+ for (const [field, collectedFields] of level) {
2595
+ const validator = collectedFields ? FieldsConflictValidator.forLevel(collectedFields) : null;
2596
+ atResponseName.set(field, validator);
2597
+ }
2598
+ byResponseName.set(name, atResponseName);
2599
+ }
2600
+ return new FieldsConflictValidator(byResponseName);
2601
+ }
2602
+
2603
+ forField(field: Field): FieldsConflictValidator {
2604
+ const validator = this.byResponseName.get(field.responseName())?.get(field);
2605
+ // This should be called on validator built on the exact selection set from field this `field` is coming, so
2606
+ // we should find it or the code is buggy.
2607
+ assert(validator, () => `Should have found validator for ${field}`);
2608
+ return validator;
2609
+ }
2610
+
2611
+ // At this point, we known that the fragment, restricted to the current parent type, matches a subset of the
2612
+ // sub-selection. However, there is still one case we we cannot use it that we need to check, and this is
2613
+ // if using the fragment would create a field "conflict" (in the sense of the graphQL spec
2614
+ // [`FieldsInSetCanMerge`](https://spec.graphql.org/draft/#FieldsInSetCanMerge())) and thus create an
2615
+ // invalid selection. To be clear, `atType.selectionSet` cannot create a conflict, since it is a subset
2616
+ // of `subSelection` and `subSelection` is valid. *But* there may be some part of the fragment that
2617
+ // is not `atType.selectionSet` due to being "dead branches" for type `parentType`. And while those
2618
+ // branches _are_ "dead" as far as execution goes, the `FieldsInSetCanMerge` validation does not take
2619
+ // this into account (it's 1st step says "including visiting fragments and inline fragments" but has
2620
+ // no logic regarding ignoring any fragment that may not apply due to the intersection of runtimes
2621
+ // between multiple fragment being empty).
2622
+ checkCanReuseFragmentAndTrackIt(fragment: FragmentRestrictionAtType): boolean {
2623
+ // No validator means that everything in the fragment selection was part of the selection we're optimizing
2624
+ // away (by using the fragment), and we know the original selection was ok, so nothing to check.
2625
+ const validator = fragment.validator;
2626
+ if (!validator) {
2627
+ return true;
2628
+ }
2629
+
2630
+ if (!this.doMergeWith(validator)) {
2631
+ return false;
2632
+ }
2633
+
2634
+ // We need to make sure the trimmed parts of `fragment` merges with the rest of the selection,
2635
+ // but also that it merge with any of the trimmed parts of any fragment we have added already.
2636
+ // Note: this last condition means that if 2 fragment conflict on their "trimmed" parts,
2637
+ // then the choice of which is used can be based on the fragment ordering and selection order,
2638
+ // which may not be optimal. This feels niche enough that we keep it simple for now, but we
2639
+ // can revisit this decision if we run into real cases that justify it (but making it optimal
2640
+ // would be a involved in general, as in theory you could have complex dependencies of fragments
2641
+ // that conflict, even cycles, and you need to take the size of fragments into account to know
2642
+ // what's best; and even then, this could even depend on overall usage, as it can be better to
2643
+ // reuse a fragment that is used in other places, than to use one for which it's the only usage.
2644
+ // Adding to all that the fact that conflict can happen in sibling branches).
2645
+ if (this.usedSpreadTrimmedPartAtLevel) {
2646
+ if (!this.usedSpreadTrimmedPartAtLevel.every((t) => validator.doMergeWith(t))) {
2647
+ return false;
2648
+ }
2649
+ } else {
2650
+ this.usedSpreadTrimmedPartAtLevel = [];
2651
+ }
2652
+
2653
+ // We're good, but track the fragment
2654
+ this.usedSpreadTrimmedPartAtLevel.push(validator);
2655
+ return true;
2656
+ }
2657
+
2658
+ doMergeWith(that: FieldsConflictValidator): boolean {
2659
+ for (const [responseName, thisFields] of this.byResponseName.entries()) {
2660
+ const thatFields = that.byResponseName.get(responseName);
2661
+ if (!thatFields) {
2662
+ continue;
2663
+ }
2664
+
2665
+ // We're basically checking [FieldInSetCanMerge](https://spec.graphql.org/draft/#FieldsInSetCanMerge()),
2666
+ // but from 2 set of fields (`thisFields` and `thatFields`) of the same response that we know individually
2667
+ // merge already.
2668
+ for (const [thisField, thisValidator] of thisFields.entries()) {
2669
+ for (const [thatField, thatValidator] of thatFields.entries()) {
2670
+ // The `SameResponseShape` test that all fields must pass.
2671
+ if (!typesCanBeMerged(thisField.definition.type!, thatField.definition.type!)) {
2672
+ return false;
2673
+ }
2674
+
2675
+ const p1 = thisField.parentType;
2676
+ const p2 = thatField.parentType;
2677
+ if (sameType(p1, p2) || !isObjectType(p1) || !isObjectType(p2)) {
2678
+ // Additional checks of `FieldsInSetCanMerge` when same parent type or one isn't object
2679
+ if (thisField.name !== thatField.name
2680
+ || !argumentsEquals(thisField.args ?? {}, thatField.args ?? {})
2681
+ || (thisValidator && thatValidator && !thisValidator.doMergeWith(thatValidator))
2682
+ ) {
2683
+ return false;
2684
+ }
2685
+ } else {
2686
+ // Otherwise, the sub-selection must pass [SameResponseShape](https://spec.graphql.org/draft/#SameResponseShape()).
2687
+ if (thisValidator && thatValidator && !thisValidator.hasSameResponseShapeThan(thatValidator)) {
2688
+ return false;
2689
+ }
2690
+ }
2691
+ }
2692
+ }
2693
+ }
2694
+ return true;
2695
+ }
2696
+
2697
+ hasSameResponseShapeThan(that: FieldsConflictValidator): boolean {
2698
+ for (const [responseName, thisFields] of this.byResponseName.entries()) {
2699
+ const thatFields = that.byResponseName.get(responseName);
2700
+ if (!thatFields) {
2701
+ continue;
2702
+ }
2703
+
2704
+ for (const [thisField, thisValidator] of thisFields.entries()) {
2705
+ for (const [thatField, thatValidator] of thatFields.entries()) {
2706
+ if (!typesCanBeMerged(thisField.definition.type!, thatField.definition.type!)
2707
+ || (thisValidator && thatValidator && !thisValidator.hasSameResponseShapeThan(thatValidator))) {
2708
+ return false;
2709
+ }
2710
+ }
2711
+ }
2712
+ }
2713
+ return true;
2714
+ }
2715
+
2716
+ toString(indent: string = ''): string {
2717
+ // For debugging/testing ...
2718
+ return '{\n'
2719
+ + [...this.byResponseName.entries()].map(([name, byFields]) => {
2720
+ const innerIndent = indent + ' ';
2721
+ return `${innerIndent}${name}: [\n`
2722
+ + [...byFields.entries()]
2723
+ .map(([field, next]) => `${innerIndent} ${field.parentType}.${field}${next ? next.toString(innerIndent + ' '): ''}`)
2724
+ .join('\n')
2725
+ + `\n${innerIndent}]`;
2726
+ }).join('\n')
2727
+ + `\n${indent}}`
2728
+ }
2729
+ }
2730
+
2389
2731
  export class FieldSelection extends AbstractSelection<Field<any>, undefined, FieldSelection> {
2390
2732
  readonly kind = 'FieldSelection' as const;
2391
2733
 
@@ -2409,6 +2751,9 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2409
2751
  }
2410
2752
 
2411
2753
  withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
2754
+ if (this.element === field && this.selectionSet === selectionSet) {
2755
+ return this;
2756
+ }
2412
2757
  return new FieldSelection(field, selectionSet);
2413
2758
  }
2414
2759
 
@@ -2416,12 +2761,14 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2416
2761
  return this.element.key();
2417
2762
  }
2418
2763
 
2419
- optimize(fragments: NamedFragments): Selection {
2764
+ optimize(fragments: NamedFragments, validator: FieldsConflictValidator): Selection {
2420
2765
  const fieldBaseType = baseType(this.element.definition.type!);
2421
2766
  if (!isCompositeType(fieldBaseType) || !this.selectionSet) {
2422
2767
  return this;
2423
2768
  }
2424
2769
 
2770
+ const fieldValidator = validator.forField(this.element);
2771
+
2425
2772
  // First, see if we can reuse fragments for the selection of this field.
2426
2773
  let optimizedSelection = this.selectionSet;
2427
2774
  if (isCompositeType(fieldBaseType) && this.selectionSet) {
@@ -2429,6 +2776,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2429
2776
  parentType: fieldBaseType,
2430
2777
  subSelection: this.selectionSet,
2431
2778
  fragments,
2779
+ validator: fieldValidator,
2432
2780
  // We can never apply a fragments that has directives on it at the field level.
2433
2781
  canUseFullMatchingFragment: (fragment) => fragment.appliedDirectives.length === 0,
2434
2782
  });
@@ -2442,19 +2790,19 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2442
2790
 
2443
2791
  // Then, recurse inside the field sub-selection (note that if we matched some fragments above,
2444
2792
  // this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
2445
- optimizedSelection = optimizedSelection.optimize(fragments);
2793
+ optimizedSelection = optimizedSelection.optimizeSelections(fragments, fieldValidator);
2446
2794
 
2447
2795
  return this.selectionSet === optimizedSelection
2448
2796
  ? this
2449
2797
  : this.withUpdatedSelectionSet(optimizedSelection);
2450
2798
  }
2451
2799
 
2452
- filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
2800
+ filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
2453
2801
  if (!this.selectionSet) {
2454
2802
  return predicate(this) ? this : undefined;
2455
2803
  }
2456
2804
 
2457
- const updatedSelectionSet = this.selectionSet.filter(predicate);
2805
+ const updatedSelectionSet = this.selectionSet.filterRecursiveDepthFirst(predicate);
2458
2806
  const thisWithFilteredSelectionSet = this.selectionSet === updatedSelectionSet
2459
2807
  ? this
2460
2808
  : new FieldSelection(this.element, updatedSelectionSet);
@@ -2547,28 +2895,41 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2547
2895
  return !!this.selectionSet?.hasDefer();
2548
2896
  }
2549
2897
 
2550
- trimUnsatisfiableBranches(_: CompositeType, options?: { recursive? : boolean }): FieldSelection {
2898
+ normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FieldSelection {
2899
+ // This could be an interface field, and if we're normalizing on one of the implementation of that
2900
+ // interface, we want to make sure we use the field of the implementation, as it may in particular
2901
+ // have a more specific type which should propagate to the recursive call to normalize.
2902
+
2903
+ const definition = parentType === this.parentType
2904
+ ? this.element.definition
2905
+ : parentType.field(this.element.name);
2906
+ assert(definition, `Cannot normalize ${this.element} at ${parentType} which does not have that field`)
2907
+
2908
+ const element = this.element.definition === definition ? this.element : this.element.withUpdatedDefinition(definition);
2551
2909
  if (!this.selectionSet) {
2552
- return this;
2910
+ return this.withUpdatedElement(element);
2553
2911
  }
2554
2912
 
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;
2913
+ const base = element.baseType();
2914
+ assert(isCompositeType(base), () => `Field ${element} should not have a sub-selection`);
2915
+ const normalizedSubSelection = (recursive ?? true) ? this.selectionSet.normalize({ parentType: base }) : this.selectionSet;
2558
2916
  // In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
2559
2917
  // sub-selection is empty. Which suggest something may be wrong with this part of the query
2560
2918
  // intent, but the query was valid while keeping an empty sub-selection isn't. So in that
2561
2919
  // 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 })],
2920
+ if (normalizedSubSelection?.isEmpty()) {
2921
+ return this.withUpdatedComponents(
2922
+ element,
2923
+ selectionSetOfElement(
2924
+ new Field(
2925
+ base.typenameField()!,
2926
+ undefined,
2927
+ [new Directive('include', { 'if': false })],
2928
+ )
2568
2929
  )
2569
- ));
2930
+ );
2570
2931
  } else {
2571
- return trimmed;
2932
+ return this.withUpdatedComponents(element, normalizedSubSelection);
2572
2933
  }
2573
2934
  }
2574
2935
 
@@ -2628,11 +2989,10 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
2628
2989
  }
2629
2990
  }
2630
2991
 
2631
- filter(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
2992
+ filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
2632
2993
  // 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
2994
+ const updatedSelectionSet = this.selectionSet.filterRecursiveDepthFirst(predicate);
2995
+ const thisWithFilteredSelectionSet = updatedSelectionSet === this.selectionSet
2636
2996
  ? this
2637
2997
  : new InlineFragmentSelection(this.element, updatedSelectionSet);
2638
2998
 
@@ -2665,6 +3025,9 @@ class InlineFragmentSelection extends FragmentSelection {
2665
3025
  }
2666
3026
 
2667
3027
  withUpdatedComponents(fragment: FragmentElement, selectionSet: SelectionSet): InlineFragmentSelection {
3028
+ if (fragment === this.element && selectionSet === this.selectionSet) {
3029
+ return this;
3030
+ }
2668
3031
  return new InlineFragmentSelection(fragment, selectionSet);
2669
3032
  }
2670
3033
 
@@ -2727,7 +3090,7 @@ class InlineFragmentSelection extends FragmentSelection {
2727
3090
  };
2728
3091
  }
2729
3092
 
2730
- optimize(fragments: NamedFragments): FragmentSelection {
3093
+ optimize(fragments: NamedFragments, validator: FieldsConflictValidator): FragmentSelection {
2731
3094
  let optimizedSelection = this.selectionSet;
2732
3095
 
2733
3096
  // First, see if we can reuse fragments for the selection of this field.
@@ -2737,12 +3100,13 @@ class InlineFragmentSelection extends FragmentSelection {
2737
3100
  parentType: typeCondition,
2738
3101
  subSelection: optimizedSelection,
2739
3102
  fragments,
3103
+ validator,
2740
3104
  canUseFullMatchingFragment: (fragment) => {
2741
3105
  // To be able to use a matching fragment, it needs to have either no directives, or if it has
2742
3106
  // some, then:
2743
3107
  // 1. all it's directives should also be on the current element.
2744
3108
  // 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
3109
+ // because if those 2 conditions are true, we can replace the whole current inline fragment
2746
3110
  // with the match spread and directives will still match.
2747
3111
  return fragment.appliedDirectives.length === 0
2748
3112
  || (
@@ -2777,7 +3141,7 @@ class InlineFragmentSelection extends FragmentSelection {
2777
3141
 
2778
3142
  // Then, recurse inside the field sub-selection (note that if we matched some fragments above,
2779
3143
  // this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
2780
- optimizedSelection = optimizedSelection.optimizeSelections(fragments);
3144
+ optimizedSelection = optimizedSelection.optimizeSelections(fragments, validator);
2781
3145
 
2782
3146
  return this.selectionSet === optimizedSelection
2783
3147
  ? this
@@ -2809,55 +3173,60 @@ class InlineFragmentSelection extends FragmentSelection {
2809
3173
  : this.withUpdatedComponents(newElement, newSelection);
2810
3174
  }
2811
3175
 
2812
- trimUnsatisfiableBranches(currentType: CompositeType, options?: { recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
2813
- const recursive = options?.recursive ?? true;
2814
-
3176
+ normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
2815
3177
  const thisCondition = this.element.typeCondition;
2816
- // Note that if the condition has directives, we preserve the fragment no matter what.
2817
- 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;
2821
- }
2822
3178
 
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)) {
2832
- return undefined;
2833
- } else {
2834
- const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType, options);
2835
- return trimmed.isEmpty() ? undefined : trimmed;
2836
- }
3179
+ // This method assumes by contract that `parentType` runtimes intersects `this.parentType`'s, but `parentType`
3180
+ // runtimes may be a subset. So first check if the selection should not be discarded on that account (that
3181
+ // is, we should not keep the selection if its condition runtimes don't intersect at all with those of
3182
+ // `parentType` as that would ultimately make an invalid selection set).
3183
+ if (thisCondition && parentType !== this.parentType) {
3184
+ const conditionRuntimes = possibleRuntimeTypes(thisCondition);
3185
+ const typeRuntimes = possibleRuntimeTypes(parentType);
3186
+ if (!conditionRuntimes.some((t) => typeRuntimes.includes(t))) {
3187
+ return undefined;
2837
3188
  }
2838
3189
  }
2839
3190
 
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;
3191
+ // We know the condition is "valid", but it may not be useful. That said, if the condition has directives,
3192
+ // we preserve the fragment no matter what.
3193
+ if (this.element.appliedDirectives.length === 0) {
3194
+ // There is a number of cases where a fragment is not useful:
3195
+ // 1. if there is not conditions (remember it also has no directives).
3196
+ // 2. if it's the same type as the current type: it's not restricting types further.
3197
+ // 3. if the current type is an object more generally: because in that case too the condition
3198
+ // cannot be restricting things further (it's typically a less precise interface/union).
3199
+ if (!thisCondition || parentType === this.element.typeCondition || isObjectType(parentType)) {
3200
+ const normalized = this.selectionSet.normalize({ parentType, recursive });
3201
+ return normalized.isEmpty() ? undefined : normalized;
3202
+ }
2843
3203
  }
2844
3204
 
2845
- // In all other cases, we first recurse on the sub-selection.
2846
- const trimmedSelectionSet = this.selectionSet.trimUnsatisfiableBranches(this.element.typeCondition ?? this.parentType);
3205
+ // We preserve the current fragment, so we only recurse within the sub-selection if we're asked to be recusive.
3206
+ // (note that even if we're not recursive, we may still have some "lifting" to do)
3207
+ let normalizedSelectionSet: SelectionSet;
3208
+ if (recursive ?? true) {
3209
+ normalizedSelectionSet = this.selectionSet.normalize({ parentType: thisCondition ?? parentType });
2847
3210
 
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
- ));
3211
+ // It could be that everything was unsatisfiable.
3212
+ if (normalizedSelectionSet.isEmpty()) {
3213
+ if (this.element.appliedDirectives.length === 0) {
3214
+ return undefined;
3215
+ } else {
3216
+ return this.withUpdatedComponents(
3217
+ this.element.rebaseOn(parentType),
3218
+ selectionSetOfElement(
3219
+ new Field(
3220
+ (this.element.typeCondition ?? parentType).typenameField()!,
3221
+ undefined,
3222
+ [new Directive('include', { 'if': false })],
3223
+ )
3224
+ )
3225
+ );
3226
+ }
2860
3227
  }
3228
+ } else {
3229
+ normalizedSelectionSet = this.selectionSet;
2861
3230
  }
2862
3231
 
2863
3232
  // Second, we check if some of the sub-selection fragments can be "lifted" outside of this fragment. This can happen if:
@@ -2865,10 +3234,10 @@ class InlineFragmentSelection extends FragmentSelection {
2865
3234
  // 2. the sub-fragment is an object type,
2866
3235
  // 3. the sub-fragment type is a valid runtime of the current type.
2867
3236
  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);
3237
+ assert(!isObjectType(parentType), () => `Should not have got here if ${parentType} is an object type`);
3238
+ const currentRuntimes = possibleRuntimeTypes(parentType);
2870
3239
  const liftableSelections: Selection[] = [];
2871
- for (const selection of trimmedSelectionSet.selections()) {
3240
+ for (const selection of normalizedSelectionSet.selections()) {
2872
3241
  if (selection.kind === 'FragmentSelection'
2873
3242
  && selection.element.typeCondition
2874
3243
  && isObjectType(selection.element.typeCondition)
@@ -2879,8 +3248,8 @@ class InlineFragmentSelection extends FragmentSelection {
2879
3248
  }
2880
3249
 
2881
3250
  // 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;
3251
+ if (liftableSelections.length === normalizedSelectionSet.selections().length) {
3252
+ return normalizedSelectionSet;
2884
3253
  }
2885
3254
 
2886
3255
  // Otherwise, if there is "liftable" selections, we must return a set comprised of those lifted selection,
@@ -2889,13 +3258,15 @@ class InlineFragmentSelection extends FragmentSelection {
2889
3258
  const newSet = new SelectionSetUpdates();
2890
3259
  newSet.add(liftableSelections);
2891
3260
  newSet.add(this.withUpdatedSelectionSet(
2892
- trimmedSelectionSet.filter((s) => !liftableSelections.includes(s)),
3261
+ normalizedSelectionSet.filter((s) => !liftableSelections.includes(s)),
2893
3262
  ));
2894
- return newSet.toSelectionSet(this.parentType);
3263
+ return newSet.toSelectionSet(parentType);
2895
3264
  }
2896
3265
  }
2897
3266
 
2898
- return this.selectionSet === trimmedSelectionSet ? this : this.withUpdatedSelectionSet(trimmedSelectionSet);
3267
+ return this.parentType === parentType && this.selectionSet === normalizedSelectionSet
3268
+ ? this
3269
+ : this.withUpdatedComponents(this.element.rebaseOn(parentType), normalizedSelectionSet);
2899
3270
  }
2900
3271
 
2901
3272
  expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection {
@@ -2956,17 +3327,20 @@ class FragmentSpreadSelection extends FragmentSelection {
2956
3327
  assert(false, `Unsupported`);
2957
3328
  }
2958
3329
 
2959
- trimUnsatisfiableBranches(parentType: CompositeType): FragmentSelection {
3330
+ normalize({ parentType }: { parentType: CompositeType }): FragmentSelection {
2960
3331
  // We must update the spread parent type if necessary since we're not going deeper,
2961
3332
  // 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');
3333
+ assert(parentType.schema() === this.parentType.schema(), 'Should not try to normalize using a type from another schema');
2963
3334
  return this.rebaseOn(parentType, this.fragments);
2964
3335
  }
2965
3336
 
2966
3337
  validate(): void {
2967
3338
  this.validateDeferAndStream();
2968
3339
 
2969
- // We don't do anything else because fragment definition are validated when created.
3340
+ validate(
3341
+ runtimeTypesIntersects(this.parentType, this.namedFragment.typeCondition),
3342
+ () => `Fragment "${this.namedFragment.name}" cannot be spread inside type ${this.parentType} as the runtime types do not intersect ${this.namedFragment.typeCondition}`
3343
+ );
2970
3344
  }
2971
3345
 
2972
3346
  toSelectionNode(): FragmentSpreadNode {
@@ -2989,7 +3363,7 @@ class FragmentSpreadSelection extends FragmentSelection {
2989
3363
  };
2990
3364
  }
2991
3365
 
2992
- optimize(_: NamedFragments): FragmentSelection {
3366
+ optimize(_1: NamedFragments, _2: FieldsConflictValidator): FragmentSelection {
2993
3367
  return this;
2994
3368
  }
2995
3369