Skip to content

Commit e725408

Browse files
committed
Implement support for @defer directive
1 parent 1ccd696 commit e725408

17 files changed

+1582
-67
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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import { expect } from 'chai';
1+
import { assert, expect } from 'chai';
22
import { describe, it } from 'mocha';
33

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

7+
import { isAsyncIterable } from '../../jsutils/isAsyncIterable';
8+
79
import { parse } from '../../language/parser';
810

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

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)