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
subscriptions exposed by the schema.
All 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
type resolution wiki for more details on how federated types are resolved. Final federated schema
is then generated by invoking the toFederatedSchema
function
(link).
In order to generate valid federated schemas, you will need to annotate both your base schema and the one extending it. Federated Gateway (e.g. Apollo) will then combine the individual graphs to form single federated graph.
If you are using custom Query
type then all of you federated GraphQL services have to use the same type. It is
not possible for federated services to have different definitions of Query
type.
Base Schema
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
any extended functionality our FederatedTypeRegistry does not include any federated resolvers.
@KeyDirective(fields = FieldSet("id"))
data class Product(val id: Int, val description: String)
class ProductQuery {
fun product(id: Int): Product? {
// grabs product from a data source, might return null
}
}
// Generate the schema
val hooks = FederatedSchemaGeneratorHooks(emptyList())
val config = FederatedSchemaGeneratorConfig(supportedPackages = listOf("org.example"), hooks = hooks)
val queries = listOf(TopLevelObject(ProductQuery()))
toFederatedSchema(config, queries)
Example above generates the following schema with additional federated types:
schema {
query: Query
}
union _Entity = Product
type Product @key(fields : "id") {
description: String!
id: Int!
}
type Query @extends {
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service
product(id: Int!): Product
}
type _Service {
sdl: String!
}
Extended Schema
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
. 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.
@KeyDirective(fields = FieldSet("id"))
@ExtendsDirective
data class Product(@ExternalDirective val id: Int) {
// Add the "reviews" field to the type
suspend fun reviews(): List<Review> = getReviewByProductId(id)
}
data class Review(val reviewId: String, val text: String)
// 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) }
}
}
// Generate the schema
val resolvers = listOf(ProductResolver())
val hooks = FederatedSchemaGeneratorHooks(resolvers)
val config = FederatedSchemaGeneratorConfig(supportedPackages = listOf("org.example"), hooks = hooks)
toFederatedSchema(config)
Our extended schema will then be generated as:
schema {
query: Query
}
union _Entity = Product
type Product @extends @key(fields : "id") {
id: Int! @external
reviews: [Review!]!
}
type Query @extends {
_entities(representations: [_Any!]!): [_Entity]!
_service: _Service
}
type Review {
reviewId: String!
text: String!
}
type _Service {
sdl: String!
}
Federated GraphQL schema
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:
schema {
query: Query
}
type Product {
description: String!
id: Int!
reviews: [Review!]!
}
type Review {
reviewId: String!
text: String!
}
type Query {
product(id: String!): Product
}
See our federation example for additional details.