Skip to content

Commit 44ac1d9

Browse files
committed
Validation: Allow to limit maximum number of validation errors
Replicates graphql/graphql-js@4339864
1 parent 712ef84 commit 44ac1d9

File tree

4 files changed

+90
-10
lines changed

4 files changed

+90
-10
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The current version 3.0.0a2 of GraphQL-core is up-to-date
1616
with GraphQL.js version 14.4.2.
1717

1818
All parts of the API are covered by an extensive test suite
19-
of currently 1979 unit tests.
19+
of currently 1981 unit tests.
2020

2121

2222
## Documentation

src/graphql/validation/validate.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@
1212
__all__ = ["assert_valid_sdl", "assert_valid_sdl_extension", "validate", "validate_sdl"]
1313

1414

15+
class ValidationAbortedError(RuntimeError):
16+
"""Error when a validation has been aborted (error limit reached)."""
17+
18+
1519
def validate(
1620
schema: GraphQLSchema,
1721
document_ast: DocumentNode,
1822
rules: Sequence[RuleType] = None,
1923
type_info: TypeInfo = None,
24+
max_errors: int = None,
2025
) -> List[GraphQLError]:
2126
"""Implements the "Validation" section of the spec.
2227
@@ -45,13 +50,34 @@ def validate(
4550
rules = specified_rules
4651
elif not isinstance(rules, (list, tuple)):
4752
raise TypeError("Rules must be passed as a list/tuple.")
48-
context = ValidationContext(schema, document_ast, type_info)
53+
if max_errors is not None and not isinstance(max_errors, int):
54+
raise TypeError("The maximum number of errors must be passed as an int.")
55+
56+
errors: List[GraphQLError] = []
57+
58+
def on_error(error: GraphQLError) -> None:
59+
if max_errors is not None and len(errors) >= max_errors:
60+
errors.append(
61+
GraphQLError(
62+
"Too many validation errors, error limit reached."
63+
" Validation aborted."
64+
)
65+
)
66+
raise ValidationAbortedError
67+
errors.append(error)
68+
69+
context = ValidationContext(schema, document_ast, type_info, on_error)
70+
4971
# This uses a specialized visitor which runs multiple visitors in parallel,
5072
# while maintaining the visitor skip and break API.
5173
visitors = [rule(context) for rule in rules]
74+
5275
# Visit the whole document with each instance of all provided rules.
53-
visit(document_ast, TypeInfoVisitor(type_info, ParallelVisitor(visitors)))
54-
return context.errors
76+
try:
77+
visit(document_ast, TypeInfoVisitor(type_info, ParallelVisitor(visitors)))
78+
except ValidationAbortedError:
79+
pass
80+
return errors
5581

5682

5783
def validate_sdl(

src/graphql/validation/validation_context.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, List, NamedTuple, Optional, Set, Union, cast
1+
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Set, Union, cast
22

33
from ..error import GraphQLError
44
from ..language import (
@@ -62,10 +62,14 @@ class ASTValidationContext:
6262
"""
6363

6464
document: DocumentNode
65+
on_error: Optional[Callable[[GraphQLError], None]]
6566
errors: List[GraphQLError]
6667

67-
def __init__(self, ast: DocumentNode) -> None:
68+
def __init__(
69+
self, ast: DocumentNode, on_error: Callable[[GraphQLError], None] = None
70+
) -> None:
6871
self.document = ast
72+
self.on_error = on_error
6973
self.errors = []
7074
self._fragments: Optional[Dict[str, FragmentDefinitionNode]] = None
7175
self._fragment_spreads: Dict[SelectionSetNode, List[FragmentSpreadNode]] = {}
@@ -75,6 +79,8 @@ def __init__(self, ast: DocumentNode) -> None:
7579

7680
def report_error(self, error: GraphQLError):
7781
self.errors.append(error)
82+
if self.on_error:
83+
self.on_error(error)
7884

7985
def get_fragment(self, name: str) -> Optional[FragmentDefinitionNode]:
8086
fragments = self._fragments
@@ -146,8 +152,13 @@ class SDLValidationContext(ASTValidationContext):
146152

147153
schema: Optional[GraphQLSchema]
148154

149-
def __init__(self, ast: DocumentNode, schema: GraphQLSchema = None) -> None:
150-
super().__init__(ast)
155+
def __init__(
156+
self,
157+
ast: DocumentNode,
158+
schema: GraphQLSchema = None,
159+
on_error: Callable[[GraphQLError], None] = None,
160+
) -> None:
161+
super().__init__(ast, on_error)
151162
self.schema = schema
152163

153164

@@ -162,9 +173,13 @@ class ValidationContext(ASTValidationContext):
162173
schema: GraphQLSchema
163174

164175
def __init__(
165-
self, schema: GraphQLSchema, ast: DocumentNode, type_info: TypeInfo
176+
self,
177+
schema: GraphQLSchema,
178+
ast: DocumentNode,
179+
type_info: TypeInfo,
180+
on_error: Callable[[GraphQLError], None] = None,
166181
) -> None:
167-
super().__init__(ast)
182+
super().__init__(ast, on_error)
168183
self.schema = schema
169184
self._type_info = type_info
170185
self._variable_usages: Dict[NodeWithSelectionSet, List[VariableUsage]] = {}

tests/validation/test_validation.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,42 @@ def validates_using_a_custom_type_info():
7272
"Cannot query field 'isHousetrained' on type 'Dog'."
7373
" Did you mean 'isHousetrained'?",
7474
]
75+
76+
77+
def describe_validate_limit_maximum_number_of_validation_errors():
78+
query = """
79+
{
80+
firstUnknownField
81+
secondUnknownField
82+
thirdUnknownField
83+
}
84+
"""
85+
doc = parse(query, no_location=True)
86+
87+
def _validate_document(max_errors=None):
88+
return validate(test_schema, doc, max_errors=max_errors)
89+
90+
def _invalid_field_error(field_name: str):
91+
return {
92+
"message": f"Cannot query field '{field_name}' on type 'QueryRoot'.",
93+
"locations": [],
94+
}
95+
96+
def when_max_errors_is_equal_to_number_of_errors():
97+
errors = _validate_document(max_errors=3)
98+
assert errors == [
99+
_invalid_field_error("firstUnknownField"),
100+
_invalid_field_error("secondUnknownField"),
101+
_invalid_field_error("thirdUnknownField"),
102+
]
103+
104+
def when_max_errors_is_less_than_number_of_errors():
105+
errors = _validate_document(max_errors=2)
106+
assert errors == [
107+
_invalid_field_error("firstUnknownField"),
108+
_invalid_field_error("secondUnknownField"),
109+
{
110+
"message": "Too many validation errors, error limit reached."
111+
" Validation aborted."
112+
},
113+
]

0 commit comments

Comments
 (0)