diff --git a/src/validation/ValidationContext.js b/src/validation/ValidationContext.js index 26400c33f1..a3a7bd0f08 100644 --- a/src/validation/ValidationContext.js +++ b/src/validation/ValidationContext.js @@ -41,6 +41,7 @@ type VariableUsage = {| */ export class ASTValidationContext { _ast: DocumentNode; + _onError: ?(err: GraphQLError) => void; _errors: Array; _fragments: ?ObjMap; _fragmentSpreads: Map>; @@ -49,18 +50,23 @@ export class ASTValidationContext { $ReadOnlyArray, >; - constructor(ast: DocumentNode): void { + constructor(ast: DocumentNode, onError?: (err: GraphQLError) => void): void { this._ast = ast; this._errors = []; this._fragments = undefined; this._fragmentSpreads = new Map(); this._recursivelyReferencedFragments = new Map(); + this._onError = onError; } reportError(error: GraphQLError): void { this._errors.push(error); + if (this._onError) { + this._onError(error); + } } + // @deprecated: use onError callback instead - will be removed in v15. getErrors(): $ReadOnlyArray { return this._errors; } @@ -140,8 +146,12 @@ export type ASTValidationRule = ASTValidationContext => ASTVisitor; export class SDLValidationContext extends ASTValidationContext { _schema: ?GraphQLSchema; - constructor(ast: DocumentNode, schema?: ?GraphQLSchema): void { - super(ast); + constructor( + ast: DocumentNode, + schema: ?GraphQLSchema, + onError: (err: GraphQLError) => void, + ): void { + super(ast, onError); this._schema = schema; } @@ -165,8 +175,9 @@ export class ValidationContext extends ASTValidationContext { schema: GraphQLSchema, ast: DocumentNode, typeInfo: TypeInfo, + onError?: (err: GraphQLError) => void, ): void { - super(ast); + super(ast, onError); 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 067255e2fa..4b1be1f6c4 100644 --- a/src/validation/__tests__/validation-test.js +++ b/src/validation/__tests__/validation-test.js @@ -75,3 +75,46 @@ describe('Validate: Supports full validation', () => { ]); }); }); + +describe('Validate: Limit maximum number of validation errors', () => { + const query = ` + { + firstUnknownField + secondUnknownField + thirdUnknownField + } + `; + const doc = parse(query, { noLocation: true }); + + function validateDocument(options) { + return validate(testSchema, doc, undefined, undefined, options); + } + + function invalidFieldError(fieldName) { + return { + message: `Cannot query field "${fieldName}" on type "QueryRoot".`, + locations: [], + }; + } + + it('when maxErrors is equal to number of errors', () => { + const errors = validateDocument({ maxErrors: 3 }); + expect(errors).to.be.deep.equal([ + invalidFieldError('firstUnknownField'), + invalidFieldError('secondUnknownField'), + invalidFieldError('thirdUnknownField'), + ]); + }); + + it('when maxErrors is less than number of errors', () => { + const errors = validateDocument({ maxErrors: 2 }); + expect(errors).to.be.deep.equal([ + invalidFieldError('firstUnknownField'), + invalidFieldError('secondUnknownField'), + { + message: + 'Too many validation errors, error limit reached. Validation aborted.', + }, + ]); + }); +}); diff --git a/src/validation/validate.js b/src/validation/validate.js index 4da3e6d9c7..1ccdd40c59 100644 --- a/src/validation/validate.js +++ b/src/validation/validate.js @@ -2,7 +2,7 @@ import devAssert from '../jsutils/devAssert'; -import { type GraphQLError } from '../error/GraphQLError'; +import { GraphQLError } from '../error/GraphQLError'; import { type DocumentNode } from '../language/ast'; import { visit, visitInParallel, visitWithTypeInfo } from '../language/visitor'; @@ -20,6 +20,8 @@ import { ValidationContext, } from './ValidationContext'; +export const ABORT_VALIDATION = Object.freeze({}); + /** * Implements the "Validation" section of the spec. * @@ -41,18 +43,45 @@ export function validate( documentAST: DocumentNode, rules?: $ReadOnlyArray = specifiedRules, typeInfo?: TypeInfo = new TypeInfo(schema), + options?: {| maxErrors?: number |}, ): $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 abortObj = Object.freeze({}); + const errors = []; + const maxErrors = options && options.maxErrors; + const context = new ValidationContext( + schema, + documentAST, + typeInfo, + error => { + if (maxErrors != null && errors.length >= maxErrors) { + errors.push( + new GraphQLError( + 'Too many validation errors, error limit reached. Validation aborted.', + ), + ); + throw abortObj; + } + errors.push(error); + }, + ); + // 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)); - return context.getErrors(); + try { + visit(documentAST, visitWithTypeInfo(typeInfo, visitor)); + } catch (e) { + if (e !== abortObj) { + throw e; + } + } + return errors; } // @internal @@ -61,10 +90,18 @@ export function validateSDL( schemaToExtend?: ?GraphQLSchema, rules?: $ReadOnlyArray = specifiedSDLRules, ): $ReadOnlyArray { - const context = new SDLValidationContext(documentAST, schemaToExtend); + const errors = []; + const context = new SDLValidationContext( + documentAST, + schemaToExtend, + error => { + errors.push(error); + }, + ); + const visitors = rules.map(rule => rule(context)); visit(documentAST, visitInParallel(visitors)); - return context.getErrors(); + return errors; } /**