Directives๐Ÿ”—

In this section, we'll cover how you can use GraphQL directives in Undine. Directives are a way to add metadata to a GraphQL document or schema, which can be accessed during document execution or by clients consuming your schema.

Directive๐Ÿ”—

In Undine, a GraphQL directive is implemented by subclassing the Directive class.

1
2
3
4
5
6
from graphql import DirectiveLocation

from undine.directives import Directive


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

Note that a Directive by itself doesn't do anything. It is only used as a way to define additional metadata, which the GraphQL server or client can use at runtime. If the directive implies some behavior, you'll need to add it, e.g., using a ValidationRule.

Note that declared Directives are automatically added to the schema, even if they are not used.

A Directive always requires the locations it can be used in to be set using the locations argument. The locations can be divided into two categories: executable locations and type system locations.

Executable locations๐Ÿ”—

Executable locations identify places in a GraphQL document (i.e. "request") where a directive can be used. See the example below on what these locations are.

QUERY๐Ÿ”—

The QUERY location corresponds to query operation.

1
2
3
4
5
6
from graphql import DirectiveLocation

from undine.directives import Directive


class NewDirective(Directive, locations=[DirectiveLocation.QUERY], schema_name="new"): ...

In schema definition:

1
2
3
4
5
6
7
query ($pk: Int!) @new {
  task(pk: $pk) {
    pk
    name
    done
  }
}

MUTATION๐Ÿ”—

The MUTATION location corresponds to mutation operation.

1
2
3
4
5
6
from graphql import DirectiveLocation

from undine.directives import Directive


class NewDirective(Directive, locations=[DirectiveLocation.MUTATION], schema_name="new"): ...

In schema definition:

1
2
3
4
5
mutation ($input: CreateTaskMutation!) @new {
  createTask(input: $input) {
    pk
  }
}

SUBSCRIPTION๐Ÿ”—

The SUBSCRIPTION location corresponds to subscription operation.

1
2
3
4
5
6
from graphql import DirectiveLocation

from undine.directives import Directive


class NewDirective(Directive, locations=[DirectiveLocation.SUBSCRIPTION], schema_name="new"): ...

In schema definition:

1
2
3
4
5
6
subscription @new {
  comments {
    username
    message
  }
}

FIELD๐Ÿ”—

The FIELD location corresponds to a field selection on an operation.

1
2
3
4
5
6
from graphql import DirectiveLocation

from undine.directives import Directive


class NewDirective(Directive, locations=[DirectiveLocation.FIELD], schema_name="new"): ...

In schema definition:

1
2
3
4
5
6
7
query {
  task(pk: 1) {
    pk @new
    name
    done
  }
}

FRAGMENT_DEFINITION๐Ÿ”—

The FRAGMENT_DEFINITION location corresponds to a fragment definition.

1
2
3
4
5
6
from graphql import DirectiveLocation

from undine.directives import Directive


class NewDirective(Directive, locations=[DirectiveLocation.FRAGMENT_DEFINITION], schema_name="new"): ...

In schema definition:

query {
  task(pk: 1) {
    ...taskFragment
  }
}

fragment taskFragment on TaskType @new {
  pk
  name
  done
}

FRAGMENT_SPREAD๐Ÿ”—

The FRAGMENT_SPREAD location corresponds to a fragment spread.

1
2
3
4
5
6
from graphql import DirectiveLocation

from undine.directives import Directive


class NewDirective(Directive, locations=[DirectiveLocation.FRAGMENT_SPREAD], schema_name="new"): ...

In schema definition:

query {
  task(pk: 1) {
    ...taskFragment @new
  }
}

fragment taskFragment on TaskType {
  pk
  name
  done
}

INLINE_FRAGMENT๐Ÿ”—

The INLINE_FRAGMENT location corresponds to an inline fragment.

1
2
3
4
5
6
from graphql import DirectiveLocation

from undine.directives import Directive


class NewDirective(Directive, locations=[DirectiveLocation.INLINE_FRAGMENT], schema_name="new"): ...

In schema definition:

1
2
3
4
5
6
7
8
query {
  node(id: "U3Vyc29yOnVzZXJuYW1lOjE=") {
    id
    ... on TaskType @new {
      name
    }
  }
}

VARIABLE_DEFINITION๐Ÿ”—

The VARIABLE_DEFINITION location corresponds to a variable definition.

1
2
3
4
5
6
from graphql import DirectiveLocation

from undine.directives import Directive


class NewDirective(Directive, locations=[DirectiveLocation.VARIABLE_DEFINITION], schema_name="new"): ...

In schema definition:

1
2
3
4
5
6
7
query ($pk: Int! @new) {
  task(pk: $pk) {
    pk
    name
    done
  }
}

Type system locations๐Ÿ”—

Type system locations identify places in a GraphQL schema (i.e. "API") where a directive can be used. Since Undine is used to define the schema, each type system location corresponds to an Undine object that accepts that "type" of directive.

SCHEMA๐Ÿ”—

The SCHEMA location corresponds to the schema definition itself. Directives can be added here by using the schema_definition_directives argument in the create_schema function.

from graphql import DirectiveLocation

from undine import Entrypoint, RootType, create_schema
from undine.directives import Directive


class NewDirective(Directive, locations=[DirectiveLocation.SCHEMA], schema_name="new"): ...


class Query(RootType):
    @Entrypoint
    def example(self, value: str) -> str:
        return value


schema = create_schema(query=Query, schema_definition_directives=[NewDirective()])

In schema definition:

1
2
3
4
5
directive @new on SCHEMA

schema @new {
  query: Query
}

SCALAR๐Ÿ”—

The SCALAR location corresponds to the scalars defined in the schema. In Undine, ScalarType accepts Directives declared for this location.

from graphql import DirectiveLocation

from undine.directives import Directive
from undine.scalars import ScalarType


class NewDirective(Directive, locations=[DirectiveLocation.SCALAR], schema_name="new"): ...


vector3_scalar = ScalarType(name="Vector3", directives=[NewDirective()])

# Alternatively...

vector3_scalar_alt = ScalarType(name="Vector3") @ NewDirective()

In schema definition:

1
2
3
directive @new on SCALAR

scalar Vector3 @new

OBJECT๐Ÿ”—

The OBJECT location corresponds to the ObjectTypes defined in the schema. In Undine, QueryTypes and RootTypes accepts Directives declared for this location.

from graphql import DirectiveLocation

from undine import Entrypoint, QueryType, RootType
from undine.directives import Directive

from .models import Task


class NewDirective(Directive, locations=[DirectiveLocation.OBJECT], schema_name="new"): ...


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


class Query(RootType, directives=[NewDirective()]):
    tasks = Entrypoint(TaskType, many=True)


# Alternatively...


@NewDirective()
class TaskTypeAlt(QueryType[Task]): ...


@NewDirective()
class QueryAlt(RootType):
    tasks = Entrypoint(TaskType, many=True)

In schema definition:

directive @new on OBJECT

type TaskType @new {
  name: String!
  createdAt: DateTime!
}

type Query @new {
  tasks: [TaskType!]!
}

FIELD_DEFINITION๐Ÿ”—

The FIELD_DEFINITION location corresponds to the fields defined in the schema. In Undine, Fields, InterfaceFields and Entrypoints accepts Directives declared for this location.

from graphql import DirectiveLocation, GraphQLNonNull, GraphQLString

from undine import Entrypoint, Field, InterfaceField, InterfaceType, QueryType, RootType
from undine.directives import Directive

from .models import Task


class NewDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="new"): ...


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

    # Alternatively...
    name_alt = InterfaceField(GraphQLNonNull(GraphQLString)) @ NewDirective()


@Named
class TaskType(QueryType[Task]):
    created_at = Field(directives=[NewDirective()])

    # Alternatively...
    created_at_alt = Field() @ NewDirective()


class Query(RootType):
    tasks = Entrypoint(TaskType, many=True, directives=[NewDirective()])

    # Alternatively...
    tasks_alt = Entrypoint(TaskType, many=True) @ NewDirective()

In schema definition:

directive @new on FIELD_DEFINITION

interface Named {
  name: String! @new
}

type TaskType implements Named {
  name: String!
  createdAt: DateTime! @new
}

type Query {
  tasks: [TaskType!]! @new
}

ARGUMENT_DEFINITION๐Ÿ”—

The ARGUMENT_DEFINITION location corresponds to the field arguments defined in the schema. In Undine, CalculationArguments and DirectiveArguments accepts Directives declared for this location.

from django.db.models import Value
from graphql import DirectiveLocation, GraphQLNonNull, GraphQLString

from undine import Calculation, CalculationArgument, DjangoExpression, GQLInfo
from undine.directives import Directive, DirectiveArgument


class NewDirective(Directive, locations=[DirectiveLocation.ARGUMENT_DEFINITION], schema_name="new"): ...


class VersionDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="version"):
    value = DirectiveArgument(GraphQLNonNull(GraphQLString), directives=[NewDirective()])

    # Alternatively...
    value_alt = DirectiveArgument(GraphQLNonNull(GraphQLString)) @ NewDirective()


class Calc(Calculation[int]):
    value = CalculationArgument(int, directives=[NewDirective()])

    # Alternatively...
    value_alt = CalculationArgument(int) @ NewDirective()

    def __call__(self, info: GQLInfo) -> DjangoExpression:
        return Value(self.value)

In schema definition:

directive @new on ARGUMENT_DEFINITION

directive @version (
  value: String! @new
) on FIELD_DEFINITION

type TaskType {
  calc(
    value: Int! @new
  ): Int!
}

INTERFACE๐Ÿ”—

The INTERFACE location corresponds to the interfaces defined in the schema. In Undine, InterfaceType accepts Directives declared for this location.

from graphql import DirectiveLocation, GraphQLNonNull, GraphQLString

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


class NewDirective(Directive, locations=[DirectiveLocation.INTERFACE], schema_name="new"): ...


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


# Alternatively...


@NewDirective()
class NamedAlt(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString))

In schema definition:

1
2
3
4
5
directive @new on INTERFACE

interface Named @new {
  name: String!
}

UNION๐Ÿ”—

The UNION location corresponds to the unions defined in the schema. In Undine, UnionType accepts Directives declared for this location.

from graphql import DirectiveLocation

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

from .models import Project, Task


class NewDirective(Directive, locations=[DirectiveLocation.UNION], schema_name="new"): ...


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


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


class SearchObject(UnionType[TaskType, ProjectType], directives=[NewDirective()]): ...


# Alternatively...


@NewDirective()
class SearchObjectAlt(UnionType[TaskType, ProjectType]): ...

In schema definition:

1
2
3
directive @new on UNION

union SearchObject @new = TaskType | ProjectType

ENUM๐Ÿ”—

The ENUM location corresponds to the enums defined in the schema. In Undine, OrderSet accepts Directives declared for this location.

from graphql import DirectiveLocation

from undine import OrderSet
from undine.directives import Directive

from .models import Task


class NewDirective(Directive, locations=[DirectiveLocation.ENUM], schema_name="new"): ...


class TaskOrderSet(OrderSet[Task], directives=[NewDirective()]): ...


# Alternatively...


@NewDirective()
class TaskOrderSetAlt(OrderSet[Task]): ...

In schema definition:

1
2
3
4
5
6
directive @new on ENUM

enum TaskOrderSet @new {
  nameAsc
  nameDesc
}

ENUM_VALUE๐Ÿ”—

The ENUM_VALUE location corresponds to the enum values defined in the schema. In Undine, Order accepts Directives declared for this location.

from graphql import DirectiveLocation

from undine import Order, OrderSet
from undine.directives import Directive

from .models import Task


class NewDirective(Directive, locations=[DirectiveLocation.ENUM_VALUE], schema_name="new"): ...


class TaskOrderSet(OrderSet[Task]):
    name = Order("name", directives=[NewDirective()])

    # Alternatively...
    name_alt = Order("name") @ NewDirective()

In schema definition:

1
2
3
4
5
6
directive @new on ENUM_VALUE

enum TaskOrderSet {
  nameAsc @new
  nameDesc @new
}  

INPUT_OBJECT๐Ÿ”—

The INPUT_OBJECT location corresponds to the input objects defined in the schema. In Undine, MutationType and FilterSet accept Directives declared for this location.

from graphql import DirectiveLocation

from undine import FilterSet, MutationType
from undine.directives import Directive

from .models import Task


class NewDirective(Directive, locations=[DirectiveLocation.INPUT_OBJECT], schema_name="new"): ...


class TaskFilterSet(FilterSet[Task], directives=[NewDirective()]): ...


class CreateTaskMutation(MutationType[Task], directives=[NewDirective()]): ...


# Alternatively...


@NewDirective()
class TaskFilterSetAlt(FilterSet[Task]): ...


@NewDirective()
class CreateTaskMutationAlt(MutationType[Task]): ...

In schema definition:

1
2
3
4
5
6
7
8
9
directive @new on INPUT_OBJECT

input TaskFilterSet @new {
  name: String
}

input TaskCreateMutation @new {
  name: String
}

INPUT_FIELD_DEFINITION๐Ÿ”—

The INPUT_FIELD_DEFINITION location corresponds to the input field definitions defined in the schema. In Undine, Input and Filter accept Directives declared for this location.

from graphql import DirectiveLocation

from undine import Filter, FilterSet, Input, MutationType
from undine.directives import Directive

from .models import Task


class NewDirective(Directive, locations=[DirectiveLocation.INPUT_FIELD_DEFINITION], schema_name="new"): ...


class TaskFilterSet(FilterSet[Task]):
    name = Filter(directives=[NewDirective()])

    # Alternatively...
    name_alt = Filter() @ NewDirective()


class CreateTaskMutation(MutationType[Task]):
    name = Input(directives=[NewDirective()])

    # Alternatively...
    name_alt = Input() @ NewDirective()

In schema definition:

1
2
3
4
5
6
7
8
9
directive @new on INPUT_FIELD_DEFINITION

input TaskFilterSet {
  name: String @new
}

input TaskCreateMutation {
  name: String @new
}

Is repeatable๐Ÿ”—

A directive can be declared as repeatable using the is_repeatable argument. This means that the directive can be used multiple times in the same location.

from graphql import DirectiveLocation

from undine import Entrypoint, RootType
from undine.directives import Directive


class NewDirective(
    Directive,
    locations=[DirectiveLocation.FIELD_DEFINITION],
    schema_name="new",
    is_repeatable=True,
): ...


class Query(RootType):
    @Entrypoint(directives=[NewDirective(), NewDirective()])
    def example(self) -> str:
        return "Example"

    # Alternatively...
    @NewDirective()
    @NewDirective()
    @Entrypoint()
    def example_alt(self) -> str:
        return "Example"

In schema definition:

1
2
3
4
5
directive @new repeatable on FIELD_DEFINITION

type Query {
  example: String! @new @new
}

Schema name๐Ÿ”—

By default, the name of the generated GraphQL directive for a Directive class is the name of the Directive 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
from graphql import DirectiveLocation

from undine.directives import Directive


class NewDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="new"): ...

Extensions๐Ÿ”—

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

1
2
3
4
5
6
from graphql import DirectiveLocation

from undine.directives import Directive


class NewDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], extensions={"foo": "bar"}): ...

Directive extensions are made available in the GraphQL directive extensions after the schema is created. The Directive itself is found in the GraphQL directive extensions under a key defined by the DIRECTIVE_EXTENSIONS_KEY setting.

DirectiveArgument๐Ÿ”—

A Directive can optionally have a number of DirectiveArguments defined in the class body. These define the arguments that can or must be used with the directive. A DirectiveArgument always requires input type of the argument, which needs to be a GraphQL input type.

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

from undine.directives import Directive, DirectiveArgument


class VersionDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="version"):
    value = DirectiveArgument(GraphQLNonNull(GraphQLString))

Schema name๐Ÿ”—

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

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

from undine.directives import Directive, DirectiveArgument


class VersionDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="version"):
    value = DirectiveArgument(GraphQLNonNull(GraphQLString), schema_name="value")

This can be useful when the desired name of the GraphQL directive argument is a Python keyword and cannot be used as the DirectiveArgument attribute name.

Description๐Ÿ”—

A description for a DirectiveArgument can be provided in on of two ways:

1) By setting the description argument.

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

from undine.directives import Directive, DirectiveArgument


class VersionDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="version"):
    value = DirectiveArgument(GraphQLNonNull(GraphQLString), description="Version value.")

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

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

from undine.directives import Directive, DirectiveArgument


class VersionDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="version"):
    value = DirectiveArgument(GraphQLNonNull(GraphQLString))
    """Version value."""

Default value๐Ÿ”—

A default_value can be provided to set the default value for the DirectiveArgument.

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

from undine.directives import Directive, DirectiveArgument


class VersionDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="version"):
    value = DirectiveArgument(GraphQLNonNull(GraphQLString), default_value="1.0.0")

Deprecation reason๐Ÿ”—

A deprecation_reason can be provided to mark the DirectiveArgument as deprecated.

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

from undine.directives import Directive, DirectiveArgument


class VersionDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="version"):
    value = DirectiveArgument(GraphQLNonNull(GraphQLString), deprecation_reason="Use something else.")

Directives๐Ÿ”—

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

from graphql import DirectiveLocation, GraphQLNonNull, GraphQLString

from undine.directives import Directive, DirectiveArgument


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


class VersionDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="version"):
    value = DirectiveArgument(GraphQLNonNull(GraphQLString), directives=[NewDirective()])

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

from graphql import DirectiveLocation, GraphQLNonNull, GraphQLString

from undine.directives import Directive, DirectiveArgument


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


class VersionDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="version"):
    value = DirectiveArgument(GraphQLNonNull(GraphQLString)) @ NewDirective()

Visibility๐Ÿ”—

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

You can hide a DirectiveArgument from certain users by decorating a method with the <arg_name>.visible decorator. Hiding a DirectiveArgument 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 DirectiveArgument didn't exist in the first place.

from graphql import DirectiveLocation, GraphQLNonNull, GraphQLString

from undine.directives import Directive, DirectiveArgument
from undine.typing import DjangoRequestProtocol


class VersionDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="version"):
    value = DirectiveArgument(GraphQLNonNull(GraphQLString))

    @value.visible
    def value_visible(self, request: DjangoRequestProtocol) -> bool:
        return request.user.is_superuser
About method signature

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

The self argument is not an instance of the Directive, but the instance of the DirectiveArgument 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.

Extensions๐Ÿ”—

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

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

from undine.directives import Directive, DirectiveArgument


class VersionDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="version"):
    value = DirectiveArgument(GraphQLNonNull(GraphQLString), extensions={"foo": "bar"})

DirectiveArgument extensions are made available in the GraphQL argument extensions after the schema is created. The DirectiveArgument itself is found in the GraphQL argument extensions under a key defined by the DIRECTIVE_ARGUMENT_EXTENSIONS_KEY setting.