@apollo/federation-internals 2.4.8 → 2.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/operations.ts CHANGED
@@ -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
 
@@ -934,9 +945,17 @@ export class Operation {
934
945
  // leaving this is not a huge deal and it's not worth the complexity, but it could be that we can
935
946
  // refactor all this later to avoid this case without additional complexity.
936
947
  if (finalFragments) {
937
- const usages = new Map<string, number>();
938
- optimizedSelection.collectUsedFragmentNames(usages);
939
- finalFragments = finalFragments.filter((f) => (usages.get(f.name) ?? 0) > 0);
948
+ // Note that removing a fragment might lead to another fragment being unused, so we need to iterate
949
+ // until there is nothing more to remove, or we're out of fragments.
950
+ let beforeRemoval: NamedFragments;
951
+ do {
952
+ beforeRemoval = finalFragments;
953
+ const usages = new Map<string, number>();
954
+ // Collecting all usages, both in the selection and within other fragments.
955
+ optimizedSelection.collectUsedFragmentNames(usages);
956
+ finalFragments.collectUsedFragmentNames(usages);
957
+ finalFragments = finalFragments.filter((f) => (usages.get(f.name) ?? 0) > 0);
958
+ } while (finalFragments && finalFragments.size < beforeRemoval.size);
940
959
  }
941
960
  }
942
961
 
@@ -1098,27 +1117,26 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1098
1117
 
1099
1118
  /**
1100
1119
  * 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".
1120
+ * (_without_ the fragment condition, hence the "directly") can be normalized at `type` and this without
1121
+ * "widening" the runtime types to types that do not intersect the fragment condition.
1103
1122
  *
1104
1123
  * For that to be true, we need one of this to be true:
1105
1124
  * 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"
1125
+ * Otherwise, putting it at `type` without its condition would "generalize" more than the fragment meant to (and
1126
+ * so we'd "widen" the runtime types more than what the query meant to.
1108
1127
  * 2. either `type` and `this.typeCondition` are equal, or `type` is an object or `this.typeCondition` is a union
1109
1128
  * The idea is that, assuming our 1st point, then:
1110
1129
  * - if both are equal, things works trivially.
1111
1130
  * - if `type` is an object, `this.typeCondition` is either the same object, or a union/interface for which
1112
1131
  * 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.
1132
+ * - if `this.typeCondition` is a union, then it's selection can only have fragments at top-level
1133
+ * (no fields save for `__typename`), and normalising is always fine with top-level fragments.
1115
1134
  * But in any other case, both types must be abstract (if `this.typeCondition` is an object, the 1st condition
1116
1135
  * imply `type` can only be the same type) and we're in one of:
1117
1136
  * - `type` and `this.typeCondition` are both different interfaces (that intersect but are different).
1118
1137
  * - `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).
1138
+ * And in both cases, since `this.typeCondition` is an interface, the fragment selection set may have field selections
1139
+ * on that interface, and those fields may not be valid for `type`.
1122
1140
  *
1123
1141
  * @param type - the type at which we're looking at applying the fragment
1124
1142
  */
@@ -1225,7 +1243,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
1225
1243
  }
1226
1244
 
1227
1245
  toString(indent?: string): string {
1228
- 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)}`;
1229
1247
  }
1230
1248
  }
1231
1249
 
@@ -1274,6 +1292,15 @@ export class NamedFragments {
1274
1292
  return this.fragments.values();
1275
1293
  }
1276
1294
 
1295
+ /**
1296
+ * Collect the usages of fragments that are used within the selection of other fragments.
1297
+ */
1298
+ collectUsedFragmentNames(collector: Map<string, number>) {
1299
+ for (const fragment of this.definitions()) {
1300
+ fragment.collectUsedFragmentNames(collector);
1301
+ }
1302
+ }
1303
+
1277
1304
  map(mapper: (def: NamedFragmentDefinition) => NamedFragmentDefinition): NamedFragments {
1278
1305
  const mapped = new NamedFragments();
1279
1306
  for (const def of this.fragments.values()) {
@@ -1347,21 +1374,39 @@ export class NamedFragments {
1347
1374
  });
1348
1375
  }
1349
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
+
1350
1399
  rebaseOn(schema: Schema): NamedFragments | undefined {
1351
1400
  return this.mapInDependencyOrder((fragment, newFragments) => {
1352
1401
  const rebasedType = schema.type(fragment.selectionSet.parentType.name);
1353
- try {
1354
- if (!rebasedType || !isCompositeType(rebasedType)) {
1355
- return undefined;
1356
- }
1357
-
1358
- const rebasedSelection = fragment.selectionSet.rebaseOn(rebasedType, newFragments);
1359
- return new NamedFragmentDefinition(schema, fragment.name, rebasedType).setSelectionSet(rebasedSelection);
1360
- } catch (e) {
1361
- // This means we cannot rebase this selection on the schema and thus cannot reuse that fragment on that
1362
- // particular schema.
1402
+ if (!rebasedType || !isCompositeType(rebasedType)) {
1363
1403
  return undefined;
1364
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;;
1365
1410
  });
1366
1411
  }
1367
1412
 
@@ -1499,6 +1544,20 @@ export class SelectionSet {
1499
1544
  return this._keyedSelections.has(typenameFieldName);
1500
1545
  }
1501
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
+
1502
1561
  fieldsInSet(): CollectedFieldsInSet {
1503
1562
  const fields = new Array<{ path: string[], field: FieldSelection }>();
1504
1563
  for (const selection of this.selections()) {
@@ -1570,7 +1629,7 @@ export class SelectionSet {
1570
1629
  // With that, `optimizeSelections` will correctly match on the `on Query` fragment; after which
1571
1630
  // we can unpack the final result.
1572
1631
  const wrapped = new InlineFragmentSelection(new FragmentElement(this.parentType, this.parentType), this);
1573
- const validator = FieldsConflictValidator.build(this);
1632
+ const validator = FieldsConflictMultiBranchValidator.ofInitial(FieldsConflictValidator.build(this));
1574
1633
  const optimized = wrapped.optimize(fragments, validator);
1575
1634
 
1576
1635
  // Now, it's possible we matched a full fragment, in which case `optimized` will be just the named fragment,
@@ -1584,7 +1643,7 @@ export class SelectionSet {
1584
1643
  // Tries to match fragments inside each selections of this selection set, and this recursively. However, note that this
1585
1644
  // may not match fragments that would apply at top-level, so you should usually use `optimize` instead (this exists mostly
1586
1645
  // for the recursion).
1587
- optimizeSelections(fragments: NamedFragments, validator: FieldsConflictValidator): SelectionSet {
1646
+ optimizeSelections(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): SelectionSet {
1588
1647
  return this.lazyMap((selection) => selection.optimize(fragments, validator));
1589
1648
  }
1590
1649
 
@@ -1657,10 +1716,12 @@ export class SelectionSet {
1657
1716
  * }
1658
1717
  * ```
1659
1718
  *
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).
1719
+ * For this operation to be valid (to not throw), `parentType` must be such that every field selection in
1720
+ * this selection set is such that the field parent type intersects `parentType` (there is no limitation
1721
+ * on the fragment selections, though any fragment selections whose condition do not intersects `parentType`
1722
+ * will be discarded). Note that `this.normalize(this.parentType)` is always valid and useful, but it is
1723
+ * also possible to pass a `parentType` that is more "restrictive" than the selection current parent type
1724
+ * (as long as the top-level fields of this selection set can be rebased on that type).
1664
1725
  *
1665
1726
  * Passing the option `recursive == false` makes the normalization only apply at the top-level, removing
1666
1727
  * any unecessary top-level inline fragments, possibly multiple layers of them, but we never recurse
@@ -1741,14 +1802,25 @@ export class SelectionSet {
1741
1802
  return updated.isEmpty() ? undefined : updated;
1742
1803
  }
1743
1804
 
1744
- 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 {
1745
1814
  if (this.parentType === parentType) {
1746
1815
  return this;
1747
1816
  }
1748
1817
 
1749
1818
  const newSelections = new Map<string, Selection>();
1750
1819
  for (const selection of this.selections()) {
1751
- 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
+ }
1752
1824
  }
1753
1825
 
1754
1826
  return new SelectionSet(parentType, newSelections);
@@ -1772,15 +1844,25 @@ export class SelectionSet {
1772
1844
  return true;
1773
1845
  }
1774
1846
 
1775
- contains(that: SelectionSet): ContainsResult {
1847
+ contains(that: SelectionSet, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
1848
+ const ignoreMissingTypename = options?.ignoreMissingTypename ?? false;
1776
1849
  if (that._selections.length > this._selections.length) {
1777
- 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
+ }
1778
1856
  }
1779
1857
 
1780
1858
  let isEqual = true;
1781
1859
  for (const [key, thatSelection] of that._keyedSelections) {
1860
+ if (key === typenameFieldName && ignoreMissingTypename) {
1861
+ continue;
1862
+ }
1863
+
1782
1864
  const thisSelection = this._keyedSelections.get(key);
1783
- const selectionResult = thisSelection?.contains(thatSelection);
1865
+ const selectionResult = thisSelection?.contains(thatSelection, options);
1784
1866
  if (selectionResult === undefined || selectionResult === ContainsResult.NOT_CONTAINED) {
1785
1867
  return ContainsResult.NOT_CONTAINED;
1786
1868
  }
@@ -1792,6 +1874,11 @@ export class SelectionSet {
1792
1874
  : ContainsResult.STRICTLY_CONTAINED;
1793
1875
  }
1794
1876
 
1877
+ containsTopLevelField(field: Field): boolean {
1878
+ const selection = this._keyedSelections.get(field.key());
1879
+ return !!selection && selection.element.equals(field);
1880
+ }
1881
+
1795
1882
  /**
1796
1883
  * Returns a selection set that correspond to this selection set but where any of the selections in the
1797
1884
  * provided selection set have been remove.
@@ -2141,10 +2228,10 @@ function makeSelection(parentType: CompositeType, updates: SelectionUpdate[], fr
2141
2228
 
2142
2229
  // Optimize for the simple case of a single selection, as we don't have to do anything complex to merge the sub-selections.
2143
2230
  if (updates.length === 1 && first instanceof AbstractSelection) {
2144
- return first.rebaseOn(parentType, fragments);
2231
+ return first.rebaseOnOrError({ parentType, fragments });
2145
2232
  }
2146
2233
 
2147
- const element = updateElement(first).rebaseOn(parentType);
2234
+ const element = updateElement(first).rebaseOnOrError(parentType);
2148
2235
  const subSelectionParentType = element.kind === 'Field' ? element.baseType() : element.castedType();
2149
2236
  if (!isCompositeType(subSelectionParentType)) {
2150
2237
  // This is a leaf, so all updates should correspond ot the same field and we just use the first.
@@ -2327,13 +2414,17 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2327
2414
 
2328
2415
  abstract key(): string;
2329
2416
 
2330
- abstract optimize(fragments: NamedFragments, validator: FieldsConflictValidator): Selection;
2417
+ abstract optimize(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): Selection;
2331
2418
 
2332
2419
  abstract toSelectionNode(): SelectionNode;
2333
2420
 
2334
2421
  abstract validate(variableDefinitions: VariableDefinitions): void;
2335
2422
 
2336
- abstract rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): TOwnType;
2423
+ abstract rebaseOn(args: { parentType: CompositeType, fragments: NamedFragments | undefined, errorIfCannotRebase: boolean}): TOwnType | undefined;
2424
+
2425
+ rebaseOnOrError({ parentType, fragments }: { parentType: CompositeType, fragments: NamedFragments | undefined }): TOwnType {
2426
+ return this.rebaseOn({ parentType, fragments, errorIfCannotRebase: true})!;
2427
+ }
2337
2428
 
2338
2429
  get parentType(): CompositeType {
2339
2430
  return this.element.parentType;
@@ -2425,7 +2516,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2425
2516
  parentType: CompositeType,
2426
2517
  subSelection: SelectionSet,
2427
2518
  fragments: NamedFragments,
2428
- validator: FieldsConflictValidator,
2519
+ validator: FieldsConflictMultiBranchValidator,
2429
2520
  canUseFullMatchingFragment: (match: NamedFragmentDefinition) => boolean,
2430
2521
  }): SelectionSet | NamedFragmentDefinition {
2431
2522
  // We limit to fragments whose selection could be applied "directly" at `parentType`, meaning without taking the fragment condition
@@ -2444,8 +2535,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2444
2535
  // applies to a subset of `subSelection`.
2445
2536
  const applyingFragments: { fragment: NamedFragmentDefinition, atType: FragmentRestrictionAtType }[] = [];
2446
2537
  for (const candidate of candidates) {
2447
- const atType = candidate.expandedSelectionSetAtType(parentType);
2448
- const selectionSetAtType = atType.selectionSet;
2538
+ let atType = candidate.expandedSelectionSetAtType(parentType);
2449
2539
  // It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
2450
2540
  // `parentType` is empty, or contains only `__typename`. For instance, suppose we have
2451
2541
  // a union `U = A | B | C`, and then a fragment:
@@ -2464,11 +2554,22 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2464
2554
  //
2465
2555
  // Using `F` in those cases is, while not 100% incorrect, at least not productive, and so we
2466
2556
  // skip it that case. This is essentially an optimisation.
2467
- if (selectionSetAtType.isEmpty() || (selectionSetAtType.selections().length === 1 && selectionSetAtType.selections()[0].isTypenameField())) {
2557
+ if (atType.selectionSet.isEmpty() || (atType.selectionSet.selections().length === 1 && atType.selectionSet.selections()[0].isTypenameField())) {
2468
2558
  continue;
2469
2559
  }
2470
2560
 
2471
- const res = subSelection.contains(selectionSetAtType);
2561
+ // As we check inclusion, we ignore the case where the fragment queries __typename but the subSelection does not.
2562
+ // The rational is that querying `__typename` unecessarily is mostly harmless (it always works and it's super cheap)
2563
+ // so we don't want to not use a fragment just to save querying a `__typename` in a few cases. But the underlying
2564
+ // context of why this matters is that the query planner always requests __typename for abstract type, and will do
2565
+ // so in fragments too, but we can have a field that _does_ return an abstract type within a fragment, but that
2566
+ // _does not_ end up returning an abstract type when applied in a "more specific" context (think a fragment on
2567
+ // an interface I1 where a inside field returns another interface I2, but applied in the context of a implementation
2568
+ // type of I1 where that particular field returns an implementation of I2 rather than I2 directly; we would have
2569
+ // added __typename to the fragment (because it's all interfaces), but the selection itself, which only deals
2570
+ // with object type, may not have __typename requested; using the fragment might still be a good idea, and
2571
+ // querying __typename needlessly is a very small price to pay for that).
2572
+ const res = subSelection.contains(atType.selectionSet, { ignoreMissingTypename: true });
2472
2573
 
2473
2574
  if (res === ContainsResult.EQUAL) {
2474
2575
  if (canUseFullMatchingFragment(candidate)) {
@@ -2558,54 +2659,24 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
2558
2659
  }
2559
2660
  }
2560
2661
 
2561
- class FieldsConflictValidator {
2662
+ class FieldsConflictMultiBranchValidator {
2562
2663
  private usedSpreadTrimmedPartAtLevel?: FieldsConflictValidator[];
2563
2664
 
2564
- private constructor(
2565
- private readonly byResponseName: Map<string, Map<Field, FieldsConflictValidator | null>>,
2665
+ constructor(
2666
+ private readonly validators: FieldsConflictValidator[],
2566
2667
  ) {
2567
2668
  }
2568
2669
 
2569
- static build(s: SelectionSet): FieldsConflictValidator {
2570
- return FieldsConflictValidator.forLevel(s.fieldsInSet());
2670
+ static ofInitial(validator: FieldsConflictValidator): FieldsConflictMultiBranchValidator {
2671
+ return new FieldsConflictMultiBranchValidator([validator]);
2571
2672
  }
2572
2673
 
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;
2674
+ forField(field: Field): FieldsConflictMultiBranchValidator {
2675
+ const forAllBranches = this.validators.flatMap((vs) => vs.forField(field));
2676
+ // As this is called on (non-leaf) field from the same query on which we have build the initial validators, we
2677
+ // should find at least one validator.
2678
+ assert(forAllBranches.length > 0, `Shoud have found at least one validator for ${field}`);
2679
+ return new FieldsConflictMultiBranchValidator(forAllBranches);
2609
2680
  }
2610
2681
 
2611
2682
  // At this point, we known that the fragment, restricted to the current parent type, matches a subset of the
@@ -2627,7 +2698,7 @@ class FieldsConflictValidator {
2627
2698
  return true;
2628
2699
  }
2629
2700
 
2630
- if (!this.doMergeWith(validator)) {
2701
+ if (!this.validators.every((v) => v.doMergeWith(validator))) {
2631
2702
  return false;
2632
2703
  }
2633
2704
 
@@ -2654,6 +2725,61 @@ class FieldsConflictValidator {
2654
2725
  this.usedSpreadTrimmedPartAtLevel.push(validator);
2655
2726
  return true;
2656
2727
  }
2728
+ }
2729
+
2730
+ class FieldsConflictValidator {
2731
+ private constructor(
2732
+ private readonly byResponseName: Map<string, Map<Field, FieldsConflictValidator | null>>,
2733
+ ) {
2734
+ }
2735
+
2736
+ static build(s: SelectionSet): FieldsConflictValidator {
2737
+ return FieldsConflictValidator.forLevel(s.fieldsInSet());
2738
+ }
2739
+
2740
+ private static forLevel(level: CollectedFieldsInSet): FieldsConflictValidator {
2741
+ const atLevel = new Map<string, Map<Field, CollectedFieldsInSet | null>>();
2742
+
2743
+ for (const { field } of level) {
2744
+ const responseName = field.element.responseName();
2745
+ let atResponseName = atLevel.get(responseName);
2746
+ if (!atResponseName) {
2747
+ atResponseName = new Map<Field, CollectedFieldsInSet>();
2748
+ atLevel.set(responseName, atResponseName);
2749
+ }
2750
+ if (field.selectionSet) {
2751
+ // It's unlikely that we've seen the same `field.element` as we don't particularly "intern" `Field` object (so even if the exact same field
2752
+ // is used in 2 parts of a selection set, it will probably be a different `Field` object), so the `get` below will probably mostly return `undefined`,
2753
+ // but it wouldn't be incorrect to re-use a `Field` object multiple side, so no reason not to handle that correctly.
2754
+ let forField = atResponseName.get(field.element) ?? [];
2755
+ atResponseName.set(field.element, forField.concat(field.selectionSet.fieldsInSet()));
2756
+ } else {
2757
+ // Note that whether a `FieldSelection` has `selectionSet` or not is entirely determined by whether the field type is a composite type
2758
+ // or not, so even if we've seen a previous version of `field.element` before, we know it's guarantee to have had no `selectionSet`.
2759
+ // So the `set` below may overwrite a previous entry, but it would be a `null` so no harm done.
2760
+ atResponseName.set(field.element, null);
2761
+ }
2762
+ }
2763
+
2764
+ const byResponseName = new Map<string, Map<Field, FieldsConflictValidator | null>>();
2765
+ for (const [name, level] of atLevel.entries()) {
2766
+ const atResponseName = new Map<Field, FieldsConflictValidator | null>();
2767
+ for (const [field, collectedFields] of level) {
2768
+ const validator = collectedFields ? FieldsConflictValidator.forLevel(collectedFields) : null;
2769
+ atResponseName.set(field, validator);
2770
+ }
2771
+ byResponseName.set(name, atResponseName);
2772
+ }
2773
+ return new FieldsConflictValidator(byResponseName);
2774
+ }
2775
+
2776
+ forField(field: Field): FieldsConflictValidator[] {
2777
+ const byResponseName = this.byResponseName.get(field.responseName());
2778
+ if (!byResponseName) {
2779
+ return [];
2780
+ }
2781
+ return mapValues(byResponseName).filter((v): v is FieldsConflictValidator => !!v);
2782
+ }
2657
2783
 
2658
2784
  doMergeWith(that: FieldsConflictValidator): boolean {
2659
2785
  for (const [responseName, thisFields] of this.byResponseName.entries()) {
@@ -2761,7 +2887,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2761
2887
  return this.element.key();
2762
2888
  }
2763
2889
 
2764
- optimize(fragments: NamedFragments, validator: FieldsConflictValidator): Selection {
2890
+ optimize(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): Selection {
2765
2891
  const fieldBaseType = baseType(this.element.definition.type!);
2766
2892
  if (!isCompositeType(fieldBaseType) || !this.selectionSet) {
2767
2893
  return this;
@@ -2770,22 +2896,20 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2770
2896
  const fieldValidator = validator.forField(this.element);
2771
2897
 
2772
2898
  // First, see if we can reuse fragments for the selection of this field.
2773
- let optimizedSelection = this.selectionSet;
2774
- if (isCompositeType(fieldBaseType) && this.selectionSet) {
2775
- const optimized = this.tryOptimizeSubselectionWithFragments({
2776
- parentType: fieldBaseType,
2777
- subSelection: this.selectionSet,
2778
- fragments,
2779
- validator: fieldValidator,
2780
- // We can never apply a fragments that has directives on it at the field level.
2781
- canUseFullMatchingFragment: (fragment) => fragment.appliedDirectives.length === 0,
2782
- });
2899
+ const optimized = this.tryOptimizeSubselectionWithFragments({
2900
+ parentType: fieldBaseType,
2901
+ subSelection: this.selectionSet,
2902
+ fragments,
2903
+ validator: fieldValidator,
2904
+ // We can never apply a fragments that has directives on it at the field level.
2905
+ canUseFullMatchingFragment: (fragment) => fragment.appliedDirectives.length === 0,
2906
+ });
2783
2907
 
2784
- if (optimized instanceof NamedFragmentDefinition) {
2785
- optimizedSelection = selectionSetOf(fieldBaseType, new FragmentSpreadSelection(fieldBaseType, fragments, optimized, []));
2786
- } else {
2787
- optimizedSelection = optimized;
2788
- }
2908
+ let optimizedSelection;
2909
+ if (optimized instanceof NamedFragmentDefinition) {
2910
+ optimizedSelection = selectionSetOf(fieldBaseType, new FragmentSpreadSelection(fieldBaseType, fragments, optimized, []));
2911
+ } else {
2912
+ optimizedSelection = optimized;
2789
2913
  }
2790
2914
 
2791
2915
  // Then, recurse inside the field sub-selection (note that if we matched some fragments above,
@@ -2828,12 +2952,24 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2828
2952
  * Obviously, this operation will only succeed if this selection (both the field itself and its subselections)
2829
2953
  * make sense from the provided parent type. If this is not the case, this method will throw.
2830
2954
  */
2831
- rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FieldSelection {
2955
+ rebaseOn({
2956
+ parentType,
2957
+ fragments,
2958
+ errorIfCannotRebase,
2959
+ }: {
2960
+ parentType: CompositeType,
2961
+ fragments: NamedFragments | undefined,
2962
+ errorIfCannotRebase: boolean,
2963
+ }): FieldSelection | undefined {
2832
2964
  if (this.element.parentType === parentType) {
2833
2965
  return this;
2834
2966
  }
2835
2967
 
2836
- const rebasedElement = this.element.rebaseOn(parentType);
2968
+ const rebasedElement = this.element.rebaseOn({ parentType, errorIfCannotRebase });
2969
+ if (!rebasedElement) {
2970
+ return undefined;
2971
+ }
2972
+
2837
2973
  if (!this.selectionSet) {
2838
2974
  return this.withUpdatedElement(rebasedElement);
2839
2975
  }
@@ -2844,7 +2980,8 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2844
2980
  }
2845
2981
 
2846
2982
  validate(isCompositeType(rebasedBase), () => `Cannot rebase field selection ${this} on ${parentType}: rebased field base return type ${rebasedBase} is not composite`);
2847
- return this.withUpdatedComponents(rebasedElement, this.selectionSet.rebaseOn(rebasedBase, fragments));
2983
+ const rebasedSelectionSet = this.selectionSet.rebaseOn({ parentType: rebasedBase, fragments, errorIfCannotRebase });
2984
+ return rebasedSelectionSet.isEmpty() ? undefined : this.withUpdatedComponents(rebasedElement, rebasedSelectionSet);
2848
2985
  }
2849
2986
 
2850
2987
  /**
@@ -2951,7 +3088,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2951
3088
  return !!that.selectionSet && this.selectionSet.equals(that.selectionSet);
2952
3089
  }
2953
3090
 
2954
- contains(that: Selection): ContainsResult {
3091
+ contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
2955
3092
  if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
2956
3093
  return ContainsResult.NOT_CONTAINED;
2957
3094
  }
@@ -2961,7 +3098,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
2961
3098
  return ContainsResult.EQUAL;
2962
3099
  }
2963
3100
  assert(that.selectionSet, '`this` and `that` have the same element, so if one has sub-selection, the other one should too')
2964
- return this.selectionSet.contains(that.selectionSet);
3101
+ return this.selectionSet.contains(that.selectionSet, options);
2965
3102
  }
2966
3103
 
2967
3104
  toString(expandFragments: boolean = true, indent?: string): string {
@@ -3005,7 +3142,27 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
3005
3142
 
3006
3143
  abstract equals(that: Selection): boolean;
3007
3144
 
3008
- abstract contains(that: Selection): ContainsResult;
3145
+ abstract contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult;
3146
+
3147
+ normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
3148
+ const thisCondition = this.element.typeCondition;
3149
+
3150
+ // This method assumes by contract that `parentType` runtimes intersects `this.parentType`'s, but `parentType`
3151
+ // runtimes may be a subset. So first check if the selection should not be discarded on that account (that
3152
+ // is, we should not keep the selection if its condition runtimes don't intersect at all with those of
3153
+ // `parentType` as that would ultimately make an invalid selection set).
3154
+ if (thisCondition && parentType !== this.parentType) {
3155
+ const conditionRuntimes = possibleRuntimeTypes(thisCondition);
3156
+ const typeRuntimes = possibleRuntimeTypes(parentType);
3157
+ if (!conditionRuntimes.some((t) => typeRuntimes.includes(t))) {
3158
+ return undefined;
3159
+ }
3160
+ }
3161
+
3162
+ return this.normalizeKnowingItIntersects({ parentType, recursive });
3163
+ }
3164
+
3165
+ protected abstract normalizeKnowingItIntersects({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined;
3009
3166
  }
3010
3167
 
3011
3168
  class InlineFragmentSelection extends FragmentSelection {
@@ -3042,18 +3199,31 @@ class InlineFragmentSelection extends FragmentSelection {
3042
3199
  this.selectionSet.validate(variableDefinitions);
3043
3200
  }
3044
3201
 
3045
- rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
3202
+ rebaseOn({
3203
+ parentType,
3204
+ fragments,
3205
+ errorIfCannotRebase,
3206
+ }: {
3207
+ parentType: CompositeType,
3208
+ fragments: NamedFragments | undefined,
3209
+ errorIfCannotRebase: boolean,
3210
+ }): FragmentSelection | undefined {
3046
3211
  if (this.parentType === parentType) {
3047
3212
  return this;
3048
3213
  }
3049
3214
 
3050
- const rebasedFragment = this.element.rebaseOn(parentType);
3215
+ const rebasedFragment = this.element.rebaseOn({ parentType, errorIfCannotRebase });
3216
+ if (!rebasedFragment) {
3217
+ return undefined;
3218
+ }
3219
+
3051
3220
  const rebasedCastedType = rebasedFragment.castedType();
3052
3221
  if (rebasedCastedType === this.selectionSet.parentType) {
3053
3222
  return this.withUpdatedElement(rebasedFragment);
3054
3223
  }
3055
3224
 
3056
- return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType, fragments));
3225
+ const rebasedSelectionSet = this.selectionSet.rebaseOn({ parentType: rebasedCastedType, fragments, errorIfCannotRebase });
3226
+ return rebasedSelectionSet.isEmpty() ? undefined : this.withUpdatedComponents(rebasedFragment, rebasedSelectionSet);
3057
3227
  }
3058
3228
 
3059
3229
  canAddTo(parentType: CompositeType): boolean {
@@ -3090,7 +3260,7 @@ class InlineFragmentSelection extends FragmentSelection {
3090
3260
  };
3091
3261
  }
3092
3262
 
3093
- optimize(fragments: NamedFragments, validator: FieldsConflictValidator): FragmentSelection {
3263
+ optimize(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): FragmentSelection {
3094
3264
  let optimizedSelection = this.selectionSet;
3095
3265
 
3096
3266
  // First, see if we can reuse fragments for the selection of this field.
@@ -3173,21 +3343,9 @@ class InlineFragmentSelection extends FragmentSelection {
3173
3343
  : this.withUpdatedComponents(newElement, newSelection);
3174
3344
  }
3175
3345
 
3176
- normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
3346
+ protected normalizeKnowingItIntersects({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
3177
3347
  const thisCondition = this.element.typeCondition;
3178
3348
 
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;
3188
- }
3189
- }
3190
-
3191
3349
  // We know the condition is "valid", but it may not be useful. That said, if the condition has directives,
3192
3350
  // we preserve the fragment no matter what.
3193
3351
  if (this.element.appliedDirectives.length === 0) {
@@ -3214,7 +3372,8 @@ class InlineFragmentSelection extends FragmentSelection {
3214
3372
  return undefined;
3215
3373
  } else {
3216
3374
  return this.withUpdatedComponents(
3217
- this.element.rebaseOn(parentType),
3375
+ // We should be able to rebase, or there is a bug, so error if that is the case.
3376
+ this.element.rebaseOnOrError(parentType),
3218
3377
  selectionSetOfElement(
3219
3378
  new Field(
3220
3379
  (this.element.typeCondition ?? parentType).typenameField()!,
@@ -3266,7 +3425,7 @@ class InlineFragmentSelection extends FragmentSelection {
3266
3425
 
3267
3426
  return this.parentType === parentType && this.selectionSet === normalizedSelectionSet
3268
3427
  ? this
3269
- : this.withUpdatedComponents(this.element.rebaseOn(parentType), normalizedSelectionSet);
3428
+ : this.withUpdatedComponents(this.element.rebaseOnOrError(parentType), normalizedSelectionSet);
3270
3429
  }
3271
3430
 
3272
3431
  expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection {
@@ -3283,12 +3442,12 @@ class InlineFragmentSelection extends FragmentSelection {
3283
3442
  && this.selectionSet.equals(that.selectionSet);
3284
3443
  }
3285
3444
 
3286
- contains(that: Selection): ContainsResult {
3445
+ contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
3287
3446
  if (!(that instanceof FragmentSelection) || !this.element.equals(that.element)) {
3288
3447
  return ContainsResult.NOT_CONTAINED;
3289
3448
  }
3290
3449
 
3291
- return this.selectionSet.contains(that.selectionSet);
3450
+ return this.selectionSet.contains(that.selectionSet, options);
3292
3451
  }
3293
3452
 
3294
3453
  toString(expandFragments: boolean = true, indent?: string): string {
@@ -3327,11 +3486,11 @@ class FragmentSpreadSelection extends FragmentSelection {
3327
3486
  assert(false, `Unsupported`);
3328
3487
  }
3329
3488
 
3330
- normalize({ parentType }: { parentType: CompositeType }): FragmentSelection {
3489
+ normalizeKnowingItIntersects({ parentType }: { parentType: CompositeType }): FragmentSelection {
3331
3490
  // We must update the spread parent type if necessary since we're not going deeper,
3332
3491
  // or we'll be fundamentally losing context.
3333
3492
  assert(parentType.schema() === this.parentType.schema(), 'Should not try to normalize using a type from another schema');
3334
- return this.rebaseOn(parentType, this.fragments);
3493
+ return this.rebaseOnOrError({ parentType, fragments: this.fragments });
3335
3494
  }
3336
3495
 
3337
3496
  validate(): void {
@@ -3363,11 +3522,19 @@ class FragmentSpreadSelection extends FragmentSelection {
3363
3522
  };
3364
3523
  }
3365
3524
 
3366
- optimize(_1: NamedFragments, _2: FieldsConflictValidator): FragmentSelection {
3525
+ optimize(_1: NamedFragments, _2: FieldsConflictMultiBranchValidator): FragmentSelection {
3367
3526
  return this;
3368
3527
  }
3369
3528
 
3370
- rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): FragmentSelection {
3529
+ rebaseOn({
3530
+ parentType,
3531
+ fragments,
3532
+ errorIfCannotRebase,
3533
+ }: {
3534
+ parentType: CompositeType,
3535
+ fragments: NamedFragments | undefined,
3536
+ errorIfCannotRebase: boolean,
3537
+ }): FragmentSelection | undefined {
3371
3538
  // We preserve the parent type here, to make sure we don't lose context, but we actually don't
3372
3539
  // want to expand the spread as that would compromise the code that optimize subgraph fetches to re-use named
3373
3540
  // fragments.
@@ -3387,7 +3554,14 @@ class FragmentSpreadSelection extends FragmentSelection {
3387
3554
  assert(fragments || this.parentType.schema() === parentType.schema(), `Must provide fragments is rebasing on other schema`);
3388
3555
  const newFragments = fragments ?? this.fragments;
3389
3556
  const namedFragment = newFragments.get(this.namedFragment.name);
3390
- assert(namedFragment, () => `Cannot rebase ${this} if it isn't part of the provided fragments`);
3557
+ // If we're rebasing on another schema (think a subgraph), then named fragments will have been rebased on that, and some
3558
+ // of them may not contain anything that is on that subgraph, in which case they will not have been included at all.
3559
+ // If so, then as long as we're not ask to error if we cannot rebase, then we're happy to skip that spread (since again,
3560
+ // it expands to nothing that apply on the schema).
3561
+ if (!namedFragment) {
3562
+ validate(!errorIfCannotRebase, () => `Cannot rebase ${this.toString(false)} if it isn't part of the provided fragments`);
3563
+ return undefined;
3564
+ }
3391
3565
  return new FragmentSpreadSelection(
3392
3566
  parentType,
3393
3567
  newFragments,
@@ -3443,7 +3617,7 @@ class FragmentSpreadSelection extends FragmentSelection {
3443
3617
  && sameDirectiveApplications(this.spreadDirectives, that.spreadDirectives);
3444
3618
  }
3445
3619
 
3446
- contains(that: Selection): ContainsResult {
3620
+ contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
3447
3621
  if (this.equals(that)) {
3448
3622
  return ContainsResult.EQUAL;
3449
3623
  }
@@ -3452,7 +3626,7 @@ class FragmentSpreadSelection extends FragmentSelection {
3452
3626
  return ContainsResult.NOT_CONTAINED;
3453
3627
  }
3454
3628
 
3455
- return this.selectionSet.contains(that.selectionSet);
3629
+ return this.selectionSet.contains(that.selectionSet, options);
3456
3630
  }
3457
3631
 
3458
3632
  toString(expandFragments: boolean = true, indent?: string): string {