@apollo/federation-internals 2.4.7 → 2.4.8
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/federationSpec.js +2 -2
- package/dist/federationSpec.js.map +1 -1
- package/dist/operations.d.ts +49 -23
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +330 -124
- 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/operations.ts +610 -236
- 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';
|
|
@@ -115,7 +116,7 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
115
116
|
|
|
116
117
|
constructor(
|
|
117
118
|
readonly definition: FieldDefinition<CompositeType>,
|
|
118
|
-
|
|
119
|
+
readonly args?: TArgs,
|
|
119
120
|
directives?: readonly Directive<any>[],
|
|
120
121
|
readonly alias?: string,
|
|
121
122
|
) {
|
|
@@ -865,69 +866,96 @@ export class Operation {
|
|
|
865
866
|
readonly name?: string) {
|
|
866
867
|
}
|
|
867
868
|
|
|
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) {
|
|
869
|
+
// Returns a copy of this operation with the provided updated selection set.
|
|
870
|
+
// Note that this method assumes that the existing `this.fragments` is still appropriate.
|
|
871
|
+
private withUpdatedSelectionSet(newSelectionSet: SelectionSet): Operation {
|
|
872
|
+
if (this.selectionSet === newSelectionSet) {
|
|
902
873
|
return this;
|
|
903
874
|
}
|
|
904
875
|
|
|
905
876
|
return new Operation(
|
|
906
877
|
this.schema,
|
|
907
878
|
this.rootKind,
|
|
908
|
-
|
|
879
|
+
newSelectionSet,
|
|
909
880
|
this.variableDefinitions,
|
|
910
|
-
|
|
881
|
+
this.fragments,
|
|
911
882
|
this.name
|
|
912
883
|
);
|
|
913
884
|
}
|
|
914
885
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
if (
|
|
886
|
+
// Returns a copy of this operation with the provided updated selection set and fragments.
|
|
887
|
+
private withUpdatedSelectionSetAndFragments(newSelectionSet: SelectionSet, newFragments: NamedFragments | undefined): Operation {
|
|
888
|
+
if (this.selectionSet === newSelectionSet && newFragments === this.fragments) {
|
|
918
889
|
return this;
|
|
919
890
|
}
|
|
920
891
|
|
|
921
892
|
return new Operation(
|
|
922
893
|
this.schema,
|
|
923
894
|
this.rootKind,
|
|
924
|
-
|
|
895
|
+
newSelectionSet,
|
|
925
896
|
this.variableDefinitions,
|
|
926
|
-
|
|
897
|
+
newFragments,
|
|
927
898
|
this.name
|
|
928
899
|
);
|
|
929
900
|
}
|
|
930
901
|
|
|
902
|
+
optimize(fragments?: NamedFragments, minUsagesToOptimize: number = 2): Operation {
|
|
903
|
+
assert(minUsagesToOptimize >= 1, `Expected 'minUsagesToOptimize' to be at least 1, but got ${minUsagesToOptimize}`)
|
|
904
|
+
if (!fragments || fragments.isEmpty()) {
|
|
905
|
+
return this;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
let optimizedSelection = this.selectionSet.optimize(fragments);
|
|
909
|
+
if (optimizedSelection === this.selectionSet) {
|
|
910
|
+
return this;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
let finalFragments = computeFragmentsToKeep(optimizedSelection, fragments, minUsagesToOptimize);
|
|
914
|
+
|
|
915
|
+
// If there is fragment usages and we're not keeping all fragments, we need to expand fragments.
|
|
916
|
+
if (finalFragments !== null && finalFragments?.size !== fragments.size) {
|
|
917
|
+
// Note that optimizing all fragments to potentially re-expand some is not entirely optimal, but it's unclear
|
|
918
|
+
// how to do otherwise, and it probably don't matter too much in practice (we only call this optimization
|
|
919
|
+
// on the final computed query plan, so not a very hot path; plus in most cases we won't even reach that
|
|
920
|
+
// point either because there is no fragment, or none will have been optimized away so we'll exit above).
|
|
921
|
+
optimizedSelection = optimizedSelection.expandFragments(finalFragments);
|
|
922
|
+
|
|
923
|
+
// Expanding fragments could create some "inefficiencies" that we wouldn't have if we hadn't re-optimized
|
|
924
|
+
// the fragments to de-optimize it later, so we do a final "normalize" pass to remove those.
|
|
925
|
+
optimizedSelection = optimizedSelection.normalize({ parentType: optimizedSelection.parentType });
|
|
926
|
+
|
|
927
|
+
// And if we've expanded some fragments but kept others, then it's not 100% impossible that some
|
|
928
|
+
// fragment was used multiple times in some expanded fragment(s), but that post-expansion all of
|
|
929
|
+
// it's usages are "dead" branches that are removed by the final `normalize`. In that case though,
|
|
930
|
+
// we need to ensure we don't include the now-unused fragment in the final list of fragments.
|
|
931
|
+
// TODO: remark that the same reasoning could leave a single instance of a fragment usage, so if
|
|
932
|
+
// we really really want to never have less than `minUsagesToOptimize`, we could do some loop of
|
|
933
|
+
// `expand then normalize` unless all fragments are provably used enough. We don't bother, because
|
|
934
|
+
// leaving this is not a huge deal and it's not worth the complexity, but it could be that we can
|
|
935
|
+
// refactor all this later to avoid this case without additional complexity.
|
|
936
|
+
if (finalFragments) {
|
|
937
|
+
const usages = new Map<string, number>();
|
|
938
|
+
optimizedSelection.collectUsedFragmentNames(usages);
|
|
939
|
+
finalFragments = finalFragments.filter((f) => (usages.get(f.name) ?? 0) > 0);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return this.withUpdatedSelectionSetAndFragments(optimizedSelection, finalFragments ?? undefined);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
expandAllFragments(): Operation {
|
|
947
|
+
// We clear up the fragments since we've expanded all.
|
|
948
|
+
// Also note that expanding fragment usually generate unecessary fragments/inefficient selections, so it
|
|
949
|
+
// basically always make sense to normalize afterwards. Besides, fragment reuse (done by `optimize`) rely
|
|
950
|
+
// on the fact that its input is normalized to work properly, so all the more reason to do it here.
|
|
951
|
+
const expanded = this.selectionSet.expandFragments();
|
|
952
|
+
return this.withUpdatedSelectionSetAndFragments(expanded.normalize({ parentType: expanded.parentType }), undefined);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
normalize(): Operation {
|
|
956
|
+
return this.withUpdatedSelectionSet(this.selectionSet.normalize({ parentType: this.selectionSet.parentType }));
|
|
957
|
+
}
|
|
958
|
+
|
|
931
959
|
/**
|
|
932
960
|
* Returns this operation but potentially modified so all/some of the @defer applications have been removed.
|
|
933
961
|
*
|
|
@@ -936,10 +964,7 @@ export class Operation {
|
|
|
936
964
|
* applications are removed.
|
|
937
965
|
*/
|
|
938
966
|
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);
|
|
967
|
+
return this.withUpdatedSelectionSet(this.selectionSet.withoutDefer(labelsToRemove));
|
|
943
968
|
}
|
|
944
969
|
|
|
945
970
|
/**
|
|
@@ -965,8 +990,7 @@ export class Operation {
|
|
|
965
990
|
const { hasDefers, hasNonLabelledOrConditionalDefers } = normalizer.init(this.selectionSet);
|
|
966
991
|
let updatedOperation: Operation = this;
|
|
967
992
|
if (hasNonLabelledOrConditionalDefers) {
|
|
968
|
-
|
|
969
|
-
updatedOperation = new Operation(this.schema, this.rootKind, updated, this.variableDefinitions, this.fragments, this.name);
|
|
993
|
+
updatedOperation = this.withUpdatedSelectionSet(this.selectionSet.withNormalizedDefer(normalizer));
|
|
970
994
|
}
|
|
971
995
|
return {
|
|
972
996
|
operation: updatedOperation,
|
|
@@ -991,6 +1015,8 @@ export class Operation {
|
|
|
991
1015
|
}
|
|
992
1016
|
}
|
|
993
1017
|
|
|
1018
|
+
export type FragmentRestrictionAtType = { selectionSet: SelectionSet, validator?: FieldsConflictValidator };
|
|
1019
|
+
|
|
994
1020
|
export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmentDefinition> {
|
|
995
1021
|
private _selectionSet: SelectionSet | undefined;
|
|
996
1022
|
|
|
@@ -1000,7 +1026,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
1000
1026
|
private _fragmentUsages: Map<string, number> | undefined;
|
|
1001
1027
|
private _includedFragmentNames: Set<string> | undefined;
|
|
1002
1028
|
|
|
1003
|
-
private readonly expandedSelectionSetsAtTypesCache = new Map<string,
|
|
1029
|
+
private readonly expandedSelectionSetsAtTypesCache = new Map<string, FragmentRestrictionAtType>();
|
|
1004
1030
|
|
|
1005
1031
|
constructor(
|
|
1006
1032
|
schema: Schema,
|
|
@@ -1027,7 +1053,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
1027
1053
|
|
|
1028
1054
|
expandedSelectionSet(): SelectionSet {
|
|
1029
1055
|
if (!this._expandedSelectionSet) {
|
|
1030
|
-
this._expandedSelectionSet = this.selectionSet.expandFragments().
|
|
1056
|
+
this._expandedSelectionSet = this.selectionSet.expandFragments().normalize({ parentType: this.typeCondition });
|
|
1031
1057
|
}
|
|
1032
1058
|
return this._expandedSelectionSet;
|
|
1033
1059
|
}
|
|
@@ -1071,13 +1097,32 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
1071
1097
|
}
|
|
1072
1098
|
|
|
1073
1099
|
/**
|
|
1074
|
-
* Whether this fragment may apply at the provided type, that
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1100
|
+
* Whether this fragment may apply _directly_ at the provided type, meaning that the fragment sub-selection
|
|
1101
|
+
* could be put directly inside a `... on type {}` inline fragment (_without_ re-adding the fragment condition
|
|
1102
|
+
* that is), and both be valid and not "lose context".
|
|
1103
|
+
*
|
|
1104
|
+
* For that to be true, we need one of this to be true:
|
|
1105
|
+
* 1. the runtime types of the fragment condition must be at least as general as those of the provided `type`.
|
|
1106
|
+
* Otherwise, putting it at `type` without its condition would "generalize" more than fragment meant to (and
|
|
1107
|
+
* so we'd "lose context"
|
|
1108
|
+
* 2. either `type` and `this.typeCondition` are equal, or `type` is an object or `this.typeCondition` is a union
|
|
1109
|
+
* The idea is that, assuming our 1st point, then:
|
|
1110
|
+
* - if both are equal, things works trivially.
|
|
1111
|
+
* - if `type` is an object, `this.typeCondition` is either the same object, or a union/interface for which
|
|
1112
|
+
* type is a valid runtime. In all case, anything valid on `this.typeCondition` would apply to `type` too.
|
|
1113
|
+
* - if `this.typeCondition` is a union, then it's selection can only have fragments on object types at top-level
|
|
1114
|
+
* (save for `__typename`), and all those selection will work at `type` too.
|
|
1115
|
+
* But in any other case, both types must be abstract (if `this.typeCondition` is an object, the 1st condition
|
|
1116
|
+
* imply `type` can only be the same type) and we're in one of:
|
|
1117
|
+
* - `type` and `this.typeCondition` are both different interfaces (that intersect but are different).
|
|
1118
|
+
* - `type` is aunion and `this.typeCondition` an interface.
|
|
1119
|
+
* And in both cases, the selection of the fragment may selection an interface that is not valid at `type` (if `type`
|
|
1120
|
+
* is a union because a direct field is always wrong, and if `type` is another interface because that interface may
|
|
1121
|
+
* not have that particular field).
|
|
1077
1122
|
*
|
|
1078
1123
|
* @param type - the type at which we're looking at applying the fragment
|
|
1079
1124
|
*/
|
|
1080
|
-
|
|
1125
|
+
canApplyDirectlyAtType(type: CompositeType): boolean {
|
|
1081
1126
|
if (sameType(type, this.typeCondition)) {
|
|
1082
1127
|
return true;
|
|
1083
1128
|
}
|
|
@@ -1090,17 +1135,20 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
1090
1135
|
|
|
1091
1136
|
const conditionRuntimes = possibleRuntimeTypes(this.typeCondition);
|
|
1092
1137
|
const typeRuntimes = possibleRuntimeTypes(type);
|
|
1093
|
-
|
|
1094
|
-
//
|
|
1095
|
-
// inside `type`, then it doesn't add restriction that weren't there without the fragment).
|
|
1138
|
+
// The fragment condition must be at least as general as the provided type (in other words, all of the
|
|
1139
|
+
// runtimes of `type` must be in `conditionRuntimes`).
|
|
1096
1140
|
// Note: the `length` test is technically redundant, but just avoid the more costly sub-set check if we
|
|
1097
1141
|
// can cheaply show it's unnecessary.
|
|
1098
|
-
|
|
1099
|
-
|
|
1142
|
+
if (conditionRuntimes.length < typeRuntimes.length
|
|
1143
|
+
|| !typeRuntimes.every((t1) => conditionRuntimes.some((t2) => sameType(t1, t2)))) {
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return isObjectType(type) || isUnionType(this.typeCondition);
|
|
1100
1148
|
}
|
|
1101
1149
|
|
|
1102
1150
|
/**
|
|
1103
|
-
* This methods *assumes* that `this.
|
|
1151
|
+
* This methods *assumes* that `this.canApplyDirectlyAtType(type)` is `true` (and may crash if this is not true), and returns
|
|
1104
1152
|
* a version fo this named fragment selection set that corresponds to the "expansion" of this named fragment at `type`
|
|
1105
1153
|
*
|
|
1106
1154
|
* 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 +1165,35 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
1117
1165
|
* 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
1166
|
* us that part.
|
|
1119
1167
|
*/
|
|
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.
|
|
1168
|
+
expandedSelectionSetAtType(type: CompositeType): FragmentRestrictionAtType {
|
|
1169
|
+
// First, if the candidate condition is an object or is the type passed, then there isn't any restriction to do.
|
|
1124
1170
|
if (sameType(type, this.typeCondition) || isObjectType(this.typeCondition)) {
|
|
1125
|
-
return expandedSelectionSet;
|
|
1171
|
+
return { selectionSet: this.expandedSelectionSet() };
|
|
1126
1172
|
}
|
|
1127
1173
|
|
|
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;
|
|
1174
|
+
let cached = this.expandedSelectionSetsAtTypesCache.get(type.name);
|
|
1175
|
+
if (!cached) {
|
|
1176
|
+
cached = this.computeExpandedSelectionSetAtType(type);
|
|
1177
|
+
this.expandedSelectionSetsAtTypesCache.set(type.name, cached);
|
|
1138
1178
|
}
|
|
1179
|
+
return cached;
|
|
1180
|
+
}
|
|
1139
1181
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1182
|
+
private computeExpandedSelectionSetAtType(type: CompositeType): FragmentRestrictionAtType {
|
|
1183
|
+
const expandedSelectionSet = this.expandedSelectionSet();
|
|
1184
|
+
// Note that what we want is get any simplification coming from normalizing at `type`, but any such simplication
|
|
1185
|
+
// stops as soon as we traverse a field, so no point in being recursive.
|
|
1186
|
+
const selectionSet = expandedSelectionSet.normalize({ parentType: type, recursive: false });
|
|
1187
|
+
|
|
1188
|
+
// Note that `trimmed` is the difference of 2 selections that may not have been normalized on the same parent type,
|
|
1189
|
+
// so in practice, it is possible that `trimmed` contains some of the selections that `selectionSet` contains, but
|
|
1190
|
+
// that they have been simplified in `selectionSet` in such a way that the `minus` call does not see it. However,
|
|
1191
|
+
// it is not trivial to deal with this, and it is fine given that we use trimmed to create the validator because
|
|
1192
|
+
// we know the non-trimmed parts cannot create field conflict issues so we're trying to build a smaller validator,
|
|
1193
|
+
// but it's ok if trimmed is not as small as it theoretically can be.
|
|
1194
|
+
const trimmed = expandedSelectionSet.minus(selectionSet);
|
|
1195
|
+
const validator = trimmed.isEmpty() ? undefined : FieldsConflictValidator.build(trimmed);
|
|
1196
|
+
return { selectionSet, validator };
|
|
1148
1197
|
}
|
|
1149
1198
|
|
|
1150
1199
|
/**
|
|
@@ -1180,6 +1229,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
1180
1229
|
}
|
|
1181
1230
|
}
|
|
1182
1231
|
|
|
1232
|
+
|
|
1183
1233
|
export class NamedFragments {
|
|
1184
1234
|
private readonly fragments = new MapWithCachedArrays<string, NamedFragmentDefinition>();
|
|
1185
1235
|
|
|
@@ -1208,8 +1258,8 @@ export class NamedFragments {
|
|
|
1208
1258
|
}
|
|
1209
1259
|
}
|
|
1210
1260
|
|
|
1211
|
-
|
|
1212
|
-
return this.fragments.values().filter(f => f.
|
|
1261
|
+
maybeApplyingDirectlyAtType(type: CompositeType): NamedFragmentDefinition[] {
|
|
1262
|
+
return this.fragments.values().filter(f => f.canApplyDirectlyAtType(type));
|
|
1213
1263
|
}
|
|
1214
1264
|
|
|
1215
1265
|
get(name: string): NamedFragmentDefinition | undefined {
|
|
@@ -1288,7 +1338,7 @@ export class NamedFragments {
|
|
|
1288
1338
|
mapper: (selectionSet: SelectionSet) => SelectionSet | undefined,
|
|
1289
1339
|
): NamedFragments | undefined {
|
|
1290
1340
|
return this.mapInDependencyOrder((fragment, newFragments) => {
|
|
1291
|
-
const mappedSelectionSet = mapper(fragment.selectionSet.expandFragments().
|
|
1341
|
+
const mappedSelectionSet = mapper(fragment.selectionSet.expandFragments().normalize({ parentType: fragment.typeCondition }));
|
|
1292
1342
|
if (!mappedSelectionSet) {
|
|
1293
1343
|
return undefined;
|
|
1294
1344
|
}
|
|
@@ -1417,6 +1467,8 @@ export enum ContainsResult {
|
|
|
1417
1467
|
EQUAL,
|
|
1418
1468
|
}
|
|
1419
1469
|
|
|
1470
|
+
export type CollectedFieldsInSet = { path: string[], field: FieldSelection }[];
|
|
1471
|
+
|
|
1420
1472
|
export class SelectionSet {
|
|
1421
1473
|
private readonly _keyedSelections: Map<string, Selection>;
|
|
1422
1474
|
private readonly _selections: readonly Selection[];
|
|
@@ -1447,7 +1499,7 @@ export class SelectionSet {
|
|
|
1447
1499
|
return this._keyedSelections.has(typenameFieldName);
|
|
1448
1500
|
}
|
|
1449
1501
|
|
|
1450
|
-
fieldsInSet():
|
|
1502
|
+
fieldsInSet(): CollectedFieldsInSet {
|
|
1451
1503
|
const fields = new Array<{ path: string[], field: FieldSelection }>();
|
|
1452
1504
|
for (const selection of this.selections()) {
|
|
1453
1505
|
if (selection.kind === 'FieldSelection') {
|
|
@@ -1463,6 +1515,22 @@ export class SelectionSet {
|
|
|
1463
1515
|
return fields;
|
|
1464
1516
|
}
|
|
1465
1517
|
|
|
1518
|
+
fieldsByResponseName(): MultiMap<string, FieldSelection> {
|
|
1519
|
+
const byResponseName = new MultiMap<string, FieldSelection>();
|
|
1520
|
+
this.collectFieldsByResponseName(byResponseName);
|
|
1521
|
+
return byResponseName;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
private collectFieldsByResponseName(collector: MultiMap<string, FieldSelection>) {
|
|
1525
|
+
for (const selection of this.selections()) {
|
|
1526
|
+
if (selection.kind === 'FieldSelection') {
|
|
1527
|
+
collector.add(selection.element.responseName(), selection);
|
|
1528
|
+
} else {
|
|
1529
|
+
selection.selectionSet.collectFieldsByResponseName(collector);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1466
1534
|
usedVariables(): Variables {
|
|
1467
1535
|
const collector = new VariableCollector();
|
|
1468
1536
|
this.collectVariables(collector);
|
|
@@ -1502,7 +1570,8 @@ export class SelectionSet {
|
|
|
1502
1570
|
// With that, `optimizeSelections` will correctly match on the `on Query` fragment; after which
|
|
1503
1571
|
// we can unpack the final result.
|
|
1504
1572
|
const wrapped = new InlineFragmentSelection(new FragmentElement(this.parentType, this.parentType), this);
|
|
1505
|
-
const
|
|
1573
|
+
const validator = FieldsConflictValidator.build(this);
|
|
1574
|
+
const optimized = wrapped.optimize(fragments, validator);
|
|
1506
1575
|
|
|
1507
1576
|
// Now, it's possible we matched a full fragment, in which case `optimized` will be just the named fragment,
|
|
1508
1577
|
// and in that case we return a singleton selection with just that. Otherwise, it's our wrapping inline fragment
|
|
@@ -1515,16 +1584,90 @@ export class SelectionSet {
|
|
|
1515
1584
|
// Tries to match fragments inside each selections of this selection set, and this recursively. However, note that this
|
|
1516
1585
|
// may not match fragments that would apply at top-level, so you should usually use `optimize` instead (this exists mostly
|
|
1517
1586
|
// for the recursion).
|
|
1518
|
-
optimizeSelections(fragments: NamedFragments): SelectionSet {
|
|
1519
|
-
return this.lazyMap((selection) => selection.optimize(fragments));
|
|
1587
|
+
optimizeSelections(fragments: NamedFragments, validator: FieldsConflictValidator): SelectionSet {
|
|
1588
|
+
return this.lazyMap((selection) => selection.optimize(fragments, validator));
|
|
1520
1589
|
}
|
|
1521
1590
|
|
|
1522
1591
|
expandFragments(updatedFragments?: NamedFragments): SelectionSet {
|
|
1523
1592
|
return this.lazyMap((selection) => selection.expandFragments(updatedFragments));
|
|
1524
1593
|
}
|
|
1525
1594
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1595
|
+
/**
|
|
1596
|
+
* Applies some normalization rules to this selection set in the context of the provided `parentType`.
|
|
1597
|
+
*
|
|
1598
|
+
* Normalization mostly removes unecessary/redundant inline fragments, so that for instance, with
|
|
1599
|
+
* schema:
|
|
1600
|
+
* ```graphql
|
|
1601
|
+
* type Query {
|
|
1602
|
+
* t1: T1
|
|
1603
|
+
* i: I
|
|
1604
|
+
* }
|
|
1605
|
+
*
|
|
1606
|
+
* interface I {
|
|
1607
|
+
* id: ID!
|
|
1608
|
+
* }
|
|
1609
|
+
*
|
|
1610
|
+
* type T1 implements I {
|
|
1611
|
+
* id: ID!
|
|
1612
|
+
* v1: Int
|
|
1613
|
+
* }
|
|
1614
|
+
*
|
|
1615
|
+
* type T2 implements I {
|
|
1616
|
+
* id: ID!
|
|
1617
|
+
* v2: Int
|
|
1618
|
+
* }
|
|
1619
|
+
* ```
|
|
1620
|
+
*
|
|
1621
|
+
* ```
|
|
1622
|
+
* normalize({
|
|
1623
|
+
* t1 {
|
|
1624
|
+
* ... on I {
|
|
1625
|
+
* id
|
|
1626
|
+
* }
|
|
1627
|
+
* }
|
|
1628
|
+
* i {
|
|
1629
|
+
* ... on T1 {
|
|
1630
|
+
* ... on I {
|
|
1631
|
+
* ... on T1 {
|
|
1632
|
+
* v1
|
|
1633
|
+
* }
|
|
1634
|
+
* ... on T2 {
|
|
1635
|
+
* v2
|
|
1636
|
+
* }
|
|
1637
|
+
* }
|
|
1638
|
+
* }
|
|
1639
|
+
* ... on T2 {
|
|
1640
|
+
* ... on I {
|
|
1641
|
+
* id
|
|
1642
|
+
* }
|
|
1643
|
+
* }
|
|
1644
|
+
* }
|
|
1645
|
+
* }) === {
|
|
1646
|
+
* t1 {
|
|
1647
|
+
* id
|
|
1648
|
+
* }
|
|
1649
|
+
* i {
|
|
1650
|
+
* ... on T1 {
|
|
1651
|
+
* v1
|
|
1652
|
+
* }
|
|
1653
|
+
* ... on T2 {
|
|
1654
|
+
* id
|
|
1655
|
+
* }
|
|
1656
|
+
* }
|
|
1657
|
+
* }
|
|
1658
|
+
* ```
|
|
1659
|
+
*
|
|
1660
|
+
* For this operation to be valid (to not throw), `parentType` must be such this selection set would
|
|
1661
|
+
* be valid as a subselection of an inline fragment `... on parentType { <this selection set> }` (and
|
|
1662
|
+
* so `this.normalize(this.parentType)` is always valid and useful, but it is possible to pass a `parentType`
|
|
1663
|
+
* that is more "restrictive" than the selection current parent type).
|
|
1664
|
+
*
|
|
1665
|
+
* Passing the option `recursive == false` makes the normalization only apply at the top-level, removing
|
|
1666
|
+
* any unecessary top-level inline fragments, possibly multiple layers of them, but we never recurse
|
|
1667
|
+
* inside the sub-selection of an selection that is not removed by the normalization.
|
|
1668
|
+
*/
|
|
1669
|
+
normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): SelectionSet {
|
|
1670
|
+
return this.lazyMap((selection) => selection.normalize({ parentType, recursive }), { parentType });
|
|
1528
1671
|
}
|
|
1529
1672
|
|
|
1530
1673
|
/**
|
|
@@ -1575,17 +1718,26 @@ export class SelectionSet {
|
|
|
1575
1718
|
}
|
|
1576
1719
|
|
|
1577
1720
|
/**
|
|
1578
|
-
* Returns the selection
|
|
1721
|
+
* Returns the selection set resulting from filtering out any of the top-level selection that does not match the provided predicate.
|
|
1579
1722
|
*
|
|
1580
|
-
* Please that this method
|
|
1581
|
-
* call `optimize` on the result if you want to re-apply some fragments.
|
|
1723
|
+
* Please that this method does not recurse within sub-selections.
|
|
1582
1724
|
*/
|
|
1583
1725
|
filter(predicate: (selection: Selection) => boolean): SelectionSet {
|
|
1584
|
-
return this.lazyMap((selection) => selection
|
|
1726
|
+
return this.lazyMap((selection) => predicate(selection) ? selection : undefined);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/**
|
|
1730
|
+
* Returns the selection set resulting from "recursively" filtering any selection that does not match the provided predicate.
|
|
1731
|
+
* This method calls `predicate` on every selection of the selection set, not just top-level ones, and apply a "depth-first"
|
|
1732
|
+
* strategy, meaning that when the predicate is call on a given selection, the it is guaranteed that filtering has happened
|
|
1733
|
+
* on all the selections of its sub-selection.
|
|
1734
|
+
*/
|
|
1735
|
+
filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): SelectionSet {
|
|
1736
|
+
return this.lazyMap((selection) => selection.filterRecursiveDepthFirst(predicate));
|
|
1585
1737
|
}
|
|
1586
1738
|
|
|
1587
1739
|
withoutEmptyBranches(): SelectionSet | undefined {
|
|
1588
|
-
const updated = this.
|
|
1740
|
+
const updated = this.filterRecursiveDepthFirst((selection) => selection.selectionSet?.isEmpty() !== true);
|
|
1589
1741
|
return updated.isEmpty() ? undefined : updated;
|
|
1590
1742
|
}
|
|
1591
1743
|
|
|
@@ -1640,51 +1792,6 @@ export class SelectionSet {
|
|
|
1640
1792
|
: ContainsResult.STRICTLY_CONTAINED;
|
|
1641
1793
|
}
|
|
1642
1794
|
|
|
1643
|
-
// Please note that this method assumes that `candidate.canApplyAtType(parentType) === true` but it is left to the caller to
|
|
1644
|
-
// validate this (`canApplyAtType` is not free, and we want to avoid repeating it multiple times).
|
|
1645
|
-
diffWithNamedFragmentIfContained(
|
|
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 };
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
1795
|
/**
|
|
1689
1796
|
* Returns a selection set that correspond to this selection set but where any of the selections in the
|
|
1690
1797
|
* provided selection set have been remove.
|
|
@@ -1706,6 +1813,28 @@ export class SelectionSet {
|
|
|
1706
1813
|
return updated.toSelectionSet(this.parentType);
|
|
1707
1814
|
}
|
|
1708
1815
|
|
|
1816
|
+
intersectionWith(that: SelectionSet): SelectionSet {
|
|
1817
|
+
if (this.isEmpty()) {
|
|
1818
|
+
return this;
|
|
1819
|
+
}
|
|
1820
|
+
if (that.isEmpty()) {
|
|
1821
|
+
return that;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
const intersection = new SelectionSetUpdates();
|
|
1825
|
+
for (const [key, thisSelection] of this._keyedSelections) {
|
|
1826
|
+
const thatSelection = that._keyedSelections.get(key);
|
|
1827
|
+
if (thatSelection) {
|
|
1828
|
+
const selection = thisSelection.intersectionWith(thatSelection);
|
|
1829
|
+
if (selection) {
|
|
1830
|
+
intersection.add(selection);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
return intersection.toSelectionSet(this.parentType);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1709
1838
|
canRebaseOn(parentTypeToTest: CompositeType): boolean {
|
|
1710
1839
|
return this.selections().every((selection) => selection.canAddTo(parentTypeToTest));
|
|
1711
1840
|
}
|
|
@@ -1923,6 +2052,19 @@ export class SelectionSetUpdates {
|
|
|
1923
2052
|
toSelectionSet(parentType: CompositeType, fragments?: NamedFragments): SelectionSet {
|
|
1924
2053
|
return makeSelectionSet(parentType, this.keyedUpdates, fragments);
|
|
1925
2054
|
}
|
|
2055
|
+
|
|
2056
|
+
toString() {
|
|
2057
|
+
return '{\n'
|
|
2058
|
+
+ [...this.keyedUpdates.entries()].map(([k, updates]) => {
|
|
2059
|
+
const updStr = updates.map((upd) =>
|
|
2060
|
+
upd instanceof AbstractSelection
|
|
2061
|
+
? upd.toString()
|
|
2062
|
+
: `${upd.path} -> ${upd.selections}`
|
|
2063
|
+
);
|
|
2064
|
+
return ` - ${k}: ${updStr}`;
|
|
2065
|
+
}).join('\n')
|
|
2066
|
+
+'\n\}'
|
|
2067
|
+
}
|
|
1926
2068
|
}
|
|
1927
2069
|
|
|
1928
2070
|
function addToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, selections: Selection | SelectionSet | readonly Selection[]) {
|
|
@@ -2185,7 +2327,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2185
2327
|
|
|
2186
2328
|
abstract key(): string;
|
|
2187
2329
|
|
|
2188
|
-
abstract optimize(fragments: NamedFragments): Selection;
|
|
2330
|
+
abstract optimize(fragments: NamedFragments, validator: FieldsConflictValidator): Selection;
|
|
2189
2331
|
|
|
2190
2332
|
abstract toSelectionNode(): SelectionNode;
|
|
2191
2333
|
|
|
@@ -2240,7 +2382,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2240
2382
|
|
|
2241
2383
|
abstract expandFragments(updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
|
|
2242
2384
|
|
|
2243
|
-
abstract
|
|
2385
|
+
abstract normalize(args: { parentType: CompositeType, recursive? : boolean }): TOwnType | SelectionSet | undefined;
|
|
2244
2386
|
|
|
2245
2387
|
isFragmentSpread(): boolean {
|
|
2246
2388
|
return false;
|
|
@@ -2258,26 +2400,52 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2258
2400
|
return undefined;
|
|
2259
2401
|
}
|
|
2260
2402
|
|
|
2403
|
+
intersectionWith(that: Selection): TOwnType | undefined {
|
|
2404
|
+
// If there is a subset, then we compute the intersection add that (if not empty).
|
|
2405
|
+
// Otherwise, the intersection is this element.
|
|
2406
|
+
if (this.selectionSet && that.selectionSet) {
|
|
2407
|
+
const subSelectionSetIntersection = this.selectionSet.intersectionWith(that.selectionSet);
|
|
2408
|
+
if (subSelectionSetIntersection.isEmpty()) {
|
|
2409
|
+
return undefined;
|
|
2410
|
+
} else {
|
|
2411
|
+
return this.withUpdatedSelectionSet(subSelectionSetIntersection);
|
|
2412
|
+
}
|
|
2413
|
+
} else {
|
|
2414
|
+
return this.us();
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2261
2418
|
protected tryOptimizeSubselectionWithFragments({
|
|
2262
2419
|
parentType,
|
|
2263
2420
|
subSelection,
|
|
2264
2421
|
fragments,
|
|
2422
|
+
validator,
|
|
2265
2423
|
canUseFullMatchingFragment,
|
|
2266
2424
|
}: {
|
|
2267
2425
|
parentType: CompositeType,
|
|
2268
2426
|
subSelection: SelectionSet,
|
|
2269
2427
|
fragments: NamedFragments,
|
|
2428
|
+
validator: FieldsConflictValidator,
|
|
2270
2429
|
canUseFullMatchingFragment: (match: NamedFragmentDefinition) => boolean,
|
|
2271
2430
|
}): SelectionSet | NamedFragmentDefinition {
|
|
2272
|
-
|
|
2431
|
+
// We limit to fragments whose selection could be applied "directly" at `parentType`, meaning without taking the fragment condition
|
|
2432
|
+
// into account. The idea being that if the fragment condition would be needed inside `parentType`, then that condition will not
|
|
2433
|
+
// 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
|
|
2434
|
+
// which point, this `maybeApplyingDirectlyAtType` method will apply.
|
|
2435
|
+
// Also note that this is because we have this restriction that calling `expandedSelectionSetAtType` is ok.
|
|
2436
|
+
let candidates = fragments.maybeApplyingDirectlyAtType(parentType);
|
|
2437
|
+
if (candidates.length === 0) {
|
|
2438
|
+
return subSelection;
|
|
2439
|
+
}
|
|
2273
2440
|
|
|
2274
2441
|
// First, we check which of the candidates do apply inside `subSelection`, if any.
|
|
2275
2442
|
// If we find a candidate that applies to the whole `subSelection`, then we stop and only return
|
|
2276
2443
|
// that one candidate. Otherwise, we cumulate in `applyingFragments` the list of fragments that
|
|
2277
2444
|
// applies to a subset of `subSelection`.
|
|
2278
|
-
const applyingFragments: NamedFragmentDefinition[] = [];
|
|
2445
|
+
const applyingFragments: { fragment: NamedFragmentDefinition, atType: FragmentRestrictionAtType }[] = [];
|
|
2279
2446
|
for (const candidate of candidates) {
|
|
2280
|
-
const
|
|
2447
|
+
const atType = candidate.expandedSelectionSetAtType(parentType);
|
|
2448
|
+
const selectionSetAtType = atType.selectionSet;
|
|
2281
2449
|
// It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
|
|
2282
2450
|
// `parentType` is empty, or contains only `__typename`. For instance, suppose we have
|
|
2283
2451
|
// a union `U = A | B | C`, and then a fragment:
|
|
@@ -2296,24 +2464,28 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2296
2464
|
//
|
|
2297
2465
|
// Using `F` in those cases is, while not 100% incorrect, at least not productive, and so we
|
|
2298
2466
|
// skip it that case. This is essentially an optimisation.
|
|
2299
|
-
if (
|
|
2467
|
+
if (selectionSetAtType.isEmpty() || (selectionSetAtType.selections().length === 1 && selectionSetAtType.selections()[0].isTypenameField())) {
|
|
2300
2468
|
continue;
|
|
2301
2469
|
}
|
|
2302
2470
|
|
|
2303
|
-
const res = subSelection.contains(
|
|
2471
|
+
const res = subSelection.contains(selectionSetAtType);
|
|
2304
2472
|
|
|
2305
2473
|
if (res === ContainsResult.EQUAL) {
|
|
2306
2474
|
if (canUseFullMatchingFragment(candidate)) {
|
|
2475
|
+
if (!validator.checkCanReuseFragmentAndTrackIt(atType)) {
|
|
2476
|
+
// We cannot use it at all, so no point in adding to `applyingFragments`.
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2307
2479
|
return candidate;
|
|
2308
2480
|
}
|
|
2309
2481
|
// If we're not going to replace the full thing, then same reasoning a below.
|
|
2310
2482
|
if (candidate.appliedDirectives.length === 0) {
|
|
2311
|
-
applyingFragments.push(candidate);
|
|
2483
|
+
applyingFragments.push({ fragment: candidate, atType});
|
|
2312
2484
|
}
|
|
2313
2485
|
// Note that if a fragment applies to only a subset of the subSelection, then we really only can use
|
|
2314
2486
|
// it if that fragment is defined _without_ directives.
|
|
2315
2487
|
} else if (res === ContainsResult.STRICTLY_CONTAINED && candidate.appliedDirectives.length === 0) {
|
|
2316
|
-
applyingFragments.push(candidate);
|
|
2488
|
+
applyingFragments.push({ fragment: candidate, atType });
|
|
2317
2489
|
}
|
|
2318
2490
|
}
|
|
2319
2491
|
|
|
@@ -2322,7 +2494,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2322
2494
|
}
|
|
2323
2495
|
|
|
2324
2496
|
// We have found the list of fragments that applies to some subset of `subSelection`. In general, we
|
|
2325
|
-
// want
|
|
2497
|
+
// want to now produce the selection set with spread for those fragments plus any selection that is not
|
|
2326
2498
|
// covered by any of the fragments. For instance, suppose that `subselection` is `{ a b c d e }`
|
|
2327
2499
|
// and we have found that `fragment F1 on X { a b c }` and `fragment F2 on X { c d }` applies, then
|
|
2328
2500
|
// we will generate `{ ...F1 ...F2 e }`.
|
|
@@ -2369,16 +2541,16 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2369
2541
|
// return `{ ...F3 ...F4 }` in that case, but it would be technically better to return only `F4`.
|
|
2370
2542
|
// However, this feels niche, and it might be costly to verify such inclusions, so not doing it
|
|
2371
2543
|
// for now.
|
|
2372
|
-
const filteredApplyingFragments = applyingFragments.filter((
|
|
2544
|
+
const filteredApplyingFragments = applyingFragments.filter(({ fragment }) => !applyingFragments.some((o) => o.fragment.includes(fragment.name)))
|
|
2373
2545
|
|
|
2374
2546
|
let notCoveredByFragments = subSelection;
|
|
2375
2547
|
const optimized = new SelectionSetUpdates();
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
notCoveredByFragments = notCoveredByFragments.
|
|
2548
|
+
for (const { fragment, atType} of filteredApplyingFragments) {
|
|
2549
|
+
if (!validator.checkCanReuseFragmentAndTrackIt(atType)) {
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
const notCovered = subSelection.minus(atType.selectionSet);
|
|
2553
|
+
notCoveredByFragments = notCoveredByFragments.intersectionWith(notCovered);
|
|
2382
2554
|
optimized.add(new FragmentSpreadSelection(parentType, fragments, fragment, []));
|
|
2383
2555
|
}
|
|
2384
2556
|
|
|
@@ -2386,6 +2558,176 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
|
|
|
2386
2558
|
}
|
|
2387
2559
|
}
|
|
2388
2560
|
|
|
2561
|
+
class FieldsConflictValidator {
|
|
2562
|
+
private usedSpreadTrimmedPartAtLevel?: FieldsConflictValidator[];
|
|
2563
|
+
|
|
2564
|
+
private constructor(
|
|
2565
|
+
private readonly byResponseName: Map<string, Map<Field, FieldsConflictValidator | null>>,
|
|
2566
|
+
) {
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
static build(s: SelectionSet): FieldsConflictValidator {
|
|
2570
|
+
return FieldsConflictValidator.forLevel(s.fieldsInSet());
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
private static forLevel(level: CollectedFieldsInSet): FieldsConflictValidator {
|
|
2574
|
+
const atLevel = new Map<string, Map<Field, CollectedFieldsInSet | null>>();
|
|
2575
|
+
|
|
2576
|
+
for (const { field } of level) {
|
|
2577
|
+
const responseName = field.element.responseName();
|
|
2578
|
+
let atResponseName = atLevel.get(responseName);
|
|
2579
|
+
if (!atResponseName) {
|
|
2580
|
+
atResponseName = new Map<Field, CollectedFieldsInSet>();
|
|
2581
|
+
atLevel.set(responseName, atResponseName);
|
|
2582
|
+
}
|
|
2583
|
+
if (field.selectionSet) {
|
|
2584
|
+
let forField = atResponseName.get(field.element) ?? [];
|
|
2585
|
+
atResponseName.set(field.element, forField.concat(field.selectionSet.fieldsInSet()));
|
|
2586
|
+
} else {
|
|
2587
|
+
atResponseName.set(field.element, null);
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
const byResponseName = new Map<string, Map<Field, FieldsConflictValidator | null>>();
|
|
2592
|
+
for (const [name, level] of atLevel.entries()) {
|
|
2593
|
+
const atResponseName = new Map<Field, FieldsConflictValidator | null>();
|
|
2594
|
+
for (const [field, collectedFields] of level) {
|
|
2595
|
+
const validator = collectedFields ? FieldsConflictValidator.forLevel(collectedFields) : null;
|
|
2596
|
+
atResponseName.set(field, validator);
|
|
2597
|
+
}
|
|
2598
|
+
byResponseName.set(name, atResponseName);
|
|
2599
|
+
}
|
|
2600
|
+
return new FieldsConflictValidator(byResponseName);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
forField(field: Field): FieldsConflictValidator {
|
|
2604
|
+
const validator = this.byResponseName.get(field.responseName())?.get(field);
|
|
2605
|
+
// This should be called on validator built on the exact selection set from field this `field` is coming, so
|
|
2606
|
+
// we should find it or the code is buggy.
|
|
2607
|
+
assert(validator, () => `Should have found validator for ${field}`);
|
|
2608
|
+
return validator;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// At this point, we known that the fragment, restricted to the current parent type, matches a subset of the
|
|
2612
|
+
// sub-selection. However, there is still one case we we cannot use it that we need to check, and this is
|
|
2613
|
+
// if using the fragment would create a field "conflict" (in the sense of the graphQL spec
|
|
2614
|
+
// [`FieldsInSetCanMerge`](https://spec.graphql.org/draft/#FieldsInSetCanMerge())) and thus create an
|
|
2615
|
+
// invalid selection. To be clear, `atType.selectionSet` cannot create a conflict, since it is a subset
|
|
2616
|
+
// of `subSelection` and `subSelection` is valid. *But* there may be some part of the fragment that
|
|
2617
|
+
// is not `atType.selectionSet` due to being "dead branches" for type `parentType`. And while those
|
|
2618
|
+
// branches _are_ "dead" as far as execution goes, the `FieldsInSetCanMerge` validation does not take
|
|
2619
|
+
// this into account (it's 1st step says "including visiting fragments and inline fragments" but has
|
|
2620
|
+
// no logic regarding ignoring any fragment that may not apply due to the intersection of runtimes
|
|
2621
|
+
// between multiple fragment being empty).
|
|
2622
|
+
checkCanReuseFragmentAndTrackIt(fragment: FragmentRestrictionAtType): boolean {
|
|
2623
|
+
// No validator means that everything in the fragment selection was part of the selection we're optimizing
|
|
2624
|
+
// away (by using the fragment), and we know the original selection was ok, so nothing to check.
|
|
2625
|
+
const validator = fragment.validator;
|
|
2626
|
+
if (!validator) {
|
|
2627
|
+
return true;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
if (!this.doMergeWith(validator)) {
|
|
2631
|
+
return false;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
// We need to make sure the trimmed parts of `fragment` merges with the rest of the selection,
|
|
2635
|
+
// but also that it merge with any of the trimmed parts of any fragment we have added already.
|
|
2636
|
+
// Note: this last condition means that if 2 fragment conflict on their "trimmed" parts,
|
|
2637
|
+
// then the choice of which is used can be based on the fragment ordering and selection order,
|
|
2638
|
+
// which may not be optimal. This feels niche enough that we keep it simple for now, but we
|
|
2639
|
+
// can revisit this decision if we run into real cases that justify it (but making it optimal
|
|
2640
|
+
// would be a involved in general, as in theory you could have complex dependencies of fragments
|
|
2641
|
+
// that conflict, even cycles, and you need to take the size of fragments into account to know
|
|
2642
|
+
// what's best; and even then, this could even depend on overall usage, as it can be better to
|
|
2643
|
+
// reuse a fragment that is used in other places, than to use one for which it's the only usage.
|
|
2644
|
+
// Adding to all that the fact that conflict can happen in sibling branches).
|
|
2645
|
+
if (this.usedSpreadTrimmedPartAtLevel) {
|
|
2646
|
+
if (!this.usedSpreadTrimmedPartAtLevel.every((t) => validator.doMergeWith(t))) {
|
|
2647
|
+
return false;
|
|
2648
|
+
}
|
|
2649
|
+
} else {
|
|
2650
|
+
this.usedSpreadTrimmedPartAtLevel = [];
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
// We're good, but track the fragment
|
|
2654
|
+
this.usedSpreadTrimmedPartAtLevel.push(validator);
|
|
2655
|
+
return true;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
doMergeWith(that: FieldsConflictValidator): boolean {
|
|
2659
|
+
for (const [responseName, thisFields] of this.byResponseName.entries()) {
|
|
2660
|
+
const thatFields = that.byResponseName.get(responseName);
|
|
2661
|
+
if (!thatFields) {
|
|
2662
|
+
continue;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
// We're basically checking [FieldInSetCanMerge](https://spec.graphql.org/draft/#FieldsInSetCanMerge()),
|
|
2666
|
+
// but from 2 set of fields (`thisFields` and `thatFields`) of the same response that we know individually
|
|
2667
|
+
// merge already.
|
|
2668
|
+
for (const [thisField, thisValidator] of thisFields.entries()) {
|
|
2669
|
+
for (const [thatField, thatValidator] of thatFields.entries()) {
|
|
2670
|
+
// The `SameResponseShape` test that all fields must pass.
|
|
2671
|
+
if (!typesCanBeMerged(thisField.definition.type!, thatField.definition.type!)) {
|
|
2672
|
+
return false;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
const p1 = thisField.parentType;
|
|
2676
|
+
const p2 = thatField.parentType;
|
|
2677
|
+
if (sameType(p1, p2) || !isObjectType(p1) || !isObjectType(p2)) {
|
|
2678
|
+
// Additional checks of `FieldsInSetCanMerge` when same parent type or one isn't object
|
|
2679
|
+
if (thisField.name !== thatField.name
|
|
2680
|
+
|| !argumentsEquals(thisField.args ?? {}, thatField.args ?? {})
|
|
2681
|
+
|| (thisValidator && thatValidator && !thisValidator.doMergeWith(thatValidator))
|
|
2682
|
+
) {
|
|
2683
|
+
return false;
|
|
2684
|
+
}
|
|
2685
|
+
} else {
|
|
2686
|
+
// Otherwise, the sub-selection must pass [SameResponseShape](https://spec.graphql.org/draft/#SameResponseShape()).
|
|
2687
|
+
if (thisValidator && thatValidator && !thisValidator.hasSameResponseShapeThan(thatValidator)) {
|
|
2688
|
+
return false;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
return true;
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
hasSameResponseShapeThan(that: FieldsConflictValidator): boolean {
|
|
2698
|
+
for (const [responseName, thisFields] of this.byResponseName.entries()) {
|
|
2699
|
+
const thatFields = that.byResponseName.get(responseName);
|
|
2700
|
+
if (!thatFields) {
|
|
2701
|
+
continue;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
for (const [thisField, thisValidator] of thisFields.entries()) {
|
|
2705
|
+
for (const [thatField, thatValidator] of thatFields.entries()) {
|
|
2706
|
+
if (!typesCanBeMerged(thisField.definition.type!, thatField.definition.type!)
|
|
2707
|
+
|| (thisValidator && thatValidator && !thisValidator.hasSameResponseShapeThan(thatValidator))) {
|
|
2708
|
+
return false;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
return true;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
toString(indent: string = ''): string {
|
|
2717
|
+
// For debugging/testing ...
|
|
2718
|
+
return '{\n'
|
|
2719
|
+
+ [...this.byResponseName.entries()].map(([name, byFields]) => {
|
|
2720
|
+
const innerIndent = indent + ' ';
|
|
2721
|
+
return `${innerIndent}${name}: [\n`
|
|
2722
|
+
+ [...byFields.entries()]
|
|
2723
|
+
.map(([field, next]) => `${innerIndent} ${field.parentType}.${field}${next ? next.toString(innerIndent + ' '): ''}`)
|
|
2724
|
+
.join('\n')
|
|
2725
|
+
+ `\n${innerIndent}]`;
|
|
2726
|
+
}).join('\n')
|
|
2727
|
+
+ `\n${indent}}`
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2389
2731
|
export class FieldSelection extends AbstractSelection<Field<any>, undefined, FieldSelection> {
|
|
2390
2732
|
readonly kind = 'FieldSelection' as const;
|
|
2391
2733
|
|
|
@@ -2409,6 +2751,9 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2409
2751
|
}
|
|
2410
2752
|
|
|
2411
2753
|
withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
|
|
2754
|
+
if (this.element === field && this.selectionSet === selectionSet) {
|
|
2755
|
+
return this;
|
|
2756
|
+
}
|
|
2412
2757
|
return new FieldSelection(field, selectionSet);
|
|
2413
2758
|
}
|
|
2414
2759
|
|
|
@@ -2416,12 +2761,14 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2416
2761
|
return this.element.key();
|
|
2417
2762
|
}
|
|
2418
2763
|
|
|
2419
|
-
optimize(fragments: NamedFragments): Selection {
|
|
2764
|
+
optimize(fragments: NamedFragments, validator: FieldsConflictValidator): Selection {
|
|
2420
2765
|
const fieldBaseType = baseType(this.element.definition.type!);
|
|
2421
2766
|
if (!isCompositeType(fieldBaseType) || !this.selectionSet) {
|
|
2422
2767
|
return this;
|
|
2423
2768
|
}
|
|
2424
2769
|
|
|
2770
|
+
const fieldValidator = validator.forField(this.element);
|
|
2771
|
+
|
|
2425
2772
|
// First, see if we can reuse fragments for the selection of this field.
|
|
2426
2773
|
let optimizedSelection = this.selectionSet;
|
|
2427
2774
|
if (isCompositeType(fieldBaseType) && this.selectionSet) {
|
|
@@ -2429,6 +2776,7 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2429
2776
|
parentType: fieldBaseType,
|
|
2430
2777
|
subSelection: this.selectionSet,
|
|
2431
2778
|
fragments,
|
|
2779
|
+
validator: fieldValidator,
|
|
2432
2780
|
// We can never apply a fragments that has directives on it at the field level.
|
|
2433
2781
|
canUseFullMatchingFragment: (fragment) => fragment.appliedDirectives.length === 0,
|
|
2434
2782
|
});
|
|
@@ -2442,19 +2790,19 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2442
2790
|
|
|
2443
2791
|
// Then, recurse inside the field sub-selection (note that if we matched some fragments above,
|
|
2444
2792
|
// this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
|
|
2445
|
-
optimizedSelection = optimizedSelection.
|
|
2793
|
+
optimizedSelection = optimizedSelection.optimizeSelections(fragments, fieldValidator);
|
|
2446
2794
|
|
|
2447
2795
|
return this.selectionSet === optimizedSelection
|
|
2448
2796
|
? this
|
|
2449
2797
|
: this.withUpdatedSelectionSet(optimizedSelection);
|
|
2450
2798
|
}
|
|
2451
2799
|
|
|
2452
|
-
|
|
2800
|
+
filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
|
|
2453
2801
|
if (!this.selectionSet) {
|
|
2454
2802
|
return predicate(this) ? this : undefined;
|
|
2455
2803
|
}
|
|
2456
2804
|
|
|
2457
|
-
const updatedSelectionSet = this.selectionSet.
|
|
2805
|
+
const updatedSelectionSet = this.selectionSet.filterRecursiveDepthFirst(predicate);
|
|
2458
2806
|
const thisWithFilteredSelectionSet = this.selectionSet === updatedSelectionSet
|
|
2459
2807
|
? this
|
|
2460
2808
|
: new FieldSelection(this.element, updatedSelectionSet);
|
|
@@ -2547,28 +2895,41 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
|
|
|
2547
2895
|
return !!this.selectionSet?.hasDefer();
|
|
2548
2896
|
}
|
|
2549
2897
|
|
|
2550
|
-
|
|
2898
|
+
normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FieldSelection {
|
|
2899
|
+
// This could be an interface field, and if we're normalizing on one of the implementation of that
|
|
2900
|
+
// interface, we want to make sure we use the field of the implementation, as it may in particular
|
|
2901
|
+
// have a more specific type which should propagate to the recursive call to normalize.
|
|
2902
|
+
|
|
2903
|
+
const definition = parentType === this.parentType
|
|
2904
|
+
? this.element.definition
|
|
2905
|
+
: parentType.field(this.element.name);
|
|
2906
|
+
assert(definition, `Cannot normalize ${this.element} at ${parentType} which does not have that field`)
|
|
2907
|
+
|
|
2908
|
+
const element = this.element.definition === definition ? this.element : this.element.withUpdatedDefinition(definition);
|
|
2551
2909
|
if (!this.selectionSet) {
|
|
2552
|
-
return this;
|
|
2910
|
+
return this.withUpdatedElement(element);
|
|
2553
2911
|
}
|
|
2554
2912
|
|
|
2555
|
-
const base =
|
|
2556
|
-
assert(isCompositeType(base), () => `Field ${
|
|
2557
|
-
const
|
|
2913
|
+
const base = element.baseType();
|
|
2914
|
+
assert(isCompositeType(base), () => `Field ${element} should not have a sub-selection`);
|
|
2915
|
+
const normalizedSubSelection = (recursive ?? true) ? this.selectionSet.normalize({ parentType: base }) : this.selectionSet;
|
|
2558
2916
|
// In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
|
|
2559
2917
|
// sub-selection is empty. Which suggest something may be wrong with this part of the query
|
|
2560
2918
|
// intent, but the query was valid while keeping an empty sub-selection isn't. So in that
|
|
2561
2919
|
// case, we just add some "non-included" __typename field just to keep the query valid.
|
|
2562
|
-
if (
|
|
2563
|
-
return
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2920
|
+
if (normalizedSubSelection?.isEmpty()) {
|
|
2921
|
+
return this.withUpdatedComponents(
|
|
2922
|
+
element,
|
|
2923
|
+
selectionSetOfElement(
|
|
2924
|
+
new Field(
|
|
2925
|
+
base.typenameField()!,
|
|
2926
|
+
undefined,
|
|
2927
|
+
[new Directive('include', { 'if': false })],
|
|
2928
|
+
)
|
|
2568
2929
|
)
|
|
2569
|
-
)
|
|
2930
|
+
);
|
|
2570
2931
|
} else {
|
|
2571
|
-
return
|
|
2932
|
+
return this.withUpdatedComponents(element, normalizedSubSelection);
|
|
2572
2933
|
}
|
|
2573
2934
|
}
|
|
2574
2935
|
|
|
@@ -2628,11 +2989,10 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
|
|
|
2628
2989
|
}
|
|
2629
2990
|
}
|
|
2630
2991
|
|
|
2631
|
-
|
|
2992
|
+
filterRecursiveDepthFirst(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
|
|
2632
2993
|
// Note that we essentially expand all fragments as part of this.
|
|
2633
|
-
const
|
|
2634
|
-
const
|
|
2635
|
-
const thisWithFilteredSelectionSet = updatedSelectionSet === selectionSet
|
|
2994
|
+
const updatedSelectionSet = this.selectionSet.filterRecursiveDepthFirst(predicate);
|
|
2995
|
+
const thisWithFilteredSelectionSet = updatedSelectionSet === this.selectionSet
|
|
2636
2996
|
? this
|
|
2637
2997
|
: new InlineFragmentSelection(this.element, updatedSelectionSet);
|
|
2638
2998
|
|
|
@@ -2665,6 +3025,9 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2665
3025
|
}
|
|
2666
3026
|
|
|
2667
3027
|
withUpdatedComponents(fragment: FragmentElement, selectionSet: SelectionSet): InlineFragmentSelection {
|
|
3028
|
+
if (fragment === this.element && selectionSet === this.selectionSet) {
|
|
3029
|
+
return this;
|
|
3030
|
+
}
|
|
2668
3031
|
return new InlineFragmentSelection(fragment, selectionSet);
|
|
2669
3032
|
}
|
|
2670
3033
|
|
|
@@ -2727,7 +3090,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2727
3090
|
};
|
|
2728
3091
|
}
|
|
2729
3092
|
|
|
2730
|
-
optimize(fragments: NamedFragments): FragmentSelection {
|
|
3093
|
+
optimize(fragments: NamedFragments, validator: FieldsConflictValidator): FragmentSelection {
|
|
2731
3094
|
let optimizedSelection = this.selectionSet;
|
|
2732
3095
|
|
|
2733
3096
|
// First, see if we can reuse fragments for the selection of this field.
|
|
@@ -2737,12 +3100,13 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2737
3100
|
parentType: typeCondition,
|
|
2738
3101
|
subSelection: optimizedSelection,
|
|
2739
3102
|
fragments,
|
|
3103
|
+
validator,
|
|
2740
3104
|
canUseFullMatchingFragment: (fragment) => {
|
|
2741
3105
|
// To be able to use a matching fragment, it needs to have either no directives, or if it has
|
|
2742
3106
|
// some, then:
|
|
2743
3107
|
// 1. all it's directives should also be on the current element.
|
|
2744
3108
|
// 2. the directives of this element should be the fragment condition.
|
|
2745
|
-
// because if those 2 conditions are true, we
|
|
3109
|
+
// because if those 2 conditions are true, we can replace the whole current inline fragment
|
|
2746
3110
|
// with the match spread and directives will still match.
|
|
2747
3111
|
return fragment.appliedDirectives.length === 0
|
|
2748
3112
|
|| (
|
|
@@ -2777,7 +3141,7 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2777
3141
|
|
|
2778
3142
|
// Then, recurse inside the field sub-selection (note that if we matched some fragments above,
|
|
2779
3143
|
// this recursion will "ignore" those as `FragmentSpreadSelection.optimize()` is a no-op).
|
|
2780
|
-
optimizedSelection = optimizedSelection.optimizeSelections(fragments);
|
|
3144
|
+
optimizedSelection = optimizedSelection.optimizeSelections(fragments, validator);
|
|
2781
3145
|
|
|
2782
3146
|
return this.selectionSet === optimizedSelection
|
|
2783
3147
|
? this
|
|
@@ -2809,55 +3173,60 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2809
3173
|
: this.withUpdatedComponents(newElement, newSelection);
|
|
2810
3174
|
}
|
|
2811
3175
|
|
|
2812
|
-
|
|
2813
|
-
const recursive = options?.recursive ?? true;
|
|
2814
|
-
|
|
3176
|
+
normalize({ parentType, recursive }: { parentType: CompositeType, recursive? : boolean }): FragmentSelection | SelectionSet | undefined {
|
|
2815
3177
|
const thisCondition = this.element.typeCondition;
|
|
2816
|
-
// Note that if the condition has directives, we preserve the fragment no matter what.
|
|
2817
|
-
if (this.element.appliedDirectives.length === 0) {
|
|
2818
|
-
if (!thisCondition || currentType === this.element.typeCondition) {
|
|
2819
|
-
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType, options);
|
|
2820
|
-
return trimmed.isEmpty() ? undefined : trimmed;
|
|
2821
|
-
}
|
|
2822
3178
|
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
if (
|
|
2831
|
-
|
|
2832
|
-
return undefined;
|
|
2833
|
-
} else {
|
|
2834
|
-
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType, options);
|
|
2835
|
-
return trimmed.isEmpty() ? undefined : trimmed;
|
|
2836
|
-
}
|
|
3179
|
+
// This method assumes by contract that `parentType` runtimes intersects `this.parentType`'s, but `parentType`
|
|
3180
|
+
// runtimes may be a subset. So first check if the selection should not be discarded on that account (that
|
|
3181
|
+
// is, we should not keep the selection if its condition runtimes don't intersect at all with those of
|
|
3182
|
+
// `parentType` as that would ultimately make an invalid selection set).
|
|
3183
|
+
if (thisCondition && parentType !== this.parentType) {
|
|
3184
|
+
const conditionRuntimes = possibleRuntimeTypes(thisCondition);
|
|
3185
|
+
const typeRuntimes = possibleRuntimeTypes(parentType);
|
|
3186
|
+
if (!conditionRuntimes.some((t) => typeRuntimes.includes(t))) {
|
|
3187
|
+
return undefined;
|
|
2837
3188
|
}
|
|
2838
3189
|
}
|
|
2839
3190
|
|
|
2840
|
-
//
|
|
2841
|
-
|
|
2842
|
-
|
|
3191
|
+
// We know the condition is "valid", but it may not be useful. That said, if the condition has directives,
|
|
3192
|
+
// we preserve the fragment no matter what.
|
|
3193
|
+
if (this.element.appliedDirectives.length === 0) {
|
|
3194
|
+
// There is a number of cases where a fragment is not useful:
|
|
3195
|
+
// 1. if there is not conditions (remember it also has no directives).
|
|
3196
|
+
// 2. if it's the same type as the current type: it's not restricting types further.
|
|
3197
|
+
// 3. if the current type is an object more generally: because in that case too the condition
|
|
3198
|
+
// cannot be restricting things further (it's typically a less precise interface/union).
|
|
3199
|
+
if (!thisCondition || parentType === this.element.typeCondition || isObjectType(parentType)) {
|
|
3200
|
+
const normalized = this.selectionSet.normalize({ parentType, recursive });
|
|
3201
|
+
return normalized.isEmpty() ? undefined : normalized;
|
|
3202
|
+
}
|
|
2843
3203
|
}
|
|
2844
3204
|
|
|
2845
|
-
//
|
|
2846
|
-
|
|
3205
|
+
// We preserve the current fragment, so we only recurse within the sub-selection if we're asked to be recusive.
|
|
3206
|
+
// (note that even if we're not recursive, we may still have some "lifting" to do)
|
|
3207
|
+
let normalizedSelectionSet: SelectionSet;
|
|
3208
|
+
if (recursive ?? true) {
|
|
3209
|
+
normalizedSelectionSet = this.selectionSet.normalize({ parentType: thisCondition ?? parentType });
|
|
2847
3210
|
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
(
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
3211
|
+
// It could be that everything was unsatisfiable.
|
|
3212
|
+
if (normalizedSelectionSet.isEmpty()) {
|
|
3213
|
+
if (this.element.appliedDirectives.length === 0) {
|
|
3214
|
+
return undefined;
|
|
3215
|
+
} else {
|
|
3216
|
+
return this.withUpdatedComponents(
|
|
3217
|
+
this.element.rebaseOn(parentType),
|
|
3218
|
+
selectionSetOfElement(
|
|
3219
|
+
new Field(
|
|
3220
|
+
(this.element.typeCondition ?? parentType).typenameField()!,
|
|
3221
|
+
undefined,
|
|
3222
|
+
[new Directive('include', { 'if': false })],
|
|
3223
|
+
)
|
|
3224
|
+
)
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
2860
3227
|
}
|
|
3228
|
+
} else {
|
|
3229
|
+
normalizedSelectionSet = this.selectionSet;
|
|
2861
3230
|
}
|
|
2862
3231
|
|
|
2863
3232
|
// Second, we check if some of the sub-selection fragments can be "lifted" outside of this fragment. This can happen if:
|
|
@@ -2865,10 +3234,10 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2865
3234
|
// 2. the sub-fragment is an object type,
|
|
2866
3235
|
// 3. the sub-fragment type is a valid runtime of the current type.
|
|
2867
3236
|
if (this.element.appliedDirectives.length === 0 && isAbstractType(thisCondition!)) {
|
|
2868
|
-
assert(!isObjectType(
|
|
2869
|
-
const currentRuntimes = possibleRuntimeTypes(
|
|
3237
|
+
assert(!isObjectType(parentType), () => `Should not have got here if ${parentType} is an object type`);
|
|
3238
|
+
const currentRuntimes = possibleRuntimeTypes(parentType);
|
|
2870
3239
|
const liftableSelections: Selection[] = [];
|
|
2871
|
-
for (const selection of
|
|
3240
|
+
for (const selection of normalizedSelectionSet.selections()) {
|
|
2872
3241
|
if (selection.kind === 'FragmentSelection'
|
|
2873
3242
|
&& selection.element.typeCondition
|
|
2874
3243
|
&& isObjectType(selection.element.typeCondition)
|
|
@@ -2879,8 +3248,8 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2879
3248
|
}
|
|
2880
3249
|
|
|
2881
3250
|
// 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
|
|
3251
|
+
if (liftableSelections.length === normalizedSelectionSet.selections().length) {
|
|
3252
|
+
return normalizedSelectionSet;
|
|
2884
3253
|
}
|
|
2885
3254
|
|
|
2886
3255
|
// Otherwise, if there is "liftable" selections, we must return a set comprised of those lifted selection,
|
|
@@ -2889,13 +3258,15 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2889
3258
|
const newSet = new SelectionSetUpdates();
|
|
2890
3259
|
newSet.add(liftableSelections);
|
|
2891
3260
|
newSet.add(this.withUpdatedSelectionSet(
|
|
2892
|
-
|
|
3261
|
+
normalizedSelectionSet.filter((s) => !liftableSelections.includes(s)),
|
|
2893
3262
|
));
|
|
2894
|
-
return newSet.toSelectionSet(
|
|
3263
|
+
return newSet.toSelectionSet(parentType);
|
|
2895
3264
|
}
|
|
2896
3265
|
}
|
|
2897
3266
|
|
|
2898
|
-
return this.
|
|
3267
|
+
return this.parentType === parentType && this.selectionSet === normalizedSelectionSet
|
|
3268
|
+
? this
|
|
3269
|
+
: this.withUpdatedComponents(this.element.rebaseOn(parentType), normalizedSelectionSet);
|
|
2899
3270
|
}
|
|
2900
3271
|
|
|
2901
3272
|
expandFragments(updatedFragments: NamedFragments | undefined): FragmentSelection {
|
|
@@ -2956,17 +3327,20 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2956
3327
|
assert(false, `Unsupported`);
|
|
2957
3328
|
}
|
|
2958
3329
|
|
|
2959
|
-
|
|
3330
|
+
normalize({ parentType }: { parentType: CompositeType }): FragmentSelection {
|
|
2960
3331
|
// We must update the spread parent type if necessary since we're not going deeper,
|
|
2961
3332
|
// or we'll be fundamentally losing context.
|
|
2962
|
-
assert(parentType.schema() === this.parentType.schema(), 'Should not try to
|
|
3333
|
+
assert(parentType.schema() === this.parentType.schema(), 'Should not try to normalize using a type from another schema');
|
|
2963
3334
|
return this.rebaseOn(parentType, this.fragments);
|
|
2964
3335
|
}
|
|
2965
3336
|
|
|
2966
3337
|
validate(): void {
|
|
2967
3338
|
this.validateDeferAndStream();
|
|
2968
3339
|
|
|
2969
|
-
|
|
3340
|
+
validate(
|
|
3341
|
+
runtimeTypesIntersects(this.parentType, this.namedFragment.typeCondition),
|
|
3342
|
+
() => `Fragment "${this.namedFragment.name}" cannot be spread inside type ${this.parentType} as the runtime types do not intersect ${this.namedFragment.typeCondition}`
|
|
3343
|
+
);
|
|
2970
3344
|
}
|
|
2971
3345
|
|
|
2972
3346
|
toSelectionNode(): FragmentSpreadNode {
|
|
@@ -2989,7 +3363,7 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2989
3363
|
};
|
|
2990
3364
|
}
|
|
2991
3365
|
|
|
2992
|
-
optimize(
|
|
3366
|
+
optimize(_1: NamedFragments, _2: FieldsConflictValidator): FragmentSelection {
|
|
2993
3367
|
return this;
|
|
2994
3368
|
}
|
|
2995
3369
|
|