@apollo/federation-internals 2.4.0-alpha.0 → 2.4.0
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 +25 -0
- package/dist/coreSpec.d.ts.map +1 -1
- package/dist/coreSpec.js +2 -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 +131 -88
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +774 -581
- 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 +148 -119
- package/src/coreSpec.ts +1 -0
- package/src/definitions.ts +53 -57
- package/src/federation.ts +27 -23
- package/src/operations.ts +1120 -811
- 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,13 @@ import {
|
|
|
24
24
|
InterfaceType,
|
|
25
25
|
isCompositeType,
|
|
26
26
|
isInterfaceType,
|
|
27
|
-
isLeafType,
|
|
28
27
|
isNullableType,
|
|
29
28
|
isUnionType,
|
|
30
29
|
ObjectType,
|
|
31
30
|
runtimeTypesIntersects,
|
|
32
31
|
Schema,
|
|
33
32
|
SchemaRootKind,
|
|
34
|
-
|
|
35
|
-
Variables,
|
|
36
|
-
variablesInArguments,
|
|
33
|
+
VariableCollector,
|
|
37
34
|
VariableDefinitions,
|
|
38
35
|
variableDefinitionsFromAST,
|
|
39
36
|
CompositeType,
|
|
@@ -46,11 +43,16 @@ import {
|
|
|
46
43
|
Variable,
|
|
47
44
|
possibleRuntimeTypes,
|
|
48
45
|
Type,
|
|
46
|
+
sameDirectiveApplication,
|
|
47
|
+
isLeafType,
|
|
48
|
+
Variables,
|
|
49
|
+
isObjectType,
|
|
49
50
|
} from "./definitions";
|
|
50
51
|
import { ERRORS } from "./error";
|
|
51
52
|
import { isDirectSubtype, sameType } from "./types";
|
|
52
|
-
import { assert, mapEntries, MapWithCachedArrays, MultiMap, SetMultiMap } from "./utils";
|
|
53
|
+
import { assert, 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,51 +257,49 @@ 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
|
|
|
@@ -284,44 +353,88 @@ export class Field<TArgs extends {[key: string]: any} = {[key: string]: any}> ex
|
|
|
284
353
|
return that.kind === 'Field'
|
|
285
354
|
&& this.name === that.name
|
|
286
355
|
&& this.alias === that.alias
|
|
287
|
-
&& argumentsEquals(this.args, that.args)
|
|
356
|
+
&& (this.args ? that.args && argumentsEquals(this.args, that.args) : !that.args)
|
|
288
357
|
&& haveSameDirectives(this, that);
|
|
289
358
|
}
|
|
290
359
|
|
|
291
360
|
toString(): string {
|
|
292
361
|
const alias = this.alias ? this.alias + ': ' : '';
|
|
293
|
-
const entries = Object.entries(this.args);
|
|
294
|
-
const args = entries.length
|
|
362
|
+
const entries = this.args ? Object.entries(this.args) : [];
|
|
363
|
+
const args = entries.length === 0
|
|
295
364
|
? ''
|
|
296
365
|
: '(' + entries.map(([n, v]) => `${n}: ${valueToString(v, this.definition.argument(n)?.type)}`).join(', ') + ')';
|
|
297
366
|
return alias + this.name + args + this.appliedDirectivesToString();
|
|
298
367
|
}
|
|
299
368
|
}
|
|
300
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Computes a string key representing a directive application, so that if 2 directive applications have the same key, then they
|
|
372
|
+
* represent the same application.
|
|
373
|
+
*
|
|
374
|
+
* Note that this is mostly just the `toString` representation of the directive, but for 2 subtlety:
|
|
375
|
+
* 1. for a handful of directives (really just `@defer` for now), we never want to consider directive applications the same, no
|
|
376
|
+
* matter that the arguments of the directive match, and this for the same reason as documented on the `sameDirectiveApplications`
|
|
377
|
+
* method in `definitions.ts`.
|
|
378
|
+
* 2. we sort the argument (by their name) before converting them to string, since argument order does not matter in graphQL.
|
|
379
|
+
*/
|
|
380
|
+
function keyForDirective(
|
|
381
|
+
directive: Directive<OperationElement>,
|
|
382
|
+
directivesNeverEqualToThemselves: string[] = [ 'defer' ],
|
|
383
|
+
): string {
|
|
384
|
+
if (directivesNeverEqualToThemselves.includes(directive.name)) {
|
|
385
|
+
return uuidv1();
|
|
386
|
+
}
|
|
387
|
+
const entries = Object.entries(directive.arguments()).filter(([_, v]) => v !== undefined);
|
|
388
|
+
entries.sort(([n1], [n2]) => n1.localeCompare(n2));
|
|
389
|
+
const args = entries.length == 0 ? '' : '(' + entries.map(([n, v]) => `${n}: ${valueToString(v, directive.argumentType(n))}`).join(', ') + ')';
|
|
390
|
+
return `@${directive.name}${args}`;
|
|
391
|
+
}
|
|
392
|
+
|
|
301
393
|
export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
302
394
|
readonly kind = 'FragmentElement' as const;
|
|
303
395
|
readonly typeCondition?: CompositeType;
|
|
396
|
+
private computedKey: string | undefined;
|
|
304
397
|
|
|
305
398
|
constructor(
|
|
306
399
|
private readonly sourceType: CompositeType,
|
|
307
400
|
typeCondition?: string | CompositeType,
|
|
401
|
+
directives?: readonly Directive<any>[],
|
|
308
402
|
) {
|
|
309
403
|
// TODO: we should do some validation here (remove the ! with proper error, and ensure we have some intersection between
|
|
310
404
|
// the source type and the type condition)
|
|
311
|
-
super(sourceType.schema(),
|
|
405
|
+
super(sourceType.schema(), directives);
|
|
312
406
|
this.typeCondition = typeCondition !== undefined && typeof typeCondition === 'string'
|
|
313
407
|
? this.schema().type(typeCondition)! as CompositeType
|
|
314
408
|
: typeCondition;
|
|
315
409
|
}
|
|
316
410
|
|
|
411
|
+
protected collectVariablesInElement(_: VariableCollector): void {
|
|
412
|
+
// Cannot have variables in fragments
|
|
413
|
+
}
|
|
414
|
+
|
|
317
415
|
get parentType(): CompositeType {
|
|
318
416
|
return this.sourceType;
|
|
319
417
|
}
|
|
320
418
|
|
|
419
|
+
key(): string {
|
|
420
|
+
if (!this.computedKey) {
|
|
421
|
+
// The key is such that 2 fragments with the same key within a selection set gets merged together. So the type-condition
|
|
422
|
+
// is include, but so are the directives.
|
|
423
|
+
const keyForDirectives = this.appliedDirectives.map((d) => keyForDirective(d)).join(' ');
|
|
424
|
+
this.computedKey = '...' + (this.typeCondition ? ' on ' + this.typeCondition.name : '') + keyForDirectives;
|
|
425
|
+
}
|
|
426
|
+
return this.computedKey;
|
|
427
|
+
}
|
|
428
|
+
|
|
321
429
|
castedType(): CompositeType {
|
|
322
430
|
return this.typeCondition ? this.typeCondition : this.sourceType;
|
|
323
431
|
}
|
|
324
432
|
|
|
433
|
+
asPathElement(): string | undefined {
|
|
434
|
+
const condition = this.typeCondition;
|
|
435
|
+
return condition ? `... on ${condition}` : undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
325
438
|
withUpdatedSourceType(newSourceType: CompositeType): FragmentElement {
|
|
326
439
|
return this.withUpdatedTypes(newSourceType, this.typeCondition);
|
|
327
440
|
}
|
|
@@ -334,34 +447,33 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
|
334
447
|
// Note that we pass the type-condition name instead of the type itself, to ensure that if `newSourceType` was from a different
|
|
335
448
|
// schema (typically, the supergraph) than `this.sourceType` (typically, a subgraph), then the new condition uses the
|
|
336
449
|
// 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
|
-
}
|
|
450
|
+
const newFragment = new FragmentElement(newSourceType, newCondition?.name, this.appliedDirectives);
|
|
341
451
|
this.copyAttachementsTo(newFragment);
|
|
342
452
|
return newFragment;
|
|
343
453
|
}
|
|
344
454
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
455
|
+
withUpdatedDirectives(newDirectives: Directive<OperationElement>[]): FragmentElement {
|
|
456
|
+
const newFragment = new FragmentElement(this.sourceType, this.typeCondition, newDirectives);
|
|
457
|
+
this.copyAttachementsTo(newFragment);
|
|
458
|
+
return newFragment;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
rebaseOn(parentType: CompositeType): FragmentElement{
|
|
350
462
|
const fragmentParent = this.parentType;
|
|
351
463
|
const typeCondition = this.typeCondition;
|
|
352
|
-
if (
|
|
464
|
+
if (parentType === fragmentParent) {
|
|
353
465
|
return this;
|
|
354
466
|
}
|
|
355
467
|
|
|
356
468
|
// This usually imply that the fragment is not from the same sugraph than then selection. So we need
|
|
357
469
|
// to update the source type of the fragment, but also "rebase" the condition to the selection set
|
|
358
470
|
// schema.
|
|
359
|
-
const { canRebase, rebasedCondition } = this.canRebaseOn(
|
|
471
|
+
const { canRebase, rebasedCondition } = this.canRebaseOn(parentType);
|
|
360
472
|
validate(
|
|
361
|
-
canRebase,
|
|
362
|
-
() => `Cannot add fragment of condition "${typeCondition}" (runtimes: [${possibleRuntimeTypes(typeCondition!)}]) to
|
|
473
|
+
canRebase,
|
|
474
|
+
() => `Cannot add fragment of condition "${typeCondition}" (runtimes: [${possibleRuntimeTypes(typeCondition!)}]) to parent type "${parentType}" (runtimes: ${possibleRuntimeTypes(parentType)})`
|
|
363
475
|
);
|
|
364
|
-
return this.withUpdatedTypes(
|
|
476
|
+
return this.withUpdatedTypes(parentType, rebasedCondition);
|
|
365
477
|
}
|
|
366
478
|
|
|
367
479
|
private canRebaseOn(parentType: CompositeType): { canRebase: boolean, rebasedCondition?: CompositeType } {
|
|
@@ -417,9 +529,8 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
|
417
529
|
return this;
|
|
418
530
|
}
|
|
419
531
|
|
|
420
|
-
const updated = new FragmentElement(this.sourceType, this.typeCondition);
|
|
532
|
+
const updated = new FragmentElement(this.sourceType, this.typeCondition, updatedDirectives);
|
|
421
533
|
this.copyAttachementsTo(updated);
|
|
422
|
-
updatedDirectives.forEach((d) => updated.applyDirective(d.definition!, d.arguments()));
|
|
423
534
|
return updated;
|
|
424
535
|
}
|
|
425
536
|
|
|
@@ -478,13 +589,13 @@ export class FragmentElement extends AbstractOperationElement<FragmentElement> {
|
|
|
478
589
|
return this;
|
|
479
590
|
}
|
|
480
591
|
|
|
481
|
-
const updated = new FragmentElement(this.sourceType, this.typeCondition);
|
|
482
|
-
this.copyAttachementsTo(updated);
|
|
483
592
|
const deferDirective = this.schema().deferDirective();
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
593
|
+
const updatedDirectives = this.appliedDirectives
|
|
594
|
+
.filter((d) => d.name !== deferDirective.name)
|
|
595
|
+
.concat(new Directive<FragmentElement>(deferDirective.name, newDeferArgs));
|
|
596
|
+
|
|
597
|
+
const updated = new FragmentElement(this.sourceType, this.typeCondition, updatedDirectives);
|
|
598
|
+
this.copyAttachementsTo(updated);
|
|
488
599
|
return updated;
|
|
489
600
|
}
|
|
490
601
|
|
|
@@ -618,13 +729,15 @@ export class Operation {
|
|
|
618
729
|
// `expandFragments` on _only_ unused fragments and that case could be dealt with more efficiently, but
|
|
619
730
|
// probably not noticeable in practice so ...).
|
|
620
731
|
const toDeoptimize = mapEntries(usages).filter(([_, count]) => count < minUsagesToOptimize).map(([name]) => name);
|
|
621
|
-
|
|
732
|
+
|
|
733
|
+
const newFragments = optimizedSelection.fragments?.without(toDeoptimize);
|
|
734
|
+
optimizedSelection = optimizedSelection.expandFragments(toDeoptimize, newFragments);
|
|
622
735
|
|
|
623
736
|
return new Operation(this.schema, this.rootKind, optimizedSelection, this.variableDefinitions, this.name);
|
|
624
737
|
}
|
|
625
738
|
|
|
626
739
|
expandAllFragments(): Operation {
|
|
627
|
-
const expandedSelections = this.selectionSet.
|
|
740
|
+
const expandedSelections = this.selectionSet.expandAllFragments();
|
|
628
741
|
if (expandedSelections === this.selectionSet) {
|
|
629
742
|
return this;
|
|
630
743
|
}
|
|
@@ -638,6 +751,21 @@ export class Operation {
|
|
|
638
751
|
);
|
|
639
752
|
}
|
|
640
753
|
|
|
754
|
+
trimUnsatisfiableBranches(): Operation {
|
|
755
|
+
const trimmedSelections = this.selectionSet.trimUnsatisfiableBranches(this.selectionSet.parentType);
|
|
756
|
+
if (trimmedSelections === this.selectionSet) {
|
|
757
|
+
return this;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return new Operation(
|
|
761
|
+
this.schema,
|
|
762
|
+
this.rootKind,
|
|
763
|
+
trimmedSelections,
|
|
764
|
+
this.variableDefinitions,
|
|
765
|
+
this.name
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
641
769
|
/**
|
|
642
770
|
* Returns this operation but potentially modified so all/some of the @defer applications have been removed.
|
|
643
771
|
*
|
|
@@ -708,40 +836,31 @@ export class Operation {
|
|
|
708
836
|
}
|
|
709
837
|
}
|
|
710
838
|
|
|
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
839
|
export class NamedFragmentDefinition extends DirectiveTargetElement<NamedFragmentDefinition> {
|
|
840
|
+
private _selectionSet: SelectionSet | undefined;
|
|
841
|
+
|
|
730
842
|
constructor(
|
|
731
843
|
schema: Schema,
|
|
732
844
|
readonly name: string,
|
|
733
845
|
readonly typeCondition: CompositeType,
|
|
734
|
-
|
|
846
|
+
directives?: Directive<NamedFragmentDefinition>[],
|
|
735
847
|
) {
|
|
736
|
-
super(schema);
|
|
848
|
+
super(schema, directives);
|
|
737
849
|
}
|
|
738
850
|
|
|
739
|
-
|
|
740
|
-
|
|
851
|
+
setSelectionSet(selectionSet: SelectionSet): NamedFragmentDefinition {
|
|
852
|
+
assert(!this._selectionSet, 'Attempting to set the selection set of a fragment definition already built')
|
|
853
|
+
this._selectionSet = selectionSet;
|
|
854
|
+
return this;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
get selectionSet(): SelectionSet {
|
|
858
|
+
assert(this._selectionSet, () => `Trying to access fragment definition ${this.name} before it is fully built`);
|
|
859
|
+
return this._selectionSet;
|
|
741
860
|
}
|
|
742
861
|
|
|
743
|
-
|
|
744
|
-
return
|
|
862
|
+
withUpdatedSelectionSet(newSelectionSet: SelectionSet): NamedFragmentDefinition {
|
|
863
|
+
return new NamedFragmentDefinition(this.schema(), this.name, this.typeCondition).setSelectionSet(newSelectionSet);
|
|
745
864
|
}
|
|
746
865
|
|
|
747
866
|
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
@@ -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
|
-
for (const selection of this.selections()) {
|
|
1099
|
-
optimized.add(selection.optimize(fragments));
|
|
1100
|
-
}
|
|
1101
|
-
return optimized;
|
|
1152
|
+
return this.lazyMap((selection) => selection.optimize(fragments), { fragments });
|
|
1102
1153
|
}
|
|
1103
1154
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1155
|
+
expandAllFragments(): SelectionSet {
|
|
1156
|
+
return this.lazyMap((selection) => selection.expandAllFragments(), { fragments: null });
|
|
1157
|
+
}
|
|
1158
|
+
|
|
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,156 +1240,17 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1179
1240
|
return updated.isEmpty() ? undefined : updated;
|
|
1180
1241
|
}
|
|
1181
1242
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
/**
|
|
1189
|
-
* Adds the selections of the provided selection set to this selection, merging common selection as necessary.
|
|
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);
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
|
|
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;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
/**
|
|
1214
|
-
* Adds the provided selection to this selection, merging it to any existing selection of this set as appropriate.
|
|
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
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
this._selections.add(key, toAdd);
|
|
1239
|
-
++this._selectionCount;
|
|
1240
|
-
this._cachedSelections = undefined;
|
|
1241
|
-
return toAdd;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
/**
|
|
1245
|
-
* If this selection contains a selection of a field with provided response name at top level, removes it.
|
|
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}`);
|
|
1252
|
-
|
|
1253
|
-
const wasRemoved = this._selections.delete(responseName);
|
|
1254
|
-
if (wasRemoved) {
|
|
1255
|
-
--this._selectionCount;
|
|
1256
|
-
this._cachedSelections = undefined;
|
|
1257
|
-
}
|
|
1258
|
-
return wasRemoved;
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
addPath(path: OperationPath, onPathEnd?: (finalSelectionSet: SelectionSet | undefined) => void) {
|
|
1262
|
-
let previousSelections: SelectionSet = this;
|
|
1263
|
-
let currentSelections: SelectionSet | undefined = this;
|
|
1264
|
-
for (const element of path) {
|
|
1265
|
-
validate(currentSelections, () => `Cannot apply selection ${element} to non-selectable parent type "${previousSelections.parentType}"`);
|
|
1266
|
-
const mergedSelection: Selection = currentSelections.add(selectionOfElement(element));
|
|
1267
|
-
previousSelections = currentSelections;
|
|
1268
|
-
currentSelections = mergedSelection.selectionSet;
|
|
1269
|
-
}
|
|
1270
|
-
if (onPathEnd) {
|
|
1271
|
-
onPathEnd(currentSelections);
|
|
1243
|
+
rebaseOn(parentType: CompositeType): SelectionSet {
|
|
1244
|
+
if (this.parentType === parentType) {
|
|
1245
|
+
return this;
|
|
1272
1246
|
}
|
|
1273
|
-
}
|
|
1274
1247
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
fieldAccessor: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined = (type, name) => type.field(name)
|
|
1279
|
-
) {
|
|
1280
|
-
if (!node) {
|
|
1281
|
-
return;
|
|
1282
|
-
}
|
|
1283
|
-
for (const selectionNode of node.selections) {
|
|
1284
|
-
this.addSelectionNode(selectionNode, variableDefinitions, fieldAccessor);
|
|
1248
|
+
const newSelections = new Map<string, Selection>();
|
|
1249
|
+
for (const selection of this.selections()) {
|
|
1250
|
+
newSelections.set(selection.key(), selection.rebaseOn(parentType));
|
|
1285
1251
|
}
|
|
1286
|
-
}
|
|
1287
1252
|
|
|
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;
|
|
1329
|
-
}
|
|
1330
|
-
addDirectiveNodesToElement(node.directives, selection.element());
|
|
1331
|
-
return selection;
|
|
1253
|
+
return new SelectionSet(parentType, newSelections, this.fragments);
|
|
1332
1254
|
}
|
|
1333
1255
|
|
|
1334
1256
|
equals(that: SelectionSet): boolean {
|
|
@@ -1336,33 +1258,27 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1336
1258
|
return true;
|
|
1337
1259
|
}
|
|
1338
1260
|
|
|
1339
|
-
if (this._selections.
|
|
1261
|
+
if (this._selections.length !== that._selections.length) {
|
|
1340
1262
|
return false;
|
|
1341
1263
|
}
|
|
1342
1264
|
|
|
1343
|
-
for (const [key,
|
|
1344
|
-
const
|
|
1345
|
-
if (!
|
|
1346
|
-
|
|
1347
|
-
|| !thisSelections.every(thisSelection => thatSelections.some(thatSelection => thisSelection.equals(thatSelection)))
|
|
1348
|
-
) {
|
|
1349
|
-
return false
|
|
1265
|
+
for (const [key, thisSelection] of this._keyedSelections) {
|
|
1266
|
+
const thatSelection = that._keyedSelections.get(key);
|
|
1267
|
+
if (!thatSelection || !thisSelection.equals(thatSelection)) {
|
|
1268
|
+
return false;
|
|
1350
1269
|
}
|
|
1351
1270
|
}
|
|
1352
1271
|
return true;
|
|
1353
1272
|
}
|
|
1354
1273
|
|
|
1355
1274
|
contains(that: SelectionSet): boolean {
|
|
1356
|
-
if (this._selections.
|
|
1275
|
+
if (this._selections.length < that._selections.length) {
|
|
1357
1276
|
return false;
|
|
1358
1277
|
}
|
|
1359
1278
|
|
|
1360
|
-
for (const [key,
|
|
1361
|
-
const
|
|
1362
|
-
if (!
|
|
1363
|
-
|| (thisSelections.length < thatSelections.length
|
|
1364
|
-
|| !thatSelections.every(thatSelection => thisSelections.some(thisSelection => thisSelection.contains(thatSelection))))
|
|
1365
|
-
) {
|
|
1279
|
+
for (const [key, thatSelection] of that._keyedSelections) {
|
|
1280
|
+
const thisSelection = this._keyedSelections.get(key);
|
|
1281
|
+
if (!thisSelection || !thisSelection.contains(thatSelection)) {
|
|
1366
1282
|
return false
|
|
1367
1283
|
}
|
|
1368
1284
|
}
|
|
@@ -1374,45 +1290,39 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1374
1290
|
* provided selection set have been remove.
|
|
1375
1291
|
*/
|
|
1376
1292
|
minus(that: SelectionSet): SelectionSet {
|
|
1377
|
-
const updated = new
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1293
|
+
const updated = new SelectionSetUpdates();
|
|
1294
|
+
|
|
1295
|
+
for (const [key, thisSelection] of this._keyedSelections) {
|
|
1296
|
+
const thatSelection = that._keyedSelections.get(key);
|
|
1297
|
+
if (!thatSelection) {
|
|
1298
|
+
updated.add(thisSelection);
|
|
1382
1299
|
} else {
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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);
|
|
1300
|
+
// If there is a subset, then we compute the diff of the subset and add that (if not empty).
|
|
1301
|
+
// Otherwise, we just skip `thisSelection` and do nothing
|
|
1302
|
+
if (thisSelection.selectionSet && thatSelection.selectionSet) {
|
|
1303
|
+
const updatedSubSelectionSet = thisSelection.selectionSet.minus(thatSelection.selectionSet);
|
|
1304
|
+
if (!updatedSubSelectionSet.isEmpty()) {
|
|
1305
|
+
updated.add(thisSelection.withUpdatedSelectionSet(updatedSubSelectionSet));
|
|
1396
1306
|
}
|
|
1397
1307
|
}
|
|
1398
1308
|
}
|
|
1399
1309
|
}
|
|
1400
|
-
return updated;
|
|
1310
|
+
return updated.toSelectionSet(this.parentType, this.fragments);
|
|
1401
1311
|
}
|
|
1402
1312
|
|
|
1403
1313
|
canRebaseOn(parentTypeToTest: CompositeType): boolean {
|
|
1404
1314
|
return this.selections().every((selection) => selection.canAddTo(parentTypeToTest));
|
|
1405
1315
|
}
|
|
1406
1316
|
|
|
1407
|
-
validate() {
|
|
1317
|
+
validate(variableDefinitions: VariableDefinitions) {
|
|
1408
1318
|
validate(!this.isEmpty(), () => `Invalid empty selection set`);
|
|
1409
1319
|
for (const selection of this.selections()) {
|
|
1410
|
-
selection.validate();
|
|
1320
|
+
selection.validate(variableDefinitions);
|
|
1411
1321
|
}
|
|
1412
1322
|
}
|
|
1413
1323
|
|
|
1414
1324
|
isEmpty(): boolean {
|
|
1415
|
-
return this._selections.
|
|
1325
|
+
return this._selections.length === 0;
|
|
1416
1326
|
}
|
|
1417
1327
|
|
|
1418
1328
|
toSelectionSetNode(): SelectionSetNode {
|
|
@@ -1445,13 +1355,12 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1445
1355
|
// By default, we will print the selection the order in which things were added to it.
|
|
1446
1356
|
// If __typename is selected however, we put it first. It's a detail but as __typename is a bit special it looks better,
|
|
1447
1357
|
// 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;
|
|
1358
|
+
const isNonAliasedTypenameSelection = (s: Selection) => s.kind === 'FieldSelection' && !s.element.alias && s.element.name === typenameFieldName;
|
|
1359
|
+
const typenameSelection = this._selections.find((s) => isNonAliasedTypenameSelection(s));
|
|
1451
1360
|
if (typenameSelection) {
|
|
1452
|
-
return typenameSelection.concat(this.selections().filter(s => !isNonAliasedTypenameSelection(s)));
|
|
1361
|
+
return [typenameSelection].concat(this.selections().filter(s => !isNonAliasedTypenameSelection(s)));
|
|
1453
1362
|
} else {
|
|
1454
|
-
return this.
|
|
1363
|
+
return this._selections;
|
|
1455
1364
|
}
|
|
1456
1365
|
}
|
|
1457
1366
|
|
|
@@ -1461,7 +1370,7 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1461
1370
|
|
|
1462
1371
|
private toOperationPathsInternal(parentPaths: OperationPath[]): OperationPath[] {
|
|
1463
1372
|
return this.selections().flatMap((selection) => {
|
|
1464
|
-
const updatedPaths = parentPaths.map(path => path.concat(selection.element
|
|
1373
|
+
const updatedPaths = parentPaths.map(path => path.concat(selection.element));
|
|
1465
1374
|
return selection.selectionSet
|
|
1466
1375
|
? selection.selectionSet.toOperationPathsInternal(updatedPaths)
|
|
1467
1376
|
: updatedPaths;
|
|
@@ -1470,29 +1379,28 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1470
1379
|
|
|
1471
1380
|
/**
|
|
1472
1381
|
* Calls the provided callback on all the "elements" (including nested ones) of this selection set.
|
|
1473
|
-
* The
|
|
1382
|
+
* The order of traversal is that of the selection set.
|
|
1474
1383
|
*/
|
|
1475
1384
|
forEachElement(callback: (elt: OperationElement) => void) {
|
|
1476
|
-
|
|
1385
|
+
// Note: we reverse to preserve ordering (since the stack re-reverse).
|
|
1386
|
+
const stack = this.selectionsInReverseOrder().concat();
|
|
1477
1387
|
while (stack.length > 0) {
|
|
1478
1388
|
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));
|
|
1389
|
+
callback(selection.element);
|
|
1390
|
+
selection.selectionSet?.selectionsInReverseOrder().forEach((s) => stack.push(s));
|
|
1483
1391
|
}
|
|
1484
1392
|
}
|
|
1485
1393
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1394
|
+
/**
|
|
1395
|
+
* Returns true if any of the element in this selection set matches the provided predicate.
|
|
1396
|
+
*/
|
|
1397
|
+
some(predicate: (elt: OperationElement) => boolean): boolean {
|
|
1488
1398
|
for (const selection of this.selections()) {
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
cloned._selections.add(clonedSelection.key(), clonedSelection);
|
|
1493
|
-
++cloned._selectionCount;
|
|
1399
|
+
if (predicate(selection.element) || (selection.selectionSet && selection.selectionSet.some(predicate))) {
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1494
1402
|
}
|
|
1495
|
-
return
|
|
1403
|
+
return false;
|
|
1496
1404
|
}
|
|
1497
1405
|
|
|
1498
1406
|
toOperationString(
|
|
@@ -1528,6 +1436,10 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1528
1436
|
includeExternalBrackets: boolean = true,
|
|
1529
1437
|
indent?: string
|
|
1530
1438
|
): string {
|
|
1439
|
+
if (this.isEmpty()) {
|
|
1440
|
+
return '{}';
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1531
1443
|
if (indent === undefined) {
|
|
1532
1444
|
const selectionsToString = this.selections().map(s => s.toString(expandFragments)).join(' ');
|
|
1533
1445
|
return includeExternalBrackets ? '{ ' + selectionsToString + ' }' : selectionsToString;
|
|
@@ -1541,76 +1453,420 @@ export class SelectionSet extends Freezable<SelectionSet> {
|
|
|
1541
1453
|
}
|
|
1542
1454
|
}
|
|
1543
1455
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
const allFields: FieldDefinition<CompositeType>[] = [];
|
|
1547
|
-
while (stack.length > 0) {
|
|
1548
|
-
const selection = stack.pop()!;
|
|
1549
|
-
if (selection.kind === 'FieldSelection') {
|
|
1550
|
-
allFields.push(selection.field.definition);
|
|
1551
|
-
}
|
|
1552
|
-
if (selection.selectionSet) {
|
|
1553
|
-
stack.push(...selection.selectionSet.selections());
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
return allFields;
|
|
1557
|
-
}
|
|
1456
|
+
type PathBasedUpdate = { path: OperationPath, selections?: Selection | SelectionSet | readonly Selection[] };
|
|
1457
|
+
type SelectionUpdate = Selection | PathBasedUpdate;
|
|
1558
1458
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1459
|
+
/**
|
|
1460
|
+
* Accumulates updates in order to build a new `SelectionSet`.
|
|
1461
|
+
*/
|
|
1462
|
+
export class SelectionSetUpdates {
|
|
1463
|
+
private readonly keyedUpdates = new MultiMap<string, SelectionUpdate>;
|
|
1564
1464
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1465
|
+
isEmpty(): boolean {
|
|
1466
|
+
return this.keyedUpdates.size === 0;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Adds the provided selections to those updates.
|
|
1471
|
+
*/
|
|
1472
|
+
add(selections: Selection | SelectionSet | readonly Selection[]): SelectionSetUpdates {
|
|
1473
|
+
addToKeyedUpdates(this.keyedUpdates, selections);
|
|
1474
|
+
return this;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Adds a path, and optional some selections following that path, to those updates.
|
|
1479
|
+
*
|
|
1480
|
+
* The final selections are optional (for instance, if `path` ends on a leaf field, then no followup selections would
|
|
1481
|
+
* make sense), but when some are provided, uncesssary fragments will be automaticaly removed at the junction between
|
|
1482
|
+
* the path and those final selections. For instance, suppose that we have:
|
|
1483
|
+
* - a `path` argument that is `a::b::c`, where the type of the last field `c` is some object type `C`.
|
|
1484
|
+
* - a `selections` argument that is `{ ... on C { d } }`.
|
|
1485
|
+
* Then the resulting built selection set will be: `{ a { b { c { d } } }`, and in particular the `... on C` fragment
|
|
1486
|
+
* will be eliminated since it is unecesasry (since again, `c` is of type `C`).
|
|
1487
|
+
*/
|
|
1488
|
+
addAtPath(path: OperationPath, selections?: Selection | SelectionSet | readonly Selection[]): SelectionSetUpdates {
|
|
1489
|
+
if (path.length === 0) {
|
|
1490
|
+
if (selections) {
|
|
1491
|
+
addToKeyedUpdates(this.keyedUpdates, selections)
|
|
1492
|
+
}
|
|
1493
|
+
} else {
|
|
1494
|
+
if (path.length === 1 && !selections) {
|
|
1495
|
+
const element = path[0];
|
|
1496
|
+
if (element.kind === 'Field' && element.isLeafField()) {
|
|
1497
|
+
// This is a somewhat common case (when we deal with @key "conditions", those are often trivial and end up here),
|
|
1498
|
+
// so we unpack it directly instead of creating unecessary temporary objects (not that we only do it for leaf
|
|
1499
|
+
// field; for non-leaf ones, we'd have to create an empty sub-selectionSet, and that may have to get merged
|
|
1500
|
+
// with other entries of this `SleectionSetUpdates`, so we wouldn't really save work).
|
|
1501
|
+
const selection = selectionOfElement(element);
|
|
1502
|
+
this.keyedUpdates.add(selection.key(), selection);
|
|
1503
|
+
return this;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
// We store the provided update "as is" (we don't convert it to a `Selection` just yet) and process everything
|
|
1507
|
+
// when we build the final `SelectionSet`. This is done because multipe different updates can intersect in various
|
|
1508
|
+
// ways, and the work to build a `Selection` now could be largely wasted due to followup updates.
|
|
1509
|
+
this.keyedUpdates.add(path[0].key(), { path, selections });
|
|
1510
|
+
}
|
|
1511
|
+
return this;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
clone(): SelectionSetUpdates {
|
|
1515
|
+
const cloned = new SelectionSetUpdates();
|
|
1516
|
+
for (const [key, values] of this.keyedUpdates.entries()) {
|
|
1517
|
+
cloned.keyedUpdates.set(key, Array.from(values));
|
|
1518
|
+
}
|
|
1519
|
+
return cloned;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
clear() {
|
|
1523
|
+
this.keyedUpdates.clear();
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
toSelectionSet(parentType: CompositeType, fragments?: NamedFragments): SelectionSet {
|
|
1527
|
+
return makeSelectionSet(parentType, this.keyedUpdates, fragments);
|
|
1528
|
+
}
|
|
1567
1529
|
}
|
|
1568
1530
|
|
|
1569
|
-
|
|
1531
|
+
function addToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, selections: Selection | SelectionSet | readonly Selection[]) {
|
|
1532
|
+
if (selections instanceof AbstractSelection) {
|
|
1533
|
+
addOneToKeyedUpdates(keyedUpdates, selections);
|
|
1534
|
+
} else {
|
|
1535
|
+
const toAdd = selections instanceof SelectionSet ? selections.selections() : selections;
|
|
1536
|
+
for (const selection of toAdd) {
|
|
1537
|
+
addOneToKeyedUpdates(keyedUpdates, selection);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1570
1541
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1542
|
+
function addOneToKeyedUpdates(keyedUpdates: MultiMap<string, SelectionUpdate>, selection: Selection) {
|
|
1543
|
+
// Keys are such that for a named fragment, only a selection of the same fragment with same directives can have the same key.
|
|
1544
|
+
// 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
|
|
1545
|
+
// each, as it would expand the fragments and make things harder. So we essentially special case spreads to avoid having
|
|
1546
|
+
// to deal with multiple time the exact same one.
|
|
1547
|
+
if (selection instanceof FragmentSpreadSelection) {
|
|
1548
|
+
keyedUpdates.set(selection.key(), [selection]);
|
|
1549
|
+
} else {
|
|
1550
|
+
keyedUpdates.add(selection.key(), selection);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function isUnecessaryFragment(parentType: CompositeType, fragment: FragmentSelection): boolean {
|
|
1555
|
+
return fragment.element.appliedDirectives.length === 0
|
|
1556
|
+
&& (!fragment.element.typeCondition || sameType(parentType, fragment.element.typeCondition));
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function withUnecessaryFragmentsRemoved(
|
|
1560
|
+
parentType: CompositeType,
|
|
1561
|
+
selections: Selection | SelectionSet | readonly Selection[],
|
|
1562
|
+
): Selection | readonly Selection[] {
|
|
1563
|
+
if (selections instanceof AbstractSelection) {
|
|
1564
|
+
if (selections.kind !== 'FragmentSelection' || !isUnecessaryFragment(parentType, selections)) {
|
|
1565
|
+
return selections;
|
|
1566
|
+
}
|
|
1567
|
+
return withUnecessaryFragmentsRemoved(parentType, selections.selectionSet);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
const toCheck = selections instanceof SelectionSet ? selections.selections() : selections;
|
|
1571
|
+
const filtered: Selection[] = [];
|
|
1572
|
+
for (const selection of toCheck) {
|
|
1573
|
+
if (selection.kind === 'FragmentSelection' && isUnecessaryFragment(parentType, selection)) {
|
|
1574
|
+
const subSelections = withUnecessaryFragmentsRemoved(parentType, selection.selectionSet);
|
|
1575
|
+
if (subSelections instanceof AbstractSelection) {
|
|
1576
|
+
filtered.push(subSelections);
|
|
1577
|
+
} else {
|
|
1578
|
+
for (const subSelection of subSelections) {
|
|
1579
|
+
filtered.push(subSelection);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
} else {
|
|
1583
|
+
filtered.push(selection);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
return filtered;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function makeSelection(parentType: CompositeType, updates: SelectionUpdate[], fragments?: NamedFragments): Selection {
|
|
1590
|
+
assert(updates.length > 0, 'Should not be called without any updates');
|
|
1591
|
+
const first = updates[0];
|
|
1592
|
+
|
|
1593
|
+
// Optimize for the simple case of a single selection, as we don't have to do anything complex to merge the sub-selections.
|
|
1594
|
+
if (updates.length === 1 && first instanceof AbstractSelection) {
|
|
1595
|
+
return first.rebaseOn(parentType);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const element = updateElement(first).rebaseOn(parentType);
|
|
1599
|
+
const subSelectionParentType = element.kind === 'Field' ? baseType(element.definition.type!) : element.castedType();
|
|
1600
|
+
if (!isCompositeType(subSelectionParentType)) {
|
|
1601
|
+
// This is a leaf, so all updates should correspond ot the same field and we just use the first.
|
|
1602
|
+
return selectionOfElement(element);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const subSelectionKeyedUpdates = new MultiMap<string, SelectionUpdate>();
|
|
1606
|
+
for (const update of updates) {
|
|
1607
|
+
if (update instanceof AbstractSelection) {
|
|
1608
|
+
if (update.selectionSet) {
|
|
1609
|
+
addToKeyedUpdates(subSelectionKeyedUpdates, update.selectionSet);
|
|
1610
|
+
}
|
|
1611
|
+
} else {
|
|
1612
|
+
addSubpathToKeyUpdates(subSelectionKeyedUpdates, subSelectionParentType, update);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
return selectionOfElement(element, makeSelectionSet(subSelectionParentType, subSelectionKeyedUpdates, fragments));
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function updateElement(update: SelectionUpdate): OperationElement {
|
|
1619
|
+
return update instanceof AbstractSelection ? update.element : update.path[0];
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function addSubpathToKeyUpdates(
|
|
1623
|
+
keyedUpdates: MultiMap<string, SelectionUpdate>,
|
|
1624
|
+
subSelectionParentType: CompositeType,
|
|
1625
|
+
pathUpdate: PathBasedUpdate
|
|
1626
|
+
) {
|
|
1627
|
+
if (pathUpdate.path.length === 1) {
|
|
1628
|
+
if (!pathUpdate.selections) {
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
addToKeyedUpdates(keyedUpdates, withUnecessaryFragmentsRemoved(subSelectionParentType, pathUpdate.selections!));
|
|
1632
|
+
} else {
|
|
1633
|
+
keyedUpdates.add(pathUpdate.path[1].key(), { path: pathUpdate.path.slice(1), selections: pathUpdate.selections });
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function makeSelectionSet(parentType: CompositeType, keyedUpdates: MultiMap<string, SelectionUpdate>, fragments?: NamedFragments): SelectionSet {
|
|
1638
|
+
const selections = new Map<string, Selection>();
|
|
1639
|
+
for (const [key, updates] of keyedUpdates.entries()) {
|
|
1640
|
+
selections.set(key, makeSelection(parentType, updates, fragments));
|
|
1641
|
+
}
|
|
1642
|
+
return new SelectionSet(parentType, selections, fragments);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* A simple wrapper over a `SelectionSetUpdates` that allows to conveniently build a selection set, then add some more updates and build it again, etc...
|
|
1647
|
+
*/
|
|
1648
|
+
export class MutableSelectionSet<TMemoizedValue extends { [key: string]: any } = {}> {
|
|
1649
|
+
private computed: SelectionSet | undefined;
|
|
1650
|
+
private _memoized: TMemoizedValue | undefined;
|
|
1651
|
+
|
|
1652
|
+
private constructor(
|
|
1653
|
+
readonly parentType: CompositeType,
|
|
1654
|
+
private readonly _updates: SelectionSetUpdates,
|
|
1655
|
+
private readonly memoizer: (s: SelectionSet) => TMemoizedValue,
|
|
1656
|
+
) {
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
static empty(parentType: CompositeType): MutableSelectionSet {
|
|
1660
|
+
return this.emptyWithMemoized(parentType, () => ({}));
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
static emptyWithMemoized<TMemoizedValue extends { [key: string]: any }>(
|
|
1664
|
+
parentType: CompositeType,
|
|
1665
|
+
memoizer: (s: SelectionSet) => TMemoizedValue,
|
|
1666
|
+
): MutableSelectionSet<TMemoizedValue> {
|
|
1667
|
+
return new MutableSelectionSet( parentType, new SelectionSetUpdates(), memoizer);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
|
|
1671
|
+
static of(selectionSet: SelectionSet): MutableSelectionSet {
|
|
1672
|
+
return this.ofWithMemoized(selectionSet, () => ({}));
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
static ofWithMemoized<TMemoizedValue extends { [key: string]: any }>(
|
|
1676
|
+
selectionSet: SelectionSet,
|
|
1677
|
+
memoizer: (s: SelectionSet) => TMemoizedValue,
|
|
1678
|
+
): MutableSelectionSet<TMemoizedValue> {
|
|
1679
|
+
const s = new MutableSelectionSet(selectionSet.parentType, new SelectionSetUpdates(), memoizer);
|
|
1680
|
+
s._updates.add(selectionSet);
|
|
1681
|
+
// Avoids needing to re-compute `selectionSet` until there is new updates.
|
|
1682
|
+
s.computed = selectionSet;
|
|
1683
|
+
return s;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
isEmpty(): boolean {
|
|
1687
|
+
return this._updates.isEmpty();
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
get(): SelectionSet {
|
|
1691
|
+
if (!this.computed) {
|
|
1692
|
+
this.computed = this._updates.toSelectionSet(this.parentType);
|
|
1693
|
+
// But now, we clear the updates an re-add the selections from computed. Of course, we could also
|
|
1694
|
+
// not clear updates at all, but that would mean that the computations going on for merging selections
|
|
1695
|
+
// would be re-done every time and that would be a lot less efficient.
|
|
1696
|
+
this._updates.clear();
|
|
1697
|
+
this._updates.add(this.computed);
|
|
1698
|
+
}
|
|
1699
|
+
return this.computed;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
updates(): SelectionSetUpdates {
|
|
1703
|
+
// We clear our cached version since we're about to add more updates and so this cached version won't
|
|
1704
|
+
// represent the mutable set properly anymore.
|
|
1705
|
+
this.computed = undefined;
|
|
1706
|
+
this._memoized = undefined;
|
|
1707
|
+
return this._updates;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
clone(): MutableSelectionSet<TMemoizedValue> {
|
|
1711
|
+
const cloned = new MutableSelectionSet(this.parentType, this._updates.clone(), this.memoizer);
|
|
1712
|
+
// Until we have more updates, we can share the computed values (if any).
|
|
1713
|
+
cloned.computed = this.computed;
|
|
1714
|
+
cloned._memoized = this._memoized;
|
|
1715
|
+
return cloned;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
rebaseOn(parentType: CompositeType): MutableSelectionSet<TMemoizedValue> {
|
|
1719
|
+
const rebased = new MutableSelectionSet(parentType, new SelectionSetUpdates(), this.memoizer);
|
|
1720
|
+
// Note that updates are always rebased on their parentType, so we won't have to call `rebaseOn` manually on `this.get()`.
|
|
1721
|
+
rebased._updates.add(this.get());
|
|
1722
|
+
return rebased;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
memoized(): TMemoizedValue {
|
|
1726
|
+
if (!this._memoized) {
|
|
1727
|
+
this._memoized = this.memoizer(this.get());
|
|
1728
|
+
}
|
|
1729
|
+
return this._memoized;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
toString() {
|
|
1733
|
+
return this.get().toString();
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1574
1736
|
|
|
1737
|
+
export function allFieldDefinitionsInSelectionSet(selection: SelectionSet): FieldDefinition<CompositeType>[] {
|
|
1738
|
+
const stack = Array.from(selection.selections());
|
|
1739
|
+
const allFields: FieldDefinition<CompositeType>[] = [];
|
|
1740
|
+
while (stack.length > 0) {
|
|
1741
|
+
const selection = stack.pop()!;
|
|
1742
|
+
if (selection.kind === 'FieldSelection') {
|
|
1743
|
+
allFields.push(selection.element.definition);
|
|
1744
|
+
}
|
|
1745
|
+
if (selection.selectionSet) {
|
|
1746
|
+
stack.push(...selection.selectionSet.selections());
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return allFields;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
export function selectionSetOf(parentType: CompositeType, selection: Selection, fragments?: NamedFragments): SelectionSet {
|
|
1753
|
+
const map = new Map<string, Selection>()
|
|
1754
|
+
map.set(selection.key(), selection);
|
|
1755
|
+
return new SelectionSet(parentType, map, fragments);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
export function selectionSetOfElement(element: OperationElement, subSelection?: SelectionSet, fragments?: NamedFragments): SelectionSet {
|
|
1759
|
+
return selectionSetOf(element.parentType, selectionOfElement(element, subSelection), fragments);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
export function selectionOfElement(element: OperationElement, subSelection?: SelectionSet): Selection {
|
|
1763
|
+
// TODO: validate that the subSelection is ok for the element
|
|
1764
|
+
return element.kind === 'Field' ? new FieldSelection(element, subSelection) : new InlineFragmentSelection(element, subSelection!);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
export type Selection = FieldSelection | FragmentSelection;
|
|
1768
|
+
|
|
1769
|
+
abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf extends undefined | never, TOwnType extends AbstractSelection<TElement, TIsLeaf, TOwnType>> {
|
|
1575
1770
|
constructor(
|
|
1576
|
-
readonly
|
|
1577
|
-
initialSelectionSet? : SelectionSet
|
|
1771
|
+
readonly element: TElement,
|
|
1578
1772
|
) {
|
|
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));
|
|
1773
|
+
// TODO: we should do validate the type of the selection set matches the element.
|
|
1583
1774
|
}
|
|
1584
1775
|
|
|
1776
|
+
abstract get selectionSet(): SelectionSet | TIsLeaf;
|
|
1777
|
+
|
|
1778
|
+
protected abstract us(): TOwnType;
|
|
1779
|
+
|
|
1780
|
+
abstract key(): string;
|
|
1781
|
+
|
|
1782
|
+
abstract optimize(fragments: NamedFragments): Selection;
|
|
1783
|
+
|
|
1784
|
+
abstract toSelectionNode(): SelectionNode;
|
|
1785
|
+
|
|
1786
|
+
abstract validate(variableDefinitions: VariableDefinitions): void;
|
|
1787
|
+
|
|
1788
|
+
abstract rebaseOn(parentType: CompositeType): TOwnType;
|
|
1789
|
+
|
|
1585
1790
|
get parentType(): CompositeType {
|
|
1586
|
-
return this.
|
|
1791
|
+
return this.element.parentType;
|
|
1587
1792
|
}
|
|
1588
1793
|
|
|
1589
|
-
|
|
1590
|
-
|
|
1794
|
+
collectVariables(collector: VariableCollector) {
|
|
1795
|
+
this.element.collectVariables(collector);
|
|
1796
|
+
this.selectionSet?.collectVariables(collector)
|
|
1591
1797
|
}
|
|
1592
1798
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1799
|
+
collectUsedFragmentNames(collector: Map<string, number>) {
|
|
1800
|
+
this.selectionSet?.collectUsedFragmentNames(collector);
|
|
1595
1801
|
}
|
|
1596
1802
|
|
|
1597
|
-
|
|
1598
|
-
return this.
|
|
1803
|
+
namedFragments(): NamedFragments | undefined {
|
|
1804
|
+
return this.selectionSet?.fragments;
|
|
1599
1805
|
}
|
|
1600
1806
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1807
|
+
abstract withUpdatedComponents(element: TElement, selectionSet: SelectionSet | TIsLeaf): TOwnType;
|
|
1808
|
+
|
|
1809
|
+
withUpdatedSelectionSet(selectionSet: SelectionSet | TIsLeaf): TOwnType {
|
|
1810
|
+
return this.withUpdatedComponents(this.element, selectionSet);
|
|
1603
1811
|
}
|
|
1604
1812
|
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1813
|
+
withUpdatedElement(element: TElement): TOwnType {
|
|
1814
|
+
return this.withUpdatedComponents(element, this.selectionSet);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
mapToSelectionSet(mapper: (s: SelectionSet) => SelectionSet): TOwnType {
|
|
1818
|
+
if (!this.selectionSet) {
|
|
1819
|
+
return this.us();
|
|
1608
1820
|
}
|
|
1821
|
+
|
|
1822
|
+
const updatedSelectionSet = mapper(this.selectionSet);
|
|
1823
|
+
return updatedSelectionSet === this.selectionSet
|
|
1824
|
+
? this.us()
|
|
1825
|
+
: this.withUpdatedSelectionSet(updatedSelectionSet);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
abstract withoutDefer(labelsToRemove?: Set<string>): TOwnType | SelectionSet;
|
|
1829
|
+
|
|
1830
|
+
abstract withNormalizedDefer(normalizer: DeferNormalizer): TOwnType | SelectionSet;
|
|
1831
|
+
|
|
1832
|
+
abstract hasDefer(): boolean;
|
|
1833
|
+
|
|
1834
|
+
abstract expandAllFragments(): TOwnType | readonly Selection[];
|
|
1835
|
+
|
|
1836
|
+
abstract expandFragments(names: string[], updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
|
|
1837
|
+
|
|
1838
|
+
abstract trimUnsatisfiableBranches(parentType: CompositeType): TOwnType | SelectionSet | undefined;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
export class FieldSelection extends AbstractSelection<Field<any>, undefined, FieldSelection> {
|
|
1842
|
+
readonly kind = 'FieldSelection' as const;
|
|
1843
|
+
|
|
1844
|
+
constructor(
|
|
1845
|
+
field: Field<any>,
|
|
1846
|
+
private readonly _selectionSet?: SelectionSet,
|
|
1847
|
+
) {
|
|
1848
|
+
super(field);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
get selectionSet(): SelectionSet | undefined {
|
|
1852
|
+
return this._selectionSet;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
protected us(): FieldSelection {
|
|
1856
|
+
return this;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
withUpdatedComponents(field: Field<any>, selectionSet: SelectionSet | undefined): FieldSelection {
|
|
1860
|
+
return new FieldSelection(field, selectionSet);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
key(): string {
|
|
1864
|
+
return this.element.key();
|
|
1609
1865
|
}
|
|
1610
1866
|
|
|
1611
1867
|
optimize(fragments: NamedFragments): Selection {
|
|
1612
1868
|
const optimizedSelection = this.selectionSet ? this.selectionSet.optimize(fragments) : undefined;
|
|
1613
|
-
const fieldBaseType = baseType(this.
|
|
1869
|
+
const fieldBaseType = baseType(this.element.definition.type!);
|
|
1614
1870
|
if (isCompositeType(fieldBaseType) && optimizedSelection) {
|
|
1615
1871
|
for (const candidate of fragments.maybeApplyingAtType(fieldBaseType)) {
|
|
1616
1872
|
// TODO: Checking `equals` here is very simple, but somewhat restrictive in theory. That is, if a query
|
|
@@ -1640,15 +1896,15 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1640
1896
|
// To do that, we can change that `equals` to `contains`, but then we should also "extract" the remainder
|
|
1641
1897
|
// of `optimizedSelection` that isn't covered by the fragment, and that is the part slighly more involved.
|
|
1642
1898
|
if (optimizedSelection.equals(candidate.selectionSet)) {
|
|
1643
|
-
const fragmentSelection = new FragmentSpreadSelection(fieldBaseType, fragments, candidate
|
|
1644
|
-
return new FieldSelection(this.
|
|
1899
|
+
const fragmentSelection = new FragmentSpreadSelection(fieldBaseType, fragments, candidate, []);
|
|
1900
|
+
return new FieldSelection(this.element, selectionSetOf(fieldBaseType, fragmentSelection));
|
|
1645
1901
|
}
|
|
1646
1902
|
}
|
|
1647
1903
|
}
|
|
1648
1904
|
|
|
1649
1905
|
return this.selectionSet === optimizedSelection
|
|
1650
1906
|
? this
|
|
1651
|
-
: new FieldSelection(this.
|
|
1907
|
+
: new FieldSelection(this.element, optimizedSelection);
|
|
1652
1908
|
}
|
|
1653
1909
|
|
|
1654
1910
|
filter(predicate: (selection: Selection) => boolean): FieldSelection | undefined {
|
|
@@ -1659,143 +1915,127 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1659
1915
|
const updatedSelectionSet = this.selectionSet.filter(predicate);
|
|
1660
1916
|
const thisWithFilteredSelectionSet = this.selectionSet === updatedSelectionSet
|
|
1661
1917
|
? this
|
|
1662
|
-
: new FieldSelection(this.
|
|
1918
|
+
: new FieldSelection(this.element, updatedSelectionSet);
|
|
1663
1919
|
return predicate(thisWithFilteredSelectionSet) ? thisWithFilteredSelectionSet : undefined;
|
|
1664
1920
|
}
|
|
1665
1921
|
|
|
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();
|
|
1922
|
+
validate(variableDefinitions: VariableDefinitions) {
|
|
1923
|
+
this.element.validate(variableDefinitions);
|
|
1694
1924
|
// Note that validation is kind of redundant since `this.selectionSet.validate()` will check that it isn't empty. But doing it
|
|
1695
1925
|
// allow to provide much better error messages.
|
|
1696
1926
|
validate(
|
|
1697
|
-
|
|
1698
|
-
() => `Invalid empty selection set for field "${this.
|
|
1699
|
-
this.
|
|
1927
|
+
this.element.isLeafField() || (this.selectionSet && !this.selectionSet.isEmpty()),
|
|
1928
|
+
() => `Invalid empty selection set for field "${this.element.definition.coordinate}" of non-leaf type ${this.element.definition.type}`,
|
|
1929
|
+
this.element.definition.sourceAST
|
|
1700
1930
|
);
|
|
1701
|
-
this.selectionSet?.validate();
|
|
1931
|
+
this.selectionSet?.validate(variableDefinitions);
|
|
1702
1932
|
}
|
|
1703
1933
|
|
|
1704
1934
|
/**
|
|
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).
|
|
1708
|
-
*
|
|
1709
|
-
* This method assumes that such a thing is possible, meaning that the parent type of the provided
|
|
1710
|
-
* selection set does have a field that correspond to this selection (which can support any sub-selection).
|
|
1711
|
-
* If that is not the case, an assertion will be thrown.
|
|
1935
|
+
* Returns a field selection "equivalent" to the one represented by this object, but such that its parent type
|
|
1936
|
+
* is the one provided as argument.
|
|
1712
1937
|
*
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
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).
|
|
1938
|
+
* Obviously, this operation will only succeed if this selection (both the field itself and its subselections)
|
|
1939
|
+
* make sense from the provided parent type. If this is not the case, this method will throw.
|
|
1731
1940
|
*/
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
return this.cloneIfFrozen();
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
// We create a new selection that not only uses the updated field, but also ensures
|
|
1739
|
-
// the underlying selection set uses the updated field type as parent type.
|
|
1740
|
-
const updatedBaseType = baseType(updatedField.definition.type!);
|
|
1741
|
-
let updatedSelectionSet : SelectionSet | undefined;
|
|
1742
|
-
if (this.selectionSet && this.selectionSet.parentType !== updatedBaseType) {
|
|
1743
|
-
assert(isCompositeType(updatedBaseType), `Expected ${updatedBaseType.coordinate} to be composite but ${updatedBaseType.kind}`);
|
|
1744
|
-
updatedSelectionSet = new SelectionSet(updatedBaseType);
|
|
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();
|
|
1941
|
+
rebaseOn(parentType: CompositeType): FieldSelection {
|
|
1942
|
+
if (this.element.parentType === parentType) {
|
|
1943
|
+
return this;
|
|
1752
1944
|
}
|
|
1753
1945
|
|
|
1754
|
-
|
|
1946
|
+
const rebasedElement = this.element.rebaseOn(parentType);
|
|
1947
|
+
if (!this.selectionSet) {
|
|
1948
|
+
return this.withUpdatedElement(rebasedElement);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
const rebasedBase = baseType(rebasedElement.definition.type!);
|
|
1952
|
+
if (rebasedBase === this.selectionSet.parentType) {
|
|
1953
|
+
return this.withUpdatedElement(rebasedElement);
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
validate(isCompositeType(rebasedBase), () => `Cannot rebase field selection ${this} on ${parentType}: rebased field base return type ${rebasedBase} is not composite`);
|
|
1957
|
+
return this.withUpdatedComponents(rebasedElement, this.selectionSet.rebaseOn(rebasedBase));
|
|
1755
1958
|
}
|
|
1756
1959
|
|
|
1757
1960
|
/**
|
|
1758
1961
|
* Essentially checks if `updateForAddingTo` would work on an selecion set of the provide parent type.
|
|
1759
1962
|
*/
|
|
1760
1963
|
canAddTo(parentType: CompositeType): boolean {
|
|
1761
|
-
if (this.
|
|
1964
|
+
if (this.element.parentType === parentType) {
|
|
1762
1965
|
return true;
|
|
1763
1966
|
}
|
|
1764
1967
|
|
|
1765
|
-
const type = this.
|
|
1968
|
+
const type = this.element.typeIfAddedTo(parentType);
|
|
1766
1969
|
if (!type) {
|
|
1767
1970
|
return false;
|
|
1768
1971
|
}
|
|
1769
1972
|
|
|
1770
1973
|
const base = baseType(type);
|
|
1771
1974
|
if (this.selectionSet && this.selectionSet.parentType !== base) {
|
|
1772
|
-
assert(isCompositeType(base), () => `${this.
|
|
1975
|
+
assert(isCompositeType(base), () => `${this.element} should have a selection set as it's type is not a composite`);
|
|
1773
1976
|
return this.selectionSet.selections().every((s) => s.canAddTo(base));
|
|
1774
1977
|
}
|
|
1775
1978
|
return true;
|
|
1776
1979
|
}
|
|
1777
1980
|
|
|
1778
1981
|
toSelectionNode(): FieldNode {
|
|
1779
|
-
const alias: NameNode | undefined = this.
|
|
1982
|
+
const alias: NameNode | undefined = this.element.alias ? { kind: Kind.NAME, value: this.element.alias, } : undefined;
|
|
1780
1983
|
return {
|
|
1781
1984
|
kind: Kind.FIELD,
|
|
1782
1985
|
name: {
|
|
1783
1986
|
kind: Kind.NAME,
|
|
1784
|
-
value: this.
|
|
1987
|
+
value: this.element.name,
|
|
1785
1988
|
},
|
|
1786
1989
|
alias,
|
|
1787
|
-
arguments: this.
|
|
1788
|
-
directives: this.element
|
|
1990
|
+
arguments: this.element.argumentsToNodes(),
|
|
1991
|
+
directives: this.element.appliedDirectivesToDirectiveNodes(),
|
|
1789
1992
|
selectionSet: this.selectionSet?.toSelectionSetNode()
|
|
1790
1993
|
};
|
|
1791
1994
|
}
|
|
1792
1995
|
|
|
1793
|
-
|
|
1794
|
-
return
|
|
1996
|
+
withoutDefer(labelsToRemove?: Set<string>): FieldSelection {
|
|
1997
|
+
return this.mapToSelectionSet((s) => s.withoutDefer(labelsToRemove));
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
withNormalizedDefer(normalizer: DeferNormalizer): FieldSelection {
|
|
2001
|
+
return this.mapToSelectionSet((s) => s.withNormalizedDefer(normalizer));
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
hasDefer(): boolean {
|
|
2005
|
+
return !!this.selectionSet?.hasDefer();
|
|
1795
2006
|
}
|
|
1796
2007
|
|
|
1797
|
-
|
|
1798
|
-
return
|
|
2008
|
+
expandAllFragments(): FieldSelection {
|
|
2009
|
+
return this.mapToSelectionSet((s) => s.expandAllFragments());
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
trimUnsatisfiableBranches(_: CompositeType): FieldSelection {
|
|
2013
|
+
if (!this.selectionSet) {
|
|
2014
|
+
return this;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const base = baseType(this.element.definition.type!)
|
|
2018
|
+
assert(isCompositeType(base), () => `Field ${this.element} should not have a sub-selection`);
|
|
2019
|
+
const trimmed = this.mapToSelectionSet((s) => s.trimUnsatisfiableBranches(base));
|
|
2020
|
+
// In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
|
|
2021
|
+
// sub-selection is empty. Which suggest something may be wrong with this part of the query
|
|
2022
|
+
// intent, but the query was valid while keeping an empty sub-selection isn't. So in that
|
|
2023
|
+
// case, we just add some "non-included" __typename field just to keep the query valid.
|
|
2024
|
+
if (trimmed.selectionSet?.isEmpty()) {
|
|
2025
|
+
return trimmed.withUpdatedSelectionSet(selectionSetOfElement(
|
|
2026
|
+
new Field(
|
|
2027
|
+
base.typenameField()!,
|
|
2028
|
+
undefined,
|
|
2029
|
+
[new Directive('include', { 'if': false })],
|
|
2030
|
+
)
|
|
2031
|
+
));
|
|
2032
|
+
} else {
|
|
2033
|
+
return trimmed;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FieldSelection {
|
|
2038
|
+
return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
|
|
1799
2039
|
}
|
|
1800
2040
|
|
|
1801
2041
|
equals(that: Selection): boolean {
|
|
@@ -1803,7 +2043,7 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1803
2043
|
return true;
|
|
1804
2044
|
}
|
|
1805
2045
|
|
|
1806
|
-
if (!(that instanceof FieldSelection) || !this.
|
|
2046
|
+
if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
|
|
1807
2047
|
return false;
|
|
1808
2048
|
}
|
|
1809
2049
|
if (!this.selectionSet) {
|
|
@@ -1813,7 +2053,7 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1813
2053
|
}
|
|
1814
2054
|
|
|
1815
2055
|
contains(that: Selection): boolean {
|
|
1816
|
-
if (!(that instanceof FieldSelection) || !this.
|
|
2056
|
+
if (!(that instanceof FieldSelection) || !this.element.equals(that.element)) {
|
|
1817
2057
|
return false;
|
|
1818
2058
|
}
|
|
1819
2059
|
|
|
@@ -1823,106 +2063,44 @@ export class FieldSelection extends Freezable<FieldSelection> {
|
|
|
1823
2063
|
return !!this.selectionSet && this.selectionSet.contains(that.selectionSet);
|
|
1824
2064
|
}
|
|
1825
2065
|
|
|
1826
|
-
namedFragments(): NamedFragments | undefined {
|
|
1827
|
-
return this.selectionSet?.fragments;
|
|
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());
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
2066
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
1852
|
-
return (indent ?? '') + this.
|
|
2067
|
+
return (indent ?? '') + this.element + (this.selectionSet ? ' ' + this.selectionSet.toString(expandFragments, true, indent) : '');
|
|
1853
2068
|
}
|
|
1854
2069
|
}
|
|
1855
2070
|
|
|
1856
|
-
export abstract class FragmentSelection extends
|
|
2071
|
+
export abstract class FragmentSelection extends AbstractSelection<FragmentElement, never, FragmentSelection> {
|
|
1857
2072
|
readonly kind = 'FragmentSelection' as const;
|
|
1858
2073
|
|
|
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
2074
|
abstract canAddTo(parentType: CompositeType): boolean;
|
|
1887
2075
|
|
|
1888
|
-
abstract withUpdatedSubSelection(newSubSelection: SelectionSet | undefined): FragmentSelection;
|
|
1889
|
-
|
|
1890
|
-
get parentType(): CompositeType {
|
|
1891
|
-
return this.element().parentType;
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
2076
|
protected us(): FragmentSelection {
|
|
1895
2077
|
return this;
|
|
1896
2078
|
}
|
|
1897
2079
|
|
|
1898
2080
|
protected validateDeferAndStream() {
|
|
1899
|
-
if (this.element
|
|
1900
|
-
const schemaDef = this.element
|
|
1901
|
-
const parentType = this.
|
|
2081
|
+
if (this.element.hasDefer() || this.element.hasStream()) {
|
|
2082
|
+
const schemaDef = this.element.schema().schemaDefinition;
|
|
2083
|
+
const parentType = this.parentType;
|
|
1902
2084
|
validate(
|
|
1903
2085
|
schemaDef.rootType('mutation') !== parentType && schemaDef.rootType('subscription') !== parentType,
|
|
1904
2086
|
() => `The @defer and @stream directives cannot be used on ${schemaDef.roots().filter((t) => t.type === parentType).pop()?.rootKind} root type "${parentType}"`,
|
|
1905
2087
|
);
|
|
1906
2088
|
}
|
|
1907
2089
|
}
|
|
1908
|
-
|
|
1909
|
-
usedVariables(): Variables {
|
|
1910
|
-
return mergeVariables(this.element().variables(), this.selectionSet.usedVariables());
|
|
1911
|
-
}
|
|
1912
|
-
|
|
2090
|
+
|
|
1913
2091
|
filter(predicate: (selection: Selection) => boolean): FragmentSelection | undefined {
|
|
1914
2092
|
// Note that we essentially expand all fragments as part of this.
|
|
1915
2093
|
const selectionSet = this.selectionSet;
|
|
1916
2094
|
const updatedSelectionSet = selectionSet.filter(predicate);
|
|
1917
2095
|
const thisWithFilteredSelectionSet = updatedSelectionSet === selectionSet
|
|
1918
2096
|
? this
|
|
1919
|
-
: new InlineFragmentSelection(this.element
|
|
2097
|
+
: new InlineFragmentSelection(this.element, updatedSelectionSet);
|
|
1920
2098
|
|
|
1921
2099
|
return predicate(thisWithFilteredSelectionSet) ? thisWithFilteredSelectionSet : undefined;
|
|
1922
2100
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
this.selectionSet.
|
|
2101
|
+
|
|
2102
|
+
hasDefer(): boolean {
|
|
2103
|
+
return this.element.hasDefer() || this.selectionSet.hasDefer();
|
|
1926
2104
|
}
|
|
1927
2105
|
|
|
1928
2106
|
equals(that: Selection): boolean {
|
|
@@ -1930,80 +2108,68 @@ export abstract class FragmentSelection extends Freezable<FragmentSelection> {
|
|
|
1930
2108
|
return true;
|
|
1931
2109
|
}
|
|
1932
2110
|
return (that instanceof FragmentSelection)
|
|
1933
|
-
&& this.element
|
|
2111
|
+
&& this.element.equals(that.element)
|
|
1934
2112
|
&& this.selectionSet.equals(that.selectionSet);
|
|
1935
2113
|
}
|
|
1936
2114
|
|
|
1937
2115
|
contains(that: Selection): boolean {
|
|
1938
2116
|
return (that instanceof FragmentSelection)
|
|
1939
|
-
&& this.element
|
|
2117
|
+
&& this.element.equals(that.element)
|
|
1940
2118
|
&& this.selectionSet.contains(that.selectionSet);
|
|
1941
2119
|
}
|
|
1942
|
-
|
|
1943
|
-
clone(): FragmentSelection {
|
|
1944
|
-
return new InlineFragmentSelection(this.element(), this.selectionSet.clone());
|
|
1945
|
-
}
|
|
1946
2120
|
}
|
|
1947
2121
|
|
|
1948
2122
|
class InlineFragmentSelection extends FragmentSelection {
|
|
1949
|
-
private readonly _selectionSet: SelectionSet;
|
|
1950
|
-
|
|
1951
2123
|
constructor(
|
|
1952
|
-
|
|
1953
|
-
|
|
2124
|
+
fragment: FragmentElement,
|
|
2125
|
+
private readonly _selectionSet: SelectionSet,
|
|
1954
2126
|
) {
|
|
1955
|
-
super();
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2127
|
+
super(fragment);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
get selectionSet(): SelectionSet {
|
|
2131
|
+
return this._selectionSet;
|
|
1960
2132
|
}
|
|
1961
2133
|
|
|
1962
2134
|
key(): string {
|
|
1963
|
-
return this.element()
|
|
2135
|
+
return this.element.key();
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
withUpdatedComponents(fragment: FragmentElement, selectionSet: SelectionSet): InlineFragmentSelection {
|
|
2139
|
+
return new InlineFragmentSelection(fragment, selectionSet);
|
|
1964
2140
|
}
|
|
1965
2141
|
|
|
1966
|
-
validate() {
|
|
2142
|
+
validate(variableDefinitions: VariableDefinitions) {
|
|
1967
2143
|
this.validateDeferAndStream();
|
|
1968
2144
|
// Note that validation is kind of redundant since `this.selectionSet.validate()` will check that it isn't empty. But doing it
|
|
1969
2145
|
// allow to provide much better error messages.
|
|
1970
2146
|
validate(
|
|
1971
2147
|
!this.selectionSet.isEmpty(),
|
|
1972
|
-
() => `Invalid empty selection set for fragment "${this.element
|
|
2148
|
+
() => `Invalid empty selection set for fragment "${this.element}"`
|
|
1973
2149
|
);
|
|
1974
|
-
this.selectionSet.validate();
|
|
2150
|
+
this.selectionSet.validate(variableDefinitions);
|
|
1975
2151
|
}
|
|
1976
2152
|
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
return this.cloneIfFrozen();
|
|
2153
|
+
rebaseOn(parentType: CompositeType): FragmentSelection {
|
|
2154
|
+
if (this.parentType === parentType) {
|
|
2155
|
+
return this;
|
|
1981
2156
|
}
|
|
1982
2157
|
|
|
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();
|
|
2158
|
+
const rebasedFragment = this.element.rebaseOn(parentType);
|
|
2159
|
+
const rebasedCastedType = rebasedFragment.castedType();
|
|
2160
|
+
if (rebasedCastedType === this.selectionSet.parentType) {
|
|
2161
|
+
return this.withUpdatedElement(rebasedFragment);
|
|
1996
2162
|
}
|
|
1997
2163
|
|
|
1998
|
-
return
|
|
2164
|
+
return this.withUpdatedComponents(rebasedFragment, this.selectionSet.rebaseOn(rebasedCastedType));
|
|
1999
2165
|
}
|
|
2000
2166
|
|
|
2001
2167
|
canAddTo(parentType: CompositeType): boolean {
|
|
2002
|
-
if (this.element
|
|
2168
|
+
if (this.element.parentType === parentType) {
|
|
2003
2169
|
return true;
|
|
2004
2170
|
}
|
|
2005
2171
|
|
|
2006
|
-
const type = this.element
|
|
2172
|
+
const type = this.element.castedTypeIfAddedTo(parentType);
|
|
2007
2173
|
if (!type) {
|
|
2008
2174
|
return false;
|
|
2009
2175
|
}
|
|
@@ -2014,21 +2180,8 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2014
2180
|
return true;
|
|
2015
2181
|
}
|
|
2016
2182
|
|
|
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
2183
|
toSelectionNode(): InlineFragmentNode {
|
|
2031
|
-
const typeCondition = this.element
|
|
2184
|
+
const typeCondition = this.element.typeCondition;
|
|
2032
2185
|
return {
|
|
2033
2186
|
kind: Kind.INLINE_FRAGMENT,
|
|
2034
2187
|
typeCondition: typeCondition
|
|
@@ -2040,120 +2193,208 @@ class InlineFragmentSelection extends FragmentSelection {
|
|
|
2040
2193
|
},
|
|
2041
2194
|
}
|
|
2042
2195
|
: undefined,
|
|
2043
|
-
directives: this.element
|
|
2196
|
+
directives: this.element.appliedDirectivesToDirectiveNodes(),
|
|
2044
2197
|
selectionSet: this.selectionSet.toSelectionSetNode()
|
|
2045
2198
|
};
|
|
2046
2199
|
}
|
|
2047
2200
|
|
|
2048
2201
|
optimize(fragments: NamedFragments): FragmentSelection {
|
|
2049
2202
|
let optimizedSelection = this.selectionSet.optimize(fragments);
|
|
2050
|
-
const typeCondition = this.element
|
|
2203
|
+
const typeCondition = this.element.typeCondition;
|
|
2051
2204
|
if (typeCondition) {
|
|
2052
2205
|
for (const candidate of fragments.maybeApplyingAtType(typeCondition)) {
|
|
2053
2206
|
// See comment in `FieldSelection.optimize` about the `equals`: this fully apply here too.
|
|
2054
2207
|
if (optimizedSelection.equals(candidate.selectionSet)) {
|
|
2055
|
-
|
|
2208
|
+
let spreadDirectives: Directive[] = [];
|
|
2209
|
+
if (this.element.appliedDirectives) {
|
|
2210
|
+
const { isSubset, difference } = diffDirectives(this.element.appliedDirectives, candidate.appliedDirectives);
|
|
2211
|
+
if (!isSubset) {
|
|
2212
|
+
// This means that while the named fragments matches the sub-selection, that name fragment also include some
|
|
2213
|
+
// directives that are _not_ on our element, so we cannot use it.
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
spreadDirectives = difference;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
const newSelection = new FragmentSpreadSelection(this.parentType, fragments, candidate, spreadDirectives);
|
|
2056
2220
|
// We use the fragment when the fragments condition is either the same, or a supertype of our current condition.
|
|
2057
2221
|
// If it's the same type, then we don't really want to preserve the current condition, it is included in the
|
|
2058
2222
|
// spread and we can return it directly. But if the fragment condition is a superset, then we should preserve
|
|
2059
2223
|
// our current condition since it restricts the selection more than the fragment actual does.
|
|
2060
2224
|
if (sameType(typeCondition, candidate.typeCondition)) {
|
|
2061
|
-
|
|
2062
|
-
this.fragmentElement.appliedDirectives.forEach((directive) => {
|
|
2063
|
-
spread.element().applyDirective(directive.definition!, directive.arguments());
|
|
2064
|
-
})
|
|
2065
|
-
return spread;
|
|
2225
|
+
return newSelection;
|
|
2066
2226
|
}
|
|
2067
|
-
|
|
2227
|
+
|
|
2228
|
+
optimizedSelection = selectionSetOf(this.parentType, newSelection);
|
|
2068
2229
|
break;
|
|
2069
2230
|
}
|
|
2070
2231
|
}
|
|
2071
2232
|
}
|
|
2072
2233
|
return this.selectionSet === optimizedSelection
|
|
2073
2234
|
? this
|
|
2074
|
-
: new InlineFragmentSelection(this.
|
|
2235
|
+
: new InlineFragmentSelection(this.element, optimizedSelection);
|
|
2075
2236
|
}
|
|
2076
2237
|
|
|
2077
|
-
|
|
2078
|
-
const
|
|
2079
|
-
|
|
2080
|
-
? this
|
|
2081
|
-
: new InlineFragmentSelection(this.element(), expandedSelection);
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
collectUsedFragmentNames(collector: Map<string, number>): void {
|
|
2085
|
-
this.selectionSet.collectUsedFragmentNames(collector);
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
withoutDefer(labelsToRemove?: Set<string>): FragmentSelection | SelectionSet {
|
|
2089
|
-
const updatedSubSelections = this.selectionSet.withoutDefer(labelsToRemove);
|
|
2090
|
-
const deferArgs = this.fragmentElement.deferDirectiveArgs();
|
|
2238
|
+
withoutDefer(labelsToRemove?: Set<string>): InlineFragmentSelection | SelectionSet {
|
|
2239
|
+
const newSelection = this.selectionSet.withoutDefer(labelsToRemove);
|
|
2240
|
+
const deferArgs = this.element.deferDirectiveArgs();
|
|
2091
2241
|
const hasDeferToRemove = deferArgs && (!labelsToRemove || (deferArgs.label && labelsToRemove.has(deferArgs.label)));
|
|
2092
|
-
if (
|
|
2242
|
+
if (newSelection === this.selectionSet && !hasDeferToRemove) {
|
|
2093
2243
|
return this;
|
|
2094
2244
|
}
|
|
2095
|
-
const
|
|
2096
|
-
if (!
|
|
2097
|
-
return
|
|
2245
|
+
const newElement = hasDeferToRemove ? this.element.withoutDefer() : this.element;
|
|
2246
|
+
if (!newElement) {
|
|
2247
|
+
return newSelection;
|
|
2098
2248
|
}
|
|
2099
|
-
return
|
|
2249
|
+
return this.withUpdatedComponents(newElement, newSelection);
|
|
2100
2250
|
}
|
|
2101
2251
|
|
|
2102
2252
|
withNormalizedDefer(normalizer: DeferNormalizer): InlineFragmentSelection | SelectionSet {
|
|
2103
|
-
const
|
|
2104
|
-
const
|
|
2105
|
-
if (!
|
|
2106
|
-
return
|
|
2253
|
+
const newElement = this.element.withNormalizedDefer(normalizer);
|
|
2254
|
+
const newSelection = this.selectionSet.withNormalizedDefer(normalizer)
|
|
2255
|
+
if (!newElement) {
|
|
2256
|
+
return newSelection;
|
|
2107
2257
|
}
|
|
2108
|
-
return
|
|
2258
|
+
return newElement === this.element && newSelection === this.selectionSet
|
|
2109
2259
|
? this
|
|
2110
|
-
:
|
|
2260
|
+
: this.withUpdatedComponents(newElement, newSelection);
|
|
2111
2261
|
}
|
|
2112
2262
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2263
|
+
trimUnsatisfiableBranches(currentType: CompositeType): FragmentSelection | SelectionSet | undefined {
|
|
2264
|
+
const thisCondition = this.element.typeCondition;
|
|
2265
|
+
// Note that if the condition has directives, we preserve the fragment no matter what.
|
|
2266
|
+
if (this.element.appliedDirectives.length === 0) {
|
|
2267
|
+
if (!thisCondition || currentType === this.element.typeCondition) {
|
|
2268
|
+
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType);
|
|
2269
|
+
return trimmed.isEmpty() ? undefined : trimmed;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
// If the current type is an object, then we never need to keep the current fragment because:
|
|
2273
|
+
// - either the fragment is also an object, but we've eliminated the case where the 2 types are the same,
|
|
2274
|
+
// so this is just an unsatisfiable branch.
|
|
2275
|
+
// - or it's not an object, but then the current type is more precise and no poitn in "casting" to a
|
|
2276
|
+
// less precise interface/union.
|
|
2277
|
+
if (isObjectType(currentType)) {
|
|
2278
|
+
if (isObjectType(thisCondition)) {
|
|
2279
|
+
return undefined;
|
|
2280
|
+
} else {
|
|
2281
|
+
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType);
|
|
2282
|
+
return trimmed.isEmpty() ? undefined : trimmed;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// In all other cases, we first recurse on the sub-selection.
|
|
2288
|
+
const trimmedSelectionSet = this.selectionSet.trimUnsatisfiableBranches(this.element.typeCondition ?? this.parentType);
|
|
2289
|
+
|
|
2290
|
+
// First, could be that everything was unsatisfiable.
|
|
2291
|
+
if (trimmedSelectionSet.isEmpty()) {
|
|
2292
|
+
if (this.element.appliedDirectives.length === 0) {
|
|
2293
|
+
return undefined;
|
|
2294
|
+
} else {
|
|
2295
|
+
return this.withUpdatedSelectionSet(selectionSetOfElement(
|
|
2296
|
+
new Field(
|
|
2297
|
+
(this.element.typeCondition ?? this.parentType).typenameField()!,
|
|
2298
|
+
undefined,
|
|
2299
|
+
[new Directive('include', { 'if': false })],
|
|
2300
|
+
)
|
|
2301
|
+
));
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// Second, we check if some of the sub-selection fragments can be "lifted" outside of this fragment. This can happen if:
|
|
2306
|
+
// 1. the current fragment is an abstract type,
|
|
2307
|
+
// 2. the sub-fragment is an object type,
|
|
2308
|
+
// 3. the sub-fragment type is a valid runtime of the current type.
|
|
2309
|
+
if (this.element.appliedDirectives.length === 0 && isAbstractType(thisCondition!)) {
|
|
2310
|
+
assert(!isObjectType(currentType), () => `Should not have got here if ${currentType} is an object type`);
|
|
2311
|
+
const currentRuntimes = possibleRuntimeTypes(currentType);
|
|
2312
|
+
const liftableSelections: Selection[] = [];
|
|
2313
|
+
for (const selection of trimmedSelectionSet.selections()) {
|
|
2314
|
+
if (selection.kind === 'FragmentSelection'
|
|
2315
|
+
&& selection.element.typeCondition
|
|
2316
|
+
&& isObjectType(selection.element.typeCondition)
|
|
2317
|
+
&& currentRuntimes.includes(selection.element.typeCondition)
|
|
2318
|
+
) {
|
|
2319
|
+
liftableSelections.push(selection);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// If we can lift all selections, then that just mean we can get rid of the current fragment altogether
|
|
2324
|
+
if (liftableSelections.length === trimmedSelectionSet.selections().length) {
|
|
2325
|
+
return trimmedSelectionSet;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// Otherwise, if there is "liftable" selections, we must return a set comprised of those lifted selection,
|
|
2329
|
+
// and the current fragment _without_ those lifted selections.
|
|
2330
|
+
if (liftableSelections.length > 0) {
|
|
2331
|
+
const newSet = new SelectionSetUpdates();
|
|
2332
|
+
newSet.add(liftableSelections);
|
|
2333
|
+
newSet.add(this.withUpdatedSelectionSet(
|
|
2334
|
+
trimmedSelectionSet.filter((s) => !liftableSelections.includes(s)),
|
|
2335
|
+
));
|
|
2336
|
+
return newSet.toSelectionSet(this.parentType);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
return this.selectionSet === trimmedSelectionSet ? this : this.withUpdatedSelectionSet(trimmedSelectionSet);
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
|
|
2344
|
+
expandAllFragments(): FragmentSelection {
|
|
2345
|
+
return this.mapToSelectionSet((s) => s.expandAllFragments());
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FragmentSelection {
|
|
2349
|
+
return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
|
|
2115
2350
|
}
|
|
2116
2351
|
|
|
2117
2352
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
2118
|
-
return (indent ?? '') + this.
|
|
2353
|
+
return (indent ?? '') + this.element + ' ' + this.selectionSet.toString(expandFragments, true, indent);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
function diffDirectives(superset: readonly Directive<any>[], maybeSubset: readonly Directive<any>[]): { isSubset: boolean, difference: Directive[] } {
|
|
2358
|
+
if (maybeSubset.every((d) => superset.some((s) => sameDirectiveApplication(d, s)))) {
|
|
2359
|
+
return { isSubset: true, difference: superset.filter((s) => !maybeSubset.some((d) => sameDirectiveApplication(d, s))) };
|
|
2360
|
+
} else {
|
|
2361
|
+
return { isSubset: false, difference: [] };
|
|
2119
2362
|
}
|
|
2120
2363
|
}
|
|
2121
2364
|
|
|
2122
2365
|
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;
|
|
2366
|
+
private computedKey: string | undefined;
|
|
2127
2367
|
|
|
2128
2368
|
constructor(
|
|
2129
2369
|
sourceType: CompositeType,
|
|
2130
2370
|
private readonly fragments: NamedFragments,
|
|
2131
|
-
|
|
2371
|
+
private readonly namedFragment: NamedFragmentDefinition,
|
|
2372
|
+
private readonly spreadDirectives: readonly Directive<any>[],
|
|
2132
2373
|
) {
|
|
2133
|
-
super();
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
this.
|
|
2138
|
-
for (const directive of fragmentDefinition.appliedDirectives) {
|
|
2139
|
-
this._element.applyDirective(directive.definition!, directive.arguments());
|
|
2140
|
-
}
|
|
2374
|
+
super(new FragmentElement(sourceType, namedFragment.typeCondition, namedFragment.appliedDirectives.concat(spreadDirectives)));
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
get selectionSet(): SelectionSet {
|
|
2378
|
+
return this.namedFragment.selectionSet;
|
|
2141
2379
|
}
|
|
2142
2380
|
|
|
2143
2381
|
key(): string {
|
|
2144
|
-
|
|
2382
|
+
if (!this.computedKey) {
|
|
2383
|
+
this.computedKey = '...' + this.namedFragment.name + (this.spreadDirectives.length === 0 ? '' : ' ' + this.spreadDirectives.join(' '));
|
|
2384
|
+
}
|
|
2385
|
+
return this.computedKey;
|
|
2145
2386
|
}
|
|
2146
2387
|
|
|
2147
|
-
|
|
2148
|
-
|
|
2388
|
+
withUpdatedComponents(_fragment: FragmentElement, _selectionSet: SelectionSet): InlineFragmentSelection {
|
|
2389
|
+
assert(false, `Unsupported`);
|
|
2149
2390
|
}
|
|
2150
2391
|
|
|
2151
|
-
|
|
2152
|
-
return this
|
|
2392
|
+
trimUnsatisfiableBranches(_: CompositeType): FragmentSelection {
|
|
2393
|
+
return this;
|
|
2153
2394
|
}
|
|
2154
2395
|
|
|
2155
|
-
|
|
2156
|
-
return this.
|
|
2396
|
+
namedFragments(): NamedFragments | undefined {
|
|
2397
|
+
return this.fragments;
|
|
2157
2398
|
}
|
|
2158
2399
|
|
|
2159
2400
|
validate(): void {
|
|
@@ -2163,10 +2404,9 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2163
2404
|
}
|
|
2164
2405
|
|
|
2165
2406
|
toSelectionNode(): FragmentSpreadNode {
|
|
2166
|
-
const
|
|
2167
|
-
const directiveNodes = spreadDirectives.length === 0
|
|
2407
|
+
const directiveNodes = this.spreadDirectives.length === 0
|
|
2168
2408
|
? undefined
|
|
2169
|
-
: spreadDirectives.map(directive => {
|
|
2409
|
+
: this.spreadDirectives.map(directive => {
|
|
2170
2410
|
return {
|
|
2171
2411
|
kind: Kind.DIRECTIVE,
|
|
2172
2412
|
name: {
|
|
@@ -2187,7 +2427,7 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2187
2427
|
return this;
|
|
2188
2428
|
}
|
|
2189
2429
|
|
|
2190
|
-
|
|
2430
|
+
rebaseOn(_parentType: CompositeType): FragmentSelection {
|
|
2191
2431
|
// This is a little bit iffy, because the fragment could link to a schema (typically the supergraph API one)
|
|
2192
2432
|
// that is different from the one of `_selectionSet` (say, a subgraph fetch selection in which we're trying to
|
|
2193
2433
|
// reuse a user fragment). But in practice, we expand all fragments when we do query planning and only re-add
|
|
@@ -2198,19 +2438,26 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2198
2438
|
}
|
|
2199
2439
|
|
|
2200
2440
|
canAddTo(_: CompositeType): boolean {
|
|
2201
|
-
// Mimicking the logic of `
|
|
2441
|
+
// Mimicking the logic of `rebaseOn`.
|
|
2202
2442
|
return true;
|
|
2203
2443
|
}
|
|
2204
2444
|
|
|
2205
|
-
|
|
2206
|
-
|
|
2445
|
+
expandAllFragments(): FragmentSelection | readonly Selection[] {
|
|
2446
|
+
const expandedSubSelections = this.selectionSet.expandAllFragments();
|
|
2447
|
+
return sameType(this.parentType, this.namedFragment.typeCondition) && this.element.appliedDirectives.length === 0
|
|
2448
|
+
? expandedSubSelections.selections()
|
|
2449
|
+
: new InlineFragmentSelection(this.element, expandedSubSelections);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FragmentSelection | readonly Selection[] {
|
|
2453
|
+
if (!names.includes(this.namedFragment.name)) {
|
|
2207
2454
|
return this;
|
|
2208
2455
|
}
|
|
2209
2456
|
|
|
2210
|
-
const expandedSubSelections = this.selectionSet.expandFragments(names,
|
|
2211
|
-
return sameType(this.
|
|
2457
|
+
const expandedSubSelections = this.selectionSet.expandFragments(names, updatedFragments);
|
|
2458
|
+
return sameType(this.parentType, this.namedFragment.typeCondition) && this.element.appliedDirectives.length === 0
|
|
2212
2459
|
? expandedSubSelections.selections()
|
|
2213
|
-
: new InlineFragmentSelection(this.
|
|
2460
|
+
: new InlineFragmentSelection(this.element, expandedSubSelections);
|
|
2214
2461
|
}
|
|
2215
2462
|
|
|
2216
2463
|
collectUsedFragmentNames(collector: Map<string, number>): void {
|
|
@@ -2219,33 +2466,98 @@ class FragmentSpreadSelection extends FragmentSelection {
|
|
|
2219
2466
|
collector.set(this.namedFragment.name, usageCount === undefined ? 1 : usageCount + 1);
|
|
2220
2467
|
}
|
|
2221
2468
|
|
|
2222
|
-
withoutDefer(_labelsToRemove?: Set<string>):
|
|
2223
|
-
assert(false, 'Unsupported, see `Operation.withoutDefer`');
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
withNormalizedDefer(_normalizezr: DeferNormalizer): FragmentSelection {
|
|
2469
|
+
withoutDefer(_labelsToRemove?: Set<string>): FragmentSpreadSelection {
|
|
2227
2470
|
assert(false, 'Unsupported, see `Operation.withAllDeferLabelled`');
|
|
2228
2471
|
}
|
|
2229
2472
|
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
withUpdatedSubSelection(_: SelectionSet | undefined): InlineFragmentSelection {
|
|
2235
|
-
assert(false, `Unssupported`);
|
|
2473
|
+
withNormalizedDefer(_normalizer: DeferNormalizer): FragmentSpreadSelection {
|
|
2474
|
+
assert(false, 'Unsupported, see `Operation.withAllDeferLabelled`');
|
|
2236
2475
|
}
|
|
2237
2476
|
|
|
2238
2477
|
toString(expandFragments: boolean = true, indent?: string): string {
|
|
2239
2478
|
if (expandFragments) {
|
|
2240
|
-
return (indent ?? '') + this.
|
|
2479
|
+
return (indent ?? '') + this.element + ' ' + this.selectionSet.toString(true, true, indent);
|
|
2241
2480
|
} else {
|
|
2242
|
-
const directives = this.spreadDirectives
|
|
2481
|
+
const directives = this.spreadDirectives;
|
|
2243
2482
|
const directiveString = directives.length == 0 ? '' : ' ' + directives.join(' ');
|
|
2244
2483
|
return (indent ?? '') + '...' + this.namedFragment.name + directiveString;
|
|
2245
2484
|
}
|
|
2246
2485
|
}
|
|
2247
2486
|
}
|
|
2248
2487
|
|
|
2488
|
+
function selectionSetOfNode(
|
|
2489
|
+
parentType: CompositeType,
|
|
2490
|
+
node: SelectionSetNode,
|
|
2491
|
+
variableDefinitions: VariableDefinitions,
|
|
2492
|
+
fragments: NamedFragments | undefined,
|
|
2493
|
+
fieldAccessor: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined = (type, name) => type.field(name)
|
|
2494
|
+
): SelectionSet {
|
|
2495
|
+
if (node.selections.length === 1) {
|
|
2496
|
+
return selectionSetOf(
|
|
2497
|
+
parentType,
|
|
2498
|
+
selectionOfNode(parentType, node.selections[0], variableDefinitions, fragments, fieldAccessor),
|
|
2499
|
+
fragments,
|
|
2500
|
+
);
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
const selections = new SelectionSetUpdates();
|
|
2504
|
+
for (const selectionNode of node.selections) {
|
|
2505
|
+
selections.add(selectionOfNode(parentType, selectionNode, variableDefinitions, fragments, fieldAccessor));
|
|
2506
|
+
}
|
|
2507
|
+
return selections.toSelectionSet(parentType, fragments);
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
function directiveOfNode<T extends DirectiveTargetElement<T>>(schema: Schema, node: DirectiveNode): Directive<T> {
|
|
2511
|
+
const directiveDef = schema.directive(node.name.value);
|
|
2512
|
+
validate(directiveDef, () => `Unknown directive "@${node.name.value}"`)
|
|
2513
|
+
return new Directive(directiveDef.name, argumentsFromAST(directiveDef.coordinate, node.arguments, directiveDef));
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
function directivesOfNodes<T extends DirectiveTargetElement<T>>(schema: Schema, nodes: readonly DirectiveNode[] | undefined): Directive<T>[] {
|
|
2517
|
+
return nodes?.map((n) => directiveOfNode(schema, n)) ?? [];
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
function selectionOfNode(
|
|
2521
|
+
parentType: CompositeType,
|
|
2522
|
+
node: SelectionNode,
|
|
2523
|
+
variableDefinitions: VariableDefinitions,
|
|
2524
|
+
fragments: NamedFragments | undefined,
|
|
2525
|
+
fieldAccessor: (type: CompositeType, fieldName: string) => FieldDefinition<any> | undefined = (type, name) => type.field(name)
|
|
2526
|
+
): Selection {
|
|
2527
|
+
let selection: Selection;
|
|
2528
|
+
const directives = directivesOfNodes(parentType.schema(), node.directives);
|
|
2529
|
+
switch (node.kind) {
|
|
2530
|
+
case Kind.FIELD:
|
|
2531
|
+
const definition: FieldDefinition<any> | undefined = fieldAccessor(parentType, node.name.value);
|
|
2532
|
+
validate(definition, () => `Cannot query field "${node.name.value}" on type "${parentType}".`, parentType.sourceAST);
|
|
2533
|
+
const type = baseType(definition.type!);
|
|
2534
|
+
const selectionSet = node.selectionSet
|
|
2535
|
+
? selectionSetOfNode(type as CompositeType, node.selectionSet, variableDefinitions, fragments, fieldAccessor)
|
|
2536
|
+
: undefined;
|
|
2537
|
+
|
|
2538
|
+
selection = new FieldSelection(
|
|
2539
|
+
new Field(definition, argumentsFromAST(definition.coordinate, node.arguments, definition), directives, node.alias?.value),
|
|
2540
|
+
selectionSet,
|
|
2541
|
+
);
|
|
2542
|
+
break;
|
|
2543
|
+
case Kind.INLINE_FRAGMENT:
|
|
2544
|
+
const element = new FragmentElement(parentType, node.typeCondition?.name.value, directives);
|
|
2545
|
+
selection = new InlineFragmentSelection(
|
|
2546
|
+
element,
|
|
2547
|
+
selectionSetOfNode(element.typeCondition ? element.typeCondition : element.parentType, node.selectionSet, variableDefinitions, fragments, fieldAccessor),
|
|
2548
|
+
);
|
|
2549
|
+
break;
|
|
2550
|
+
case Kind.FRAGMENT_SPREAD:
|
|
2551
|
+
const fragmentName = node.name.value;
|
|
2552
|
+
validate(fragments, () => `Cannot find fragment name "${fragmentName}" (no fragments were provided)`);
|
|
2553
|
+
const fragment = fragments.get(fragmentName);
|
|
2554
|
+
validate(fragment, () => `Cannot find fragment name "${fragmentName}" (provided fragments are: [${fragments.names().join(', ')}])`);
|
|
2555
|
+
selection = new FragmentSpreadSelection(parentType, fragments, fragment, directives);
|
|
2556
|
+
break;
|
|
2557
|
+
}
|
|
2558
|
+
return selection;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2249
2561
|
export function operationFromDocument(
|
|
2250
2562
|
schema: Schema,
|
|
2251
2563
|
document: DocumentNode,
|
|
@@ -2277,9 +2589,7 @@ export function operationFromDocument(
|
|
|
2277
2589
|
if (!isCompositeType(typeCondition)) {
|
|
2278
2590
|
throw ERRORS.INVALID_GRAPHQL.err(`Invalid fragment "${name}" on non-composite type "${typeName}"`, { nodes: definition });
|
|
2279
2591
|
}
|
|
2280
|
-
|
|
2281
|
-
addDirectiveNodesToElement(definition.directives, fragment);
|
|
2282
|
-
fragments.add(fragment);
|
|
2592
|
+
fragments.add(new NamedFragmentDefinition(schema, name, typeCondition, directivesOfNodes(schema, definition.directives)));
|
|
2283
2593
|
break;
|
|
2284
2594
|
}
|
|
2285
2595
|
});
|
|
@@ -2295,11 +2605,11 @@ export function operationFromDocument(
|
|
|
2295
2605
|
switch (definition.kind) {
|
|
2296
2606
|
case Kind.FRAGMENT_DEFINITION:
|
|
2297
2607
|
const fragment = fragments.get(definition.name.value)!;
|
|
2298
|
-
fragment.
|
|
2608
|
+
fragment.setSelectionSet(selectionSetOfNode(fragment.typeCondition, definition.selectionSet, variableDefinitions, fragments));
|
|
2299
2609
|
break;
|
|
2300
2610
|
}
|
|
2301
2611
|
});
|
|
2302
|
-
fragments.validate();
|
|
2612
|
+
fragments.validate(variableDefinitions);
|
|
2303
2613
|
return operationFromAST({schema, operation, variableDefinitions, fragments, validateInput: options?.validate});
|
|
2304
2614
|
}
|
|
2305
2615
|
|
|
@@ -2325,7 +2635,7 @@ function operationFromAST({
|
|
|
2325
2635
|
parentType: rootType.type,
|
|
2326
2636
|
source: operation.selectionSet,
|
|
2327
2637
|
variableDefinitions,
|
|
2328
|
-
fragments,
|
|
2638
|
+
fragments: fragments.isEmpty() ? undefined : fragments,
|
|
2329
2639
|
validate: validateInput,
|
|
2330
2640
|
}),
|
|
2331
2641
|
variableDefinitions,
|
|
@@ -2347,7 +2657,7 @@ export function parseOperation(
|
|
|
2347
2657
|
export function parseSelectionSet({
|
|
2348
2658
|
parentType,
|
|
2349
2659
|
source,
|
|
2350
|
-
variableDefinitions,
|
|
2660
|
+
variableDefinitions = new VariableDefinitions(),
|
|
2351
2661
|
fragments,
|
|
2352
2662
|
fieldAccessor,
|
|
2353
2663
|
validate = true,
|
|
@@ -2363,10 +2673,9 @@ export function parseSelectionSet({
|
|
|
2363
2673
|
const node = typeof source === 'string'
|
|
2364
2674
|
? parseOperationAST(source.trim().startsWith('{') ? source : `{${source}}`).selectionSet
|
|
2365
2675
|
: source;
|
|
2366
|
-
const selectionSet = new
|
|
2367
|
-
selectionSet.addSelectionSetNode(node, variableDefinitions ?? new VariableDefinitions(), fieldAccessor);
|
|
2676
|
+
const selectionSet = selectionSetOfNode(parentType, node, variableDefinitions ?? new VariableDefinitions(), fragments, fieldAccessor);
|
|
2368
2677
|
if (validate)
|
|
2369
|
-
selectionSet.validate();
|
|
2678
|
+
selectionSet.validate(variableDefinitions);
|
|
2370
2679
|
return selectionSet;
|
|
2371
2680
|
}
|
|
2372
2681
|
|