Skip to content

Commit 62760c2

Browse files
samuelAndalonsamvazquez
andauthored
feat: graphql-kotlin-dataloader (#1415)
* feat: graphql-kotlin-dataloader * feat: update references * feat: update references * feat: remove test, it was moved to kotlin-dataloader moodule * feat: update references * feat: update references * feat: update references * feat: update docs * feat: update links in docs * feat: KotlinDataLoaderRegistryFactory * feat: update docs and readme Co-authored-by: samvazquez <[email protected]>
1 parent 1060522 commit 62760c2

File tree

31 files changed

+421
-67
lines changed

31 files changed

+421
-67
lines changed

examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/KtorDataLoaderRegistryFactory.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ package com.expediagroup.graphql.examples.server.ktor
1919
import com.expediagroup.graphql.examples.server.ktor.schema.dataloaders.BookDataLoader
2020
import com.expediagroup.graphql.examples.server.ktor.schema.dataloaders.CourseDataLoader
2121
import com.expediagroup.graphql.examples.server.ktor.schema.dataloaders.UniversityDataLoader
22-
import com.expediagroup.graphql.server.execution.dataloader.DataLoaderRegistryFactory
22+
import com.expediagroup.graphql.dataloader.DataLoaderRegistryFactory
2323
import org.dataloader.DataLoaderFactory
2424
import org.dataloader.DataLoaderRegistry
2525

examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/schema/dataloaders/BookDataLoader.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package com.expediagroup.graphql.examples.server.ktor.schema.dataloaders
1818

1919
import com.expediagroup.graphql.examples.server.ktor.schema.models.Book
20-
import com.expediagroup.graphql.server.execution.dataloader.KotlinDataLoader
20+
import com.expediagroup.graphql.dataloader.KotlinDataLoader
2121
import kotlinx.coroutines.runBlocking
2222
import org.dataloader.BatchLoader
2323
import java.util.concurrent.CompletableFuture

examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/schema/dataloaders/CourseDataLoader.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package com.expediagroup.graphql.examples.server.ktor.schema.dataloaders
1818

1919
import com.expediagroup.graphql.examples.server.ktor.schema.models.Course
20-
import com.expediagroup.graphql.server.execution.dataloader.KotlinDataLoader
20+
import com.expediagroup.graphql.dataloader.KotlinDataLoader
2121
import kotlinx.coroutines.runBlocking
2222
import org.dataloader.BatchLoader
2323
import java.util.concurrent.CompletableFuture

examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/schema/dataloaders/UniversityDataLoader.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package com.expediagroup.graphql.examples.server.ktor.schema.dataloaders
1818

1919
import com.expediagroup.graphql.examples.server.ktor.schema.models.University
20-
import com.expediagroup.graphql.server.execution.dataloader.KotlinDataLoader
20+
import com.expediagroup.graphql.dataloader.KotlinDataLoader
2121
import kotlinx.coroutines.runBlocking
2222
import org.dataloader.BatchLoader
2323
import java.util.concurrent.CompletableFuture

examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/dataloaders/CompanyDataLoader.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package com.expediagroup.graphql.examples.server.spring.dataloaders
1818

1919
import com.expediagroup.graphql.examples.server.spring.model.Company
20-
import com.expediagroup.graphql.server.execution.dataloader.KotlinDataLoader
20+
import com.expediagroup.graphql.dataloader.KotlinDataLoader
2121
import org.dataloader.BatchLoader
2222
import org.springframework.stereotype.Component
2323
import java.util.concurrent.CompletableFuture
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# GraphQL Kotlin Data Loader
2+
[![Maven Central](https://img.shields.io/maven-central/v/com.expediagroup/graphql-kotlin-dataloader.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.expediagroup%22%20AND%20a:%22graphql-kotlin-dataloader%22)
3+
[![Javadocs](https://img.shields.io/maven-central/v/com.expediagroup/graphql-kotlin-dataloader.svg?label=javadoc&colorB=brightgreen)](https://www.javadoc.io/doc/com.expediagroup/graphql-kotlin-dataloader)
4+
5+
Data Loaders are a popular caching pattern from the [JavaScript GraphQL implementation](https://github.com/graphql/dataloader).
6+
7+
Since `graphql-java` provides [support for this pattern](https://www.graphql-java.com/documentation/batching/)
8+
using the [DataLoader](https://github.com/graphql-java/java-dataloader/blob/master/src/main/java/org/dataloader/DataLoader.java)
9+
and [DataLoaderRegistry](https://github.com/graphql-java/java-dataloader/blob/master/src/main/java/org/dataloader/DataLoaderRegistry.java).
10+
11+
`graphql-kotlin` provides support for `DataLoaders` with this `graphql-kotlin-dataloader` module through common interfaces.
12+
13+
14+
## KotlinDataLoader
15+
16+
To help in the registration of `DataLoaders`, we have created a basic interface `KotlinDataLoader`:
17+
18+
```kotlin
19+
interface KotlinDataLoader<K, V> {
20+
val dataLoaderName: String
21+
fun getBatchLoader(): BatchLoader<K, V>
22+
fun getOptions(): DataLoaderOptions = DataLoaderOptions.newOptions()
23+
}
24+
```
25+
26+
This allows for library users to still have full control over the creation of the DataLoader
27+
and its various configuration options.
28+
29+
```kotlin
30+
class UserDataLoader : KotlinDataLoader<ID, User> {
31+
override val dataLoaderName = "UserDataLoader"
32+
override fun getBatchLoader() = BatchLoader<ID, User> { ids ->
33+
CompletableFuture.supplyAsync {
34+
ids.map { id -> userService.getUser(id) }
35+
}
36+
}
37+
override fun getOptions() = DataLoaderOptions.newOptions().setCachingEnabled(false)
38+
}
39+
```
40+
41+
## KotlinDataLoaderRegistryFactory
42+
43+
Factory that facilitates the instantiation of a [KotlinDataLoaderRegistry](src/main/kotlin/com/expediagroup/graphql/dataloader/KotlinDataLoaderRegistry.kt) which is just
44+
a decorator of the original `graphql-java` [DataLoaderRegistry](https://github.com/graphql-java/java-dataloader/blob/master/src/main/java/org/dataloader/DataLoaderRegistry.java).
45+
with the addition of allowing access to the state of the `CacheMap` (futures cache) of each `DataLoader` in order to know
46+
all futures state.
47+
48+
## Install it
49+
50+
Using a JVM dependency manager, link `graphql-kotlin-dataloader` to your project.
51+
52+
With Maven:
53+
54+
```xml
55+
<dependency>
56+
<groupId>com.expediagroup</groupId>
57+
<artifactId>graphql-kotlin-dataloader</artifactId>
58+
<version>${latestVersion}</version>
59+
</dependency>
60+
```
61+
62+
With Gradle (example using kts):
63+
64+
```kotlin
65+
implementation("com.expediagroup:graphql-kotlin-dataloader:$latestVersion")
66+
```
67+
68+
## Use it
69+
70+
Use `KotlinDataLoaderRegistryFactory`
71+
72+
```kotlin
73+
val kotlinDataLoaderRegistry = KotlinDataLoaderRegistryFactory(
74+
UserDataLoader()
75+
).generate()
76+
77+
val executionInput = ExecutionInput.newExecutionInput()
78+
.query("query MyAwesomeQuery { foo { bar } }")
79+
.dataLoaderRegistry(kotlinDataLoaderRegistry)
80+
.build()
81+
82+
val result = graphQL.executeAsync(executionInput)
83+
```
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
description = "Graphql Kotlin Data Loader"
2+
3+
val junitVersion: String by project
4+
val graphQLJavaDataLoaderVersion: String by project
5+
val reactorVersion: String by project
6+
val reactorExtensionsVersion: String by project
7+
8+
dependencies {
9+
api("com.graphql-java:java-dataloader:$graphQLJavaDataLoaderVersion")
10+
testImplementation("io.projectreactor.kotlin:reactor-kotlin-extensions:$reactorExtensionsVersion")
11+
testImplementation("io.projectreactor:reactor-core:$reactorVersion")
12+
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion")
13+
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")
14+
}
15+
16+
tasks {
17+
jacocoTestCoverageVerification {
18+
violationRules {
19+
rule {
20+
limit {
21+
counter = "INSTRUCTION"
22+
value = "COVEREDRATIO"
23+
minimum = "0.62".toBigDecimal()
24+
}
25+
limit {
26+
counter = "BRANCH"
27+
value = "COVEREDRATIO"
28+
minimum = "0.80".toBigDecimal()
29+
}
30+
}
31+
}
32+
}
33+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2022 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.dataloader
18+
19+
import org.dataloader.DataLoaderRegistry
20+
21+
/**
22+
* Factory used to generate [DataLoaderRegistry] per GraphQL execution.
23+
*/
24+
interface DataLoaderRegistryFactory {
25+
/**
26+
* Generate [DataLoaderRegistry] to be used for GraphQL request execution.
27+
*/
28+
fun generate(): DataLoaderRegistry
29+
}

servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/execution/dataloader/KotlinDataLoader.kt renamed to executions/graphql-kotlin-dataloader/src/main/kotlin/com/expediagroup/graphql/dataloader/KotlinDataLoader.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.server.execution.dataloader
17+
package com.expediagroup.graphql.dataloader
1818

1919
import org.dataloader.BatchLoader
2020
import org.dataloader.DataLoader
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2022 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.dataloader
18+
19+
import org.dataloader.CacheMap
20+
import org.dataloader.DataLoader
21+
import org.dataloader.DataLoaderRegistry
22+
import org.dataloader.stats.Statistics
23+
import java.util.concurrent.CompletableFuture
24+
import java.util.function.Function
25+
26+
/**
27+
* Custom [DataLoaderRegistry] decorator that has access to the [CacheMap] of each registered [DataLoader]
28+
* in order to keep track of the [futuresToComplete] when [dispatchAll] is invoked,
29+
* that way we can know if all dependants of the [CompletableFuture]s were executed.
30+
*/
31+
class KotlinDataLoaderRegistry(
32+
private val registry: DataLoaderRegistry,
33+
private val futureCacheMaps: List<KotlinDefaultCacheMap<*, *>>
34+
) : DataLoaderRegistry() {
35+
36+
private val futuresToComplete: MutableList<CompletableFuture<*>> = mutableListOf()
37+
38+
override fun register(key: String, dataLoader: DataLoader<*, *>): DataLoaderRegistry = registry.register(key, dataLoader)
39+
override fun <K, V> computeIfAbsent(key: String, mappingFunction: Function<String, DataLoader<*, *>>): DataLoader<K, V> = registry.computeIfAbsent(key, mappingFunction)
40+
override fun combine(registry: DataLoaderRegistry): DataLoaderRegistry = this.registry.combine(registry)
41+
override fun getDataLoaders(): MutableList<DataLoader<*, *>> = registry.dataLoaders
42+
override fun getDataLoadersMap(): MutableMap<String, DataLoader<*, *>> = registry.dataLoadersMap
43+
override fun unregister(key: String): DataLoaderRegistry = registry.unregister(key)
44+
override fun <K, V> getDataLoader(key: String): DataLoader<K, V> = registry.getDataLoader(key)
45+
override fun getKeys(): MutableSet<String> = registry.keys
46+
override fun dispatchAllWithCount(): Int = registry.dispatchAllWithCount()
47+
override fun dispatchDepth(): Int = registry.dispatchDepth()
48+
override fun getStatistics(): Statistics = registry.statistics
49+
50+
/**
51+
* This will invoke [DataLoader.dispatch] on each of the registered [DataLoader]s,
52+
* it will start to keep track of the [CompletableFuture]s of each [DataLoader] by adding them to
53+
* [futuresToComplete]
54+
*/
55+
override fun dispatchAll() {
56+
futuresToComplete.addAll(
57+
futureCacheMaps.map(KotlinDefaultCacheMap<*, *>::values).flatten()
58+
)
59+
registry.dispatchAll()
60+
}
61+
62+
/**
63+
* will return futures that are still waiting for completion
64+
* @return list of completable futures that are waiting for completion
65+
*/
66+
fun getFuturesToComplete(): List<CompletableFuture<*>> = futuresToComplete
67+
68+
/**
69+
* Will signal when all dependants of all [futuresToComplete] were invoked,
70+
* [futuresToComplete] is the list of all [CompletableFuture]s that will complete because the [dispatchAll]
71+
* method was invoked
72+
*/
73+
fun isDispatchedAndCompleted(): Boolean =
74+
futuresToComplete
75+
.all { it.numberOfDependents == 0 }
76+
.also { allFuturesCompleted ->
77+
if (allFuturesCompleted) {
78+
futuresToComplete.clear()
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)