You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

288 lines
8.6 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. package wow.doge.http4sdemo.services
  2. import io.circe.generic.semiauto._
  3. import monix.bio.IO
  4. import monix.bio.Task
  5. import monix.bio.UIO
  6. import monix.reactive.Observable
  7. import org.http4s.dsl.Http4sDsl
  8. import slick.jdbc.JdbcBackend
  9. import slick.jdbc.JdbcProfile
  10. import wow.doge.http4sdemo.dto.Author
  11. import wow.doge.http4sdemo.dto.Book
  12. import wow.doge.http4sdemo.dto.BookSearchMode
  13. import wow.doge.http4sdemo.dto.BookSearchMode.AuthorName
  14. import wow.doge.http4sdemo.dto.BookSearchMode.BookTitle
  15. import wow.doge.http4sdemo.dto.BookUpdate
  16. import wow.doge.http4sdemo.dto.NewAuthor
  17. import wow.doge.http4sdemo.dto.NewBook
  18. import wow.doge.http4sdemo.implicits._
  19. import wow.doge.http4sdemo.slickcodegen.Tables
  20. object LibraryService {
  21. sealed trait Error extends Exception {
  22. def message: String
  23. override def getMessage(): String = message
  24. def toResponse = {
  25. val dsl = Http4sDsl[Task]
  26. import org.http4s.circe.CirceEntityCodec._
  27. import dsl._
  28. implicit val codec = Error.codec
  29. this match {
  30. case e @ LibraryService.EntityDoesNotExist(message) =>
  31. NotFound(e: LibraryService.Error).hideErrors
  32. case e @ LibraryService.EntityAlreadyExists(message) =>
  33. BadRequest(e: LibraryService.Error).hideErrors
  34. }
  35. }
  36. }
  37. final case class EntityDoesNotExist(message: String) extends Error
  38. final case class EntityAlreadyExists(message: String) extends Error
  39. // final case class MessageBodyError(cause: MessageBodyFailure) extends Error
  40. // final case class MyError2(message: String) extends Error
  41. // case object C3 extends Error { val message: String = "C3" }
  42. object Error {
  43. implicit val codec = deriveCodec[Error]
  44. // def convert(e: MessageBodyFailure) = e match {
  45. // case InvalidMessageBodyFailure(details, cause) => ()
  46. // case MalformedMessageBodyFailure(details, cause) => ()
  47. // }
  48. }
  49. }
  50. trait LibraryService {
  51. import LibraryService._
  52. def getBooks: Observable[Book]
  53. def getBookById(id: Int): Task[Option[Book]]
  54. def searchBook(mode: BookSearchMode, value: String): Observable[Book]
  55. def updateBook(id: Int, updateData: BookUpdate): IO[Error, Unit]
  56. def deleteBook(id: Int): Task[Int]
  57. def insertBook(newBook: NewBook): IO[Error, Book]
  58. def insertAuthor(a: NewAuthor): Task[Int]
  59. def booksForAuthor(authorId: Int): Observable[Book]
  60. }
  61. class LibraryServiceImpl(
  62. profile: JdbcProfile,
  63. dbio: LibraryDbio,
  64. db: JdbcBackend.DatabaseDef
  65. ) extends LibraryService {
  66. import profile.api._
  67. import LibraryService._
  68. def getBooks = db.streamO(dbio.getBooks.transactionally)
  69. def getBookById(id: Int) = db.runL(dbio.getBookById(id))
  70. def searchBook(mode: BookSearchMode, value: String): Observable[Book] =
  71. mode match {
  72. case BookTitle =>
  73. db.streamO(dbio.getBooksByTitle(value))
  74. case AuthorName =>
  75. Observable
  76. .fromTask((for {
  77. author <- db.runL(dbio.getAuthorByName(value)).flatMap {
  78. case None =>
  79. IO.raiseError(
  80. new EntityDoesNotExist(
  81. s"Author with name=$value does not exist"
  82. )
  83. )
  84. case Some(value) => IO.pure(value)
  85. }
  86. books = db
  87. .streamO(dbio.getBooksForAuthor(author.authorId))
  88. .map(Book.fromBooksRow)
  89. } yield books).toTask)
  90. .flatten
  91. }
  92. def insertAuthor(a: NewAuthor): Task[Int] = db.runL(dbio.insertAuthor(a))
  93. def updateBook(id: Int, updateData: BookUpdate): IO[Error, Unit] =
  94. for {
  95. action <- UIO.deferAction(implicit s =>
  96. UIO(for {
  97. mbRow <- Tables.Books.filter(_.bookId === id).result.headOption
  98. updatedRow <- mbRow match {
  99. case Some(value) =>
  100. println(s"Original value -> $value")
  101. println(s"Value to be updated with -> $updateData")
  102. DBIO.successful(updateData.update(value))
  103. case None =>
  104. DBIO.failed(
  105. EntityDoesNotExist(s"Book with id=$id does not exist")
  106. )
  107. }
  108. updateAction = Tables.Books.filter(_.bookId === id).update(updatedRow)
  109. _ = println(s"SQL = ${updateAction.statements}")
  110. _ <- updateAction
  111. } yield ())
  112. )
  113. _ <- db
  114. .runTryL(action.transactionally.asTry)
  115. .mapErrorPartial { case e: Error =>
  116. e
  117. }
  118. } yield ()
  119. def deleteBook(id: Int) = db.runL(dbio.deleteBook(id))
  120. def insertBook(newBook: NewBook): IO[Error, Book] =
  121. IO.deferAction { implicit s =>
  122. for {
  123. action <- UIO(for {
  124. _ <- Tables.Books
  125. .filter(_.isbn === newBook.isbn)
  126. .map(Book.fromBooksTableFn)
  127. .result
  128. .headOption
  129. .flatMap {
  130. case None => DBIO.successful(())
  131. case Some(_) =>
  132. DBIO.failed(
  133. EntityAlreadyExists(
  134. s"Book with isbn=${newBook.isbn} already exists"
  135. )
  136. )
  137. }
  138. _ <- dbio.getAuthor(newBook.authorId).flatMap {
  139. case None =>
  140. DBIO.failed(
  141. EntityDoesNotExist(
  142. s"Author with id=${newBook.authorId} does not exist"
  143. )
  144. )
  145. case Some(_) => DBIO.successful(())
  146. }
  147. book <- dbio.insertBookAndGetBook(newBook)
  148. } yield book)
  149. book <- db
  150. .runTryL(action.transactionally.asTry)
  151. .mapErrorPartial { case e: Error =>
  152. e
  153. }
  154. } yield book
  155. }
  156. def booksForAuthor(authorId: Int) =
  157. db.streamO(dbio.getBooksForAuthor(authorId).transactionally)
  158. .map(Book.fromBooksRow)
  159. }
  160. class LibraryDbio(val profile: JdbcProfile) {
  161. import profile.api._
  162. /* */
  163. def getBooks: StreamingDBIO[Seq[Book], Book] = Query.getBooksInner.result
  164. def insertBookAndGetId(newBook: NewBook): DBIO[Int] =
  165. Query.insertBookGetId += newBook
  166. def insertBookAndGetBook(newBook: NewBook): DBIO[Book] =
  167. Query.insertBookGetBook += newBook
  168. def insertAuthor(newAuthor: NewAuthor): DBIO[Int] =
  169. Query.insertAuthorGetId += newAuthor
  170. def getAuthor(id: Int): DBIO[Option[Author]] =
  171. Query.selectAuthor(id).map(Author.fromAuthorsTableFn).result.headOption
  172. def getAuthorByName(name: String): DBIO[Option[Author]] =
  173. Tables.Authors
  174. .filter(_.authorName === name)
  175. .map(Author.fromAuthorsTableFn)
  176. .result
  177. .headOption
  178. def deleteBook(id: Int): DBIO[Int] = Query.selectBookById(id).delete
  179. def getBookById(id: Int): DBIO[Option[Book]] = Query
  180. .selectBookById(id)
  181. .map(Book.fromBooksTableFn)
  182. .result
  183. .headOption
  184. def getBooksByTitle(title: String): StreamingDBIO[Seq[Book], Book] =
  185. Tables.Books.filter(_.bookTitle === title).map(Book.fromBooksTableFn).result
  186. def getBooksForAuthor(
  187. authorId: Int
  188. ): StreamingDBIO[Seq[Tables.BooksRow], Tables.BooksRow] =
  189. Query.booksForAuthorInner(authorId).result
  190. private object Query {
  191. val getBooksInner = Book.fromBooksTable
  192. val insertBookGetId =
  193. NewBook.fromBooksTable.returning(Tables.Books.map(_.bookId))
  194. val insertBookGetBook = NewBook.fromBooksTable.returning(getBooksInner)
  195. val insertAuthorGetId =
  196. Tables.Authors
  197. .map(a => (a.authorName).mapTo[NewAuthor])
  198. .returning(Tables.Authors.map(_.authorId))
  199. // val insertAuthor = NewAuthor.fromAuthorsTable
  200. def booksForAuthorInner(authorId: Int) = for {
  201. b <- Tables.Books
  202. a <- Tables.Authors
  203. if b.authorId === a.authorId && b.authorId === authorId
  204. } yield b
  205. def selectAuthor(authorId: Int) =
  206. Tables.Authors.filter(_.authorId === authorId)
  207. def selectBookById(id: Int) = Tables.Books.filter(_.bookId === id)
  208. def selectBookByIsbn(isbn: String) = Tables.Books.filter(_.isbn === isbn)
  209. }
  210. }
  211. trait NoopLibraryService extends LibraryService {
  212. def getBooks: Observable[Book] =
  213. Observable.raiseError(new NotImplementedError)
  214. def getBookById(id: Int): Task[Option[Book]] =
  215. IO.terminate(new NotImplementedError)
  216. def searchBook(
  217. mode: BookSearchMode,
  218. value: String
  219. ): Observable[Book] = Observable.raiseError(new NotImplementedError)
  220. def updateBook(
  221. id: Int,
  222. updateData: BookUpdate
  223. ): IO[LibraryService.Error, Unit] = IO.terminate(new NotImplementedError)
  224. def deleteBook(id: Int): Task[Int] = IO.terminate(new NotImplementedError)
  225. def insertBook(newBook: NewBook): IO[LibraryService.Error, Book] =
  226. IO.terminate(new NotImplementedError)
  227. def insertAuthor(a: NewAuthor): Task[Int] =
  228. IO.terminate(new NotImplementedError)
  229. def booksForAuthor(authorId: Int): Observable[Book] =
  230. Observable.raiseError(new NotImplementedError)
  231. }