@apollo/federation-internals 2.4.10 → 2.4.11

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
@@ -85,7 +85,11 @@ abstract class AbstractOperationElement<T extends AbstractOperationElement<T>> e
85
85
 
86
86
  abstract asPathElement(): string | undefined;
87
87
 
88
- abstract rebaseOn(parentType: CompositeType): T;
88
+ abstract rebaseOn(args: { parentType: CompositeType, errorIfCannotRebase: boolean }): T | undefined;
89
+
90
+ rebaseOnOrError(parentType: CompositeType): T {
91
+ return this.rebaseOn({ parentType, errorIfCannotRebase: true })!;
92
+ }
89
93
 
90
94
  abstract withUpdatedDirectives(newDirectives: readonly Directive<any>[]): T;
91
95
 
@@ -290,7 +294,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
290
294
  }
291
295
  }
292
296
 
293
- rebaseOn(parentType: CompositeType): Field<TArgs> {
297
+ rebaseOn({ parentType, errorIfCannotRebase }: { parentType: CompositeType, errorIfCannotRebase: boolean }): Field<TArgs> | undefined {
294
298
  const fieldParent = this.definition.parent;
295
299
  if (parentType === fieldParent) {
296
300
  return this;
@@ -300,12 +304,16 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
300
304
  return this.withUpdatedDefinition(parentType.typenameField()!);
301
305
  }
302
306
 
303
- validate(
304
- this.canRebaseOn(parentType),
305
- () => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${parentType}"`
306
- );
307
307
  const fieldDef = parentType.field(this.name);
308
- validate(fieldDef, () => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${parentType}" (that does not declare that field)`);
308
+ const canRebase = this.canRebaseOn(parentType) && fieldDef;
309
+ if (!canRebase) {
310
+ validate(
311
+ !errorIfCannotRebase,
312
+ () => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${parentType}"`
313
+ );
314
+ return undefined;
315
+ }
316
+
309
317
  return this.withUpdatedDefinition(fieldDef);
310
318
  }
311
319
 
@@ -466,7 +474,7 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
466
474
  return newFragment;
467
475
  }
468
476
 
469
- rebaseOn(parentType: CompositeType): FragmentElement {
477
+ rebaseOn({ parentType, errorIfCannotRebase }: { parentType: CompositeType, errorIfCannotRebase: boolean }): FragmentElement | undefined {
470
478
  const fragmentParent = this.parentType;
471
479
  const typeCondition = this.typeCondition;
472
480
  if (parentType === fragmentParent) {
@@ -477,10 +485,13 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
477
485
  // to update the source type of the fragment, but also "rebase" the condition to the selection set
478
486
  // schema.
479
487
  const { canRebase, rebasedCondition } = this.canRebaseOn(parentType);
480
- validate(
481
- canRebase,
482
- () => `Cannot add fragment of condition "${typeCondition}" (runtimes: [${possibleRuntimeTypes(typeCondition!)}]) to parent type "${parentType}" (runtimes: ${possibleRuntimeTypes(parentType)})`
483
- );
488
+ if (!canRebase) {
489
+ validate(
490
+ !errorIfCannotRebase,
491
+ () => `Cannot add fragment of condition "${typeCondition}" (runtimes: [${possibleRuntimeTypes(typeCondition!)}]) to parent type "${parentType}" (runtimes: ${possibleRuntimeTypes(parentType)})`
492
+ );
493
+ return undefined;
494
+ }
484
495
  return this.withUpdatedTypes(parentType, rebasedCondition);
485
496
  }
486
497
 
@@ -1232,7 +1243,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1232
1243
  }
1233
1244
 
1234
1245
  toString(indent?: string): string {
1235
- return (indent ?? '') + `fragment ${this.name} on ${this.typeCondition}${this.appliedDirectivesToString()} ${this.selectionSet.toString(false, true, indent)}`;
1246
+ return `fragment ${this.name} on ${this.typeCondition}${this.appliedDirectivesToString()} ${this.selectionSet.toString(false, true, indent)}`;
1236
1247
  }
1237
1248
  }
1238
1249
 
@@ -1363,21 +1374,39 @@ export class NamedFragments {
1363
1374
  });
1364
1375
  }
1365
1376
 
1377
+ // When we rebase named fragments on a subgraph schema, only a subset of what the fragment handles may belong
1378
+ // to that particular subgraph. And there are a few sub-cases where that subset is such that we basically need or
1379
+ // want to consider to ignore the fragment for that subgraph, and that is when:
1380
+ // 1. the subset that apply is actually empty. The fragment wouldn't be valid in this case anyway.
1381
+ // 2. the subset is a single leaf field: in that case, using the one field directly is just shorter than using
1382
+ // the fragment, so we consider the fragment don't really apply to that subgraph. Technically, using the
1383
+ // fragment could still be of value if the fragment name is a lot smaller than the one field name, but it's
1384
+ // enough of a niche case that we ignore it. Note in particular that one sub-case of this rule that is likely
1385
+ // to be common is when the subset ends up being just `__typename`: this would basically mean the fragment
1386
+ // don't really apply to the subgraph, and that this will ensure this is the case.
1387
+ private selectionSetIsWorthUsing(selectionSet: SelectionSet): boolean {
1388
+ const selections = selectionSet.selections();
1389
+ if (selections.length === 0) {
1390
+ return false;
1391
+ }
1392
+ if (selections.length === 1) {
1393
+ const s = selections[0];
1394
+ return !(s.kind === 'FieldSelection' && s.element.isLeafField());
1395
+ }
1396
+ return true;
1397
+ }
1398
+
1366
1399
  rebaseOn(schema: Schema): NamedFragments | undefined {
1367
1400
  return this.mapInDependencyOrder((fragment, newFragments) => {
1368
1401
  const rebasedType = schema.type(fragment.selectionSet.parentType.name);
1369
- try {
1370
- if (!rebasedType || !isCompositeType(rebasedType)) {
1371
- return undefined;
1372
- }
1373
-
1374
- const rebasedSelection = fragment.selectionSet.rebaseOn(rebasedType, newFragments);
1375
- return new NamedFragmentDefinition(schema, fragment.name, rebasedType).setSelectionSet(rebasedSelection);
1376
- } catch (e) {
1377
- // This means we cannot rebase this selection on the schema and thus cannot reuse that fragment on that
1378
- // particular schema.
1402
+ if (!rebasedType || !isCompositeType(rebasedType)) {
1379
1403
  return undefined;
1380
1404
  }
1405
+
1406
+ const rebasedSelection = fragment.selectionSet.rebaseOn({ parentType: rebasedType, fragments: newFragments, errorIfCannotRebase: false });
1407
+ return this.selectionSetIsWorthUsing(rebasedSelection)
1408
+ ? new NamedFragmentDefinition(schema, fragment.name, rebasedType).setSelectionSet(rebasedSelection)
1409
+ : undefined;
1381
1410
  });
1382
1411
  }
1383
1412
 
@@ -1515,6 +1544,20 @@ export class SelectionSet {
1515
1544
  return this._keyedSelections.has(typenameFieldName);
1516
1545
  }
1517
1546
 
1547
+ withoutTopLevelTypenameField(): SelectionSet {
1548
+ if (!this.hasTopLevelTypenameField) {
1549
+ return this;
1550
+ }
1551
+
1552
+ const newKeyedSelections = new Map<string, Selection>();
1553
+ for (const [key, selection] of this._keyedSelections) {
1554
+ if (key !== typenameFieldName) {
1555
+ newKeyedSelections.set(key, selection);
1556
+ }
1557
+ }
1558
+ return new SelectionSet(this.parentType, newKeyedSelections);
1559
+ }
1560
+
1518
1561
  fieldsInSet(): CollectedFieldsInSet {
1519
1562
  const fields = new Array<{ path: string[], field: FieldSelection }>();
1520
1563
  for (const selection of this.selections()) {
@@ -1759,14 +1802,25 @@ export class SelectionSet {
1759
1802
  return updated.isEmpty() ? undefined : updated;
1760
1803
  }
1761
1804
 
1762
- rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): SelectionSet {
1805
+ rebaseOn({
1806
+ parentType,
1807
+ fragments,
1808
+ errorIfCannotRebase,
1809
+ }: {
1810
+ parentType: CompositeType,
1811
+ fragments: NamedFragments | undefined
1812
+ errorIfCannotRebase: boolean,
1813
+ }): SelectionSet {
1763
1814
  if (this.parentType === parentType) {
1764
1815
  return this;
1765
1816
  }
1766
1817
 
1767
1818
  const newSelections = new Map<string, Selection>();
1768
1819
  for (const selection of this.selections()) {
1769
- newSelections.set(selection.key(), selection.rebaseOn(parentType, fragments));
1820
+ const rebasedSelection = selection.rebaseOn({ parentType, fragments, errorIfCannotRebase });
1821
+ if (rebasedSelection) {
1822
+ newSelections.set(selection.key(), rebasedSelection);
1823
+ }
1770
1824
  }
1771
1825
 
1772
1826
  return new SelectionSet(parentType, newSelections);
@@ -1790,22 +1844,36 @@ export class SelectionSet {
1790
1844
  return true;
1791
1845
  }
1792
1846
 
1793
- contains(that: SelectionSet): ContainsResult {
1847
+ contains(that: SelectionSet, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
1848
+ const ignoreMissingTypename = options?.ignoreMissingTypename ?? false;
1794
1849
  if (that._selections.length > this._selections.length) {
1795
- return ContainsResult.NOT_CONTAINED;
1850
+ // If `that` has more selections but we're ignoring missing __typename, then in the case where
1851
+ // `that` has a __typename but `this` does not, then we need the length of `that` to be at
1852
+ // least 2 more than that of `this` to be able to conclude there is no contains.
1853
+ if (!ignoreMissingTypename || that._selections.length > this._selections.length + 1 || this.hasTopLevelTypenameField() || !that.hasTopLevelTypenameField()) {
1854
+ return ContainsResult.NOT_CONTAINED;
1855
+ }
1796
1856
  }
1797
1857
 
1798
1858
  let isEqual = true;
1859
+ let didIgnoreTypename = false;
1799
1860
  for (const [key, thatSelection] of that._keyedSelections) {
1861
+ if (key === typenameFieldName && ignoreMissingTypename) {
1862
+ if (!this._keyedSelections.has(typenameFieldName)) {
1863
+ didIgnoreTypename = true;
1864
+ }
1865
+ continue;
1866
+ }
1867
+
1800
1868
  const thisSelection = this._keyedSelections.get(key);
1801
- const selectionResult = thisSelection?.contains(thatSelection);
1869
+ const selectionResult = thisSelection?.contains(thatSelection, options);
1802
1870
  if (selectionResult === undefined || selectionResult === ContainsResult.NOT_CONTAINED) {
1803
1871
  return ContainsResult.NOT_CONTAINED;
1804
1872
  }
1805
1873
  isEqual &&= selectionResult === ContainsResult.EQUAL;
1806
1874
  }
1807
1875
 
1808
- return isEqual && that._selections.length === this._selections.length
1876
+ return isEqual && that._selections.length === (this._selections.length + (didIgnoreTypename ? 1 : 0))
1809
1877
  ? ContainsResult.EQUAL
1810
1878
  : ContainsResult.STRICTLY_CONTAINED;
1811
1879
  }
@@ -2164,10 +2232,10 @@ function makeSelection(parentType: CompositeType, updates: SelectionUpdate[], fr
2164
2232
 
2165
2233
  // Optimize for the simple case of a single selection, as we don't have to do anything complex to merge the sub-selections.
2166
2234
  if (updates.length === 1 && first instanceof AbstractSelection) {
2167
- return first.rebaseOn(parentType, fragments);
2235
+ return first.rebaseOnOrError({ parentType, fragments });
2168
2236
  }
2169
2237
 
2170
- const element = updateElement(first).rebaseOn(parentType);
2238
+ const element = updateElement(first).rebaseOnOrError(parentType);
2171
2239
  const subSelectionParentType = element.kind === 'Field' ? element.baseType() : element.castedType();
2172
2240
  if (!isCompositeType(subSelectionParentType)) {
2173
2241
  // This is a leaf, so all updates should correspond ot the same field and we just use the first.
@@ -2356,7 +2424,11 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2356
2424
 
2357
2425
  abstract validate(variableDefinitions: VariableDefinitions): void;
2358
2426
 
2359
- abstract rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): TOwnType;
2427
+ abstract rebaseOn(args: { parentType: CompositeType, fragments: NamedFragments | undefined, errorIfCannotRebase: boolean}): TOwnType | undefined;
2428
+
2429
+ rebaseOnOrError({ parentType, fragments }: { parentType: CompositeType, fragments: NamedFragments | undefined }): TOwnType {
2430
+ return this.rebaseOn({ parentType, fragments, errorIfCannotRebase: true})!;
2431
+ }
2360
2432
 
2361
2433
  get parentType(): CompositeType {
2362
2434
  return this.element.parentType;
@@ -2468,7 +2540,6 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2468
2540
  const applyingFragments: { fragment: NamedFragmentDefinition, atType: FragmentRestrictionAtType }[] = [];
2469
2541
  for (const candidate of candidates) {
2470
2542
  const atType = candidate.expandedSelectionSetAtType(parentType);
2471
- const selectionSetAtType = atType.selectionSet;
2472
2543
  // It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
2473
2544
  // `parentType` is empty, or contains only `__typename`. For instance, suppose we have
2474
2545
  // a union `U = A | B | C`, and then a fragment:
@@ -2487,11 +2558,22 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2487
2558
  //
2488
2559
  // Using `F` in those cases is, while not 100% incorrect, at least not productive, and so we
2489
2560
  // skip it that case. This is essentially an optimisation.
2490
- if (selectionSetAtType.isEmpty() || (selectionSetAtType.selections().length === 1 && selectionSetAtType.selections()[0].isTypenameField())) {
2561
+ if (atType.selectionSet.isEmpty() || (atType.selectionSet.selections().length === 1 && atType.selectionSet.selections()[0].isTypenameField())) {
2491
2562
  continue;
2492
2563
  }
2493
2564
 
2494
- const res = subSelection.contains(selectionSetAtType);
2565
+ // As we check inclusion, we ignore the case where the fragment queries __typename but the subSelection does not.
2566
+ // The rational is that querying `__typename` unecessarily is mostly harmless (it always works and it's super cheap)
2567
+ // so we don't want to not use a fragment just to save querying a `__typename` in a few cases. But the underlying
2568
+ // context of why this matters is that the query planner always requests __typename for abstract type, and will do
2569
+ // so in fragments too, but we can have a field that _does_ return an abstract type within a fragment, but that
2570
+ // _does not_ end up returning an abstract type when applied in a "more specific" context (think a fragment on
2571
+ // an interface I1 where a inside field returns another interface I2, but applied in the context of a implementation
2572
+ // type of I1 where that particular field returns an implementation of I2 rather than I2 directly; we would have
2573
+ // added __typename to the fragment (because it's all interfaces), but the selection itself, which only deals
2574
+ // with object type, may not have __typename requested; using the fragment might still be a good idea, and
2575
+ // querying __typename needlessly is a very small price to pay for that).
2576
+ const res = subSelection.contains(atType.selectionSet, { ignoreMissingTypename: true });
2495
2577
 
2496
2578
  if (res === ContainsResult.EQUAL) {
2497
2579
  if (canUseFullMatchingFragment(candidate)) {
@@ -2568,7 +2650,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2568
2650
 
2569
2651
  let notCoveredByFragments = subSelection;
2570
2652
  const optimized = new SelectionSetUpdates();
2571
- for (const { fragment, atType} of filteredApplyingFragments) {
2653
+ for (const { fragment, atType } of filteredApplyingFragments) {
2572
2654
  if (!validator.checkCanReuseFragmentAndTrackIt(atType)) {
2573
2655
  continue;
2574
2656
  }
@@ -2874,12 +2956,24 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2874
2956
  * Obviously, this operation will only succeed if this selection (both the field itself and its subselections)
2875
2957
  * make sense from the provided parent type. If this is not the case, this method will throw.
2876
2958
  */
2877
- rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FieldSelection {
2959
+ rebaseOn({
2960
+ parentType,
2961
+ fragments,
2962
+ errorIfCannotRebase,
2963
+ }: {
2964
+ parentType: CompositeType,
2965
+ fragments: NamedFragments | undefined,
2966
+ errorIfCannotRebase: boolean,
2967
+ }): FieldSelection | undefined {
2878
2968
  if (this.element.parentType === parentType) {
2879
2969
  return this;
2880
2970
  }
2881
2971
 
2882
- const rebasedElement = this.element.rebaseOn(parentType);
2972
+ const rebasedElement = this.element.rebaseOn({ parentType, errorIfCannotRebase });
2973
+ if (!rebasedElement) {
2974
+ return undefined;
2975
+ }
2976
+
2883
2977
  if (!this.selectionSet) {
2884
2978
  return this.withUpdatedElement(rebasedElement);
2885
2979
  }
@@ -2890,7 +2984,8 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2890
2984
  }
2891
2985
 
2892
2986
  validate(isCompositeType(rebasedBase), () => `Cannot rebase field selection ${this} on ${parentType}: rebased field base return type ${rebasedBase} is not composite`);
2893
- return this.withUpdatedComponents(rebasedElement, this.selectionSet.rebaseOn(rebasedBase, fragments));
2987
+ const rebasedSelectionSet = this.selectionSet.rebaseOn({ parentType: rebasedBase, fragments, errorIfCannotRebase });
2988
+ return rebasedSelectionSet.isEmpty() ? undefined : this.withUpdatedComponents(rebasedElement, rebasedSelectionSet);
2894
2989
  }
2895
2990
 
2896
2991
  /**
@@ -2997,7 +3092,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2997
3092
  return !!that.selectionSet && this.selectionSet.equals(that.selectionSet);
2998
3093
  }
2999
3094
 
3000
- contains(that: Selection): ContainsResult {
3095
+ contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
3001
3096
  if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
3002
3097
  return ContainsResult.NOT_CONTAINED;
3003
3098
  }
@@ -3007,7 +3102,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
3007
3102
  return ContainsResult.EQUAL;
3008
3103
  }
3009
3104
  assert(that.selectionSet, '`this` and `that` have the same element, so if one has sub-selection, the other one should too')
3010
- return this.selectionSet.contains(that.selectionSet);
3105
+ return this.selectionSet.contains(that.selectionSet, options);
3011
3106
  }
3012
3107
 
3013
3108
  toString(expandFragments: boolean = true, indent?: string): string {
@@ -3051,7 +3146,7 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
3051
3146
 
3052
3147
  abstract equals(that: Selection): boolean;
3053
3148
 
3054
- abstract contains(that: Selection): ContainsResult;
3149
+ abstract contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult;
3055
3150
 
3056
3151
  normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
3057
3152
  const thisCondition = this.element.typeCondition;
@@ -3108,18 +3203,31 @@ class InlineFragmentSelection extends FragmentSelection {
3108
3203
  this.selectionSet.validate(variableDefinitions);
3109
3204
  }
3110
3205
 
3111
- rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
3206
+ rebaseOn({
3207
+ parentType,
3208
+ fragments,
3209
+ errorIfCannotRebase,
3210
+ }: {
3211
+ parentType: CompositeType,
3212
+ fragments: NamedFragments | undefined,
3213
+ errorIfCannotRebase: boolean,
3214
+ }): FragmentSelection | undefined {
3112
3215
  if (this.parentType === parentType) {
3113
3216
  return this;
3114
3217
  }
3115
3218
 
3116
- const rebasedFragment = this.element.rebaseOn(parentType);
3219
+ const rebasedFragment = this.element.rebaseOn({ parentType, errorIfCannotRebase });
3220
+ if (!rebasedFragment) {
3221
+ return undefined;
3222
+ }
3223
+
3117
3224
  const rebasedCastedType = rebasedFragment.castedType();
3118
3225
  if (rebasedCastedType === this.selectionSet.parentType) {
3119
3226
  return this.withUpdatedElement(rebasedFragment);
3120
3227
  }
3121
3228
 
3122
- return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType, fragments));
3229
+ const rebasedSelectionSet = this.selectionSet.rebaseOn({ parentType: rebasedCastedType, fragments, errorIfCannotRebase });
3230
+ return rebasedSelectionSet.isEmpty() ? undefined : this.withUpdatedComponents(rebasedFragment, rebasedSelectionSet);
3123
3231
  }
3124
3232
 
3125
3233
  canAddTo(parentType: CompositeType): boolean {
@@ -3268,7 +3376,8 @@ class InlineFragmentSelection extends FragmentSelection {
3268
3376
  return undefined;
3269
3377
  } else {
3270
3378
  return this.withUpdatedComponents(
3271
- this.element.rebaseOn(parentType),
3379
+ // We should be able to rebase, or there is a bug, so error if that is the case.
3380
+ this.element.rebaseOnOrError(parentType),
3272
3381
  selectionSetOfElement(
3273
3382
  new Field(
3274
3383
  (this.element.typeCondition ?? parentType).typenameField()!,
@@ -3320,7 +3429,7 @@ class InlineFragmentSelection extends FragmentSelection {
3320
3429
 
3321
3430
  return this.parentType === parentType && this.selectionSet === normalizedSelectionSet
3322
3431
  ? this
3323
- : this.withUpdatedComponents(this.element.rebaseOn(parentType), normalizedSelectionSet);
3432
+ : this.withUpdatedComponents(this.element.rebaseOnOrError(parentType), normalizedSelectionSet);
3324
3433
  }
3325
3434
 
3326
3435
  expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection {
@@ -3337,12 +3446,12 @@ class InlineFragmentSelection extends FragmentSelection {
3337
3446
  && this.selectionSet.equals(that.selectionSet);
3338
3447
  }
3339
3448
 
3340
- contains(that: Selection): ContainsResult {
3449
+ contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
3341
3450
  if (!(that instanceof FragmentSelection) || !this.element.equals(that.element)) {
3342
3451
  return ContainsResult.NOT_CONTAINED;
3343
3452
  }
3344
3453
 
3345
- return this.selectionSet.contains(that.selectionSet);
3454
+ return this.selectionSet.contains(that.selectionSet, options);
3346
3455
  }
3347
3456
 
3348
3457
  toString(expandFragments: boolean = true, indent?: string): string {
@@ -3385,7 +3494,7 @@ class FragmentSpreadSelection extends FragmentSelection {
3385
3494
  // We must update the spread parent type if necessary since we're not going deeper,
3386
3495
  // or we'll be fundamentally losing context.
3387
3496
  assert(parentType.schema() === this.parentType.schema(), 'Should not try to normalize using a type from another schema');
3388
- return this.rebaseOn(parentType, this.fragments);
3497
+ return this.rebaseOnOrError({ parentType, fragments: this.fragments });
3389
3498
  }
3390
3499
 
3391
3500
  validate(): void {
@@ -3421,7 +3530,15 @@ class FragmentSpreadSelection extends FragmentSelection {
3421
3530
  return this;
3422
3531
  }
3423
3532
 
3424
- rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
3533
+ rebaseOn({
3534
+ parentType,
3535
+ fragments,
3536
+ errorIfCannotRebase,
3537
+ }: {
3538
+ parentType: CompositeType,
3539
+ fragments: NamedFragments | undefined,
3540
+ errorIfCannotRebase: boolean,
3541
+ }): FragmentSelection | undefined {
3425
3542
  // We preserve the parent type here, to make sure we don't lose context, but we actually don't
3426
3543
  // want to expand the spread as that would compromise the code that optimize subgraph fetches to re-use named
3427
3544
  // fragments.
@@ -3441,7 +3558,14 @@ class FragmentSpreadSelection extends FragmentSelection {
3441
3558
  assert(fragments || this.parentType.schema() === parentType.schema(), `Must provide fragments is rebasing on other schema`);
3442
3559
  const newFragments = fragments ?? this.fragments;
3443
3560
  const namedFragment = newFragments.get(this.namedFragment.name);
3444
- assert(namedFragment, () => `Cannot rebase ${this} if it isn't part of the provided fragments`);
3561
+ // If we're rebasing on another schema (think a subgraph), then named fragments will have been rebased on that, and some
3562
+ // of them may not contain anything that is on that subgraph, in which case they will not have been included at all.
3563
+ // If so, then as long as we're not ask to error if we cannot rebase, then we're happy to skip that spread (since again,
3564
+ // it expands to nothing that apply on the schema).
3565
+ if (!namedFragment) {
3566
+ validate(!errorIfCannotRebase, () => `Cannot rebase ${this.toString(false)} if it isn't part of the provided fragments`);
3567
+ return undefined;
3568
+ }
3445
3569
  return new FragmentSpreadSelection(
3446
3570
  parentType,
3447
3571
  newFragments,
@@ -3497,7 +3621,7 @@ class FragmentSpreadSelection extends FragmentSelection {
3497
3621
  && sameDirectiveApplications(this.spreadDirectives, that.spreadDirectives);
3498
3622
  }
3499
3623
 
3500
- contains(that: Selection): ContainsResult {
3624
+ contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
3501
3625
  if (this.equals(that)) {
3502
3626
  return ContainsResult.EQUAL;
3503
3627
  }
@@ -3506,7 +3630,7 @@ class FragmentSpreadSelection extends FragmentSelection {
3506
3630
  return ContainsResult.NOT_CONTAINED;
3507
3631
  }
3508
3632
 
3509
- return this.selectionSet.contains(that.selectionSet);
3633
+ return this.selectionSet.contains(that.selectionSet, options);
3510
3634
  }
3511
3635
 
3512
3636
  toString(expandFragments: boolean = true, indent?: string): string {