Skip to content
This repository has been archived by the owner on Aug 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #4 from Dasiu/upgraded-version-to-2.7
Browse files Browse the repository at this point in the history
Migrated to Scala 2.13
  • Loading branch information
Dasiu authored Aug 8, 2019
2 parents 2912c33 + 5b68f55 commit dedd1b9
Show file tree
Hide file tree
Showing 80 changed files with 893 additions and 803 deletions.
5 changes: 1 addition & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ install:
- pip install --user codecov

scala:
- 2.12.6

jdk:
- oraclejdk8
- 2.13.0

cache:
directories:
Expand Down
14 changes: 5 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ For more information on how to this works with other frontends/backends, head ov
# Getting started

## You need installed:
* Java 8,
* Java >= 8 <= 11,
* sbt.

Then:
Expand All @@ -20,35 +20,31 @@ Then:

# Project's structure

Application is divided into three main modules: commons, authentication and core.
Application is divided into three main modules: articles, authentication and users.

For simplification they are not represented as sbt's subprojects/submodules. Module is represented as single class with `Components` suffix,
for instance `AuthenticationComponents` or `ArticleComponents`.

Core module contains main business logic which is also divided into `articles` and `users` (and other) modules.
Class `RealWorldApplicationLoader` contains "description" of whole application, it combines all modules together and set up
things like logging, evolutions (db migrations), etc.

Compile time dependency injection is used.

# Security

Pack4j is used to simplify JWT authentication implementation. Generally authentication is implemented as external module implementing
core's API's (`authentication.api`). The idea behind it was to allow replacing module's implementation without touching core's code.
Simple - naive - JWT authentication implementation is used. Authentication module exists to separate authentication logic from business logic related to users, like update of user's profile, etc.

# Database

Project uses a H2 in memory database by default, it can be changed in the `application.conf`.
Tests override that properties to use H2 nevertheless, H2 is used for convenience and can be changed easily as well in the `TestUtils.config`.
Project uses a H2 in memory database by default, it can be changed in the `application.conf` (and in `TestUtils.config` for tests).

Proper JDBC driver needs to be added as dependency in build.sbt.

Additionally to avoid Slick's dependent types all over the place, static imports to concrete Slick's profile are used.
For instance `import slick.jdbc.H2Profile.api.{DBIO => _, MappedTo => _, Rep => _, TableQuery => _, _}`. They should be changed as well in case of changing underlying database. It looks ugly but imho still better than usage of
dynamic import through dependent types (check out Slick examples to see that).

Slick was used to implement data access layer mainly because it is supported by Lightbend. It also looks more "scalish"
and gives perspective for JPA standard in Java ecosystem.
Slick was used to implement data access layer mainly because it is supported by Lightbend. It also looks more "scalish".

======

Expand Down
2 changes: 1 addition & 1 deletion app/articles/ArticleComponents.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import articles.controllers.{ArticleController, CommentController, TagController
import articles.models.{ArticleMetaModel, CommentId, MainFeedPageRequest, UserFeedPageRequest}
import articles.repositories._
import articles.services._
import authentication.api.AuthenticatedActionBuilder
import commons.CommonsComponents
import users.UserComponents
import play.api.routing.Router
import play.api.routing.sird._
import users.controllers.AuthenticatedActionBuilder

trait ArticleComponents
extends WithControllerComponents
Expand Down
34 changes: 17 additions & 17 deletions app/articles/controllers/ArticleController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import commons.services.ActionRunner
import articles.exceptions.AuthorMismatchException
import articles.models._
import articles.services.{ArticleReadService, ArticleWriteService}
import authentication.api.{AuthenticatedActionBuilder, OptionallyAuthenticatedActionBuilder}
import commons.controllers.RealWorldAbstractController
import org.apache.commons.lang3.StringUtils
import play.api.libs.json._
import play.api.mvc.{Action, AnyContent, ControllerComponents}
import users.controllers.{AuthenticatedActionBuilder, OptionallyAuthenticatedActionBuilder}

class ArticleController(authenticatedAction: AuthenticatedActionBuilder,
optionallyAuthenticatedActionBuilder: OptionallyAuthenticatedActionBuilder,
Expand All @@ -22,8 +22,8 @@ class ArticleController(authenticatedAction: AuthenticatedActionBuilder,
def unfavorite(slug: String): Action[AnyContent] = authenticatedAction.async { request =>
require(slug != null)

val currentUserEmail = request.user.email
actionRunner.runTransactionally(articleWriteService.unfavorite(slug, currentUserEmail))
val userId = request.user.userId
actionRunner.runTransactionally(articleWriteService.unfavorite(slug, userId))
.map(ArticleWrapper(_))
.map(Json.toJson(_))
.map(Ok(_))
Expand All @@ -35,8 +35,8 @@ class ArticleController(authenticatedAction: AuthenticatedActionBuilder,
def favorite(slug: String): Action[AnyContent] = authenticatedAction.async { request =>
require(slug != null)

val currentUserEmail = request.user.email
actionRunner.runTransactionally(articleWriteService.favorite(slug, currentUserEmail))
val userId = request.user.userId
actionRunner.runTransactionally(articleWriteService.favorite(slug, userId))
.map(ArticleWrapper(_))
.map(Json.toJson(_))
.map(Ok(_))
Expand All @@ -48,8 +48,8 @@ class ArticleController(authenticatedAction: AuthenticatedActionBuilder,
def findBySlug(slug: String): Action[AnyContent] = optionallyAuthenticatedActionBuilder.async { request =>
require(StringUtils.isNotBlank(slug))

val maybeCurrentUserEmail = request.authenticatedUserOption.map(_.email)
actionRunner.runTransactionally(articleReadService.findBySlug(slug, maybeCurrentUserEmail))
val maybeUserId = request.authenticatedUserOption.map(_.userId)
actionRunner.runTransactionally(articleReadService.findBySlug(slug, maybeUserId))
.map(ArticleWrapper(_))
.map(Json.toJson(_))
.map(Ok(_))
Expand All @@ -61,8 +61,8 @@ class ArticleController(authenticatedAction: AuthenticatedActionBuilder,
def findAll(pageRequest: MainFeedPageRequest): Action[AnyContent] = optionallyAuthenticatedActionBuilder.async { request =>
require(pageRequest != null)

val currentUserEmail = request.authenticatedUserOption.map(_.email)
actionRunner.runTransactionally(articleReadService.findAll(pageRequest, currentUserEmail))
val maybeUserId = request.authenticatedUserOption.map(_.userId)
actionRunner.runTransactionally(articleReadService.findAll(pageRequest, maybeUserId))
.map(page => ArticlePage(page.models, page.count))
.map(Json.toJson(_))
.map(Ok(_))
Expand All @@ -71,17 +71,17 @@ class ArticleController(authenticatedAction: AuthenticatedActionBuilder,
def findFeed(pageRequest: UserFeedPageRequest): Action[AnyContent] = authenticatedAction.async { request =>
require(pageRequest != null)

val currentUserEmail = request.user.email
actionRunner.runTransactionally(articleReadService.findFeed(pageRequest, currentUserEmail))
val userId = request.user.userId
actionRunner.runTransactionally(articleReadService.findFeed(pageRequest, userId))
.map(page => ArticlePage(page.models, page.count))
.map(Json.toJson(_))
.map(Ok(_))
}

def create: Action[NewArticleWrapper] = authenticatedAction.async(validateJson[NewArticleWrapper]) { request =>
val article = request.body.article
val currentUserEmail = request.user.email
actionRunner.runTransactionally(articleWriteService.create(article, currentUserEmail))
val userId = request.user.userId
actionRunner.runTransactionally(articleWriteService.create(article, userId))
.map(ArticleWrapper(_))
.map(Json.toJson(_))
.map(Ok(_))
Expand All @@ -91,8 +91,8 @@ class ArticleController(authenticatedAction: AuthenticatedActionBuilder,
def update(slug: String): Action[ArticleUpdateWrapper] = {
authenticatedAction.async(validateJson[ArticleUpdateWrapper]) { request =>
val articleUpdate = request.body.article
val currentUserEmail = request.user.email
actionRunner.runTransactionally(articleWriteService.update(slug, articleUpdate, currentUserEmail))
val userId = request.user.userId
actionRunner.runTransactionally(articleWriteService.update(slug, articleUpdate, userId))
.map(ArticleWrapper(_))
.map(Json.toJson(_))
.map(Ok(_))
Expand All @@ -105,8 +105,8 @@ class ArticleController(authenticatedAction: AuthenticatedActionBuilder,
def delete(slug: String): Action[AnyContent] = authenticatedAction.async { request =>
require(slug != null)

val currentUserEmail = request.user.email
actionRunner.runTransactionally(articleWriteService.delete(slug, currentUserEmail))
val userId = request.user.userId
actionRunner.runTransactionally(articleWriteService.delete(slug, userId))
.map(_ => Ok)
.recover({
case _: AuthorMismatchException => Forbidden
Expand Down
12 changes: 6 additions & 6 deletions app/articles/controllers/CommentController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import commons.services.ActionRunner
import articles.exceptions.AuthorMismatchException
import articles.models._
import articles.services.CommentService
import authentication.api.{AuthenticatedActionBuilder, OptionallyAuthenticatedActionBuilder}
import commons.controllers.RealWorldAbstractController
import org.apache.commons.lang3.StringUtils
import play.api.libs.json._
import play.api.mvc.{Action, AnyContent, ControllerComponents}
import users.controllers.{AuthenticatedActionBuilder, OptionallyAuthenticatedActionBuilder}

class CommentController(authenticatedAction: AuthenticatedActionBuilder,
optionallyAuthenticatedActionBuilder: OptionallyAuthenticatedActionBuilder,
Expand All @@ -20,7 +20,7 @@ class CommentController(authenticatedAction: AuthenticatedActionBuilder,

def delete(id: CommentId): Action[AnyContent] = authenticatedAction.async { request =>

actionRunner.runTransactionally(commentService.delete(id, request.user.email))
actionRunner.runTransactionally(commentService.delete(id, request.user.userId))
.map(_ => Ok)
.recover({
case _: AuthorMismatchException => Forbidden
Expand All @@ -31,8 +31,8 @@ class CommentController(authenticatedAction: AuthenticatedActionBuilder,
def findByArticleSlug(slug: String): Action[AnyContent] = optionallyAuthenticatedActionBuilder.async { request =>
require(StringUtils.isNotBlank(slug))

val maybeCurrentUserEmail = request.authenticatedUserOption.map(_.email)
actionRunner.runTransactionally(commentService.findByArticleSlug(slug, maybeCurrentUserEmail))
val maybeUserId = request.authenticatedUserOption.map(_.userId)
actionRunner.runTransactionally(commentService.findByArticleSlug(slug, maybeUserId))
.map(CommentList(_))
.map(Json.toJson(_))
.map(Ok(_))
Expand All @@ -45,9 +45,9 @@ class CommentController(authenticatedAction: AuthenticatedActionBuilder,
require(StringUtils.isNotBlank(slug))

val newComment = request.body.comment
val email = request.user.email
val userId = request.user.userId

actionRunner.runTransactionally(commentService.create(newComment, slug, email)
actionRunner.runTransactionally(commentService.create(newComment, slug, userId)
.map(CommentWrapper(_))
.map(Json.toJson(_))
.map(Ok(_)))
Expand Down
2 changes: 1 addition & 1 deletion app/articles/models/ArticleTagAssociation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ case class ArticleTagAssociation(id: ArticleTagAssociationId,
tagId: TagId) extends WithId[Long, ArticleTagAssociationId]

object ArticleTagAssociation {
def from(article: Article, tag: Tag): ArticleTagAssociation =
def from(article: Article, tag: articles.models.Tag): ArticleTagAssociation =
ArticleTagAssociation(ArticleTagAssociationId(-1), article.id, tag.id)
}

Expand Down
77 changes: 37 additions & 40 deletions app/articles/repositories/ArticleWithTagsRepo.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package articles.repositories

import commons.models.{Email, Page}
import articles.models._
import commons.models.Page
import slick.dbio.DBIO
import users.models.{Profile, User, UserId}
import users.repositories.{ProfileRepo, UserRepo}
import slick.dbio.DBIO

import scala.concurrent.ExecutionContext

Expand All @@ -16,32 +16,30 @@ class ArticleWithTagsRepo(articleRepo: ArticleRepo,
profileRepo: ProfileRepo,
implicit private val ex: ExecutionContext) {

def findBySlug(slug: String, maybeUserEmail: Option[Email]): DBIO[ArticleWithTags] = {
require(slug != null && maybeUserEmail != null)
def findBySlug(slug: String, maybeUserId: Option[UserId]): DBIO[ArticleWithTags] = {
require(slug != null && maybeUserId != null)

articleRepo.findBySlug(slug)
.flatMap(article => getArticleWithTags(article, maybeUserEmail))
.flatMap(article => getArticleWithTags(article, maybeUserId))
}

private def getArticleWithTags(article: Article, maybeCurrentUserEmail: Option[Email]): DBIO[ArticleWithTags] = {
private def getArticleWithTags(article: Article, maybeUserId: Option[UserId]): DBIO[ArticleWithTags] = {
for {
tags <- articleTagRepo.findTagsByArticleId(article.id)
profile <- profileRepo.findByUserId(article.authorId, maybeCurrentUserEmail)
favorited <- isFavorited(article.id, maybeCurrentUserEmail)
profile <- profileRepo.findByUserId(article.authorId, maybeUserId)
favorited <- isFavorited(article.id, maybeUserId)
favoritesCount <- getFavoritesCount(article.id)
} yield ArticleWithTags(article, tags, profile, favorited, favoritesCount)
}

private def isFavorited(articleId: ArticleId, maybeEmail: Option[Email]): DBIO[Boolean] = {
maybeEmail.map(email => isFavorited(articleId, email))
private def isFavorited(articleId: ArticleId, maybeUserId: Option[UserId]): DBIO[Boolean] = {
maybeUserId.map(userId => isFavorited(articleId, userId))
.getOrElse(DBIO.successful(false))
}

private def isFavorited(articleId: ArticleId, email: Email) = {
for {
user <- userRepo.findByEmail(email)
maybeFavoriteAssociation <- favoriteAssociationRepo.findByUserAndArticle(user.id, articleId)
} yield maybeFavoriteAssociation.isDefined
private def isFavorited(articleId: ArticleId, userId: UserId) = {
favoriteAssociationRepo.findByUserAndArticle(userId, articleId)
.map(maybeFavoriteAssociation => maybeFavoriteAssociation.isDefined)
}

private def getFavoritesCount(articleId: ArticleId) = {
Expand All @@ -50,50 +48,49 @@ class ArticleWithTagsRepo(articleRepo: ArticleRepo,
.map(_.map(_._2).getOrElse(0))
}

def getArticleWithTags(article: Article, currentUserEmail: Email): DBIO[ArticleWithTags] = {
getArticleWithTags(article, Some(currentUserEmail))
def getArticleWithTags(article: Article, userId: UserId): DBIO[ArticleWithTags] = {
getArticleWithTags(article, Some(userId))
}

def getArticleWithTags(article: Article, tags: Seq[Tag], currentUserEmail: Email): DBIO[ArticleWithTags] = {
def getArticleWithTags(article: Article, tags: Seq[Tag], userId: UserId): DBIO[ArticleWithTags] = {
userRepo.findById(article.authorId)
.flatMap(author => getArticleWithTags(article, author, tags, Some(currentUserEmail)))
.flatMap(author => getArticleWithTags(article, author, tags, Some(userId)))
}

private def getArticleWithTags(article: Article, author: User, tags: Seq[Tag],
maybeCurrentUserEmail: Option[Email]): DBIO[ArticleWithTags] = {
maybeUserId: Option[UserId]): DBIO[ArticleWithTags] = {
for {
profile <- profileRepo.findByUserId(author.id, maybeCurrentUserEmail)
favorited <- isFavorited(article.id, maybeCurrentUserEmail)
profile <- profileRepo.findByUserId(author.id, maybeUserId)
favorited <- isFavorited(article.id, maybeUserId)
favoritesCount <- getFavoritesCount(article.id)
} yield ArticleWithTags(article, tags, profile, favorited, favoritesCount)
}

def findFeed(pageRequest: UserFeedPageRequest, currentUserEmail: Email): DBIO[Page[ArticleWithTags]] = {
require(pageRequest != null && currentUserEmail != null)
def findFeed(pageRequest: UserFeedPageRequest, userId: UserId): DBIO[Page[ArticleWithTags]] = {
require(pageRequest != null && userId != null)

getArticlesPage(pageRequest, currentUserEmail)
.flatMap(articlesPage => getArticlesWithTagsPage(articlesPage, Some(currentUserEmail)))
getArticlesPage(pageRequest, userId)
.flatMap(articlesPage => getArticlesWithTagsPage(articlesPage, Some(userId)))
}

private def getArticlesPage(pageRequest: UserFeedPageRequest, currentUserEmail: Email) = {
userRepo.findByEmail(currentUserEmail)
.flatMap(currentUser => articleRepo.findByUserFeedPageRequest(pageRequest, currentUser.id))
private def getArticlesPage(pageRequest: UserFeedPageRequest, userId: UserId) = {
articleRepo.findByUserFeedPageRequest(pageRequest, userId)
}

def findAll(pageRequest: MainFeedPageRequest, maybeCurrentUserEmail: Option[Email]): DBIO[Page[ArticleWithTags]] = {
def findAll(pageRequest: MainFeedPageRequest, maybeUserId: Option[UserId]): DBIO[Page[ArticleWithTags]] = {
require(pageRequest != null)

articleRepo.findByMainFeedPageRequest(pageRequest)
.flatMap(articlesPage => getArticlesWithTagsPage(articlesPage, maybeCurrentUserEmail))
.flatMap(articlesPage => getArticlesWithTagsPage(articlesPage, maybeUserId))
}

private def getArticlesWithTagsPage(articlesPage: Page[Article],
maybeCurrentUserEmail: Option[Email]) = {
maybeUserId: Option[UserId]) = {
val Page(articles, count) = articlesPage
for {
profileByUserId <- getProfileByUserId(articles, maybeCurrentUserEmail)
profileByUserId <- getProfileByUserId(articles, maybeUserId)
tagsByArticleId <- getGroupedTagsByArticleId(articles)
favoritedArticleIds <- getFavoritedArticleIds(articles, maybeCurrentUserEmail)
favoritedArticleIds <- getFavoritedArticleIds(articles, maybeUserId)
favoritesCountByArticleId <- getFavoritesCount(articles)
} yield {
val articlesWithTags = createArticlesWithTags(articles, profileByUserId, tagsByArticleId, favoritedArticleIds,
Expand All @@ -103,18 +100,18 @@ class ArticleWithTagsRepo(articleRepo: ArticleRepo,
}
}

private def getProfileByUserId(articles: Seq[Article], maybeCurrentUserEmail: Option[Email]) = {
private def getProfileByUserId(articles: Seq[Article], maybeUserId: Option[UserId]) = {
val authorIds = articles.map(_.authorId)
profileRepo.getProfileByUserId(authorIds, maybeCurrentUserEmail)
profileRepo.getProfileByUserId(authorIds, maybeUserId)
}

private def getFavoritedArticleIds(articles: Seq[Article], maybeFollowerEmail: Option[Email]): DBIO[Set[ArticleId]] = {
maybeFollowerEmail.map(email => getFavoritedArticleIds(articles, email))
private def getFavoritedArticleIds(articles: Seq[Article], maybeFollowerId: Option[UserId]): DBIO[Set[ArticleId]] = {
maybeFollowerId.map(id => getFavoritedArticleIds(articles, id))
.getOrElse(DBIO.successful(Set.empty))
}

private def getFavoritedArticleIds(articles: Seq[Article], followerEmail: Email): DBIO[Set[ArticleId]] = {
userRepo.findByEmail(followerEmail)
private def getFavoritedArticleIds(articles: Seq[Article], followerId: UserId): DBIO[Set[ArticleId]] = {
userRepo.findById(followerId)
.flatMap(follower => getFavoritedArticleIds(articles, follower))
}

Expand Down
Loading

0 comments on commit dedd1b9

Please sign in to comment.