From f076b74bde4f153cd9266b1a7a6332c717948e7c Mon Sep 17 00:00:00 2001 From: Dasiu Date: Sun, 19 Jan 2020 13:08:10 +0100 Subject: [PATCH] Made articles list filters independent * Also: Removed java.time slick mappings - they are built-in since Slick 3.3.X * Also: Replaced MaybeFilter with filterOpt - it was introduced in Slick 3.3.X, no need for MaybeFiler anymore --- app/articles/ArticleComponents.scala | 21 +- .../controllers/ArticleController.scala | 55 ++++- app/articles/models/ArticlesPageRequest.scala | 13 + app/articles/models/MainFeedPageRequest.scala | 10 - app/articles/repositories/ArticleRepo.scala | 227 +++++++++++------- .../ArticleTagAssociationRepo.scala | 60 +++-- .../repositories/ArticleWithTagsRepo.scala | 21 +- app/articles/repositories/CommentRepo.scala | 79 +++--- .../FavoriteAssociationRepo.scala | 69 +++--- app/articles/repositories/TagRepo.scala | 52 ++-- .../services/ArticleReadService.scala | 4 +- .../repositories/SecurityUserRepo.scala | 71 +++--- app/commons/models/Username.scala | 2 +- app/commons/repositories/BaseRepo.scala | 130 ---------- app/commons/repositories/MaybeFilter.scala | 12 - .../mappings/JavaTimeDbMappings.scala | 19 -- app/users/controllers/BaseActionBuilder.scala | 4 +- .../JwtAuthenticatedActionBuilder.scala | 8 +- ...OptionallyAuthenticatedActionBuilder.scala | 8 +- .../repositories/FollowAssociationRepo.scala | 51 ++-- app/users/repositories/ProfileRepo.scala | 2 +- app/users/repositories/UserRepo.scala | 87 ++++--- test/articles/ArticleListTest.scala | 58 +++-- .../test_helpers/ArticleTestHelper.scala | 29 ++- test/commons/repositories/BaseRepoTest.scala | 175 -------------- .../test_helpers/UserRegistrations.scala | 4 +- 26 files changed, 576 insertions(+), 695 deletions(-) create mode 100644 app/articles/models/ArticlesPageRequest.scala delete mode 100644 app/articles/models/MainFeedPageRequest.scala delete mode 100644 app/commons/repositories/BaseRepo.scala delete mode 100644 app/commons/repositories/MaybeFilter.scala delete mode 100644 app/commons/repositories/mappings/JavaTimeDbMappings.scala delete mode 100644 test/commons/repositories/BaseRepoTest.scala diff --git a/app/articles/ArticleComponents.scala b/app/articles/ArticleComponents.scala index af900d4..1cd6de5 100644 --- a/app/articles/ArticleComponents.scala +++ b/app/articles/ArticleComponents.scala @@ -4,7 +4,7 @@ import com.softwaremill.macwire.wire import commons.config.{WithControllerComponents, WithExecutionContextComponents} import commons.models._ import articles.controllers.{ArticleController, CommentController, TagController} -import articles.models.{ArticleMetaModel, CommentId, MainFeedPageRequest, UserFeedPageRequest} +import articles.models._ import articles.repositories._ import articles.services._ import commons.CommonsComponents @@ -19,9 +19,6 @@ trait ArticleComponents with CommonsComponents with WithExecutionContextComponents { - private lazy val defaultOffset = 0L - private lazy val defaultLimit = 20L - def authenticatedAction: AuthenticatedActionBuilder lazy val articleController: ArticleController = wire[ArticleController] @@ -48,21 +45,11 @@ trait ArticleComponents q_o"offset=${long(maybeOffset)}" & q_o"tag=$maybeTag" & q_o"author=$maybeAuthor" & - q_o"favorited=$maybeFavorited") => - - val limit = maybeLimit.getOrElse(defaultLimit) - val offset = maybeOffset.getOrElse(defaultOffset) - val maybeAuthorUsername = maybeAuthor.map(Username(_)) - val maybeFavoritedUsername = maybeFavorited.map(Username(_)) + q_o"favorited =$maybeFavorited") => - articleController.findAll(MainFeedPageRequest(maybeTag, maybeAuthorUsername, maybeFavoritedUsername, limit, offset, - List(Ordering(ArticleMetaModel.createdAt, Descending)))) + articleController.findAll(maybeTag, maybeAuthor, maybeFavorited, maybeLimit, maybeOffset) case GET(p"/articles/feed" ? q_o"limit=${long(limit)}" & q_o"offset=${long(offset)}") => - val theLimit = limit.getOrElse(defaultLimit) - val theOffset = offset.getOrElse(defaultOffset) - - articleController.findFeed(UserFeedPageRequest(theLimit, theOffset, - List(Ordering(ArticleMetaModel.createdAt, Descending)))) + articleController.findFeed(limit, offset) case GET(p"/articles/$slug") => articleController.findBySlug(slug) case POST(p"/articles") => diff --git a/app/articles/controllers/ArticleController.scala b/app/articles/controllers/ArticleController.scala index e3152d1..64a3088 100644 --- a/app/articles/controllers/ArticleController.scala +++ b/app/articles/controllers/ArticleController.scala @@ -1,11 +1,13 @@ package articles.controllers -import commons.exceptions.MissingModelException -import commons.services.ActionRunner +import articles.controllers.ArticleController.{defaultLimit, defaultOffset} import articles.exceptions.AuthorMismatchException import articles.models._ import articles.services.{ArticleReadService, ArticleWriteService} import commons.controllers.RealWorldAbstractController +import commons.exceptions.MissingModelException +import commons.models.{Descending, Ordering, Username} +import commons.services.ActionRunner import org.apache.commons.lang3.StringUtils import play.api.libs.json._ import play.api.mvc.{Action, AnyContent, ControllerComponents} @@ -58,9 +60,14 @@ class ArticleController(authenticatedAction: AuthenticatedActionBuilder, }) } - def findAll(pageRequest: MainFeedPageRequest): Action[AnyContent] = optionallyAuthenticatedActionBuilder.async { request => - require(pageRequest != null) + def findAll(maybeTag: Option[String], + maybeAuthor: Option[String], + maybeFavorited: Option[String], + maybeLimit: Option[Long], + maybeOffset: Option[Long]): Action[AnyContent] = optionallyAuthenticatedActionBuilder.async { request => + validateNoMoreThanOneFilterIsGiven(maybeTag, maybeAuthor, maybeFavorited) + val pageRequest = buildPageRequest(maybeTag, maybeAuthor, maybeFavorited, maybeLimit, maybeOffset) val maybeUserId = request.authenticatedUserOption.map(_.userId) actionRunner.runTransactionally(articleReadService.findAll(pageRequest, maybeUserId)) .map(page => ArticlePage(page.models, page.count)) @@ -68,9 +75,32 @@ class ArticleController(authenticatedAction: AuthenticatedActionBuilder, .map(Ok(_)) } - def findFeed(pageRequest: UserFeedPageRequest): Action[AnyContent] = authenticatedAction.async { request => - require(pageRequest != null) + private def buildPageRequest(maybeTag: Option[String], maybeAuthor: Option[String], + maybeFavorited: Option[String], maybeLimit: Option[Long], + maybeOffset: Option[Long]) = { + val limit = maybeLimit.getOrElse(defaultLimit) + val offset = maybeOffset.getOrElse(defaultOffset) + if (maybeTag.isDefined) { + ArticlesByTag(maybeTag.get, limit, offset) + } else if (maybeAuthor.isDefined) { + ArticlesByAuthor(maybeAuthor.map(Username(_)).get, limit, offset) + } else if (maybeFavorited.isDefined) { + ArticlesByFavorited(maybeFavorited.map(Username(_)).get, limit, offset) + } else { + ArticlesAll(limit, offset) + } + } + private def validateNoMoreThanOneFilterIsGiven(maybeTag: Option[String], maybeAuthor: Option[String], maybeFavorited: Option[String]) = { + val possibleFilters = List(maybeTag, maybeAuthor, maybeFavorited) + val filtersCount = possibleFilters.count(_.isDefined) + if (filtersCount >= 2) { + BadRequest("Can not use more than one filter at the time") + } + } + + def findFeed(maybeLimit: Option[Long], maybeOffset: Option[Long]): Action[AnyContent] = authenticatedAction.async { request => + val pageRequest: UserFeedPageRequest = buildPageRequest(maybeLimit, maybeOffset) val userId = request.user.userId actionRunner.runTransactionally(articleReadService.findFeed(pageRequest, userId)) .map(page => ArticlePage(page.models, page.count)) @@ -78,6 +108,14 @@ class ArticleController(authenticatedAction: AuthenticatedActionBuilder, .map(Ok(_)) } + private def buildPageRequest(maybeLimit: Option[Long], maybeOffset: Option[Long]) = { + val limit = maybeLimit.getOrElse(20L) + val offset = maybeOffset.getOrElse(0L) + val pageRequest = UserFeedPageRequest(limit, offset, + List(Ordering(ArticleMetaModel.createdAt, Descending))) + pageRequest + } + def create: Action[NewArticleWrapper] = authenticatedAction.async(validateJson[NewArticleWrapper]) { request => val article = request.body.article val userId = request.user.userId @@ -115,3 +153,8 @@ class ArticleController(authenticatedAction: AuthenticatedActionBuilder, } } + +object ArticleController { + private lazy val defaultOffset = 0L + private lazy val defaultLimit = 20L +} diff --git a/app/articles/models/ArticlesPageRequest.scala b/app/articles/models/ArticlesPageRequest.scala new file mode 100644 index 0000000..8bc0fea --- /dev/null +++ b/app/articles/models/ArticlesPageRequest.scala @@ -0,0 +1,13 @@ +package articles.models + +import commons.models.Username + +sealed trait ArticlesPageRequest { + def limit: Long + def offset: Long +} + +case class ArticlesAll(limit: Long, offset: Long) extends ArticlesPageRequest +case class ArticlesByTag(tag: String, limit: Long, offset: Long) extends ArticlesPageRequest +case class ArticlesByAuthor(author: Username, limit: Long, offset: Long) extends ArticlesPageRequest +case class ArticlesByFavorited(favoritedBy: Username, limit: Long, offset: Long) extends ArticlesPageRequest \ No newline at end of file diff --git a/app/articles/models/MainFeedPageRequest.scala b/app/articles/models/MainFeedPageRequest.scala deleted file mode 100644 index aeaba36..0000000 --- a/app/articles/models/MainFeedPageRequest.scala +++ /dev/null @@ -1,10 +0,0 @@ -package articles.models - -import commons.models.{Ordering, Username} - -case class MainFeedPageRequest(tag: Option[String] = None, - author: Option[Username] = None, - favorited: Option[Username] = None, - limit: Long, - offset: Long, - orderings: List[Ordering] = Nil) \ No newline at end of file diff --git a/app/articles/repositories/ArticleRepo.scala b/app/articles/repositories/ArticleRepo.scala index 3f1f346..e63910b 100644 --- a/app/articles/repositories/ArticleRepo.scala +++ b/app/articles/repositories/ArticleRepo.scala @@ -2,33 +2,32 @@ package articles.repositories import java.time.Instant +import articles.models.{Tag => _, _} import commons.exceptions.MissingModelException import commons.models._ -import commons.repositories._ -import commons.repositories.mappings.JavaTimeDbMappings import commons.utils.DbioUtils -import articles.models.{Tag => _, _} -import users.models.{User, UserId} -import users.repositories.{FollowAssociationRepo, UserRepo, UserTable} import org.apache.commons.lang3.StringUtils import slick.dbio.DBIO import slick.jdbc.H2Profile.api.{DBIO => _, MappedTo => _, Rep => _, TableQuery => _, _} -import slick.lifted.{ProvenShape, Rep} +import slick.lifted.{ProvenShape, Rep, TableQuery} +import users.models.{User, UserId} +import users.repositories.{FollowAssociationRepo, UsersTable} import scala.concurrent.ExecutionContext -class ArticleRepo(userRepo: UserRepo, - articleTagRepo: ArticleTagAssociationRepo, - tagRepo: TagRepo, +class ArticleRepo(articleTagAssociationRepo: ArticleTagAssociationRepo, followAssociationRepo: FollowAssociationRepo, - favoriteAssociation: FavoriteAssociationRepo, - implicit private val ec: ExecutionContext) extends BaseRepo[ArticleId, Article, ArticleTable] - with JavaTimeDbMappings { + implicit private val ec: ExecutionContext) { + import ArticleTable.articles + import ArticleTagAssociationTable.articleTagAssociations + import FavoriteAssociationTable.favoriteAssociations + import TagTable.tags + import UsersTable.users def findBySlugOption(slug: String): DBIO[Option[Article]] = { require(StringUtils.isNotBlank(slug)) - query + articles .filter(_.slug === slug) .result .headOption @@ -42,38 +41,91 @@ class ArticleRepo(userRepo: UserRepo, } def findByIdWithUser(id: ArticleId): DBIO[(Article, User)] = { - query - .join(userRepo.query).on(_.authorId === _.id) + articles + .join(users).on(_.authorId === _.id) .filter(_._1.id === id) .result .headOption .map(_.get) } - def findByMainFeedPageRequest(pageRequest: MainFeedPageRequest): DBIO[Page[Article]] = { - require(pageRequest != null) + def findPageRequest(pageRequest: ArticlesPageRequest): DBIO[Page[Article]] = { + def getAll = { + val rows = articles + .sortBy(_.createdAt.desc) + .drop(pageRequest.offset) + .take(pageRequest.limit) + .result + + val count = articles + .size + .result + + rows.zip(count) + .map(buildPage) + } + + def buildPage(rowsWithCount: (Seq[Article], Int)) = { + Page(rowsWithCount._1, rowsWithCount._2) + } + + def getByTag(pg: ArticlesByTag) = { + val queryBase = articles + .join(articleTagAssociations).on(_.id === _.articleId) + .join(tags).on((tables, tagTable) => tables._2.tagId === tagTable.id) + .filter({ + case (_, tagTable) => tagTable.name === pg.tag + }) + + val count = queryBase.size.result + + queryBase + .map(_._1._1) + .result.zip(count) + .map(buildPage) + } + + def getByAuthor(pg: ArticlesByAuthor) = { + val queryBase = articles + .join(users).on(_.authorId === _.id) + .filter({ + case (_, userTable) => userTable.username === pg.author + }) - val joinsWithFilters = getQueryBase(pageRequest) - - val count = joinsWithFilters - .map(tables => getArticleTab(tables).id) - .distinct - .size - - val articleIdsAndCreatedAtPage = joinsWithFilters - .map(tables => { - val articleTable = getArticleTab(tables) - (articleTable.id, articleTable.createdAt) - }) - .distinct - .sortBy(idAndCreatedAt => idAndCreatedAt._2.desc) - .drop(pageRequest.offset) - .take(pageRequest.limit) - - articleIdsAndCreatedAtPage.result.map(_.map(_._1)) - .flatMap(articleIds => findByIds(articleIds, Ordering(ArticleMetaModel.createdAt, Descending))) - .zip(count.result) - .map(articlesAndAuthorsWithCount => Page(articlesAndAuthorsWithCount._1, articlesAndAuthorsWithCount._2)) + val count = queryBase.size.result + + queryBase + .map(_._1) + .result.zip(count) + .map(buildPage) + } + + def getByFavorited(pg: ArticlesByFavorited) = { + val queryBase = articles + .join(favoriteAssociations).on(_.id === _.favoritedId) + .join(users).on((tables, userTable) => tables._2.userId === userTable.id) + .filter({ + case (_, userTable) => userTable.username === pg.favoritedBy + }) + + val count = queryBase.size.result + + queryBase + .map(_._1._1) + .result.zip(count) + .map(buildPage) + } + + pageRequest match { + case _: ArticlesAll => + getAll + case pg: ArticlesByTag => + getByTag(pg) + case pg: ArticlesByAuthor => + getByAuthor(pg) + case pg: ArticlesByFavorited => + getByFavorited(pg) + } } def findByUserFeedPageRequest(pageRequest: UserFeedPageRequest, userId: UserId): DBIO[Page[Article]] = { @@ -85,8 +137,8 @@ class ArticleRepo(userRepo: UserRepo, } def byUserFeedPageRequest(followedIds: Seq[UserId]) = { - val base = query - .join(userRepo.query).on(_.authorId === _.id) + val base = articles + .join(users).on(_.authorId === _.id) .filter(_._2.id inSet followedIds) .map(_._1) @@ -104,73 +156,76 @@ class ArticleRepo(userRepo: UserRepo, .flatMap(followedIds => byUserFeedPageRequest(followedIds)) } - private def getQueryBase(pageRequest: MainFeedPageRequest) = { - val joins = query - .join(userRepo.query).on(_.authorId === _.id) - .joinLeft(articleTagRepo.query).on(_._1.id === _.articleId) - .joinLeft(tagRepo.query).on((tables, tagTable) => tables._2.map(_.tagId === tagTable.id)) - .joinLeft(favoriteAssociation.query) - .on((tables, favoritedAssociationTable) => tables._1._1._1.id === favoritedAssociationTable.favoritedId) - - MaybeFilter(joins) - .filter(pageRequest.author)(authorUsername => tables => getUserTable(tables).username === authorUsername) - .filter(pageRequest.tag)(tagValue => tables => getTagTable(tables).map(_.name === tagValue)) - .filter(pageRequest.favorited)(favoritedUsername => tables => { - getFavoritedAssociationTable(tables).map(favoritedAssociationTable => { - val userTable = getUserTable(tables) - favoritedAssociationTable.userId === userTable.id && userTable.username === favoritedUsername - }) - }) - .query + def insertAndGet(model: Article): DBIO[Article] = { + require(model != null) + + insertAndGet(Seq(model)) + .map(_.head) } - private def getArticleTab(tables: ((((ArticleTable, UserTable), Rep[Option[ArticleTagAssociationTable]]), Rep[Option[TagTable]]), Rep[Option[FavoriteAssociationTable]])) = { - tables._1._1._1._1 + private def insertAndGet(models: Iterable[Article]): DBIO[Seq[Article]] = { + if (models == null && models.isEmpty) DBIO.successful(Seq.empty) + else articles.returning(articles.map(_.id)) + .++=(models) + .flatMap(ids => findByIds(ids)) } - private def getTagTable(tables: ((((ArticleTable, UserTable), Rep[Option[ArticleTagAssociationTable]]), Rep[Option[TagTable]]), Rep[Option[FavoriteAssociationTable]])) = { - tables._1._2 + def updateAndGet(article: Article): DBIO[Article] = { + require(article != null) + + articles + .filter(_.id === article.id) + .update(article) + .flatMap(_ => findById(article.id)) } - private def getUserTable(tables: ((((ArticleTable, UserTable), Rep[Option[ArticleTagAssociationTable]]), Rep[Option[TagTable]]), Rep[Option[FavoriteAssociationTable]])) = { - tables._1._1._1._2 + def findById(articleId: ArticleId): DBIO[Article] = { + articles + .filter(_.id === articleId) + .result + .headOption + .flatMap(maybeModel => DbioUtils.optionToDbio(maybeModel, new MissingModelException(s"model id: $articleId"))) } - private def getFavoritedAssociationTable(tables: ((((ArticleTable, UserTable), Rep[Option[ArticleTagAssociationTable]]), Rep[Option[TagTable]]), Rep[Option[FavoriteAssociationTable]])) = { - tables._2 + private def findByIds(modelIds: Iterable[ArticleId]): DBIO[Seq[Article]] = { + if (modelIds == null || modelIds.isEmpty) DBIO.successful(Seq.empty) + else articles + .filter(_.id inSet modelIds) + .result } - override protected val mappingConstructor: Tag => ArticleTable = new ArticleTable(_) + def delete(articleId: ArticleId): DBIO[Int] = { + require(articleId != null) - override protected val modelIdMapping: BaseColumnType[ArticleId] = ArticleId.articleIdDbMapping + articles + .filter(_.id === articleId) + .delete + } - override protected val metaModel: IdMetaModel = ArticleMetaModel +} - override protected val metaModelToColumnsMapping: Map[Property[_], ArticleTable => Rep[_]] = Map( - ArticleMetaModel.id -> (table => table.id), - ArticleMetaModel.createdAt -> (table => table.createdAt), - ArticleMetaModel.updatedAt -> (table => table.updatedAt), - ) +object ArticleTable { + val articles = TableQuery[Articles] -} + protected class Articles(tag: Tag) extends Table[Article](tag, "articles") { -protected class ArticleTable(tag: Tag) extends IdTable[ArticleId, Article](tag, "articles") - with JavaTimeDbMappings { + def id: Rep[ArticleId] = column[ArticleId]("id", O.PrimaryKey, O.AutoInc) - def slug: Rep[String] = column(ArticleMetaModel.slug.name) + def slug: Rep[String] = column(ArticleMetaModel.slug.name) - def title: Rep[String] = column(ArticleMetaModel.title.name) + def title: Rep[String] = column(ArticleMetaModel.title.name) - def description: Rep[String] = column(ArticleMetaModel.description.name) + def description: Rep[String] = column(ArticleMetaModel.description.name) - def body: Rep[String] = column(ArticleMetaModel.body.name) + def body: Rep[String] = column(ArticleMetaModel.body.name) - def authorId: Rep[UserId] = column("author_id") + def authorId: Rep[UserId] = column("author_id") - def createdAt: Rep[Instant] = column("created_at") + def createdAt: Rep[Instant] = column("created_at") - def updatedAt: Rep[Instant] = column("updated_at") + def updatedAt: Rep[Instant] = column("updated_at") - def * : ProvenShape[Article] = (id, slug, title, description, body, createdAt, updatedAt, authorId) <> ( - (Article.apply _).tupled, Article.unapply) + def * : ProvenShape[Article] = (id, slug, title, description, body, createdAt, updatedAt, authorId) <> ( + (Article.apply _).tupled, Article.unapply) + } } diff --git a/app/articles/repositories/ArticleTagAssociationRepo.scala b/app/articles/repositories/ArticleTagAssociationRepo.scala index ce204cf..5f21f54 100644 --- a/app/articles/repositories/ArticleTagAssociationRepo.scala +++ b/app/articles/repositories/ArticleTagAssociationRepo.scala @@ -1,8 +1,6 @@ package articles.repositories -import commons.models.{IdMetaModel, Property} -import commons.repositories._ import articles.models import articles.models.{Tag => _, _} import slick.dbio.DBIO @@ -13,8 +11,9 @@ import scala.concurrent.ExecutionContext case class ArticleIdWithTag(articleId: ArticleId, tag: models.Tag) -class ArticleTagAssociationRepo(tagRepo: TagRepo, implicit private val ec: ExecutionContext) - extends BaseRepo[ArticleTagAssociationId, ArticleTagAssociation, ArticleTagAssociationTable] { +class ArticleTagAssociationRepo(implicit private val ec: ExecutionContext) { + import ArticleTagAssociationTable.articleTagAssociations + import TagTable.tags def findTagsByArticleId(articleId: ArticleId): DBIO[Seq[models.Tag]] = { require(articleId != null) @@ -26,7 +25,7 @@ class ArticleTagAssociationRepo(tagRepo: TagRepo, implicit private val ec: Execu def findByArticleId(articleId: ArticleId): DBIO[Seq[ArticleTagAssociation]] = { require(articleId != null) - query + articleTagAssociations .filter(_.articleId === articleId) .result } @@ -34,9 +33,9 @@ class ArticleTagAssociationRepo(tagRepo: TagRepo, implicit private val ec: Execu def findByArticleIds(articleIds: Seq[ArticleId]): DBIO[Seq[ArticleIdWithTag]] = { if (articleIds == null || articleIds.isEmpty) DBIO.successful(Seq.empty) else { - query + articleTagAssociations .filter(_.articleId inSet articleIds) - .join(tagRepo.query).on(_.tagId === _.id) + .join(tags).on(_.tagId === _.id) .map(tables => { val (articleTagTable, tagTable) = tables @@ -47,28 +46,43 @@ class ArticleTagAssociationRepo(tagRepo: TagRepo, implicit private val ec: Execu } } - override protected val mappingConstructor: Tag => ArticleTagAssociationTable = new ArticleTagAssociationTable(_) - - override protected val modelIdMapping: BaseColumnType[ArticleTagAssociationId] = ArticleTagAssociationId.articleTagAssociationIdDbMapping + def insertAndGet(models: Iterable[ArticleTagAssociation]): DBIO[Seq[ArticleTagAssociation]] = { + if (models == null && models.isEmpty) DBIO.successful(Seq.empty) + else articleTagAssociations.returning(articleTagAssociations.map(_.id)) + .++=(models) + .flatMap(ids => findByIds(ids)) + } - override protected val metaModel: IdMetaModel = ArticleTagAssociationMetaModel + private def findByIds(modelIds: Iterable[ArticleTagAssociationId]): DBIO[Seq[ArticleTagAssociation]] = { + if (modelIds == null || modelIds.isEmpty) DBIO.successful(Seq.empty) + else articleTagAssociations + .filter(_.id inSet modelIds) + .result + } - override protected val metaModelToColumnsMapping: Map[Property[_], ArticleTagAssociationTable => Rep[_]] = Map( - ArticleTagAssociationMetaModel.id -> (table => table.id), - ArticleTagAssociationMetaModel.articleId -> (table => table.articleId), - ArticleTagAssociationMetaModel.tagId -> (table => table.tagId) - ) + def delete(articleTagAssociationIds: Iterable[ArticleTagAssociationId]): DBIO[Int] = { + if (articleTagAssociationIds == null || articleTagAssociationIds.isEmpty) DBIO.successful(0) + else articleTagAssociations + .filter(_.id inSet articleTagAssociationIds) + .delete + } } -protected class ArticleTagAssociationTable(tag: Tag) - extends IdTable[ArticleTagAssociationId, ArticleTagAssociation](tag, "articles_tags") { +object ArticleTagAssociationTable { + val articleTagAssociations = TableQuery[ArticleTagAssociations] - def articleId: Rep[ArticleId] = column("article_id") + protected class ArticleTagAssociations(tag: Tag) extends Table[ArticleTagAssociation](tag, "articles_tags") { - def tagId: Rep[TagId] = column("tag_id") + def id: Rep[ArticleTagAssociationId] = column[ArticleTagAssociationId]("id", O.PrimaryKey, O.AutoInc) - def * : ProvenShape[ArticleTagAssociation] = (id, articleId, tagId) <> ((ArticleTagAssociation.apply _).tupled, - ArticleTagAssociation.unapply) + def articleId: Rep[ArticleId] = column("article_id") + + def tagId: Rep[TagId] = column("tag_id") + + def * : ProvenShape[ArticleTagAssociation] = (id, articleId, tagId) <> ((ArticleTagAssociation.apply _).tupled, + ArticleTagAssociation.unapply) + + } +} -} \ No newline at end of file diff --git a/app/articles/repositories/ArticleWithTagsRepo.scala b/app/articles/repositories/ArticleWithTagsRepo.scala index eea0bb2..60434da 100644 --- a/app/articles/repositories/ArticleWithTagsRepo.scala +++ b/app/articles/repositories/ArticleWithTagsRepo.scala @@ -4,16 +4,15 @@ import articles.models._ import commons.models.Page import slick.dbio.DBIO import users.models.{Profile, User, UserId} -import users.repositories.{ProfileRepo, UserRepo} +import users.repositories._ import scala.concurrent.ExecutionContext class ArticleWithTagsRepo(articleRepo: ArticleRepo, - articleTagRepo: ArticleTagAssociationRepo, - tagRepo: TagRepo, - userRepo: UserRepo, - favoriteAssociationRepo: FavoriteAssociationRepo, profileRepo: ProfileRepo, + articleTagAssociationRepo: ArticleTagAssociationRepo, + favoriteAssociationRepo: FavoriteAssociationRepo, + userRepo: UserRepo, implicit private val ex: ExecutionContext) { def findBySlug(slug: String, maybeUserId: Option[UserId]): DBIO[ArticleWithTags] = { @@ -25,7 +24,7 @@ class ArticleWithTagsRepo(articleRepo: ArticleRepo, private def getArticleWithTags(article: Article, maybeUserId: Option[UserId]): DBIO[ArticleWithTags] = { for { - tags <- articleTagRepo.findTagsByArticleId(article.id) + tags <- articleTagAssociationRepo.findTagsByArticleId(article.id) profile <- profileRepo.findByUserId(article.authorId, maybeUserId) favorited <- isFavorited(article.id, maybeUserId) favoritesCount <- getFavoritesCount(article.id) @@ -53,7 +52,7 @@ class ArticleWithTagsRepo(articleRepo: ArticleRepo, } def getArticleWithTags(article: Article, tags: Seq[Tag], userId: UserId): DBIO[ArticleWithTags] = { - userRepo.findById(article.authorId) + userRepo.findById(article.authorId) .flatMap(author => getArticleWithTags(article, author, tags, Some(userId))) } @@ -77,15 +76,15 @@ class ArticleWithTagsRepo(articleRepo: ArticleRepo, articleRepo.findByUserFeedPageRequest(pageRequest, userId) } - def findAll(pageRequest: MainFeedPageRequest, maybeUserId: Option[UserId]): DBIO[Page[ArticleWithTags]] = { + def findAll(pageRequest: ArticlesPageRequest, maybeUserId: Option[UserId]): DBIO[Page[ArticleWithTags]] = { require(pageRequest != null) - articleRepo.findByMainFeedPageRequest(pageRequest) + articleRepo.findPageRequest(pageRequest) .flatMap(articlesPage => getArticlesWithTagsPage(articlesPage, maybeUserId)) } private def getArticlesWithTagsPage(articlesPage: Page[Article], - maybeUserId: Option[UserId]) = { + maybeUserId: Option[UserId]): DBIO[Page[ArticleWithTags]] = { val Page(articles, count) = articlesPage for { profileByUserId <- getProfileByUserId(articles, maybeUserId) @@ -130,7 +129,7 @@ class ArticleWithTagsRepo(articleRepo: ArticleRepo, private def getGroupedTagsByArticleId(articles: Seq[Article]) = { val articleIds = articles.map(_.id) - articleTagRepo.findByArticleIds(articleIds) + articleTagAssociationRepo.findByArticleIds(articleIds) .map(_.groupBy(_.articleId)) } diff --git a/app/articles/repositories/CommentRepo.scala b/app/articles/repositories/CommentRepo.scala index 143b27f..4477176 100644 --- a/app/articles/repositories/CommentRepo.scala +++ b/app/articles/repositories/CommentRepo.scala @@ -3,59 +3,78 @@ package articles.repositories import java.time.Instant -import commons.models._ -import commons.repositories._ -import commons.repositories.mappings.JavaTimeDbMappings import articles.models.{Tag => _, _} -import users.models.{User, UserId} -import users.repositories.UserRepo +import commons.exceptions.MissingModelException +import commons.utils.DbioUtils import slick.dbio.DBIO import slick.jdbc.H2Profile.api.{DBIO => _, MappedTo => _, Rep => _, TableQuery => _, _} import slick.lifted.{ProvenShape, _} +import users.models.UserId +import users.repositories.UserRepo import scala.concurrent.ExecutionContext -class CommentRepo(userRepo: UserRepo, implicit private val ec: ExecutionContext) - extends BaseRepo[CommentId, Comment, CommentTable] with JavaTimeDbMappings { +class CommentRepo(userRepo: UserRepo, implicit private val ec: ExecutionContext) { + import CommentTable.comments def findByArticleId(articleId: ArticleId): DBIO[Seq[Comment]] = { - query + comments .filter(_.articleId === articleId) - .sortBy(commentTable => toSlickOrderingSupplier(Ordering(CommentMetaModel.createdAt, Descending))(commentTable)) + .sortBy(_.createdAt.desc) .result } - override protected val mappingConstructor: Tag => CommentTable = new CommentTable(_) + def insertAndGet(comment: Comment): DBIO[Comment] = { + require(comment != null) - override protected val modelIdMapping: BaseColumnType[CommentId] = CommentId.commentIdDbMapping + insert(comment) + .flatMap(findById) + } - override protected val metaModel: IdMetaModel = CommentMetaModel + private def insert(comment: Comment): DBIO[CommentId] = { + comments.returning(comments.map(_.id)) += comment + } - override protected val metaModelToColumnsMapping: Map[Property[_], CommentTable => Rep[_]] = Map( + def findById(commentId: CommentId): DBIO[Comment] = { + comments + .filter(_.id === commentId) + .result + .headOption + .flatMap(maybeModel => DbioUtils.optionToDbio(maybeModel, new MissingModelException(s"model id: $commentId"))) + } - CommentMetaModel.id -> (table => table.id), - CommentMetaModel.articleId -> (table => table.articleId), - CommentMetaModel.authorId -> (table => table.authorId), - CommentMetaModel.body -> (table => table.body), - CommentMetaModel.createdAt -> (table => table.createdAt), - CommentMetaModel.updatedAt -> (table => table.updatedAt) - ) + def delete(commentId: CommentId): DBIO[Int] = { + comments + .filter(_.id === commentId) + .delete + } + + def delete(commentIds: Seq[CommentId]): DBIO[Int] = { + comments + .filter(_.id inSet commentIds) + .delete + } } -protected class CommentTable(tag: Tag) extends IdTable[CommentId, Comment](tag, "comments") - with JavaTimeDbMappings { +object CommentTable { + val comments = TableQuery[Comments] + + protected class Comments(tag: Tag) extends Table[Comment](tag, "comments") { - def articleId: Rep[ArticleId] = column("article_id") + def id: Rep[CommentId] = column[CommentId]("id", O.PrimaryKey, O.AutoInc) - def authorId: Rep[UserId] = column("author_id") + def articleId: Rep[ArticleId] = column("article_id") - def body: Rep[String] = column("body") + def authorId: Rep[UserId] = column("author_id") - def createdAt: Rep[Instant] = column("created_at") + def body: Rep[String] = column("body") - def updatedAt: Rep[Instant] = column("updated_at") + def createdAt: Rep[Instant] = column("created_at") - def * : ProvenShape[Comment] = (id, articleId, authorId, body, createdAt, updatedAt) <> ((Comment.apply _).tupled, - Comment.unapply) -} \ No newline at end of file + def updatedAt: Rep[Instant] = column("updated_at") + + def * : ProvenShape[Comment] = (id, articleId, authorId, body, createdAt, updatedAt) <> ((Comment.apply _).tupled, + Comment.unapply) + } +} diff --git a/app/articles/repositories/FavoriteAssociationRepo.scala b/app/articles/repositories/FavoriteAssociationRepo.scala index 56a7ca1..69cabce 100644 --- a/app/articles/repositories/FavoriteAssociationRepo.scala +++ b/app/articles/repositories/FavoriteAssociationRepo.scala @@ -1,23 +1,42 @@ package articles.repositories -import commons.models.{IdMetaModel, Property} -import commons.repositories._ -import articles.models.{ArticleId, FavoriteAssociation, FavoriteAssociationId, FavoriteAssociationMetaModel} -import users.models.UserId +import articles.models.{Tag => _, _} import slick.dbio.DBIO import slick.jdbc.MySQLProfile.api.{DBIO => _, MappedTo => _, Rep => _, TableQuery => _, _} import slick.lifted.{ProvenShape, _} +import users.models.UserId import scala.concurrent.ExecutionContext -class FavoriteAssociationRepo(implicit private val ec: ExecutionContext) - extends BaseRepo[FavoriteAssociationId, FavoriteAssociation, FavoriteAssociationTable] { +class FavoriteAssociationRepo(implicit private val ec: ExecutionContext) { + import FavoriteAssociationTable.favoriteAssociations + + def delete(favoriteAssociationIds: Iterable[FavoriteAssociationId]): DBIO[Int] = { + if (favoriteAssociationIds == null || favoriteAssociationIds.isEmpty) DBIO.successful(0) + else favoriteAssociations + .filter(_.id inSet favoriteAssociationIds) + .delete + } + + def delete(favoriteAssociationId: FavoriteAssociationId): DBIO[Int] = { + if (favoriteAssociationId == null) DBIO.successful(0) + else favoriteAssociations + .filter(_.id === favoriteAssociationId) + .delete + } + + def insert(favoriteAssociation: FavoriteAssociation): DBIO[FavoriteAssociationId] = { + require(favoriteAssociation != null) + + favoriteAssociations + .returning(favoriteAssociations.map(_.id)) += favoriteAssociation + } def groupByArticleAndCount(articleIds: Seq[ArticleId]): DBIO[Seq[(ArticleId, Int)]] = { require(articleIds != null) - query + favoriteAssociations .filter(_.favoritedId inSet articleIds) .groupBy(_.favoritedId) .map({ @@ -30,14 +49,14 @@ class FavoriteAssociationRepo(implicit private val ec: ExecutionContext) def findByUserAndArticles(userId: UserId, articleIds: Seq[ArticleId]): DBIO[Seq[FavoriteAssociation]] = { require(articleIds != null) - query + favoriteAssociations .filter(_.userId === userId) .filter(_.favoritedId inSet articleIds) .result } def findByUserAndArticle(userId: UserId, articleId: ArticleId): DBIO[Option[FavoriteAssociation]] = { - query + favoriteAssociations .filter(_.userId === userId) .filter(_.favoritedId === articleId) .result @@ -45,32 +64,26 @@ class FavoriteAssociationRepo(implicit private val ec: ExecutionContext) } def findByArticle(articleId: ArticleId): DBIO[Seq[FavoriteAssociation]] = { - query + favoriteAssociations .filter(_.favoritedId === articleId) .result } - override protected val mappingConstructor: Tag => FavoriteAssociationTable = new FavoriteAssociationTable(_) +} - override protected val modelIdMapping: BaseColumnType[FavoriteAssociationId] = - FavoriteAssociationId.favoriteAssociationIdDbMapping +object FavoriteAssociationTable { + val favoriteAssociations = TableQuery[FavoriteAssociations] - override protected val metaModel: IdMetaModel = FavoriteAssociationMetaModel + protected class FavoriteAssociations(tag: Tag) + extends Table[FavoriteAssociation](tag, "favorite_associations") { - override protected val metaModelToColumnsMapping: Map[Property[_], FavoriteAssociationTable => Rep[_]] = Map( - FavoriteAssociationMetaModel.id -> (table => table.id), - FavoriteAssociationMetaModel.userId -> (table => table.userId), - FavoriteAssociationMetaModel.favoritedId -> (table => table.favoritedId), - ) -} + def id: Rep[FavoriteAssociationId] = column[FavoriteAssociationId]("id", O.PrimaryKey, O.AutoInc) -protected class FavoriteAssociationTable(tag: Tag) - extends IdTable[FavoriteAssociationId, FavoriteAssociation](tag, "favorite_associations") { + def userId: Rep[UserId] = column("user_id") - def userId: Rep[UserId] = column("user_id") + def favoritedId: Rep[ArticleId] = column("favorited_id") - def favoritedId: Rep[ArticleId] = column("favorited_id") - - def * : ProvenShape[FavoriteAssociation] = (id, userId, favoritedId) <> ((FavoriteAssociation.apply _).tupled, - FavoriteAssociation.unapply) -} \ No newline at end of file + def * : ProvenShape[FavoriteAssociation] = (id, userId, favoritedId) <> ((FavoriteAssociation.apply _).tupled, + FavoriteAssociation.unapply) + } +} diff --git a/app/articles/repositories/TagRepo.scala b/app/articles/repositories/TagRepo.scala index af30728..66189d1 100644 --- a/app/articles/repositories/TagRepo.scala +++ b/app/articles/repositories/TagRepo.scala @@ -1,42 +1,54 @@ package articles.repositories -import commons.models.{IdMetaModel, Property} -import commons.repositories._ -import commons.repositories.mappings.JavaTimeDbMappings -import articles.models.{Tag, TagId, TagMetaModel} -import slick.dbio.{DBIO, Effect} +import articles.models.{Tag, _} +import slick.dbio.DBIO import slick.jdbc.H2Profile.api.{DBIO => _, MappedTo => _, Rep => _, TableQuery => _, _} import slick.lifted.{ProvenShape, _} import scala.concurrent.ExecutionContext -class TagRepo(implicit private val ec: ExecutionContext) - extends BaseRepo[TagId, Tag, TagTable] { +class TagRepo(implicit private val ec: ExecutionContext) { + import TagTable.tags def findByNames(tagNames: Seq[String]): DBIO[Seq[Tag]] = { if (tagNames == null || tagNames.isEmpty) DBIO.successful(Seq.empty) - else query + else tags .filter(_.name.inSet(tagNames)) .result } - override protected val mappingConstructor: slick.lifted.Tag => TagTable = new TagTable(_) - - override protected val modelIdMapping: BaseColumnType[TagId] = TagId.tagIdDbMapping + def insertAndGet(models: Iterable[Tag]): DBIO[Seq[Tag]] = { + if (models == null && models.isEmpty) DBIO.successful(Seq.empty) + else tags.returning(tags.map(_.id)) + .++=(models) + .flatMap(ids => findByIds(ids)) + } - override protected val metaModel: IdMetaModel = TagMetaModel + private def findByIds(modelIds: Iterable[TagId]): DBIO[Seq[Tag]] = { + if (modelIds == null || modelIds.isEmpty) DBIO.successful(Seq.empty) + else tags + .filter(_.id inSet modelIds) + .result + } - override protected val metaModelToColumnsMapping: Map[Property[_], TagTable => Rep[_]] = Map( - TagMetaModel.id -> (table => table.id), - TagMetaModel.name -> (table => table.name), - ) + def findAll(): DBIO[Seq[Tag]] = { + tags + .sortBy(_.id.desc) + .result + } } -protected class TagTable(tableTag: slick.lifted.Tag) extends IdTable[TagId, Tag](tableTag, "tags") - with JavaTimeDbMappings { +object TagTable { + val tags = TableQuery[Tags] - def name: Rep[String] = column(TagMetaModel.name.name) + protected class Tags(tableTag: slick.lifted.Tag) extends Table[Tag](tableTag, "tags") { + + def id: Rep[TagId] = column[TagId]("id", O.PrimaryKey, O.AutoInc) + + def name: Rep[String] = column(TagMetaModel.name.name) + + def * : ProvenShape[Tag] = (id, name) <> ((Tag.apply _).tupled, Tag.unapply) + } - def * : ProvenShape[Tag] = (id, name) <> ((Tag.apply _).tupled, Tag.unapply) } diff --git a/app/articles/services/ArticleReadService.scala b/app/articles/services/ArticleReadService.scala index dff711e..3671a77 100644 --- a/app/articles/services/ArticleReadService.scala +++ b/app/articles/services/ArticleReadService.scala @@ -1,6 +1,6 @@ package articles.services -import articles.models.{ArticleWithTags, MainFeedPageRequest, UserFeedPageRequest} +import articles.models.{ArticleWithTags, ArticlesPageRequest, UserFeedPageRequest} import articles.repositories.ArticleWithTagsRepo import commons.models.Page import slick.dbio.DBIO @@ -14,7 +14,7 @@ class ArticleReadService(articleWithTagsRepo: ArticleWithTagsRepo) { articleWithTagsRepo.findBySlug(slug, maybeUserId) } - def findAll(pageRequest: MainFeedPageRequest, maybeUserId: Option[UserId]): DBIO[Page[ArticleWithTags]] = { + def findAll(pageRequest: ArticlesPageRequest, maybeUserId: Option[UserId]): DBIO[Page[ArticleWithTags]] = { require(pageRequest != null && maybeUserId != null) articleWithTagsRepo.findAll(pageRequest, maybeUserId) diff --git a/app/authentication/repositories/SecurityUserRepo.scala b/app/authentication/repositories/SecurityUserRepo.scala index bb2e2cc..702b01b 100644 --- a/app/authentication/repositories/SecurityUserRepo.scala +++ b/app/authentication/repositories/SecurityUserRepo.scala @@ -3,10 +3,10 @@ package authentication.repositories import java.time.Instant import authentication.exceptions.MissingSecurityUserException -import commons.models.{Email, IdMetaModel, Property} -import commons.repositories._ -import commons.repositories.mappings.JavaTimeDbMappings import authentication.models.{PasswordHash, SecurityUser, SecurityUserId} +import commons.exceptions.MissingModelException +import commons.models.Email +import commons.utils.DbioUtils import commons.utils.DbioUtils.optionToDbio import slick.dbio.DBIO import slick.jdbc.H2Profile.api.{DBIO => _, MappedTo => _, Rep => _, TableQuery => _, _} @@ -14,13 +14,14 @@ import slick.lifted.{ProvenShape, _} import scala.concurrent.ExecutionContext -private[authentication] class SecurityUserRepo(implicit private val ex: ExecutionContext) - extends BaseRepo[SecurityUserId, SecurityUser, SecurityUserTable] { +private[authentication] class SecurityUserRepo(implicit private val ex: ExecutionContext) { + + import SecurityUserTable.securityUsers def findByEmailOption(email: Email): DBIO[Option[SecurityUser]] = { require(email != null) - query + securityUsers .filter(_.email === email) .result .headOption @@ -33,41 +34,57 @@ private[authentication] class SecurityUserRepo(implicit private val ex: Executio .flatMap(optionToDbio(_, new MissingSecurityUserException(email.toString))) } + def insertAndGet(securityUser: SecurityUser): DBIO[SecurityUser] = { + require(securityUser != null) + + insert(securityUser) + .flatMap(findById) + } - override protected val mappingConstructor: Tag => SecurityUserTable = new SecurityUserTable(_) + private def insert(securityUser: SecurityUser): DBIO[SecurityUserId] = { + securityUsers.returning(securityUsers.map(_.id)) += securityUser + } - override protected val modelIdMapping: BaseColumnType[SecurityUserId] = SecurityUserId.securityUserIdDbMapping + def findById(securityUserId: SecurityUserId): DBIO[SecurityUser] = { + securityUsers + .filter(_.id === securityUserId) + .result + .headOption + .flatMap(maybeModel => DbioUtils.optionToDbio(maybeModel, new MissingModelException(s"model id: $securityUserId"))) + } - override protected val metaModel: IdMetaModel = SecurityUserMetaModel + def updateAndGet(securityUser: SecurityUser): DBIO[SecurityUser] = { + update(securityUser).flatMap(_ => findById(securityUser.id)) + } - override protected val metaModelToColumnsMapping: Map[Property[_], SecurityUserTable => Rep[_]] = Map( - SecurityUserMetaModel.id -> (table => table.id), - SecurityUserMetaModel.email -> (table => table.email), - SecurityUserMetaModel.password -> (table => table.password) - ) + private def update(securityUser: SecurityUser): DBIO[Int] = { + require(securityUser != null) + securityUsers + .filter(_.id === securityUser.id) + .update(securityUser) + } } -protected class SecurityUserTable(tag: Tag) extends IdTable[SecurityUserId, SecurityUser](tag, "security_users") - with JavaTimeDbMappings { +object SecurityUserTable { + val securityUsers = TableQuery[SecurityUsers] - def email: Rep[Email] = column("email") + protected class SecurityUsers(tag: Tag) extends Table[SecurityUser](tag, "security_users") { - def password: Rep[PasswordHash] = column("password") + def id: Rep[SecurityUserId] = column[SecurityUserId]("id", O.PrimaryKey, O.AutoInc) - def createdAt: Rep[Instant] = column("created_at") + def email: Rep[Email] = column("email") - def updatedAt: Rep[Instant] = column("updated_at") + def password: Rep[PasswordHash] = column("password") - def * : ProvenShape[SecurityUser] = (id, email, password, createdAt, updatedAt) <> (SecurityUser.tupled, - SecurityUser.unapply) -} + def createdAt: Rep[Instant] = column("created_at") + + def updatedAt: Rep[Instant] = column("updated_at") -private[authentication] object SecurityUserMetaModel extends IdMetaModel { - override type ModelId = SecurityUserId + def * : ProvenShape[SecurityUser] = (id, email, password, createdAt, updatedAt) <> (SecurityUser.tupled, + SecurityUser.unapply) + } - val email: Property[Email] = Property("email") - val password: Property[PasswordHash] = Property("password") } diff --git a/app/commons/models/Username.scala b/app/commons/models/Username.scala index e67f054..43e4a9c 100644 --- a/app/commons/models/Username.scala +++ b/app/commons/models/Username.scala @@ -13,7 +13,7 @@ object Username { override def writes(o: Username): JsValue = Writes.StringWrites.writes(o.value) } - implicit val emailDbMapping: BaseColumnType[Username] = MappedColumnType.base[Username, String]( + implicit val usernameDbMapping: BaseColumnType[Username] = MappedColumnType.base[Username, String]( vo => vo.value, username => Username(username) ) diff --git a/app/commons/repositories/BaseRepo.scala b/app/commons/repositories/BaseRepo.scala deleted file mode 100644 index 164df74..0000000 --- a/app/commons/repositories/BaseRepo.scala +++ /dev/null @@ -1,130 +0,0 @@ -package commons.repositories - -import commons.exceptions.MissingModelException -import commons.models.{BaseId, Descending, IdMetaModel, Ordering, Property, WithId} -import commons.utils.DbioUtils -import slick.dbio.DBIO -import slick.jdbc.H2Profile.api.{DBIO => _, MappedTo => _, Rep => _, TableQuery => _, _} -import slick.lifted._ - -import scala.concurrent.ExecutionContext.Implicits._ - -trait BaseRepo[ModelId <: BaseId[Long], Model <: WithId[Long, ModelId], ModelTable <: IdTable[ModelId, Model]] { - // lazy required to init table query with concrete mappingConstructor value - lazy val query: TableQuery[ModelTable] = TableQuery[ModelTable](mappingConstructor) - protected val mappingConstructor: Tag => ModelTable - implicit protected val modelIdMapping: BaseColumnType[ModelId] - protected val metaModelToColumnsMapping: Map[Property[_], ModelTable => Rep[_]] - protected val metaModel: IdMetaModel - - def findAll: DBIO[Seq[Model]] = findAll(List(Ordering(metaModel.id, Descending))) - - def findAll(orderings: Seq[Ordering]): DBIO[Seq[Model]] = { - if (orderings == null || orderings.isEmpty) findAll - else { - // multiple sortBy calls are reversed comparing to SQLs order by clause - val slickOrderings = orderings.map(toSlickOrderingSupplier).reverse - - var sortQuery = query.sortBy(slickOrderings.head) - slickOrderings.tail.foreach(getSlickOrdering => { - sortQuery = sortQuery.sortBy(getSlickOrdering) - }) - - sortQuery.result - } - } - - def insertAndGet(model: Model): DBIO[Model] = { - require(model != null) - - insertAndGet(Seq(model)) - .map(_.head) - } - - def insertAndGet(models: Iterable[Model]): DBIO[Seq[Model]] = { - if (models == null && models.isEmpty) DBIO.successful(Seq.empty) - else query.returning(query.map(_.id)) - .++=(models) - .flatMap(ids => findByIds(ids)) - } - - def findByIds(modelIds: Iterable[ModelId]): DBIO[Seq[Model]] = { - if (modelIds == null || modelIds.isEmpty) DBIO.successful(Seq.empty) - else query - .filter(_.id inSet modelIds) - .result - } - - def findByIds(modelIds: Iterable[ModelId], ordering: Ordering): DBIO[Seq[Model]] = { - if (modelIds == null || modelIds.isEmpty) DBIO.successful(Seq.empty) - else query - .filter(_.id inSet modelIds) - .sortBy(toSlickOrderingSupplier(ordering)) - .result - } - - protected def toSlickOrderingSupplier(ordering: Ordering): ModelTable => ColumnOrdered[_] = { - implicit val Ordering(property, direction) = ordering - val getColumn = metaModelToColumnsMapping(property) - getColumn.andThen(RepoHelper.createSlickColumnOrdered) - } - - def insert(model: Model): DBIO[ModelId] = { - require(model != null) - - insert(Seq(model)) - .map(_.head) - } - - def insert(models: Iterable[Model]): DBIO[Seq[ModelId]] = { - if (models != null && models.isEmpty) DBIO.successful(Seq.empty) - else query.returning(query.map(_.id)).++=(models) - } - - def updateAndGet(model: Model): DBIO[Model] = { - require(model != null) - - query - .filter(_.id === model.id) - .update(model) - .flatMap(_ => findById(model.id)) - } - - def findByIdOption(modelId: ModelId): DBIO[Option[Model]] = { - require(modelId != null) - - findByIds(Seq(modelId)) - .map(_.headOption) - } - - def findById(modelId: ModelId): DBIO[Model] = { - findByIdOption(modelId) - .flatMap(maybeModel => DbioUtils.optionToDbio(maybeModel, new MissingModelException(s"model id: $modelId"))) - } - - def delete(modelId: ModelId): DBIO[Int] = { - require(modelId != null) - - delete(Seq(modelId)) - } - - def delete(modelIds: Seq[ModelId]): DBIO[Int] = { - if (modelIds == null || modelIds.isEmpty) DBIO.successful(0) - else query - .filter(_.id inSet modelIds) - .delete - } - -} - -abstract class IdTable[Id <: BaseId[Long], Entity <: WithId[Long, Id]] -(tag: Tag, schemaName: Option[String], tableName: String) -(implicit val mapping: BaseColumnType[Id]) - extends Table[Entity](tag, schemaName, tableName) { - - protected val idColumnName: String = "id" - - def this(tag: Tag, tableName: String)(implicit mapping: BaseColumnType[Id]) = this(tag, None, tableName) - - final def id: Rep[Id] = column[Id](idColumnName, O.PrimaryKey, O.AutoInc) -} \ No newline at end of file diff --git a/app/commons/repositories/MaybeFilter.scala b/app/commons/repositories/MaybeFilter.scala deleted file mode 100644 index 00267e7..0000000 --- a/app/commons/repositories/MaybeFilter.scala +++ /dev/null @@ -1,12 +0,0 @@ -package commons.repositories - -import slick.lifted.{CanBeQueryCondition, Query, Rep} - -// optionally filter on a column with a supplied predicate - taken from: https://gist.github.com/cvogt/9193220 -case class MaybeFilter[X, Y](query: Query[X, Y, Seq]) { - - def filter[T, R <: Rep[_] : CanBeQueryCondition](data: Option[T])(f: T => X => R): MaybeFilter[X, Y] = { - data.map(v => MaybeFilter(query.filter(f(v)))).getOrElse(this) - } - -} \ No newline at end of file diff --git a/app/commons/repositories/mappings/JavaTimeDbMappings.scala b/app/commons/repositories/mappings/JavaTimeDbMappings.scala deleted file mode 100644 index 745cc87..0000000 --- a/app/commons/repositories/mappings/JavaTimeDbMappings.scala +++ /dev/null @@ -1,19 +0,0 @@ -package commons.repositories.mappings - -import java.sql.Timestamp -import java.time.Instant - -import slick.jdbc.H2Profile.api.{DBIO => _, MappedTo => _, Rep => _, TableQuery => _, _} - -trait JavaTimeDbMappings { - - implicit val localDateTimeMapper: BaseColumnType[Instant] = MappedColumnType.base[Instant, Timestamp]( - localDateTime => - if (localDateTime == null) null - else Timestamp.from(localDateTime), - timestamp => - if (timestamp == null) null - else timestamp.toInstant - ) - -} diff --git a/app/users/controllers/BaseActionBuilder.scala b/app/users/controllers/BaseActionBuilder.scala index d752930..350fc3a 100644 --- a/app/users/controllers/BaseActionBuilder.scala +++ b/app/users/controllers/BaseActionBuilder.scala @@ -10,8 +10,8 @@ import users.repositories.UserRepo import scala.concurrent.ExecutionContext private[controllers] class BaseActionBuilder( - jwtAuthenticator: JwtAuthenticator, - userRepo: UserRepo, + jwtAuthenticator: JwtAuthenticator, + userRepo: UserRepo, )(implicit ec: ExecutionContext) { protected def authenticate(requestHeader: RequestHeader): DBIO[(User, String)] = { diff --git a/app/users/controllers/JwtAuthenticatedActionBuilder.scala b/app/users/controllers/JwtAuthenticatedActionBuilder.scala index 0fc4b23..d50fb8b 100644 --- a/app/users/controllers/JwtAuthenticatedActionBuilder.scala +++ b/app/users/controllers/JwtAuthenticatedActionBuilder.scala @@ -14,10 +14,10 @@ import users.repositories.UserRepo import scala.concurrent.{ExecutionContext, Future} private[users] class JwtAuthenticatedActionBuilder( - parsers: PlayBodyParsers, - jwtAuthenticator: JwtAuthenticator, - userRepo: UserRepo, - actionRunner: ActionRunner + parsers: PlayBodyParsers, + jwtAuthenticator: JwtAuthenticator, + userRepo: UserRepo, + actionRunner: ActionRunner ) (implicit ec: ExecutionContext) extends BaseActionBuilder(jwtAuthenticator, userRepo) with AuthenticatedActionBuilder { diff --git a/app/users/controllers/JwtOptionallyAuthenticatedActionBuilder.scala b/app/users/controllers/JwtOptionallyAuthenticatedActionBuilder.scala index 006adb2..8e69e76 100644 --- a/app/users/controllers/JwtOptionallyAuthenticatedActionBuilder.scala +++ b/app/users/controllers/JwtOptionallyAuthenticatedActionBuilder.scala @@ -12,10 +12,10 @@ import users.repositories.UserRepo import scala.concurrent.{ExecutionContext, Future} private[users] class JwtOptionallyAuthenticatedActionBuilder( - parsers: PlayBodyParsers, - jwtAuthenticator: JwtAuthenticator, - userRepo: UserRepo, - actionRunner: ActionRunner + parsers: PlayBodyParsers, + jwtAuthenticator: JwtAuthenticator, + userRepo: UserRepo, + actionRunner: ActionRunner )(implicit ec: ExecutionContext) extends BaseActionBuilder(jwtAuthenticator, userRepo) with OptionallyAuthenticatedActionBuilder { diff --git a/app/users/repositories/FollowAssociationRepo.scala b/app/users/repositories/FollowAssociationRepo.scala index 34ec98a..b1d24d4 100644 --- a/app/users/repositories/FollowAssociationRepo.scala +++ b/app/users/repositories/FollowAssociationRepo.scala @@ -1,20 +1,18 @@ package users.repositories -import commons.models.{IdMetaModel, Property} -import commons.repositories._ -import users.models.{FollowAssociation, FollowAssociationId, FollowAssociationMetaModel, UserId} import slick.dbio.DBIO import slick.jdbc.MySQLProfile.api.{DBIO => _, MappedTo => _, Rep => _, TableQuery => _, _} import slick.lifted.{ProvenShape, _} +import users.models.{FollowAssociation, FollowAssociationId, UserId} import scala.concurrent.ExecutionContext -class FollowAssociationRepo(implicit private val ec: ExecutionContext) - extends BaseRepo[FollowAssociationId, FollowAssociation, FollowAssociationTable] { +class FollowAssociationRepo(implicit private val ec: ExecutionContext) { + import FollowAssociationTable.followAssociations def findByFollower(followerId: UserId): DBIO[Seq[FollowAssociation]] = { - query + followAssociations .filter(_.followerId === followerId) .result } @@ -28,32 +26,39 @@ class FollowAssociationRepo(implicit private val ec: ExecutionContext) } private def getFollowerAndFollowedQuery(followerId: UserId, followedIds: Iterable[UserId]) = { - query + followAssociations .filter(_.followerId === followerId) .filter(_.followedId inSet followedIds) } - override protected val mappingConstructor: Tag => FollowAssociationTable = new FollowAssociationTable(_) + def delete(id: FollowAssociationId): DBIO[Int] = { + require(id != null) - override protected val modelIdMapping: BaseColumnType[FollowAssociationId] = - FollowAssociationId.followAssociationIdDbMapping + followAssociations + .filter(_.id === id) + .delete + } - override protected val metaModel: IdMetaModel = FollowAssociationMetaModel + def insert(followAccociation: FollowAssociation): DBIO[FollowAssociationId] = { + require(followAccociation != null) - override protected val metaModelToColumnsMapping: Map[Property[_], FollowAssociationTable => Rep[_]] = Map( - FollowAssociationMetaModel.id -> (table => table.id), - FollowAssociationMetaModel.followerId -> (table => table.followerId), - FollowAssociationMetaModel.followedId -> (table => table.followedId), - ) + followAssociations + .returning(followAssociations.map(_.id)) += followAccociation + } } -protected class FollowAssociationTable(tag: Tag) - extends IdTable[FollowAssociationId, FollowAssociation](tag, "follow_associations") { +object FollowAssociationTable { + val followAssociations = TableQuery[FollowAssociations] + + protected class FollowAssociations(tag: Tag) extends Table[FollowAssociation](tag, "follow_associations") { - def followerId: Rep[UserId] = column("follower_id") + def id: Rep[FollowAssociationId] = column[FollowAssociationId]("id", O.PrimaryKey, O.AutoInc) - def followedId: Rep[UserId] = column("followed_id") + def followerId: Rep[UserId] = column("follower_id") - def * : ProvenShape[FollowAssociation] = (id, followerId, followedId) <> ((FollowAssociation.apply _).tupled, - FollowAssociation.unapply) -} \ No newline at end of file + def followedId: Rep[UserId] = column("followed_id") + + def * : ProvenShape[FollowAssociation] = (id, followerId, followedId) <> ((FollowAssociation.apply _).tupled, + FollowAssociation.unapply) + } +} diff --git a/app/users/repositories/ProfileRepo.scala b/app/users/repositories/ProfileRepo.scala index 9d1f240..00235a8 100644 --- a/app/users/repositories/ProfileRepo.scala +++ b/app/users/repositories/ProfileRepo.scala @@ -8,7 +8,7 @@ import scala.concurrent.ExecutionContext class ProfileRepo(userRepo: UserRepo, followAssociationRepo: FollowAssociationRepo, - implicit private val ec: ExecutionContext) { + implicit private val ec: ExecutionContext) { def getProfileByUserId(userIds: Iterable[UserId], maybeUserId: Option[UserId]): DBIO[Map[UserId, Profile]] = { require(userIds != null && maybeUserId != null) diff --git a/app/users/repositories/UserRepo.scala b/app/users/repositories/UserRepo.scala index ccf9453..131c0e4 100644 --- a/app/users/repositories/UserRepo.scala +++ b/app/users/repositories/UserRepo.scala @@ -4,23 +4,37 @@ import java.time.Instant import authentication.models.SecurityUserId import commons.exceptions.MissingModelException -import commons.models.{Email, IdMetaModel, Property, Username} -import commons.repositories._ -import commons.repositories.mappings.JavaTimeDbMappings +import commons.models.{Email, Username} import commons.utils.DbioUtils -import users.models.{User, UserId, UserMetaModel} import slick.dbio.DBIO import slick.jdbc.H2Profile.api.{DBIO => _, MappedTo => _, Rep => _, TableQuery => _, _} import slick.lifted.{ProvenShape, _} +import users.models.{User, UserId} import scala.concurrent.ExecutionContext -class UserRepo(implicit private val ec: ExecutionContext) extends BaseRepo[UserId, User, UserTable] { +class UserRepo(implicit private val ec: ExecutionContext) { + import UsersTable.users + + def findById(userId: UserId): DBIO[User] = { + users + .filter(_.id === userId) + .result + .headOption + .flatMap(maybeUser => DbioUtils.optionToDbio(maybeUser, new MissingModelException(s"User id: $userId"))) + } + + def findByIds(modelIds: Iterable[UserId]): DBIO[Seq[User]] = { + if (modelIds == null || modelIds.isEmpty) DBIO.successful(Seq.empty) + else users + .filter(_.id inSet modelIds) + .result + } def findBySecurityUserIdOption(securityUserId: SecurityUserId): DBIO[Option[User]] = { require(securityUserId != null) - query + users .filter(_.securityUserId === securityUserId) .result .headOption @@ -35,7 +49,7 @@ class UserRepo(implicit private val ec: ExecutionContext) extends BaseRepo[UserI def findByEmailOption(email: Email): DBIO[Option[User]] = { require(email != null) - query + users .filter(_.email === email) .result .headOption @@ -49,7 +63,7 @@ class UserRepo(implicit private val ec: ExecutionContext) extends BaseRepo[UserI def findByUsernameOption(username: Username): DBIO[Option[User]] = { require(username != null) - query + users .filter(_.username === username) .result .headOption @@ -61,40 +75,51 @@ class UserRepo(implicit private val ec: ExecutionContext) extends BaseRepo[UserI new MissingModelException(s"user with username $username"))) } - override protected val mappingConstructor: Tag => UserTable = new UserTable(_) + def insertAndGet(user: User): DBIO[User] = { + insert(user).flatMap(findById) + } + + def updateAndGet(user: User): DBIO[User] = { + update(user).flatMap(_ => findById(user.id)) + } - override protected val modelIdMapping: BaseColumnType[UserId] = UserId.userIdDbMapping + private def update(user: User): DBIO[Int] = { + require(user != null) - override protected val metaModel: IdMetaModel = UserMetaModel + users + .filter(_.id === user.id) + .update(user) + } - override protected val metaModelToColumnsMapping: Map[Property[_], UserTable => Rep[_]] = Map( - UserMetaModel.id -> (table => table.id), - UserMetaModel.username -> (table => table.username) - ) + private def insert(user: User): DBIO[UserId] = { + require(user != null) - implicit val usernameMapping: BaseColumnType[Username] = MappedColumnType.base[Username, String]( - username => username.value, - str => Username(str) - ) + users.returning(users.map(_.id)) += user + } } -class UserTable(tag: Tag) extends IdTable[UserId, User](tag, "users") - with JavaTimeDbMappings { +object UsersTable { + val users = TableQuery[Users] - def securityUserId: Rep[SecurityUserId] = column[SecurityUserId]("security_user_id") + class Users(tag: Tag) extends Table[User](tag, "users") { - def username: Rep[Username] = column[Username]("username") + def id: Rep[UserId] = column[UserId]("id", O.PrimaryKey, O.AutoInc) - def email: Rep[Email] = column[Email]("email") + def securityUserId: Rep[SecurityUserId] = column[SecurityUserId]("security_user_id") - def bio: Rep[String] = column[String]("bio") + def username: Rep[Username] = column[Username]("username") - def image: Rep[String] = column[String]("image") + def email: Rep[Email] = column[Email]("email") - def createdAt: Rep[Instant] = column("created_at") + def bio: Rep[String] = column[String]("bio") - def updatedAt: Rep[Instant] = column("updated_at") + def image: Rep[String] = column[String]("image") - def * : ProvenShape[User] = (id, securityUserId, username, email, bio.?, image.?, createdAt, updatedAt) <> ((User.apply _).tupled, - User.unapply) -} \ No newline at end of file + def createdAt: Rep[Instant] = column("created_at") + + def updatedAt: Rep[Instant] = column("updated_at") + + def * : ProvenShape[User] = (id, securityUserId, username, email, bio.?, image.?, createdAt, updatedAt) <> ((User.apply _).tupled, + User.unapply) + } +} diff --git a/test/articles/ArticleListTest.scala b/test/articles/ArticleListTest.scala index a0cefc1..4f52c3c 100644 --- a/test/articles/ArticleListTest.scala +++ b/test/articles/ArticleListTest.scala @@ -1,6 +1,6 @@ package articles -import articles.models.{ArticlePage, ArticleWithTags, MainFeedPageRequest} +import articles.models._ import articles.test_helpers.{Articles, Tags} import commons.models.Username import commons_test.test_helpers.{RealWorldWithServerAndTestConfigBaseTest, WithArticleTestHelper, WithUserTestHelper} @@ -16,7 +16,7 @@ class ArticleListTest extends RealWorldWithServerAndTestConfigBaseTest with With userDetailsWithToken <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.petycjaRegistration) persistedArticle <- articleTestHelper.create[ArticleWithTags](newArticle, userDetailsWithToken.token) - response <- articleTestHelper.findAll[WSResponse](MainFeedPageRequest(limit = 5L, offset = 0L)) + response <- articleTestHelper.findAll[WSResponse](ArticlesAll(limit = 5L, offset = 0L)) } yield { response.status.mustBe(OK) val page = response.json.as[ArticlePage] @@ -32,7 +32,7 @@ class ArticleListTest extends RealWorldWithServerAndTestConfigBaseTest with With userDetailsWithToken <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.petycjaRegistration) _ <- articleTestHelper.create[ArticleWithTags](newArticle, userDetailsWithToken.token) - response <- articleTestHelper.findAll[WSResponse](MainFeedPageRequest(limit = 0L, offset = 0L)) + response <- articleTestHelper.findAll[WSResponse](ArticlesAll(limit = 0L, offset = 0L)) } yield { response.status.mustBe(OK) response.json.as[ArticlePage].mustBe(ArticlePage(Nil, 1L)) @@ -46,7 +46,7 @@ class ArticleListTest extends RealWorldWithServerAndTestConfigBaseTest with With persistedArticle <- articleTestHelper.create[ArticleWithTags](newArticle, userDetailsWithToken.token) persistedNewerArticle <- articleTestHelper.create[ArticleWithTags](newArticle, userDetailsWithToken.token) - response <- articleTestHelper.findAll[WSResponse](MainFeedPageRequest(limit = 5L, offset = 0L)) + response <- articleTestHelper.findAll[WSResponse](ArticlesAll(limit = 5L, offset = 0L)) } yield { response.status.mustBe(OK) val page = response.json.as[ArticlePage] @@ -59,15 +59,14 @@ class ArticleListTest extends RealWorldWithServerAndTestConfigBaseTest with With it should "return article created by requested user" in await { val newArticle = Articles.hotToTrainYourDragon for { - userDetailsWithToken <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.petycjaRegistration) - persistedArticle <- articleTestHelper.create[ArticleWithTags](newArticle, userDetailsWithToken.token) - - response <- articleTestHelper.findAll[WSResponse](MainFeedPageRequest(limit = 5L, offset = 0L, author = Some(userDetailsWithToken.username))) + author <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.petycjaRegistration) + article <- articleTestHelper.create[ArticleWithTags](newArticle, author.token) + response <- articleTestHelper.findAll[WSResponse](ArticlesByAuthor(limit = 5L, offset = 0L, author = author.username)) } yield { response.status.mustBe(OK) val page = response.json.as[ArticlePage] page.articlesCount.mustBe(1L) - page.articles.head.id.mustBe(persistedArticle.id) + page.articles.head.id.mustBe(article.id) } } @@ -75,10 +74,10 @@ class ArticleListTest extends RealWorldWithServerAndTestConfigBaseTest with With it should "return empty array of articles when requested user have not created any articles" in await { val newArticle = Articles.hotToTrainYourDragon for { - userDetailsWithToken <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.petycjaRegistration) - _ <- articleTestHelper.create[ArticleWithTags](newArticle, userDetailsWithToken.token) + author <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.petycjaRegistration) + _ <- articleTestHelper.create[ArticleWithTags](newArticle, author.token) - response <- articleTestHelper.findAll[WSResponse](MainFeedPageRequest(limit = 5L, offset = 0L, author = Some(Username("not existing username")))) + response <- articleTestHelper.findAll[WSResponse](ArticlesByAuthor(limit = 5L, offset = 0L, author = Username("not existing username"))) } yield { response.status.mustBe(OK) val page = response.json.as[ArticlePage] @@ -92,7 +91,7 @@ class ArticleListTest extends RealWorldWithServerAndTestConfigBaseTest with With userDetailsWithToken <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.petycjaRegistration) persistedArticle <- articleTestHelper.create[ArticleWithTags](newArticle, userDetailsWithToken.token) - response <- articleTestHelper.findAll[WSResponse](MainFeedPageRequest(limit = 5L, offset = 0L, tag = Some(newArticle.tagList.head))) + response <- articleTestHelper.findAll[WSResponse](ArticlesByTag(limit = 5L, offset = 0L, tag = newArticle.tagList.head)) } yield { response.status.mustBe(OK) val page = response.json.as[ArticlePage] @@ -107,7 +106,7 @@ class ArticleListTest extends RealWorldWithServerAndTestConfigBaseTest with With userDetailsWithToken <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.petycjaRegistration) _ <- articleTestHelper.create[ArticleWithTags](newArticle, userDetailsWithToken.token) - response <- articleTestHelper.findAll[WSResponse](MainFeedPageRequest(limit = 5L, offset = 0L, tag = Some(Tags.dragons.name))) + response <- articleTestHelper.findAll[WSResponse](ArticlesByTag(limit = 5L, offset = 0L, tag = Tags.dragons.name)) } yield { response.status.mustBe(OK) val page = response.json.as[ArticlePage] @@ -115,19 +114,38 @@ class ArticleListTest extends RealWorldWithServerAndTestConfigBaseTest with With } } - it should "return article created by followed user" in await { + it should "return article favorited by requested user" in await { val newArticle = Articles.hotToTrainYourDragon for { - articleAuthor <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.petycjaRegistration) - authenticatedUser <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.kopernikRegistration) - _ <- profileTestHelper.follow[WSResponse](articleAuthor.username, authenticatedUser.token) - _ <- articleTestHelper.create[WSResponse](newArticle, articleAuthor.token) + author <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.petycjaRegistration) + userWhoFavoritesTheArticle <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.kopernikRegistration) + + article <- articleTestHelper.create[ArticleWithTags](newArticle, author.token) + _ <- articleTestHelper.favorite[WSResponse](article.slug, userWhoFavoritesTheArticle.token) - response <- articleTestHelper.findAll[WSResponse](MainFeedPageRequest(limit = 5L, offset = 0L, author = Some(articleAuthor.username))) + response <- articleTestHelper.findAll[WSResponse](ArticlesByFavorited(limit = 5L, offset = 0L, favoritedBy = userWhoFavoritesTheArticle.username)) } yield { response.status.mustBe(OK) val page = response.json.as[ArticlePage] page.articlesCount.mustBe(1L) } } + + it should "not return article, which was not favorited by requested user" in await { + val newArticle = Articles.hotToTrainYourDragon + for { + author <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.petycjaRegistration) + userWhoFavoritesTheArticle <- userTestHelper.register[UserDetailsWithToken](UserRegistrations.kopernikRegistration) + + article <- articleTestHelper.create[ArticleWithTags](newArticle, author.token) + _ <- articleTestHelper.favorite[WSResponse](article.slug, userWhoFavoritesTheArticle.token) + + response <- articleTestHelper.findAll[WSResponse](ArticlesByFavorited(limit = 5L, offset = 0L, favoritedBy = author.username)) + } yield { + response.status.mustBe(OK) + val page = response.json.as[ArticlePage] + page.articlesCount.mustBe(0L) + } + } + } diff --git a/test/articles/test_helpers/ArticleTestHelper.scala b/test/articles/test_helpers/ArticleTestHelper.scala index 25529fa..f926fef 100644 --- a/test/articles/test_helpers/ArticleTestHelper.scala +++ b/test/articles/test_helpers/ArticleTestHelper.scala @@ -22,19 +22,26 @@ class ArticleTestHelper(executionContext: ExecutionContext) extends WsScalaTestC implicit private val ex: ExecutionContext = executionContext - private def getQueryParams(mainFeedPageRequest: MainFeedPageRequest) = { - import mainFeedPageRequest._ - val optionalParams = Seq( - favorited.map("favorite" -> _.value), - author.map("author" -> _.value), - tag.map("tag" -> _) - ).filter(_.isDefined) - .map(_.get) - - Seq("limit" -> limit.toString, "offset" -> offset.toString) ++ optionalParams + private def getQueryParams(articlesPageRequest: ArticlesPageRequest) = { + import articlesPageRequest._ + + val requiredParams = Seq("limit" -> limit.toString, "offset" -> offset.toString) + val maybeFilterParam = articlesPageRequest match { + case pg: ArticlesByTag => + Some("tag" -> pg.tag) + case pg: ArticlesByAuthor => + Some("author" -> pg.author.value) + case pg: ArticlesByFavorited => + Some("favorited " -> pg.favoritedBy.value) + case _: ArticlesAll => + None + } + + maybeFilterParam.map(requiredParams.prepended) + .getOrElse(requiredParams) } - def findAll[ReturnType](mainFeedPageRequest: MainFeedPageRequest, maybeToken: Option[String] = None) + def findAll[ReturnType](mainFeedPageRequest: ArticlesPageRequest, maybeToken: Option[String] = None) (implicit testWsClient: TestWsClient, responseTransformer: ResponseTransformer[ReturnType]): Future[ReturnType] = { val queryParams = getQueryParams(mainFeedPageRequest) diff --git a/test/commons/repositories/BaseRepoTest.scala b/test/commons/repositories/BaseRepoTest.scala deleted file mode 100644 index c929e3d..0000000 --- a/test/commons/repositories/BaseRepoTest.scala +++ /dev/null @@ -1,175 +0,0 @@ -package commons.repositories - -import java.time.Instant - -import commons.models._ -import commons.repositories.mappings.JavaTimeDbMappings -import commons.services.ActionRunner -import commons_test.test_helpers.RealWorldWithServerAndTestConfigBaseTest.RealWorldWithTestConfig -import commons_test.test_helpers.{ProgrammaticDateTimeProvider, RealWorldWithServerBaseTest, TestUtils} -import play.api.ApplicationLoader.Context -import slick.lifted.{ProvenShape, Rep, Tag} - -class BaseRepoTest extends RealWorldWithServerBaseTest { - - val dateTime: Instant = Instant.now - val programmaticDateTimeProvider: ProgrammaticDateTimeProvider = new ProgrammaticDateTimeProvider - - override type TestComponents = AppWithTestRepo - - override def createComponents: TestComponents = { - new AppWithTestRepo(programmaticDateTimeProvider, context) - } - - "Base repo" should "sort by id desc by default" in runAndAwait { - val testModelRepo = components.testModelRepo - - val apple = testModelRepo.createBlocking(NewTestModel("apple", 21).toTestModel(dateTime)) - val orange = testModelRepo.createBlocking(NewTestModel("orange", 12).toTestModel(dateTime)) - val peach = testModelRepo.createBlocking(NewTestModel("peach", 17).toTestModel(dateTime)) - - // when - val all = testModelRepo.findAll(List()) - - // then - all.map({ - case Seq(elem1, elem2, elem3) => - elem1.mustBe(peach) - elem2.mustBe(orange) - elem3.mustBe(apple) - case _ => fail() - }) - }(components.actionRunner) - - it should "sort by age desc and id asc" in runAndAwait { - val testModelRepo = components.testModelRepo - // given - val apple = testModelRepo.createBlocking(TestModel(TestModelId(-1), "apple", 1, dateTime, dateTime)) - val orange = testModelRepo.createBlocking(TestModel(TestModelId(-1), "orange", 5, dateTime, dateTime)) - val peach = testModelRepo.createBlocking(TestModel(TestModelId(-1), "peach", 5, dateTime, dateTime)) - - // when - val all = testModelRepo.findAll(List(Ordering(TestModelMetaModel.age, Descending), - Ordering(TestModelMetaModel.id, Ascending))) - - // then - all.map({ - case Seq(elem1, elem2, elem3) => - elem1.mustBe(orange) - elem2.mustBe(peach) - elem3.mustBe(apple) - case _ => fail() - }) - }(components.actionRunner) - - it should "set modified at date time when updated" in runAndAwait { - val testModelRepo = components.testModelRepo - // given - programmaticDateTimeProvider.currentTime = dateTime - val apple = testModelRepo.createBlocking(TestModel(TestModelId(-1), "apple", 1, dateTime, dateTime)) - - val laterDateTime = Instant.now - programmaticDateTimeProvider.currentTime = laterDateTime - - val updatedApple = apple.copy(updatedAt = laterDateTime) - - // when - val updateAction = testModelRepo.updateAndGet(updatedApple) - - // then - updateAction.map(result => { - result.createdAt.mustBe(dateTime) - result.updatedAt.mustBe(laterDateTime) - }) - }(components.actionRunner) - - override def afterServerStarted(): Unit = { - runAndAwait(components.testModelRepo.createTable)(components.actionRunner) - } -} - -class AppWithTestRepo(dateTimeProvider: ProgrammaticDateTimeProvider, context: Context) extends RealWorldWithTestConfig(context) { - - lazy val testModelRepo: TestModelRepo = new TestModelRepo(actionRunner) -} - - -case class TestModel(id: TestModelId, - name: String, - age: Int, - createdAt: Instant, - updatedAt: Instant) extends WithId[Long, TestModelId] - -case class NewTestModel(name: String, age: Int) { - def toTestModel(now: Instant): TestModel = TestModel(TestModelId(-1), name, age, now, now) -} - -object TestModelMetaModel extends IdMetaModel { - override type ModelId = TestModelId - - val name: Property[String] = Property("name") - val age: Property[Int] = Property("age") -} - -import slick.dbio.DBIO -import slick.jdbc.H2Profile.api.{DBIO => _, MappedTo => _, Rep => _, TableQuery => _, _} - -class TestModelRepo(private val actionRunner: ActionRunner) - extends BaseRepo[TestModelId, TestModel, TestModelTable] - with JavaTimeDbMappings { - - override protected val mappingConstructor: Tag => TestModelTable = new TestModelTable(_) - override protected val modelIdMapping: BaseColumnType[TestModelId] = TestModelId.testModelIdDbMapping - override protected val metaModel: IdMetaModel = TestModelMetaModel - override protected val metaModelToColumnsMapping: Map[Property[_], TestModelTable => Rep[_]] = Map( - TestModelMetaModel.id -> (table => table.id), - TestModelMetaModel.name -> (table => table.name), - TestModelMetaModel.age -> (table => table.age) - ) - - def createBlocking(testModel: TestModel): TestModel = { - val action = insertAndGet(testModel) - TestUtils.runAndAwaitResult(action)(actionRunner) - } - - def createTable: DBIO[Int] = - sqlu""" - CREATE TABLE test_model ( - id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - age INT NOT NULL, - created_at TIMESTAMP WITH TIME ZONE, - updated_at TIMESTAMP WITH TIME ZONE - ); - """ - - def dropTable: DBIO[Int] = - sqlu""" - DROP TABLE test_model; - """ -} - - -class TestModelTable(tag: Tag) extends IdTable[TestModelId, TestModel](tag, "test_model") - with JavaTimeDbMappings { - - def * : ProvenShape[TestModel] = (id, name, age, createdAt, updatedAt) <> (TestModel.tupled, TestModel.unapply) - - def age: Rep[Int] = column("age") - - def name: Rep[String] = column("name") - - def createdAt: Rep[Instant] = column("created_at") - - def updatedAt: Rep[Instant] = column("updated_at") -} - -case class TestModelId(override val value: Long) extends AnyVal with BaseId[Long] - -object TestModelId { - implicit val testModelIdDbMapping: BaseColumnType[TestModelId] = MappedColumnType.base[TestModelId, Long]( - vo => vo.value, - id => TestModelId(id) - ) -} - diff --git a/test/users/test_helpers/UserRegistrations.scala b/test/users/test_helpers/UserRegistrations.scala index ee2340f..4e81212 100644 --- a/test/users/test_helpers/UserRegistrations.scala +++ b/test/users/test_helpers/UserRegistrations.scala @@ -6,8 +6,8 @@ import users.models.UserRegistration object UserRegistrations { val petycjaRegistration: UserRegistration = - UserRegistration(Username("petycja"), PlainTextPassword("a valid password"), Email("petycja@buziaczek.pl")) + UserRegistration(Username("petycja"), PlainTextPassword("a valid password"), Email("petycja@example.com")) val kopernikRegistration: UserRegistration = - UserRegistration(Username("kopernik"), PlainTextPassword("a valid password"), Email("kopernik@torun.pl")) + UserRegistration(Username("kopernik"), PlainTextPassword("a valid password"), Email("kopernik@example.com")) } \ No newline at end of file