Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions realworld-app/web/src/main/kotlin/io/realworld/Application.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package io.realworld

import arrow.core.getOrElse
import arrow.effects.ForIO
import arrow.effects.IO
import arrow.effects.fix
import arrow.effects.instances.io.monadDefer.monadDefer
import io.realworld.domain.common.Auth
import io.realworld.domain.common.Settings
import io.realworld.domain.common.Token
Expand Down Expand Up @@ -35,22 +39,25 @@ class Application : WebMvcConfigurer {
@Bean
fun userCreator() = object : (Token) -> User {
@Autowired
lateinit var repo: UserRepository
lateinit var repo: UserRepository<ForIO>

override fun invoke(token: Token): User {
return repo.findById(token.id).unsafeRunSync().map { it.user }.getOrElse { throw UnauthorizedException() }
return repo.findById(token.id).fix().unsafeRunSync().map { it.user }.getOrElse { throw UnauthorizedException() }
}
}

@Bean
fun auth() = Auth(settings().security)

@Bean
fun userRepository(jdbcTemplate: NamedParameterJdbcTemplate) = UserRepository(jdbcTemplate)
fun userRepository(jdbcTemplate: NamedParameterJdbcTemplate) = UserRepository(jdbcTemplate, IO.monadDefer())

@Bean
fun articleRepository(jdbcTemplate: NamedParameterJdbcTemplate, userRepository: UserRepository) = ArticleRepository(
jdbcTemplate, userRepository
fun articleRepository(
jdbcTemplate: NamedParameterJdbcTemplate,
userRepository: UserRepository<ForIO>
) = ArticleRepository(
jdbcTemplate, userRepository, IO.monadDefer()
)

override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
Expand Down
8 changes: 4 additions & 4 deletions realworld-app/web/src/main/kotlin/io/realworld/Tx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import org.springframework.transaction.support.TransactionTemplate
fun <A> IO<A>.runReadTx(txManager: PlatformTransactionManager): A = TransactionTemplate(txManager)
.apply {
isReadOnly = true
setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED)
isolationLevel = TransactionDefinition.ISOLATION_READ_COMMITTED
propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW
}
.execute({ this.unsafeRunSync() })!!
.execute { this.unsafeRunSync() }!!

fun <A> IO<A>.runWriteTx(txManager: PlatformTransactionManager): A = TransactionTemplate(txManager)
.apply {
setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED)
isolationLevel = TransactionDefinition.ISOLATION_READ_COMMITTED
propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRES_NEW
}
.execute({ this.unsafeRunSync() })!!
.execute { this.unsafeRunSync() }!!
95 changes: 59 additions & 36 deletions realworld-app/web/src/main/kotlin/io/realworld/articles/Routes.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.realworld.articles

import arrow.effects.ForIO
import arrow.effects.IO
import arrow.effects.fix
import arrow.effects.instances.io.monadDefer.monadDefer
import io.realworld.ForbiddenException
import io.realworld.JwtTokenResolver
import io.realworld.authHeader
Expand Down Expand Up @@ -31,7 +35,7 @@ import io.realworld.domain.articles.GetArticlesUseCase
import io.realworld.domain.articles.GetCommentsCommand
import io.realworld.domain.articles.GetCommentsUseCase
import io.realworld.domain.articles.GetFeedsCommand
import io.realworld.domain.articles.GetFeedsUsecase
import io.realworld.domain.articles.GetFeedsUseCase
import io.realworld.domain.articles.GetTagsCommand
import io.realworld.domain.articles.GetTagsUseCase
import io.realworld.domain.articles.UnfavoriteArticleCommand
Expand Down Expand Up @@ -96,25 +100,28 @@ data class TagsResponse(val tags: Set<String>)
@RestController
class ArticleController(
private val auth: Auth,
private val articleRepo: ArticleRepository,
private val userRepo: UserRepository,
private val articleRepo: ArticleRepository<ForIO>,
private val userRepo: UserRepository<ForIO>,
private val txManager: PlatformTransactionManager
) {
@PostMapping("/api/articles")
fun create(@Valid @RequestBody dto: CreationDto, user: User): ResponseEntity<ArticleResponse> {
val createUniqueSlugSrv = object : CreateUniqueSlugService {
val ioMonadDefer = IO.monadDefer()
val createUniqueSlugSrv = object : CreateUniqueSlugService<ForIO> {
override val existsBySlug = articleRepo::existsBySlug
override val MD = ioMonadDefer
}

return object : CreateArticleUseCase {
return object : CreateArticleUseCase<ForIO> {
override val createUniqueSlug = createUniqueSlugSrv::slufigy
override val createArticle = articleRepo::create
override val MD = ioMonadDefer
}.run {
CreateArticleCommand(
data = dto.toDomain(),
user = user
).runUseCase()
}.runWriteTx(txManager).let {
}.fix().runWriteTx(txManager).let {
ResponseEntity.status(HttpStatus.CREATED).body(ArticleResponse.fromDomain(it))
}
}
Expand All @@ -128,15 +135,16 @@ class ArticleController(
val user = JwtTokenResolver(auth::parse)(
webRequest.authHeader()
).toOption().flatMap {
userRepo.findById(it.id).unsafeRunSync().map { it.user }
userRepo.findById(it.id).fix().unsafeRunSync().map { it.user }
}

return object : GetArticlesUseCase {
return object : GetArticlesUseCase<ForIO> {
override val getArticles = articleRepo::getArticles
override val getArticlesCount = articleRepo::getArticlesCount
override val MD = IO.monadDefer()
}.run {
GetArticlesCommand(filter, user).runUseCase()
}.runReadTx(txManager).let {
}.fix().runReadTx(txManager).let {
ResponseEntity.ok(ArticlesResponse.fromDomain(it))
}
}
Expand All @@ -146,12 +154,13 @@ class ArticleController(
filter: FeedFilter,
user: User
): ResponseEntity<ArticlesResponse> {
return object : GetFeedsUsecase {
return object : GetFeedsUseCase<ForIO> {
override val getFeeds = articleRepo::getFeeds
override val getFeedsCount = articleRepo::getFeedsCount
override val MD = IO.monadDefer()
}.run {
GetFeedsCommand(filter, user).runUseCase()
}.runReadTx(txManager).let {
}.fix().runReadTx(txManager).let {
ResponseEntity.ok(ArticlesResponse.fromDomain(it))
}
}
Expand All @@ -165,14 +174,14 @@ class ArticleController(
val user = JwtTokenResolver(auth::parse)(
webRequest.authHeader()
).toOption().flatMap {
userRepo.findById(it.id).unsafeRunSync().map { it.user }
userRepo.findById(it.id).fix().unsafeRunSync().map { it.user }
}

return object : GetArticleUseCase {
return object : GetArticleUseCase<ForIO> {
override val getArticleBySlug = articleRepo::getBySlug
}.run {
GetArticleCommand(slug, user).runUseCase()
}.runReadTx(txManager).fold(
}.fix().runReadTx(txManager).fold(
{ ResponseEntity.notFound().build() },
{ ResponseEntity.ok(ArticleResponse.fromDomain(it)) }
)
Expand All @@ -183,16 +192,20 @@ class ArticleController(
@PathVariable("slug") slug: String,
user: User
): ResponseEntity<Unit> {
return object : DeleteArticleUseCase {
return object : DeleteArticleUseCase<ForIO> {
override val getArticleBySlug = articleRepo::getBySlug
override val deleteArticle = articleRepo::deleteArticle
override val ME = IO.monadDefer()
}.run {
DeleteArticleCommand(slug, user).runUseCase()
}.runWriteTx(txManager).fold(
}.fix().attempt().runWriteTx(txManager).fold(
{
when (it) {
is ArticleDeleteError.NotAuthor -> throw ForbiddenException()
is ArticleDeleteError.NotFound -> ResponseEntity.notFound().build()
is ArticleDeleteError -> when (it) {
is ArticleDeleteError.NotAuthor -> throw ForbiddenException()
is ArticleDeleteError.NotFound -> ResponseEntity.notFound().build()
}
else -> throw it
}
},
{ ResponseEntity.noContent().build() }
Expand All @@ -205,21 +218,26 @@ class ArticleController(
@Valid @RequestBody update: UpdateDto,
user: User
): ResponseEntity<ArticleResponse> {
val createUniqueSlugSrv = object : CreateUniqueSlugService {
val ioMonadDefer = IO.monadDefer()
val createUniqueSlugSrv = object : CreateUniqueSlugService<ForIO> {
override val existsBySlug = articleRepo::existsBySlug
override val MD = ioMonadDefer
}

val validateUpdateSrv = object : ValidateArticleUpdateService {
val validateUpdateSrv = object : ValidateArticleUpdateService<ForIO> {
override val createUniqueSlug = createUniqueSlugSrv::slufigy
override val getArticleBySlug = articleRepo::getBySlug
override val MD = ioMonadDefer
}

return object : UpdateArticleUseCase {
override val validateUpdate: ValidateArticleUpdate = { x, y, z -> validateUpdateSrv.run { x.validate(y, z) } }
return object : UpdateArticleUseCase<ForIO> {
override val validateUpdate: ValidateArticleUpdate<ForIO> =
{ x, y, z -> validateUpdateSrv.run { x.validate(y, z) } }
override val updateArticle = articleRepo::updateArticle
override val MD = ioMonadDefer
}.run {
UpdateArticleCommand(update.toDomain(), slug, user).runUseCase()
}.runWriteTx(txManager).fold(
}.fix().runWriteTx(txManager).fold(
{
when (it) {
is ArticleUpdateError.NotAuthor -> throw ForbiddenException()
Expand All @@ -236,12 +254,13 @@ class ArticleController(
user: User
): ResponseEntity<ArticleResponse> {

return object : FavoriteUseCase {
return object : FavoriteUseCase<ForIO> {
override val getArticleBySlug = articleRepo::getBySlug
override val addFavorite = articleRepo::addFavorite
override val MD = IO.monadDefer()
}.run {
FavoriteArticleCommand(slug, user).runUseCase()
}.runWriteTx(txManager).fold(
}.fix().runWriteTx(txManager).fold(
{
when (it) {
is ArticleFavoriteError.Author -> throw ForbiddenException()
Expand All @@ -257,12 +276,13 @@ class ArticleController(
@PathVariable("slug") slug: String,
user: User
): ResponseEntity<ArticleResponse> {
return object : UnfavoriteUseCase {
return object : UnfavoriteUseCase<ForIO> {
override val getArticleBySlug = articleRepo::getBySlug
override val removeFavorite = articleRepo::removeFavorite
override val MD = IO.monadDefer()
}.run {
UnfavoriteArticleCommand(slug, user).runUseCase()
}.runWriteTx(txManager).fold(
}.fix().runWriteTx(txManager).fold(
{
when (it) {
is ArticleUnfavoriteError.NotFound -> ResponseEntity.notFound().build()
Expand All @@ -281,15 +301,16 @@ class ArticleController(
val user = JwtTokenResolver(auth::parse)(
webRequest.authHeader()
).toOption().flatMap {
userRepo.findById(it.id).unsafeRunSync().map { it.user }
userRepo.findById(it.id).fix().unsafeRunSync().map { it.user }
}

return object : GetCommentsUseCase {
return object : GetCommentsUseCase<ForIO> {
override val getArticleBySlug = articleRepo::getBySlug
override val getComments = articleRepo::getComments
override val MD = IO.monadDefer()
}.run {
GetCommentsCommand(slug, user).runUseCase()
}.runReadTx(txManager).fold(
}.fix().runReadTx(txManager).fold(
{ ResponseEntity.notFound().build() },
{ ResponseEntity.ok(CommentsResponse.fromDomain(it)) }
)
Expand All @@ -301,12 +322,13 @@ class ArticleController(
@Valid @RequestBody comment: CommentDto,
user: User
): ResponseEntity<CommentResponse> {
return object : CommentUseCase {
return object : CommentUseCase<ForIO> {
override val getArticleBySlug = articleRepo::getBySlug
override val addComment = articleRepo::addComment
override val MD = IO.monadDefer()
}.run {
CommentArticleCommand(slug, comment.body, user).runUsecase()
}.runWriteTx(txManager).fold(
}.fix().runWriteTx(txManager).fold(
{
when (it) {
is ArticleCommentError.NotFound -> ResponseEntity.notFound().build()
Expand All @@ -322,13 +344,14 @@ class ArticleController(
@PathVariable("id") commentId: Long,
user: User
): ResponseEntity<Void> {
return object : DeleteCommentUseCase {
return object : DeleteCommentUseCase<ForIO> {
override val getArticleBySlug = articleRepo::getBySlug
override val deleteComment = articleRepo::deleteComment
override val getComment = articleRepo::getComment
override val MD = IO.monadDefer()
}.run {
DeleteCommentCommand(slug, commentId, user).runUseCase()
}.runWriteTx(txManager).fold(
}.fix().runWriteTx(txManager).fold(
{
when (it) {
is ArticleCommentDeleteError.ArticleNotFound -> ResponseEntity.notFound().build()
Expand All @@ -342,11 +365,11 @@ class ArticleController(

@GetMapping("/api/tags")
fun getTags(): ResponseEntity<TagsResponse> {
return object : GetTagsUseCase {
return object : GetTagsUseCase<ForIO> {
override val getTags = articleRepo::getTags
}.run {
GetTagsCommand.runUseCase()
}.runReadTx(txManager).let {
}.fix().runReadTx(txManager).let {
ResponseEntity.ok(TagsResponse(it))
}
}
Expand Down
22 changes: 13 additions & 9 deletions realworld-app/web/src/main/kotlin/io/realworld/profiles/Routes.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.realworld.profiles

import arrow.effects.ForIO
import arrow.effects.fix
import io.realworld.JwtTokenResolver
import io.realworld.authHeader
import io.realworld.domain.common.Auth
Expand Down Expand Up @@ -32,9 +34,8 @@ data class ProfileResponse(val profile: ProfileResponseDto) {
@RestController
class ProfileController(
private val auth: Auth,
private val repo: UserRepository,
private val repo: UserRepository<ForIO>,
private val txManager: PlatformTransactionManager

) {
@GetMapping("/api/profiles/{username}")
fun getProfile(
Expand All @@ -45,15 +46,16 @@ class ProfileController(
val user = JwtTokenResolver(auth::parse)(
webRequest.authHeader()
).toOption().flatMap {
repo.findById(it.id).unsafeRunSync().map { it.user }
repo.findById(it.id).fix().unsafeRunSync().map { it.user }
}

return object : GetProfileUseCase {
return object : GetProfileUseCase<ForIO> {
override val getUser = repo::findByUsername
override val hasFollower = repo::hasFollower
override val M = repo.MD
}.run {
GetProfileCommand(username, user).runUseCase()
}.runReadTx(txManager).fold(
}.fix().runReadTx(txManager).fold(
{ ResponseEntity.notFound().build() },
{ ResponseEntity.ok(ProfileResponse.fromDomain(it)) }
)
Expand All @@ -64,12 +66,13 @@ class ProfileController(
@PathVariable("username") username: String,
current: User
): ResponseEntity<ProfileResponse> {
return object : FollowUseCase {
return object : FollowUseCase<ForIO> {
override val addFollower = repo::addFollower
override val getUser = repo::findByUsername
override val M = repo.MD
}.run {
FollowCommand(username, current).runUseCase()
}.runWriteTx(txManager).fold(
}.fix().runWriteTx(txManager).fold(
{ ResponseEntity.notFound().build() },
{ ResponseEntity.ok(ProfileResponse.fromDomain(it)) }
)
Expand All @@ -80,12 +83,13 @@ class ProfileController(
@PathVariable("username") username: String,
current: User
): ResponseEntity<ProfileResponse> {
return object : UnfollowUseCase {
return object : UnfollowUseCase<ForIO> {
override val getUser = repo::findByUsername
override val removeFollower = repo::removeFollower
override val M = repo.MD
}.run {
UnfollowCommand(username, current).runUseCase()
}.runWriteTx(txManager).fold(
}.fix().runWriteTx(txManager).fold(
{ ResponseEntity.notFound().build() },
{ ResponseEntity.ok(ProfileResponse.fromDomain(it)) }
)
Expand Down
Loading