@apollo/federation-internals 2.4.7 → 2.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coreSpec.js +1 -1
- package/dist/coreSpec.js.map +1 -1
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +3 -0
- package/dist/definitions.js.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.d.ts.map +1 -1
- package/dist/extractSubgraphsFromSupergraph.js +25 -7
- package/dist/extractSubgraphsFromSupergraph.js.map +1 -1
- package/dist/federationSpec.js +2 -2
- package/dist/federationSpec.js.map +1 -1
- package/dist/operations.d.ts +108 -32
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +462 -173
- package/dist/operations.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +14 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/definitions.ts +4 -0
- package/src/extractSubgraphsFromSupergraph.ts +36 -10
- package/src/operations.ts +844 -296
- package/src/types.ts +22 -1
package/src/operations.ts
CHANGED
|
@@ -47,10 +47,11 @@ import {
|
|
|
47
47
|
Variables,
|
|
48
48
|
isObjectType,
|
|
49
49
|
NamedType,
|
|
50
|
+
isUnionType,
|
|
50
51
|
} from "./definitions";
|
|
51
52
|
import { isInterfaceObjectType } from "./federation";
|
|
52
53
|
import { ERRORS } from "./error";
|
|
53
|
-
import { isSubtype, sameType } from "./types";
|
|
54
|
+
import { isSubtype, sameType, typesCanBeMerged } from "./types";
|
|
54
55
|
import { assert, mapKeys, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
|
|
55
56
|
import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
|
|
56
57
|
import { v1 as uuidv1 } from 'uuid';
|
|
@@ -84,7 +85,11 @@ abstract class AbstractOperationElement<T extends AbstractOperationElement<T>> e
|
|
|
84
85
|
|
|
85
86
|
abstract asPathElement(): string | undefined;
|
|
86
87
|
|
|
87
|
-
abstract rebaseOn(parentType: CompositeType): T;
|
|
88
|
+
abstract rebaseOn(args: { parentType: CompositeType, errorIfCannotRebase: boolean }): T | undefined;
|
|
89
|
+
|
|
90
|
+
rebaseOnOrError(parentType: CompositeType): T {
|
|
91
|
+
return this.rebaseOn({ parentType, errorIfCannotRebase: true })!;
|
|
92
|
+
}
|
|
88
93
|
|
|
89
94
|
abstract withUpdatedDirectives(newDirectives: readonly Directive<any>[]): T;
|
|
90
95
|
|
|
@@ -115,7 +120,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
115
120
|
|
|
116
121
|
constructor(
|
|
117
122
|
readonly definition: FieldDefinition<CompositeType>,
|
|
118
|
-
|
|
123
|
+
readonly args?: TArgs,
|
|
119
124
|
directives?: readonly Directive<any>[],
|
|
120
125
|
readonly alias?: string,
|
|
121
126
|
) {
|
|
@@ -289,7 +294,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
289
294
|
}
|
|
290
295
|
}
|
|
291
296
|
|
|
292
|
-
rebaseOn(parentType: CompositeType): Field<TArgs> {
|
|
297
|
+
rebaseOn({ parentType, errorIfCannotRebase }: { parentType: CompositeType, errorIfCannotRebase: boolean }): Field<TArgs> | undefined {
|
|
293
298
|
const fieldParent = this.definition.parent;
|
|
294
299
|
if (parentType === fieldParent) {
|
|
295
300
|
return this;
|
|
@@ -299,12 +304,16 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
299
304
|
return this.withUpdatedDefinition(parentType.typenameField()!);
|
|
300
305
|
}
|
|
301
306
|
|
|
302
|
-
validate(
|
|
303
|
-
this.canRebaseOn(parentType),
|
|
304
|
-
() => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${parentType}"`
|
|
305
|
-
);
|
|
306
307
|
const fieldDef = parentType.field(this.name);
|
|
307
|
-
|
|
308
|
+
const canRebase = this.canRebaseOn(parentType) && fieldDef;
|
|
309
|
+
if (!canRebase) {
|
|
310
|
+
validate(
|
|
311
|
+
!errorIfCannotRebase,
|
|
312
|
+
() => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${parentType}"`
|
|
313
|
+
);
|
|
314
|
+
return undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
308
317
|
return this.withUpdatedDefinition(fieldDef);
|
|
309
318
|
}
|
|
310
319
|
|
|
@@ -465,7 +474,7 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
|
465
474
|
return newFragment;
|
|
466
475
|
}
|
|
467
476
|
|
|
468
|
-
rebaseOn(parentType: CompositeType): FragmentElement {
|
|
477
|
+
rebaseOn({ parentType, errorIfCannotRebase }: { parentType: CompositeType, errorIfCannotRebase: boolean }): FragmentElement | undefined {
|
|
469
478
|
const fragmentParent = this.parentType;
|
|
470
479
|
const typeCondition = this.typeCondition;
|
|
471
480
|
if (parentType === fragmentParent) {
|
|
@@ -476,10 +485,13 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
|
476
485
|
// to update the source type of the fragment, but also "rebase" the condition to the selection set
|
|
477
486
|
// schema.
|
|
478
487
|
const { canRebase, rebasedCondition } = this.canRebaseOn(parentType);
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
488
|
+
if (!canRebase) {
|
|
489
|
+
validate(
|
|
490
|
+
!errorIfCannotRebase,
|
|
491
|
+
() => `Cannot add fragment of condition "${typeCondition}" (runtimes: [${possibleRuntimeTypes(typeCondition!)}]) to parent type "${parentType}" (runtimes: ${possibleRuntimeTypes(parentType)})`
|
|
492
|
+
);
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
483
495
|
return this.withUpdatedTypes(parentType, rebasedCondition);
|
|
484
496
|
}
|
|
485
497
|
|
|
@@ -865,69 +877,104 @@ export class Operation {
|
|
|
865
877
|
readonly name?: string) {
|
|
866
878
|
}
|
|
867
879
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
let optimizedSelection = this.selectionSet.optimize(fragments);
|
|
875
|
-
if (optimizedSelection === this.selectionSet) {
|
|
876
|
-
return this;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
const finalFragments = computeFragmentsToKeep(optimizedSelection, fragments, minUsagesToOptimize);
|
|
880
|
-
if (finalFragments === null || finalFragments?.size === fragments.size) {
|
|
881
|
-
// This means either that there is no fragment usage whatsoever in `optimizedSelection`, or that
|
|
882
|
-
// we're keeping all fragments. In both cases, we need no additional work on `optimizedSelection`.
|
|
883
|
-
return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, finalFragments ?? undefined, this.name);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// If we get here, it means some fragments need to be expanded, so we do so.
|
|
887
|
-
// Optimizing all fragments to potentially re-expand some is not entirely optimal, but it's unclear
|
|
888
|
-
// how to do otherwise, and it probably don't matter too much in practice (we only call this optimization
|
|
889
|
-
// on the final computed query plan, so not a very hot path; plus in most cases we won't even reach that
|
|
890
|
-
// point either because there is no fragment, or none will have been optimized away so we'll exit above).
|
|
891
|
-
optimizedSelection = optimizedSelection.expandFragments(finalFragments);
|
|
892
|
-
|
|
893
|
-
// Expanding fragments could create some "inefficiencies" that we wouldn't have if we hadn't re-optimized
|
|
894
|
-
// the fragments to de-optimize it later, so we do a final "trim" pass to remove those.
|
|
895
|
-
optimizedSelection = optimizedSelection.trimUnsatisfiableBranches(optimizedSelection.parentType);
|
|
896
|
-
return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, finalFragments, this.name);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
expandAllFragments(): Operation {
|
|
900
|
-
const expandedSelections = this.selectionSet.expandFragments();
|
|
901
|
-
if (expandedSelections === this.selectionSet) {
|
|
880
|
+
// Returns a copy of this operation with the provided updated selection set.
|
|
881
|
+
// Note that this method assumes that the existing `this.fragments` is still appropriate.
|
|
882
|
+
private withUpdatedSelectionSet(newSelectionSet: SelectionSet): Operation {
|
|
883
|
+
if (this.selectionSet === newSelectionSet) {
|
|
902
884
|
return this;
|
|
903
885
|
}
|
|
904
886
|
|
|
905
887
|
return new Operation(
|
|
906
888
|
this.schema,
|
|
907
889
|
this.rootKind,
|
|
908
|
-
|
|
890
|
+
newSelectionSet,
|
|
909
891
|
this.variableDefinitions,
|
|
910
|
-
|
|
892
|
+
this.fragments,
|
|
911
893
|
this.name
|
|
912
894
|
);
|
|
913
895
|
}
|
|
914
896
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
if (
|
|
897
|
+
// Returns a copy of this operation with the provided updated selection set and fragments.
|
|
898
|
+
private withUpdatedSelectionSetAndFragments(newSelectionSet: SelectionSet, newFragments: NamedFragments | undefined): Operation {
|
|
899
|
+
if (this.selectionSet === newSelectionSet && newFragments === this.fragments) {
|
|
918
900
|
return this;
|
|
919
901
|
}
|
|
920
902
|
|
|
921
903
|
return new Operation(
|
|
922
904
|
this.schema,
|
|
923
905
|
this.rootKind,
|
|
924
|
-
|
|
906
|
+
newSelectionSet,
|
|
925
907
|
this.variableDefinitions,
|
|
926
|
-
|
|
908
|
+
newFragments,
|
|
927
909
|
this.name
|
|
928
910
|
);
|
|
929
911
|
}
|
|
930
912
|
|
|
913
|
+
optimize(fragments?: NamedFragments, minUsagesToOptimize: number = 2): Operation {
|
|
914
|
+
assert(minUsagesToOptimize >= 1, `Expected 'minUsagesToOptimize' to be at least 1, but got ${minUsagesToOptimize}`)
|
|
915
|
+
if (!fragments || fragments.isEmpty()) {
|
|
916
|
+
return this;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
let optimizedSelection = this.selectionSet.optimize(fragments);
|
|
920
|
+
if (optimizedSelection === this.selectionSet) {
|
|
921
|
+
return this;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
let finalFragments = computeFragmentsToKeep(optimizedSelection, fragments, minUsagesToOptimize);
|
|
925
|
+
|
|
926
|
+
// If there is fragment usages and we're not keeping all fragments, we need to expand fragments.
|
|
927
|
+
if (finalFragments !== null && finalFragments?.size !== fragments.size) {
|
|
928
|
+
// Note that optimizing all fragments to potentially re-expand some is not entirely optimal, but it's unclear
|
|
929
|
+
// how to do otherwise, and it probably don't matter too much in practice (we only call this optimization
|
|
930
|
+
// on the final computed query plan, so not a very hot path; plus in most cases we won't even reach that
|
|
931
|
+
// point either because there is no fragment, or none will have been optimized away so we'll exit above).
|
|
932
|
+
optimizedSelection = optimizedSelection.expandFragments(finalFragments);
|
|
933
|
+
|
|
934
|
+
// Expanding fragments could create some "inefficiencies" that we wouldn't have if we hadn't re-optimized
|
|
935
|
+
// the fragments to de-optimize it later, so we do a final "normalize" pass to remove those.
|
|
936
|
+
optimizedSelection = optimizedSelection.normalize({ parentType: optimizedSelection.parentType });
|
|
937
|
+
|
|
938
|
+
// And if we've expanded some fragments but kept others, then it's not 100% impossible that some
|
|
939
|
+
// fragment was used multiple times in some expanded fragment(s), but that post-expansion all of
|
|
940
|
+
// it's usages are "dead" branches that are removed by the final `normalize`. In that case though,
|
|
941
|
+
// we need to ensure we don't include the now-unused fragment in the final list of fragments.
|
|
942
|
+
// TODO: remark that the same reasoning could leave a single instance of a fragment usage, so if
|
|
943
|
+
// we really really want to never have less than `minUsagesToOptimize`, we could do some loop of
|
|
944
|
+
// `expand then normalize` unless all fragments are provably used enough. We don't bother, because
|
|
945
|
+
// leaving this is not a huge deal and it's not worth the complexity, but it could be that we can
|
|
946
|
+
// refactor all this later to avoid this case without additional complexity.
|
|
947
|
+
if (finalFragments) {
|
|
948
|
+
// Note that removing a fragment might lead to another fragment being unused, so we need to iterate
|
|
949
|
+
// until there is nothing more to remove, or we're out of fragments.
|
|
950
|
+
let beforeRemoval: NamedFragments;
|
|
951
|
+
do {
|
|
952
|
+
beforeRemoval = finalFragments;
|
|
953
|
+
const usages = new Map<string, number>();
|
|
954
|
+
// Collecting all usages, both in the selection and within other fragments.
|
|
955
|
+
optimizedSelection.collectUsedFragmentNames(usages);
|
|
956
|
+
finalFragments.collectUsedFragmentNames(usages);
|
|
957
|
+
finalFragments = finalFragments.filter((f) => (usages.get(f.name) ?? 0) > 0);
|
|
958
|
+
} while (finalFragments && finalFragments.size < beforeRemoval.size);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return this.withUpdatedSelectionSetAndFragments(optimizedSelection, finalFragments ?? undefined);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
expandAllFragments(): Operation {
|
|
966
|
+
// We clear up the fragments since we've expanded all.
|
|
967
|
+
// Also note that expanding fragment usually generate unecessary fragments/inefficient selections, so it
|
|
968
|
+
// basically always make sense to normalize afterwards. Besides, fragment reuse (done by `optimize`) rely
|
|
969
|
+
// on the fact that its input is normalized to work properly, so all the more reason to do it here.
|
|
970
|
+
const expanded = this.selectionSet.expandFragments();
|
|
971
|
+
return this.withUpdatedSelectionSetAndFragments(expanded.normalize({ parentType: expanded.parentType }), undefined);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
normalize(): Operation {
|
|
975
|
+
return this.withUpdatedSelectionSet(this.selectionSet.normalize({ parentType: this.selectionSet.parentType }));
|
|
976
|
+
}
|
|
977
|
+
|
|
931
978
|
/**
|
|
932
979
|
* Returns this operation but potentially modified so all/some of the @defer applications have been removed.
|
|
933
980
|
*
|
|
@@ -936,10 +983,7 @@ export class Operation {
|
|
|
936
983
|
* applications are removed.
|
|
937
984
|
*/
|
|
938
985
|
withoutDefer(labelsToRemove?: Set<string>): Operation {
|
|
939
|
-
|
|
940
|
-
return updated == this.selectionSet
|
|
941
|
-
? this
|
|
942
|
-
: new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.fragments, this.name);
|
|
986
|
+
return this.withUpdatedSelectionSet(this.selectionSet.withoutDefer(labelsToRemove));
|
|
943
987
|
}
|
|
944
988
|
|
|
945
989
|
/**
|
|
@@ -965,8 +1009,7 @@ export class Operation {
|
|
|
965
1009
|
const { hasDefers, hasNonLabelledOrConditionalDefers } = normalizer.init(this.selectionSet);
|
|
966
1010
|
let updatedOperation: Operation = this;
|
|
967
1011
|
if (hasNonLabelledOrConditionalDefers) {
|
|
968
|
-
|
|
969
|
-
updatedOperation = new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.fragments, this.name);
|
|
1012
|
+
updatedOperation = this.withUpdatedSelectionSet(this.selectionSet.withNormalizedDefer(normalizer));
|
|
970
1013
|
}
|
|
971
1014
|
return {
|
|
972
1015
|
operation: updatedOperation,
|
|
@@ -991,6 +1034,8 @@ export class Operation {
|
|
|
991
1034
|
}
|
|
992
1035
|
}
|
|
993
1036
|
|
|
1037
|
+
export type FragmentRestrictionAtType = { selectionSet: SelectionSet, validator?: FieldsConflictValidator };
|
|
1038
|
+
|
|
994
1039
|
export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmentDefinition> {
|
|
995
1040
|
private _selectionSet: SelectionSet | undefined;
|
|
996
1041
|
|
|
@@ -1000,7 +1045,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
1000
1045
|
private _fragmentUsages: Map<string, number> | undefined;
|
|
1001
1046
|
private _includedFragmentNames: Set<string> | undefined;
|
|
1002
1047
|
|
|
1003
|
-
private readonly expandedSelectionSetsAtTypesCache = new Map<string,
|
|
1048
|
+
private readonly expandedSelectionSetsAtTypesCache = new Map<string, FragmentRestrictionAtType>();
|
|
1004
1049
|
|
|
1005
1050
|
constructor(
|
|
1006
1051
|
schema: Schema,
|
|
@@ -1027,7 +1072,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
1027
1072
|
|
|
1028
1073
|
expandedSelectionSet(): SelectionSet {
|
|
1029
1074
|
if (!this._expandedSelectionSet) {
|
|
1030
|
-
this._expandedSelectionSet = this.selectionSet.expandFragments().
|
|
1075
|
+
this._expandedSelectionSet = this.selectionSet.expandFragments().normalize({ parentType: this.typeCondition });
|
|
1031
1076
|
}
|
|
1032
1077
|
return this._expandedSelectionSet;
|
|
1033
1078
|
}
|
|
@@ -1071,13 +1116,31 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
1071
1116
|
}
|
|
1072
1117
|
|
|
1073
1118
|
/**
|
|
1074
|
-
* Whether this fragment may apply at the provided type, that
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1119
|
+
* Whether this fragment may apply _directly_ at the provided type, meaning that the fragment sub-selection
|
|
1120
|
+
* (_without_ the fragment condition, hence the "directly") can be normalized at `type` and this without
|
|
1121
|
+
* "widening" the runtime types to types that do not intersect the fragment condition.
|
|
1122
|
+
*
|
|
1123
|
+
* For that to be true, we need one of this to be true:
|
|
1124
|
+
* 1. the runtime types of the fragment condition must be at least as general as those of the provided `type`.
|
|
1125
|
+
* Otherwise, putting it at `type` without its condition would "generalize" more than the fragment meant to (and
|
|
1126
|
+
* so we'd "widen" the runtime types more than what the query meant to.
|
|
1127
|
+
* 2. either `type` and `this.typeCondition` are equal, or `type` is an object or `this.typeCondition` is a union
|
|
1128
|
+
* The idea is that, assuming our 1st point, then:
|
|
1129
|
+
* - if both are equal, things works trivially.
|
|
1130
|
+
* - if `type` is an object, `this.typeCondition` is either the same object, or a union/interface for which
|
|
1131
|
+
* type is a valid runtime. In all case, anything valid on `this.typeCondition` would apply to `type` too.
|
|
1132
|
+
* - if `this.typeCondition` is a union, then it's selection can only have fragments at top-level
|
|
1133
|
+
* (no fields save for `__typename`), and normalising is always fine with top-level fragments.
|
|
1134
|
+
* But in any other case, both types must be abstract (if `this.typeCondition` is an object, the 1st condition
|
|
1135
|
+
* imply `type` can only be the same type) and we're in one of:
|
|
1136
|
+
* - `type` and `this.typeCondition` are both different interfaces (that intersect but are different).
|
|
1137
|
+
* - `type` is aunion and `this.typeCondition` an interface.
|
|
1138
|
+
* And in both cases, since `this.typeCondition` is an interface, the fragment selection set may have field selections
|
|
1139
|
+
* on that interface, and those fields may not be valid for `type`.
|
|
1077
1140
|
*
|
|
1078
1141
|
* @param type - the type at which we're looking at applying the fragment
|
|
1079
1142
|
*/
|
|
1080
|
-
|
|
1143
|
+
canApplyDirectlyAtType(type: CompositeType): boolean {
|
|
1081
1144
|
if (sameType(type, this.typeCondition)) {
|
|
1082
1145
|
return true;
|
|
1083
1146
|
}
|
|
@@ -1090,17 +1153,20 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
1090
1153
|
|
|
1091
1154
|
const conditionRuntimes = possibleRuntimeTypes(this.typeCondition);
|
|
1092
1155
|
const typeRuntimes = possibleRuntimeTypes(type);
|
|
1093
|
-
|
|
1094
|
-
//
|
|
1095
|
-
// inside `type`, then it doesn't add restriction that weren't there without the fragment).
|
|
1156
|
+
// The fragment condition must be at least as general as the provided type (in other words, all of the
|
|
1157
|
+
// runtimes of `type` must be in `conditionRuntimes`).
|
|
1096
1158
|
// Note: the `length` test is technically redundant, but just avoid the more costly sub-set check if we
|
|
1097
1159
|
// can cheaply show it's unnecessary.
|
|
1098
|
-
|
|
1099
|
-
|
|
1160
|
+
if (conditionRuntimes.length < typeRuntimes.length
|
|
1161
|
+
|| !typeRuntimes.every((t1) => conditionRuntimes.some((t2) => sameType(t1, t2)))) {
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
return isObjectType(type) || isUnionType(this.typeCondition);
|
|
1100
1166
|
}
|
|
1101
1167
|
|
|
1102
1168
|
/**
|
|
1103
|
-
* This methods *assumes* that `this.
|
|
1169
|
+
* This methods *assumes* that `this.canApplyDirectlyAtType(type)` is `true` (and may crash if this is not true), and returns
|
|
1104
1170
|
* a version fo this named fragment selection set that corresponds to the "expansion" of this named fragment at `type`
|
|
1105
1171
|
*
|
|
1106
1172
|
* The overall idea here is that if we have an interface I with 2 implementations T1 and T2, and we have a fragment like:
|
|
@@ -1117,34 +1183,35 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
1117
1183
|
* 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
|
|
1118
1184
|
* us that part.
|
|
1119
1185
|
*/
|
|
1120
|
-
expandedSelectionSetAtType(type: CompositeType):
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
// First, if the candidate condition is an object or is the type passed, then there isn't any additional restriction to do.
|
|
1186
|
+
expandedSelectionSetAtType(type: CompositeType): FragmentRestrictionAtType {
|
|
1187
|
+
// First, if the candidate condition is an object or is the type passed, then there isn't any restriction to do.
|
|
1124
1188
|
if (sameType(type, this.typeCondition) || isObjectType(this.typeCondition)) {
|
|
1125
|
-
return expandedSelectionSet;
|
|
1189
|
+
return { selectionSet: this.expandedSelectionSet() };
|
|
1126
1190
|
}
|
|
1127
1191
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
// and costly to do however, so we just return the selection unchanged for now, which is always valid but simply may not be absolutely
|
|
1133
|
-
// optimal.
|
|
1134
|
-
// Concretely, this means that there may be corner cases where a named fragment could be reused but isn't, but waiting on finding
|
|
1135
|
-
// concrete examples where this matter to decide if it's worth the complexity.
|
|
1136
|
-
if (!isObjectType(type)) {
|
|
1137
|
-
return expandedSelectionSet;
|
|
1192
|
+
let cached = this.expandedSelectionSetsAtTypesCache.get(type.name);
|
|
1193
|
+
if (!cached) {
|
|
1194
|
+
cached = this.computeExpandedSelectionSetAtType(type);
|
|
1195
|
+
this.expandedSelectionSetsAtTypesCache.set(type.name, cached);
|
|
1138
1196
|
}
|
|
1197
|
+
return cached;
|
|
1198
|
+
}
|
|
1139
1199
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1200
|
+
private computeExpandedSelectionSetAtType(type: CompositeType): FragmentRestrictionAtType {
|
|
1201
|
+
const expandedSelectionSet = this.expandedSelectionSet();
|
|
1202
|
+
// Note that what we want is get any simplification coming from normalizing at `type`, but any such simplication
|
|
1203
|
+
// stops as soon as we traverse a field, so no point in being recursive.
|
|
1204
|
+
const selectionSet = expandedSelectionSet.normalize({ parentType: type, recursive: false });
|
|
1205
|
+
|
|
1206
|
+
// Note that `trimmed` is the difference of 2 selections that may not have been normalized on the same parent type,
|
|
1207
|
+
// so in practice, it is possible that `trimmed` contains some of the selections that `selectionSet` contains, but
|
|
1208
|
+
// that they have been simplified in `selectionSet` in such a way that the `minus` call does not see it. However,
|
|
1209
|
+
// it is not trivial to deal with this, and it is fine given that we use trimmed to create the validator because
|
|
1210
|
+
// we know the non-trimmed parts cannot create field conflict issues so we're trying to build a smaller validator,
|
|
1211
|
+
// but it's ok if trimmed is not as small as it theoretically can be.
|
|
1212
|
+
const trimmed = expandedSelectionSet.minus(selectionSet);
|
|
1213
|
+
const validator = trimmed.isEmpty() ? undefined : FieldsConflictValidator.build(trimmed);
|
|
1214
|
+
return { selectionSet, validator };
|
|
1148
1215
|
}
|
|
1149
1216
|
|
|
1150
1217
|
/**
|
|
@@ -1176,10 +1243,11 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
1176
1243
|
}
|
|
1177
1244
|
|
|
1178
1245
|
toString(indent?: string): string {
|
|
1179
|
-
return
|
|
1246
|
+
return `fragment ${this.name} on ${this.typeCondition}${this.appliedDirectivesToString()} ${this.selectionSet.toString(false, true, indent)}`;
|
|
1180
1247
|
}
|
|
1181
1248
|
}
|
|
1182
1249
|
|
|
1250
|
+
|
|
1183
1251
|
export class NamedFragments {
|
|
1184
1252
|
private readonly fragments = new MapWithCachedArrays<string, NamedFragmentDefinition>();
|
|
1185
1253
|
|
|
@@ -1208,8 +1276,8 @@ export class NamedFragments {
|
|
|
1208
1276
|
}
|
|
1209
1277
|
}
|
|
1210
1278
|
|
|
1211
|
-
|
|
1212
|
-
return this.fragments.values().filter(f => f.
|
|
1279
|
+
maybeApplyingDirectlyAtType(type: CompositeType): NamedFragmentDefinition[] {
|
|
1280
|
+
return this.fragments.values().filter(f => f.canApplyDirectlyAtType(type));
|
|
1213
1281
|
}
|
|
1214
1282
|
|
|
1215
1283
|
get(name: string): NamedFragmentDefinition | undefined {
|
|
@@ -1224,6 +1292,15 @@ export class NamedFragments {
|
|
|
1224
1292
|
return this.fragments.values();
|
|
1225
1293
|
}
|
|
1226
1294
|
|
|
1295
|
+
/**
|
|
1296
|
+
* Collect the usages of fragments that are used within the selection of other fragments.
|
|
1297
|
+
*/
|
|
1298
|
+
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
1299
|
+
for (const fragment of this.definitions()) {
|
|
1300
|
+
fragment.collectUsedFragmentNames(collector);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1227
1304
|
map(mapper: (def: NamedFragmentDefinition) => NamedFragmentDefinition): NamedFragments {
|
|
1228
1305
|
const mapped = new NamedFragments();
|
|
1229
1306
|
for (const def of this.fragments.values()) {
|
|
@@ -1288,7 +1365,7 @@ export class NamedFragments {
|
|
|
1288
1365
|
mapper: (selectionSet: SelectionSet) => SelectionSet | undefined,
|
|
1289
1366
|
): NamedFragments | undefined {
|
|
1290
1367
|
return this.mapInDependencyOrder((fragment, newFragments) => {
|
|
1291
|
-
const mappedSelectionSet = mapper(fragment.selectionSet.expandFragments().
|
|
1368
|
+
const mappedSelectionSet = mapper(fragment.selectionSet.expandFragments().normalize({ parentType: fragment.typeCondition }));
|
|
1292
1369
|
if (!mappedSelectionSet) {
|
|
1293
1370
|
return undefined;
|
|
1294
1371
|
}
|
|
@@ -1297,21 +1374,39 @@ export class NamedFragments {
|
|
|
1297
1374
|
});
|
|
1298
1375
|
}
|
|
1299
1376
|
|
|
1377
|
+
// When we rebase named fragments on a subgraph schema, only a subset of what the fragment handles may belong
|
|
1378
|
+
// to that particular subgraph. And there are a few sub-cases where that subset is such that we basically need or
|
|
1379
|
+
// want to consider to ignore the fragment for that subgraph, and that is when:
|
|
1380
|
+
// 1. the subset that apply is actually empty. The fragment wouldn't be valid in this case anyway.
|
|
1381
|
+
// 2. the subset is a single leaf field: in that case, using the one field directly is just shorter than using
|
|
1382
|
+
// the fragment, so we consider the fragment don't really apply to that subgraph. Technically, using the
|
|
1383
|
+
// fragment could still be of value if the fragment name is a lot smaller than the one field name, but it's
|
|
1384
|
+
// enough of a niche case that we ignore it. Note in particular that one sub-case of this rule that is likely
|
|
1385
|
+
// to be common is when the subset ends up being just `__typename`: this would basically mean the fragment
|
|
1386
|
+
// don't really apply to the subgraph, and that this will ensure this is the case.
|
|
1387
|
+
private selectionSetIsWorthUsing(selectionSet: SelectionSet): boolean {
|
|
1388
|
+
const selections = selectionSet.selections();
|
|
1389
|
+
if (selections.length === 0) {
|
|
1390
|
+
return false;
|
|
1391
|
+
}
|
|
1392
|
+
if (selections.length === 1) {
|
|
1393
|
+
const s = selections[0];
|
|
1394
|
+
return !(s.kind === 'FieldSelection' && s.element.isLeafField());
|
|
1395
|
+
}
|
|
1396
|
+
return true;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1300
1399
|
rebaseOn(schema: Schema): NamedFragments | undefined {
|
|
1301
1400
|
return this.mapInDependencyOrder((fragment, newFragments) => {
|
|
1302
1401
|
const rebasedType = schema.type(fragment.selectionSet.parentType.name);
|
|
1303
|
-
|
|
1304
|
-
if (!rebasedType || !isCompositeType(rebasedType)) {
|
|
1305
|
-
return undefined;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
const rebasedSelection = fragment.selectionSet.rebaseOn(rebasedType, newFragments);
|
|
1309
|
-
return new NamedFragmentDefinition(schema, fragment.name, rebasedType).setSelectionSet(rebasedSelection);
|
|
1310
|
-
} catch (e) {
|
|
1311
|
-
// This means we cannot rebase this selection on the schema and thus cannot reuse that fragment on that
|
|
1312
|
-
// particular schema.
|
|
1402
|
+
if (!rebasedType || !isCompositeType(rebasedType)) {
|
|
1313
1403
|
return undefined;
|
|
1314
1404
|
}
|
|
1405
|
+
|
|
1406
|
+
const rebasedSelection = fragment.selectionSet.rebaseOn({ parentType: rebasedType, fragments: newFragments, errorIfCannotRebase: false });
|
|
1407
|
+
return this.selectionSetIsWorthUsing(rebasedSelection)
|
|
1408
|
+
? new NamedFragmentDefinition(schema, fragment.name, rebasedType).setSelectionSet(rebasedSelection)
|
|
1409
|
+
: undefined;;
|
|
1315
1410
|
});
|
|
1316
1411
|
}
|
|
1317
1412
|
|
|
@@ -1417,6 +1512,8 @@ export enum ContainsResult {
|
|
|
1417
1512
|
EQUAL,
|
|
1418
1513
|
}
|
|
1419
1514
|
|
|
1515
|
+
export type CollectedFieldsInSet = { path: string[], field: FieldSelection }[];
|
|
1516
|
+
|
|
1420
1517
|
export class SelectionSet {
|
|
1421
1518
|
private readonly _keyedSelections: Map<string, Selection>;
|
|
1422
1519
|
private readonly _selections: readonly Selection[];
|
|
@@ -1447,7 +1544,21 @@ export class SelectionSet {
|
|
|
1447
1544
|
return this._keyedSelections.has(typenameFieldName);
|
|
1448
1545
|
}
|
|
1449
1546
|
|
|
1450
|
-
|
|
1547
|
+
withoutTopLevelTypenameField(): SelectionSet {
|
|
1548
|
+
if (!this.hasTopLevelTypenameField) {
|
|
1549
|
+
return this;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
const newKeyedSelections = new Map<string, Selection>();
|
|
1553
|
+
for (const [key, selection] of this._keyedSelections) {
|
|
1554
|
+
if (key !== typenameFieldName) {
|
|
1555
|
+
newKeyedSelections.set(key, selection);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return new SelectionSet(this.parentType, newKeyedSelections);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
fieldsInSet(): CollectedFieldsInSet {
|
|
1451
1562
|
const fields = new Array<{ path: string[], field: FieldSelection }>();
|
|
1452
1563
|
for (const selection of this.selections()) {
|
|
1453
1564
|
if (selection.kind === 'FieldSelection') {
|
|
@@ -1463,6 +1574,22 @@ export class SelectionSet {
|
|
|
1463
1574
|
return fields;
|
|
1464
1575
|
}
|
|
1465
1576
|
|
|
1577
|
+
fieldsByResponseName(): MultiMap<string, FieldSelection> {
|
|
1578
|
+
const byResponseName = new MultiMap<string, FieldSelection>();
|
|
1579
|
+
this.collectFieldsByResponseName(byResponseName);
|
|
1580
|
+
return byResponseName;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
private collectFieldsByResponseName(collector: MultiMap<string, FieldSelection>) {
|
|
1584
|
+
for (const selection of this.selections()) {
|
|
1585
|
+
if (selection.kind === 'FieldSelection') {
|
|
1586
|
+
collector.add(selection.element.responseName(), selection);
|
|
1587
|
+
} else {
|
|
1588
|
+
selection.selectionSet.collectFieldsByResponseName(collector);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1466
1593
|
usedVariables(): Variables {
|
|
1467
1594
|
const collector = new VariableCollector();
|
|
1468
1595
|
this.collectVariables(collector);
|
|
@@ -1502,7 +1629,8 @@ export class SelectionSet {
|
|
|
1502
1629
|
// With that, `optimizeSelections` will correctly match on the `on Query` fragment; after which
|
|
1503
1630
|
// we can unpack the final result.
|
|
1504
1631
|
const wrapped = new InlineFragmentSelection(new FragmentElement(this.parentType, this.parentType), this);
|
|
1505
|
-
const
|
|
1632
|
+
const validator = FieldsConflictMultiBranchValidator.ofInitial(FieldsConflictValidator.build(this));
|
|
1633
|
+
const optimized = wrapped.optimize(fragments, validator);
|
|
1506
1634
|
|
|
1507
1635
|
// Now, it's possible we matched a full fragment, in which case `optimized` will be just the named fragment,
|
|
1508
1636
|
// and in that case we return a singleton selection with just that. Otherwise, it's our wrapping inline fragment
|
|
@@ -1515,16 +1643,92 @@ export class SelectionSet {
|
|
|
1515
1643
|
// Tries to match fragments inside each selections of this selection set, and this recursively. However, note that this
|
|
1516
1644
|
// may not match fragments that would apply at top-level, so you should usually use `optimize` instead (this exists mostly
|
|
1517
1645
|
// for the recursion).
|
|
1518
|
-
optimizeSelections(fragments: NamedFragments): SelectionSet {
|
|
1519
|
-
return this.lazyMap((selection) => selection.optimize(fragments));
|
|
1646
|
+
optimizeSelections(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): SelectionSet {
|
|
1647
|
+
return this.lazyMap((selection) => selection.optimize(fragments, validator));
|
|
1520
1648
|
}
|
|
1521
1649
|
|
|
1522
1650
|
expandFragments(updatedFragments?: NamedFragments): SelectionSet {
|
|
1523
1651
|
return this.lazyMap((selection) => selection.expandFragments(updatedFragments));
|
|
1524
1652
|
}
|
|
1525
1653
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1654
|
+
/**
|
|
1655
|
+
* Applies some normalization rules to this selection set in the context of the provided `parentType`.
|
|
1656
|
+
*
|
|
1657
|
+
* Normalization mostly removes unecessary/redundant inline fragments, so that for instance, with
|
|
1658
|
+
* schema:
|
|
1659
|
+
* ```graphql
|
|
1660
|
+
* type Query {
|
|
1661
|
+
* t1: T1
|
|
1662
|
+
* i: I
|
|
1663
|
+
* }
|
|
1664
|
+
*
|
|
1665
|
+
* interface I {
|
|
1666
|
+
* id: ID!
|
|
1667
|
+
* }
|
|
1668
|
+
*
|
|
1669
|
+
* type T1 implements I {
|
|
1670
|
+
* id: ID!
|
|
1671
|
+
* v1: Int
|
|
1672
|
+
* }
|
|
1673
|
+
*
|
|
1674
|
+
* type T2 implements I {
|
|
1675
|
+
* id: ID!
|
|
1676
|
+
* v2: Int
|
|
1677
|
+
* }
|
|
1678
|
+
* ```
|
|
1679
|
+
*
|
|
1680
|
+
* ```
|
|
1681
|
+
* normalize({
|
|
1682
|
+
* t1 {
|
|
1683
|
+
* ... on I {
|
|
1684
|
+
* id
|
|
1685
|
+
* }
|
|
1686
|
+
* }
|
|
1687
|
+
* i {
|
|
1688
|
+
* ... on T1 {
|
|
1689
|
+
* ... on I {
|
|
1690
|
+
* ... on T1 {
|
|
1691
|
+
* v1
|
|
1692
|
+
* }
|
|
1693
|
+
* ... on T2 {
|
|
1694
|
+
* v2
|
|
1695
|
+
* }
|
|
1696
|
+
* }
|
|
1697
|
+
* }
|
|
1698
|
+
* ... on T2 {
|
|
1699
|
+
* ... on I {
|
|
1700
|
+
* id
|
|
1701
|
+
* }
|
|
1702
|
+
* }
|
|
1703
|
+
* }
|
|
1704
|
+
* }) === {
|
|
1705
|
+
* t1 {
|
|
1706
|
+
* id
|
|
1707
|
+
* }
|
|
1708
|
+
* i {
|
|
1709
|
+
* ... on T1 {
|
|
1710
|
+
* v1
|
|
1711
|
+
* }
|
|
1712
|
+
* ... on T2 {
|
|
1713
|
+
* id
|
|
1714
|
+
* }
|
|
1715
|
+
* }
|
|
1716
|
+
* }
|
|
1717
|
+
* ```
|
|
1718
|
+
*
|
|
1719
|
+
* For this operation to be valid (to not throw), `parentType` must be such that every field selection in
|
|
1720
|
+
* this selection set is such that the field parent type intersects `parentType` (there is no limitation
|
|
1721
|
+
* on the fragment selections, though any fragment selections whose condition do not intersects `parentType`
|
|
1722
|
+
* will be discarded). Note that `this.normalize(this.parentType)` is always valid and useful, but it is
|
|
1723
|
+
* also possible to pass a `parentType` that is more "restrictive" than the selection current parent type
|
|
1724
|
+
* (as long as the top-level fields of this selection set can be rebased on that type).
|
|
1725
|
+
*
|
|
1726
|
+
* Passing the option `recursive == false` makes the normalization only apply at the top-level, removing
|
|
1727
|
+
* any unecessary top-level inline fragments, possibly multiple layers of them, but we never recurse
|
|
1728
|
+
* inside the sub-selection of an selection that is not removed by the normalization.
|
|
1729
|
+
*/
|
|
1730
|
+
normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): SelectionSet {
|
|
1731
|
+
return this.lazyMap((selection) => selection.normalize({ parentType, recursive }), { parentType });
|
|
1528
1732
|
}
|
|
1529
1733
|
|
|
1530
1734
|
/**
|
|
@@ -1575,28 +1779,48 @@ export class SelectionSet {
|
|
|
1575
1779
|
}
|
|
1576
1780
|
|
|
1577
1781
|
/**
|
|
1578
|
-
* Returns the selection
|
|
1782
|
+
* Returns the selection set resulting from filtering out any of the top-level selection that does not match the provided predicate.
|
|
1579
1783
|
*
|
|
1580
|
-
* Please that this method
|
|
1581
|
-
* call `optimize` on the result if you want to re-apply some fragments.
|
|
1784
|
+
* Please that this method does not recurse within sub-selections.
|
|
1582
1785
|
*/
|
|
1583
1786
|
filter(predicate: (selection: Selection) => boolean): SelectionSet {
|
|
1584
|
-
return this.lazyMap((selection) => selection
|
|
1787
|
+
return this.lazyMap((selection) => predicate(selection) ? selection : undefined);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
/**
|
|
1791
|
+
* Returns the selection set resulting from "recursively" filtering any selection that does not match the provided predicate.
|
|
1792
|
+
* This method calls `predicate` on every selection of the selection set, not just top-level ones, and apply a "depth-first"
|
|
1793
|
+
* strategy, meaning that when the predicate is call on a given selection, the it is guaranteed that filtering has happened
|
|
1794
|
+
* on all the selections of its sub-selection.
|
|
1795
|
+
*/
|
|
1796
|
+
filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): SelectionSet {
|
|
1797
|
+
return this.lazyMap((selection) => selection.filterRecursiveDepthFirst(predicate));
|
|
1585
1798
|
}
|
|
1586
1799
|
|
|
1587
1800
|
withoutEmptyBranches(): SelectionSet | undefined {
|
|
1588
|
-
const updated = this.
|
|
1801
|
+
const updated = this.filterRecursiveDepthFirst((selection) => selection.selectionSet?.isEmpty() !== true);
|
|
1589
1802
|
return updated.isEmpty() ? undefined : updated;
|
|
1590
1803
|
}
|
|
1591
1804
|
|
|
1592
|
-
rebaseOn(
|
|
1805
|
+
rebaseOn({
|
|
1806
|
+
parentType,
|
|
1807
|
+
fragments,
|
|
1808
|
+
errorIfCannotRebase,
|
|
1809
|
+
}: {
|
|
1810
|
+
parentType: CompositeType,
|
|
1811
|
+
fragments: NamedFragments | undefined
|
|
1812
|
+
errorIfCannotRebase: boolean,
|
|
1813
|
+
}): SelectionSet {
|
|
1593
1814
|
if (this.parentType === parentType) {
|
|
1594
1815
|
return this;
|
|
1595
1816
|
}
|
|
1596
1817
|
|
|
1597
1818
|
const newSelections = new Map<string, Selection>();
|
|
1598
1819
|
for (const selection of this.selections()) {
|
|
1599
|
-
|
|
1820
|
+
const rebasedSelection = selection.rebaseOn({ parentType, fragments, errorIfCannotRebase });
|
|
1821
|
+
if (rebasedSelection) {
|
|
1822
|
+
newSelections.set(selection.key(), rebasedSelection);
|
|
1823
|
+
}
|
|
1600
1824
|
}
|
|
1601
1825
|
|
|
1602
1826
|
return new SelectionSet(parentType, newSelections);
|
|
@@ -1620,15 +1844,25 @@ export class SelectionSet {
|
|
|
1620
1844
|
return true;
|
|
1621
1845
|
}
|
|
1622
1846
|
|
|
1623
|
-
contains(that: SelectionSet): ContainsResult {
|
|
1847
|
+
contains(that: SelectionSet, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
|
|
1848
|
+
const ignoreMissingTypename = options?.ignoreMissingTypename ?? false;
|
|
1624
1849
|
if (that._selections.length > this._selections.length) {
|
|
1625
|
-
|
|
1850
|
+
// If `that` has more selections but we're ignoring missing __typename, then in the case where
|
|
1851
|
+
// `that` has a __typename but `this` does not, then we need the length of `that` to be at
|
|
1852
|
+
// least 2 more than that of `this` to be able to conclude there is no contains.
|
|
1853
|
+
if (!ignoreMissingTypename || that._selections.length > this._selections.length + 1 || this.hasTopLevelTypenameField() || !that.hasTopLevelTypenameField()) {
|
|
1854
|
+
return ContainsResult.NOT_CONTAINED;
|
|
1855
|
+
}
|
|
1626
1856
|
}
|
|
1627
1857
|
|
|
1628
1858
|
let isEqual = true;
|
|
1629
1859
|
for (const [key, thatSelection] of that._keyedSelections) {
|
|
1860
|
+
if (key === typenameFieldName && ignoreMissingTypename) {
|
|
1861
|
+
continue;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1630
1864
|
const thisSelection = this._keyedSelections.get(key);
|
|
1631
|
-
const selectionResult = thisSelection?.contains(thatSelection);
|
|
1865
|
+
const selectionResult = thisSelection?.contains(thatSelection, options);
|
|
1632
1866
|
if (selectionResult === undefined || selectionResult === ContainsResult.NOT_CONTAINED) {
|
|
1633
1867
|
return ContainsResult.NOT_CONTAINED;
|
|
1634
1868
|
}
|
|
@@ -1640,49 +1874,9 @@ export class SelectionSet {
|
|
|
1640
1874
|
: ContainsResult.STRICTLY_CONTAINED;
|
|
1641
1875
|
}
|
|
1642
1876
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
candidate: NamedFragmentDefinition,
|
|
1647
|
-
parentType: CompositeType,
|
|
1648
|
-
fragments: NamedFragments,
|
|
1649
|
-
): { contains: boolean, diff?: SelectionSet } {
|
|
1650
|
-
const that = candidate.expandedSelectionSetAtType(parentType);
|
|
1651
|
-
// It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
|
|
1652
|
-
// `parentType` is empty, or contains only `__typename`. For instance, suppose we have
|
|
1653
|
-
// a union `U = A | B | C`, and then a fragment:
|
|
1654
|
-
// ```graphql
|
|
1655
|
-
// fragment F on U {
|
|
1656
|
-
// ... on A {
|
|
1657
|
-
// x
|
|
1658
|
-
// }
|
|
1659
|
-
// ... on b {
|
|
1660
|
-
// y
|
|
1661
|
-
// }
|
|
1662
|
-
// }
|
|
1663
|
-
// ```
|
|
1664
|
-
// It is then possible to apply `F` when the parent type is `C`, but this ends up selecting
|
|
1665
|
-
// nothing at all.
|
|
1666
|
-
//
|
|
1667
|
-
// Returning `contains: true` in those cases is, while not 100% incorrect, at least not productive,
|
|
1668
|
-
// and so we skip right away in that case. This is essentially an optimisation.
|
|
1669
|
-
if (that.isEmpty() || (that.selections().length === 1 && that.selections()[0].isTypenameField())) {
|
|
1670
|
-
return { contains: false };
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
if (this.contains(that)) {
|
|
1674
|
-
// One subtlety here is that at "this" sub-selections may already have been optimized with some fragments. It's
|
|
1675
|
-
// usually ok because `candidate` will also use those fragments, but one fragments that `candidate` can never be
|
|
1676
|
-
// using is itself (the `contains` check is fine with this, but it's harder to deal in `minus`). So we expand
|
|
1677
|
-
// the candidate we're currently looking at in "this" to avoid some issues.
|
|
1678
|
-
let updatedThis = this.expandFragments(fragments.filter((f) => f.name !== candidate.name));
|
|
1679
|
-
if (updatedThis !== this) {
|
|
1680
|
-
updatedThis = updatedThis.trimUnsatisfiableBranches(parentType);
|
|
1681
|
-
}
|
|
1682
|
-
const diff = updatedThis.minus(that);
|
|
1683
|
-
return { contains: true, diff: diff.isEmpty() ? undefined : diff };
|
|
1684
|
-
}
|
|
1685
|
-
return { contains: false };
|
|
1877
|
+
containsTopLevelField(field: Field): boolean {
|
|
1878
|
+
const selection = this._keyedSelections.get(field.key());
|
|
1879
|
+
return !!selection && selection.element.equals(field);
|
|
1686
1880
|
}
|
|
1687
1881
|
|
|
1688
1882
|
/**
|
|
@@ -1706,6 +1900,28 @@ export class SelectionSet {
|
|
|
1706
1900
|
return updated.toSelectionSet(this.parentType);
|
|
1707
1901
|
}
|
|
1708
1902
|
|
|
1903
|
+
intersectionWith(that: SelectionSet): SelectionSet {
|
|
1904
|
+
if (this.isEmpty()) {
|
|
1905
|
+
return this;
|
|
1906
|
+
}
|
|
1907
|
+
if (that.isEmpty()) {
|
|
1908
|
+
return that;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const intersection = new SelectionSetUpdates();
|
|
1912
|
+
for (const [key, thisSelection] of this._keyedSelections) {
|
|
1913
|
+
const thatSelection = that._keyedSelections.get(key);
|
|
1914
|
+
if (thatSelection) {
|
|
1915
|
+
const selection = thisSelection.intersectionWith(thatSelection);
|
|
1916
|
+
if (selection) {
|
|
1917
|
+
intersection.add(selection);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
return intersection.toSelectionSet(this.parentType);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1709
1925
|
canRebaseOn(parentTypeToTest: CompositeType): boolean {
|
|
1710
1926
|
return this.selections().every((selection) => selection.canAddTo(parentTypeToTest));
|
|
1711
1927
|
}
|
|
@@ -1923,6 +2139,19 @@ export class SelectionSetUpdates {
|
|
|
1923
2139
|
toSelectionSet(parentType: CompositeType, fragments?: NamedFragments): SelectionSet {
|
|
1924
2140
|
return makeSelectionSet(parentType, this.keyedUpdates, fragments);
|
|
1925
2141
|
}
|
|
2142
|
+
|
|
2143
|
+
toString() {
|
|
2144
|
+
return '{\n'
|
|
2145
|
+
+ [...this.keyedUpdates.entries()].map(([k, updates]) => {
|
|
2146
|
+
const updStr = updates.map((upd) =>
|
|
2147
|
+
upd instanceof AbstractSelection
|
|
2148
|
+
? upd.toString()
|
|
2149
|
+
: `${upd.path} -> ${upd.selections}`
|
|
2150
|
+
);
|
|
2151
|
+
return ` - ${k}: ${updStr}`;
|
|
2152
|
+
}).join('\n')
|
|
2153
|
+
+'\n\}'
|
|
2154
|
+
}
|
|
1926
2155
|
}
|
|
1927
2156
|
|
|
1928
2157
|
function addToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, selections: Selection | SelectionSet | readonly Selection[]) {
|
|
@@ -1999,10 +2228,10 @@ function makeSelection(parentType: CompositeType, updates: SelectionUpdate[], fr
|
|
|
1999
2228
|
|
|
2000
2229
|
// Optimize for the simple case of a single selection, as we don't have to do anything complex to merge the sub-selections.
|
|
2001
2230
|
if (updates.length === 1 && first instanceof AbstractSelection) {
|
|
2002
|
-
return first.
|
|
2231
|
+
return first.rebaseOnOrError({ parentType, fragments });
|
|
2003
2232
|
}
|
|
2004
2233
|
|
|
2005
|
-
const element = updateElement(first).
|
|
2234
|
+
const element = updateElement(first).rebaseOnOrError(parentType);
|
|
2006
2235
|
const subSelectionParentType = element.kind === 'Field' ? element.baseType() : element.castedType();
|
|
2007
2236
|
if (!isCompositeType(subSelectionParentType)) {
|
|
2008
2237
|
// This is a leaf, so all updates should correspond ot the same field and we just use the first.
|
|
@@ -2185,13 +2414,17 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2185
2414
|
|
|
2186
2415
|
abstract key(): string;
|
|
2187
2416
|
|
|
2188
|
-
abstract optimize(fragments: NamedFragments): Selection;
|
|
2417
|
+
abstract optimize(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): Selection;
|
|
2189
2418
|
|
|
2190
2419
|
abstract toSelectionNode(): SelectionNode;
|
|
2191
2420
|
|
|
2192
2421
|
abstract validate(variableDefinitions: VariableDefinitions): void;
|
|
2193
2422
|
|
|
2194
|
-
abstract rebaseOn(parentType: CompositeType, fragments: NamedFragments | undefined): TOwnType;
|
|
2423
|
+
abstract rebaseOn(args: { parentType: CompositeType, fragments: NamedFragments | undefined, errorIfCannotRebase: boolean}): TOwnType | undefined;
|
|
2424
|
+
|
|
2425
|
+
rebaseOnOrError({ parentType, fragments }: { parentType: CompositeType, fragments: NamedFragments | undefined }): TOwnType {
|
|
2426
|
+
return this.rebaseOn({ parentType, fragments, errorIfCannotRebase: true})!;
|
|
2427
|
+
}
|
|
2195
2428
|
|
|
2196
2429
|
get parentType(): CompositeType {
|
|
2197
2430
|
return this.element.parentType;
|
|
@@ -2240,7 +2473,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2240
2473
|
|
|
2241
2474
|
abstract expandFragments(updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
|
|
2242
2475
|
|
|
2243
|
-
abstract
|
|
2476
|
+
abstract normalize(args: { parentType: CompositeType, recursive? : boolean }): TOwnType | SelectionSet | undefined;
|
|
2244
2477
|
|
|
2245
2478
|
isFragmentSpread(): boolean {
|
|
2246
2479
|
return false;
|
|
@@ -2258,26 +2491,51 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2258
2491
|
return undefined;
|
|
2259
2492
|
}
|
|
2260
2493
|
|
|
2494
|
+
intersectionWith(that: Selection): TOwnType | undefined {
|
|
2495
|
+
// If there is a subset, then we compute the intersection add that (if not empty).
|
|
2496
|
+
// Otherwise, the intersection is this element.
|
|
2497
|
+
if (this.selectionSet && that.selectionSet) {
|
|
2498
|
+
const subSelectionSetIntersection = this.selectionSet.intersectionWith(that.selectionSet);
|
|
2499
|
+
if (subSelectionSetIntersection.isEmpty()) {
|
|
2500
|
+
return undefined;
|
|
2501
|
+
} else {
|
|
2502
|
+
return this.withUpdatedSelectionSet(subSelectionSetIntersection);
|
|
2503
|
+
}
|
|
2504
|
+
} else {
|
|
2505
|
+
return this.us();
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2261
2509
|
protected tryOptimizeSubselectionWithFragments({
|
|
2262
2510
|
parentType,
|
|
2263
2511
|
subSelection,
|
|
2264
2512
|
fragments,
|
|
2513
|
+
validator,
|
|
2265
2514
|
canUseFullMatchingFragment,
|
|
2266
2515
|
}: {
|
|
2267
2516
|
parentType: CompositeType,
|
|
2268
2517
|
subSelection: SelectionSet,
|
|
2269
2518
|
fragments: NamedFragments,
|
|
2519
|
+
validator: FieldsConflictMultiBranchValidator,
|
|
2270
2520
|
canUseFullMatchingFragment: (match: NamedFragmentDefinition) => boolean,
|
|
2271
2521
|
}): SelectionSet | NamedFragmentDefinition {
|
|
2272
|
-
|
|
2522
|
+
// We limit to fragments whose selection could be applied "directly" at `parentType`, meaning without taking the fragment condition
|
|
2523
|
+
// into account. The idea being that if the fragment condition would be needed inside `parentType`, then that condition will not
|
|
2524
|
+
// have been "normalized away" and so we want for this very call to be called on the fragment whose type _is_ the fragment condition (at
|
|
2525
|
+
// which point, this `maybeApplyingDirectlyAtType` method will apply.
|
|
2526
|
+
// Also note that this is because we have this restriction that calling `expandedSelectionSetAtType` is ok.
|
|
2527
|
+
let candidates = fragments.maybeApplyingDirectlyAtType(parentType);
|
|
2528
|
+
if (candidates.length === 0) {
|
|
2529
|
+
return subSelection;
|
|
2530
|
+
}
|
|
2273
2531
|
|
|
2274
2532
|
// First, we check which of the candidates do apply inside `subSelection`, if any.
|
|
2275
2533
|
// If we find a candidate that applies to the whole `subSelection`, then we stop and only return
|
|
2276
2534
|
// that one candidate. Otherwise, we cumulate in `applyingFragments` the list of fragments that
|
|
2277
2535
|
// applies to a subset of `subSelection`.
|
|
2278
|
-
const applyingFragments: NamedFragmentDefinition[] = [];
|
|
2536
|
+
const applyingFragments: { fragment: NamedFragmentDefinition, atType: FragmentRestrictionAtType }[] = [];
|
|
2279
2537
|
for (const candidate of candidates) {
|
|
2280
|
-
|
|
2538
|
+
let atType = candidate.expandedSelectionSetAtType(parentType);
|
|
2281
2539
|
// It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
|
|
2282
2540
|
// `parentType` is empty, or contains only `__typename`. For instance, suppose we have
|
|
2283
2541
|
// a union `U = A | B | C`, and then a fragment:
|
|
@@ -2296,24 +2554,39 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2296
2554
|
//
|
|
2297
2555
|
// Using `F` in those cases is, while not 100% incorrect, at least not productive, and so we
|
|
2298
2556
|
// skip it that case. This is essentially an optimisation.
|
|
2299
|
-
if (
|
|
2557
|
+
if (atType.selectionSet.isEmpty() || (atType.selectionSet.selections().length === 1 && atType.selectionSet.selections()[0].isTypenameField())) {
|
|
2300
2558
|
continue;
|
|
2301
2559
|
}
|
|
2302
2560
|
|
|
2303
|
-
|
|
2561
|
+
// As we check inclusion, we ignore the case where the fragment queries __typename but the subSelection does not.
|
|
2562
|
+
// The rational is that querying `__typename` unecessarily is mostly harmless (it always works and it's super cheap)
|
|
2563
|
+
// so we don't want to not use a fragment just to save querying a `__typename` in a few cases. But the underlying
|
|
2564
|
+
// context of why this matters is that the query planner always requests __typename for abstract type, and will do
|
|
2565
|
+
// so in fragments too, but we can have a field that _does_ return an abstract type within a fragment, but that
|
|
2566
|
+
// _does not_ end up returning an abstract type when applied in a "more specific" context (think a fragment on
|
|
2567
|
+
// an interface I1 where a inside field returns another interface I2, but applied in the context of a implementation
|
|
2568
|
+
// type of I1 where that particular field returns an implementation of I2 rather than I2 directly; we would have
|
|
2569
|
+
// added __typename to the fragment (because it's all interfaces), but the selection itself, which only deals
|
|
2570
|
+
// with object type, may not have __typename requested; using the fragment might still be a good idea, and
|
|
2571
|
+
// querying __typename needlessly is a very small price to pay for that).
|
|
2572
|
+
const res = subSelection.contains(atType.selectionSet, { ignoreMissingTypename: true });
|
|
2304
2573
|
|
|
2305
2574
|
if (res === ContainsResult.EQUAL) {
|
|
2306
2575
|
if (canUseFullMatchingFragment(candidate)) {
|
|
2576
|
+
if (!validator.checkCanReuseFragmentAndTrackIt(atType)) {
|
|
2577
|
+
// We cannot use it at all, so no point in adding to `applyingFragments`.
|
|
2578
|
+
continue;
|
|
2579
|
+
}
|
|
2307
2580
|
return candidate;
|
|
2308
2581
|
}
|
|
2309
2582
|
// If we're not going to replace the full thing, then same reasoning a below.
|
|
2310
2583
|
if (candidate.appliedDirectives.length === 0) {
|
|
2311
|
-
applyingFragments.push(candidate);
|
|
2584
|
+
applyingFragments.push({ fragment: candidate, atType});
|
|
2312
2585
|
}
|
|
2313
2586
|
// Note that if a fragment applies to only a subset of the subSelection, then we really only can use
|
|
2314
2587
|
// it if that fragment is defined _without_ directives.
|
|
2315
2588
|
} else if (res === ContainsResult.STRICTLY_CONTAINED && candidate.appliedDirectives.length === 0) {
|
|
2316
|
-
applyingFragments.push(candidate);
|
|
2589
|
+
applyingFragments.push({ fragment: candidate, atType });
|
|
2317
2590
|
}
|
|
2318
2591
|
}
|
|
2319
2592
|
|
|
@@ -2322,7 +2595,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2322
2595
|
}
|
|
2323
2596
|
|
|
2324
2597
|
// We have found the list of fragments that applies to some subset of `subSelection`. In general, we
|
|
2325
|
-
// want
|
|
2598
|
+
// want to now produce the selection set with spread for those fragments plus any selection that is not
|
|
2326
2599
|
// covered by any of the fragments. For instance, suppose that `subselection` is `{ a b c d e }`
|
|
2327
2600
|
// and we have found that `fragment F1 on X { a b c }` and `fragment F2 on X { c d }` applies, then
|
|
2328
2601
|
// we will generate `{ ...F1 ...F2 e }`.
|
|
@@ -2369,16 +2642,16 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2369
2642
|
// return `{ ...F3 ...F4 }` in that case, but it would be technically better to return only `F4`.
|
|
2370
2643
|
// However, this feels niche, and it might be costly to verify such inclusions, so not doing it
|
|
2371
2644
|
// for now.
|
|
2372
|
-
const filteredApplyingFragments = applyingFragments.filter((
|
|
2645
|
+
const filteredApplyingFragments = applyingFragments.filter(({ fragment }) => !applyingFragments.some((o) => o.fragment.includes(fragment.name)))
|
|
2373
2646
|
|
|
2374
2647
|
let notCoveredByFragments = subSelection;
|
|
2375
2648
|
const optimized = new SelectionSetUpdates();
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
notCoveredByFragments = notCoveredByFragments.
|
|
2649
|
+
for (const { fragment, atType} of filteredApplyingFragments) {
|
|
2650
|
+
if (!validator.checkCanReuseFragmentAndTrackIt(atType)) {
|
|
2651
|
+
continue;
|
|
2652
|
+
}
|
|
2653
|
+
const notCovered = subSelection.minus(atType.selectionSet);
|
|
2654
|
+
notCoveredByFragments = notCoveredByFragments.intersectionWith(notCovered);
|
|
2382
2655
|
optimized.add(new FragmentSpreadSelection(parentType, fragments, fragment, []));
|
|
2383
2656
|
}
|
|
2384
2657
|
|
|
@@ -2386,6 +2659,201 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2386
2659
|
}
|
|
2387
2660
|
}
|
|
2388
2661
|
|
|
2662
|
+
class FieldsConflictMultiBranchValidator {
|
|
2663
|
+
private usedSpreadTrimmedPartAtLevel?: FieldsConflictValidator[];
|
|
2664
|
+
|
|
2665
|
+
constructor(
|
|
2666
|
+
private readonly validators: FieldsConflictValidator[],
|
|
2667
|
+
) {
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
static ofInitial(validator: FieldsConflictValidator): FieldsConflictMultiBranchValidator {
|
|
2671
|
+
return new FieldsConflictMultiBranchValidator([validator]);
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
forField(field: Field): FieldsConflictMultiBranchValidator {
|
|
2675
|
+
const forAllBranches = this.validators.flatMap((vs) => vs.forField(field));
|
|
2676
|
+
// As this is called on (non-leaf) field from the same query on which we have build the initial validators, we
|
|
2677
|
+
// should find at least one validator.
|
|
2678
|
+
assert(forAllBranches.length > 0, `Shoud have found at least one validator for ${field}`);
|
|
2679
|
+
return new FieldsConflictMultiBranchValidator(forAllBranches);
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
// At this point, we known that the fragment, restricted to the current parent type, matches a subset of the
|
|
2683
|
+
// sub-selection. However, there is still one case we we cannot use it that we need to check, and this is
|
|
2684
|
+
// if using the fragment would create a field "conflict" (in the sense of the graphQL spec
|
|
2685
|
+
// [`FieldsInSetCanMerge`](https://spec.graphql.org/draft/#FieldsInSetCanMerge())) and thus create an
|
|
2686
|
+
// invalid selection. To be clear, `atType.selectionSet` cannot create a conflict, since it is a subset
|
|
2687
|
+
// of `subSelection` and `subSelection` is valid. *But* there may be some part of the fragment that
|
|
2688
|
+
// is not `atType.selectionSet` due to being "dead branches" for type `parentType`. And while those
|
|
2689
|
+
// branches _are_ "dead" as far as execution goes, the `FieldsInSetCanMerge` validation does not take
|
|
2690
|
+
// this into account (it's 1st step says "including visiting fragments and inline fragments" but has
|
|
2691
|
+
// no logic regarding ignoring any fragment that may not apply due to the intersection of runtimes
|
|
2692
|
+
// between multiple fragment being empty).
|
|
2693
|
+
checkCanReuseFragmentAndTrackIt(fragment: FragmentRestrictionAtType): boolean {
|
|
2694
|
+
// No validator means that everything in the fragment selection was part of the selection we're optimizing
|
|
2695
|
+
// away (by using the fragment), and we know the original selection was ok, so nothing to check.
|
|
2696
|
+
const validator = fragment.validator;
|
|
2697
|
+
if (!validator) {
|
|
2698
|
+
return true;
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
if (!this.validators.every((v) => v.doMergeWith(validator))) {
|
|
2702
|
+
return false;
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// We need to make sure the trimmed parts of `fragment` merges with the rest of the selection,
|
|
2706
|
+
// but also that it merge with any of the trimmed parts of any fragment we have added already.
|
|
2707
|
+
// Note: this last condition means that if 2 fragment conflict on their "trimmed" parts,
|
|
2708
|
+
// then the choice of which is used can be based on the fragment ordering and selection order,
|
|
2709
|
+
// which may not be optimal. This feels niche enough that we keep it simple for now, but we
|
|
2710
|
+
// can revisit this decision if we run into real cases that justify it (but making it optimal
|
|
2711
|
+
// would be a involved in general, as in theory you could have complex dependencies of fragments
|
|
2712
|
+
// that conflict, even cycles, and you need to take the size of fragments into account to know
|
|
2713
|
+
// what's best; and even then, this could even depend on overall usage, as it can be better to
|
|
2714
|
+
// reuse a fragment that is used in other places, than to use one for which it's the only usage.
|
|
2715
|
+
// Adding to all that the fact that conflict can happen in sibling branches).
|
|
2716
|
+
if (this.usedSpreadTrimmedPartAtLevel) {
|
|
2717
|
+
if (!this.usedSpreadTrimmedPartAtLevel.every((t) => validator.doMergeWith(t))) {
|
|
2718
|
+
return false;
|
|
2719
|
+
}
|
|
2720
|
+
} else {
|
|
2721
|
+
this.usedSpreadTrimmedPartAtLevel = [];
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
// We're good, but track the fragment
|
|
2725
|
+
this.usedSpreadTrimmedPartAtLevel.push(validator);
|
|
2726
|
+
return true;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
class FieldsConflictValidator {
|
|
2731
|
+
private constructor(
|
|
2732
|
+
private readonly byResponseName: Map<string, Map<Field, FieldsConflictValidator | null>>,
|
|
2733
|
+
) {
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
static build(s: SelectionSet): FieldsConflictValidator {
|
|
2737
|
+
return FieldsConflictValidator.forLevel(s.fieldsInSet());
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
private static forLevel(level: CollectedFieldsInSet): FieldsConflictValidator {
|
|
2741
|
+
const atLevel = new Map<string, Map<Field, CollectedFieldsInSet | null>>();
|
|
2742
|
+
|
|
2743
|
+
for (const { field } of level) {
|
|
2744
|
+
const responseName = field.element.responseName();
|
|
2745
|
+
let atResponseName = atLevel.get(responseName);
|
|
2746
|
+
if (!atResponseName) {
|
|
2747
|
+
atResponseName = new Map<Field, CollectedFieldsInSet>();
|
|
2748
|
+
atLevel.set(responseName, atResponseName);
|
|
2749
|
+
}
|
|
2750
|
+
if (field.selectionSet) {
|
|
2751
|
+
// It's unlikely that we've seen the same `field.element` as we don't particularly "intern" `Field` object (so even if the exact same field
|
|
2752
|
+
// is used in 2 parts of a selection set, it will probably be a different `Field` object), so the `get` below will probably mostly return `undefined`,
|
|
2753
|
+
// but it wouldn't be incorrect to re-use a `Field` object multiple side, so no reason not to handle that correctly.
|
|
2754
|
+
let forField = atResponseName.get(field.element) ?? [];
|
|
2755
|
+
atResponseName.set(field.element, forField.concat(field.selectionSet.fieldsInSet()));
|
|
2756
|
+
} else {
|
|
2757
|
+
// Note that whether a `FieldSelection` has `selectionSet` or not is entirely determined by whether the field type is a composite type
|
|
2758
|
+
// or not, so even if we've seen a previous version of `field.element` before, we know it's guarantee to have had no `selectionSet`.
|
|
2759
|
+
// So the `set` below may overwrite a previous entry, but it would be a `null` so no harm done.
|
|
2760
|
+
atResponseName.set(field.element, null);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
const byResponseName = new Map<string, Map<Field, FieldsConflictValidator | null>>();
|
|
2765
|
+
for (const [name, level] of atLevel.entries()) {
|
|
2766
|
+
const atResponseName = new Map<Field, FieldsConflictValidator | null>();
|
|
2767
|
+
for (const [field, collectedFields] of level) {
|
|
2768
|
+
const validator = collectedFields ? FieldsConflictValidator.forLevel(collectedFields) : null;
|
|
2769
|
+
atResponseName.set(field, validator);
|
|
2770
|
+
}
|
|
2771
|
+
byResponseName.set(name, atResponseName);
|
|
2772
|
+
}
|
|
2773
|
+
return new FieldsConflictValidator(byResponseName);
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
forField(field: Field): FieldsConflictValidator[] {
|
|
2777
|
+
const byResponseName = this.byResponseName.get(field.responseName());
|
|
2778
|
+
if (!byResponseName) {
|
|
2779
|
+
return [];
|
|
2780
|
+
}
|
|
2781
|
+
return mapValues(byResponseName).filter((v): v is FieldsConflictValidator => !!v);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
doMergeWith(that: FieldsConflictValidator): boolean {
|
|
2785
|
+
for (const [responseName, thisFields] of this.byResponseName.entries()) {
|
|
2786
|
+
const thatFields = that.byResponseName.get(responseName);
|
|
2787
|
+
if (!thatFields) {
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// We're basically checking [FieldInSetCanMerge](https://spec.graphql.org/draft/#FieldsInSetCanMerge()),
|
|
2792
|
+
// but from 2 set of fields (`thisFields` and `thatFields`) of the same response that we know individually
|
|
2793
|
+
// merge already.
|
|
2794
|
+
for (const [thisField, thisValidator] of thisFields.entries()) {
|
|
2795
|
+
for (const [thatField, thatValidator] of thatFields.entries()) {
|
|
2796
|
+
// The `SameResponseShape` test that all fields must pass.
|
|
2797
|
+
if (!typesCanBeMerged(thisField.definition.type!, thatField.definition.type!)) {
|
|
2798
|
+
return false;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
const p1 = thisField.parentType;
|
|
2802
|
+
const p2 = thatField.parentType;
|
|
2803
|
+
if (sameType(p1, p2) || !isObjectType(p1) || !isObjectType(p2)) {
|
|
2804
|
+
// Additional checks of `FieldsInSetCanMerge` when same parent type or one isn't object
|
|
2805
|
+
if (thisField.name !== thatField.name
|
|
2806
|
+
|| !argumentsEquals(thisField.args ?? {}, thatField.args ?? {})
|
|
2807
|
+
|| (thisValidator && thatValidator && !thisValidator.doMergeWith(thatValidator))
|
|
2808
|
+
) {
|
|
2809
|
+
return false;
|
|
2810
|
+
}
|
|
2811
|
+
} else {
|
|
2812
|
+
// Otherwise, the sub-selection must pass [SameResponseShape](https://spec.graphql.org/draft/#SameResponseShape()).
|
|
2813
|
+
if (thisValidator && thatValidator && !thisValidator.hasSameResponseShapeThan(thatValidator)) {
|
|
2814
|
+
return false;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
return true;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
hasSameResponseShapeThan(that: FieldsConflictValidator): boolean {
|
|
2824
|
+
for (const [responseName, thisFields] of this.byResponseName.entries()) {
|
|
2825
|
+
const thatFields = that.byResponseName.get(responseName);
|
|
2826
|
+
if (!thatFields) {
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
for (const [thisField, thisValidator] of thisFields.entries()) {
|
|
2831
|
+
for (const [thatField, thatValidator] of thatFields.entries()) {
|
|
2832
|
+
if (!typesCanBeMerged(thisField.definition.type!, thatField.definition.type!)
|
|
2833
|
+
|| (thisValidator && thatValidator && !thisValidator.hasSameResponseShapeThan(thatValidator))) {
|
|
2834
|
+
return false;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
return true;
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
toString(indent: string = ''): string {
|
|
2843
|
+
// For debugging/testing ...
|
|
2844
|
+
return '{\n'
|
|
2845
|
+
+ [...this.byResponseName.entries()].map(([name, byFields]) => {
|
|
2846
|
+
const innerIndent = indent + ' ';
|
|
2847
|
+
return `${innerIndent}${name}: [\n`
|
|
2848
|
+
+ [...byFields.entries()]
|
|
2849
|
+
.map(([field, next]) => `${innerIndent} ${field.parentType}.${field}${next ? next.toString(innerIndent + ' '): ''}`)
|
|
2850
|
+
.join('\n')
|
|
2851
|
+
+ `\n${innerIndent}]`;
|
|
2852
|
+
}).join('\n')
|
|
2853
|
+
+ `\n${indent}}`
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2389
2857
|
export class FieldSelection extends AbstractSelection<Field<any>, undefined, FieldSelection> {
|
|
2390
2858
|
readonly kind = 'FieldSelection' as const;
|
|
2391
2859
|
|
|
@@ -2409,6 +2877,9 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2409
2877
|
}
|
|
2410
2878
|
|
|
2411
2879
|
withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
|
|
2880
|
+
if (this.element === field && this.selectionSet === selectionSet) {
|
|
2881
|
+
return this;
|
|
2882
|
+
}
|
|
2412
2883
|
return new FieldSelection(field, selectionSet);
|
|
2413
2884
|
}
|
|
2414
2885
|
|
|
@@ -2416,45 +2887,46 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2416
2887
|
return this.element.key();
|
|
2417
2888
|
}
|
|
2418
2889
|
|
|
2419
|
-
optimize(fragments: NamedFragments): Selection {
|
|
2890
|
+
optimize(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): Selection {
|
|
2420
2891
|
const fieldBaseType = baseType(this.element.definition.type!);
|
|
2421
2892
|
if (!isCompositeType(fieldBaseType) || !this.selectionSet) {
|
|
2422
2893
|
return this;
|
|
2423
2894
|
}
|
|
2424
2895
|
|
|
2896
|
+
const fieldValidator = validator.forField(this.element);
|
|
2897
|
+
|
|
2425
2898
|
// First, see if we can reuse fragments for the selection of this field.
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
});
|
|
2899
|
+
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
2900
|
+
parentType: fieldBaseType,
|
|
2901
|
+
subSelection: this.selectionSet,
|
|
2902
|
+
fragments,
|
|
2903
|
+
validator: fieldValidator,
|
|
2904
|
+
// We can never apply a fragments that has directives on it at the field level.
|
|
2905
|
+
canUseFullMatchingFragment: (fragment) => fragment.appliedDirectives.length === 0,
|
|
2906
|
+
});
|
|
2435
2907
|
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2908
|
+
let optimizedSelection;
|
|
2909
|
+
if (optimized instanceof NamedFragmentDefinition) {
|
|
2910
|
+
optimizedSelection = selectionSetOf(fieldBaseType, new FragmentSpreadSelection(fieldBaseType, fragments, optimized, []));
|
|
2911
|
+
} else {
|
|
2912
|
+
optimizedSelection = optimized;
|
|
2441
2913
|
}
|
|
2442
2914
|
|
|
2443
2915
|
// Then, recurse inside the field sub-selection (note that if we matched some fragments above,
|
|
2444
2916
|
// this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
|
|
2445
|
-
optimizedSelection = optimizedSelection.
|
|
2917
|
+
optimizedSelection = optimizedSelection.optimizeSelections(fragments, fieldValidator);
|
|
2446
2918
|
|
|
2447
2919
|
return this.selectionSet === optimizedSelection
|
|
2448
2920
|
? this
|
|
2449
2921
|
: this.withUpdatedSelectionSet(optimizedSelection);
|
|
2450
2922
|
}
|
|
2451
2923
|
|
|
2452
|
-
|
|
2924
|
+
filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
|
|
2453
2925
|
if (!this.selectionSet) {
|
|
2454
2926
|
return predicate(this) ? this : undefined;
|
|
2455
2927
|
}
|
|
2456
2928
|
|
|
2457
|
-
const updatedSelectionSet = this.selectionSet.
|
|
2929
|
+
const updatedSelectionSet = this.selectionSet.filterRecursiveDepthFirst(predicate);
|
|
2458
2930
|
const thisWithFilteredSelectionSet = this.selectionSet === updatedSelectionSet
|
|
2459
2931
|
? this
|
|
2460
2932
|
: new FieldSelection(this.element, updatedSelectionSet);
|
|
@@ -2480,12 +2952,24 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2480
2952
|
* Obviously, this operation will only succeed if this selection (both the field itself and its subselections)
|
|
2481
2953
|
* make sense from the provided parent type. If this is not the case, this method will throw.
|
|
2482
2954
|
*/
|
|
2483
|
-
rebaseOn(
|
|
2955
|
+
rebaseOn({
|
|
2956
|
+
parentType,
|
|
2957
|
+
fragments,
|
|
2958
|
+
errorIfCannotRebase,
|
|
2959
|
+
}: {
|
|
2960
|
+
parentType: CompositeType,
|
|
2961
|
+
fragments: NamedFragments | undefined,
|
|
2962
|
+
errorIfCannotRebase: boolean,
|
|
2963
|
+
}): FieldSelection | undefined {
|
|
2484
2964
|
if (this.element.parentType === parentType) {
|
|
2485
2965
|
return this;
|
|
2486
2966
|
}
|
|
2487
2967
|
|
|
2488
|
-
const rebasedElement = this.element.rebaseOn(parentType);
|
|
2968
|
+
const rebasedElement = this.element.rebaseOn({ parentType, errorIfCannotRebase });
|
|
2969
|
+
if (!rebasedElement) {
|
|
2970
|
+
return undefined;
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2489
2973
|
if (!this.selectionSet) {
|
|
2490
2974
|
return this.withUpdatedElement(rebasedElement);
|
|
2491
2975
|
}
|
|
@@ -2496,7 +2980,8 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2496
2980
|
}
|
|
2497
2981
|
|
|
2498
2982
|
validate(isCompositeType(rebasedBase), () => `Cannot rebase field selection ${this} on ${parentType}: rebased field base return type ${rebasedBase} is not composite`);
|
|
2499
|
-
|
|
2983
|
+
const rebasedSelectionSet = this.selectionSet.rebaseOn({ parentType: rebasedBase, fragments, errorIfCannotRebase });
|
|
2984
|
+
return rebasedSelectionSet.isEmpty() ? undefined : this.withUpdatedComponents(rebasedElement, rebasedSelectionSet);
|
|
2500
2985
|
}
|
|
2501
2986
|
|
|
2502
2987
|
/**
|
|
@@ -2547,28 +3032,41 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2547
3032
|
return !!this.selectionSet?.hasDefer();
|
|
2548
3033
|
}
|
|
2549
3034
|
|
|
2550
|
-
|
|
3035
|
+
normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FieldSelection {
|
|
3036
|
+
// This could be an interface field, and if we're normalizing on one of the implementation of that
|
|
3037
|
+
// interface, we want to make sure we use the field of the implementation, as it may in particular
|
|
3038
|
+
// have a more specific type which should propagate to the recursive call to normalize.
|
|
3039
|
+
|
|
3040
|
+
const definition = parentType === this.parentType
|
|
3041
|
+
? this.element.definition
|
|
3042
|
+
: parentType.field(this.element.name);
|
|
3043
|
+
assert(definition, `Cannot normalize ${this.element} at ${parentType} which does not have that field`)
|
|
3044
|
+
|
|
3045
|
+
const element = this.element.definition === definition ? this.element : this.element.withUpdatedDefinition(definition);
|
|
2551
3046
|
if (!this.selectionSet) {
|
|
2552
|
-
return this;
|
|
3047
|
+
return this.withUpdatedElement(element);
|
|
2553
3048
|
}
|
|
2554
3049
|
|
|
2555
|
-
const base =
|
|
2556
|
-
assert(isCompositeType(base), () => `Field ${
|
|
2557
|
-
const
|
|
3050
|
+
const base = element.baseType();
|
|
3051
|
+
assert(isCompositeType(base), () => `Field ${element} should not have a sub-selection`);
|
|
3052
|
+
const normalizedSubSelection = (recursive ?? true) ? this.selectionSet.normalize({ parentType: base }) : this.selectionSet;
|
|
2558
3053
|
// In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
|
|
2559
3054
|
// sub-selection is empty. Which suggest something may be wrong with this part of the query
|
|
2560
3055
|
// intent, but the query was valid while keeping an empty sub-selection isn't. So in that
|
|
2561
3056
|
// case, we just add some "non-included" __typename field just to keep the query valid.
|
|
2562
|
-
if (
|
|
2563
|
-
return
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
3057
|
+
if (normalizedSubSelection?.isEmpty()) {
|
|
3058
|
+
return this.withUpdatedComponents(
|
|
3059
|
+
element,
|
|
3060
|
+
selectionSetOfElement(
|
|
3061
|
+
new Field(
|
|
3062
|
+
base.typenameField()!,
|
|
3063
|
+
undefined,
|
|
3064
|
+
[new Directive('include', { 'if': false })],
|
|
3065
|
+
)
|
|
2568
3066
|
)
|
|
2569
|
-
)
|
|
3067
|
+
);
|
|
2570
3068
|
} else {
|
|
2571
|
-
return
|
|
3069
|
+
return this.withUpdatedComponents(element, normalizedSubSelection);
|
|
2572
3070
|
}
|
|
2573
3071
|
}
|
|
2574
3072
|
|
|
@@ -2590,7 +3088,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2590
3088
|
return !!that.selectionSet && this.selectionSet.equals(that.selectionSet);
|
|
2591
3089
|
}
|
|
2592
3090
|
|
|
2593
|
-
contains(that: Selection): ContainsResult {
|
|
3091
|
+
contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
|
|
2594
3092
|
if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
|
|
2595
3093
|
return ContainsResult.NOT_CONTAINED;
|
|
2596
3094
|
}
|
|
@@ -2600,7 +3098,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2600
3098
|
return ContainsResult.EQUAL;
|
|
2601
3099
|
}
|
|
2602
3100
|
assert(that.selectionSet, '`this` and `that` have the same element, so if one has sub-selection, the other one should too')
|
|
2603
|
-
return this.selectionSet.contains(that.selectionSet);
|
|
3101
|
+
return this.selectionSet.contains(that.selectionSet, options);
|
|
2604
3102
|
}
|
|
2605
3103
|
|
|
2606
3104
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
@@ -2628,11 +3126,10 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
|
|
|
2628
3126
|
}
|
|
2629
3127
|
}
|
|
2630
3128
|
|
|
2631
|
-
|
|
3129
|
+
filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
|
|
2632
3130
|
// Note that we essentially expand all fragments as part of this.
|
|
2633
|
-
const
|
|
2634
|
-
const
|
|
2635
|
-
const thisWithFilteredSelectionSet = updatedSelectionSet === selectionSet
|
|
3131
|
+
const updatedSelectionSet = this.selectionSet.filterRecursiveDepthFirst(predicate);
|
|
3132
|
+
const thisWithFilteredSelectionSet = updatedSelectionSet === this.selectionSet
|
|
2636
3133
|
? this
|
|
2637
3134
|
: new InlineFragmentSelection(this.element, updatedSelectionSet);
|
|
2638
3135
|
|
|
@@ -2645,7 +3142,27 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
|
|
|
2645
3142
|
|
|
2646
3143
|
abstract equals(that: Selection): boolean;
|
|
2647
3144
|
|
|
2648
|
-
abstract contains(that: Selection): ContainsResult;
|
|
3145
|
+
abstract contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult;
|
|
3146
|
+
|
|
3147
|
+
normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
|
|
3148
|
+
const thisCondition = this.element.typeCondition;
|
|
3149
|
+
|
|
3150
|
+
// This method assumes by contract that `parentType` runtimes intersects `this.parentType`'s, but `parentType`
|
|
3151
|
+
// runtimes may be a subset. So first check if the selection should not be discarded on that account (that
|
|
3152
|
+
// is, we should not keep the selection if its condition runtimes don't intersect at all with those of
|
|
3153
|
+
// `parentType` as that would ultimately make an invalid selection set).
|
|
3154
|
+
if (thisCondition && parentType !== this.parentType) {
|
|
3155
|
+
const conditionRuntimes = possibleRuntimeTypes(thisCondition);
|
|
3156
|
+
const typeRuntimes = possibleRuntimeTypes(parentType);
|
|
3157
|
+
if (!conditionRuntimes.some((t) => typeRuntimes.includes(t))) {
|
|
3158
|
+
return undefined;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
return this.normalizeKnowingItIntersects({ parentType, recursive });
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
protected abstract normalizeKnowingItIntersects({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined;
|
|
2649
3166
|
}
|
|
2650
3167
|
|
|
2651
3168
|
class InlineFragmentSelection extends FragmentSelection {
|
|
@@ -2665,6 +3182,9 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2665
3182
|
}
|
|
2666
3183
|
|
|
2667
3184
|
withUpdatedComponents(fragment: FragmentElement, selectionSet: SelectionSet): InlineFragmentSelection {
|
|
3185
|
+
if (fragment === this.element && selectionSet === this.selectionSet) {
|
|
3186
|
+
return this;
|
|
3187
|
+
}
|
|
2668
3188
|
return new InlineFragmentSelection(fragment, selectionSet);
|
|
2669
3189
|
}
|
|
2670
3190
|
|
|
@@ -2679,18 +3199,31 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2679
3199
|
this.selectionSet.validate(variableDefinitions);
|
|
2680
3200
|
}
|
|
2681
3201
|
|
|
2682
|
-
rebaseOn(
|
|
3202
|
+
rebaseOn({
|
|
3203
|
+
parentType,
|
|
3204
|
+
fragments,
|
|
3205
|
+
errorIfCannotRebase,
|
|
3206
|
+
}: {
|
|
3207
|
+
parentType: CompositeType,
|
|
3208
|
+
fragments: NamedFragments | undefined,
|
|
3209
|
+
errorIfCannotRebase: boolean,
|
|
3210
|
+
}): FragmentSelection | undefined {
|
|
2683
3211
|
if (this.parentType === parentType) {
|
|
2684
3212
|
return this;
|
|
2685
3213
|
}
|
|
2686
3214
|
|
|
2687
|
-
const rebasedFragment = this.element.rebaseOn(parentType);
|
|
3215
|
+
const rebasedFragment = this.element.rebaseOn({ parentType, errorIfCannotRebase });
|
|
3216
|
+
if (!rebasedFragment) {
|
|
3217
|
+
return undefined;
|
|
3218
|
+
}
|
|
3219
|
+
|
|
2688
3220
|
const rebasedCastedType = rebasedFragment.castedType();
|
|
2689
3221
|
if (rebasedCastedType === this.selectionSet.parentType) {
|
|
2690
3222
|
return this.withUpdatedElement(rebasedFragment);
|
|
2691
3223
|
}
|
|
2692
3224
|
|
|
2693
|
-
|
|
3225
|
+
const rebasedSelectionSet = this.selectionSet.rebaseOn({ parentType: rebasedCastedType, fragments, errorIfCannotRebase });
|
|
3226
|
+
return rebasedSelectionSet.isEmpty() ? undefined : this.withUpdatedComponents(rebasedFragment, rebasedSelectionSet);
|
|
2694
3227
|
}
|
|
2695
3228
|
|
|
2696
3229
|
canAddTo(parentType: CompositeType): boolean {
|
|
@@ -2727,7 +3260,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2727
3260
|
};
|
|
2728
3261
|
}
|
|
2729
3262
|
|
|
2730
|
-
optimize(fragments: NamedFragments): FragmentSelection {
|
|
3263
|
+
optimize(fragments: NamedFragments, validator: FieldsConflictMultiBranchValidator): FragmentSelection {
|
|
2731
3264
|
let optimizedSelection = this.selectionSet;
|
|
2732
3265
|
|
|
2733
3266
|
// First, see if we can reuse fragments for the selection of this field.
|
|
@@ -2737,12 +3270,13 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2737
3270
|
parentType: typeCondition,
|
|
2738
3271
|
subSelection: optimizedSelection,
|
|
2739
3272
|
fragments,
|
|
3273
|
+
validator,
|
|
2740
3274
|
canUseFullMatchingFragment: (fragment) => {
|
|
2741
3275
|
// To be able to use a matching fragment, it needs to have either no directives, or if it has
|
|
2742
3276
|
// some, then:
|
|
2743
3277
|
// 1. all it's directives should also be on the current element.
|
|
2744
3278
|
// 2. the directives of this element should be the fragment condition.
|
|
2745
|
-
// because if those 2 conditions are true, we
|
|
3279
|
+
// because if those 2 conditions are true, we can replace the whole current inline fragment
|
|
2746
3280
|
// with the match spread and directives will still match.
|
|
2747
3281
|
return fragment.appliedDirectives.length === 0
|
|
2748
3282
|
|| (
|
|
@@ -2777,7 +3311,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2777
3311
|
|
|
2778
3312
|
// Then, recurse inside the field sub-selection (note that if we matched some fragments above,
|
|
2779
3313
|
// this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
|
|
2780
|
-
optimizedSelection = optimizedSelection.optimizeSelections(fragments);
|
|
3314
|
+
optimizedSelection = optimizedSelection.optimizeSelections(fragments, validator);
|
|
2781
3315
|
|
|
2782
3316
|
return this.selectionSet === optimizedSelection
|
|
2783
3317
|
? this
|
|
@@ -2809,55 +3343,49 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2809
3343
|
: this.withUpdatedComponents(newElement, newSelection);
|
|
2810
3344
|
}
|
|
2811
3345
|
|
|
2812
|
-
|
|
2813
|
-
const recursive = options?.recursive ?? true;
|
|
2814
|
-
|
|
3346
|
+
protected normalizeKnowingItIntersects({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
|
|
2815
3347
|
const thisCondition = this.element.typeCondition;
|
|
2816
|
-
|
|
3348
|
+
|
|
3349
|
+
// We know the condition is "valid", but it may not be useful. That said, if the condition has directives,
|
|
3350
|
+
// we preserve the fragment no matter what.
|
|
2817
3351
|
if (this.element.appliedDirectives.length === 0) {
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
3352
|
+
// There is a number of cases where a fragment is not useful:
|
|
3353
|
+
// 1. if there is not conditions (remember it also has no directives).
|
|
3354
|
+
// 2. if it's the same type as the current type: it's not restricting types further.
|
|
3355
|
+
// 3. if the current type is an object more generally: because in that case too the condition
|
|
3356
|
+
// cannot be restricting things further (it's typically a less precise interface/union).
|
|
3357
|
+
if (!thisCondition || parentType === this.element.typeCondition || isObjectType(parentType)) {
|
|
3358
|
+
const normalized = this.selectionSet.normalize({ parentType, recursive });
|
|
3359
|
+
return normalized.isEmpty() ? undefined : normalized;
|
|
2821
3360
|
}
|
|
3361
|
+
}
|
|
2822
3362
|
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
//
|
|
2830
|
-
if (
|
|
2831
|
-
if (
|
|
3363
|
+
// We preserve the current fragment, so we only recurse within the sub-selection if we're asked to be recusive.
|
|
3364
|
+
// (note that even if we're not recursive, we may still have some "lifting" to do)
|
|
3365
|
+
let normalizedSelectionSet: SelectionSet;
|
|
3366
|
+
if (recursive ?? true) {
|
|
3367
|
+
normalizedSelectionSet = this.selectionSet.normalize({ parentType: thisCondition ?? parentType });
|
|
3368
|
+
|
|
3369
|
+
// It could be that everything was unsatisfiable.
|
|
3370
|
+
if (normalizedSelectionSet.isEmpty()) {
|
|
3371
|
+
if (this.element.appliedDirectives.length === 0) {
|
|
2832
3372
|
return undefined;
|
|
2833
3373
|
} else {
|
|
2834
|
-
|
|
2835
|
-
|
|
3374
|
+
return this.withUpdatedComponents(
|
|
3375
|
+
// We should be able to rebase, or there is a bug, so error if that is the case.
|
|
3376
|
+
this.element.rebaseOnOrError(parentType),
|
|
3377
|
+
selectionSetOfElement(
|
|
3378
|
+
new Field(
|
|
3379
|
+
(this.element.typeCondition ?? parentType).typenameField()!,
|
|
3380
|
+
undefined,
|
|
3381
|
+
[new Directive('include', { 'if': false })],
|
|
3382
|
+
)
|
|
3383
|
+
)
|
|
3384
|
+
);
|
|
2836
3385
|
}
|
|
2837
3386
|
}
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2840
|
-
// As we preserve the current fragment, the rest is about recursing. If we don't recurse, we're done
|
|
2841
|
-
if (!recursive) {
|
|
2842
|
-
return this;
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
// In all other cases, we first recurse on the sub-selection.
|
|
2846
|
-
const trimmedSelectionSet = this.selectionSet.trimUnsatisfiableBranches(this.element.typeCondition ?? this.parentType);
|
|
2847
|
-
|
|
2848
|
-
// First, could be that everything was unsatisfiable.
|
|
2849
|
-
if (trimmedSelectionSet.isEmpty()) {
|
|
2850
|
-
if (this.element.appliedDirectives.length === 0) {
|
|
2851
|
-
return undefined;
|
|
2852
|
-
} else {
|
|
2853
|
-
return this.withUpdatedSelectionSet(selectionSetOfElement(
|
|
2854
|
-
new Field(
|
|
2855
|
-
(this.element.typeCondition ?? this.parentType).typenameField()!,
|
|
2856
|
-
undefined,
|
|
2857
|
-
[new Directive('include', { 'if': false })],
|
|
2858
|
-
)
|
|
2859
|
-
));
|
|
2860
|
-
}
|
|
3387
|
+
} else {
|
|
3388
|
+
normalizedSelectionSet = this.selectionSet;
|
|
2861
3389
|
}
|
|
2862
3390
|
|
|
2863
3391
|
// Second, we check if some of the sub-selection fragments can be "lifted" outside of this fragment. This can happen if:
|
|
@@ -2865,10 +3393,10 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2865
3393
|
// 2. the sub-fragment is an object type,
|
|
2866
3394
|
// 3. the sub-fragment type is a valid runtime of the current type.
|
|
2867
3395
|
if (this.element.appliedDirectives.length === 0 && isAbstractType(thisCondition!)) {
|
|
2868
|
-
assert(!isObjectType(
|
|
2869
|
-
const currentRuntimes = possibleRuntimeTypes(
|
|
3396
|
+
assert(!isObjectType(parentType), () => `Should not have got here if ${parentType} is an object type`);
|
|
3397
|
+
const currentRuntimes = possibleRuntimeTypes(parentType);
|
|
2870
3398
|
const liftableSelections: Selection[] = [];
|
|
2871
|
-
for (const selection of
|
|
3399
|
+
for (const selection of normalizedSelectionSet.selections()) {
|
|
2872
3400
|
if (selection.kind === 'FragmentSelection'
|
|
2873
3401
|
&& selection.element.typeCondition
|
|
2874
3402
|
&& isObjectType(selection.element.typeCondition)
|
|
@@ -2879,8 +3407,8 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2879
3407
|
}
|
|
2880
3408
|
|
|
2881
3409
|
// If we can lift all selections, then that just mean we can get rid of the current fragment altogether
|
|
2882
|
-
if (liftableSelections.length ===
|
|
2883
|
-
return
|
|
3410
|
+
if (liftableSelections.length === normalizedSelectionSet.selections().length) {
|
|
3411
|
+
return normalizedSelectionSet;
|
|
2884
3412
|
}
|
|
2885
3413
|
|
|
2886
3414
|
// Otherwise, if there is "liftable" selections, we must return a set comprised of those lifted selection,
|
|
@@ -2889,13 +3417,15 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2889
3417
|
const newSet = new SelectionSetUpdates();
|
|
2890
3418
|
newSet.add(liftableSelections);
|
|
2891
3419
|
newSet.add(this.withUpdatedSelectionSet(
|
|
2892
|
-
|
|
3420
|
+
normalizedSelectionSet.filter((s) => !liftableSelections.includes(s)),
|
|
2893
3421
|
));
|
|
2894
|
-
return newSet.toSelectionSet(
|
|
3422
|
+
return newSet.toSelectionSet(parentType);
|
|
2895
3423
|
}
|
|
2896
3424
|
}
|
|
2897
3425
|
|
|
2898
|
-
return this.
|
|
3426
|
+
return this.parentType === parentType && this.selectionSet === normalizedSelectionSet
|
|
3427
|
+
? this
|
|
3428
|
+
: this.withUpdatedComponents(this.element.rebaseOnOrError(parentType), normalizedSelectionSet);
|
|
2899
3429
|
}
|
|
2900
3430
|
|
|
2901
3431
|
expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection {
|
|
@@ -2912,12 +3442,12 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2912
3442
|
&& this.selectionSet.equals(that.selectionSet);
|
|
2913
3443
|
}
|
|
2914
3444
|
|
|
2915
|
-
contains(that: Selection): ContainsResult {
|
|
3445
|
+
contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
|
|
2916
3446
|
if (!(that instanceof FragmentSelection) || !this.element.equals(that.element)) {
|
|
2917
3447
|
return ContainsResult.NOT_CONTAINED;
|
|
2918
3448
|
}
|
|
2919
3449
|
|
|
2920
|
-
return this.selectionSet.contains(that.selectionSet);
|
|
3450
|
+
return this.selectionSet.contains(that.selectionSet, options);
|
|
2921
3451
|
}
|
|
2922
3452
|
|
|
2923
3453
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
@@ -2956,17 +3486,20 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2956
3486
|
assert(false, `Unsupported`);
|
|
2957
3487
|
}
|
|
2958
3488
|
|
|
2959
|
-
|
|
3489
|
+
normalizeKnowingItIntersects({ parentType }: { parentType: CompositeType }): FragmentSelection {
|
|
2960
3490
|
// We must update the spread parent type if necessary since we're not going deeper,
|
|
2961
3491
|
// or we'll be fundamentally losing context.
|
|
2962
|
-
assert(parentType.schema() === this.parentType.schema(), 'Should not try to
|
|
2963
|
-
return this.
|
|
3492
|
+
assert(parentType.schema() === this.parentType.schema(), 'Should not try to normalize using a type from another schema');
|
|
3493
|
+
return this.rebaseOnOrError({ parentType, fragments: this.fragments });
|
|
2964
3494
|
}
|
|
2965
3495
|
|
|
2966
3496
|
validate(): void {
|
|
2967
3497
|
this.validateDeferAndStream();
|
|
2968
3498
|
|
|
2969
|
-
|
|
3499
|
+
validate(
|
|
3500
|
+
runtimeTypesIntersects(this.parentType, this.namedFragment.typeCondition),
|
|
3501
|
+
() => `Fragment "${this.namedFragment.name}" cannot be spread inside type ${this.parentType} as the runtime types do not intersect ${this.namedFragment.typeCondition}`
|
|
3502
|
+
);
|
|
2970
3503
|
}
|
|
2971
3504
|
|
|
2972
3505
|
toSelectionNode(): FragmentSpreadNode {
|
|
@@ -2989,11 +3522,19 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2989
3522
|
};
|
|
2990
3523
|
}
|
|
2991
3524
|
|
|
2992
|
-
optimize(
|
|
3525
|
+
optimize(_1: NamedFragments, _2: FieldsConflictMultiBranchValidator): FragmentSelection {
|
|
2993
3526
|
return this;
|
|
2994
3527
|
}
|
|
2995
3528
|
|
|
2996
|
-
rebaseOn(
|
|
3529
|
+
rebaseOn({
|
|
3530
|
+
parentType,
|
|
3531
|
+
fragments,
|
|
3532
|
+
errorIfCannotRebase,
|
|
3533
|
+
}: {
|
|
3534
|
+
parentType: CompositeType,
|
|
3535
|
+
fragments: NamedFragments | undefined,
|
|
3536
|
+
errorIfCannotRebase: boolean,
|
|
3537
|
+
}): FragmentSelection | undefined {
|
|
2997
3538
|
// We preserve the parent type here, to make sure we don't lose context, but we actually don't
|
|
2998
3539
|
// want to expand the spread as that would compromise the code that optimize subgraph fetches to re-use named
|
|
2999
3540
|
// fragments.
|
|
@@ -3013,7 +3554,14 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
3013
3554
|
assert(fragments || this.parentType.schema() === parentType.schema(), `Must provide fragments is rebasing on other schema`);
|
|
3014
3555
|
const newFragments = fragments ?? this.fragments;
|
|
3015
3556
|
const namedFragment = newFragments.get(this.namedFragment.name);
|
|
3016
|
-
|
|
3557
|
+
// If we're rebasing on another schema (think a subgraph), then named fragments will have been rebased on that, and some
|
|
3558
|
+
// of them may not contain anything that is on that subgraph, in which case they will not have been included at all.
|
|
3559
|
+
// If so, then as long as we're not ask to error if we cannot rebase, then we're happy to skip that spread (since again,
|
|
3560
|
+
// it expands to nothing that apply on the schema).
|
|
3561
|
+
if (!namedFragment) {
|
|
3562
|
+
validate(!errorIfCannotRebase, () => `Cannot rebase ${this.toString(false)} if it isn't part of the provided fragments`);
|
|
3563
|
+
return undefined;
|
|
3564
|
+
}
|
|
3017
3565
|
return new FragmentSpreadSelection(
|
|
3018
3566
|
parentType,
|
|
3019
3567
|
newFragments,
|
|
@@ -3069,7 +3617,7 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
3069
3617
|
&& sameDirectiveApplications(this.spreadDirectives, that.spreadDirectives);
|
|
3070
3618
|
}
|
|
3071
3619
|
|
|
3072
|
-
contains(that: Selection): ContainsResult {
|
|
3620
|
+
contains(that: Selection, options?: { ignoreMissingTypename?: boolean }): ContainsResult {
|
|
3073
3621
|
if (this.equals(that)) {
|
|
3074
3622
|
return ContainsResult.EQUAL;
|
|
3075
3623
|
}
|
|
@@ -3078,7 +3626,7 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
3078
3626
|
return ContainsResult.NOT_CONTAINED;
|
|
3079
3627
|
}
|
|
3080
3628
|
|
|
3081
|
-
return
|
|
3629
|
+
return this.selectionSet.contains(that.selectionSet, options);
|
|
3082
3630
|
}
|
|
3083
3631
|
|
|
3084
3632
|
toString(expandFragments: boolean = true, indent?: string): string {
|