Skip to content

Commit 27dcb17

Browse files
committed
Implement support for @defer directive
1 parent 2821589 commit 27dcb17

18 files changed

+1577
-64
lines changed

src/execution/__tests__/defer-test.ts

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

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)