Skip to content

Commit ab08cdd

Browse files
samuelAndalonsamvazquez
and
samvazquez
authored
feat: CompletableFuture in FederatedTypeResolver, standardize entities resolver with other subgraph implementations (#1514)
* feat: use additional directives for schema directives * feat: add test for contact directive * feat: move test to federated schema v2 * feat: remove unused import * feat: initial commit * feat: support completable future in entities data fetcher * feat: fix tests * feat: simplify signature to match other subgraph implementations * feat: update example * feat: update example * feat: update examples * feat: update examples * feat: update examples * feat: update docs * feat: federatedSchemaGeneratorHooks to autowire FederatedTypeResolver implementations * feat: fix test Co-authored-by: samvazquez <[email protected]>
1 parent 7010f6f commit ab08cdd

File tree

33 files changed

+934
-409
lines changed

33 files changed

+934
-409
lines changed

examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/CustomFederatedHooks.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import java.util.UUID
1111
import kotlin.reflect.KType
1212

1313
@Component
14-
class CustomFederatedHooks(resolvers: List<FederatedTypeResolver<*>>) : FederatedSchemaGeneratorHooks(resolvers, true) {
14+
class CustomFederatedHooks(resolvers: List<FederatedTypeResolver>) : FederatedSchemaGeneratorHooks(resolvers, true) {
1515
override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier) {
1616
UUID::class -> graphqlUUIDType
1717
ULocale::class -> graphqlULocaleType

examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/FederatedQuery.kt

+9-13
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import com.expediagroup.graphql.generator.federation.directives.OverrideDirectiv
1010
import com.expediagroup.graphql.generator.federation.directives.ProvidesDirective
1111
import com.expediagroup.graphql.generator.federation.directives.ShareableDirective
1212
import com.expediagroup.graphql.generator.federation.directives.TagDirective
13-
import com.expediagroup.graphql.generator.federation.execution.FederatedTypeResolver
13+
import com.expediagroup.graphql.generator.federation.execution.FederatedTypeSuspendResolver
1414
import com.expediagroup.graphql.generator.scalars.ID
1515
import com.expediagroup.graphql.server.operations.Query
1616
import graphql.schema.DataFetchingEnvironment
@@ -105,28 +105,24 @@ data class User(
105105
)
106106

107107
@Component
108-
class ProductsResolver : FederatedTypeResolver<Product> {
108+
class ProductsResolver : FederatedTypeSuspendResolver<Product> {
109109
override val typeName: String = "Product"
110110

111111
override suspend fun resolve(
112112
environment: DataFetchingEnvironment,
113-
representations: List<Map<String, Any>>
114-
): List<Product?> = representations.map {
115-
Product.byReference(it)
116-
}
113+
representation: Map<String, Any>
114+
): Product? = Product.byReference(representation)
117115
}
118116

119117
@Component
120-
class UserResolver : FederatedTypeResolver<User> {
118+
class UserResolver : FederatedTypeSuspendResolver<User> {
121119
override val typeName: String = "User"
122120

123121
override suspend fun resolve(
124122
environment: DataFetchingEnvironment,
125-
representations: List<Map<String, Any>>
126-
): List<User?> {
127-
return representations.map {
128-
val email = it["email"]?.toString() ?: throw RuntimeException("invalid entity reference")
129-
User(email = email, name = "default", totalProductsCreated = 1337)
130-
}
123+
representation: Map<String, Any>
124+
): User? {
125+
val email = representation["email"]?.toString() ?: throw RuntimeException("invalid entity reference")
126+
return User(email = email, name = "default", totalProductsCreated = 1337)
131127
}
132128
}

examples/federation/base-app/src/main/kotlin/com/expediagroup/graphql/examples/federation/base/hooks/CustomFederationSchemaGeneratorHooks.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import kotlin.reflect.KType
3030
/**
3131
* Schema generator hook that adds additional scalar types.
3232
*/
33-
class CustomFederationSchemaGeneratorHooks(resolvers: List<FederatedTypeResolver<*>>) : FederatedSchemaGeneratorHooks(resolvers) {
33+
class CustomFederationSchemaGeneratorHooks(resolvers: List<FederatedTypeResolver>) : FederatedSchemaGeneratorHooks(resolvers) {
3434

3535
/**
3636
* Register additional GraphQL scalar types.

examples/federation/extend-app/src/main/kotlin/com/expediagroup/graphql/examples/federation/extend/schema/WidgetResolver.kt

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,24 +17,27 @@
1717
package com.expediagroup.graphql.examples.federation.extend.schema
1818

1919
import com.expediagroup.graphql.examples.federation.extend.service.RandomNumberService
20-
import com.expediagroup.graphql.generator.federation.execution.FederatedTypeResolver
20+
import com.expediagroup.graphql.generator.federation.execution.FederatedTypeSuspendResolver
2121
import graphql.schema.DataFetchingEnvironment
2222
import org.springframework.stereotype.Component
2323

2424
@Component
25-
class WidgetResolver(private val randomNumberService: RandomNumberService) : FederatedTypeResolver<Widget> {
25+
class WidgetResolver(private val randomNumberService: RandomNumberService) : FederatedTypeSuspendResolver<Widget> {
2626
override val typeName: String = "Widget"
2727

2828
@Suppress("UNCHECKED_CAST")
29-
override suspend fun resolve(environment: DataFetchingEnvironment, representations: List<Map<String, Any>>): List<Widget?> = representations.map {
30-
// Extract the 'id' from the other service
31-
val id = it["id"]?.toString()?.toIntOrNull() ?: throw InvalidWidgetIdException()
32-
val listOfValues = it["listOfValues"] as? List<Int>
29+
override suspend fun resolve(
30+
environment: DataFetchingEnvironment,
31+
representation: Map<String, Any>
32+
): Widget? {
33+
// Extract the 'id' from the representation map provided by the other service
34+
val id = representation["id"]?.toString()?.toIntOrNull() ?: throw InvalidWidgetIdException()
35+
val listOfValues = representation["listOfValues"] as? List<Int>
3336

3437
// If we needed to construct a Widget which has data from other APIs,
3538
// this is the place where we could call them with the widget id
3639
val valueFromExtend = randomNumberService.getInt()
37-
Widget(id, listOfValues, valueFromExtend)
40+
return Widget(id, listOfValues, valueFromExtend)
3841
}
3942

4043
class InvalidWidgetIdException : RuntimeException()

generator/graphql-kotlin-federation/README.md

+11-5
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,16 @@ type _Service {
121121

122122
### Extended Schema (Reviews Subgraph)
123123

124-
Extended federated GraphQL schemas provide additional functionality to the types already exposed by other GraphQL services. In the example below, `Product` type is extended to add new `reviews` field to it. Primary key needed to instantiate the `Product` type (i.e. `id`) has to match the `@key` definition on the base type. Since primary keys are defined on the base type and are only referenced from the extended type, all of the fields that are part of the field set specified in `@key` directive have to be marked as `@external`.
124+
Extended federated GraphQL schemas provide additional functionality to the types already exposed by other GraphQL services.
125+
In the example below, `Product` type is extended to add new `reviews` field to it. Primary key needed to instantiate
126+
the `Product` type (i.e. `id`) has to match the `@key` definition on the base type.
127+
Since primary keys are defined on the base type and are only referenced from the extended type,
128+
all the fields that are part of the field set specified in `@key` directive have to be marked as `@external`.
125129

126130
```kotlin
127131
@KeyDirective(fields = FieldSet("id"))
128132
@ExtendsDirective
129133
data class Product(@ExternalDirective val id: Int) {
130-
131134
fun reviews(): List<Review> {
132135
// returns list of product reviews
133136
}
@@ -136,9 +139,12 @@ data class Product(@ExternalDirective val id: Int) {
136139
data class Review(val reviewId: String, val text: String)
137140

138141
// Generate the schema
139-
val productResolver = object: FederatedTypeResolver<Product> {
140-
override fun resolve(keys: Map<String, Any>): Product {
141-
val id = keys["id"]?.toString()?.toIntOrNull()
142+
val productResolver = object: FederatedTypeSuspendResolver<Product> {
143+
override fun resolve(
144+
environment: DataFetchingEnvironment,
145+
representation: Map<String, Any>
146+
): Product {
147+
val id = representation["id"]?.toString()?.toIntOrNull()
142148
// instantiate product using id
143149
}
144150
}

generator/graphql-kotlin-federation/build.gradle.kts

+4
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ description = "Federated GraphQL schema generator"
22

33
val junitVersion: String by project
44
val federationGraphQLVersion: String by project
5+
val reactorVersion: String by project
6+
val reactorExtensionsVersion: String by project
57

68
dependencies {
79
api(project(path = ":graphql-kotlin-schema-generator"))
810
api("com.apollographql.federation:federation-graphql-java-support:$federationGraphQLVersion")
11+
testImplementation("io.projectreactor.kotlin:reactor-kotlin-extensions:$reactorExtensionsVersion")
12+
testImplementation("io.projectreactor:reactor-core:$reactorVersion")
913
testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion")
1014
}
1115

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorHooks.kt

+6-3
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import com.expediagroup.graphql.generator.federation.directives.SHAREABLE_DIRECT
4040
import com.expediagroup.graphql.generator.federation.directives.TAG_DIRECTIVE_TYPE
4141
import com.expediagroup.graphql.generator.federation.directives.appliedLinkDirective
4242
import com.expediagroup.graphql.generator.federation.exception.IncorrectFederatedDirectiveUsage
43-
import com.expediagroup.graphql.generator.federation.execution.EntityResolver
43+
import com.expediagroup.graphql.generator.federation.execution.EntitiesDataFetcher
4444
import com.expediagroup.graphql.generator.federation.execution.FederatedTypeResolver
4545
import com.expediagroup.graphql.generator.federation.types.ANY_SCALAR_TYPE
4646
import com.expediagroup.graphql.generator.federation.types.ENTITY_UNION_NAME
@@ -67,7 +67,10 @@ import kotlin.reflect.full.findAnnotation
6767
/**
6868
* Hooks for generating federated GraphQL schema.
6969
*/
70-
open class FederatedSchemaGeneratorHooks(private val resolvers: List<FederatedTypeResolver<*>>, private val optInFederationV2: Boolean = false) : SchemaGeneratorHooks {
70+
open class FederatedSchemaGeneratorHooks(
71+
private val resolvers: List<FederatedTypeResolver>,
72+
private val optInFederationV2: Boolean = false
73+
) : SchemaGeneratorHooks {
7174
private val validator = FederatedSchemaValidator()
7275

7376
private val federationV2OnlyDirectiveNames: Set<String> = setOf(
@@ -158,7 +161,7 @@ open class FederatedSchemaGeneratorHooks(private val resolvers: List<FederatedTy
158161
val entityField = generateEntityFieldDefinition(entityTypeNames)
159162
federatedQuery.field(entityField)
160163

161-
federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, entityField.name), EntityResolver(resolvers))
164+
federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, entityField.name), EntitiesDataFetcher(resolvers))
162165
federatedCodeRegistry.typeResolver(ENTITY_UNION_NAME) { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObjectName()) }
163166

164167
builder.query(federatedQuery)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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.generator.federation.execution
18+
19+
import com.expediagroup.graphql.generator.federation.exception.InvalidFederatedRequest
20+
import com.expediagroup.graphql.generator.federation.execution.resolverexecutor.FederatedTypePromiseResolverExecutor
21+
import com.expediagroup.graphql.generator.federation.execution.resolverexecutor.FederatedTypeSuspendResolverExecutor
22+
import com.expediagroup.graphql.generator.federation.execution.resolverexecutor.ResolvableEntity
23+
import com.expediagroup.graphql.generator.federation.extensions.collectAll
24+
import com.expediagroup.graphql.generator.federation.extensions.toDataFetcherResult
25+
import graphql.execution.DataFetcherResult
26+
import graphql.schema.DataFetcher
27+
import graphql.schema.DataFetchingEnvironment
28+
import java.util.concurrent.CompletableFuture
29+
30+
private const val TYPENAME_FIELD = "__typename"
31+
private const val REPRESENTATIONS = "representations"
32+
33+
/**
34+
* Federated _entities field data fetcher.
35+
*/
36+
open class EntitiesDataFetcher(
37+
resolvers: List<FederatedTypeResolver>
38+
) : DataFetcher<CompletableFuture<DataFetcherResult<List<Any?>>>> {
39+
40+
constructor(vararg resolvers: FederatedTypeResolver) : this(resolvers.toList())
41+
/**
42+
* Pre-compute resolvers by typename so, we don't have to search on every request
43+
*/
44+
private val resolversByType: Map<String, FederatedTypeResolver> = resolvers.associateBy(FederatedTypeResolver::typeName)
45+
46+
/**
47+
* Resolves entities based on the passed in representations argument. Entities are resolved in the same order
48+
* they are specified in the list of representations. If target representation cannot be resolved, NULL will
49+
* be returned instead.
50+
*
51+
* Representations are grouped by the underlying typename and each batch is resolved asynchronously before merging
52+
* the results back into a single list that preserves the original order.
53+
*
54+
* @return list of resolved nullable entities
55+
*/
56+
override fun get(env: DataFetchingEnvironment): CompletableFuture<DataFetcherResult<List<Any?>>> {
57+
val representations: List<Map<String, Any>> = env.getArgument(REPRESENTATIONS)
58+
59+
val representationsWithoutResolver = mutableListOf<IndexedValue<Map<String, Any>>>()
60+
val entitiesWithPromiseResolver = mutableListOf<ResolvableEntity<FederatedTypePromiseResolver<*>>>()
61+
val entitiesWithSuspendResolver = mutableListOf<ResolvableEntity<FederatedTypeSuspendResolver<*>>>()
62+
63+
representations.withIndex()
64+
.groupBy { (_, representation) -> representation[TYPENAME_FIELD].toString() }
65+
.forEach { (typeName, indexedRequests) ->
66+
when (val resolver = resolversByType[typeName]) {
67+
is FederatedTypePromiseResolver<*> -> {
68+
entitiesWithPromiseResolver += ResolvableEntity(typeName, indexedRequests, resolver)
69+
}
70+
is FederatedTypeSuspendResolver<*> -> {
71+
entitiesWithSuspendResolver += ResolvableEntity(typeName, indexedRequests, resolver)
72+
}
73+
null -> {
74+
representationsWithoutResolver += indexedRequests
75+
}
76+
}
77+
}
78+
79+
val noResolverErrors: CompletableFuture<List<Map<Int, Any?>>> = CompletableFuture.completedFuture(
80+
listOf(
81+
representationsWithoutResolver.associateBy(IndexedValue<Map<String, Any>>::index) { (_, representation) ->
82+
InvalidFederatedRequest("Unable to resolve federated type, representation=$representation")
83+
}
84+
)
85+
)
86+
87+
val promises: List<CompletableFuture<List<Map<Int, Any?>>>> = listOf(
88+
FederatedTypePromiseResolverExecutor.execute(entitiesWithPromiseResolver, env),
89+
FederatedTypeSuspendResolverExecutor.execute(entitiesWithSuspendResolver, env),
90+
noResolverErrors
91+
)
92+
93+
return promises
94+
.collectAll()
95+
.thenApply { results ->
96+
results.asSequence()
97+
.flatten()
98+
.map(Map<Int, Any?>::toList)
99+
.flatten()
100+
.sortedBy(Pair<Int, Any?>::first)
101+
.map(Pair<Int, Any?>::second)
102+
.toDataFetcherResult()
103+
}
104+
}
105+
}

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/execution/EntityResolver.kt

-92
This file was deleted.

0 commit comments

Comments
 (0)