mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-06-08 15:43:09 +03:00
Handle database transactions (#1039)
* Handle Django database atomic requests * Create and handle database atomic mutations * Make code compatible with Python 2.7 * Code style * Define set_rollback instead of using the one in rest_framework.views because of backward compatibility * Implement mock.patch.dict
This commit is contained in:
parent
a51c2bffd9
commit
8f63199a63
|
@ -230,3 +230,121 @@ This argument is also sent back to the client with the mutation result
|
||||||
(you do not have to do anything). For services that manage
|
(you do not have to do anything). For services that manage
|
||||||
a pool of many GraphQL requests in bulk, the ``clientIDMutation``
|
a pool of many GraphQL requests in bulk, the ``clientIDMutation``
|
||||||
allows you to match up a specific mutation with the response.
|
allows you to match up a specific mutation with the response.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Django Database Transactions
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Django gives you a few ways to control how database transactions are managed.
|
||||||
|
|
||||||
|
Tying transactions to HTTP requests
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A common way to handle transactions in Django is to wrap each request in a transaction.
|
||||||
|
Set ``ATOMIC_REQUESTS`` settings to ``True`` in the configuration of each database for
|
||||||
|
which you want to enable this behavior.
|
||||||
|
|
||||||
|
It works like this. Before calling ``GraphQLView`` Django starts a transaction. If the
|
||||||
|
response is produced without problems, Django commits the transaction. If the view, a
|
||||||
|
``DjangoFormMutation`` or a ``DjangoModelFormMutation`` produces an exception, Django
|
||||||
|
rolls back the transaction.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
While the simplicity of this transaction model is appealing, it also makes it
|
||||||
|
inefficient when traffic increases. Opening a transaction for every request has some
|
||||||
|
overhead. The impact on performance depends on the query patterns of your application
|
||||||
|
and on how well your database handles locking.
|
||||||
|
|
||||||
|
Check the next section for a better solution.
|
||||||
|
|
||||||
|
Tying transactions to mutations
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A mutation can contain multiple fields, just like a query. There's one important
|
||||||
|
distinction between queries and mutations, other than the name:
|
||||||
|
|
||||||
|
..
|
||||||
|
|
||||||
|
`While query fields are executed in parallel, mutation fields run in series, one
|
||||||
|
after the other.`
|
||||||
|
|
||||||
|
This means that if we send two ``incrementCredits`` mutations in one request, the first
|
||||||
|
is guaranteed to finish before the second begins, ensuring that we don't end up with a
|
||||||
|
race condition with ourselves.
|
||||||
|
|
||||||
|
On the other hand, if the first ``incrementCredits`` runs successfully but the second
|
||||||
|
one does not, the operation cannot be retried as it is. That's why is a good idea to
|
||||||
|
run all mutation fields in a transaction, to guarantee all occur or nothing occurs.
|
||||||
|
|
||||||
|
To enable this behavior for all databases set the graphene ``ATOMIC_MUTATIONS`` settings
|
||||||
|
to ``True`` in your settings file:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
GRAPHENE = {
|
||||||
|
# ...
|
||||||
|
"ATOMIC_MUTATIONS": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
On the contrary, if you want to enable this behavior for a specific database, set
|
||||||
|
``ATOMIC_MUTATIONS`` to ``True`` in your database settings:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
# ...
|
||||||
|
"ATOMIC_MUTATIONS": True,
|
||||||
|
},
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
|
||||||
|
Now, given the following example mutation:
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
mutation IncreaseCreditsTwice {
|
||||||
|
|
||||||
|
increaseCredits1: increaseCredits(input: { amount: 10 }) {
|
||||||
|
balance
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
increaseCredits2: increaseCredits(input: { amount: -1 }) {
|
||||||
|
balance
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
The server is going to return something like:
|
||||||
|
|
||||||
|
.. code:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"increaseCredits1": {
|
||||||
|
"balance": 10.0,
|
||||||
|
"errors": []
|
||||||
|
},
|
||||||
|
"increaseCredits2": {
|
||||||
|
"balance": null,
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"field": "amount",
|
||||||
|
"message": "Amount should be a positive number"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
But the balance will remain the same.
|
||||||
|
|
1
graphene_django/constants.py
Normal file
1
graphene_django/constants.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
MUTATION_ERRORS_FLAG = "graphene_mutation_has_errors"
|
|
@ -11,8 +11,13 @@ from graphene.types.mutation import MutationOptions
|
||||||
# InputObjectType,
|
# InputObjectType,
|
||||||
# )
|
# )
|
||||||
from graphene.types.utils import yank_fields_from_attrs
|
from graphene.types.utils import yank_fields_from_attrs
|
||||||
|
from graphene_django.constants import MUTATION_ERRORS_FLAG
|
||||||
from graphene_django.registry import get_global_registry
|
from graphene_django.registry import get_global_registry
|
||||||
|
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
from ..types import ErrorType
|
from ..types import ErrorType
|
||||||
from .converter import convert_form_field
|
from .converter import convert_form_field
|
||||||
|
|
||||||
|
@ -46,6 +51,7 @@ class BaseDjangoFormMutation(ClientIDMutation):
|
||||||
return cls.perform_mutate(form, info)
|
return cls.perform_mutate(form, info)
|
||||||
else:
|
else:
|
||||||
errors = ErrorType.from_errors(form.errors)
|
errors = ErrorType.from_errors(form.errors)
|
||||||
|
_set_errors_flag_to_context(info)
|
||||||
|
|
||||||
return cls(errors=errors, **form.data)
|
return cls(errors=errors, **form.data)
|
||||||
|
|
||||||
|
@ -170,6 +176,7 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
|
||||||
return cls.perform_mutate(form, info)
|
return cls.perform_mutate(form, info)
|
||||||
else:
|
else:
|
||||||
errors = ErrorType.from_errors(form.errors)
|
errors = ErrorType.from_errors(form.errors)
|
||||||
|
_set_errors_flag_to_context(info)
|
||||||
|
|
||||||
return cls(errors=errors)
|
return cls(errors=errors)
|
||||||
|
|
||||||
|
@ -178,3 +185,9 @@ class DjangoModelFormMutation(BaseDjangoFormMutation):
|
||||||
obj = form.save()
|
obj = form.save()
|
||||||
kwargs = {cls._meta.return_field_name: obj}
|
kwargs = {cls._meta.return_field_name: obj}
|
||||||
return cls(errors=[], **kwargs)
|
return cls(errors=[], **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_errors_flag_to_context(info):
|
||||||
|
# This is not ideal but necessary to keep the response errors empty
|
||||||
|
if info and info.context:
|
||||||
|
setattr(info.context, MUTATION_ERRORS_FLAG, True)
|
||||||
|
|
|
@ -5,21 +5,13 @@ from py.test import raises
|
||||||
|
|
||||||
from graphene import Field, ObjectType, Schema, String
|
from graphene import Field, ObjectType, Schema, String
|
||||||
from graphene_django import DjangoObjectType
|
from graphene_django import DjangoObjectType
|
||||||
|
from graphene_django.tests.forms import PetForm
|
||||||
from graphene_django.tests.models import Pet
|
from graphene_django.tests.models import Pet
|
||||||
|
from graphene_django.tests.mutations import PetMutation
|
||||||
|
|
||||||
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
|
from ..mutation import DjangoFormMutation, DjangoModelFormMutation
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def pet_type():
|
|
||||||
class PetType(DjangoObjectType):
|
|
||||||
class Meta:
|
|
||||||
model = Pet
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
||||||
return PetType
|
|
||||||
|
|
||||||
|
|
||||||
class MyForm(forms.Form):
|
class MyForm(forms.Form):
|
||||||
text = forms.CharField()
|
text = forms.CharField()
|
||||||
|
|
||||||
|
@ -33,18 +25,6 @@ class MyForm(forms.Form):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PetForm(forms.ModelForm):
|
|
||||||
class Meta:
|
|
||||||
model = Pet
|
|
||||||
fields = "__all__"
|
|
||||||
|
|
||||||
def clean_age(self):
|
|
||||||
age = self.cleaned_data["age"]
|
|
||||||
if age >= 99:
|
|
||||||
raise ValidationError("Too old")
|
|
||||||
return age
|
|
||||||
|
|
||||||
|
|
||||||
def test_needs_form_class():
|
def test_needs_form_class():
|
||||||
with raises(Exception) as exc:
|
with raises(Exception) as exc:
|
||||||
|
|
||||||
|
@ -70,11 +50,18 @@ def test_has_input_fields():
|
||||||
assert "text" in MyMutation.Input._meta.fields
|
assert "text" in MyMutation.Input._meta.fields
|
||||||
|
|
||||||
|
|
||||||
def test_mutation_error_camelcased(pet_type, graphene_settings):
|
def test_mutation_error_camelcased(graphene_settings):
|
||||||
class ExtraPetForm(PetForm):
|
class ExtraPetForm(PetForm):
|
||||||
test_field = forms.CharField(required=True)
|
test_field = forms.CharField(required=True)
|
||||||
|
|
||||||
|
class PetType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Pet
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
pet = Field(PetType)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
form_class = ExtraPetForm
|
form_class = ExtraPetForm
|
||||||
|
|
||||||
|
@ -146,21 +133,13 @@ def test_form_valid_input():
|
||||||
assert result.data["myMutation"]["text"] == "VALID_INPUT"
|
assert result.data["myMutation"]["text"] == "VALID_INPUT"
|
||||||
|
|
||||||
|
|
||||||
def test_default_meta_fields(pet_type):
|
def test_default_meta_fields():
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
assert PetMutation._meta.model is Pet
|
assert PetMutation._meta.model is Pet
|
||||||
assert PetMutation._meta.return_field_name == "pet"
|
assert PetMutation._meta.return_field_name == "pet"
|
||||||
assert "pet" in PetMutation._meta.fields
|
assert "pet" in PetMutation._meta.fields
|
||||||
|
|
||||||
|
|
||||||
def test_default_input_meta_fields(pet_type):
|
def test_default_input_meta_fields():
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
assert PetMutation._meta.model is Pet
|
assert PetMutation._meta.model is Pet
|
||||||
assert PetMutation._meta.return_field_name == "pet"
|
assert PetMutation._meta.return_field_name == "pet"
|
||||||
assert "name" in PetMutation.Input._meta.fields
|
assert "name" in PetMutation.Input._meta.fields
|
||||||
|
@ -168,8 +147,15 @@ def test_default_input_meta_fields(pet_type):
|
||||||
assert "id" in PetMutation.Input._meta.fields
|
assert "id" in PetMutation.Input._meta.fields
|
||||||
|
|
||||||
|
|
||||||
def test_exclude_fields_input_meta_fields(pet_type):
|
def test_exclude_fields_input_meta_fields():
|
||||||
|
class PetType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Pet
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
pet = Field(PetType)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
form_class = PetForm
|
form_class = PetForm
|
||||||
exclude_fields = ["id"]
|
exclude_fields = ["id"]
|
||||||
|
@ -182,8 +168,15 @@ def test_exclude_fields_input_meta_fields(pet_type):
|
||||||
assert "id" not in PetMutation.Input._meta.fields
|
assert "id" not in PetMutation.Input._meta.fields
|
||||||
|
|
||||||
|
|
||||||
def test_custom_return_field_name(pet_type):
|
def test_custom_return_field_name():
|
||||||
|
class PetType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Pet
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
class PetMutation(DjangoModelFormMutation):
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
pet = Field(PetType)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
form_class = PetForm
|
form_class = PetForm
|
||||||
model = Pet
|
model = Pet
|
||||||
|
@ -194,13 +187,7 @@ def test_custom_return_field_name(pet_type):
|
||||||
assert "animal" in PetMutation._meta.fields
|
assert "animal" in PetMutation._meta.fields
|
||||||
|
|
||||||
|
|
||||||
def test_model_form_mutation_mutate_existing(pet_type):
|
def test_model_form_mutation_mutate_existing():
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
pet = Field(pet_type)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
class Mutation(ObjectType):
|
class Mutation(ObjectType):
|
||||||
pet_mutation = PetMutation.Field()
|
pet_mutation = PetMutation.Field()
|
||||||
|
|
||||||
|
@ -229,13 +216,7 @@ def test_model_form_mutation_mutate_existing(pet_type):
|
||||||
assert pet.name == "Mia"
|
assert pet.name == "Mia"
|
||||||
|
|
||||||
|
|
||||||
def test_model_form_mutation_creates_new(pet_type):
|
def test_model_form_mutation_creates_new():
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
pet = Field(pet_type)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
class Mutation(ObjectType):
|
class Mutation(ObjectType):
|
||||||
pet_mutation = PetMutation.Field()
|
pet_mutation = PetMutation.Field()
|
||||||
|
|
||||||
|
@ -265,13 +246,7 @@ def test_model_form_mutation_creates_new(pet_type):
|
||||||
assert pet.age == 10
|
assert pet.age == 10
|
||||||
|
|
||||||
|
|
||||||
def test_model_form_mutation_invalid_input(pet_type):
|
def test_model_form_mutation_invalid_input():
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
pet = Field(pet_type)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
class Mutation(ObjectType):
|
class Mutation(ObjectType):
|
||||||
pet_mutation = PetMutation.Field()
|
pet_mutation = PetMutation.Field()
|
||||||
|
|
||||||
|
@ -301,11 +276,7 @@ def test_model_form_mutation_invalid_input(pet_type):
|
||||||
assert Pet.objects.count() == 0
|
assert Pet.objects.count() == 0
|
||||||
|
|
||||||
|
|
||||||
def test_model_form_mutation_mutate_invalid_form(pet_type):
|
def test_model_form_mutation_mutate_invalid_form():
|
||||||
class PetMutation(DjangoModelFormMutation):
|
|
||||||
class Meta:
|
|
||||||
form_class = PetForm
|
|
||||||
|
|
||||||
result = PetMutation.mutate_and_get_payload(None, None)
|
result = PetMutation.mutate_and_get_payload(None, None)
|
||||||
|
|
||||||
# A pet was not created
|
# A pet was not created
|
||||||
|
@ -317,3 +288,98 @@ def test_model_form_mutation_mutate_invalid_form(pet_type):
|
||||||
assert result.errors[1].messages == ["This field is required."]
|
assert result.errors[1].messages == ["This field is required."]
|
||||||
assert "age" in fields_w_error
|
assert "age" in fields_w_error
|
||||||
assert "name" in fields_w_error
|
assert "name" in fields_w_error
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_form_mutation_multiple_creation_valid():
|
||||||
|
class Mutation(ObjectType):
|
||||||
|
pet_mutation = PetMutation.Field()
|
||||||
|
|
||||||
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
"""
|
||||||
|
mutation PetMutations {
|
||||||
|
petMutation1: petMutation(input: { name: "Mia", age: 10 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
petMutation2: petMutation(input: { name: "Enzo", age: 0 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert result.errors is None
|
||||||
|
assert result.data["petMutation1"]["pet"] == {"name": "Mia", "age": 10}
|
||||||
|
assert result.data["petMutation2"]["pet"] == {"name": "Enzo", "age": 0}
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 2
|
||||||
|
|
||||||
|
pet1 = Pet.objects.first()
|
||||||
|
assert pet1.name == "Mia"
|
||||||
|
assert pet1.age == 10
|
||||||
|
|
||||||
|
pet2 = Pet.objects.last()
|
||||||
|
assert pet2.name == "Enzo"
|
||||||
|
assert pet2.age == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_form_mutation_multiple_creation_invalid():
|
||||||
|
class Mutation(ObjectType):
|
||||||
|
pet_mutation = PetMutation.Field()
|
||||||
|
|
||||||
|
schema = Schema(query=MockQuery, mutation=Mutation)
|
||||||
|
|
||||||
|
result = schema.execute(
|
||||||
|
"""
|
||||||
|
mutation PetMutations {
|
||||||
|
petMutation1: petMutation(input: { name: "Mia", age: 99 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
petMutation2: petMutation(input: { name: "Enzo", age: 0 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
assert result.errors is None
|
||||||
|
|
||||||
|
assert result.data["petMutation1"]["pet"] is None
|
||||||
|
assert result.data["petMutation1"]["errors"] == [
|
||||||
|
{"field": "age", "messages": ["Too old"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert result.data["petMutation2"]["pet"] == {"name": "Enzo", "age": 0}
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 1
|
||||||
|
|
||||||
|
pet = Pet.objects.get()
|
||||||
|
assert pet.name == "Enzo"
|
||||||
|
assert pet.age == 0
|
||||||
|
|
|
@ -45,6 +45,7 @@ DEFAULTS = {
|
||||||
# This sets headerEditorEnabled GraphiQL option, for details go to
|
# This sets headerEditorEnabled GraphiQL option, for details go to
|
||||||
# https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
|
# https://github.com/graphql/graphiql/tree/main/packages/graphiql#options
|
||||||
"GRAPHIQL_HEADER_EDITOR_ENABLED": True,
|
"GRAPHIQL_HEADER_EDITOR_ENABLED": True,
|
||||||
|
"ATOMIC_MUTATIONS": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
16
graphene_django/tests/forms.py
Normal file
16
graphene_django/tests/forms.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from django import forms
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from .models import Pet
|
||||||
|
|
||||||
|
|
||||||
|
class PetForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Pet
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
def clean_age(self):
|
||||||
|
age = self.cleaned_data["age"]
|
||||||
|
if age >= 99:
|
||||||
|
raise ValidationError("Too old")
|
||||||
|
return age
|
18
graphene_django/tests/mutations.py
Normal file
18
graphene_django/tests/mutations.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from graphene import Field
|
||||||
|
|
||||||
|
from graphene_django.forms.mutation import DjangoFormMutation, DjangoModelFormMutation
|
||||||
|
|
||||||
|
from .forms import PetForm
|
||||||
|
from .types import PetType
|
||||||
|
|
||||||
|
|
||||||
|
class PetFormMutation(DjangoFormMutation):
|
||||||
|
class Meta:
|
||||||
|
form_class = PetForm
|
||||||
|
|
||||||
|
|
||||||
|
class PetMutation(DjangoModelFormMutation):
|
||||||
|
pet = Field(PetType)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
form_class = PetForm
|
|
@ -1,6 +1,8 @@
|
||||||
import graphene
|
import graphene
|
||||||
from graphene import ObjectType, Schema
|
from graphene import ObjectType, Schema
|
||||||
|
|
||||||
|
from .mutations import PetFormMutation, PetMutation
|
||||||
|
|
||||||
|
|
||||||
class QueryRoot(ObjectType):
|
class QueryRoot(ObjectType):
|
||||||
|
|
||||||
|
@ -19,6 +21,8 @@ class QueryRoot(ObjectType):
|
||||||
|
|
||||||
|
|
||||||
class MutationRoot(ObjectType):
|
class MutationRoot(ObjectType):
|
||||||
|
pet_form_mutation = PetFormMutation.Field()
|
||||||
|
pet_mutation = PetMutation.Field()
|
||||||
write_test = graphene.Field(QueryRoot)
|
write_test = graphene.Field(QueryRoot)
|
||||||
|
|
||||||
def resolve_write_test(self, info):
|
def resolve_write_test(self, info):
|
||||||
|
|
|
@ -2,6 +2,14 @@ import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
from graphene_django.settings import graphene_settings
|
||||||
|
|
||||||
|
from .models import Pet
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from urllib import urlencode
|
from urllib import urlencode
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -558,3 +566,265 @@ def test_passes_request_into_context_request(client):
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response_json(response) == {"data": {"request": "testing"}}
|
assert response_json(response) == {"data": {"request": "testing"}}
|
||||||
|
|
||||||
|
|
||||||
|
@patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False)
|
||||||
|
@patch.dict(
|
||||||
|
connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": True}
|
||||||
|
)
|
||||||
|
def test_form_mutation_multiple_creation_invalid_atomic_request(client):
|
||||||
|
query = """
|
||||||
|
mutation PetMutations {
|
||||||
|
petFormMutation1: petFormMutation(input: { name: "Mia", age: 99 }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
petFormMutation2: petFormMutation(input: { name: "Enzo", age: 0 }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = client.post(url_string(query=query))
|
||||||
|
content = response_json(response)
|
||||||
|
|
||||||
|
assert "errors" not in content
|
||||||
|
|
||||||
|
assert content["data"]["petFormMutation1"]["errors"] == [
|
||||||
|
{"field": "age", "messages": ["Too old"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert content["data"]["petFormMutation2"]["errors"] == []
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False)
|
||||||
|
@patch.dict(
|
||||||
|
connection.settings_dict, {"ATOMIC_MUTATIONS": True, "ATOMIC_REQUESTS": False}
|
||||||
|
)
|
||||||
|
def test_form_mutation_multiple_creation_invalid_atomic_mutation_1(client):
|
||||||
|
query = """
|
||||||
|
mutation PetMutations {
|
||||||
|
petFormMutation1: petFormMutation(input: { name: "Mia", age: 99 }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
petFormMutation2: petFormMutation(input: { name: "Enzo", age: 0 }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = client.post(url_string(query=query))
|
||||||
|
content = response_json(response)
|
||||||
|
|
||||||
|
assert "errors" not in content
|
||||||
|
|
||||||
|
assert content["data"]["petFormMutation1"]["errors"] == [
|
||||||
|
{"field": "age", "messages": ["Too old"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert content["data"]["petFormMutation2"]["errors"] == []
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", True)
|
||||||
|
@patch.dict(
|
||||||
|
connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": False}
|
||||||
|
)
|
||||||
|
def test_form_mutation_multiple_creation_invalid_atomic_mutation_2(client):
|
||||||
|
query = """
|
||||||
|
mutation PetMutations {
|
||||||
|
petFormMutation1: petFormMutation(input: { name: "Mia", age: 99 }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
petFormMutation2: petFormMutation(input: { name: "Enzo", age: 0 }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = client.post(url_string(query=query))
|
||||||
|
content = response_json(response)
|
||||||
|
|
||||||
|
assert "errors" not in content
|
||||||
|
|
||||||
|
assert content["data"]["petFormMutation1"]["errors"] == [
|
||||||
|
{"field": "age", "messages": ["Too old"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert content["data"]["petFormMutation2"]["errors"] == []
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False)
|
||||||
|
@patch.dict(
|
||||||
|
connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": False}
|
||||||
|
)
|
||||||
|
def test_form_mutation_multiple_creation_invalid_non_atomic(client):
|
||||||
|
query = """
|
||||||
|
mutation PetMutations {
|
||||||
|
petFormMutation1: petFormMutation(input: { name: "Mia", age: 99 }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
petFormMutation2: petFormMutation(input: { name: "Enzo", age: 0 }) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = client.post(url_string(query=query))
|
||||||
|
content = response_json(response)
|
||||||
|
|
||||||
|
assert "errors" not in content
|
||||||
|
|
||||||
|
assert content["data"]["petFormMutation1"]["errors"] == [
|
||||||
|
{"field": "age", "messages": ["Too old"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert content["data"]["petFormMutation2"]["errors"] == []
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 1
|
||||||
|
|
||||||
|
pet = Pet.objects.get()
|
||||||
|
assert pet.name == "Enzo"
|
||||||
|
assert pet.age == 0
|
||||||
|
|
||||||
|
|
||||||
|
@patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False)
|
||||||
|
@patch.dict(
|
||||||
|
connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": True}
|
||||||
|
)
|
||||||
|
def test_model_form_mutation_multiple_creation_invalid_atomic_request(client):
|
||||||
|
query = """
|
||||||
|
mutation PetMutations {
|
||||||
|
petMutation1: petMutation(input: { name: "Mia", age: 99 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
petMutation2: petMutation(input: { name: "Enzo", age: 0 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = client.post(url_string(query=query))
|
||||||
|
content = response_json(response)
|
||||||
|
|
||||||
|
assert "errors" not in content
|
||||||
|
|
||||||
|
assert content["data"]["petMutation1"]["pet"] is None
|
||||||
|
assert content["data"]["petMutation1"]["errors"] == [
|
||||||
|
{"field": "age", "messages": ["Too old"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert content["data"]["petMutation2"]["pet"] == {"name": "Enzo", "age": 0}
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 0
|
||||||
|
|
||||||
|
|
||||||
|
@patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False)
|
||||||
|
@patch.dict(
|
||||||
|
connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": False}
|
||||||
|
)
|
||||||
|
def test_model_form_mutation_multiple_creation_invalid_non_atomic(client):
|
||||||
|
query = """
|
||||||
|
mutation PetMutations {
|
||||||
|
petMutation1: petMutation(input: { name: "Mia", age: 99 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
petMutation2: petMutation(input: { name: "Enzo", age: 0 }) {
|
||||||
|
pet {
|
||||||
|
name
|
||||||
|
age
|
||||||
|
}
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = client.post(url_string(query=query))
|
||||||
|
content = response_json(response)
|
||||||
|
|
||||||
|
assert "errors" not in content
|
||||||
|
|
||||||
|
assert content["data"]["petMutation1"]["pet"] is None
|
||||||
|
assert content["data"]["petMutation1"]["errors"] == [
|
||||||
|
{"field": "age", "messages": ["Too old"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert content["data"]["petMutation2"]["pet"] == {"name": "Enzo", "age": 0}
|
||||||
|
|
||||||
|
assert Pet.objects.count() == 1
|
||||||
|
|
||||||
|
pet = Pet.objects.get()
|
||||||
|
assert pet.name == "Enzo"
|
||||||
|
assert pet.age == 0
|
||||||
|
|
||||||
|
|
||||||
|
@patch("graphene_django.utils.utils.transaction.set_rollback")
|
||||||
|
@patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False)
|
||||||
|
@patch.dict(
|
||||||
|
connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": True}
|
||||||
|
)
|
||||||
|
def test_query_errors_atomic_request(set_rollback_mock, client):
|
||||||
|
client.get(url_string(query="force error"))
|
||||||
|
set_rollback_mock.assert_called_once_with(True)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("graphene_django.utils.utils.transaction.set_rollback")
|
||||||
|
@patch("graphene_django.settings.graphene_settings.ATOMIC_MUTATIONS", False)
|
||||||
|
@patch.dict(
|
||||||
|
connection.settings_dict, {"ATOMIC_MUTATIONS": False, "ATOMIC_REQUESTS": False}
|
||||||
|
)
|
||||||
|
def test_query_errors_non_atomic(set_rollback_mock, client):
|
||||||
|
client.get(url_string(query="force error"))
|
||||||
|
set_rollback_mock.assert_not_called()
|
||||||
|
|
9
graphene_django/tests/types.py
Normal file
9
graphene_django/tests/types.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from graphene_django.types import DjangoObjectType
|
||||||
|
|
||||||
|
from .models import Pet
|
||||||
|
|
||||||
|
|
||||||
|
class PetType(DjangoObjectType):
|
||||||
|
class Meta:
|
||||||
|
model = Pet
|
||||||
|
fields = "__all__"
|
|
@ -1,7 +1,7 @@
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from django.db import models
|
from django.db import connection, models, transaction
|
||||||
from django.db.models.manager import Manager
|
from django.db.models.manager import Manager
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
|
@ -100,3 +100,9 @@ def import_single_dispatch():
|
||||||
)
|
)
|
||||||
|
|
||||||
return singledispatch
|
return singledispatch
|
||||||
|
|
||||||
|
|
||||||
|
def set_rollback():
|
||||||
|
atomic_requests = connection.settings_dict.get("ATOMIC_REQUESTS", False)
|
||||||
|
if atomic_requests and connection.in_atomic_block:
|
||||||
|
transaction.set_rollback(True)
|
||||||
|
|
|
@ -3,6 +3,7 @@ import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
from django.db import connection, transaction
|
||||||
from django.http import HttpResponse, HttpResponseNotAllowed
|
from django.http import HttpResponse, HttpResponseNotAllowed
|
||||||
from django.http.response import HttpResponseBadRequest
|
from django.http.response import HttpResponseBadRequest
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
@ -17,6 +18,9 @@ from graphql.execution import ExecutionResult
|
||||||
from graphql.type.schema import GraphQLSchema
|
from graphql.type.schema import GraphQLSchema
|
||||||
from graphql.execution.middleware import MiddlewareManager
|
from graphql.execution.middleware import MiddlewareManager
|
||||||
|
|
||||||
|
from graphene_django.constants import MUTATION_ERRORS_FLAG
|
||||||
|
from graphene_django.utils.utils import set_rollback
|
||||||
|
|
||||||
from .settings import graphene_settings
|
from .settings import graphene_settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -203,11 +207,15 @@ class GraphQLView(View):
|
||||||
request, data, query, variables, operation_name, show_graphiql
|
request, data, query, variables, operation_name, show_graphiql
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if getattr(request, MUTATION_ERRORS_FLAG, False) is True:
|
||||||
|
set_rollback()
|
||||||
|
|
||||||
status_code = 200
|
status_code = 200
|
||||||
if execution_result:
|
if execution_result:
|
||||||
response = {}
|
response = {}
|
||||||
|
|
||||||
if execution_result.errors:
|
if execution_result.errors:
|
||||||
|
set_rollback()
|
||||||
response["errors"] = [
|
response["errors"] = [
|
||||||
self.format_error(e) for e in execution_result.errors
|
self.format_error(e) for e in execution_result.errors
|
||||||
]
|
]
|
||||||
|
@ -312,14 +320,27 @@ class GraphQLView(View):
|
||||||
# executor is not a valid argument in all backends
|
# executor is not a valid argument in all backends
|
||||||
extra_options["executor"] = self.executor
|
extra_options["executor"] = self.executor
|
||||||
|
|
||||||
return document.execute(
|
options = {
|
||||||
root_value=self.get_root_value(request),
|
"root_value": self.get_root_value(request),
|
||||||
variable_values=variables,
|
"variable_values": variables,
|
||||||
operation_name=operation_name,
|
"operation_name": operation_name,
|
||||||
context_value=self.get_context(request),
|
"context_value": self.get_context(request),
|
||||||
middleware=self.get_middleware(request),
|
"middleware": self.get_middleware(request),
|
||||||
**extra_options
|
}
|
||||||
)
|
options.update(extra_options)
|
||||||
|
|
||||||
|
operation_type = document.get_operation_type(operation_name)
|
||||||
|
if operation_type == "mutation" and (
|
||||||
|
graphene_settings.ATOMIC_MUTATIONS is True
|
||||||
|
or connection.settings_dict.get("ATOMIC_MUTATIONS", False) is True
|
||||||
|
):
|
||||||
|
with transaction.atomic():
|
||||||
|
result = document.execute(**options)
|
||||||
|
if getattr(request, MUTATION_ERRORS_FLAG, False) is True:
|
||||||
|
transaction.set_rollback(True)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return document.execute(**options)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return ExecutionResult(errors=[e], invalid=True)
|
return ExecutionResult(errors=[e], invalid=True)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user