@apollo/federation-internals 2.4.0 → 2.4.2
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 +36 -0
- package/dist/argumentCompositionStrategies.d.ts +34 -0
- package/dist/argumentCompositionStrategies.d.ts.map +1 -0
- package/dist/argumentCompositionStrategies.js +35 -0
- package/dist/argumentCompositionStrategies.js.map +1 -0
- package/dist/coreSpec.d.ts +12 -3
- package/dist/coreSpec.d.ts.map +1 -1
- package/dist/coreSpec.js +69 -18
- package/dist/coreSpec.js.map +1 -1
- package/dist/definitions.d.ts +1 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +30 -27
- package/dist/definitions.js.map +1 -1
- package/dist/directiveAndTypeSpecification.d.ts +26 -7
- package/dist/directiveAndTypeSpecification.d.ts.map +1 -1
- package/dist/directiveAndTypeSpecification.js +56 -4
- package/dist/directiveAndTypeSpecification.js.map +1 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +24 -2
- package/dist/federation.js.map +1 -1
- package/dist/federationSpec.d.ts +2 -13
- package/dist/federationSpec.d.ts.map +1 -1
- package/dist/federationSpec.js +10 -60
- package/dist/federationSpec.js.map +1 -1
- package/dist/inaccessibleSpec.d.ts +0 -2
- package/dist/inaccessibleSpec.d.ts.map +1 -1
- package/dist/inaccessibleSpec.js +3 -6
- package/dist/inaccessibleSpec.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/knownCoreFeatures.d.ts +1 -0
- package/dist/knownCoreFeatures.d.ts.map +1 -1
- package/dist/knownCoreFeatures.js +5 -1
- package/dist/knownCoreFeatures.js.map +1 -1
- package/dist/operations.d.ts +92 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +192 -48
- package/dist/operations.js.map +1 -1
- package/dist/print.d.ts +7 -1
- package/dist/print.d.ts.map +1 -1
- package/dist/print.js +33 -5
- package/dist/print.js.map +1 -1
- package/dist/tagSpec.d.ts +0 -2
- package/dist/tagSpec.d.ts.map +1 -1
- package/dist/tagSpec.js +4 -10
- package/dist/tagSpec.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/directiveAndTypeSpecifications.test.ts +41 -0
- package/src/__tests__/operations.test.ts +598 -45
- package/src/__tests__/schemaUpgrader.test.ts +1 -1
- package/src/argumentCompositionStrategies.ts +39 -0
- package/src/coreSpec.ts +94 -34
- package/src/definitions.ts +35 -29
- package/src/directiveAndTypeSpecification.ts +101 -14
- package/src/federation.ts +33 -4
- package/src/federationSpec.ts +13 -73
- package/src/inaccessibleSpec.ts +4 -11
- package/src/index.ts +3 -0
- package/src/knownCoreFeatures.ts +9 -0
- package/src/operations.ts +318 -102
- package/src/print.ts +39 -4
- package/src/tagSpec.ts +4 -12
- 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 { isSubtype, 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) {
|
|
@@ -676,12 +678,12 @@ function isUselessFollowupElement(first: OperationElement, followup: OperationEl
|
|
|
676
678
|
: first.typeCondition;
|
|
677
679
|
|
|
678
680
|
// The followup is useless if it's a fragment (with no directives we would want to preserve) whose type
|
|
679
|
-
// is already that of the first element.
|
|
681
|
+
// is already that of the first element (or a supertype).
|
|
680
682
|
return !!typeOfFirst
|
|
681
683
|
&& followup.kind === 'FragmentElement'
|
|
682
684
|
&& !!followup.typeCondition
|
|
683
685
|
&& (followup.appliedDirectives.length === 0 || isDirectiveApplicationsSubset(conditionals, followup.appliedDirectives))
|
|
684
|
-
&&
|
|
686
|
+
&& isSubtype(followup.typeCondition, typeOfFirst);
|
|
685
687
|
}
|
|
686
688
|
|
|
687
689
|
export type RootOperationPath = {
|
|
@@ -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
|
}
|
|
@@ -1551,9 +1598,19 @@ function addOneToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, s
|
|
|
1551
1598
|
}
|
|
1552
1599
|
}
|
|
1553
1600
|
|
|
1601
|
+
function maybeRebaseOnSchema(toRebase: CompositeType, schema: Schema): CompositeType {
|
|
1602
|
+
if (toRebase.schema() === schema) {
|
|
1603
|
+
return toRebase;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const rebased = schema.type(toRebase.name);
|
|
1607
|
+
assert(rebased && isCompositeType(rebased), () => `Expected ${toRebase} to exists and be composite in the rebased schema, but got ${rebased?.kind}`);
|
|
1608
|
+
return rebased;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1554
1611
|
function isUnecessaryFragment(parentType: CompositeType, fragment: FragmentSelection): boolean {
|
|
1555
1612
|
return fragment.element.appliedDirectives.length === 0
|
|
1556
|
-
&& (!fragment.element.typeCondition ||
|
|
1613
|
+
&& (!fragment.element.typeCondition || isSubtype(maybeRebaseOnSchema(fragment.element.typeCondition, parentType.schema()), parentType));
|
|
1557
1614
|
}
|
|
1558
1615
|
|
|
1559
1616
|
function withUnecessaryFragmentsRemoved(
|
|
@@ -1765,7 +1822,6 @@ export function selectionOfElement(element: OperationElement, subSelection?: Sel
|
|
|
1765
1822
|
}
|
|
1766
1823
|
|
|
1767
1824
|
export type Selection = FieldSelection | FragmentSelection;
|
|
1768
|
-
|
|
1769
1825
|
abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf extends undefined | never, TOwnType extends AbstractSelection<TElement, TIsLeaf, TOwnType>> {
|
|
1770
1826
|
constructor(
|
|
1771
1827
|
readonly element: TElement,
|
|
@@ -1836,6 +1892,63 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
1836
1892
|
abstract expandFragments(names: string[], updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
|
|
1837
1893
|
|
|
1838
1894
|
abstract trimUnsatisfiableBranches(parentType: CompositeType): TOwnType | SelectionSet | undefined;
|
|
1895
|
+
|
|
1896
|
+
minus(that: Selection): TOwnType | undefined {
|
|
1897
|
+
// If there is a subset, then we compute the diff of the subset and add that (if not empty).
|
|
1898
|
+
// Otherwise, we have no diff.
|
|
1899
|
+
if (this.selectionSet && that.selectionSet) {
|
|
1900
|
+
const updatedSubSelectionSet = this.selectionSet.minus(that.selectionSet);
|
|
1901
|
+
if (!updatedSubSelectionSet.isEmpty()) {
|
|
1902
|
+
return this.withUpdatedSelectionSet(updatedSubSelectionSet);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return undefined;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
protected tryOptimizeSubselectionOnce(_: {
|
|
1909
|
+
parentType: CompositeType,
|
|
1910
|
+
subSelection: SelectionSet,
|
|
1911
|
+
candidates: NamedFragmentDefinition[],
|
|
1912
|
+
fragments: NamedFragments,
|
|
1913
|
+
}): {
|
|
1914
|
+
spread?: FragmentSpreadSelection,
|
|
1915
|
+
optimizedSelection?: SelectionSet,
|
|
1916
|
+
hasDiff?: boolean,
|
|
1917
|
+
} {
|
|
1918
|
+
// Field and inline fragment override this, but this should never be called for a spread.
|
|
1919
|
+
assert(false, `UNSUPPORTED`);
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
protected tryOptimizeSubselectionWithFragments({
|
|
1923
|
+
parentType,
|
|
1924
|
+
subSelection,
|
|
1925
|
+
fragments,
|
|
1926
|
+
fragmentFilter,
|
|
1927
|
+
}: {
|
|
1928
|
+
parentType: CompositeType,
|
|
1929
|
+
subSelection: SelectionSet,
|
|
1930
|
+
fragments: NamedFragments,
|
|
1931
|
+
fragmentFilter?: (f: NamedFragmentDefinition) => boolean,
|
|
1932
|
+
}): SelectionSet | FragmentSpreadSelection {
|
|
1933
|
+
let candidates = fragments.maybeApplyingAtType(parentType);
|
|
1934
|
+
if (fragmentFilter) {
|
|
1935
|
+
candidates = candidates.filter(fragmentFilter);
|
|
1936
|
+
}
|
|
1937
|
+
let shouldTryAgain: boolean;
|
|
1938
|
+
do {
|
|
1939
|
+
const { spread, optimizedSelection, hasDiff } = this.tryOptimizeSubselectionOnce({ parentType, subSelection, candidates, fragments });
|
|
1940
|
+
if (optimizedSelection) {
|
|
1941
|
+
subSelection = optimizedSelection;
|
|
1942
|
+
} else if (spread) {
|
|
1943
|
+
return spread;
|
|
1944
|
+
}
|
|
1945
|
+
shouldTryAgain = !!spread && !!hasDiff;
|
|
1946
|
+
if (shouldTryAgain) {
|
|
1947
|
+
candidates = candidates.filter((c) => c !== spread?.namedFragment)
|
|
1948
|
+
}
|
|
1949
|
+
} while (shouldTryAgain);
|
|
1950
|
+
return subSelection;
|
|
1951
|
+
}
|
|
1839
1952
|
}
|
|
1840
1953
|
|
|
1841
1954
|
export class FieldSelection extends AbstractSelection<Field<any>, undefined, FieldSelection> {
|
|
@@ -1865,41 +1978,21 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
1865
1978
|
}
|
|
1866
1979
|
|
|
1867
1980
|
optimize(fragments: NamedFragments): Selection {
|
|
1868
|
-
|
|
1981
|
+
let optimizedSelection = this.selectionSet ? this.selectionSet.optimize(fragments) : undefined;
|
|
1869
1982
|
const fieldBaseType = baseType(this.element.definition.type!);
|
|
1870
1983
|
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
|
-
}
|
|
1984
|
+
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
1985
|
+
parentType: fieldBaseType,
|
|
1986
|
+
subSelection: optimizedSelection,
|
|
1987
|
+
fragments,
|
|
1988
|
+
// We can never apply a fragments that has directives on it at the field level (but when those are expanded,
|
|
1989
|
+
// their type condition would always be preserved due to said applied directives, so they will always
|
|
1990
|
+
// be handled by `InlineFragmentSelection.optimize` anyway).
|
|
1991
|
+
fragmentFilter: (f) => f.appliedDirectives.length === 0,
|
|
1992
|
+
});
|
|
1993
|
+
|
|
1994
|
+
assert(!(optimized instanceof FragmentSpreadSelection), 'tryOptimizeSubselectionOnce should never return only a spread');
|
|
1995
|
+
optimizedSelection = optimized;
|
|
1903
1996
|
}
|
|
1904
1997
|
|
|
1905
1998
|
return this.selectionSet === optimizedSelection
|
|
@@ -1907,6 +2000,37 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
1907
2000
|
: new FieldSelection(this.element, optimizedSelection);
|
|
1908
2001
|
}
|
|
1909
2002
|
|
|
2003
|
+
protected tryOptimizeSubselectionOnce({
|
|
2004
|
+
parentType,
|
|
2005
|
+
subSelection,
|
|
2006
|
+
candidates,
|
|
2007
|
+
fragments,
|
|
2008
|
+
}: {
|
|
2009
|
+
parentType: CompositeType,
|
|
2010
|
+
subSelection: SelectionSet,
|
|
2011
|
+
candidates: NamedFragmentDefinition[],
|
|
2012
|
+
fragments: NamedFragments,
|
|
2013
|
+
}): {
|
|
2014
|
+
spread?: FragmentSpreadSelection,
|
|
2015
|
+
optimizedSelection?: SelectionSet,
|
|
2016
|
+
hasDiff?: boolean,
|
|
2017
|
+
}{
|
|
2018
|
+
let optimizedSelection = subSelection;
|
|
2019
|
+
for (const candidate of candidates) {
|
|
2020
|
+
const { contains, diff } = optimizedSelection.diffIfContains(candidate.selectionSet);
|
|
2021
|
+
if (contains) {
|
|
2022
|
+
// We can optimize the selection with this fragment. The replaced sub-selection will be
|
|
2023
|
+
// comprised of this new spread and the remaining `diff` if there is any.
|
|
2024
|
+
const spread = new FragmentSpreadSelection(parentType, fragments, candidate, []);
|
|
2025
|
+
optimizedSelection = diff
|
|
2026
|
+
? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
|
|
2027
|
+
: selectionSetOf(parentType, spread);
|
|
2028
|
+
return { spread, optimizedSelection, hasDiff: !!diff }
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return {};
|
|
2032
|
+
}
|
|
2033
|
+
|
|
1910
2034
|
filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
|
|
1911
2035
|
if (!this.selectionSet) {
|
|
1912
2036
|
return predicate(this) ? this : undefined;
|
|
@@ -2063,6 +2187,11 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2063
2187
|
return !!this.selectionSet && this.selectionSet.contains(that.selectionSet);
|
|
2064
2188
|
}
|
|
2065
2189
|
|
|
2190
|
+
isUnecessaryInlineFragment(_: CompositeType): this is InlineFragmentSelection {
|
|
2191
|
+
// Overridden by inline fragments
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2066
2195
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
2067
2196
|
return (indent ?? '') + this.element + (this.selectionSet ? ' ' + this.selectionSet.toString(expandFragments, true, indent) : '');
|
|
2068
2197
|
}
|
|
@@ -2103,20 +2232,19 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
|
|
|
2103
2232
|
return this.element.hasDefer() || this.selectionSet.hasDefer();
|
|
2104
2233
|
}
|
|
2105
2234
|
|
|
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
|
-
}
|
|
2235
|
+
abstract equals(that: Selection): boolean;
|
|
2114
2236
|
|
|
2115
|
-
contains(that: Selection): boolean
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2237
|
+
abstract contains(that: Selection): boolean;
|
|
2238
|
+
|
|
2239
|
+
isUnecessaryInlineFragment(parentType: CompositeType): boolean {
|
|
2240
|
+
return this.element.appliedDirectives.length === 0
|
|
2241
|
+
&& !!this.element.typeCondition
|
|
2242
|
+
&& (
|
|
2243
|
+
this.element.typeCondition.name === parentType.name
|
|
2244
|
+
|| (isObjectType(parentType) && possibleRuntimeTypes(this.element.typeCondition).some((t) => t.name === parentType.name))
|
|
2245
|
+
);
|
|
2119
2246
|
}
|
|
2247
|
+
|
|
2120
2248
|
}
|
|
2121
2249
|
|
|
2122
2250
|
class InlineFragmentSelection extends FragmentSelection {
|
|
@@ -2202,37 +2330,83 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2202
2330
|
let optimizedSelection = this.selectionSet.optimize(fragments);
|
|
2203
2331
|
const typeCondition = this.element.typeCondition;
|
|
2204
2332
|
if (typeCondition) {
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2333
|
+
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
2334
|
+
parentType: typeCondition,
|
|
2335
|
+
subSelection: optimizedSelection,
|
|
2336
|
+
fragments,
|
|
2337
|
+
});
|
|
2338
|
+
if (optimized instanceof FragmentSpreadSelection) {
|
|
2339
|
+
// This means the whole inline fragment can be replaced by the spread.
|
|
2340
|
+
return optimized;
|
|
2341
|
+
}
|
|
2342
|
+
optimizedSelection = optimized;
|
|
2343
|
+
}
|
|
2344
|
+
return this.selectionSet === optimizedSelection
|
|
2345
|
+
? this
|
|
2346
|
+
: new InlineFragmentSelection(this.element, optimizedSelection);
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
protected tryOptimizeSubselectionOnce({
|
|
2350
|
+
parentType,
|
|
2351
|
+
subSelection,
|
|
2352
|
+
candidates,
|
|
2353
|
+
fragments,
|
|
2354
|
+
}: {
|
|
2355
|
+
parentType: CompositeType,
|
|
2356
|
+
subSelection: SelectionSet,
|
|
2357
|
+
candidates: NamedFragmentDefinition[],
|
|
2358
|
+
fragments: NamedFragments,
|
|
2359
|
+
}): {
|
|
2360
|
+
spread?: FragmentSpreadSelection,
|
|
2361
|
+
optimizedSelection?: SelectionSet,
|
|
2362
|
+
hasDiff?: boolean,
|
|
2363
|
+
}{
|
|
2364
|
+
let optimizedSelection = subSelection;
|
|
2365
|
+
for (const candidate of candidates) {
|
|
2366
|
+
const { contains, diff } = optimizedSelection.diffIfContains(candidate.selectionSet);
|
|
2367
|
+
if (contains) {
|
|
2368
|
+
// The candidate selection is included in our sub-selection. One remaining thing to take into account
|
|
2369
|
+
// is applied directives: if the candidate has directives, then we can only use it if 1) there is
|
|
2370
|
+
// no `diff`, 2) the type condition of this fragment matches the candidate one and 3) the directives
|
|
2371
|
+
// in question are also on this very fragment. In that case, we can replace this whole inline fragment
|
|
2372
|
+
// by a spread of the candidate.
|
|
2373
|
+
if (!diff && sameType(this.element.typeCondition!, candidate.typeCondition)) {
|
|
2374
|
+
// We can potentially replace the whole fragment by the candidate; but as said above, still needs
|
|
2375
|
+
// to check the directives.
|
|
2376
|
+
let spreadDirectives: Directive<any>[] = this.element.appliedDirectives;
|
|
2377
|
+
if (candidate.appliedDirectives.length > 0) {
|
|
2210
2378
|
const { isSubset, difference } = diffDirectives(this.element.appliedDirectives, candidate.appliedDirectives);
|
|
2211
2379
|
if (!isSubset) {
|
|
2212
|
-
//
|
|
2213
|
-
//
|
|
2380
|
+
// While the candidate otherwise match, it has directives that are not on this element, so we
|
|
2381
|
+
// cannot reuse it.
|
|
2214
2382
|
continue;
|
|
2215
2383
|
}
|
|
2384
|
+
// Otherwise, any directives on this element that are not on the candidate should be kept and used
|
|
2385
|
+
// on the spread created.
|
|
2216
2386
|
spreadDirectives = difference;
|
|
2217
2387
|
}
|
|
2388
|
+
// Returning a spread without a subselection will make the code "replace" this whole inline fragment
|
|
2389
|
+
// by the spread, which is what we want. Do not that as we're replacing the whole inline fragment,
|
|
2390
|
+
// we use `this.parentType` instead of `parentType` (the later being `this.element.typeCondition` basically).
|
|
2391
|
+
return {
|
|
2392
|
+
spread: new FragmentSpreadSelection(this.parentType, fragments, candidate, spreadDirectives),
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2218
2395
|
|
|
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;
|
|
2396
|
+
// We're already dealt with the one case where we might be able to handle a candidate that has directives.
|
|
2397
|
+
if (candidate.appliedDirectives.length > 0) {
|
|
2398
|
+
continue;
|
|
2230
2399
|
}
|
|
2400
|
+
|
|
2401
|
+
const spread = new FragmentSpreadSelection(parentType, fragments, candidate, []);
|
|
2402
|
+
optimizedSelection = diff
|
|
2403
|
+
? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
|
|
2404
|
+
: selectionSetOf(parentType, spread);
|
|
2405
|
+
|
|
2406
|
+
return { spread, optimizedSelection, hasDiff: !!diff };
|
|
2231
2407
|
}
|
|
2232
2408
|
}
|
|
2233
|
-
return
|
|
2234
|
-
? this
|
|
2235
|
-
: new InlineFragmentSelection(this.element, optimizedSelection);
|
|
2409
|
+
return {};
|
|
2236
2410
|
}
|
|
2237
2411
|
|
|
2238
2412
|
withoutDefer(labelsToRemove?: Set<string>): InlineFragmentSelection | SelectionSet {
|
|
@@ -2272,10 +2446,12 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2272
2446
|
// If the current type is an object, then we never need to keep the current fragment because:
|
|
2273
2447
|
// - either the fragment is also an object, but we've eliminated the case where the 2 types are the same,
|
|
2274
2448
|
// 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.
|
|
2449
|
+
// - or it's not an object, but then the current type is more precise and no point in "casting" to a
|
|
2450
|
+
// less precise interface/union. And if the current type is not even a valid runtime of said interface/union,
|
|
2451
|
+
// then we should completely ignore the branch (or, since we're eliminating `thisCondition`, we would be
|
|
2452
|
+
// building an invalid selection).
|
|
2277
2453
|
if (isObjectType(currentType)) {
|
|
2278
|
-
if (isObjectType(thisCondition)) {
|
|
2454
|
+
if (isObjectType(thisCondition) || !possibleRuntimeTypes(thisCondition).includes(currentType)) {
|
|
2279
2455
|
return undefined;
|
|
2280
2456
|
} else {
|
|
2281
2457
|
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType);
|
|
@@ -2340,7 +2516,6 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2340
2516
|
return this.selectionSet === trimmedSelectionSet ? this : this.withUpdatedSelectionSet(trimmedSelectionSet);
|
|
2341
2517
|
}
|
|
2342
2518
|
|
|
2343
|
-
|
|
2344
2519
|
expandAllFragments(): FragmentSelection {
|
|
2345
2520
|
return this.mapToSelectionSet((s) => s.expandAllFragments());
|
|
2346
2521
|
}
|
|
@@ -2349,6 +2524,22 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2349
2524
|
return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
|
|
2350
2525
|
}
|
|
2351
2526
|
|
|
2527
|
+
equals(that: Selection): boolean {
|
|
2528
|
+
if (this === that) {
|
|
2529
|
+
return true;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
return (that instanceof FragmentSelection)
|
|
2533
|
+
&& this.element.equals(that.element)
|
|
2534
|
+
&& this.selectionSet.equals(that.selectionSet);
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
contains(that: Selection): boolean {
|
|
2538
|
+
return (that instanceof FragmentSelection)
|
|
2539
|
+
&& this.element.equals(that.element)
|
|
2540
|
+
&& this.selectionSet.contains(that.selectionSet);
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2352
2543
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
2353
2544
|
return (indent ?? '') + this.element + ' ' + this.selectionSet.toString(expandFragments, true, indent);
|
|
2354
2545
|
}
|
|
@@ -2368,7 +2559,7 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2368
2559
|
constructor(
|
|
2369
2560
|
sourceType: CompositeType,
|
|
2370
2561
|
private readonly fragments: NamedFragments,
|
|
2371
|
-
|
|
2562
|
+
readonly namedFragment: NamedFragmentDefinition,
|
|
2372
2563
|
private readonly spreadDirectives: readonly Directive<any>[],
|
|
2373
2564
|
) {
|
|
2374
2565
|
super(new FragmentElement(sourceType, namedFragment.typeCondition, namedFragment.appliedDirectives.concat(spreadDirectives)));
|
|
@@ -2474,6 +2665,31 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2474
2665
|
assert(false, 'Unsupported, see `Operation.withAllDeferLabelled`');
|
|
2475
2666
|
}
|
|
2476
2667
|
|
|
2668
|
+
minus(that: Selection): undefined {
|
|
2669
|
+
assert(this.equals(that), () => `Invalid operation for ${this.toString(false)} and ${that.toString(false)}`);
|
|
2670
|
+
return undefined;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
equals(that: Selection): boolean {
|
|
2674
|
+
if (this === that) {
|
|
2675
|
+
return true;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
return (that instanceof FragmentSpreadSelection)
|
|
2679
|
+
&& this.namedFragment.name === that.namedFragment.name
|
|
2680
|
+
&& sameDirectiveApplications(this.spreadDirectives, that.spreadDirectives);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
contains(that: Selection): boolean {
|
|
2684
|
+
if (this.equals(that)) {
|
|
2685
|
+
return true;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
return (that instanceof FragmentSelection)
|
|
2689
|
+
&& this.element.equals(that.element)
|
|
2690
|
+
&& this.selectionSet.contains(that.selectionSet);
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2477
2693
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
2478
2694
|
if (expandFragments) {
|
|
2479
2695
|
return (indent ?? '') + this.element + ' ' + this.selectionSet.toString(true, true, indent);
|