sangria-federated is a library that allows sangria users to implement services that adhere to Apollo's Federation Specification, and can be used as part of a federated data graph.
SBT Configuration:
libraryDependencies += "org.sangria-graphql" %% "sangria-federated" % "<latest version>"
The library adds Apollo's Federation Specification on top of the provided sangria graphql schema.
To make it possible to use _Any
as a scalar, the library upgrades the used marshaller.
To be able to communicate with Apollo's federation gateway, the graphql sangria service should be using both the federated schema and unmarshaller.
As an example, let's consider an application using circe with a state and review service.
-
The state service defines the state entity, annotated with
@key("id")
. And for each entity, we need to define an entity resolver (reference resolver), see code below:import sangria.federation.Decoder import io.circe.Json, io.circe.generic.semiauto._ import sangria.schema._ case class State( id: Int, key: String, value: String) object State { case class StateArg(id: Int) implicit val decoder: Decoder[Json, StateArg] = deriveDecoder[StateArg].decodeJson(_) val stateResolver = EntityResolver[StateService, Json, State, StateArg]( __typeName = "State", ev = State.decoder, arg => env.getState(arg.id)) val stateSchema = ObjectType( "State", fields[Unit, State]( Field( "id", IntType, resolve = _.value.id), Field( "key", StringType, resolve = _.value.key), Field( "value", StringType, resolve = _.value.value)) ).copy(astDirectives = Vector(federation.Directives.Key("id"))) }
The entity resolver implements:
- the deserialization of the fields in
_Any
object to the EntityArg. - how to fetch the EntityArg to get the proper Entity (in our case State).
As for the query type, let's suppose the schema below:
import sangria.schema._ object StateAPI { val Query = ObjectType( "Query", fields[StateService, Unit]( Field( name = "states", fieldType = ListType(State.stateSchema), resolve = _.ctx.getStates))) }
Now in the definition of the GraphQL server, we federate the Query type and the unmarshaller while supplying the entity resolvers. Then, we use both the federated schema and unmarshaller as arguments for the server.
def graphQL[F[_]: Effect]: GraphQL[F] = { val (schema, um) = federation.Federation.federate[StateService, Json]( Schema(StateAPI.Query), sangria.marshalling.circe.CirceInputUnmarshaller, stateResolver) GraphQL( schema, env.pure[F])(implicitly[Effect[F]], um) }
And, the GraphQL server should use the provided schema and unmarshaller as arguments for the sangria executor:
import cats.effect._ import cats.implicits._ import io.circe._ import sangria.ast.Document import sangria.execution._ import sangria.marshalling.InputUnmarshaller import sangria.marshalling.circe.CirceResultMarshaller import sangria.schema.Schema object GraphQL { def apply[F[_], A]( schema: Schema[A, Unit], userContext: F[A] )(implicit F: Async[F], um: InputUnmarshaller[Json]): GraphQL[F] = new GraphQL[F] { import scala.concurrent.ExecutionContext.Implicits.global def exec( schema: Schema[A, Unit], userContext: F[A], query: Document, operationName: Option[String], variables: Json): F[Either[Json, Json]] = userContext.flatMap { ctx => F.async { (cb: Either[Throwable, Json] => Unit) => Executor.execute( schema = schema, queryAst = query, userContext = ctx, variables = variables, operationName = operationName, exceptionHandler = ExceptionHandler { case (_, e) ⇒ HandledException(e.getMessage) } ).onComplete { case Success(value) => cb(Right(value)) case Failure(error) => cb(Left(error)) } } }.attempt.flatMap { case Right(json) => F.pure(json.asRight) case Left(err: WithViolations) => ??? case Left(err) => ??? } } }
- the deserialization of the fields in
-
The review service defines the review type, which has a reference to the state type. And, for each entity referenced by another service, a stub type should be created (containing just the minimal information that will allow to reference the entity).
import sangria.schema._ case class Review( id: Int, key: Option[String] = None, state: State) object Review { val reviewSchema = ObjectType( "Review", fields[Unit, Review]( Field( "id", IntType, resolve = _.value.id), Field( "key", OptionType(StringType), resolve = _.value.key), Field( "state", State.stateSchema, resolve = _.value.state))) } case class State(id: Int) object State { import sangria.federation.Directives._ val stateSchema = ObjectType( "State", fields[Unit, State]( Field[Unit, State, Int, Int]( name = "id", fieldType = IntType, resolve = _.value.id).copy(astDirectives = Vector(External))) ).copy(astDirectives = Vector(Key("id"), Extends)) }
In the end, the same code used to federate the state service is used to federate the review service.
-
The sangria GraphQL services endpoints can now be configured in the
serviceList
of Apollo's Gatewqay as follows:const gateway = new ApolloGateway({ serviceList: [ { name: 'states', url: 'http://localhost:9081/api/graphql'}, { name: 'reviews', url: 'http://localhost:9082/api/graphql'} ], debug: true })
All the code of the example is available here.
- This is a technology preview and should not be used in a production environment.
- The library upgrades the marshaller too, by making map values scalars (e.g. json objects as scalars). This can lead to security issues as discussed here.
Contributions are warmly desired 🤗. Please follow the standard process of forking the repo and making PRs 🤓
sangria-federated is licensed under Apache License, Version 2.0.