@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/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +25 -7
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/operations.d.ts +66 -16
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +175 -92
- package/dist/operations.js.map +1 -1
- package/package.json +1 -1
- package/src/extractSubgraphsFromSupergraph.ts +36 -10
- package/src/operations.ts +321 -147
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
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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
|
-
*
|
|
1102
|
-
*
|
|
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 "
|
|
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
|
|
1114
|
-
* (save for `__typename`), and
|
|
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
|
-
*
|
|
1120
|
-
*
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
1661
|
-
*
|
|
1662
|
-
*
|
|
1663
|
-
* that is
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2231
|
+
return first.rebaseOnOrError({ parentType, fragments });
|
|
2145
2232
|
}
|
|
2146
2233
|
|
|
2147
|
-
const element = updateElement(first).
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
2557
|
+
if (atType.selectionSet.isEmpty() || (atType.selectionSet.selections().length === 1 && atType.selectionSet.selections()[0].isTypenameField())) {
|
|
2468
2558
|
continue;
|
|
2469
2559
|
}
|
|
2470
2560
|
|
|
2471
|
-
|
|
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
|
|
2662
|
+
class FieldsConflictMultiBranchValidator {
|
|
2562
2663
|
private usedSpreadTrimmedPartAtLevel?: FieldsConflictValidator[];
|
|
2563
2664
|
|
|
2564
|
-
|
|
2565
|
-
private readonly
|
|
2665
|
+
constructor(
|
|
2666
|
+
private readonly validators: FieldsConflictValidator[],
|
|
2566
2667
|
) {
|
|
2567
2668
|
}
|
|
2568
2669
|
|
|
2569
|
-
static
|
|
2570
|
-
return
|
|
2670
|
+
static ofInitial(validator: FieldsConflictValidator): FieldsConflictMultiBranchValidator {
|
|
2671
|
+
return new FieldsConflictMultiBranchValidator([validator]);
|
|
2571
2672
|
}
|
|
2572
2673
|
|
|
2573
|
-
|
|
2574
|
-
const
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
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:
|
|
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
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
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
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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:
|
|
3525
|
+
optimize(_1: NamedFragments, _2: FieldsConflictMultiBranchValidator): FragmentSelection {
|
|
3367
3526
|
return this;
|
|
3368
3527
|
}
|
|
3369
3528
|
|
|
3370
|
-
rebaseOn(
|
|
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
|
-
|
|
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
|
|
3629
|
+
return this.selectionSet.contains(that.selectionSet, options);
|
|
3456
3630
|
}
|
|
3457
3631
|
|
|
3458
3632
|
toString(expandFragments: boolean = true, indent?: string): string {
|