Skip to content

Commit a41fc35

Browse files
smyrickShane Myrick
andauthored
Refactor federation code for readability (#887)
Co-authored-by: Shane Myrick <[email protected]>
1 parent 3984d83 commit a41fc35

File tree

6 files changed

+83
-49
lines changed

6 files changed

+83
-49
lines changed

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

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.expediagroup.graphql.federation.execution.EntityResolver
3030
import com.expediagroup.graphql.federation.execution.FederatedTypeResolver
3131
import com.expediagroup.graphql.federation.extensions.addDirectivesIfNotPresent
3232
import com.expediagroup.graphql.federation.types.ANY_SCALAR_TYPE
33+
import com.expediagroup.graphql.federation.types.ENTITY_UNION_NAME
3334
import com.expediagroup.graphql.federation.types.FIELD_SET_SCALAR_TYPE
3435
import com.expediagroup.graphql.federation.types.SERVICE_FIELD_DEFINITION
3536
import com.expediagroup.graphql.federation.types._Service
@@ -83,52 +84,71 @@ open class FederatedSchemaGeneratorHooks(private val resolvers: List<FederatedTy
8384
.field(SERVICE_FIELD_DEFINITION)
8485
.withDirective(EXTENDS_DIRECTIVE_TYPE)
8586

86-
/**
87-
* Register the data fetcher for the SDL returned by _service field.
88-
*
89-
* It should NOT contain:
90-
* - default schema definition
91-
* - empty Query type
92-
* - any directive definitions
93-
* - any custom directives
94-
* - new federated scalars
95-
*/
96-
val sdl = originalSchema.print(
97-
includeDefaultSchemaDefinition = false,
98-
includeDirectiveDefinitions = false,
99-
includeDirectivesFilter = customDirectivePredicate
100-
).replace(scalarDefinitionRegex, "")
101-
.replace(emptyQueryRegex, "")
102-
.trim()
87+
// Register the data fetcher for the _service query
88+
val sdl = getFederatedServiceSdl(originalSchema)
10389
federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, SERVICE_FIELD_DEFINITION.name), DataFetcher { _Service(sdl) })
10490

105-
val entityTypeNames = originalSchema.allTypesAsList
106-
.asSequence()
107-
.filterIsInstance<GraphQLObjectType>()
108-
.filter { type -> type.getDirective(KEY_DIRECTIVE_NAME) != null }
109-
.map { it.name }
110-
.toSet()
111-
112-
// Add the _entities field to the query
91+
// Add the _entities field to the query and register all the _Entity union types
92+
val entityTypeNames = getFederatedEntities(originalSchema)
11393
if (entityTypeNames.isNotEmpty()) {
11494
val entityField = generateEntityFieldDefinition(entityTypeNames)
11595
federatedQuery.field(entityField)
11696

11797
federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, entityField.name), EntityResolver(resolvers))
118-
federatedCodeRegistry.typeResolver("_Entity") { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObjectName()) }
98+
federatedCodeRegistry.typeResolver(ENTITY_UNION_NAME) { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObjectName()) }
11999
federatedSchemaBuilder.additionalType(ANY_SCALAR_TYPE)
120100
}
121101

122102
return federatedSchemaBuilder.query(federatedQuery.build())
123103
.codeRegistry(federatedCodeRegistry.build())
124104
}
125105

126-
// skip validation for empty query type - federation will add _service query
106+
/**
107+
* Skip validation for empty query type - federation will add _service query
108+
*/
127109
override fun didGenerateQueryObject(type: GraphQLObjectType): GraphQLObjectType = type
128-
}
129110

130-
private fun TypeResolutionEnvironment.getObjectName(): String? {
131-
val kClass = this.getObject<Any>().javaClass.kotlin
132-
return kClass.findAnnotation<GraphQLName>()?.value
133-
?: kClass.simpleName
111+
/**
112+
* Get the modified SDL returned by _service field
113+
*
114+
* It should NOT contain:
115+
* - default schema definition
116+
* - empty Query type
117+
* - any directive definitions
118+
* - any custom directives
119+
* - new federated scalars
120+
*
121+
* See the federation spec for more details:
122+
* https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#query_service
123+
*/
124+
private fun getFederatedServiceSdl(schema: GraphQLSchema): String {
125+
return schema.print(
126+
includeDefaultSchemaDefinition = false,
127+
includeDirectiveDefinitions = false,
128+
includeDirectivesFilter = customDirectivePredicate
129+
).replace(scalarDefinitionRegex, "")
130+
.replace(emptyQueryRegex, "")
131+
.trim()
132+
}
133+
134+
/**
135+
* Get all the federation entities in the _Entity union, aka all the types with the @key directive.
136+
*
137+
* See the federation spec:
138+
* https://www.apollographql.com/docs/apollo-server/federation/federation-spec/#union-_entity
139+
*/
140+
private fun getFederatedEntities(originalSchema: GraphQLSchema): Set<String> {
141+
return originalSchema.allTypesAsList
142+
.asSequence()
143+
.filterIsInstance<GraphQLObjectType>()
144+
.filter { type -> type.getDirective(KEY_DIRECTIVE_NAME) != null }
145+
.map { it.name }
146+
.toSet()
147+
}
148+
149+
private fun TypeResolutionEnvironment.getObjectName(): String? {
150+
val kClass = this.getObject<Any>().javaClass.kotlin
151+
return kClass.findAnnotation<GraphQLName>()?.value
152+
?: kClass.simpleName
153+
}
134154
}

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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ import graphql.GraphQLError
2020
import graphql.execution.DataFetcherResult
2121
import graphql.schema.DataFetcher
2222
import graphql.schema.DataFetchingEnvironment
23+
import java.util.concurrent.CompletableFuture
2324
import kotlinx.coroutines.GlobalScope
2425
import kotlinx.coroutines.async
2526
import kotlinx.coroutines.awaitAll
27+
import kotlinx.coroutines.coroutineScope
2628
import kotlinx.coroutines.future.future
27-
import java.util.concurrent.CompletableFuture
2829

2930
private const val TYPENAME_FIELD = "__typename"
3031
private const val REPRESENTATIONS = "representations"
@@ -51,16 +52,13 @@ open class EntityResolver(resolvers: List<FederatedTypeResolver<*>>) : DataFetch
5152
*/
5253
override fun get(env: DataFetchingEnvironment): CompletableFuture<DataFetcherResult<List<Any?>>> {
5354
val representations: List<Map<String, Any>> = env.getArgument(REPRESENTATIONS)
54-
5555
val indexedBatchRequestsByType = representations.withIndex().groupBy { it.value[TYPENAME_FIELD].toString() }
56+
5657
return GlobalScope.future {
5758
val data = mutableListOf<Any?>()
5859
val errors = mutableListOf<GraphQLError>()
59-
indexedBatchRequestsByType.map { (typeName, indexedRequests) ->
60-
async {
61-
resolveType(env, typeName, indexedRequests, resolverMap)
62-
}
63-
}.awaitAll()
60+
61+
resolveRequests(indexedBatchRequestsByType, env)
6462
.flatten()
6563
.sortedBy { it.first }
6664
.forEach {
@@ -72,10 +70,21 @@ open class EntityResolver(resolvers: List<FederatedTypeResolver<*>>) : DataFetch
7270
data.add(result)
7371
}
7472
}
73+
7574
DataFetcherResult.newResult<List<Any?>>()
7675
.data(data)
7776
.errors(errors)
7877
.build()
7978
}
8079
}
80+
81+
private suspend fun resolveRequests(indexedBatchRequestsByType: Map<String, List<IndexedValue<Map<String, Any>>>>, env: DataFetchingEnvironment): List<List<Pair<Int, Any?>>> {
82+
return coroutineScope {
83+
indexedBatchRequestsByType.map { (typeName, indexedRequests) ->
84+
async {
85+
resolveType(env, typeName, indexedRequests, resolverMap)
86+
}
87+
}.awaitAll()
88+
}
89+
}
8190
}

graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/types/_Entity.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import graphql.schema.GraphQLNonNull
2323
import graphql.schema.GraphQLTypeReference
2424
import graphql.schema.GraphQLUnionType
2525

26+
internal const val ENTITY_UNION_NAME = "_Entity"
27+
internal const val ENTITIES_FIELD_NAME = "_entities"
28+
2629
/**
2730
* Generates union of all types that use the @key directive, including both types native to the schema and extended types.
2831
*/
@@ -47,14 +50,14 @@ internal fun generateEntityFieldDefinition(federatedTypes: Set<String>): GraphQL
4750
val graphQLType = GraphQLNonNull(
4851
GraphQLList(
4952
GraphQLUnionType.newUnionType()
50-
.name("_Entity")
53+
.name(ENTITY_UNION_NAME)
5154
.possibleTypes(*possibleTypes)
5255
.build()
5356
)
5457
)
5558

5659
return GraphQLFieldDefinition.newFieldDefinition()
57-
.name("_entities")
60+
.name(ENTITIES_FIELD_NAME)
5861
.description("Union of all types that use the @key directive, including both types native to the schema and extended types")
5962
.argument(graphQLArgument)
6063
.type(graphQLType)

graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/FederatedSchemaGeneratorTest.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ import com.expediagroup.graphql.extensions.print
2121
import com.expediagroup.graphql.federation.data.queries.simple.NestedQuery
2222
import com.expediagroup.graphql.federation.data.queries.simple.SimpleQuery
2323
import com.expediagroup.graphql.federation.directives.KEY_DIRECTIVE_NAME
24+
import com.expediagroup.graphql.federation.types.ENTITY_UNION_NAME
2425
import graphql.schema.GraphQLUnionType
25-
import org.junit.jupiter.api.Assertions.assertEquals
26-
import org.junit.jupiter.api.Test
2726
import kotlin.test.assertNotNull
2827
import kotlin.test.assertTrue
28+
import org.junit.jupiter.api.Assertions.assertEquals
29+
import org.junit.jupiter.api.Test
2930

3031
private val FEDERATED_SDL =
3132
"""
@@ -137,7 +138,7 @@ class FederatedSchemaGeneratorTest {
137138
assertNotNull(productType)
138139
assertNotNull(productType.getDirective(KEY_DIRECTIVE_NAME))
139140

140-
val entityUnion = schema.getType("_Entity") as? GraphQLUnionType
141+
val entityUnion = schema.getType(ENTITY_UNION_NAME) as? GraphQLUnionType
141142
assertNotNull(entityUnion)
142143
assertTrue(entityUnion.types.contains(productType))
143144
}

graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/execution/FederatedQueryResolverTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ package com.expediagroup.graphql.federation.execution
1919
import com.expediagroup.graphql.federation.data.BookResolver
2020
import com.expediagroup.graphql.federation.data.UserResolver
2121
import com.expediagroup.graphql.federation.data.federatedTestSchema
22+
import com.expediagroup.graphql.federation.types.ENTITIES_FIELD_NAME
2223
import graphql.ExecutionInput
2324
import graphql.GraphQL
24-
import org.junit.jupiter.api.Test
2525
import kotlin.test.assertEquals
2626
import kotlin.test.assertFalse
2727
import kotlin.test.assertNotNull
2828
import kotlin.test.assertNull
29+
import org.junit.jupiter.api.Test
2930

3031
const val FEDERATED_QUERY =
3132
"""
@@ -65,7 +66,7 @@ class FederatedQueryResolverTest {
6566
val result = graphQL.executeAsync(executionInput).get().toSpecification()
6667

6768
assertNotNull(result["data"] as? Map<*, *>) { data ->
68-
assertNotNull(data["_entities"] as? List<*>) { entities ->
69+
assertNotNull(data[ENTITIES_FIELD_NAME] as? List<*>) { entities ->
6970
assertFalse(entities.isEmpty())
7071
assertEquals(representations.size, entities.size)
7172
assertNotNull(entities[0] as? Map<*, *>) { user ->

graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/federation/types/EntityTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ package com.expediagroup.graphql.federation.types
1818

1919
import com.expediagroup.graphql.extensions.unwrapType
2020
import graphql.schema.GraphQLUnionType
21-
import org.junit.jupiter.api.Test
2221
import kotlin.test.assertEquals
2322
import kotlin.test.assertFailsWith
2423
import kotlin.test.assertFalse
2524
import kotlin.test.assertNotNull
25+
import org.junit.jupiter.api.Test
2626

2727
class EntityTest {
2828

@@ -37,14 +37,14 @@ class EntityTest {
3737
fun `generateEntityFieldDefinition should return a valid type on a single set`() {
3838
val result = generateEntityFieldDefinition(setOf("MyType"))
3939
assertNotNull(result)
40-
assertEquals(expected = "_entities", actual = result.name)
40+
assertEquals(expected = ENTITIES_FIELD_NAME, actual = result.name)
4141
assertFalse(result.description.isNullOrEmpty())
4242
assertEquals(expected = 1, actual = result.arguments.size)
4343

4444
val graphQLUnionType = result.type.unwrapType() as? GraphQLUnionType
4545

4646
assertNotNull(graphQLUnionType)
47-
assertEquals(expected = "_Entity", actual = graphQLUnionType.name)
47+
assertEquals(expected = ENTITY_UNION_NAME, actual = graphQLUnionType.name)
4848
assertEquals(expected = 1, actual = graphQLUnionType.types.size)
4949
assertEquals(expected = "MyType", actual = graphQLUnionType.types.first().name)
5050
}

0 commit comments

Comments
 (0)