Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tidied up demo project #636

Merged
merged 1 commit into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 12 additions & 14 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ val catsEffectVersion = "3.5.4"
val circeVersion = "0.14.8"
val disciplineMunitVersion = "2.0.0-M3"
val doobieVersion = "1.0.0-RC5"
val flywayVersion = "10.15.0"
val fs2Version = "3.10.2"
val http4sVersion = "0.23.27"
val jnrUnixsocketVersion = "0.38.22"
Expand Down Expand Up @@ -252,24 +251,23 @@ lazy val generic = crossProject(JVMPlatform, JSPlatform, NativePlatform)
lazy val demo = project
.in(file("demo"))
.enablePlugins(NoPublishPlugin, AutomateHeaderPlugin)
.dependsOn(core.jvm, generic.jvm, doobie)
.dependsOn(buildInfo.jvm, core.jvm, generic.jvm, doobie)
.settings(commonSettings)
.settings(
name := "grackle-demo",
coverageEnabled := false,
libraryDependencies ++= Seq(
"org.typelevel" %% "log4cats-slf4j" % log4catsVersion,
"ch.qos.logback" % "logback-classic" % logbackVersion,
"org.tpolecat" %% "doobie-core" % doobieVersion,
"org.tpolecat" %% "doobie-postgres" % doobieVersion,
"org.tpolecat" %% "doobie-hikari" % doobieVersion,
"org.http4s" %% "http4s-ember-server" % http4sVersion,
"org.http4s" %% "http4s-ember-client" % http4sVersion,
"org.http4s" %% "http4s-circe" % http4sVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion,
"org.flywaydb" % "flyway-database-postgresql" % flywayVersion,
"io.chrisdavenport" %% "whale-tail-manager" % whaleTailVersion,
"com.github.jnr" % "jnr-unixsocket" % jnrUnixsocketVersion
"org.typelevel" %% "log4cats-slf4j" % log4catsVersion,
"ch.qos.logback" % "logback-classic" % logbackVersion,
"org.tpolecat" %% "doobie-core" % doobieVersion,
"org.tpolecat" %% "doobie-postgres" % doobieVersion,
"org.tpolecat" %% "doobie-hikari" % doobieVersion,
"org.http4s" %% "http4s-ember-server" % http4sVersion,
"org.http4s" %% "http4s-ember-client" % http4sVersion,
"org.http4s" %% "http4s-circe" % http4sVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion,
"io.chrisdavenport" %% "whale-tail-manager" % whaleTailVersion,
"com.github.jnr" % "jnr-unixsocket" % jnrUnixsocketVersion
)
)

Expand Down
2 changes: 1 addition & 1 deletion demo/src/main/scala/demo/DemoServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import org.http4s.server.staticcontent.resourceServiceBuilder

// #server
object DemoServer {
def resource(graphQLRoutes: HttpRoutes[IO]): Resource[IO, Unit] = {
def mkServer(graphQLRoutes: HttpRoutes[IO]): Resource[IO, Unit] = {
val httpApp0 = (
// Routes for static resources, i.e. GraphQL Playground
resourceServiceBuilder[IO]("/assets").toRoutes <+>
Expand Down
15 changes: 3 additions & 12 deletions demo/src/main/scala/demo/GraphQLService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,8 @@ import org.http4s.dsl.Http4sDsl
import org.http4s.{HttpRoutes, InvalidMessageBodyFailure, ParseFailure, QueryParamDecoder}

// #service
trait GraphQLService[F[_]] {
def runQuery(op: Option[String], vars: Option[Json], query: String): F[Json]
}

object GraphQLService {

def fromMapping[F[_]: Concurrent](mapping: Mapping[F]): GraphQLService[F] =
(op: Option[String], vars: Option[Json], query: String) =>
mapping.compileAndRun(query, op, vars)

def routes[F[_]: Concurrent](prefix: String, svc: GraphQLService[F]): HttpRoutes[F] = {
def mkRoutes[F[_]: Concurrent](prefix: String)(mapping: Mapping[F]): HttpRoutes[F] = {
val dsl = new Http4sDsl[F]{}
import dsl._

Expand All @@ -60,7 +51,7 @@ object GraphQLService {
errors => BadRequest(errors.map(_.sanitized).mkString_("", ",", "")),
vars =>
for {
result <- svc.runQuery(op, vars, query)
result <- mapping.compileAndRun(query, op, vars)
resp <- Ok(result)
} yield resp
)
Expand All @@ -77,7 +68,7 @@ object GraphQLService {
)
op = obj("operationName").flatMap(_.asString)
vars = obj("variables")
result <- svc.runQuery(op, vars, query)
result <- mapping.compileAndRun(query, op, vars)
resp <- Ok(result)
} yield resp
}
Expand Down
97 changes: 10 additions & 87 deletions demo/src/main/scala/demo/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,99 +15,22 @@

package demo

import java.util.concurrent.Executors

import scala.concurrent.ExecutionContext
import scala.concurrent.duration._

import cats.effect.{ExitCode, IO, IOApp, Resource}
import cats.effect.{ExitCode, IO, IOApp}
import cats.syntax.all._
import demo.starwars.{StarWarsData, StarWarsMapping}
import demo.starwars.StarWarsMapping
import demo.world.WorldMapping
import doobie.hikari.HikariTransactor
import io.chrisdavenport.whaletail.Docker
import io.chrisdavenport.whaletail.manager._
import org.flywaydb.core.Flyway

import GraphQLService.mkRoutes
import DemoServer.mkServer

// #main
object Main extends IOApp {
def run(args: List[String]): IO[ExitCode] = {
DBSetup.run { xa =>
val worldGraphQLRoutes = GraphQLService.routes(
"world",
GraphQLService.fromMapping(WorldMapping.mkMappingFromTransactor(xa))
)
val starWarsGraphQLRoutes = GraphQLService.routes[IO](
"starwars",
GraphQLService.fromMapping(new StarWarsMapping[IO] with StarWarsData[IO])
)
DemoServer.resource(worldGraphQLRoutes <+> starWarsGraphQLRoutes)
}
(for {
starWarsRoutes <- StarWarsMapping[IO].map(mkRoutes("starwars"))
worldRoutes <- WorldMapping[IO].map(mkRoutes("world"))
_ <- mkServer(starWarsRoutes <+> worldRoutes)
} yield ()).useForever
}
}
// #main

object DBSetup {
def run(body: HikariTransactor[IO] => Resource[IO, Unit]): IO[Nothing] =
container.evalTap(dbMigration(_)).flatMap(transactor(_)).flatMap(body).useForever

case class PostgresConnectionInfo(host: String, port: Int) {
val driverClassName = "org.postgresql.Driver"
val databaseName = "test"
val jdbcUrl = s"jdbc:postgresql://$host:$port/$databaseName"
val username = "test"
val password = "test"
}
object PostgresConnectionInfo {
val DefaultPort = 5432
}

val container: Resource[IO, PostgresConnectionInfo] = Docker.default[IO].flatMap(client =>
WhaleTailContainer.build(
client,
image = "postgres",
tag = "11.8".some,
ports = Map(PostgresConnectionInfo.DefaultPort -> None),
env = Map(
"POSTGRES_USER" -> "test",
"POSTGRES_PASSWORD" -> "test",
"POSTGRES_DB" -> "test"
),
labels = Map.empty
).evalTap(
ReadinessStrategy.checkReadiness(
client,
_,
ReadinessStrategy.LogRegex(".*database system is ready to accept connections.*".r, 2),
30.seconds
)
)
).flatMap(container =>
Resource.eval(
container.ports.get(PostgresConnectionInfo.DefaultPort).liftTo[IO](new Throwable("Missing Port"))
)
).map {
case (host, port) => PostgresConnectionInfo(host, port)
}

def transactor(connInfo: PostgresConnectionInfo): Resource[IO, HikariTransactor[IO]] = {
import connInfo._
HikariTransactor.newHikariTransactor[IO](
driverClassName,
jdbcUrl,
username,
password,
ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(4))
)
}

def dbMigration(connInfo: PostgresConnectionInfo): IO[Unit] = {
import connInfo._
IO.blocking {
val flyway = Flyway
.configure()
.dataSource(jdbcUrl, username, password)
flyway.load().migrate()
}.void
}
}
7 changes: 7 additions & 0 deletions demo/src/main/scala/demo/starwars/StarWarsMapping.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

package demo.starwars

import cats.MonadThrow
import cats.syntax.all._
import cats.effect.Resource
import grackle.Predicate._
import grackle.Query._
import grackle.QueryCompiler._
Expand Down Expand Up @@ -106,6 +108,11 @@ trait StarWarsMapping[F[_]] extends GenericMapping[F] { self: StarWarsData[F] =>
// #elaborator
}

object StarWarsMapping {
def apply[F[_]: MonadThrow]: Resource[F, StarWarsMapping[F]] =
Resource.pure(new StarWarsMapping[F] with StarWarsData[F])
}

// The types and values for the in-memory Star Wars example.
trait StarWarsData[F[_]] extends GenericMapping[F] { self: StarWarsMapping[F] =>
import semiauto._
Expand Down
85 changes: 85 additions & 0 deletions demo/src/main/scala/demo/world/WorldData.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
// Copyright (c) 2016-2023 Grackle Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package demo.world

import java.util.concurrent.Executors

import scala.concurrent.ExecutionContext
import scala.concurrent.duration._

import cats.effect.{Async, Resource}
import cats.syntax.all._
import doobie.hikari.HikariTransactor
import io.chrisdavenport.whaletail.{Containers, Docker}
import io.chrisdavenport.whaletail.manager._

object WorldData {
def mkContainer[F[_]: Async]: Resource[F, PostgresConnectionInfo] =
Docker.default[F].flatMap(client =>
WhaleTailContainer.build(
client,
image = "postgres",
tag = "11.8".some,
ports = Map(PostgresConnectionInfo.DefaultPort -> None),
binds = List(Containers.Bind(bindPath("demo/src/main/resources/db/"), "/docker-entrypoint-initdb.d/", "ro")),
env = Map(
"POSTGRES_USER" -> "test",
"POSTGRES_PASSWORD" -> "test",
"POSTGRES_DB" -> "test"
),
labels = Map.empty
).evalTap(
ReadinessStrategy.checkReadiness(
client,
_,
ReadinessStrategy.LogRegex(".*database system is ready to accept connections.*".r, 2),
30.seconds
)
)
).flatMap(container =>
Resource.eval(
container.ports.get(PostgresConnectionInfo.DefaultPort).liftTo[F](new Throwable("Missing Port"))
)
).map {
case (host, port) => PostgresConnectionInfo(host, port)
}

def mkTransactor[F[_]: Async](connInfo: PostgresConnectionInfo): Resource[F, HikariTransactor[F]] = {
import connInfo._
HikariTransactor.newHikariTransactor[F](
driverClassName,
jdbcUrl,
username,
password,
ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(4))
)
}

case class PostgresConnectionInfo(host: String, port: Int) {
val driverClassName = "org.postgresql.Driver"
val databaseName = "test"
val jdbcUrl = s"jdbc:postgresql://$host:$port/$databaseName"
val username = "test"
val password = "test"
}

object PostgresConnectionInfo {
val DefaultPort = 5432
}

def bindPath(path: String): String =
buildinfo.BuildInfo.baseDirectory + "/" + path
}
19 changes: 14 additions & 5 deletions demo/src/main/scala/demo/world/WorldMapping.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

package demo.world

import _root_.doobie.{Meta, Transactor}
import cats.effect.Sync
import cats.effect.{Async, Resource, Sync}
import doobie.{Meta, Transactor}
import grackle.Predicate._
import grackle.Query._
import grackle.QueryCompiler._
Expand All @@ -28,6 +28,8 @@ import grackle.syntax._
import org.typelevel.log4cats.Logger
import org.typelevel.log4cats.slf4j.Slf4jLogger

import WorldData._

trait WorldMapping[F[_]] extends DoobieMapping[F] {
// #db_tables
object country extends TableDef("country") {
Expand Down Expand Up @@ -278,8 +280,15 @@ object WorldMapping extends LoggedDoobieMappingCompanion {
def mkMapping[F[_]: Sync](transactor: Transactor[F], monitor: DoobieMonitor[F]): WorldMapping[F] =
new DoobieMapping(transactor, monitor) with WorldMapping[F]

def mkMappingFromTransactor[F[_]: Sync](transactor: Transactor[F]): Mapping[F] = {
implicit val logger: Logger[F] = Slf4jLogger.getLoggerFromName[F]("SqlQueryLogger")
mkMapping(transactor)
def mkMappingFromTransactor[F[_]: Sync](transactor: Transactor[F]): WorldMapping[F] = {
val logger: Logger[F] = Slf4jLogger.getLoggerFromName[F]("SqlQueryLogger")
val monitor: DoobieMonitor[F] = DoobieMonitor.loggerMonitor[F](logger)
mkMapping(transactor, monitor)
}

def apply[F[_]: Async]: Resource[F, WorldMapping[F]] =
for {
connInfo <- mkContainer[F]
transactor <- mkTransactor[F](connInfo)
} yield mkMappingFromTransactor[F](transactor)
}
4 changes: 2 additions & 2 deletions docs/tutorial/db-backed-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ println(grackle.docs.Output.snip("demo/src/main/scala/demo/world/WorldMapping.sc
To expose the GraphQL API via http4s we will use the `GraphQLService` and `DemoServer` from the [in-memory
example](in-memory-model.md#the-service).

The `run` method starts the dockerized PostgreSQL database, creates the database schema, writes initial data and
exposes the GraphQL API for both the in-memory and the db-backend models,
The `run` method starts the dockerized PostgreSQL database, and exposes the GraphQL API for both the in-memory and the
db-backend models,

```scala mdoc:passthrough
println(grackle.docs.Output.snip("demo/src/main/scala/demo/Main.scala", "#main"))
Expand Down
9 changes: 4 additions & 5 deletions docs/tutorial/in-memory-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,11 +337,10 @@ Finally we need to run all of this on top of http4s. Here we have a simple `IOAp
```scala
object Main extends IOApp {
def run(args: List[String]): IO[ExitCode] = {
val starWarsGraphQLRoutes = GraphQLService.routes[IO](
"starwars",
GraphQLService.fromMapping(new StarWarsMapping[IO] with StarWarsData[IO])
)
DemoServer.resource(starWarsGraphQLRoutes).useForever
(for {
starWarsRoutes <- StarWarsMapping[IO].map(mkRoutes("starwars"))
_ <- mkServer(starWarsRoutes)
} yield ()).useForever
}
}
```
Expand Down
Loading