Skip to content

Instantly share code, notes, and snippets.

@paulpdaniels
Last active November 4, 2022 15:43
Show Gist options
  • Save paulpdaniels/d8e932b9faee19812d2de8f56dd77a51 to your computer and use it in GitHub Desktop.
Save paulpdaniels/d8e932b9faee19812d2de8f56dd77a51 to your computer and use it in GitHub Desktop.
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]] =
ZQuery.some(User(id))
def lookupTeam(id: String): UQuery[Option[Team]] =
ZQuery.some(Team(id))
val node = Node.instance
.withType(lookupUser)
.withType(lookupTeam)
implicit val nodeSchema: Schema[Clock, Node] = node.build
case class Queries(
node: NodeArg => IO[CalibanError, Node]
)
val graphQL = GraphQL.graphQL(
RootResolver(
Queries(
node = arg => Node(arg.id)
)
)
)
}
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(_._1.name.getOrElse(""))
val impl = types.map(_._1.copy(interfaces = () => Some(List(toType(isInput, isSubscription)))))
val commonFields = impl
.flatMap(_.fields(__DeprecatedArgs(Some(true))))
.flatten
.groupBy(_.name)
.collect {
case (name, list)
if impl.forall(_.fields(__DeprecatedArgs(Some(true))).getOrElse(Nil).exists(_.name == name)) &&
list.map(t => Types.name(t.`type`())).distinct.length == 1 =>
list.headOption
}
.flatten
Types.makeInterface(Some("Node"), None, commonFields.toList, impl, None)
}
private lazy val _lookup: Map[String, (__Type, Resolver[R])] = subtypes.flatMap(r => r._1.name.map(_ -> r)).toMap
override def resolve(value: Node): Step[R] =
_lookup.get(value.`type`).fold(Step.NullStep: Step[R]) {
case (_, resolver) => QueryStep(resolver(value.id))
}
}
}
def instance: NodeBuilder[Any] = NodeBuilder(Nil)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment