@apollo/federation-internals 2.4.0 → 2.4.1

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
@@ -25,7 +25,6 @@ import {
25
25
  isCompositeType,
26
26
  isInterfaceType,
27
27
  isNullableType,
28
- isUnionType,
29
28
  ObjectType,
30
29
  runtimeTypesIntersects,
31
30
  Schema,
@@ -48,9 +47,10 @@ import {
48
47
  Variables,
49
48
  isObjectType,
50
49
  } from "./definitions";
50
+ import { isInterfaceObjectType } from "./federation";
51
51
  import { ERRORS } from "./error";
52
- import { isDirectSubtype, sameType } from "./types";
53
- import { assert, mapEntries, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
52
+ import { sameType } from "./types";
53
+ import { assert, isDefined, mapEntries, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
54
54
  import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
55
55
  import { v1 as uuidv1 } from 'uuid';
56
56
 
@@ -304,16 +304,18 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
304
304
  }
305
305
 
306
306
  private canRebaseOn(parentType: CompositeType) {
307
+ const fieldParentType = this.definition.parent
307
308
  // There is 2 valid cases we want to allow:
308
309
  // 1. either `selectionParent` and `fieldParent` are the same underlying type (same name) but from different underlying schema. Typically,
309
310
  // happens when we're building subgraph queries but using selections from the original query which is against the supergraph API schema.
310
- // 2. or they are not the same underlying type, and we only accept this if we're adding an interface field to a selection of one of its
311
- // subtype, and this for convenience. Note that in that case too, `selectinParent` and `fieldParent` may or may be from the same exact
312
- // underlying schema, and so we avoid relying on `isDirectSubtype` in the check.
313
- // In both cases, we just get the field from `selectionParent`, ensuring the return field parent _is_ `selectionParent`.
314
- const fieldParentType = this.definition.parent
311
+ // 2. or they are not the same underlying type, but the field parent type is from an interface (or an interface object, which is the same
312
+ // here), in which case we may be rebasing an interface field on one of the implementation type, which is ok. Note that we don't verify
313
+ // that `parentType` is indeed an implementation of `fieldParentType` because it's possible that this implementation relationship exists
314
+ // in the supergraph, but not in any of the subgraph schema involved here. So we just let it be. Not that `rebaseOn` will complain anyway
315
+ // if the field name simply does not exists in `parentType`.
315
316
  return parentType.name === fieldParentType.name
316
- || (isInterfaceType(fieldParentType) && fieldParentType.allImplementations().some(i => i.name === parentType.name));
317
+ || isInterfaceType(fieldParentType)
318
+ || isInterfaceObjectType(fieldParentType);
317
319
  }
318
320
 
319
321
  typeIfAddedTo(parentType: CompositeType): Type | undefined {
@@ -458,7 +460,7 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
458
460
  return newFragment;
459
461
  }
460
462
 
461
- rebaseOn(parentType: CompositeType): FragmentElement{
463
+ rebaseOn(parentType: CompositeType): FragmentElement {
462
464
  const fragmentParent = this.parentType;
463
465
  const typeCondition = this.typeCondition;
464
466
  if (parentType === fragmentParent) {
@@ -886,15 +888,13 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
886
888
  }
887
889
 
888
890
  /**
889
- * Whether this fragment may apply at the provided type, that is if its type condition matches the type
890
- * or is a supertype of it.
891
+ * Whether this fragment may apply at the provided type, that is if its type condition runtime types intersects with the
892
+ * runtimes of the provided type.
891
893
  *
892
894
  * @param type - the type at which we're looking at applying the fragment
893
895
  */
894
896
  canApplyAtType(type: CompositeType): boolean {
895
- const applyAtType =
896
- sameType(this.typeCondition, type)
897
- || (isAbstractType(this.typeCondition) && !isUnionType(type) && isDirectSubtype(this.typeCondition, type));
897
+ const applyAtType = sameType(type, this.typeCondition) || runtimeTypesIntersects(type, this.typeCondition);
898
898
  return applyAtType
899
899
  && this.validForSchema(type.schema());
900
900
  }
@@ -1271,20 +1271,69 @@ export class SelectionSet {
1271
1271
  return true;
1272
1272
  }
1273
1273
 
1274
- contains(that: SelectionSet): boolean {
1275
- if (this._selections.length < that._selections.length) {
1276
- return false;
1274
+ private triviallyNestedSelectionsForKey(parentType: CompositeType, key: string): Selection[] {
1275
+ const found: Selection[] = [];
1276
+ for (const selection of this.selections()) {
1277
+ if (selection.isUnecessaryInlineFragment(parentType)) {
1278
+ const selectionForKey = selection.selectionSet._keyedSelections.get(key);
1279
+ if (selectionForKey) {
1280
+ found.push(selectionForKey);
1281
+ }
1282
+ for (const nestedSelection of selection.selectionSet.triviallyNestedSelectionsForKey(parentType, key)) {
1283
+ found.push(nestedSelection);
1284
+ }
1285
+ }
1277
1286
  }
1287
+ return found;
1288
+ }
1289
+
1290
+ private mergeSameKeySelections(selections: Selection[]): Selection | undefined {
1291
+ if (selections.length === 0) {
1292
+ return undefined;
1293
+ }
1294
+ const first = selections[0];
1295
+ // We know that all the selections passed are for exactly the same element (same "key"). So if it is a
1296
+ // leaf field or a named fragment, then we know that even if we have more than 1 selection, all of them
1297
+ // are the exact same and we can just return the first one. Only if we have a composite field or an
1298
+ // inline fragment do we need to merge the underlying sub-selection (which may differ).
1299
+ if (!first.selectionSet || (first instanceof FragmentSpreadSelection) || selections.length === 1) {
1300
+ return first;
1301
+ }
1302
+ const mergedSubselections = new SelectionSetUpdates();
1303
+ for (const selection of selections) {
1304
+ mergedSubselections.add(selection.selectionSet!);
1305
+ }
1306
+ return first.withUpdatedSelectionSet(mergedSubselections.toSelectionSet(first.selectionSet.parentType));
1307
+ }
1308
+
1309
+ contains(that: SelectionSet): boolean {
1310
+ // Note that we cannot really rely on the number of selections in `this` and `that` to short-cut this method
1311
+ // due to the handling of "trivially nested selections". That is, `this` might have less top-level selections
1312
+ // than `that`, and yet contains a named fragment directly on the parent type that includes everything in `that`.
1278
1313
 
1279
1314
  for (const [key, thatSelection] of that._keyedSelections) {
1280
1315
  const thisSelection = this._keyedSelections.get(key);
1281
- if (!thisSelection || !thisSelection.contains(thatSelection)) {
1316
+ const otherSelections = this.triviallyNestedSelectionsForKey(this.parentType, key);
1317
+ const mergedSelection = this.mergeSameKeySelections([thisSelection].concat(otherSelections).filter(isDefined));
1318
+
1319
+ if (!(mergedSelection && mergedSelection.contains(thatSelection))
1320
+ && !(thatSelection.isUnecessaryInlineFragment(this.parentType) && this.contains(thatSelection.selectionSet))
1321
+ ) {
1282
1322
  return false
1283
1323
  }
1284
1324
  }
1285
1325
  return true;
1286
1326
  }
1287
1327
 
1328
+ diffIfContains(that: SelectionSet): { contains: boolean, diff?: SelectionSet } {
1329
+ if (this.contains(that)) {
1330
+ const diff = this.minus(that);
1331
+ return { contains: true, diff: diff.isEmpty() ? undefined : diff };
1332
+ }
1333
+
1334
+ return { contains: false };
1335
+ }
1336
+
1288
1337
  /**
1289
1338
  * Returns a selection set that correspond to this selection set but where any of the selections in the
1290
1339
  * provided selection set have been remove.
@@ -1294,16 +1343,14 @@ export class SelectionSet {
1294
1343
 
1295
1344
  for (const [key, thisSelection] of this._keyedSelections) {
1296
1345
  const thatSelection = that._keyedSelections.get(key);
1297
- if (!thatSelection) {
1346
+ const otherSelections = that.triviallyNestedSelectionsForKey(this.parentType, key);
1347
+ const allSelections = thatSelection ? [thatSelection].concat(otherSelections) : otherSelections;
1348
+ if (allSelections.length === 0) {
1298
1349
  updated.add(thisSelection);
1299
1350
  } else {
1300
- // If there is a subset, then we compute the diff of the subset and add that (if not empty).
1301
- // Otherwise, we just skip `thisSelection` and do nothing
1302
- if (thisSelection.selectionSet && thatSelection.selectionSet) {
1303
- const updatedSubSelectionSet = thisSelection.selectionSet.minus(thatSelection.selectionSet);
1304
- if (!updatedSubSelectionSet.isEmpty()) {
1305
- updated.add(thisSelection.withUpdatedSelectionSet(updatedSubSelectionSet));
1306
- }
1351
+ const selectionDiff = allSelections.reduce<Selection | undefined>((prev, val) => prev?.minus(val), thisSelection);
1352
+ if (selectionDiff) {
1353
+ updated.add(selectionDiff);
1307
1354
  }
1308
1355
  }
1309
1356
  }
@@ -1765,7 +1812,6 @@ export function selectionOfElement(element: OperationElement, subSelection?: Sel
1765
1812
  }
1766
1813
 
1767
1814
  export type Selection = FieldSelection | FragmentSelection;
1768
-
1769
1815
  abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf extends undefined | never, TOwnType extends AbstractSelection<TElement, TIsLeaf, TOwnType>> {
1770
1816
  constructor(
1771
1817
  readonly element: TElement,
@@ -1836,6 +1882,63 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
1836
1882
  abstract expandFragments(names: string[], updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
1837
1883
 
1838
1884
  abstract trimUnsatisfiableBranches(parentType: CompositeType): TOwnType | SelectionSet | undefined;
1885
+
1886
+ minus(that: Selection): TOwnType | undefined {
1887
+ // If there is a subset, then we compute the diff of the subset and add that (if not empty).
1888
+ // Otherwise, we have no diff.
1889
+ if (this.selectionSet && that.selectionSet) {
1890
+ const updatedSubSelectionSet = this.selectionSet.minus(that.selectionSet);
1891
+ if (!updatedSubSelectionSet.isEmpty()) {
1892
+ return this.withUpdatedSelectionSet(updatedSubSelectionSet);
1893
+ }
1894
+ }
1895
+ return undefined;
1896
+ }
1897
+
1898
+ protected tryOptimizeSubselectionOnce(_: {
1899
+ parentType: CompositeType,
1900
+ subSelection: SelectionSet,
1901
+ candidates: NamedFragmentDefinition[],
1902
+ fragments: NamedFragments,
1903
+ }): {
1904
+ spread?: FragmentSpreadSelection,
1905
+ optimizedSelection?: SelectionSet,
1906
+ hasDiff?: boolean,
1907
+ } {
1908
+ // Field and inline fragment override this, but this should never be called for a spread.
1909
+ assert(false, `UNSUPPORTED`);
1910
+ }
1911
+
1912
+ protected tryOptimizeSubselectionWithFragments({
1913
+ parentType,
1914
+ subSelection,
1915
+ fragments,
1916
+ fragmentFilter,
1917
+ }: {
1918
+ parentType: CompositeType,
1919
+ subSelection: SelectionSet,
1920
+ fragments: NamedFragments,
1921
+ fragmentFilter?: (f: NamedFragmentDefinition) => boolean,
1922
+ }): SelectionSet | FragmentSpreadSelection {
1923
+ let candidates = fragments.maybeApplyingAtType(parentType);
1924
+ if (fragmentFilter) {
1925
+ candidates = candidates.filter(fragmentFilter);
1926
+ }
1927
+ let shouldTryAgain: boolean;
1928
+ do {
1929
+ const { spread, optimizedSelection, hasDiff } = this.tryOptimizeSubselectionOnce({ parentType, subSelection, candidates, fragments });
1930
+ if (optimizedSelection) {
1931
+ subSelection = optimizedSelection;
1932
+ } else if (spread) {
1933
+ return spread;
1934
+ }
1935
+ shouldTryAgain = !!spread && !!hasDiff;
1936
+ if (shouldTryAgain) {
1937
+ candidates = candidates.filter((c) => c !== spread?.namedFragment)
1938
+ }
1939
+ } while (shouldTryAgain);
1940
+ return subSelection;
1941
+ }
1839
1942
  }
1840
1943
 
1841
1944
  export class FieldSelection extends AbstractSelection<Field<any>, undefined, FieldSelection> {
@@ -1865,41 +1968,21 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
1865
1968
  }
1866
1969
 
1867
1970
  optimize(fragments: NamedFragments): Selection {
1868
- const optimizedSelection = this.selectionSet ? this.selectionSet.optimize(fragments) : undefined;
1971
+ let optimizedSelection = this.selectionSet ? this.selectionSet.optimize(fragments) : undefined;
1869
1972
  const fieldBaseType = baseType(this.element.definition.type!);
1870
1973
  if (isCompositeType(fieldBaseType) && optimizedSelection) {
1871
- for (const candidate of fragments.maybeApplyingAtType(fieldBaseType)) {
1872
- // TODO: Checking `equals` here is very simple, but somewhat restrictive in theory. That is, if a query
1873
- // is:
1874
- // {
1875
- // t {
1876
- // a
1877
- // b
1878
- // c
1879
- // }
1880
- // }
1881
- // and we have:
1882
- // fragment X on T {
1883
- // t {
1884
- // a
1885
- // b
1886
- // }
1887
- // }
1888
- // then the current code will not use the fragment because `c` is not in the fragment, but in relatity,
1889
- // we could use it and make the result be:
1890
- // {
1891
- // ...X
1892
- // t {
1893
- // c
1894
- // }
1895
- // }
1896
- // To do that, we can change that `equals` to `contains`, but then we should also "extract" the remainder
1897
- // of `optimizedSelection` that isn't covered by the fragment, and that is the part slighly more involved.
1898
- if (optimizedSelection.equals(candidate.selectionSet)) {
1899
- const fragmentSelection = new FragmentSpreadSelection(fieldBaseType, fragments, candidate, []);
1900
- return new FieldSelection(this.element, selectionSetOf(fieldBaseType, fragmentSelection));
1901
- }
1902
- }
1974
+ const optimized = this.tryOptimizeSubselectionWithFragments({
1975
+ parentType: fieldBaseType,
1976
+ subSelection: optimizedSelection,
1977
+ fragments,
1978
+ // We can never apply a fragments that has directives on it at the field level (but when those are expanded,
1979
+ // their type condition would always be preserved due to said applied directives, so they will always
1980
+ // be handled by `InlineFragmentSelection.optimize` anyway).
1981
+ fragmentFilter: (f) => f.appliedDirectives.length === 0,
1982
+ });
1983
+
1984
+ assert(!(optimized instanceof FragmentSpreadSelection), 'tryOptimizeSubselectionOnce should never return only a spread');
1985
+ optimizedSelection = optimized;
1903
1986
  }
1904
1987
 
1905
1988
  return this.selectionSet === optimizedSelection
@@ -1907,6 +1990,37 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
1907
1990
  : new FieldSelection(this.element, optimizedSelection);
1908
1991
  }
1909
1992
 
1993
+ protected tryOptimizeSubselectionOnce({
1994
+ parentType,
1995
+ subSelection,
1996
+ candidates,
1997
+ fragments,
1998
+ }: {
1999
+ parentType: CompositeType,
2000
+ subSelection: SelectionSet,
2001
+ candidates: NamedFragmentDefinition[],
2002
+ fragments: NamedFragments,
2003
+ }): {
2004
+ spread?: FragmentSpreadSelection,
2005
+ optimizedSelection?: SelectionSet,
2006
+ hasDiff?: boolean,
2007
+ }{
2008
+ let optimizedSelection = subSelection;
2009
+ for (const candidate of candidates) {
2010
+ const { contains, diff } = optimizedSelection.diffIfContains(candidate.selectionSet);
2011
+ if (contains) {
2012
+ // We can optimize the selection with this fragment. The replaced sub-selection will be
2013
+ // comprised of this new spread and the remaining `diff` if there is any.
2014
+ const spread = new FragmentSpreadSelection(parentType, fragments, candidate, []);
2015
+ optimizedSelection = diff
2016
+ ? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
2017
+ : selectionSetOf(parentType, spread);
2018
+ return { spread, optimizedSelection, hasDiff: !!diff }
2019
+ }
2020
+ }
2021
+ return {};
2022
+ }
2023
+
1910
2024
  filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
1911
2025
  if (!this.selectionSet) {
1912
2026
  return predicate(this) ? this : undefined;
@@ -2063,6 +2177,11 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2063
2177
  return !!this.selectionSet && this.selectionSet.contains(that.selectionSet);
2064
2178
  }
2065
2179
 
2180
+ isUnecessaryInlineFragment(_: CompositeType): this is InlineFragmentSelection {
2181
+ // Overridden by inline fragments
2182
+ return false;
2183
+ }
2184
+
2066
2185
  toString(expandFragments: boolean = true, indent?: string): string {
2067
2186
  return (indent ?? '') + this.element + (this.selectionSet ? ' ' + this.selectionSet.toString(expandFragments, true, indent) : '');
2068
2187
  }
@@ -2103,20 +2222,19 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
2103
2222
  return this.element.hasDefer() || this.selectionSet.hasDefer();
2104
2223
  }
2105
2224
 
2106
- equals(that: Selection): boolean {
2107
- if (this === that) {
2108
- return true;
2109
- }
2110
- return (that instanceof FragmentSelection)
2111
- && this.element.equals(that.element)
2112
- && this.selectionSet.equals(that.selectionSet);
2113
- }
2225
+ abstract equals(that: Selection): boolean;
2114
2226
 
2115
- contains(that: Selection): boolean {
2116
- return (that instanceof FragmentSelection)
2117
- && this.element.equals(that.element)
2118
- && this.selectionSet.contains(that.selectionSet);
2227
+ abstract contains(that: Selection): boolean;
2228
+
2229
+ isUnecessaryInlineFragment(parentType: CompositeType): boolean {
2230
+ return this.element.appliedDirectives.length === 0
2231
+ && !!this.element.typeCondition
2232
+ && (
2233
+ this.element.typeCondition.name === parentType.name
2234
+ || (isObjectType(parentType) && possibleRuntimeTypes(this.element.typeCondition).some((t) => t.name === parentType.name))
2235
+ );
2119
2236
  }
2237
+
2120
2238
  }
2121
2239
 
2122
2240
  class InlineFragmentSelection extends FragmentSelection {
@@ -2202,37 +2320,83 @@ class InlineFragmentSelection extends FragmentSelection {
2202
2320
  let optimizedSelection = this.selectionSet.optimize(fragments);
2203
2321
  const typeCondition = this.element.typeCondition;
2204
2322
  if (typeCondition) {
2205
- for (const candidate of fragments.maybeApplyingAtType(typeCondition)) {
2206
- // See comment in `FieldSelection.optimize` about the `equals`: this fully apply here too.
2207
- if (optimizedSelection.equals(candidate.selectionSet)) {
2208
- let spreadDirectives: Directive[] = [];
2209
- if (this.element.appliedDirectives) {
2323
+ const optimized = this.tryOptimizeSubselectionWithFragments({
2324
+ parentType: typeCondition,
2325
+ subSelection: optimizedSelection,
2326
+ fragments,
2327
+ });
2328
+ if (optimized instanceof FragmentSpreadSelection) {
2329
+ // This means the whole inline fragment can be replaced by the spread.
2330
+ return optimized;
2331
+ }
2332
+ optimizedSelection = optimized;
2333
+ }
2334
+ return this.selectionSet === optimizedSelection
2335
+ ? this
2336
+ : new InlineFragmentSelection(this.element, optimizedSelection);
2337
+ }
2338
+
2339
+ protected tryOptimizeSubselectionOnce({
2340
+ parentType,
2341
+ subSelection,
2342
+ candidates,
2343
+ fragments,
2344
+ }: {
2345
+ parentType: CompositeType,
2346
+ subSelection: SelectionSet,
2347
+ candidates: NamedFragmentDefinition[],
2348
+ fragments: NamedFragments,
2349
+ }): {
2350
+ spread?: FragmentSpreadSelection,
2351
+ optimizedSelection?: SelectionSet,
2352
+ hasDiff?: boolean,
2353
+ }{
2354
+ let optimizedSelection = subSelection;
2355
+ for (const candidate of candidates) {
2356
+ const { contains, diff } = optimizedSelection.diffIfContains(candidate.selectionSet);
2357
+ if (contains) {
2358
+ // The candidate selection is included in our sub-selection. One remaining thing to take into account
2359
+ // is applied directives: if the candidate has directives, then we can only use it if 1) there is
2360
+ // no `diff`, 2) the type condition of this fragment matches the candidate one and 3) the directives
2361
+ // in question are also on this very fragment. In that case, we can replace this whole inline fragment
2362
+ // by a spread of the candidate.
2363
+ if (!diff && sameType(this.element.typeCondition!, candidate.typeCondition)) {
2364
+ // We can potentially replace the whole fragment by the candidate; but as said above, still needs
2365
+ // to check the directives.
2366
+ let spreadDirectives: Directive<any>[] = this.element.appliedDirectives;
2367
+ if (candidate.appliedDirectives.length > 0) {
2210
2368
  const { isSubset, difference } = diffDirectives(this.element.appliedDirectives, candidate.appliedDirectives);
2211
2369
  if (!isSubset) {
2212
- // This means that while the named fragments matches the sub-selection, that name fragment also include some
2213
- // directives that are _not_ on our element, so we cannot use it.
2370
+ // While the candidate otherwise match, it has directives that are not on this element, so we
2371
+ // cannot reuse it.
2214
2372
  continue;
2215
2373
  }
2374
+ // Otherwise, any directives on this element that are not on the candidate should be kept and used
2375
+ // on the spread created.
2216
2376
  spreadDirectives = difference;
2217
2377
  }
2378
+ // Returning a spread without a subselection will make the code "replace" this whole inline fragment
2379
+ // by the spread, which is what we want. Do not that as we're replacing the whole inline fragment,
2380
+ // we use `this.parentType` instead of `parentType` (the later being `this.element.typeCondition` basically).
2381
+ return {
2382
+ spread: new FragmentSpreadSelection(this.parentType, fragments, candidate, spreadDirectives),
2383
+ };
2384
+ }
2218
2385
 
2219
- const newSelection = new FragmentSpreadSelection(this.parentType, fragments, candidate, spreadDirectives);
2220
- // We use the fragment when the fragments condition is either the same, or a supertype of our current condition.
2221
- // If it's the same type, then we don't really want to preserve the current condition, it is included in the
2222
- // spread and we can return it directly. But if the fragment condition is a superset, then we should preserve
2223
- // our current condition since it restricts the selection more than the fragment actual does.
2224
- if (sameType(typeCondition, candidate.typeCondition)) {
2225
- return newSelection;
2226
- }
2227
-
2228
- optimizedSelection = selectionSetOf(this.parentType, newSelection);
2229
- break;
2386
+ // We're already dealt with the one case where we might be able to handle a candidate that has directives.
2387
+ if (candidate.appliedDirectives.length > 0) {
2388
+ continue;
2230
2389
  }
2390
+
2391
+ const spread = new FragmentSpreadSelection(parentType, fragments, candidate, []);
2392
+ optimizedSelection = diff
2393
+ ? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
2394
+ : selectionSetOf(parentType, spread);
2395
+
2396
+ return { spread, optimizedSelection, hasDiff: !!diff };
2231
2397
  }
2232
2398
  }
2233
- return this.selectionSet === optimizedSelection
2234
- ? this
2235
- : new InlineFragmentSelection(this.element, optimizedSelection);
2399
+ return {};
2236
2400
  }
2237
2401
 
2238
2402
  withoutDefer(labelsToRemove?: Set<string>): InlineFragmentSelection | SelectionSet {
@@ -2272,10 +2436,12 @@ class InlineFragmentSelection extends FragmentSelection {
2272
2436
  // If the current type is an object, then we never need to keep the current fragment because:
2273
2437
  // - either the fragment is also an object, but we've eliminated the case where the 2 types are the same,
2274
2438
  // so this is just an unsatisfiable branch.
2275
- // - or it's not an object, but then the current type is more precise and no poitn in "casting" to a
2276
- // less precise interface/union.
2439
+ // - or it's not an object, but then the current type is more precise and no point in "casting" to a
2440
+ // less precise interface/union. And if the current type is not even a valid runtime of said interface/union,
2441
+ // then we should completely ignore the branch (or, since we're eliminating `thisCondition`, we would be
2442
+ // building an invalid selection).
2277
2443
  if (isObjectType(currentType)) {
2278
- if (isObjectType(thisCondition)) {
2444
+ if (isObjectType(thisCondition) || !possibleRuntimeTypes(thisCondition).includes(currentType)) {
2279
2445
  return undefined;
2280
2446
  } else {
2281
2447
  const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType);
@@ -2340,7 +2506,6 @@ class InlineFragmentSelection extends FragmentSelection {
2340
2506
  return this.selectionSet === trimmedSelectionSet ? this : this.withUpdatedSelectionSet(trimmedSelectionSet);
2341
2507
  }
2342
2508
 
2343
-
2344
2509
  expandAllFragments(): FragmentSelection {
2345
2510
  return this.mapToSelectionSet((s) => s.expandAllFragments());
2346
2511
  }
@@ -2349,6 +2514,22 @@ class InlineFragmentSelection extends FragmentSelection {
2349
2514
  return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
2350
2515
  }
2351
2516
 
2517
+ equals(that: Selection): boolean {
2518
+ if (this === that) {
2519
+ return true;
2520
+ }
2521
+
2522
+ return (that instanceof FragmentSelection)
2523
+ && this.element.equals(that.element)
2524
+ && this.selectionSet.equals(that.selectionSet);
2525
+ }
2526
+
2527
+ contains(that: Selection): boolean {
2528
+ return (that instanceof FragmentSelection)
2529
+ && this.element.equals(that.element)
2530
+ && this.selectionSet.contains(that.selectionSet);
2531
+ }
2532
+
2352
2533
  toString(expandFragments: boolean = true, indent?: string): string {
2353
2534
  return (indent ?? '') + this.element + ' ' + this.selectionSet.toString(expandFragments, true, indent);
2354
2535
  }
@@ -2368,7 +2549,7 @@ class FragmentSpreadSelection extends FragmentSelection {
2368
2549
  constructor(
2369
2550
  sourceType: CompositeType,
2370
2551
  private readonly fragments: NamedFragments,
2371
- private readonly namedFragment: NamedFragmentDefinition,
2552
+ readonly namedFragment: NamedFragmentDefinition,
2372
2553
  private readonly spreadDirectives: readonly Directive<any>[],
2373
2554
  ) {
2374
2555
  super(new FragmentElement(sourceType, namedFragment.typeCondition, namedFragment.appliedDirectives.concat(spreadDirectives)));
@@ -2474,6 +2655,31 @@ class FragmentSpreadSelection extends FragmentSelection {
2474
2655
  assert(false, 'Unsupported, see `Operation.withAllDeferLabelled`');
2475
2656
  }
2476
2657
 
2658
+ minus(that: Selection): undefined {
2659
+ assert(this.equals(that), () => `Invalid operation for ${this.toString(false)} and ${that.toString(false)}`);
2660
+ return undefined;
2661
+ }
2662
+
2663
+ equals(that: Selection): boolean {
2664
+ if (this === that) {
2665
+ return true;
2666
+ }
2667
+
2668
+ return (that instanceof FragmentSpreadSelection)
2669
+ && this.namedFragment.name === that.namedFragment.name
2670
+ && sameDirectiveApplications(this.spreadDirectives, that.spreadDirectives);
2671
+ }
2672
+
2673
+ contains(that: Selection): boolean {
2674
+ if (this.equals(that)) {
2675
+ return true;
2676
+ }
2677
+
2678
+ return (that instanceof FragmentSelection)
2679
+ && this.element.equals(that.element)
2680
+ && this.selectionSet.contains(that.selectionSet);
2681
+ }
2682
+
2477
2683
  toString(expandFragments: boolean = true, indent?: string): string {
2478
2684
  if (expandFragments) {
2479
2685
  return (indent ?? '') + this.element + ' ' + this.selectionSet.toString(true, true, indent);