@apollo/federation-internals 2.4.5 → 2.4.7

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.
@@ -1,1266 +0,0 @@
1
- import {
2
- defaultRootName,
3
- Schema,
4
- SchemaRootKind,
5
- } from '../../dist/definitions';
6
- import { buildSchema } from '../../dist/buildSchema';
7
- import { MutableSelectionSet, Operation, operationFromDocument, parseOperation } from '../../dist/operations';
8
- import './matchers';
9
- import { DocumentNode, FieldNode, GraphQLError, Kind, OperationDefinitionNode, OperationTypeNode, SelectionNode, SelectionSetNode } from 'graphql';
10
-
11
- function parseSchema(schema: string): Schema {
12
- try {
13
- return buildSchema(schema);
14
- } catch (e) {
15
- throw new Error('Error parsing the schema:\n' + e.toString());
16
- }
17
- }
18
-
19
- function astField(name: string, selectionSet?: SelectionSetNode): FieldNode {
20
- return {
21
- kind: Kind.FIELD,
22
- name: { kind: Kind.NAME, value: name },
23
- selectionSet,
24
- };
25
- }
26
-
27
- function astSSet(...selections: SelectionNode[]): SelectionSetNode {
28
- return {
29
- kind: Kind.SELECTION_SET,
30
- selections,
31
- };
32
- }
33
-
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
-
63
- test('handles fragments using other fragments', () => {
64
- const schema = parseSchema(`
65
- type Query {
66
- t: T1
67
- }
68
-
69
- interface I {
70
- b: Int
71
- }
72
-
73
- type T1 {
74
- a: Int
75
- b: Int
76
- u: U
77
- }
78
-
79
- type T2 {
80
- x: String
81
- y: String
82
- b: Int
83
- u: U
84
- }
85
-
86
- union U = T1 | T2
87
- `);
88
-
89
- const operation = parseOperation(schema, `
90
- fragment OnT1 on T1 {
91
- a
92
- b
93
- }
94
-
95
- fragment OnT2 on T2 {
96
- x
97
- y
98
- }
99
-
100
- fragment OnI on I {
101
- b
102
- }
103
-
104
- fragment OnU on U {
105
- ...OnI
106
- ...OnT1
107
- ...OnT2
108
- }
109
-
110
- query {
111
- t {
112
- ...OnT1
113
- ...OnT2
114
- ...OnI
115
- u {
116
- ...OnU
117
- }
118
- }
119
- }
120
- `);
121
-
122
- const withoutFragments = parseOperation(schema, operation.toString(true, true));
123
- expect(withoutFragments.toString()).toMatchString(`
124
- {
125
- t {
126
- ... on T1 {
127
- a
128
- b
129
- }
130
- ... on T2 {
131
- x
132
- y
133
- }
134
- ... on I {
135
- b
136
- }
137
- u {
138
- ... on U {
139
- ... on I {
140
- b
141
- }
142
- ... on T1 {
143
- a
144
- b
145
- }
146
- ... on T2 {
147
- x
148
- y
149
- }
150
- }
151
- }
152
- }
153
- }
154
- `);
155
-
156
- const optimized = withoutFragments.optimize(operation.selectionSet.fragments!);
157
- expect(optimized.toString()).toMatchString(`
158
- fragment OnT1 on T1 {
159
- a
160
- b
161
- }
162
-
163
- fragment OnT2 on T2 {
164
- x
165
- y
166
- }
167
-
168
- fragment OnI on I {
169
- b
170
- }
171
-
172
- {
173
- t {
174
- ...OnI
175
- ...OnT1
176
- ...OnT2
177
- u {
178
- ...OnI
179
- ...OnT1
180
- ...OnT2
181
- }
182
- }
183
- }
184
- `);
185
- });
186
-
187
- test('handles fragments with nested selections', () => {
188
- const schema = parseSchema(`
189
- type Query {
190
- t1a: T1
191
- t2a: T1
192
- }
193
-
194
- type T1 {
195
- t2: T2
196
- }
197
-
198
- type T2 {
199
- x: String
200
- y: String
201
- }
202
- `);
203
-
204
- testFragmentsRoundtrip({
205
- schema,
206
- query: `
207
- fragment OnT1 on T1 {
208
- t2 {
209
- x
210
- }
211
- }
212
-
213
- query {
214
- t1a {
215
- ...OnT1
216
- t2 {
217
- y
218
- }
219
- }
220
- t2a {
221
- ...OnT1
222
- }
223
- }
224
- `,
225
- expanded: `
226
- {
227
- t1a {
228
- t2 {
229
- x
230
- y
231
- }
232
- }
233
- t2a {
234
- t2 {
235
- x
236
- }
237
- }
238
- }
239
- `,
240
- });
241
- });
242
-
243
- test('handles nested fragments with field intersection', () => {
244
- const schema = parseSchema(`
245
- type Query {
246
- t: T
247
- }
248
-
249
- type T {
250
- a: A
251
- b: Int
252
- }
253
-
254
- type A {
255
- x: String
256
- y: String
257
- z: String
258
- }
259
- `);
260
-
261
-
262
- // The subtlety here is that `FA` contains `__typename` and so after we're reused it, the
263
- // selection will look like:
264
- // {
265
- // t {
266
- // a {
267
- // ...FA
268
- // }
269
- // }
270
- // }
271
- // But to recognize that `FT` can be reused from there, we need to be able to see that
272
- // the `__typename` that `FT` wants is inside `FA` (and since FA applies on the parent type `A`
273
- // directly, it is fine to reuse).
274
- testFragmentsRoundtrip({
275
- schema,
276
- query: `
277
- fragment FA on A {
278
- __typename
279
- x
280
- y
281
- }
282
-
283
- fragment FT on T {
284
- a {
285
- __typename
286
- ...FA
287
- }
288
- }
289
-
290
- query {
291
- t {
292
- ...FT
293
- }
294
- }
295
- `,
296
- expanded: `
297
- {
298
- t {
299
- a {
300
- __typename
301
- x
302
- y
303
- }
304
- }
305
- }
306
- `,
307
- });
308
- });
309
-
310
- test('handles fragment matching subset of field selection', () => {
311
- const schema = parseSchema(`
312
- type Query {
313
- t: T
314
- }
315
-
316
- type T {
317
- a: String
318
- b: B
319
- c: Int
320
- d: D
321
- }
322
-
323
- type B {
324
- x: String
325
- y: String
326
- }
327
-
328
- type D {
329
- m: String
330
- n: String
331
- }
332
- `);
333
-
334
- testFragmentsRoundtrip({
335
- schema,
336
- query: `
337
- fragment FragT on T {
338
- b {
339
- __typename
340
- x
341
- }
342
- c
343
- d {
344
- m
345
- }
346
- }
347
-
348
- {
349
- t {
350
- ...FragT
351
- d {
352
- n
353
- }
354
- a
355
- }
356
- }
357
- `,
358
- expanded: `
359
- {
360
- t {
361
- b {
362
- __typename
363
- x
364
- }
365
- c
366
- d {
367
- m
368
- n
369
- }
370
- a
371
- }
372
- }
373
- `,
374
- });
375
- });
376
-
377
- test('handles fragment matching subset of inline fragment selection', () => {
378
- // Pretty much the same test than the previous one, but matching inside a fragment selection inside
379
- // of inside a field selection.
380
- const schema = parseSchema(`
381
- type Query {
382
- i: I
383
- }
384
-
385
- interface I {
386
- a: String
387
- }
388
-
389
- type T {
390
- a: String
391
- b: B
392
- c: Int
393
- d: D
394
- }
395
-
396
- type B {
397
- x: String
398
- y: String
399
- }
400
-
401
- type D {
402
- m: String
403
- n: String
404
- }
405
- `);
406
-
407
- testFragmentsRoundtrip({
408
- schema,
409
- query: `
410
- fragment FragT on T {
411
- b {
412
- __typename
413
- x
414
- }
415
- c
416
- d {
417
- m
418
- }
419
- }
420
-
421
- {
422
- i {
423
- ... on T {
424
- ...FragT
425
- d {
426
- n
427
- }
428
- a
429
- }
430
- }
431
- }
432
- `,
433
- expanded: `
434
- {
435
- i {
436
- ... on T {
437
- b {
438
- __typename
439
- x
440
- }
441
- c
442
- d {
443
- m
444
- n
445
- }
446
- a
447
- }
448
- }
449
- }
450
- `,
451
- });
452
- });
453
-
454
- test('intersecting fragments', () => {
455
- const schema = parseSchema(`
456
- type Query {
457
- t: T
458
- }
459
-
460
- type T {
461
- a: String
462
- b: B
463
- c: Int
464
- d: D
465
- }
466
-
467
- type B {
468
- x: String
469
- y: String
470
- }
471
-
472
- type D {
473
- m: String
474
- n: String
475
- }
476
- `);
477
-
478
- testFragmentsRoundtrip({
479
- schema,
480
- // Note: the code that reuse fragments iterates on fragments in the order they are defined in the document, but when it reuse
481
- // a fragment, it puts it at the beginning of the selection (somewhat random, it just feel often easier to read), so the net
482
- // effect on this example is that `Frag2`, which will be reused after `Frag1` will appear first in the re-optimized selection.
483
- // So we put it first in the input too so that input and output actually match (the `testFragmentsRoundtrip` compares strings,
484
- // so it is sensible to ordering; we could theoretically use `Operation.equals` instead of string equality, which wouldn't
485
- // really on ordering, but `Operation.equals` is not entirely trivial and comparing strings make problem a bit more obvious).
486
- query: `
487
- fragment Frag1 on T {
488
- b {
489
- x
490
- }
491
- c
492
- d {
493
- m
494
- }
495
- }
496
-
497
- fragment Frag2 on T {
498
- a
499
- b {
500
- __typename
501
- x
502
- }
503
- d {
504
- m
505
- n
506
- }
507
- }
508
-
509
- {
510
- t {
511
- ...Frag2
512
- ...Frag1
513
- }
514
- }
515
- `,
516
- expanded: `
517
- {
518
- t {
519
- a
520
- b {
521
- __typename
522
- x
523
- }
524
- d {
525
- m
526
- n
527
- }
528
- c
529
- }
530
- }
531
- `,
532
- });
533
- });
534
-
535
- test('fragments whose application makes a type condition trivial', () => {
536
- const schema = parseSchema(`
537
- type Query {
538
- t: T
539
- }
540
-
541
- interface I {
542
- x: String
543
- }
544
-
545
- type T implements I {
546
- x: String
547
- a: String
548
- }
549
- `);
550
-
551
- testFragmentsRoundtrip({
552
- schema,
553
- query: `
554
- fragment FragI on I {
555
- x
556
- ... on T {
557
- a
558
- }
559
- }
560
-
561
- {
562
- t {
563
- ...FragI
564
- }
565
- }
566
- `,
567
- expanded: `
568
- {
569
- t {
570
- x
571
- a
572
- }
573
- }
574
- `,
575
- });
576
- });
577
-
578
- test('handles fragment matching at the top level of another fragment', () => {
579
- const schema = parseSchema(`
580
- type Query {
581
- t: T
582
- }
583
-
584
- type T {
585
- a: String
586
- u: U
587
- }
588
-
589
- type U {
590
- x: String
591
- y: String
592
- }
593
- `);
594
-
595
- testFragmentsRoundtrip({
596
- schema,
597
- query: `
598
- fragment Frag1 on T {
599
- a
600
- }
601
-
602
- fragment Frag2 on T {
603
- u {
604
- x
605
- y
606
- }
607
- ...Frag1
608
- }
609
-
610
- fragment Frag3 on Query {
611
- t {
612
- ...Frag2
613
- }
614
- }
615
-
616
- {
617
- ...Frag3
618
- }
619
- `,
620
- expanded: `
621
- {
622
- t {
623
- u {
624
- x
625
- y
626
- }
627
- a
628
- }
629
- }
630
- `,
631
- });
632
- });
633
-
634
- test('handles fragments used in a context where they get trimmed', () => {
635
- const schema = parseSchema(`
636
- type Query {
637
- t1: T1
638
- }
639
-
640
- interface I {
641
- x: Int
642
- }
643
-
644
- type T1 implements I {
645
- x: Int
646
- y: Int
647
- }
648
-
649
- type T2 implements I {
650
- x: Int
651
- z: Int
652
- }
653
- `);
654
-
655
- testFragmentsRoundtrip({
656
- schema,
657
- query: `
658
- fragment FragOnI on I {
659
- ... on T1 {
660
- y
661
- }
662
- ... on T2 {
663
- z
664
- }
665
- }
666
-
667
- {
668
- t1 {
669
- ...FragOnI
670
- }
671
- }
672
- `,
673
- expanded: `
674
- {
675
- t1 {
676
- y
677
- }
678
- }
679
- `,
680
- });
681
- });
682
-
683
- test('handles fragments used in the context of non-intersecting abstract types', () => {
684
- const schema = parseSchema(`
685
- type Query {
686
- i2: I2
687
- }
688
-
689
- interface I1 {
690
- x: Int
691
- }
692
-
693
- interface I2 {
694
- y: Int
695
- }
696
-
697
- interface I3 {
698
- z: Int
699
- }
700
-
701
- type T1 implements I1 & I2 {
702
- x: Int
703
- y: Int
704
- }
705
-
706
- type T2 implements I1 & I3 {
707
- x: Int
708
- z: Int
709
- }
710
- `);
711
-
712
- testFragmentsRoundtrip({
713
- schema,
714
- query: `
715
- fragment FragOnI1 on I1 {
716
- ... on I2 {
717
- y
718
- }
719
- ... on I3 {
720
- z
721
- }
722
- }
723
-
724
- {
725
- i2 {
726
- ...FragOnI1
727
- }
728
- }
729
- `,
730
- expanded: `
731
- {
732
- i2 {
733
- ... on I1 {
734
- ... on I2 {
735
- y
736
- }
737
- ... on I3 {
738
- z
739
- }
740
- }
741
- }
742
- }
743
- `,
744
- });
745
- });
746
-
747
- describe('applied directives', () => {
748
- test('reuse fragments with directives on the fragment, but only when there is those directives', () => {
749
- const schema = parseSchema(`
750
- type Query {
751
- t1: T
752
- t2: T
753
- t3: T
754
- }
755
-
756
- type T {
757
- a: Int
758
- b: Int
759
- c: Int
760
- d: Int
761
- }
762
- `);
763
-
764
- testFragmentsRoundtrip({
765
- schema,
766
- query: `
767
- fragment DirectiveOnDef on T @include(if: $cond1) {
768
- a
769
- }
770
-
771
- query myQuery($cond1: Boolean!, $cond2: Boolean!) {
772
- t1 {
773
- ...DirectiveOnDef
774
- }
775
- t2 {
776
- ... on T @include(if: $cond2) {
777
- a
778
- }
779
- }
780
- t3 {
781
- ...DirectiveOnDef @include(if: $cond2)
782
- }
783
- }
784
- `,
785
- expanded: `
786
- query myQuery($cond1: Boolean!, $cond2: Boolean!) {
787
- t1 {
788
- ... on T @include(if: $cond1) {
789
- a
790
- }
791
- }
792
- t2 {
793
- ... on T @include(if: $cond2) {
794
- a
795
- }
796
- }
797
- t3 {
798
- ... on T @include(if: $cond1) @include(if: $cond2) {
799
- a
800
- }
801
- }
802
- }
803
- `,
804
- });
805
- });
806
-
807
- test('reuse fragments with directives in the fragment selection, but only when there is those directives', () => {
808
- const schema = parseSchema(`
809
- type Query {
810
- t1: T
811
- t2: T
812
- t3: T
813
- }
814
-
815
- type T {
816
- a: Int
817
- b: Int
818
- c: Int
819
- d: Int
820
- }
821
- `);
822
-
823
- testFragmentsRoundtrip({
824
- schema,
825
- query: `
826
- fragment DirectiveInDef on T {
827
- a @include(if: $cond1)
828
- }
829
-
830
- query myQuery($cond1: Boolean!, $cond2: Boolean!) {
831
- t1 {
832
- a
833
- }
834
- t2 {
835
- ...DirectiveInDef
836
- }
837
- t3 {
838
- a @include(if: $cond2)
839
- }
840
- }
841
- `,
842
- expanded: `
843
- query myQuery($cond1: Boolean!, $cond2: Boolean!) {
844
- t1 {
845
- a
846
- }
847
- t2 {
848
- a @include(if: $cond1)
849
- }
850
- t3 {
851
- a @include(if: $cond2)
852
- }
853
- }
854
- `,
855
- });
856
- });
857
-
858
- test('reuse fragments with directives on spread, but only when there is those directives', () => {
859
- const schema = parseSchema(`
860
- type Query {
861
- t1: T
862
- t2: T
863
- t3: T
864
- }
865
-
866
- type T {
867
- a: Int
868
- b: Int
869
- c: Int
870
- d: Int
871
- }
872
- `);
873
-
874
- testFragmentsRoundtrip({
875
- schema,
876
- query: `
877
- fragment NoDirectiveDef on T {
878
- a
879
- }
880
-
881
- query myQuery($cond1: Boolean!) {
882
- t1 {
883
- ...NoDirectiveDef
884
- }
885
- t2 {
886
- ...NoDirectiveDef @include(if: $cond1)
887
- }
888
- }
889
- `,
890
- expanded: `
891
- query myQuery($cond1: Boolean!) {
892
- t1 {
893
- a
894
- }
895
- t2 {
896
- ... on T @include(if: $cond1) {
897
- a
898
- }
899
- }
900
- }
901
- `,
902
- });
903
- });
904
- });
905
- });
906
-
907
- describe('validations', () => {
908
- test.each([
909
- { directive: '@defer', rootKind: 'mutation' },
910
- { directive: '@defer', rootKind: 'subscription' },
911
- { directive: '@stream', rootKind: 'mutation' },
912
- { directive: '@stream', rootKind: 'subscription' },
913
- ])('reject $directive on $rootKind type', ({ directive, rootKind }) => {
914
- const schema = parseSchema(`
915
- type Query {
916
- x: String
917
- }
918
-
919
- type Mutation {
920
- x: String
921
- }
922
-
923
- type Subscription {
924
- x: String
925
- }
926
- `);
927
-
928
- expect(() => {
929
- parseOperation(schema, `
930
- ${rootKind} {
931
- ... ${directive} {
932
- x
933
- }
934
- }
935
- `)
936
- }).toThrowError(new GraphQLError(`The @defer and @stream directives cannot be used on ${rootKind} root type "${defaultRootName(rootKind as SchemaRootKind)}"`));
937
- });
938
-
939
- test('allows nullable variable for non-nullable input field with default', () => {
940
- const schema = parseSchema(`
941
- input I {
942
- x: Int! = 42
943
- }
944
-
945
- type Query {
946
- f(i: I): Int
947
- }
948
- `);
949
-
950
- // Just testing that this parse correctly and does not throw an exception.
951
- parseOperation(schema, `
952
- query test($x: Int) {
953
- f(i: { x: $x })
954
- }
955
- `);
956
- });
957
- });
958
-
959
- describe('empty branches removal', () => {
960
- const schema = parseSchema(`
961
- type Query {
962
- t: T
963
- u: Int
964
- }
965
-
966
- type T {
967
- a: Int
968
- b: Int
969
- c: C
970
- }
971
-
972
- type C {
973
- x: String
974
- y: String
975
- }
976
- `);
977
-
978
- const withoutEmptyBranches = (op: string | SelectionSetNode) => {
979
- let operation: Operation;
980
- if (typeof op === 'string') {
981
- operation = parseOperation(schema, op);
982
- } else {
983
- // Note that testing the removal of empty branches requires to take inputs that are not valid operations in the first place,
984
- // so we can't build those from `parseOperation` (this call the graphQL-js `parse` under the hood, and there is no way to
985
- // disable validation for that method). So instead, we manually build the AST (using some helper methods defined above) and
986
- // build the operation from there, disabling validation.
987
- const opDef: OperationDefinitionNode = {
988
- kind: Kind.OPERATION_DEFINITION,
989
- operation: OperationTypeNode.QUERY,
990
- selectionSet: op,
991
- }
992
- const document: DocumentNode = {
993
- kind: Kind.DOCUMENT,
994
- definitions: [opDef],
995
- }
996
- operation = operationFromDocument(schema, document, { validate: false });
997
- }
998
- return operation.selectionSet.withoutEmptyBranches()?.toString()
999
- };
1000
-
1001
-
1002
- it.each([
1003
- '{ t { a } }',
1004
- '{ t { a b } }',
1005
- '{ t { a c { x y } } }',
1006
- ])('is identity if there is no empty branch', (op) => {
1007
- expect(withoutEmptyBranches(op)).toBe(op);
1008
- });
1009
-
1010
- it('removes simple empty branches', () => {
1011
- expect(withoutEmptyBranches(
1012
- astSSet(
1013
- astField('t', astSSet(
1014
- astField('a'),
1015
- astField('c', astSSet()),
1016
- ))
1017
- )
1018
- )).toBe('{ t { a } }');
1019
-
1020
- expect(withoutEmptyBranches(
1021
- astSSet(
1022
- astField('t', astSSet(
1023
- astField('c', astSSet()),
1024
- astField('a'),
1025
- ))
1026
- )
1027
- )).toBe('{ t { a } }');
1028
-
1029
- expect(withoutEmptyBranches(
1030
- astSSet(
1031
- astField('t', astSSet())
1032
- )
1033
- )).toBeUndefined();
1034
- });
1035
-
1036
- it('removes cascading empty branches', () => {
1037
- expect(withoutEmptyBranches(
1038
- astSSet(
1039
- astField('t', astSSet(
1040
- astField('c', astSSet()),
1041
- ))
1042
- )
1043
- )).toBeUndefined();
1044
-
1045
- expect(withoutEmptyBranches(
1046
- astSSet(
1047
- astField('u'),
1048
- astField('t', astSSet(
1049
- astField('c', astSSet()),
1050
- ))
1051
- )
1052
- )).toBe('{ u }');
1053
-
1054
- expect(withoutEmptyBranches(
1055
- astSSet(
1056
- astField('t', astSSet(
1057
- astField('c', astSSet()),
1058
- )),
1059
- astField('u'),
1060
- )
1061
- )).toBe('{ u }');
1062
- });
1063
- });
1064
-
1065
- describe('basic operations', () => {
1066
- const schema = parseSchema(`
1067
- type Query {
1068
- t: T
1069
- i: I
1070
- }
1071
-
1072
- type T {
1073
- v1: Int
1074
- v2: String
1075
- v3: I
1076
- }
1077
-
1078
- interface I {
1079
- x: Int
1080
- y: Int
1081
- }
1082
-
1083
- type A implements I {
1084
- x: Int
1085
- y: Int
1086
- a1: String
1087
- a2: String
1088
- }
1089
-
1090
- type B implements I {
1091
- x: Int
1092
- y: Int
1093
- b1: Int
1094
- b2: T
1095
- }
1096
- `);
1097
-
1098
- const operation = parseOperation(schema, `
1099
- {
1100
- t {
1101
- v1
1102
- v3 {
1103
- x
1104
- }
1105
- }
1106
- i {
1107
- ... on A {
1108
- a1
1109
- a2
1110
- }
1111
- ... on B {
1112
- y
1113
- b2 {
1114
- v2
1115
- }
1116
- }
1117
- }
1118
- }
1119
- `);
1120
-
1121
- test('forEachElement', () => {
1122
- // We collect a pair of (parent type, field-or-fragment).
1123
- const actual: [string, string][] = [];
1124
- operation.selectionSet.forEachElement((elt) => actual.push([elt.parentType.name, elt.toString()]));
1125
- expect(actual).toStrictEqual([
1126
- ['Query', 't'],
1127
- ['T', 'v1'],
1128
- ['T', 'v3'],
1129
- ['I', 'x'],
1130
- ['Query', 'i'],
1131
- ['I', '... on A'],
1132
- ['A', 'a1'],
1133
- ['A', 'a2'],
1134
- ['I', '... on B'],
1135
- ['B', 'y'],
1136
- ['B', 'b2'],
1137
- ['T', 'v2'],
1138
- ]);
1139
- })
1140
- });
1141
-
1142
- describe('MutableSelectionSet', () => {
1143
- test('memoizer', () => {
1144
- const schema = parseSchema(`
1145
- type Query {
1146
- t: T
1147
- }
1148
-
1149
- type T {
1150
- v1: Int
1151
- v2: String
1152
- v3: Int
1153
- v4: Int
1154
- }
1155
- `);
1156
-
1157
- type Value = {
1158
- count: number
1159
- };
1160
-
1161
- let calls = 0;
1162
- const sets: string[] = [];
1163
-
1164
- const queryType = schema.schemaDefinition.rootType('query')!;
1165
- const ss = MutableSelectionSet.emptyWithMemoized<Value>(
1166
- queryType,
1167
- (s) => {
1168
- sets.push(s.toString());
1169
- return { count: ++calls };
1170
- }
1171
- );
1172
-
1173
- expect(ss.memoized().count).toBe(1);
1174
- // Calling a 2nd time with no change to make sure we're not re-generating the value.
1175
- expect(ss.memoized().count).toBe(1);
1176
-
1177
- ss.updates().add(parseOperation(schema, `{ t { v1 } }`).selectionSet);
1178
-
1179
- expect(ss.memoized().count).toBe(2);
1180
- expect(sets).toStrictEqual(['{}', '{ t { v1 } }']);
1181
-
1182
- ss.updates().add(parseOperation(schema, `{ t { v3 } }`).selectionSet);
1183
-
1184
- expect(ss.memoized().count).toBe(3);
1185
- expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }']);
1186
-
1187
- // Still making sure we don't re-compute without updates.
1188
- expect(ss.memoized().count).toBe(3);
1189
-
1190
- const cloned = ss.clone();
1191
- expect(cloned.memoized().count).toBe(3);
1192
-
1193
- cloned.updates().add(parseOperation(schema, `{ t { v2 } }`).selectionSet);
1194
-
1195
- // The value of `ss` should not have be recomputed, so it should still be 3.
1196
- expect(ss.memoized().count).toBe(3);
1197
- // But that of the clone should have changed.
1198
- expect(cloned.memoized().count).toBe(4);
1199
- expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }']);
1200
-
1201
- // And here we make sure that if we update the fist selection, we don't have v3 in the set received
1202
- ss.updates().add(parseOperation(schema, `{ t { v4 } }`).selectionSet);
1203
- // Here, only `ss` memoized value has been recomputed. But since both increment the same `calls` variable,
1204
- // the total count should be 5 (even if the previous count for `ss` was only 3).
1205
- expect(ss.memoized().count).toBe(5);
1206
- expect(cloned.memoized().count).toBe(4);
1207
- expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }', '{ t { v1 v3 v4 } }']);
1208
- });
1209
- });
1210
-
1211
- describe('unsatisfiable branches removal', () => {
1212
- const schema = parseSchema(`
1213
- type Query {
1214
- i: I
1215
- j: J
1216
- }
1217
-
1218
- interface I {
1219
- a: Int
1220
- b: Int
1221
- }
1222
-
1223
- interface J {
1224
- b: Int
1225
- }
1226
-
1227
- type T1 implements I & J {
1228
- a: Int
1229
- b: Int
1230
- c: Int
1231
- }
1232
-
1233
- type T2 implements I {
1234
- a: Int
1235
- b: Int
1236
- d: Int
1237
- }
1238
-
1239
- type T3 implements J {
1240
- a: Int
1241
- b: Int
1242
- d: Int
1243
- }
1244
- `);
1245
-
1246
- const withoutUnsatisfiableBranches = (op: string) => {
1247
- return parseOperation(schema, op).trimUnsatisfiableBranches().toString(false, false)
1248
- };
1249
-
1250
-
1251
- it.each([
1252
- '{ i { a } }',
1253
- '{ i { ... on T1 { a b c } } }',
1254
- ])('is identity if there is no unsatisfiable branches', (op) => {
1255
- expect(withoutUnsatisfiableBranches(op)).toBe(op);
1256
- });
1257
-
1258
- it.each([
1259
- { input: '{ i { ... on I { a } } }', output: '{ i { a } }' },
1260
- { input: '{ i { ... on T1 { ... on I { a b } } } }', output: '{ i { ... on T1 { a b } } }' },
1261
- { input: '{ i { ... on I { a ... on T2 { d } } } }', output: '{ i { a ... on T2 { d } } }' },
1262
- { input: '{ i { ... on T2 { ... on I { a ... on J { b } } } } }', output: '{ i { ... on T2 { a } } }' },
1263
- ])('removes unsatisfiable branches', ({input, output}) => {
1264
- expect(withoutUnsatisfiableBranches(input)).toBe(output);
1265
- });
1266
- });