Directives🔗

In this section, we'll cover the GraphQL directives in Undine. Directives are a way to add metadata to your GraphQL schema, which can be accessed during query 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 does not do anything. It is only used as a way to define additional metadata, which the GraphQL server can use at runtime. If the directive implies some behavior, you'll need to add it, e.g., using a ValidationRule. See the ValidationRules in the graphql-core repository for examples. Custom ValidationRules should be registered using the ADDITIONAL_VALIDATION_RULES setting.

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

A Directive always requires the locations it will 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🔗

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

MUTATION🔗

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

SUBSCRIPTION🔗

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

FIELD🔗

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

FRAGMENT_DEFINITION🔗

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

fragment taskFragment on TaskType @new {
  pk
  name
  done
}

FRAGMENT_SPREAD🔗

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

fragment taskFragment on TaskType {
  pk
  name
  done
}

INLINE_FRAGMENT🔗

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

VARIABLE_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, GraphQLNonNull, GraphQLString

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


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


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


schema = create_schema(
    query=Query,
    schema_definition_directives=[VersionDirective(value="v1.0.0")],
)
In schema definition
1
2
3
4
5
6
directive @version(value: String!) on SCHEMA

schema @version(value: "v1.0.0") {
  query: Query
  mutation: Mutation
}

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, GraphQLNonNull, GraphQLString

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


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


Vector3 = tuple[int, int, int]

vector3_scalar: ScalarType[Vector3, str] = ScalarType(
    name="Vector3",
    description="Represents a 3D vector as a string in format 'X,Y,Z'.",
    directives=[VersionDirective(value="v1.0.0")],
)
In schema definition
1
2
3
directive @version(value: String!) on SCALAR

scalar Vector3 @version(value: "1.0.0")

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, GraphQLNonNull, GraphQLString

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

from .models import Task


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


class TaskType(QueryType[Task], directives=[VersionDirective(value="v1.0.0")]): ...


class Query(RootType, directives=[VersionDirective(value="v2.0.0")]):
    tasks = Entrypoint(TaskType, many=True)
In schema definition
directive @version(value: String!) on OBJECT

type TaskType @version(value: "v1.0.0") {
  name: String!
  done: Boolean!
  createdAt: DateTime!
}

type Query @version(value: "v1.0.0") {
  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, DirectiveArgument

from .models import Task


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


class Named(InterfaceType):
    name = InterfaceField(GraphQLNonNull(GraphQLString), directives=[AddedInDirective(version="v1.0.0")])


class TaskType(QueryType[Task], interfaces=[Named]):
    created_at = Field(directives=[AddedInDirective(version="v1.0.0")])


class Query(RootType):
    tasks = Entrypoint(TaskType, many=True, directives=[AddedInDirective(version="v1.0.0")])
In schema definition
directive @addedIn(version: String!) on FIELD_DEFINITION

interface Named {
  name: String! @addedIn(version: "v1.0.0")
}

type TaskType implements Named {
  name: String!
  done: Boolean!
  createdAt: DateTime! @addedIn(version: "v1.0.0")
}

type Query {
  tasks: [TaskType!]! @addedIn(version: "v1.0.0")
}

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


# Actual directive can be defined in multiple locations, but omit those for brevity.
class AddedInDirective(Directive, locations=[DirectiveLocation.ARGUMENT_DEFINITION], schema_name="addedIn"):
    version = DirectiveArgument(GraphQLNonNull(GraphQLString))


class NewDirective(Directive, locations=[DirectiveLocation.FIELD_DEFINITION], schema_name="new"):
    version = DirectiveArgument(
        GraphQLNonNull(GraphQLString),
        directives=[AddedInDirective(version="v1.0.0")],
    )


class Calc(Calculation[int]):
    value = CalculationArgument(int, directives=[AddedInDirective(version="v1.0.0")])

    def __call__(self, info: GQLInfo) -> DjangoExpression:
        return Value(self.value)
In schema definition
directive @addedIn(version: String!) on ARGUMENT_DEFINITION

directive @new (
  version: String! @addedIn(version: "v1.0.0")
) on FIELD_DEFINITION

type TaskType {
  calc(
    value: Int! @addedIn(version: "v1.0.0")
  ): 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, DirectiveArgument


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


class Named(InterfaceType, directives=[VersionDirective(value="v1.0.0")]):
    name = InterfaceField(GraphQLNonNull(GraphQLString))
In schema definition
1
2
3
4
5
directive @version(value: String!) on INTERFACE

interface Named @version(value: "v1.0.0") {
  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, GraphQLNonNull, GraphQLString

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

from .models import Project, Task


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


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


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


class SearchObject(UnionType[TaskType, ProjectType], directives=[VersionDirective(value="v1.0.0")]): ...
In schema definition
1
2
3
directive @version(value: String!) on UNION

union SearchObject @version(value: "v1.0.0") = 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, GraphQLNonNull, GraphQLString

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

from .models import Task


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


class TaskOrderSet(OrderSet[Task], directives=[VersionDirective(value="v1.0.0")]): ...
In schema definition
1
2
3
4
5
6
directive @version(value: String!) on ENUM

enum TaskOrderSet @version(value: "v1.0.0") {
  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, GraphQLNonNull, GraphQLString

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

from .models import Task


class AddedInDirective(Directive, locations=[DirectiveLocation.ENUM_VALUE], schema_name="addedIn"):
    version = DirectiveArgument(GraphQLNonNull(GraphQLString))


class TaskOrderSet(OrderSet[Task]):
    name = Order("name", directives=[AddedInDirective(version="v1.0.0")])
In schema definition
1
2
3
4
5
6
directive @addedIn(version: String!) on ENUM_VALUE

enum TaskOrderSet {
  nameAsc @addedIn(version: "v1.0.0")
  nameDesc @addedIn(version: "v1.0.0")
}  

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, GraphQLNonNull, GraphQLString

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

from .models import Task


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


class TaskFilterSet(FilterSet[Task], directives=[VersionDirective(value="v1.0.0")]): ...


class CreateTaskMutation(MutationType[Task], directives=[VersionDirective(value="v1.0.0")]): ...
In schema definition
1
2
3
4
5
6
7
8
9
directive @version(value: String!) on INPUT_OBJECT

input TaskFilterSet @version(value: "v1.0.0") {
  name: String
}

input TaskCreateMutation @version(value: "v1.0.0") {
  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, GraphQLNonNull, GraphQLString

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

from .models import Task


class AddedInDirective(Directive, locations=[DirectiveLocation.INPUT_FIELD_DEFINITION], schema_name="addedIn"):
    value = DirectiveArgument(GraphQLNonNull(GraphQLString))


class TaskFilterSet(FilterSet[Task]):
    name = Filter(directives=[AddedInDirective(value="v1.0.0")])


class CreateTaskMutation(MutationType[Task]):
    name = Input(directives=[AddedInDirective(value="v1.0.0")])
In schema definition
1
2
3
4
5
6
7
8
9
directive @addedIn(version: String!) on INPUT_FIELD_DEFINITION

input TaskFilterSet {
  name: String @addedIn(version: "v1.0.0")
}

input TaskCreateMutation {
  name: String @addedIn(version: "v1.0.0")
}

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, GraphQLNonNull, GraphQLString

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


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


class Query(RootType):
    @Entrypoint(directives=[VersionDirective(value="v1.0.0"), VersionDirective(value="v2.0.0")])
    def example(self) -> str:
        return "Example"
In schema definition
1
2
3
4
5
directive @version(value: String!) repeatable on FIELD_DEFINITION

type Query {
  example: String! @version(value: "v1.0.0") @version(value: "v2.0.0")
}

Schema name🔗

By default, the name of the generated Directive is the same as the name of the Directive class. You can change this by setting the schema_name argument to 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], extensions={"foo": "bar"}): ...

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 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 argument is the same as the name of the attribute to which the DirectiveArgument was defined to in the Directive class. You can change this by setting the schema_name argument to the DirectiveArgument class.

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

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.

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

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 extensions under a key defined by the DIRECTIVE_ARGUMENT_EXTENSIONS_KEY setting.