Skip to content

Commit

Permalink
Explicit support for deprecated directive in the schema (#247)
Browse files Browse the repository at this point in the history
* Explicit support for deprecated directive in the schema

* custom SDL printer

* customize schema print extension, fix unit tests and detekt

* add printer tests

* revert changes to GraphQLName
  • Loading branch information
dariuszkuc authored and Shane Myrick committed Jul 3, 2019
1 parent 1d00b99 commit 0d6bee7
Show file tree
Hide file tree
Showing 14 changed files with 378 additions and 35 deletions.
17 changes: 3 additions & 14 deletions example/src/main/kotlin/com/expedia/graphql/sample/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.expedia.graphql.SchemaGeneratorConfig
import com.expedia.graphql.TopLevelObject
import com.expedia.graphql.directives.KotlinDirectiveWiringFactory
import com.expedia.graphql.execution.KotlinDataFetcherFactoryProvider
import com.expedia.graphql.extensions.print
import com.expedia.graphql.hooks.SchemaGeneratorHooks
import com.expedia.graphql.sample.datafetchers.CustomDataFetcherFactoryProvider
import com.expedia.graphql.sample.datafetchers.SpringDataFetcherFactory
Expand All @@ -20,7 +21,6 @@ import graphql.execution.AsyncSerialExecutionStrategy
import graphql.execution.DataFetcherExceptionHandler
import graphql.execution.SubscriptionExecutionStrategy
import graphql.schema.GraphQLSchema
import graphql.schema.idl.SchemaPrinter
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
Expand Down Expand Up @@ -51,23 +51,12 @@ class Application {
dataFetcherFactoryProvider = dataFetcherFactoryProvider
)

@Bean
fun schemaPrinter() = SchemaPrinter(
SchemaPrinter.Options.defaultOptions()
.includeScalarTypes(true)
.includeExtendedScalarTypes(true)
.includeIntrospectionTypes(true)
.includeSchemaDefintion(true)
.includeDirectives(true)
)

@Bean
fun schema(
queries: List<Query>,
mutations: List<Mutation>,
subscriptions: List<Subscription>,
schemaConfig: SchemaGeneratorConfig,
schemaPrinter: SchemaPrinter
schemaConfig: SchemaGeneratorConfig
): GraphQLSchema {
fun List<Any>.toTopLevelObjects() = this.map {
TopLevelObject(it)
Expand All @@ -80,7 +69,7 @@ class Application {
subscriptions = subscriptions.toTopLevelObjects()
)

logger.info(schemaPrinter.print(schema))
logger.info(schema.print())

return schema
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.expedia.graphql.sample

import com.expedia.graphql.extensions.print
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.type.MapType
import com.fasterxml.jackson.databind.type.TypeFactory
import graphql.schema.GraphQLSchema
import graphql.schema.idl.SchemaPrinter
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
Expand All @@ -21,7 +21,6 @@ import reactor.core.publisher.Mono
@Configuration
class RoutesConfiguration(
val schema: GraphQLSchema,
val schemaPrinter: SchemaPrinter,
private val queryHandler: QueryHandler,
private val objectMapper: ObjectMapper,
@Value("classpath:/graphql-playground.html") private val playgroundHtml: Resource
Expand All @@ -42,7 +41,7 @@ class RoutesConfiguration(
@Bean
fun sdlRoute() = router {
GET("/sdl") {
ok().contentType(MediaType.TEXT_PLAIN).syncBody(schemaPrinter.print(schema))
ok().contentType(MediaType.TEXT_PLAIN).syncBody(schema.print())
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.expedia.graphql.directives

import graphql.Scalars
import graphql.introspection.Introspection
import graphql.schema.GraphQLArgument
import graphql.schema.GraphQLDirective
import graphql.schema.GraphQLNonNull

const val DEPRECATED_DIRECTIVE_NAME = "deprecated"

private val DefaultDeprecatedArgument: GraphQLArgument = GraphQLArgument.newArgument()
.name("reason")
.type(GraphQLNonNull.nonNull(Scalars.GraphQLString))
.defaultValue("No longer supported")
.build()

internal val DeprecatedDirective: GraphQLDirective = GraphQLDirective.newDirective()
.name(DEPRECATED_DIRECTIVE_NAME)
.description("Marks the target field/enum value as deprecated")
.argument(DefaultDeprecatedArgument)
.validLocations(Introspection.DirectiveLocation.FIELD_DEFINITION, Introspection.DirectiveLocation.ENUM_VALUE)
.build()

internal fun deprecatedDirectiveWithReason(reason: String): GraphQLDirective = DeprecatedDirective.transform { directive ->
directive.argument(DefaultDeprecatedArgument.transform { arg ->
arg.value(reason)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import graphql.schema.GraphQLDirectiveContainer
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLType

/**
* Default no-op wiring for deprecated directive.
*/
private val defaultDeprecatedWiring = object : KotlinSchemaDirectiveWiring {}

/**
* Wiring factory that is used to provide the directives.
*/
Expand Down Expand Up @@ -66,10 +71,16 @@ open class KotlinDirectiveWiringFactory(
return modifiedObject
}

private fun discoverWiringProvider(directiveName: String, env: KotlinSchemaDirectiveEnvironment<GraphQLDirectiveContainer>): KotlinSchemaDirectiveWiring? =
if (directiveName in manualWiring) {
private fun discoverWiringProvider(directiveName: String, env: KotlinSchemaDirectiveEnvironment<GraphQLDirectiveContainer>): KotlinSchemaDirectiveWiring? {
var wiring = if (directiveName in manualWiring) {
manualWiring[directiveName]
} else {
getSchemaDirectiveWiring(env)
}

if (null == wiring && DEPRECATED_DIRECTIVE_NAME == directiveName) {
wiring = defaultDeprecatedWiring
}
return wiring
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.expedia.graphql.extensions

import com.expedia.graphql.directives.DeprecatedDirective
import graphql.Directives
import graphql.schema.GraphQLSchema
import graphql.schema.idl.SchemaPrinter

/**
* Prints out SDL representation of a target schema.
*
* @param includeIntrospectionTypes boolean flag indicating whether SDL should include introspection types
* @param includeScalarTypes boolean flag indicating whether SDL should include custom schema scalars
* @param includeExtendedScalarTypes boolean flag indicating whether SDL should include extended scalars (e.g. Long)
* supported by graphql-java, if set will automatically also set the includeScalarTypes flag
* @param includeDefaultSchemaDefinition boolean flag indicating whether SDL should include schema definition if using
* default root type names
* @param includeDirectives boolean flag indicating whether SDL should include directive information
*/
fun GraphQLSchema.print(
includeIntrospectionTypes: Boolean = false,
includeScalarTypes: Boolean = true,
includeExtendedScalarTypes: Boolean = true,
includeDefaultSchemaDefinition: Boolean = true,
includeDirectives: Boolean = true
): String {
val schemaPrinter = SchemaPrinter(
SchemaPrinter.Options.defaultOptions()
.includeIntrospectionTypes(includeIntrospectionTypes)
.includeScalarTypes(includeScalarTypes || includeExtendedScalarTypes)
.includeExtendedScalarTypes(includeExtendedScalarTypes)
.includeSchemaDefintion(includeDefaultSchemaDefinition)
.includeDirectives(includeDirectives)
)

var schemaString = schemaPrinter.print(this)
if (includeDirectives) {
// graphql-java SchemaPrinter filters out common directives, below is a workaround to print default built-in directives
val defaultDirectives = arrayOf(Directives.IncludeDirective, Directives.SkipDirective, DeprecatedDirective)
val directivesToString = defaultDirectives.joinToString("\n\n") { directive -> """
#${directive.description}
directive @${directive.name} on ${directive.validLocations().joinToString(" | ") { loc -> loc.name }}
""".trimIndent()
}
schemaString += "\n" + directivesToString
}
return schemaString
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.expedia.graphql.generator.state

import com.expedia.graphql.directives.DeprecatedDirective
import graphql.Directives
import graphql.schema.GraphQLDirective
import graphql.schema.GraphQLType

Expand All @@ -9,4 +11,14 @@ internal class SchemaGeneratorState(supportedPackages: List<String>) {
val directives = mutableSetOf<GraphQLDirective>()

fun getValidAdditionalTypes(): List<GraphQLType> = additionalTypes.filter { cache.doesNotContainGraphQLType(it) }

init {
// NOTE: @include and @defer query directives are added by graphql-java by default
// adding them explicitly here to keep it consistent with missing deprecated directive
directives.add(Directives.IncludeDirective)
directives.add(Directives.SkipDirective)

// graphql-kotlin default directives
directives.add(DeprecatedDirective)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.expedia.graphql.generator.types

import com.expedia.graphql.directives.deprecatedDirectiveWithReason
import com.expedia.graphql.generator.SchemaGenerator
import com.expedia.graphql.generator.TypeBuilder
import com.expedia.graphql.generator.extensions.getDeprecationReason
Expand Down Expand Up @@ -40,8 +41,11 @@ internal class EnumBuilder(generator: SchemaGenerator) : TypeBuilder(generator)
}

valueBuilder.description(valueField.getGraphQLDescription())
valueBuilder.deprecationReason(valueField.getDeprecationReason())

valueField.getDeprecationReason()?.let {
valueBuilder.deprecationReason(it)
valueBuilder.withDirective(deprecatedDirectiveWithReason(it))
}
return config.hooks.onRewireGraphQLType(valueBuilder.build()).safeCast()
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.expedia.graphql.generator.types

import com.expedia.graphql.directives.deprecatedDirectiveWithReason
import com.expedia.graphql.exceptions.InvalidInputFieldTypeException
import com.expedia.graphql.generator.SchemaGenerator
import com.expedia.graphql.generator.TypeBuilder
Expand Down Expand Up @@ -34,6 +35,7 @@ internal class FunctionBuilder(generator: SchemaGenerator) : TypeBuilder(generat

fn.getDeprecationReason()?.let {
builder.deprecate(it)
builder.withDirective(deprecatedDirectiveWithReason(it))
}

generator.directives(fn).forEach {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.expedia.graphql.generator.types

import com.expedia.graphql.directives.deprecatedDirectiveWithReason
import com.expedia.graphql.generator.SchemaGenerator
import com.expedia.graphql.generator.TypeBuilder
import com.expedia.graphql.generator.extensions.getPropertyDeprecationReason
Expand All @@ -22,7 +23,11 @@ internal class PropertyBuilder(generator: SchemaGenerator) : TypeBuilder(generat
.description(prop.getPropertyDescription(parentClass))
.name(prop.name)
.type(propertyType)
.deprecate(prop.getPropertyDeprecationReason(parentClass))

prop.getPropertyDeprecationReason(parentClass)?.let {
fieldBuilder.deprecate(it)
fieldBuilder.withDirective(deprecatedDirectiveWithReason(it))
}

generator.directives(prop).forEach {
fieldBuilder.withDirective(it)
Expand Down
Loading

0 comments on commit 0d6bee7

Please sign in to comment.