From 00b8c5af1cc40110d4001b86422397d6e2395366 Mon Sep 17 00:00:00 2001 From: Rob Norris Date: Fri, 8 Sep 2023 11:40:14 -0500 Subject: [PATCH] update grackle --- build.sbt | 15 ++----- .../lucuma/graphql/routes/Connection.scala | 29 +++++-------- .../routes/GrackleGraphQLService.scala | 42 +++++++++---------- .../graphql/routes/GraphQLService.scala | 17 +++----- .../scala/lucuma/graphql/routes/Routes.scala | 26 ++++++------ .../lucuma/graphql/routes/conversions.scala | 0 6 files changed, 50 insertions(+), 79 deletions(-) rename modules/{grackle => core}/src/main/scala/lucuma/graphql/routes/GrackleGraphQLService.scala (61%) rename modules/{grackle => core}/src/main/scala/lucuma/graphql/routes/conversions.scala (100%) diff --git a/build.sbt b/build.sbt index 770c504..483674e 100644 --- a/build.sbt +++ b/build.sbt @@ -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, ), ) diff --git a/modules/core/src/main/scala/lucuma/graphql/routes/Connection.scala b/modules/core/src/main/scala/lucuma/graphql/routes/Connection.scala index 8becc97..03d942c 100644 --- a/modules/core/src/main/scala/lucuma/graphql/routes/Connection.scala +++ b/modules/core/src/main/scala/lucuma/graphql/routes/Connection.scala @@ -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[_]] { @@ -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 @@ -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]) = @@ -137,8 +137,6 @@ object Connection { new ConnectionState[F] { - import service.ParsedGraphQLRequest - override def reset( service: GraphQLService[F], r: Option[FromServer] => F[Unit], @@ -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]) = @@ -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( @@ -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]) = diff --git a/modules/grackle/src/main/scala/lucuma/graphql/routes/GrackleGraphQLService.scala b/modules/core/src/main/scala/lucuma/graphql/routes/GrackleGraphQLService.scala similarity index 61% rename from modules/grackle/src/main/scala/lucuma/graphql/routes/GrackleGraphQLService.scala rename to modules/core/src/main/scala/lucuma/graphql/routes/GrackleGraphQLService.scala index 1abfb87..ef4ebea 100644 --- a/modules/grackle/src/main/scala/lucuma/graphql/routes/GrackleGraphQLService.scala +++ b/modules/core/src/main/scala/lucuma/graphql/routes/GrackleGraphQLService.scala @@ -9,12 +9,8 @@ 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 @@ -22,39 +18,41 @@ 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.") diff --git a/modules/core/src/main/scala/lucuma/graphql/routes/GraphQLService.scala b/modules/core/src/main/scala/lucuma/graphql/routes/GraphQLService.scala index 8abf52e..08aea1d 100644 --- a/modules/core/src/main/scala/lucuma/graphql/routes/GraphQLService.scala +++ b/modules/core/src/main/scala/lucuma/graphql/routes/GraphQLService.scala @@ -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] diff --git a/modules/core/src/main/scala/lucuma/graphql/routes/Routes.scala b/modules/core/src/main/scala/lucuma/graphql/routes/Routes.scala index 58f970c..8d730c2 100644 --- a/modules/core/src/main/scala/lucuma/graphql/routes/Routes.scala +++ b/modules/core/src/main/scala/lucuma/graphql/routes/Routes.scala @@ -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 @@ -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._ @@ -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 } @@ -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 diff --git a/modules/grackle/src/main/scala/lucuma/graphql/routes/conversions.scala b/modules/core/src/main/scala/lucuma/graphql/routes/conversions.scala similarity index 100% rename from modules/grackle/src/main/scala/lucuma/graphql/routes/conversions.scala rename to modules/core/src/main/scala/lucuma/graphql/routes/conversions.scala