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