Skip to content

Commit c17c7bb

Browse files
Gateway over-merging fields of unioned types (#3581)
Group fields by response name AND parent type The groupByResponseName function was insufficient for determining the merge condition for selections. The parent type name is also required for the merge condition, as it's possible for them to differ.
1 parent e3d3b90 commit c17c7bb

File tree

4 files changed

+103
-5
lines changed

4 files changed

+103
-5
lines changed

packages/apollo-gateway/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the the appropriate changes within that release will be moved into the new section.
66
7+
* Gateway over-merging fields of unioned types [#3581](https://github.com/apollographql/apollo-server/pull/3581)
8+
79
# v0.11.0
810

911
> [See complete versioning details.](https://github.com/apollographql/apollo-server/commit/93002737d53dd9a50b473ab9cef14849b3e539aa)

packages/apollo-gateway/src/FieldSet.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,25 @@ function groupBy<T, U>(keyFunction: (element: T) => U) {
6464
};
6565
}
6666

67-
export const groupByResponseName = groupBy<Field, string>(field =>
68-
getResponseName(field.fieldNode),
67+
// The response name isn't sufficient for determining uniqueness. In the case of
68+
// unions, for example, we can see a response name collision where the parent type
69+
// is different. In this case, these should not be merged (media)!
70+
// query {
71+
// content {
72+
// ... on Audio {
73+
// media {
74+
// url
75+
// }
76+
// }
77+
// ... on Video {
78+
// media {
79+
// aspectRatio
80+
// }
81+
// }
82+
// }
83+
// }
84+
export const groupByParentTypeAndResponseName = groupBy<Field, string>(field =>
85+
`${field.scope.parentType}:${getResponseName(field.fieldNode)}`,
6986
);
7087

7188
export const groupByParentType = groupBy<Field, GraphQLCompositeType>(
@@ -81,7 +98,7 @@ export function selectionSetFromFieldSet(
8198
selections: Array.from(groupByParentType(fields)).flatMap(
8299
([typeCondition, fieldsByParentType]: [GraphQLCompositeType, FieldSet]) =>
83100
wrapInInlineFragmentIfNeeded(
84-
Array.from(groupByResponseName(fieldsByParentType).values()).map(
101+
Array.from(groupByParentTypeAndResponseName(fieldsByParentType).values()).map(
85102
fieldsByResponseName => {
86103
return combineFields(fieldsByResponseName)
87104
.fieldNode;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import gql from 'graphql-tag';
2+
import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers';
3+
import { execute } from '../execution-utils';
4+
5+
expect.addSnapshotSerializer(astSerializer);
6+
expect.addSnapshotSerializer(queryPlanSerializer);
7+
8+
it('handles multiple union type conditions that share a response name (media)', async () => {
9+
const query = gql`
10+
query {
11+
content {
12+
...Audio
13+
... on Video {
14+
media {
15+
aspectRatio
16+
}
17+
}
18+
}
19+
}
20+
fragment Audio on Audio {
21+
media {
22+
url
23+
}
24+
}
25+
`;
26+
27+
const { queryPlan, errors } = await execute(
28+
[
29+
{
30+
name: 'contentService',
31+
typeDefs: gql`
32+
extend type Query {
33+
content: Content
34+
}
35+
union Content = Audio | Video
36+
type Audio {
37+
media: AudioURL
38+
}
39+
type AudioURL {
40+
url: String
41+
}
42+
type Video {
43+
media: VideoAspectRatio
44+
}
45+
type VideoAspectRatio {
46+
aspectRatio: String
47+
}
48+
`,
49+
resolvers: {
50+
Query: {},
51+
},
52+
},
53+
],
54+
{ query },
55+
);
56+
57+
expect(errors).toBeUndefined();
58+
expect(queryPlan).toMatchInlineSnapshot(`
59+
QueryPlan {
60+
Fetch(service: "contentService") {
61+
{
62+
content {
63+
__typename
64+
... on Audio {
65+
media {
66+
url
67+
}
68+
}
69+
... on Video {
70+
media {
71+
aspectRatio
72+
}
73+
}
74+
}
75+
}
76+
},
77+
}
78+
`);
79+
});

packages/apollo-gateway/src/buildQueryPlan.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
Field,
3232
FieldSet,
3333
groupByParentType,
34-
groupByResponseName,
34+
groupByParentTypeAndResponseName,
3535
matchesField,
3636
selectionSetFromFieldSet,
3737
Scope,
@@ -373,7 +373,7 @@ function splitFields(
373373
fields: FieldSet,
374374
groupForField: (field: Field<GraphQLObjectType>) => FetchGroup,
375375
) {
376-
for (const fieldsForResponseName of groupByResponseName(fields).values()) {
376+
for (const fieldsForResponseName of groupByParentTypeAndResponseName(fields).values()) {
377377
for (const [parentType, fieldsForParentType] of groupByParentType(
378378
fieldsForResponseName,
379379
)) {

0 commit comments

Comments
 (0)