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)
+ }
+ }
}