Skip to content

Commit

Permalink
Errors thrown from resolvers have the execution path (#396)
Browse files Browse the repository at this point in the history
* Errors thrown from resolvers have the execution path

This path is also passed in the `info` object to resolvers.
This information is useful for ease of debugging and more detailed logging.

* Remove PathedError

* rename property executionPath to path

* remove an unnecessary block

* info.executionPath -> info.path

* a minor tweak to make the body of executeFields look closer to executeFieldsSerially

* remove the unnecessary clone of info

* Add a test for a path with non-nullable fields

* stylistic changes

* remove stray property
  • Loading branch information
Slava authored and leebyron committed Jun 10, 2016
1 parent 572bbda commit 6223245
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 14 deletions.
15 changes: 15 additions & 0 deletions src/__tests__/starWarsIntrospection-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ describe('Star Wars Introspection Tests', () => {
kind: 'LIST'
}
},
{
name: 'secretBackstory',
type: {
name: 'String',
kind: 'SCALAR'
}
},
{
name: 'primaryFunction',
type: {
Expand Down Expand Up @@ -284,6 +291,14 @@ describe('Star Wars Introspection Tests', () => {
}
}
},
{
name: 'secretBackstory',
type: {
name: 'String',
kind: 'SCALAR',
ofType: null
}
},
{
name: 'primaryFunction',
type: {
Expand Down
164 changes: 164 additions & 0 deletions src/__tests__/starWarsQuery-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import { expect } from 'chai';
import { describe, it } from 'mocha';
import { StarWarsSchema } from './starWarsSchema.js';
import { graphql } from '../graphql';
import {
GraphQLObjectType,
GraphQLNonNull,
GraphQLSchema,
GraphQLString,
} from '../type';

// 80+ char lines are useful in describe/it, so ignore in this file.
/* eslint-disable max-len */
Expand Down Expand Up @@ -364,4 +370,162 @@ describe('Star Wars Query Tests', () => {
expect(result).to.deep.equal({ data: expected });
});
});

describe('Reporting errors raised in resolvers', () => {
it('Correctly reports error on accessing secretBackstory', async () => {
const query = `
query HeroNameQuery {
hero {
name
secretBackstory
}
}
`;
const expected = {
hero: {
name: 'R2-D2',
secretBackstory: null
}
};
const expectedErrors = [ 'secretBackstory is secret.' ];
const result = await graphql(StarWarsSchema, query);
expect(result.data).to.deep.equal(expected);
expect(result.errors.map(e => e.message)).to.deep.equal(expectedErrors);
expect(
result.errors.map(e => e.path)).to.deep.equal(
[ [ 'hero', 'secretBackstory' ] ]);
});

it('Correctly reports error on accessing secretBackstory in a list', async () => {
const query = `
query HeroNameQuery {
hero {
name
friends {
name
secretBackstory
}
}
}
`;
const expected = {
hero: {
name: 'R2-D2',
friends: [
{
name: 'Luke Skywalker',
secretBackstory: null,
},
{
name: 'Han Solo',
secretBackstory: null,
},
{
name: 'Leia Organa',
secretBackstory: null,
},
]
}
};
const expectedErrors = [
'secretBackstory is secret.',
'secretBackstory is secret.',
'secretBackstory is secret.',
];
const result = await graphql(StarWarsSchema, query);
expect(result.data).to.deep.equal(expected);
expect(result.errors.map(e => e.message)).to.deep.equal(expectedErrors);
expect(
result.errors.map(e => e.path)
).to.deep.equal(
[
[ 'hero', 'friends', 0, 'secretBackstory' ],
[ 'hero', 'friends', 1, 'secretBackstory' ],
[ 'hero', 'friends', 2, 'secretBackstory' ],
]);
});

it('Correctly reports error on accessing through an alias', async () => {
const query = `
query HeroNameQuery {
mainHero: hero {
name
story: secretBackstory
}
}
`;
const expected = {
mainHero: {
name: 'R2-D2',
story: null,
}
};
const expectedErrors = [
'secretBackstory is secret.',
];
const result = await graphql(StarWarsSchema, query);
expect(result.data).to.deep.equal(expected);
expect(result.errors.map(e => e.message)).to.deep.equal(expectedErrors);
expect(
result.errors.map(e => e.path)
).to.deep.equal([ [ 'mainHero', 'story' ] ]);
});

it('Full response path is included when fields are non-nullable', async () => {
const A = new GraphQLObjectType({
name: 'A',
fields: () => ({
nullableA: {
type: A,
resolve: () => ({}),
},
nonNullA: {
type: new GraphQLNonNull(A),
resolve: () => ({}),
},
throws: {
type: new GraphQLNonNull(GraphQLString),
resolve: () => { throw new Error('Catch me if you can'); },
},
}),
});
const queryType = new GraphQLObjectType({
name: 'query',
fields: () => ({
nullableA: {
type: A,
resolve: () => ({})
}
}),
});
const schema = new GraphQLSchema({
query: queryType,
});

const query = `
query {
nullableA {
nullableA {
nonNullA {
nonNullA {
throws
}
}
}
}
}
`;

const result = await graphql(schema, query);
const expected = {
nullableA: {
nullableA: null
}
};
expect(result.data).to.deep.equal(expected);
expect(
result.errors.map(e => e.path)).to.deep.equal(
[ [ 'nullableA', 'nullableA', 'nonNullA', 'nonNullA', 'throws' ] ]);
});
});
});
21 changes: 21 additions & 0 deletions src/__tests__/starWarsSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const episodeEnum = new GraphQLEnumType({
* name: String
* friends: [Character]
* appearsIn: [Episode]
* secretBackstory: String
* }
*/
const characterInterface = new GraphQLInterfaceType({
Expand All @@ -125,6 +126,10 @@ const characterInterface = new GraphQLInterfaceType({
type: new GraphQLList(episodeEnum),
description: 'Which movies they appear in.',
},
secretBackstory: {
type: GraphQLString,
description: 'All secrets about their past.',
},
}),
resolveType: character => {
return getHuman(character.id) ? humanType : droidType;
Expand All @@ -140,6 +145,7 @@ const characterInterface = new GraphQLInterfaceType({
* name: String
* friends: [Character]
* appearsIn: [Episode]
* secretBackstory: String
* }
*/
const humanType = new GraphQLObjectType({
Expand Down Expand Up @@ -168,6 +174,13 @@ const humanType = new GraphQLObjectType({
type: GraphQLString,
description: 'The home planet of the human, or null if unknown.',
},
secretBackstory: {
type: GraphQLString,
description: 'Where are they from and how they came to be who they are.',
resolve: () => {
throw new Error('secretBackstory is secret.');
},
},
}),
interfaces: [ characterInterface ]
});
Expand All @@ -181,6 +194,7 @@ const humanType = new GraphQLObjectType({
* name: String
* friends: [Character]
* appearsIn: [Episode]
* secretBackstory: String
* primaryFunction: String
* }
*/
Expand All @@ -206,6 +220,13 @@ const droidType = new GraphQLObjectType({
type: new GraphQLList(episodeEnum),
description: 'Which movies they appear in.',
},
secretBackstory: {
type: GraphQLString,
description: 'Construction date and the name of the designer.',
resolve: () => {
throw new Error('secretBackstory is secret.');
},
},
primaryFunction: {
type: GraphQLString,
description: 'The primary function of the droid.',
Expand Down
1 change: 1 addition & 0 deletions src/error/GraphQLError.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class GraphQLError extends Error {
source: Source;
positions: Array<number>;
locations: any;
path: Array<string | number>;
originalError: ?Error;

constructor(
Expand Down
4 changes: 3 additions & 1 deletion src/error/locatedError.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import { GraphQLError } from './GraphQLError';
*/
export function locatedError(
originalError: ?Error,
nodes: Array<any>
nodes: Array<any>,
path: Array<string | number>
): GraphQLError {
const message = originalError ?
originalError.message || String(originalError) :
'An unknown error occurred.';
const stack = originalError ? originalError.stack : null;
const error = new GraphQLError(message, nodes, stack);
error.originalError = originalError;
error.path = path;
return error;
}
Loading

0 comments on commit 6223245

Please sign in to comment.