Skip to content

Commit

Permalink
docs
Browse files Browse the repository at this point in the history
  • Loading branch information
danadajian committed May 28, 2024
1 parent 492fb70 commit 6bce5a6
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 16 deletions.
127 changes: 127 additions & 0 deletions docs/docs/inheritance.md
Original file line number Diff line number Diff line change
@@ -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!
31 changes: 15 additions & 16 deletions docs/docs/recommended-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ type Query {
}

type MyType {
field1: String!
field2: String
foo: String!
bar: String
}
```

Expand All @@ -38,8 +38,8 @@ open class Query {
}

data class MyType(
val field1: String,
val field2: String? = null
val foo: String,
val bar: String? = null
)
```

Expand All @@ -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!
Expand Down Expand Up @@ -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.")
}
```

Expand All @@ -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.

0 comments on commit 6bce5a6

Please sign in to comment.