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.

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

from .models import Task


class TaskMutation(MutationType[Task]):
    name = Input()
    done = Input()

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.

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

from .models import Task


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

kind can also be omitted, in which case the MutationType will determine the mutation kind using these rules:

  1. If the word create can be found in the name of the MutationType, kind will be create.
  2. If the word update can be found in the name of the MutationType, kind will be update.
  3. If the word delete can be found in the name of the MutationType, kind will be delete.
  4. If either the __mutate__ or __bulk_mutate__ method has been defined on the MutationType, kind will be custom.
  5. Otherwise, an error will be raised.
1
2
3
4
5
6
7
8
from undine import Input, MutationType

from .models import Task


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

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:

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 using auto-generation 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. This is essentially a fully partial update mutation.

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 instance to delete.

1
2
3
input TaskDeleteMutation {
    pk: Int!
}

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.

1
2
3
4
5
6
from undine import MutationType

from .models import Task


class TaskCreateMutation(MutationType[Task], auto=True): ...

You can exclude some Model fields 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], auto=True, exclude=["name"]): ...

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.

from undine import Entrypoint, Field, Input, MutationType, QueryType, RootType

from .models import Task


# This QueryType is registered and then used
# by TaskCreateMutation as its output type since
# they have the same Django Model.
class TaskType(QueryType[Task]):
    pk = Field()
    name = Field()
    done = Field()
    created_at = Field()


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


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

This would create 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 Field, Input, MutationType, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    pk = Field()
    name = Field()
    done = Field()
    created_at = Field()


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

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

If you wanted a fully custom output type, you can override the __output_type__ classmethod.

from typing import Any

from graphql import GraphQLField, GraphQLInt, GraphQLNonNull, GraphQLObjectType

from undine import GQLInfo, Input, MutationType
from undine.utils.graphql.type_registry import get_or_create_graphql_object_type

from .models import Task


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

    @classmethod
    def __mutate__(cls, instance: Task, info: GQLInfo, input_data: dict[str, Any]) -> dict[str, Any]:
        return {"foo": 1}

    @classmethod
    def __output_type__(cls) -> GraphQLObjectType:
        fields = {"foo": GraphQLField(GraphQLNonNull(GraphQLInt))}
        return get_or_create_graphql_object_type(name="TaskMutationOutput", fields=fields)

Permissions๐Ÿ”—

You can add mutation-level permission checks to mutations executed using a MutationType by defining the __permissions__ classmethod.

from typing import Any

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

from .models import Task


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

    @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. 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.

from typing import Any

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

from .models import Task


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

    @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. 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.

from typing import Any

from undine import GQLInfo, Input, MutationType

from .models import Task


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

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

from typing import Any

from undine import GQLInfo, Input, MutationType

from .models import Task


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

    @classmethod
    def __mutate__(cls, instance: Task, info: GQLInfo, input_data: dict[str, Any]) -> Task:
        # Some custom mutation logic here
        return instance

    @classmethod
    def __bulk_mutate__(cls, instances: list[Task], info: GQLInfo, input_data: list[dict[str, Any]]) -> list[Task]:
        # Some custom bulk mutation logic here
        return instances

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.

from typing import Any

from undine import GQLInfo, Input, MutationType

from .models import Task


class TaskMutation(MutationType[Task], kind="custom"):
    name = Input()

    @classmethod
    def __mutate__(cls, instance: Task, info: GQLInfo, input_data: dict[str, Any]) -> Task:
        # Some custom mutation logic here
        return instance

This affects the creation of the MutationType in the following ways:

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.

from typing import Any

from graphql import GraphQLField, GraphQLInt, GraphQLNonNull, GraphQLObjectType

from undine import GQLInfo, Input, MutationType
from undine.utils.graphql.type_registry import get_or_create_graphql_object_type

from .models import Task


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

    @classmethod
    def __mutate__(cls, instance: Task, info: GQLInfo, input_data: dict[str, Any]) -> dict[str, Any]:
        return {"foo": 1}

    @classmethod
    def __output_type__(cls) -> GraphQLObjectType:
        fields = {"foo": GraphQLField(GraphQLNonNull(GraphQLInt))}
        return get_or_create_graphql_object_type(name="TaskMutationOutput", fields=fields)

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 link two mutation types using a special related kind of MutationType.

from undine import Input, MutationType

from .models import Project, Task


class TaskProject(MutationType[Project], kind="related"):
    pk = Input()
    name = Input()


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

This creates the following InputObjectTypes:

input TaskProject {
    pk: Int
    name: String
}

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

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:

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
  }
}

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).

from typing import Any

from undine import GQLInfo, Input, MutationType

from .models import Project, Task


class TaskProject(MutationType[Project], kind="related"):
    pk = Input()
    name = Input()

    @classmethod
    def __permissions__(cls, instance: Project, info: GQLInfo, input_data: dict[str, Any]) -> None:
        # Some permission check logic here
        return


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

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.

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.
from undine import Input, MutationType

from .models import Project, Task


class ProjectTask(MutationType[Task], kind="related"):
    pk = Input()
    name = Input()


class ProjectUpdateMutation(MutationType[Project], related_action="delete"):
    pk = Input()
    name = Input()
    done = Input()
    tasks = Input(ProjectTask)

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:

  1. Model inputs have their Model instances fetched.
  2. Hidden inputs are be added to the input data.
  3. Function inputs are run.
  4. MutationType permissions and Input permissions are checked.
  5. MutationType validation and Input validation are run.
  6. Input-only inputs are removed from the input data.
  7. Mutation is executed.
  8. MutationType after 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
{
    "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 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:

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

from .models import Task


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

Description๐Ÿ”—

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

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

from .models import Task


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

    name = Input()

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.

from graphql import DirectiveLocation

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

from .models import Task


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


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

You can also add them using the decorator syntax.

from graphql import DirectiveLocation

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

from .models import Task


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


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

See the Directives section for more details on directives.

Visibility๐Ÿ”—

This is an experimental feature that needs to be enabled using the EXPERIMENTAL_VISIBILITY_CHECKS setting.

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.

from undine import Input, MutationType
from undine.typing import DjangoRequestProtocol

from .models import Task


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

    @classmethod
    def __is_visible__(cls, request: DjangoRequestProtocol) -> bool:
        return request.user.is_superuser

When using visibility checks, you should also disable "did you mean" suggestions using the ALLOW_DID_YOU_MEAN_SUGGESTIONS setting. 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.

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

from .models import Task


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

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.

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)

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

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

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.

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

from .models import Project, Task


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

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.

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.

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.

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.

from undine import Input, MutationType

from .models import Task


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

    @name.convert
    def convert_name(self, value: str) -> str:
        return value.upper()
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.

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.

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.

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.

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.

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:

from django.contrib.auth.models import User

from undine import GQLInfo, Input, MutationType

from .models import Task


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

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.

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

from .models import Task


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

Note that due to GraphQL implementation details, there is no distinction between required and nullable. Therefore, non-required Inputs can always accept null values, and required inputs cannot accept null values.

Field name๐Ÿ”—

A field_name can be provided to explicitly set the Django Model field that the Input corresponds to.

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")

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:

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")

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.

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 attribute docstrings, if ENABLE_CLASS_ATTRIBUTE_DOCSTRINGS is enabled.

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 attribute 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 as deprecated. 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. The directive must be usable in the INPUT_FIELD_DEFINITION location.

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()])

You can also add them using the @ operator (which kind of looks like GraphQL syntax):

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() @ MyDirective()

See the Directives section for more details on directives.

Visibility๐Ÿ”—

This is an experimental feature that needs to be enabled using the EXPERIMENTAL_VISIBILITY_CHECKS setting.

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.

from undine import Input, MutationType
from undine.typing import DjangoRequestProtocol

from .models import Task


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

    @name.visible
    def name_visible(self, request: DjangoRequestProtocol) -> bool:
        return request.user.is_superuser
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_SUGGESTIONS setting. 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.

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