@apollo/federation-internals 2.4.0-alpha.1 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/coreSpec.js +1 -1
- package/dist/coreSpec.js.map +1 -1
- package/dist/definitions.d.ts +15 -15
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +35 -55
- package/dist/definitions.js.map +1 -1
- package/dist/federation.d.ts.map +1 -1
- package/dist/federation.js +19 -18
- package/dist/federation.js.map +1 -1
- package/dist/operations.d.ts +222 -88
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +934 -605
- package/dist/operations.js.map +1 -1
- package/dist/precompute.d.ts.map +1 -1
- package/dist/precompute.js +13 -10
- package/dist/precompute.js.map +1 -1
- package/dist/values.d.ts +3 -3
- package/dist/values.d.ts.map +1 -1
- package/dist/values.js +22 -28
- package/dist/values.js.map +1 -1
- package/package.json +4 -2
- package/src/__tests__/operations.test.ts +727 -145
- package/src/__tests__/schemaUpgrader.test.ts +1 -1
- package/src/definitions.ts +53 -57
- package/src/federation.ts +27 -23
- package/src/operations.ts +1370 -855
- package/src/precompute.ts +18 -12
- package/src/values.ts +24 -30
- package/tsconfig.tsbuildinfo +1 -1
package/src/operations.ts
CHANGED
|
@@ -24,16 +24,12 @@ import {
|
|
|
24
24
|
InterfaceType,
|
|
25
25
|
isCompositeType,
|
|
26
26
|
isInterfaceType,
|
|
27
|
-
isLeafType,
|
|
28
27
|
isNullableType,
|
|
29
|
-
isUnionType,
|
|
30
28
|
ObjectType,
|
|
31
29
|
runtimeTypesIntersects,
|
|
32
30
|
Schema,
|
|
33
31
|
SchemaRootKind,
|
|
34
|
-
|
|
35
|
-
Variables,
|
|
36
|
-
variablesInArguments,
|
|
32
|
+
VariableCollector,
|
|
37
33
|
VariableDefinitions,
|
|
38
34
|
variableDefinitionsFromAST,
|
|
39
35
|
CompositeType,
|
|
@@ -46,11 +42,17 @@ import {
|
|
|
46
42
|
Variable,
|
|
47
43
|
possibleRuntimeTypes,
|
|
48
44
|
Type,
|
|
45
|
+
sameDirectiveApplication,
|
|
46
|
+
isLeafType,
|
|
47
|
+
Variables,
|
|
48
|
+
isObjectType,
|
|
49
49
|
} from "./definitions";
|
|
50
|
+
import { isInterfaceObjectType } from "./federation";
|
|
50
51
|
import { ERRORS } from "./error";
|
|
51
|
-
import {
|
|
52
|
-
import { assert, mapEntries, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
|
|
52
|
+
import { sameType } from "./types";
|
|
53
|
+
import { assert, isDefined, mapEntries, mapValues, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
|
|
53
54
|
import { argumentsEquals, argumentsFromAST, isValidValue, valueToAST, valueToString } from "./values";
|
|
55
|
+
import { v1 as uuidv1 } from 'uuid';
|
|
54
56
|
|
|
55
57
|
function validate(condition: any, message: () => string, sourceAST?: ASTNode): asserts condition {
|
|
56
58
|
if (!condition) {
|
|
@@ -67,19 +69,25 @@ abstract class AbstractOperationElement<T extends AbstractOperationElement<T>> e
|
|
|
67
69
|
|
|
68
70
|
constructor(
|
|
69
71
|
schema: Schema,
|
|
70
|
-
|
|
72
|
+
directives?: readonly Directive<any>[],
|
|
71
73
|
) {
|
|
72
|
-
super(schema);
|
|
74
|
+
super(schema, directives);
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
collectVariables(collector: VariableCollector) {
|
|
78
|
+
this.collectVariablesInElement(collector);
|
|
79
|
+
this.collectVariablesInAppliedDirectives(collector);
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
abstract key(): string;
|
|
83
|
+
|
|
84
|
+
abstract asPathElement(): string | undefined;
|
|
85
|
+
|
|
86
|
+
abstract rebaseOn(parentType: CompositeType): T;
|
|
87
|
+
|
|
88
|
+
abstract withUpdatedDirectives(newDirectives: readonly Directive<any>[]): T;
|
|
89
|
+
|
|
90
|
+
protected abstract collectVariablesInElement(collector: VariableCollector): void;
|
|
83
91
|
|
|
84
92
|
addAttachement(key: string, value: string) {
|
|
85
93
|
if (!this.attachements) {
|
|
@@ -106,49 +114,112 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
106
114
|
|
|
107
115
|
constructor(
|
|
108
116
|
readonly definition: FieldDefinition<CompositeType>,
|
|
109
|
-
readonly args
|
|
110
|
-
readonly
|
|
111
|
-
readonly alias?: string
|
|
117
|
+
private readonly args?: TArgs,
|
|
118
|
+
directives?: readonly Directive<any>[],
|
|
119
|
+
readonly alias?: string,
|
|
112
120
|
) {
|
|
113
|
-
super(definition.schema(),
|
|
121
|
+
super(definition.schema(), directives);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
protected collectVariablesInElement(collector: VariableCollector): void {
|
|
125
|
+
if (this.args) {
|
|
126
|
+
collector.collectInArguments(this.args);
|
|
127
|
+
}
|
|
114
128
|
}
|
|
115
129
|
|
|
116
130
|
get name(): string {
|
|
117
131
|
return this.definition.name;
|
|
118
132
|
}
|
|
119
133
|
|
|
134
|
+
argumentValue(name: string): any {
|
|
135
|
+
return this.args ? this.args[name] : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
120
138
|
responseName(): string {
|
|
121
139
|
return this.alias ? this.alias : this.name;
|
|
122
140
|
}
|
|
123
141
|
|
|
142
|
+
key(): string {
|
|
143
|
+
return this.responseName();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
asPathElement(): string {
|
|
147
|
+
return this.responseName();
|
|
148
|
+
}
|
|
149
|
+
|
|
124
150
|
get parentType(): CompositeType {
|
|
125
151
|
return this.definition.parent;
|
|
126
152
|
}
|
|
127
153
|
|
|
154
|
+
isLeafField(): boolean {
|
|
155
|
+
return isLeafType(baseType(this.definition.type!));
|
|
156
|
+
}
|
|
157
|
+
|
|
128
158
|
withUpdatedDefinition(newDefinition: FieldDefinition<any>): Field<TArgs> {
|
|
129
|
-
const newField = new Field<TArgs>(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
159
|
+
const newField = new Field<TArgs>(
|
|
160
|
+
newDefinition,
|
|
161
|
+
this.args,
|
|
162
|
+
this.appliedDirectives,
|
|
163
|
+
this.alias,
|
|
164
|
+
);
|
|
133
165
|
this.copyAttachementsTo(newField);
|
|
134
166
|
return newField;
|
|
135
167
|
}
|
|
136
168
|
|
|
137
169
|
withUpdatedAlias(newAlias: string | undefined): Field<TArgs> {
|
|
138
|
-
const newField = new Field<TArgs>(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
170
|
+
const newField = new Field<TArgs>(
|
|
171
|
+
this.definition,
|
|
172
|
+
this.args,
|
|
173
|
+
this.appliedDirectives,
|
|
174
|
+
newAlias,
|
|
175
|
+
);
|
|
176
|
+
this.copyAttachementsTo(newField);
|
|
177
|
+
return newField;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
withUpdatedDirectives(newDirectives: readonly Directive<any>[]): Field<TArgs> {
|
|
181
|
+
const newField = new Field<TArgs>(
|
|
182
|
+
this.definition,
|
|
183
|
+
this.args,
|
|
184
|
+
newDirectives,
|
|
185
|
+
this.alias,
|
|
186
|
+
);
|
|
142
187
|
this.copyAttachementsTo(newField);
|
|
143
188
|
return newField;
|
|
144
189
|
}
|
|
145
190
|
|
|
191
|
+
argumentsToNodes(): ArgumentNode[] | undefined {
|
|
192
|
+
if (!this.args) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const entries = Object.entries(this.args);
|
|
197
|
+
if (entries.length === 0) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return entries.map(([n, v]) => {
|
|
202
|
+
return {
|
|
203
|
+
kind: Kind.ARGUMENT,
|
|
204
|
+
name: { kind: Kind.NAME, value: n },
|
|
205
|
+
value: valueToAST(v, this.definition.argument(n)!.type!)!,
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
146
211
|
appliesTo(type: ObjectType | InterfaceType): boolean {
|
|
147
212
|
const definition = type.field(this.name);
|
|
148
213
|
return !!definition && this.selects(definition);
|
|
149
214
|
}
|
|
150
215
|
|
|
151
|
-
selects(
|
|
216
|
+
selects(
|
|
217
|
+
definition: FieldDefinition<any>,
|
|
218
|
+
assumeValid: boolean = false,
|
|
219
|
+
variableDefinitions?: VariableDefinitions,
|
|
220
|
+
): boolean {
|
|
221
|
+
assert(assumeValid || variableDefinitions, 'Must provide variable definitions if validation is needed');
|
|
222
|
+
|
|
152
223
|
// We've already validated that the field selects the definition on which it was built.
|
|
153
224
|
if (definition === this.definition) {
|
|
154
225
|
return true;
|
|
@@ -163,20 +234,20 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
163
234
|
|
|
164
235
|
// We need to make sure the field has valid values for every non-optional argument.
|
|
165
236
|
for (const argDef of definition.arguments()) {
|
|
166
|
-
const appliedValue = this.
|
|
237
|
+
const appliedValue = this.argumentValue(argDef.name);
|
|
167
238
|
if (appliedValue === undefined) {
|
|
168
239
|
if (argDef.defaultValue === undefined && !isNullableType(argDef.type!)) {
|
|
169
240
|
return false;
|
|
170
241
|
}
|
|
171
242
|
} else {
|
|
172
|
-
if (!assumeValid && !isValidValue(appliedValue, argDef,
|
|
243
|
+
if (!assumeValid && !isValidValue(appliedValue, argDef, variableDefinitions!)) {
|
|
173
244
|
return false;
|
|
174
245
|
}
|
|
175
246
|
}
|
|
176
247
|
}
|
|
177
248
|
|
|
178
249
|
// We also make sure the field application does not have non-null values for field that are not part of the definition.
|
|
179
|
-
if (!assumeValid) {
|
|
250
|
+
if (!assumeValid && this.args) {
|
|
180
251
|
for (const [name, value] of Object.entries(this.args)) {
|
|
181
252
|
if (value !== null && definition.argument(name) === undefined) {
|
|
182
253
|
return false
|
|
@@ -186,65 +257,65 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
186
257
|
return true;
|
|
187
258
|
}
|
|
188
259
|
|
|
189
|
-
validate() {
|
|
260
|
+
validate(variableDefinitions: VariableDefinitions) {
|
|
190
261
|
validate(this.name === this.definition.name, () => `Field name "${this.name}" cannot select field "${this.definition.coordinate}: name mismatch"`);
|
|
191
262
|
|
|
192
263
|
// We need to make sure the field has valid values for every non-optional argument.
|
|
193
264
|
for (const argDef of this.definition.arguments()) {
|
|
194
|
-
const appliedValue = this.
|
|
265
|
+
const appliedValue = this.argumentValue(argDef.name);
|
|
195
266
|
if (appliedValue === undefined) {
|
|
196
267
|
validate(
|
|
197
268
|
argDef.defaultValue !== undefined || isNullableType(argDef.type!),
|
|
198
269
|
() => `Missing mandatory value for argument "${argDef.name}" of field "${this.definition.coordinate}" in selection "${this}"`);
|
|
199
270
|
} else {
|
|
200
271
|
validate(
|
|
201
|
-
isValidValue(appliedValue, argDef,
|
|
272
|
+
isValidValue(appliedValue, argDef, variableDefinitions),
|
|
202
273
|
() => `Invalid value ${valueToString(appliedValue)} for argument "${argDef.coordinate}" of type ${argDef.type}`)
|
|
203
274
|
}
|
|
204
275
|
}
|
|
205
276
|
|
|
206
277
|
// We also make sure the field application does not have non-null values for field that are not part of the definition.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
278
|
+
if (this.args) {
|
|
279
|
+
for (const [name, value] of Object.entries(this.args)) {
|
|
280
|
+
validate(
|
|
281
|
+
value === null || this.definition.argument(name) !== undefined,
|
|
282
|
+
() => `Unknown argument "${name}" in field application of "${this.name}"`);
|
|
283
|
+
}
|
|
211
284
|
}
|
|
212
285
|
}
|
|
213
286
|
|
|
214
|
-
|
|
215
|
-
* See `FielSelection.updateForAddingTo` for a discussion of why this method exists and what it does.
|
|
216
|
-
*/
|
|
217
|
-
updateForAddingTo(selectionSet: SelectionSet): Field<TArgs> {
|
|
218
|
-
const selectionParent = selectionSet.parentType;
|
|
287
|
+
rebaseOn(parentType: CompositeType): Field<TArgs> {
|
|
219
288
|
const fieldParent = this.definition.parent;
|
|
220
|
-
if (
|
|
289
|
+
if (parentType === fieldParent) {
|
|
221
290
|
return this;
|
|
222
291
|
}
|
|
223
292
|
|
|
224
293
|
if (this.name === typenameFieldName) {
|
|
225
|
-
return this.withUpdatedDefinition(
|
|
294
|
+
return this.withUpdatedDefinition(parentType.typenameField()!);
|
|
226
295
|
}
|
|
227
296
|
|
|
228
297
|
validate(
|
|
229
|
-
this.canRebaseOn(
|
|
230
|
-
() => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${
|
|
298
|
+
this.canRebaseOn(parentType),
|
|
299
|
+
() => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${parentType}"`
|
|
231
300
|
);
|
|
232
|
-
const fieldDef =
|
|
233
|
-
validate(fieldDef, () => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${
|
|
301
|
+
const fieldDef = parentType.field(this.name);
|
|
302
|
+
validate(fieldDef, () => `Cannot add selection of field "${this.definition.coordinate}" to selection set of parent type "${parentType}" (that does not declare that field)`);
|
|
234
303
|
return this.withUpdatedDefinition(fieldDef);
|
|
235
304
|
}
|
|
236
305
|
|
|
237
306
|
private canRebaseOn(parentType: CompositeType) {
|
|
307
|
+
const fieldParentType = this.definition.parent
|
|
238
308
|
// There is 2 valid cases we want to allow:
|
|
239
309
|
// 1. either `selectionParent` and `fieldParent` are the same underlying type (same name) but from different underlying schema. Typically,
|
|
240
310
|
// happens when we're building subgraph queries but using selections from the original query which is against the supergraph API schema.
|
|
241
|
-
// 2. or they are not the same underlying type,
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
311
|
+
// 2. or they are not the same underlying type, but the field parent type is from an interface (or an interface object, which is the same
|
|
312
|
+
// here), in which case we may be rebasing an interface field on one of the implementation type, which is ok. Note that we don't verify
|
|
313
|
+
// that `parentType` is indeed an implementation of `fieldParentType` because it's possible that this implementation relationship exists
|
|
314
|
+
// in the supergraph, but not in any of the subgraph schema involved here. So we just let it be. Not that `rebaseOn` will complain anyway
|
|
315
|
+
// if the field name simply does not exists in `parentType`.
|
|
246
316
|
return parentType.name === fieldParentType.name
|
|
247
|
-
||
|
|
317
|
+
|| isInterfaceType(fieldParentType)
|
|
318
|
+
|| isInterfaceObjectType(fieldParentType);
|
|
248
319
|
}
|
|
249
320
|
|
|
250
321
|
typeIfAddedTo(parentType: CompositeType): Type | undefined {
|
|
@@ -284,44 +355,88 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
284
355
|
return that.kind === 'Field'
|
|
285
356
|
&& this.name === that.name
|
|
286
357
|
&& this.alias === that.alias
|
|
287
|
-
&& argumentsEquals(this.args, that.args)
|
|
358
|
+
&& (this.args ? that.args && argumentsEquals(this.args, that.args) : !that.args)
|
|
288
359
|
&& haveSameDirectives(this, that);
|
|
289
360
|
}
|
|
290
361
|
|
|
291
362
|
toString(): string {
|
|
292
363
|
const alias = this.alias ? this.alias + ': ' : '';
|
|
293
|
-
const entries = Object.entries(this.args);
|
|
294
|
-
const args = entries.length
|
|
364
|
+
const entries = this.args ? Object.entries(this.args) : [];
|
|
365
|
+
const args = entries.length === 0
|
|
295
366
|
? ''
|
|
296
367
|
: '(' + entries.map(([n, v]) => `${n}: ${valueToString(v, this.definition.argument(n)?.type)}`).join(', ') + ')';
|
|
297
368
|
return alias + this.name + args + this.appliedDirectivesToString();
|
|
298
369
|
}
|
|
299
370
|
}
|
|
300
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Computes a string key representing a directive application, so that if 2 directive applications have the same key, then they
|
|
374
|
+
* represent the same application.
|
|
375
|
+
*
|
|
376
|
+
* Note that this is mostly just the `toString` representation of the directive, but for 2 subtlety:
|
|
377
|
+
* 1. for a handful of directives (really just `@defer` for now), we never want to consider directive applications the same, no
|
|
378
|
+
* matter that the arguments of the directive match, and this for the same reason as documented on the `sameDirectiveApplications`
|
|
379
|
+
* method in `definitions.ts`.
|
|
380
|
+
* 2. we sort the argument (by their name) before converting them to string, since argument order does not matter in graphQL.
|
|
381
|
+
*/
|
|
382
|
+
function keyForDirective(
|
|
383
|
+
directive: Directive<OperationElement>,
|
|
384
|
+
directivesNeverEqualToThemselves: string[] = [ 'defer' ],
|
|
385
|
+
): string {
|
|
386
|
+
if (directivesNeverEqualToThemselves.includes(directive.name)) {
|
|
387
|
+
return uuidv1();
|
|
388
|
+
}
|
|
389
|
+
const entries = Object.entries(directive.arguments()).filter(([_, v]) => v !== undefined);
|
|
390
|
+
entries.sort(([n1], [n2]) => n1.localeCompare(n2));
|
|
391
|
+
const args = entries.length == 0 ? '' : '(' + entries.map(([n, v]) => `${n}: ${valueToString(v, directive.argumentType(n))}`).join(', ') + ')';
|
|
392
|
+
return `@${directive.name}${args}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
301
395
|
export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
302
396
|
readonly kind = 'FragmentElement' as const;
|
|
303
397
|
readonly typeCondition?: CompositeType;
|
|
398
|
+
private computedKey: string | undefined;
|
|
304
399
|
|
|
305
400
|
constructor(
|
|
306
401
|
private readonly sourceType: CompositeType,
|
|
307
402
|
typeCondition?: string | CompositeType,
|
|
403
|
+
directives?: readonly Directive<any>[],
|
|
308
404
|
) {
|
|
309
405
|
// TODO: we should do some validation here (remove the ! with proper error, and ensure we have some intersection between
|
|
310
406
|
// the source type and the type condition)
|
|
311
|
-
super(sourceType.schema(),
|
|
407
|
+
super(sourceType.schema(), directives);
|
|
312
408
|
this.typeCondition = typeCondition !== undefined && typeof typeCondition === 'string'
|
|
313
409
|
? this.schema().type(typeCondition)! as CompositeType
|
|
314
410
|
: typeCondition;
|
|
315
411
|
}
|
|
316
412
|
|
|
413
|
+
protected collectVariablesInElement(_: VariableCollector): void {
|
|
414
|
+
// Cannot have variables in fragments
|
|
415
|
+
}
|
|
416
|
+
|
|
317
417
|
get parentType(): CompositeType {
|
|
318
418
|
return this.sourceType;
|
|
319
419
|
}
|
|
320
420
|
|
|
421
|
+
key(): string {
|
|
422
|
+
if (!this.computedKey) {
|
|
423
|
+
// The key is such that 2 fragments with the same key within a selection set gets merged together. So the type-condition
|
|
424
|
+
// is include, but so are the directives.
|
|
425
|
+
const keyForDirectives = this.appliedDirectives.map((d) => keyForDirective(d)).join(' ');
|
|
426
|
+
this.computedKey = '...' + (this.typeCondition ? ' on ' + this.typeCondition.name : '') + keyForDirectives;
|
|
427
|
+
}
|
|
428
|
+
return this.computedKey;
|
|
429
|
+
}
|
|
430
|
+
|
|
321
431
|
castedType(): CompositeType {
|
|
322
432
|
return this.typeCondition ? this.typeCondition : this.sourceType;
|
|
323
433
|
}
|
|
324
434
|
|
|
435
|
+
asPathElement(): string | undefined {
|
|
436
|
+
const condition = this.typeCondition;
|
|
437
|
+
return condition ? `... on ${condition}` : undefined;
|
|
438
|
+
}
|
|
439
|
+
|
|
325
440
|
withUpdatedSourceType(newSourceType: CompositeType): FragmentElement {
|
|
326
441
|
return this.withUpdatedTypes(newSourceType, this.typeCondition);
|
|
327
442
|
}
|
|
@@ -334,34 +449,33 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
|
334
449
|
// Note that we pass the type-condition name instead of the type itself, to ensure that if `newSourceType` was from a different
|
|
335
450
|
// schema (typically, the supergraph) than `this.sourceType` (typically, a subgraph), then the new condition uses the
|
|
336
451
|
// definition of the proper schema (the supergraph in such cases, instead of the subgraph).
|
|
337
|
-
const newFragment = new FragmentElement(newSourceType, newCondition?.name);
|
|
338
|
-
for (const directive of this.appliedDirectives) {
|
|
339
|
-
newFragment.applyDirective(directive.definition!, directive.arguments());
|
|
340
|
-
}
|
|
452
|
+
const newFragment = new FragmentElement(newSourceType, newCondition?.name, this.appliedDirectives);
|
|
341
453
|
this.copyAttachementsTo(newFragment);
|
|
342
454
|
return newFragment;
|
|
343
455
|
}
|
|
344
456
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
457
|
+
withUpdatedDirectives(newDirectives: Directive<OperationElement>[]): FragmentElement {
|
|
458
|
+
const newFragment = new FragmentElement(this.sourceType, this.typeCondition, newDirectives);
|
|
459
|
+
this.copyAttachementsTo(newFragment);
|
|
460
|
+
return newFragment;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
rebaseOn(parentType: CompositeType): FragmentElement {
|
|
350
464
|
const fragmentParent = this.parentType;
|
|
351
465
|
const typeCondition = this.typeCondition;
|
|
352
|
-
if (
|
|
466
|
+
if (parentType === fragmentParent) {
|
|
353
467
|
return this;
|
|
354
468
|
}
|
|
355
469
|
|
|
356
470
|
// This usually imply that the fragment is not from the same sugraph than then selection. So we need
|
|
357
471
|
// to update the source type of the fragment, but also "rebase" the condition to the selection set
|
|
358
472
|
// schema.
|
|
359
|
-
const { canRebase, rebasedCondition } = this.canRebaseOn(
|
|
473
|
+
const { canRebase, rebasedCondition } = this.canRebaseOn(parentType);
|
|
360
474
|
validate(
|
|
361
|
-
canRebase,
|
|
362
|
-
() => `Cannot add fragment of condition "${typeCondition}" (runtimes: [${possibleRuntimeTypes(typeCondition!)}]) to
|
|
475
|
+
canRebase,
|
|
476
|
+
() => `Cannot add fragment of condition "${typeCondition}" (runtimes: [${possibleRuntimeTypes(typeCondition!)}]) to parent type "${parentType}" (runtimes: ${possibleRuntimeTypes(parentType)})`
|
|
363
477
|
);
|
|
364
|
-
return this.withUpdatedTypes(
|
|
478
|
+
return this.withUpdatedTypes(parentType, rebasedCondition);
|
|
365
479
|
}
|
|
366
480
|
|
|
367
481
|
private canRebaseOn(parentType: CompositeType): { canRebase: boolean, rebasedCondition?: CompositeType } {
|
|
@@ -417,9 +531,8 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
|
417
531
|
return this;
|
|
418
532
|
}
|
|
419
533
|
|
|
420
|
-
const updated = new FragmentElement(this.sourceType, this.typeCondition);
|
|
534
|
+
const updated = new FragmentElement(this.sourceType, this.typeCondition, updatedDirectives);
|
|
421
535
|
this.copyAttachementsTo(updated);
|
|
422
|
-
updatedDirectives.forEach((d) => updated.applyDirective(d.definition!, d.arguments()));
|
|
423
536
|
return updated;
|
|
424
537
|
}
|
|
425
538
|
|
|
@@ -478,13 +591,13 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
|
478
591
|
return this;
|
|
479
592
|
}
|
|
480
593
|
|
|
481
|
-
const updated = new FragmentElement(this.sourceType, this.typeCondition);
|
|
482
|
-
this.copyAttachementsTo(updated);
|
|
483
594
|
const deferDirective = this.schema().deferDirective();
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
595
|
+
const updatedDirectives = this.appliedDirectives
|
|
596
|
+
.filter((d) => d.name !== deferDirective.name)
|
|
597
|
+
.concat(new Directive<FragmentElement>(deferDirective.name, newDeferArgs));
|
|
598
|
+
|
|
599
|
+
const updated = new FragmentElement(this.sourceType, this.typeCondition, updatedDirectives);
|
|
600
|
+
this.copyAttachementsTo(updated);
|
|
488
601
|
return updated;
|
|
489
602
|
}
|
|
490
603
|
|
|
@@ -618,13 +731,15 @@ export class Operation {
|
|
|
618
731
|
// `expandFragments` on _only_ unused fragments and that case could be dealt with more efficiently, but
|
|
619
732
|
// probably not noticeable in practice so ...).
|
|
620
733
|
const toDeoptimize = mapEntries(usages).filter(([_, count]) => count < minUsagesToOptimize).map(([name]) => name);
|
|
621
|
-
|
|
734
|
+
|
|
735
|
+
const newFragments = optimizedSelection.fragments?.without(toDeoptimize);
|
|
736
|
+
optimizedSelection = optimizedSelection.expandFragments(toDeoptimize, newFragments);
|
|
622
737
|
|
|
623
738
|
return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, this.name);
|
|
624
739
|
}
|
|
625
740
|
|
|
626
741
|
expandAllFragments(): Operation {
|
|
627
|
-
const expandedSelections = this.selectionSet.
|
|
742
|
+
const expandedSelections = this.selectionSet.expandAllFragments();
|
|
628
743
|
if (expandedSelections === this.selectionSet) {
|
|
629
744
|
return this;
|
|
630
745
|
}
|
|
@@ -638,6 +753,21 @@ export class Operation {
|
|
|
638
753
|
);
|
|
639
754
|
}
|
|
640
755
|
|
|
756
|
+
trimUnsatisfiableBranches(): Operation {
|
|
757
|
+
const trimmedSelections = this.selectionSet.trimUnsatisfiableBranches(this.selectionSet.parentType);
|
|
758
|
+
if (trimmedSelections === this.selectionSet) {
|
|
759
|
+
return this;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return new Operation(
|
|
763
|
+
this.schema,
|
|
764
|
+
this.rootKind,
|
|
765
|
+
trimmedSelections,
|
|
766
|
+
this.variableDefinitions,
|
|
767
|
+
this.name
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
641
771
|
/**
|
|
642
772
|
* Returns this operation but potentially modified so all/some of the @defer applications have been removed.
|
|
643
773
|
*
|
|
@@ -708,40 +838,31 @@ export class Operation {
|
|
|
708
838
|
}
|
|
709
839
|
}
|
|
710
840
|
|
|
711
|
-
function addDirectiveNodesToElement(directiveNodes: readonly DirectiveNode[] | undefined, element: DirectiveTargetElement<any>) {
|
|
712
|
-
if (!directiveNodes) {
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
const schema = element.schema();
|
|
716
|
-
for (const node of directiveNodes) {
|
|
717
|
-
const directiveDef = schema.directive(node.name.value);
|
|
718
|
-
validate(directiveDef, () => `Unknown directive "@${node.name.value}" in selection`)
|
|
719
|
-
element.applyDirective(directiveDef, argumentsFromAST(directiveDef.coordinate, node.arguments, directiveDef));
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
export function selectionSetOf(parentType: CompositeType, selection: Selection): SelectionSet {
|
|
724
|
-
const selectionSet = new SelectionSet(parentType);
|
|
725
|
-
selectionSet.add(selection);
|
|
726
|
-
return selectionSet;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
841
|
export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmentDefinition> {
|
|
842
|
+
private _selectionSet: SelectionSet | undefined;
|
|
843
|
+
|
|
730
844
|
constructor(
|
|
731
845
|
schema: Schema,
|
|
732
846
|
readonly name: string,
|
|
733
847
|
readonly typeCondition: CompositeType,
|
|
734
|
-
|
|
848
|
+
directives?: Directive<NamedFragmentDefinition>[],
|
|
735
849
|
) {
|
|
736
|
-
super(schema);
|
|
850
|
+
super(schema, directives);
|
|
737
851
|
}
|
|
738
852
|
|
|
739
|
-
|
|
740
|
-
|
|
853
|
+
setSelectionSet(selectionSet: SelectionSet): NamedFragmentDefinition {
|
|
854
|
+
assert(!this._selectionSet, 'Attempting to set the selection set of a fragment definition already built')
|
|
855
|
+
this._selectionSet = selectionSet;
|
|
856
|
+
return this;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
get selectionSet(): SelectionSet {
|
|
860
|
+
assert(this._selectionSet, () => `Trying to access fragment definition ${this.name} before it is fully built`);
|
|
861
|
+
return this._selectionSet;
|
|
741
862
|
}
|
|
742
863
|
|
|
743
|
-
|
|
744
|
-
return
|
|
864
|
+
withUpdatedSelectionSet(newSelectionSet: SelectionSet): NamedFragmentDefinition {
|
|
865
|
+
return new NamedFragmentDefinition(this.schema(), this.name, this.typeCondition).setSelectionSet(newSelectionSet);
|
|
745
866
|
}
|
|
746
867
|
|
|
747
868
|
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
@@ -767,15 +888,13 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
767
888
|
}
|
|
768
889
|
|
|
769
890
|
/**
|
|
770
|
-
* Whether this fragment may apply at the provided type, that is if its type condition
|
|
771
|
-
*
|
|
891
|
+
* Whether this fragment may apply at the provided type, that is if its type condition runtime types intersects with the
|
|
892
|
+
* runtimes of the provided type.
|
|
772
893
|
*
|
|
773
894
|
* @param type - the type at which we're looking at applying the fragment
|
|
774
895
|
*/
|
|
775
896
|
canApplyAtType(type: CompositeType): boolean {
|
|
776
|
-
const applyAtType =
|
|
777
|
-
sameType(this.typeCondition, type)
|
|
778
|
-
|| (isAbstractType(this.typeCondition) && !isUnionType(type) && isDirectSubtype(this.typeCondition, type));
|
|
897
|
+
const applyAtType = sameType(type, this.typeCondition) || runtimeTypesIntersects(type, this.typeCondition);
|
|
779
898
|
return applyAtType
|
|
780
899
|
&& this.validForSchema(type.schema());
|
|
781
900
|
}
|
|
@@ -794,8 +913,7 @@ export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmen
|
|
|
794
913
|
|
|
795
914
|
// We try "rebasing" the selection into the provided schema and checks if that succeed.
|
|
796
915
|
try {
|
|
797
|
-
|
|
798
|
-
rebasedSelection.mergeIn(this.selectionSet);
|
|
916
|
+
this.selectionSet.rebaseOn(typeInSchema);
|
|
799
917
|
// If this succeed, it means the fragment could be applied to that schema and be valid.
|
|
800
918
|
return true;
|
|
801
919
|
} catch (e) {
|
|
@@ -816,14 +934,6 @@ export class NamedFragments {
|
|
|
816
934
|
return this.fragments.size === 0;
|
|
817
935
|
}
|
|
818
936
|
|
|
819
|
-
variables(): Variables {
|
|
820
|
-
let variables: Variables = [];
|
|
821
|
-
for (const fragment of this.fragments.values()) {
|
|
822
|
-
variables = mergeVariables(variables, fragment.variables());
|
|
823
|
-
}
|
|
824
|
-
return variables;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
937
|
names(): readonly string[] {
|
|
828
938
|
return this.fragments.keys();
|
|
829
939
|
}
|
|
@@ -845,7 +955,7 @@ export class NamedFragments {
|
|
|
845
955
|
return this.fragments.values().filter(f => f.canApplyAtType(type));
|
|
846
956
|
}
|
|
847
957
|
|
|
848
|
-
without(names: string[]): NamedFragments {
|
|
958
|
+
without(names: string[]): NamedFragments | undefined {
|
|
849
959
|
if (!names.some(n => this.fragments.has(n))) {
|
|
850
960
|
return this;
|
|
851
961
|
}
|
|
@@ -855,14 +965,14 @@ export class NamedFragments {
|
|
|
855
965
|
if (!names.includes(fragment.name)) {
|
|
856
966
|
// We want to keep that fragment. But that fragment might use a fragment we
|
|
857
967
|
// remove, and if so, we need to expand that removed fragment.
|
|
858
|
-
const
|
|
859
|
-
const newFragment =
|
|
968
|
+
const updatedSelectionSet = fragment.selectionSet.expandFragments(names, newFragments);
|
|
969
|
+
const newFragment = updatedSelectionSet === fragment.selectionSet
|
|
860
970
|
? fragment
|
|
861
|
-
:
|
|
971
|
+
: fragment.withUpdatedSelectionSet(updatedSelectionSet);
|
|
862
972
|
newFragments.add(newFragment);
|
|
863
973
|
}
|
|
864
974
|
}
|
|
865
|
-
return newFragments;
|
|
975
|
+
return newFragments.isEmpty() ? undefined : newFragments;
|
|
866
976
|
}
|
|
867
977
|
|
|
868
978
|
get(name: string): NamedFragmentDefinition | undefined {
|
|
@@ -877,9 +987,17 @@ export class NamedFragments {
|
|
|
877
987
|
return this.fragments.values();
|
|
878
988
|
}
|
|
879
989
|
|
|
880
|
-
|
|
990
|
+
map(mapper: (def: NamedFragmentDefinition) => NamedFragmentDefinition): NamedFragments {
|
|
991
|
+
const mapped = new NamedFragments();
|
|
992
|
+
for (const def of this.fragments.values()) {
|
|
993
|
+
mapped.fragments.set(def.name, mapper(def));
|
|
994
|
+
}
|
|
995
|
+
return mapped;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
validate(variableDefinitions: VariableDefinitions) {
|
|
881
999
|
for (const fragment of this.fragments.values()) {
|
|
882
|
-
fragment.selectionSet.validate();
|
|
1000
|
+
fragment.selectionSet.validate(variableDefinitions);
|
|
883
1001
|
}
|
|
884
1002
|
}
|
|
885
1003
|
|
|
@@ -892,55 +1010,6 @@ export class NamedFragments {
|
|
|
892
1010
|
}
|
|
893
1011
|
}
|
|
894
1012
|
|
|
895
|
-
abstract class Freezable<T> {
|
|
896
|
-
private _isFrozen: boolean = false;
|
|
897
|
-
|
|
898
|
-
protected abstract us(): T;
|
|
899
|
-
|
|
900
|
-
/**
|
|
901
|
-
* Freezes this selection/selection set, making it immutable after that point (that is, attempts to modify it will error out).
|
|
902
|
-
*
|
|
903
|
-
* This method should be used when a selection/selection set should not be modified. It ensures both that:
|
|
904
|
-
* 1. direct attempts to modify the selection afterward fails (at runtime, but the goal is to fetch bugs early and easily).
|
|
905
|
-
* 2. if this selection/selection set is "added" to another non-frozen selection (say, if this is input to `anotherSet.mergeIn(this)`),
|
|
906
|
-
* then it is automatically cloned first (thus ensuring this copy is not modified). Note that this properly is not guaranteed for
|
|
907
|
-
* non frozen selections. Meaning that if one does `s1.mergeIn(s2)` and `s2` is not frozen, then `s1` may (or may not) reference
|
|
908
|
-
* `s2` directly (without cloning) and thus later modifications to `s1` may (or may not) modify `s2`. This
|
|
909
|
-
* do-not-defensively-clone-by-default behaviour is done for performance reasons.
|
|
910
|
-
*
|
|
911
|
-
* Note that freezing is a "deep" operation, in that the whole structure of the selection/selection set is frozen by this method
|
|
912
|
-
* (and so this is not an excessively cheap operation).
|
|
913
|
-
*
|
|
914
|
-
* @return this selection/selection set (for convenience, to allow method chaining).
|
|
915
|
-
*/
|
|
916
|
-
freeze(): T {
|
|
917
|
-
if (!this.isFrozen()) {
|
|
918
|
-
this.freezeInternals();
|
|
919
|
-
this._isFrozen = true;
|
|
920
|
-
}
|
|
921
|
-
return this.us();
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
protected abstract freezeInternals(): void;
|
|
925
|
-
|
|
926
|
-
/**
|
|
927
|
-
* Whether this selection/selection set is frozen. See `freeze` for details.
|
|
928
|
-
*/
|
|
929
|
-
isFrozen(): boolean {
|
|
930
|
-
return this._isFrozen;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* A shortcut for returning a mutable version of this selection/selection set by cloning it if it is frozen, but returning this set directly
|
|
935
|
-
* if it is not frozen.
|
|
936
|
-
*/
|
|
937
|
-
cloneIfFrozen(): T {
|
|
938
|
-
return this.isFrozen() ? this.clone() : this.us();
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
abstract clone(): T;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
1013
|
/**
|
|
945
1014
|
* Utility class used to handle "normalizing" the @defer in an operation.
|
|
946
1015
|
*
|
|
@@ -965,7 +1034,7 @@ class DeferNormalizer {
|
|
|
965
1034
|
while (stack.length > 0) {
|
|
966
1035
|
const selection = stack.pop()!;
|
|
967
1036
|
if (selection.kind === 'FragmentSelection') {
|
|
968
|
-
const deferArgs = selection.element
|
|
1037
|
+
const deferArgs = selection.element.deferDirectiveArgs();
|
|
969
1038
|
if (deferArgs) {
|
|
970
1039
|
hasDefers = true;
|
|
971
1040
|
if (!deferArgs.label || deferArgs.if !== undefined) {
|
|
@@ -1003,57 +1072,47 @@ class DeferNormalizer {
|
|
|
1003
1072
|
}
|
|
1004
1073
|
}
|
|
1005
1074
|
|
|
1006
|
-
export class SelectionSet
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
private readonly _selections = new MultiMap<string, Selection>();
|
|
1010
|
-
private _selectionCount = 0;
|
|
1011
|
-
private _cachedSelections?: readonly Selection[];
|
|
1075
|
+
export class SelectionSet {
|
|
1076
|
+
private readonly _keyedSelections: Map<string, Selection>;
|
|
1077
|
+
private readonly _selections: readonly Selection[];
|
|
1012
1078
|
|
|
1013
1079
|
constructor(
|
|
1014
1080
|
readonly parentType: CompositeType,
|
|
1015
|
-
|
|
1081
|
+
keyedSelections: Map<string, Selection> = new Map(),
|
|
1082
|
+
readonly fragments?: NamedFragments,
|
|
1016
1083
|
) {
|
|
1017
|
-
|
|
1018
|
-
|
|
1084
|
+
this._keyedSelections = keyedSelections;
|
|
1085
|
+
this._selections = mapValues(keyedSelections);
|
|
1019
1086
|
}
|
|
1020
1087
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1088
|
+
selectionsInReverseOrder(): readonly Selection[] {
|
|
1089
|
+
const length = this._selections.length;
|
|
1090
|
+
const reversed = new Array<Selection>(length);
|
|
1091
|
+
for (let i = 0; i < length; i++) {
|
|
1092
|
+
reversed[i] = this._selections[length - i - 1];
|
|
1093
|
+
}
|
|
1094
|
+
return reversed;
|
|
1023
1095
|
}
|
|
1024
1096
|
|
|
1025
|
-
selections(
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
this._cachedSelections = selections;
|
|
1035
|
-
}
|
|
1036
|
-
assert(this._cachedSelections, 'Cache should have been populated');
|
|
1037
|
-
if (reversedOrder && this._cachedSelections.length > 1) {
|
|
1038
|
-
const reversed = new Array(this._selectionCount);
|
|
1039
|
-
for (let i = 0; i < this._selectionCount; i++) {
|
|
1040
|
-
reversed[i] = this._cachedSelections[this._selectionCount - i - 1];
|
|
1041
|
-
}
|
|
1042
|
-
return reversed;
|
|
1043
|
-
}
|
|
1044
|
-
return this._cachedSelections;
|
|
1097
|
+
selections(): readonly Selection[] {
|
|
1098
|
+
return this._selections;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Returns whether the selection contains a _non-aliased_ selection of __typename.
|
|
1102
|
+
hasTopLevelTypenameField(): boolean {
|
|
1103
|
+
return this._keyedSelections.has(typenameFieldName);
|
|
1045
1104
|
}
|
|
1046
1105
|
|
|
1047
|
-
fieldsInSet(): { path: string[], field: FieldSelection
|
|
1048
|
-
const fields = new Array<{ path: string[], field: FieldSelection
|
|
1106
|
+
fieldsInSet(): { path: string[], field: FieldSelection }[] {
|
|
1107
|
+
const fields = new Array<{ path: string[], field: FieldSelection }>();
|
|
1049
1108
|
for (const selection of this.selections()) {
|
|
1050
1109
|
if (selection.kind === 'FieldSelection') {
|
|
1051
|
-
fields.push({ path: [], field: selection
|
|
1110
|
+
fields.push({ path: [], field: selection });
|
|
1052
1111
|
} else {
|
|
1053
|
-
const condition = selection.element
|
|
1112
|
+
const condition = selection.element.typeCondition;
|
|
1054
1113
|
const header = condition ? [`... on ${condition}`] : [];
|
|
1055
|
-
for (const { path, field
|
|
1056
|
-
fields.push({ path: header.concat(path), field
|
|
1114
|
+
for (const { path, field } of selection.selectionSet.fieldsInSet()) {
|
|
1115
|
+
fields.push({ path: header.concat(path), field});
|
|
1057
1116
|
}
|
|
1058
1117
|
}
|
|
1059
1118
|
}
|
|
@@ -1061,23 +1120,20 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1061
1120
|
}
|
|
1062
1121
|
|
|
1063
1122
|
usedVariables(): Variables {
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1123
|
+
const collector = new VariableCollector();
|
|
1124
|
+
this.collectVariables(collector);
|
|
1125
|
+
return collector.variables();
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
collectVariables(collector: VariableCollector) {
|
|
1129
|
+
for (const selection of this.selections()) {
|
|
1130
|
+
selection.collectVariables(collector);
|
|
1072
1131
|
}
|
|
1073
|
-
return variables;
|
|
1074
1132
|
}
|
|
1075
1133
|
|
|
1076
1134
|
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
1077
|
-
for (const
|
|
1078
|
-
for (const selection of byResponseName) {
|
|
1135
|
+
for (const selection of this.selections()) {
|
|
1079
1136
|
selection.collectUsedFragmentNames(collector);
|
|
1080
|
-
}
|
|
1081
1137
|
}
|
|
1082
1138
|
}
|
|
1083
1139
|
|
|
@@ -1086,38 +1142,30 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1086
1142
|
return this;
|
|
1087
1143
|
}
|
|
1088
1144
|
|
|
1089
|
-
//
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1092
|
-
//
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1145
|
+
// Handling the case where the selection may alreayd have some fragments adds complexity,
|
|
1146
|
+
// not only because we need to deal with merging new and existing fragments, but also because
|
|
1147
|
+
// things get weird if some fragment names are in common to both. Since we currently only care
|
|
1148
|
+
// about this method when optimizing subgraph fetch selections and those are initially created
|
|
1149
|
+
// without any fragments, we don't bother handling this more complex case.
|
|
1150
|
+
assert(!this.fragments || this.fragments.isEmpty(), `Should not be called on selection that already has named fragments, but got ${this.fragments}`)
|
|
1096
1151
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
return
|
|
1152
|
+
return this.lazyMap((selection) => selection.optimize(fragments), { fragments });
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
expandAllFragments(): SelectionSet {
|
|
1156
|
+
return this.lazyMap((selection) => selection.expandAllFragments(), { fragments: null });
|
|
1102
1157
|
}
|
|
1103
1158
|
|
|
1104
|
-
expandFragments(names
|
|
1105
|
-
if (names
|
|
1159
|
+
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): SelectionSet {
|
|
1160
|
+
if (names.length === 0) {
|
|
1106
1161
|
return this;
|
|
1107
1162
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
if (Array.isArray(expanded)) {
|
|
1115
|
-
withExpanded.addAll(expanded);
|
|
1116
|
-
} else {
|
|
1117
|
-
withExpanded.add(expanded as Selection);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
return withExpanded;
|
|
1163
|
+
|
|
1164
|
+
return this.lazyMap((selection) => selection.expandFragments(names, updatedFragments), { fragments: updatedFragments ?? null });
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
trimUnsatisfiableBranches(parentType: CompositeType): SelectionSet {
|
|
1168
|
+
return this.lazyMap((selection) => selection.trimUnsatisfiableBranches(parentType), { parentType });
|
|
1121
1169
|
}
|
|
1122
1170
|
|
|
1123
1171
|
/**
|
|
@@ -1128,30 +1176,39 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1128
1176
|
* objects when that is the case. This does mean that the resulting selection set may be `this`
|
|
1129
1177
|
* directly, or may alias some of the sub-selection in `this`.
|
|
1130
1178
|
*/
|
|
1131
|
-
lazyMap(
|
|
1132
|
-
|
|
1179
|
+
lazyMap(
|
|
1180
|
+
mapper: (selection: Selection) => Selection | readonly Selection[] | SelectionSet | undefined,
|
|
1181
|
+
options?: {
|
|
1182
|
+
fragments?: NamedFragments | null,
|
|
1183
|
+
parentType?: CompositeType,
|
|
1184
|
+
}
|
|
1185
|
+
): SelectionSet {
|
|
1133
1186
|
const selections = this.selections();
|
|
1187
|
+
const updatedFragments = options?.fragments;
|
|
1188
|
+
const newFragments = updatedFragments === undefined ? this.fragments : (updatedFragments ?? undefined);
|
|
1189
|
+
|
|
1190
|
+
let updatedSelections: SelectionSetUpdates | undefined = undefined;
|
|
1134
1191
|
for (let i = 0; i < selections.length; i++) {
|
|
1135
1192
|
const selection = selections[i];
|
|
1136
1193
|
const updated = mapper(selection);
|
|
1137
1194
|
if (updated !== selection && !updatedSelections) {
|
|
1138
|
-
updatedSelections =
|
|
1195
|
+
updatedSelections = new SelectionSetUpdates();
|
|
1139
1196
|
for (let j = 0; j < i; j++) {
|
|
1140
|
-
updatedSelections.
|
|
1197
|
+
updatedSelections.add(selections[j]);
|
|
1141
1198
|
}
|
|
1142
1199
|
}
|
|
1143
1200
|
if (!!updated && updatedSelections) {
|
|
1144
|
-
|
|
1145
|
-
updated.selections().forEach((s) => updatedSelections!.push(s));
|
|
1146
|
-
} else {
|
|
1147
|
-
updatedSelections.push(updated);
|
|
1148
|
-
}
|
|
1201
|
+
updatedSelections.add(updated);
|
|
1149
1202
|
}
|
|
1150
1203
|
}
|
|
1151
1204
|
if (!updatedSelections) {
|
|
1152
|
-
return this;
|
|
1205
|
+
return this.withUpdatedFragments(newFragments);
|
|
1153
1206
|
}
|
|
1154
|
-
return
|
|
1207
|
+
return updatedSelections.toSelectionSet(options?.parentType ?? this.parentType, newFragments);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
private withUpdatedFragments(newFragments: NamedFragments | undefined): SelectionSet {
|
|
1211
|
+
return this.fragments === newFragments ? this : new SelectionSet(this.parentType, this._keyedSelections, newFragments);
|
|
1155
1212
|
}
|
|
1156
1213
|
|
|
1157
1214
|
withoutDefer(labelsToRemove?: Set<string>): SelectionSet {
|
|
@@ -1164,6 +1221,10 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1164
1221
|
return this.lazyMap((selection) => selection.withNormalizedDefer(normalizer));
|
|
1165
1222
|
}
|
|
1166
1223
|
|
|
1224
|
+
hasDefer(): boolean {
|
|
1225
|
+
return this.selections().some((s) => s.hasDefer());
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1167
1228
|
/**
|
|
1168
1229
|
* Returns the selection select from filtering out any selection that does not match the provided predicate.
|
|
1169
1230
|
*
|
|
@@ -1179,172 +1240,84 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1179
1240
|
return updated.isEmpty() ? undefined : updated;
|
|
1180
1241
|
}
|
|
1181
1242
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1243
|
+
rebaseOn(parentType: CompositeType): SelectionSet {
|
|
1244
|
+
if (this.parentType === parentType) {
|
|
1245
|
+
return this;
|
|
1185
1246
|
}
|
|
1186
|
-
}
|
|
1187
1247
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
* Please note that by default, the selection from the input may (or may not) be directly referenced by this selection
|
|
1192
|
-
* set after this method return. That is, future modification of this selection set may end up modifying the input
|
|
1193
|
-
* set due to direct aliasing. If direct aliasing should be prevented, the input selection set should be frozen (see
|
|
1194
|
-
* `freeze` for details).
|
|
1195
|
-
*/
|
|
1196
|
-
mergeIn(selectionSet: SelectionSet) {
|
|
1197
|
-
for (const selection of selectionSet.selections()) {
|
|
1198
|
-
this.add(selection);
|
|
1248
|
+
const newSelections = new Map<string, Selection>();
|
|
1249
|
+
for (const selection of this.selections()) {
|
|
1250
|
+
newSelections.set(selection.key(), selection.rebaseOn(parentType));
|
|
1199
1251
|
}
|
|
1200
|
-
}
|
|
1201
1252
|
|
|
1202
|
-
|
|
1203
|
-
* Adds the provided selections to this selection, merging common selection as necessary.
|
|
1204
|
-
*
|
|
1205
|
-
* This is very similar to `mergeIn` except that it takes a direct array of selection, and the direct aliasing
|
|
1206
|
-
* remarks from `mergeInd` applies here too.
|
|
1207
|
-
*/
|
|
1208
|
-
addAll(selections: readonly Selection[]): SelectionSet {
|
|
1209
|
-
selections.forEach(s => this.add(s));
|
|
1210
|
-
return this;
|
|
1253
|
+
return new SelectionSet(parentType, newSelections, this.fragments);
|
|
1211
1254
|
}
|
|
1212
1255
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
* Please note that by default, the input selection may (or may not) be directly referenced by this selection
|
|
1217
|
-
* set after this method return. That is, future modification of this selection set may end up modifying the input
|
|
1218
|
-
* selection due to direct aliasing. If direct aliasing should be prevented, the input selection should be frozen
|
|
1219
|
-
* (see `freeze` for details).
|
|
1220
|
-
*/
|
|
1221
|
-
add(selection: Selection): Selection {
|
|
1222
|
-
// It's a bug to try to add to a frozen selection set
|
|
1223
|
-
assert(!this.isFrozen(), () => `Cannot add to frozen selection: ${this}`);
|
|
1224
|
-
|
|
1225
|
-
const toAdd = selection.updateForAddingTo(this);
|
|
1226
|
-
const key = toAdd.key();
|
|
1227
|
-
const existing: Selection[] | undefined = this._selections.get(key);
|
|
1228
|
-
if (existing) {
|
|
1229
|
-
for (const existingSelection of existing) {
|
|
1230
|
-
if (existingSelection.kind === toAdd.kind && haveSameDirectives(existingSelection.element(), toAdd.element())) {
|
|
1231
|
-
if (toAdd.selectionSet) {
|
|
1232
|
-
existingSelection.selectionSet!.mergeIn(toAdd.selectionSet);
|
|
1233
|
-
}
|
|
1234
|
-
return existingSelection;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1256
|
+
equals(that: SelectionSet): boolean {
|
|
1257
|
+
if (this === that) {
|
|
1258
|
+
return true;
|
|
1237
1259
|
}
|
|
1238
|
-
this._selections.add(key, toAdd);
|
|
1239
|
-
++this._selectionCount;
|
|
1240
|
-
this._cachedSelections = undefined;
|
|
1241
|
-
return toAdd;
|
|
1242
|
-
}
|
|
1243
1260
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
* @return whether a selection was removed.
|
|
1248
|
-
*/
|
|
1249
|
-
removeTopLevelField(responseName: string): boolean {
|
|
1250
|
-
// It's a bug to try to remove from a frozen selection set
|
|
1251
|
-
assert(!this.isFrozen(), () => `Cannot remove from frozen selection: ${this}`);
|
|
1261
|
+
if (this._selections.length !== that._selections.length) {
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1252
1264
|
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1265
|
+
for (const [key, thisSelection] of this._keyedSelections) {
|
|
1266
|
+
const thatSelection = that._keyedSelections.get(key);
|
|
1267
|
+
if (!thatSelection || !thisSelection.equals(thatSelection)) {
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1257
1270
|
}
|
|
1258
|
-
return
|
|
1271
|
+
return true;
|
|
1259
1272
|
}
|
|
1260
1273
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1274
|
+
private triviallyNestedSelectionsForKey(parentType: CompositeType, key: string): Selection[] {
|
|
1275
|
+
const found: Selection[] = [];
|
|
1276
|
+
for (const selection of this.selections()) {
|
|
1277
|
+
if (selection.isUnecessaryInlineFragment(parentType)) {
|
|
1278
|
+
const selectionForKey = selection.selectionSet._keyedSelections.get(key);
|
|
1279
|
+
if (selectionForKey) {
|
|
1280
|
+
found.push(selectionForKey);
|
|
1281
|
+
}
|
|
1282
|
+
for (const nestedSelection of selection.selectionSet.triviallyNestedSelectionsForKey(parentType, key)) {
|
|
1283
|
+
found.push(nestedSelection);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1272
1286
|
}
|
|
1287
|
+
return found;
|
|
1273
1288
|
}
|
|
1274
1289
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
fieldAccessor: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined = (type, name) => type.field(name)
|
|
1279
|
-
) {
|
|
1280
|
-
if (!node) {
|
|
1281
|
-
return;
|
|
1290
|
+
private mergeSameKeySelections(selections: Selection[]): Selection | undefined {
|
|
1291
|
+
if (selections.length === 0) {
|
|
1292
|
+
return undefined;
|
|
1282
1293
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1294
|
+
const first = selections[0];
|
|
1295
|
+
// We know that all the selections passed are for exactly the same element (same "key"). So if it is a
|
|
1296
|
+
// leaf field or a named fragment, then we know that even if we have more than 1 selection, all of them
|
|
1297
|
+
// are the exact same and we can just return the first one. Only if we have a composite field or an
|
|
1298
|
+
// inline fragment do we need to merge the underlying sub-selection (which may differ).
|
|
1299
|
+
if (!first.selectionSet || (first instanceof FragmentSpreadSelection) || selections.length === 1) {
|
|
1300
|
+
return first;
|
|
1285
1301
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
node: SelectionNode,
|
|
1290
|
-
variableDefinitions: VariableDefinitions,
|
|
1291
|
-
fieldAccessor: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined = (type, name) => type.field(name)
|
|
1292
|
-
) {
|
|
1293
|
-
this.add(this.nodeToSelection(node, variableDefinitions, fieldAccessor));
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
private nodeToSelection(
|
|
1297
|
-
node: SelectionNode,
|
|
1298
|
-
variableDefinitions: VariableDefinitions,
|
|
1299
|
-
fieldAccessor: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined
|
|
1300
|
-
): Selection {
|
|
1301
|
-
let selection: Selection;
|
|
1302
|
-
switch (node.kind) {
|
|
1303
|
-
case Kind.FIELD:
|
|
1304
|
-
const definition: FieldDefinition<any> | undefined = fieldAccessor(this.parentType, node.name.value);
|
|
1305
|
-
validate(definition, () => `Cannot query field "${node.name.value}" on type "${this.parentType}".`, this.parentType.sourceAST);
|
|
1306
|
-
const type = baseType(definition.type!);
|
|
1307
|
-
selection = new FieldSelection(
|
|
1308
|
-
new Field(definition, argumentsFromAST(definition.coordinate, node.arguments, definition), variableDefinitions, node.alias?.value),
|
|
1309
|
-
isLeafType(type) ? undefined : new SelectionSet(type as CompositeType, this.fragments)
|
|
1310
|
-
);
|
|
1311
|
-
if (node.selectionSet) {
|
|
1312
|
-
validate(selection.selectionSet, () => `Unexpected selection set on leaf field "${selection.element()}"`, selection.element().definition.sourceAST);
|
|
1313
|
-
selection.selectionSet.addSelectionSetNode(node.selectionSet, variableDefinitions, fieldAccessor);
|
|
1314
|
-
}
|
|
1315
|
-
break;
|
|
1316
|
-
case Kind.INLINE_FRAGMENT:
|
|
1317
|
-
const element = new FragmentElement(this.parentType, node.typeCondition?.name.value);
|
|
1318
|
-
selection = new InlineFragmentSelection(
|
|
1319
|
-
element,
|
|
1320
|
-
new SelectionSet(element.typeCondition ? element.typeCondition : element.parentType, this.fragments)
|
|
1321
|
-
);
|
|
1322
|
-
selection.selectionSet.addSelectionSetNode(node.selectionSet, variableDefinitions, fieldAccessor);
|
|
1323
|
-
break;
|
|
1324
|
-
case Kind.FRAGMENT_SPREAD:
|
|
1325
|
-
const fragmentName = node.name.value;
|
|
1326
|
-
validate(this.fragments, () => `Cannot find fragment name "${fragmentName}" (no fragments were provided)`);
|
|
1327
|
-
selection = new FragmentSpreadSelection(this.parentType, this.fragments, fragmentName);
|
|
1328
|
-
break;
|
|
1302
|
+
const mergedSubselections = new SelectionSetUpdates();
|
|
1303
|
+
for (const selection of selections) {
|
|
1304
|
+
mergedSubselections.add(selection.selectionSet!);
|
|
1329
1305
|
}
|
|
1330
|
-
|
|
1331
|
-
return selection;
|
|
1306
|
+
return first.withUpdatedSelectionSet(mergedSubselections.toSelectionSet(first.selectionSet.parentType));
|
|
1332
1307
|
}
|
|
1333
1308
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1309
|
+
contains(that: SelectionSet): boolean {
|
|
1310
|
+
// Note that we cannot really rely on the number of selections in `this` and `that` to short-cut this method
|
|
1311
|
+
// due to the handling of "trivially nested selections". That is, `this` might have less top-level selections
|
|
1312
|
+
// than `that`, and yet contains a named fragment directly on the parent type that includes everything in `that`.
|
|
1338
1313
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1314
|
+
for (const [key, thatSelection] of that._keyedSelections) {
|
|
1315
|
+
const thisSelection = this._keyedSelections.get(key);
|
|
1316
|
+
const otherSelections = this.triviallyNestedSelectionsForKey(this.parentType, key);
|
|
1317
|
+
const mergedSelection = this.mergeSameKeySelections([thisSelection].concat(otherSelections).filter(isDefined));
|
|
1342
1318
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
if (!thatSelections
|
|
1346
|
-
|| thisSelections.length !== thatSelections.length
|
|
1347
|
-
|| !thisSelections.every(thisSelection => thatSelections.some(thatSelection => thisSelection.equals(thatSelection)))
|
|
1319
|
+
if (!(mergedSelection && mergedSelection.contains(thatSelection))
|
|
1320
|
+
&& !(thatSelection.isUnecessaryInlineFragment(this.parentType) && this.contains(thatSelection.selectionSet))
|
|
1348
1321
|
) {
|
|
1349
1322
|
return false
|
|
1350
1323
|
}
|
|
@@ -1352,21 +1325,13 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1352
1325
|
return true;
|
|
1353
1326
|
}
|
|
1354
1327
|
|
|
1355
|
-
|
|
1356
|
-
if (this.
|
|
1357
|
-
|
|
1328
|
+
diffIfContains(that: SelectionSet): { contains: boolean, diff?: SelectionSet } {
|
|
1329
|
+
if (this.contains(that)) {
|
|
1330
|
+
const diff = this.minus(that);
|
|
1331
|
+
return { contains: true, diff: diff.isEmpty() ? undefined : diff };
|
|
1358
1332
|
}
|
|
1359
1333
|
|
|
1360
|
-
|
|
1361
|
-
const thisSelections = this._selections.get(key);
|
|
1362
|
-
if (!thisSelections
|
|
1363
|
-
|| (thisSelections.length < thatSelections.length
|
|
1364
|
-
|| !thatSelections.every(thatSelection => thisSelections.some(thisSelection => thisSelection.contains(thatSelection))))
|
|
1365
|
-
) {
|
|
1366
|
-
return false
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
return true;
|
|
1334
|
+
return { contains: false };
|
|
1370
1335
|
}
|
|
1371
1336
|
|
|
1372
1337
|
/**
|
|
@@ -1374,45 +1339,37 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1374
1339
|
* provided selection set have been remove.
|
|
1375
1340
|
*/
|
|
1376
1341
|
minus(that: SelectionSet): SelectionSet {
|
|
1377
|
-
const updated = new
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1342
|
+
const updated = new SelectionSetUpdates();
|
|
1343
|
+
|
|
1344
|
+
for (const [key, thisSelection] of this._keyedSelections) {
|
|
1345
|
+
const thatSelection = that._keyedSelections.get(key);
|
|
1346
|
+
const otherSelections = that.triviallyNestedSelectionsForKey(this.parentType, key);
|
|
1347
|
+
const allSelections = thatSelection ? [thatSelection].concat(otherSelections) : otherSelections;
|
|
1348
|
+
if (allSelections.length === 0) {
|
|
1349
|
+
updated.add(thisSelection);
|
|
1382
1350
|
} else {
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
// If there is a subset, then we compute the diff of the subset and add that (if not empty).
|
|
1387
|
-
// Otherwise, we just skip `thisSelection` and do nothing
|
|
1388
|
-
if (thisSelection.selectionSet && thatSelection.selectionSet) {
|
|
1389
|
-
const updatedSubSelectionSet = thisSelection.selectionSet.minus(thatSelection.selectionSet);
|
|
1390
|
-
if (!updatedSubSelectionSet.isEmpty()) {
|
|
1391
|
-
updated._selections.add(key, thisSelection.withUpdatedSubSelection(updatedSubSelectionSet));
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
} else {
|
|
1395
|
-
updated._selections.add(key, thisSelection);
|
|
1396
|
-
}
|
|
1351
|
+
const selectionDiff = allSelections.reduce<Selection | undefined>((prev, val) => prev?.minus(val), thisSelection);
|
|
1352
|
+
if (selectionDiff) {
|
|
1353
|
+
updated.add(selectionDiff);
|
|
1397
1354
|
}
|
|
1398
1355
|
}
|
|
1399
1356
|
}
|
|
1400
|
-
return updated;
|
|
1357
|
+
return updated.toSelectionSet(this.parentType, this.fragments);
|
|
1401
1358
|
}
|
|
1402
1359
|
|
|
1403
1360
|
canRebaseOn(parentTypeToTest: CompositeType): boolean {
|
|
1404
1361
|
return this.selections().every((selection) => selection.canAddTo(parentTypeToTest));
|
|
1405
1362
|
}
|
|
1406
1363
|
|
|
1407
|
-
validate() {
|
|
1364
|
+
validate(variableDefinitions: VariableDefinitions) {
|
|
1408
1365
|
validate(!this.isEmpty(), () => `Invalid empty selection set`);
|
|
1409
1366
|
for (const selection of this.selections()) {
|
|
1410
|
-
selection.validate();
|
|
1367
|
+
selection.validate(variableDefinitions);
|
|
1411
1368
|
}
|
|
1412
1369
|
}
|
|
1413
1370
|
|
|
1414
1371
|
isEmpty(): boolean {
|
|
1415
|
-
return this._selections.
|
|
1372
|
+
return this._selections.length === 0;
|
|
1416
1373
|
}
|
|
1417
1374
|
|
|
1418
1375
|
toSelectionSetNode(): SelectionSetNode {
|
|
@@ -1445,13 +1402,12 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1445
1402
|
// By default, we will print the selection the order in which things were added to it.
|
|
1446
1403
|
// If __typename is selected however, we put it first. It's a detail but as __typename is a bit special it looks better,
|
|
1447
1404
|
// and it happens to mimic prior behavior on the query plan side so it saves us from changing tests for no good reasons.
|
|
1448
|
-
const
|
|
1449
|
-
const
|
|
1450
|
-
(s: Selection) => s.kind === 'FieldSelection' && !s.field.alias && s.field.name === typenameFieldName;
|
|
1405
|
+
const isNonAliasedTypenameSelection = (s: Selection) => s.kind === 'FieldSelection' && !s.element.alias && s.element.name === typenameFieldName;
|
|
1406
|
+
const typenameSelection = this._selections.find((s) => isNonAliasedTypenameSelection(s));
|
|
1451
1407
|
if (typenameSelection) {
|
|
1452
|
-
return typenameSelection.concat(this.selections().filter(s => !isNonAliasedTypenameSelection(s)));
|
|
1408
|
+
return [typenameSelection].concat(this.selections().filter(s => !isNonAliasedTypenameSelection(s)));
|
|
1453
1409
|
} else {
|
|
1454
|
-
return this.
|
|
1410
|
+
return this._selections;
|
|
1455
1411
|
}
|
|
1456
1412
|
}
|
|
1457
1413
|
|
|
@@ -1461,7 +1417,7 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1461
1417
|
|
|
1462
1418
|
private toOperationPathsInternal(parentPaths: OperationPath[]): OperationPath[] {
|
|
1463
1419
|
return this.selections().flatMap((selection) => {
|
|
1464
|
-
const updatedPaths = parentPaths.map(path => path.concat(selection.element
|
|
1420
|
+
const updatedPaths = parentPaths.map(path => path.concat(selection.element));
|
|
1465
1421
|
return selection.selectionSet
|
|
1466
1422
|
? selection.selectionSet.toOperationPathsInternal(updatedPaths)
|
|
1467
1423
|
: updatedPaths;
|
|
@@ -1470,29 +1426,28 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1470
1426
|
|
|
1471
1427
|
/**
|
|
1472
1428
|
* Calls the provided callback on all the "elements" (including nested ones) of this selection set.
|
|
1473
|
-
* The
|
|
1429
|
+
* The order of traversal is that of the selection set.
|
|
1474
1430
|
*/
|
|
1475
1431
|
forEachElement(callback: (elt: OperationElement) => void) {
|
|
1476
|
-
|
|
1432
|
+
// Note: we reverse to preserve ordering (since the stack re-reverse).
|
|
1433
|
+
const stack = this.selectionsInReverseOrder().concat();
|
|
1477
1434
|
while (stack.length > 0) {
|
|
1478
1435
|
const selection = stack.pop()!;
|
|
1479
|
-
callback(selection.element
|
|
1480
|
-
|
|
1481
|
-
// and make output a bit more intuitive.
|
|
1482
|
-
selection.selectionSet?.selections(true).forEach((s) => stack.push(s));
|
|
1436
|
+
callback(selection.element);
|
|
1437
|
+
selection.selectionSet?.selectionsInReverseOrder().forEach((s) => stack.push(s));
|
|
1483
1438
|
}
|
|
1484
1439
|
}
|
|
1485
1440
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1441
|
+
/**
|
|
1442
|
+
* Returns true if any of the element in this selection set matches the provided predicate.
|
|
1443
|
+
*/
|
|
1444
|
+
some(predicate: (elt: OperationElement) => boolean): boolean {
|
|
1488
1445
|
for (const selection of this.selections()) {
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
cloned._selections.add(clonedSelection.key(), clonedSelection);
|
|
1493
|
-
++cloned._selectionCount;
|
|
1446
|
+
if (predicate(selection.element) || (selection.selectionSet && selection.selectionSet.some(predicate))) {
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1494
1449
|
}
|
|
1495
|
-
return
|
|
1450
|
+
return false;
|
|
1496
1451
|
}
|
|
1497
1452
|
|
|
1498
1453
|
toOperationString(
|
|
@@ -1528,6 +1483,10 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1528
1483
|
includeExternalBrackets: boolean = true,
|
|
1529
1484
|
indent?: string
|
|
1530
1485
|
): string {
|
|
1486
|
+
if (this.isEmpty()) {
|
|
1487
|
+
return '{}';
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1531
1490
|
if (indent === undefined) {
|
|
1532
1491
|
const selectionsToString = this.selections().map(s => s.toString(expandFragments)).join(' ');
|
|
1533
1492
|
return includeExternalBrackets ? '{ ' + selectionsToString + ' }' : selectionsToString;
|
|
@@ -1541,13 +1500,294 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1541
1500
|
}
|
|
1542
1501
|
}
|
|
1543
1502
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1503
|
+
type PathBasedUpdate = { path: OperationPath, selections?: Selection | SelectionSet | readonly Selection[] };
|
|
1504
|
+
type SelectionUpdate = Selection | PathBasedUpdate;
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* Accumulates updates in order to build a new `SelectionSet`.
|
|
1508
|
+
*/
|
|
1509
|
+
export class SelectionSetUpdates {
|
|
1510
|
+
private readonly keyedUpdates = new MultiMap<string, SelectionUpdate>;
|
|
1511
|
+
|
|
1512
|
+
isEmpty(): boolean {
|
|
1513
|
+
return this.keyedUpdates.size === 0;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Adds the provided selections to those updates.
|
|
1518
|
+
*/
|
|
1519
|
+
add(selections: Selection | SelectionSet | readonly Selection[]): SelectionSetUpdates {
|
|
1520
|
+
addToKeyedUpdates(this.keyedUpdates, selections);
|
|
1521
|
+
return this;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Adds a path, and optional some selections following that path, to those updates.
|
|
1526
|
+
*
|
|
1527
|
+
* The final selections are optional (for instance, if `path` ends on a leaf field, then no followup selections would
|
|
1528
|
+
* make sense), but when some are provided, uncesssary fragments will be automaticaly removed at the junction between
|
|
1529
|
+
* the path and those final selections. For instance, suppose that we have:
|
|
1530
|
+
* - a `path` argument that is `a::b::c`, where the type of the last field `c` is some object type `C`.
|
|
1531
|
+
* - a `selections` argument that is `{ ... on C { d } }`.
|
|
1532
|
+
* Then the resulting built selection set will be: `{ a { b { c { d } } }`, and in particular the `... on C` fragment
|
|
1533
|
+
* will be eliminated since it is unecesasry (since again, `c` is of type `C`).
|
|
1534
|
+
*/
|
|
1535
|
+
addAtPath(path: OperationPath, selections?: Selection | SelectionSet | readonly Selection[]): SelectionSetUpdates {
|
|
1536
|
+
if (path.length === 0) {
|
|
1537
|
+
if (selections) {
|
|
1538
|
+
addToKeyedUpdates(this.keyedUpdates, selections)
|
|
1539
|
+
}
|
|
1540
|
+
} else {
|
|
1541
|
+
if (path.length === 1 && !selections) {
|
|
1542
|
+
const element = path[0];
|
|
1543
|
+
if (element.kind === 'Field' && element.isLeafField()) {
|
|
1544
|
+
// This is a somewhat common case (when we deal with @key "conditions", those are often trivial and end up here),
|
|
1545
|
+
// so we unpack it directly instead of creating unecessary temporary objects (not that we only do it for leaf
|
|
1546
|
+
// field; for non-leaf ones, we'd have to create an empty sub-selectionSet, and that may have to get merged
|
|
1547
|
+
// with other entries of this `SleectionSetUpdates`, so we wouldn't really save work).
|
|
1548
|
+
const selection = selectionOfElement(element);
|
|
1549
|
+
this.keyedUpdates.add(selection.key(), selection);
|
|
1550
|
+
return this;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
// We store the provided update "as is" (we don't convert it to a `Selection` just yet) and process everything
|
|
1554
|
+
// when we build the final `SelectionSet`. This is done because multipe different updates can intersect in various
|
|
1555
|
+
// ways, and the work to build a `Selection` now could be largely wasted due to followup updates.
|
|
1556
|
+
this.keyedUpdates.add(path[0].key(), { path, selections });
|
|
1557
|
+
}
|
|
1558
|
+
return this;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
clone(): SelectionSetUpdates {
|
|
1562
|
+
const cloned = new SelectionSetUpdates();
|
|
1563
|
+
for (const [key, values] of this.keyedUpdates.entries()) {
|
|
1564
|
+
cloned.keyedUpdates.set(key, Array.from(values));
|
|
1565
|
+
}
|
|
1566
|
+
return cloned;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
clear() {
|
|
1570
|
+
this.keyedUpdates.clear();
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
toSelectionSet(parentType: CompositeType, fragments?: NamedFragments): SelectionSet {
|
|
1574
|
+
return makeSelectionSet(parentType, this.keyedUpdates, fragments);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function addToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, selections: Selection | SelectionSet | readonly Selection[]) {
|
|
1579
|
+
if (selections instanceof AbstractSelection) {
|
|
1580
|
+
addOneToKeyedUpdates(keyedUpdates, selections);
|
|
1581
|
+
} else {
|
|
1582
|
+
const toAdd = selections instanceof SelectionSet ? selections.selections() : selections;
|
|
1583
|
+
for (const selection of toAdd) {
|
|
1584
|
+
addOneToKeyedUpdates(keyedUpdates, selection);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function addOneToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, selection: Selection) {
|
|
1590
|
+
// Keys are such that for a named fragment, only a selection of the same fragment with same directives can have the same key.
|
|
1591
|
+
// But if we end up with multiple spread of the same named fragment, we don't want to try to "merge" the sub-selections of
|
|
1592
|
+
// each, as it would expand the fragments and make things harder. So we essentially special case spreads to avoid having
|
|
1593
|
+
// to deal with multiple time the exact same one.
|
|
1594
|
+
if (selection instanceof FragmentSpreadSelection) {
|
|
1595
|
+
keyedUpdates.set(selection.key(), [selection]);
|
|
1596
|
+
} else {
|
|
1597
|
+
keyedUpdates.add(selection.key(), selection);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function isUnecessaryFragment(parentType: CompositeType, fragment: FragmentSelection): boolean {
|
|
1602
|
+
return fragment.element.appliedDirectives.length === 0
|
|
1603
|
+
&& (!fragment.element.typeCondition || sameType(parentType, fragment.element.typeCondition));
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function withUnecessaryFragmentsRemoved(
|
|
1607
|
+
parentType: CompositeType,
|
|
1608
|
+
selections: Selection | SelectionSet | readonly Selection[],
|
|
1609
|
+
): Selection | readonly Selection[] {
|
|
1610
|
+
if (selections instanceof AbstractSelection) {
|
|
1611
|
+
if (selections.kind !== 'FragmentSelection' || !isUnecessaryFragment(parentType, selections)) {
|
|
1612
|
+
return selections;
|
|
1613
|
+
}
|
|
1614
|
+
return withUnecessaryFragmentsRemoved(parentType, selections.selectionSet);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
const toCheck = selections instanceof SelectionSet ? selections.selections() : selections;
|
|
1618
|
+
const filtered: Selection[] = [];
|
|
1619
|
+
for (const selection of toCheck) {
|
|
1620
|
+
if (selection.kind === 'FragmentSelection' && isUnecessaryFragment(parentType, selection)) {
|
|
1621
|
+
const subSelections = withUnecessaryFragmentsRemoved(parentType, selection.selectionSet);
|
|
1622
|
+
if (subSelections instanceof AbstractSelection) {
|
|
1623
|
+
filtered.push(subSelections);
|
|
1624
|
+
} else {
|
|
1625
|
+
for (const subSelection of subSelections) {
|
|
1626
|
+
filtered.push(subSelection);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
} else {
|
|
1630
|
+
filtered.push(selection);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
return filtered;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function makeSelection(parentType: CompositeType, updates: SelectionUpdate[], fragments?: NamedFragments): Selection {
|
|
1637
|
+
assert(updates.length > 0, 'Should not be called without any updates');
|
|
1638
|
+
const first = updates[0];
|
|
1639
|
+
|
|
1640
|
+
// Optimize for the simple case of a single selection, as we don't have to do anything complex to merge the sub-selections.
|
|
1641
|
+
if (updates.length === 1 && first instanceof AbstractSelection) {
|
|
1642
|
+
return first.rebaseOn(parentType);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const element = updateElement(first).rebaseOn(parentType);
|
|
1646
|
+
const subSelectionParentType = element.kind === 'Field' ? baseType(element.definition.type!) : element.castedType();
|
|
1647
|
+
if (!isCompositeType(subSelectionParentType)) {
|
|
1648
|
+
// This is a leaf, so all updates should correspond ot the same field and we just use the first.
|
|
1649
|
+
return selectionOfElement(element);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const subSelectionKeyedUpdates = new MultiMap<string, SelectionUpdate>();
|
|
1653
|
+
for (const update of updates) {
|
|
1654
|
+
if (update instanceof AbstractSelection) {
|
|
1655
|
+
if (update.selectionSet) {
|
|
1656
|
+
addToKeyedUpdates(subSelectionKeyedUpdates, update.selectionSet);
|
|
1657
|
+
}
|
|
1658
|
+
} else {
|
|
1659
|
+
addSubpathToKeyUpdates(subSelectionKeyedUpdates, subSelectionParentType, update);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
return selectionOfElement(element, makeSelectionSet(subSelectionParentType, subSelectionKeyedUpdates, fragments));
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function updateElement(update: SelectionUpdate): OperationElement {
|
|
1666
|
+
return update instanceof AbstractSelection ? update.element : update.path[0];
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function addSubpathToKeyUpdates(
|
|
1670
|
+
keyedUpdates: MultiMap<string, SelectionUpdate>,
|
|
1671
|
+
subSelectionParentType: CompositeType,
|
|
1672
|
+
pathUpdate: PathBasedUpdate
|
|
1673
|
+
) {
|
|
1674
|
+
if (pathUpdate.path.length === 1) {
|
|
1675
|
+
if (!pathUpdate.selections) {
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
addToKeyedUpdates(keyedUpdates, withUnecessaryFragmentsRemoved(subSelectionParentType, pathUpdate.selections!));
|
|
1679
|
+
} else {
|
|
1680
|
+
keyedUpdates.add(pathUpdate.path[1].key(), { path: pathUpdate.path.slice(1), selections: pathUpdate.selections });
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function makeSelectionSet(parentType: CompositeType, keyedUpdates: MultiMap<string, SelectionUpdate>, fragments?: NamedFragments): SelectionSet {
|
|
1685
|
+
const selections = new Map<string, Selection>();
|
|
1686
|
+
for (const [key, updates] of keyedUpdates.entries()) {
|
|
1687
|
+
selections.set(key, makeSelection(parentType, updates, fragments));
|
|
1688
|
+
}
|
|
1689
|
+
return new SelectionSet(parentType, selections, fragments);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* A simple wrapper over a `SelectionSetUpdates` that allows to conveniently build a selection set, then add some more updates and build it again, etc...
|
|
1694
|
+
*/
|
|
1695
|
+
export class MutableSelectionSet<TMemoizedValue extends { [key: string]: any } = {}> {
|
|
1696
|
+
private computed: SelectionSet | undefined;
|
|
1697
|
+
private _memoized: TMemoizedValue | undefined;
|
|
1698
|
+
|
|
1699
|
+
private constructor(
|
|
1700
|
+
readonly parentType: CompositeType,
|
|
1701
|
+
private readonly _updates: SelectionSetUpdates,
|
|
1702
|
+
private readonly memoizer: (s: SelectionSet) => TMemoizedValue,
|
|
1703
|
+
) {
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
static empty(parentType: CompositeType): MutableSelectionSet {
|
|
1707
|
+
return this.emptyWithMemoized(parentType, () => ({}));
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
static emptyWithMemoized<TMemoizedValue extends { [key: string]: any }>(
|
|
1711
|
+
parentType: CompositeType,
|
|
1712
|
+
memoizer: (s: SelectionSet) => TMemoizedValue,
|
|
1713
|
+
): MutableSelectionSet<TMemoizedValue> {
|
|
1714
|
+
return new MutableSelectionSet( parentType, new SelectionSetUpdates(), memoizer);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
static of(selectionSet: SelectionSet): MutableSelectionSet {
|
|
1719
|
+
return this.ofWithMemoized(selectionSet, () => ({}));
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
static ofWithMemoized<TMemoizedValue extends { [key: string]: any }>(
|
|
1723
|
+
selectionSet: SelectionSet,
|
|
1724
|
+
memoizer: (s: SelectionSet) => TMemoizedValue,
|
|
1725
|
+
): MutableSelectionSet<TMemoizedValue> {
|
|
1726
|
+
const s = new MutableSelectionSet(selectionSet.parentType, new SelectionSetUpdates(), memoizer);
|
|
1727
|
+
s._updates.add(selectionSet);
|
|
1728
|
+
// Avoids needing to re-compute `selectionSet` until there is new updates.
|
|
1729
|
+
s.computed = selectionSet;
|
|
1730
|
+
return s;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
isEmpty(): boolean {
|
|
1734
|
+
return this._updates.isEmpty();
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
get(): SelectionSet {
|
|
1738
|
+
if (!this.computed) {
|
|
1739
|
+
this.computed = this._updates.toSelectionSet(this.parentType);
|
|
1740
|
+
// But now, we clear the updates an re-add the selections from computed. Of course, we could also
|
|
1741
|
+
// not clear updates at all, but that would mean that the computations going on for merging selections
|
|
1742
|
+
// would be re-done every time and that would be a lot less efficient.
|
|
1743
|
+
this._updates.clear();
|
|
1744
|
+
this._updates.add(this.computed);
|
|
1745
|
+
}
|
|
1746
|
+
return this.computed;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
updates(): SelectionSetUpdates {
|
|
1750
|
+
// We clear our cached version since we're about to add more updates and so this cached version won't
|
|
1751
|
+
// represent the mutable set properly anymore.
|
|
1752
|
+
this.computed = undefined;
|
|
1753
|
+
this._memoized = undefined;
|
|
1754
|
+
return this._updates;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
clone(): MutableSelectionSet<TMemoizedValue> {
|
|
1758
|
+
const cloned = new MutableSelectionSet(this.parentType, this._updates.clone(), this.memoizer);
|
|
1759
|
+
// Until we have more updates, we can share the computed values (if any).
|
|
1760
|
+
cloned.computed = this.computed;
|
|
1761
|
+
cloned._memoized = this._memoized;
|
|
1762
|
+
return cloned;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
rebaseOn(parentType: CompositeType): MutableSelectionSet<TMemoizedValue> {
|
|
1766
|
+
const rebased = new MutableSelectionSet(parentType, new SelectionSetUpdates(), this.memoizer);
|
|
1767
|
+
// Note that updates are always rebased on their parentType, so we won't have to call `rebaseOn` manually on `this.get()`.
|
|
1768
|
+
rebased._updates.add(this.get());
|
|
1769
|
+
return rebased;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
memoized(): TMemoizedValue {
|
|
1773
|
+
if (!this._memoized) {
|
|
1774
|
+
this._memoized = this.memoizer(this.get());
|
|
1775
|
+
}
|
|
1776
|
+
return this._memoized;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
toString() {
|
|
1780
|
+
return this.get().toString();
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
export function allFieldDefinitionsInSelectionSet(selection: SelectionSet): FieldDefinition<CompositeType>[] {
|
|
1785
|
+
const stack = Array.from(selection.selections());
|
|
1786
|
+
const allFields: FieldDefinition<CompositeType>[] = [];
|
|
1787
|
+
while (stack.length > 0) {
|
|
1788
|
+
const selection = stack.pop()!;
|
|
1549
1789
|
if (selection.kind === 'FieldSelection') {
|
|
1550
|
-
allFields.push(selection.
|
|
1790
|
+
allFields.push(selection.element.definition);
|
|
1551
1791
|
}
|
|
1552
1792
|
if (selection.selectionSet) {
|
|
1553
1793
|
stack.push(...selection.selectionSet.selections());
|
|
@@ -1556,99 +1796,229 @@ export function allFieldDefinitionsInSelectionSet(selection: SelectionSet): Fiel
|
|
|
1556
1796
|
return allFields;
|
|
1557
1797
|
}
|
|
1558
1798
|
|
|
1559
|
-
export function
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
return
|
|
1799
|
+
export function selectionSetOf(parentType: CompositeType, selection: Selection, fragments?: NamedFragments): SelectionSet {
|
|
1800
|
+
const map = new Map<string, Selection>()
|
|
1801
|
+
map.set(selection.key(), selection);
|
|
1802
|
+
return new SelectionSet(parentType, map, fragments);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
export function selectionSetOfElement(element: OperationElement, subSelection?: SelectionSet, fragments?: NamedFragments): SelectionSet {
|
|
1806
|
+
return selectionSetOf(element.parentType, selectionOfElement(element, subSelection), fragments);
|
|
1563
1807
|
}
|
|
1564
1808
|
|
|
1565
1809
|
export function selectionOfElement(element: OperationElement, subSelection?: SelectionSet): Selection {
|
|
1566
|
-
|
|
1810
|
+
// TODO: validate that the subSelection is ok for the element
|
|
1811
|
+
return element.kind === 'Field' ? new FieldSelection(element, subSelection) : new InlineFragmentSelection(element, subSelection!);
|
|
1567
1812
|
}
|
|
1568
1813
|
|
|
1569
1814
|
export type Selection = FieldSelection | FragmentSelection;
|
|
1570
|
-
|
|
1571
|
-
export class FieldSelection extends Freezable<FieldSelection> {
|
|
1572
|
-
readonly kind = 'FieldSelection' as const;
|
|
1573
|
-
readonly selectionSet?: SelectionSet;
|
|
1574
|
-
|
|
1815
|
+
abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf extends undefined | never, TOwnType extends AbstractSelection<TElement, TIsLeaf, TOwnType>> {
|
|
1575
1816
|
constructor(
|
|
1576
|
-
readonly
|
|
1577
|
-
initialSelectionSet? : SelectionSet
|
|
1817
|
+
readonly element: TElement,
|
|
1578
1818
|
) {
|
|
1579
|
-
|
|
1580
|
-
const type = baseType(field.definition.type!);
|
|
1581
|
-
// Field types are output type, and a named typethat is an output one and isn't a leaf is guaranteed to be selectable.
|
|
1582
|
-
this.selectionSet = isLeafType(type) ? undefined : (initialSelectionSet ? initialSelectionSet.cloneIfFrozen() : new SelectionSet(type as CompositeType));
|
|
1819
|
+
// TODO: we should do validate the type of the selection set matches the element.
|
|
1583
1820
|
}
|
|
1584
1821
|
|
|
1822
|
+
abstract get selectionSet(): SelectionSet | TIsLeaf;
|
|
1823
|
+
|
|
1824
|
+
protected abstract us(): TOwnType;
|
|
1825
|
+
|
|
1826
|
+
abstract key(): string;
|
|
1827
|
+
|
|
1828
|
+
abstract optimize(fragments: NamedFragments): Selection;
|
|
1829
|
+
|
|
1830
|
+
abstract toSelectionNode(): SelectionNode;
|
|
1831
|
+
|
|
1832
|
+
abstract validate(variableDefinitions: VariableDefinitions): void;
|
|
1833
|
+
|
|
1834
|
+
abstract rebaseOn(parentType: CompositeType): TOwnType;
|
|
1835
|
+
|
|
1585
1836
|
get parentType(): CompositeType {
|
|
1586
|
-
return this.
|
|
1837
|
+
return this.element.parentType;
|
|
1587
1838
|
}
|
|
1588
1839
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1840
|
+
collectVariables(collector: VariableCollector) {
|
|
1841
|
+
this.element.collectVariables(collector);
|
|
1842
|
+
this.selectionSet?.collectVariables(collector)
|
|
1591
1843
|
}
|
|
1592
1844
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1845
|
+
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
1846
|
+
this.selectionSet?.collectUsedFragmentNames(collector);
|
|
1595
1847
|
}
|
|
1596
1848
|
|
|
1597
|
-
|
|
1598
|
-
return this.
|
|
1849
|
+
namedFragments(): NamedFragments | undefined {
|
|
1850
|
+
return this.selectionSet?.fragments;
|
|
1599
1851
|
}
|
|
1600
1852
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1853
|
+
abstract withUpdatedComponents(element: TElement, selectionSet: SelectionSet | TIsLeaf): TOwnType;
|
|
1854
|
+
|
|
1855
|
+
withUpdatedSelectionSet(selectionSet: SelectionSet | TIsLeaf): TOwnType {
|
|
1856
|
+
return this.withUpdatedComponents(this.element, selectionSet);
|
|
1603
1857
|
}
|
|
1604
1858
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1859
|
+
withUpdatedElement(element: TElement): TOwnType {
|
|
1860
|
+
return this.withUpdatedComponents(element, this.selectionSet);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
mapToSelectionSet(mapper: (s: SelectionSet) => SelectionSet): TOwnType {
|
|
1864
|
+
if (!this.selectionSet) {
|
|
1865
|
+
return this.us();
|
|
1608
1866
|
}
|
|
1867
|
+
|
|
1868
|
+
const updatedSelectionSet = mapper(this.selectionSet);
|
|
1869
|
+
return updatedSelectionSet === this.selectionSet
|
|
1870
|
+
? this.us()
|
|
1871
|
+
: this.withUpdatedSelectionSet(updatedSelectionSet);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
abstract withoutDefer(labelsToRemove?: Set<string>): TOwnType | SelectionSet;
|
|
1875
|
+
|
|
1876
|
+
abstract withNormalizedDefer(normalizer: DeferNormalizer): TOwnType | SelectionSet;
|
|
1877
|
+
|
|
1878
|
+
abstract hasDefer(): boolean;
|
|
1879
|
+
|
|
1880
|
+
abstract expandAllFragments(): TOwnType | readonly Selection[];
|
|
1881
|
+
|
|
1882
|
+
abstract expandFragments(names: string[], updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
|
|
1883
|
+
|
|
1884
|
+
abstract trimUnsatisfiableBranches(parentType: CompositeType): TOwnType | SelectionSet | undefined;
|
|
1885
|
+
|
|
1886
|
+
minus(that: Selection): TOwnType | undefined {
|
|
1887
|
+
// If there is a subset, then we compute the diff of the subset and add that (if not empty).
|
|
1888
|
+
// Otherwise, we have no diff.
|
|
1889
|
+
if (this.selectionSet && that.selectionSet) {
|
|
1890
|
+
const updatedSubSelectionSet = this.selectionSet.minus(that.selectionSet);
|
|
1891
|
+
if (!updatedSubSelectionSet.isEmpty()) {
|
|
1892
|
+
return this.withUpdatedSelectionSet(updatedSubSelectionSet);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return undefined;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
protected tryOptimizeSubselectionOnce(_: {
|
|
1899
|
+
parentType: CompositeType,
|
|
1900
|
+
subSelection: SelectionSet,
|
|
1901
|
+
candidates: NamedFragmentDefinition[],
|
|
1902
|
+
fragments: NamedFragments,
|
|
1903
|
+
}): {
|
|
1904
|
+
spread?: FragmentSpreadSelection,
|
|
1905
|
+
optimizedSelection?: SelectionSet,
|
|
1906
|
+
hasDiff?: boolean,
|
|
1907
|
+
} {
|
|
1908
|
+
// Field and inline fragment override this, but this should never be called for a spread.
|
|
1909
|
+
assert(false, `UNSUPPORTED`);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
protected tryOptimizeSubselectionWithFragments({
|
|
1913
|
+
parentType,
|
|
1914
|
+
subSelection,
|
|
1915
|
+
fragments,
|
|
1916
|
+
fragmentFilter,
|
|
1917
|
+
}: {
|
|
1918
|
+
parentType: CompositeType,
|
|
1919
|
+
subSelection: SelectionSet,
|
|
1920
|
+
fragments: NamedFragments,
|
|
1921
|
+
fragmentFilter?: (f: NamedFragmentDefinition) => boolean,
|
|
1922
|
+
}): SelectionSet | FragmentSpreadSelection {
|
|
1923
|
+
let candidates = fragments.maybeApplyingAtType(parentType);
|
|
1924
|
+
if (fragmentFilter) {
|
|
1925
|
+
candidates = candidates.filter(fragmentFilter);
|
|
1926
|
+
}
|
|
1927
|
+
let shouldTryAgain: boolean;
|
|
1928
|
+
do {
|
|
1929
|
+
const { spread, optimizedSelection, hasDiff } = this.tryOptimizeSubselectionOnce({ parentType, subSelection, candidates, fragments });
|
|
1930
|
+
if (optimizedSelection) {
|
|
1931
|
+
subSelection = optimizedSelection;
|
|
1932
|
+
} else if (spread) {
|
|
1933
|
+
return spread;
|
|
1934
|
+
}
|
|
1935
|
+
shouldTryAgain = !!spread && !!hasDiff;
|
|
1936
|
+
if (shouldTryAgain) {
|
|
1937
|
+
candidates = candidates.filter((c) => c !== spread?.namedFragment)
|
|
1938
|
+
}
|
|
1939
|
+
} while (shouldTryAgain);
|
|
1940
|
+
return subSelection;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
export class FieldSelection extends AbstractSelection<Field<any>, undefined, FieldSelection> {
|
|
1945
|
+
readonly kind = 'FieldSelection' as const;
|
|
1946
|
+
|
|
1947
|
+
constructor(
|
|
1948
|
+
field: Field<any>,
|
|
1949
|
+
private readonly _selectionSet?: SelectionSet,
|
|
1950
|
+
) {
|
|
1951
|
+
super(field);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
get selectionSet(): SelectionSet | undefined {
|
|
1955
|
+
return this._selectionSet;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
protected us(): FieldSelection {
|
|
1959
|
+
return this;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
|
|
1963
|
+
return new FieldSelection(field, selectionSet);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
key(): string {
|
|
1967
|
+
return this.element.key();
|
|
1609
1968
|
}
|
|
1610
1969
|
|
|
1611
1970
|
optimize(fragments: NamedFragments): Selection {
|
|
1612
|
-
|
|
1613
|
-
const fieldBaseType = baseType(this.
|
|
1971
|
+
let optimizedSelection = this.selectionSet ? this.selectionSet.optimize(fragments) : undefined;
|
|
1972
|
+
const fieldBaseType = baseType(this.element.definition.type!);
|
|
1614
1973
|
if (isCompositeType(fieldBaseType) && optimizedSelection) {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
//
|
|
1620
|
-
//
|
|
1621
|
-
//
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
// t {
|
|
1628
|
-
// a
|
|
1629
|
-
// b
|
|
1630
|
-
// }
|
|
1631
|
-
// }
|
|
1632
|
-
// then the current code will not use the fragment because `c` is not in the fragment, but in relatity,
|
|
1633
|
-
// we could use it and make the result be:
|
|
1634
|
-
// {
|
|
1635
|
-
// ...X
|
|
1636
|
-
// t {
|
|
1637
|
-
// c
|
|
1638
|
-
// }
|
|
1639
|
-
// }
|
|
1640
|
-
// To do that, we can change that `equals` to `contains`, but then we should also "extract" the remainder
|
|
1641
|
-
// of `optimizedSelection` that isn't covered by the fragment, and that is the part slighly more involved.
|
|
1642
|
-
if (optimizedSelection.equals(candidate.selectionSet)) {
|
|
1643
|
-
const fragmentSelection = new FragmentSpreadSelection(fieldBaseType, fragments, candidate.name);
|
|
1644
|
-
return new FieldSelection(this.field, selectionSetOf(fieldBaseType, fragmentSelection));
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1974
|
+
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
1975
|
+
parentType: fieldBaseType,
|
|
1976
|
+
subSelection: optimizedSelection,
|
|
1977
|
+
fragments,
|
|
1978
|
+
// We can never apply a fragments that has directives on it at the field level (but when those are expanded,
|
|
1979
|
+
// their type condition would always be preserved due to said applied directives, so they will always
|
|
1980
|
+
// be handled by `InlineFragmentSelection.optimize` anyway).
|
|
1981
|
+
fragmentFilter: (f) => f.appliedDirectives.length === 0,
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
assert(!(optimized instanceof FragmentSpreadSelection), 'tryOptimizeSubselectionOnce should never return only a spread');
|
|
1985
|
+
optimizedSelection = optimized;
|
|
1647
1986
|
}
|
|
1648
1987
|
|
|
1649
1988
|
return this.selectionSet === optimizedSelection
|
|
1650
1989
|
? this
|
|
1651
|
-
: new FieldSelection(this.
|
|
1990
|
+
: new FieldSelection(this.element, optimizedSelection);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
protected tryOptimizeSubselectionOnce({
|
|
1994
|
+
parentType,
|
|
1995
|
+
subSelection,
|
|
1996
|
+
candidates,
|
|
1997
|
+
fragments,
|
|
1998
|
+
}: {
|
|
1999
|
+
parentType: CompositeType,
|
|
2000
|
+
subSelection: SelectionSet,
|
|
2001
|
+
candidates: NamedFragmentDefinition[],
|
|
2002
|
+
fragments: NamedFragments,
|
|
2003
|
+
}): {
|
|
2004
|
+
spread?: FragmentSpreadSelection,
|
|
2005
|
+
optimizedSelection?: SelectionSet,
|
|
2006
|
+
hasDiff?: boolean,
|
|
2007
|
+
}{
|
|
2008
|
+
let optimizedSelection = subSelection;
|
|
2009
|
+
for (const candidate of candidates) {
|
|
2010
|
+
const { contains, diff } = optimizedSelection.diffIfContains(candidate.selectionSet);
|
|
2011
|
+
if (contains) {
|
|
2012
|
+
// We can optimize the selection with this fragment. The replaced sub-selection will be
|
|
2013
|
+
// comprised of this new spread and the remaining `diff` if there is any.
|
|
2014
|
+
const spread = new FragmentSpreadSelection(parentType, fragments, candidate, []);
|
|
2015
|
+
optimizedSelection = diff
|
|
2016
|
+
? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
|
|
2017
|
+
: selectionSetOf(parentType, spread);
|
|
2018
|
+
return { spread, optimizedSelection, hasDiff: !!diff }
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
return {};
|
|
1652
2022
|
}
|
|
1653
2023
|
|
|
1654
2024
|
filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
|
|
@@ -1659,143 +2029,127 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1659
2029
|
const updatedSelectionSet = this.selectionSet.filter(predicate);
|
|
1660
2030
|
const thisWithFilteredSelectionSet = this.selectionSet === updatedSelectionSet
|
|
1661
2031
|
? this
|
|
1662
|
-
: new FieldSelection(this.
|
|
2032
|
+
: new FieldSelection(this.element, updatedSelectionSet);
|
|
1663
2033
|
return predicate(thisWithFilteredSelectionSet) ? thisWithFilteredSelectionSet : undefined;
|
|
1664
2034
|
}
|
|
1665
2035
|
|
|
1666
|
-
|
|
1667
|
-
this.
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
expandFragments(names?: string[], updateSelectionSetFragments: boolean = true): FieldSelection {
|
|
1671
|
-
const expandedSelection = this.selectionSet ? this.selectionSet.expandFragments(names, updateSelectionSetFragments) : undefined;
|
|
1672
|
-
return this.selectionSet === expandedSelection
|
|
1673
|
-
? this
|
|
1674
|
-
: new FieldSelection(this.field, expandedSelection);
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
private fieldArgumentsToAST(): ArgumentNode[] | undefined {
|
|
1678
|
-
const entries = Object.entries(this.field.args);
|
|
1679
|
-
if (entries.length === 0) {
|
|
1680
|
-
return undefined;
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
return entries.map(([n, v]) => {
|
|
1684
|
-
return {
|
|
1685
|
-
kind: Kind.ARGUMENT,
|
|
1686
|
-
name: { kind: Kind.NAME, value: n },
|
|
1687
|
-
value: valueToAST(v, this.field.definition.argument(n)!.type!)!,
|
|
1688
|
-
};
|
|
1689
|
-
});
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
validate() {
|
|
1693
|
-
this.field.validate();
|
|
2036
|
+
validate(variableDefinitions: VariableDefinitions) {
|
|
2037
|
+
this.element.validate(variableDefinitions);
|
|
1694
2038
|
// Note that validation is kind of redundant since `this.selectionSet.validate()` will check that it isn't empty. But doing it
|
|
1695
2039
|
// allow to provide much better error messages.
|
|
1696
2040
|
validate(
|
|
1697
|
-
|
|
1698
|
-
() => `Invalid empty selection set for field "${this.
|
|
1699
|
-
this.
|
|
2041
|
+
this.element.isLeafField() || (this.selectionSet && !this.selectionSet.isEmpty()),
|
|
2042
|
+
() => `Invalid empty selection set for field "${this.element.definition.coordinate}" of non-leaf type ${this.element.definition.type}`,
|
|
2043
|
+
this.element.definition.sourceAST
|
|
1700
2044
|
);
|
|
1701
|
-
this.selectionSet?.validate();
|
|
2045
|
+
this.selectionSet?.validate(variableDefinitions);
|
|
1702
2046
|
}
|
|
1703
2047
|
|
|
1704
2048
|
/**
|
|
1705
|
-
* Returns a field selection "equivalent" to the one represented by this object, but such that
|
|
1706
|
-
*
|
|
1707
|
-
* 2. it is not frozen (which might involve cloning).
|
|
2049
|
+
* Returns a field selection "equivalent" to the one represented by this object, but such that its parent type
|
|
2050
|
+
* is the one provided as argument.
|
|
1708
2051
|
*
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1711
|
-
* If that is not the case, an assertion will be thrown.
|
|
1712
|
-
*
|
|
1713
|
-
* Note that in the simple cases where this selection parent type is already the one of the provide
|
|
1714
|
-
* `selectionSet`, then this method is mostly a no-op, except for the potential cloning if this selection
|
|
1715
|
-
* is frozen. But this method mostly exists to make working with multiple "similar" schema easier.
|
|
1716
|
-
* That is, `Selection` and `SelectionSet` are intrinsically linked to a particular `Schema` object since
|
|
1717
|
-
* their underlying `OperationElement` points to fields and types of a particular `Schema`. And we want to
|
|
1718
|
-
* make sure that _everything_ within a particular `SelectionSet` does link to the same `Schema` object,
|
|
1719
|
-
* or things could get really confusing (nor would it make much sense; a selection set is that of a particular
|
|
1720
|
-
* schema fundamentally). In many cases, when we work with a single schema (when we parse an operation string
|
|
1721
|
-
* against a given schema for instance), this problem is moot, but as we do query planning for instance, we
|
|
1722
|
-
* end up building queries over subgraphs _based_ on some selections from the supergraph API schema, and so
|
|
1723
|
-
* we need to deal with the fact that the code can easily mix selection from different schema. One option
|
|
1724
|
-
* could be to simply hard-reject such mixing, meaning that `SelectionSet.add(Selection)` could error out
|
|
1725
|
-
* if the provided selection is not of the same schema of that of the selection set we add to, thus forcing
|
|
1726
|
-
* the caller to first ensure the selection is properly "rebased" on the same schema. But this would be a
|
|
1727
|
-
* bit inconvenient and so this this method instead provide a sort of "automatic rebasing": that is, it
|
|
1728
|
-
* allows `this` selection not be of the same schema as the provided `selectionSet` as long as both are
|
|
1729
|
-
* "compatible", and as long as it's the case, it return an equivalent selection that is suitable to be
|
|
1730
|
-
* added to `selectionSet` (it's against the same fundamental schema).
|
|
2052
|
+
* Obviously, this operation will only succeed if this selection (both the field itself and its subselections)
|
|
2053
|
+
* make sense from the provided parent type. If this is not the case, this method will throw.
|
|
1731
2054
|
*/
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
// Note that re-adding every selection ensures that anything frozen will be cloned as needed, on top of handling any knock-down
|
|
1746
|
-
// effect of the type change.
|
|
1747
|
-
for (const selection of this.selectionSet.selections()) {
|
|
1748
|
-
updatedSelectionSet.add(selection);
|
|
1749
|
-
}
|
|
1750
|
-
} else {
|
|
1751
|
-
updatedSelectionSet = this.selectionSet?.cloneIfFrozen();
|
|
2055
|
+
rebaseOn(parentType: CompositeType): FieldSelection {
|
|
2056
|
+
if (this.element.parentType === parentType) {
|
|
2057
|
+
return this;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
const rebasedElement = this.element.rebaseOn(parentType);
|
|
2061
|
+
if (!this.selectionSet) {
|
|
2062
|
+
return this.withUpdatedElement(rebasedElement);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
const rebasedBase = baseType(rebasedElement.definition.type!);
|
|
2066
|
+
if (rebasedBase === this.selectionSet.parentType) {
|
|
2067
|
+
return this.withUpdatedElement(rebasedElement);
|
|
1752
2068
|
}
|
|
1753
2069
|
|
|
1754
|
-
return
|
|
2070
|
+
validate(isCompositeType(rebasedBase), () => `Cannot rebase field selection ${this} on ${parentType}: rebased field base return type ${rebasedBase} is not composite`);
|
|
2071
|
+
return this.withUpdatedComponents(rebasedElement, this.selectionSet.rebaseOn(rebasedBase));
|
|
1755
2072
|
}
|
|
1756
2073
|
|
|
1757
2074
|
/**
|
|
1758
2075
|
* Essentially checks if `updateForAddingTo` would work on an selecion set of the provide parent type.
|
|
1759
2076
|
*/
|
|
1760
2077
|
canAddTo(parentType: CompositeType): boolean {
|
|
1761
|
-
if (this.
|
|
2078
|
+
if (this.element.parentType === parentType) {
|
|
1762
2079
|
return true;
|
|
1763
2080
|
}
|
|
1764
2081
|
|
|
1765
|
-
const type = this.
|
|
2082
|
+
const type = this.element.typeIfAddedTo(parentType);
|
|
1766
2083
|
if (!type) {
|
|
1767
2084
|
return false;
|
|
1768
2085
|
}
|
|
1769
2086
|
|
|
1770
2087
|
const base = baseType(type);
|
|
1771
2088
|
if (this.selectionSet && this.selectionSet.parentType !== base) {
|
|
1772
|
-
assert(isCompositeType(base), () => `${this.
|
|
2089
|
+
assert(isCompositeType(base), () => `${this.element} should have a selection set as it's type is not a composite`);
|
|
1773
2090
|
return this.selectionSet.selections().every((s) => s.canAddTo(base));
|
|
1774
2091
|
}
|
|
1775
2092
|
return true;
|
|
1776
2093
|
}
|
|
1777
2094
|
|
|
1778
2095
|
toSelectionNode(): FieldNode {
|
|
1779
|
-
const alias: NameNode | undefined = this.
|
|
2096
|
+
const alias: NameNode | undefined = this.element.alias ? { kind: Kind.NAME, value: this.element.alias, } : undefined;
|
|
1780
2097
|
return {
|
|
1781
2098
|
kind: Kind.FIELD,
|
|
1782
2099
|
name: {
|
|
1783
2100
|
kind: Kind.NAME,
|
|
1784
|
-
value: this.
|
|
2101
|
+
value: this.element.name,
|
|
1785
2102
|
},
|
|
1786
2103
|
alias,
|
|
1787
|
-
arguments: this.
|
|
1788
|
-
directives: this.element
|
|
2104
|
+
arguments: this.element.argumentsToNodes(),
|
|
2105
|
+
directives: this.element.appliedDirectivesToDirectiveNodes(),
|
|
1789
2106
|
selectionSet: this.selectionSet?.toSelectionSetNode()
|
|
1790
2107
|
};
|
|
1791
2108
|
}
|
|
1792
2109
|
|
|
1793
|
-
|
|
1794
|
-
return
|
|
2110
|
+
withoutDefer(labelsToRemove?: Set<string>): FieldSelection {
|
|
2111
|
+
return this.mapToSelectionSet((s) => s.withoutDefer(labelsToRemove));
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
withNormalizedDefer(normalizer: DeferNormalizer): FieldSelection {
|
|
2115
|
+
return this.mapToSelectionSet((s) => s.withNormalizedDefer(normalizer));
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
hasDefer(): boolean {
|
|
2119
|
+
return !!this.selectionSet?.hasDefer();
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
expandAllFragments(): FieldSelection {
|
|
2123
|
+
return this.mapToSelectionSet((s) => s.expandAllFragments());
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
trimUnsatisfiableBranches(_: CompositeType): FieldSelection {
|
|
2127
|
+
if (!this.selectionSet) {
|
|
2128
|
+
return this;
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
const base = baseType(this.element.definition.type!)
|
|
2132
|
+
assert(isCompositeType(base), () => `Field ${this.element} should not have a sub-selection`);
|
|
2133
|
+
const trimmed = this.mapToSelectionSet((s) => s.trimUnsatisfiableBranches(base));
|
|
2134
|
+
// In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
|
|
2135
|
+
// sub-selection is empty. Which suggest something may be wrong with this part of the query
|
|
2136
|
+
// intent, but the query was valid while keeping an empty sub-selection isn't. So in that
|
|
2137
|
+
// case, we just add some "non-included" __typename field just to keep the query valid.
|
|
2138
|
+
if (trimmed.selectionSet?.isEmpty()) {
|
|
2139
|
+
return trimmed.withUpdatedSelectionSet(selectionSetOfElement(
|
|
2140
|
+
new Field(
|
|
2141
|
+
base.typenameField()!,
|
|
2142
|
+
undefined,
|
|
2143
|
+
[new Directive('include', { 'if': false })],
|
|
2144
|
+
)
|
|
2145
|
+
));
|
|
2146
|
+
} else {
|
|
2147
|
+
return trimmed;
|
|
2148
|
+
}
|
|
1795
2149
|
}
|
|
1796
2150
|
|
|
1797
|
-
|
|
1798
|
-
return
|
|
2151
|
+
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FieldSelection {
|
|
2152
|
+
return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
|
|
1799
2153
|
}
|
|
1800
2154
|
|
|
1801
2155
|
equals(that: Selection): boolean {
|
|
@@ -1803,7 +2157,7 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1803
2157
|
return true;
|
|
1804
2158
|
}
|
|
1805
2159
|
|
|
1806
|
-
if (!(that instanceof FieldSelection) || !this.
|
|
2160
|
+
if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
|
|
1807
2161
|
return false;
|
|
1808
2162
|
}
|
|
1809
2163
|
if (!this.selectionSet) {
|
|
@@ -1813,7 +2167,7 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1813
2167
|
}
|
|
1814
2168
|
|
|
1815
2169
|
contains(that: Selection): boolean {
|
|
1816
|
-
if (!(that instanceof FieldSelection) || !this.
|
|
2170
|
+
if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
|
|
1817
2171
|
return false;
|
|
1818
2172
|
}
|
|
1819
2173
|
|
|
@@ -1823,187 +2177,117 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1823
2177
|
return !!this.selectionSet && this.selectionSet.contains(that.selectionSet);
|
|
1824
2178
|
}
|
|
1825
2179
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
withoutDefer(labelsToRemove?: Set<string>): FieldSelection {
|
|
1831
|
-
const updatedSubSelections = this.selectionSet?.withoutDefer(labelsToRemove);
|
|
1832
|
-
return updatedSubSelections === this.selectionSet
|
|
1833
|
-
? this
|
|
1834
|
-
: new FieldSelection(this.field, updatedSubSelections);
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
withNormalizedDefer(normalizer: DeferNormalizer): FieldSelection {
|
|
1838
|
-
const updatedSubSelections = this.selectionSet?.withNormalizedDefer(normalizer);
|
|
1839
|
-
return updatedSubSelections === this.selectionSet
|
|
1840
|
-
? this
|
|
1841
|
-
: new FieldSelection(this.field, updatedSubSelections);
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
clone(): FieldSelection {
|
|
1845
|
-
if (!this.selectionSet) {
|
|
1846
|
-
return this;
|
|
1847
|
-
}
|
|
1848
|
-
return new FieldSelection(this.field, this.selectionSet.clone());
|
|
2180
|
+
isUnecessaryInlineFragment(_: CompositeType): this is InlineFragmentSelection {
|
|
2181
|
+
// Overridden by inline fragments
|
|
2182
|
+
return false;
|
|
1849
2183
|
}
|
|
1850
2184
|
|
|
1851
2185
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
1852
|
-
return (indent ?? '') + this.
|
|
2186
|
+
return (indent ?? '') + this.element + (this.selectionSet ? ' ' + this.selectionSet.toString(expandFragments, true, indent) : '');
|
|
1853
2187
|
}
|
|
1854
2188
|
}
|
|
1855
2189
|
|
|
1856
|
-
export abstract class FragmentSelection extends
|
|
2190
|
+
export abstract class FragmentSelection extends AbstractSelection<FragmentElement, never, FragmentSelection> {
|
|
1857
2191
|
readonly kind = 'FragmentSelection' as const;
|
|
1858
2192
|
|
|
1859
|
-
abstract key(): string;
|
|
1860
|
-
|
|
1861
|
-
abstract element(): FragmentElement;
|
|
1862
|
-
|
|
1863
|
-
abstract get selectionSet(): SelectionSet;
|
|
1864
|
-
|
|
1865
|
-
abstract collectUsedFragmentNames(collector: Map<string, number>): void;
|
|
1866
|
-
|
|
1867
|
-
abstract namedFragments(): NamedFragments | undefined;
|
|
1868
|
-
|
|
1869
|
-
abstract optimize(fragments: NamedFragments): FragmentSelection;
|
|
1870
|
-
|
|
1871
|
-
abstract expandFragments(names?: string[]): Selection | readonly Selection[];
|
|
1872
|
-
|
|
1873
|
-
abstract toSelectionNode(): SelectionNode;
|
|
1874
|
-
|
|
1875
|
-
abstract validate(): void;
|
|
1876
|
-
|
|
1877
|
-
abstract withoutDefer(labelsToRemove?: Set<string>): FragmentSelection | SelectionSet;
|
|
1878
|
-
|
|
1879
|
-
abstract withNormalizedDefer(normalizer: DeferNormalizer): FragmentSelection | SelectionSet;
|
|
1880
|
-
|
|
1881
|
-
/**
|
|
1882
|
-
* See `FielSelection.updateForAddingTo` for a discussion of why this method exists and what it does.
|
|
1883
|
-
*/
|
|
1884
|
-
abstract updateForAddingTo(selectionSet: SelectionSet): FragmentSelection;
|
|
1885
|
-
|
|
1886
2193
|
abstract canAddTo(parentType: CompositeType): boolean;
|
|
1887
2194
|
|
|
1888
|
-
abstract withUpdatedSubSelection(newSubSelection: SelectionSet | undefined): FragmentSelection;
|
|
1889
|
-
|
|
1890
|
-
get parentType(): CompositeType {
|
|
1891
|
-
return this.element().parentType;
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
2195
|
protected us(): FragmentSelection {
|
|
1895
2196
|
return this;
|
|
1896
2197
|
}
|
|
1897
2198
|
|
|
1898
2199
|
protected validateDeferAndStream() {
|
|
1899
|
-
if (this.element
|
|
1900
|
-
const schemaDef = this.element
|
|
1901
|
-
const parentType = this.
|
|
2200
|
+
if (this.element.hasDefer() || this.element.hasStream()) {
|
|
2201
|
+
const schemaDef = this.element.schema().schemaDefinition;
|
|
2202
|
+
const parentType = this.parentType;
|
|
1902
2203
|
validate(
|
|
1903
2204
|
schemaDef.rootType('mutation') !== parentType && schemaDef.rootType('subscription') !== parentType,
|
|
1904
2205
|
() => `The @defer and @stream directives cannot be used on ${schemaDef.roots().filter((t) => t.type === parentType).pop()?.rootKind} root type "${parentType}"`,
|
|
1905
2206
|
);
|
|
1906
2207
|
}
|
|
1907
2208
|
}
|
|
1908
|
-
|
|
1909
|
-
usedVariables(): Variables {
|
|
1910
|
-
return mergeVariables(this.element().variables(), this.selectionSet.usedVariables());
|
|
1911
|
-
}
|
|
1912
|
-
|
|
2209
|
+
|
|
1913
2210
|
filter(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
|
|
1914
2211
|
// Note that we essentially expand all fragments as part of this.
|
|
1915
2212
|
const selectionSet = this.selectionSet;
|
|
1916
2213
|
const updatedSelectionSet = selectionSet.filter(predicate);
|
|
1917
2214
|
const thisWithFilteredSelectionSet = updatedSelectionSet === selectionSet
|
|
1918
2215
|
? this
|
|
1919
|
-
: new InlineFragmentSelection(this.element
|
|
2216
|
+
: new InlineFragmentSelection(this.element, updatedSelectionSet);
|
|
1920
2217
|
|
|
1921
2218
|
return predicate(thisWithFilteredSelectionSet) ? thisWithFilteredSelectionSet : undefined;
|
|
1922
2219
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
this.selectionSet.
|
|
2220
|
+
|
|
2221
|
+
hasDefer(): boolean {
|
|
2222
|
+
return this.element.hasDefer() || this.selectionSet.hasDefer();
|
|
1926
2223
|
}
|
|
1927
2224
|
|
|
1928
|
-
equals(that: Selection): boolean
|
|
1929
|
-
if (this === that) {
|
|
1930
|
-
return true;
|
|
1931
|
-
}
|
|
1932
|
-
return (that instanceof FragmentSelection)
|
|
1933
|
-
&& this.element().equals(that.element())
|
|
1934
|
-
&& this.selectionSet.equals(that.selectionSet);
|
|
1935
|
-
}
|
|
2225
|
+
abstract equals(that: Selection): boolean;
|
|
1936
2226
|
|
|
1937
|
-
contains(that: Selection): boolean
|
|
1938
|
-
return (that instanceof FragmentSelection)
|
|
1939
|
-
&& this.element().equals(that.element())
|
|
1940
|
-
&& this.selectionSet.contains(that.selectionSet);
|
|
1941
|
-
}
|
|
2227
|
+
abstract contains(that: Selection): boolean;
|
|
1942
2228
|
|
|
1943
|
-
|
|
1944
|
-
return
|
|
2229
|
+
isUnecessaryInlineFragment(parentType: CompositeType): boolean {
|
|
2230
|
+
return this.element.appliedDirectives.length === 0
|
|
2231
|
+
&& !!this.element.typeCondition
|
|
2232
|
+
&& (
|
|
2233
|
+
this.element.typeCondition.name === parentType.name
|
|
2234
|
+
|| (isObjectType(parentType) && possibleRuntimeTypes(this.element.typeCondition).some((t) => t.name === parentType.name))
|
|
2235
|
+
);
|
|
1945
2236
|
}
|
|
2237
|
+
|
|
1946
2238
|
}
|
|
1947
2239
|
|
|
1948
2240
|
class InlineFragmentSelection extends FragmentSelection {
|
|
1949
|
-
private readonly _selectionSet: SelectionSet;
|
|
1950
|
-
|
|
1951
2241
|
constructor(
|
|
1952
|
-
|
|
1953
|
-
|
|
2242
|
+
fragment: FragmentElement,
|
|
2243
|
+
private readonly _selectionSet: SelectionSet,
|
|
1954
2244
|
) {
|
|
1955
|
-
super();
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2245
|
+
super(fragment);
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
get selectionSet(): SelectionSet {
|
|
2249
|
+
return this._selectionSet;
|
|
1960
2250
|
}
|
|
1961
2251
|
|
|
1962
2252
|
key(): string {
|
|
1963
|
-
return this.element()
|
|
2253
|
+
return this.element.key();
|
|
1964
2254
|
}
|
|
1965
2255
|
|
|
1966
|
-
|
|
2256
|
+
withUpdatedComponents(fragment: FragmentElement, selectionSet: SelectionSet): InlineFragmentSelection {
|
|
2257
|
+
return new InlineFragmentSelection(fragment, selectionSet);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
validate(variableDefinitions: VariableDefinitions) {
|
|
1967
2261
|
this.validateDeferAndStream();
|
|
1968
2262
|
// Note that validation is kind of redundant since `this.selectionSet.validate()` will check that it isn't empty. But doing it
|
|
1969
2263
|
// allow to provide much better error messages.
|
|
1970
2264
|
validate(
|
|
1971
2265
|
!this.selectionSet.isEmpty(),
|
|
1972
|
-
() => `Invalid empty selection set for fragment "${this.element
|
|
2266
|
+
() => `Invalid empty selection set for fragment "${this.element}"`
|
|
1973
2267
|
);
|
|
1974
|
-
this.selectionSet.validate();
|
|
2268
|
+
this.selectionSet.validate(variableDefinitions);
|
|
1975
2269
|
}
|
|
1976
2270
|
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
return this.cloneIfFrozen();
|
|
2271
|
+
rebaseOn(parentType: CompositeType): FragmentSelection {
|
|
2272
|
+
if (this.parentType === parentType) {
|
|
2273
|
+
return this;
|
|
1981
2274
|
}
|
|
1982
2275
|
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
if (this.selectionSet.parentType !== updatedCastedType) {
|
|
1988
|
-
updatedSelectionSet = new SelectionSet(updatedCastedType);
|
|
1989
|
-
// Note that re-adding every selection ensures that anything frozen will be cloned as needed, on top of handling any knock-down
|
|
1990
|
-
// effect of the type change.
|
|
1991
|
-
for (const selection of this.selectionSet.selections()) {
|
|
1992
|
-
updatedSelectionSet.add(selection);
|
|
1993
|
-
}
|
|
1994
|
-
} else {
|
|
1995
|
-
updatedSelectionSet = this.selectionSet?.cloneIfFrozen();
|
|
2276
|
+
const rebasedFragment = this.element.rebaseOn(parentType);
|
|
2277
|
+
const rebasedCastedType = rebasedFragment.castedType();
|
|
2278
|
+
if (rebasedCastedType === this.selectionSet.parentType) {
|
|
2279
|
+
return this.withUpdatedElement(rebasedFragment);
|
|
1996
2280
|
}
|
|
1997
2281
|
|
|
1998
|
-
return
|
|
2282
|
+
return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType));
|
|
1999
2283
|
}
|
|
2000
2284
|
|
|
2001
2285
|
canAddTo(parentType: CompositeType): boolean {
|
|
2002
|
-
if (this.element
|
|
2286
|
+
if (this.element.parentType === parentType) {
|
|
2003
2287
|
return true;
|
|
2004
2288
|
}
|
|
2005
2289
|
|
|
2006
|
-
const type = this.element
|
|
2290
|
+
const type = this.element.castedTypeIfAddedTo(parentType);
|
|
2007
2291
|
if (!type) {
|
|
2008
2292
|
return false;
|
|
2009
2293
|
}
|
|
@@ -2014,21 +2298,8 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2014
2298
|
return true;
|
|
2015
2299
|
}
|
|
2016
2300
|
|
|
2017
|
-
|
|
2018
|
-
get selectionSet(): SelectionSet {
|
|
2019
|
-
return this._selectionSet;
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
namedFragments(): NamedFragments | undefined {
|
|
2023
|
-
return this.selectionSet.fragments;
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
element(): FragmentElement {
|
|
2027
|
-
return this.fragmentElement;
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
2301
|
toSelectionNode(): InlineFragmentNode {
|
|
2031
|
-
const typeCondition = this.element
|
|
2302
|
+
const typeCondition = this.element.typeCondition;
|
|
2032
2303
|
return {
|
|
2033
2304
|
kind: Kind.INLINE_FRAGMENT,
|
|
2034
2305
|
typeCondition: typeCondition
|
|
@@ -2040,120 +2311,271 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2040
2311
|
},
|
|
2041
2312
|
}
|
|
2042
2313
|
: undefined,
|
|
2043
|
-
directives: this.element
|
|
2314
|
+
directives: this.element.appliedDirectivesToDirectiveNodes(),
|
|
2044
2315
|
selectionSet: this.selectionSet.toSelectionSetNode()
|
|
2045
2316
|
};
|
|
2046
2317
|
}
|
|
2047
2318
|
|
|
2048
2319
|
optimize(fragments: NamedFragments): FragmentSelection {
|
|
2049
2320
|
let optimizedSelection = this.selectionSet.optimize(fragments);
|
|
2050
|
-
const typeCondition = this.element
|
|
2321
|
+
const typeCondition = this.element.typeCondition;
|
|
2051
2322
|
if (typeCondition) {
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
if (sameType(typeCondition, candidate.typeCondition)) {
|
|
2061
|
-
// If we ignore the current condition, then we need to ensure any directive applied to it are preserved.
|
|
2062
|
-
this.fragmentElement.appliedDirectives.forEach((directive) => {
|
|
2063
|
-
spread.element().applyDirective(directive.definition!, directive.arguments());
|
|
2064
|
-
})
|
|
2065
|
-
return spread;
|
|
2066
|
-
}
|
|
2067
|
-
optimizedSelection = selectionSetOf(spread.element().parentType, spread);
|
|
2068
|
-
break;
|
|
2069
|
-
}
|
|
2323
|
+
const optimized = this.tryOptimizeSubselectionWithFragments({
|
|
2324
|
+
parentType: typeCondition,
|
|
2325
|
+
subSelection: optimizedSelection,
|
|
2326
|
+
fragments,
|
|
2327
|
+
});
|
|
2328
|
+
if (optimized instanceof FragmentSpreadSelection) {
|
|
2329
|
+
// This means the whole inline fragment can be replaced by the spread.
|
|
2330
|
+
return optimized;
|
|
2070
2331
|
}
|
|
2332
|
+
optimizedSelection = optimized;
|
|
2071
2333
|
}
|
|
2072
2334
|
return this.selectionSet === optimizedSelection
|
|
2073
2335
|
? this
|
|
2074
|
-
: new InlineFragmentSelection(this.
|
|
2075
|
-
}
|
|
2336
|
+
: new InlineFragmentSelection(this.element, optimizedSelection);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
protected tryOptimizeSubselectionOnce({
|
|
2340
|
+
parentType,
|
|
2341
|
+
subSelection,
|
|
2342
|
+
candidates,
|
|
2343
|
+
fragments,
|
|
2344
|
+
}: {
|
|
2345
|
+
parentType: CompositeType,
|
|
2346
|
+
subSelection: SelectionSet,
|
|
2347
|
+
candidates: NamedFragmentDefinition[],
|
|
2348
|
+
fragments: NamedFragments,
|
|
2349
|
+
}): {
|
|
2350
|
+
spread?: FragmentSpreadSelection,
|
|
2351
|
+
optimizedSelection?: SelectionSet,
|
|
2352
|
+
hasDiff?: boolean,
|
|
2353
|
+
}{
|
|
2354
|
+
let optimizedSelection = subSelection;
|
|
2355
|
+
for (const candidate of candidates) {
|
|
2356
|
+
const { contains, diff } = optimizedSelection.diffIfContains(candidate.selectionSet);
|
|
2357
|
+
if (contains) {
|
|
2358
|
+
// The candidate selection is included in our sub-selection. One remaining thing to take into account
|
|
2359
|
+
// is applied directives: if the candidate has directives, then we can only use it if 1) there is
|
|
2360
|
+
// no `diff`, 2) the type condition of this fragment matches the candidate one and 3) the directives
|
|
2361
|
+
// in question are also on this very fragment. In that case, we can replace this whole inline fragment
|
|
2362
|
+
// by a spread of the candidate.
|
|
2363
|
+
if (!diff && sameType(this.element.typeCondition!, candidate.typeCondition)) {
|
|
2364
|
+
// We can potentially replace the whole fragment by the candidate; but as said above, still needs
|
|
2365
|
+
// to check the directives.
|
|
2366
|
+
let spreadDirectives: Directive<any>[] = this.element.appliedDirectives;
|
|
2367
|
+
if (candidate.appliedDirectives.length > 0) {
|
|
2368
|
+
const { isSubset, difference } = diffDirectives(this.element.appliedDirectives, candidate.appliedDirectives);
|
|
2369
|
+
if (!isSubset) {
|
|
2370
|
+
// While the candidate otherwise match, it has directives that are not on this element, so we
|
|
2371
|
+
// cannot reuse it.
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2374
|
+
// Otherwise, any directives on this element that are not on the candidate should be kept and used
|
|
2375
|
+
// on the spread created.
|
|
2376
|
+
spreadDirectives = difference;
|
|
2377
|
+
}
|
|
2378
|
+
// Returning a spread without a subselection will make the code "replace" this whole inline fragment
|
|
2379
|
+
// by the spread, which is what we want. Do not that as we're replacing the whole inline fragment,
|
|
2380
|
+
// we use `this.parentType` instead of `parentType` (the later being `this.element.typeCondition` basically).
|
|
2381
|
+
return {
|
|
2382
|
+
spread: new FragmentSpreadSelection(this.parentType, fragments, candidate, spreadDirectives),
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2076
2385
|
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
: new InlineFragmentSelection(this.element(), expandedSelection);
|
|
2082
|
-
}
|
|
2386
|
+
// We're already dealt with the one case where we might be able to handle a candidate that has directives.
|
|
2387
|
+
if (candidate.appliedDirectives.length > 0) {
|
|
2388
|
+
continue;
|
|
2389
|
+
}
|
|
2083
2390
|
|
|
2084
|
-
|
|
2085
|
-
|
|
2391
|
+
const spread = new FragmentSpreadSelection(parentType, fragments, candidate, []);
|
|
2392
|
+
optimizedSelection = diff
|
|
2393
|
+
? new SelectionSetUpdates().add(spread).add(diff).toSelectionSet(parentType, fragments)
|
|
2394
|
+
: selectionSetOf(parentType, spread);
|
|
2395
|
+
|
|
2396
|
+
return { spread, optimizedSelection, hasDiff: !!diff };
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
return {};
|
|
2086
2400
|
}
|
|
2087
2401
|
|
|
2088
|
-
withoutDefer(labelsToRemove?: Set<string>):
|
|
2089
|
-
const
|
|
2090
|
-
const deferArgs = this.
|
|
2402
|
+
withoutDefer(labelsToRemove?: Set<string>): InlineFragmentSelection | SelectionSet {
|
|
2403
|
+
const newSelection = this.selectionSet.withoutDefer(labelsToRemove);
|
|
2404
|
+
const deferArgs = this.element.deferDirectiveArgs();
|
|
2091
2405
|
const hasDeferToRemove = deferArgs && (!labelsToRemove || (deferArgs.label && labelsToRemove.has(deferArgs.label)));
|
|
2092
|
-
if (
|
|
2406
|
+
if (newSelection === this.selectionSet && !hasDeferToRemove) {
|
|
2093
2407
|
return this;
|
|
2094
2408
|
}
|
|
2095
|
-
const
|
|
2096
|
-
if (!
|
|
2097
|
-
return
|
|
2409
|
+
const newElement = hasDeferToRemove ? this.element.withoutDefer() : this.element;
|
|
2410
|
+
if (!newElement) {
|
|
2411
|
+
return newSelection;
|
|
2098
2412
|
}
|
|
2099
|
-
return
|
|
2413
|
+
return this.withUpdatedComponents(newElement, newSelection);
|
|
2100
2414
|
}
|
|
2101
2415
|
|
|
2102
2416
|
withNormalizedDefer(normalizer: DeferNormalizer): InlineFragmentSelection | SelectionSet {
|
|
2103
|
-
const
|
|
2104
|
-
const
|
|
2105
|
-
if (!
|
|
2106
|
-
return
|
|
2417
|
+
const newElement = this.element.withNormalizedDefer(normalizer);
|
|
2418
|
+
const newSelection = this.selectionSet.withNormalizedDefer(normalizer)
|
|
2419
|
+
if (!newElement) {
|
|
2420
|
+
return newSelection;
|
|
2107
2421
|
}
|
|
2108
|
-
return
|
|
2422
|
+
return newElement === this.element && newSelection === this.selectionSet
|
|
2109
2423
|
? this
|
|
2110
|
-
:
|
|
2424
|
+
: this.withUpdatedComponents(newElement, newSelection);
|
|
2111
2425
|
}
|
|
2112
2426
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2427
|
+
trimUnsatisfiableBranches(currentType: CompositeType): FragmentSelection | SelectionSet | undefined {
|
|
2428
|
+
const thisCondition = this.element.typeCondition;
|
|
2429
|
+
// Note that if the condition has directives, we preserve the fragment no matter what.
|
|
2430
|
+
if (this.element.appliedDirectives.length === 0) {
|
|
2431
|
+
if (!thisCondition || currentType === this.element.typeCondition) {
|
|
2432
|
+
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType);
|
|
2433
|
+
return trimmed.isEmpty() ? undefined : trimmed;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
// If the current type is an object, then we never need to keep the current fragment because:
|
|
2437
|
+
// - either the fragment is also an object, but we've eliminated the case where the 2 types are the same,
|
|
2438
|
+
// so this is just an unsatisfiable branch.
|
|
2439
|
+
// - or it's not an object, but then the current type is more precise and no point in "casting" to a
|
|
2440
|
+
// less precise interface/union. And if the current type is not even a valid runtime of said interface/union,
|
|
2441
|
+
// then we should completely ignore the branch (or, since we're eliminating `thisCondition`, we would be
|
|
2442
|
+
// building an invalid selection).
|
|
2443
|
+
if (isObjectType(currentType)) {
|
|
2444
|
+
if (isObjectType(thisCondition) || !possibleRuntimeTypes(thisCondition).includes(currentType)) {
|
|
2445
|
+
return undefined;
|
|
2446
|
+
} else {
|
|
2447
|
+
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType);
|
|
2448
|
+
return trimmed.isEmpty() ? undefined : trimmed;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// In all other cases, we first recurse on the sub-selection.
|
|
2454
|
+
const trimmedSelectionSet = this.selectionSet.trimUnsatisfiableBranches(this.element.typeCondition ?? this.parentType);
|
|
2455
|
+
|
|
2456
|
+
// First, could be that everything was unsatisfiable.
|
|
2457
|
+
if (trimmedSelectionSet.isEmpty()) {
|
|
2458
|
+
if (this.element.appliedDirectives.length === 0) {
|
|
2459
|
+
return undefined;
|
|
2460
|
+
} else {
|
|
2461
|
+
return this.withUpdatedSelectionSet(selectionSetOfElement(
|
|
2462
|
+
new Field(
|
|
2463
|
+
(this.element.typeCondition ?? this.parentType).typenameField()!,
|
|
2464
|
+
undefined,
|
|
2465
|
+
[new Directive('include', { 'if': false })],
|
|
2466
|
+
)
|
|
2467
|
+
));
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// Second, we check if some of the sub-selection fragments can be "lifted" outside of this fragment. This can happen if:
|
|
2472
|
+
// 1. the current fragment is an abstract type,
|
|
2473
|
+
// 2. the sub-fragment is an object type,
|
|
2474
|
+
// 3. the sub-fragment type is a valid runtime of the current type.
|
|
2475
|
+
if (this.element.appliedDirectives.length === 0 && isAbstractType(thisCondition!)) {
|
|
2476
|
+
assert(!isObjectType(currentType), () => `Should not have got here if ${currentType} is an object type`);
|
|
2477
|
+
const currentRuntimes = possibleRuntimeTypes(currentType);
|
|
2478
|
+
const liftableSelections: Selection[] = [];
|
|
2479
|
+
for (const selection of trimmedSelectionSet.selections()) {
|
|
2480
|
+
if (selection.kind === 'FragmentSelection'
|
|
2481
|
+
&& selection.element.typeCondition
|
|
2482
|
+
&& isObjectType(selection.element.typeCondition)
|
|
2483
|
+
&& currentRuntimes.includes(selection.element.typeCondition)
|
|
2484
|
+
) {
|
|
2485
|
+
liftableSelections.push(selection);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// If we can lift all selections, then that just mean we can get rid of the current fragment altogether
|
|
2490
|
+
if (liftableSelections.length === trimmedSelectionSet.selections().length) {
|
|
2491
|
+
return trimmedSelectionSet;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// Otherwise, if there is "liftable" selections, we must return a set comprised of those lifted selection,
|
|
2495
|
+
// and the current fragment _without_ those lifted selections.
|
|
2496
|
+
if (liftableSelections.length > 0) {
|
|
2497
|
+
const newSet = new SelectionSetUpdates();
|
|
2498
|
+
newSet.add(liftableSelections);
|
|
2499
|
+
newSet.add(this.withUpdatedSelectionSet(
|
|
2500
|
+
trimmedSelectionSet.filter((s) => !liftableSelections.includes(s)),
|
|
2501
|
+
));
|
|
2502
|
+
return newSet.toSelectionSet(this.parentType);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
return this.selectionSet === trimmedSelectionSet ? this : this.withUpdatedSelectionSet(trimmedSelectionSet);
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
expandAllFragments(): FragmentSelection {
|
|
2510
|
+
return this.mapToSelectionSet((s) => s.expandAllFragments());
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FragmentSelection {
|
|
2514
|
+
return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
equals(that: Selection): boolean {
|
|
2518
|
+
if (this === that) {
|
|
2519
|
+
return true;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
return (that instanceof FragmentSelection)
|
|
2523
|
+
&& this.element.equals(that.element)
|
|
2524
|
+
&& this.selectionSet.equals(that.selectionSet);
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
contains(that: Selection): boolean {
|
|
2528
|
+
return (that instanceof FragmentSelection)
|
|
2529
|
+
&& this.element.equals(that.element)
|
|
2530
|
+
&& this.selectionSet.contains(that.selectionSet);
|
|
2115
2531
|
}
|
|
2116
2532
|
|
|
2117
2533
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
2118
|
-
return (indent ?? '') + this.
|
|
2534
|
+
return (indent ?? '') + this.element + ' ' + this.selectionSet.toString(expandFragments, true, indent);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
function diffDirectives(superset: readonly Directive<any>[], maybeSubset: readonly Directive<any>[]): { isSubset: boolean, difference: Directive[] } {
|
|
2539
|
+
if (maybeSubset.every((d) => superset.some((s) => sameDirectiveApplication(d, s)))) {
|
|
2540
|
+
return { isSubset: true, difference: superset.filter((s) => !maybeSubset.some((d) => sameDirectiveApplication(d, s))) };
|
|
2541
|
+
} else {
|
|
2542
|
+
return { isSubset: false, difference: [] };
|
|
2119
2543
|
}
|
|
2120
2544
|
}
|
|
2121
2545
|
|
|
2122
2546
|
class FragmentSpreadSelection extends FragmentSelection {
|
|
2123
|
-
private
|
|
2124
|
-
// Note that the named fragment directives are copied on this element and appear first (the spreadDirectives
|
|
2125
|
-
// method rely on this to be able to extract the directives that are specific to the spread itself).
|
|
2126
|
-
private readonly _element : FragmentElement;
|
|
2547
|
+
private computedKey: string | undefined;
|
|
2127
2548
|
|
|
2128
2549
|
constructor(
|
|
2129
2550
|
sourceType: CompositeType,
|
|
2130
2551
|
private readonly fragments: NamedFragments,
|
|
2131
|
-
|
|
2552
|
+
readonly namedFragment: NamedFragmentDefinition,
|
|
2553
|
+
private readonly spreadDirectives: readonly Directive<any>[],
|
|
2132
2554
|
) {
|
|
2133
|
-
super();
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
this.
|
|
2138
|
-
for (const directive of fragmentDefinition.appliedDirectives) {
|
|
2139
|
-
this._element.applyDirective(directive.definition!, directive.arguments());
|
|
2140
|
-
}
|
|
2555
|
+
super(new FragmentElement(sourceType, namedFragment.typeCondition, namedFragment.appliedDirectives.concat(spreadDirectives)));
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
get selectionSet(): SelectionSet {
|
|
2559
|
+
return this.namedFragment.selectionSet;
|
|
2141
2560
|
}
|
|
2142
2561
|
|
|
2143
2562
|
key(): string {
|
|
2144
|
-
|
|
2563
|
+
if (!this.computedKey) {
|
|
2564
|
+
this.computedKey = '...' + this.namedFragment.name + (this.spreadDirectives.length === 0 ? '' : ' ' + this.spreadDirectives.join(' '));
|
|
2565
|
+
}
|
|
2566
|
+
return this.computedKey;
|
|
2145
2567
|
}
|
|
2146
2568
|
|
|
2147
|
-
|
|
2148
|
-
|
|
2569
|
+
withUpdatedComponents(_fragment: FragmentElement, _selectionSet: SelectionSet): InlineFragmentSelection {
|
|
2570
|
+
assert(false, `Unsupported`);
|
|
2149
2571
|
}
|
|
2150
2572
|
|
|
2151
|
-
|
|
2152
|
-
return this
|
|
2573
|
+
trimUnsatisfiableBranches(_: CompositeType): FragmentSelection {
|
|
2574
|
+
return this;
|
|
2153
2575
|
}
|
|
2154
2576
|
|
|
2155
|
-
|
|
2156
|
-
return this.
|
|
2577
|
+
namedFragments(): NamedFragments | undefined {
|
|
2578
|
+
return this.fragments;
|
|
2157
2579
|
}
|
|
2158
2580
|
|
|
2159
2581
|
validate(): void {
|
|
@@ -2163,10 +2585,9 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2163
2585
|
}
|
|
2164
2586
|
|
|
2165
2587
|
toSelectionNode(): FragmentSpreadNode {
|
|
2166
|
-
const
|
|
2167
|
-
const directiveNodes = spreadDirectives.length === 0
|
|
2588
|
+
const directiveNodes = this.spreadDirectives.length === 0
|
|
2168
2589
|
? undefined
|
|
2169
|
-
: spreadDirectives.map(directive => {
|
|
2590
|
+
: this.spreadDirectives.map(directive => {
|
|
2170
2591
|
return {
|
|
2171
2592
|
kind: Kind.DIRECTIVE,
|
|
2172
2593
|
name: {
|
|
@@ -2187,7 +2608,7 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2187
2608
|
return this;
|
|
2188
2609
|
}
|
|
2189
2610
|
|
|
2190
|
-
|
|
2611
|
+
rebaseOn(_parentType: CompositeType): FragmentSelection {
|
|
2191
2612
|
// This is a little bit iffy, because the fragment could link to a schema (typically the supergraph API one)
|
|
2192
2613
|
// that is different from the one of `_selectionSet` (say, a subgraph fetch selection in which we're trying to
|
|
2193
2614
|
// reuse a user fragment). But in practice, we expand all fragments when we do query planning and only re-add
|
|
@@ -2198,19 +2619,26 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2198
2619
|
}
|
|
2199
2620
|
|
|
2200
2621
|
canAddTo(_: CompositeType): boolean {
|
|
2201
|
-
// Mimicking the logic of `
|
|
2622
|
+
// Mimicking the logic of `rebaseOn`.
|
|
2202
2623
|
return true;
|
|
2203
2624
|
}
|
|
2204
2625
|
|
|
2205
|
-
|
|
2206
|
-
|
|
2626
|
+
expandAllFragments(): FragmentSelection | readonly Selection[] {
|
|
2627
|
+
const expandedSubSelections = this.selectionSet.expandAllFragments();
|
|
2628
|
+
return sameType(this.parentType, this.namedFragment.typeCondition) && this.element.appliedDirectives.length === 0
|
|
2629
|
+
? expandedSubSelections.selections()
|
|
2630
|
+
: new InlineFragmentSelection(this.element, expandedSubSelections);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FragmentSelection | readonly Selection[] {
|
|
2634
|
+
if (!names.includes(this.namedFragment.name)) {
|
|
2207
2635
|
return this;
|
|
2208
2636
|
}
|
|
2209
2637
|
|
|
2210
|
-
const expandedSubSelections = this.selectionSet.expandFragments(names,
|
|
2211
|
-
return sameType(this.
|
|
2638
|
+
const expandedSubSelections = this.selectionSet.expandFragments(names, updatedFragments);
|
|
2639
|
+
return sameType(this.parentType, this.namedFragment.typeCondition) && this.element.appliedDirectives.length === 0
|
|
2212
2640
|
? expandedSubSelections.selections()
|
|
2213
|
-
: new InlineFragmentSelection(this.
|
|
2641
|
+
: new InlineFragmentSelection(this.element, expandedSubSelections);
|
|
2214
2642
|
}
|
|
2215
2643
|
|
|
2216
2644
|
collectUsedFragmentNames(collector: Map<string, number>): void {
|
|
@@ -2219,33 +2647,123 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2219
2647
|
collector.set(this.namedFragment.name, usageCount === undefined ? 1 : usageCount + 1);
|
|
2220
2648
|
}
|
|
2221
2649
|
|
|
2222
|
-
withoutDefer(_labelsToRemove?: Set<string>):
|
|
2223
|
-
assert(false, 'Unsupported, see `Operation.
|
|
2650
|
+
withoutDefer(_labelsToRemove?: Set<string>): FragmentSpreadSelection {
|
|
2651
|
+
assert(false, 'Unsupported, see `Operation.withAllDeferLabelled`');
|
|
2224
2652
|
}
|
|
2225
2653
|
|
|
2226
|
-
withNormalizedDefer(
|
|
2654
|
+
withNormalizedDefer(_normalizer: DeferNormalizer): FragmentSpreadSelection {
|
|
2227
2655
|
assert(false, 'Unsupported, see `Operation.withAllDeferLabelled`');
|
|
2228
2656
|
}
|
|
2229
2657
|
|
|
2230
|
-
|
|
2231
|
-
|
|
2658
|
+
minus(that: Selection): undefined {
|
|
2659
|
+
assert(this.equals(that), () => `Invalid operation for ${this.toString(false)} and ${that.toString(false)}`);
|
|
2660
|
+
return undefined;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
equals(that: Selection): boolean {
|
|
2664
|
+
if (this === that) {
|
|
2665
|
+
return true;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
return (that instanceof FragmentSpreadSelection)
|
|
2669
|
+
&& this.namedFragment.name === that.namedFragment.name
|
|
2670
|
+
&& sameDirectiveApplications(this.spreadDirectives, that.spreadDirectives);
|
|
2232
2671
|
}
|
|
2233
2672
|
|
|
2234
|
-
|
|
2235
|
-
|
|
2673
|
+
contains(that: Selection): boolean {
|
|
2674
|
+
if (this.equals(that)) {
|
|
2675
|
+
return true;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
return (that instanceof FragmentSelection)
|
|
2679
|
+
&& this.element.equals(that.element)
|
|
2680
|
+
&& this.selectionSet.contains(that.selectionSet);
|
|
2236
2681
|
}
|
|
2237
2682
|
|
|
2238
2683
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
2239
2684
|
if (expandFragments) {
|
|
2240
|
-
return (indent ?? '') + this.
|
|
2685
|
+
return (indent ?? '') + this.element + ' ' + this.selectionSet.toString(true, true, indent);
|
|
2241
2686
|
} else {
|
|
2242
|
-
const directives = this.spreadDirectives
|
|
2687
|
+
const directives = this.spreadDirectives;
|
|
2243
2688
|
const directiveString = directives.length == 0 ? '' : ' ' + directives.join(' ');
|
|
2244
2689
|
return (indent ?? '') + '...' + this.namedFragment.name + directiveString;
|
|
2245
2690
|
}
|
|
2246
2691
|
}
|
|
2247
2692
|
}
|
|
2248
2693
|
|
|
2694
|
+
function selectionSetOfNode(
|
|
2695
|
+
parentType: CompositeType,
|
|
2696
|
+
node: SelectionSetNode,
|
|
2697
|
+
variableDefinitions: VariableDefinitions,
|
|
2698
|
+
fragments: NamedFragments | undefined,
|
|
2699
|
+
fieldAccessor: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined = (type, name) => type.field(name)
|
|
2700
|
+
): SelectionSet {
|
|
2701
|
+
if (node.selections.length === 1) {
|
|
2702
|
+
return selectionSetOf(
|
|
2703
|
+
parentType,
|
|
2704
|
+
selectionOfNode(parentType, node.selections[0], variableDefinitions, fragments, fieldAccessor),
|
|
2705
|
+
fragments,
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
const selections = new SelectionSetUpdates();
|
|
2710
|
+
for (const selectionNode of node.selections) {
|
|
2711
|
+
selections.add(selectionOfNode(parentType, selectionNode, variableDefinitions, fragments, fieldAccessor));
|
|
2712
|
+
}
|
|
2713
|
+
return selections.toSelectionSet(parentType, fragments);
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
function directiveOfNode<T extends DirectiveTargetElement<T>>(schema: Schema, node: DirectiveNode): Directive<T> {
|
|
2717
|
+
const directiveDef = schema.directive(node.name.value);
|
|
2718
|
+
validate(directiveDef, () => `Unknown directive "@${node.name.value}"`)
|
|
2719
|
+
return new Directive(directiveDef.name, argumentsFromAST(directiveDef.coordinate, node.arguments, directiveDef));
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
function directivesOfNodes<T extends DirectiveTargetElement<T>>(schema: Schema, nodes: readonly DirectiveNode[] | undefined): Directive<T>[] {
|
|
2723
|
+
return nodes?.map((n) => directiveOfNode(schema, n)) ?? [];
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
function selectionOfNode(
|
|
2727
|
+
parentType: CompositeType,
|
|
2728
|
+
node: SelectionNode,
|
|
2729
|
+
variableDefinitions: VariableDefinitions,
|
|
2730
|
+
fragments: NamedFragments | undefined,
|
|
2731
|
+
fieldAccessor: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined = (type, name) => type.field(name)
|
|
2732
|
+
): Selection {
|
|
2733
|
+
let selection: Selection;
|
|
2734
|
+
const directives = directivesOfNodes(parentType.schema(), node.directives);
|
|
2735
|
+
switch (node.kind) {
|
|
2736
|
+
case Kind.FIELD:
|
|
2737
|
+
const definition: FieldDefinition<any> | undefined = fieldAccessor(parentType, node.name.value);
|
|
2738
|
+
validate(definition, () => `Cannot query field "${node.name.value}" on type "${parentType}".`, parentType.sourceAST);
|
|
2739
|
+
const type = baseType(definition.type!);
|
|
2740
|
+
const selectionSet = node.selectionSet
|
|
2741
|
+
? selectionSetOfNode(type as CompositeType, node.selectionSet, variableDefinitions, fragments, fieldAccessor)
|
|
2742
|
+
: undefined;
|
|
2743
|
+
|
|
2744
|
+
selection = new FieldSelection(
|
|
2745
|
+
new Field(definition, argumentsFromAST(definition.coordinate, node.arguments, definition), directives, node.alias?.value),
|
|
2746
|
+
selectionSet,
|
|
2747
|
+
);
|
|
2748
|
+
break;
|
|
2749
|
+
case Kind.INLINE_FRAGMENT:
|
|
2750
|
+
const element = new FragmentElement(parentType, node.typeCondition?.name.value, directives);
|
|
2751
|
+
selection = new InlineFragmentSelection(
|
|
2752
|
+
element,
|
|
2753
|
+
selectionSetOfNode(element.typeCondition ? element.typeCondition : element.parentType, node.selectionSet, variableDefinitions, fragments, fieldAccessor),
|
|
2754
|
+
);
|
|
2755
|
+
break;
|
|
2756
|
+
case Kind.FRAGMENT_SPREAD:
|
|
2757
|
+
const fragmentName = node.name.value;
|
|
2758
|
+
validate(fragments, () => `Cannot find fragment name "${fragmentName}" (no fragments were provided)`);
|
|
2759
|
+
const fragment = fragments.get(fragmentName);
|
|
2760
|
+
validate(fragment, () => `Cannot find fragment name "${fragmentName}" (provided fragments are: [${fragments.names().join(', ')}])`);
|
|
2761
|
+
selection = new FragmentSpreadSelection(parentType, fragments, fragment, directives);
|
|
2762
|
+
break;
|
|
2763
|
+
}
|
|
2764
|
+
return selection;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2249
2767
|
export function operationFromDocument(
|
|
2250
2768
|
schema: Schema,
|
|
2251
2769
|
document: DocumentNode,
|
|
@@ -2277,9 +2795,7 @@ export function operationFromDocument(
|
|
|
2277
2795
|
if (!isCompositeType(typeCondition)) {
|
|
2278
2796
|
throw ERRORS.INVALID_GRAPHQL.err(`Invalid fragment "${name}" on non-composite type "${typeName}"`, { nodes: definition });
|
|
2279
2797
|
}
|
|
2280
|
-
|
|
2281
|
-
addDirectiveNodesToElement(definition.directives, fragment);
|
|
2282
|
-
fragments.add(fragment);
|
|
2798
|
+
fragments.add(new NamedFragmentDefinition(schema, name, typeCondition, directivesOfNodes(schema, definition.directives)));
|
|
2283
2799
|
break;
|
|
2284
2800
|
}
|
|
2285
2801
|
});
|
|
@@ -2295,11 +2811,11 @@ export function operationFromDocument(
|
|
|
2295
2811
|
switch (definition.kind) {
|
|
2296
2812
|
case Kind.FRAGMENT_DEFINITION:
|
|
2297
2813
|
const fragment = fragments.get(definition.name.value)!;
|
|
2298
|
-
fragment.
|
|
2814
|
+
fragment.setSelectionSet(selectionSetOfNode(fragment.typeCondition, definition.selectionSet, variableDefinitions, fragments));
|
|
2299
2815
|
break;
|
|
2300
2816
|
}
|
|
2301
2817
|
});
|
|
2302
|
-
fragments.validate();
|
|
2818
|
+
fragments.validate(variableDefinitions);
|
|
2303
2819
|
return operationFromAST({schema, operation, variableDefinitions, fragments, validateInput: options?.validate});
|
|
2304
2820
|
}
|
|
2305
2821
|
|
|
@@ -2325,7 +2841,7 @@ function operationFromAST({
|
|
|
2325
2841
|
parentType: rootType.type,
|
|
2326
2842
|
source: operation.selectionSet,
|
|
2327
2843
|
variableDefinitions,
|
|
2328
|
-
fragments,
|
|
2844
|
+
fragments: fragments.isEmpty() ? undefined : fragments,
|
|
2329
2845
|
validate: validateInput,
|
|
2330
2846
|
}),
|
|
2331
2847
|
variableDefinitions,
|
|
@@ -2347,7 +2863,7 @@ export function parseOperation(
|
|
|
2347
2863
|
export function parseSelectionSet({
|
|
2348
2864
|
parentType,
|
|
2349
2865
|
source,
|
|
2350
|
-
variableDefinitions,
|
|
2866
|
+
variableDefinitions = new VariableDefinitions(),
|
|
2351
2867
|
fragments,
|
|
2352
2868
|
fieldAccessor,
|
|
2353
2869
|
validate = true,
|
|
@@ -2363,10 +2879,9 @@ export function parseSelectionSet({
|
|
|
2363
2879
|
const node = typeof source === 'string'
|
|
2364
2880
|
? parseOperationAST(source.trim().startsWith('{') ? source : `{${source}}`).selectionSet
|
|
2365
2881
|
: source;
|
|
2366
|
-
const selectionSet = new
|
|
2367
|
-
selectionSet.addSelectionSetNode(node, variableDefinitions ?? new VariableDefinitions(), fieldAccessor);
|
|
2882
|
+
const selectionSet = selectionSetOfNode(parentType, node, variableDefinitions ?? new VariableDefinitions(), fragments, fieldAccessor);
|
|
2368
2883
|
if (validate)
|
|
2369
|
-
selectionSet.validate();
|
|
2884
|
+
selectionSet.validate(variableDefinitions);
|
|
2370
2885
|
return selectionSet;
|
|
2371
2886
|
}
|
|
2372
2887
|
|