Directives
GraphQL directives can be used to transform the schema types, fields and arguments as well as modify the runtime
behavior of the query (e.g. implement access control, etc). Common use cases involve limiting functionality based on the
user authentication and authorization. While GraphQL
spec specifies two types of directives -
executable
(aka query) and type system
(aka schema) directives, only the latter one is supported by
graphql-kotlin-schema-generator
.
Default Directives
@deprecated
- schema directive used to represent deprecated portion of the schema.
See @Deprecated and @GraphQLDeprecated annotation documentation for more details
type Query {
deprecatedQuery: Boolean! @deprecated(reason: "No longer supported")
}
@skip
- query directive that allows for conditional exclusion of fields or fragments
query myQuery($shouldSkip: Boolean) {
myField @skip(if: $shouldSkip)
}
@include
- query directive that allows for conditional inclusion of fields or fragments
query myQuery($shouldInclude: Boolean) {
myField @include(if: $shouldInclude)
}
Custom Directives
Custom directives can be added to the schema using custom annotations:
@GraphQLDirective(
name = "awesome",
description = "This element is great",
locations = [FIELD_DEFINITION]
)
annotation class AwesomeDirective(val value: String)
class MyQuery {
@AwesomeDirective("cool stuff")
val somethingGreat: String = "Hello World"
}
The directive will then added to the schema as:
# This element is great
directive @awesome(value: String) on FIELD_DEFINITION
type MyQuery {
somethingGreat: String @awesome("cool stuff")
}
Directives can be added to various places in the schema. See the
graphql.introspection.Introspection.DirectiveLocation
enum from graphql-java
for a full list of valid locations.
GraphQL directives are currently not available through introspection and you have to use SDL directly
instead (you can use convenient print
extension function of GraphQLSchema
). See GraphQL
issue and corresponding graphql-java
issue for more details about the introspection issue.
Naming Convention
As described in the example above, the directive name in the schema will by default come from the
@GraphQLDirective.name
attribute which should follow lowerCamelCase
format. If this value is not specified, the
directive name will default to the normalized decapitalized name of the annotated annotation (eg: awesomeDirective
in
the example above).
Customizing Behavior
Directives allow you to customize the behavior of your schema based on some predefined conditions. Simplest way to
modify the default behavior of your GraphQLTypes is by providing your custom KotlinSchemaDirectiveWiring
through
KotlinDirectiveWiringFactory
factory used by your SchemaGeneratorHooks
.
Example of a directive that converts field to lowercase
@GraphQLDirective(name = "lowercase", description = "Modifies the string field to lowercase")
annotation class LowercaseDirective
class LowercaseSchemaDirectiveWiring : KotlinSchemaDirectiveWiring {
override fun onField(environment: KotlinFieldDirectiveEnvironment): GraphQLFieldDefinition {
val field = environment.element
val originalDataFetcher: DataFetcher<Any> = environment.getDataFetcher()
val lowerCaseFetcher = DataFetcherFactories.wrapDataFetcher(
originalDataFetcher,
BiFunction<DataFetchingEnvironment, Any, Any>{ _, value -> value.toString().toLowerCase() }
)
environment.setDataFetcher(lowerCaseFetcher)
return field
}
}
While you can manually apply all the runtime wirings to the corresponding GraphQL types directly in
SchemaGeneratorHooks#onRewireGraphQLType
, we recommend the usage of our
KotlinDirectiveWiringFactory
to simplify the integrations. KotlinDirectiveWiringFactory
accepts a mapping of directives to corresponding wirings or
could be extended to provide the wirings through KotlinDirectiveWiringFactory#getSchemaDirectiveWiring
that accepts
KotlinSchemaDirectiveEnvironment
.
val queries = ...
val customWiringFactory = KotlinDirectiveWiringFactory(
manualWiring = mapOf<String, KotlinSchemaDirectiveWiring>("lowercase" to LowercaseSchemaDirectiveWiring()))
val customHooks = object : SchemaGeneratorHooks {
override val wiringFactory: KotlinDirectiveWiringFactory
get() = customWiringFactory
}
val schemaGeneratorConfig = SchemaGeneratorConfig(hooks = customHooks)
val schema = toSchema(queries = queries, config = schemaGeneratorConfig)
While providing directives on different schema elements you will be able to modify the underlying GraphQL types. Keep in mind though that data fetchers are used to resolve the fields so only field directives (and by association their arguments directives) can modify runtime behavior based on the context and user input.
graphql-kotlin
prioritizes manual wiring mappings over the wirings provided by the KotlinDirectiveWiringFactory#getSchemaDirectiveWiring
.
This is a different behavior than graphql-java
which will first attempt to use WiringFactory
and then fallback to manual overrides.
For more details please refer to the example usage of directives in our example app.
Repeatable Directives
GraphQL supports repeatable directives (e.g. Apollo federation allows developers to specify multiple @key
directives).
By default, Kotlin does not allow applying same annotation multiple times. In order to make your directives repeatable,
you need to annotate it with kotlin.annotation.Repeatable
annotation.
@Repeatable
@GraphQLDirective(locations = [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE])
annotation class MyRepeatableDirective(val value: String)
Generates the above directive as
directive @myRepeatableDirective(value: String!) repeatable on OBJECT | INTERFACE
Directive Chaining
Directives are applied in the order annotations are declared on the given object. Given
@Directive1
@Directive2
fun doSomething(): String {
// does something
}
Directive1
will be applied first followed by the Directive2
.
Ignoring Directive Arguments
Normally if you wanted to exclude a field or argument from the schema, you could use @GraphQLIgnore. However, due to reflection and kotlin limitations, the generated JVM code for interface arguments can only have annotations on getters.
This is easily fixable though using the @get:
target prefix
See graphql-kotlin#763 for more details.
@GraphQLDirective
annotation class DirectiveWithIgnoredArgs(
val string: String,
@get:GraphQLIgnore
val ignoreMe: String
)
This will generate the following schema
directive @directiveWithIgnoredArgs(
string: String!
) on ...
Limitations
GraphQL specification allows usage of any valid input objects as directive arguments. Since we rely on Kotlin annotation functionality to define our custom directives, we are limited in what can be used as annotation parameter - only primitives (or scalars), Strings, Enums, other annotations or an array of any of the above are supported.
Support for input objects can be added by providing that object representation as an annotation class and then adding support for it through custom schema generator hooks.