Unions๐Ÿ”—

In this section, we'll cover how GraphQL Unions work in Undine. Unions are abstract GraphQL types that represent a group of ObjectTypes that need to be returned together, e.g. for a search result.

UnionType๐Ÿ”—

In Undine, a GraphQL Union between two or more QueryTypes is implemented using a UnionType. The QueryTypes in the Union should be added as generic type parameters to the UnionType.

from undine import QueryType, UnionType

from .models import Project, Task


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


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


class SearchObjects(UnionType[TaskType, ProjectType]): ...

Usage in Entrypoints๐Ÿ”—

An Entrypoint created using a UnionType as the reference will return all instances of all QueryTypes it contains.

from undine import Entrypoint, QueryType, RootType, UnionType

from .models import Project, Task


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


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


class SearchObjects(UnionType[TaskType, ProjectType]): ...


class Query(RootType):
    search_objects = Entrypoint(SearchObjects, many=True)

This Entrypoint can be queried like this:

query {
  searchObjects {
    ... on TaskType {
      name
    }
    ... on ProjectType {
      name
    }
    __typename
  }
}

Filtering๐Ÿ”—

By default, an Entrypoint for a UnionType will return all instances of all QueryTypes it contains. However, if those QueryTypes implement a FilterSet or an OrderSet, those will also be available on the UnionType Entrypoint.

from undine import Entrypoint, FilterSet, OrderSet, QueryType, RootType, UnionType

from .models import Project, Task


class TaskFilterSet(FilterSet[Task]): ...


class TaskOrderSet(OrderSet[Task]): ...


@TaskFilterSet
@TaskOrderSet
class TaskType(QueryType[Task]): ...


class ProjectFilterSet(FilterSet[Project]): ...


class ProjectOrderSet(OrderSet[Project]): ...


@ProjectFilterSet
@ProjectOrderSet
class ProjectType(QueryType[Project]): ...


class SearchObjects(UnionType[TaskType, ProjectType]): ...


class Query(RootType):
    search_objects = Entrypoint(SearchObjects, many=True)

This creates the following Entrypoint:

1
2
3
4
5
6
7
8
type Query {
  searchObjects(
    filterTask: TaskFilterSet
    orderByTask: [TaskOrderSet!]
    filterProject: ProjectFilterSet
    orderByProject: [ProjectOrderSet!]
  ): [Commentable!]!
}

This allows filtering and ordering the different types of models in the UnionType separately.

To filter and order across different Models in the Union, you can implement a FilterSet or an OrderSet for the same Models as the QueryTypes in the UnionType and add it to the UnionType.

from undine import Entrypoint, FilterSet, OrderSet, QueryType, RootType, UnionType

from .models import Project, Task


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


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


class SearchObjectsFilterSet(FilterSet[Task, Project]): ...


class SearchObjectsOrderSet(OrderSet[Task, Project]): ...


@SearchObjectsFilterSet
@SearchObjectsOrderSet
class SearchObjects(UnionType[TaskType, ProjectType]): ...


class Query(RootType):
    search_objects = Entrypoint(SearchObjects, many=True)

This creates the following Entrypoint:

1
2
3
4
5
6
type Query {
  searchObjects(
    filter: SearchObjectsFilterSet
    orderBy: [SearchObjectsOrderSet!]
  ): [Commentable!]!
}

Note that a FilterSet or OrderSet created for multiple Models like this should only contain Filters and Orders which will work on all Models in the UnionType. For example, a "name" Filter can be added to the FilterSet if all Models contain a "name" field of type CharField.

Pagination๐Ÿ”—

UnionTypes can be paginated just like any QueryType.

from undine import Entrypoint, QueryType, RootType, UnionType
from undine.relay import Connection

from .models import Project, Task


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


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


class SearchObjects(UnionType[TaskType, ProjectType]): ...


class Query(RootType):
    search_objects = Entrypoint(Connection(SearchObjects))

See the Pagination section for more details on pagination.

Schema name๐Ÿ”—

By default, the name of the generated GraphQL Union for a UnionType class is the name of the UnionType class. If you want to change the name separately, you can do so by setting the schema_name argument:

from undine import QueryType, UnionType

from .models import Project, Task


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


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


class SearchObjects(UnionType[TaskType, ProjectType], schema_name="Search"): ...

Description๐Ÿ”—

A description for a UnionType can be provided as a docstring.

from undine import QueryType, UnionType

from .models import Project, Task


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


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


class SearchObjects(UnionType[TaskType, ProjectType]):
    """Description"""

Directives๐Ÿ”—

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

from graphql import DirectiveLocation

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

from .models import Project, Task


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


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


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


class SearchObjects(UnionType[TaskType, ProjectType], directives=[MyDirective()]): ...

You can also add directives using decorator syntax.

from graphql import DirectiveLocation

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

from .models import Project, Task


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


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


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


@MyDirective()
class SearchObjects(UnionType[TaskType, ProjectType]): ...

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

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

from .models import Project, Task


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


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


class SearchObjects(UnionType[TaskType, ProjectType]):
    @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 UnionType by providing an extensions argument with a dictionary containing them.

from undine import QueryType, UnionType

from .models import Project, Task


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


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


class SearchObjects(UnionType[TaskType, ProjectType], extensions={"foo": "bar"}): ...

UnionType extensions are made available in the GraphQL Union extensions after the schema is created. The UnionType itself is found in the GraphQL Union extensions under a key defined by the UNION_TYPE_EXTENSIONS_KEY setting.