Mutations๐
In this section, we'll cover Undine's MutationTypes
which allow you to create mutations base on your Django Models.
For mutations not concerning your Django Models,
you can create function Entrypoints.
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. You must also add at least one
Input to the class body of the MutationType.
Mutation kind๐
How a mutation using a MutationType resolves is determined by its kind.
The basic types of mutations are create, update, and delete, which can be used
to create, update and delete instances of a MutationType's Model respectively.
There are also two special mutation kinds: custom and related, which are covered
in custom mutations and related mutations respectively.
kind can also be omitted, in which case the MutationType
will determine the mutation kind using these rules:
- If the word
createcan be found in the name of theMutationType,kindwill becreate. - If the word
updatecan be found in the name of theMutationType,kindwill beupdate. - If the word
deletecan be found in the name of theMutationType,kindwill bedelete. - If either the
__mutate__or__bulk_mutate__method has been defined on theMutationType,kindwill becustom. - Otherwise, an error will be raised.
Auto-generation๐
A MutationType can automatically introspect its Django Model and convert the Model's fields
to Inputs on the MutationType. For example, if the Task model has the following fields:
Then the GraphQL InputObjectType for a MutationType for a create mutation using auto-generation 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. This is essentially a fully partial update mutation.
For a delete mutation, only the pk field is included for selecting the
instance to delete.
To use auto-generation, either set AUTOGENERATION setting to True
to enable it globally, or set the auto argument to True in the MutationType class definition.
With this, you can leave the MutationType class body empty.
You can exclude some Model fields from the auto-generation by setting the exclude argument:
Output type๐
By default, a MutationType uses a QueryType with the same Model as its output type.
This means that one must be created, even if not used for querying outside of the MutationType.
You don't need to explicitly link the QueryType to the MutationType
since the MutationType will automatically look up the QueryType
from the QueryType registry.
This would create 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.
If you wanted a fully custom output type, you can override the __output_type__ classmethod.
Permissions๐
You can add mutation-level permission checks to mutations executed using a MutationType
by defining the __permissions__ classmethod.
About method signature
For create mutations, the instance is a brand new instance of the model.
Note that this also means that it doesn't have a primary key yet.
For update and delete mutations, instance is the existing model
instance that is being mutated.
This method will be called for each instance of Task that is mutated
by this MutationType.
You can raise any GraphQLError when a permission check fails, but it's recommended to
raise a GraphQLPermissionError from the undine.exceptions module.
Validation๐
You can add mutation-level validation to mutations executed using a MutationType
by defining the __validate__ classmethod.
About method signature
For create mutations, the instance is a brand new instance of the model.
Note that this also means that it doesn't have a primary key yet.
For update and delete mutations, instance is the existing model
instance that is being mutated.
You can raise any GraphQLError when a validation check 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, instance is the model instance that was either
created or updated.
For delete mutations, instance is the instance that was deleted.
This means that its relations have been disconnected, and its primary key
has been set to None.
input_data contains the input data that was used in the mutation.
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 still a create mutation,
just with some custom mutation logic. The MutationType kind still affects auto-generation,
which resolvers are used (whether the mutation creates a new instance or modifies an existing one),
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
Inputis input-only by default
Custom mutations will resolve like create or update mutations, depending
on if an Input named pk is present on 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:
Auto-generation 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
(for performance reasons).
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:
- Model inputs have their Model instances fetched.
- Hidden inputs are be added to the input data.
- Function inputs are run.
MutationTypepermissions andInputpermissions are checked.MutationTypevalidation andInputvalidation are run.- Input-only inputs are removed from the input data.
- Mutation is executed.
MutationTypeafter handling is run.
If multiple GraphQLErrors are raised in the permission or validation steps for different inputs,
those errors are returned together. 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 GraphQL InputObjectType for a MutationType class
is the name of the MutationType class. If you want to change the name separately,
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.
The directive must be usable in the INPUT_OBJECT location.
You can also add them using the decorator syntax.
See the Directives section for more details on directives.
Visibility๐
This is an experimental feature that needs to be enabled using the
EXPERIMENTAL_VISIBILITY_CHECKSsetting.
You can hide a MutationType from certain users using the __is_visible__ method.
Hiding an MutationType means that it will not be included in introspection queries,
and trying to use it in operations will result in an error that looks exactly like
the Entrypoint or Input using the MutationType didn't exist in the first place.
When using visibility checks, you should also disable "did you mean" suggestions using the
ALLOW_DID_YOU_MEAN_SUGGESTIONSsetting. Otherwise, a hidden field might show up in them.
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 GraphQL InputObjectType extensions
under a key defined by the MUTATION_TYPE_EXTENSIONS_KEY setting.
Inputs๐
An Input is used to define a possible input in a MutationType.
Usually Inputs correspond to fields on the Django Model for their respective MutationType.
In GraphQL, an Input represents a GraphQLInputField on 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๐
For Inputs corresponding to Django Model fields, the Input can be used without passing in 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:
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 determines the input given by the user, which can then be transformed
into the input data for the mutation in the function. The type of the value argument
determines the input type of the function input in. The value argument can also be left out,
in which case the input will become a hidden input.
Model references๐
A Model class can also be used as an Input reference.
In this case, a Model instance will be fetched to the input data from a primary key
provided to the Input before permission and validation checks (see order of operation).
If an instance is not found, the Input will raise an error before any other checks are run.
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.
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.
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 converted.
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.
Note that you, indeed, can use lists and dictionaries as default values, even though they are mutable. Undine will make a copy of any non-hashable default value before mutating it, so that you won't accidentally change the default value.
Input-only inputs๐
Input-only Inputs show up in the GraphQL schema, but their values are removed from the mutation data
before the actual mutation (see order of operations),
usually because they are not part of the Model being mutated. They can be used as additional data for
validation and permissions checks, e.g. flags to control the behavior of the mutation.
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.
Hidden inputs๐
Hidden Inputs are not included in the GraphQL schema, but their values are added before
the mutation is executed (see order of operations).
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's required or not based on
its reference, as well as the kind of MutationType it's used in. If you want to
set this manually, you can set the required argument on the Input.
Note that due to GraphQL implementation details, there is no distinction between required and nullable. Therefore, non-required
Inputscan always acceptnullvalues, and required inputs cannot acceptnullvalues.
Field name๐
A field_name can be provided to explicitly set the Django Model field
that the Input corresponds to.
This can be useful when the Input has a different name and type in the GraphQL schema than in the Model.
Schema name๐
By default, the name of the InputObjectType field generated from an Input is the same
as the name of the Input on the MutationType class (converted to camelCase if
CAMEL_CASE_SCHEMA_FIELDS is enabled).
If you want to change the name of the InputObjectType field separately,
you can do so by setting the schema_name argument:
This can be useful when the desired name of the InputObjectType field 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 as deprecated.
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.
The directive must be usable in the INPUT_FIELD_DEFINITION location.
You can also add them using the @ operator (which kind of looks like GraphQL syntax):
See the Directives section for more details on directives.
Visibility๐
This is an experimental feature that needs to be enabled using the
EXPERIMENTAL_VISIBILITY_CHECKSsetting.
You can hide an Input from certain users by decorating a method with the
<input_name>.visible decorator. Hiding an Input means that it will not be included in introspection queries,
and trying to use it in operations will result in an error that looks exactly like
the Input didn't exist in the first place.
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 instance of the Input that is being used.
Since visibility checks occur in the validation phase of the GraphQL request,
GraphQL resolver info is not yet available. However, you can access the
Django request object using the request argument.
From this, you can, e.g., access the request user for permission checks.
When using visibility checks, you should also disable "did you mean" suggestions using the
ALLOW_DID_YOU_MEAN_SUGGESTIONSsetting. Otherwise, a hidden field might show up in them.
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 InputObjectType field extensions
after the schema is created. The Input itself is found in the GraphQL input field extensions
under a key defined by the INPUT_EXTENSIONS_KEY
setting.