@apollo/gateway 2.2.2 → 2.3.0-alpha.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.
@@ -3516,4 +3516,1271 @@ describe('executeQueryPlan', () => {
3516
3516
  `);
3517
3517
  });
3518
3518
  });
3519
+
3520
+ describe('@interfaceObject', () => {
3521
+ const defineSchema = ({
3522
+ s1,
3523
+ }: {
3524
+ s1?: {
3525
+ iResolversExtra?: any,
3526
+ hasIResolveReference?: boolean,
3527
+ iResolveReferenceExtra?: (id: string) => { [k: string]: any },
3528
+ aResolversExtra?: any,
3529
+ bResolversExtra?: any,
3530
+ }
3531
+ }) => {
3532
+
3533
+ // The example uses 2 entities:
3534
+ // - one of type A with id='idA' (x=1, y=2, z=3)
3535
+ // - one of type B with id='idB' (x=10, y=20, w=30)
3536
+
3537
+ const s1IBaseResolvers = (s1?.hasIResolveReference ?? true)
3538
+ ? {
3539
+ __resolveReference(ref: { id: string }) {
3540
+ const extraFct = s1?.iResolveReferenceExtra;
3541
+ const extraData = extraFct ? extraFct(ref.id) : {};
3542
+ return ref.id === 'idA'
3543
+ ? { id: ref.id, x: 1, z: 3, ...extraData }
3544
+ : { id: ref.id, x: 10, w: 30, ...extraData };
3545
+ }
3546
+ }
3547
+ : {};
3548
+
3549
+ const subgraph1 = {
3550
+ name: 'S1',
3551
+ typeDefs: gql`
3552
+ extend schema
3553
+ @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key"])
3554
+
3555
+ type Query {
3556
+ iFromS1: I
3557
+ }
3558
+
3559
+ interface I @key(fields: "id") {
3560
+ id: ID!
3561
+ x: Int
3562
+ }
3563
+
3564
+ type A implements I @key(fields: "id") {
3565
+ id: ID!
3566
+ x: Int
3567
+ z: Int
3568
+ }
3569
+
3570
+ type B implements I @key(fields: "id") {
3571
+ id: ID!
3572
+ x: Int
3573
+ w: Int
3574
+ }
3575
+ `,
3576
+ resolvers: {
3577
+ Query: {
3578
+ iFromS1() {
3579
+ return { __typename: 'A', id: 'idA' };
3580
+ }
3581
+ },
3582
+ I: {
3583
+ ...s1IBaseResolvers,
3584
+ ...(s1?.iResolversExtra ?? {}),
3585
+ },
3586
+ A: {
3587
+ ...(s1?.aResolversExtra ?? {}),
3588
+ },
3589
+ B: {
3590
+ ...(s1?.bResolversExtra ?? {}),
3591
+ },
3592
+ }
3593
+ }
3594
+
3595
+ const subgraph2 = {
3596
+ name: 'S2',
3597
+ typeDefs: gql`
3598
+ extend schema
3599
+ @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@interfaceObject"])
3600
+
3601
+ type Query {
3602
+ iFromS2: I
3603
+ }
3604
+
3605
+ type I @interfaceObject @key(fields: "id") {
3606
+ id: ID!
3607
+ y: Int
3608
+ }
3609
+ `,
3610
+ resolvers: {
3611
+ Query: {
3612
+ iFromS2() {
3613
+ return {
3614
+ __typename: 'I',
3615
+ id: 'idB',
3616
+ y: 20,
3617
+ };
3618
+ }
3619
+ },
3620
+ I: {
3621
+ __resolveReference(ref: { id: string }) {
3622
+ return {
3623
+ id: ref.id,
3624
+ y: ref.id === 'idA' ? 2 : 20,
3625
+ }
3626
+ },
3627
+ },
3628
+ }
3629
+ }
3630
+
3631
+ const { serviceMap, schema, queryPlanner } = getFederatedTestingSchema([ subgraph1, subgraph2 ]);
3632
+ return async (op: string): Promise<{ plan: QueryPlan, response: GatewayExecutionResult }> => {
3633
+ const operation = parseOp(op, schema);
3634
+ const plan = buildPlan(operation, queryPlanner);
3635
+ const response = await executePlan(plan, operation, undefined, schema, serviceMap);
3636
+ return { plan, response };
3637
+ };
3638
+ }
3639
+
3640
+
3641
+ test('handles __typename rewriting when using @key to @interfaceObject', async () => {
3642
+ // We don't need extra resolving from S1 in this case.
3643
+ const tester = defineSchema({});
3644
+
3645
+ let { plan, response } = await tester(`
3646
+ query {
3647
+ iFromS1 {
3648
+ __typename
3649
+ y
3650
+ }
3651
+ }
3652
+ `);
3653
+
3654
+ expect(plan).toMatchInlineSnapshot(`
3655
+ QueryPlan {
3656
+ Sequence {
3657
+ Fetch(service: "S1") {
3658
+ {
3659
+ iFromS1 {
3660
+ __typename
3661
+ id
3662
+ }
3663
+ }
3664
+ },
3665
+ Flatten(path: "iFromS1") {
3666
+ Fetch(service: "S2") {
3667
+ {
3668
+ ... on I {
3669
+ __typename
3670
+ id
3671
+ }
3672
+ } =>
3673
+ {
3674
+ ... on I {
3675
+ y
3676
+ }
3677
+ }
3678
+ },
3679
+ },
3680
+ },
3681
+ }
3682
+ `);
3683
+
3684
+ expect(response.errors).toBeUndefined();
3685
+ expect(response.data).toMatchInlineSnapshot(`
3686
+ Object {
3687
+ "iFromS1": Object {
3688
+ "__typename": "A",
3689
+ "y": 2,
3690
+ },
3691
+ }
3692
+ `);
3693
+
3694
+ // Same, but with an explicit cast to A
3695
+ ({ plan, response } = await tester(`
3696
+ query {
3697
+ iFromS1 {
3698
+ ... on A {
3699
+ y
3700
+ }
3701
+ }
3702
+ }
3703
+ `));
3704
+
3705
+ expect(plan).toMatchInlineSnapshot(`
3706
+ QueryPlan {
3707
+ Sequence {
3708
+ Fetch(service: "S1") {
3709
+ {
3710
+ iFromS1 {
3711
+ __typename
3712
+ ... on A {
3713
+ __typename
3714
+ id
3715
+ }
3716
+ }
3717
+ }
3718
+ },
3719
+ Flatten(path: "iFromS1") {
3720
+ Fetch(service: "S2") {
3721
+ {
3722
+ ... on A {
3723
+ __typename
3724
+ id
3725
+ }
3726
+ } =>
3727
+ {
3728
+ ... on I {
3729
+ y
3730
+ }
3731
+ }
3732
+ },
3733
+ },
3734
+ },
3735
+ }
3736
+ `);
3737
+
3738
+ expect(response.errors).toBeUndefined();
3739
+ expect(response.data).toMatchInlineSnapshot(`
3740
+ Object {
3741
+ "iFromS1": Object {
3742
+ "y": 2,
3743
+ },
3744
+ }
3745
+ `);
3746
+
3747
+ // And lastly, make sure that we explicitly cast to B, we get nothing
3748
+ ({ plan, response } = await tester(`
3749
+ query {
3750
+ iFromS1 {
3751
+ ... on B {
3752
+ y
3753
+ }
3754
+ }
3755
+ }
3756
+ `));
3757
+
3758
+ expect(plan).toMatchInlineSnapshot(`
3759
+ QueryPlan {
3760
+ Sequence {
3761
+ Fetch(service: "S1") {
3762
+ {
3763
+ iFromS1 {
3764
+ __typename
3765
+ ... on B {
3766
+ __typename
3767
+ id
3768
+ }
3769
+ }
3770
+ }
3771
+ },
3772
+ Flatten(path: "iFromS1") {
3773
+ Fetch(service: "S2") {
3774
+ {
3775
+ ... on B {
3776
+ __typename
3777
+ id
3778
+ }
3779
+ } =>
3780
+ {
3781
+ ... on I {
3782
+ y
3783
+ }
3784
+ }
3785
+ },
3786
+ },
3787
+ },
3788
+ }
3789
+ `);
3790
+
3791
+ expect(response.errors).toBeUndefined();
3792
+ expect(response.data).toMatchInlineSnapshot(`
3793
+ Object {
3794
+ "iFromS1": Object {},
3795
+ }
3796
+ `);
3797
+ });
3798
+
3799
+ test.each([{
3800
+ name: 'with manual __typename',
3801
+ s1: {
3802
+ iResolveReferenceExtra: (id: string) => ({ __typename: id === 'idA' ? 'A' : 'B' }),
3803
+ },
3804
+ }, {
3805
+ name: 'with __resolveType',
3806
+ s1: {
3807
+ iResolversExtra: {
3808
+ __resolveType(ref: { id: string }) {
3809
+ return ref.id === 'idA' ? 'A' : 'B';
3810
+ }
3811
+ },
3812
+ },
3813
+ }, {
3814
+ name: 'with isTypeOf',
3815
+ s1: {
3816
+ aResolversExtra: {
3817
+ __isTypeOf(ref: { id: string }) {
3818
+ return ref.id === 'idA';
3819
+ }
3820
+ },
3821
+ bResolversExtra: {
3822
+ __isTypeOf(ref: { id: string }) {
3823
+ // Same remark as above.
3824
+ return ref.id === 'idB';
3825
+ }
3826
+ },
3827
+ },
3828
+ }, {
3829
+ name: 'with only a __resolveType on the interface but per-runtime-types __resolveReference',
3830
+ s1: {
3831
+ hasIResolveReference: false,
3832
+ iResolversExtra: {
3833
+ __resolveType(ref: { id: string }) {
3834
+ return ref.id === 'idA' ? 'A' : 'B';
3835
+ }
3836
+ },
3837
+ aResolversExtra: {
3838
+ __resolveReference(ref: { id: string }) {
3839
+ return ref.id === 'idA'
3840
+ ? { id: ref.id, x: 1, z: 3 }
3841
+ : undefined;
3842
+ }
3843
+ },
3844
+ bResolversExtra: {
3845
+ __resolveReference(ref: { id: string }) {
3846
+ return ref.id === 'idB'
3847
+ ? { id: ref.id, x: 10, w: 30 }
3848
+ : undefined;
3849
+ }
3850
+ },
3851
+ },
3852
+ }, {
3853
+ name: 'errors when nothing provides the runtime type',
3854
+ expectedErrors: [
3855
+ 'Abstract type "I" `__resolveReference` method must resolve to an Object type at runtime. '
3856
+ + 'Either the object returned by "I.__resolveReference" must include a valid `__typename` field, '
3857
+ + 'or the "I" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.'
3858
+ ],
3859
+ }])('resolving an interface @key $name', async ({s1, expectedErrors}) => {
3860
+ const tester = defineSchema({ s1 });
3861
+
3862
+ const { plan, response } = await tester(`
3863
+ query {
3864
+ iFromS2 {
3865
+ __typename
3866
+ x
3867
+ y
3868
+ ... on A {
3869
+ z
3870
+ }
3871
+ ... on B {
3872
+ w
3873
+ }
3874
+ }
3875
+ }
3876
+ `);
3877
+
3878
+ expect(plan).toMatchInlineSnapshot(`
3879
+ QueryPlan {
3880
+ Sequence {
3881
+ Fetch(service: "S2") {
3882
+ {
3883
+ iFromS2 {
3884
+ __typename
3885
+ id
3886
+ y
3887
+ }
3888
+ }
3889
+ },
3890
+ Flatten(path: "iFromS2") {
3891
+ Fetch(service: "S1") {
3892
+ {
3893
+ ... on I {
3894
+ __typename
3895
+ id
3896
+ }
3897
+ } =>
3898
+ {
3899
+ ... on I {
3900
+ __typename
3901
+ x
3902
+ ... on A {
3903
+ z
3904
+ }
3905
+ ... on B {
3906
+ w
3907
+ }
3908
+ }
3909
+ }
3910
+ },
3911
+ },
3912
+ },
3913
+ }
3914
+ `);
3915
+
3916
+ if (expectedErrors) {
3917
+ expect(response.errors?.map((e) => e.message)).toEqual(expectedErrors);
3918
+ expect(response.data).toMatchInlineSnapshot(`
3919
+ Object {
3920
+ "iFromS2": null,
3921
+ }
3922
+ `);
3923
+ } else {
3924
+ expect(response.errors).toBeUndefined();
3925
+ expect(response.data).toMatchInlineSnapshot(`
3926
+ Object {
3927
+ "iFromS2": Object {
3928
+ "__typename": "B",
3929
+ "w": 30,
3930
+ "x": 10,
3931
+ "y": 20,
3932
+ },
3933
+ }
3934
+ `);
3935
+ }
3936
+ });
3937
+ });
3938
+
3939
+ describe('fields with conflicting types needing aliasing', () => {
3940
+ it('handles @requires of fields on union leading to conflict', async () => {
3941
+ const s1 = {
3942
+ name: 'S1',
3943
+ typeDefs: gql`
3944
+ type Query {
3945
+ us: [U]
3946
+ }
3947
+
3948
+ union U = A | B
3949
+
3950
+ type A @key(fields: "id") {
3951
+ id: ID!
3952
+ g: Int
3953
+ }
3954
+
3955
+ type B @key(fields: "id") {
3956
+ id: ID!
3957
+ g: String
3958
+ }
3959
+ `,
3960
+ resolvers: {
3961
+ Query: {
3962
+ us() {
3963
+ return [
3964
+ { __typename: 'A', id: 'keyA', g: 1 },
3965
+ { __typename: 'B', id: 'keyB', g: 'foo' },
3966
+ ];
3967
+ }
3968
+ },
3969
+ }
3970
+ }
3971
+
3972
+ const s2 = {
3973
+ name: 'S2',
3974
+ typeDefs: gql`
3975
+ type A @key(fields: "id") {
3976
+ id: ID!
3977
+ f: String @requires(fields: "g")
3978
+ g: Int @external
3979
+ }
3980
+
3981
+ type B @key(fields: "id") {
3982
+ id: ID!
3983
+ f: String @requires(fields: "g")
3984
+ g: String @external
3985
+ }
3986
+ `,
3987
+ resolvers: {
3988
+ A: {
3989
+ __resolveReference(ref: { id: string, g: any }) {
3990
+ return { __typename: 'A', id: ref.id, f: `g is type ${typeof ref.g}` };
3991
+ },
3992
+ },
3993
+ B: {
3994
+ __resolveReference(ref: { id: string, g: any }) {
3995
+ return { __typename: 'B', id: ref.id, f: `g is type ${typeof ref.g}` };
3996
+ },
3997
+ },
3998
+ }
3999
+ }
4000
+
4001
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
4002
+
4003
+ const operation = parseOp(`
4004
+ query {
4005
+ us {
4006
+ ... on A {
4007
+ f
4008
+ }
4009
+ ... on B {
4010
+ f
4011
+ }
4012
+ }
4013
+ }
4014
+ `, schema);
4015
+ const queryPlan = buildPlan(operation, queryPlanner);
4016
+ // In the initial fetch, it's important that one of the `g` is aliased, since it's queried twice at the same level
4017
+ // but with different types.
4018
+ expect(queryPlan).toMatchInlineSnapshot(`
4019
+ QueryPlan {
4020
+ Sequence {
4021
+ Fetch(service: "S1") {
4022
+ {
4023
+ us {
4024
+ __typename
4025
+ ... on A {
4026
+ __typename
4027
+ id
4028
+ g
4029
+ }
4030
+ ... on B {
4031
+ __typename
4032
+ id
4033
+ g__alias_0: g
4034
+ }
4035
+ }
4036
+ }
4037
+ },
4038
+ Flatten(path: "us.@") {
4039
+ Fetch(service: "S2") {
4040
+ {
4041
+ ... on A {
4042
+ __typename
4043
+ id
4044
+ g
4045
+ }
4046
+ ... on B {
4047
+ __typename
4048
+ id
4049
+ g
4050
+ }
4051
+ } =>
4052
+ {
4053
+ ... on A {
4054
+ f
4055
+ }
4056
+ ... on B {
4057
+ f
4058
+ }
4059
+ }
4060
+ },
4061
+ },
4062
+ },
4063
+ }
4064
+ `);
4065
+
4066
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
4067
+ expect(response.errors).toBeUndefined();
4068
+ expect(response.data).toMatchInlineSnapshot(`
4069
+ Object {
4070
+ "us": Array [
4071
+ Object {
4072
+ "f": "g is type number",
4073
+ },
4074
+ Object {
4075
+ "f": "g is type string",
4076
+ },
4077
+ ],
4078
+ }
4079
+ `);
4080
+ });
4081
+
4082
+ it('handles @requires of fields on interface leading to conflict', async () => {
4083
+ const s1 = {
4084
+ name: 'S1',
4085
+ typeDefs: gql`
4086
+ type Query {
4087
+ us: [U]
4088
+ }
4089
+
4090
+ interface U {
4091
+ id: ID!
4092
+ f: String
4093
+ }
4094
+
4095
+ type A implements U @key(fields: "id") {
4096
+ id: ID!
4097
+ f: String @external
4098
+ g: Int
4099
+ }
4100
+
4101
+ type B implements U @key(fields: "id") {
4102
+ id: ID!
4103
+ f: String @external
4104
+ g: String
4105
+ }
4106
+ `,
4107
+ resolvers: {
4108
+ Query: {
4109
+ us() {
4110
+ return [
4111
+ { __typename: 'A', id: 'keyA', g: 1 },
4112
+ { __typename: 'B', id: 'keyB', g: 'foo' },
4113
+ ];
4114
+ }
4115
+ },
4116
+ }
4117
+ }
4118
+
4119
+ const s2 = {
4120
+ name: 'S2',
4121
+ typeDefs: gql`
4122
+ type A @key(fields: "id") {
4123
+ id: ID!
4124
+ f: String @requires(fields: "g")
4125
+ g: Int @external
4126
+ }
4127
+
4128
+ type B @key(fields: "id") {
4129
+ id: ID!
4130
+ f: String @requires(fields: "g")
4131
+ g: String @external
4132
+ }
4133
+ `,
4134
+ resolvers: {
4135
+ A: {
4136
+ __resolveReference(ref: { id: string, g: any }) {
4137
+ return { __typename: 'A', id: ref.id, f: `g is type ${typeof ref.g}` };
4138
+ },
4139
+ },
4140
+ B: {
4141
+ __resolveReference(ref: { id: string, g: any }) {
4142
+ return { __typename: 'B', id: ref.id, f: `g is type ${typeof ref.g}` };
4143
+ },
4144
+ },
4145
+ }
4146
+ }
4147
+
4148
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
4149
+
4150
+ const operation = parseOp(`
4151
+ query {
4152
+ us {
4153
+ f
4154
+ }
4155
+ }
4156
+ `, schema);
4157
+ const queryPlan = buildPlan(operation, queryPlanner);
4158
+ // In the initial fetch, it's important that one of the `g` is aliased, since it's queried twice at the same level
4159
+ // but with different types.
4160
+ expect(queryPlan).toMatchInlineSnapshot(`
4161
+ QueryPlan {
4162
+ Sequence {
4163
+ Fetch(service: "S1") {
4164
+ {
4165
+ us {
4166
+ __typename
4167
+ ... on A {
4168
+ __typename
4169
+ id
4170
+ g
4171
+ }
4172
+ ... on B {
4173
+ __typename
4174
+ id
4175
+ g__alias_0: g
4176
+ }
4177
+ }
4178
+ }
4179
+ },
4180
+ Flatten(path: "us.@") {
4181
+ Fetch(service: "S2") {
4182
+ {
4183
+ ... on A {
4184
+ __typename
4185
+ id
4186
+ g
4187
+ }
4188
+ ... on B {
4189
+ __typename
4190
+ id
4191
+ g
4192
+ }
4193
+ } =>
4194
+ {
4195
+ ... on A {
4196
+ f
4197
+ }
4198
+ ... on B {
4199
+ f
4200
+ }
4201
+ }
4202
+ },
4203
+ },
4204
+ },
4205
+ }
4206
+ `);
4207
+
4208
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
4209
+ expect(response.errors).toBeUndefined();
4210
+ expect(response.data).toMatchInlineSnapshot(`
4211
+ Object {
4212
+ "us": Array [
4213
+ Object {
4214
+ "f": "g is type number",
4215
+ },
4216
+ Object {
4217
+ "f": "g is type string",
4218
+ },
4219
+ ],
4220
+ }
4221
+ `);
4222
+ });
4223
+
4224
+ it('handles @key on interface leading to conflict', async () => {
4225
+ const s1 = {
4226
+ name: 'S1',
4227
+ typeDefs: gql`
4228
+ type Query {
4229
+ us: [U]
4230
+ }
4231
+
4232
+ interface U {
4233
+ f: String
4234
+ }
4235
+
4236
+ type A implements U @key(fields: "g") {
4237
+ f: String @external
4238
+ g: String
4239
+ }
4240
+
4241
+ type B implements U @key(fields: "g") {
4242
+ f: String @external
4243
+ g: Int
4244
+ }
4245
+ `,
4246
+ resolvers: {
4247
+ Query: {
4248
+ us() {
4249
+ return [
4250
+ { __typename: 'A', g: 'foo' },
4251
+ { __typename: 'B', g: 1 },
4252
+ ];
4253
+ }
4254
+ },
4255
+ }
4256
+ }
4257
+
4258
+ const s2 = {
4259
+ name: 'S2',
4260
+ typeDefs: gql`
4261
+ type A @key(fields: "g") {
4262
+ g: String
4263
+ f: String
4264
+ }
4265
+
4266
+ type B @key(fields: "g") {
4267
+ g: Int
4268
+ f: String
4269
+ }
4270
+ `,
4271
+ resolvers: {
4272
+ A: {
4273
+ __resolveReference(ref: { g: string }) {
4274
+ return { __typename: 'A', g: ref.g, f: ref.g == 'foo' ? 'fA' : '<error>' };
4275
+ },
4276
+ },
4277
+ B: {
4278
+ __resolveReference(ref: { g: number }) {
4279
+ return { __typename: 'B', g: ref.g, f: ref.g === 1 ? 'fB' : '<error>' };
4280
+ },
4281
+ },
4282
+ }
4283
+ }
4284
+
4285
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
4286
+
4287
+ const operation = parseOp(`
4288
+ query {
4289
+ us {
4290
+ f
4291
+ }
4292
+ }
4293
+ `, schema);
4294
+ const queryPlan = buildPlan(operation, queryPlanner);
4295
+ // In the initial fetch, it's important that one of the `g` is aliased, since it's queried twice at the same level
4296
+ // but with different types.
4297
+ expect(queryPlan).toMatchInlineSnapshot(`
4298
+ QueryPlan {
4299
+ Sequence {
4300
+ Fetch(service: "S1") {
4301
+ {
4302
+ us {
4303
+ __typename
4304
+ ... on A {
4305
+ __typename
4306
+ g
4307
+ }
4308
+ ... on B {
4309
+ __typename
4310
+ g__alias_0: g
4311
+ }
4312
+ }
4313
+ }
4314
+ },
4315
+ Flatten(path: "us.@") {
4316
+ Fetch(service: "S2") {
4317
+ {
4318
+ ... on A {
4319
+ __typename
4320
+ g
4321
+ }
4322
+ ... on B {
4323
+ __typename
4324
+ g
4325
+ }
4326
+ } =>
4327
+ {
4328
+ ... on A {
4329
+ f
4330
+ }
4331
+ ... on B {
4332
+ f
4333
+ }
4334
+ }
4335
+ },
4336
+ },
4337
+ },
4338
+ }
4339
+ `);
4340
+
4341
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
4342
+ expect(response.errors).toBeUndefined();
4343
+ expect(response.data).toMatchInlineSnapshot(`
4344
+ Object {
4345
+ "us": Array [
4346
+ Object {
4347
+ "f": "fA",
4348
+ },
4349
+ Object {
4350
+ "f": "fB",
4351
+ },
4352
+ ],
4353
+ }
4354
+ `);
4355
+ });
4356
+
4357
+ it('handles field conflicting when type-exploding', async () => {
4358
+ const s1 = {
4359
+ name: 'S1',
4360
+ typeDefs: gql`
4361
+ type Query {
4362
+ us: [U] @provides(fields: "... on A { f }")
4363
+ }
4364
+
4365
+ interface U {
4366
+ f: String
4367
+ }
4368
+
4369
+ type A implements U @key(fields: "id") {
4370
+ id: ID!
4371
+ f: String @external
4372
+ }
4373
+
4374
+ type B implements U {
4375
+ f: String!
4376
+ }
4377
+ `,
4378
+ resolvers: {
4379
+ Query: {
4380
+ us() {
4381
+ return [
4382
+ { __typename: 'A', id: 'keyA', f: 'fA'},
4383
+ { __typename: 'B', f: 'fB' },
4384
+ ];
4385
+ }
4386
+ },
4387
+ }
4388
+ }
4389
+
4390
+ const s2 = {
4391
+ name: 'S2',
4392
+ typeDefs: gql`
4393
+ type A @key(fields: "id") {
4394
+ id: ID!
4395
+ f: String
4396
+ }
4397
+ `,
4398
+ resolvers: {
4399
+ A: {
4400
+ __resolveReference(ref: { id: string }) {
4401
+ return { __typename: 'A', id: ref.id, f: 'fA' };
4402
+ },
4403
+ },
4404
+ }
4405
+ }
4406
+
4407
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
4408
+
4409
+ const operation = parseOp(`
4410
+ query {
4411
+ us {
4412
+ f
4413
+ }
4414
+ }
4415
+ `, schema);
4416
+ const queryPlan = buildPlan(operation, queryPlanner);
4417
+ // Here, the presence of the @provides "forces" the query planner to check type-explosion, and as type-exploding
4418
+ // is the most efficient solution, it is chosen. But as this result in `f` being queried twice at the same level
4419
+ // without the same type (it is non-nullable in B, not in A, which is invalid GraphQL in that case), we must make
4420
+ // sure the 2nd occurrence is aliased.
4421
+ expect(queryPlan).toMatchInlineSnapshot(`
4422
+ QueryPlan {
4423
+ Fetch(service: "S1") {
4424
+ {
4425
+ us {
4426
+ __typename
4427
+ ... on A {
4428
+ f
4429
+ }
4430
+ ... on B {
4431
+ f__alias_0: f
4432
+ }
4433
+ }
4434
+ }
4435
+ },
4436
+ }
4437
+ `);
4438
+
4439
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
4440
+ expect(response.errors).toBeUndefined();
4441
+ expect(response.data).toMatchInlineSnapshot(`
4442
+ Object {
4443
+ "us": Array [
4444
+ Object {
4445
+ "f": "fA",
4446
+ },
4447
+ Object {
4448
+ "f": "fB",
4449
+ },
4450
+ ],
4451
+ }
4452
+ `);
4453
+ });
4454
+
4455
+ it('handles field conflict in non-root fetches', async () => {
4456
+ // This test is similar in spirit to the previous ones, but is simply ensures that the aliasing/rewriting logic
4457
+ // works correctly when it doesn't happen in a root fetch (in particular, the rewriting logic takes a slightly
4458
+ // different code path in that case, so this is what we're testing here).
4459
+ const s1 = {
4460
+ name: 'S1',
4461
+ typeDefs: gql`
4462
+ type T @key(fields: "id") {
4463
+ id: ID!
4464
+ us: [U]
4465
+ }
4466
+
4467
+ interface U {
4468
+ f: String
4469
+ }
4470
+
4471
+ type A implements U @key(fields: "g") {
4472
+ f: String @external
4473
+ g: String
4474
+ }
4475
+
4476
+ type B implements U @key(fields: "g") {
4477
+ f: String @external
4478
+ g: Int
4479
+ }
4480
+ `,
4481
+ resolvers: {
4482
+ T: {
4483
+ us() {
4484
+ return [
4485
+ { __typename: 'A', g: 'foo' },
4486
+ { __typename: 'B', g: 1 },
4487
+ ];
4488
+ }
4489
+ },
4490
+ }
4491
+ }
4492
+
4493
+ const s2 = {
4494
+ name: 'S2',
4495
+ typeDefs: gql`
4496
+ type Query {
4497
+ t: T
4498
+ }
4499
+
4500
+ type T @key(fields: "id") {
4501
+ id: ID!
4502
+ }
4503
+
4504
+ type A @key(fields: "g") {
4505
+ g: String
4506
+ f: String
4507
+ }
4508
+
4509
+ type B @key(fields: "g") {
4510
+ g: Int
4511
+ f: String
4512
+ }
4513
+ `,
4514
+ resolvers: {
4515
+ Query: {
4516
+ t() {
4517
+ return ({ id: 0 });
4518
+ }
4519
+ },
4520
+ A: {
4521
+ __resolveReference(ref: { g: string }) {
4522
+ return { __typename: 'A', g: ref.g, f: ref.g == 'foo' ? 'fA' : '<error>' };
4523
+ },
4524
+ },
4525
+ B: {
4526
+ __resolveReference(ref: { g: number }) {
4527
+ return { __typename: 'B', g: ref.g, f: ref.g === 1 ? 'fB' : '<error>' };
4528
+ },
4529
+ },
4530
+ }
4531
+ }
4532
+
4533
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
4534
+
4535
+ const operation = parseOp(`
4536
+ query {
4537
+ t {
4538
+ us {
4539
+ f
4540
+ }
4541
+ }
4542
+ }
4543
+ `, schema);
4544
+ const queryPlan = buildPlan(operation, queryPlanner);
4545
+ // In the 2nd fetch, it's important that one of the `g` is aliased, since it's queried twice at the same level
4546
+ // but with different types.
4547
+ expect(queryPlan).toMatchInlineSnapshot(`
4548
+ QueryPlan {
4549
+ Sequence {
4550
+ Fetch(service: "S2") {
4551
+ {
4552
+ t {
4553
+ __typename
4554
+ id
4555
+ }
4556
+ }
4557
+ },
4558
+ Flatten(path: "t") {
4559
+ Fetch(service: "S1") {
4560
+ {
4561
+ ... on T {
4562
+ __typename
4563
+ id
4564
+ }
4565
+ } =>
4566
+ {
4567
+ ... on T {
4568
+ us {
4569
+ __typename
4570
+ ... on A {
4571
+ __typename
4572
+ g
4573
+ }
4574
+ ... on B {
4575
+ __typename
4576
+ g__alias_0: g
4577
+ }
4578
+ }
4579
+ }
4580
+ }
4581
+ },
4582
+ },
4583
+ Flatten(path: "t.us.@") {
4584
+ Fetch(service: "S2") {
4585
+ {
4586
+ ... on A {
4587
+ __typename
4588
+ g
4589
+ }
4590
+ ... on B {
4591
+ __typename
4592
+ g
4593
+ }
4594
+ } =>
4595
+ {
4596
+ ... on A {
4597
+ f
4598
+ }
4599
+ ... on B {
4600
+ f
4601
+ }
4602
+ }
4603
+ },
4604
+ },
4605
+ },
4606
+ }
4607
+ `);
4608
+
4609
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
4610
+ expect(response.errors).toBeUndefined();
4611
+ expect(response.data).toMatchInlineSnapshot(`
4612
+ Object {
4613
+ "t": Object {
4614
+ "us": Array [
4615
+ Object {
4616
+ "f": "fA",
4617
+ },
4618
+ Object {
4619
+ "f": "fB",
4620
+ },
4621
+ ],
4622
+ },
4623
+ }
4624
+ `);
4625
+ });
4626
+
4627
+ it('handles clashes with existing aliases during alias generation on conflict', async () => {
4628
+ const s1 = {
4629
+ name: 'S1',
4630
+ typeDefs: gql`
4631
+ type Query {
4632
+ us: [U]
4633
+ }
4634
+
4635
+ interface U {
4636
+ id: ID!
4637
+ x: String
4638
+ f: String
4639
+ y: String
4640
+ }
4641
+
4642
+ type A implements U @key(fields: "id") {
4643
+ id: ID!
4644
+ x: String
4645
+ f: String @external
4646
+ g: Int
4647
+ y: String
4648
+ }
4649
+
4650
+ type B implements U @key(fields: "id") {
4651
+ id: ID!
4652
+ x: String
4653
+ f: String @external
4654
+ g: String
4655
+ y: String
4656
+ }
4657
+ `,
4658
+ resolvers: {
4659
+ Query: {
4660
+ us() {
4661
+ return [
4662
+ { __typename: 'A', id: 'keyA', g: 1, x: 'xA', y: 'yA' },
4663
+ { __typename: 'B', id: 'keyB', g: 'foo', x: 'xB', y: 'yB' },
4664
+ ];
4665
+ }
4666
+ },
4667
+ }
4668
+ }
4669
+
4670
+ const s2 = {
4671
+ name: 'S2',
4672
+ typeDefs: gql`
4673
+ type A @key(fields: "id") {
4674
+ id: ID!
4675
+ f: String @requires(fields: "g")
4676
+ g: Int @external
4677
+ }
4678
+
4679
+ type B @key(fields: "id") {
4680
+ id: ID!
4681
+ f: String @requires(fields: "g")
4682
+ g: String @external
4683
+ }
4684
+ `,
4685
+ resolvers: {
4686
+ A: {
4687
+ __resolveReference(ref: { id: string, g: any }) {
4688
+ return { __typename: 'A', id: ref.id, f: `g is type ${typeof ref.g}` };
4689
+ },
4690
+ },
4691
+ B: {
4692
+ __resolveReference(ref: { id: string, g: any }) {
4693
+ return { __typename: 'B', id: ref.id, f: `g is type ${typeof ref.g}` };
4694
+ },
4695
+ },
4696
+ }
4697
+ }
4698
+
4699
+ const { serviceMap, schema, queryPlanner} = getFederatedTestingSchema([ s1, s2 ]);
4700
+
4701
+ // We known that `g` will need to be aliased in the 2nd occurrence on B, and by default it would be aliased
4702
+ // as `g__alias_0`. So we query something with that exact alias to check that we avoid the conflict. We
4703
+ // also use alias `g__alias_1` to further ensure multiple possible conflict are handled.
4704
+ const operation = parseOp(`
4705
+ query {
4706
+ us {
4707
+ g__alias_0: x
4708
+ f
4709
+ g__alias_1: y
4710
+ }
4711
+ }
4712
+ `, schema);
4713
+ global.console = require('console')
4714
+ const queryPlan = buildPlan(operation, queryPlanner);
4715
+ expect(queryPlan).toMatchInlineSnapshot(`
4716
+ QueryPlan {
4717
+ Sequence {
4718
+ Fetch(service: "S1") {
4719
+ {
4720
+ us {
4721
+ __typename
4722
+ g__alias_0: x
4723
+ ... on A {
4724
+ __typename
4725
+ id
4726
+ g
4727
+ }
4728
+ ... on B {
4729
+ __typename
4730
+ id
4731
+ g__alias_1: g
4732
+ }
4733
+ g__alias_1__alias_0: y
4734
+ }
4735
+ }
4736
+ },
4737
+ Flatten(path: "us.@") {
4738
+ Fetch(service: "S2") {
4739
+ {
4740
+ ... on A {
4741
+ __typename
4742
+ id
4743
+ g
4744
+ }
4745
+ ... on B {
4746
+ __typename
4747
+ id
4748
+ g
4749
+ }
4750
+ } =>
4751
+ {
4752
+ ... on A {
4753
+ f
4754
+ }
4755
+ ... on B {
4756
+ f
4757
+ }
4758
+ }
4759
+ },
4760
+ },
4761
+ },
4762
+ }
4763
+ `);
4764
+
4765
+ const response = await executePlan(queryPlan, operation, undefined, schema, serviceMap);
4766
+ expect(response.errors).toBeUndefined();
4767
+ // We double-check that the final aliases are the one from the query
4768
+ expect(response.data).toMatchInlineSnapshot(`
4769
+ Object {
4770
+ "us": Array [
4771
+ Object {
4772
+ "f": "g is type number",
4773
+ "g__alias_0": "xA",
4774
+ "g__alias_1": "yA",
4775
+ },
4776
+ Object {
4777
+ "f": "g is type string",
4778
+ "g__alias_0": "xB",
4779
+ "g__alias_1": "yB",
4780
+ },
4781
+ ],
4782
+ }
4783
+ `);
4784
+ });
4785
+ });
3519
4786
  });