Skip to content

Commit

Permalink
Validation: Allow to limit maximum number of validation errors (#2074)
Browse files Browse the repository at this point in the history
* [validation] Add "onError" option to allow for custom error handling behavior when performing validation

* Swithch to `maxErrors` and code cleanup
  • Loading branch information
skevy authored and IvanGoncharov committed Aug 20, 2019
1 parent fb06d0b commit 4339864
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 10 deletions.
19 changes: 15 additions & 4 deletions src/validation/ValidationContext.js
Expand Up @@ -41,6 +41,7 @@ type VariableUsage = {|
*/
export class ASTValidationContext {
_ast: DocumentNode;
_onError: ?(err: GraphQLError) => void;
_errors: Array<GraphQLError>;
_fragments: ?ObjMap<FragmentDefinitionNode>;
_fragmentSpreads: Map<SelectionSetNode, $ReadOnlyArray<FragmentSpreadNode>>;
Expand All @@ -49,18 +50,23 @@ export class ASTValidationContext {
$ReadOnlyArray<FragmentDefinitionNode>,
>;

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<GraphQLError> {
return this._errors;
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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();
Expand Down
43 changes: 43 additions & 0 deletions src/validation/__tests__/validation-test.js
Expand Up @@ -74,3 +74,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.',
},
]);
});
});
49 changes: 43 additions & 6 deletions src/validation/validate.js
Expand Up @@ -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';
Expand All @@ -20,6 +20,8 @@ import {
ValidationContext,
} from './ValidationContext';

export const ABORT_VALIDATION = Object.freeze({});

/**
* Implements the "Validation" section of the spec.
*
Expand All @@ -41,18 +43,45 @@ export function validate(
documentAST: DocumentNode,
rules?: $ReadOnlyArray<ValidationRule> = specifiedRules,
typeInfo?: TypeInfo = new TypeInfo(schema),
options?: {| maxErrors?: number |},
): $ReadOnlyArray<GraphQLError> {
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
Expand All @@ -61,10 +90,18 @@ export function validateSDL(
schemaToExtend?: ?GraphQLSchema,
rules?: $ReadOnlyArray<SDLValidationRule> = specifiedSDLRules,
): $ReadOnlyArray<GraphQLError> {
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;
}

/**
Expand Down

0 comments on commit 4339864

Please sign in to comment.