Skip to content

Instantly share code, notes, and snippets.

Last active Aug 23, 2020
What would you like to do?
An example of the relay node specification with Caliban
object Example extends GenericSchema[Clock] {
implicit val extractorStrategy: ExtractorStrategy = ExtractorStrategy.hyphen
case class User(id: String)
case class Team(id: String)
case class NodeArg(id: String)
val userSchema = gen[User]
val teamSchema: Schema[Clock, Team] = gen[Team]
def lookupUser(id: String): UQuery[Option[User]] =
def lookupTeam(id: String): UQuery[Option[Team]] =
val node = Node.instance
implicit val nodeSchema: Schema[Clock, Node] =
case class Queries(
node: NodeArg => IO[CalibanError, Node]
val graphQL = GraphQL.graphQL(
node = arg => Node(
import caliban.CalibanError
import caliban.introspection.adt.{ __DeprecatedArgs, __Type }
import caliban.schema.Step.QueryStep
import caliban.schema.{ GenericSchema, Schema, Step, Types }
import zio.clock.Clock
import zio.query.{ UQuery, ZQuery }
class Node private (`type`: String, id: String)
object Node {
* The apply method is used to convert a string into a Node which is used to derive a polymorphic type under the hood
def apply(id: String)(implicit es: ExtractorStrategy): IO[ExecutionError, Node] =
IO.fromEither(es(id).toRight(ExecutionError(s"Could not extract type from id: $id"))).map {
case (typ, id) => Node(typ, id)
// Defines a strategy by which we should extract the type and id info from an opaque id
trait ExtractorStrategy {
def apply(id: String): (String, String)
object ExtractorStrategy {
def apply(fn: String => (String, String)): ExtractorStrategy = new ExtractorStrategy {
override def apply(id: String): (String, String) = fn(id)
def delim(char: Char): ExtractorStrategy = ExtractorStrategy(s => s.splitAt(s.indexOf(char.toInt)))
val hyphen: ExtractorStrategy = delim('-')
type Resolver[-R1] = String => ZQuery[R1, CalibanError, Step[R1]]
// Allows a declarative syntax for defining subtypes of the Node interface without needing an explicit
// sealed trait hierarchy
case class NodeBuilder[-R](private val subtypes: List[(__Type, Resolver[R])]) {
// Adds a new type resolver to the node set
def withType[R1 <: R, T](
fn: String => ZQuery[R1, CalibanError, Option[T]]
)(implicit schema: Schema[R1, T]): NodeBuilder[R1] = {
val step: Resolver[R1] = (id: String) => fn(id).map(_.fold[Step[R1]](Step.NullStep)(schema.resolve))
copy(subtypes = (schema.toType_() -> step) :: subtypes)
def build: Schema[R, Node] = new Schema[R, Node] {
override protected[this] def toType(isInput: Boolean, isSubscription: Boolean): __Type = {
val types = subtypes.sortBy(""))
val impl = = () => Some(List(toType(isInput, isSubscription)))))
val commonFields = impl
.collect {
case (name, list)
if impl.forall(_.fields(__DeprecatedArgs(Some(true))).getOrElse(Nil).exists( == name)) && =>`type`())).distinct.length == 1 =>
Types.makeInterface(Some("Node"), None, commonFields.toList, impl, None)
private lazy val _lookup: Map[String, (__Type, Resolver[R])] = subtypes.flatMap(r => -> r)).toMap
override def resolve(value: Node): Step[R] =
_lookup.get(value.`type`).fold(Step.NullStep: Step[R]) {
case (_, resolver) => QueryStep(resolver(
def instance: NodeBuilder[Any] = NodeBuilder(Nil)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment