Queries๐Ÿ”—

In this section, we'll cover Undine's QueryTypes which allow you to expose your Django Models through the GraphQL schema for querying.

For queries not concerning 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. You must also add at least one Field to the class body of the QueryType.

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

from .models import Task


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

Auto-generation๐Ÿ”—

A QueryType can automatically introspect its Django Model and convert the Model's fields to Fields on the QueryType. 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 using auto-generation would be:

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

To use auto-generation, either set AUTOGENERATION setting to True to enable it globally, or set the auto argument to True in the QueryType class definition. With this, you can leave the QueryType class body empty, as fields will be automatically added based on the Django Model.

1
2
3
4
5
6
from undine import QueryType

from .models import Task


class TaskType(QueryType[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 QueryType

from .models import Task


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

Filtering๐Ÿ”—

When a QueryType is used to return multiple items, either in a list Entrypoint or a many-related Field, its results can be filtered in one of 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 will always be called even if no other filtering is applied to the results. 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 Field, GQLInfo, QueryType

from .models import Task


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

    @classmethod
    def __filter_queryset__(cls, queryset: QuerySet, info: GQLInfo) -> QuerySet:
        return queryset.filter(archived=False)

Ordering๐Ÿ”—

When a QueryType is used to return multiple items, either in a list Entrypoint or a many-related Field, its results can be ordered in one of 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 ordering is always applied to 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 Field, GQLInfo, QueryType

from .models import Task


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

    @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 Field, GQLInfo, QueryType
from undine.exceptions import GraphQLPermissionError

from .models import Task


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

    @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 in Entrypoints or related Fields.

You can raise any GraphQLError when a permission check fails, but it's recommended to raise a GraphQLPermissionError from the undine.exceptions module.

Instead of raising an exception from a permission check, 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 Field, GQLInfo, QueryType

from .models import Task


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

    @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 many related Field, 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 related Fields (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 in the registry by setting the register argument to False in the QueryType class definition.

from undine import Field, QueryType

from .models import Task


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


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

You'll then need to use this QueryType explicitly when required.

Custom optimizations๐Ÿ”—

The optimizer is covered more thoroughly in the Optimizer section.

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

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

from .models import Task


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

    @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 GraphQL ObjectType for a QueryType class is the name of the QueryType 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 Field, QueryType

from .models import Task


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

Description๐Ÿ”—

You can provide a description for the QueryType by adding a docstring to the class.

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

from .models import Task


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

    name = Field()

Interfaces๐Ÿ”—

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

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

from .models import Task


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

You can also add interfaces using decorator syntax.

1
2
3
4
5
6
7
8
9
from undine import Field, QueryType
from undine.relay import Node

from .models import Task


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

See the Interfaces section for more details on interfaces.

Directives๐Ÿ”—

You can add directives to the QueryType by providing them using the directives argument. The directive must be usable in the OBJECT location.

from graphql import DirectiveLocation

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

from .models import Task


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


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

You can also add directives using decorator syntax.

from graphql import DirectiveLocation

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

from .models import Task


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


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

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 QueryType from certain users by using the __is_visible__ method. Hiding the QueryType 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 Field using the QueryType didn't exist in the first place.

from undine import Field, QueryType
from undine.typing import DjangoRequestProtocol

from .models import Task


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

    @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 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
7
from undine import Field, QueryType

from .models import Task


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

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

Fields๐Ÿ”—

A Field is used to define a queryable value on a QueryType. Usually Fields correspond to fields on the Django Model for their respective QueryType. In GraphQL, a Field represents a GraphQLField on 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๐Ÿ”—

For Fields corresponding to Django Model fields, the Field can be used without passing in 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 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 Django ORM 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 and add Fields for the related fields.

from undine import Field, QueryType

from .models import Project, Task


class ProjectType(QueryType[Project]):
    pk = Field()
    name = Field()
    tasks = Field()


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

The QueryTypes will be 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!
}

Undine will make sure that queries between the relations are optimized.

Permissions๐Ÿ”—

You can add a permission check 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

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 Model 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 that the Field corresponds to.

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

This can be useful when the Field has a different name and type in the GraphQL schema than in the Model.

Schema name๐Ÿ”—

By default, the name of the ObjectType field generated from a Field is the same as the name of the Field on the QueryType class (converted to camelCase if CAMEL_CASE_SCHEMA_FIELDS is enabled). If you want to change the name of the ObjectType field separately, you can do so by setting the schema_name argument:

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

This can be useful when the desired name of the ObjectType field is a Python keyword and cannot be used as the Field attribute 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, if ENABLE_CLASS_ATTRIBUTE_DOCSTRINGS is enabled.

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 attribute 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}!"

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 use of Model fields in resolvers without additional optimizations can result in a lot of database queries.

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. The directive must be usable in the FIELD_DEFINITION location.

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

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

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() @ 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 a Field from certain users by decorating a method with the <field_name>.visible decorator. Hiding a Field 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 Field didn't exist in the first place.

from undine import Field, QueryType
from undine.typing import DjangoRequestProtocol

from .models import Task


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

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

The self argument is not an instance of the QueryType, but the instance of the Field 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 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 field extensions after the schema is created. The QueryType itself is found in the GraphQL field extensions under a key defined by the QUERY_TYPE_EXTENSIONS_KEY setting.