Skip to content

Commit

Permalink
Allows buildASTSchema to throw errors with source locations.
Browse files Browse the repository at this point in the history
  • Loading branch information
tcr committed Mar 20, 2017
1 parent 9e2e083 commit d86f752
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 15 deletions.
50 changes: 43 additions & 7 deletions src/error/syntaxError.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@
import { getLocation } from '../language/location';
import type { Source } from '../language/source';
import { GraphQLError } from './GraphQLError';
import type { ASTNode } from '../language/ast';

/**
*
*/
export function formatError(
source: Source,
position: number,
message: string,
description?: string,
): GraphQLError {
const location = getLocation(source, position);
const body = `${message} (${location.line}:${location.column})` +
(description ? ' ' + description : '') +
'\n\n' + highlightSourceAtLocation(source, location);
return new GraphQLError(
body,
undefined,
source,
[ position ]
);
}

/**
* Produces a GraphQLError representing a syntax error, containing useful
Expand All @@ -21,15 +43,29 @@ export function syntaxError(
position: number,
description: string
): GraphQLError {
const location = getLocation(source, position);
const error = new GraphQLError(
`Syntax Error ${source.name} (${location.line}:${location.column}) ` +
description + '\n\n' + highlightSourceAtLocation(source, location),
undefined,
return formatError(
source,
[ position ]
position,
`Syntax Error ${source.name}`,
description,
);
return error;
}

/**
* Produces a string for the invariant(...) function that renders the location
* where an error occurred. If no source is passed in, it renders the message
* without context.
*/
export function validationError(
message: string,
node: ASTNode,
source: ?Source
): GraphQLError {
const position = node.loc ? node.loc.start : null;
if (position == null || source == null) {
return new GraphQLError(message);
}
return formatError(source, position, message);
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/jsutils/invariant.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/

export default function invariant(condition: mixed, message: string) {
export default function invariant(condition: mixed, message: string | Error) {
if (!condition) {
if (message instanceof Error) {
throw message;
}
throw new Error(message);
}
}
27 changes: 27 additions & 0 deletions src/utilities/__tests__/buildASTSchema-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
GraphQLSkipDirective,
GraphQLIncludeDirective,
GraphQLDeprecatedDirective,
Source,
} from '../../';

/**
Expand Down Expand Up @@ -802,4 +803,30 @@ fragment Foo on Type { field }
.to.throw('Specified query type "Foo" not found in document.');
});


it('warns with a location where validation errors occur', () => {
const body = new Source(`type OutputType {
value: String
}
input InputType {
value: String
}
type Query {
output: [OutputType]
}
type Mutation {
signup (password: OutputType): OutputType
}`);
const doc = parse(body);
expect(() => buildASTSchema(doc, body))
.to.throw(`GraphQLError: Expected Input type (14:25)
13: type Mutation {
14: signup (password: OutputType): OutputType
^
15: }`);
});
});
23 changes: 16 additions & 7 deletions src/utilities/buildASTSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import keyValMap from '../jsutils/keyValMap';
import { valueFromAST } from './valueFromAST';
import { TokenKind } from '../language/lexer';
import { parse } from '../language/parser';
import type { Source } from '../language/source';
import { Source } from '../language/source';
import { getArgumentValues } from '../execution/values';
import { validationError } from '../error/syntaxError';

import {
LIST_TYPE,
Expand Down Expand Up @@ -135,7 +136,10 @@ function getNamedTypeNode(typeNode: TypeNode): NamedTypeNode {
* Given that AST it constructs a GraphQLSchema. The resulting schema
* has no resolve methods, so execution will use default resolvers.
*/
export function buildASTSchema(ast: DocumentNode): GraphQLSchema {
export function buildASTSchema(
ast: DocumentNode,
source?: Source
): GraphQLSchema {
if (!ast || ast.kind !== DOCUMENT) {
throw new Error('Must provide a document ast.');
}
Expand Down Expand Up @@ -300,25 +304,29 @@ export function buildASTSchema(ast: DocumentNode): GraphQLSchema {

function produceInputType(typeNode: TypeNode): GraphQLInputType {
const type = produceType(typeNode);
invariant(isInputType(type), 'Expected Input type.');
invariant(isInputType(type),
validationError('Expected Input type', typeNode, source));
return (type: any);
}

function produceOutputType(typeNode: TypeNode): GraphQLOutputType {
const type = produceType(typeNode);
invariant(isOutputType(type), 'Expected Output type.');
invariant(isOutputType(type),
validationError('Expected Output type', typeNode, source));
return (type: any);
}

function produceObjectType(typeNode: TypeNode): GraphQLObjectType {
const type = produceType(typeNode);
invariant(type instanceof GraphQLObjectType, 'Expected Object type.');
invariant(type instanceof GraphQLObjectType,
validationError('Expected Object type', typeNode, source));
return type;
}

function produceInterfaceType(typeNode: TypeNode): GraphQLInterfaceType {
const type = produceType(typeNode);
invariant(type instanceof GraphQLInterfaceType, 'Expected Interface type.');
invariant(type instanceof GraphQLInterfaceType,
validationError('Expected Interface type', typeNode, source));
return type;
}

Expand Down Expand Up @@ -524,7 +532,8 @@ export function getDescription(node: { loc?: Location }): ?string {
* document.
*/
export function buildSchema(source: string | Source): GraphQLSchema {
return buildASTSchema(parse(source));
const sourceObj = typeof source === 'string' ? new Source(source) : source;
return buildASTSchema(parse(sourceObj), sourceObj);
}

// Count the number of spaces on the starting side of a string.
Expand Down

0 comments on commit d86f752

Please sign in to comment.