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:

1
2
3
4
5
6
from undine import MutationType

from .models import Task


class TaskMutation(MutationType[Task]): ...

Mutation kind🔗

MutationType supports 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.

1
2
3
4
5
6
from undine import MutationType

from .models import Task


class TaskMutation(MutationType[Task], kind="create"): ...

However, kind can also be omitted, in which case the MutationType will first check if the __mutate__ method has been defined on the MutationType, and if so, treat the mutation as a custom mutation.

from typing import Any

from undine import GQLInfo, MutationType

from .models import Task


class TaskMutation(MutationType[Task]):
    @classmethod
    def __mutate__(cls, root: Any, info: GQLInfo, input_data: dict[str, Any]) -> Any:
        pass  # Some custom mutation logic here

If __mutate__ is not defined, the MutationType will then check if the word create, update, or delete can be found in the name of the MutationType, and if so, treat the mutation as that kind of mutation. Otherwise, the MutationType will treat the mutation as a custom mutation.

1
2
3
4
5
6
7
from undine import MutationType

from .models import Task


# Create mutation, since has "create" in the name.
class TaskCreateMutation(MutationType[Task]): ...

MutationTypes can also be a special "related" kind of MutationType. These MutationTypes allow you to freely modify the related objects during a mutation. See the related mutations section for more details.

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:

1
2
3
4
5
6
7
from django.db import models


class Task(models.Model):
    name = models.CharField(max_length=255)
    done = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

Then the GraphQL InputObjectType for a MutationType for a create mutation would be:

1
2
3
4
5
input TaskCreateMutation {
    name: String!
    done: Boolean = true
    # `createdAt` not included since it has `auto_now_add=True`
}

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.

1
2
3
4
5
input TaskUpdateMutation {
    pk: Int!
    name: String
    done: Boolean
}

For a delete mutation, only the pk field is included for selecting the mutation target.

1
2
3
input TaskDeleteMutation {
    pk: Int!
}

You can disable auto-generation by setting the auto argument to False in the class definition:

1
2
3
4
5
6
7
from undine import MutationType

from .models import Task


# This would create an empty `InputObjectType`, which is not allowed in GraphQL.
class TaskCreateMutation(MutationType[Task], auto=False): ...

Alternatively, you could exclude some Inputs from the auto-generation by setting the exclude argument:

1
2
3
4
5
6
from undine import MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task], exclude=["name"]): ...

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.

from undine import Entrypoint, MutationType, QueryType, RootType

from .models import Task


class TaskType(QueryType[Task]): ...


class TaskCreateMutation(MutationType[Task]): ...


class Mutation(RootType):
    create_task = Entrypoint(TaskCreateMutation)

This would generate the following mutation in the GraphQL schema:

type TaskType {
    pk: Int!
    name: String!
    done: Boolean!
    createdAt: DateTime!
}

input TaskCreateMutation {
    name: String!
    done: Boolean = false
}

type Mutation {
    createTask(input: TaskCreateMutation!): TaskType!
}

If you wanted to link the QueryType explicitly, you could do so by overriding the __query_type__ classmethod.

from undine import MutationType, QueryType

from .models import Task


class TaskType(QueryType[Task]): ...


class TaskCreateMutation(MutationType[Task]):
    @classmethod
    def __query_type__(cls) -> type[QueryType]:
        return TaskType

Permissions🔗

You can add mutation-level permission checks for a MutationType by defining the __permissions__ classmethod.

from typing import Any

from undine import GQLInfo, MutationType
from undine.exceptions import GraphQLPermissionError

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    @classmethod
    def __permissions__(cls, instance: Task, info: GQLInfo, input_data: dict[str, Any]) -> None:
        if not info.context.user.is_authenticated:
            msg = "Only authenticated users can create tasks."
            raise GraphQLPermissionError(msg)
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.

from typing import Any

from undine import GQLInfo, MutationType
from undine.exceptions import GraphQLValidationError

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    @classmethod
    def __validate__(cls, instance: Task, info: GQLInfo, input_data: dict[str, Any]) -> None:
        if len(input_data["name"]) < 3:
            msg = "Name must be at least 3 characters long."
            raise GraphQLValidationError(msg)
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.

from typing import Any

from undine import GQLInfo, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    @classmethod
    def __after__(cls, instance: Task, info: GQLInfo, previous_data: dict[str, Any]) -> None:
        pass  # Some post-mutation handling here
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.

previous_data contains the field values in the instance before the mutation. For create mutations, this will be empty. Related objects are not included.

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.

Order of operations🔗

The order of operations for executing a mutation using a MutationType is as follows:

  1. MutationType permissions are checked
  2. For each Input:
  3. MutationType validation is run
  4. Mutation is executed
  5. MutationType after handling is run

If GraphQLErrors are raised during steps 1-3, the validation and permission checks continue until step 4, 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
{
    "data": null,
    "errors": [
        {
            "message": "Validation error.",
            "extensions": {
                "status_code": 400,
                "error_code": "VALIDATION_ERROR"
            },
            "path": ["createTask", "name"]
        },
        {
            "message": "Validation error.",
            "extensions": {
                "status_code": 400,
                "error_code": "VALIDATION_ERROR"
            },
            "path": ["createTask", "done"]
        }
    ]
}

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:

1
2
3
4
5
6
from undine import MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task], schema_name="CreateTask"): ...

Description🔗

To provide a description for the MutationType, you can add a docstring to the class.

1
2
3
4
5
6
7
from undine import MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    """Description."""

Directives🔗

You can add directives to the MutationType by providing them using the directives argument.

from graphql import DirectiveLocation

from undine import MutationType
from undine.directives import Directive

from .models import Task


class MyDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION]): ...


class TaskCreateMutation(MutationType[Task], directives=[MyDirective()]): ...

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.

1
2
3
4
5
6
from undine import MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task], extensions={"foo": "bar"}): ...

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.

1
2
3
4
5
6
7
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input()

To be a bit more explicit, you could use a string referencing the model field:

1
2
3
4
5
6
7
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input("name")

For better type safety, you can also use the model field itself:

1
2
3
4
5
6
7
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input(Task.name)

Being explicit like this is only required if the name of the argument in the GraphQL schema is different from the model field name.

1
2
3
4
5
6
7
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    title = Input("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.

1
2
3
4
5
6
7
8
9
from undine import GQLInfo, Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    @Input
    def name(self, info: GQLInfo, value: str) -> str:
        return value.upper()
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:

1
2
3
4
5
6
7
8
9
from undine import GQLInfo, Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    @Input
    def current_user(self, info: GQLInfo) -> int | None:
        return info.context.user.id

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.

Let's say you have the following models:

from django.db import models


class Project(models.Model):
    name = models.CharField(max_length=255)


class Task(models.Model):
    name = models.CharField(max_length=255)
    done = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="tasks")

If you wanted to create both a Task and its related Project in a single mutation, you could use a special related kind of MutationType as a Input reference.

from undine import Input, MutationType

from .models import Project, Task


class TaskProject(MutationType[Project], kind="related"): ...


class TaskCreateMutation(MutationType[Task]):
    project = Input(TaskProject)

This creates the following InputObjectTypes:

input TaskProject {
    pk: Int
    name: String
}

input TaskCreateMutation {
    name: String!
    done: Boolean = false
    project: TaskProject!
}

Related MutationTypes are only allowed as references Inputs, not in Entrypoints. They can be used to create, update, delete, or link existing related models, whether the "main" mutation is for creating or updating the main model. That's why all the fields in the the created TaskProject are nullable, even if the Project model requires the name field to be provided when creating a new Project.

Let's give a few examples. Assuming you added the TaskCreateMutation to our Schema with an Entrypoint create_task, you can create a new Task together with a new Project like this:

mutation {
  createTask(
    input: {
      name: "New task"
      project: {
        name: "New project"
      }
    }
  ) {
    pk
  }
}

Or you can link an existing Project to a new Task like this:

mutation {
  createTask(
    input: {
      name: "New task"
      project: {
        pk: 1
      }
    }
  ) {
    pk
    name
  }
}

Or you can link an existing project while modifying it:

mutation {
  createTask(
    input: {
      name: "New task"
      project: {
        pk: 1
        name: "Updated project"
      }
    }
  ) {
    pk
    name
  }
}

Since the project relation on the Task model is not nullable, the input is required, but if it was nullable, you could also unlink relations during update mutations like this:

mutation {
  updateTask(
    input: {
      pk: 1
      name: "Updated task"
      project: null
    }
  ) {
    pk
  }
}

Since the relation's nullability also affects whether the Input is required or not, a nullable relation can be left out during mutations. If left out during create mutations, the relation will be set to null, and in update mutations, the relation won't be updated.

Note that the total amount of objects that can be mutated in a single mutation is limited by the MUTATION_INSTANCE_LIMIT setting. This also affects bulk mutations.

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.

from undine import GQLInfo, Input, MutationType
from undine.exceptions import GraphQLPermissionError

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input()

    @name.permissions
    def name_permissions(self, info: GQLInfo, value: str) -> None:
        if not info.context.user.is_authenticated:
            msg = "Only authenticated users can set task names."
            raise GraphQLPermissionError(msg)
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.

from undine import GQLInfo, Input, MutationType
from undine.exceptions import GraphQLValidationError

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input()

    @name.validate
    def validate_name(self, info: GQLInfo, value: str) -> None:
        if len(value) < 3:
            msg = "Name must be at least 3 characters long."
            raise GraphQLValidationError(msg)
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.

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.

1
2
3
4
5
6
7
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input(default_value="New task")

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.

from typing import Any

from undine import GQLInfo, Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    logging_enabled = Input(bool, input_only=True)

    @classmethod
    def __validate__(cls, instance: Task, info: GQLInfo, input_data: dict[str, Any]) -> None:
        if input_data.get("logging_enabled"):
            print("Logging enabled")

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.

1
2
3
4
5
6
7
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input(hidden=True, default_value="New task")

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:

1
2
3
4
5
6
7
8
9
from undine import GQLInfo, Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    @Input
    def user(self, info: GQLInfo) -> int | None:
        return info.context.user.id

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.

1
2
3
4
5
6
7
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input(required=True)

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.

1
2
3
4
5
6
7
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    title = Input(field_name="name")

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.

1
2
3
4
5
6
7
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input(schema_name="title")

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.

1
2
3
4
5
6
7
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input(description="The name of the task.")

2) As class variable docstrings.

1
2
3
4
5
6
7
8
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input()
    """The name of the task."""

When using function references, instead of a class variable docstring, you add a docstring to the function/method used as the reference instead.

from undine import GQLInfo, Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    @Input
    def name(self, info: GQLInfo, value: str) -> str:
        """Name of the task."""
        return value.upper()

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.

1
2
3
4
5
6
7
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input(deprecation_reason="Use something else.")

Directives🔗

You can add directives to the Input by providing them using the directives argument.

from graphql import DirectiveLocation

from undine import Input, MutationType
from undine.directives import Directive

from .models import Task


class MyDirective(Directive, locations=[DirectiveLocation.INPUT_FIELD_DEFINITION]): ...


class TaskCreateMutation(MutationType[Task]):
    name = Input(directives=[MyDirective()])

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.

1
2
3
4
5
6
7
from undine import Input, MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task]):
    name = Input(extensions={"foo": "bar"})

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.