Description
Is your feature request related to a problem? Please describe.
Directives are defined as annotations which works great in many cases but we are also constrained by the JVM limitations. Currently JVM does not support null
value for annotation attribute so it is not possible to define directive with nullable arguments by using annotation only approach. While we can create custom directive definition using willGenerateDirective
hook, annotation arguments cannot be null so it is not possible to apply the annotation with nullable attributes.
We cannot use the existing OptionalInput
approach either as JVM has constraints on annotation attributes - attributes can only be a primitive value, String, Enum, Class, Annotation or an array of any of the above. Furthermore, we cannot just recreate generic OptionalInput
as an annotation because of A) lack of inheritance support in annotations and B) generic annotation attributes are not supported.
Describe the solution you'd like
Schema generator should provide support for creating and applying directives with nullable arguments. Ideally we wouldn't need any custom hooks to do it and everything would be derived from annotation attributes.
Describe alternatives you've considered
--- Alternative # 1 ---
We could potentially create specific wrappers for each supported attribute type (e.g. @OptionalStringArg(val value = "", val defined: Boolean = false)
).
Drawbacks:
- we would need to create wrappers for each possible type + their array representation
- since generics are not supported we wouldn't be able to support enums nor custom annotations
--- Alternative # 2 ---
Since we can provide our own directive definition using hooks, we could potentially introduce other hooks to allow for filtering directive arguments , e.g.
// new hook
fun isValidDirectiveArgumentValue(directiveName: String, argumentName: String, value: Any?): Boolean = true
// example usage
override fun isValidDirectiveArgumentValue(directiveName: String, argumentName: String, value: Any?): Boolean {
if (directiveName == LINK_DIRECTIVE_NAME && argumentName == "as" && value.toString().isBlank()) {
return false
}
return super.isValidDirectiveArgumentValue(directiveName, argumentName, value)
}
Drawbacks:
- still require manually providing custom directive definition in
willGenerateDirective
- directives could be renamed so passed
directiveName
would have to be the original one before the renames - you could potentially filter out required arguments leading to invalid applied directive in the schema (it would fail the validation so I guess it is fine?)
--- Alternative # 3 ---
OR we could create a hook to transform final applied directive (currently hooks only execute around generation of directive definitions), e.g.
// new hook
fun didGenerateAppliedDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLAppliedDirective): GraphQLAppliedDirective = directive
// example usage
override fun didGenerateAppliedDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLAppliedDirective): GraphQLAppliedDirective {
if (directiveInfo.effectiveName == LINK_DIRECTIVE_NAME) {
val asArg = directive.getArgument("as")
if (asArg.getValue<String>().isEmpty()) {
// `as` argument was not specified so we need to remove it from arguments
// since there is no API to do it we have to manually remove ALL and re-add args
return directive.transform {
val urlArg = directive.getArgument("url")
val importArg = directive.getArgument("import")
it.clearArguments()
it.argument(urlArg)
it.argument(importArg)
}
}
}
return super.didGenerateAppliedDirective(directiveInfo, directive)
}
Drawbacks:
- still require manually providing custom directive definition in
willGenerateDirective
- transformation logic is much more complex than the simple filtering in alternative # 2 (
isValidDirectiveArgumentValue
hook) but it is a very flexible approach
Additional context
Example use case from Apollo Federation spec - @link
directive defines single required argument
directive @link(url: String!, as: String, import: [link__Import]) repeatable on SCHEMA
but we cannot apply the directive without providing values for all optional fields, i.e. we cannot represent the basic link when there are no imports and namespace is derived. Ideally we would like to be able to specify just
@link(url: "https://myspecs.dev/foo/1.0")
Since this is not possible, currently users will always have to provide ALL optional values
# `as` namespace should default to the spec name from the url
@link(url: "https://myspecs.dev/foo/1.0", as: "foo", imports: [])