diff --git a/src/execution/__tests__/sync-test.js b/src/execution/__tests__/sync-test.js new file mode 100644 index 0000000000..0c5b466107 --- /dev/null +++ b/src/execution/__tests__/sync-test.js @@ -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.'); + }); + }); +}); diff --git a/src/execution/execute.js b/src/execution/execute.js index 7c82e048c9..ebe246fb5c 100644 --- a/src/execution/execute.js +++ b/src/execution/execute.js @@ -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; +declare function execute( + ExecutionArgs, + ..._: [] +): Promise | ExecutionResult; /* eslint-disable no-redeclare */ declare function execute( schema: GraphQLSchema, @@ -135,7 +140,7 @@ declare function execute( variableValues?: ?{ [variable: string]: mixed }, operationName?: ?string, fieldResolver?: ?GraphQLFieldResolver, -): Promise; +): Promise | ExecutionResult; export function execute( argsOrSchema, document, @@ -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 @@ -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 | null> | ObjMap | null, +) { + const promise = getPromise(data); + if (promise) { + return promise.then(resolved => buildResponse(context, resolved)); + } + return context.errors.length === 0 + ? { data } + : { errors: context.errors, data }; } /** @@ -333,7 +351,7 @@ function executeOperation( exeContext: ExecutionContext, operation: OperationDefinitionNode, rootValue: mixed, -): ?(Promise> | ObjMap) { +): Promise | null> | ObjMap | null { const type = getOperationRootType(exeContext.schema, operation); const fields = collectFields( exeContext, diff --git a/src/graphql.js b/src/graphql.js index a1d5492a7d..a98680580e 100644 --- a/src/graphql.js +++ b/src/graphql.js @@ -46,18 +46,16 @@ import type { ExecutionResult } from './execution/execute'; * If not provided, the default field resolver is used (which looks for a * value or method on the source value with the field's name). */ -declare function graphql( - {| - schema: GraphQLSchema, - source: string | Source, - rootValue?: mixed, - contextValue?: mixed, - variableValues?: ?ObjMap, - operationName?: ?string, - fieldResolver?: ?GraphQLFieldResolver, - |}, - ..._: [] -): Promise; +export type GraphQLArgs = {| + schema: GraphQLSchema, + source: string | Source, + rootValue?: mixed, + contextValue?: mixed, + variableValues?: ?ObjMap, + operationName?: ?string, + fieldResolver?: ?GraphQLFieldResolver, +|}; +declare function graphql(GraphQLArgs, ..._: []): Promise; /* eslint-disable no-redeclare */ declare function graphql( schema: GraphQLSchema, @@ -76,27 +74,88 @@ export function graphql( variableValues, operationName, fieldResolver, +) { + // Always return a Promise for a consistent API. + return new Promise(resolve => + resolve( + // Extract arguments from object args if provided. + arguments.length === 1 + ? graphqlImpl( + argsOrSchema.schema, + argsOrSchema.source, + argsOrSchema.rootValue, + argsOrSchema.contextValue, + argsOrSchema.variableValues, + argsOrSchema.operationName, + argsOrSchema.fieldResolver, + ) + : graphqlImpl( + argsOrSchema, + source, + rootValue, + contextValue, + variableValues, + operationName, + fieldResolver, + ), + ), + ); +} + +/** + * The graphqlSync function also fulfills GraphQL operations by parsing, + * validating, and executing a GraphQL document along side a GraphQL schema. + * However, it guarantees to complete synchronously (or throw an error) assuming + * that all field resolvers are also synchronous. + */ +declare function graphqlSync(GraphQLArgs, ..._: []): ExecutionResult; +/* eslint-disable no-redeclare */ +declare function graphqlSync( + schema: GraphQLSchema, + source: Source | string, + rootValue?: mixed, + contextValue?: mixed, + variableValues?: ?ObjMap, + operationName?: ?string, + fieldResolver?: ?GraphQLFieldResolver, +): ExecutionResult; +export function graphqlSync( + argsOrSchema, + source, + rootValue, + contextValue, + variableValues, + operationName, + fieldResolver, ) { // Extract arguments from object args if provided. - return arguments.length === 1 - ? graphqlImpl( - argsOrSchema.schema, - argsOrSchema.source, - argsOrSchema.rootValue, - argsOrSchema.contextValue, - argsOrSchema.variableValues, - argsOrSchema.operationName, - argsOrSchema.fieldResolver, - ) - : graphqlImpl( - argsOrSchema, - source, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - ); + const result = + arguments.length === 1 + ? graphqlImpl( + argsOrSchema.schema, + argsOrSchema.source, + argsOrSchema.rootValue, + argsOrSchema.contextValue, + argsOrSchema.variableValues, + argsOrSchema.operationName, + argsOrSchema.fieldResolver, + ) + : graphqlImpl( + argsOrSchema, + source, + rootValue, + contextValue, + variableValues, + operationName, + fieldResolver, + ); + + // Assert that the execution was synchronous. + if (result.then) { + throw new Error('GraphQL execution failed to complete synchronously.'); + } + + return result; } function graphqlImpl( @@ -107,33 +166,29 @@ function graphqlImpl( variableValues, operationName, fieldResolver, -) { - return new Promise(resolve => { - // Parse - let document; - try { - document = parse(source); - } catch (syntaxError) { - return resolve({ errors: [syntaxError] }); - } +): Promise | ExecutionResult { + // Parse + let document; + try { + document = parse(source); + } catch (syntaxError) { + return { errors: [syntaxError] }; + } - // Validate - const validationErrors = validate(schema, document); - if (validationErrors.length > 0) { - return resolve({ errors: validationErrors }); - } + // Validate + const validationErrors = validate(schema, document); + if (validationErrors.length > 0) { + return { errors: validationErrors }; + } - // Execute - resolve( - execute( - schema, - document, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - ), - ); - }); + // Execute + return execute( + schema, + document, + rootValue, + contextValue, + variableValues, + operationName, + fieldResolver, + ); } diff --git a/src/index.js b/src/index.js index 8bab21b217..b4d7a79095 100644 --- a/src/index.js +++ b/src/index.js @@ -32,7 +32,7 @@ */ // The primary entry point into fulfilling a GraphQL request. -export { graphql } from './graphql'; +export { graphql, graphqlSync } from './graphql'; // Create and operate on GraphQL type definitions and schema. export {