Skip to content

Commit 8e0289c

Browse files
committed
defer/stream: split incremental delivery into new entry point
This PR changes the `execute` and `graphql` APIs on the `defer-stream` branch back to having the same API as on `main`: they can only produce a single `ExecutionResult` and do not support incremental delivery. Incremental delivery is now provided by the new entry point `experimentalExecuteIncrementally`. (We will remove "experimental" once the proposal has been merged into the GraphQL spec.) This function always returns a Promise containing an object that is *either* a single result *or* an initial result plus a generator for subsequent results. This will make upgrading to graphql@17 easier, and increases the clarity of return types. Use distinct types for "the only result of a single-payload execution", "the first payload of a multi-payload execution", and "subsequent payloads of a multi-payload execution", since the data structures are different. (Multi-payload executions *always* have at least one payload, and the structure differs, which is why the new types separate the initial result from subsequent results.) Note that with the previous types, you actually had to use a function with a type guard like `isAsyncIterable`: you couldn't just write `if (Symbol.asyncIterator in result)`. I think both explicitly separating the first (differently-typed) element from the rest of the elements *and* making it possible to differentiate the types with a simple `in` check are improvements. Additionally, somebody using the API in JavaScript who doesn't realize that there is new functionality available who is confused that the result no longer has `data`/`errors` fields can just print out the result and see and search for the terms `initialResult` and `subsequentResults` instead of only seeing iterator internals. Fix `Formatted*Result` types to use `GraphQLFormattedError` rather than `GraphQLError` for `errors` nested inside `incremental`.
1 parent aa830a4 commit 8e0289c

File tree

11 files changed

+400
-165
lines changed

11 files changed

+400
-165
lines changed

src/execution/__tests__/defer-test.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { describe, it } from 'mocha';
22

33
import { expectJSON } from '../../__testUtils__/expectJSON';
4+
import { expectPromise } from '../../__testUtils__/expectPromise';
45
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick';
56

6-
import { isAsyncIterable } from '../../jsutils/isAsyncIterable';
7-
87
import type { DocumentNode } from '../../language/ast';
98
import { parse } from '../../language/parser';
109

@@ -16,7 +15,11 @@ import {
1615
import { GraphQLID, GraphQLString } from '../../type/scalars';
1716
import { GraphQLSchema } from '../../type/schema';
1817

19-
import { execute } from '../execute';
18+
import type {
19+
InitialIncrementalExecutionResult,
20+
SubsequentIncrementalExecutionResult,
21+
} from '../execute';
22+
import { execute, experimentalExecuteIncrementally } from '../execute';
2023

2124
const friendType = new GraphQLObjectType({
2225
fields: {
@@ -77,23 +80,25 @@ const query = new GraphQLObjectType({
7780
name: 'Query',
7881
});
7982

80-
async function complete(document: DocumentNode) {
81-
const schema = new GraphQLSchema({ query });
83+
const schema = new GraphQLSchema({ query });
8284

83-
const result = await execute({
85+
async function complete(document: DocumentNode) {
86+
const result = await experimentalExecuteIncrementally({
8487
schema,
8588
document,
8689
rootValue: {},
8790
});
8891

89-
if (isAsyncIterable(result)) {
90-
const results = [];
91-
for await (const patch of result) {
92+
if ('initialResult' in result) {
93+
const results: Array<
94+
InitialIncrementalExecutionResult | SubsequentIncrementalExecutionResult
95+
> = [result.initialResult];
96+
for await (const patch of result.subsequentResults) {
9297
results.push(patch);
9398
}
9499
return results;
95100
}
96-
return result;
101+
return result.singleResult;
97102
}
98103

99104
describe('Execute: defer directive', () => {
@@ -641,4 +646,21 @@ describe('Execute: defer directive', () => {
641646
},
642647
]);
643648
});
649+
650+
it('original execute function errors if anything is deferred', async () => {
651+
const doc = `
652+
query Deferred {
653+
... @defer { hero { id } }
654+
}
655+
`;
656+
await expectPromise(
657+
execute({
658+
schema,
659+
document: parse(doc),
660+
rootValue: {},
661+
}),
662+
).toRejectWith(
663+
'Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)',
664+
);
665+
});
644666
});

src/execution/__tests__/lists-test.ts

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

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

6-
import type { PromiseOrValue } from '../../jsutils/PromiseOrValue';
7-
86
import { parse } from '../../language/parser';
97

108
import type { GraphQLFieldResolver } from '../../type/definition';
@@ -14,8 +12,12 @@ import { GraphQLSchema } from '../../type/schema';
1412

1513
import { buildSchema } from '../../utilities/buildASTSchema';
1614

17-
import type { AsyncExecutionResult, ExecutionResult } from '../execute';
18-
import { execute, executeSync } from '../execute';
15+
import type { ExperimentalExecuteIncrementallyResults } from '../execute';
16+
import {
17+
execute,
18+
executeSync,
19+
experimentalExecuteIncrementally,
20+
} from '../execute';
1921

2022
describe('Execute: Accepts any iterable as list value', () => {
2123
function complete(rootValue: unknown) {
@@ -85,9 +87,7 @@ describe('Execute: Accepts async iterables as list value', () => {
8587

8688
function completeObjectList(
8789
resolve: GraphQLFieldResolver<{ index: number }, unknown>,
88-
): PromiseOrValue<
89-
ExecutionResult | AsyncGenerator<AsyncExecutionResult, void, void>
90-
> {
90+
): Promise<ExperimentalExecuteIncrementallyResults> {
9191
const schema = new GraphQLSchema({
9292
query: new GraphQLObjectType({
9393
name: 'Query',
@@ -113,7 +113,7 @@ describe('Execute: Accepts async iterables as list value', () => {
113113
},
114114
}),
115115
});
116-
return execute({
116+
return experimentalExecuteIncrementally({
117117
schema,
118118
document: parse('{ listField { index } }'),
119119
});
@@ -191,7 +191,9 @@ describe('Execute: Accepts async iterables as list value', () => {
191191
expectJSON(
192192
await completeObjectList(({ index }) => Promise.resolve(index)),
193193
).toDeepEqual({
194-
data: { listField: [{ index: '0' }, { index: '1' }, { index: '2' }] },
194+
singleResult: {
195+
data: { listField: [{ index: '0' }, { index: '1' }, { index: '2' }] },
196+
},
195197
});
196198
});
197199

@@ -204,14 +206,16 @@ describe('Execute: Accepts async iterables as list value', () => {
204206
return Promise.resolve(index);
205207
}),
206208
).toDeepEqual({
207-
data: { listField: [{ index: '0' }, { index: '1' }, { index: null }] },
208-
errors: [
209-
{
210-
message: 'bad',
211-
locations: [{ line: 1, column: 15 }],
212-
path: ['listField', 2, 'index'],
213-
},
214-
],
209+
singleResult: {
210+
data: { listField: [{ index: '0' }, { index: '1' }, { index: null }] },
211+
errors: [
212+
{
213+
message: 'bad',
214+
locations: [{ line: 1, column: 15 }],
215+
path: ['listField', 2, 'index'],
216+
},
217+
],
218+
},
215219
});
216220
});
217221
it('Handles nulls yielded by async generator', async () => {

src/execution/__tests__/mutations-test.ts

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

7-
import { isAsyncIterable } from '../../jsutils/isAsyncIterable';
8-
97
import { parse } from '../../language/parser';
108

119
import { GraphQLObjectType } from '../../type/definition';
1210
import { GraphQLInt } from '../../type/scalars';
1311
import { GraphQLSchema } from '../../type/schema';
1412

15-
import { execute, executeSync } from '../execute';
13+
import {
14+
execute,
15+
executeSync,
16+
experimentalExecuteIncrementally,
17+
} from '../execute';
1618

1719
class NumberHolder {
1820
theNumber: number;
@@ -216,15 +218,16 @@ describe('Execute: Handles mutation execution ordering', () => {
216218
`);
217219

218220
const rootValue = new Root(6);
219-
const mutationResult = await execute({
221+
const mutationResult = await experimentalExecuteIncrementally({
220222
schema,
221223
document,
222224
rootValue,
223225
});
224226
const patches = [];
225227

226-
assert(isAsyncIterable(mutationResult));
227-
for await (const patch of mutationResult) {
228+
assert('initialResult' in mutationResult);
229+
patches.push(mutationResult.initialResult);
230+
for await (const patch of mutationResult.subsequentResults) {
228231
patches.push(patch);
229232
}
230233

@@ -291,15 +294,16 @@ describe('Execute: Handles mutation execution ordering', () => {
291294
`);
292295

293296
const rootValue = new Root(6);
294-
const mutationResult = await execute({
297+
const mutationResult = await experimentalExecuteIncrementally({
295298
schema,
296299
document,
297300
rootValue,
298301
});
299302
const patches = [];
300303

301-
assert(isAsyncIterable(mutationResult));
302-
for await (const patch of mutationResult) {
304+
assert('initialResult' in mutationResult);
305+
patches.push(mutationResult.initialResult);
306+
for await (const patch of mutationResult.subsequentResults) {
303307
patches.push(patch);
304308
}
305309

src/execution/__tests__/nonnull-test.ts

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

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

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

1919
const syncError = new Error('sync');
@@ -111,9 +111,7 @@ const schema = buildSchema(`
111111
function executeQuery(
112112
query: string,
113113
rootValue: unknown,
114-
): PromiseOrValue<
115-
ExecutionResult | AsyncGenerator<AsyncExecutionResult, void, void>
116-
> {
114+
): PromiseOrValue<ExecutionResult> {
117115
return execute({ schema, document: parse(query), rootValue });
118116
}
119117

src/execution/__tests__/simplePubSub.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class SimplePubSub<T> {
5151
emptyQueue();
5252
return Promise.resolve({ value: undefined, done: true });
5353
},
54+
/* c8 ignore next 4 */
5455
throw(error: unknown) {
5556
emptyQueue();
5657
return Promise.reject(error);

0 commit comments

Comments
 (0)