sangria-graphql / sangria-federated

Running a sangria server as federated

Version Matrix

Sangria Logo

sangria-federated

Continuous Integration Maven Central License Scaladocs Join the chat at https://gitter.im/sangria-graphql/sangria

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>"

How does it work?

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.

How to use it?

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 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.

Caution 🚨 🚨

  • 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.

Contribute

Contributions are warmly desired 🤗 . Please follow the standard process of forking the repo and making PRs 🤓

License

sangria-federated is licensed under Apache License, Version 2.0.