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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -32,6 +32,34 @@ function astSSet(...selections: SelectionNode[]): SelectionSetNode {
32
32
  }
33
33
 
34
34
  describe('fragments optimization', () => {
35
+ // Takes a query with fragments as inputs, expand all those fragments, and ensures that all the
36
+ // fragments gets optimized back, and that we get back the exact same query.
37
+ function testFragmentsRoundtrip({
38
+ schema,
39
+ query,
40
+ expanded,
41
+ }: {
42
+ schema: Schema,
43
+ query: string,
44
+ expanded: string,
45
+ }) {
46
+ const operation = parseOperation(schema, query);
47
+ // We call `trimUnsatisfiableBranches` because the selections we care about in the query planner
48
+ // will effectively have had gone through that function (and even if that function wasn't called,
49
+ // the query planning algorithm would still end up removing unsatisfiable branches anyway), so
50
+ // it is a more interesting test.
51
+ const withoutFragments = operation.expandAllFragments().trimUnsatisfiableBranches();
52
+
53
+ expect(withoutFragments.toString()).toMatchString(expanded);
54
+
55
+ // We force keeping all reused fragments, even if they are used only once, because the tests using
56
+ // this are just about testing the reuse of fragments and this make things shorter/easier to write.
57
+ // There is tests in `buildPlan.test.ts` that double-check that we don't reuse fragments used only
58
+ // once in actual query plans.
59
+ const optimized = withoutFragments.optimize(operation.selectionSet.fragments!, 1);
60
+ expect(optimized.toString()).toMatchString(operation.toString());
61
+ }
62
+
35
63
  test('handles fragments using other fragments', () => {
36
64
  const schema = parseSchema(`
37
65
  type Query {
@@ -126,9 +154,8 @@ describe('fragments optimization', () => {
126
154
  `);
127
155
 
128
156
  const optimized = withoutFragments.optimize(operation.selectionSet.fragments!);
129
- // Note that we expect onU to *not* be recreated because, by default, optimize only
130
- // add add back a fragment if it is used at least twice (otherwise, the fragment just
131
- // make the query bigger).
157
+ // Note that while we didn't use `onU` for `t` in the query, it's technically ok to use
158
+ // it and it makes the query smaller, so it gets used.
132
159
  expect(optimized.toString()).toMatchString(`
133
160
  fragment OnT1 on T1 {
134
161
  a
@@ -144,15 +171,17 @@ describe('fragments optimization', () => {
144
171
  b
145
172
  }
146
173
 
174
+ fragment OnU on U {
175
+ ...OnI
176
+ ...OnT1
177
+ ...OnT2
178
+ }
179
+
147
180
  {
148
181
  t {
149
- ...OnT1
150
- ...OnT2
151
- ...OnI
182
+ ...OnU
152
183
  u {
153
- ...OnI
154
- ...OnT1
155
- ...OnT2
184
+ ...OnU
156
185
  }
157
186
  }
158
187
  }
@@ -176,187 +205,537 @@ describe('fragments optimization', () => {
176
205
  }
177
206
  `);
178
207
 
179
- const operation = parseOperation(schema, `
180
- fragment OnT1 on T1 {
181
- t2 {
182
- x
183
- }
184
- }
185
-
186
- query {
187
- t1a {
188
- ...OnT1
208
+ testFragmentsRoundtrip({
209
+ schema,
210
+ query: `
211
+ fragment OnT1 on T1 {
189
212
  t2 {
190
- y
213
+ x
191
214
  }
192
215
  }
193
- t2a {
194
- ...OnT1
195
- }
196
- }
197
- `);
198
216
 
199
- const withoutFragments = parseOperation(schema, operation.toString(true, true));
200
- expect(withoutFragments.toString()).toMatchString(`
201
- {
202
- t1a {
203
- ... on T1 {
217
+ query {
218
+ t1a {
219
+ ...OnT1
204
220
  t2 {
205
- x
221
+ y
206
222
  }
207
223
  }
208
- t2 {
209
- y
224
+ t2a {
225
+ ...OnT1
210
226
  }
211
227
  }
212
- t2a {
213
- ... on T1 {
228
+ `,
229
+ expanded: `
230
+ {
231
+ t1a {
232
+ t2 {
233
+ x
234
+ y
235
+ }
236
+ }
237
+ t2a {
214
238
  t2 {
215
239
  x
216
240
  }
217
241
  }
218
242
  }
243
+ `,
244
+ });
245
+ });
246
+
247
+ test('handles nested fragments with field intersection', () => {
248
+ const schema = parseSchema(`
249
+ type Query {
250
+ t: T
251
+ }
252
+
253
+ type T {
254
+ a: A
255
+ b: Int
256
+ }
257
+
258
+ type A {
259
+ x: String
260
+ y: String
261
+ z: String
219
262
  }
220
263
  `);
221
264
 
222
- const optimized = withoutFragments.optimize(operation.selectionSet.fragments!);
223
- expect(optimized.toString()).toMatchString(`
224
- fragment OnT1 on T1 {
225
- t2 {
265
+
266
+ // The subtlety here is that `FA` contains `__typename` and so after we're reused it, the
267
+ // selection will look like:
268
+ // {
269
+ // t {
270
+ // a {
271
+ // ...FA
272
+ // }
273
+ // }
274
+ // }
275
+ // But to recognize that `FT` can be reused from there, we need to be able to see that
276
+ // the `__typename` that `FT` wants is inside `FA` (and since FA applies on the parent type `A`
277
+ // directly, it is fine to reuse).
278
+ testFragmentsRoundtrip({
279
+ schema,
280
+ query: `
281
+ fragment FA on A {
282
+ __typename
226
283
  x
284
+ y
227
285
  }
228
- }
229
286
 
230
- {
231
- t1a {
232
- ...OnT1
233
- t2 {
234
- y
287
+ fragment FT on T {
288
+ a {
289
+ __typename
290
+ ...FA
235
291
  }
236
292
  }
237
- t2a {
238
- ...OnT1
293
+
294
+ query {
295
+ t {
296
+ ...FT
297
+ }
298
+ }
299
+ `,
300
+ expanded: `
301
+ {
302
+ t {
303
+ a {
304
+ __typename
305
+ x
306
+ y
307
+ }
308
+ }
239
309
  }
310
+ `,
311
+ });
312
+ });
313
+
314
+ test('handles fragment matching subset of field selection', () => {
315
+ const schema = parseSchema(`
316
+ type Query {
317
+ t: T
318
+ }
319
+
320
+ type T {
321
+ a: String
322
+ b: B
323
+ c: Int
324
+ d: D
325
+ }
326
+
327
+ type B {
328
+ x: String
329
+ y: String
330
+ }
331
+
332
+ type D {
333
+ m: String
334
+ n: String
240
335
  }
241
336
  `);
337
+
338
+ testFragmentsRoundtrip({
339
+ schema,
340
+ query: `
341
+ fragment FragT on T {
342
+ b {
343
+ __typename
344
+ x
345
+ }
346
+ c
347
+ d {
348
+ m
349
+ }
350
+ }
351
+
352
+ {
353
+ t {
354
+ ...FragT
355
+ d {
356
+ n
357
+ }
358
+ a
359
+ }
360
+ }
361
+ `,
362
+ expanded: `
363
+ {
364
+ t {
365
+ b {
366
+ __typename
367
+ x
368
+ }
369
+ c
370
+ d {
371
+ m
372
+ n
373
+ }
374
+ a
375
+ }
376
+ }
377
+ `,
378
+ });
242
379
  });
243
- });
244
380
 
245
- describe('selection set freezing', () => {
246
- const schema = parseSchema(`
247
- type Query {
248
- t: T
249
- }
381
+ test('handles fragment matching subset of inline fragment selection', () => {
382
+ // Pretty much the same test than the previous one, but matching inside a fragment selection inside
383
+ // of inside a field selection.
384
+ const schema = parseSchema(`
385
+ type Query {
386
+ i: I
387
+ }
250
388
 
251
- type T {
252
- a: Int
253
- b: Int
254
- }
255
- `);
389
+ interface I {
390
+ a: String
391
+ }
256
392
 
257
- const tField = schema.schemaDefinition.rootType('query')!.field('t')!;
393
+ type T {
394
+ a: String
395
+ b: B
396
+ c: Int
397
+ d: D
398
+ }
258
399
 
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;
400
+ type B {
401
+ x: String
402
+ y: String
403
+ }
264
404
 
265
- const s = new SelectionSet(schema.schemaDefinition.rootType('query')!);
405
+ type D {
406
+ m: String
407
+ n: String
408
+ }
409
+ `);
266
410
 
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();
411
+ testFragmentsRoundtrip({
412
+ schema,
413
+ query: `
414
+ fragment FragT on T {
415
+ b {
416
+ __typename
417
+ x
418
+ }
419
+ c
420
+ d {
421
+ m
422
+ }
423
+ }
270
424
 
271
- s.freeze();
272
- expect(s.isFrozen()).toBeTruthy();
273
- expect(() => s.mergeIn(s2)).toThrowError('Cannot add to frozen selection: { t { a } }');
425
+ {
426
+ i {
427
+ ... on T {
428
+ ...FragT
429
+ d {
430
+ n
431
+ }
432
+ a
433
+ }
434
+ }
435
+ }
436
+ `,
437
+ expanded: `
438
+ {
439
+ i {
440
+ ... on T {
441
+ b {
442
+ __typename
443
+ x
444
+ }
445
+ c
446
+ d {
447
+ m
448
+ n
449
+ }
450
+ a
451
+ }
452
+ }
453
+ }
454
+ `,
455
+ });
274
456
  });
275
457
 
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
- });
458
+ test('intersecting fragments', () => {
459
+ const schema = parseSchema(`
460
+ type Query {
461
+ t: T
462
+ }
463
+
464
+ type T {
465
+ a: String
466
+ b: B
467
+ c: Int
468
+ d: D
469
+ }
470
+
471
+ type B {
472
+ x: String
473
+ y: String
474
+ }
309
475
 
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')!);
476
+ type D {
477
+ m: String
478
+ n: String
479
+ }
480
+ `);
314
481
 
315
- s.mergeIn(s1);
316
- s.mergeIn(s2);
482
+ testFragmentsRoundtrip({
483
+ schema,
484
+ // Note: the code that reuse fragments iterates on fragments in the order they are defined in the document, but when it reuse
485
+ // a fragment, it puts it at the beginning of the selection (somewhat random, it just feel often easier to read), so the net
486
+ // effect on this example is that `Frag2`, which will be reused after `Frag1` will appear first in the re-optimized selection.
487
+ // So we put it first in the input too so that input and output actually match (the `testFragmentsRoundtrip` compares strings,
488
+ // so it is sensible to ordering; we could theoretically use `Operation.equals` instead of string equality, which wouldn't
489
+ // really on ordering, but `Operation.equals` is not entirely trivial and comparing strings make problem a bit more obvious).
490
+ query: `
491
+ fragment Frag1 on T {
492
+ b {
493
+ x
494
+ }
495
+ c
496
+ d {
497
+ m
498
+ }
499
+ }
317
500
 
318
- // We check S is what we expect...
319
- expect(s.toString()).toBe('{ t { a b } }');
501
+ fragment Frag2 on T {
502
+ a
503
+ b {
504
+ __typename
505
+ x
506
+ }
507
+ d {
508
+ m
509
+ n
510
+ }
511
+ }
320
512
 
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 } }');
513
+ {
514
+ t {
515
+ ...Frag2
516
+ ...Frag1
517
+ }
518
+ }
519
+ `,
520
+ expanded: `
521
+ {
522
+ t {
523
+ a
524
+ b {
525
+ __typename
526
+ x
527
+ }
528
+ d {
529
+ m
530
+ n
531
+ }
532
+ c
533
+ }
534
+ }
535
+ `,
536
+ });
324
537
  });
325
538
 
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')!);
539
+ test('fragments whose application makes a type condition trivial', () => {
540
+ const schema = parseSchema(`
541
+ type Query {
542
+ t: T
543
+ }
544
+
545
+ interface I {
546
+ x: String
547
+ }
331
548
 
332
- s.mergeIn(s1);
333
- s.mergeIn(s2);
549
+ type T implements I {
550
+ x: String
551
+ a: String
552
+ }
553
+ `);
334
554
 
335
- expect(s.toString()).toBe('{ ... on Query { t { ... on T { a b } } } }');
555
+ testFragmentsRoundtrip({
556
+ schema,
557
+ query: `
558
+ fragment FragI on I {
559
+ x
560
+ ... on T {
561
+ a
562
+ }
563
+ }
336
564
 
337
- expect(s1.toString()).toBe('{ ... on Query { t { ... on T { a } } } }');
338
- expect(s2.toString()).toBe('{ ... on Query { t { ... on T { b } } } }');
565
+ {
566
+ t {
567
+ ...FragI
568
+ }
569
+ }
570
+ `,
571
+ expanded: `
572
+ {
573
+ t {
574
+ x
575
+ a
576
+ }
577
+ }
578
+ `,
579
+ });
339
580
  });
340
581
 
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 } }');
582
+ describe('applied directives', () => {
583
+ test('reuse fragments with directives on the fragment, but only when there is those directives', () => {
584
+ const schema = parseSchema(`
585
+ type Query {
586
+ t1: T
587
+ t2: T
588
+ t3: T
589
+ }
590
+
591
+ type T {
592
+ a: Int
593
+ b: Int
594
+ c: Int
595
+ d: Int
596
+ }
597
+ `);
598
+
599
+ testFragmentsRoundtrip({
600
+ schema,
601
+ query: `
602
+ fragment DirectiveOnDef on T @include(if: $cond1) {
603
+ a
604
+ }
605
+
606
+ query myQuery($cond1: Boolean!, $cond2: Boolean!) {
607
+ t1 {
608
+ ...DirectiveOnDef
609
+ }
610
+ t2 {
611
+ ... on T @include(if: $cond2) {
612
+ a
613
+ }
614
+ }
615
+ t3 {
616
+ ...DirectiveOnDef @include(if: $cond2)
617
+ }
618
+ }
619
+ `,
620
+ expanded: `
621
+ query myQuery($cond1: Boolean!, $cond2: Boolean!) {
622
+ t1 {
623
+ ... on T @include(if: $cond1) {
624
+ a
625
+ }
626
+ }
627
+ t2 {
628
+ ... on T @include(if: $cond2) {
629
+ a
630
+ }
631
+ }
632
+ t3 {
633
+ ... on T @include(if: $cond1) @include(if: $cond2) {
634
+ a
635
+ }
636
+ }
637
+ }
638
+ `,
639
+ });
640
+ });
641
+
642
+ test('reuse fragments with directives in the fragment selection, but only when there is those directives', () => {
643
+ const schema = parseSchema(`
644
+ type Query {
645
+ t1: T
646
+ t2: T
647
+ t3: T
648
+ }
649
+
650
+ type T {
651
+ a: Int
652
+ b: Int
653
+ c: Int
654
+ d: Int
655
+ }
656
+ `);
657
+
658
+ testFragmentsRoundtrip({
659
+ schema,
660
+ query: `
661
+ fragment DirectiveInDef on T {
662
+ a @include(if: $cond1)
663
+ }
664
+
665
+ query myQuery($cond1: Boolean!, $cond2: Boolean!) {
666
+ t1 {
667
+ a
668
+ }
669
+ t2 {
670
+ ...DirectiveInDef
671
+ }
672
+ t3 {
673
+ a @include(if: $cond2)
674
+ }
675
+ }
676
+ `,
677
+ expanded: `
678
+ query myQuery($cond1: Boolean!, $cond2: Boolean!) {
679
+ t1 {
680
+ a
681
+ }
682
+ t2 {
683
+ a @include(if: $cond1)
684
+ }
685
+ t3 {
686
+ a @include(if: $cond2)
687
+ }
688
+ }
689
+ `,
690
+ });
691
+ });
692
+
693
+ test('reuse fragments with directives on spread, but only when there is those directives', () => {
694
+ const schema = parseSchema(`
695
+ type Query {
696
+ t1: T
697
+ t2: T
698
+ t3: T
699
+ }
700
+
701
+ type T {
702
+ a: Int
703
+ b: Int
704
+ c: Int
705
+ d: Int
706
+ }
707
+ `);
708
+
709
+ testFragmentsRoundtrip({
710
+ schema,
711
+ query: `
712
+ fragment NoDirectiveDef on T {
713
+ a
714
+ }
715
+
716
+ query myQuery($cond1: Boolean!) {
717
+ t1 {
718
+ ...NoDirectiveDef
719
+ }
720
+ t2 {
721
+ ...NoDirectiveDef @include(if: $cond1)
722
+ }
723
+ }
724
+ `,
725
+ expanded: `
726
+ query myQuery($cond1: Boolean!) {
727
+ t1 {
728
+ a
729
+ }
730
+ t2 {
731
+ ... on T @include(if: $cond1) {
732
+ a
733
+ }
734
+ }
735
+ }
736
+ `,
737
+ });
738
+ });
360
739
  });
361
740
  });
362
741
 
@@ -517,3 +896,206 @@ describe('empty branches removal', () => {
517
896
  )).toBe('{ u }');
518
897
  });
519
898
  });
899
+
900
+ describe('basic operations', () => {
901
+ const schema = parseSchema(`
902
+ type Query {
903
+ t: T
904
+ i: I
905
+ }
906
+
907
+ type T {
908
+ v1: Int
909
+ v2: String
910
+ v3: I
911
+ }
912
+
913
+ interface I {
914
+ x: Int
915
+ y: Int
916
+ }
917
+
918
+ type A implements I {
919
+ x: Int
920
+ y: Int
921
+ a1: String
922
+ a2: String
923
+ }
924
+
925
+ type B implements I {
926
+ x: Int
927
+ y: Int
928
+ b1: Int
929
+ b2: T
930
+ }
931
+ `);
932
+
933
+ const operation = parseOperation(schema, `
934
+ {
935
+ t {
936
+ v1
937
+ v3 {
938
+ x
939
+ }
940
+ }
941
+ i {
942
+ ... on A {
943
+ a1
944
+ a2
945
+ }
946
+ ... on B {
947
+ y
948
+ b2 {
949
+ v2
950
+ }
951
+ }
952
+ }
953
+ }
954
+ `);
955
+
956
+ test('forEachElement', () => {
957
+ // We collect a pair of (parent type, field-or-fragment).
958
+ const actual: [string, string][] = [];
959
+ operation.selectionSet.forEachElement((elt) => actual.push([elt.parentType.name, elt.toString()]));
960
+ expect(actual).toStrictEqual([
961
+ ['Query', 't'],
962
+ ['T', 'v1'],
963
+ ['T', 'v3'],
964
+ ['I', 'x'],
965
+ ['Query', 'i'],
966
+ ['I', '... on A'],
967
+ ['A', 'a1'],
968
+ ['A', 'a2'],
969
+ ['I', '... on B'],
970
+ ['B', 'y'],
971
+ ['B', 'b2'],
972
+ ['T', 'v2'],
973
+ ]);
974
+ })
975
+ });
976
+
977
+ describe('MutableSelectionSet', () => {
978
+ test('memoizer', () => {
979
+ const schema = parseSchema(`
980
+ type Query {
981
+ t: T
982
+ }
983
+
984
+ type T {
985
+ v1: Int
986
+ v2: String
987
+ v3: Int
988
+ v4: Int
989
+ }
990
+ `);
991
+
992
+ type Value = {
993
+ count: number
994
+ };
995
+
996
+ let calls = 0;
997
+ const sets: string[] = [];
998
+
999
+ const queryType = schema.schemaDefinition.rootType('query')!;
1000
+ const ss = MutableSelectionSet.emptyWithMemoized<Value>(
1001
+ queryType,
1002
+ (s) => {
1003
+ sets.push(s.toString());
1004
+ return { count: ++calls };
1005
+ }
1006
+ );
1007
+
1008
+ expect(ss.memoized().count).toBe(1);
1009
+ // Calling a 2nd time with no change to make sure we're not re-generating the value.
1010
+ expect(ss.memoized().count).toBe(1);
1011
+
1012
+ ss.updates().add(parseOperation(schema, `{ t { v1 } }`).selectionSet);
1013
+
1014
+ expect(ss.memoized().count).toBe(2);
1015
+ expect(sets).toStrictEqual(['{}', '{ t { v1 } }']);
1016
+
1017
+ ss.updates().add(parseOperation(schema, `{ t { v3 } }`).selectionSet);
1018
+
1019
+ expect(ss.memoized().count).toBe(3);
1020
+ expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }']);
1021
+
1022
+ // Still making sure we don't re-compute without updates.
1023
+ expect(ss.memoized().count).toBe(3);
1024
+
1025
+ const cloned = ss.clone();
1026
+ expect(cloned.memoized().count).toBe(3);
1027
+
1028
+ cloned.updates().add(parseOperation(schema, `{ t { v2 } }`).selectionSet);
1029
+
1030
+ // The value of `ss` should not have be recomputed, so it should still be 3.
1031
+ expect(ss.memoized().count).toBe(3);
1032
+ // But that of the clone should have changed.
1033
+ expect(cloned.memoized().count).toBe(4);
1034
+ expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }']);
1035
+
1036
+ // And here we make sure that if we update the fist selection, we don't have v3 in the set received
1037
+ ss.updates().add(parseOperation(schema, `{ t { v4 } }`).selectionSet);
1038
+ // Here, only `ss` memoized value has been recomputed. But since both increment the same `calls` variable,
1039
+ // the total count should be 5 (even if the previous count for `ss` was only 3).
1040
+ expect(ss.memoized().count).toBe(5);
1041
+ expect(cloned.memoized().count).toBe(4);
1042
+ expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }', '{ t { v1 v3 v4 } }']);
1043
+ });
1044
+ });
1045
+
1046
+ describe('unsatisfiable branches removal', () => {
1047
+ const schema = parseSchema(`
1048
+ type Query {
1049
+ i: I
1050
+ j: J
1051
+ }
1052
+
1053
+ interface I {
1054
+ a: Int
1055
+ b: Int
1056
+ }
1057
+
1058
+ interface J {
1059
+ b: Int
1060
+ }
1061
+
1062
+ type T1 implements I & J {
1063
+ a: Int
1064
+ b: Int
1065
+ c: Int
1066
+ }
1067
+
1068
+ type T2 implements I {
1069
+ a: Int
1070
+ b: Int
1071
+ d: Int
1072
+ }
1073
+
1074
+ type T3 implements J {
1075
+ a: Int
1076
+ b: Int
1077
+ d: Int
1078
+ }
1079
+ `);
1080
+
1081
+ const withoutUnsatisfiableBranches = (op: string) => {
1082
+ return parseOperation(schema, op).trimUnsatisfiableBranches().toString(false, false)
1083
+ };
1084
+
1085
+
1086
+ it.each([
1087
+ '{ i { a } }',
1088
+ '{ i { ... on T1 { a b c } } }',
1089
+ ])('is identity if there is no unsatisfiable branches', (op) => {
1090
+ expect(withoutUnsatisfiableBranches(op)).toBe(op);
1091
+ });
1092
+
1093
+ it.each([
1094
+ { input: '{ i { ... on I { a } } }', output: '{ i { a } }' },
1095
+ { input: '{ i { ... on T1 { ... on I { a b } } } }', output: '{ i { ... on T1 { a b } } }' },
1096
+ { input: '{ i { ... on I { a ... on T2 { d } } } }', output: '{ i { a ... on T2 { d } } }' },
1097
+ { input: '{ i { ... on T2 { ... on I { a ... on J { b } } } } }', output: '{ i { ... on T2 { a } } }' },
1098
+ ])('removes unsatisfiable branches', ({input, output}) => {
1099
+ expect(withoutUnsatisfiableBranches(input)).toBe(output);
1100
+ });
1101
+ });