Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions src/execution/__tests__/sync-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* 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 { graphqlSync } from '../../graphql';
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' },
});
});

describe('graphqlSync', () => {
it('does not return a Promise for syntax errors', () => {
const doc = 'fragment Example on Query { { { syncField }';
const result = graphqlSync({
schema,
source: doc,
});
expect(result).to.containSubset({
errors: [
{
message:
'Syntax Error GraphQL request (1:29) Expected Name, found {\n\n' +
'1: fragment Example on Query { { { syncField }\n' +
' ^\n',
locations: [{ line: 1, column: 29 }],
},
],
});
});

it('does not return a Promise for validation errors', () => {
const doc = 'fragment Example on Query { unknownField }';
const result = graphqlSync({
schema,
source: doc,
});
expect(result).to.containSubset({
errors: [
{
message:
'Cannot query field "unknownField" on type "Query". Did you ' +
'mean "syncField" or "asyncField"?',
locations: [{ line: 1, column: 29 }],
},
],
});
});

it('does not return a Promise for sync execution', () => {
const doc = 'query Example { syncField }';
const result = graphqlSync({
schema,
source: doc,
rootValue: 'rootValue',
});
expect(result).to.deep.equal({ data: { syncField: 'rootValue' } });
});

it('throws if encountering async execution', () => {
const doc = 'query Example { syncField, asyncField }';
expect(() => {
graphqlSync({
schema,
source: doc,
rootValue: 'rootValue',
});
}).to.throw('GraphQL execution failed to complete synchronously.');
});
});
});
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
Loading