@apollo/gateway 2.2.3 → 2.3.0-beta.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.
- package/dist/executeQueryPlan.d.ts.map +1 -1
- package/dist/executeQueryPlan.js +81 -7
- package/dist/executeQueryPlan.js.map +1 -1
- package/package.json +5 -6
- package/src/__tests__/executeQueryPlan.test.ts +1267 -0
- package/src/__tests__/gateway/lifecycle-hooks.test.ts +1 -1
- package/src/executeQueryPlan.ts +96 -5
|
@@ -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
|
});
|