Skip to content

Commit 41fca40

Browse files
authored
Implementing federation spec (ExpediaGroup#280)
* Create new graphql-kotlin-federation module that extends graphql-kotlin-schema-generator to generate federated GraphQL schemas. Federated GraphQL architecture is an alternative to schema stitching approach that relies on declarative programming composition of schemas. graphql-kotlin-federation adds support for the following: new @key, @extends, @external, @requires and @provides directives new _FieldSet and _Any scalar types new _Entity union type of all federated types new _service query that returns _Service object with sdl field new _entity query that returns list of _Entity objects In order to generate federate schemas we have to annotate the underlying type with proper new directives and then use new toFederatedSchema function to build the schema (instead of toSchema function from graphql-kotlin-schema-generator). FederatedSchemaGeneratorHooks also require to specify FederatedTypeRegistry that holds a mapping between federated type names and their corresponding resolver. Those new FederatedTypeResolvers are used to instantiate the federated types based on the federated _entity query generated from the gateway. See: https://www.apollographql.com/docs/apollo-server/federation/introduction/ Resolves: ExpediaGroup#238
1 parent dbffd67 commit 41fca40

39 files changed

+2197
-41
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ GraphQL Kotlin consists of number of libraries that aim to simplify GraphQL inte
1010

1111
## Modules
1212

13+
* [graphql-kotlin-federation](graphql-kotlin-federation/README.md) - schema generator extension to build federated GraphQL schemas
1314
* [graphql-kotlin-schema-generator](graphql-kotlin-schema-generator/README.md) - code only GraphQL schema generation for Kotlin
1415
* [graphql-kotlin-spring-example](graphql-kotlin-spring-example/README.md) - example SpringBoot app that uses GraphQL Kotlin schema generator
1516

graphql-kotlin-federation/README.md

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# GraphQL Kotlin Federated Schema Generator
2+
3+
`graphql-kotlin-federation` extends the functionality of `graphql-kotlin-schema-generator` and allows you to easily generate federated GraphQL schemas directly from the code. Federated schemas rely on a number of directives to instrument the behavior of the underlying graph, see corresponding wiki pages to learn more about new directives. Once all the federated objects are annotated, you will also have to configure corresponding [FederatedTypeResolver]s that are used to instantiate federated objects and finally generate the schema using `toFederatedSchema` function ([link]).
4+
5+
See more
6+
* [Federation Spec](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/)
7+
8+
## Installation
9+
10+
Using a JVM dependency manager, simply link `graphql-kotlin-federation` to your project.
11+
12+
With Maven:
13+
14+
```xml
15+
<dependency>
16+
<groupId>com.expedia</groupId>
17+
<artifactId>graphql-kotlin-federation</artifactId>
18+
<version>${latestVersion}</version>
19+
</dependency>
20+
```
21+
22+
With Gradle:
23+
24+
```groovy
25+
compile(group: 'com.expedia', name: 'graphql-kotlin-federation', version: "$latestVersion")
26+
```
27+
28+
## Usage
29+
30+
In order to generate valid federated schemas, you will need to annotate both your base schemas and the one extending them. Federated Gateway (e.g. Apollo) will then combine the individual graphs to form single federated graph.
31+
32+
#### Base Schema
33+
34+
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.
35+
36+
```kotlin
37+
@KeyDirective(fields = FieldSet("id"))
38+
data class Product(val id: Int, val description: String)
39+
40+
class ProductQuery {
41+
fun product(id: Int): Product? {
42+
// grabs product from a data source, might return null
43+
}
44+
}
45+
46+
// Generate the schema
47+
val federatedTypeRegistry = FederatedTypeRegistry(emptyMap())
48+
val config = FederatedSchemaGeneratorConfig(supportedPackages = listOf("org.example"), hooks = FederatedSchemaGeneratorHooks(federatedTypeRegistry))
49+
val queries = listOf(TopLevelObject(ProductQuery()))
50+
51+
toFederatedSchema(config, queries)
52+
```
53+
54+
Generates the following schema with additional federated types
55+
56+
```graphql
57+
schema {
58+
query: Query
59+
}
60+
61+
union _Entity = Product
62+
63+
type Product @key(fields : "id") {
64+
description: Int!
65+
id: String!
66+
}
67+
68+
type Query {
69+
_entities(representations: [_Any!]!): [_Entity]!
70+
_service: _Service
71+
product(id: Int!): Product!
72+
}
73+
74+
type _Service {
75+
sdl: String!
76+
}
77+
```
78+
79+
#### Extended Schema
80+
81+
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`.
82+
83+
```kotlin
84+
@KeyDirective(fields = FieldSet("id"))
85+
@ExtendsDirective
86+
data class Product(@property:ExternalDirective val id: Int) {
87+
88+
fun reviews(): List<Review> {
89+
// returns list of product reviews
90+
}
91+
}
92+
93+
data class Review(val reviewId: String, val text: String)
94+
95+
// Generate the schema
96+
val productResolver = object: FederatedTypeResolver<Product> {
97+
override fun resolve(keys: Map<String, Any>): Product {
98+
val id = keys["id"]?.toString()?.toIntOrNull()
99+
// instantiate product using id
100+
}
101+
}
102+
val federatedTypeRegistry = FederatedTypeRegistry(mapOf("Product" to productResolver))
103+
val config = FederatedSchemaGeneratorConfig(supportedPackages = listOf("org.example"), hooks = FederatedSchemaGeneratorHooks(federatedTypeRegistry))
104+
105+
toFederatedSchema(config)
106+
```
107+
108+
Generates the following federated schema
109+
110+
```graphql
111+
schema {
112+
query: Query
113+
}
114+
115+
union _Entity = Product
116+
117+
type Product @extends @key(fields : "id") {
118+
id: Int! @external
119+
reviews: [Review!]!
120+
}
121+
122+
type Query {
123+
_entities(representations: [_Any!]!): [_Entity]!
124+
_service: _Service
125+
}
126+
127+
type Review {
128+
reviewId: String!
129+
text: String!
130+
}
131+
132+
type _Service {
133+
sdl: String!
134+
}
135+
```
136+
137+
Federated Gateway will then combine the schemas from the individual services to generate single schema.
138+
139+
#### Federated GraphQL schema
140+
141+
```graphql
142+
schema {
143+
query: Query
144+
}
145+
146+
type Product {
147+
description: String!
148+
id: String!
149+
reviews: [Review!]!
150+
}
151+
152+
type Review {
153+
reviewId: String!
154+
text: String!
155+
}
156+
157+
type Query {
158+
product(id: String!): Product!
159+
}
160+
```
161+
162+
## Documentation
163+
164+
There are more examples and documentation in our [Uncyclo](https://github.com/ExpediaDotCom/graphql-kotlin/wiki) or you can view the [javadocs](https://www.javadoc.io/doc/com.expedia/graphql-kotlin) for all published versions.
165+
166+
If you have a question about something you can not find in our wiki or javadocs, feel free to [create an issue](https://github.com/ExpediaDotCom/graphql-kotlin/issues) and tag it with the question label.

graphql-kotlin-federation/pom.xml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>com.expedia</groupId>
8+
<artifactId>graphql-kotlin</artifactId>
9+
<version>1.0.0-RC4-SNAPSHOT</version>
10+
<relativePath>..</relativePath>
11+
</parent>
12+
13+
<artifactId>graphql-kotlin-federation</artifactId>
14+
<name>graphql-kotlin-federation</name>
15+
<packaging>jar</packaging>
16+
17+
<build>
18+
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
19+
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
20+
<plugins>
21+
<plugin>
22+
<groupId>org.apache.maven.plugins</groupId>
23+
<artifactId>maven-antrun-plugin</artifactId>
24+
</plugin>
25+
</plugins>
26+
</build>
27+
28+
<dependencies>
29+
<dependency>
30+
<groupId>com.expedia</groupId>
31+
<artifactId>graphql-kotlin-schema-generator</artifactId>
32+
<version>${project.parent.version}</version>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.junit.jupiter</groupId>
36+
<artifactId>junit-jupiter-params</artifactId>
37+
<version>${junit-jupiter.version}</version>
38+
</dependency>
39+
</dependencies>
40+
</project>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.expedia.graphql.federation
2+
3+
import com.expedia.graphql.TopLevelObject
4+
import com.expedia.graphql.federation.directives.ExtendsDirective
5+
import com.expedia.graphql.generator.SchemaGenerator
6+
import graphql.schema.GraphQLSchema
7+
import org.reflections.Reflections
8+
import kotlin.reflect.full.createType
9+
10+
/**
11+
* Generates federated GraphQL schemas based on the specified configuration.
12+
*/
13+
class FederatedSchemaGenerator(generatorConfig: FederatedSchemaGeneratorConfig) : SchemaGenerator(generatorConfig) {
14+
15+
override fun generate(
16+
queries: List<TopLevelObject>,
17+
mutations: List<TopLevelObject>,
18+
subscriptions: List<TopLevelObject>,
19+
builder: GraphQLSchema.Builder
20+
): GraphQLSchema {
21+
builder.federation(config.supportedPackages)
22+
return super.generate(queries, mutations, subscriptions, builder)
23+
}
24+
25+
/**
26+
* Scans specified packages for all the federated (extended) types and adds them to the target schema.
27+
*/
28+
fun GraphQLSchema.Builder.federation(supportedPackages: List<String>): GraphQLSchema.Builder {
29+
supportedPackages
30+
.map { pkg -> Reflections(pkg).getTypesAnnotatedWith(ExtendsDirective::class.java).map { it.kotlin } }
31+
.flatten()
32+
.map {
33+
val graphQLType = if (it.isAbstract) {
34+
interfaceType(it)
35+
} else {
36+
objectType(it)
37+
}
38+
39+
// workaround to explicitly apply validation
40+
config.hooks.didGenerateGraphQLType(it.createType(), graphQLType)
41+
}
42+
.forEach {
43+
this.additionalType(it)
44+
}
45+
return this
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.expedia.graphql.federation
2+
3+
import com.expedia.graphql.SchemaGeneratorConfig
4+
import com.expedia.graphql.TopLevelNames
5+
import com.expedia.graphql.execution.KotlinDataFetcherFactoryProvider
6+
7+
/**
8+
* Settings for generating the federated schema.
9+
*/
10+
class FederatedSchemaGeneratorConfig(
11+
override val supportedPackages: List<String>,
12+
override val topLevelNames: TopLevelNames = TopLevelNames(),
13+
override val hooks: FederatedSchemaGeneratorHooks,
14+
override val dataFetcherFactoryProvider: KotlinDataFetcherFactoryProvider = KotlinDataFetcherFactoryProvider(hooks)
15+
) : SchemaGeneratorConfig(supportedPackages, topLevelNames, hooks, dataFetcherFactoryProvider)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.expedia.graphql.federation
2+
3+
import com.expedia.graphql.annotations.GraphQLName
4+
import com.expedia.graphql.extensions.print
5+
import com.expedia.graphql.federation.directives.FieldSet
6+
import com.expedia.graphql.federation.types.ANY_SCALAR_TYPE
7+
import com.expedia.graphql.federation.types.FIELD_SET_SCALAR_TYPE
8+
import com.expedia.graphql.federation.types.SERVICE_FIELD_DEFINITION
9+
import com.expedia.graphql.federation.types._Service
10+
import com.expedia.graphql.federation.types.generateEntityFieldDefinition
11+
import com.expedia.graphql.hooks.SchemaGeneratorHooks
12+
import graphql.TypeResolutionEnvironment
13+
import graphql.schema.DataFetcher
14+
import graphql.schema.FieldCoordinates
15+
import graphql.schema.GraphQLCodeRegistry
16+
import graphql.schema.GraphQLObjectType
17+
import graphql.schema.GraphQLSchema
18+
import graphql.schema.GraphQLType
19+
import kotlin.reflect.KType
20+
import kotlin.reflect.full.findAnnotation
21+
22+
/**
23+
* Hooks for generating federated GraphQL schema.
24+
*/
25+
open class FederatedSchemaGeneratorHooks(private val federatedTypeRegistry: FederatedTypeRegistry) : SchemaGeneratorHooks {
26+
27+
private val validator = FederatedSchemaValidator()
28+
29+
override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier) {
30+
FieldSet::class -> FIELD_SET_SCALAR_TYPE
31+
else -> null
32+
}
33+
34+
override fun didGenerateGraphQLType(type: KType, generatedType: GraphQLType): GraphQLType {
35+
validator.validateGraphQLType(generatedType)
36+
return super.didGenerateGraphQLType(type, generatedType)
37+
}
38+
39+
override fun willBuildSchema(builder: GraphQLSchema.Builder): GraphQLSchema.Builder {
40+
val originalSchema = builder.build()
41+
val originalQuery = originalSchema.queryType
42+
43+
val federatedSchema = GraphQLSchema.newSchema(originalSchema)
44+
val federatedQuery = GraphQLObjectType.newObject(originalQuery)
45+
.field(SERVICE_FIELD_DEFINITION)
46+
val federatedCodeRegistry = GraphQLCodeRegistry.newCodeRegistry(originalSchema.codeRegistry)
47+
48+
val entityTypeNames = originalSchema.allTypesAsList
49+
.asSequence()
50+
.filterIsInstance<GraphQLObjectType>()
51+
.filter { type -> type.getDirective("key") != null }
52+
.map { it.name }
53+
.toSet()
54+
55+
// register new federated queries
56+
if (entityTypeNames.isNotEmpty()) {
57+
val entityField = generateEntityFieldDefinition(entityTypeNames)
58+
federatedQuery.field(entityField)
59+
60+
val sdl = originalSchema.print()
61+
federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, SERVICE_FIELD_DEFINITION.name), DataFetcher { _Service(sdl) })
62+
federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, entityField.name), DataFetcher {
63+
val representations: List<Map<String, Any>> = it.getArgument("representations")
64+
representations.map { representation ->
65+
val type = representation["__typename"]?.toString() ?: throw FederationException("invalid _entity query - missing __typename in the representation, representation=$representation")
66+
val resolver = federatedTypeRegistry.getFederatedResolver(type) ?: throw FederationException("Federation exception - cannot find resolver for $type")
67+
resolver.resolve(representation)
68+
}.toList()
69+
})
70+
federatedCodeRegistry.typeResolver("_Entity") { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObjectName()) }
71+
federatedSchema.additionalType(ANY_SCALAR_TYPE)
72+
}
73+
74+
return federatedSchema.query(federatedQuery.build())
75+
.codeRegistry(federatedCodeRegistry.build())
76+
}
77+
}
78+
79+
private fun TypeResolutionEnvironment.getObjectName(): String? {
80+
val kClass = this.getObject<Any>().javaClass.kotlin
81+
return kClass.findAnnotation<GraphQLName>()?.value
82+
?: kClass.simpleName
83+
}

0 commit comments

Comments
 (0)