Skip to content

Commit 4634a9c

Browse files
lilianammmatosrobrichard
authored andcommitted
Implement support for @defer directive
1 parent 256e252 commit 4634a9c

16 files changed

+1310
-63
lines changed

src/execution/__tests__/defer-test.ts

Lines changed: 473 additions & 0 deletions
Large diffs are not rendered by default.

src/execution/__tests__/lists-test.ts

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

1515
import { buildSchema } from '../../utilities/buildASTSchema';
1616

17-
import type { ExecutionResult } from '../execute';
17+
import type { AsyncExecutionResult, ExecutionResult } from '../execute';
1818
import { execute, executeSync } from '../execute';
1919

2020
describe('Execute: Accepts any iterable as list value', () => {
@@ -85,7 +85,9 @@ describe('Execute: Accepts async iterables as list value', () => {
8585

8686
function completeObjectList(
8787
resolve: GraphQLFieldResolver<{ index: number }, unknown>,
88-
): PromiseOrValue<ExecutionResult> {
88+
): PromiseOrValue<
89+
ExecutionResult | AsyncGenerator<AsyncExecutionResult, void, void>
90+
> {
8991
const schema = new GraphQLSchema({
9092
query: new GraphQLObjectType({
9193
name: 'Query',

src/execution/__tests__/mutations-test.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { describe, it } from 'mocha';
44
import { expectJSON } from '../../__testUtils__/expectJSON';
55
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick';
66

7+
import { invariant } from '../../jsutils/invariant';
8+
import { isAsyncIterable } from '../../jsutils/isAsyncIterable';
9+
710
import { parse } from '../../language/parser';
811

912
import { GraphQLObjectType } from '../../type/definition';
@@ -50,6 +53,15 @@ class Root {
5053
const numberHolderType = new GraphQLObjectType({
5154
fields: {
5255
theNumber: { type: GraphQLInt },
56+
promiseToGetTheNumber: {
57+
type: GraphQLInt,
58+
resolve: (root) =>
59+
new Promise((resolve) => {
60+
process.nextTick(() => {
61+
resolve(root.theNumber);
62+
});
63+
}),
64+
},
5365
},
5466
name: 'NumberHolder',
5567
});
@@ -94,6 +106,7 @@ const schema = new GraphQLSchema({
94106
},
95107
name: 'Mutation',
96108
}),
109+
enableDeferStream: true,
97110
});
98111

99112
describe('Execute: Handles mutation execution ordering', () => {
@@ -191,4 +204,122 @@ describe('Execute: Handles mutation execution ordering', () => {
191204
],
192205
});
193206
});
207+
it('Mutation fields with @defer do not block next mutation', async () => {
208+
const document = parse(`
209+
mutation M {
210+
first: promiseToChangeTheNumber(newNumber: 1) {
211+
...DeferFragment @defer(label: "defer-label")
212+
},
213+
second: immediatelyChangeTheNumber(newNumber: 2) {
214+
theNumber
215+
}
216+
}
217+
fragment DeferFragment on NumberHolder {
218+
promiseToGetTheNumber
219+
}
220+
`);
221+
222+
const rootValue = new Root(6);
223+
const mutationResult = await execute({
224+
schema,
225+
document,
226+
rootValue,
227+
});
228+
const patches = [];
229+
230+
invariant(isAsyncIterable(mutationResult));
231+
for await (const patch of mutationResult) {
232+
patches.push(patch);
233+
}
234+
235+
expect(patches).to.deep.equal([
236+
{
237+
data: {
238+
first: {},
239+
second: { theNumber: 2 },
240+
},
241+
hasNext: true,
242+
},
243+
{
244+
label: 'defer-label',
245+
path: ['first'],
246+
data: {
247+
promiseToGetTheNumber: 2,
248+
},
249+
hasNext: false,
250+
},
251+
]);
252+
});
253+
it('Mutation inside of a fragment', async () => {
254+
const document = parse(`
255+
mutation M {
256+
...MutationFragment
257+
second: immediatelyChangeTheNumber(newNumber: 2) {
258+
theNumber
259+
}
260+
}
261+
fragment MutationFragment on Mutation {
262+
first: promiseToChangeTheNumber(newNumber: 1) {
263+
theNumber
264+
},
265+
}
266+
`);
267+
268+
const rootValue = new Root(6);
269+
const mutationResult = await execute({ schema, document, rootValue });
270+
271+
expect(mutationResult).to.deep.equal({
272+
data: {
273+
first: { theNumber: 1 },
274+
second: { theNumber: 2 },
275+
},
276+
});
277+
});
278+
it('Mutation with @defer is not executed serially', async () => {
279+
const document = parse(`
280+
mutation M {
281+
...MutationFragment @defer(label: "defer-label")
282+
second: immediatelyChangeTheNumber(newNumber: 2) {
283+
theNumber
284+
}
285+
}
286+
fragment MutationFragment on Mutation {
287+
first: promiseToChangeTheNumber(newNumber: 1) {
288+
theNumber
289+
},
290+
}
291+
`);
292+
293+
const rootValue = new Root(6);
294+
const mutationResult = await execute({
295+
schema,
296+
document,
297+
rootValue,
298+
});
299+
const patches = [];
300+
301+
invariant(isAsyncIterable(mutationResult));
302+
for await (const patch of mutationResult) {
303+
patches.push(patch);
304+
}
305+
306+
expect(patches).to.deep.equal([
307+
{
308+
data: {
309+
second: { theNumber: 2 },
310+
},
311+
hasNext: true,
312+
},
313+
{
314+
label: 'defer-label',
315+
path: [],
316+
data: {
317+
first: {
318+
theNumber: 1,
319+
},
320+
},
321+
hasNext: false,
322+
},
323+
]);
324+
});
194325
});

src/execution/__tests__/nonnull-test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { describe, it } from 'mocha';
33

44
import { expectJSON } from '../../__testUtils__/expectJSON';
55

6+
import type { PromiseOrValue } from '../../jsutils/PromiseOrValue';
7+
68
import { parse } from '../../language/parser';
79

810
import { GraphQLNonNull, GraphQLObjectType } from '../../type/definition';
@@ -11,7 +13,7 @@ import { GraphQLSchema } from '../../type/schema';
1113

1214
import { buildSchema } from '../../utilities/buildASTSchema';
1315

14-
import type { ExecutionResult } from '../execute';
16+
import type { AsyncExecutionResult, ExecutionResult } from '../execute';
1517
import { execute, executeSync } from '../execute';
1618

1719
const syncError = new Error('sync');
@@ -109,7 +111,9 @@ const schema = buildSchema(`
109111
function executeQuery(
110112
query: string,
111113
rootValue: unknown,
112-
): ExecutionResult | Promise<ExecutionResult> {
114+
): PromiseOrValue<
115+
ExecutionResult | AsyncGenerator<AsyncExecutionResult, void, void>
116+
> {
113117
return execute({ schema, document: parse(query), rootValue });
114118
}
115119

src/execution/__tests__/sync-test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ describe('Execute: synchronously when possible', () => {
4545
},
4646
},
4747
}),
48+
enableDeferStream: true,
4849
});
4950

5051
it('does not return a Promise for initial errors', () => {
@@ -113,6 +114,24 @@ describe('Execute: synchronously when possible', () => {
113114
});
114115
}).to.throw('GraphQL execution failed to complete synchronously.');
115116
});
117+
118+
it('throws if encountering async iterable execution', () => {
119+
const doc = `
120+
query Example {
121+
...deferFrag @defer(label: "deferLabel")
122+
}
123+
fragment deferFrag on Query {
124+
syncField
125+
}
126+
`;
127+
expect(() => {
128+
executeSync({
129+
schema,
130+
document: parse(doc),
131+
rootValue: 'rootValue',
132+
});
133+
}).to.throw('GraphQL execution failed to complete synchronously.');
134+
});
116135
});
117136

118137
describe('graphqlSync', () => {

0 commit comments

Comments
 (0)