From 39dad9148aed19df58fc417378a96acd16cd70d2 Mon Sep 17 00:00:00 2001 From: donut Date: Thu, 6 Apr 2023 14:06:33 +0200 Subject: [PATCH 1/2] Changed usage of SQliteZioJdbcContext[SnakeCase] to jdbczio.Quill.Sqlite[SnakeCase]. Change motivated by https://zio.dev/zio-quill/getting-started and https://zio.dev/zio-quill/getting-started. --- .../articles/ArticlesRepository.scala | 25 ++++++------------- .../com/softwaremill/realworld/db/Db.scala | 12 +++------ .../profiles/ProfilesRepository.scala | 15 ++++++----- .../realworld/users/UsersRepository.scala | 18 +++++-------- .../realworld/utils/TestUtils.scala | 7 +++--- 5 files changed, 29 insertions(+), 48 deletions(-) diff --git a/src/main/scala/com/softwaremill/realworld/articles/ArticlesRepository.scala b/src/main/scala/com/softwaremill/realworld/articles/ArticlesRepository.scala index 221a7f5b92..210a012a3b 100644 --- a/src/main/scala/com/softwaremill/realworld/articles/ArticlesRepository.scala +++ b/src/main/scala/com/softwaremill/realworld/articles/ArticlesRepository.scala @@ -8,6 +8,7 @@ import com.softwaremill.realworld.common.{Exceptions, Pagination} import com.softwaremill.realworld.profiles.ProfileRow import com.softwaremill.realworld.users.UserRow import io.getquill.* +import io.getquill.jdbczio.* import org.sqlite.{SQLiteErrorCode, SQLiteException} import zio.{Console, IO, Task, UIO, ZIO, ZLayer} @@ -16,10 +17,7 @@ import java.time.Instant import javax.sql.DataSource import scala.collection.immutable -class ArticlesRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: DataSource): - - private val dsLayer: ZLayer[Any, Nothing, DataSource] = ZLayer.succeed(dataSource) - +class ArticlesRepository(quill: Quill.Sqlite[SnakeCase]): import quill.* private inline def queryArticle = quote(querySchema[ArticleRow](entity = "articles")) @@ -59,7 +57,6 @@ class ArticlesRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: Dat pr <- queryProfile if ar.authorId == pr.userId } yield (ar, pr, tr.map(_._2), fr.map(_._2))) .map(_.map(article)) - .provide(dsLayer) } def findBySlug(slug: String): IO[SQLException, Option[ArticleData]] = @@ -75,7 +72,6 @@ class ArticlesRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: Dat } yield (ar, pr, tr.map(_._2), fr.map(_._2), false)) .map(_.headOption) .map(_.map(mapToArticleData)) - .provide(dsLayer) def findBySlugAsSeenBy(slug: String, viewerEmail: String): IO[SQLException, Option[ArticleData]] = run(for { @@ -95,7 +91,6 @@ class ArticlesRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: Dat } yield (ar, pr, tr.map(_._2), fr.map(_._2), isFavorite)) .map(_.headOption) .map(_.map(mapToArticleData)) - .provide(dsLayer) def addTag(tag: String, slug: String): IO[Exception, Unit] = run( queryTagArticle @@ -104,7 +99,6 @@ class ArticlesRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: Dat _.articleSlug -> lift(slug) ) ).unit - .provide(dsLayer) def add(article: ArticleRow): Task[Unit] = run(queryArticle.insertValue(lift(article))).unit @@ -113,7 +107,6 @@ class ArticlesRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: Dat Exceptions.AlreadyInUse("Article name already exists") case e => e } - .provide(dsLayer) def updateBySlug(updateData: ArticleData, slug: String): IO[Exception, Unit] = run( queryArticle @@ -130,15 +123,14 @@ class ArticlesRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: Dat Exceptions.AlreadyInUse("Article name already exists") case e => e } - .provide(dsLayer) def makeFavorite(slug: String, userId: Int) = run( queryFavoriteArticle.insertValue(lift(ArticleFavoriteRow(userId, slug))).onConflictIgnore - ).unit.provide(dsLayer) + ).unit def removeFavorite(slug: String, userId: Int) = run( queryFavoriteArticle.filter(a => (a.profileId == lift(userId)) && (a.articleSlug == lift(slug))).delete - ).provide(dsLayer) + ) def addComment(slug: String, authorId: Int, comment: String) = { val now = Instant.now() @@ -152,15 +144,14 @@ class ArticlesRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: Dat _.body -> lift(comment) ) .returningGenerated(_.commentId) - }.provide(dsLayer) + } } def findComment(commentId: Int) = run(queryCommentArticle.filter(_.commentId == lift(commentId))) .map(_.headOption) - .provide(dsLayer) .someOrFail(Exceptions.NotFound(s"Comment with ID=$commentId doesn't exist")) - def deleteComment(commentId: Int) = run(queryCommentArticle.filter(_.commentId == lift(commentId)).delete).provide(dsLayer) + def deleteComment(commentId: Int) = run(queryCommentArticle.filter(_.commentId == lift(commentId)).delete) private def article(tuple: (ArticleRow, ProfileRow, Option[String], Option[Int])): ArticleData = { val (ar, pr, tags, favorites) = tuple @@ -199,5 +190,5 @@ class ArticlesRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: Dat object ArticlesRepository: - val live: ZLayer[SqliteZioJdbcContext[SnakeCase] with DataSource, Nothing, ArticlesRepository] = - ZLayer.fromFunction(new ArticlesRepository(_, _)) + val live: ZLayer[Quill.Sqlite[SnakeCase], Nothing, ArticlesRepository] = + ZLayer.fromFunction(new ArticlesRepository(_)) diff --git a/src/main/scala/com/softwaremill/realworld/db/Db.scala b/src/main/scala/com/softwaremill/realworld/db/Db.scala index 5cd77aeff7..67f461c4ca 100644 --- a/src/main/scala/com/softwaremill/realworld/db/Db.scala +++ b/src/main/scala/com/softwaremill/realworld/db/Db.scala @@ -1,10 +1,10 @@ package com.softwaremill.realworld.db import com.zaxxer.hikari.{HikariConfig, HikariDataSource} -import io.getquill.{SnakeCase, SqliteZioJdbcContext} +import io.getquill.* +import io.getquill.jdbczio.* import zio.{ZIO, ZLayer} -import java.io.Closeable import javax.sql.DataSource object Db: @@ -28,9 +28,5 @@ object Db: } // Quill framework object used for specifying sql queries. - val quillLive: ZLayer[Any, Nothing, SqliteZioJdbcContext[SnakeCase]] = - ZLayer.scoped { - ZIO.fromAutoCloseable { - ZIO.succeed(new SqliteZioJdbcContext(SnakeCase)) - } - } + val quillLive: ZLayer[DataSource, Nothing, Quill.Sqlite[SnakeCase]] = + Quill.Sqlite.fromNamingStrategy(SnakeCase) diff --git a/src/main/scala/com/softwaremill/realworld/profiles/ProfilesRepository.scala b/src/main/scala/com/softwaremill/realworld/profiles/ProfilesRepository.scala index eaacfc1945..415053184c 100644 --- a/src/main/scala/com/softwaremill/realworld/profiles/ProfilesRepository.scala +++ b/src/main/scala/com/softwaremill/realworld/profiles/ProfilesRepository.scala @@ -1,29 +1,28 @@ package com.softwaremill.realworld.profiles import io.getquill.* +import io.getquill.jdbczio.* import zio.ZLayer import javax.sql.DataSource -class ProfilesRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: DataSource) { - - private val dsLayer: ZLayer[Any, Nothing, DataSource] = ZLayer.succeed(dataSource) +class ProfilesRepository(quill: Quill.Sqlite[SnakeCase]) { import quill.* def follow(followedId: Int, followerId: Int) = run { query[Followers].insert(_.userId -> lift(followedId), _.followerId -> lift(followerId)).onConflictIgnore - }.provide(dsLayer) + } def unfollow(followedId: Int, followerId: Int) = run { query[Followers].filter(f => (f.userId == lift(followedId)) && (f.followerId == lift(followerId))).delete - }.provide(dsLayer) + } def isFollowing(followedId: Int, followerId: Int) = run { query[Followers].filter(_.userId == lift(followedId)).filter(_.followerId == lift(followerId)).map(_ => 1).nonEmpty - }.provide(dsLayer) + } } object ProfilesRepository: - val live: ZLayer[SqliteZioJdbcContext[SnakeCase] with DataSource, Nothing, ProfilesRepository] = - ZLayer.fromFunction(new ProfilesRepository(_, _)) + val live: ZLayer[Quill.Sqlite[SnakeCase], Nothing, ProfilesRepository] = + ZLayer.fromFunction(new ProfilesRepository(_)) diff --git a/src/main/scala/com/softwaremill/realworld/users/UsersRepository.scala b/src/main/scala/com/softwaremill/realworld/users/UsersRepository.scala index 51e9c4d58a..5bfe849c0c 100644 --- a/src/main/scala/com/softwaremill/realworld/users/UsersRepository.scala +++ b/src/main/scala/com/softwaremill/realworld/users/UsersRepository.scala @@ -3,6 +3,7 @@ package com.softwaremill.realworld.users import com.softwaremill.realworld.common.Exceptions import com.softwaremill.realworld.users.UserMapper.{toUserData, toUserDataWithPassword} import io.getquill.* +import io.getquill.jdbczio.* import org.sqlite.SQLiteErrorCode.SQLITE_CONSTRAINT_UNIQUE import org.sqlite.SQLiteException import zio.{Console, IO, RIO, Task, UIO, ZIO, ZLayer} @@ -11,16 +12,13 @@ import java.sql.SQLException import javax.sql.DataSource import scala.util.chaining.* -class UsersRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: DataSource): - - private val dsLayer: ZLayer[Any, Nothing, DataSource] = ZLayer.succeed(dataSource) - +class UsersRepository(quill: Quill.Sqlite[SnakeCase]): import quill.* private inline def queryUser = quote(querySchema[UserRow](entity = "users")) def findById(id: Int): Task[Option[UserRow]] = - run(queryUser.filter(_.userId == lift(id))).map(_.headOption).provide(dsLayer) + run(queryUser.filter(_.userId == lift(id))).map(_.headOption) def findByEmail(email: String): IO[Exception, Option[UserRow]] = run( // TODO hm should I add additional DTO or returning row from repo in this case is OK? @@ -29,10 +27,9 @@ class UsersRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: DataSo } yield ur ) .map(_.headOption) - .provide(dsLayer) def findByUsername(username: String): IO[Exception, Option[UserRow]] = - run(queryUser.filter(u => u.username == lift(username))).map(_.headOption).provide(dsLayer) + run(queryUser.filter(u => u.username == lift(username))).map(_.headOption) def findUserWithPasswordByEmail(email: String): IO[Exception, Option[UserWithPassword]] = run( for { @@ -41,7 +38,6 @@ class UsersRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: DataSo ) .map(_.headOption) .map(_.map(toUserDataWithPassword)) - .provide(dsLayer) def add(user: UserRegisterData): Task[Unit] = run( queryUser @@ -52,7 +48,6 @@ class UsersRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: DataSo ) ).unit .pipe(mapUniqueConstraintViolationError) - .provide(dsLayer) def updateByEmail(updateData: UserUpdateData, email: String): Task[UserUpdateData] = run( queryUser @@ -66,7 +61,6 @@ class UsersRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: DataSo ) ).map(_ => updateData) .pipe(mapUniqueConstraintViolationError) - .provide(dsLayer) private def mapUniqueConstraintViolationError[R, A](task: RIO[R, A]): RIO[R, A] = task.mapError { case e: SQLiteException if e.getResultCode == SQLITE_CONSTRAINT_UNIQUE => @@ -76,5 +70,5 @@ class UsersRepository(quill: SqliteZioJdbcContext[SnakeCase], dataSource: DataSo object UsersRepository: - val live: ZLayer[SqliteZioJdbcContext[SnakeCase] with DataSource, Nothing, UsersRepository] = - ZLayer.fromFunction(new UsersRepository(_, _)) + val live: ZLayer[Quill.Sqlite[SnakeCase], Nothing, UsersRepository] = + ZLayer.fromFunction(new UsersRepository(_)) diff --git a/src/test/scala/com/softwaremill/realworld/utils/TestUtils.scala b/src/test/scala/com/softwaremill/realworld/utils/TestUtils.scala index 218a78ca61..08e089aeb8 100644 --- a/src/test/scala/com/softwaremill/realworld/utils/TestUtils.scala +++ b/src/test/scala/com/softwaremill/realworld/utils/TestUtils.scala @@ -5,7 +5,8 @@ import com.auth0.jwt.algorithms.Algorithm import com.softwaremill.realworld.auth.AuthService import com.softwaremill.realworld.db.{Db, DbConfig, DbMigrator} import com.softwaremill.realworld.{CustomDecodeFailureHandler, DefectHandler} -import io.getquill.{SnakeCase, SqliteZioJdbcContext} +import io.getquill.* +import io.getquill.jdbczio.* import sttp.client3.SttpBackend import sttp.client3.testing.SttpBackendStub import sttp.tapir.server.stub.TapirStubInterpreter @@ -36,7 +37,7 @@ object TestUtils: .thenRunLogic() .backend() - type TestDbLayer = DbConfig with DataSource with DbMigrator with SqliteZioJdbcContext[SnakeCase] + type TestDbLayer = DbConfig with DataSource with DbMigrator with Quill.Sqlite[SnakeCase] def validAuthorizationHeader(email: String = "admin@example.com"): Map[String, String] = { // start TODO [This is workaround. Need to replace below with service's function call] @@ -101,7 +102,7 @@ object TestUtils: } val testDbLayer: ZLayer[Any, Nothing, TestDbLayer] = - (testDbConfigLive >+> Db.dataSourceLive >+> DbMigrator.live) ++ Db.quillLive + testDbConfigLive >+> Db.dataSourceLive >+> Db.quillLive >+> DbMigrator.live val testDbLayerWithEmptyDb: ZLayer[Any, Nothing, TestDbLayer] = testDbLayer >+> ZLayer.fromZIO(withEmptyDb.orDie) From 6689d642ef4bdbded9e9fedd05b49ce0c675b73b Mon Sep 17 00:00:00 2001 From: donut Date: Fri, 7 Apr 2023 10:04:23 +0200 Subject: [PATCH 2/2] Proof of concept implementation of generic repository --- .../profiles/ProfilesRepository.scala | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/main/scala/com/softwaremill/realworld/profiles/ProfilesRepository.scala b/src/main/scala/com/softwaremill/realworld/profiles/ProfilesRepository.scala index 415053184c..a73993cb1e 100644 --- a/src/main/scala/com/softwaremill/realworld/profiles/ProfilesRepository.scala +++ b/src/main/scala/com/softwaremill/realworld/profiles/ProfilesRepository.scala @@ -1,28 +1,37 @@ package com.softwaremill.realworld.profiles import io.getquill.* +import io.getquill.context.sql.idiom.SqlIdiom import io.getquill.jdbczio.* -import zio.ZLayer +import zio.{Tag, ZIO, ZLayer} +import java.sql.SQLException import javax.sql.DataSource -class ProfilesRepository(quill: Quill.Sqlite[SnakeCase]) { - import quill.* +trait ProfilesRepository { + def follow(followedId: Int, followerId: Int): ZIO[Any, SQLException, Long] + def unfollow(followedId: Int, followerId: Int): ZIO[Any, SQLException, Long] + def isFollowing(followedId: Int, followerId: Int): ZIO[Any, SQLException, Boolean] +} - def follow(followedId: Int, followerId: Int) = run { - query[Followers].insert(_.userId -> lift(followedId), _.followerId -> lift(followerId)).onConflictIgnore - } +object ProfilesRepository { + def live[I <: SqlIdiom: Tag, N <: NamingStrategy: Tag]: ZLayer[Quill[I, N], Nothing, ProfilesRepository] = + ZLayer.fromFunction(ProfilesRepositoryLive[I, N](_)) - def unfollow(followedId: Int, followerId: Int) = run { - query[Followers].filter(f => (f.userId == lift(followedId)) && (f.followerId == lift(followerId))).delete - } + private class ProfilesRepositoryLive[I <: SqlIdiom, N <: NamingStrategy](quill: Quill[I, N]) extends ProfilesRepository { - def isFollowing(followedId: Int, followerId: Int) = run { - query[Followers].filter(_.userId == lift(followedId)).filter(_.followerId == lift(followerId)).map(_ => 1).nonEmpty - } + import quill.* -} + def follow(followedId: Int, followerId: Int): ZIO[Any, SQLException, Long] = run { + query[Followers].insert(_.userId -> lift(followedId), _.followerId -> lift(followerId)).onConflictIgnore + } + + def unfollow(followedId: Int, followerId: Int): ZIO[Any, SQLException, Long] = run { + query[Followers].filter(f => (f.userId == lift(followedId)) && (f.followerId == lift(followerId))).delete + } -object ProfilesRepository: - val live: ZLayer[Quill.Sqlite[SnakeCase], Nothing, ProfilesRepository] = - ZLayer.fromFunction(new ProfilesRepository(_)) + def isFollowing(followedId: Int, followerId: Int): ZIO[Any, SQLException, Boolean] = run { + query[Followers].filter(_.userId == lift(followedId)).filter(_.followerId == lift(followerId)).map(_ => 1).nonEmpty + } + } +}