-
Couldn't load subscription status.
- Fork 2.1k
Allow buildASTSchema to throw errors with source locations. #722
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
fa6ee06 to
f5d48d7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tcr I think this is great. Lee and I just chatted about having a general util function for handling type errors with source location, used in extending/building the schema - and I think this is very close to (or exactly) what we need.
| return error; | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can even refactor these functions to be more generic:
export function formatError(errorType: string) { ... }
export function syntaxError() { return formatError('syntax'); }
export function validationError() { return formatError('validation'); }Not exactly like that, but you get the gist
src/error/syntaxError.js
Outdated
| if (position == null || source == null) { | ||
| return message; | ||
| } | ||
| const location = getLocation(source, position); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this can actually return new GraphQLError(...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@asiandrummer This prints strange output from the invariant function, which inlined would be:
if (!condition) {
throw new Error(new GraphQLError(...));
}
But, I could change invariant(...) to check first if its message argument is a string, and if it's actually an error, just rethrow. Then I could make validationError return a GraphQLError. Sensible?
| */ | ||
| export function buildSchema(source: string | Source): GraphQLSchema { | ||
| return buildASTSchema(parse(source)); | ||
| const sourceObj = typeof source === 'string' ? new Source(source) : source; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to avoid recreating the Source object many times in invariantError, buildASTSchema now does so explicitly. But is this necessary?
Maybe we could construct a custom function globally to do this. I don't have a good answer for this as well, but yeah, passing an argument just for invariantError (or whatever we'll end up with) seems a bit unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also note that ast nodes include a reference to the source they come from, so you shouldn't need to pass the second argument around
| * Produces a string for the invarant(...) function that renders the location | ||
| * where an error occurred. If no source is passed in, it renders the message | ||
| * without context. | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eh, maybe typeError/validationError?
d86f752 to
be5244f
Compare
|
Updated this PR:
The caveat still exists that buildASTSchema requires both "Source" and "ast" to return validation errors with full source body. |
| path?: ?Array<string | number>, | ||
| originalError?: ?Error | ||
| ) { | ||
| // Define message so it can be captured in stack trace. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry if I misunderstood, but any reason why this was moved to the top?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@asiandrummer Ah, yeah this is the rebase over #718. I needed this to add the test case for the validation error—otherwise the message reports as just "GraphQLError" and we can't meaningfully test it.
| * returns an error containing the message, without context. | ||
| */ | ||
| export function validationError( | ||
| message: string, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, but should we order the argument similar to the functions above? That probably means to move source to the top.
|
This looks good to me - maybe @leebyron should have another look at this |
| if (!condition) { | ||
| if (message instanceof Error) { | ||
| throw message; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This bit is unnecessary - no need for this change, then you can change the invariants below to just be if (!condition) { throw validationError() }
src/error/syntaxError.js
Outdated
| * Produces a string for formatting a syntax or validation error with an | ||
| * embedded location. | ||
| */ | ||
| export function formatError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have another function called formatError which does something slightly different.
How about printLocatedError?
| * location where a validation error occurred. If no source is passed in, it | ||
| * returns an error containing the message, without context. | ||
| */ | ||
| export function validationError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only used from buildASTSchema, so perhaps it should live there? - Also perhaps this should be "Schema Error" instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, in the rebased diff it is now used in buildASTSchema.js and definition.js.
|
Sorry for not seeing this sooner! This is great! There is also some discussion in #746 which might lead to even more coverage of schema validation improvements like this one. Do you mind rebasing? |
|
Rebased—let me know if anything got lost in the transition. |
|
@tcr was your intention to replace all other cases of graphql-js/src/utilities/buildASTSchema.js Lines 153 to 159 in 37717b8
For example, in the above snippet it would be great that the error that is thrown includes the location of both schema definition AST nodes. This would allow tools (like schema linters) to surface this information to the user. At the moment this isn't possible because a generic Happy to help if you don't have time to ship this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really want to have this, but perhaps we could make this more generic to apply to all schema, not just those created in buildASTSchema?
| source, | ||
| typeNode, | ||
| `Expected ${String(type)} to be a GraphQL input type.`)); | ||
| return type; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These methods are used throughout and not only in validation, so instead anywhere doing schema validation should be using isInputType and throwing the appropriate error instead of this general purpose function changing
| */ | ||
| export function buildSchema(source: string | Source): GraphQLSchema { | ||
| return buildASTSchema(parse(source)); | ||
| const sourceObj = typeof source === 'string' ? new Source(source) : source; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also note that ast nodes include a reference to the source they come from, so you shouldn't need to pass the second argument around
Lifted from / inspired by a similar change in #722, this creates a new function `printError()` (and uses it as the implementation for `GraphQLError#toString()`) which prints location information in the context of an error. This is moved from the syntax error where it used to be hard-coded, so it may now be used to format validation errors, value coercion errors, or any other error which may be associated with a location.
Lifted from / inspired by a similar change in #722, this creates a new function `printError()` (and uses it as the implementation for `GraphQLError#toString()`) which prints location information in the context of an error. This is moved from the syntax error where it used to be hard-coded, so it may now be used to format validation errors, value coercion errors, or any other error which may be associated with a location.
Lifted from / inspired by a similar change in #722, this creates a new function `printError()` (and uses it as the implementation for `GraphQLError#toString()`) which prints location information in the context of an error. This is moved from the syntax error where it used to be hard-coded, so it may now be used to format validation errors, value coercion errors, or any other error which may be associated with a location.
See #546 for context. This patch is one possible implementation of having type errors in a schema list their source location. Given the following code:
Instead of this error:
$ node schema.js /Users/trim/github/graphql-js/dist/jsutils/invariant.js:19 throw new Error(message); ^ Error: Expected Input type at invariant (/Users/trim/github/graphql-js/dist/jsutils/invariant.js:19:11) ...It would throw this:
$ node schema.js /Users/trim/github/graphql-js/dist/jsutils/invariant.js:19 throw new Error(message); ^ Error: Expected Input type (15:23) 14: type Mutation { 15: signup (password: OutputType): OutputType ^ 16: } at invariant (/Users/trim/github/graphql-js/dist/jsutils/invariant.js:19:11) ....Caveats:
Sourceobject many times ininvariantError,buildASTSchemanow does so explicitly. But is this necessary?syntaxErrormodule knowledge of the AST, though with a bit of work this implementation could pass in a position instead.invariantErrorI chose here is pretty weak, there's probably a better pattern for wrappinginvariant(...)we could use.syntaxErrorto useinvariantError?