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

[validation] Add "onError" option to allow for custom error handling behavior when performing validation #2074

Merged
merged 3 commits into from Aug 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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.',
},
]);
});
});
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