Merge pull request #94 from usbharu/feature/entity-builder

Feature/entity builder
This commit is contained in:
usbharu 2023-10-23 14:00:36 +09:00 committed by GitHub
commit 8347bd7d0f
23 changed files with 273 additions and 198 deletions

View File

@ -1,64 +0,0 @@
package dev.usbharu.hideout.config
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@Deprecated("Config is deprecated")
object Config {
var configData: ConfigData = ConfigData()
}
@Deprecated("Config is deprecated")
data class ConfigData(
val url: String = "",
val domain: String = url.substringAfter("://").substringBeforeLast(":"),
val objectMapper: ObjectMapper = jacksonObjectMapper(),
val characterLimit: CharacterLimit = CharacterLimit()
)
@Deprecated("Config is deprecated")
data class CharacterLimit(
val general: General = General.of(),
val post: Post = Post(),
val account: Account = Account(),
val instance: Instance = Instance()
) {
@Deprecated("Config is deprecated")
data class General private constructor(
val url: Int,
val domain: Int,
val publicKey: Int,
val privateKey: Int
) {
companion object {
@Suppress("FunctionMinLength")
fun of(url: Int? = null, domain: Int? = null, publicKey: Int? = null, privateKey: Int? = null): General {
return General(
url ?: 1000,
domain ?: 1000,
publicKey ?: 10000,
privateKey ?: 10000
)
}
}
}
@Deprecated("Config is deprecated")
data class Post(
val text: Int = 3000,
val overview: Int = 3000
)
@Deprecated("Config is deprecated")
data class Account(
val id: Int = 300,
val name: Int = 300,
val description: Int = 10000
)
@Deprecated("Config is deprecated")
data class Instance(
val name: Int = 600,
val description: Int = 10000
)
}

View File

@ -43,3 +43,35 @@ data class StorageConfig(
val accessKey: String, val accessKey: String,
val secretKey: String val secretKey: String
) )
@ConfigurationProperties("hideout.character-limit")
data class CharacterLimit(
val general: General = General(),
val post: Post = Post(),
val account: Account = Account(),
val instance: Instance = Instance()
) {
data class General(
val url: Int = 1000,
val domain: Int = 1000,
val publicKey: Int = 10000,
val privateKey: Int = 10000
)
data class Post(
val text: Int = 3000,
val overview: Int = 3000
)
data class Account(
val id: Int = 300,
val name: Int = 300,
val description: Int = 10000
)
data class Instance(
val name: Int = 600,
val description: Int = 10000
)
}

View File

@ -1,6 +1,7 @@
package dev.usbharu.hideout.domain.model.hideout.entity package dev.usbharu.hideout.domain.model.hideout.entity
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.CharacterLimit
import org.springframework.stereotype.Component
data class Post private constructor( data class Post private constructor(
val id: Long, val id: Long,
@ -16,7 +17,9 @@ data class Post private constructor(
val apId: String = url, val apId: String = url,
val mediaIds: List<Long> = emptyList() val mediaIds: List<Long> = emptyList()
) { ) {
companion object {
@Component
class PostBuilder(private val characterLimit: CharacterLimit) {
@Suppress("FunctionMinLength", "LongParameterList") @Suppress("FunctionMinLength", "LongParameterList")
fun of( fun of(
id: Long, id: Long,
@ -32,8 +35,6 @@ data class Post private constructor(
apId: String = url, apId: String = url,
mediaIds: List<Long> = emptyList() mediaIds: List<Long> = emptyList()
): Post { ): Post {
val characterLimit = Config.configData.characterLimit
require(id >= 0) { "id must be greater than or equal to 0." } require(id >= 0) { "id must be greater than or equal to 0." }
require(userId >= 0) { "userId must be greater than or equal to 0." } require(userId >= 0) { "userId must be greater than or equal to 0." }

View File

@ -1,7 +1,9 @@
package dev.usbharu.hideout.domain.model.hideout.entity package dev.usbharu.hideout.domain.model.hideout.entity
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.config.CharacterLimit
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.time.Instant import java.time.Instant
data class User private constructor( data class User private constructor(
@ -27,9 +29,9 @@ data class User private constructor(
" privateKey=$privateKey, createdAt=$createdAt, keyId='$keyId', followers=$followers," + " privateKey=$privateKey, createdAt=$createdAt, keyId='$keyId', followers=$followers," +
" following=$following)" " following=$following)"
companion object { @Component
class UserBuilder(private val characterLimit: CharacterLimit, private val applicationConfig: ApplicationConfig) {
private val logger = LoggerFactory.getLogger(User::class.java) private val logger = LoggerFactory.getLogger(UserBuilder::class.java)
@Suppress("LongParameterList", "FunctionMinLength", "LongMethod") @Suppress("LongParameterList", "FunctionMinLength", "LongMethod")
fun of( fun of(
@ -49,8 +51,6 @@ data class User private constructor(
following: String? = null, following: String? = null,
followers: String? = null followers: String? = null
): User { ): User {
val characterLimit = Config.configData.characterLimit
// idは0未満ではいけない // idは0未満ではいけない
require(id >= 0) { "id must be greater than or equal to 0." } require(id >= 0) { "id must be greater than or equal to 0." }
@ -93,7 +93,7 @@ data class User private constructor(
} }
// ローカルユーザーはpasswordとprivateKeyをnullにしてはいけない // ローカルユーザーはpasswordとprivateKeyをnullにしてはいけない
if (domain == Config.configData.domain) { if (domain == applicationConfig.url.host) {
requireNotNull(password) { "password and privateKey must not be null for local users." } requireNotNull(password) { "password and privateKey must not be null for local users." }
requireNotNull(privateKey) { "password and privateKey must not be null for local users." } requireNotNull(privateKey) { "password and privateKey must not be null for local users." }
} }

View File

@ -9,7 +9,7 @@ import org.springframework.stereotype.Repository
import java.time.Instant import java.time.Instant
@Repository @Repository
class FollowerQueryServiceImpl : FollowerQueryService { class FollowerQueryServiceImpl(private val userBuilder: User.UserBuilder) : FollowerQueryService {
override suspend fun findFollowersById(id: Long): List<User> { override suspend fun findFollowersById(id: Long): List<User> {
val followers = Users.alias("FOLLOWERS") val followers = Users.alias("FOLLOWERS")
return Users.innerJoin( return Users.innerJoin(
@ -41,7 +41,7 @@ class FollowerQueryServiceImpl : FollowerQueryService {
) )
.select { Users.id eq id } .select { Users.id eq id }
.map { .map {
User.of( userBuilder.of(
id = it[followers[Users.id]], id = it[followers[Users.id]],
name = it[followers[Users.name]], name = it[followers[Users.name]],
domain = it[followers[Users.domain]], domain = it[followers[Users.domain]],
@ -92,7 +92,7 @@ class FollowerQueryServiceImpl : FollowerQueryService {
) )
.select { Users.name eq name and (Users.domain eq domain) } .select { Users.name eq name and (Users.domain eq domain) }
.map { .map {
User.of( userBuilder.of(
id = it[followers[Users.id]], id = it[followers[Users.id]],
name = it[followers[Users.name]], name = it[followers[Users.name]],
domain = it[followers[Users.domain]], domain = it[followers[Users.domain]],
@ -143,7 +143,7 @@ class FollowerQueryServiceImpl : FollowerQueryService {
) )
.select { followers[Users.id] eq id } .select { followers[Users.id] eq id }
.map { .map {
User.of( userBuilder.of(
id = it[followers[Users.id]], id = it[followers[Users.id]],
name = it[followers[Users.name]], name = it[followers[Users.name]],
domain = it[followers[Users.domain]], domain = it[followers[Users.domain]],
@ -194,7 +194,7 @@ class FollowerQueryServiceImpl : FollowerQueryService {
) )
.select { followers[Users.name] eq name and (followers[Users.domain] eq domain) } .select { followers[Users.name] eq name and (followers[Users.domain] eq domain) }
.map { .map {
User.of( userBuilder.of(
id = it[followers[Users.id]], id = it[followers[Users.id]],
name = it[followers[Users.name]], name = it[followers[Users.name]],
domain = it[followers[Users.domain]], domain = it[followers[Users.domain]],

View File

@ -4,27 +4,32 @@ import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.exception.FailedToGetResourcesException import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.repository.Posts import dev.usbharu.hideout.repository.Posts
import dev.usbharu.hideout.repository.PostsMedia import dev.usbharu.hideout.repository.PostsMedia
import dev.usbharu.hideout.repository.toPost import dev.usbharu.hideout.repository.QueryMapper
import dev.usbharu.hideout.repository.ResultRowMapper
import dev.usbharu.hideout.util.singleOr import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
class PostQueryServiceImpl : PostQueryService { class PostQueryServiceImpl(
private val postResultRowMapper: ResultRowMapper<Post>,
private val postQueryMapper: QueryMapper<Post>
) : PostQueryService {
override suspend fun findById(id: Long): Post = override suspend fun findById(id: Long): Post =
Posts.leftJoin(PostsMedia) Posts.leftJoin(PostsMedia)
.select { Posts.id eq id } .select { Posts.id eq id }
.singleOr { FailedToGetResourcesException("id: $id is duplicate or does not exist.", it) }.toPost() .singleOr { FailedToGetResourcesException("id: $id is duplicate or does not exist.", it) }
.let(postResultRowMapper::map)
override suspend fun findByUrl(url: String): Post = override suspend fun findByUrl(url: String): Post =
Posts.leftJoin(PostsMedia) Posts.leftJoin(PostsMedia)
.select { Posts.url eq url } .select { Posts.url eq url }
.toPost() .let(postQueryMapper::map)
.singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) } .singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) }
override suspend fun findByApId(string: String): Post = override suspend fun findByApId(string: String): Post =
Posts.leftJoin(PostsMedia) Posts.leftJoin(PostsMedia)
.select { Posts.apId eq string } .select { Posts.apId eq string }
.toPost() .let(postQueryMapper::map)
.singleOr { FailedToGetResourcesException("apId: $string is duplicate or does not exist.", it) } .singleOr { FailedToGetResourcesException("apId: $string is duplicate or does not exist.", it) }
} }

View File

@ -2,8 +2,9 @@ package dev.usbharu.hideout.query
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.exception.FailedToGetResourcesException import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.repository.QueryMapper
import dev.usbharu.hideout.repository.ResultRowMapper
import dev.usbharu.hideout.repository.Users import dev.usbharu.hideout.repository.Users
import dev.usbharu.hideout.repository.toUser
import dev.usbharu.hideout.util.singleOr import dev.usbharu.hideout.util.singleOr
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
@ -12,17 +13,22 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
class UserQueryServiceImpl : UserQueryService { class UserQueryServiceImpl(
private val userResultRowMapper: ResultRowMapper<User>,
private val userQueryMapper: QueryMapper<User>
) : UserQueryService {
private val logger = LoggerFactory.getLogger(UserQueryServiceImpl::class.java) private val logger = LoggerFactory.getLogger(UserQueryServiceImpl::class.java)
override suspend fun findAll(limit: Int, offset: Long): List<User> = override suspend fun findAll(limit: Int, offset: Long): List<User> =
Users.selectAll().limit(limit, offset).map { it.toUser() } Users.selectAll().limit(limit, offset).let(userQueryMapper::map)
override suspend fun findById(id: Long): User = Users.select { Users.id eq id } override suspend fun findById(id: Long): User = Users.select { Users.id eq id }
.singleOr { FailedToGetResourcesException("id: $id is duplicate or does not exist.", it) }.toUser() .singleOr { FailedToGetResourcesException("id: $id is duplicate or does not exist.", it) }
.let(userResultRowMapper::map)
override suspend fun findByName(name: String): List<User> = Users.select { Users.name eq name }.map { it.toUser() } override suspend fun findByName(name: String): List<User> =
Users.select { Users.name eq name }.let(userQueryMapper::map)
override suspend fun findByNameAndDomain(name: String, domain: String): User = override suspend fun findByNameAndDomain(name: String, domain: String): User =
Users Users
@ -30,17 +36,17 @@ class UserQueryServiceImpl : UserQueryService {
.singleOr { .singleOr {
FailedToGetResourcesException("name: $name,domain: $domain is duplicate or does not exist.", it) FailedToGetResourcesException("name: $name,domain: $domain is duplicate or does not exist.", it)
} }
.toUser() .let(userResultRowMapper::map)
override suspend fun findByUrl(url: String): User { override suspend fun findByUrl(url: String): User {
logger.trace("findByUrl url: $url") logger.trace("findByUrl url: $url")
return Users.select { Users.url eq url } return Users.select { Users.url eq url }
.singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) } .singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) }
.toUser() .let(userResultRowMapper::map)
} }
override suspend fun findByIds(ids: List<Long>): List<User> = override suspend fun findByIds(ids: List<Long>): List<User> =
Users.select { Users.id inList ids }.map { it.toUser() } Users.select { Users.id inList ids }.let(userQueryMapper::map)
override suspend fun existByNameAndDomain(name: String, domain: String): Boolean = override suspend fun existByNameAndDomain(name: String, domain: String): Boolean =
Users.select { Users.name eq name and (Users.domain eq domain) }.empty().not() Users.select { Users.name eq name and (Users.domain eq domain) }.empty().not()
@ -48,6 +54,6 @@ class UserQueryServiceImpl : UserQueryService {
override suspend fun findByKeyId(keyId: String): User { override suspend fun findByKeyId(keyId: String): User {
return Users.select { Users.keyId eq keyId } return Users.select { Users.keyId eq keyId }
.singleOr { FailedToGetResourcesException("keyId: $keyId is duplicate or does not exist.", it) } .singleOr { FailedToGetResourcesException("keyId: $keyId is duplicate or does not exist.", it) }
.toUser() .let(userResultRowMapper::map)
} }
} }

View File

@ -13,14 +13,15 @@ import org.springframework.stereotype.Repository
import java.time.Instant import java.time.Instant
@Repository @Repository
class NoteQueryServiceImpl(private val postRepository: PostRepository) : NoteQueryService { class NoteQueryServiceImpl(private val postRepository: PostRepository, private val postQueryMapper: QueryMapper<Post>) :
NoteQueryService {
override suspend fun findById(id: Long): Pair<Note, Post> { override suspend fun findById(id: Long): Pair<Note, Post> {
return Posts return Posts
.leftJoin(Users) .leftJoin(Users)
.leftJoin(PostsMedia) .leftJoin(PostsMedia)
.leftJoin(Media) .leftJoin(Media)
.select { Posts.id eq id } .select { Posts.id eq id }
.let { it.toNote() to it.toPost().first() } .let { it.toNote() to postQueryMapper.map(it).first() }
} }
private suspend fun ResultRow.toNote(mediaList: List<dev.usbharu.hideout.domain.model.hideout.entity.Media>): Note { private suspend fun ResultRow.toNote(mediaList: List<dev.usbharu.hideout.domain.model.hideout.entity.Media>): Note {

View File

@ -0,0 +1,17 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import org.jetbrains.exposed.sql.Query
import org.springframework.stereotype.Component
@Component
class PostQueryMapper(private val postResultRowMapper: ResultRowMapper<Post>) : QueryMapper<Post> {
override fun map(query: Query): List<Post> {
return query.groupBy { it[Posts.id] }
.map { it.value }
.map {
it.first().let(postResultRowMapper::map)
.copy(mediaIds = it.mapNotNull { it.getOrNull(PostsMedia.mediaId) })
}
}
}

View File

@ -1,7 +1,6 @@
package dev.usbharu.hideout.repository package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.exception.FailedToGetResourcesException import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.service.core.IdGenerateService import dev.usbharu.hideout.service.core.IdGenerateService
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
@ -9,7 +8,10 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
class PostRepositoryImpl(private val idGenerateService: IdGenerateService) : PostRepository { class PostRepositoryImpl(
private val idGenerateService: IdGenerateService,
private val postQueryMapper: QueryMapper<Post>
) : PostRepository {
override suspend fun generateId(): Long = idGenerateService.generateId() override suspend fun generateId(): Long = idGenerateService.generateId()
@ -65,7 +67,7 @@ class PostRepositoryImpl(private val idGenerateService: IdGenerateService) : Pos
override suspend fun findById(id: Long): Post = override suspend fun findById(id: Long): Post =
Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId }) Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId })
.select { Posts.id eq id } .select { Posts.id eq id }
.toPost() .let(postQueryMapper::map)
.singleOrNull() .singleOrNull()
?: throw FailedToGetResourcesException("id: $id was not found.") ?: throw FailedToGetResourcesException("id: $id was not found.")
@ -94,25 +96,3 @@ object PostsMedia : Table() {
val mediaId = long("media_id").references(Media.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE) val mediaId = long("media_id").references(Media.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE)
override val primaryKey = PrimaryKey(postId, mediaId) override val primaryKey = PrimaryKey(postId, mediaId)
} }
fun ResultRow.toPost(): Post {
return Post.of(
id = this[Posts.id],
userId = this[Posts.userId],
overview = this[Posts.overview],
text = this[Posts.text],
createdAt = this[Posts.createdAt],
visibility = Visibility.values().first { visibility -> visibility.ordinal == this[Posts.visibility] },
url = this[Posts.url],
repostId = this[Posts.repostId],
replyId = this[Posts.replyId],
sensitive = this[Posts.sensitive],
apId = this[Posts.apId],
)
}
fun Query.toPost(): List<Post> {
return this.groupBy { it[Posts.id] }
.map { it.value }
.map { it.first().toPost().copy(mediaIds = it.mapNotNull { it.getOrNull(PostsMedia.mediaId) }) }
}

View File

@ -0,0 +1,25 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import org.jetbrains.exposed.sql.ResultRow
import org.springframework.stereotype.Component
@Component
class PostResultRowMapper(private val postBuilder: Post.PostBuilder) : ResultRowMapper<Post> {
override fun map(resultRow: ResultRow): Post {
return postBuilder.of(
id = resultRow[Posts.id],
userId = resultRow[Posts.userId],
overview = resultRow[Posts.overview],
text = resultRow[Posts.text],
createdAt = resultRow[Posts.createdAt],
visibility = Visibility.values().first { visibility -> visibility.ordinal == resultRow[Posts.visibility] },
url = resultRow[Posts.url],
repostId = resultRow[Posts.repostId],
replyId = resultRow[Posts.replyId],
sensitive = resultRow[Posts.sensitive],
apId = resultRow[Posts.apId],
)
}
}

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.repository
import org.jetbrains.exposed.sql.Query
interface QueryMapper<T> {
fun map(query: Query): List<T>
}

View File

@ -0,0 +1,7 @@
package dev.usbharu.hideout.repository
import org.jetbrains.exposed.sql.ResultRow
interface ResultRowMapper<T> {
fun map(resultRow: ResultRow): T
}

View File

@ -0,0 +1,12 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.User
import org.jetbrains.exposed.sql.Query
import org.springframework.stereotype.Component
@Component
class UserQueryMapper(private val userResultRowMapper: ResultRowMapper<User>) : QueryMapper<User> {
override fun map(query: Query): List<User> {
return query.map(userResultRowMapper::map)
}
}

View File

@ -1,16 +1,17 @@
package dev.usbharu.hideout.repository package dev.usbharu.hideout.repository
import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.service.core.IdGenerateService import dev.usbharu.hideout.service.core.IdGenerateService
import org.jetbrains.exposed.dao.id.LongIdTable import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.time.Instant
@Repository @Repository
class UserRepositoryImpl(private val idGenerateService: IdGenerateService) : class UserRepositoryImpl(
private val idGenerateService: IdGenerateService,
private val userResultRowMapper: ResultRowMapper<User>
) :
UserRepository { UserRepository {
override suspend fun save(user: User): User { override suspend fun save(user: User): User {
@ -55,9 +56,7 @@ class UserRepositoryImpl(private val idGenerateService: IdGenerateService) :
} }
override suspend fun findById(id: Long): User? { override suspend fun findById(id: Long): User? {
return Users.select { Users.id eq id }.map { return Users.select { Users.id eq id }.singleOrNull()?.let(userResultRowMapper::map)
it.toUser()
}.singleOrNull()
} }
override suspend fun deleteFollowRequest(id: Long, follower: Long) { override suspend fun deleteFollowRequest(id: Long, follower: Long) {
@ -78,26 +77,26 @@ class UserRepositoryImpl(private val idGenerateService: IdGenerateService) :
object Users : Table("users") { object Users : Table("users") {
val id: Column<Long> = long("id") val id: Column<Long> = long("id")
val name: Column<String> = varchar("name", length = Config.configData.characterLimit.account.id) val name: Column<String> = varchar("name", length = 300)
val domain: Column<String> = varchar("domain", length = Config.configData.characterLimit.general.domain) val domain: Column<String> = varchar("domain", length = 1000)
val screenName: Column<String> = varchar("screen_name", length = Config.configData.characterLimit.account.name) val screenName: Column<String> = varchar("screen_name", length = 300)
val description: Column<String> = varchar( val description: Column<String> = varchar(
"description", "description",
length = Config.configData.characterLimit.account.description length = 10000
) )
val password: Column<String?> = varchar("password", length = 255).nullable() val password: Column<String?> = varchar("password", length = 255).nullable()
val inbox: Column<String> = varchar("inbox", length = Config.configData.characterLimit.general.url).uniqueIndex() val inbox: Column<String> = varchar("inbox", length = 1000).uniqueIndex()
val outbox: Column<String> = varchar("outbox", length = Config.configData.characterLimit.general.url).uniqueIndex() val outbox: Column<String> = varchar("outbox", length = 1000).uniqueIndex()
val url: Column<String> = varchar("url", length = Config.configData.characterLimit.general.url).uniqueIndex() val url: Column<String> = varchar("url", length = 1000).uniqueIndex()
val publicKey: Column<String> = varchar("public_key", length = Config.configData.characterLimit.general.publicKey) val publicKey: Column<String> = varchar("public_key", length = 10000)
val privateKey: Column<String?> = varchar( val privateKey: Column<String?> = varchar(
"private_key", "private_key",
length = Config.configData.characterLimit.general.privateKey length = 10000
).nullable() ).nullable()
val createdAt: Column<Long> = long("created_at") val createdAt: Column<Long> = long("created_at")
val keyId = varchar("key_id", length = Config.configData.characterLimit.general.url) val keyId = varchar("key_id", length = 1000)
val following = varchar("following", length = Config.configData.characterLimit.general.url).nullable() val following = varchar("following", length = 1000).nullable()
val followers = varchar("followers", length = Config.configData.characterLimit.general.url).nullable() val followers = varchar("followers", length = 1000).nullable()
override val primaryKey: PrimaryKey = PrimaryKey(id) override val primaryKey: PrimaryKey = PrimaryKey(id)
@ -106,26 +105,6 @@ object Users : Table("users") {
} }
} }
fun ResultRow.toUser(): User {
return User.of(
id = this[Users.id],
name = this[Users.name],
domain = this[Users.domain],
screenName = this[Users.screenName],
description = this[Users.description],
password = this[Users.password],
inbox = this[Users.inbox],
outbox = this[Users.outbox],
url = this[Users.url],
publicKey = this[Users.publicKey],
privateKey = this[Users.privateKey],
createdAt = Instant.ofEpochMilli((this[Users.createdAt])),
keyId = this[Users.keyId],
followers = this[Users.followers],
following = this[Users.following]
)
}
object UsersFollowers : LongIdTable("users_followers") { object UsersFollowers : LongIdTable("users_followers") {
val userId: Column<Long> = long("user_id").references(Users.id).index() val userId: Column<Long> = long("user_id").references(Users.id).index()
val followerId: Column<Long> = long("follower_id").references(Users.id) val followerId: Column<Long> = long("follower_id").references(Users.id)

View File

@ -0,0 +1,29 @@
package dev.usbharu.hideout.repository
import dev.usbharu.hideout.domain.model.hideout.entity.User
import org.jetbrains.exposed.sql.ResultRow
import org.springframework.stereotype.Component
import java.time.Instant
@Component
class UserResultRowMapper(private val userBuilder: User.UserBuilder) : ResultRowMapper<User> {
override fun map(resultRow: ResultRow): User {
return userBuilder.of(
id = resultRow[Users.id],
name = resultRow[Users.name],
domain = resultRow[Users.domain],
screenName = resultRow[Users.screenName],
description = resultRow[Users.description],
password = resultRow[Users.password],
inbox = resultRow[Users.inbox],
outbox = resultRow[Users.outbox],
url = resultRow[Users.url],
publicKey = resultRow[Users.publicKey],
privateKey = resultRow[Users.privateKey],
createdAt = Instant.ofEpochMilli((resultRow[Users.createdAt])),
keyId = resultRow[Users.keyId],
followers = resultRow[Users.followers],
following = resultRow[Users.following]
)
}
}

View File

@ -69,6 +69,7 @@ class APNoteServiceImpl(
private val postService: PostService, private val postService: PostService,
private val apResourceResolveService: APResourceResolveService, private val apResourceResolveService: APResourceResolveService,
private val apRequestService: APRequestService, private val apRequestService: APRequestService,
private val postBuilder: Post.PostBuilder,
private val transaction: Transaction private val transaction: Transaction
) : APNoteService, PostCreateInterceptor { ) : APNoteService, PostCreateInterceptor {
@ -224,7 +225,7 @@ class APNoteServiceImpl(
// TODO: リモートのメディア処理を追加 // TODO: リモートのメディア処理を追加
postService.createRemote( postService.createRemote(
Post.of( postBuilder.of(
id = postRepository.generateId(), id = postRepository.generateId(),
userId = person.second.id, userId = person.second.id,
text = note.content.orEmpty(), text = note.content.orEmpty(),

View File

@ -16,7 +16,8 @@ class PostServiceImpl(
private val postRepository: PostRepository, private val postRepository: PostRepository,
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val timelineService: TimelineService, private val timelineService: TimelineService,
private val postQueryService: PostQueryService private val postQueryService: PostQueryService,
private val postBuilder: Post.PostBuilder
) : PostService { ) : PostService {
private val interceptors = Collections.synchronizedList(mutableListOf<PostCreateInterceptor>()) private val interceptors = Collections.synchronizedList(mutableListOf<PostCreateInterceptor>())
@ -45,7 +46,7 @@ class PostServiceImpl(
private suspend fun internalCreate(post: PostCreateDto, isLocal: Boolean): Post { private suspend fun internalCreate(post: PostCreateDto, isLocal: Boolean): Post {
val user = userRepository.findById(post.userId) ?: throw UserNotFoundException("${post.userId} was not found") val user = userRepository.findById(post.userId) ?: throw UserNotFoundException("${post.userId} was not found")
val id = postRepository.generateId() val id = postRepository.generateId()
val createPost = Post.of( val createPost = postBuilder.of(
id = id, id = id,
userId = post.userId, userId = post.userId,
overview = post.overview, overview = post.overview,

View File

@ -21,6 +21,7 @@ class UserServiceImpl(
private val apSendFollowService: APSendFollowService, private val apSendFollowService: APSendFollowService,
private val userQueryService: UserQueryService, private val userQueryService: UserQueryService,
private val followerQueryService: FollowerQueryService, private val followerQueryService: FollowerQueryService,
private val userBuilder: User.UserBuilder,
private val applicationConfig: ApplicationConfig private val applicationConfig: ApplicationConfig
) : ) :
UserService { UserService {
@ -35,7 +36,7 @@ class UserServiceImpl(
val hashedPassword = userAuthService.hash(user.password) val hashedPassword = userAuthService.hash(user.password)
val keyPair = userAuthService.generateKeyPair() val keyPair = userAuthService.generateKeyPair()
val userUrl = "${applicationConfig.url}/users/${user.name}" val userUrl = "${applicationConfig.url}/users/${user.name}"
val userEntity = User.of( val userEntity = userBuilder.of(
id = nextId, id = nextId,
name = user.name, name = user.name,
domain = applicationConfig.url.host, domain = applicationConfig.url.host,
@ -57,7 +58,7 @@ class UserServiceImpl(
override suspend fun createRemoteUser(user: RemoteUserCreateDto): User { override suspend fun createRemoteUser(user: RemoteUserCreateDto): User {
val nextId = userRepository.nextId() val nextId = userRepository.nextId()
val userEntity = User.of( val userEntity = userBuilder.of(
id = nextId, id = nextId,
name = user.name, name = user.name,
domain = user.domain, domain = user.domain,

View File

@ -3,8 +3,8 @@
package dev.usbharu.hideout.service.ap package dev.usbharu.hideout.service.ap
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.config.ConfigData import dev.usbharu.hideout.config.CharacterLimit
import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
@ -23,21 +23,28 @@ import org.junit.jupiter.api.Test
import org.mockito.Mockito.anyLong import org.mockito.Mockito.anyLong
import org.mockito.Mockito.eq import org.mockito.Mockito.eq
import org.mockito.kotlin.* import org.mockito.kotlin.*
import utils.JsonObjectMapper
import utils.JsonObjectMapper.objectMapper import utils.JsonObjectMapper.objectMapper
import utils.TestApplicationConfig.testApplicationConfig import utils.TestApplicationConfig.testApplicationConfig
import java.net.URL
import java.time.Instant import java.time.Instant
import kotlin.test.assertEquals import kotlin.test.assertEquals
class APNoteServiceImplTest { class APNoteServiceImplTest {
val userBuilder = User.UserBuilder(CharacterLimit(), ApplicationConfig(URL("https://example.com")))
val postBuilder = Post.PostBuilder(CharacterLimit())
@Test @Test
fun `createPost 新しい投稿`() { fun `createPost 新しい投稿`() {
val mediaQueryService = mock<MediaQueryService> { val mediaQueryService = mock<MediaQueryService> {
onBlocking { findByPostId(anyLong()) } doReturn emptyList() onBlocking { findByPostId(anyLong()) } doReturn emptyList()
} }
runTest { runTest {
val followers = listOf( val followers = listOf(
User.of( userBuilder.of(
2L, 2L,
"follower", "follower",
"follower.example.com", "follower.example.com",
@ -51,7 +58,7 @@ class APNoteServiceImplTest {
createdAt = Instant.now(), createdAt = Instant.now(),
keyId = "a" keyId = "a"
), ),
User.of( userBuilder.of(
3L, 3L,
"follower2", "follower2",
"follower2.example.com", "follower2.example.com",
@ -67,7 +74,7 @@ class APNoteServiceImplTest {
) )
) )
val userQueryService = mock<UserQueryService> { val userQueryService = mock<UserQueryService> {
onBlocking { findById(eq(1L)) } doReturn User.of( onBlocking { findById(eq(1L)) } doReturn userBuilder.of(
1L, 1L,
"test", "test",
"example.com", "example.com",
@ -101,9 +108,10 @@ class APNoteServiceImplTest {
postService = mock(), postService = mock(),
apResourceResolveService = mock(), apResourceResolveService = mock(),
apRequestService = mock(), apRequestService = mock(),
transaction = mock() transaction = mock(),
postBuilder = postBuilder
) )
val postEntity = Post.of( val postEntity = postBuilder.of(
1L, 1L,
1L, 1L,
null, null,
@ -123,7 +131,7 @@ class APNoteServiceImplTest {
val mediaQueryService = mock<MediaQueryService> { val mediaQueryService = mock<MediaQueryService> {
onBlocking { findByPostId(anyLong()) } doReturn emptyList() onBlocking { findByPostId(anyLong()) } doReturn emptyList()
} }
Config.configData = ConfigData(objectMapper = JsonObjectMapper.objectMapper)
val httpClient = HttpClient( val httpClient = HttpClient(
MockEngine { httpRequestData -> MockEngine { httpRequestData ->
assertEquals("https://follower.example.com/inbox", httpRequestData.url.toString()) assertEquals("https://follower.example.com/inbox", httpRequestData.url.toString())
@ -143,7 +151,8 @@ class APNoteServiceImplTest {
postService = mock(), postService = mock(),
apResourceResolveService = mock(), apResourceResolveService = mock(),
apRequestService = mock(), apRequestService = mock(),
transaction = mock() transaction = mock(),
postBuilder = postBuilder
) )
activityPubNoteService.createNoteJob( activityPubNoteService.createNoteJob(
JobProps( JobProps(

View File

@ -3,12 +3,13 @@
package dev.usbharu.hideout.service.ap package dev.usbharu.hideout.service.ap
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.config.ConfigData import dev.usbharu.hideout.config.CharacterLimit
import dev.usbharu.hideout.domain.model.ap.Follow import dev.usbharu.hideout.domain.model.ap.Follow
import dev.usbharu.hideout.domain.model.ap.Image import dev.usbharu.hideout.domain.model.ap.Image
import dev.usbharu.hideout.domain.model.ap.Key import dev.usbharu.hideout.domain.model.ap.Key
import dev.usbharu.hideout.domain.model.ap.Person import dev.usbharu.hideout.domain.model.ap.Person
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob
import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.query.UserQueryService
@ -23,12 +24,16 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.* import org.mockito.kotlin.*
import utils.JsonObjectMapper
import utils.JsonObjectMapper.objectMapper import utils.JsonObjectMapper.objectMapper
import utils.TestTransaction import utils.TestTransaction
import java.net.URL
import java.time.Instant import java.time.Instant
class APReceiveFollowServiceImplTest { class APReceiveFollowServiceImplTest {
val userBuilder = User.UserBuilder(CharacterLimit(), ApplicationConfig(URL("https://example.com")))
val postBuilder = Post.PostBuilder(CharacterLimit())
@Test @Test
fun `receiveFollow フォロー受付処理`() = runTest { fun `receiveFollow フォロー受付処理`() = runTest {
val jobQueueParentService = mock<JobQueueParentService> { val jobQueueParentService = mock<JobQueueParentService> {
@ -80,7 +85,6 @@ class APReceiveFollowServiceImplTest {
@Test @Test
fun `receiveFollowJob フォロー受付処理のJob`() = runTest { fun `receiveFollowJob フォロー受付処理のJob`() = runTest {
Config.configData = ConfigData(objectMapper = JsonObjectMapper.objectMapper)
val person = Person( val person = Person(
type = emptyList(), type = emptyList(),
name = "follower", name = "follower",
@ -112,7 +116,7 @@ class APReceiveFollowServiceImplTest {
} }
val userQueryService = mock<UserQueryService> { val userQueryService = mock<UserQueryService> {
onBlocking { findByUrl(eq("https://example.com")) } doReturn onBlocking { findByUrl(eq("https://example.com")) } doReturn
User.of( userBuilder.of(
id = 1L, id = 1L,
name = "test", name = "test",
domain = "example.com", domain = "example.com",
@ -122,11 +126,13 @@ class APReceiveFollowServiceImplTest {
outbox = "https://example.com/outbox", outbox = "https://example.com/outbox",
url = "https://example.com", url = "https://example.com",
publicKey = "", publicKey = "",
password = "a",
privateKey = "a",
createdAt = Instant.now(), createdAt = Instant.now(),
keyId = "a" keyId = "a"
) )
onBlocking { findByUrl(eq("https://follower.example.com")) } doReturn onBlocking { findByUrl(eq("https://follower.example.com")) } doReturn
User.of( userBuilder.of(
id = 2L, id = 2L,
name = "follower", name = "follower",
domain = "follower.example.com", domain = "follower.example.com",

View File

@ -1,6 +1,9 @@
package dev.usbharu.hideout.service.ap.resource package dev.usbharu.hideout.service.ap.resource
import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.config.CharacterLimit
import dev.usbharu.hideout.domain.model.ap.Object import dev.usbharu.hideout.domain.model.ap.Object
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.repository.UserRepository import dev.usbharu.hideout.repository.UserRepository
import io.ktor.client.* import io.ktor.client.*
@ -16,6 +19,7 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
import java.net.URL
import java.time.Instant import java.time.Instant
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -23,6 +27,9 @@ import kotlin.test.assertEquals
@Disabled @Disabled
class APResourceResolveServiceImplTest { class APResourceResolveServiceImplTest {
val userBuilder = User.UserBuilder(CharacterLimit(), ApplicationConfig(URL("https://example.com")))
val postBuilder = Post.PostBuilder(CharacterLimit())
@Test @Test
fun `単純な一回のリクエスト`() = runTest { fun `単純な一回のリクエスト`() = runTest {
@ -36,7 +43,7 @@ class APResourceResolveServiceImplTest {
val userRepository = mock<UserRepository>() val userRepository = mock<UserRepository>()
whenever(userRepository.findById(any())).doReturn( whenever(userRepository.findById(any())).doReturn(
User.of( userBuilder.of(
2L, 2L,
"follower", "follower",
"follower.example.com", "follower.example.com",
@ -72,7 +79,7 @@ class APResourceResolveServiceImplTest {
val userRepository = mock<UserRepository>() val userRepository = mock<UserRepository>()
whenever(userRepository.findById(any())).doReturn( whenever(userRepository.findById(any())).doReturn(
User.of( userBuilder.of(
2L, 2L,
"follower", "follower",
"follower.example.com", "follower.example.com",
@ -111,7 +118,7 @@ class APResourceResolveServiceImplTest {
val userRepository = mock<UserRepository>() val userRepository = mock<UserRepository>()
whenever(userRepository.findById(any())).doReturn( whenever(userRepository.findById(any())).doReturn(
User.of( userBuilder.of(
2L, 2L,
"follower", "follower",
"follower.example.com", "follower.example.com",
@ -161,7 +168,7 @@ class APResourceResolveServiceImplTest {
val userRepository = mock<UserRepository>() val userRepository = mock<UserRepository>()
whenever(userRepository.findById(any())).doReturn( whenever(userRepository.findById(any())).doReturn(
User.of( userBuilder.of(
2L, 2L,
"follower", "follower",
"follower.example.com", "follower.example.com",

View File

@ -2,10 +2,12 @@
package dev.usbharu.hideout.service.user package dev.usbharu.hideout.service.user
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.config.ConfigData import dev.usbharu.hideout.config.CharacterLimit
import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.RemoteUserCreateDto
import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.UserCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.Post
import dev.usbharu.hideout.domain.model.hideout.entity.User
import dev.usbharu.hideout.repository.UserRepository import dev.usbharu.hideout.repository.UserRepository
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -13,14 +15,17 @@ import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.* import org.mockito.kotlin.*
import utils.TestApplicationConfig.testApplicationConfig import utils.TestApplicationConfig.testApplicationConfig
import java.net.URL
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNull import kotlin.test.assertNull
class UserServiceTest { class UserServiceTest {
val userBuilder = User.UserBuilder(CharacterLimit(), ApplicationConfig(URL("https://example.com")))
val postBuilder = Post.PostBuilder(CharacterLimit())
@Test @Test
fun `createLocalUser ローカルユーザーを作成できる`() = runTest { fun `createLocalUser ローカルユーザーを作成できる`() = runTest {
Config.configData = ConfigData(domain = "example.com", url = "https://example.com")
val userRepository = mock<UserRepository> { val userRepository = mock<UserRepository> {
onBlocking { nextId() } doReturn 110001L onBlocking { nextId() } doReturn 110001L
} }
@ -30,7 +35,15 @@ class UserServiceTest {
onBlocking { generateKeyPair() } doReturn generateKeyPair onBlocking { generateKeyPair() } doReturn generateKeyPair
} }
val userService = val userService =
UserServiceImpl(userRepository, userAuthService, mock(), mock(), mock(), testApplicationConfig) UserServiceImpl(
userRepository,
userAuthService,
mock(),
mock(),
mock(),
userBuilder,
testApplicationConfig,
)
userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test")) userService.createLocalUser(UserCreateDto("test", "testUser", "XXXXXXXXXXXXX", "test"))
verify(userRepository, times(1)).save(any()) verify(userRepository, times(1)).save(any())
argumentCaptor<dev.usbharu.hideout.domain.model.hideout.entity.User> { argumentCaptor<dev.usbharu.hideout.domain.model.hideout.entity.User> {
@ -51,20 +64,20 @@ class UserServiceTest {
@Test @Test
fun `createRemoteUser リモートユーザーを作成できる`() = runTest { fun `createRemoteUser リモートユーザーを作成できる`() = runTest {
Config.configData = ConfigData(domain = "remote.example.com", url = "https://remote.example.com")
val userRepository = mock<UserRepository> { val userRepository = mock<UserRepository> {
onBlocking { nextId() } doReturn 113345L onBlocking { nextId() } doReturn 113345L
} }
val userService = UserServiceImpl(userRepository, mock(), mock(), mock(), mock(), testApplicationConfig) val userService =
UserServiceImpl(userRepository, mock(), mock(), mock(), mock(), userBuilder, testApplicationConfig)
val user = RemoteUserCreateDto( val user = RemoteUserCreateDto(
name = "test", name = "test",
domain = "example.com", domain = "remote.example.com",
screenName = "testUser", screenName = "testUser",
description = "test user", description = "test user",
inbox = "https://example.com/inbox", inbox = "https://remote.example.com/inbox",
outbox = "https://example.com/outbox", outbox = "https://remote.example.com/outbox",
url = "https://example.com", url = "https://remote.example.com",
publicKey = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----", publicKey = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----",
keyId = "a", keyId = "a",
following = "", following = "",
@ -79,10 +92,10 @@ class UserServiceTest {
assertEquals("test user", firstValue.description) assertEquals("test user", firstValue.description)
assertNull(firstValue.password) assertNull(firstValue.password)
assertEquals(113345L, firstValue.id) assertEquals(113345L, firstValue.id)
assertEquals("https://example.com", firstValue.url) assertEquals("https://remote.example.com", firstValue.url)
assertEquals("example.com", firstValue.domain) assertEquals("remote.example.com", firstValue.domain)
assertEquals("https://example.com/inbox", firstValue.inbox) assertEquals("https://remote.example.com/inbox", firstValue.inbox)
assertEquals("https://example.com/outbox", firstValue.outbox) assertEquals("https://remote.example.com/outbox", firstValue.outbox)
assertEquals("-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----", firstValue.publicKey) assertEquals("-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----", firstValue.publicKey)
assertNull(firstValue.privateKey) assertNull(firstValue.privateKey)
} }