Queries🔗

In this section, we'll cover Undine's QueryTypes which allow you to expose your Django models through the GraphQL schema for querying, expanding on the basics introduced in the Tutorial.

If you need to query data outside of your Django models, see the function references section in the schema documentation.

QueryTypes🔗

A QueryType represents a GraphQL ObjectType for querying data from a Django model in the GraphQL schema. A basic QueryType is created by subclassing QueryType and adding a Django model to it as a generic type parameter:

1
2
3
4
5
6
from undine import QueryType

from .models import Task


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

Auto-generation🔗

By default, a QueryType automatically introspects its model and converts the model's fields to fields on the generated ObjectType. 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 ObjectType for the QueryType would be:

1
2
3
4
5
6
type TaskType {
    pk: Int!
    name: String!
    done: Boolean!
    createdAt: DateTime!
}

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 QueryType

from .models import Task


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

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

1
2
3
4
5
6
from undine import QueryType

from .models import Task


class TaskType(QueryType[Task], exclude=["name"]): ...

Filtering🔗

Results from QueryTypes can be filtered in two ways:

1) Adding a FilterSet to the QueryType. These are explained in detail in the Filtering section.

2) Defining a __filter_queryset__ classmethod. This method is used to filter all results returned by the QueryType. Use it to filter out items that should never be returned by the QueryType, e.g. archived items.

from django.db.models import QuerySet

from undine import GQLInfo, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    @classmethod
    def __filter_queryset__(cls, queryset: QuerySet, info: GQLInfo) -> QuerySet:
        return queryset.filter(archived=False)

Ordering🔗

Results from QueryTypes can be ordered in two ways:

1) Adding an OrderSet to the QueryType. These are explained in detail in the Ordering section.

2) Defining a __filter_queryset__ classmethod. Same as custom filtering, this is used for all results returned by the QueryType. However, since queryset ordering is reset when a new ordering is applied to the queryset, ordering added here serves as the default ordering for the QueryType, and is overridden if any ordering is applied using an OrderSet.

from django.db.models import QuerySet

from undine import GQLInfo, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    @classmethod
    def __filter_queryset__(cls, queryset: QuerySet, info: GQLInfo) -> QuerySet:
        return queryset.order_by("name")

Permissions🔗

You can add a permission check for querying data from a QueryType by adding a __permissions__ classmethod it.

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

from .models import Task


class TaskType(QueryType[Task]):
    @classmethod
    def __permissions__(cls, instance: Task, info: GQLInfo) -> None:
        if not info.context.user.is_authenticated:
            msg = "Only authenticated users can query tasks."
            raise GraphQLPermissionError(msg)

This method will be called for each instance of Task that is returned by this QueryType. For lists, this means that the method will be called for each item in the list.

Instead of raising an exception, you might want to filter out items the user doesn't have permission to access. You can do this using the __filter_queryset__ classmethod.

from django.db.models import QuerySet

from undine import GQLInfo, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    @classmethod
    def __filter_queryset__(cls, queryset: QuerySet, info: GQLInfo) -> QuerySet:
        if not info.context.user.is_authenticated:
            return queryset.none()
        return queryset

Now, when the QueryType is used in a list entrypoint or in "to-many" relations, items that the user doesn't have permission to access will be filtered out. For single-item entrypoints or "to-one" relations, a null value will be returned instead. Note that you'll need to manually check all Fields and Entrypoints where the QueryType is used and mark them as nullable if they would otherwise not be.

If your permissions check requires data from outside of the GraphQL execution context, you should check the Optimizer section on how you can make sure permissions checks don't cause an excessive database queries.

QueryType registry🔗

When a new QueryType is created, Undine automatically registers it for its given model. This allows other QueryTypes to look up the QueryType for linking relations (see relations), and MutationTypes to find out their matching output type (see mutation output types).

The QueryType registry only allows one QueryType to be registered for each model. During QueryType registration, if a QueryType is already registered for the model, an error will be raised.

If you need to create multiple QueryTypes for the same model, you can choose to not register a QueryType for the model by setting the register argument to False in the QueryType class definition.

1
2
3
4
5
6
7
8
9
from undine import QueryType

from .models import Task


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


class OtherTaskType(QueryType[Task], register=False): ...

You then need to use this QueryType explicitly when required.

Custom optimizations🔗

The optimizer is covered more thoroughly in the Optimizer section.

Usually touching the QueryType optimizations is not necessary, but if required, you can override the __optimizations__ classmethod on the QueryType to do so.

from undine import GQLInfo, QueryType
from undine.optimizer import OptimizationData

from .models import Task


class TaskType(QueryType[Task]):
    @classmethod
    def __optimizations__(cls, data: OptimizationData, info: GQLInfo) -> None:
        pass  # Some optimization here

This hook can be helpful when you require data from outside the GraphQL execution context to e.g. make permission checks.

Schema name🔗

By default, the name of the generated ObjectType for a QueryType is the same as the name of the QueryType class. If you want to change the name of the ObjectType, you can do so by setting the schema_name argument:

1
2
3
4
5
6
from undine import QueryType

from .models import Task


class TaskType(QueryType[Task], schema_name="Task"): ...

Description🔗

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

1
2
3
4
5
6
7
from undine import QueryType

from .models import Task


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

Interfaces🔗

You can add interfaces to the QueryType by providing them using the interfaces argument.

1
2
3
4
5
6
7
from undine import QueryType
from undine.relay import Node

from .models import Task


class TaskType(QueryType[Task], interfaces=[Node]): ...

See the Interfaces section for more details on interfaces.

Directives🔗

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

from graphql import DirectiveLocation

from undine import QueryType
from undine.directives import Directive

from .models import Task


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


class TaskType(QueryType[Task], directives=[MyDirective()]): ...

See the Directives section for more details on directives.

GraphQL extensions🔗

You can provide custom extensions for the QueryType by providing a extensions argument with a dictionary containing them. These can then be used however you wish to extend the functionality of the QueryType.

1
2
3
4
5
6
from undine import QueryType

from .models import Task


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

QueryType extensions are made available in the GraphQL ObjectType extensions after the schema is created. The QueryType itself is found in the extensions under a key defined by the QUERY_TYPE_EXTENSIONS_KEY setting.

Fields🔗

A Field is a class that is used to define a queryable value for a QueryType. Usually Fields correspond to fields on the Django model for their respective QueryType. In GraphQL, a Field represents a GraphQLField in an ObjectType.

A Field always requires a reference from which it will create the proper GraphQL resolver, output type, and arguments for the Field.

Model field references🔗

As seen in the QueryType 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 Fields to the QueryType class body. In this case, the Field can be used without a reference, as its attribute name in the QueryType class body can be used to identify the corresponding model field.

1
2
3
4
5
6
7
from undine import Field, QueryType

from .models import Task


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

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 Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    name = Field("name")

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

1
2
3
4
5
6
7
from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    name = Field(Task.name)

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

1
2
3
4
5
6
7
from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    title = Field("name")

Expression references🔗

Django ORM expressions can also be used as the references. These create an annotation on the model instances when fetched.

1
2
3
4
5
6
7
8
9
from django.db.models.functions import Upper

from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    upper_name = Field(Upper("name"))

Remember that subqueries are also counted as expressions.

from django.db.models import OuterRef

from undine import Field, QueryType
from undine.utils.model_utils import SubqueryCount

from .models import Task


class TaskType(QueryType[Task]):
    copies = Field(SubqueryCount(Task.objects.filter(name=OuterRef("name"))))

Function references🔗

Functions (or methods) can also be used to create Fields. This can be done by decorating a method with the Field class.

1
2
3
4
5
6
7
8
9
from undine import Field, GQLInfo, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    @Field
    def greeting(self, info: GQLInfo) -> str:
        return "Hello World!"

The Field will use the decorated method as its GraphQL resolver. The method's return type will be used as the output type for the Field, so typing it is required. You can even use a TypedDict to return an object with multiple fields.

About method signature

The decorated method is treated as a static method by the Field.

The self argument is not an instance of the QueryType, but root argument of the GraphQLField resolver. To clarify this, it's recommended to change the argument's name to root, as defined by the RESOLVER_ROOT_PARAM_NAME setting.

The value of the root argument for a Field is the model instance being queried.

The info argument can be left out, but if it's included, it should always have the GQLInfo type annotation.

Arguments added to the function signatures will be added as Field arguments in the GraphQL schema. Typing these arguments is required to determine their input type.

1
2
3
4
5
6
7
8
9
from undine import Field, GQLInfo, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    @Field
    def greeting(self, info: GQLInfo, *, name: str) -> str:
        return f"Hello, {name}!"

If the method requires fields from the root instance, you should add custom optimization rules for the Field so that the fields are available when the resolver is called. See custom optimizations for how to add these, although it might be simpler to use a Calculation reference.

Calculation references🔗

A Calculation reference is like a combination of function references and expression references. They can accept data from input arguments like a function reference, and return an expression that should be annotated to a queryset like an expression reference. A Calculation references can be created by subclassing the Calculation class and adding the required CalculationArguments to its class body.

from django.db.models import Value

from undine import Calculation, CalculationArgument, DjangoExpression, Field, GQLInfo, QueryType

from .models import Task


class ExampleCalculation(Calculation[int]):
    value = CalculationArgument(int)

    def __call__(self, info: GQLInfo) -> DjangoExpression:
        # Some impressive calculation here
        return Value(self.value)


class TaskType(QueryType[Task]):
    calc = Field(ExampleCalculation)

Calculation objects always require the generic type argument to be set, which describes the return type of the calculation. This should be a python type matching the expression that is returned in the __call__ method.

CalculationArguments can be defined in the class body of the Calculation class. These define the input arguments for the calculation. When the calculation is executed, the CalculationArguments can be used to access the input data for that specific argument.

The __call__ method should always be defined in the Calculation class. This should return a Django ORM expression that can be annotated to a queryset. You may access other fields using F-expressions and use request-specific data from the info argument.

The Field will look like this in the GraphQL schema:

1
2
3
type TaskType {
    calc(value: Int!): Int!
}

A Calculation reference is a good replacement for a function reference when the calculation is expensive enough that resolving it for each field would be slow. However, the calculation needs to be able to be executed in the database since __call__ needs to return a Django ORM expression to be annotated to a queryset.

A Calculation reference is a good replacement for an expression reference when the expression requires input data from the request.

Relations🔗

Let's say there is a Task model with a ForeignKey to a Project model:

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)

You can then create QueryTypes for both models.

1
2
3
4
5
6
7
8
9
from undine import QueryType

from .models import Project, Task


class ProjectType(QueryType[Project]): ...


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

When auto is used for the QueryTypes they will be automatically linked together in the GraphQL schema by their relations:

type ProjectType {
    pk: Int!
    name: String!
    tasks: [TaskType!]!
}

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

You could also link them explicitly by using the QueryTypes as the reference.

from undine import Field, QueryType

from .models import Project, Task


class ProjectType(QueryType[Project]):
    tasks = Field(lambda: TaskType, many=True)  # lazy evaluation


class TaskType(QueryType[Task]):
    project = Field(ProjectType)

In this case, if the name of the field in the GraphQL schema is different from the model field name, you can use the field_name argument to specify the name in the GraphQL schema.

from undine import Field, QueryType

from .models import Project, Task


class ProjectType(QueryType[Project]): ...


class TaskType(QueryType[Task]):
    project = Field(ProjectType, field_name="task_list")

Permissions🔗

You can add a permissions for querying any data from an individual Field by decorating a method with @<field_name>.permissions.

from undine import Field, GQLInfo, QueryType
from undine.exceptions import GraphQLPermissionError

from .models import Task


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

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

If Field permissions are defined for a related field, the related QueryType permissions are overridden by the Field permissions.

from undine import Field, GQLInfo, QueryType
from undine.exceptions import GraphQLPermissionError

from .models import Project, Task


class ProjectType(QueryType[Project]):
    @classmethod
    def __permissions__(cls, instance: Project, info: GQLInfo) -> None:
        # Not called if 'ProjectType' is accessed from 'TaskType.project'
        # because it has a permissions check already
        if not info.context.user.is_superuser:
            raise GraphQLPermissionError


class TaskType(QueryType[Task]):
    project = Field()

    @project.permissions
    def project_permissions(self, info: GQLInfo, value: Project) -> None:
        if not info.context.user.is_authenticated:
            raise GraphQLPermissionError

Instead of raising an exception, you might want a failed permission check to result in a null value instead of an error. You can do this overriding the Field's resolver and manually checking the permissions there, returning None when permission is denied. Note that you'll need to manually set the Field as nullable if it would otherwise not be.

from undine import Field, GQLInfo, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    name = Field(nullable=True)

    @name.resolve
    def name_resolver(self, info: GQLInfo) -> str | None:
        if not info.context.user.is_authenticated:
            return None
        return self.name

Descriptions🔗

By default, a Field 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, you can provide a description in one of two ways:

1) By setting the description argument.

1
2
3
4
5
6
7
from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    name = Field(description="The name of the task.")

2) As class attribute docstrings.

1
2
3
4
5
6
7
8
from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    name = Field()
    """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 Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    @Field
    def testing(self, name: str) -> str:
        """
        Return a greeting.

        :param name: The name to greet.
        """
        return f"Hello, {name}!"

Many🔗

By default, a Field is able to determine whether it returns a list of items based on its reference. For example, for a model field, a ManyToManyField will return a list of items. If you want to configure this manually, you can do so by adding the many argument to the Field.

1
2
3
4
5
6
7
from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    name = Field(many=False)

Nullable🔗

By default, a Field is able to determine whether it's nullable or not based on its reference. For example, for a model field, nullability is determined from its null attribute. If you want to configure this manually, you can do so by adding the nullable argument to the Field.

1
2
3
4
5
6
7
from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    name = Field(nullable=False)

Complexity🔗

The complexity value of a Field is used by Undine to calculate how expensive a given query to the schema would be. Queries are rejected by Undine if they would exceed the maximum allowed complexity, as set by the MAX_QUERY_COMPLEXITY setting.

By default, a Field is able to determine its complexity based on its reference. For example, a related field has a complexity of 1, and a regular model field has a complexity of 0. If you want to configure this manually, you can do so by adding the complexity argument to the Field.

1
2
3
4
5
6
7
from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    name = Field(complexity=1)

Field name🔗

A field_name can be provided to explicitly set the Django model field name that the Field corresponds to. This can be useful when you need multiple Fields for the same model field, or 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 Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    title = Field(field_name="name")

Schema name🔗

A schema_name can be provided to override the name of the Field 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 Field attribute name.

1
2
3
4
5
6
7
from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    name = Field(schema_name="title")

Deprecation reason🔗

A deprecation_reason can be provided to mark the Field 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 Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    name = Field(deprecation_reason="Use something else.")

Custom resolvers🔗

Usually using a custom Field resolver is not necessary, and should be avoided if possible. This is because most modifications to resolvers can result in canceling query optimizations (see the optimizer section for details).

You can override the resolver for a Field by adding a method to the class body of the QueryType and decorating it with the @<field_name>.resolve decorator.

from undine import Field, GQLInfo, QueryType

from .models import Task


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

    @name.resolve
    def resolve_name(self, info: GQLInfo) -> str:
        return self.name.upper()
About method signature

The decorated method is treated as a static method by the Field.

The self argument is not an instance of the QueryType, but root argument of the GraphQLField resolver. To clarify this, it's recommended to change the argument's name to root, as defined by the RESOLVER_ROOT_PARAM_NAME setting.

The value of the root argument for a Field is the model instance being queried.

The info argument can be left out, but if it's included, it should always have the GQLInfo type annotation.

Custom optimizations🔗

The optimizer is covered more thoroughly in the Optimizer section.

Usually touching the Field optimizations is not necessary, but if required, you can do so by adding a method to the class body of the QueryType and decorating it with the @<field_name>.optimize decorator.

from undine import Field, GQLInfo, QueryType
from undine.optimizer import OptimizationData

from .models import Task


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

    @name.optimize
    def optimize_name(self, data: OptimizationData, info: GQLInfo) -> None:
        pass  # Some optimization here

This hook can be helpful when you require data from outside the GraphQL execution context to e.g. make permission checks.

Directives🔗

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

from graphql import DirectiveLocation

from undine import Field, QueryType
from undine.directives import Directive

from .models import Task


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


class TaskType(QueryType[Task]):
    name = Field(directives=[MyDirective()])

See the Directives section for more details on directives.

GraphQL extensions🔗

You can provide custom extensions for the Field by providing a extensions argument with a dictionary containing them. These can then be used however you wish to extend the functionality of the Field.

1
2
3
4
5
6
7
from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    name = Field(extensions={"foo": "bar"})

Field extensions are made available in the GraphQL ObjectType extensions after the schema is created. The QueryType itself is found in the extensions under a key defined by the QUERY_TYPE_EXTENSIONS_KEY setting.