Skip to content

Commit be5244f

Browse files
committed
Allows buildASTSchema to throw errors with source locations.
1 parent 9e2e083 commit be5244f

File tree

4 files changed

+91
-15
lines changed

4 files changed

+91
-15
lines changed

src/error/syntaxError.js

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,29 @@
1111
import { getLocation } from '../language/location';
1212
import type { Source } from '../language/source';
1313
import { GraphQLError } from './GraphQLError';
14+
import type { ASTNode } from '../language/ast';
15+
16+
/**
17+
* Produces a string for formatting a syntax or validation error with an
18+
* embedded location.
19+
*/
20+
export function formatError(
21+
source: Source,
22+
position: number,
23+
message: string,
24+
description?: string,
25+
): GraphQLError {
26+
const location = getLocation(source, position);
27+
const body = `${message} (${location.line}:${location.column})` +
28+
(description ? ' ' + description : '') +
29+
'\n\n' + highlightSourceAtLocation(source, location);
30+
return new GraphQLError(
31+
body,
32+
undefined,
33+
source,
34+
[ position ]
35+
);
36+
}
1437

1538
/**
1639
* Produces a GraphQLError representing a syntax error, containing useful
@@ -21,15 +44,29 @@ export function syntaxError(
2144
position: number,
2245
description: string
2346
): GraphQLError {
24-
const location = getLocation(source, position);
25-
const error = new GraphQLError(
26-
`Syntax Error ${source.name} (${location.line}:${location.column}) ` +
27-
description + '\n\n' + highlightSourceAtLocation(source, location),
28-
undefined,
47+
return formatError(
2948
source,
30-
[ position ]
49+
position,
50+
`Syntax Error ${source.name}`,
51+
description,
3152
);
32-
return error;
53+
}
54+
55+
/**
56+
* Produces a GraphQLError for the invariant(...) function that renders the
57+
* location where a validation error occurred. If no source is passed in, it
58+
* returns an error containing the message, without context.
59+
*/
60+
export function validationError(
61+
message: string,
62+
node: ASTNode,
63+
source: ?Source
64+
): GraphQLError {
65+
const position = node.loc ? node.loc.start : null;
66+
if (position == null || source == null) {
67+
return new GraphQLError(message);
68+
}
69+
return formatError(source, position, `Validation Error: ${message}`);
3370
}
3471

3572
/**

src/jsutils/invariant.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
* of patent rights can be found in the PATENTS file in the same directory.
99
*/
1010

11-
export default function invariant(condition: mixed, message: string) {
11+
export default function invariant(condition: mixed, message: string | Error) {
1212
if (!condition) {
13+
if (message instanceof Error) {
14+
throw message;
15+
}
1316
throw new Error(message);
1417
}
1518
}

src/utilities/__tests__/buildASTSchema-test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
GraphQLSkipDirective,
1818
GraphQLIncludeDirective,
1919
GraphQLDeprecatedDirective,
20+
Source,
2021
} from '../../';
2122

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

806+
807+
it('warns with a location where validation errors occur', () => {
808+
const body = new Source(`type OutputType {
809+
value: String
810+
}
811+
812+
input InputType {
813+
value: String
814+
}
815+
816+
type Query {
817+
output: [OutputType]
818+
}
819+
820+
type Mutation {
821+
signup (password: OutputType): OutputType
822+
}`);
823+
const doc = parse(body);
824+
expect(() => buildASTSchema(doc, body))
825+
.to.throw(`GraphQLError: Validation Error: Expected Input type (14:25)
826+
827+
13: type Mutation {
828+
14: signup (password: OutputType): OutputType
829+
^
830+
15: }`);
831+
});
805832
});

src/utilities/buildASTSchema.js

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import keyValMap from '../jsutils/keyValMap';
1414
import { valueFromAST } from './valueFromAST';
1515
import { TokenKind } from '../language/lexer';
1616
import { parse } from '../language/parser';
17-
import type { Source } from '../language/source';
17+
import { Source } from '../language/source';
1818
import { getArgumentValues } from '../execution/values';
19+
import { validationError } from '../error/syntaxError';
1920

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

301305
function produceInputType(typeNode: TypeNode): GraphQLInputType {
302306
const type = produceType(typeNode);
303-
invariant(isInputType(type), 'Expected Input type.');
307+
invariant(isInputType(type),
308+
validationError('Expected Input type', typeNode, source));
304309
return (type: any);
305310
}
306311

307312
function produceOutputType(typeNode: TypeNode): GraphQLOutputType {
308313
const type = produceType(typeNode);
309-
invariant(isOutputType(type), 'Expected Output type.');
314+
invariant(isOutputType(type),
315+
validationError('Expected Output type', typeNode, source));
310316
return (type: any);
311317
}
312318

313319
function produceObjectType(typeNode: TypeNode): GraphQLObjectType {
314320
const type = produceType(typeNode);
315-
invariant(type instanceof GraphQLObjectType, 'Expected Object type.');
321+
invariant(type instanceof GraphQLObjectType,
322+
validationError('Expected Object type', typeNode, source));
316323
return type;
317324
}
318325

319326
function produceInterfaceType(typeNode: TypeNode): GraphQLInterfaceType {
320327
const type = produceType(typeNode);
321-
invariant(type instanceof GraphQLInterfaceType, 'Expected Interface type.');
328+
invariant(type instanceof GraphQLInterfaceType,
329+
validationError('Expected Interface type', typeNode, source));
322330
return type;
323331
}
324332

@@ -524,7 +532,8 @@ export function getDescription(node: { loc?: Location }): ?string {
524532
* document.
525533
*/
526534
export function buildSchema(source: string | Source): GraphQLSchema {
527-
return buildASTSchema(parse(source));
535+
const sourceObj = typeof source === 'string' ? new Source(source) : source;
536+
return buildASTSchema(parse(sourceObj), sourceObj);
528537
}
529538

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

0 commit comments

Comments
 (0)