Mutations๐
In this section, we'll cover Undine's MutationTypes
which allow you to expose your Django models through the GraphQL schema for mutations,
expanding on the basics introduced in the Tutorial.
If you to mutate data outside of your Django models, see the Function References section in the Schema documentation.
MutationTypes๐
A MutationType
represents a GraphQL InputObjectType
for mutating a Django model
in the GraphQL schema. A basic MutationType
is created by subclassing MutationType
and adding a Django model to it as a generic type parameter:
Mutation kind๐
MutationType
can be used for create
, update
, delete
as well as custom
mutations.
The kind of mutation a certain MutationType
is for is determined by its
kind
, which can be set in the MutationType
class definition.
This allows Undine to generate the correct fields using auto-generation,
as well as link the correct mutation resolver to the MutationType
.
kind
can also be omitted, in which case the MutationType
will determine the mutation kind
using these rules:
- If the word
create
can be found in the name of theMutationType
,kind
will becreate
. - If the word
update
can be found in the name of theMutationType
,kind
will beupdate
. - If the word
delete
can be found in the name of theMutationType
,kind
will bedelete
. - If either the
__mutate__
or__bulk_mutate__
method has been defined on theMutationType
,kind
will becustom
(see custom mutations). - Otherwise, an error will be raised.
Auto-generation๐
By default, a MutationType
automatically introspects its model and converts the model's fields
to input fields on the generated InputObjectType
. For example, if the Task
model has the following fields:
Then the GraphQL InputObjectType
for a MutationType
for a create
mutation would be:
For an update
mutation, the pk
field is included for selecting the
mutation target, the rest of the fields are all made nullable (=not required),
and no default values are added.
For a delete
mutation, only the pk
field is included for selecting the
mutation target.
You can disable auto-generation globally using the AUTOGENERATION
setting,
or the MutationType
by setting the auto
argument to False
in the class definition:
Alternatively, you could exclude some Inputs
from the auto-generation by setting the exclude
argument:
Output type๐
A MutationType
requires a QueryType
for the same model to exist in the schema,
since the MutationType
will use the ObjectType
generated from the QueryType
as the output type of the mutation.
You don't need to explicitly link the QueryType
to the MutationType
since MutationType
will automatically look up the QueryType
for the same model
from the QueryType
registry.
This would generate the following mutation in the GraphQL schema:
If you wanted to link the QueryType
explicitly, you could do so by overriding the
__query_type__
classmethod.
Permissions๐
You can add mutation-level permission checks for a MutationType
by defining the __permissions__
classmethod.
About method signature
For create
mutations, the instance
is a brand new instance of the model,
without any of the input_data
values applied. This also means that it doesn't
have a primary key yet.
For update
and delete
mutations, the instance
is the instance that is being
mutated, with the input_data
values applied.
This method will be called for each instance of Task
that is mutated
by this MutationType
. For bulk mutations, this means that the method will be called
for each item in the mutation input data.
You can raise any GraphQLError
when validation fails, but it's recommended to
raise a GraphQLPermissionError
from the undine.exceptions
module.
Validation๐
You can add mutation-level validation for a MutationType
by defining the __validate__
classmethod.
About method signature
For create
mutations, the instance
is a brand new instance of the model,
without any of the input_data
values applied. This also means that it doesn't
have a primary key yet.
For update
and delete
mutations, the instance
is the instance that is being
mutated, with the input_data
values applied.
You can raise any GraphQLError
when validation fails, but it's recommended to
raise a GraphQLValidationError
from the undine.exceptions
module.
After mutation handling๐
You can add custom handling that happens after the mutation is done by defining the__after__
classmethod on the MutationType
.
About method signature
For create
and update
mutations, the instance
is the instance that was either
created or updated, with the input_data
values applied.
input_data
contains the input data for the mutation.
For delete
mutations, the instance
is the instance that was deleted.
This means that its relations have been disconnected, and its primary key
has been set to None
.
This can be useful for doing things like sending emails.
Custom mutations๐
You can define your own custom logic by defining the __mutate__
or __bulk_mutate__
method on
the MutationType
class for single or bulk mutations respectively.
In the above example, the MutationType
is still considered a create
kind of mutation,
just with some custom mutation logic. The MutationType
kind
still affects which fields
are autogenerated, which resolvers are used, as well as some inference rules for its Inputs
.
You can also use a special custom
mutation kind
when using custom resolvers.
This affects the creation of the MutationType
in the following ways:
- Auto-generation is not used, even if it is enabled
- No
Input
is input-only by default
These MutationTypes
will otherwise resolve like create or update mutations, depending
on if an Input
named pk
is present in the MutationType
.
By default, the output type of a custom mutation is still the ObjectType
from the QueryType
matching the MutationType's
model. If your custom
mutation returns an instance of that model, it will work without additional changes.
However, if you want to return a different type, you can do so by overriding
the __output_type__
classmethod on the MutationType
.
Related mutations๐
Let's say you have the following models:
If you wanted to create both a Task
and its related Project
in a single mutation,
you could link two mutation types using a special related
kind
of MutationType
.
This creates the following InputObjectTypes
:
Autogeneration and inference rules for related
MutationTypes
are the same as for update
MutationTypes
,
except the pk
field is also not required. This allows you to create, update, link, or unlink
new or existing related models during the mutation as you see fit.
Let's give a few examples. Assuming you added the TaskCreateMutation
to the schema with
an Entrypoint
create_task
, you can create a new Task
together with a new Project
like this:
Or you can link an existing Project
to a new Task
like this:
Or you can link an existing project while modifying it:
Permission an validation checks are run for related
MutationTypes
and their Inputs
as well,
although existing instances are not fetched from the database even if the input contains its primary key.
Note that if the Input
connecting the related
MutationType
defines a permission or validation check,
that check is run instead of the related
MutationType
permission or validation check.
Related mutation action๐
When updating an instance and its relations using a related mutation, that instance may already have existing related objects. For some relations, it's clear what should happen to relations that are not selected in the related mutation.
- Forward one-to-one relation: Selects the new related object to attach to, or set the relation to null. Reverse one-to-one relation can always be missing.
- Forward foreign key (many-to-one) relation: Selects the new related object to attach to, or set the relation to null. Reverse relations do not have any constraints.
- Many-to-many relations: Selects the new related objects that the current instance should be linked to. Non-selected objects are unlinked, meaning through table rows are deleted.
For other relations, you might need different behavior depending on the situation:
- Reverse one-to-one relation: You might want to delete the exiting related object, or set the relation to null (although the forward part of the relation might not be nullable).
- Reverse foreign key (one-to-many) relation: You might want to delete exiting related objects, or set their relation to null (although the forward part of the relation might not be nullable). You might even want to leave the existing relations as they are.
The action that should be taken for the relations is defined by the MutationType
related_action
argument.
The actions are as follows:
null
: Set the relaton to null. If the relation is not nullable, an error is raised. Default action.delete
: Delete the related objects.ignore
: Leave the existing relations as they are. For one-to-one relations, an error is raised.
Note that this action applies to all related mutations executed from the "parent" MutationType
.
If you need more granular control, you should make the mutation a custom mutation
instead.
Order of operations๐
The order of operations for executing a mutation using a MutationType
is as follows:
MutationType
permissions andInput
permissions are checkedMutationType
validation andInput
validation are run- Mutation is executed
MutationType
after handling is run
If GraphQLErrors
are raised during steps 1 and 2, the validation and permission checks continue until
step 3, and then all exceptions are raised at once. The error's path
will point to the Input
where
the exception was raised.
Example result with multiple errors
Schema name๐
By default, the name of the generated InputObjectType
is the same as the name of the MutationType
class.
If you want to change the name, you can do so by setting the schema_name
argument:
Description๐
To provide a description for the MutationType
, you can add a docstring to the class.
Directives๐
You can add directives to the MutationType
by providing them using the directives
argument.
See the Directives section for more details on directives.
GraphQL extensions๐
You can provide custom extensions for the MutationType
by providing a
extensions
argument with a dictionary containing them. These can then be used
however you wish to extend the functionality of the MutationType
.
MutationType
extensions are made available in the GraphQL InputObjectType
extensions
after the schema is created. The MutationType
itself is found in the extensions
under a key defined by the MUTATION_TYPE_EXTENSIONS_KEY
setting.
Inputs๐
An Input
is a class that is used to define a possible input for a MutationType
.
Usually Inputs
correspond to fields on the Django model for their respective MutationType
.
In GraphQL, an Input
represents a GraphQLInputField
in an InputObjectType
.
An Input
always requires a reference from which it will create the proper
input type and default value for the Input
.
Model field references๐
As seen in the MutationType
section, you don't need to provide model fields
explicitly thanks to auto-generation, but if you wanted to be more explicit,
you could add the Inputs
to the MutationType
class body. In this case, the Input
can be used
without a reference, as its attribute name in the MutationType
class body can be used to identify
the corresponding model field.
To be a bit more explicit, you could use a string referencing the model field:
For better type safety, you can also use the model field itself:
Being explicit like this is only required if the name of the argument in the GraphQL schema is different from the model field name.
Function references๐
Functions (or methods) can also be used to create Inputs
.
This can be done by decorating a method with the Input
class.
About method signature
The decorated method is treated as a static method by the Input
.
The self
argument is not an instance of the MutationType
,
but the model instance that is being mutated.
The info
argument can be left out, but if it's included, it should always
have the GQLInfo
type annotation.
The value
argument can be left out:
This makes the Input
hidden
in the GraphQL
schema since it takes no user input.
Also, since current_user
is not a field on the Task
model,
the Input
is also input_only
.
Model references๐
Models classes can also be used as Input
references.
In this case, the model will be fetched to the input data before
permission and validation checks.
The model doesn't necessarily need to be a related model of the parent MutationType
model,
but if it is not, the input will be an input-only input by default. Otherwise, if the model
instance is not found, the input will raise an error before any other checks are run.
Permissions๐
You can restrict the use of an Input
by first defining the Input
in the class body
of the MutationType
and then adding a method with the @<input_name>.permissions
decorator.
About method signature
The decorated method is treated as a static method by the Input
.
The self
argument is not an instance of the MutationType
,
but the model instance that is being mutated.
The info
argument is the GraphQL resolve info for the request.
The value
argument is the value provided for the input.
This method will be called for each instance of Task
that is mutated
by this MutationType
. For bulk mutations, this means that the method will be called
for each item in the mutation input data.
You can raise any GraphQLError
when validation fails, but it's recommended to
raise a GraphQLPermissionError
from the undine.exceptions
module.
Validation๐
You can validate the value of an Input
by first defining the Input
in the class body
of the MutationType
and then adding a method with the @<input_name>.validate
decorator.
About method signature
The self
argument is not an instance of the MutationType
,
but the model instance that is being mutated.
The info
argument is the GraphQL resolve info for the request.
The value
argument is the value provided for the input.
You can raise any GraphQLError
when validation fails, but it's recommended to
raise a GraphQLValidationError
from the undine.exceptions
module.
Conversion๐
Normally, values for Inputs
are parsed and converted based on the Input's
Scalar
.
However, you can add additional convertion for an individual Input
by first defining
the Input
in the class body of the MutationType
and then adding a method with
the @<input_name>.convert
decorator.
About method signature
The self
argument is not an instance of the MutationType
,
but the Input
whose value is being coerced.
The value
argument is the value provided for the Input
.
Note that conversion functions are also run for default values.
Default values๐
By default, an Input
is able to determine its default value based on its reference.
For example, for a model field, the default value is taken from its default
attribute.
However, default values are only added automatically for create mutations,
as update mutations should only update fields that have been provided.
If you want to set the default value for an Input
manually, you can set
the default_value
argument on the Input
.
Note that the default value needs to be a valid GraphQL default value (i.e. a string, integer, float, boolean, or null, or a list or dictionary of these).
Input-only inputs๐
Input-only Inputs
show up in the GraphQL schema, but are not part of the actual mutation,
usually because they are not part of the model being mutated.
They can be used as additional data for validation and permissions checks.
Notice that the Input
reference is bool
. This is to indicate the input type,
as there is no model field to infer the type from. For the same reason, you don't
actually need to specify the input_only
argument.
Hidden inputs๐
Hidden Inputs
are not included in the GraphQL schema, but their values are added before
the mutation is executed. They can be used, for example, to set default values
for fields that should not be overridden by users.
One common use case for hidden inputs is to set the current user
as the default value for a relational field. Let's suppose that
the Task
model has a foreign key user
to the User
model.
To assign a new task to the current user during creation,
you can define a hidden input for the user
field:
See Function References for more details.
Required inputs๐
By default, an Input
is able to determine whether it is required or not based on
is reference, as well as the kind
of MutationType
it is used in. If you want to
override this, you can set the required
argument on the Input
.
Field name๐
A field_name
can be provided to explicitly set the Django model field name
that the Input
corresponds to. This can be useful when the field has a different
name and type in the GraphQL schema than in the model.
Schema name๐
An Input
is also able to override the name of the Input
in the GraphQL schema.
This can be useful for renaming fields for the schema, or when the desired name is a Python keyword
and cannot be used as the Input
attribute name.
Descriptions๐
By default, an Input
is able to determine its description based on its reference.
For example, for a model field, the description is taken from its help_text
.
If the reference has no description, or you wish to add a different one, this can be done in two ways:
1) By setting the description
argument.
2) As class attribute docstrings, if ENABLE_CLASS_ATTRIBUTE_DOCSTRINGS
is enabled.
When using function references, instead of a class attribute docstring, you add a docstring to the function/method used as the reference instead.
Deprecation reason๐
A deprecation_reason
can be provided to mark the Input
Input
This is for documentation purposes only, and does not affect the use of the Field
.
Directives๐
You can add directives to the Input
by providing them using the directives
argument.
See the Directives section for more details on directives.
GraphQL extensions๐
You can provide custom extensions for the Input
by providing a
extensions
argument with a dictionary containing them. These can then be used
however you wish to extend the functionality of the Input
.
Input
extensions are made available in the GraphQL InputField
extensions
after the schema is created. The Input
itself is found in the extensions
under a key defined by the INPUT_EXTENSIONS_KEY
setting.