Skip to content

EntityResolver interface #1336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.expediagroup.graphql.generator.federation.directives.PROVIDES_DIRECTI
import com.expediagroup.graphql.generator.federation.directives.REQUIRES_DIRECTIVE_TYPE
import com.expediagroup.graphql.generator.federation.execution.EntityResolver
import com.expediagroup.graphql.generator.federation.execution.FederatedTypeResolver
import com.expediagroup.graphql.generator.federation.execution.GlobalScopeEntityResolver
import com.expediagroup.graphql.generator.federation.extensions.addDirectivesIfNotPresent
import com.expediagroup.graphql.generator.federation.types.ANY_SCALAR_TYPE
import com.expediagroup.graphql.generator.federation.types.ENTITY_UNION_NAME
Expand All @@ -52,7 +53,10 @@ import kotlin.reflect.full.findAnnotation
/**
* Hooks for generating federated GraphQL schema.
*/
open class FederatedSchemaGeneratorHooks(private val resolvers: List<FederatedTypeResolver<*>>) : SchemaGeneratorHooks {
open class FederatedSchemaGeneratorHooks(private val entityResolver: EntityResolver<*>) : SchemaGeneratorHooks {

constructor(resolvers: List<FederatedTypeResolver<*>>) : this(GlobalScopeEntityResolver(resolvers))

private val scalarDefinitionRegex = "(^\".+\"$[\\r\\n])?^scalar (_FieldSet|_Any)$[\\r\\n]*".toRegex(setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
private val emptyQueryRegex = "^type Query @extends \\s*\\{\\s*}\\s*".toRegex(setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
private val serviceFieldRegex = "\\s*_service: _Service".toRegex(setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
Expand Down Expand Up @@ -92,7 +96,7 @@ open class FederatedSchemaGeneratorHooks(private val resolvers: List<FederatedTy
val entityField = generateEntityFieldDefinition(entityTypeNames)
federatedQuery.field(entityField)

federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, entityField.name), EntityResolver(resolvers))
federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, entityField.name), entityResolver)
federatedCodeRegistry.typeResolver(ENTITY_UNION_NAME) { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObjectName()) }
federatedSchemaBuilder.additionalType(ANY_SCALAR_TYPE)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,22 @@ import graphql.GraphQLError
import graphql.execution.DataFetcherResult
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.future.future
import java.util.concurrent.CompletableFuture

private const val TYPENAME_FIELD = "__typename"
private const val REPRESENTATIONS = "representations"

/**
* Federated _entities query resolver.
*/
open class EntityResolver(resolvers: List<FederatedTypeResolver<*>>) : DataFetcher<CompletableFuture<DataFetcherResult<List<Any?>>>> {
interface EntityResolver<T> : DataFetcher<T> {

/**
* Pre-compute the resolves by typename so we don't have to search on every request
* Pre-computed resolvers by typename, so we don't have to search on every request
*/
private val resolverMap: Map<String, FederatedTypeResolver<*>> = resolvers.associateBy { it.typeName }
val resolverMap: Map<String, FederatedTypeResolver<*>>

/**
* Resolves entities based on the passed in representations argument. Entities are resolved in the same order
Expand All @@ -50,32 +47,30 @@ open class EntityResolver(resolvers: List<FederatedTypeResolver<*>>) : DataFetch
*
* @return list of resolved nullable entities
*/
override fun get(env: DataFetchingEnvironment): CompletableFuture<DataFetcherResult<List<Any?>>> {
suspend fun resolve(env: DataFetchingEnvironment): DataFetcherResult<List<Any?>> {
val representations: List<Map<String, Any>> = env.getArgument(REPRESENTATIONS)
val indexedBatchRequestsByType = representations.withIndex().groupBy { it.value[TYPENAME_FIELD].toString() }

return GlobalScope.future {
val data = mutableListOf<Any?>()
val errors = mutableListOf<GraphQLError>()
val data = mutableListOf<Any?>()
val errors = mutableListOf<GraphQLError>()

resolveRequests(indexedBatchRequestsByType, env)
.flatten()
.sortedBy { it.first }
.forEach {
val result = it.second
if (result is GraphQLError) {
data.add(null)
errors.add(result)
} else {
data.add(result)
}
resolveRequests(indexedBatchRequestsByType, env)
.flatten()
.sortedBy { it.first }
.forEach {
val result = it.second
if (result is GraphQLError) {
data.add(null)
errors.add(result)
} else {
data.add(result)
}
}

DataFetcherResult.newResult<List<Any?>>()
.data(data)
.errors(errors)
.build()
}
return DataFetcherResult.newResult<List<Any?>>()
.data(data)
.errors(errors)
.build()
}

private suspend fun resolveRequests(indexedBatchRequestsByType: Map<String, List<IndexedValue<Map<String, Any>>>>, env: DataFetchingEnvironment): List<List<Pair<Int, Any?>>> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2021 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.execution

import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.future.future
import java.util.concurrent.CompletableFuture

/**
* [EntityResolver] that uses the `GlobalScope` coroutine for resolving the Entities
*/
open class GlobalScopeEntityResolver(resolvers: List<FederatedTypeResolver<*>>) : EntityResolver<CompletableFuture<DataFetcherResult<List<Any?>>>> {

override val resolverMap: Map<String, FederatedTypeResolver<*>> = resolvers.associateBy { it.typeName }

override fun get(env: DataFetchingEnvironment): CompletableFuture<DataFetcherResult<List<Any?>>> {
return GlobalScope.future {
resolve(env)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,28 @@ import com.expediagroup.graphql.generator.federation.data.UserResolver
import com.expediagroup.graphql.generator.federation.data.queries.federated.Book
import com.expediagroup.graphql.generator.federation.data.queries.federated.User
import graphql.GraphQLError
import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class EntityQueryResolverTest {

@Test
fun `verify can resolve federated entities`() {
val resolver = EntityResolver(listOf(UserResolver()))
val resolver = generateResolver(listOf(UserResolver()))
val representations = listOf(mapOf<String, Any>("__typename" to "User", "userId" to 123, "name" to "testName"))
val env = mockk<DataFetchingEnvironment> {
every { getArgument<Any>(any()) } returns representations
}

val result = resolver.get(env).get()
val result = resolver.get(env)
verifyData(result.data, User(123, "testName"))
verifyErrors(result.errors)
}
Expand All @@ -53,25 +55,25 @@ class EntityQueryResolverTest {
val mockUserResolver: FederatedTypeResolver<*> = mockk {
every { typeName } returns "User"
}
val resolver = EntityResolver(listOf(mockBookResolver, mockUserResolver))
val resolver = generateResolver(listOf(mockBookResolver, mockUserResolver))
val env = mockk<DataFetchingEnvironment> {
every { getArgument<Any>(any()) } returns listOf(emptyMap<String, Any>())
}

val result = resolver.get(env).get()
val result = resolver.get(env)
verifyData(result.data, null)
verifyErrors(result.errors, "Unable to resolve federated type, representation={}")
}

@Test
fun `verify federated entity resolver returns GraphQLError if __typename cannot be resolved`() {
val resolver = EntityResolver(emptyList())
val resolver = generateResolver(emptyList())
val representations = listOf(mapOf<String, Any>("__typename" to "User", "userId" to 123, "name" to "testName"))
val env = mockk<DataFetchingEnvironment> {
every { getArgument<Any>(any()) } returns representations
}

val result = resolver.get(env).get()
val result = resolver.get(env)
verifyData(result.data, null)
verifyErrors(result.errors, "Unable to resolve federated type, representation={__typename=User, userId=123, name=testName}")
}
Expand All @@ -82,13 +84,13 @@ class EntityQueryResolverTest {
every { typeName } returns "User"
coEvery { resolve(any(), any()) } throws RuntimeException("JUnit exception")
}
val resolver = EntityResolver(listOf(mockUserResolver))
val resolver = generateResolver(listOf(mockUserResolver))
val representations = listOf(mapOf<String, Any>("__typename" to "User", "userId" to 123, "name" to "testName"))
val env: DataFetchingEnvironment = mockk {
every { getArgument<Any>(any()) } returns representations
}

val result = resolver.get(env).get()
val result = resolver.get(env)
verifyData(result.data, null)
verifyErrors(result.errors, "Exception was thrown while trying to resolve federated type, representation={__typename=User, userId=123, name=testName}")
}
Expand All @@ -107,8 +109,8 @@ class EntityQueryResolverTest {

val spyUserResolver = spyk(UserResolver())
val spyBookResolver = spyk(BookResolver())
val resolver = EntityResolver(listOf(spyUserResolver, spyBookResolver))
val result = resolver.get(env).get()
val resolver = generateResolver(listOf(spyUserResolver, spyBookResolver))
val result = resolver.get(env)

verifyData(result.data, user1, book, user2)
verifyErrors(result.errors)
Expand All @@ -135,8 +137,8 @@ class EntityQueryResolverTest {
every { typeName } returns "Book"
coEvery { resolve(any(), any()) } throws RuntimeException("JUnit")
}
val resolver = EntityResolver(listOf(spyUserResolver, mockBookResolver))
val result = resolver.get(env).get()
val resolver = generateResolver(listOf(spyUserResolver, mockBookResolver))
val result = resolver.get(env)

verifyData(result.data, user, null)
verifyErrors(result.errors, "Exception was thrown while trying to resolve federated type, representation={__typename=Book, id=988, weight=1.0}")
Expand All @@ -159,8 +161,8 @@ class EntityQueryResolverTest {
every { typeName } returns "User"
coEvery { resolve(any(), any()) } returns listOf(user)
}
val resolver = EntityResolver(listOf(mockUserResolver))
val result = resolver.get(env).get()
val resolver = generateResolver(listOf(mockUserResolver))
val result = resolver.get(env)

verifyData(result.data, null, null)
verifyErrors(
Expand Down Expand Up @@ -188,6 +190,14 @@ class EntityQueryResolverTest {
}
}

private fun generateResolver(resolvers: List<FederatedTypeResolver<*>>) = object : EntityResolver<DataFetcherResult<List<Any?>>> {
override val resolverMap: Map<String, FederatedTypeResolver<*>> = resolvers.associateBy { it.typeName }

override fun get(environment: DataFetchingEnvironment): DataFetcherResult<List<Any?>> {
return runBlocking { resolve(environment) }
}
}

private fun Book.toRepresentation() = mapOf("__typename" to "Book", "id" to id, "weight" to weight)
private fun User.toRepresentation() = mapOf("__typename" to "User", "userId" to userId, "name" to name)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2021 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.generator.federation.execution

import com.expediagroup.graphql.generator.federation.data.UserResolver
import graphql.schema.DataFetchingEnvironment
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import kotlin.test.assertNotNull

class GlobalScopeEntityQueryResolverTest {

@Test
fun `verify it returns a CompletableFuture`() {
val resolver = GlobalScopeEntityResolver(listOf(UserResolver()))
val representations = listOf(mapOf<String, Any>("__typename" to "User", "userId" to 123, "name" to "testName"))
val env = mockk<DataFetchingEnvironment> {
every { getArgument<Any>(any()) } returns representations
}

val result = resolver.get(env).get()
assertNotNull(result)
}
}