In-memory Model

The GraphQL reference implementation defines an API over a simple data model representing characters and films from the Star Wars series. Because of its appearance in the reference implementation it is used as the basis for many GraphQL tutorials, and many GraphQL server implementations provide it as an example. Grackle is no exception.

In this tutorial we are going to implement the Star Wars demo using Grackle backed by an in-memory model, i.e. a simple Scala data structure which captures the information required to service the GraphQL API.

Running the demo

The demo is packaged as submodule demo in the Grackle project. It is a http4s-based application which can be run from the SBT REPL using sbt-revolver,

sbt:root> demo/reStart
[info] Application demo not yet started
[info] Starting application demo in the background ...
demo Starting demo.Main.main()
demo INFO  - Ember-Server service bound to address: [::]:8080

This application hosts the demo services for in-memory and db-backend models, as well as a web-based GraphQL client (GraphQL Playground) which can be used to interact with them. You can run the client for in-memory model in your browser at http://localhost:8080/playground.html?endpoint=starwars.

Query examples

You can use the Playground to run queries against the model. Paste the following into the query field on left,

query {
  hero(episode: EMPIRE) {
    name
    appearsIn
  }
}

Click the play button in the centre and you should see the following response on the right,

{
  "data": {
    "hero": {
      "name": "Luke Skywalker",
      "appearsIn": [
        "NEWHOPE",
        "EMPIRE",
        "JEDI"
      ]
    }
  }
}

The Schema

The Star Wars API is described by a GraphQL schema,

type Query {
  hero(episode: Episode!): Character
  character(id: ID!): Character
  human(id: ID!): Human
  droid(id: ID!): Droid
}

enum Episode {
  NEWHOPE
  EMPIRE
  JEDI
}

interface Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
}

type Droid implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  primaryFunction: String
}

type Human implements Character {
  id: ID!
  name: String!
  friends: [Character]
  appearsIn: [Episode]!
  homePlanet: String
}

Any one of the parametrized fields in the Query type may be used as the top level query, with nested queries over fields of the result type. The structure of the query follows the schema, and the structure of the result follows the structure of the query. For example,

query {
  character(id: 1002) {
    name
    friends {
      name
    }
  }
}

yields the result,

{
  "data": {
    "character": {
      "name": "Han Solo",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Leia Organa"
        },
        {
          "name": "R2-D2"
        }
      ]
    }
  }
}

Grackle represents schemas as Scala values of type Schema which can be constructed given a schema text,

  val schema =
    schema"""
      type Query {
          hero(episode: Episode!): Character!
          character(id: ID!): Character
          human(id: ID!): Human
          droid(id: ID!): Droid
        }
        enum Episode {
          NEWHOPE
          EMPIRE
          JEDI
        }
        interface Character {
          id: String!
          name: String
          friends: [Character!]
          appearsIn: [Episode!]
        }
        type Human implements Character {
          id: String!
          name: String
          friends: [Character!]
          appearsIn: [Episode!]
          homePlanet: String
        }
        type Droid implements Character {
          id: String!
          name: String
          friends: [Character!]
          appearsIn: [Episode!]
          primaryFunction: String
        }
    """

The use of the schema string interpolator here causes the content of the string literal to be evaluated and checked as a valid GraphQL schema at compile time.

The Scala model

The API is backed by values of an ordinary Scala data types with no Grackle dependencies,

  object Episode extends Enumeration {
    val NEWHOPE, EMPIRE, JEDI = Value
  }

  sealed trait Character {
    def id: String
    def name: Option[String]
    def appearsIn: Option[List[Episode.Value]]
    def friends: Option[List[String]]
  }

  object Character {
    implicit val cursorBuilder: CursorBuilder[Character] =
      deriveInterfaceCursorBuilder[Character](CharacterType)
  }

  case class Human(
    id: String,
    name: Option[String],
    appearsIn: Option[List[Episode.Value]],
    friends: Option[List[String]],
    homePlanet: Option[String]
  ) extends Character

  object Human {
    implicit val cursorBuilder: CursorBuilder[Human] =
      deriveObjectCursorBuilder[Human](HumanType)
        .transformField("friends")(resolveFriends)
  }

  case class Droid(
    id: String,
    name: Option[String],
    appearsIn: Option[List[Episode.Value]],
    friends: Option[List[String]],
    primaryFunction: Option[String]
  ) extends Character

  object Droid {
    implicit val cursorBuilder: CursorBuilder[Droid] =
      deriveObjectCursorBuilder[Droid](DroidType)
        .transformField("friends")(resolveFriends)
  }

  def resolveFriends(c: Character): Result[Option[List[Character]]] =
    c.friends match {
      case None => None.success
      case Some(ids) =>
        ids.traverse(id =>
          characters.find(_.id == id).toResultOrError(s"Bad id '$id'")
        ).map(_.some)
    }

The data structure is slightly complicated by the need to support cycles of friendship, e.g.,

query {
  character(id: 1000) {
    name
    friends {
      name
      friends {
        name
      }
    }
  }
}

yields,

{
  "data": {
    "character": {
      "name": "Luke Skywalker",
      "friends": [
        {
          "name": "Han Solo",
          "friends": [
            {
              "name": "Luke Skywalker"
            },
            ...
        }
    }
  }
}

Here the root of the result is "Luke Skywalker" and we loop back to Luke through the mutual friendship with Han Solo.

The data type is not itself recursive, instead friend are identified by their ids. When traversing through the list of friends the resolveFriends method is used to locate the next Character value to visit.

  // The character database ...
  lazy val characters: List[Character] = List(
    Human(
      id = "1000",
      name = Some("Luke Skywalker"),
      friends = Some(List("1002", "1003", "2000", "2001")),
      appearsIn = Some(List(Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI)),
      homePlanet = Some("Tatooine")
    ),
    Human(
      id = "1001",
      name = Some("Darth Vader"),
      friends = Some(List("1004")),
      appearsIn = Some(List(Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI)),
      homePlanet = Some("Tatooine")
    ),
    Human(
      id = "1002",
      name = Some("Han Solo"),
      friends = Some(List("1000", "1003", "2001")),
      appearsIn = Some(List(Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI)),
      homePlanet = None
    ),
    Human(
      id = "1003",
      name = Some("Leia Organa"),
      friends = Some(List("1000", "1002", "2000", "2001")),
      appearsIn = Some(List(Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI)),
      homePlanet = Some("Alderaan")
    ),
    Human(
      id = "1004",
      name = Some("Wilhuff Tarkin"),
      friends = Some(List("1001")),
      appearsIn = Some(List(Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI)),
      homePlanet = None
    ),
    Droid(
      id = "2000",
      name = Some("C-3PO"),
      friends = Some(List("1000", "1002", "1003", "2001")),
      appearsIn = Some(List(Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI)),
      primaryFunction = Some("Protocol")
    ),
    Droid(
      id = "2001",
      name = Some("R2-D2"),
      friends = Some(List("1000", "1002", "1003")),
      appearsIn = Some(List(Episode.NEWHOPE, Episode.EMPIRE, Episode.JEDI)),
      primaryFunction = Some("Astromech")
    )
  )

The query compiler and elaborator

GraphQL queries are compiled into values of a Scala ADT which represents a query algebra. These query algebra terms are then transformed in a variety of ways, resulting in a program which can be interpreted against the model to produce the query result. The process of transforming these values is called elaboration, and each elaboration step simplifies or expands the term to bring it into a form which can be executed directly by the query interpreter.

Grackle's query algebra consists of the following elements,

case class UntypedSelect(
  name: String, alias: Option[String],
  args: List[Binding], directives: List[Directive],
  child: Query
)
case class Select(name: String, alias: Option[String], child: Query)
case class Group(queries: List[Query])
case class Unique(child: Query)
case class Filter(pred: Predicate, child: Query)
case class Introspect(schema: Schema, child: Query)
case class Environment(env: Env, child: Query)
case class Narrow(subtpe: TypeRef, child: Query)
case class Limit(num: Int, child: Query)
case class Offset(num: Int, child: Query)
case class OrderBy(selections: OrderSelections, child: Query)
case class Count(child: Query)
case class TransformCursor(f: Cursor => Result[Cursor], child: Query)
case class Component[F[_]](mapping: Mapping[F], ...)
case class Effect[F[_]](handler: EffectHandler[F], child: Query)
case object Empty

A simple query like this,

query {
  character(id: 1000) {
    name
  }
}

is first translated into a term in the query algebra of the form,

UntypedSelect("character", None, List(IntBinding("id", 1000)), Nil,
  UntypedSelect("name", None, Nil, Nil, Empty)
)

This first step is performed without reference to a GraphQL schema, hence the id argument is initially inferred to be of GraphQL type Int rather than the type ID which the schema expects.

Following this initial translation the Star Wars example has a single elaboration step whose role is to translate the selection into something executable. Elaboration uses the GraphQL schema and so is able to translate an input value parsed as an Int into a GraphQL ID. The semantics associated with this (i.e. what an id is and how it relates to the model) is specific to this model, so we have to provide that semantic via some model-specific code,

  override val selectElaborator = SelectElaborator {
    // The hero selector takes an Episode argument and yields a single value. We transform
    // the nested child to use the Filter and Unique operators to pick out the target using
    // the Eql predicate.
    case (QueryType, "hero", List(Binding("episode", EnumValue(e)))) =>
      for {
        ep <- Elab.liftR(
                Episode.values.find(_.toString == e).toResult(s"Unknown episode '$e'")
              )
        _  <- Elab.transformChild { child =>
                Unique(Filter(Eql(CharacterType / "id", Const(hero(ep).id)), child))
              }
      } yield ()

    // The character, human and droid selectors all take a single ID argument and yield a
    // single value (if any) or null. We transform the nested child to use the Unique and
    // Filter operators to pick out the target using the Eql predicate.
    case (QueryType, "character" | "human" | "droid", List(Binding("id", IDValue(id)))) =>
      Elab.transformChild { child =>
        Unique(Filter(Eql(CharacterType / "id", Const(id)), child))
      }
  }

Extracting out the case for the character selector,

case (QueryType, "character", List(Binding("id", IDValue(id)))) =>
  Elab.transformChild { child =>
    Unique(Filter(Eql(CharacterType / "id", Const(id)), child))
  }

the previous term is transformed as follows,

Select("character", None,
  Unique(Eql(CharacterType / "id"), Const("1000")), Select("name", None, Empty))
)

Here the original UntypedSelect terms have been converted to typed Select terms with the argument to the character selector translated into a predicate which refines the root data of the model to the single element which satisfies it via Unique. The remainder of the query (Select("name", None, Nil)) is then within the scope of that constraint. We have eliminated something with model-specific semantics (character(id: 1000)) in favour of something universal which can be interpreted directly against the model.

The query interpreter and cursor

The data required to construct the response to a query is determined by the structure of the query and gives rise to a more or less arbitrary traversal of the model. To support this Grackle provides a functional Cursor abstraction which points into the model and can navigate through GraphQL fields, arrays and values as required by a given query.

For in-memory models where the structure of the model ADT closely matches the GraphQL schema a Cursor can be derived automatically by a GenericMapping which only needs to be supplemented with a specification of the root mappings for the top level fields of the GraphQL schema. The root mappings enable the query interpreter to construct an appropriate initial Cursor for the query being evaluated.

For the Star Wars model the root definitions are of the following form,

      ObjectMapping(QueryType)(
        GenericField("hero", characters),
        GenericField("character", characters),
        GenericField("human", characters.collect { case h: Human => h }),
        GenericField("droid", characters.collect { case d: Droid => d })
      )

The first argument of the GenericField constructor corresponds to the top-level selection of the query (see the schema above) and the second argument is the initial model value for which a Cursor will be derived. When the query is executed, navigation will start with that Cursor and the corresponding GraphQL type.

The service

What we've seen so far allows us to compile and execute GraphQL queries against our in-memory model. We now need to expose that via HTTP. The following shows how we do that for http4s,

object GraphQLService {
  def mkRoutes[F[_]: Concurrent](prefix: String)(mapping: Mapping[F]): HttpRoutes[F] = {
    val dsl = new Http4sDsl[F]{}
    import dsl._

    implicit val jsonQPDecoder: QueryParamDecoder[Json] =
      QueryParamDecoder[String].emap { s =>
        parser.parse(s).leftMap {
          case ParsingFailure(msg, _) => ParseFailure("Invalid variables", msg)
        }
      }

    object QueryMatcher
      extends QueryParamDecoderMatcher[String]("query")
    object OperationNameMatcher
      extends OptionalQueryParamDecoderMatcher[String]("operationName")
    object VariablesMatcher
      extends OptionalValidatingQueryParamDecoderMatcher[Json]("variables")

    HttpRoutes.of[F] {
      // GraphQL query is embedded in the URI query string when queried via GET
      case GET -> Root / `prefix` :? 
             QueryMatcher(query) +& OperationNameMatcher(op) +& VariablesMatcher(vars0) =>
        vars0.sequence.fold(
          errors => BadRequest(errors.map(_.sanitized).mkString_("", ",", "")),
          vars =>
            for {
              result <- mapping.compileAndRun(query, op, vars)
              resp   <- Ok(result)
            } yield resp
        )

      // GraphQL query is embedded in a Json request body when queried via POST
      case req @ POST -> Root / `prefix` =>
        for {
          body   <- req.as[Json]
          obj    <- body.asObject.liftTo[F](
                      InvalidMessageBodyFailure("Invalid GraphQL query")
                    )
          query  <- obj("query").flatMap(_.asString).liftTo[F](
                      InvalidMessageBodyFailure("Missing query field")
                    )
          op     =  obj("operationName").flatMap(_.asString)
          vars   =  obj("variables")
          result <- mapping.compileAndRun(query, op, vars)
          resp   <- Ok(result)
        } yield resp
    }
  }
}

The GraphQL specification supports queries both via HTTP GET and POST requests, so we provide routes for both methods. For queries via GET the query is embedded in the URI query string in the form ... ?query=<URI encoded GraphQL query>. For queries via POST, the query is embedded in a JSON value of the form,

{
  "operationName": "Query",
  "query": "character(id: 1000) ..."
}

In each of these cases we extract the operation name and query from the request and pass them to the service for compilation and execution.

Many GraphQL client tools expect servers to be able to respond to a query named IntrospectionQuery returning a representation of the GraphQL schema supported by the endpoint which they use to provide client-side highlighting, validation, auto completion etc. The demo service provides this as well as normal query execution.

Putting it all together

Finally we need to run all of this on top of http4s. Here we have a simple IOApp running an EmberServer with the StarWarsService defined above, and a ResourceService to serve the GraphQL Playground web client,

object Main extends IOApp {
  def run(args: List[String]): IO[ExitCode] = {
    (for {
      starWarsRoutes <- StarWarsMapping[IO].map(mkRoutes("starwars"))
      _              <- mkServer(starWarsRoutes)
    } yield ()).useForever
  }
}
object DemoServer {
  def mkServer(graphQLRoutes: HttpRoutes[IO]): Resource[IO, Unit] = {
    val httpApp0 = (
      // Routes for static resources, i.e. GraphQL Playground
      resourceServiceBuilder[IO]("/assets").toRoutes <+>
      // GraphQL routes
      graphQLRoutes
    ).orNotFound

    val httpApp = Logger.httpApp(true, false)(httpApp0)

    val withErrorLogging: HttpApp[IO] = ErrorHandling.Recover.total(
      ErrorAction.log(
        httpApp,
        messageFailureLogAction = errorHandler,
        serviceErrorLogAction = errorHandler))

    // Spin up the server ...
    EmberServerBuilder.default[IO]
      .withHost(ip"0.0.0.0")
      .withPort(port"8080")
      .withHttpApp(withErrorLogging)
      .build.void
  }

  def errorHandler(t: Throwable, msg: => String) : IO[Unit] =
    IO.println(msg) >> IO.println(t) >> IO.println(t.printStackTrace())
}