From cb9524eac7ebbf69a3db8c86cf6b347df8016d70 Mon Sep 17 00:00:00 2001 From: Rohan Sircar Date: Sat, 22 Aug 2020 16:18:11 +0530 Subject: [PATCH] Many changes Rewrote slick db service using monix task instead of future Added cats IO to controller to make use of above db service Added action methods that support effect wrappers other than future Added play-flyway to auto-manage migrations Moved migration files to appropriate directory as a result of above Added cors filter to work with separately hosted frontend Made logger log at debug --- app/Module.scala | 31 +++++- app/controllers/HomeController.scala | 25 +++-- app/controllers/MonixHomeController.scala | 34 +++++++ app/services/MyService.scala | 28 +++--- app/util/Schedulers.scala | 18 ++++ app/util/Util.scala | 58 +++++++++++ build.sbt | 32 +++--- conf/application.conf | 14 +++ conf/logback.xml | 26 ++--- conf/routes | 3 +- .../example/services/TaskLibraryService.scala | 20 ++++ .../{ => default}/V1__create_users_table.sql | 0 .../migration/{ => default}/V2__add_user.sql | 0 .../{ => default}/V3__create_cars_table.sql | 0 .../migration/{ => default}/V4__add_car.sql | 0 .../{ => default}/V5__authors_books_table.sql | 0 .../V6__insert_books_and_authors.sql | 0 .../db/migration/default/V7__test.sql | 0 .../slick/src/main/resources/application.conf | 3 +- .../user/slick/dbios/SlickLibraryDbio.scala | 12 +-- .../slick/services/SlickLibraryService.scala | 15 +-- .../services/TaskSlickLibraryService.scala | 97 +++++++++++++++++++ 22 files changed, 353 insertions(+), 63 deletions(-) create mode 100644 app/controllers/MonixHomeController.scala create mode 100644 app/util/Schedulers.scala create mode 100644 app/util/Util.scala create mode 100644 modules/api/src/main/scala/com/example/services/TaskLibraryService.scala rename modules/flyway/src/main/resources/db/migration/{ => default}/V1__create_users_table.sql (100%) rename modules/flyway/src/main/resources/db/migration/{ => default}/V2__add_user.sql (100%) rename modules/flyway/src/main/resources/db/migration/{ => default}/V3__create_cars_table.sql (100%) rename modules/flyway/src/main/resources/db/migration/{ => default}/V4__add_car.sql (100%) rename modules/flyway/src/main/resources/db/migration/{ => default}/V5__authors_books_table.sql (100%) rename modules/flyway/src/main/resources/db/migration/{ => default}/V6__insert_books_and_authors.sql (100%) create mode 100644 modules/flyway/src/main/resources/db/migration/default/V7__test.sql create mode 100644 modules/slick/src/main/scala/com/example/user/slick/services/TaskSlickLibraryService.scala diff --git a/app/Module.scala b/app/Module.scala index 51cf614..83f1a46 100644 --- a/app/Module.scala +++ b/app/Module.scala @@ -13,6 +13,11 @@ import com.example.user.CarDAO import com.example.Car.slick.SlickCarDAO import com.example.services.LibraryService import com.example.user.slick.services.SlickLibraryService +import com.example.services.TaskLibraryService +import com.example.user.slick.services.TaskSlickLibraryService +import monix.execution.Scheduler +import util.{Schedulers, SchedulersImpl} +import slick.jdbc.JdbcProfile /** * This module handles the bindings for the API to the Slick implementation. @@ -23,9 +28,13 @@ class Module(environment: Environment, configuration: Configuration) extends AbstractModule { override def configure(): Unit = { bind(classOf[Database]).toProvider(classOf[DatabaseProvider]) + bind(classOf[JdbcProfile]).toProvider(classOf[JdbcProfileProvider]) bind(classOf[UserDAO]).to(classOf[SlickUserDAO]) bind(classOf[CarDAO]).to(classOf[SlickCarDAO]) bind(classOf[LibraryService]).to(classOf[SlickLibraryService]) + bind(classOf[TaskLibraryService]).to(classOf[TaskSlickLibraryService]) + bind(classOf[Scheduler]).toProvider(classOf[SchedulerProvider]) + bind(classOf[Schedulers]).toProvider(classOf[AppSchedulersProvider]) bind(classOf[DBCloseHook]).asEagerSingleton() } } @@ -35,7 +44,27 @@ class DatabaseProvider @Inject() (config: Config) extends Provider[Database] { lazy val get = Database.forConfig("myapp.database", config) } -/** Closes database connections safely. Important on dev restart. */ +@Singleton +class SchedulerProvider() extends Provider[Scheduler] { + lazy val get = Scheduler.io() +} + +@Singleton +class AppSchedulersProvider() extends Provider[Schedulers] { + lazy val get = + new SchedulersImpl( + dbScheduler = Scheduler.io(), + cpuScheduler = Scheduler.global + ) +} + +@Singleton +class JdbcProfileProvider() extends Provider[JdbcProfile] { + lazy val get: JdbcProfile = _root_.slick.jdbc.H2Profile +} + +@Singleton class DBCloseHook @Inject() (db: Database, lifecycle: ApplicationLifecycle) { + lifecycle.addStopHook { () => Future.successful(db.close()) } } diff --git a/app/controllers/HomeController.scala b/app/controllers/HomeController.scala index f965d80..05aff42 100644 --- a/app/controllers/HomeController.scala +++ b/app/controllers/HomeController.scala @@ -9,21 +9,28 @@ import scala.concurrent.ExecutionContext import com.example.user.CarDAO import com.example.services.LibraryService import play.api.libs.json.Json +import com.example.services.TaskLibraryService + +case class UserForm(name: String) +object UserForm { + implicit val userFormFormat = Json.format[UserForm] +} @Singleton class HomeController @Inject() ( userDAO: UserDAO, carDAO: CarDAO, libraryService: LibraryService, + taskLibraryService: TaskLibraryService, cc: ControllerComponents )(implicit ec: ExecutionContext) extends AbstractController(cc) { - def index = Action.async { implicit request => + def index = Action.async { userDAO.all.map { users => Ok(views.html.index(users)) } } - def cars = Action.async { implicit request => + def cars = Action.async { carDAO.all.map { cars => Ok(views.html.cars(cars)) } } @@ -36,10 +43,16 @@ class HomeController @Inject() ( } yield (Ok(Json.toJson(maybeBook))) } - def authors(bookId: Long) = Action.async { implicit request => - libraryService.getAuthorsForBook(bookId).map( t => { - Ok(Json.toJson(t)) - }) + def authors(bookId: Long) = Action.async { + libraryService + .getAuthorsForBook(bookId) + .map(t => { + Ok(Json.toJson(t)) + }) + } + + def user = Action { + Ok(Json.toJson(UserForm("hello"))) } } diff --git a/app/controllers/MonixHomeController.scala b/app/controllers/MonixHomeController.scala new file mode 100644 index 0000000..1eeeb74 --- /dev/null +++ b/app/controllers/MonixHomeController.scala @@ -0,0 +1,34 @@ +package controllers + +import javax.inject.{Inject, Singleton} +import play.api.mvc._ +import play.api.libs.json.Json +import com.example.services.TaskLibraryService +import util.IOHttp._ +import com.typesafe.scalalogging.LazyLogging +import cats.effect.IO +import scala.concurrent.ExecutionContext +import util.Schedulers + +@Singleton +class MonixHomeController @Inject() ( + taskLibraryService: TaskLibraryService, + cc: ControllerComponents +)(schedulers: Schedulers, ec: ExecutionContext) + extends AbstractController(cc) + with LazyLogging { + + private implicit val defaultScheduler = schedulers.dbScheduler + + def authors(bookId: Long) = Action.asyncF { + for { + _ <- IO(logger.debug("Getting Authors")) + authors <- taskLibraryService.getAuthorsForBook(bookId).to[IO] + _ <- IO(logger.debug("Where am I now?")) + _ <- IO.shift(ec) + _ <- IO(logger.info("Got Authors")) + res <- IO.pure(Ok(Json.toJson(authors))) + } yield (res) + } + +} diff --git a/app/services/MyService.scala b/app/services/MyService.scala index 6b8a512..0dc9239 100644 --- a/app/services/MyService.scala +++ b/app/services/MyService.scala @@ -1,17 +1,17 @@ -package service +// package service -import javax.inject._ -import play.api.db.slick.DatabaseConfigProvider -import scala.concurrent.ExecutionContext -import play.api.db.slick.HasDatabaseConfigProvider -import slick.jdbc.JdbcProfile +// import javax.inject._ +// import play.api.db.slick.DatabaseConfigProvider +// import scala.concurrent.ExecutionContext +// import play.api.db.slick.HasDatabaseConfigProvider +// import slick.jdbc.JdbcProfile -@Singleton -class MyService @Inject() ( - protected val dbConfigProvider: DatabaseConfigProvider, - implicit val ec: ExecutionContext -) extends HasDatabaseConfigProvider[JdbcProfile] { +// @Singleton +// class MyService @Inject() ( +// protected val dbConfigProvider: DatabaseConfigProvider, +// implicit val ec: ExecutionContext +// ) extends HasDatabaseConfigProvider[JdbcProfile] { - import profile.api._ - db -} \ No newline at end of file +// import profile.api._ +// db +// } diff --git a/app/util/Schedulers.scala b/app/util/Schedulers.scala new file mode 100644 index 0000000..6ecae7a --- /dev/null +++ b/app/util/Schedulers.scala @@ -0,0 +1,18 @@ +package util + +import monix.execution.Scheduler + +/** + * Class containing various schedulers for offloading blocking tasks + * from play's default thread pool + * + * @param dbScheduler + * @param cpuScheduler + */ +trait Schedulers { + def dbScheduler: Scheduler + def cpuScheduler: Scheduler +} + +class SchedulersImpl(val dbScheduler: Scheduler, val cpuScheduler: Scheduler) + extends Schedulers diff --git a/app/util/Util.scala b/app/util/Util.scala new file mode 100644 index 0000000..eba5e37 --- /dev/null +++ b/app/util/Util.scala @@ -0,0 +1,58 @@ +package util + +import scala.concurrent.Future +import cats.effect.IO +import cats.implicits._ +import scala.util.Success +import scala.util.Failure +import cats.effect.Effect +import play.api.mvc._ +import scala.concurrent.ExecutionContext + +object IOHttp { + + /** + * Actions to convert an effect wrapper F such as cats IO + * or monix Task into a future implicitly + * + * @param ab + */ + implicit class ActionBuilderOps[+R[_], B](ab: ActionBuilder[R, B]) { + + import cats.effect.implicits._ + + /** + * Action to convert an effect wrapper F such as cats IO + * or monix Task into a future implicitly + * + * @param ab + */ + def asyncFR[F[_]: Effect](cb: R[B] => F[Result]): Action[B] = ab.async { + c => cb(c).toIO.unsafeToFuture() + } + + /** + * Action to convert an effect wrapper F such as cats IO + * or monix Task into a future implicitly + * + * @param ab + */ + def asyncF[F[_]: Effect](cb: => F[Result]): Action[AnyContent] = + ab.async { cb.toIO.unsafeToFuture() } + + } +} + +object RepoUtil { + def fromFuture[IOEffect[A], A]( + f: => Future[A] + )(implicit ec: ExecutionContext): IO[A] = + IO.delay(f) >>= (f => + IO.async[A] { cb => + f.onComplete { + case Success(a) => cb(Right(a)) + case Failure(ex) => cb(Left(ex)) + } + } + ) +} diff --git a/build.sbt b/build.sbt index 3f95e1e..daa47e7 100644 --- a/build.sbt +++ b/build.sbt @@ -20,9 +20,8 @@ resolvers in ThisBuild += Resolver.sonatypeRepo("snapshots") libraryDependencies in ThisBuild ++= Seq( "javax.inject" % "javax.inject" % "1", - // "joda-time" % "joda-time" % "2.10.2", - // "org.joda" % "joda-convert" % "2.2.1", - "com.google.inject" % "guice" % "4.2.3" + "com.google.inject" % "guice" % "4.2.3", + "org.flywaydb" %% "flyway-play" % "6.0.0" ) scalaVersion in ThisBuild := "2.13.1" @@ -35,13 +34,13 @@ scalacOptions in ThisBuild ++= Seq( "-Xlint", "-Ywarn-numeric-widen" ) -javacOptions in ThisBuild ++= Seq("-source", "1.8", "-target", "1.8") +javacOptions in ThisBuild ++= Seq("-source", "11", "-target", "11") lazy val flyway = (project in file("modules/flyway")) .enablePlugins(FlywayPlugin) .settings( libraryDependencies += "org.flywaydb" % "flyway-core" % FlywayVersion, - flywayLocations := Seq("classpath:db/migration"), + flywayLocations := Seq("classpath:conf/db/migration/default"), flywayUrl := databaseUrl, flywayUser := databaseUser, flywayPassword := databasePassword, @@ -50,8 +49,13 @@ lazy val flyway = (project in file("modules/flyway")) lazy val api = (project in file("modules/api")).settings( libraryDependencies ++= Seq( - "com.typesafe.play" %% "play-json" % "2.8.1", - ), + "com.typesafe.play" %% "play-json" % "2.8.1", + "org.typelevel" %% "cats-core" % "2.1.1", + "org.typelevel" %% "cats-effect" % "2.1.4", + "io.monix" %% "monix" % "3.2.2", + "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2", + "ch.qos.logback" % "logback-classic" % "1.2.3" + ) ) lazy val slick = (project in file("modules/slick")) @@ -62,8 +66,12 @@ lazy val slick = (project in file("modules/slick")) "com.typesafe.slick" %% "slick" % "3.3.2", "com.typesafe.slick" %% "slick-hikaricp" % "3.3.2", "io.bfil" %% "automapper" % "0.7.0", - "io.scalaland" %% "chimney" % "0.5.2" - // "com.github.tototoshi" %% "slick-joda-mapper" % "2.4.1" + "io.scalaland" %% "chimney" % "0.5.2", + "org.typelevel" %% "cats-core" % "2.1.1", + "org.typelevel" %% "cats-effect" % "2.1.4", + "io.monix" %% "monix" % "3.2.2", + "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2", + "ch.qos.logback" % "logback-classic" % "1.2.3" ), slickCodegenDatabaseUrl := databaseUrl, slickCodegenDatabaseUser := databaseUser, @@ -74,9 +82,6 @@ lazy val slick = (project in file("modules/slick")) slickCodegenExcludedTables := Seq("schema_version"), slickCodegenCodeGenerator := { (model: m.Model) => new SourceCodeGenerator(model) { - // override def code = - // "import com.github.tototoshi.slick.H2JodaSupport._\n" + "import org.joda.time.DateTime\n" + super.code - override def Table = new Table(_) { override def Column = new Column(_) { override def rawType = model.tpe match { @@ -104,7 +109,6 @@ lazy val root = (project in file(".")) "com.h2database" % "h2" % "1.4.199", ws % Test, "org.flywaydb" % "flyway-core" % FlywayVersion % Test, - "com.typesafe.play" %% "play-slick" % "5.0.0", "org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0" % Test, "com.typesafe.play" %% "play-json" % "2.8.1", "io.bfil" %% "automapper" % "0.7.0" @@ -113,4 +117,4 @@ lazy val root = (project in file(".")) ) .aggregate(slick) .dependsOn(slick) -// fork := true + .dependsOn(flyway) diff --git a/conf/application.conf b/conf/application.conf index 12e82d7..0705c64 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -5,4 +5,18 @@ myapp = { numThreads=20 maxConnections=20 } +} + +play.filters.enabled += "play.filters.cors.CORSFilter" +play.modules.enabled += "org.flywaydb.play.PlayModule" + +db.default.driver=${myapp.database.driver} +db.default.url=${myapp.database.url} +db.default.username=${myapp.database.user} +db.default.password=${myapp.database.password} + +play.filters.cors { + allowedHttpMethods = ["GET", "POST", "OPTION"] + allowedHttpHeaders = ["Accept"] + preflightMaxAge = 3 days } \ No newline at end of file diff --git a/conf/logback.xml b/conf/logback.xml index de7e962..6abc32e 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -1,6 +1,6 @@ - + ${application.home:-.}/logs/application.log @@ -11,29 +11,31 @@ - %coloredLevel %logger{15} - %message%n%xException{10} + %date{HH:mm:ss.SSS} %thread %coloredLevel %logger{15} - %message%n%xException{10} - + - + - - + + - - - + + + - - - + + + + + diff --git a/conf/routes b/conf/routes index 2f57c98..8b7b1d8 100644 --- a/conf/routes +++ b/conf/routes @@ -6,7 +6,8 @@ GET / controllers.HomeController.index GET /cars controllers.HomeController.cars GET /book controllers.HomeController.book -GET /authors/:bookId controllers.HomeController.authors(bookId: Long) +GET /authors/:bookId controllers.MonixHomeController.authors(bookId: Long) +GET /user controllers.HomeController.user # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) diff --git a/modules/api/src/main/scala/com/example/services/TaskLibraryService.scala b/modules/api/src/main/scala/com/example/services/TaskLibraryService.scala new file mode 100644 index 0000000..a8d73f5 --- /dev/null +++ b/modules/api/src/main/scala/com/example/services/TaskLibraryService.scala @@ -0,0 +1,20 @@ +package com.example.services + +import monix.eval.Task +import com.example.models.Book +import com.example.models.Author +import com.example.models.NewAuthor +import com.example.models.NewBook + +trait TaskLibraryService { + // Simple function that returns a book + def findBookById(id: Long): Task[Option[Book]] + + // Simple function that returns a list of books with it's author + def findBooksWithAuthor: Task[Seq[(Book, Author)]] + + // Insert a book and an author composing two DBIOs in a transaction + def insertBookAndAuthor(book: NewBook, author: NewAuthor): Task[(Long, Long)] + + def getAuthorsForBook(id: Long): Task[Seq[(Author, Book)]] +} diff --git a/modules/flyway/src/main/resources/db/migration/V1__create_users_table.sql b/modules/flyway/src/main/resources/db/migration/default/V1__create_users_table.sql similarity index 100% rename from modules/flyway/src/main/resources/db/migration/V1__create_users_table.sql rename to modules/flyway/src/main/resources/db/migration/default/V1__create_users_table.sql diff --git a/modules/flyway/src/main/resources/db/migration/V2__add_user.sql b/modules/flyway/src/main/resources/db/migration/default/V2__add_user.sql similarity index 100% rename from modules/flyway/src/main/resources/db/migration/V2__add_user.sql rename to modules/flyway/src/main/resources/db/migration/default/V2__add_user.sql diff --git a/modules/flyway/src/main/resources/db/migration/V3__create_cars_table.sql b/modules/flyway/src/main/resources/db/migration/default/V3__create_cars_table.sql similarity index 100% rename from modules/flyway/src/main/resources/db/migration/V3__create_cars_table.sql rename to modules/flyway/src/main/resources/db/migration/default/V3__create_cars_table.sql diff --git a/modules/flyway/src/main/resources/db/migration/V4__add_car.sql b/modules/flyway/src/main/resources/db/migration/default/V4__add_car.sql similarity index 100% rename from modules/flyway/src/main/resources/db/migration/V4__add_car.sql rename to modules/flyway/src/main/resources/db/migration/default/V4__add_car.sql diff --git a/modules/flyway/src/main/resources/db/migration/V5__authors_books_table.sql b/modules/flyway/src/main/resources/db/migration/default/V5__authors_books_table.sql similarity index 100% rename from modules/flyway/src/main/resources/db/migration/V5__authors_books_table.sql rename to modules/flyway/src/main/resources/db/migration/default/V5__authors_books_table.sql diff --git a/modules/flyway/src/main/resources/db/migration/V6__insert_books_and_authors.sql b/modules/flyway/src/main/resources/db/migration/default/V6__insert_books_and_authors.sql similarity index 100% rename from modules/flyway/src/main/resources/db/migration/V6__insert_books_and_authors.sql rename to modules/flyway/src/main/resources/db/migration/default/V6__insert_books_and_authors.sql diff --git a/modules/flyway/src/main/resources/db/migration/default/V7__test.sql b/modules/flyway/src/main/resources/db/migration/default/V7__test.sql new file mode 100644 index 0000000..e69de29 diff --git a/modules/slick/src/main/resources/application.conf b/modules/slick/src/main/resources/application.conf index 2f644fe..e6d284b 100644 --- a/modules/slick/src/main/resources/application.conf +++ b/modules/slick/src/main/resources/application.conf @@ -23,7 +23,8 @@ myapp = { connectionTimeout = 5000 validationTimeout = 5000 - connectionPool=disabled + connectionPool = disabled + keepAlive = true } } diff --git a/modules/slick/src/main/scala/com/example/user/slick/dbios/SlickLibraryDbio.scala b/modules/slick/src/main/scala/com/example/user/slick/dbios/SlickLibraryDbio.scala index e66f64d..57cc29e 100644 --- a/modules/slick/src/main/scala/com/example/user/slick/dbios/SlickLibraryDbio.scala +++ b/modules/slick/src/main/scala/com/example/user/slick/dbios/SlickLibraryDbio.scala @@ -5,13 +5,11 @@ import com.example.models._ import io.scalaland.chimney.dsl._ import com.example.user.slick.Tables import javax.inject.Singleton -// import slick.jdbc.H2Profile.api._ -// import scala.concurrent.ExecutionContext +import javax.inject._ @Singleton -class SlickLibraryDbio extends Tables { - - override val profile: JdbcProfile = _root_.slick.jdbc.H2Profile +class SlickLibraryDbio @Inject() (override val profile: JdbcProfile) + extends Tables { import profile.api._ @@ -110,12 +108,12 @@ class SlickLibraryDbio extends Tables { } } yield (author, book) - lazy val authorOfBook4 = (bookId: Long) => + lazy val authorOfBook4 = (bookId: Long) => for { book <- Books author <- book.authorsFk } yield (author, book) - + } case class BookWithoutId(title: String, authorId: Long) // def test() = { diff --git a/modules/slick/src/main/scala/com/example/user/slick/services/SlickLibraryService.scala b/modules/slick/src/main/scala/com/example/user/slick/services/SlickLibraryService.scala index e7a62d4..98f9e27 100644 --- a/modules/slick/src/main/scala/com/example/user/slick/services/SlickLibraryService.scala +++ b/modules/slick/src/main/scala/com/example/user/slick/services/SlickLibraryService.scala @@ -16,18 +16,19 @@ class SlickLibraryService @Inject() ( libraryDbio: SlickLibraryDbio )(implicit ec: ExecutionContext) extends LibraryService { + import libraryDbio.profile.api._ - override def getAuthorsForBook(id: Long): Future[Seq[(Author, Book)]] = { - db.run{ - libraryDbio.getAuthorsForBook(id).map(_.map { + override def getAuthorsForBook(id: Long): Future[Seq[(Author, Book)]] = { + db.run { + libraryDbio + .getAuthorsForBook(id) + .map(_.map { case (x, y) => { - ( libraryDbio.authorsRowToAuthor(x), libraryDbio.booksRowToBooks(y)) + (libraryDbio.authorsRowToAuthor(x), libraryDbio.booksRowToBooks(y)) } }) - } } - - import libraryDbio.profile.api._ + } // Simple function that returns a book def findBookById(id: Long): Future[Option[Book]] = diff --git a/modules/slick/src/main/scala/com/example/user/slick/services/TaskSlickLibraryService.scala b/modules/slick/src/main/scala/com/example/user/slick/services/TaskSlickLibraryService.scala new file mode 100644 index 0000000..1c3693b --- /dev/null +++ b/modules/slick/src/main/scala/com/example/user/slick/services/TaskSlickLibraryService.scala @@ -0,0 +1,97 @@ +package com.example.user.slick.services + +import javax.inject._ +// import slick.jdbc.JdbcProfile +import slick.jdbc.JdbcBackend.Database +import com.example.models._ +import com.example.user.slick.dbios.SlickLibraryDbio +import monix.eval.Task +import com.example.services.TaskLibraryService + +@Singleton +class TaskSlickLibraryService @Inject() ( + db: Database, + libraryDbio: SlickLibraryDbio +) extends TaskLibraryService { + import libraryDbio.profile.api._ + + override def findBookById(id: Long): Task[Option[Book]] = + Task + .deferFuture { + db.run( + libraryDbio.findBookById(id) + ) + } + .flatMap(record => Task.eval(record.map(libraryDbio.booksRowToBooks))) + + override def findBooksWithAuthor: Task[Seq[(Book, Author)]] = + Task.deferFuture(db.run(libraryDbio.findBooksWithAuthor)).flatMap { + records => + Task.traverse(records) { + case (x, y) => + Task.eval { + ( + libraryDbio.booksRowToBooks(x), + libraryDbio.authorsRowToAuthor(y) + ) + } + } + } + + // override def storeMulti(aggregates: Seq[AggregateType]): Task[Long] = + // for { + // records <- Task.traverse(aggregates) { aggregate => + // convertToRecord(aggregate) + // } + // result <- Task.deferFutureAction { implicit ec => + // import profile.api._ + // db.run(DBIO.sequence(records.foldLeft(Seq.empty[DBIO[Long]]) { + // case (result, record) => + // result :+ dao.insertOrUpdate(record).map(_.toLong) + // })) + // .map(_.sum) + // } + // } yield result + + override def insertBookAndAuthor( + book: NewBook, + author: NewAuthor + ): Task[(Long, Long)] = + Task.deferFutureAction { implicit scheduler => + val action = for { + authorId <- libraryDbio.insertAuthor2(author) + bookId <- libraryDbio.insertBook2(book.copy(authorId = authorId)) + } yield (bookId, authorId) + + db.run(action.transactionally) + } + + override def getAuthorsForBook(id: Long): Task[Seq[(Author, Book)]] = + for { + records <- Task.deferFuture(db.run(libraryDbio.getAuthorsForBook((id)))) + result <- Task.traverse(records) { + case (authorsRow, booksRow) => + Task.eval( + ( + libraryDbio.authorsRowToAuthor(authorsRow), + libraryDbio.booksRowToBooks(booksRow) + ) + ) + } + } yield (result) + + // Task.deferFutureAction { implicit scheduler => + // db.run( + // libraryDbio + // .getAuthorsForBook(id) + // .map(_.map { + // case (x, y) => { + // ( + // libraryDbio.authorsRowToAuthor(x), + // libraryDbio.booksRowToBooks(y) + // ) + // } + // }) + // ) + // } +}