Skip to content

Commit 49c44eb

Browse files
committed
Merge commit 'refs/pull/4064/head' of github.com:apollographql/apollo-server into release-2.14.1
2 parents d159e32 + 89467d4 commit 49c44eb

File tree

5 files changed

+292
-21
lines changed

5 files changed

+292
-21
lines changed

packages/apollo-gateway/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
77
- _Nothing yet! Stay tuned!_
88

9+
## 0.16.2
10+
11+
- __FIX__: Collapse nested required fields into a single body in the query plan. Before, some nested fields' selection sets were getting split, causing some of their subfields to be dropped when executing the query. This fix collapses the split selection sets into one. [#4064](https://github.com/apollographql/apollo-server/pull/4064)
12+
913
## 0.16.1
1014

1115
- __NEW__: Provide the ability to pass a custom `fetcher` during `RemoteGraphQLDataSource` construction to be used when executing operations against downstream services. Providing a custom `fetcher` may be necessary to accommodate more advanced needs, e.g., configuring custom TLS certificates for internal services. [PR #4149](https://github.com/apollographql/apollo-server/pull/4149)

packages/apollo-gateway/src/FieldSet.ts

+34-20
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
GraphQLObjectType,
1111
} from 'graphql';
1212
import { getResponseName } from './utilities/graphql';
13+
import { partition, groupBy } from './utilities/array';
1314

1415
export interface Field<
1516
TParent extends GraphQLCompositeType = GraphQLCompositeType
@@ -45,25 +46,6 @@ export function matchesField(field: Field) {
4546
};
4647
}
4748

48-
function groupBy<T, U>(keyFunction: (element: T) => U) {
49-
return (iterable: Iterable<T>) => {
50-
const result = new Map<U, T[]>();
51-
52-
for (const element of iterable) {
53-
const key = keyFunction(element);
54-
const group = result.get(key);
55-
56-
if (group) {
57-
group.push(element);
58-
} else {
59-
result.set(key, [element]);
60-
}
61-
}
62-
63-
return result;
64-
};
65-
}
66-
6749
export const groupByResponseName = groupBy<Field, string>(field =>
6850
getResponseName(field.fieldNode)
6951
);
@@ -147,6 +129,38 @@ function mergeSelectionSets(fieldNodes: FieldNode[]): SelectionSetNode {
147129

148130
return {
149131
kind: 'SelectionSet',
150-
selections,
132+
selections: mergeFieldNodeSelectionSets(selections),
151133
};
152134
}
135+
136+
function mergeFieldNodeSelectionSets(
137+
selectionNodes: SelectionNode[],
138+
): SelectionNode[] {
139+
const [fieldNodes, fragmentNodes] = partition(
140+
selectionNodes,
141+
(node): node is FieldNode => node.kind === Kind.FIELD,
142+
);
143+
144+
const [aliasedFieldNodes, nonAliasedFieldNodes] = partition(
145+
fieldNodes,
146+
node => !!node.alias,
147+
);
148+
149+
const mergedFieldNodes = Array.from(
150+
groupBy((node: FieldNode) => node.name.value)(
151+
nonAliasedFieldNodes,
152+
).values(),
153+
).map((nodesWithSameName) => {
154+
const node = nodesWithSameName[0];
155+
if (node.selectionSet) {
156+
node.selectionSet.selections = mergeFieldNodeSelectionSets(
157+
nodesWithSameName.flatMap(
158+
(node) => node.selectionSet?.selections || [],
159+
),
160+
);
161+
}
162+
return node;
163+
});
164+
165+
return [...mergedFieldNodes, ...aliasedFieldNodes, ...fragmentNodes];
166+
}

packages/apollo-gateway/src/__tests__/integration/complex-key.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ it('works fetches data correctly with complex / nested @key fields', async () =>
171171
organization {
172172
id
173173
__typename
174-
id
175174
}
176175
}
177176
}

packages/apollo-gateway/src/__tests__/integration/requires.test.ts

+227
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import gql from 'graphql-tag';
12
import { execute } from '../execution-utils';
3+
import { serializeQueryPlan } from '../..';
24

35
it('supports passing additional fields defined by a requires', async () => {
46
const query = `#graphql
@@ -38,3 +40,228 @@ it('supports passing additional fields defined by a requires', async () => {
3840
expect(queryPlan).toCallService('product');
3941
expect(queryPlan).toCallService('books');
4042
});
43+
44+
const serviceA = {
45+
name: 'a',
46+
typeDefs: gql`
47+
type Query {
48+
user: User
49+
}
50+
51+
type User @key(fields: "id") {
52+
id: ID!
53+
preferences: Preferences
54+
}
55+
56+
type Preferences {
57+
favorites: Things
58+
}
59+
60+
type Things {
61+
color: String
62+
animal: String
63+
}
64+
`,
65+
resolvers: {
66+
Query: {
67+
user() {
68+
return {
69+
id: '1',
70+
preferences: {
71+
favorites: { color: 'limegreen', animal: 'platypus' },
72+
},
73+
};
74+
},
75+
},
76+
},
77+
};
78+
79+
const serviceB = {
80+
name: 'b',
81+
typeDefs: gql`
82+
extend type User @key(fields: "id") {
83+
id: ID! @external
84+
preferences: Preferences @external
85+
favoriteColor: String
86+
@requires(fields: "preferences { favorites { color } }")
87+
favoriteAnimal: String
88+
@requires(fields: "preferences { favorites { animal } }")
89+
}
90+
91+
extend type Preferences {
92+
favorites: Things @external
93+
}
94+
95+
extend type Things {
96+
color: String @external
97+
animal: String @external
98+
}
99+
`,
100+
resolvers: {
101+
User: {
102+
favoriteColor(user: any) {
103+
return user.preferences.favorites.color;
104+
},
105+
favoriteAnimal(user: any) {
106+
return user.preferences.favorites.animal;
107+
},
108+
},
109+
},
110+
};
111+
112+
it('collapses nested requires', async () => {
113+
const query = `#graphql
114+
query UserFavorites {
115+
user {
116+
favoriteColor
117+
favoriteAnimal
118+
}
119+
}
120+
`;
121+
122+
const { data, errors, queryPlan } = await execute(
123+
{
124+
query,
125+
},
126+
[serviceA, serviceB],
127+
);
128+
129+
expect(errors).toEqual(undefined);
130+
131+
expect(serializeQueryPlan(queryPlan)).toMatchInlineSnapshot(`
132+
"QueryPlan {
133+
Sequence {
134+
Fetch(service: \\"a\\") {
135+
{
136+
user {
137+
__typename
138+
id
139+
preferences {
140+
favorites {
141+
color
142+
animal
143+
}
144+
}
145+
}
146+
}
147+
},
148+
Flatten(path: \\"user\\") {
149+
Fetch(service: \\"b\\") {
150+
{
151+
... on User {
152+
__typename
153+
id
154+
preferences {
155+
favorites {
156+
color
157+
animal
158+
}
159+
}
160+
}
161+
} =>
162+
{
163+
... on User {
164+
favoriteColor
165+
favoriteAnimal
166+
}
167+
}
168+
},
169+
},
170+
},
171+
}"
172+
`);
173+
174+
expect(data).toEqual({
175+
user: {
176+
favoriteAnimal: 'platypus',
177+
favoriteColor: 'limegreen',
178+
},
179+
});
180+
181+
expect(queryPlan).toCallService('a');
182+
expect(queryPlan).toCallService('b');
183+
});
184+
185+
it('collapses nested requires with user-defined fragments', async () => {
186+
const query = `#graphql
187+
query UserFavorites {
188+
user {
189+
favoriteAnimal
190+
...favoriteColor
191+
}
192+
}
193+
194+
fragment favoriteColor on User {
195+
preferences {
196+
favorites {
197+
color
198+
}
199+
}
200+
}
201+
`;
202+
203+
const { data, errors, queryPlan } = await execute(
204+
{
205+
query,
206+
},
207+
[serviceA, serviceB],
208+
);
209+
210+
expect(errors).toEqual(undefined);
211+
212+
expect(serializeQueryPlan(queryPlan)).toMatchInlineSnapshot(`
213+
"QueryPlan {
214+
Sequence {
215+
Fetch(service: \\"a\\") {
216+
{
217+
user {
218+
__typename
219+
id
220+
preferences {
221+
favorites {
222+
animal
223+
color
224+
}
225+
}
226+
}
227+
}
228+
},
229+
Flatten(path: \\"user\\") {
230+
Fetch(service: \\"b\\") {
231+
{
232+
... on User {
233+
__typename
234+
id
235+
preferences {
236+
favorites {
237+
animal
238+
color
239+
}
240+
}
241+
}
242+
} =>
243+
{
244+
... on User {
245+
favoriteAnimal
246+
}
247+
}
248+
},
249+
},
250+
},
251+
}"
252+
`);
253+
254+
expect(data).toEqual({
255+
user: {
256+
favoriteAnimal: 'platypus',
257+
preferences: {
258+
favorites: {
259+
color: 'limegreen',
260+
},
261+
},
262+
},
263+
});
264+
265+
expect(queryPlan).toCallService('a');
266+
expect(queryPlan).toCallService('b');
267+
});

packages/apollo-gateway/src/utilities/array.ts

+27
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ export function compactMap<T, U>(
1616
);
1717
}
1818

19+
export function partition<T, U extends T>(
20+
array: T[],
21+
predicate: (element: T, index: number, array: T[]) => element is U,
22+
): [U[], T[]];
23+
export function partition<T>(
24+
array: T[],
25+
predicate: (element: T, index: number, array: T[]) => boolean,
26+
): [T[], T[]];
1927
export function partition<T>(
2028
array: T[],
2129
predicate: (element: T, index: number, array: T[]) => boolean,
@@ -48,3 +56,22 @@ export function findAndExtract<T>(
4856

4957
return [array[index], remaining];
5058
}
59+
60+
export function groupBy<T, U>(keyFunction: (element: T) => U) {
61+
return (iterable: Iterable<T>) => {
62+
const result = new Map<U, T[]>();
63+
64+
for (const element of iterable) {
65+
const key = keyFunction(element);
66+
const group = result.get(key);
67+
68+
if (group) {
69+
group.push(element);
70+
} else {
71+
result.set(key, [element]);
72+
}
73+
}
74+
75+
return result;
76+
};
77+
}

0 commit comments

Comments
 (0)