Skip to content

Commit c93bf4d

Browse files
committed
Implement support for @defer directive
1 parent d5c9dab commit c93bf4d

File tree

12 files changed

+895
-63
lines changed

12 files changed

+895
-63
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import { describe, it } from 'mocha';
2+
3+
import { expectJSON } from '../../__testUtils__/expectJSON';
4+
import { isAsyncIterable } from '../../jsutils/isAsyncIterable';
5+
import { parse } from '../../language/parser';
6+
7+
import { GraphQLID, GraphQLString } from '../../type/scalars';
8+
import { GraphQLSchema } from '../../type/schema';
9+
import { GraphQLObjectType, GraphQLList } from '../../type/definition';
10+
11+
import type { DocumentNode } from '../../language/ast';
12+
13+
import { execute } from '../execute';
14+
15+
const friendType = new GraphQLObjectType({
16+
fields: {
17+
id: { type: GraphQLID },
18+
name: { type: GraphQLString },
19+
},
20+
name: 'Friend',
21+
});
22+
23+
const friends = [
24+
{ name: 'Han', id: 2 },
25+
{ name: 'Leia', id: 3 },
26+
{ name: 'C-3PO', id: 4 },
27+
];
28+
29+
const heroType = new GraphQLObjectType({
30+
fields: {
31+
id: { type: GraphQLID },
32+
name: { type: GraphQLString },
33+
errorField: {
34+
type: GraphQLString,
35+
resolve: () => {
36+
throw new Error('bad');
37+
},
38+
},
39+
friends: {
40+
type: new GraphQLList(friendType),
41+
resolve: () => friends,
42+
},
43+
},
44+
name: 'Hero',
45+
});
46+
47+
const hero = { name: 'Luke', id: 1 };
48+
49+
const query = new GraphQLObjectType({
50+
fields: {
51+
hero: {
52+
type: heroType,
53+
resolve: () => hero,
54+
},
55+
},
56+
name: 'Query',
57+
});
58+
59+
async function complete(document: DocumentNode) {
60+
const schema = new GraphQLSchema({ query });
61+
62+
const result = await execute({
63+
schema,
64+
document,
65+
rootValue: {},
66+
});
67+
68+
if (isAsyncIterable(result)) {
69+
const results = [];
70+
for await (const patch of result) {
71+
results.push(patch);
72+
}
73+
return results;
74+
}
75+
return result;
76+
}
77+
78+
describe('Execute: defer directive', () => {
79+
it('Can defer fragments containing scalar types', async () => {
80+
const document = parse(`
81+
query HeroNameQuery {
82+
hero {
83+
id
84+
...NameFragment @defer
85+
}
86+
}
87+
fragment NameFragment on Hero {
88+
id
89+
name
90+
}
91+
`);
92+
const result = await complete(document);
93+
94+
expectJSON(result).toDeepEqual([
95+
{
96+
data: {
97+
hero: {
98+
id: '1',
99+
},
100+
},
101+
hasNext: true,
102+
},
103+
{
104+
data: {
105+
id: '1',
106+
name: 'Luke',
107+
},
108+
path: ['hero'],
109+
hasNext: false,
110+
},
111+
]);
112+
});
113+
it('Can disable defer using if argument', async () => {
114+
const document = parse(`
115+
query HeroNameQuery {
116+
hero {
117+
id
118+
...NameFragment @defer(if: false)
119+
}
120+
}
121+
fragment NameFragment on Hero {
122+
name
123+
}
124+
`);
125+
const result = await complete(document);
126+
127+
expectJSON(result).toDeepEqual({
128+
data: {
129+
hero: {
130+
id: '1',
131+
name: 'Luke',
132+
},
133+
},
134+
});
135+
});
136+
it('Can defer fragments containing on the top level Query field', async () => {
137+
const document = parse(`
138+
query HeroNameQuery {
139+
...QueryFragment @defer(label: "DeferQuery")
140+
}
141+
fragment QueryFragment on Query {
142+
hero {
143+
id
144+
}
145+
}
146+
`);
147+
const result = await complete(document);
148+
149+
expectJSON(result).toDeepEqual([
150+
{
151+
data: {},
152+
hasNext: true,
153+
},
154+
{
155+
data: {
156+
hero: {
157+
id: '1',
158+
},
159+
},
160+
path: [],
161+
label: 'DeferQuery',
162+
hasNext: false,
163+
},
164+
]);
165+
});
166+
it('Can defer a fragment within an already deferred fragment', async () => {
167+
const document = parse(`
168+
query HeroNameQuery {
169+
hero {
170+
id
171+
...TopFragment @defer(label: "DeferTop")
172+
}
173+
}
174+
fragment TopFragment on Hero {
175+
name
176+
...NestedFragment @defer(label: "DeferNested")
177+
}
178+
fragment NestedFragment on Hero {
179+
friends {
180+
name
181+
}
182+
}
183+
`);
184+
const result = await complete(document);
185+
186+
expectJSON(result).toDeepEqual([
187+
{
188+
data: {
189+
hero: {
190+
id: '1',
191+
},
192+
},
193+
hasNext: true,
194+
},
195+
{
196+
data: {
197+
friends: [{ name: 'Han' }, { name: 'Leia' }, { name: 'C-3PO' }],
198+
},
199+
path: ['hero'],
200+
label: 'DeferNested',
201+
hasNext: true,
202+
},
203+
{
204+
data: {
205+
name: 'Luke',
206+
},
207+
path: ['hero'],
208+
label: 'DeferTop',
209+
hasNext: false,
210+
},
211+
]);
212+
});
213+
it('Can defer an inline fragment', async () => {
214+
const document = parse(`
215+
query HeroNameQuery {
216+
hero {
217+
id
218+
... on Hero @defer(label: "InlineDeferred") {
219+
name
220+
}
221+
}
222+
}
223+
`);
224+
const result = await complete(document);
225+
226+
expectJSON(result).toDeepEqual([
227+
{
228+
data: { hero: { id: '1' } },
229+
hasNext: true,
230+
},
231+
{
232+
data: { name: 'Luke' },
233+
path: ['hero'],
234+
label: 'InlineDeferred',
235+
hasNext: false,
236+
},
237+
]);
238+
});
239+
it('Handles errors thrown in deferred fragments', async () => {
240+
const document = parse(`
241+
query HeroNameQuery {
242+
hero {
243+
id
244+
...NameFragment @defer
245+
}
246+
}
247+
fragment NameFragment on Hero {
248+
errorField
249+
}
250+
`);
251+
const result = await complete(document);
252+
expectJSON(result).toDeepEqual([
253+
{
254+
data: { hero: { id: '1' } },
255+
hasNext: true,
256+
},
257+
{
258+
data: { errorField: null },
259+
path: ['hero'],
260+
errors: [
261+
{
262+
message: 'bad',
263+
locations: [{ line: 9, column: 9 }],
264+
path: ['hero', 'errorField'],
265+
},
266+
],
267+
hasNext: false,
268+
},
269+
]);
270+
});
271+
});

src/execution/__tests__/lists-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { GraphQLSchema } from '../../type/schema';
1212

1313
import { buildSchema } from '../../utilities/buildASTSchema';
1414

15-
import type { ExecutionResult } from '../execute';
15+
import type { ExecutionResult, AsyncExecutionResult } from '../execute';
1616
import { execute, executeSync } from '../execute';
1717

1818
describe('Execute: Accepts any iterable as list value', () => {
@@ -83,7 +83,7 @@ describe('Execute: Accepts async iterables as list value', () => {
8383

8484
function completeObjectList(
8585
resolve: GraphQLFieldResolver<{ index: number }, unknown>,
86-
): PromiseOrValue<ExecutionResult> {
86+
): PromiseOrValue<ExecutionResult | AsyncIterable<AsyncExecutionResult>> {
8787
const schema = new GraphQLSchema({
8888
query: new GraphQLObjectType({
8989
name: 'Query',

0 commit comments

Comments
 (0)