Skip to content

Commit

Permalink
Remove federated type registry (ExpediaGroup#861)
Browse files Browse the repository at this point in the history
* Remove FederatedTypeRegistry

* Fix test file

* Update docs on type registry

Co-authored-by: Shane Myrick <accounts@shanemyrick.com>
  • Loading branch information
2 people authored and huehnerlady committed Oct 16, 2020
1 parent 8301c62 commit 57c5aba
Show file tree
Hide file tree
Showing 17 changed files with 93 additions and 120 deletions.
51 changes: 26 additions & 25 deletions docs/federated/federated-schemas.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
---
id: federated-schemas
id: federated-schemas
title: Federated Schemas
---

`graphql-kotlin-federation` library extends the functionality of the `graphql-kotlin-schema-generator` and allows you to
easily generate federated GraphQL schemas directly from the code. Federated schema is generated by calling
`toFederatedSchema` function that accepts federated configuration as well as a list of regular queries, mutations and
`graphql-kotlin-federation` library extends the functionality of the `graphql-kotlin-schema-generator` and allows you to
easily generate federated GraphQL schemas directly from the code. Federated schema is generated by calling
`toFederatedSchema` function that accepts federated configuration as well as a list of regular queries, mutations and
subscriptions exposed by the schema.

All [federated directives](federated-directives) are provided as annotations that are used to decorate your classes,
properties and functions. Since federated types might not be accessible through the regular query execution path, they
All [federated directives](federated-directives) are provided as annotations that are used to decorate your classes,
properties and functions. Since federated types might not be accessible through the regular query execution path, they
are explicitly picked up by the schema generator based on their directives. Due to the above, we also need to provide
a way to instantiate the underlying federated objects by implementing corresponding `FederatedTypeResolvers`. See
a way to instantiate the underlying federated objects by implementing corresponding `FederatedTypeResolvers`. See
[type resolution wiki](type-resolution) for more details on how federated types are resolved. Final federated schema
is then generated by invoking the `toFederatedSchema` function
([link](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/toFederatedSchema.kt#L34)).
Expand All @@ -26,7 +26,7 @@ it**. Federated Gateway (e.g. Apollo) will then combine the individual graphs to

Base schema defines GraphQL types that will be extended by schemas exposed by other GraphQL services. In the example
below, we define base `Product` type with `id` and `description` fields. `id` is the primary key that uniquely
identifies the `Product` type object and is specified in `@key` directive. Since it is a base schema that doesn't expose
identifies the `Product` type object and is specified in `@key` directive. Since it is a base schema that doesn't expose
any extended functionality our FederatedTypeRegistry does not include any federated resolvers.

```kotlin
Expand All @@ -40,8 +40,8 @@ class ProductQuery {
}

// Generate the schema
val federatedTypeRegistry = FederatedTypeRegistry(emptyMap())
val config = FederatedSchemaGeneratorConfig(supportedPackages = listOf("org.example"), hooks = FederatedSchemaGeneratorHooks(federatedTypeRegistry))
val hooks = FederatedSchemaGeneratorHooks(emptyList())
val config = FederatedSchemaGeneratorConfig(supportedPackages = listOf("org.example"), hooks = hooks)
val queries = listOf(TopLevelObject(ProductQuery()))

toFederatedSchema(config, queries)
Expand Down Expand Up @@ -78,32 +78,33 @@ Extended federated GraphQL schemas provide additional functionality to the types
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`. Finally, we also need to specify an "entry point"
for the federated type - we need to create a FederatedTypeResolver that will be used to instantiate the federated
set specified in `@key` directive have to be marked as `@external`. Finally, we also need to specify an "entry point"
for the federated type - we need to create a FederatedTypeResolver that will be used to instantiate the federated
`Product` type when processing federated queries.

```kotlin
@KeyDirective(fields = FieldSet("id"))
@ExtendsDirective
data class Product(@ExternalDirective val id: Int) {

fun reviews(): List<Review> {
// returns list of product reviews
}
// Add the "reviews" field to the type
suspend fun reviews(): List<Review> = getReviewByProductId(id)
}

data class Review(val reviewId: String, val text: String)

// Generate the schema
val productResolver = object: FederatedTypeResolver<Product> {
override suspend fun resolve(representations: List<Map<String, Any>>): List<Product?> = representations.map { keys ->
keys["id"]?.toString()?.toIntOrNull()?.let { id ->
Product(id)
}
// Resolve a "Product" type from the _entities query
class ProductResolver : FederatedTypeResolver<Product> {
override val typeName = "Product"

override suspend fun resolve(environment: DataFetchingEnvironment, representations: List<Map<String, Any>>): List<Product?> = representations.map { keys ->
keys["id"]?.toString()?.toIntOrNull()?.let { id -> Product(id) }
}
}
val federatedTypeRegistry = FederatedTypeRegistry(mapOf("Product" to productResolver))
val config = FederatedSchemaGeneratorConfig(supportedPackages = listOf("org.example"), hooks = FederatedSchemaGeneratorHooks(federatedTypeRegistry))

// Generate the schema
val resolvers = listOf(ProductResolver())
val hooks = FederatedSchemaGeneratorHooks(resolvers)
val config = FederatedSchemaGeneratorConfig(supportedPackages = listOf("org.example"), hooks = hooks)

toFederatedSchema(config)
```
Expand Down Expand Up @@ -139,7 +140,7 @@ type _Service {

#### Federated GraphQL schema

Once we have both base and extended GraphQL services up and running, we will also need to configure Federated Gateway
Once we have both base and extended GraphQL services up and running, we will also need to configure Federated Gateway
to combine them into a single schema. Using the examples above, our final federated schema will be generated as:

```graphql
Expand Down
13 changes: 6 additions & 7 deletions docs/federated/type-resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ query ($_representations: [_Any!]!) {
### Federated Type Resolver

In order to simplify the integrations, `graphql-kotlin-federation` provides a default `_entities` query resolver that
relies on
[FederatedTypeRegistry](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/execution/FederatedTypeRegistry.kt)
to retrieve the
retrieves the
[FederatedTypeResolver](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/execution/FederatedTypeResolver.kt)
that is used to resolve the specified `__typename`.

Expand Down Expand Up @@ -69,9 +67,10 @@ class ProductResolver : FederatedTypeResolver<Product> {
}
}

// The FederatedTypeRegistry provides mapping between the "__typename" and the corresponding type resolver
// If you are using "graphql-kotlin-spring-server" this is created for you if your FederatedTypeResolvers are marked as Spring beans
val federatedTypeRegistry = FederatedTypeRegistry(listOf(productResolver))
val config = FederatedSchemaGeneratorConfig(supportedPackages = listOf("org.example"), hooks = FederatedSchemaGeneratorHooks(federatedTypeRegistry))
// If you are using "graphql-kotlin-spring-server", your FederatedTypeResolvers can be marked as Spring beans
// and will automatically be added to the hooks
val resolvers = listOf(productResolver)
val hooks = FederatedSchemaGeneratorHooks(resolvers)
val config = FederatedSchemaGeneratorConfig(supportedPackages = listOf("org.example"), hooks = hooks)
val schema = toFederatedSchema(config)
```
13 changes: 10 additions & 3 deletions docs/spring-server/spring-beans.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,25 @@ can be customized by providing custom beans in your application context. See sec


## Non-Federated Schema
_Created only if federation is disabled_

| Bean | Description |
|:---------------------------------|:------------|
| SchemaGeneratorConfig | Schema generator configuration information, see [Schema Generator Configuration](../schema-generator/customizing-schemas/generator-config.md) for details. Can be customized by providing `TopLevelNames`, [SchemaGeneratorHooks](../schema-generator/customizing-schemas/generator-config.md) and `KotlinDataFetcherFactoryProvider` beans. |
| GraphQLSchema | GraphQL schema generated based on the schema generator configuration and `Query`, `Mutation` and `Subscription` objects available in the application context. |


## Federated Schema
_Created only if federation is enabled_

| Bean | Description |
|:---------------------------------|:------------|
| FederatedTypeRegistry | Default type registry that adds any resolvers also marked as Spring beans. See [Federated Type Resolution](../federated/type-resolution.md) for more details.<br><br>_Created only if federation is enabled. You should register your custom type registry bean whenever implementing federated GraphQL schema with extended types_. |
| FederatedSchemaGeneratorHooks | Schema generator hooks used to build federated schema.<br><br>_Created only if federation is enabled_. |
| FederatedSchemaGeneratorConfig | Federated schema generator configuration information. You can customize the configuration by providing `TopLevelNames`, `FederatedSchemaGeneratorHooks` and `KotlinDataFetcherFactoryProvider` beans.<br><br>_Created instead of default `SchemaGeneratorConfig` if federation is enabled_. |
| FederatedTypeResolvers | List of `FederatedTypeResolvers` marked as beans that should be added to hooks. See [Federated Type Resolution](../federated/type-resolution.md) for more details |
| FederatedSchemaGeneratorHooks | Schema generator hooks used to build federated schema |
| FederatedSchemaGeneratorConfig | Federated schema generator configuration information. You can customize the configuration by providing `TopLevelNames`, `FederatedSchemaGeneratorHooks` and `KotlinDataFetcherFactoryProvider` beans |
| GraphQLSchema | GraphQL schema generated based on the federated schema generator configuration and `Query`, `Mutation` and `Subscription` objects available in the application context. |


## GraphQL Configuration
| Bean | Description |
|:---------------------------------|:------------|
Expand All @@ -39,6 +44,8 @@ can be customized by providing custom beans in your application context. See sec


## Subscriptions
_Created only if the `Subscription` marker interface is used _

| Bean | Description |
|:---------------------------------|:------------|
| SubscriptionHandler | Web socket handler for executing GraphQL subscriptions, defaults to [SimpleSubscriptionHandler](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionHandler.kt#L49).<br><br>_Created only if `Subscription` bean is available in the context._ |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,15 @@
package com.expediagroup.graphql.examples

import com.expediagroup.graphql.examples.hooks.CustomFederationSchemaGeneratorHooks
import com.expediagroup.graphql.federation.execution.FederatedTypeRegistry
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean

@SpringBootApplication
class Application {

@Bean
fun hooks(federatedTypeRegistry: FederatedTypeRegistry) =
CustomFederationSchemaGeneratorHooks(federatedTypeRegistry)
fun hooks() = CustomFederationSchemaGeneratorHooks(emptyList())
}

@Suppress("SpreadOperator")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package com.expediagroup.graphql.examples.hooks

import com.expediagroup.graphql.federation.FederatedSchemaGeneratorHooks
import com.expediagroup.graphql.federation.execution.FederatedTypeRegistry
import com.expediagroup.graphql.federation.execution.FederatedTypeResolver
import graphql.language.StringValue
import graphql.schema.Coercing
import graphql.schema.GraphQLScalarType
Expand All @@ -28,7 +28,7 @@ import kotlin.reflect.KType
/**
* Schema generator hook that adds additional scalar types.
*/
class CustomFederationSchemaGeneratorHooks(federatedTypeRegistry: FederatedTypeRegistry) : FederatedSchemaGeneratorHooks(federatedTypeRegistry) {
class CustomFederationSchemaGeneratorHooks(resolvers: List<FederatedTypeResolver<*>>) : FederatedSchemaGeneratorHooks(resolvers) {

/**
* Register additional GraphQL scalar types.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import com.expediagroup.graphql.federation.directives.KEY_DIRECTIVE_TYPE
import com.expediagroup.graphql.federation.directives.PROVIDES_DIRECTIVE_TYPE
import com.expediagroup.graphql.federation.directives.REQUIRES_DIRECTIVE_TYPE
import com.expediagroup.graphql.federation.execution.EntityResolver
import com.expediagroup.graphql.federation.execution.FederatedTypeRegistry
import com.expediagroup.graphql.federation.execution.FederatedTypeResolver
import com.expediagroup.graphql.federation.extensions.addDirectivesIfNotPresent
import com.expediagroup.graphql.federation.types.ANY_SCALAR_TYPE
import com.expediagroup.graphql.federation.types.FIELD_SET_SCALAR_TYPE
Expand All @@ -51,7 +51,7 @@ import kotlin.reflect.full.findAnnotation
/**
* Hooks for generating federated GraphQL schema.
*/
open class FederatedSchemaGeneratorHooks(private val federatedTypeRegistry: FederatedTypeRegistry) : SchemaGeneratorHooks {
open class FederatedSchemaGeneratorHooks(private val resolvers: List<FederatedTypeResolver<*>>) : SchemaGeneratorHooks {
private val scalarDefinitionRegex = "(^\".+\"$[\\r\\n])?^scalar (_FieldSet|_Any)$[\\r\\n]*".toRegex(setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
private val emptyQueryRegex = "^type Query(?!\\s*\\{)\\s+".toRegex(setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
private val validator = FederatedSchemaValidator()
Expand Down Expand Up @@ -114,7 +114,7 @@ open class FederatedSchemaGeneratorHooks(private val federatedTypeRegistry: Fede
val entityField = generateEntityFieldDefinition(entityTypeNames)
federatedQuery.field(entityField)

federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, entityField.name), EntityResolver(federatedTypeRegistry))
federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, entityField.name), EntityResolver(resolvers))
federatedCodeRegistry.typeResolver("_Entity") { 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 @@ -26,10 +26,18 @@ import kotlinx.coroutines.awaitAll
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(private val federatedTypeRegistry: FederatedTypeRegistry) : DataFetcher<CompletableFuture<DataFetcherResult<List<Any?>>>> {
open class EntityResolver(resolvers: List<FederatedTypeResolver<*>>) : DataFetcher<CompletableFuture<DataFetcherResult<List<Any?>>>> {

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

/**
* Resolves entities based on the passed in representations argument. Entities are resolved in the same order
Expand All @@ -42,15 +50,15 @@ open class EntityResolver(private val federatedTypeRegistry: FederatedTypeRegist
* @return list of resolved nullable entities
*/
override fun get(env: DataFetchingEnvironment): CompletableFuture<DataFetcherResult<List<Any?>>> {
val representations: List<Map<String, Any>> = env.getArgument("representations")
val representations: List<Map<String, Any>> = env.getArgument(REPRESENTATIONS)

val indexedBatchRequestsByType = representations.withIndex().groupBy { it.value["__typename"].toString() }
val indexedBatchRequestsByType = representations.withIndex().groupBy { it.value[TYPENAME_FIELD].toString() }
return GlobalScope.future {
val data = mutableListOf<Any?>()
val errors = mutableListOf<GraphQLError>()
indexedBatchRequestsByType.map { (typeName, indexedRequests) ->
async {
resolveType(env, typeName, indexedRequests, federatedTypeRegistry)
resolveType(env, typeName, indexedRequests, resolverMap)
}
}.awaitAll()
.flatten()
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ internal suspend fun resolveType(
environment: DataFetchingEnvironment,
typeName: String,
indexedRequests: List<IndexedValue<Map<String, Any>>>,
federatedTypeRegistry: FederatedTypeRegistry
resolverMap: Map<String, FederatedTypeResolver<*>>
): List<Pair<Int, Any?>> {
val indices = indexedRequests.map { it.index }
val batch = indexedRequests.map { it.value }
val results = resolveBatch(environment, typeName, batch, federatedTypeRegistry)
val results = resolveBatch(environment, typeName, batch, resolverMap)
return if (results.size != indices.size) {
indices.map {
it to FederatedRequestFailure("Federation batch request for $typeName generated different number of results than requested, representations=${indices.size}, results=${results.size}")
Expand All @@ -39,8 +39,13 @@ internal suspend fun resolveType(
}

@Suppress("TooGenericExceptionCaught")
private suspend fun resolveBatch(environment: DataFetchingEnvironment, typeName: String, batch: List<Map<String, Any>>, federatedTypeRegistry: FederatedTypeRegistry): List<Any?> {
val resolver = federatedTypeRegistry.getFederatedResolver(typeName)
private suspend fun resolveBatch(
environment: DataFetchingEnvironment,
typeName: String,
batch: List<Map<String, Any>>,
resolverMap: Map<String, FederatedTypeResolver<*>>
): List<Any?> {
val resolver = resolverMap[typeName]
return if (resolver != null) {
try {
resolver.resolve(environment, batch)
Expand Down

0 comments on commit 57c5aba

Please sign in to comment.