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.
| 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
| query ($pk: Int!) @new {
task(pk: $pk) {
pk
name
done
}
}
|
MUTATION
| mutation ($input: CreateTaskMutation!) @new {
createTask(input: $input) {
pk
}
}
|
SUBSCRIPTION
| subscription @new {
comments {
username
message
}
}
|
FIELD
| 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
| query {
node(id: "U3Vyc29yOnVzZXJuYW1lOjE=") {
id
... on TaskType @new {
name
}
}
}
|
VARIABLE_DEFINITION
| 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
| 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
| 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
| 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
| 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
| 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
| directive @addedIn(version: String!) on ENUM_VALUE
enum TaskOrderSet {
nameAsc @addedIn(version: "v1.0.0")
nameDesc @addedIn(version: "v1.0.0")
}
|
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
| 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
}
|
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
| 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
| 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.
| 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
.
| 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.
| 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.
| 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.
| 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.
| 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
.
| 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.
| 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
.
| 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.