Query DSL
gql provides a dsl for building graphql queries and response parsers. When you compose your query with the dsl, you automatically compose both a query and a json decoder for the query response.
Selections
The simplest combinator is sel
which declares a field selection:
import gql.client._
import gql.client.dsl._
import cats.implicits._
sel[Option[String]]("name")
// res0: SelectionSet[Option[String]] = SelectionSet(
// impl = Fmap(
// fa = Lift(
// fa = Field(
// fieldName = "name",
// alias0 = None,
// args0 = List(),
// subQuery = OptionModifier(
// subQuery = Terminal(decoder = io.circe.Decoder$$anon$26@22aaf127)
// ),
// directives0 = List()
// )
// ),
// f = gql.client.SelectionSet$$$Lambda$25715/0x00000008043bf840@376fb10e
// )
// )
Most combinators in the dsl have multiple overloads to provide various features.
sel.build[Option[String]]("name", _.alias("n"))
sel.build[Option[String]]("name", _.args(arg("id", 42)))
Every selection related structure forms an Applicative
such that you can compose multiple selections together:
val s1 = sel[Option[String]]("name")
val s2 = sel[Option[Int]]("age")
val s3: SelectionSet[(Option[String], Option[Int])] = (s1, s2).tupled
final case class PersonQuery(name: Option[String], age: Option[Int])
val pq: SelectionSet[PersonQuery] = (s1, s2).mapN(PersonQuery.apply)
Queries can also act as sub-selections (SubQuery
in gql):
sel[PersonQuery]("person") {
pq
}
In the first examples the sub-query is captured implicitly. We can also do this for custom types:
implicit val pq2: SelectionSet[PersonQuery] = pq
sel[PersonQuery]("person")
Fragments
Like in graphql we can define fragments to reuse selections:
val frag = fragment[String]("MyFragment", on="Person") {
sel[String]("name")
}
val fragmentSpreads = sel[(Option[String], Option[Int])]("person") {
(
fragment.spread(frag),
inlineFrag[Int]("Person") {
sel[Int]("age")
}
).tupled
}
Notice that both fragment
and inlineFrag
return an optional result.
This is because the spread may not match on the type (if the spread condition is a sub-type of the spread-on type).
This is not always the desired behavior, and as such, fragments can be required:
fragment.spread(frag).required: SelectionSet[String]
You can provide additional information, should the fragment turn out to actually be missing:
fragment.spread(frag).requiredFragment("MyFragment", on="Person")
Fragments should be preferred over re-using selections directly to reduce the rendered query size.
Variables
Variables are accumulated into a sort of writer monad, such that they can be declared ad-hoc:
variable[String]("name")
// res7: Var[String, VariableName[String]] = Var(
// impl = WriterT(
// run = (
// Singleton(
// a = One(
// name = VariableName(name = "name"),
// tpe = "String!",
// default = None
// )
// ),
// io.circe.Encoder$AsObject$$anon$68@988be61
// )
// ),
// variableNames = VariableName(name = "name")
// )
Variables can be combined with the ~
operator:
variable[String]("name") ~ variable[Int]("age")
// res8: Var[(String, Int), (VariableName[String], VariableName[Int])] = Var(
// impl = WriterT(
// run = (
// Append(
// leftNE = Singleton(
// a = One(
// name = VariableName(name = "name"),
// tpe = "String!",
// default = None
// )
// ),
// rightNE = Singleton(
// a = One(
// name = VariableName(name = "age"),
// tpe = "Int!",
// default = None
// )
// )
// ),
// io.circe.Encoder$AsObject$$anon$68@7d03869c
// )
// ),
// variableNames = (VariableName(name = "name"), VariableName(name = "age"))
// )
Variables can also be declared as omittable, optionally with a default value:
omittableVariable[String]("name", value("John")) ~
omittableVariable[Int]("age")
// res9: Var[(Option[String], Option[Int]), (VariableName[String], VariableName[Int])] = Var(
// impl = WriterT(
// run = (
// Append(
// leftNE = Singleton(
// a = One(
// name = VariableName(name = "name"),
// tpe = "String!",
// default = Some(value = StringValue(v = "John", c = ()))
// )
// ),
// rightNE = Singleton(
// a = One(
// name = VariableName(name = "age"),
// tpe = "Int!",
// default = None
// )
// )
// ),
// io.circe.Encoder$AsObject$$anon$68@278939b0
// )
// ),
// variableNames = (VariableName(name = "name"), VariableName(name = "age"))
// )
Variables can be "materialized" into a VariableClosure
by introducing them to a query:
// Given a variable of type String, we can construct a query that returns an Int
val queryWithVariable: VariableClosure[String, Int] =
variable[String]("name").introduce{ name: VariableName[String] =>
sel.build[Int]("id", _.args(arg("name", name)))
}
VariableClosure
can be combined via ~
and have their selections modified via modify
:
def subQuery1: VariableClosure[String, Int] = queryWithVariable
def subQuery2: VariableClosure[String, Int] =
variable[String]("name2").introduce{ name: VariableName[String] =>
sel.build[Int]("id2", _.args(arg("name", name)))
}
def combined: VariableClosure[(String, String), Int] =
(subQuery1 ~ subQuery2).modify(_.map{ case (v1, v2) => v1 + v2 })
// VariableClosure also forms a profunctor so we can also use rmap
(subQuery1 ~ subQuery2).rmap{ case (v1, v2) => v1 + v2 }
Execution
Once a query has been constructed, there are three ways to wrap it together.
simple
if the query is parameter-less and name-less, named
if your query is named and parameterized
if it is both named and parameterized:
import gql.parser.QueryAst.OperationType
def simpleQuery = Query.simple(
OperationType.Query,
sel[Unit]("person") {
(
sel[Int]("id"),
sel.build[Int]("age", _.args(arg("numbers", List(42))))
).tupled.void
}
)
simpleQuery.compile.query
// res11: String = "query { person { age( numbers: [42] ), id } }"
Query.named(
OperationType.Mutation,
"MyMutation",
sel[String]("name")
).compile.query
// res12: String = "mutation MyMutation { name }"
def paramQuery = Query.parameterized(
OperationType.Subscription,
"MySubscription",
combined
)
def compiledParamQuery = paramQuery.compile(("first", "second"))
compiledParamQuery.query
// res13: String = """subscription MySubscription( $name : String!, $name2 : String! ) {
// id2( name: $name2 ),
// id( name: $name )
// }"""
compiledParamQuery.variables
// res14: Option[io.circe.JsonObject] = Some(
// value = object[name -> "first",name2 -> "second"]
// )