Skip to content

Commit f6a2de2

Browse files
authored
RFC: Synchronous execution (#1115)
* RFC: Synchronous execution **This is a breaking change. Existing uses of execute() outside of async functions which assume a promise response will need to wrap in Promise.resolve()** This allows `execute()` to return synchronously if all fields it encounters have synchronous resolvers. Notable this is the case for client-side introspection, querying from a cache, and other useful cases. Note that the top level `graphql()` function remains a Promise to minimize the breaking change, and that `validate()` has always been synchronous. * Include graphqlSync top level function * Include tests for graphqlSync
1 parent f59f44a commit f6a2de2

File tree

4 files changed

+279
-70
lines changed

4 files changed

+279
-70
lines changed

src/execution/__tests__/sync-test.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { expect } from 'chai';
9+
import { describe, it } from 'mocha';
10+
import { graphqlSync } from '../../graphql';
11+
import { execute } from '../execute';
12+
import { parse } from '../../language';
13+
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from '../../type';
14+
15+
describe('Execute: synchronously when possible', () => {
16+
const schema = new GraphQLSchema({
17+
query: new GraphQLObjectType({
18+
name: 'Query',
19+
fields: {
20+
syncField: {
21+
type: GraphQLString,
22+
resolve(rootValue) {
23+
return rootValue;
24+
},
25+
},
26+
asyncField: {
27+
type: GraphQLString,
28+
async resolve(rootValue) {
29+
return await rootValue;
30+
},
31+
},
32+
},
33+
}),
34+
});
35+
36+
it('does not return a Promise for initial errors', () => {
37+
const doc = 'fragment Example on Query { syncField }';
38+
const result = execute({
39+
schema,
40+
document: parse(doc),
41+
rootValue: 'rootValue',
42+
});
43+
expect(result).to.deep.equal({
44+
errors: [
45+
{
46+
message: 'Must provide an operation.',
47+
locations: undefined,
48+
path: undefined,
49+
},
50+
],
51+
});
52+
});
53+
54+
it('does not return a Promise if fields are all synchronous', () => {
55+
const doc = 'query Example { syncField }';
56+
const result = execute({
57+
schema,
58+
document: parse(doc),
59+
rootValue: 'rootValue',
60+
});
61+
expect(result).to.deep.equal({ data: { syncField: 'rootValue' } });
62+
});
63+
64+
it('returns a Promise if any field is asynchronous', async () => {
65+
const doc = 'query Example { syncField, asyncField }';
66+
const result = execute({
67+
schema,
68+
document: parse(doc),
69+
rootValue: 'rootValue',
70+
});
71+
expect(result).to.be.instanceOf(Promise);
72+
expect(await result).to.deep.equal({
73+
data: { syncField: 'rootValue', asyncField: 'rootValue' },
74+
});
75+
});
76+
77+
describe('graphqlSync', () => {
78+
it('does not return a Promise for syntax errors', () => {
79+
const doc = 'fragment Example on Query { { { syncField }';
80+
const result = graphqlSync({
81+
schema,
82+
source: doc,
83+
});
84+
expect(result).to.containSubset({
85+
errors: [
86+
{
87+
message:
88+
'Syntax Error GraphQL request (1:29) Expected Name, found {\n\n' +
89+
'1: fragment Example on Query { { { syncField }\n' +
90+
' ^\n',
91+
locations: [{ line: 1, column: 29 }],
92+
},
93+
],
94+
});
95+
});
96+
97+
it('does not return a Promise for validation errors', () => {
98+
const doc = 'fragment Example on Query { unknownField }';
99+
const result = graphqlSync({
100+
schema,
101+
source: doc,
102+
});
103+
expect(result).to.containSubset({
104+
errors: [
105+
{
106+
message:
107+
'Cannot query field "unknownField" on type "Query". Did you ' +
108+
'mean "syncField" or "asyncField"?',
109+
locations: [{ line: 1, column: 29 }],
110+
},
111+
],
112+
});
113+
});
114+
115+
it('does not return a Promise for sync execution', () => {
116+
const doc = 'query Example { syncField }';
117+
const result = graphqlSync({
118+
schema,
119+
source: doc,
120+
rootValue: 'rootValue',
121+
});
122+
expect(result).to.deep.equal({ data: { syncField: 'rootValue' } });
123+
});
124+
125+
it('throws if encountering async execution', () => {
126+
const doc = 'query Example { syncField, asyncField }';
127+
expect(() => {
128+
graphqlSync({
129+
schema,
130+
source: doc,
131+
rootValue: 'rootValue',
132+
});
133+
}).to.throw('GraphQL execution failed to complete synchronously.');
134+
});
135+
});
136+
});

src/execution/execute.js

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,19 @@ export type ExecutionArgs = {|
118118
/**
119119
* Implements the "Evaluating requests" section of the GraphQL specification.
120120
*
121-
* Returns a Promise that will eventually be resolved and never rejected.
121+
* Returns either a synchronous ExecutionResult (if all encountered resolvers
122+
* are synchronous), or a Promise of an ExecutionResult that will eventually be
123+
* resolved and never rejected.
122124
*
123125
* If the arguments to this function do not result in a legal execution context,
124126
* a GraphQLError will be thrown immediately explaining the invalid input.
125127
*
126128
* Accepts either an object with named arguments, or individual arguments.
127129
*/
128-
declare function execute(ExecutionArgs, ..._: []): Promise<ExecutionResult>;
130+
declare function execute(
131+
ExecutionArgs,
132+
..._: []
133+
): Promise<ExecutionResult> | ExecutionResult;
129134
/* eslint-disable no-redeclare */
130135
declare function execute(
131136
schema: GraphQLSchema,
@@ -135,7 +140,7 @@ declare function execute(
135140
variableValues?: ?{ [variable: string]: mixed },
136141
operationName?: ?string,
137142
fieldResolver?: ?GraphQLFieldResolver<any, any>,
138-
): Promise<ExecutionResult>;
143+
): Promise<ExecutionResult> | ExecutionResult;
139144
export function execute(
140145
argsOrSchema,
141146
document,
@@ -193,7 +198,7 @@ function executeImpl(
193198
fieldResolver,
194199
);
195200
} catch (error) {
196-
return Promise.resolve({ errors: [error] });
201+
return { errors: [error] };
197202
}
198203

199204
// Return a Promise that will eventually resolve to the data described by
@@ -203,12 +208,25 @@ function executeImpl(
203208
// field and its descendants will be omitted, and sibling fields will still
204209
// be executed. An execution which encounters errors will still result in a
205210
// resolved Promise.
206-
return Promise.resolve(
207-
executeOperation(context, context.operation, rootValue),
208-
).then(
209-
data =>
210-
context.errors.length === 0 ? { data } : { errors: context.errors, data },
211-
);
211+
const data = executeOperation(context, context.operation, rootValue);
212+
return buildResponse(context, data);
213+
}
214+
215+
/**
216+
* Given a completed execution context and data, build the { errors, data }
217+
* response defined by the "Response" section of the GraphQL specification.
218+
*/
219+
function buildResponse(
220+
context: ExecutionContext,
221+
data: Promise<ObjMap<mixed> | null> | ObjMap<mixed> | null,
222+
) {
223+
const promise = getPromise(data);
224+
if (promise) {
225+
return promise.then(resolved => buildResponse(context, resolved));
226+
}
227+
return context.errors.length === 0
228+
? { data }
229+
: { errors: context.errors, data };
212230
}
213231

214232
/**
@@ -333,7 +351,7 @@ function executeOperation(
333351
exeContext: ExecutionContext,
334352
operation: OperationDefinitionNode,
335353
rootValue: mixed,
336-
): ?(Promise<?ObjMap<mixed>> | ObjMap<mixed>) {
354+
): Promise<ObjMap<mixed> | null> | ObjMap<mixed> | null {
337355
const type = getOperationRootType(exeContext.schema, operation);
338356
const fields = collectFields(
339357
exeContext,

0 commit comments

Comments
 (0)