@apollo/federation-internals 2.4.1 → 2.4.3
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 +21 -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 +68 -17
- 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 +18 -6
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +102 -37
- 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 +175 -10
- 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 +198 -40
- package/src/print.ts +39 -4
- package/src/tagSpec.ts +4 -12
- package/tsconfig.tsbuildinfo +1 -1
package/src/operations.ts
CHANGED
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
} from "./definitions";
|
|
50
50
|
import { isInterfaceObjectType } from "./federation";
|
|
51
51
|
import { ERRORS } from "./error";
|
|
52
|
-
import { sameType } from "./types";
|
|
52
|
+
import { isSubtype, sameType } from "./types";
|
|
53
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';
|
|
@@ -678,12 +678,12 @@ function isUselessFollowupElement(first: OperationElement, followup: OperationEl
|
|
|
678
678
|
: first.typeCondition;
|
|
679
679
|
|
|
680
680
|
// The followup is useless if it's a fragment (with no directives we would want to preserve) whose type
|
|
681
|
-
// is already that of the first element.
|
|
681
|
+
// is already that of the first element (or a supertype).
|
|
682
682
|
return !!typeOfFirst
|
|
683
683
|
&& followup.kind === 'FragmentElement'
|
|
684
684
|
&& !!followup.typeCondition
|
|
685
685
|
&& (followup.appliedDirectives.length === 0 || isDirectiveApplicationsSubset(conditionals, followup.appliedDirectives))
|
|
686
|
-
&&
|
|
686
|
+
&& isSubtype(followup.typeCondition, typeOfFirst);
|
|
687
687
|
}
|
|
688
688
|
|
|
689
689
|
export type RootOperationPath = {
|
|
@@ -732,8 +732,13 @@ export class Operation {
|
|
|
732
732
|
// probably not noticeable in practice so ...).
|
|
733
733
|
const toDeoptimize = mapEntries(usages).filter(([_, count]) => count < minUsagesToOptimize).map(([name]) => name);
|
|
734
734
|
|
|
735
|
-
|
|
736
|
-
|
|
735
|
+
if (toDeoptimize.length > 0) {
|
|
736
|
+
const newFragments = optimizedSelection.fragments?.without(toDeoptimize);
|
|
737
|
+
optimizedSelection = optimizedSelection.expandFragments(toDeoptimize, newFragments);
|
|
738
|
+
// Expanding fragments could create some "inefficiencies" that we wouldn't have if we hadn't re-optimized
|
|
739
|
+
// the fragments to de-optimize it later, so we do a final "trim" pass to remove those.
|
|
740
|
+
optimizedSelection = optimizedSelection.trimUnsatisfiableBranches(optimizedSelection.parentType);
|
|
741
|
+
}
|
|
737
742
|
|
|
738
743
|
return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, this.name);
|
|
739
744
|
}
|
|
@@ -841,6 +846,8 @@ export class Operation {
|
|
|
841
846
|
export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmentDefinition> {
|
|
842
847
|
private _selectionSet: SelectionSet | undefined;
|
|
843
848
|
|
|
849
|
+
private readonly selectionSetsAtTypesCache = new Map<string, SelectionSet>();
|
|
850
|
+
|
|
844
851
|
constructor(
|
|
845
852
|
schema: Schema,
|
|
846
853
|
readonly name: string,
|
|
@@ -852,6 +859,9 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
852
859
|
|
|
853
860
|
setSelectionSet(selectionSet: SelectionSet): NamedFragmentDefinition {
|
|
854
861
|
assert(!this._selectionSet, 'Attempting to set the selection set of a fragment definition already built')
|
|
862
|
+
// We set the selection set post-construction to simplify the handling of fragments that use other fragments,
|
|
863
|
+
// but let's make sure we've properly used the fragment type condition as parent type of the selection set, as we should.
|
|
864
|
+
assert(selectionSet.parentType === this.typeCondition, `Fragment selection set parent is ${selectionSet.parentType} differs from the fragment condition type ${this.typeCondition}`);
|
|
855
865
|
this._selectionSet = selectionSet;
|
|
856
866
|
return this;
|
|
857
867
|
}
|
|
@@ -894,32 +904,53 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
894
904
|
* @param type - the type at which we're looking at applying the fragment
|
|
895
905
|
*/
|
|
896
906
|
canApplyAtType(type: CompositeType): boolean {
|
|
897
|
-
|
|
898
|
-
return applyAtType
|
|
899
|
-
&& this.validForSchema(type.schema());
|
|
907
|
+
return sameType(type, this.typeCondition) || runtimeTypesIntersects(type, this.typeCondition);
|
|
900
908
|
}
|
|
901
909
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
910
|
+
/**
|
|
911
|
+
* This methods *assumes* that `this.canApplyAtType(type)` is `true` (and may crash if this is not true), and returns
|
|
912
|
+
* a version fo this named fragment selection set that corresponds to the "expansion" of this named fragment at `type`
|
|
913
|
+
*
|
|
914
|
+
* The overall idea here is that if we have an interface I with 2 implementations T1 and T2, and we have a fragment like:
|
|
915
|
+
* ```graphql
|
|
916
|
+
* fragment X on I {
|
|
917
|
+
* ... on T1 {
|
|
918
|
+
* <stuff>
|
|
919
|
+
* }
|
|
920
|
+
* ... on T2 {
|
|
921
|
+
* <stuff>
|
|
922
|
+
* }
|
|
923
|
+
* }
|
|
924
|
+
* ```
|
|
925
|
+
* then if the current type is `T1`, then all we care about matching for this fragment is the `... on T1` part, and this method gives
|
|
926
|
+
* us that part.
|
|
927
|
+
*/
|
|
928
|
+
selectionSetAtType(type: CompositeType): SelectionSet {
|
|
929
|
+
// First, if the candidate condition is an object or is the type passed, then there isn't any additional restriction to do.
|
|
930
|
+
if (sameType(type, this.typeCondition) || isObjectType(this.typeCondition)) {
|
|
931
|
+
return this.selectionSet;
|
|
907
932
|
}
|
|
908
933
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
934
|
+
// We should not call `trimUnsatisfiableBranches` where `type` is an abstract type (`interface` or `union`) as it currently could
|
|
935
|
+
// create an invalid selection set (and throw down the line). In theory, when `type` is an abstract type, we could look at the
|
|
936
|
+
// intersection of its runtime types with those of `this.typeCondition`, call `trimUnsatisfiableBranches` for each of the resulting
|
|
937
|
+
// object types, and merge all those selection sets, and this "may" result in a smaller selection at times. This is a bit complex
|
|
938
|
+
// and costly to do however, so we just return the selection unchanged for now, which is always valid but simply may not be absolutely
|
|
939
|
+
// optimal.
|
|
940
|
+
// Concretely, this means that there may be corner cases where a named fragment could be reused but isn't, but waiting on finding
|
|
941
|
+
// concrete examples where this matter to decide if it's worth the complexity.
|
|
942
|
+
if (!isObjectType(type)) {
|
|
943
|
+
return this.selectionSet;
|
|
912
944
|
}
|
|
913
945
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
//
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
// We don't really care what kind of error was triggered; only that it doesn't work.
|
|
921
|
-
return false;
|
|
946
|
+
let selectionSet = this.selectionSetsAtTypesCache.get(type.name);
|
|
947
|
+
if (!selectionSet) {
|
|
948
|
+
// Note that all we want is removing any top-level branches that don't apply due to the current type. There is no point
|
|
949
|
+
// in going recursive however: any simplification due to `type` stops as soon as we traverse a field. And so we don't bother.
|
|
950
|
+
selectionSet = this.selectionSet.trimUnsatisfiableBranches(type, { recursive: false });
|
|
951
|
+
this.selectionSetsAtTypesCache.set(type.name, selectionSet);
|
|
922
952
|
}
|
|
953
|
+
return selectionSet;
|
|
923
954
|
}
|
|
924
955
|
|
|
925
956
|
toString(indent?: string): string {
|
|
@@ -995,6 +1026,72 @@ export class NamedFragments {
|
|
|
995
1026
|
return mapped;
|
|
996
1027
|
}
|
|
997
1028
|
|
|
1029
|
+
/**
|
|
1030
|
+
* This method:
|
|
1031
|
+
* - expands all nested fragments,
|
|
1032
|
+
* - applies the provided mapper to the selection set of the fragments,
|
|
1033
|
+
* - and finally re-fragments the nested fragments.
|
|
1034
|
+
*/
|
|
1035
|
+
mapToExpandedSelectionSets(
|
|
1036
|
+
mapper: (selectionSet: SelectionSet) => SelectionSet | undefined,
|
|
1037
|
+
recreateFct: (frag: NamedFragmentDefinition, newSelectionSet: SelectionSet) => NamedFragmentDefinition = (f, s) => f.withUpdatedSelectionSet(s),
|
|
1038
|
+
): NamedFragments | undefined {
|
|
1039
|
+
type FragmentInfo = {
|
|
1040
|
+
original: NamedFragmentDefinition,
|
|
1041
|
+
mappedSelectionSet: SelectionSet,
|
|
1042
|
+
dependsOn: string[],
|
|
1043
|
+
};
|
|
1044
|
+
const fragmentsMap = new Map<string, FragmentInfo>();
|
|
1045
|
+
|
|
1046
|
+
const removedFragments = new Set<string>();
|
|
1047
|
+
for (const fragment of this.definitions()) {
|
|
1048
|
+
const mappedSelectionSet = mapper(fragment.selectionSet.expandAllFragments().trimUnsatisfiableBranches(fragment.typeCondition));
|
|
1049
|
+
if (!mappedSelectionSet) {
|
|
1050
|
+
removedFragments.add(fragment.name);
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const otherFragmentsUsages = new Map<string, number>();
|
|
1055
|
+
fragment.collectUsedFragmentNames(otherFragmentsUsages);
|
|
1056
|
+
fragmentsMap.set(fragment.name, {
|
|
1057
|
+
original: fragment,
|
|
1058
|
+
mappedSelectionSet,
|
|
1059
|
+
dependsOn: Array.from(otherFragmentsUsages.keys()),
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const mappedFragments = new NamedFragments();
|
|
1064
|
+
while (fragmentsMap.size > 0) {
|
|
1065
|
+
for (const [name, info] of fragmentsMap) {
|
|
1066
|
+
// Note that graphQL specifies that named fragments cannot have cycles (https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles)
|
|
1067
|
+
// and so we're guaranteed that on every iteration, at least one element of the map is removed (so the `while` loop will terminate).
|
|
1068
|
+
if (info.dependsOn.every((n) => mappedFragments.has(n) || removedFragments.has(n))) {
|
|
1069
|
+
const reoptimizedSelectionSet = info.mappedSelectionSet.optimize(mappedFragments);
|
|
1070
|
+
mappedFragments.add(recreateFct(info.original, reoptimizedSelectionSet));
|
|
1071
|
+
fragmentsMap.delete(name);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return mappedFragments.isEmpty() ? undefined : mappedFragments;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
rebaseOn(schema: Schema): NamedFragments | undefined {
|
|
1080
|
+
return this.mapToExpandedSelectionSets(
|
|
1081
|
+
(s) => {
|
|
1082
|
+
const rebasedType = schema.type(s.parentType.name);
|
|
1083
|
+
try {
|
|
1084
|
+
return rebasedType && isCompositeType(rebasedType) ? s.rebaseOn(rebasedType) : undefined;
|
|
1085
|
+
} catch (e) {
|
|
1086
|
+
// This means we cannot rebase this selection on the schema and thus cannot reuse that fragment on that
|
|
1087
|
+
// particular schema.
|
|
1088
|
+
return undefined;
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
(orig, newSelection) => new NamedFragmentDefinition(schema, orig.name, newSelection.parentType).setSelectionSet(newSelection),
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
998
1095
|
validate(variableDefinitions: VariableDefinitions) {
|
|
999
1096
|
for (const fragment of this.fragments.values()) {
|
|
1000
1097
|
fragment.selectionSet.validate(variableDefinitions);
|
|
@@ -1142,6 +1239,36 @@ export class SelectionSet {
|
|
|
1142
1239
|
return this;
|
|
1143
1240
|
}
|
|
1144
1241
|
|
|
1242
|
+
// Calling optimizeSelections() will not match a fragment that would have expanded at top-level.
|
|
1243
|
+
// That is, say we have the selection set `{ x y }` for a top-level `Query`, and we have a fragment
|
|
1244
|
+
// ```
|
|
1245
|
+
// fragment F on Query {
|
|
1246
|
+
// x
|
|
1247
|
+
// y
|
|
1248
|
+
// }
|
|
1249
|
+
// ```
|
|
1250
|
+
// then calling `this.optimizeSelections(fragments)` would only apply check if F apply to `x` and
|
|
1251
|
+
// then `y`.
|
|
1252
|
+
//
|
|
1253
|
+
// To ensure the fragment match in this case, we "wrap" the selection into a trivial fragment of
|
|
1254
|
+
// the selection parent, so in the example above, we create selection `... on Query { x y}`.
|
|
1255
|
+
// With that, `optimizeSelections` will correctly match on the `on Query` fragment; after which
|
|
1256
|
+
// we can unpack the final result.
|
|
1257
|
+
const wrapped = new InlineFragmentSelection(new FragmentElement(this.parentType, this.parentType), this);
|
|
1258
|
+
const optimized = wrapped.optimize(fragments);
|
|
1259
|
+
|
|
1260
|
+
// Now, it's possible we matched a full fragment, in which case `optimized` will be just the named fragment,
|
|
1261
|
+
// and in that case we return a singleton selection with just that. Otherwise, it's our wrapping inline fragment
|
|
1262
|
+
// with the sub-selections optimized, and we just return that subselection.
|
|
1263
|
+
return optimized instanceof FragmentSpreadSelection
|
|
1264
|
+
? selectionSetOf(this.parentType, optimized, fragments)
|
|
1265
|
+
: optimized.selectionSet;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Tries to match fragments inside each selections of this selection set, and this recursively. However, note that this
|
|
1269
|
+
// may not match fragments that would apply at top-level, so you should usually use `optimize` instead (this exists mostly
|
|
1270
|
+
// for the recursion).
|
|
1271
|
+
optimizeSelections(fragments: NamedFragments): SelectionSet {
|
|
1145
1272
|
// Handling the case where the selection may alreayd have some fragments adds complexity,
|
|
1146
1273
|
// not only because we need to deal with merging new and existing fragments, but also because
|
|
1147
1274
|
// things get weird if some fragment names are in common to both. Since we currently only care
|
|
@@ -1164,8 +1291,8 @@ export class SelectionSet {
|
|
|
1164
1291
|
return this.lazyMap((selection) => selection.expandFragments(names, updatedFragments), { fragments: updatedFragments ?? null });
|
|
1165
1292
|
}
|
|
1166
1293
|
|
|
1167
|
-
trimUnsatisfiableBranches(parentType: CompositeType): SelectionSet {
|
|
1168
|
-
return this.lazyMap((selection) => selection.trimUnsatisfiableBranches(parentType), { parentType });
|
|
1294
|
+
trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): SelectionSet {
|
|
1295
|
+
return this.lazyMap((selection) => selection.trimUnsatisfiableBranches(parentType, options), { parentType });
|
|
1169
1296
|
}
|
|
1170
1297
|
|
|
1171
1298
|
/**
|
|
@@ -1325,12 +1452,22 @@ export class SelectionSet {
|
|
|
1325
1452
|
return true;
|
|
1326
1453
|
}
|
|
1327
1454
|
|
|
1328
|
-
|
|
1455
|
+
// Please note that this method assumes that `candidate.canApplyAtType(parentType) === true` but it is left to the caller to
|
|
1456
|
+
// validate this (`canApplyAtType` is not free, and we want to avoid repeating it multiple times).
|
|
1457
|
+
diffWithNamedFragmentIfContained(candidate: NamedFragmentDefinition, parentType: CompositeType): { contains: boolean, diff?: SelectionSet } {
|
|
1458
|
+
const that = candidate.selectionSetAtType(parentType);
|
|
1329
1459
|
if (this.contains(that)) {
|
|
1330
|
-
|
|
1460
|
+
// One subtlety here is that at "this" sub-selections may already have been optimized with some fragments. It's
|
|
1461
|
+
// usually ok because `candidate` will also use those fragments, but one fragments that `candidate` can never be
|
|
1462
|
+
// using is itself (the `contains` check is fine with this, but it's harder to deal in `minus`). So we expand
|
|
1463
|
+
// the candidate we're currently looking at in "this" to avoid some issues.
|
|
1464
|
+
let updatedThis = this.expandFragments([candidate.name], this.fragments);
|
|
1465
|
+
if (updatedThis !== this) {
|
|
1466
|
+
updatedThis = updatedThis.trimUnsatisfiableBranches(parentType);
|
|
1467
|
+
}
|
|
1468
|
+
const diff = updatedThis.minus(that);
|
|
1331
1469
|
return { contains: true, diff: diff.isEmpty() ? undefined : diff };
|
|
1332
1470
|
}
|
|
1333
|
-
|
|
1334
1471
|
return { contains: false };
|
|
1335
1472
|
}
|
|
1336
1473
|
|
|
@@ -1598,9 +1735,19 @@ function addOneToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, s
|
|
|
1598
1735
|
}
|
|
1599
1736
|
}
|
|
1600
1737
|
|
|
1738
|
+
function maybeRebaseOnSchema(toRebase: CompositeType, schema: Schema): CompositeType {
|
|
1739
|
+
if (toRebase.schema() === schema) {
|
|
1740
|
+
return toRebase;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const rebased = schema.type(toRebase.name);
|
|
1744
|
+
assert(rebased && isCompositeType(rebased), () => `Expected ${toRebase} to exists and be composite in the rebased schema, but got ${rebased?.kind}`);
|
|
1745
|
+
return rebased;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1601
1748
|
function isUnecessaryFragment(parentType: CompositeType, fragment: FragmentSelection): boolean {
|
|
1602
1749
|
return fragment.element.appliedDirectives.length === 0
|
|
1603
|
-
&& (!fragment.element.typeCondition ||
|
|
1750
|
+
&& (!fragment.element.typeCondition || isSubtype(maybeRebaseOnSchema(fragment.element.typeCondition, parentType.schema()), parentType));
|
|
1604
1751
|
}
|
|
1605
1752
|
|
|
1606
1753
|
function withUnecessaryFragmentsRemoved(
|
|
@@ -1881,7 +2028,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
1881
2028
|
|
|
1882
2029
|
abstract expandFragments(names: string[], updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
|
|
1883
2030
|
|
|
1884
|
-
abstract trimUnsatisfiableBranches(parentType: CompositeType): TOwnType | SelectionSet | undefined;
|
|
2031
|
+
abstract trimUnsatisfiableBranches(parentType: CompositeType, options?: { recursive? : boolean }): TOwnType | SelectionSet | undefined;
|
|
1885
2032
|
|
|
1886
2033
|
minus(that: Selection): TOwnType | undefined {
|
|
1887
2034
|
// If there is a subset, then we compute the diff of the subset and add that (if not empty).
|
|
@@ -1895,6 +2042,10 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
1895
2042
|
return undefined;
|
|
1896
2043
|
}
|
|
1897
2044
|
|
|
2045
|
+
// Attempts to optimize the subselection of this field selection using named fragments `candidates` _assuming_ that
|
|
2046
|
+
// those candidates do apply at `parentType` (that is, `candidates.every((c) => c.canApplyAtType(parentType))` is true,
|
|
2047
|
+
// which is ensured by the fact that `tryOptimizeSubselectionWithFragments` calls this on a subset of the candidates
|
|
2048
|
+
// returned by `maybeApplyingAtType`).
|
|
1898
2049
|
protected tryOptimizeSubselectionOnce(_: {
|
|
1899
2050
|
parentType: CompositeType,
|
|
1900
2051
|
subSelection: SelectionSet,
|
|
@@ -1968,7 +2119,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
1968
2119
|
}
|
|
1969
2120
|
|
|
1970
2121
|
optimize(fragments: NamedFragments): Selection {
|
|
1971
|
-
let optimizedSelection = this.selectionSet ? this.selectionSet.
|
|
2122
|
+
let optimizedSelection = this.selectionSet ? this.selectionSet.optimizeSelections(fragments) : undefined;
|
|
1972
2123
|
const fieldBaseType = baseType(this.element.definition.type!);
|
|
1973
2124
|
if (isCompositeType(fieldBaseType) && optimizedSelection) {
|
|
1974
2125
|
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
@@ -2007,7 +2158,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2007
2158
|
}{
|
|
2008
2159
|
let optimizedSelection = subSelection;
|
|
2009
2160
|
for (const candidate of candidates) {
|
|
2010
|
-
const { contains, diff } = optimizedSelection.
|
|
2161
|
+
const { contains, diff } = optimizedSelection.diffWithNamedFragmentIfContained(candidate, parentType);
|
|
2011
2162
|
if (contains) {
|
|
2012
2163
|
// We can optimize the selection with this fragment. The replaced sub-selection will be
|
|
2013
2164
|
// comprised of this new spread and the remaining `diff` if there is any.
|
|
@@ -2123,14 +2274,14 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2123
2274
|
return this.mapToSelectionSet((s) => s.expandAllFragments());
|
|
2124
2275
|
}
|
|
2125
2276
|
|
|
2126
|
-
trimUnsatisfiableBranches(_: CompositeType): FieldSelection {
|
|
2277
|
+
trimUnsatisfiableBranches(_: CompositeType, options?: { recursive? : boolean }): FieldSelection {
|
|
2127
2278
|
if (!this.selectionSet) {
|
|
2128
2279
|
return this;
|
|
2129
2280
|
}
|
|
2130
2281
|
|
|
2131
2282
|
const base = baseType(this.element.definition.type!)
|
|
2132
2283
|
assert(isCompositeType(base), () => `Field ${this.element} should not have a sub-selection`);
|
|
2133
|
-
const trimmed = this.mapToSelectionSet((s) => s.trimUnsatisfiableBranches(base));
|
|
2284
|
+
const trimmed = (options?.recursive ?? true) ? this.mapToSelectionSet((s) => s.trimUnsatisfiableBranches(base)) : this;
|
|
2134
2285
|
// In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
|
|
2135
2286
|
// sub-selection is empty. Which suggest something may be wrong with this part of the query
|
|
2136
2287
|
// intent, but the query was valid while keeping an empty sub-selection isn't. So in that
|
|
@@ -2317,7 +2468,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2317
2468
|
}
|
|
2318
2469
|
|
|
2319
2470
|
optimize(fragments: NamedFragments): FragmentSelection {
|
|
2320
|
-
let optimizedSelection = this.selectionSet.
|
|
2471
|
+
let optimizedSelection = this.selectionSet.optimizeSelections(fragments);
|
|
2321
2472
|
const typeCondition = this.element.typeCondition;
|
|
2322
2473
|
if (typeCondition) {
|
|
2323
2474
|
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
@@ -2353,7 +2504,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2353
2504
|
}{
|
|
2354
2505
|
let optimizedSelection = subSelection;
|
|
2355
2506
|
for (const candidate of candidates) {
|
|
2356
|
-
const { contains, diff } = optimizedSelection.
|
|
2507
|
+
const { contains, diff } = optimizedSelection.diffWithNamedFragmentIfContained(candidate, parentType);
|
|
2357
2508
|
if (contains) {
|
|
2358
2509
|
// The candidate selection is included in our sub-selection. One remaining thing to take into account
|
|
2359
2510
|
// is applied directives: if the candidate has directives, then we can only use it if 1) there is
|
|
@@ -2424,12 +2575,14 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2424
2575
|
: this.withUpdatedComponents(newElement, newSelection);
|
|
2425
2576
|
}
|
|
2426
2577
|
|
|
2427
|
-
trimUnsatisfiableBranches(currentType: CompositeType): FragmentSelection | SelectionSet | undefined {
|
|
2578
|
+
trimUnsatisfiableBranches(currentType: CompositeType, options?: { recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
|
|
2579
|
+
const recursive = options?.recursive ?? true;
|
|
2580
|
+
|
|
2428
2581
|
const thisCondition = this.element.typeCondition;
|
|
2429
2582
|
// Note that if the condition has directives, we preserve the fragment no matter what.
|
|
2430
2583
|
if (this.element.appliedDirectives.length === 0) {
|
|
2431
2584
|
if (!thisCondition || currentType === this.element.typeCondition) {
|
|
2432
|
-
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType);
|
|
2585
|
+
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType, options);
|
|
2433
2586
|
return trimmed.isEmpty() ? undefined : trimmed;
|
|
2434
2587
|
}
|
|
2435
2588
|
|
|
@@ -2444,12 +2597,17 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2444
2597
|
if (isObjectType(thisCondition) || !possibleRuntimeTypes(thisCondition).includes(currentType)) {
|
|
2445
2598
|
return undefined;
|
|
2446
2599
|
} else {
|
|
2447
|
-
const trimmed =
|
|
2600
|
+
const trimmed =this.selectionSet.trimUnsatisfiableBranches(currentType, options);
|
|
2448
2601
|
return trimmed.isEmpty() ? undefined : trimmed;
|
|
2449
2602
|
}
|
|
2450
2603
|
}
|
|
2451
2604
|
}
|
|
2452
2605
|
|
|
2606
|
+
// As we preserve the current fragment, the rest is about recursing. If we don't recurse, we're done
|
|
2607
|
+
if (!recursive) {
|
|
2608
|
+
return this;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2453
2611
|
// In all other cases, we first recurse on the sub-selection.
|
|
2454
2612
|
const trimmedSelectionSet = this.selectionSet.trimUnsatisfiableBranches(this.element.typeCondition ?? this.parentType);
|
|
2455
2613
|
|
package/src/print.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
Directive,
|
|
4
4
|
DirectiveDefinition,
|
|
5
5
|
EnumType,
|
|
6
|
+
EnumValue,
|
|
6
7
|
ExtendableElement,
|
|
7
8
|
Extension,
|
|
8
9
|
FieldDefinition,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
SchemaDefinition,
|
|
19
20
|
SchemaElement,
|
|
20
21
|
SchemaRootKind,
|
|
22
|
+
UnionMember,
|
|
21
23
|
UnionType
|
|
22
24
|
} from "./definitions";
|
|
23
25
|
import { assert } from "./utils";
|
|
@@ -28,6 +30,11 @@ export type PrintOptions = {
|
|
|
28
30
|
definitionsOrder: ('schema' | 'types' | 'directives')[],
|
|
29
31
|
rootTypesOrder: SchemaRootKind[],
|
|
30
32
|
typeCompareFn?: (t1: NamedType, t2: NamedType) => number;
|
|
33
|
+
implementedInterfaceCompareFn?: (t1: InterfaceImplementation<any>, t2: InterfaceImplementation<any>) => number;
|
|
34
|
+
fieldCompareFn?: (t1: FieldDefinition<any>, t2: FieldDefinition<any>) => number;
|
|
35
|
+
unionMemberCompareFn?: (t1: UnionMember, t2: UnionMember) => number;
|
|
36
|
+
enumValueCompareFn?: (t1: EnumValue, t2: EnumValue) => number;
|
|
37
|
+
inputObjectFieldCompareFn?: (t1: InputFieldDefinition, t2: InputFieldDefinition) => number;
|
|
31
38
|
directiveCompareFn?: (d1: DirectiveDefinition, d2: DirectiveDefinition) => number;
|
|
32
39
|
mergeTypesAndExtensions: boolean;
|
|
33
40
|
showAllBuiltIns: boolean;
|
|
@@ -51,6 +58,19 @@ export const defaultPrintOptions: PrintOptions = {
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
export function orderPrintedDefinitions(options: PrintOptions): PrintOptions {
|
|
61
|
+
return {
|
|
62
|
+
...options,
|
|
63
|
+
typeCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
|
|
64
|
+
implementedInterfaceCompareFn: (t1, t2) => t1.interface.name.localeCompare(t2.interface.name),
|
|
65
|
+
fieldCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
|
|
66
|
+
unionMemberCompareFn: (t1, t2) => t1.type.name.localeCompare(t2.type.name),
|
|
67
|
+
enumValueCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
|
|
68
|
+
inputObjectFieldCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
|
|
69
|
+
directiveCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function shallowOrderPrintedDefinitions(options: PrintOptions): PrintOptions {
|
|
54
74
|
return {
|
|
55
75
|
...options,
|
|
56
76
|
typeCompareFn: (t1, t2) => t1.name.localeCompare(t2.name),
|
|
@@ -255,7 +275,7 @@ function printImplementedInterfaces(implementations: readonly InterfaceImplement
|
|
|
255
275
|
|
|
256
276
|
function printFieldBasedTypeDefinitionOrExtension(kind: string, type: ObjectType | InterfaceType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
|
|
257
277
|
const directives = appliedDirectives(type, options, extension);
|
|
258
|
-
|
|
278
|
+
let interfaces = forExtension<InterfaceImplementation<any>>(type.interfaceImplementations(), extension);
|
|
259
279
|
let fields = forExtension<FieldDefinition<any>>(type.fields(), extension);
|
|
260
280
|
if (options.fieldFilter) {
|
|
261
281
|
fields = fields.filter(options.fieldFilter);
|
|
@@ -263,6 +283,12 @@ function printFieldBasedTypeDefinitionOrExtension(kind: string, type: ObjectType
|
|
|
263
283
|
if (!directives.length && !interfaces.length && !fields.length && (extension || !type.preserveEmptyDefinition)) {
|
|
264
284
|
return undefined;
|
|
265
285
|
}
|
|
286
|
+
if (options.implementedInterfaceCompareFn) {
|
|
287
|
+
interfaces = interfaces.concat().sort(options.implementedInterfaceCompareFn);
|
|
288
|
+
}
|
|
289
|
+
if (options.fieldCompareFn) {
|
|
290
|
+
fields = fields.concat().sort(options.fieldCompareFn);
|
|
291
|
+
}
|
|
266
292
|
return printDescription(type, options, extension)
|
|
267
293
|
+ printIsExtension(extension)
|
|
268
294
|
+ kind + ' ' + type
|
|
@@ -274,10 +300,13 @@ function printFieldBasedTypeDefinitionOrExtension(kind: string, type: ObjectType
|
|
|
274
300
|
|
|
275
301
|
function printUnionDefinitionOrExtension(type: UnionType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
|
|
276
302
|
const directives = appliedDirectives(type, options, extension);
|
|
277
|
-
|
|
303
|
+
let members = forExtension(type.members(), extension);
|
|
278
304
|
if (!directives.length && !members.length && (extension || !type.preserveEmptyDefinition)) {
|
|
279
305
|
return undefined;
|
|
280
306
|
}
|
|
307
|
+
if (options.unionMemberCompareFn) {
|
|
308
|
+
members = members.concat().sort(options.unionMemberCompareFn);
|
|
309
|
+
}
|
|
281
310
|
const possibleTypes = members.length ? ' = ' + members.map(m => m.type).join(' | ') : '';
|
|
282
311
|
return printDescription(type, options, extension)
|
|
283
312
|
+ printIsExtension(extension)
|
|
@@ -288,10 +317,13 @@ function printUnionDefinitionOrExtension(type: UnionType, options: PrintOptions,
|
|
|
288
317
|
|
|
289
318
|
function printEnumDefinitionOrExtension(type: EnumType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
|
|
290
319
|
const directives = appliedDirectives(type, options, extension);
|
|
291
|
-
|
|
320
|
+
let values = forExtension(type.values, extension);
|
|
292
321
|
if (!directives.length && !values.length && (extension || !type.preserveEmptyDefinition)) {
|
|
293
322
|
return undefined;
|
|
294
323
|
}
|
|
324
|
+
if (options.enumValueCompareFn) {
|
|
325
|
+
values = values.concat().sort(options.enumValueCompareFn);
|
|
326
|
+
}
|
|
295
327
|
const vals = values.map((v, i) =>
|
|
296
328
|
printDescription(v, options, extension, options.indentString, !i)
|
|
297
329
|
+ options.indentString
|
|
@@ -307,10 +339,13 @@ function printEnumDefinitionOrExtension(type: EnumType, options: PrintOptions, e
|
|
|
307
339
|
|
|
308
340
|
function printInputDefinitionOrExtension(type: InputObjectType, options: PrintOptions, extension?: Extension<any> | null): string | undefined {
|
|
309
341
|
const directives = appliedDirectives(type, options, extension);
|
|
310
|
-
|
|
342
|
+
let fields = forExtension(type.fields(), extension);
|
|
311
343
|
if (!directives.length && !fields.length && (extension || !type.preserveEmptyDefinition)) {
|
|
312
344
|
return undefined;
|
|
313
345
|
}
|
|
346
|
+
if (options.inputObjectFieldCompareFn) {
|
|
347
|
+
fields = fields.concat().sort(options.inputObjectFieldCompareFn);
|
|
348
|
+
}
|
|
314
349
|
return printDescription(type, options, extension)
|
|
315
350
|
+ printIsExtension(extension)
|
|
316
351
|
+ 'input ' + type
|
package/src/tagSpec.ts
CHANGED
|
@@ -41,11 +41,11 @@ export class TagSpecDefinition extends FeatureDefinition {
|
|
|
41
41
|
name:'tag',
|
|
42
42
|
locations: this.tagLocations,
|
|
43
43
|
repeatable: true,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}),
|
|
44
|
+
args: [{ name: 'name', type: (schema) => new NonNullType(schema.stringType()) }],
|
|
45
|
+
composes: true,
|
|
46
|
+
supergraphSpecification: () => TAG_VERSIONS.latest(),
|
|
48
47
|
});
|
|
48
|
+
this.registerDirective(this.tagDirectiveSpec);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
private isV01() {
|
|
@@ -56,10 +56,6 @@ export class TagSpecDefinition extends FeatureDefinition {
|
|
|
56
56
|
return this.version.equals(new FeatureVersion(0, 2))
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
addElementsToSchema(schema: Schema): GraphQLError[] {
|
|
60
|
-
return this.addDirectiveSpec(schema, this.tagDirectiveSpec);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
59
|
tagDirective(schema: Schema): DirectiveDefinition<{name: string}> {
|
|
64
60
|
return this.directive(schema, 'tag')!;
|
|
65
61
|
}
|
|
@@ -76,10 +72,6 @@ export class TagSpecDefinition extends FeatureDefinition {
|
|
|
76
72
|
}
|
|
77
73
|
return undefined;
|
|
78
74
|
}
|
|
79
|
-
|
|
80
|
-
allElementNames(): string[] {
|
|
81
|
-
return ["@tag"];
|
|
82
|
-
}
|
|
83
75
|
}
|
|
84
76
|
|
|
85
77
|
export const TAG_VERSIONS = new FeatureDefinitions<TagSpecDefinition>(tagIdentity)
|