Last active
November 4, 2022 15:43
-
-
Save paulpdaniels/d8e932b9faee19812d2de8f56dd77a51 to your computer and use it in GitHub Desktop.
An example of the relay node specification with Caliban
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
) | |
) | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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