Skip to content

Commit 4339864

Browse files
skevyIvanGoncharov
authored andcommitted
Validation: Allow to limit maximum number of validation errors (#2074)
* [validation] Add "onError" option to allow for custom error handling behavior when performing validation * Swithch to `maxErrors` and code cleanup
1 parent fb06d0b commit 4339864

File tree

3 files changed

+101
-10
lines changed

3 files changed

+101
-10
lines changed

src/validation/ValidationContext.js

+15-4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type VariableUsage = {|
4141
*/
4242
export class ASTValidationContext {
4343
_ast: DocumentNode;
44+
_onError: ?(err: GraphQLError) => void;
4445
_errors: Array<GraphQLError>;
4546
_fragments: ?ObjMap<FragmentDefinitionNode>;
4647
_fragmentSpreads: Map<SelectionSetNode, $ReadOnlyArray<FragmentSpreadNode>>;
@@ -49,18 +50,23 @@ export class ASTValidationContext {
4950
$ReadOnlyArray<FragmentDefinitionNode>,
5051
>;
5152

52-
constructor(ast: DocumentNode): void {
53+
constructor(ast: DocumentNode, onError?: (err: GraphQLError) => void): void {
5354
this._ast = ast;
5455
this._errors = [];
5556
this._fragments = undefined;
5657
this._fragmentSpreads = new Map();
5758
this._recursivelyReferencedFragments = new Map();
59+
this._onError = onError;
5860
}
5961

6062
reportError(error: GraphQLError): void {
6163
this._errors.push(error);
64+
if (this._onError) {
65+
this._onError(error);
66+
}
6267
}
6368

69+
// @deprecated: use onError callback instead - will be removed in v15.
6470
getErrors(): $ReadOnlyArray<GraphQLError> {
6571
return this._errors;
6672
}
@@ -140,8 +146,12 @@ export type ASTValidationRule = ASTValidationContext => ASTVisitor;
140146
export class SDLValidationContext extends ASTValidationContext {
141147
_schema: ?GraphQLSchema;
142148

143-
constructor(ast: DocumentNode, schema?: ?GraphQLSchema): void {
144-
super(ast);
149+
constructor(
150+
ast: DocumentNode,
151+
schema: ?GraphQLSchema,
152+
onError: (err: GraphQLError) => void,
153+
): void {
154+
super(ast, onError);
145155
this._schema = schema;
146156
}
147157

@@ -165,8 +175,9 @@ export class ValidationContext extends ASTValidationContext {
165175
schema: GraphQLSchema,
166176
ast: DocumentNode,
167177
typeInfo: TypeInfo,
178+
onError?: (err: GraphQLError) => void,
168179
): void {
169-
super(ast);
180+
super(ast, onError);
170181
this._schema = schema;
171182
this._typeInfo = typeInfo;
172183
this._variableUsages = new Map();

src/validation/__tests__/validation-test.js

+43
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,46 @@ describe('Validate: Supports full validation', () => {
7474
]);
7575
});
7676
});
77+
78+
describe('Validate: Limit maximum number of validation errors', () => {
79+
const query = `
80+
{
81+
firstUnknownField
82+
secondUnknownField
83+
thirdUnknownField
84+
}
85+
`;
86+
const doc = parse(query, { noLocation: true });
87+
88+
function validateDocument(options) {
89+
return validate(testSchema, doc, undefined, undefined, options);
90+
}
91+
92+
function invalidFieldError(fieldName) {
93+
return {
94+
message: `Cannot query field "${fieldName}" on type "QueryRoot".`,
95+
locations: [],
96+
};
97+
}
98+
99+
it('when maxErrors is equal to number of errors', () => {
100+
const errors = validateDocument({ maxErrors: 3 });
101+
expect(errors).to.be.deep.equal([
102+
invalidFieldError('firstUnknownField'),
103+
invalidFieldError('secondUnknownField'),
104+
invalidFieldError('thirdUnknownField'),
105+
]);
106+
});
107+
108+
it('when maxErrors is less than number of errors', () => {
109+
const errors = validateDocument({ maxErrors: 2 });
110+
expect(errors).to.be.deep.equal([
111+
invalidFieldError('firstUnknownField'),
112+
invalidFieldError('secondUnknownField'),
113+
{
114+
message:
115+
'Too many validation errors, error limit reached. Validation aborted.',
116+
},
117+
]);
118+
});
119+
});

src/validation/validate.js

+43-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import devAssert from '../jsutils/devAssert';
44

5-
import { type GraphQLError } from '../error/GraphQLError';
5+
import { GraphQLError } from '../error/GraphQLError';
66

77
import { type DocumentNode } from '../language/ast';
88
import { visit, visitInParallel, visitWithTypeInfo } from '../language/visitor';
@@ -20,6 +20,8 @@ import {
2020
ValidationContext,
2121
} from './ValidationContext';
2222

23+
export const ABORT_VALIDATION = Object.freeze({});
24+
2325
/**
2426
* Implements the "Validation" section of the spec.
2527
*
@@ -41,18 +43,45 @@ export function validate(
4143
documentAST: DocumentNode,
4244
rules?: $ReadOnlyArray<ValidationRule> = specifiedRules,
4345
typeInfo?: TypeInfo = new TypeInfo(schema),
46+
options?: {| maxErrors?: number |},
4447
): $ReadOnlyArray<GraphQLError> {
4548
devAssert(documentAST, 'Must provide document');
4649
// If the schema used for validation is invalid, throw an error.
4750
assertValidSchema(schema);
4851

49-
const context = new ValidationContext(schema, documentAST, typeInfo);
52+
const abortObj = Object.freeze({});
53+
const errors = [];
54+
const maxErrors = options && options.maxErrors;
55+
const context = new ValidationContext(
56+
schema,
57+
documentAST,
58+
typeInfo,
59+
error => {
60+
if (maxErrors != null && errors.length >= maxErrors) {
61+
errors.push(
62+
new GraphQLError(
63+
'Too many validation errors, error limit reached. Validation aborted.',
64+
),
65+
);
66+
throw abortObj;
67+
}
68+
errors.push(error);
69+
},
70+
);
71+
5072
// This uses a specialized visitor which runs multiple visitors in parallel,
5173
// while maintaining the visitor skip and break API.
5274
const visitor = visitInParallel(rules.map(rule => rule(context)));
75+
5376
// Visit the whole document with each instance of all provided rules.
54-
visit(documentAST, visitWithTypeInfo(typeInfo, visitor));
55-
return context.getErrors();
77+
try {
78+
visit(documentAST, visitWithTypeInfo(typeInfo, visitor));
79+
} catch (e) {
80+
if (e !== abortObj) {
81+
throw e;
82+
}
83+
}
84+
return errors;
5685
}
5786

5887
// @internal
@@ -61,10 +90,18 @@ export function validateSDL(
6190
schemaToExtend?: ?GraphQLSchema,
6291
rules?: $ReadOnlyArray<SDLValidationRule> = specifiedSDLRules,
6392
): $ReadOnlyArray<GraphQLError> {
64-
const context = new SDLValidationContext(documentAST, schemaToExtend);
93+
const errors = [];
94+
const context = new SDLValidationContext(
95+
documentAST,
96+
schemaToExtend,
97+
error => {
98+
errors.push(error);
99+
},
100+
);
101+
65102
const visitors = rules.map(rule => rule(context));
66103
visit(documentAST, visitInParallel(visitors));
67-
return context.getErrors();
104+
return errors;
68105
}
69106

70107
/**

0 commit comments

Comments
 (0)