@apollo/federation-internals 2.4.0-alpha.0 → 2.4.0

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