diff --git a/src/__tests__/starWarsData.js b/src/__tests__/starWarsData.js index 49f6265a72..84c1d31cda 100644 --- a/src/__tests__/starWarsData.js +++ b/src/__tests__/starWarsData.js @@ -88,13 +88,16 @@ var droidData = { * Helper function to get a character by ID. */ function getCharacter(id) { - if (humanData[id] !== undefined) { - return humanData[id]; - } - if (droidData[id] !== undefined) { - return droidData[id]; - } - return null; + // Returning a promise just to illustrate GraphQL.js's support. + return new Promise(resolve => { + if (humanData[id] !== undefined) { + return resolve(humanData[id]); + } + if (droidData[id] !== undefined) { + return resolve(droidData[id]); + } + return resolve(null); + }); } /** @@ -108,4 +111,3 @@ export var starWarsData = { Humans: humanData, Droids: droidData, }; - diff --git a/src/error/index.js b/src/error/index.js index 12be9ac57e..bb2e9d79b6 100644 --- a/src/error/index.js +++ b/src/error/index.js @@ -13,7 +13,7 @@ import { getLocation } from '../language'; import type { Node } from '../language/ast'; -export class GraphQLError { +export class GraphQLError extends Error { message: string; stack: string; nodes: ?Array; @@ -27,6 +27,7 @@ export class GraphQLError { nodes?: Array, stack?: any ) { + super(message); this.message = message; this.stack = stack || message; if (nodes) { @@ -45,7 +46,16 @@ export class GraphQLError { } } -(GraphQLError: any).prototype = Error.prototype; +export function locatedError(error: any, nodes: Array): GraphQLError { + if (error instanceof GraphQLError) { + return error; + } + return new GraphQLError( + error && error.message, + nodes, + error ? error.stack : null + ); +} export type GraphQLFormattedError = { message: string, diff --git a/src/executor/__tests__/lists.js b/src/executor/__tests__/lists.js index eeceec8a5a..5422f2a451 100644 --- a/src/executor/__tests__/lists.js +++ b/src/executor/__tests__/lists.js @@ -22,355 +22,372 @@ import { GraphQLNonNull } from '../../type'; -var data = { - list() { return [1, 2]; }, - listOfNonNull() { return [1, 2]; }, - nonNullList() { return [1, 2]; }, - nonNullListOfNonNull() { return [1, 2]; }, - listContainsNull() { return [1, null, 2]; }, - listOfNonNullContainsNull() { return [1, null, 2]; }, - nonNullListContainsNull() { return [1, null, 2]; }, - nonNullListOfNonNullContainsNull() { return [1, null, 2]; }, - listReturnsNull() { return null; }, - listOfNonNullReturnsNull() { return null; }, - nonNullListReturnsNull() { return null; }, - nonNullListOfNonNullReturnsNull() { return null; }, - nest() { - return data; - }, -}; - -var dataType = new GraphQLObjectType({ - name: 'DataType', - fields: () => ({ - list: { - type: new GraphQLList(GraphQLInt) - }, - listOfNonNull: { - type: new GraphQLList(new GraphQLNonNull(GraphQLInt)) - }, - nonNullList: { - type: new GraphQLNonNull(new GraphQLList(GraphQLInt)) - }, - nonNullListOfNonNull: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLInt))) - }, - listContainsNull: { - type: new GraphQLList(GraphQLInt) - }, - listOfNonNullContainsNull: { - type: new GraphQLList(new GraphQLNonNull(GraphQLInt)) - }, - nonNullListContainsNull: { - type: new GraphQLNonNull(new GraphQLList(GraphQLInt)) - }, - nonNullListOfNonNullContainsNull: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLInt))) - }, - listReturnsNull: { - type: new GraphQLList(GraphQLInt) - }, - listOfNonNullReturnsNull: { - type: new GraphQLList(new GraphQLNonNull(GraphQLInt)) - }, - nonNullListReturnsNull: { - type: new GraphQLNonNull(new GraphQLList(GraphQLInt)) - }, - nonNullListOfNonNullReturnsNull: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLInt))) - }, - nest: { type: dataType }, - }) -}); -var schema = new GraphQLSchema({ - query: dataType -}); +var resolve = Promise.resolve.bind(Promise); +var reject = Promise.reject.bind(Promise); + +function check(testType, testData, expected) { + return function () { + var data = { test: testData }; + + var dataType = new GraphQLObjectType({ + name: 'DataType', + fields: () => ({ + test: { type: testType }, + nest: { type: dataType, resolve: () => data }, + }) + }); + + var schema = new GraphQLSchema({ query: dataType }); + + var ast = parse('{ nest { test } }'); + + return expect(execute(schema, data, ast)).to.become(expected); + }; +} describe('Execute: Handles list nullability', () => { - it('handles lists when they return non-null values', () => { - var doc = ` - query Q { - nest { - list, - } - } - `; - - var ast = parse(doc); - - var expected = { - data: { - nest: { - list: [1,2], - } - } - }; - - return expect(execute(schema, data, ast, 'Q', {})) - .to.become(expected); - }); + describe('[T]', () => { + var type = new GraphQLList(GraphQLInt); - it('handles lists of non-nulls when they return non-null values', () => { - var doc = ` - query Q { - nest { - listOfNonNull, - } - } - `; - - var ast = parse(doc); - - var expected = { - data: { - nest: { - listOfNonNull: [1,2], - } - } - }; - - return expect(execute(schema, data, ast, 'Q', {})) - .to.become(expected); - }); + describe('Array', () => { - it('handles non-null lists of when they return non-null values', () => { - var doc = ` - query Q { - nest { - nonNullList, - } - } - `; - - var ast = parse(doc); - - var expected = { - data: { - nest: { - nonNullList: [1,2], - } - } - }; - - return expect(execute(schema, data, ast, 'Q', {})) - .to.become(expected); - }); + it('Contains values', check(type, + [1, 2], + { data: { nest: { test: [ 1, 2 ] } } } + )); - it('handles non-null lists of non-nulls when they return non-null values', () => { - var doc = ` - query Q { - nest { - nonNullListOfNonNull, - } - } - `; - - var ast = parse(doc); - - var expected = { - data: { - nest: { - nonNullListOfNonNull: [1,2], - } - } - }; - - return expect(execute(schema, data, ast, 'Q', {})) - .to.become(expected); - }); + it('Contains null', check(type, + [1, null, 2], + { data: { nest: { test: [ 1, null, 2 ] } } } + )); - it('handles lists when they return null as a value', () => { - var doc = ` - query Q { - nest { - listContainsNull, - } - } - `; - - var ast = parse(doc); - - var expected = { - data: { - nest: { - listContainsNull: [1,null,2], - } - } - }; - - return expect(execute(schema, data, ast, 'Q', {})) - .to.become(expected); - }); + it('Returns null', check(type, + null, + { data: { nest: { test: null } } } + )); - it('handles lists of non-nulls when they return null as a value', () => { - var doc = ` - query Q { - nest { - listOfNonNullContainsNull, - } - } - `; - - var ast = parse(doc); - - var expected = { - data: { - nest: { - listOfNonNullContainsNull: null - } - }, - errors: [ - { message: 'Cannot return null for non-nullable type.', - locations: [ { line: 4, column: 11 } ] } - ] - }; - - return expect(execute(schema, data, ast, 'Q', {})) - .to.become(expected); - }); + }); - it('handles non-null lists of when they return null as a value', () => { - var doc = ` - query Q { - nest { - nonNullListContainsNull, - } - } - `; - - var ast = parse(doc); - - var expected = { - data: { - nest: { - nonNullListContainsNull: [1,null,2], - } - } - }; - - return expect(execute(schema, data, ast, 'Q', {})) - .to.become(expected); - }); + describe('Promise>', () => { - it('handles non-null lists of non-nulls when they return null as a value', () => { - var doc = ` - query Q { - nest { - nonNullListOfNonNullContainsNull, - } - } - `; - - var ast = parse(doc); - - var expected = { - data: { - nest: null - }, - errors: [ - { message: 'Cannot return null for non-nullable type.', - locations: [ { line: 4, column: 11 } ] } - ] - }; - - return expect(execute(schema, data, ast, 'Q', {})) - .to.become(expected); - }); + it('Contains values', check(type, + resolve([1, 2]), + { data: { nest: { test: [ 1, 2 ] } } } + )); + + it('Contains null', check(type, + resolve([1, null, 2]), + { data: { nest: { test: [ 1, null, 2 ] } } } + )); + + + it('Returns null', check(type, + resolve(null), + { data: { nest: { test: null } } } + )); + + it('Rejected', check(type, + reject(new Error('bad')), + { data: { nest: { test: null } }, + errors: [ + { message: 'bad', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + }); + + describe('Array>', () => { + + it('Contains values', check(type, + [resolve(1), resolve(2)], + { data: { nest: { test: [ 1, 2 ] } } } + )); + + it('Contains null', check(type, + [resolve(1), resolve(null), resolve(2)], + { data: { nest: { test: [ 1, null, 2 ] } } } + )); + + it('Contains reject', check(type, + [resolve(1), reject(new Error('bad')), resolve(2)], + { data: { nest: { test: [ 1, null, 2 ] } }, + errors: [ + { message: 'bad', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + }); - it('handles lists when they return null', () => { - var doc = ` - query Q { - nest { - listReturnsNull, - } - } - `; - - var ast = parse(doc); - - var expected = { - data: { - nest: { - listReturnsNull: null - } - } - }; - - return expect(execute(schema, data, ast, 'Q', {})) - .to.become(expected); }); - it('handles lists of non-nulls when they return null', () => { - var doc = ` - query Q { - nest { - listOfNonNullReturnsNull, - } - } - `; - - var ast = parse(doc); - - var expected = { - data: { - nest: { - listOfNonNullReturnsNull: null - } - } - }; - - return expect(execute(schema, data, ast, 'Q', {})) - .to.become(expected); + describe('[T]!', () => { + var type = new GraphQLNonNull(new GraphQLList(GraphQLInt)); + + describe('Array', () => { + + it('Contains values', check(type, + [1, 2], + { data: { nest: { test: [ 1, 2 ] } } } + )); + + it('Contains null', check(type, + [1, null, 2], + { data: { nest: { test: [ 1, null, 2 ] } } } + )); + + it('Returns null', check(type, + null, + { data: { nest: null }, + errors: [ + { message: 'Cannot return null for non-nullable type.', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + }); + + describe('Promise>', () => { + + it('Contains values', check(type, + resolve([1, 2]), + { data: { nest: { test: [ 1, 2 ] } } } + )); + + it('Contains null', check(type, + resolve([1, null, 2]), + { data: { nest: { test: [ 1, null, 2 ] } } } + )); + + it('Returns null', check(type, + resolve(null), + { data: { nest: null }, + errors: [ + { message: 'Cannot return null for non-nullable type.', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + it('Rejected', check(type, + reject(new Error('bad')), + { data: { nest: null }, + errors: [ + { message: 'bad', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + }); + + describe('Array>', () => { + + it('Contains values', check(type, + [resolve(1), resolve(2)], + { data: { nest: { test: [ 1, 2 ] } } } + )); + + it('Contains null', check(type, + [resolve(1), resolve(null), resolve(2)], + { data: { nest: { test: [ 1, null, 2 ] } } } + )); + + it('Contains reject', check(type, + [resolve(1), reject(new Error('bad')), resolve(2)], + { data: { nest: { test: [ 1, null, 2 ] } }, + errors: [ + { message: 'bad', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + }); + }); - it('handles non-null lists of when they return null', () => { - var doc = ` - query Q { - nest { - nonNullListReturnsNull, - } - } - `; - - var ast = parse(doc); - - var expected = { - data: { - nest: null, - }, - errors: [ - { message: 'Cannot return null for non-nullable type.', - locations: [ { line: 4, column: 11 } ] } - ] - }; - - return expect(execute(schema, data, ast, 'Q', {})) - .to.become(expected); + describe('[T!]', () => { + var type = new GraphQLList(new GraphQLNonNull(GraphQLInt)); + + describe('Array', () => { + + it('Contains values', check(type, + [1, 2], + { data: { nest: { test: [ 1, 2 ] } } } + )); + + it('Contains null', check(type, + [1, null, 2], + { data: { nest: { test: null } }, + errors: [ + { message: 'Cannot return null for non-nullable type.', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + it('Returns null', check(type, + null, + { data: { nest: { test: null } } } + )); + + }); + + describe('Promise>', () => { + + it('Contains values', check(type, + resolve([1, 2]), + { data: { nest: { test: [ 1, 2 ] } } } + )); + + it('Contains null', check(type, + resolve([1, null, 2]), + { data: { nest: { test: null } }, + errors: [ + { message: 'Cannot return null for non-nullable type.', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + it('Returns null', check(type, + resolve(null), + { data: { nest: { test: null } } } + )); + + it('Rejected', check(type, + reject(new Error('bad')), + { data: { nest: { test: null } }, + errors: [ + { message: 'bad', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + }); + + describe('Array>', () => { + + it('Contains values', check(type, + [resolve(1), resolve(2)], + { data: { nest: { test: [ 1, 2 ] } } } + )); + + it('Contains null', check(type, + [resolve(1), resolve(null), resolve(2)], + { data: { nest: { test: null } }, + errors: [ + { message: 'Cannot return null for non-nullable type.', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + it('Contains reject', check(type, + [resolve(1), reject(new Error('bad')), resolve(2)], + { data: { nest: { test: null } }, + errors: [ + { message: 'bad', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + }); + }); - it('handles non-null lists of non-nulls when they return null', () => { - var doc = ` - query Q { - nest { - nonNullListOfNonNullReturnsNull, - } - } - `; - - var ast = parse(doc); - - var expected = { - data: { - nest: null - }, - errors: [ - { message: 'Cannot return null for non-nullable type.', - locations: [ { line: 4, column: 11 } ] } - ] - }; - - return expect(execute(schema, data, ast, 'Q', {})) - .to.become(expected); + describe('[T!]!', () => { + var type = + new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLInt))); + + describe('Array', () => { + + it('Contains values', check(type, + [1, 2], + { data: { nest: { test: [ 1, 2 ] } } } + )); + + + it('Contains null', check(type, + [1, null, 2], + { data: { nest: null }, + errors: [ + { message: 'Cannot return null for non-nullable type.', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + it('Returns null', check(type, + null, + { data: { nest: null }, + errors: [ + { message: 'Cannot return null for non-nullable type.', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + }); + + describe('Promise>', () => { + + it('Contains values', check(type, + resolve([1, 2]), + { data: { nest: { test: [ 1, 2 ] } } } + )); + + it('Contains null', check(type, + resolve([1, null, 2]), + { data: { nest: null }, + errors: [ + { message: 'Cannot return null for non-nullable type.', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + it('Returns null', check(type, + resolve(null), + { data: { nest: null }, + errors: [ + { message: 'Cannot return null for non-nullable type.', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + it('Rejected', check(type, + reject(new Error('bad')), + { data: { nest: null }, + errors: [ + { message: 'bad', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + }); + + describe('Array>', () => { + + it('Contains values', check(type, + [resolve(1), resolve(2)], + { data: { nest: { test: [ 1, 2 ] } } } + )); + + it('Contains null', check(type, + [resolve(1), resolve(null), resolve(2)], + { data: { nest: null }, + errors: [ + { message: 'Cannot return null for non-nullable type.', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + it('Contains reject', check(type, + [resolve(1), reject(new Error('bad')), resolve(2)], + { data: { nest: null }, + errors: [ + { message: 'bad', + locations: [ { line: 1, column: 10 } ] } + ] } + )); + + }); + }); + }); diff --git a/src/executor/__tests__/nonnull.js b/src/executor/__tests__/nonnull.js index 3382cae2f1..85d1daa22d 100644 --- a/src/executor/__tests__/nonnull.js +++ b/src/executor/__tests__/nonnull.js @@ -13,7 +13,6 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { execute } from '../executor'; -import { GraphQLError } from '../../error'; import { parse } from '../../language'; import { GraphQLSchema, @@ -22,10 +21,10 @@ import { GraphQLNonNull } from '../../type'; -var syncError = new GraphQLError('sync'); -var nonNullSyncError = new GraphQLError('nonNullSync'); -var promiseError = new GraphQLError('promise'); -var nonNullPromiseError = new GraphQLError('nonNullPromise'); +var syncError = new Error('sync'); +var nonNullSyncError = new Error('nonNullSync'); +var promiseError = new Error('promise'); +var nonNullPromiseError = new Error('nonNullPromise'); var throwingData = { sync() { throw syncError; }, diff --git a/src/executor/executor.js b/src/executor/executor.js index 4bd3a9d02d..15d5746ed4 100644 --- a/src/executor/executor.js +++ b/src/executor/executor.js @@ -8,7 +8,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -import { GraphQLError, formatError } from '../error'; +import { GraphQLError, locatedError, formatError } from '../error'; import type { GraphQLFormattedError } from '../error'; import invariant from '../utils/invariant'; import typeFromAST from '../utils/typeFromAST'; @@ -431,8 +431,10 @@ function getFieldEntryKey(node: Field): string { } /** - * A wrapper function for resolving the field, that catches the error - * and adds it to the context's global if the error is not rethrowable. + * Resolves the field on the given source object. In particular, this + * figures out the value that the field returns by calling its resolve function, + * then calls completeValue to complete promises, coerce scalars, or execute + * the sub-selection-set for objects. */ function resolveField( exeContext: ExecutionContext, @@ -440,60 +442,13 @@ function resolveField( source: Object, fieldASTs: Array ): any { - var fieldDef = getFieldDef(exeContext.schema, parentType, fieldASTs[0]); + var fieldAST = fieldASTs[0]; + + var fieldDef = getFieldDef(exeContext.schema, parentType, fieldAST); if (!fieldDef) { return; } - // If the field type is non-nullable, then it is resolved without any - // protection from errors. - if (fieldDef.type instanceof GraphQLNonNull) { - return resolveFieldOrError( - exeContext, - parentType, - source, - fieldASTs, - fieldDef - ); - } - - // Otherwise, error protection is applied, logging the error and resolving - // a null value for this field if one is encountered. - try { - var result = resolveFieldOrError( - exeContext, - parentType, - source, - fieldASTs, - fieldDef - ); - if (isThenable(result)) { - return result.then(undefined, error => { - exeContext.errors.push(error); - return Promise.resolve(null); - }); - } - return result; - } catch (error) { - exeContext.errors.push(error); - return null; - } -} - -/** - * Resolves the field on the given source object. In particular, this - * figures out the object that the field returns using the resolve function, - * then calls completeField to coerce scalars or execute the sub - * selection set for objects. - */ -function resolveFieldOrError( - exeContext: ExecutionContext, - parentType: GraphQLObjectType, - source: Object, - fieldASTs: Array, - fieldDef: GraphQLFieldDefinition -): any { - var fieldAST = fieldASTs[0]; var fieldType = fieldDef.type; var resolveFn = fieldDef.resolve || defaultResolveFn; @@ -506,6 +461,10 @@ function resolveFieldOrError( exeContext.variables ); + // If an error occurs while calling the field `resolve` function, ensure that + // it is wrapped as a GraphQLError with locations. Log this error and return + // null if allowed, otherwise throw the error so the parent field can handle + // it. try { var result = resolveFn( source, @@ -518,29 +477,46 @@ function resolveFieldOrError( exeContext.schema ); } catch (error) { - throw new GraphQLError(error.message, [fieldAST], error.stack); + var reportedError = new GraphQLError(error.message, fieldASTs, error.stack); + if (fieldType instanceof GraphQLNonNull) { + throw reportedError; + } + exeContext.errors.push(reportedError); + return null; } - if (isThenable(result)) { - return result.then( - resolvedResult => completeField( - exeContext, - fieldType, - fieldASTs, - resolvedResult - ), - error => Promise.reject( - new GraphQLError(error.message, [fieldAST], error.stack) - ) - ); + return completeValueCatchingError(exeContext, fieldType, fieldASTs, result); +} + +function completeValueCatchingError( + exeContext: ExecutionContext, + fieldType: GraphQLType, + fieldASTs: Array, + result: any +): any { + // If the field type is non-nullable, then it is resolved without any + // protection from errors. + if (fieldType instanceof GraphQLNonNull) { + return completeValue(exeContext, fieldType, fieldASTs, result); } - return completeField( - exeContext, - fieldType, - fieldASTs, - result - ); + // Otherwise, error protection is applied, logging the error and resolving + // a null value for this field if one is encountered. + try { + var completed = completeValue(exeContext, fieldType, fieldASTs, result); + if (isThenable(completed)) { + // Note: we don't rely on a `catch` method, but we do expect "thenable" + // to take a second callback for the error case. + return completed.then(undefined, error => { + exeContext.errors.push(error); + return Promise.resolve(null); + }); + } + return completed; + } catch (error) { + exeContext.errors.push(error); + return null; + } } /** @@ -560,16 +536,25 @@ function resolveFieldOrError( * Otherwise, the field type expects a sub-selection set, and will complete the * value by evaluating all sub-selections. */ -function completeField( +function completeValue( exeContext: ExecutionContext, fieldType: GraphQLType, fieldASTs: Array, result: any ): any { + // If result is a Promise, resolve it, if the Promise is rejected, construct + // a GraphQLError with proper locations. + if (isThenable(result)) { + return result.then( + resolved => completeValue(exeContext, fieldType, fieldASTs, resolved), + error => Promise.reject(locatedError(error, fieldASTs)) + ); + } + // If field type is NonNull, complete for inner type, and throw field error // if result is null. if (fieldType instanceof GraphQLNonNull) { - var completed = completeField( + var completed = completeValue( exeContext, fieldType.ofType, fieldASTs, @@ -591,17 +576,25 @@ function completeField( // If field type is List, complete each item in the list with the inner type if (fieldType instanceof GraphQLList) { - var itemType = fieldType.ofType; invariant( Array.isArray(result), 'User Error: expected iterable, but did not find one.' ); - return result.map(item => completeField( - exeContext, - itemType, - fieldASTs, - item - )); + + // This is specified as a simple map, however we're optimizing the path + // where the list contains no Promises by avoiding creating another Promise. + var itemType = fieldType.ofType; + var containsPromise = false; + var completedResults = result.map(item => { + var completedItem = + completeValueCatchingError(exeContext, itemType, fieldASTs, item); + if (!containsPromise && isThenable(completedItem)) { + containsPromise = true; + } + return completedItem; + }); + + return containsPromise ? Promise.all(completedResults) : completedResults; } // If field type is Scalar or Enum, coerce to a valid value, returning null