From 1eadb9fe89e1df817b00c0643bb3a35f12111004 Mon Sep 17 00:00:00 2001 From: Adam Miskiewicz Date: Mon, 5 Aug 2019 20:05:22 -0700 Subject: [PATCH] [validation] Add "bail" option to allow for bailing early when validating a document --- src/validation/ValidationContext.js | 12 +++++++++-- src/validation/__tests__/validation-test.js | 24 +++++++++++++++++++++ src/validation/validate.js | 16 +++++++++++--- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/validation/ValidationContext.js b/src/validation/ValidationContext.js index 26400c33f1b..37107607f67 100644 --- a/src/validation/ValidationContext.js +++ b/src/validation/ValidationContext.js @@ -34,6 +34,8 @@ type VariableUsage = {| +defaultValue: ?mixed, |}; +export class ValidationBailEarlyError extends Error {} + /** * An instance of this class is passed as the "this" context to all validators, * allowing access to commonly useful contextual information from within a @@ -41,6 +43,7 @@ type VariableUsage = {| */ export class ASTValidationContext { _ast: DocumentNode; + _bail: boolean; _errors: Array; _fragments: ?ObjMap; _fragmentSpreads: Map>; @@ -49,8 +52,9 @@ export class ASTValidationContext { $ReadOnlyArray, >; - constructor(ast: DocumentNode): void { + constructor(ast: DocumentNode, bail: boolean = false): void { this._ast = ast; + this._bail = bail; this._errors = []; this._fragments = undefined; this._fragmentSpreads = new Map(); @@ -59,6 +63,9 @@ export class ASTValidationContext { reportError(error: GraphQLError): void { this._errors.push(error); + if (this._bail) { + throw new ValidationBailEarlyError(); + } } getErrors(): $ReadOnlyArray { @@ -165,8 +172,9 @@ export class ValidationContext extends ASTValidationContext { schema: GraphQLSchema, ast: DocumentNode, typeInfo: TypeInfo, + bail: boolean = false, ): void { - super(ast); + super(ast, bail); this._schema = schema; this._typeInfo = typeInfo; this._variableUsages = new Map(); diff --git a/src/validation/__tests__/validation-test.js b/src/validation/__tests__/validation-test.js index 067255e2fa3..22ab19ad0f2 100644 --- a/src/validation/__tests__/validation-test.js +++ b/src/validation/__tests__/validation-test.js @@ -74,4 +74,28 @@ describe('Validate: Supports full validation', () => { 'Cannot query field "isHousetrained" on type "Dog". Did you mean "isHousetrained"?', ]); }); + + it('exits validation early when bail option is set', () => { + const doc = parse(` + query { + cat { + name + someNonExistentField + } + dog { + name + anotherNonExistentField + } + } + `); + + // Ensure that the number of errors without the bail option is 2 + const errors = validate(testSchema, doc, specifiedRules); + expect(errors).to.be.length(2); + + const bailedErrors = validate(testSchema, doc, specifiedRules, undefined, true); + expect(bailedErrors).to.be.length(1); + const errorMessages = errors.map(err => err.message); + expect(errorMessages[0]).to.equal('Cannot query field "someNonExistentField" on type "Cat".'); + }); }); diff --git a/src/validation/validate.js b/src/validation/validate.js index 4da3e6d9c74..57da5f73a9d 100644 --- a/src/validation/validate.js +++ b/src/validation/validate.js @@ -18,6 +18,7 @@ import { type ValidationRule, SDLValidationContext, ValidationContext, + ValidationBailEarlyError, } from './ValidationContext'; /** @@ -41,17 +42,26 @@ export function validate( documentAST: DocumentNode, rules?: $ReadOnlyArray = specifiedRules, typeInfo?: TypeInfo = new TypeInfo(schema), + bail?: boolean = false, ): $ReadOnlyArray { devAssert(documentAST, 'Must provide document'); // If the schema used for validation is invalid, throw an error. assertValidSchema(schema); - const context = new ValidationContext(schema, documentAST, typeInfo); + const context = new ValidationContext(schema, documentAST, typeInfo, bail); // This uses a specialized visitor which runs multiple visitors in parallel, // while maintaining the visitor skip and break API. const visitor = visitInParallel(rules.map(rule => rule(context))); - // Visit the whole document with each instance of all provided rules. - visit(documentAST, visitWithTypeInfo(typeInfo, visitor)); + try { + // Visit the whole document with each instance of all provided rules. + visit(documentAST, visitWithTypeInfo(typeInfo, visitor)); + } catch (e) { + // If the caught error is not a `ValidationBailEarlyError`, rethrow as the + // error is fatal + if (!(e instanceof ValidationBailEarlyError)) { + throw e; + } + } return context.getErrors(); }