I'm looking at the Sangria library for coding a GraphQL server in Scala. It feels odd, however, that the same type system must be implemented twice: (1) as part of the GraphQL type declarations, and (2) also at the server side, as Scala case classes, with accompanying ObjectType, InterfaceType, etc. vals.
Hardcoding the type system in Scala is especially irksome, since my purpose is to be able to CRUD aggregates of arbitrary shape, where each shape is defined as a GraphQL collection of types. For example, say an instance of type Shape contains a GraphQL document as a field; and an instance of type Entity has a reference to its Shape and also contains a Json object of the shape defined in that Shape.
case class Shape(id: String, name: String, doc: sangria.ast.Document)
case class Entity(id: String, name: String, shape: Shape, content: JsValue)
For example, if the shape document is something like this:
type Person {
firstName: String!
lastName: String!
age: Int
}
then the Json content in the entity could be something like this:
{
"firstName": "John",
"lastName": "Smith",
"age": 30
}
(A real example would, of course, also have nested types, etc.)
Thus, I seek to be able to define instances of type Entity whose shape is defined in their corresponding Shape. I do NOT want to hardcode the corresponding sangria.schema.Schema but want to derive it directly from the shape document.
Is there a ready way to generate a GraphQL schema programmatically from a GraphQL document containing type declarations?
When doing query batching, the GraphQL server executes multiple queries in a single request. But those queries are still independent from each other. They just happen to be executed one after the other, to avoid the latency from multiple requests.
The GraphQL schema language supports the scalar types of String , Int , Float , Boolean , and ID , so you can use these directly in the schema you pass to buildSchema . By default, every type is nullable - it's legitimate to return null as any of the scalar types.
For such dynamic use-cases, sangria provides a way to build a schema from GraphQL IDL. Here is how you can do it (I simplified your example a bit, but the same can be implemented when all this data comes from separate classes like Shape
and Entity
):
import sangria.ast._
import sangria.schema._
import sangria.macros._
import sangria.marshalling.sprayJson._
import sangria.execution.Executor
import scala.concurrent.ExecutionContext.Implicits.global
import spray.json._
val schemaAst =
gql"""
type Person {
firstName: String!
lastName: String!
age: Int
}
type Query {
people: [Person!]
}
"""
val schema = Schema.buildFromAst(schemaAst, builder)
val query =
gql"""
{
people {
firstName
age
}
}
"""
val data =
"""
{
"people": [{
"firstName": "John",
"lastName": "Smith",
"age": 30
}]
}
""".parseJson
val result = Executor.execute(schema, query, data)
In order to define how resolve
functions should be generated, you need to create a custom schema builder, like this one, and just override resolveField
method:
val builder =
new DefaultAstSchemaBuilder[JsValue] {
override def resolveField(typeDefinition: TypeDefinition, definition: FieldDefinition) =
typeDefinition.name match {
case "Query" ⇒
c ⇒ c.ctx.asJsObject.fields get c.field.name map fromJson
case _ ⇒
c ⇒ fromJson(c.value.asInstanceOf[JsObject].fields(c.field.name))
}
def fromJson(v: JsValue) = v match {
case JsArray(l) ⇒ l
case JsString(s) ⇒ s
case JsNumber(n) ⇒ n.intValue()
case other ⇒ other
}
}
When you execute this example, you will see following JSON result
:
{
"data": {
"people": [{
"firstName": "John",
"age": 30
}]
}
}
If you would like to see a more complex example, I would recommend you to check out GrapohQL Toolbox "proxy". This project takes it one step further and even adds custom directives to control the resolve function generation. The code can be found here:
https://github.com/OlegIlyenko/graphql-toolbox
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With