Skip to content

Commit

Permalink
feat: Export schema in SDL (#90)
Browse files Browse the repository at this point in the history
Adds support for exporting an existing schema in the human-readable
schema definition language (SDL). The Ktor feature by default provides
it via `GET /<endpoint>?schema`.

SDL follows introspection config, so if introspection queries are
disabled, export in SDL will also be disabled.

As implementations that support SDL have to provide the `@deprecated`
directive, this also adds that directive to the built-in directives
(without attempting to fix existing issues there).

Lastly, this fixes a small inconsistency with the spec by making the
`description` of a `__Type` optional (and subsequently gets rid of some
artificial empty string defaults).

Resolves #79
  • Loading branch information
stuebingerb authored Dec 5, 2024
2 parents 61af442 + 4aa6c9b commit 02a0304
Show file tree
Hide file tree
Showing 20 changed files with 1,198 additions and 81 deletions.
41 changes: 41 additions & 0 deletions docs/content/Plugins/ktor.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,44 @@ schema {
}
}
```

## Schema Definition Language (SDL)

The [Schema Definition Language](https://graphql.org/learn/schema/#type-language) (or Type System Definition Language) is a human-readable, language-agnostic
representation of a GraphQL schema.

See the following comparison:

=== "KGraphQL"
```kotlin
schema {
data class SampleData(
val id: Int,
val stringData: String,
val optionalList: List<String>?
)

query("getSampleData") {
resolver { quantity: Int ->
(1..quantity).map { SampleData(it, "sample-$it", emptyList()) }
}.withArgs {
arg<Int> { name = "quantity"; defaultValue = 10 }
}
}
}
```
=== "SDL"
```
type Query {
getSampleData(quantity: Int! = 10): [SampleData!]!
}

type SampleData {
id: Int!
optionalList: [String!]
stringData: String!
}
```

If schema introspection is enabled, the ktor feature will expose the current schema in Schema Definition
Language under [http://localhost:8080/graphql?schema](http://localhost:8080/graphql?schema).
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ class GraphQL(val schema: Schema) {
}

class FeatureInstance(featureKey: String = "KGraphQL") : Plugin<Application, Configuration, GraphQL> {
companion object {
private val playgroundHtml: ByteArray? by lazy {
KtorGraphQLConfiguration::class.java.classLoader.getResource("playground.html")?.readBytes()
}
}

override val key = AttributeKey<GraphQL>(featureKey)

override fun install(pipeline: Application, configure: Configuration.() -> Unit): GraphQL {
Expand All @@ -87,12 +93,15 @@ class GraphQL(val schema: Schema) {
)
call.respondText(result, contentType = ContentType.Application.Json)
}
if (config.playground) get {
@Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
val playgroundHtml =
KtorGraphQLConfiguration::class.java.classLoader.getResource("playground.html")
.readBytes()
call.respondBytes(playgroundHtml, contentType = ContentType.Text.Html)
get {
val schemaRequested = call.request.queryParameters["schema"] != null
if (schemaRequested && config.introspection) {
call.respondText(schema.printSchema())
} else if (config.playground) {
playgroundHtml?.let {
call.respondBytes(it, contentType = ContentType.Text.Html)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class KtorMultipleEndpoints : KtorTest() {
}
}

client.get("/graphql").status shouldBeEqualTo HttpStatusCode.MethodNotAllowed
client.get("/graphql").status shouldBeEqualTo HttpStatusCode.NotFound
}

@Test
Expand All @@ -79,4 +79,38 @@ class KtorMultipleEndpoints : KtorTest() {
response.status shouldBeEqualTo HttpStatusCode.OK
response.bodyAsText() shouldBeEqualTo playgroundHtml
}

@Test
fun `SDL should be provided by default`() = testApplication {
install(GraphQL) {
schema {
query("check") {
resolver { -> "OK" }
}
}
}

val response = client.get("/graphql?schema")
response.status shouldBeEqualTo HttpStatusCode.OK
response.bodyAsText() shouldBeEqualTo """
type Query {
check: String!
}
""".trimIndent()
}

@Test
fun `SDL should not be provided when introspection is disabled`() = testApplication {
install(GraphQL) {
introspection = false
schema {
query("check") {
resolver { -> "OK" }
}
}
}

client.get("/graphql?schema").status shouldBeEqualTo HttpStatusCode.NotFound
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ data class SchemaConfiguration(
val wrapErrors: Boolean,
val executor: Executor,
val timeout: Long?,
// allow schema introspection
val introspection: Boolean = true,
val plugins: MutableMap<KClass<*>, Any>,
val genericTypeResolver: GenericTypeResolver,
val genericTypeResolver: GenericTypeResolver
) {
@Suppress("UNCHECKED_CAST")
operator fun <T : Any> get(type: KClass<T>) = plugins[type] as T?
Expand Down
13 changes: 11 additions & 2 deletions kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,11 @@ open class Parser {
return VariableDefinitionNode(
variable = parseVariable(),
type = expectToken(COLON).let { parseTypeReference() },
defaultValue = if (expectOptionalToken(EQUALS) != null) parseValueLiteral(true) else null,
defaultValue = if (expectOptionalToken(EQUALS) != null) {
parseValueLiteral(true)
} else {
null
},
directives = parseDirectives(true),
loc = loc(start)
)
Expand Down Expand Up @@ -545,7 +549,11 @@ open class Parser {
*/
private fun parseTypeSystemDefinition(): DefinitionNode.TypeSystemDefinitionNode {
// Many definitions begin with a description and require a lookahead.
val keywordToken = if (peekDescription()) lexer.lookahead() else lexer.token
val keywordToken = if (peekDescription()) {
lexer.lookahead()
} else {
lexer.token
}

if (keywordToken.kind == NAME) {
return when (keywordToken.value) {
Expand Down Expand Up @@ -937,6 +945,7 @@ open class Parser {
* `FRAGMENT_DEFINITION`
* `FRAGMENT_SPREAD`
* `INLINE_FRAGMENT`
* `VARIABLE_DEFINITION`
*
* TypeSystemDirectiveLocation : one of
* `SCHEMA`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class DefaultSchema(
)
}

override fun printSchema() = SchemaPrinter().print(model)

override fun typeByKClass(kClass: KClass<*>): Type? = model.queryTypes[kClass]

override fun typeByKType(kType: KType): Type? = typeByKClass(kType.jvmErasure)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ interface Schema : __Schema {
options: ExecutionOptions = ExecutionOptions(),
operationName: String? = null
) = runBlocking { execute(request, variables, context, options, operationName) }

/**
* Prints the current schema in schema definition language (SDL)
*/
fun printSchema(): String
}
Loading

0 comments on commit 02a0304

Please sign in to comment.