Skip to content

Commit 558378e

Browse files
committed
Add support for custom validators
Support extending the GraphQLCoreBackend class with custom validators by adding a function \"get_validation_rules\" that can be overridden in subclasses where needed. This change in combination with setting the default graphql backend allows for easy additions to validation rules. An example use case would be if there's a need to perform query cost or depth analysis, one can create a validator that restricts execution of the query based on it's execution cost. Of course this could also be used to remove validators if that is necessary for some use case. Resolves graphql-python#267
1 parent fe703f0 commit 558378e

File tree

2 files changed

+58
-5
lines changed

2 files changed

+58
-5
lines changed

graphql/backend/core.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
from ..execution import execute, ExecutionResult
66
from ..language.base import parse, print_ast
77
from ..language import ast
8-
from ..validation import validate
8+
from ..validation import validate, specified_rules
99
from .base import GraphQLBackend, GraphQLDocument
1010

1111
# Necessary for static type checking
1212
if False: # flake8: noqa
13-
from typing import Any, Optional, Union
13+
from typing import Any, Optional, Union, List, Type
14+
from ..validation.rules import ValidationRule
1415
from ..language.ast import Document
1516
from ..type.schema import GraphQLSchema
1617
from rx import Observable
@@ -24,8 +25,9 @@ def execute_and_validate(
2425
):
2526
# type: (...) -> Union[ExecutionResult, Observable]
2627
do_validation = kwargs.get("validate", True)
28+
validation_rules = kwargs.get("validation_rules", specified_rules)
2729
if do_validation:
28-
validation_errors = validate(schema, document_ast)
30+
validation_errors = validate(schema, document_ast, validation_rules)
2931
if validation_errors:
3032
return ExecutionResult(errors=validation_errors, invalid=True)
3133

@@ -38,7 +40,14 @@ class GraphQLCoreBackend(GraphQLBackend):
3840

3941
def __init__(self, executor=None):
4042
# type: (Optional[Any]) -> None
41-
self.execute_params = {"executor": executor}
43+
self.execute_params = {
44+
"executor": executor,
45+
"validation_rules": self.get_validation_rules(),
46+
}
47+
48+
def get_validation_rules(self):
49+
# type: () -> List[Type[ValidationRule]]
50+
return specified_rules
4251

4352
def document_from_string(self, schema, document_string):
4453
# type: (GraphQLSchema, Union[Document, str]) -> GraphQLDocument

graphql/backend/tests/test_core.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33
"""Tests for `graphql.backend.core` module."""
44

55
import pytest
6+
7+
from graphql import GraphQLError
68
from graphql.execution.executors.sync import SyncExecutor
9+
from graphql.validation.rules.base import ValidationRule
710

811
from ..base import GraphQLBackend, GraphQLDocument
912
from ..core import GraphQLCoreBackend
1013
from .schema import schema
1114

1215
if False:
13-
from typing import Any
16+
from pytest_mock import MockFixture
17+
from typing import Any, List, Optional, Type
18+
from graphql.language.ast import Document
1419

1520

1621
def test_core_backend():
@@ -52,3 +57,42 @@ def test_backend_can_execute_custom_executor():
5257
assert not result.errors
5358
assert result.data == {"hello": "World"}
5459
assert executor.executed
60+
61+
62+
class AlwaysFailValidator(ValidationRule):
63+
# noinspection PyPep8Naming
64+
def enter_Document(self, node, key, parent, path, ancestors):
65+
# type: (Document, Optional[Any], Optional[Any], List, List) -> None
66+
self.context.report_error(GraphQLError("Test validator failure", [node]))
67+
68+
69+
class CustomValidatorBackend(GraphQLCoreBackend):
70+
def get_validation_rules(self):
71+
# type: () -> List[Type[ValidationRule]]
72+
return [AlwaysFailValidator]
73+
74+
75+
def test_backend_custom_validators_result():
76+
# type: () -> None
77+
backend = CustomValidatorBackend()
78+
assert isinstance(backend, CustomValidatorBackend)
79+
document = backend.document_from_string(schema, "{ hello }")
80+
assert isinstance(document, GraphQLDocument)
81+
result = document.execute()
82+
assert result.errors
83+
assert len(result.errors) == 1
84+
assert result.errors[0].message == "Test validator failure"
85+
86+
87+
def test_backend_custom_validators_in_validation_args(mocker):
88+
# type: (MockFixture) -> None
89+
mocked_validate = mocker.patch("graphql.backend.core.validate")
90+
backend = CustomValidatorBackend()
91+
assert isinstance(backend, CustomValidatorBackend)
92+
document = backend.document_from_string(schema, "{ hello }")
93+
assert isinstance(document, GraphQLDocument)
94+
mocked_validate.assert_not_called()
95+
result = document.execute()
96+
mocked_validate.assert_called_once()
97+
(args, kwargs) = mocked_validate.call_args
98+
assert [AlwaysFailValidator] in args

0 commit comments

Comments
 (0)