@apollo/federation-internals 2.4.0-alpha.1 → 2.4.1

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