Skip to content

Commit

Permalink
RFC: Synchronous execution
Browse files Browse the repository at this point in the history
**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.
  • Loading branch information
leebyron committed Dec 5, 2017
1 parent f59f44a commit 393cb97
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 11 deletions.
75 changes: 75 additions & 0 deletions src/execution/__tests__/sync-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import { expect } from 'chai';
import { describe, it } from 'mocha';
import { execute } from '../execute';
import { parse } from '../../language';
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from '../../type';

describe('Execute: synchronously when possible', () => {
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
syncField: {
type: GraphQLString,
resolve(rootValue) {
return rootValue;
},
},
asyncField: {
type: GraphQLString,
async resolve(rootValue) {
return await rootValue;
},
},
},
}),
});

it('does not return a Promise for initial errors', () => {
const doc = 'fragment Example on Query { syncField }';
const result = execute({
schema,
document: parse(doc),
rootValue: 'rootValue',
});
expect(result).to.deep.equal({
errors: [
{
message: 'Must provide an operation.',
locations: undefined,
path: undefined,
},
],
});
});

it('does not return a Promise if fields are all synchronous', () => {
const doc = 'query Example { syncField }';
const result = execute({
schema,
document: parse(doc),
rootValue: 'rootValue',
});
expect(result).to.deep.equal({ data: { syncField: 'rootValue' } });
});

it('returns a Promise if any field is asynchronous', async () => {
const doc = 'query Example { syncField, asyncField }';
const result = execute({
schema,
document: parse(doc),
rootValue: 'rootValue',
});
expect(result).to.be.instanceOf(Promise);
expect(await result).to.deep.equal({
data: { syncField: 'rootValue', asyncField: 'rootValue' },
});
});
});
40 changes: 29 additions & 11 deletions src/execution/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,19 @@ export type ExecutionArgs = {|
/**
* Implements the "Evaluating requests" section of the GraphQL specification.
*
* Returns a Promise that will eventually be resolved and never rejected.
* Returns either a synchronous ExecutionResult (if all encountered resolvers
* are synchronous), or a Promise of an ExecutionResult that will eventually be
* resolved and never rejected.
*
* If the arguments to this function do not result in a legal execution context,
* a GraphQLError will be thrown immediately explaining the invalid input.
*
* Accepts either an object with named arguments, or individual arguments.
*/
declare function execute(ExecutionArgs, ..._: []): Promise<ExecutionResult>;
declare function execute(
ExecutionArgs,
..._: []
): Promise<ExecutionResult> | ExecutionResult;
/* eslint-disable no-redeclare */
declare function execute(
schema: GraphQLSchema,
Expand All @@ -135,7 +140,7 @@ declare function execute(
variableValues?: ?{ [variable: string]: mixed },
operationName?: ?string,
fieldResolver?: ?GraphQLFieldResolver<any, any>,
): Promise<ExecutionResult>;
): Promise<ExecutionResult> | ExecutionResult;
export function execute(
argsOrSchema,
document,
Expand Down Expand Up @@ -193,7 +198,7 @@ function executeImpl(
fieldResolver,
);
} catch (error) {
return Promise.resolve({ errors: [error] });
return { errors: [error] };
}

// Return a Promise that will eventually resolve to the data described by
Expand All @@ -203,12 +208,25 @@ function executeImpl(
// field and its descendants will be omitted, and sibling fields will still
// be executed. An execution which encounters errors will still result in a
// resolved Promise.
return Promise.resolve(
executeOperation(context, context.operation, rootValue),
).then(
data =>
context.errors.length === 0 ? { data } : { errors: context.errors, data },
);
const data = executeOperation(context, context.operation, rootValue);
return buildResponse(context, data);
}

/**
* Given a completed execution context and data, build the { errors, data }
* response defined by the "Response" section of the GraphQL specification.
*/
function buildResponse(
context: ExecutionContext,
data: Promise<ObjMap<mixed> | null> | ObjMap<mixed> | null,
) {
const promise = getPromise(data);
if (promise) {
return promise.then(resolved => buildResponse(context, resolved));
}
return context.errors.length === 0
? { data }
: { errors: context.errors, data };
}

/**
Expand Down Expand Up @@ -333,7 +351,7 @@ function executeOperation(
exeContext: ExecutionContext,
operation: OperationDefinitionNode,
rootValue: mixed,
): ?(Promise<?ObjMap<mixed>> | ObjMap<mixed>) {
): Promise<ObjMap<mixed> | null> | ObjMap<mixed> | null {
const type = getOperationRootType(exeContext.schema, operation);
const fields = collectFields(
exeContext,
Expand Down

0 comments on commit 393cb97

Please sign in to comment.