diff --git a/docs/docs/inheritance.md b/docs/docs/inheritance.md new file mode 100644 index 0000000..d7efaf3 --- /dev/null +++ b/docs/docs/inheritance.md @@ -0,0 +1,127 @@ +--- +sidebar_position: 6 +--- + +# Inheritance + +When dealing with GraphQL schema containing [field arguments](https://graphql.com/learn/arguments/), +generating Kotlin code can be a bit tricky. This is because the generated code cannot in itself be the implementation +in a resolver. + +Here's an example: + +```graphql +type Query { + resolveMyType: MyType! +} + +type MyType { + resolveMe(input: String!): String! +} +``` + +Generated Kotlin: + +```kotlin +package com.types.generated + +open class Query { + open fun resolveMyType(): MyType = throw NotImplementedError("Query.resolveMyType must be implemented.") +} + +open class MyType { + open fun resolveMe(input: String): String = throw NotImplementedError("MyType.resolveMe must be implemented.") +} +``` + +Source code: + +```kotlin +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import com.expediagroup.graphql.server.operations.Query +import com.types.generated.MyType as MyTypeInterface +import com.types.generated.Query as QueryInterface + +class MyQuery : Query, QueryInterface() { + override fun resolveMyType(): MyTypeInterface = MyType() +} + +@GraphQLIgnore +class MyType : MyTypeInterface() { + override fun resolveMe(input: String): String = "Hello world!" +} +``` + +As you can see, the generated code is not part of the implementation. Rather, it becomes an interface to inherit from in your implementation. +This enforces a type contract between the schema and your resolver code. + +## Top Level Types + +When dealing with top-level types, i.e. `Query` and `Mutation`, you can inherit from the corresponding generated class +to enforce the type contract. This is fine as long as all of your resolvers are contained in the same Query or Mutation class. + +```graphql +type Query { + foo: String! + bar: String! +} +``` + +```kotlin +import com.expediagroup.graphql.server.operations.Query +import com.types.generated.Query as QueryInterface + +class MyQuery : Query, QueryInterface() { + override fun foo(): String = "Hello" + override fun bar(): String = "World" +} +``` + +However, you might want to separate the implementation into multiple classes like so: + +```kotlin +import com.expediagroup.graphql.server.operations.Query + +class FooQuery : Query { + override fun foo(): String = "Hello" +} + +class BarQuery : Query { + override fun bar(): String = "World" +} +``` + +If you try to inherit from the generated `Query` class, you will get an error during schema generation. + +```kotlin +import com.expediagroup.graphql.server.operations.Query +import com.types.generated.Query as QueryInterface + +class FooQuery : Query, QueryInterface() { + override fun foo(): String = "Hello" +} + +class BarQuery : Query, QueryInterface() { + override fun bar(): String = "World" +} +``` + +This is because the generated `Query` class contains both `foo` and `bar` fields, which creates a conflict when inherited by multiple implementation classes. + +Instead, you should inherit from the field-level generated `Query` classes like so: + +```kotlin +import com.expediagroup.graphql.server.operations.Query +import com.types.generated.FooQuery as FooQueryInterface +import com.types.generated.BarQuery as BarQueryInterface + +class FooQuery : Query, FooQueryInterface() { + override fun foo(): String = "Hello" +} + +class BarQuery : Query, BarQueryInterface() { + override fun bar(): String = "World" +} +``` + +This way, schema generation can complete without conflict, and you can separate your implementation into multiple classes! diff --git a/docs/docs/recommended-usage.md b/docs/docs/recommended-usage.md index 3459037..b345ba8 100644 --- a/docs/docs/recommended-usage.md +++ b/docs/docs/recommended-usage.md @@ -21,8 +21,8 @@ type Query { } type MyType { - field1: String! - field2: String + foo: String! + bar: String } ``` @@ -38,8 +38,8 @@ open class Query { } data class MyType( - val field1: String, - val field2: String? = null + val foo: String, + val bar: String? = null ) ``` @@ -53,16 +53,15 @@ import com.types.generated.Query as QueryInterface class MyQuery : Query, QueryInterface() { override fun resolveMyType(input: String): MyType = MyType( - field1 = myExpensiveCall1(), - field2 = myExpensiveCall2() + foo = myExpensiveCall1(), + bar = myExpensiveCall2() ) } - ``` The resulting source code is extremely unperformant. The `MyType` class is a data class, which means -that the `field1` and `field2` properties are both initialized when the `MyType` object is created, and -`myExpensiveCall1()` and `myExpensiveCall2()` will both be called in sequence! Even if I only query for `field1`, not +that the `foo` and `bar` properties are both initialized when the `MyType` object is created, and +`myExpensiveCall1()` and `myExpensiveCall2()` will both be called in sequence! Even if I only query for `foo`, not only will `myExpensiveCall2()` still run, but it will also wait until `myExpensiveCall1()` is totally finished. ### Instead, use the `resolverInterfaces` config! @@ -91,8 +90,8 @@ open class Query { } open class MyType { - open fun field1(): String = throw NotImplementedError("MyType.field1 must be implemented.") - open fun field2(): String? = throw NotImplementedError("MyType.field2 must be implemented.") + open fun foo(): String = throw NotImplementedError("MyType.foo must be implemented.") + open fun bar(): String? = throw NotImplementedError("MyType.bar must be implemented.") } ``` @@ -108,13 +107,13 @@ class MyQuery : Query, QueryInterface() { @GraphQLIgnore class MyType : MyTypeInterface() { - override fun field1(): String = myExpensiveCall1() - override fun field2(): String? = myExpensiveCall2() + override fun foo(): String = myExpensiveCall1() + override fun bar(): String? = myExpensiveCall2() } ``` -This code is much more performant! The `MyType` class is no longer a data class, so the `field1` and `field2` properties -can now be resolved independently of each other. If I query for only `field1`, only `myExpensiveCall1()` will be called, and -if I query for only `field2`, only `myExpensiveCall2()` will be called. +This code is much more performant! The `MyType` class is no longer a data class, so the `foo` and `bar` properties +can now be resolved independently of each other. If I query for only `foo`, only `myExpensiveCall1()` will be called, and +if I query for only `bar`, only `myExpensiveCall2()` will be called. Check out the [related GraphQL Kotlin docs](https://opensource.expediagroup.com/graphql-kotlin/docs/schema-generator/execution/fetching-data/) for more information on this topic.