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.
| 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.
| from graphql import DirectiveLocation
from undine.directives import Directive
class NewDirective(Directive, locations=[DirectiveLocation.QUERY], schema_name="new"): ...
|
In schema definition:
| query ($pk: Int!) @new {
task(pk: $pk) {
pk
name
done
}
}
|
MUTATION
The MUTATION location corresponds to mutation operation.
| from graphql import DirectiveLocation
from undine.directives import Directive
class NewDirective(Directive, locations=[DirectiveLocation.MUTATION], schema_name="new"): ...
|
In schema definition:
| mutation ($input: CreateTaskMutation!) @new {
createTask(input: $input) {
pk
}
}
|
SUBSCRIPTION
The SUBSCRIPTION location corresponds to subscription operation.
| from graphql import DirectiveLocation
from undine.directives import Directive
class NewDirective(Directive, locations=[DirectiveLocation.SUBSCRIPTION], schema_name="new"): ...
|
In schema definition:
| subscription @new {
comments {
username
message
}
}
|
FIELD
The FIELD location corresponds to a field selection on an operation.
| from graphql import DirectiveLocation
from undine.directives import Directive
class NewDirective(Directive, locations=[DirectiveLocation.FIELD], schema_name="new"): ...
|
In schema definition:
| query {
task(pk: 1) {
pk @new
name
done
}
}
|
FRAGMENT_DEFINITION
The FRAGMENT_DEFINITION location corresponds to a fragment definition.
| 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.
| 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.
| from graphql import DirectiveLocation
from undine.directives import Directive
class NewDirective(Directive, locations=[DirectiveLocation.INLINE_FRAGMENT], schema_name="new"): ...
|
In schema definition:
| query {
node(id: "U3Vyc29yOnVzZXJuYW1lOjE=") {
id
... on TaskType @new {
name
}
}
}
|
VARIABLE_DEFINITION
The VARIABLE_DEFINITION location corresponds to a variable definition.
| from graphql import DirectiveLocation
from undine.directives import Directive
class NewDirective(Directive, locations=[DirectiveLocation.VARIABLE_DEFINITION], schema_name="new"): ...
|
In schema 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
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:
| 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:
| 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:
| 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:
| 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:
| 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:
| directive @new on ENUM_VALUE
enum TaskOrderSet {
nameAsc @new
nameDesc @new
}
|
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:
| directive @new on INPUT_OBJECT
input TaskFilterSet @new {
name: String
}
input TaskCreateMutation @new {
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
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:
| 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:
| 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:
| 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.
| 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.
| 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:
| 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.
| 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.
| 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.")
|
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.
| 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.