Interfaces๐Ÿ”—

In this section, we'll cover how GraphQL Interfaces work in Undine. Interfaces are abstract GraphQL types that represent a group of fields that an ObjectType can implement.

InterfaceType๐Ÿ”—

In Undine, a GraphQL Interface is implemented using the InterfaceType class and defining a number of InterfaceFields in its class body.

1
2
3
4
5
6
7
from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))

QueryTypes can implement InterfaceTypes by adding them to the QueryType using the interfaces argument in their class definition.

from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType, QueryType

from .models import Task


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))


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

You can also use decorator syntax to add an InterfaceType to a QueryType.

from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType, QueryType

from .models import Task


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))


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

Note that InterfaceTypes can also implement other InterfaceTypes.

from graphql import GraphQLInt, GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))


class Person(InterfaceType, interfaces=[Named]):
    age = InterfaceField(GraphQLNonNull(GraphQLInt))

Usage in Entrypoints๐Ÿ”—

An Entrypoint created using an InterfaceType as the reference will return all implementations of the InterfaceType.

from graphql import GraphQLNonNull, GraphQLString

from undine import Entrypoint, InterfaceField, InterfaceType, QueryType, RootType

from .models import Step, Task


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))


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


class StepType(QueryType[Step], interfaces=[Named]): ...


class Query(RootType):
    named = Entrypoint(Named, many=True)

This Entrypoint can be queried like this:

query {
  named {
    name
    ... on TaskType {
      createdAt
    }
    ... on StepType {
      done
    }
    __typename
  }
}

Filtering๐Ÿ”—

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

from graphql import GraphQLNonNull, GraphQLString

from undine import Entrypoint, FilterSet, InterfaceField, InterfaceType, OrderSet, QueryType, RootType

from .models import Step, Task


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))


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


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


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


class StepFilterSet(FilterSet[Step]): ...


class StepOrderSet(OrderSet[Step]): ...


@Named
@StepFilterSet
@StepOrderSet
class StepType(QueryType[Step]): ...


class Query(RootType):
    named = Entrypoint(Named, many=True)

This creates the following Entrypoint:

1
2
3
4
5
6
7
8
type Query {
  named(
    filterTask: TaskFilterSet
    orderByTask: [TaskOrderSet!]
    filterStep: StepFilterSet
    orderByStep: [StepOrderSet!]
  ): [Named!]!
}

This allows filtering the different types of models in the InterfaceType separately.

Pagination๐Ÿ”—

InterfaceTypes can be paginated just like any QueryType.

from graphql import GraphQLNonNull, GraphQLString

from undine import Entrypoint, InterfaceField, InterfaceType, QueryType, RootType
from undine.relay import Connection

from .models import Step, Task


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))


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


@Named
class StepType(QueryType[Step]): ...


class Query(RootType):
    named = Entrypoint(Connection(Named))

See the Pagination section for more details on pagination.

Schema name๐Ÿ”—

By default, the name of the generated GraphQL Interface for a InterfaceType class is the name of the InterfaceType 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 graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType, schema_name="HasName"):
    name = InterfaceField(GraphQLNonNull(GraphQLString))

Description๐Ÿ”—

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

1
2
3
4
5
6
7
8
9
from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType):
    """Description."""

    name = InterfaceField(GraphQLNonNull(GraphQLString))

Directives๐Ÿ”—

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

from graphql import DirectiveLocation, GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType
from undine.directives import Directive


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


class Named(InterfaceType, directives=[MyDirective()]):
    name = InterfaceField(GraphQLNonNull(GraphQLString))

You can also add directives using decorator syntax.

from graphql import DirectiveLocation, GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType
from undine.directives import Directive


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


@MyDirective()
class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))

See the Directives section for more details on directives.

Visibility๐Ÿ”—

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

You can hide an InterfaceType from certain users by using the __is_visible__ method. Hiding the InterfaceType 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 QueryTypes or other InterfaceTypes didn't implement the InterfaceType.

from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType
from undine.typing import DjangoRequestProtocol


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))

    @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 InterfaceType by providing an extensions argument with a dictionary containing them. These can then be used however you wish to extend the functionality of the InterfaceType.

1
2
3
4
5
6
7
from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType, extensions={"foo": "bar"}):
    name = InterfaceField(GraphQLNonNull(GraphQLString))

InterfaceType extensions are made available in the GraphQL Interface extensions after the schema is created. The InterfaceType itself is found in the GraphQL Interface extensions under a key defined by the INTERFACE_TYPE_EXTENSIONS_KEY setting.

InterfaceField๐Ÿ”—

When a QueryType implements an InterfaceType, all of the InterfaceFields on the InterfaceType are converted to Fields on the QueryType. The converted Field must correspond to a Model field on the QueryType Model, and the InterfaceField output type must match the GraphQL output type converted from Model field. In other words, all InterfaceFields must correspond to Model fields when implemented on a QueryType.

An InterfaceField always requires its desired GraphQL output type to be defined.

1
2
3
4
5
6
7
from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))

Optionally, you can define arguments that the InterfaceField requires. If defined, these must also match the Model field of the implementing QueryType.

from graphql import GraphQLArgument, GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType):
    name = InterfaceField(
        GraphQLNonNull(GraphQLString),
        args={"name": GraphQLArgument(GraphQLNonNull(GraphQLString))},
    )

Field name๐Ÿ”—

By default, the name of the field in the Django model is the same as the name of the InterfaceField. If you want to change the name of the field in the Django model separately, you can do so by setting the field_name argument:

1
2
3
4
5
6
7
from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString), field_name="name")

Schema name๐Ÿ”—

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

1
2
3
4
5
6
7
from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString), schema_name="name")

This can be useful when the desired name of the Interface field is a Python keyword and cannot be used as the Field attribute name.

Description๐Ÿ”—

A description for a field can be provided in one of two ways:

1) By setting the description argument.

1
2
3
4
5
6
7
from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString), description="The name of the object.")

2) As class attribute docstrings, if ENABLE_CLASS_ATTRIBUTE_DOCSTRINGS is enabled.

1
2
3
4
5
6
7
8
from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))
    """The name of the object."""

Deprecation reason๐Ÿ”—

A deprecation_reason can be provided to mark the InterfaceField as deprecated. This is for documentation purposes only, and does not affect the use of the InterfaceField.

1
2
3
4
5
6
7
from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString), deprecation_reason="Use `title` instead.")

Directives๐Ÿ”—

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

from graphql import DirectiveLocation, GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType
from undine.directives import Directive


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


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString), directives=[MyDirective()])

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

from graphql import DirectiveLocation, GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType
from undine.directives import Directive


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


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString)) @ 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 InterfaceField from certain users by decorating a method with the <interface_field_name>.visible decorator. Hiding a InterfaceField 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 InterfaceField didn't exist in the first place.

from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType
from undine.typing import DjangoRequestProtocol


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))

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

The self argument is not an instance of the InterfaceType, but the instance of the InterfaceField 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 InterfaceField by providing a extensions argument with a dictionary containing them. These can then be used however you wish to extend the functionality of the InterfaceField.

1
2
3
4
5
6
7
from graphql import GraphQLNonNull, GraphQLString

from undine import InterfaceField, InterfaceType


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString), extensions={"foo": "bar"})

InterfaceField extensions are made available in the GraphQL Interface field extensions after the schema is created. The InterfaceField itself is found in the GraphQL Interface field extensions under a key defined by the INTERFACE_FIELD_EXTENSIONS_KEY setting.