@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.
@@ -4,7 +4,7 @@ import {
4
4
  SchemaRootKind,
5
5
  } from '../../dist/definitions';
6
6
  import { buildSchema } from '../../dist/buildSchema';
7
- import { Field, FieldSelection, Operation, operationFromDocument, parseOperation, SelectionSet } from '../../dist/operations';
7
+ import { MutableSelectionSet, Operation, operationFromDocument, parseOperation } from '../../dist/operations';
8
8
  import './matchers';
9
9
  import { DocumentNode, FieldNode, GraphQLError, Kind, OperationDefinitionNode, OperationTypeNode, SelectionNode, SelectionSetNode } from 'graphql';
10
10
 
@@ -242,124 +242,6 @@ describe('fragments optimization', () => {
242
242
  });
243
243
  });
244
244
 
245
- describe('selection set freezing', () => {
246
- const schema = parseSchema(`
247
- type Query {
248
- t: T
249
- }
250
-
251
- type T {
252
- a: Int
253
- b: Int
254
- }
255
- `);
256
-
257
- const tField = schema.schemaDefinition.rootType('query')!.field('t')!;
258
-
259
- it('throws if one tries to modify a frozen selection set', () => {
260
- // Note that we use parseOperation to help us build selection/selection sets because it's more readable/convenient
261
- // thant to build the object "programmatically".
262
- const s1 = parseOperation(schema, `{ t { a } }`).selectionSet;
263
- const s2 = parseOperation(schema, `{ t { b } }`).selectionSet;
264
-
265
- const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
266
-
267
- // Control: check we can add to the selection set while not yet frozen
268
- expect(s.isFrozen()).toBeFalsy();
269
- expect(() => s.mergeIn(s1)).not.toThrow();
270
-
271
- s.freeze();
272
- expect(s.isFrozen()).toBeTruthy();
273
- expect(() => s.mergeIn(s2)).toThrowError('Cannot add to frozen selection: { t { a } }');
274
- });
275
-
276
- it('it does not clone non-frozen selections when adding to another one', () => {
277
- // This test is a bit debatable because what it tests is not so much a behaviour
278
- // we *need* absolutely to preserve, but rather test how things happens to
279
- // behave currently and illustrate the current contrast between frozen and
280
- // non-frozen selection set.
281
- // That is, this test show what happens if the test
282
- // "it automaticaly clones frozen selections when adding to another one"
283
- // is done without freezing.
284
-
285
- const s1 = parseOperation(schema, `{ t { a } }`).selectionSet;
286
- const s2 = parseOperation(schema, `{ t { b } }`).selectionSet;
287
- const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
288
-
289
- s.mergeIn(s1);
290
- s.mergeIn(s2);
291
-
292
- expect(s.toString()).toBe('{ t { a b } }');
293
-
294
- // This next assertion is where differs from the case where `s1` is frozen. Namely,
295
- // when we do `s.mergeIn(s1)`, then `s` directly references `s1` without cloning
296
- // and thus the next modification (`s.mergeIn(s2)`) ends up modifying both `s` and `s1`.
297
- // Note that we don't mean by this test that the fact that `s.mergeIn(s1)` does
298
- // not clone `s1` is a behaviour one should *rely* on, but it currently done for
299
- // efficiencies sake: query planning does a lot of selection set building through
300
- // `SelectionSet::mergeIn` and `SelectionSet::add` and we often pass to those method
301
- // newly constructed selections as input, so cloning them would wast CPU and early
302
- // query planning benchmarking showed that this could add up on the more expansive
303
- // plan computations. This is why freezing exists: it allows us to save cloning
304
- // in general, but to protect those selection set we know should be immutable
305
- // so they do get cloned in such situation.
306
- expect(s1.toString()).toBe('{ t { a b } }');
307
- expect(s2.toString()).toBe('{ t { b } }');
308
- });
309
-
310
- it('it automaticaly clones frozen field selections when merging to another one', () => {
311
- const s1 = parseOperation(schema, `{ t { a } }`).selectionSet.freeze();
312
- const s2 = parseOperation(schema, `{ t { b } }`).selectionSet.freeze();
313
- const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
314
-
315
- s.mergeIn(s1);
316
- s.mergeIn(s2);
317
-
318
- // We check S is what we expect...
319
- expect(s.toString()).toBe('{ t { a b } }');
320
-
321
- // ... but more importantly for this test, that s1/s2 were not modified.
322
- expect(s1.toString()).toBe('{ t { a } }');
323
- expect(s2.toString()).toBe('{ t { b } }');
324
- });
325
-
326
- it('it automaticaly clones frozen fragment selections when merging to another one', () => {
327
- // Note: needlessly complex queries, but we're just ensuring the cloning story works when fragments are involved
328
- const s1 = parseOperation(schema, `{ ... on Query { t { ... on T { a } } } }`).selectionSet.freeze();
329
- const s2 = parseOperation(schema, `{ ... on Query { t { ... on T { b } } } }`).selectionSet.freeze();
330
- const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
331
-
332
- s.mergeIn(s1);
333
- s.mergeIn(s2);
334
-
335
- expect(s.toString()).toBe('{ ... on Query { t { ... on T { a b } } } }');
336
-
337
- expect(s1.toString()).toBe('{ ... on Query { t { ... on T { a } } } }');
338
- expect(s2.toString()).toBe('{ ... on Query { t { ... on T { b } } } }');
339
- });
340
-
341
- it('it automaticaly clones frozen field selections when adding to another one', () => {
342
- const s1 = parseOperation(schema, `{ t { a } }`).selectionSet.freeze();
343
- const s2 = parseOperation(schema, `{ t { b } }`).selectionSet.freeze();
344
- const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
345
- const tSelection = new FieldSelection(new Field(tField));
346
- s.add(tSelection);
347
-
348
- // Note that this test both checks the auto-cloning for the `add` method, but
349
- // also shows that freezing dose apply deeply (since we freeze the whole `s1`/`s2`
350
- // but only add some sub-selection of it).
351
- tSelection.selectionSet!.add(s1.selections()[0].selectionSet!.selections()[0]);
352
- tSelection.selectionSet!.add(s2.selections()[0].selectionSet!.selections()[0]);
353
-
354
- // We check S is what we expect...
355
- expect(s.toString()).toBe('{ t { a b } }');
356
-
357
- // ... but more importantly for this test, that s1/s2 were not modified.
358
- expect(s1.toString()).toBe('{ t { a } }');
359
- expect(s2.toString()).toBe('{ t { b } }');
360
- });
361
- });
362
-
363
245
  describe('validations', () => {
364
246
  test.each([
365
247
  { directive: '@defer', rootKind: 'mutation' },
@@ -517,3 +399,150 @@ describe('empty branches removal', () => {
517
399
  )).toBe('{ u }');
518
400
  });
519
401
  });
402
+
403
+ describe('basic operations', () => {
404
+ const schema = parseSchema(`
405
+ type Query {
406
+ t: T
407
+ i: I
408
+ }
409
+
410
+ type T {
411
+ v1: Int
412
+ v2: String
413
+ v3: I
414
+ }
415
+
416
+ interface I {
417
+ x: Int
418
+ y: Int
419
+ }
420
+
421
+ type A implements I {
422
+ x: Int
423
+ y: Int
424
+ a1: String
425
+ a2: String
426
+ }
427
+
428
+ type B implements I {
429
+ x: Int
430
+ y: Int
431
+ b1: Int
432
+ b2: T
433
+ }
434
+ `);
435
+
436
+ const operation = parseOperation(schema, `
437
+ {
438
+ t {
439
+ v1
440
+ v3 {
441
+ x
442
+ }
443
+ }
444
+ i {
445
+ ... on A {
446
+ a1
447
+ a2
448
+ }
449
+ ... on B {
450
+ y
451
+ b2 {
452
+ v2
453
+ }
454
+ }
455
+ }
456
+ }
457
+ `);
458
+
459
+ test('forEachElement', () => {
460
+ // We collect a pair of (parent type, field-or-fragment).
461
+ const actual: [string, string][] = [];
462
+ operation.selectionSet.forEachElement((elt) => actual.push([elt.parentType.name, elt.toString()]));
463
+ expect(actual).toStrictEqual([
464
+ ['Query', 't'],
465
+ ['T', 'v1'],
466
+ ['T', 'v3'],
467
+ ['I', 'x'],
468
+ ['Query', 'i'],
469
+ ['I', '... on A'],
470
+ ['A', 'a1'],
471
+ ['A', 'a2'],
472
+ ['I', '... on B'],
473
+ ['B', 'y'],
474
+ ['B', 'b2'],
475
+ ['T', 'v2'],
476
+ ]);
477
+ })
478
+ });
479
+
480
+
481
+ describe('MutableSelectionSet', () => {
482
+ test('memoizer', () => {
483
+ const schema = parseSchema(`
484
+ type Query {
485
+ t: T
486
+ }
487
+
488
+ type T {
489
+ v1: Int
490
+ v2: String
491
+ v3: Int
492
+ v4: Int
493
+ }
494
+ `);
495
+
496
+ type Value = {
497
+ count: number
498
+ };
499
+
500
+ let calls = 0;
501
+ const sets: string[] = [];
502
+
503
+ const queryType = schema.schemaDefinition.rootType('query')!;
504
+ const ss = MutableSelectionSet.emptyWithMemoized<Value>(
505
+ queryType,
506
+ (s) => {
507
+ sets.push(s.toString());
508
+ return { count: ++calls };
509
+ }
510
+ );
511
+
512
+ expect(ss.memoized().count).toBe(1);
513
+ // Calling a 2nd time with no change to make sure we're not re-generating the value.
514
+ expect(ss.memoized().count).toBe(1);
515
+
516
+ ss.updates().add(parseOperation(schema, `{ t { v1 } }`).selectionSet);
517
+
518
+ expect(ss.memoized().count).toBe(2);
519
+ expect(sets).toStrictEqual(['{}', '{ t { v1 } }']);
520
+
521
+ ss.updates().add(parseOperation(schema, `{ t { v3 } }`).selectionSet);
522
+
523
+ expect(ss.memoized().count).toBe(3);
524
+ expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }']);
525
+
526
+ // Still making sure we don't re-compute without updates.
527
+ expect(ss.memoized().count).toBe(3);
528
+
529
+ const cloned = ss.clone();
530
+ expect(cloned.memoized().count).toBe(3);
531
+
532
+ cloned.updates().add(parseOperation(schema, `{ t { v2 } }`).selectionSet);
533
+
534
+ // The value of `ss` should not have be recomputed, so it should still be 3.
535
+ expect(ss.memoized().count).toBe(3);
536
+ // But that of the clone should have changed.
537
+ expect(cloned.memoized().count).toBe(4);
538
+ expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }']);
539
+
540
+ // And here we make sure that if we update the fist selection, we don't have v3 in the set received
541
+ ss.updates().add(parseOperation(schema, `{ t { v4 } }`).selectionSet);
542
+ // Here, only `ss` memoized value has been recomputed. But since both increment the same `calls` variable,
543
+ // the total count should be 5 (even if the previous count for `ss` was only 3).
544
+ expect(ss.memoized().count).toBe(5);
545
+ expect(cloned.memoized().count).toBe(4);
546
+ expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }', '{ t { v1 v3 v4 } }']);
547
+ });
548
+ });
package/src/coreSpec.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ASTNode, DirectiveLocation, GraphQLError, StringValueNode } from "graphql";
2
+ import { URL } from "url";
2
3
  import { CoreFeature, Directive, DirectiveDefinition, EnumType, ErrGraphQLAPISchemaValidationFailed, ErrGraphQLValidationFailed, InputType, ListType, NamedType, NonNullType, ScalarType, Schema, SchemaDefinition, SchemaElement, sourceASTs } from "./definitions";
3
4
  import { sameType } from "./types";
4
5
  import { assert, firstOf } from './utils';
@@ -32,7 +32,16 @@ import {
32
32
  removeAllCoreFeatures,
33
33
  } from "./coreSpec";
34
34
  import { assert, mapValues, MapWithCachedArrays, removeArrayElement } from "./utils";
35
- import { withDefaultValues, valueEquals, valueToString, valueToAST, variablesInValue, valueFromAST, valueNodeToConstValueNode, argumentsEquals } from "./values";
35
+ import {
36
+ withDefaultValues,
37
+ valueEquals,
38
+ valueToString,
39
+ valueToAST,
40
+ valueFromAST,
41
+ valueNodeToConstValueNode,
42
+ argumentsEquals,
43
+ collectVariablesInValue
44
+ } from "./values";
36
45
  import { removeInaccessibleElements } from "./inaccessibleSpec";
37
46
  import { printDirectiveDefinition, printSchema } from './print';
38
47
  import { sameType } from './types';
@@ -343,54 +352,39 @@ export interface Named {
343
352
  export type ExtendableElement = SchemaDefinition | NamedType;
344
353
 
345
354
  export class DirectiveTargetElement<T extends DirectiveTargetElement<T>> {
346
- private _appliedDirectives: Directive<T>[] | undefined;
355
+ readonly appliedDirectives: Directive<T>[];
347
356
 
348
- constructor(private readonly _schema: Schema) {}
357
+ constructor(
358
+ private readonly _schema: Schema,
359
+ directives: readonly Directive<any>[] = [],
360
+ ) {
361
+ this.appliedDirectives = directives.map((d) => this.attachDirective(d));
362
+ }
349
363
 
350
364
  schema(): Schema {
351
365
  return this._schema;
352
366
  }
353
367
 
368
+ private attachDirective(directive: Directive<any>): Directive<T> {
369
+ // if the directive is not attached, we can assume we're fine just attaching it to use. Otherwise, we're "copying" it.
370
+ const toAdd = directive.isAttached()
371
+ ? new Directive(directive.name, directive.arguments())
372
+ : directive;
373
+
374
+ Element.prototype['setParent'].call(toAdd, this);
375
+ return toAdd;
376
+ }
377
+
354
378
  appliedDirectivesOf<TApplicationArgs extends {[key: string]: any} = {[key: string]: any}>(nameOrDefinition: string | DirectiveDefinition<TApplicationArgs>): Directive<T, TApplicationArgs>[] {
355
379
  const directiveName = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name;
356
380
  return this.appliedDirectives.filter(d => d.name == directiveName) as Directive<T, TApplicationArgs>[];
357
381
  }
358
382
 
359
- get appliedDirectives(): readonly Directive<T>[] {
360
- return this._appliedDirectives ?? [];
361
- }
362
-
363
383
  hasAppliedDirective(nameOrDefinition: string | DirectiveDefinition): boolean {
364
384
  const directiveName = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name;
365
385
  return this.appliedDirectives.some(d => d.name == directiveName);
366
386
  }
367
387
 
368
- applyDirective<TApplicationArgs extends {[key: string]: any} = {[key: string]: any}>(
369
- defOrDirective: Directive<T, TApplicationArgs> | DirectiveDefinition<TApplicationArgs>,
370
- args?: TApplicationArgs
371
- ): Directive<T, TApplicationArgs> {
372
- let toAdd: Directive<T, TApplicationArgs>;
373
- if (defOrDirective instanceof Directive) {
374
- if (defOrDirective.schema() != this.schema()) {
375
- throw new Error(`Cannot add directive ${defOrDirective} to ${this} as it is attached to another schema`);
376
- }
377
- toAdd = defOrDirective;
378
- if (args) {
379
- toAdd.setArguments(args);
380
- }
381
- } else {
382
- toAdd = new Directive<T, TApplicationArgs>(defOrDirective.name, args ?? Object.create(null));
383
- }
384
- Element.prototype['setParent'].call(toAdd, this);
385
- // TODO: we should typecheck arguments or our TApplicationArgs business is just a lie.
386
- if (this._appliedDirectives) {
387
- this._appliedDirectives.push(toAdd);
388
- } else {
389
- this._appliedDirectives = [ toAdd ];
390
- }
391
- return toAdd;
392
- }
393
-
394
388
  appliedDirectivesToDirectiveNodes() : ConstDirectiveNode[] | undefined {
395
389
  if (this.appliedDirectives.length == 0) {
396
390
  return undefined;
@@ -414,8 +408,10 @@ export class DirectiveTargetElement<T extends DirectiveTargetElement<T>> {
414
408
  : ' ' + this.appliedDirectives.join(' ');
415
409
  }
416
410
 
417
- variablesInAppliedDirectives(): Variables {
418
- return this.appliedDirectives.reduce((acc: Variables, d) => mergeVariables(acc, variablesInArguments(d.arguments())), []);
411
+ collectVariablesInAppliedDirectives(collector: VariableCollector) {
412
+ for (const applied of this.appliedDirectives) {
413
+ collector.collectInArguments(applied.arguments());
414
+ }
419
415
  }
420
416
  }
421
417
 
@@ -3069,7 +3065,7 @@ export class Directive<
3069
3065
  // applied to a field that is part of an extension, the field will have its extension set, but not the underlying directive.
3070
3066
  private _extension?: Extension<any>;
3071
3067
 
3072
- constructor(readonly name: string, private _args: TArgs) {
3068
+ constructor(readonly name: string, private _args: TArgs = Object.create(null)) {
3073
3069
  super();
3074
3070
  }
3075
3071
 
@@ -3314,38 +3310,38 @@ export class Variable {
3314
3310
 
3315
3311
  export type Variables = readonly Variable[];
3316
3312
 
3317
- export function mergeVariables(v1s: Variables, v2s: Variables): Variables {
3318
- if (v1s.length == 0) {
3319
- return v2s;
3313
+ export class VariableCollector {
3314
+ private readonly _variables = new Map<string, Variable>();
3315
+
3316
+ add(variable: Variable) {
3317
+ this._variables.set(variable.name, variable);
3320
3318
  }
3321
- if (v2s.length == 0) {
3322
- return v1s;
3319
+
3320
+ addAll(variables: Variables) {
3321
+ for (const variable of variables) {
3322
+ this.add(variable);
3323
+ }
3323
3324
  }
3324
- const res: Variable[] = v1s.concat();
3325
- for (const v of v2s) {
3326
- if (!containsVariable(v1s, v)) {
3327
- res.push(v);
3325
+
3326
+ collectInArguments(args: {[key: string]: any}) {
3327
+ for (const value of Object.values(args)) {
3328
+ collectVariablesInValue(value, this);
3328
3329
  }
3329
3330
  }
3330
- return res;
3331
- }
3332
3331
 
3333
- export function containsVariable(variables: Variables, toCheck: Variable): boolean {
3334
- return variables.some(v => v.name == toCheck.name);
3332
+ variables() {
3333
+ return mapValues(this._variables);
3334
+ }
3335
+
3336
+ toString(): string {
3337
+ return this.variables().toString();
3338
+ }
3335
3339
  }
3336
3340
 
3337
3341
  export function isVariable(v: any): v is Variable {
3338
3342
  return v instanceof Variable;
3339
3343
  }
3340
3344
 
3341
- export function variablesInArguments(args: {[key: string]: any}): Variables {
3342
- let variables: Variables = [];
3343
- for (const value of Object.values(args)) {
3344
- variables = mergeVariables(variables, variablesInValue(value));
3345
- }
3346
- return variables;
3347
- }
3348
-
3349
3345
  export class VariableDefinition extends DirectiveTargetElement<VariableDefinition> {
3350
3346
  constructor(
3351
3347
  schema: Schema,
package/src/federation.ts CHANGED
@@ -48,7 +48,7 @@ import {
48
48
  } from "graphql";
49
49
  import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule";
50
50
  import { buildSchema, buildSchemaFromAST } from "./buildSchema";
51
- import { parseSelectionSet, selectionOfElement, SelectionSet } from './operations';
51
+ import { parseSelectionSet, SelectionSet } from './operations';
52
52
  import { TAG_VERSIONS } from "./tagSpec";
53
53
  import {
54
54
  errorCodeDef,
@@ -129,7 +129,7 @@ function validateFieldSetSelections({
129
129
  allowFieldsWithArguments: boolean,
130
130
  }): void {
131
131
  for (const selection of selectionSet.selections()) {
132
- const appliedDirectives = selection.element().appliedDirectives;
132
+ const appliedDirectives = selection.element.appliedDirectives;
133
133
  if (appliedDirectives.length > 0) {
134
134
  onError(ERROR_CATEGORIES.DIRECTIVE_IN_FIELDS_ARG.get(directiveName).err(
135
135
  `cannot have directive applications in the @${directiveName}(fields:) argument but found ${appliedDirectives.join(', ')}.`,
@@ -137,7 +137,7 @@ function validateFieldSetSelections({
137
137
  }
138
138
 
139
139
  if (selection.kind === 'FieldSelection') {
140
- const field = selection.element().definition;
140
+ const field = selection.element.definition;
141
141
  const isExternal = metadata.isFieldExternal(field);
142
142
  if (!allowFieldsWithArguments && field.hasArguments()) {
143
143
  onError(ERROR_CATEGORIES.FIELDS_HAS_ARGS.get(directiveName).err(
@@ -1817,12 +1817,17 @@ class ExternalTester {
1817
1817
  private collectProvidedFields() {
1818
1818
  for (const provides of this.metadata().providesDirective().applications()) {
1819
1819
  const parent = provides.parent as FieldDefinition<CompositeType>;
1820
- collectTargetFields({
1821
- parentType: baseType(parent.type!) as CompositeType,
1822
- directive: provides as Directive<any, {fields: any}>,
1823
- includeInterfaceFieldsImplementations: true,
1824
- validate: false,
1825
- }).forEach((f) => this.providedFields.add(f.coordinate));
1820
+ const parentType = baseType(parent.type!);
1821
+ // If `parentType` is not a composite, that means an invalid @provides, but we ignore such errors
1822
+ // for now (also why we pass 'validate: false'). Proper errors will be thrown later during validation.
1823
+ if (isCompositeType(parentType)) {
1824
+ collectTargetFields({
1825
+ parentType,
1826
+ directive: provides as Directive<any, {fields: any}>,
1827
+ includeInterfaceFieldsImplementations: true,
1828
+ validate: false,
1829
+ }).forEach((f) => this.providedFields.add(f.coordinate));
1830
+ }
1826
1831
  }
1827
1832
  }
1828
1833
 
@@ -1854,7 +1859,7 @@ class ExternalTester {
1854
1859
 
1855
1860
  selectsAnyExternalField(selectionSet: SelectionSet): boolean {
1856
1861
  for (const selection of selectionSet.selections()) {
1857
- if (selection.kind === 'FieldSelection' && this.isExternal(selection.element().definition)) {
1862
+ if (selection.kind === 'FieldSelection' && this.isExternal(selection.element.definition)) {
1858
1863
  return true;
1859
1864
  }
1860
1865
  if (selection.selectionSet) {
@@ -1966,7 +1971,7 @@ function selectsNonExternalLeafField(selection: SelectionSet): boolean {
1966
1971
  return selection.selections().some(s => {
1967
1972
  if (s.kind === 'FieldSelection') {
1968
1973
  // If it's external, we're good and don't need to recurse.
1969
- if (isExternalOrHasExternalImplementations(s.field.definition)) {
1974
+ if (isExternalOrHasExternalImplementations(s.element.definition)) {
1970
1975
  return false;
1971
1976
  }
1972
1977
  // Otherwise, we select a non-external if it's a leaf, or the sub-selection does.
@@ -1978,25 +1983,24 @@ function selectsNonExternalLeafField(selection: SelectionSet): boolean {
1978
1983
  }
1979
1984
 
1980
1985
  function withoutNonExternalLeafFields(selectionSet: SelectionSet): SelectionSet {
1981
- const newSelectionSet = new SelectionSet(selectionSet.parentType);
1982
- for (const selection of selectionSet.selections()) {
1986
+ return selectionSet.lazyMap((selection) => {
1983
1987
  if (selection.kind === 'FieldSelection') {
1984
- if (isExternalOrHasExternalImplementations(selection.field.definition)) {
1988
+ if (isExternalOrHasExternalImplementations(selection.element.definition)) {
1985
1989
  // That field is external, so we can add the selection back entirely.
1986
- newSelectionSet.add(selection);
1987
- continue;
1990
+ return selection;
1988
1991
  }
1989
1992
  }
1990
- // Note that for fragments will always be true (and we just recurse), while
1991
- // for fields, we'll only get here if the field is not external, and so
1992
- // we want to add the selection only if it's not a leaf and even then, only
1993
- // the part where we've recursed.
1994
1993
  if (selection.selectionSet) {
1994
+ // Note that for fragments this will always be true (and we just recurse), while
1995
+ // for fields, we'll only get here if the field is not external, and so
1996
+ // we want to add the selection only if it's not a leaf and even then, only
1997
+ // the part where we've recursed.
1995
1998
  const updated = withoutNonExternalLeafFields(selection.selectionSet);
1996
1999
  if (!updated.isEmpty()) {
1997
- newSelectionSet.add(selectionOfElement(selection.element(), updated));
2000
+ return selection.withUpdatedSelectionSet(updated);
1998
2001
  }
1999
2002
  }
2000
- }
2001
- return newSelectionSet;
2003
+ // We skip that selection.
2004
+ return undefined;
2005
+ });
2002
2006
  }