diff --git a/adapters/akka-http/src/main/scala/caliban/AkkaHttpAdapter.scala b/adapters/akka-http/src/main/scala/caliban/AkkaHttpAdapter.scala index a0715b9b8..11820ef6e 100644 --- a/adapters/akka-http/src/main/scala/caliban/AkkaHttpAdapter.scala +++ b/adapters/akka-http/src/main/scala/caliban/AkkaHttpAdapter.scala @@ -11,6 +11,7 @@ import sttp.capabilities.WebSockets import sttp.capabilities.akka.AkkaStreams import sttp.capabilities.akka.AkkaStreams.Pipe import sttp.model.StatusCode +import sttp.monad.{ FutureMonad, MonadError } import sttp.tapir.Codec.JsonCodec import sttp.tapir.PublicEndpoint import sttp.tapir.model.ServerRequest @@ -22,6 +23,8 @@ import zio.stream.ZStream import scala.concurrent.{ ExecutionContext, Future } class AkkaHttpAdapter private (private val options: AkkaHttpServerOptions)(implicit ec: ExecutionContext) { + private implicit val monadErrorFuture: MonadError[Future] = new FutureMonad + private val akkaInterpreter = AkkaHttpServerInterpreter(options)(ec) def makeHttpService[R, E]( @@ -58,6 +61,18 @@ class AkkaHttpAdapter private (private val options: AkkaHttpServerOptions)(impli ) ) + /** + * Creates a route which serves the GraphiQL UI from CDN. + * + * @param apiPath The path at which the API can be introspected. + * + * @see [[https://github.com/graphql/graphiql/tree/main/examples/graphiql-cdn]] + */ + def makeGraphiqlService(apiPath: String): Route = + akkaInterpreter.toRoute( + HttpInterpreter.makeGraphiqlEndpoint[Future](apiPath) + ) + private implicit def streamConstructor(implicit runtime: Runtime[Any], mat: Materializer diff --git a/adapters/http4s/src/main/scala/caliban/Http4sAdapter.scala b/adapters/http4s/src/main/scala/caliban/Http4sAdapter.scala index f8a3b0158..e595fc76e 100644 --- a/adapters/http4s/src/main/scala/caliban/Http4sAdapter.scala +++ b/adapters/http4s/src/main/scala/caliban/Http4sAdapter.scala @@ -13,13 +13,14 @@ import sttp.capabilities.fs2.Fs2Streams import sttp.capabilities.zio.ZioStreams import sttp.tapir.Codec.JsonCodec import sttp.tapir.Endpoint +import sttp.tapir.integ.cats.effect.CatsMonadError import sttp.tapir.server.ServerEndpoint import sttp.tapir.server.http4s.Http4sServerInterpreter import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter import zio._ import zio.interop.catz.concurrentInstance -import zio.stream.interop.fs2z._ import zio.stream.ZStream +import zio.stream.interop.fs2z._ object Http4sAdapter { @@ -52,6 +53,20 @@ object Http4sAdapter { Http4sServerInterpreter().toRoutes(endpointF) } + /** + * Creates a route which serves the GraphiQL UI from CDN. + * + * @param apiPath The path at which the API can be introspected. + * + * @see [[https://github.com/graphql/graphiql/tree/main/examples/graphiql-cdn]] + */ + def makeGraphiqlService[F[_]: Async](apiPath: String): HttpRoutes[F] = { + implicit val F: CatsMonadError[F] = new CatsMonadError[F] + Http4sServerInterpreter().toRoutes( + HttpInterpreter.makeGraphiqlEndpoint[F](apiPath) + ) + } + def makeWebSocketService[R, R1 <: R, E]( builder: WebSocketBuilder2[RIO[R, *]], interpreter: WebSocketInterpreter[R1, E] diff --git a/adapters/pekko-http/src/main/scala/caliban/PekkoHttpAdapter.scala b/adapters/pekko-http/src/main/scala/caliban/PekkoHttpAdapter.scala index d0f2c519f..519db6f82 100644 --- a/adapters/pekko-http/src/main/scala/caliban/PekkoHttpAdapter.scala +++ b/adapters/pekko-http/src/main/scala/caliban/PekkoHttpAdapter.scala @@ -11,6 +11,7 @@ import sttp.capabilities.WebSockets import sttp.capabilities.pekko.PekkoStreams import sttp.capabilities.pekko.PekkoStreams.Pipe import sttp.model.StatusCode +import sttp.monad.{ FutureMonad, MonadError } import sttp.tapir.Codec.JsonCodec import sttp.tapir.PublicEndpoint import sttp.tapir.model.ServerRequest @@ -22,6 +23,8 @@ import zio.stream.ZStream import scala.concurrent.{ ExecutionContext, Future } class PekkoHttpAdapter private (val options: PekkoHttpServerOptions)(implicit ec: ExecutionContext) { + private implicit val monadErrorFuture: MonadError[Future] = new FutureMonad + private val pekkoInterpreter = PekkoHttpServerInterpreter(options)(ec) def makeHttpService[R, E]( @@ -58,6 +61,16 @@ class PekkoHttpAdapter private (val options: PekkoHttpServerOptions)(implicit ec ) ) + /** + * Creates a route which serves the GraphiQL UI from CDN. + * + * @param apiPath The path at which the API can be introspected. + * + * @see [[https://github.com/graphql/graphiql/tree/main/examples/graphiql-cdn]] + */ + def makeGraphiqlService(apiPath: String): Route = + pekkoInterpreter.toRoute(HttpInterpreter.makeGraphiqlEndpoint[Future](apiPath)) + private implicit def streamConstructor(implicit runtime: Runtime[Any], mat: Materializer diff --git a/adapters/play/src/main/scala/caliban/PlayAdapter.scala b/adapters/play/src/main/scala/caliban/PlayAdapter.scala index f2aaee6d9..ac6d18ab8 100644 --- a/adapters/play/src/main/scala/caliban/PlayAdapter.scala +++ b/adapters/play/src/main/scala/caliban/PlayAdapter.scala @@ -10,6 +10,7 @@ import sttp.capabilities.WebSockets import sttp.capabilities.pekko.PekkoStreams import sttp.capabilities.pekko.PekkoStreams.Pipe import sttp.model.StatusCode +import sttp.monad.FutureMonad import sttp.tapir.Codec.JsonCodec import sttp.tapir.PublicEndpoint import sttp.tapir.model.ServerRequest @@ -37,6 +38,18 @@ class PlayAdapter private (private val options: Option[PlayServerOptions]) { ): Routes = playInterpreter.toRoutes(interpreter.serverEndpointFuture[PekkoStreams](PekkoStreams)(runtime)) + /** + * Creates a route which serves the GraphiQL UI from CDN. + * + * @param apiPath The path at which the API can be introspected. + * + * @see [[https://github.com/graphql/graphiql/tree/main/examples/graphiql-cdn]] + */ + def makeGraphiqlService(apiPath: String)(implicit materializer: Materializer): Routes = { + implicit val F: FutureMonad = new FutureMonad()(materializer.executionContext) + playInterpreter.toRoutes(HttpInterpreter.makeGraphiqlEndpoint[Future](apiPath)) + } + def makeWebSocketService[R, E]( interpreter: WebSocketInterpreter[R, E] )(implicit runtime: Runtime[R], materializer: Materializer): Routes = { diff --git a/adapters/quick/src/main/scala/caliban/GraphiQLHandler.scala b/adapters/quick/src/main/scala/caliban/GraphiQLHandler.scala index 7b3c44053..cdee29dd8 100644 --- a/adapters/quick/src/main/scala/caliban/GraphiQLHandler.scala +++ b/adapters/quick/src/main/scala/caliban/GraphiQLHandler.scala @@ -11,10 +11,21 @@ object GraphiQLHandler { * Creates a handler which serves the GraphiQL UI from CDN. * * @param apiPath The path at which the API can be introspected. - * @param graphiqlPath The path at which the GraphiQL UI will be served. * * @see [[https://github.com/graphql/graphiql/tree/main/examples/graphiql-cdn]] */ + def handler(apiPath: String): RequestHandler[Any, Nothing] = { + val headers = Headers(Header.ContentType(MediaType.text.html).untyped) + zio.http.handler { (req: Request) => + Response( + Status.Ok, + headers, + Body.fromString(html(apiPath, req.path.encode)) + ) + } + } + + @deprecated("Use overloaded method without providing the graphiqlPath param", since = "2.8.2") def handler(apiPath: String, graphiqlPath: String): RequestHandler[Any, Nothing] = Response( Status.Ok, @@ -22,73 +33,5 @@ object GraphiQLHandler { Body.fromString(html(apiPath, graphiqlPath)) ).toHandler - def html(apiPath: String, uiPath: String): String = - s""" - | - | - | - | - | GraphiQL - | - | - | - | - | - | - | - | - | - | - | - |
Loading...
- | - | - | - |""".stripMargin - + def html(apiPath: String, uiPath: String): String = HttpUtils.graphiqlHtml(apiPath, uiPath) } diff --git a/adapters/quick/src/main/scala/caliban/QuickAdapter.scala b/adapters/quick/src/main/scala/caliban/QuickAdapter.scala index 64243e4c6..e9fe65b97 100644 --- a/adapters/quick/src/main/scala/caliban/QuickAdapter.scala +++ b/adapters/quick/src/main/scala/caliban/QuickAdapter.scala @@ -43,7 +43,7 @@ final class QuickAdapter[R] private (requestHandler: QuickRequestHandler[R]) { RoutePattern(Method.GET, apiPath) -> handlers.api ) val graphiqlRoute = graphiqlPath.toList.map { uiPath => - RoutePattern(Method.GET, uiPath) -> GraphiQLHandler.handler(apiPath, uiPath) + RoutePattern(Method.GET, uiPath) -> GraphiQLHandler.handler(apiPath) } val uploadRoute = uploadPath.toList.map { uPath => RoutePattern(Method.POST, uPath) -> handlers.upload diff --git a/core/src/main/scala/caliban/HttpUtils.scala b/core/src/main/scala/caliban/HttpUtils.scala index 9c19116c0..3f5723cbe 100644 --- a/core/src/main/scala/caliban/HttpUtils.scala +++ b/core/src/main/scala/caliban/HttpUtils.scala @@ -79,4 +79,65 @@ private[caliban] object HttpUtils { def serverSentEvents: Boolean = length >= 17 && header.contains("text/event-stream") } + + def graphiqlHtml(apiPath: String, uiPath: String): String = + s""" + | + | + | GraphiQL + | + | + | + | + | + | + | + | + | + | + | + |
Loading...
+ | + | + | + |""".stripMargin } diff --git a/examples/src/main/resources/graphiql.html b/examples/src/main/resources/graphiql.html deleted file mode 100644 index 7e35f08ca..000000000 --- a/examples/src/main/resources/graphiql.html +++ /dev/null @@ -1,156 +0,0 @@ - - - - - - - - - - - - - - - - - - - -
Loading...
- - - - diff --git a/examples/src/main/scala/example/akkahttp/AuthExampleApp.scala b/examples/src/main/scala/example/akkahttp/AuthExampleApp.scala index a155cd75e..b49ee9f33 100644 --- a/examples/src/main/scala/example/akkahttp/AuthExampleApp.scala +++ b/examples/src/main/scala/example/akkahttp/AuthExampleApp.scala @@ -51,7 +51,7 @@ object AuthExampleApp extends App { HttpInterpreter(interpreter).configure(Configurator.setEnableIntrospection(false)).intercept(auth) ) } ~ path("graphiql") { - getFromResource("graphiql.html") + adapter.makeGraphiqlService("/api/graphql") } val bindingFuture = Http().newServerAt("localhost", 8088).bind(route) diff --git a/examples/src/main/scala/example/akkahttp/ExampleApp.scala b/examples/src/main/scala/example/akkahttp/ExampleApp.scala index 61ce03b52..3c50d6c11 100644 --- a/examples/src/main/scala/example/akkahttp/ExampleApp.scala +++ b/examples/src/main/scala/example/akkahttp/ExampleApp.scala @@ -39,7 +39,7 @@ object ExampleApp extends App { } ~ path("ws" / "graphql") { adapter.makeWebSocketService(WebSocketInterpreter(interpreter)) } ~ path("graphiql") { - getFromResource("graphiql.html") + adapter.makeGraphiqlService("/api/graphql") } val bindingFuture = Http().newServerAt("localhost", 8088).bind(route) diff --git a/examples/src/main/scala/example/http4s/ExampleApp.scala b/examples/src/main/scala/example/http4s/ExampleApp.scala index 7b9bfedb2..d74057278 100644 --- a/examples/src/main/scala/example/http4s/ExampleApp.scala +++ b/examples/src/main/scala/example/http4s/ExampleApp.scala @@ -33,7 +33,7 @@ object ExampleApp extends ZIOAppDefault { "/ws/graphql" -> CORS.policy( Http4sAdapter.makeWebSocketService(wsBuilder, WebSocketInterpreter(interpreter)) ), - "/graphiql" -> Kleisli.liftF(StaticFile.fromResource("/graphiql.html", None)) + "/graphiql" -> Http4sAdapter.makeGraphiqlService("/api/graphql") ).orNotFound ) .build diff --git a/examples/src/main/scala/example/http4s/ExampleAppF.scala b/examples/src/main/scala/example/http4s/ExampleAppF.scala index 815335e74..2953b61f3 100644 --- a/examples/src/main/scala/example/http4s/ExampleAppF.scala +++ b/examples/src/main/scala/example/http4s/ExampleAppF.scala @@ -46,7 +46,7 @@ object ExampleAppF extends IOApp { .makeWebSocketServiceF[IO, Any, CalibanError](wsBuilder, WebSocketInterpreter(interpreter)) ), "/graphiql" -> - Kleisli.liftF(StaticFile.fromResource("/graphiql.html", None)) + Http4sAdapter.makeGraphiqlService("/api/graphql") ).orNotFound ) .build diff --git a/examples/src/main/scala/example/pekkohttp/AuthExampleApp.scala b/examples/src/main/scala/example/pekkohttp/AuthExampleApp.scala index 9ef022205..f903c7c2a 100644 --- a/examples/src/main/scala/example/pekkohttp/AuthExampleApp.scala +++ b/examples/src/main/scala/example/pekkohttp/AuthExampleApp.scala @@ -51,7 +51,7 @@ object AuthExampleApp extends App { HttpInterpreter(interpreter).configure(Configurator.setEnableIntrospection(false)).intercept(auth) ) } ~ path("graphiql") { - getFromResource("graphiql.html") + adapter.makeGraphiqlService("/api/graphql") } val bindingFuture = Http().newServerAt("localhost", 8088).bind(route) diff --git a/examples/src/main/scala/example/pekkohttp/ExampleApp.scala b/examples/src/main/scala/example/pekkohttp/ExampleApp.scala index 6bfeb4fa2..e69773152 100644 --- a/examples/src/main/scala/example/pekkohttp/ExampleApp.scala +++ b/examples/src/main/scala/example/pekkohttp/ExampleApp.scala @@ -39,7 +39,7 @@ object ExampleApp extends App { } ~ path("ws" / "graphql") { adapter.makeWebSocketService(WebSocketInterpreter(interpreter)) } ~ path("graphiql") { - getFromResource("graphiql.html") + adapter.makeGraphiqlService("/api/graphql") } val bindingFuture = Http().newServerAt("localhost", 8088).bind(route) diff --git a/examples/src/main/scala/example/play/ExampleApp.scala b/examples/src/main/scala/example/play/ExampleApp.scala index e7c4a6a50..526747e09 100644 --- a/examples/src/main/scala/example/play/ExampleApp.scala +++ b/examples/src/main/scala/example/play/ExampleApp.scala @@ -36,6 +36,8 @@ object ExampleApp extends ZIOAppDefault { PlayAdapter.makeHttpService(HttpInterpreter(interpreter)).apply(req) case req @ GET(p"/ws/graphql") => PlayAdapter.makeWebSocketService(WebSocketInterpreter(interpreter)).apply(req) + case req @ GET(p"/graphiql") => + PlayAdapter.makeGraphiqlService("/api/graphql").apply(req) }.routes } ) diff --git a/examples/src/main/scala/example/quick/AuthExampleApp.scala b/examples/src/main/scala/example/quick/AuthExampleApp.scala index 7cdf60161..b39b6350d 100644 --- a/examples/src/main/scala/example/quick/AuthExampleApp.scala +++ b/examples/src/main/scala/example/quick/AuthExampleApp.scala @@ -60,17 +60,16 @@ object AuthExampleApp extends ZIOAppDefault { def run = (for { - exampleApi <- ZIO.service[GraphQL[Any]] - handlers <- (exampleApi |+| Authed.api).handlers.map(_ @@ Auth.middleware) - graphiqlHandler = GraphiQLHandler.handler(apiPath = "/api/graphql", graphiqlPath = "/graphiql") - port <- Server.install( - Routes( - Method.POST / "api" / "graphql" -> handlers.api, - Method.GET / "graphiql" -> graphiqlHandler - ) - ) - _ <- ZIO.logInfo(s"Server started on port $port") - _ <- ZIO.never + exampleApi <- ZIO.service[GraphQL[Any]] + handlers <- (exampleApi |+| Authed.api).handlers.map(_ @@ Auth.middleware) + port <- Server.install( + Routes( + Method.POST / "api" / "graphql" -> handlers.api, + Method.GET / "graphiql" -> GraphiQLHandler.handler("/api/graphql") + ) + ) + _ <- ZIO.logInfo(s"Server started on port $port") + _ <- ZIO.never } yield ()) .provide( ExampleService.make(sampleCharacters), diff --git a/examples/src/main/scala/example/tapirtocaliban/ExampleApp.scala b/examples/src/main/scala/example/tapirtocaliban/ExampleApp.scala index 3973cf848..f83dc6b4c 100644 --- a/examples/src/main/scala/example/tapirtocaliban/ExampleApp.scala +++ b/examples/src/main/scala/example/tapirtocaliban/ExampleApp.scala @@ -53,7 +53,7 @@ object ExampleApp extends CatsApp { .withHttpApp( Router[MyTask]( "/api/graphql" -> CORS.policy(Http4sAdapter.makeHttpService(HttpInterpreter(interpreter))), - "/graphiql" -> Kleisli.liftF(StaticFile.fromResource("/graphiql.html", None)) + "/graphiql" -> Http4sAdapter.makeGraphiqlService("/api/graphql") ).orNotFound ) .build diff --git a/interop/tapir/src/main/scala/caliban/interop/tapir/HttpInterpreter.scala b/interop/tapir/src/main/scala/caliban/interop/tapir/HttpInterpreter.scala index ac8e6b7fc..5ec84984d 100644 --- a/interop/tapir/src/main/scala/caliban/interop/tapir/HttpInterpreter.scala +++ b/interop/tapir/src/main/scala/caliban/interop/tapir/HttpInterpreter.scala @@ -4,10 +4,11 @@ import caliban._ import caliban.interop.tapir.TapirAdapter._ import sttp.capabilities.Streams import sttp.model.{ headers => _, _ } +import sttp.monad.MonadError import sttp.shared.Identity import sttp.tapir.Codec.JsonCodec -import sttp.tapir.model.ServerRequest import sttp.tapir._ +import sttp.tapir.model.ServerRequest import sttp.tapir.server.ServerEndpoint import zio._ @@ -214,4 +215,26 @@ object HttpInterpreter { postEndpoint :: getEndpoint :: Nil } + + /** + * Creates an endpoint that serves the GraphiQL UI from CDN. + * + * @param apiPath The path at which the API can be introspected. + * + * @see [[https://github.com/graphql/graphiql/tree/main/examples/graphiql-cdn]] + */ + def makeGraphiqlEndpoint[F[_]]( + apiPath: String + )(implicit F: MonadError[F]): ServerEndpoint.Full[Unit, Unit, ServerRequest, Nothing, String, Any, F] = { + val apiPath0 = apiPath.split("/").filter(_.nonEmpty).mkString("/", "/", "") + infallibleEndpoint.get + .in(extractFromRequest(identity)) + .out(htmlBodyUtf8) + .serverLogic[F] { req => + val segments = req.pathSegments + val uiPath = segments.mkString("/", "/", "") + val entity = Right(HttpUtils.graphiqlHtml(apiPath = apiPath0, uiPath = uiPath)) + F.unit(entity) + } + } }