CommentRepositoryImpl.kt

package com.example.realworldkotlinspringbootjdbc.infra

import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.example.realworldkotlinspringbootjdbc.domain.ArticleId
import com.example.realworldkotlinspringbootjdbc.domain.Comment
import com.example.realworldkotlinspringbootjdbc.domain.CommentRepository
import com.example.realworldkotlinspringbootjdbc.domain.article.Slug
import com.example.realworldkotlinspringbootjdbc.domain.comment.Body
import com.example.realworldkotlinspringbootjdbc.domain.comment.CommentId
import com.example.realworldkotlinspringbootjdbc.domain.user.UserId
import org.springframework.context.annotation.Primary
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import java.text.SimpleDateFormat
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.Date

@Repository
@Primary
class CommentRepositoryImpl(val namedParameterJdbcTemplate: NamedParameterJdbcTemplate) : CommentRepository {
    override fun list(slug: Slug): Either<CommentRepository.ListError, List<Comment>> {
        /**
         * article を取得
         */
        val selectArticleSql = """
            SELECT
                id AS id
            FROM
                articles
            WHERE
                articles.slug = :slug
            ;
        """.trimIndent()
        val selectArticleSqlParams = MapSqlParameterSource()
            .addValue("slug", slug.value)
        val articleList = namedParameterJdbcTemplate.queryForList(selectArticleSql, selectArticleSqlParams)

        /**
         * article が存在しなかった時 NotFoundError
         */
        if (articleList.isEmpty()) {
            return CommentRepository.ListError.NotFoundArticleBySlug(slug).left()
        }
        val articleId = ArticleId(articleList.first()["id"].toString().toInt())

        /**
         * comment を取得
         */
        val selectCommentsSql = """
            SELECT
                id AS id
                , body AS body
                , created_at AS created_at
                , updated_at AS updated_at
                , author_id AS author_id
            FROM
                article_comments
            WHERE
                article_comments.article_id = :article_id
            ;
        """.trimIndent()
        val selectCommentsSqlParams = MapSqlParameterSource()
            .addValue("article_id", articleId.value)
        val commentsMap = namedParameterJdbcTemplate.queryForList(selectCommentsSql, selectCommentsSqlParams)

        return commentsMap.map {
            Comment.newWithoutValidation(
                CommentId.newWithoutValidation(it["id"].toString().toInt()),
                Body.newWithoutValidation(it["body"].toString()),
                createdAt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(it["created_at"].toString()),
                updatedAt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(it["updated_at"].toString()),
                UserId(it["author_id"].toString().toInt()),
            )
        }.right()
    }

    override fun create(slug: Slug, body: Body, currentUserId: UserId): Either<CommentRepository.CreateError, Comment> {
        /**
         * article を取得
         */
        val selectArticleSql = """
            SELECT
                id
            FROM
                articles
            WHERE
                slug = :slug
        """.trimIndent()

        val selectArticleSqlParams = MapSqlParameterSource()
            .addValue("slug", slug.value)
        val articleFromDb = namedParameterJdbcTemplate.queryForList(selectArticleSql, selectArticleSqlParams)

        /**
         * article が存在しなかった時 NotFoundError
         */
        if (articleFromDb.isEmpty()) {
            return CommentRepository.CreateError.NotFoundArticleBySlug(slug).left()
        }
        val articleId = UserId(articleFromDb.first()["id"].toString().toInt())

        /**
         * comment を作成
         */
        val insertCommentSql = """
            INSERT INTO
                article_comments (
                    author_id
                    , article_id
                    , body
                    , created_at
                    , updated_at
                )
            VALUES (
                :author_id
                , :article_id
                , :body
                , :created_at
                , :updated_at
            )
            RETURNING
                id
            ;
        """.trimIndent()
        val now = LocalDateTime.now()
        val insertCommentSqlParams = MapSqlParameterSource()
            .addValue("author_id", currentUserId.value)
            .addValue("article_id", articleId.value)
            .addValue("body", body.value)
            .addValue("created_at", now)
            .addValue("updated_at", now)
        val commentMap = namedParameterJdbcTemplate.queryForList(insertCommentSql, insertCommentSqlParams)

        val commentId = CommentId.newWithoutValidation(commentMap.first()["id"].toString().toInt())

        return Comment.newWithoutValidation(
            commentId,
            body,
            createdAt = Date.from(now.toLocalDate().atStartOfDay(ZoneId.systemDefault()).toInstant()),
            updatedAt = Date.from(now.toLocalDate().atStartOfDay(ZoneId.systemDefault()).toInstant()),
            currentUserId
        ).right()
    }

    override fun delete(
        slug: Slug,
        commentId: CommentId,
        currentUserId: UserId
    ): Either<CommentRepository.DeleteError, Unit> {
        /**
         * article を取得
         */
        val selectArticleSql = """
            SELECT
                id
            FROM
                articles
            WHERE
                slug = :slug
        """.trimIndent()

        val selectArticleSqlParams = MapSqlParameterSource()
            .addValue("slug", slug.value)
        val articleIdMap = namedParameterJdbcTemplate.queryForList(selectArticleSql, selectArticleSqlParams)

        /**
         * article が存在しなかった時 NotFoundError
         */
        if (articleIdMap.isEmpty()) {
            return CommentRepository.DeleteError.NotFoundArticleBySlug(slug, commentId, currentUserId).left()
        }
        val articleId = articleIdMap.first()["id"].toString().toInt()

        /**
         * article_comments テーブルで以下を確認
         * - commentId に該当する article_comments.id が存在する
         * - commentId に該当するレコードの author_id カラムが currentUserId と同一である
         */
        val checkValidCommentIdOrAuthorIdSql = """
            SELECT
                COUNT(
                    CASE
                        WHEN article_comments.id = :comment_id THEN 1
                        ELSE NULL
                    END
                ) AS comment_id_count
                , COUNT(
                    CASE
                        WHEN article_comments.id = :comment_id AND article_comments.author_id = :current_user_id THEN 1
                        ELSE NULL
                    END
                ) AS author_id_count
            FROM
                article_comments
            ;
        """.trimIndent()
        val checkValidCommentIdOrAuthorIdSqlParams = MapSqlParameterSource()
            .addValue("current_user_id", currentUserId.value)
            .addValue("comment_id", commentId.value)
        val checkValidCommentIdOrAuthorIdMap = namedParameterJdbcTemplate.queryForMap(
            checkValidCommentIdOrAuthorIdSql,
            checkValidCommentIdOrAuthorIdSqlParams
        )
        val (commentIdCount, commentAuthorIdCount) = Pair(
            checkValidCommentIdOrAuthorIdMap["comment_id_count"].toString().toInt(),
            checkValidCommentIdOrAuthorIdMap["author_id_count"].toString().toInt()
        )
        /**
         * 該当するコメントが存在するとき、CommentNotFoundByCommentId エラー
         */
        if (commentIdCount == 0) {
            return CommentRepository.DeleteError.NotFoundCommentByCommentId(slug, commentId, currentUserId).left()
        }
        /**
         * 該当するコメントが存在しなかったとき、DeleteCommentNotAuthorized エラー
         */
        if (commentAuthorIdCount == 0) {
            return CommentRepository.DeleteError.NotAuthorizedDeleteComment(slug, commentId, currentUserId).left()
        }

        /**
         * comment を削除
         */
        val deleteCommentsSql = """
            DELETE FROM
                article_comments
            WHERE
                article_comments.author_id = :current_user_id
                AND article_comments.article_id = :article_id
                AND article_comments.id = :comment_id
            ;
        """.trimIndent()
        val deleteCommentSqlParams = MapSqlParameterSource()
            .addValue("current_user_id", currentUserId.value)
            .addValue("article_id", articleId)
            .addValue("comment_id", commentId.value)
        namedParameterJdbcTemplate.update(deleteCommentsSql, deleteCommentSqlParams)
        return Unit.right()
    }

    override fun deleteAll(articleId: ArticleId): Either.Right<Unit> {
        namedParameterJdbcTemplate.update(
            """
                DELETE FROM
                    article_comments
                WHERE
                    article_id = :article_id
                ;
            """.trimIndent(),
            MapSqlParameterSource().addValue("article_id", articleId.value)
        )
        return Either.Right(Unit)
    }
}