Skip to content
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

Allow buildASTSchema to throw errors with source locations. #722

Closed
wants to merge 4 commits into from

Conversation

tcr
Copy link
Contributor

@tcr tcr commented Feb 17, 2017

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:

const { graphql, buildSchema } = require('graphql');

const schema = buildSchema(`
  type OutputType {
    value: String
  }

  input InputType {
    value: String
  }

  type Query {
    output: [OutputType]
  }

  type Mutation {
    signup (password: OutputType): OutputType
  }
`)

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:

  • We'd have to thread the Source object through buildASTSchema in order to have content for line numbers and the source error itself. In this implementation, I made this optional so buildASTSchema just uses it as context for the error if it's provided.
  • In order to avoid recreating the Source object many times in invariantError, buildASTSchema now does so explicitly. But is this necessary?
  • This makes our invariant calls more costly, because it constructs the error message and deduces the line numbers & syntax highlighting whether or not it's thrown.
  • We have to give the syntaxError module knowledge of the AST, though with a bit of work this implementation could pass in a position instead.
  • The implementation invariantError I chose here is pretty weak, there's probably a better pattern for wrapping invariant(...) we could use.
  • Can we merge the formatting logic of syntaxError to use invariantError?
  • Lastly, I didn't apply the invariantError to all functions, just a handful. That would have to be finished before merge.

Copy link
Contributor

@asiandrummer asiandrummer left a 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.

@@ -33,6 +34,25 @@ export function syntaxError(
}

/**
* Produces a string for the invarant(...) function that renders the location
Copy link
Contributor

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

return message;
}
const location = getLocation(source, position);
return `${message} (${location.line}:${location.column}) ` +
Copy link
Contributor

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(...)

Copy link
Contributor Author

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?

@@ -518,7 +526,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;
Copy link
Contributor

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.

Copy link
Contributor

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

* where an error occurred. If no source is passed in, it renders the message
* without context.
*/
export function invariantError(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eh, maybe typeError/validationError?

@tcr tcr force-pushed the tcr-astschemasource branch 3 times, most recently from d86f752 to be5244f Compare March 20, 2017 15:50
@tcr
Copy link
Contributor Author

tcr commented Mar 20, 2017

Updated this PR:

  • This now rolls up Adds message to stack trace of GraphQLError. #718 to allow the GraphQLError test to function properly.
  • Allows invariant(...) to rethrow errors if they are passed in. This change can be reverted if we want validationError() to simply return a string.
  • Broke out formatError into its own method.

The caveat still exists that buildASTSchema requires both "Source" and "ast" to return validation errors with full source body.

@@ -77,6 +77,16 @@ export function GraphQLError( // eslint-disable-line no-redeclare
path?: ?Array<string | number>,
originalError?: ?Error
) {
// Define message so it can be captured in stack trace.
Copy link
Contributor

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?

Copy link
Contributor Author

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,
Copy link
Contributor

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.

@asiandrummer
Copy link
Contributor

This looks good to me - maybe @leebyron should have another look at this

if (!condition) {
if (message instanceof Error) {
throw message;
}
Copy link
Contributor

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() }

* Produces a string for formatting a syntax or validation error with an
* embedded location.
*/
export function formatError(
Copy link
Contributor

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(
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@leebyron
Copy link
Contributor

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?

@tcr
Copy link
Contributor Author

tcr commented May 18, 2017

Rebased—let me know if anything got lost in the transition.

@cjoudrey
Copy link

cjoudrey commented Dec 1, 2017

@tcr was your intention to replace all other cases of throw new Error with an error class that includes location of the mistake?

switch (d.kind) {
case Kind.SCHEMA_DEFINITION:
if (schemaDef) {
throw new Error('Must provide only one schema definition.');
}
schemaDef = d;
break;

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 Error with only a message is thrown.

Happy to help if you don't have time to ship this.

Copy link
Contributor

@leebyron leebyron left a 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?

validationError(
source,
typeNode,
`Expected ${String(type)} to be a GraphQL input type.`));
return type;
Copy link
Contributor

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

@@ -518,7 +526,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;
Copy link
Contributor

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

leebyron added a commit that referenced this pull request Dec 8, 2017
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.
@leebyron leebyron mentioned this pull request Dec 8, 2017
leebyron added a commit that referenced this pull request Dec 8, 2017
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.
leebyron added a commit that referenced this pull request Dec 8, 2017
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants