@apollo/federation-internals 2.4.0 → 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.
@@ -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,69 +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
208
+ testFragmentsRoundtrip({
209
+ schema,
210
+ query: `
211
+ fragment OnT1 on T1 {
212
+ t2 {
213
+ x
214
+ }
183
215
  }
184
- }
185
216
 
186
- query {
187
- t1a {
188
- ...OnT1
189
- t2 {
190
- y
217
+ query {
218
+ t1a {
219
+ ...OnT1
220
+ t2 {
221
+ y
222
+ }
223
+ }
224
+ t2a {
225
+ ...OnT1
191
226
  }
192
227
  }
193
- t2a {
194
- ...OnT1
228
+ `,
229
+ expanded: `
230
+ {
231
+ t1a {
232
+ t2 {
233
+ x
234
+ y
235
+ }
236
+ }
237
+ t2a {
238
+ t2 {
239
+ x
240
+ }
241
+ }
195
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
196
262
  }
197
263
  `);
198
264
 
199
- const withoutFragments = parseOperation(schema, operation.toString(true, true));
200
- expect(withoutFragments.toString()).toMatchString(`
201
- {
202
- t1a {
203
- ... on T1 {
204
- 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
283
+ x
284
+ y
285
+ }
286
+
287
+ fragment FT on T {
288
+ a {
289
+ __typename
290
+ ...FA
291
+ }
292
+ }
293
+
294
+ query {
295
+ t {
296
+ ...FT
297
+ }
298
+ }
299
+ `,
300
+ expanded: `
301
+ {
302
+ t {
303
+ a {
304
+ __typename
205
305
  x
306
+ y
206
307
  }
207
308
  }
208
- t2 {
209
- y
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
335
+ }
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
210
349
  }
211
350
  }
212
- t2a {
213
- ... on T1 {
214
- t2 {
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
215
367
  x
216
368
  }
369
+ c
370
+ d {
371
+ m
372
+ n
373
+ }
374
+ a
217
375
  }
218
376
  }
377
+ `,
378
+ });
379
+ });
380
+
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
+ }
388
+
389
+ interface I {
390
+ a: String
391
+ }
392
+
393
+ type T {
394
+ a: String
395
+ b: B
396
+ c: Int
397
+ d: D
398
+ }
399
+
400
+ type B {
401
+ x: String
402
+ y: String
403
+ }
404
+
405
+ type D {
406
+ m: String
407
+ n: String
219
408
  }
220
409
  `);
221
410
 
222
- const optimized = withoutFragments.optimize(operation.selectionSet.fragments!);
223
- expect(optimized.toString()).toMatchString(`
224
- fragment OnT1 on T1 {
225
- t2 {
226
- x
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
+ }
424
+
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
+ }
227
453
  }
454
+ `,
455
+ });
456
+ });
457
+
458
+ test('intersecting fragments', () => {
459
+ const schema = parseSchema(`
460
+ type Query {
461
+ t: T
228
462
  }
229
463
 
230
- {
231
- t1a {
232
- ...OnT1
233
- t2 {
234
- y
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
+ }
475
+
476
+ type D {
477
+ m: String
478
+ n: String
479
+ }
480
+ `);
481
+
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
235
498
  }
236
499
  }
237
- t2a {
238
- ...OnT1
500
+
501
+ fragment Frag2 on T {
502
+ a
503
+ b {
504
+ __typename
505
+ x
506
+ }
507
+ d {
508
+ m
509
+ n
510
+ }
511
+ }
512
+
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
+ }
239
534
  }
535
+ `,
536
+ });
537
+ });
538
+
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
+ }
548
+
549
+ type T implements I {
550
+ x: String
551
+ a: String
240
552
  }
241
553
  `);
554
+
555
+ testFragmentsRoundtrip({
556
+ schema,
557
+ query: `
558
+ fragment FragI on I {
559
+ x
560
+ ... on T {
561
+ a
562
+ }
563
+ }
564
+
565
+ {
566
+ t {
567
+ ...FragI
568
+ }
569
+ }
570
+ `,
571
+ expanded: `
572
+ {
573
+ t {
574
+ x
575
+ a
576
+ }
577
+ }
578
+ `,
579
+ });
580
+ });
581
+
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
+ });
242
739
  });
243
740
  });
244
741
 
@@ -477,7 +974,6 @@ describe('basic operations', () => {
477
974
  })
478
975
  });
479
976
 
480
-
481
977
  describe('MutableSelectionSet', () => {
482
978
  test('memoizer', () => {
483
979
  const schema = parseSchema(`
@@ -546,3 +1042,60 @@ describe('MutableSelectionSet', () => {
546
1042
  expect(sets).toStrictEqual(['{}', '{ t { v1 } }', '{ t { v1 v3 } }', '{ t { v1 v3 v2 } }', '{ t { v1 v3 v4 } }']);
547
1043
  });
548
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
+ });
@@ -212,7 +212,7 @@ test('reject @interfaceObject usage if not all subgraphs are fed2', () => {
212
212
 
213
213
  const s1 = `
214
214
  extend schema
215
- @link(url: "https://specs.apollo.dev/federation/v2.4", import: [ "@key", "@interfaceObject"])
215
+ @link(url: "https://specs.apollo.dev/federation/v2.3", import: [ "@key", "@interfaceObject"])
216
216
 
217
217
  type Query {
218
218
  a: A