Skip to main content

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@7ad1f762)
// ),
// directives0 = List()
// )
// ),
// f = gql.client.SelectionSet$$$Lambda$12453/0x00000008032b7040@7b78470
// )
// )

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

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@4aaa166d
// )
// ),
// 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@49486fcd
// )
// ),
// 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@86db9a4
// )
// ),
// 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"]
// )