---
description: Documentation on queries in Undine.
---

# Queries

In this section, we'll cover Undine's [`QueryTypes`](#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](schema.md#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`](#fields) to the class body of the `QueryType`.

```python
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:

```python
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:

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

To use auto-generation, either set [`AUTOGENERATION`](settings.md#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.

```python
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:

```python
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`](schema.md#entrypoints)
or a many-related [`Field`](#fields), 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](filtering.md) 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.

```python
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`](schema.md#entrypoints)
or a many-related [`Field`](#fields), 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](ordering.md) section.

2) Defining a `__filter_queryset__` classmethod.
   Same as custom [filtering](#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`.

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

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

```python
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](optimizer.md) 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](#relations)), and `MutationTypes` to find out their matching output type
(see [mutation output types](mutations.md#output-type)).

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.

```python
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](optimizer.md) section.

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

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

from .models import Task


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

    @classmethod
    def __optimize__(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:

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

```python
from undine import Field, QueryType

from .models import Task


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

    name = Field()
```

### Caching

You can set custom caching rules for `QueryTypes` using the `cache_time`
and `cache_per_user` arguments.

```python
from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task], cache_time=10, cache_per_user=True):
    name = Field()
```

See the [Caching](caching.md) section for more details.

### Interfaces

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

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

```python
from undine import Field, QueryType
from undine.relay import Node

from .models import Task


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

See the [Interfaces](interfaces.md) 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.

```python
from graphql import DirectiveLocation

from undine import Directive, Field, QueryType

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.

```python
from graphql import DirectiveLocation

from undine import Directive, Field, QueryType

from .models import Task


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


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

See the [Directives](directives.md) 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`.

```python
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`](settings.md#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.

```python
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:

```python
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:

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

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

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

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

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

/// details | 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`](settings.md#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.

```python
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](#custom-optimizations) for how to add these, although it might be
> simpler to use a [Calculation reference](#calculation-references).

### Calculation references

A `Calculation` reference is like a combination of [function references](#function-references) and
[expression references](#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.

```python
from django.db.models import Value

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

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:

```graphql
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:

```python hl_lines="4 5 13"
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.

```python hl_lines="9 17"
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:

```graphql hl_lines="4 12"
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`.

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

```python
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](#custom-resolvers) and manually checking the permissions there,
returning `None` when permission is denied. Note that you'll need to manually set the
`Field` as [nullable](#nullable) if it would otherwise not be.

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

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

```python
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`](settings.md#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`.

```python
from undine import Field, QueryType

from .models import Task


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

> Note that complexity tracks how many database queries an operation runs, not how many rows it returns.
> Nested to-many relations are bounded separately by the
> [`MaxListNestingDepthRule`](validation-rules.md#maxlistnestingdepthrule).

### Caching

You can set custom caching rules for `Fields` using the `cache_time`
and `cache_per_user` arguments.

```python
from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    name = Field(cache_time=10, cache_per_user=True)
```

See the [Caching](caching.md) section for more details.

### Field name

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

```python
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`](settings.md#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:

```python hl_lines="13"
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](#model-field-references), 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.

```python
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`](settings.md#enable_class_attribute_docstrings) is enabled.

```python
from undine import Field, QueryType

from .models import Task


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

When using [function references](#function-references), instead of a class attribute docstring,
you add a docstring to the function/method used as the reference instead.

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

```python hl_lines="13"
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](#custom-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.

```python
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()
```

/// details | 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`](settings.md#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](optimizer.md) 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.

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

### Errors as data

By default, when an error occurs during the execution of a field, it follows the
GraphQL error handling rules as defined in the [GraphQL spec][error-spec]{:target="_blank"}.
If the `Field` where the error occurs is not nullable, the error will cascade up
until it finds a nullable field, all the way up to the root. This behavior can prevent the client
from receiving partial results in case it can handle them. However, simply making all fields nullable
makes handling of the values a pain on the client with null values everywhere.

[error-spec]: https://spec.graphql.org/September2025/#sec-Handling-Execution-Errors

While there are many solutions to this problem ([`@semanticNonNull`][semantic]{:target="_blank"},
[`graphql-toe`][graphql-toe]{:target="_blank"}), Undine also provides support for
the ["Error as Data"]{:target="_blank"} pattern, where errors are represented
in the `Field's` type as a union. To use this on a field, simply add the exceptions that
a field can raise to the `errors` argument of the `Field`.

[semantic]: https://relay.dev/docs/guides/semantic-nullability/
[graphql-toe]: https://github.com/graphile/graphql-toe
["Error as Data"]: https://www.apollographql.com/docs/graphos/schema-design/guides/errors-as-data-explained

```python
import random

from graphql import GraphQLError

from undine import Field, QueryType

from .models import Task


class TaskType(QueryType[Task]):
    @Field(errors=[GraphQLError])
    def example(self) -> str:
        if random.random() > 0.5:
            msg = "Failed"
            raise GraphQLError(msg)
        return "OK"
```

This creates the following schema:

```graphql
type TaskTypeExampleValue {
    value: String!
}

type GraphQLError {
    message: String!
}

union TaskTypeExample = TaskTypeExampleValue | GraphQLError

type TaskType {
    example: TaskTypeExample!
}
```

You can query this field like this:

```graphql
query {
  task(id: 1) {
    example {
       __typename
      ... on TaskTypeExampleValue {
        value
      }
      ... on GraphQLError {
        message
      }
    }
  }
}
```

You'll receive either the successful response:

```json
{
  "data": {
    "task": {
      "example": {
        "__typename": "TaskTypeExampleValue",
        "value": "OK"
      }
    }
  }
}
```

Or the error response:

```json
{
  "data": {
    "task": {
      "example": {
        "__typename": "GraphQLError",
        "message": "Failed"
      }
    }
  }
}
```

The way this works is that the `Field` resolver is wrapped in a middleware that catches any raised exceptions that are
in the list, or are those exceptions' subclasses, and returns them as data instead.

If you raise custom exceptions, you can add two static methods to your Exception class
to customize the GraphQL fields available from the error type: `graphql_fields` and `graphql_resolve`.

```python
from __future__ import annotations

import datetime
import random
from typing import TYPE_CHECKING, Any

from graphql import GraphQLField, GraphQLNonNull, GraphQLString

from undine import Field, QueryType
from undine.scalars import GraphQLDateTime

from .models import Task

if TYPE_CHECKING:
    from undine import GQLInfo


class TimestampedError(Exception):
    def __init__(self, message: str, /, *, timestamp: datetime.datetime) -> None:
        self.timestamp = timestamp
        super().__init__(message)

    @staticmethod
    def graphql_fields() -> dict[str, GraphQLField]:
        return {
            "message": GraphQLField(GraphQLNonNull(GraphQLString)),
            "timestamp": GraphQLField(GraphQLNonNull(GraphQLDateTime)),
        }

    @staticmethod
    def graphql_resolve(root: TimestampedError, info: GQLInfo, **kwargs: Any) -> dict[str, Any]:
        return {
            "message": str(root),
            "timestamp": root.timestamp,
        }


class TaskType(QueryType[Task]):
    @Field(errors=[TimestampedError])
    def example(self) -> str:
        if random.random() > 0.5:
            msg = "Failed"
            raise TimestampedError(msg, timestamp=datetime.datetime.now(tz=datetime.UTC))
        return "OK"
```

This will result in the following schema:

```graphql
type TaskTypeExampleValue {
    value: String!
}

type TimestampedError {
    message: String!
    timestamp: DateTime!
}

union TaskTypeExample = TaskTypeExampleValue | TimestampedError

type TaskType {
    example: TaskTypeExample!
}
```

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

```python
from graphql import DirectiveLocation

from undine import Directive, Field, QueryType

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

```python
from graphql import DirectiveLocation

from undine import Directive, Field, QueryType

from .models import Task


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


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

See the [Directives](directives.md) 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`.

```python
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`](settings.md#query_type_extensions_key)
setting.
