Skip to content

Commit

Permalink
update grackle
Browse files Browse the repository at this point in the history
  • Loading branch information
tpolecat committed Sep 8, 2023
1 parent 411de6b commit 00b8c5a
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 79 deletions.
15 changes: 3 additions & 12 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,14 @@ ThisBuild / tlVersionIntroduced := Map("3" -> "0.3.3")
lazy val core = project
.in(file("modules/core"))
.settings(
name := "lucuma-graphql-routes-core",
name := "lucuma-graphql-routes",
libraryDependencies ++= Seq(
"edu.gemini" %% "clue-model" % clueVersion,
"org.http4s" %% "http4s-server" % http4sVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion,
"org.http4s" %% "http4s-circe" % http4sVersion,
"org.typelevel" %% "log4cats-core" % log4catsVersion,
),
)

lazy val grackle = project
.in(file("modules/grackle"))
.dependsOn(core)
.settings(
name := "lucuma-graphql-routes-grackle",
libraryDependencies ++= Seq(
"edu.gemini" %% "gsp-graphql-core" % grackleVersion,
"org.tpolecat" %% "natchez-core" % natchezVersion,
"edu.gemini" %% "gsp-graphql-core" % grackleVersion,
"org.tpolecat" %% "natchez-core" % natchezVersion,
),
)
29 changes: 10 additions & 19 deletions modules/core/src/main/scala/lucuma/graphql/routes/Connection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import clue.model.GraphQLRequest
import clue.model.StreamingMessage.FromClient._
import clue.model.StreamingMessage.FromServer._
import clue.model.StreamingMessage._
import io.circe.Encoder
import io.circe.Json
import io.circe.syntax._
import org.http4s.ParseResult
import org.http4s.headers.Authorization
import org.typelevel.log4cats.Logger
import edu.gemini.grackle.Operation
import io.circe.Json
import io.circe.JsonObject

/** A web-socket connection that receives messages from a client and processes them. */
sealed trait Connection[F[_]] {
Expand Down Expand Up @@ -62,7 +62,7 @@ object Connection {
* Starts a Graph QL operation associated with a particular id.
* @return state transition and action to execute
*/
def start[V: Encoder](id: String, req: GraphQLRequest[V]): (ConnectionState[F], F[Unit])
def start(id: String, req: GraphQLRequest[JsonObject]): (ConnectionState[F], F[Unit])

/**
* Terminates a Graph QL subscription associated with a particular id
Expand Down Expand Up @@ -111,7 +111,7 @@ object Connection {
} yield ()
)

override def start[V: Encoder](id: String, req: GraphQLRequest[V]): (ConnectionState[F], F[Unit]) =
override def start(id: String, req: GraphQLRequest[JsonObject]): (ConnectionState[F], F[Unit]) =
doClose(s"start($id, $req)")

override def stop(id: String): (ConnectionState[F], F[Unit]) =
Expand All @@ -137,8 +137,6 @@ object Connection {

new ConnectionState[F] {

import service.ParsedGraphQLRequest

override def reset(
service: GraphQLService[F],
r: Option[FromServer] => F[Unit],
Expand All @@ -150,20 +148,13 @@ object Connection {
r(Some(ConnectionKeepAlive))
)

override def start[V: Encoder](id: String, raw: GraphQLRequest[V]): (ConnectionState[F], F[Unit]) = {

val parseResult =
service
.parse(raw.query, raw.operationName)
.map(ParsedGraphQLRequest(_, raw.operationName, raw.variables.map(_.asJson)))

override def start(id: String, raw: GraphQLRequest[JsonObject]): (ConnectionState[F], F[Unit]) = {
val parseResult = service.parse(raw.query, raw.operationName, raw.variables)
val action = parseResult match {
case Left(err) => service.format(err).flatMap { errors => send(Some(Error(id, errors))) }
case Right(req) => if (service.isSubscription(req)) subscribe(id, req) else execute(id, req)
}

(this, action)

}

override def stop(id: String): (ConnectionState[F], F[Unit]) =
Expand All @@ -172,10 +163,10 @@ object Connection {
override val stopAll: (ConnectionState[F], F[Unit]) =
(this, subscriptions.removeAll)

def subscribe(id: String, request: ParsedGraphQLRequest): F[Unit] =
def subscribe(id: String, request: Operation): F[Unit] =
subscriptions.add(id, service.subscribe(request))

def execute(id: String, request: ParsedGraphQLRequest): F[Unit] =
def execute(id: String, request: Operation): F[Unit] =
for {
r <- service.query(request)
_ <- r.fold(
Expand Down Expand Up @@ -203,7 +194,7 @@ object Connection {
): (ConnectionState[F], F[Unit]) =
raiseError

override def start[V: Encoder](id: String, req: GraphQLRequest[V]): (ConnectionState[F], F[Unit]) =
override def start(id: String, req: GraphQLRequest[JsonObject]): (ConnectionState[F], F[Unit]) =
raiseError

override def stop(id: String): (ConnectionState[F], F[Unit]) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,52 +9,50 @@ import cats.data.NonEmptyList
import cats.syntax.all._
import clue.model.GraphQLError
import clue.model.GraphQLErrors
import edu.gemini.grackle.Cursor
import edu.gemini.grackle.Mapping
import edu.gemini.grackle.Problem
import edu.gemini.grackle.QueryParser
import edu.gemini.grackle.UntypedOperation
import edu.gemini.grackle.UntypedOperation.UntypedSubscription
import fs2.Compiler
import fs2.Stream
import io.circe.Json
import lucuma.graphql.routes.conversions._
import natchez.Trace
import org.typelevel.log4cats.Logger

import scala.util.control.NonFatal
import edu.gemini.grackle.Operation
import edu.gemini.grackle.Result.Failure
import edu.gemini.grackle.Result
import edu.gemini.grackle.Result.Success
import edu.gemini.grackle.Result.Warning
import io.circe.JsonObject
import edu.gemini.grackle.Env

class GrackleGraphQLService[F[_]: MonadThrow: Logger: Trace](
mapping: Mapping[F],
)(implicit ev: Compiler[F,F]) extends GraphQLService[F] {

type Document = UntypedOperation
def isSubscription(req: Operation): Boolean =
mapping.schema.subscriptionType.exists(_ =:= req.rootTpe)

def isSubscription(req: ParsedGraphQLRequest): Boolean =
req.query match {
case UntypedSubscription(_, _) => true
case _ => false
def parse(query: String, op: Option[String], vars: Option[JsonObject]): Either[Throwable, Operation] =
mapping.compiler.compile(query, op, vars.map(_.toJson)) match {
case Result.InternalError(error) => Left(error)
case Success(value) => Right(value)
case Failure(problems) => Left(GrackleException(problems))
case Warning(_, value) => Right(value) // todo: log warnings
}

def parse(query: String, op: Option[String]): Either[Throwable, Document] =
QueryParser.parseText(query, op).toEither.leftMap(_.map(GrackleException(_)).merge)

def query(request: ParsedGraphQLRequest): F[Either[Throwable, Json]] =
def query(request: Operation): F[Either[Throwable, Json]] =
Trace[F].span("graphql") {
Trace[F].put("graphql.query" -> request.query.query.render) *>
Trace[F].put("graphql.query" -> request.query.render) *>
subscribe(request).compile.toList.map {
case List(e) => e
case other => GrackleException(Problem(s"Expected exactly one result, found ${other.length}.")).asLeft
}
}

def subscribe(request: ParsedGraphQLRequest): Stream[F, Either[Throwable, Json]] =
mapping.compiler.compileUntyped(request.query, request.vars).toEither match {
case Right(operation) =>
mapping.run(operation.query, operation.rootTpe, Cursor.Env.empty)
.map(_.asRight[Throwable]) recover { case NonFatal(t) => Left(t) }
case Left(e) => Stream.emit(Left(e.map(GrackleException(_)).merge))
}
def subscribe(op: Operation): Stream[F, Either[Throwable, Json]] =
// HMM: we don't get throwables on the left anymore
mapping.interpreter.run(op.query, op.rootTpe, Env.EmptyEnv).evalMap(mapping.mkResponse).map(_.asRight)

def format(err: Throwable): F[GraphQLErrors] =
Logger[F].error(err)("Error computing GraphQL response.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,17 @@ package lucuma.graphql.routes
import clue.model.GraphQLErrors
import fs2.Stream
import io.circe._
import edu.gemini.grackle.Operation

trait GraphQLService[F[_]] {

type Document
def parse(query: String, op: Option[String], vars: Option[JsonObject]): Either[Throwable, Operation]

case class ParsedGraphQLRequest(
query: Document,
op: Option[String],
vars: Option[Json]
)
def isSubscription(doc: Operation): Boolean

def parse(query: String, op: Option[String]): Either[Throwable, Document]
def query(request: Operation): F[Either[Throwable, Json]]

def isSubscription(doc: ParsedGraphQLRequest): Boolean

def query(request: ParsedGraphQLRequest): F[Either[Throwable, Json]]

def subscribe(request: ParsedGraphQLRequest): Stream[F, Either[Throwable, Json]]
def subscribe(request: Operation): Stream[F, Either[Throwable, Json]]

def format(err: Throwable): F[GraphQLErrors]

Expand Down
26 changes: 12 additions & 14 deletions modules/core/src/main/scala/lucuma/graphql/routes/Routes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,16 @@ object Routes {
val dsl = new Http4sDsl[F]{}
import dsl._

implicit val jsonQPDecoder: QueryParamDecoder[Json] = QueryParamDecoder[String].emap { s =>
parser.parse(s).leftMap { case ParsingFailure(msg, _) => ParseFailure("Invalid variables", msg) }
implicit val jsonQPDecoder: QueryParamDecoder[JsonObject] = QueryParamDecoder[String].emap { s =>
parser.parse(s) match {
case Left(ParsingFailure(msg, _)) => Left(ParseFailure("Invalid variables", msg))
case Right(json) => json.asObject.toRight(ParseFailure("Expected JsonObject", json.spaces2))
}
}

object QueryMatcher extends QueryParamDecoderMatcher[String]("query")
object OperationNameMatcher extends OptionalQueryParamDecoderMatcher[String]("operationName")
object VariablesMatcher extends OptionalValidatingQueryParamDecoderMatcher[Json]("variables")
object VariablesMatcher extends OptionalValidatingQueryParamDecoderMatcher[JsonObject]("variables")

def handler(req: Request[F]): F[Option[HttpRouteHandler[F]]] =
Nested(service(req.headers.get[Authorization])).map(new HttpRouteHandler(_)).value
Expand Down Expand Up @@ -101,8 +104,6 @@ object Routes {

class HttpRouteHandler[F[_]: Temporal](service: GraphQLService[F]) {

import service.{ Document, ParsedGraphQLRequest }

val dsl: Http4sDsl[F] = new Http4sDsl[F]{}
import dsl._

Expand All @@ -117,26 +118,23 @@ class HttpRouteHandler[F[_]: Temporal](service: GraphQLService[F]) {
case Right(json) => Ok(json)
}

private def parse(query: String, op: Option[String]): Either[Throwable, Document] =
service.parse(query, op)

def oneOffGet(
query: String,
op: Option[String],
vars0: Option[ValidatedNel[ParseFailure, Json]]
vars0: Option[ValidatedNel[ParseFailure, JsonObject]]
): F[Response[F]] =
vars0.sequence.fold(
errors =>
Ok(errors.map(_.sanitized).mkString_("", ",", "")), // in GraphQL errors are reported in a 200 Ok response (!)

vars =>
parse(query, op) match {
service.parse(query, op, vars) match {
case Left(error) =>
Ok(service.format(error).map(errorsToJson)) // in GraphQL errors are reported in a 200 Ok response (!)

case Right(ast) =>
case Right(op) =>
for {
result <- service.query(ParsedGraphQLRequest(ast, op, vars))
result <- service.query(op)
resp <- toResponse(result)
} yield resp
}
Expand All @@ -148,8 +146,8 @@ class HttpRouteHandler[F[_]: Temporal](service: GraphQLService[F]) {
obj <- body.asObject.liftTo[F](InvalidMessageBodyFailure("Invalid GraphQL query"))
query <- obj("query").flatMap(_.asString).liftTo[F](InvalidMessageBodyFailure("Missing query field"))
op = obj("operationName").flatMap(_.asString)
vars = obj("variables")
parsed = parse(query, op).map(ParsedGraphQLRequest(_, op, vars))
vars = obj("variables").flatMap(_.asObject)
parsed = service.parse(query, op, vars)
result <- parsed.traverse(service.query).map(_.flatten)
resp <- toResponse(result)
} yield resp
Expand Down

0 comments on commit 00b8c5a

Please sign in to comment.