Skip to content

Instantly share code, notes, and snippets.

@frekw
Last active April 6, 2021 10:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save frekw/61c786b83e043fd347ed8e958f2c85d6 to your computer and use it in GitHub Desktop.
Save frekw/61c786b83e043fd347ed8e958f2c85d6 to your computer and use it in GitHub Desktop.
Caliban remote schemas
package gql.remoteschema
import zio._
import zio.query._
import caliban._
import caliban.schema._
import caliban.introspection.adt._
import caliban.execution.Field
import gql.remoteschema.json._
import caliban.interop.circe.json._
import io.circe.syntax._
import sttp.client3._
import sttp.client3.circe._
import sttp.client3.asynchttpclient.zio._
object Executor {
def fromRemoteAPI(s: __Schema, uri: String): GraphQL[SttpClient] = {
new GraphQL[SttpClient] {
protected val additionalDirectives: List[__Directive] = List()
protected val schemaBuilder: caliban.schema.RootSchemaBuilder[SttpClient] =
toRemoteSchemaBuilder(s, uri)
protected val wrappers: List[caliban.wrappers.Wrapper[SttpClient]] =
List()
}
}
private def toRemoteSchemaBuilder(s: __Schema, uri: String): RootSchemaBuilder[SttpClient] = {
RootSchemaBuilder(
query = Some(
Operation[SttpClient](
s.queryType,
Step.MetadataFunctionStep((args: caliban.execution.Field) => {
val q = GraphQLRequest(
query = Some(renderQuery(args))
)
val outgoing =
basicRequest
.post(uri"$uri")
.body(q)
.response(asJson[GraphQLResponse[CalibanError]])
val eff =
(for {
res <- send(outgoing)
body <- ZIO
.fromEither(res.body)
.catchAll(e => {
ZIO.succeed(
GraphQLResponse[CalibanError](
data = ResponseValue.ObjectValue(List()),
errors = List(
CalibanError.ExecutionError("No body from upstream")
),
None
)
)
})
} yield Step.PureStep(
body.data
))
Step.QueryStep(ZQuery.fromEffect(eff))
})
)
),
mutation = None,
subscription = None
)
}
def renderQuery(field: Field): String = {
val children =
if (field.fields.size > 0)
field.fields.map(renderQuery(_)).mkString("{ ", " ", " }")
else ""
val args =
if (field.arguments.size > 0)
field.arguments
.map({
case (k, v: Value.EnumValue) => s"$k: ${v.value}"
case (k, v) => s"$k: $v"
})
.mkString("(", ", ", ") ")
else ""
val alias = field.alias.map(a => s"$a: ").getOrElse("")
val res = s"$alias${field.name}$args$children"
println(s"renderQuery: $res")
res
}
}
package gql.remoteschema
import io.circe._, io.circe.syntax._, io.circe.generic.semiauto._
import caliban.interop.circe.json._
import caliban._
package object json {
implicit val decodeCalibanError: io.circe.Decoder[CalibanError] =
Decoder.instance(cursor =>
cursor
.downField("message")
.as[String]
.map(e => CalibanError.ExecutionError(e))
)
implicit val decodeGraphQLResponse: io.circe.Decoder[GraphQLResponse[CalibanError]] =
Decoder.instance(cursor =>
for {
data <- cursor
.downField("data")
.as[ResponseValue]
errors <- cursor
.downField("errors")
.as[Option[List[CalibanError]]]
} yield GraphQLResponse[CalibanError](
data = data,
errors = errors.getOrElse(List()),
extensions = None
)
)
implicit val encodeGraphQLRequest: io.circe.Encoder[GraphQLRequest] =
Encoder.instance[GraphQLRequest](r =>
Json.obj(
"query" -> r.query.asJson,
"operationName" -> r.operationName.asJson,
"variables" -> r.variables.asJson,
"extensions" -> r.extensions.asJson
)
)
}
package gql
import zio._
import zio.query._
import caliban.{GraphQLRequest, GraphQLResponse, CalibanError, ResponseValue}
import caliban.schema._
import caliban.introspection.adt._
import gql.remoteschema.Executor.renderQuery
import zio.query._
import gql.remoteschema.json._
import caliban.interop.circe.json._
import io.circe.syntax._
import sttp.client3._
import sttp.client3.circe._
import sttp.client3.asynchttpclient.zio._
package object remoteschema {
case class RemoteResolver(typeMap: Map[String, __Type], apiURL: String) {
def remoteResolver[R, A](typeName: String)(
prepare: (A, caliban.execution.Field) => caliban.execution.Field,
beforeSend: RequestT[Identity, Either[String, String], Any] => RequestT[
Identity,
Either[String, String],
Any
] = identity
) = new PartialSchema[SttpClient, R, A] {
def resolve(a: A, args: caliban.execution.Field): ZIO[SttpClient, CalibanError, ResponseValue] = {
val q = GraphQLRequest(
query = Some(
"{ " + renderQuery(
prepare(a, args)
) + " }"
)
)
val req = beforeSend(basicRequest.post(uri"$apiURL"))
.body(q)
.response(asJson[GraphQLResponse[CalibanError]])
(for {
res <- send(req)
body <- ZIO.fromEither(res.body)
} yield body.data).mapError(e => CalibanError.ExecutionError(e.toString()))
}
def toType(isInput: Boolean, isSubscription: Boolean): __Type = typeMap(typeName)
}
}
object RemoteResolver {
def fromSchema(schema: __Schema, apiURL: String): RemoteResolver = {
val typeMap = schema.types
.collect({ t =>
(t.name) match {
case Some(name) => name -> t
}
})
.toMap
RemoteResolver(typeMap, apiURL)
}
}
trait PartialSchema[R0, R, A] { self =>
def toType(isInput: Boolean, isSubscription: Boolean): __Type
def resolve(value: A, args: caliban.execution.Field): ZIO[R0, CalibanError, ResponseValue]
def provide[R1 <: R0](env: R1): Schema[R, A] = new Schema[R, A] {
def resolve(value: A): Step[R] =
Step.MetadataFunctionStep((args: caliban.execution.Field) => {
Step.QueryStep(ZQuery.fromEffect(self.resolve(value, args).map(Step.PureStep(_)).provide(env)))
})
protected def toType(isInput: Boolean, isSubscription: Boolean): __Type =
self.toType(isInput, isSubscription)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment