UserRepositoryImpl.kt

package com.example.realworldkotlinspringbootjdbc.infra

import arrow.core.Either
import arrow.core.left
import arrow.core.right
import arrow.core.toOption
import com.example.realworldkotlinspringbootjdbc.domain.RegisteredUser
import com.example.realworldkotlinspringbootjdbc.domain.UnregisteredUser
import com.example.realworldkotlinspringbootjdbc.domain.UpdatableRegisteredUser
import com.example.realworldkotlinspringbootjdbc.domain.UserRepository
import com.example.realworldkotlinspringbootjdbc.domain.UserRepository.FindByEmailWithPasswordError
import com.example.realworldkotlinspringbootjdbc.domain.UserRepository.FindByUserIdError
import com.example.realworldkotlinspringbootjdbc.domain.user.Bio
import com.example.realworldkotlinspringbootjdbc.domain.user.Email
import com.example.realworldkotlinspringbootjdbc.domain.user.Image
import com.example.realworldkotlinspringbootjdbc.domain.user.Password
import com.example.realworldkotlinspringbootjdbc.domain.user.UserId
import com.example.realworldkotlinspringbootjdbc.domain.user.Username
import com.example.realworldkotlinspringbootjdbc.infra.entity.Profile
import com.example.realworldkotlinspringbootjdbc.infra.entity.User
import com.example.realworldkotlinspringbootjdbc.infra.entity.profile
import com.example.realworldkotlinspringbootjdbc.infra.entity.user
import org.komapper.core.dsl.Meta
import org.komapper.core.dsl.QueryDsl
import org.komapper.core.dsl.expression.When
import org.komapper.core.dsl.operator.alias
import org.komapper.core.dsl.operator.case
import org.komapper.core.dsl.operator.count
import org.komapper.core.dsl.operator.literal
import org.komapper.core.dsl.query.first
import org.komapper.core.dsl.query.firstOrNull
import org.komapper.jdbc.JdbcDatabase
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional

typealias RegisteredWithPassword = Pair<RegisteredUser, Password>

@Repository
class UserRepositoryImpl(
    val namedParameterJdbcTemplate: NamedParameterJdbcTemplate,
    val jdbcDatabase: JdbcDatabase,
) : UserRepository {
    companion object {
        /**
         * email, usernameの重複確認用クエリ
         *
         * @param email
         * @param username
         */
        fun emailAndUsernameMatchedCountQuery(email: Email, username: Username) =
            QueryDsl.from(Meta.user).selectNotNull(
                count(case(When({ Meta.user.email eq email.value }, literal(1)))).alias("email_cnt"),
                count(case(When({ Meta.user.username eq username.value }, literal(1)))).alias("username_cnt")
            )

        fun findByEmailWithPasswordQuery(email: Email) =
            QueryDsl.from(Meta.user).innerJoin(Meta.profile) {
                Meta.profile.userId eq Meta.user.id
                and { Meta.user.email eq email.value }
            }.select(
                Meta.user.id,
                Meta.user.email,
                Meta.user.username,
                Meta.user.password,
                Meta.profile.bio,
                Meta.profile.image,
            )
    }

    override fun register(user: UnregisteredUser): Either<UserRepository.RegisterError, RegisteredUser> {
        /**
         * Email と Username の数をそれぞれカウント
         */
        val (emailCount, usernameCount) = jdbcDatabase.runQuery {
            emailAndUsernameMatchedCountQuery(user.email, user.username).first()
        }

        return when {
            /**
             * エラー: Email が既に使われている
             */
            0 < emailCount -> UserRepository.RegisterError.AlreadyRegisteredEmail(user.email).left()
            /**
             * エラー: Username が既に使われている
             */
            0 < usernameCount -> UserRepository.RegisterError.AlreadyRegisteredUsername(user.username).left()
            /**
             * ユーザー登録
             */
            else -> {
                val userId = registerTransactionApply(user)
                RegisteredUser.newWithoutValidation(
                    userId,
                    user.email,
                    user.username,
                    Bio.newWithoutValidation(""),
                    Image.newWithoutValidation("")
                ).right()
            }
        }
    }

    /**
     * ユーザー登録処理
     */
    @Transactional
    fun registerTransactionApply(user: UnregisteredUser): UserId {
        val userEntity = jdbcDatabase.runQuery {
            QueryDsl.insert(Meta.user).single(
                User(
                    email = user.email.value,
                    password = user.password.value,
                    username = user.username.value
                )
            )
        }
        jdbcDatabase.runQuery {
            QueryDsl.insert(Meta.profile).single(
                Profile(
                    userId = userEntity.id,
                    bio = "",
                    image = "",
                )
            )
        }
        return UserId(userEntity.id)
    }

    override fun findByEmailWithPassword(email: Email): Either<FindByEmailWithPasswordError, RegisteredWithPassword> {
        val userRecord = jdbcDatabase.runQuery {
            findByEmailWithPasswordQuery(email).firstOrNull()
        }.toOption().fold(
            /**
             * 見つからなかった
             */
            { return FindByEmailWithPasswordError.NotFound(email).left() },
            /**
             * 見つかった
             */
            { it }
        )

        val user = RegisteredUser.newWithoutValidation(
            UserId(userRecord[Meta.user.id].toString().toInt()),
            Email.newWithoutValidation(userRecord[Meta.user.email].toString()),
            Username.newWithoutValidation(userRecord[Meta.user.username].toString()),
            Bio.newWithoutValidation(userRecord[Meta.profile.bio].toString()),
            Image.newWithoutValidation(userRecord[Meta.profile.image].toString()),
        )
        val password = Password.newWithoutValidation(userRecord[Meta.user.password].toString())
        return Pair(user, password).right()
    }

    override fun findByUserId(id: UserId): Either<UserRepository.FindByUserIdError, RegisteredUser> {
        val sql = """
            SELECT
                users.id
                , users.email
                , users.username
                , users.password
                , profiles.bio
                , profiles.image
            FROM
                users
            JOIN
                profiles
            ON
                profiles.user_id = users.id
                AND users.id = :user_id
            ;
        """.trimIndent()
        val sqlParams = MapSqlParameterSource().addValue("user_id", id.value)
        val users = namedParameterJdbcTemplate.queryForList(sql, sqlParams)

        return when {
            /**
             * エラー: ユーザーが見つからなかった
             */
            users.isEmpty() -> FindByUserIdError.NotFound(id).left()
            /**
             * ユーザーが見つかった
             */
            else -> {
                val userRecord = users.first()
                RegisteredUser.newWithoutValidation(
                    UserId(userRecord["id"].toString().toInt()),
                    Email.newWithoutValidation(userRecord["email"].toString()),
                    Username.newWithoutValidation(userRecord["username"].toString()),
                    Bio.newWithoutValidation(userRecord["bio"].toString()),
                    Image.newWithoutValidation(userRecord["image"].toString()),
                ).right()
            }
        }
    }

    @Transactional
    override fun update(user: UpdatableRegisteredUser): Either<UserRepository.UpdateError, Unit> {
        /**
         * UserId と Email と Username の数をそれぞれカウント
         */
        val sql = """
            SELECT
                COUNT(
                    CASE
                         WHEN
                             users.id = :user_id
                         THEN 1
                         ELSE NULL
                    END
                ) AS USER_CNT
                , COUNT(
                    CASE
                         WHEN
                             users.id != :user_id
                             AND users.email = :email
                         THEN 1
                         ELSE NULL
                    END
                ) AS EMAIL_CNT
                , COUNT(
                    CASE
                         WHEN
                             users.id != :user_id
                             AND users.username = :username
                         THEN 1
                         ELSE NULL
                    END
                ) AS USERNAME_CNT
            FROM
                users
            ;
        """.trimIndent()
        val sqlParams = MapSqlParameterSource()
            .addValue("user_id", user.userId.value)
            .addValue("email", user.email.value)
            .addValue("username", user.username.value)
        val countMap = namedParameterJdbcTemplate.queryForMap(sql, sqlParams)
        val userCount = countMap["user_cnt"].toString().toInt()
        val emailCount = countMap["email_cnt"].toString().toInt()
        val usernameCount = countMap["username_cnt"].toString().toInt()

        return when {
            /**
             * エラー: ユーザー が見つからなかった
             */
            userCount < 1 -> UserRepository.UpdateError.NotFound(user.userId).left()
            /**
             * エラー: Email が既に使われている
             */
            0 < emailCount -> UserRepository.UpdateError.AlreadyRegisteredEmail(user.email).left()
            /**
             * エラー: Username が既に使われている
             */
            0 < usernameCount -> UserRepository.UpdateError.AlreadyRegisteredUsername(user.username).left()
            /**
             * ユーザー 更新
             */
            else -> {
                val updatedCount = namedParameterJdbcTemplate.update(
                    """
                        UPDATE
                            users
                        SET
                            email = :email
                            , username = :username
                            , updated_at = :updated_at
                        WHERE
                            id = :user_id
                        ;
                        UPDATE
                            profiles
                        SET
                            bio = :bio
                            , image = :image
                            , updated_at = :updated_at
                        WHERE
                            user_id = :user_id
                        ;
                    """.trimIndent(),
                    MapSqlParameterSource()
                        .addValue("user_id", user.userId.value)
                        .addValue("email", user.email.value)
                        .addValue("username", user.username.value)
                        .addValue("bio", user.bio.value)
                        .addValue("image", user.image.value)
                        .addValue("updated_at", user.updatedAt)
                )
                when (updatedCount) {
                    /**
                     * エラー: ユーザーが見つからなかった
                     */
                    0 -> UserRepository.UpdateError.NotFound(user.userId).left()
                    /**
                     * 成功
                     */
                    else -> Unit.right()
                }
            }
        }
    }
}