Skip to content

Commit df01dec

Browse files
committed
Implement support for @defer directive
1 parent 4a284d7 commit df01dec

18 files changed

+1577
-63
lines changed

src/execution/__tests__/defer-test.ts

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

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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,24 @@ describe('Execute: synchronously when possible', () => {
113113
});
114114
}).to.throw('GraphQL execution failed to complete synchronously.');
115115
});
116+
117+
it('throws if encountering async iterable execution', () => {
118+
const doc = `
119+
query Example {
120+
...deferFrag @defer(label: "deferLabel")
121+
}
122+
fragment deferFrag on Query {
123+
syncField
124+
}
125+
`;
126+
expect(() => {
127+
executeSync({
128+
schema,
129+
document: parse(doc),
130+
rootValue: 'rootValue',
131+
});
132+
}).to.throw('GraphQL execution failed to complete synchronously.');
133+
});
116134
});
117135

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

0 commit comments

Comments
 (0)