Move comments classes into comment component

This commit is contained in:
2021-01-14 22:51:33 +01:00
parent 64f74d0449
commit 91ab800272
22 changed files with 304 additions and 215 deletions

View File

@@ -0,0 +1,59 @@
package fr.dcproject.component.comment.article
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.component.comment.generic.CommentForView
import fr.dcproject.component.comment.generic.CommentRepositoryAbs
import fr.dcproject.entity.TargetI
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.UuidEntityI
import java.util.*
class CommentArticleRepository(requester: Requester) : CommentRepositoryAbs<ArticleForView>(requester) {
override fun findById(id: UUID): CommentForView<ArticleForView, CitizenRef>? {
return requester
.getFunction("find_comment_by_id")
.selectOne(mapOf("id" to id))
}
override fun findByCitizen(
citizen: CitizenI,
page: Int,
limit: Int
): Paginated<CommentForView<ArticleForView, CitizenRef>> {
return requester.run {
getFunction("find_comments_by_citizen")
.select(
page, limit,
"created_by_id" to citizen.id,
"reference" to TargetI.getReference(ArticleRef::class)
)
}
}
override fun findByTarget(
target: UuidEntityI,
page: Int,
limit: Int,
sort: Sort
): Paginated<CommentForView<ArticleForView, CitizenRef>> = requester
.getFunction("find_comments_by_target")
.select(
page, limit,
"target_id" to target.id,
"sort" to sort.sql
)
enum class Sort(val sql: String) {
CREATED_AT("created_at"), VOTES("votes");
companion object {
fun fromString(string: String): Sort? {
return values().firstOrNull { it.sql == string }
}
}
}
}

View File

@@ -0,0 +1,77 @@
package fr.dcproject.component.comment.generic
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.*
import fr.dcproject.entity.EntityI
import fr.postgresjson.entity.*
import org.joda.time.DateTime
import java.util.*
class CommentForView<T : TargetI, C : CitizenRef>(
id: UUID = UUID.randomUUID(),
override val createdBy: C,
override val target: T,
override var content: String,
override val parent: CommentParent<T>? = null,
val childrenCount: Int? = null,
override val deletedAt: DateTime? = null
) : ExtraI<T, C>,
CommentForUpdate<T, C>(id, createdBy, target, content, parent, deletedAt),
CommentWithTargetI<T>,
EntityCreatedBy<C> by EntityCreatedByImp(createdBy),
EntityUpdatedAt by EntityUpdatedAtImp(),
EntityDeletedAt by EntityDeletedAtImp(),
Votable by VotableImp(),
TargetI {
constructor(
createdBy: C,
parent: CommentParent<T>,
content: String
) : this(
createdBy = createdBy,
parent = parent,
target = parent.target,
content = content
)
}
open class CommentForUpdate<T : TargetI, C: CitizenRef>(
override val id: UUID = UUID.randomUUID(),
override val createdBy: C,
override val target: T,
open var content: String,
open val parent: CommentParent<T>? = null,
override val deletedAt: DateTime? = null
) : CommentParent<T>(id, deletedAt, target),
ExtraI<T, C>,
CommentWithTargetI<T>,
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<C>,
EntityDeletedAt,
TargetI {
constructor(
createdBy: C,
parent: CommentParent<T>,
content: String
) : this(
createdBy = createdBy,
parent = parent,
target = parent.target,
content = content
)
}
open class CommentParent<T: TargetI>(
override val id: UUID,
override val deletedAt: DateTime?,
override val target: T
) : CommentRef(id),
CommentParentI<T>
interface CommentParentI<T: TargetI> : CommentI, EntityDeletedAt, CommentWithTargetI<T>
interface CommentWithTargetI<T : TargetI> : CommentI, TargetI, AsTarget<T>
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentI, TargetRef(id)
interface CommentI : EntityI

View File

@@ -0,0 +1,123 @@
package fr.dcproject.component.comment.generic
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.component.comment.article.CommentArticleRepository
import fr.dcproject.entity.TargetI
import fr.dcproject.entity.TargetRef
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.UuidEntityI
import fr.postgresjson.repository.RepositoryI
import java.util.*
abstract class CommentRepositoryAbs<T : TargetI>(override var requester: Requester) : RepositoryI {
abstract fun findById(id: UUID): CommentForView<T, CitizenRef>?
abstract fun findByCitizen(
citizen: CitizenI,
page: Int = 1,
limit: Int = 50
): Paginated<CommentForView<T, CitizenRef>>
open fun findByParent(
parent: CommentForView<T, CitizenRef>,
page: Int = 1,
limit: Int = 50
): Paginated<CommentForView<T, CitizenRef>> {
return findByParent(parent.id, page, limit)
}
open fun findByParent(
parentId: UUID,
page: Int = 1,
limit: Int = 50
): Paginated<CommentForView<T, CitizenRef>> {
return requester.run {
getFunction("find_comments_by_parent")
.select(
page, limit,
"parent_id" to parentId
)
}
}
open fun findByTarget(
target: UuidEntityI,
page: Int = 1,
limit: Int = 50,
sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.CREATED_AT
): Paginated<CommentForView<T, CitizenRef>> {
return findByTarget(target.id, page, limit, sort)
}
open fun findByTarget(
targetId: UUID,
page: Int = 1,
limit: Int = 50,
sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.CREATED_AT
): Paginated<CommentForView<T, CitizenRef>> {
return requester.run {
getFunction("find_comments_by_target")
.select(
page, limit,
"target_id" to targetId,
"sort" to sort.sql
)
}
}
fun <I : T, C: CitizenRef> comment(comment: CommentForUpdate<I, C>) {
requester
.getFunction("comment")
.sendQuery(
"reference" to comment.target.reference,
"resource" to comment
)
}
fun <I : T> edit(comment: CommentForUpdate<I, CitizenRef>) {
requester
.getFunction("edit_comment")
.sendQuery(
"id" to comment.id,
"content" to comment.content
)
}
}
class CommentRepository(requester: Requester) : CommentRepositoryAbs<TargetRef>(requester) {
override fun findById(id: UUID): CommentForView<TargetRef, CitizenRef>? {
return requester
.getFunction("find_comment_by_id")
.selectOne(mapOf("id" to id))
}
override fun findByCitizen(
citizen: CitizenI,
page: Int,
limit: Int
): Paginated<CommentForView<TargetRef, CitizenRef>> {
return requester.run {
getFunction("find_comments_by_citizen")
.select(
page, limit,
"created_by_id" to citizen.id
)
}
}
override fun findByParent(
parentId: UUID,
page: Int,
limit: Int
): Paginated<CommentForView<TargetRef, CitizenRef>> {
return requester.run {
getFunction("find_comments_by_parent")
.select(
page, limit,
"parent_id" to parentId
)
}
}
}

View File

@@ -0,0 +1,61 @@
package fr.dcproject.component.comment.generic
import fr.dcproject.citizenOrNull
import fr.dcproject.voter.NoRuleDefinedException
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import fr.postgresjson.entity.EntityDeletedAt
import io.ktor.application.*
class CommentVoter : Voter<ApplicationCall> {
enum class Action : ActionI {
CREATE,
UPDATE,
VIEW,
DELETE
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if (!(action is Action && subject is CommentI?)) return abstain()
val citizen = context.citizenOrNull
if (subject == null) {
throw NoSubjectDefinedException(action)
}
if (action == Action.CREATE) {
return when {
citizen == null -> denied("You must be connected to create user", "comment.create.notConnected")
subject !is CommentForUpdate<*, *> -> throw NoSubjectDefinedException(action)
subject.createdBy.id != citizen.id -> denied("You cannot create a comment with other user than yours", "comment.create.wrongUser")
subject.parent?.isDeleted() ?: false -> denied("You cannot create a comment on deleted parent", "comment.create.deletedParent")
subject.target.let { it is EntityDeletedAt && it.isDeleted() } -> denied("You cannot create a comment on deleted target", "comment.create.deletedTarget")
else -> granted()
}
}
if (action == Action.VIEW) {
return when {
subject !is CommentForView<*, *> -> throw NoSubjectDefinedException(action)
subject.isDeleted() -> denied("Your cannot view a deleted comment", "comment.view.deleted")
else -> granted()
}
}
if (action == Action.UPDATE) {
if (citizen == null) return denied("You must be connected to update comment", "comment.update.notConnected")
return when {
subject !is CommentForUpdate<*, *> -> throw NoSubjectDefinedException(action)
citizen.id == subject.createdBy.id -> granted()
else -> denied("You cannot update another user of yours", "comment.update.notYours")
}
}
if (action == Action.DELETE) {
return denied("A comment can never be deleted", "comment.deleted.never")
}
throw NoRuleDefinedException(action)
}
}

View File

@@ -0,0 +1,40 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.citizen
import fr.dcproject.component.comment.generic.CommentForUpdate
import fr.dcproject.component.comment.generic.CommentRef
import fr.dcproject.component.comment.generic.CommentRepository
import fr.dcproject.component.comment.generic.CommentVoter
import fr.ktorVoter.assertCan
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.util.*
@KtorExperimentalLocationsAPI
@Location("/comments/{comment}/children")
class CreateCommentChildrenRequest(val comment: CommentRef) {
class Input(val content: String)
}
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
fun Route.createCommentChildren(repo: CommentRepository) {
post<CreateCommentChildrenRequest> {
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
val newComment = CommentForUpdate(
content = call.receive<CreateCommentChildrenRequest.Input>().content,
createdBy = citizen,
parent = parent
)
assertCan(CommentVoter.Action.CREATE, newComment)
repo.comment(newComment)
call.respond(HttpStatusCode.Created, newComment)
}
}

View File

@@ -0,0 +1,31 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.component.comment.generic.CommentRef
import fr.dcproject.component.comment.generic.CommentRepository
import fr.dcproject.component.comment.generic.CommentVoter
import fr.ktorVoter.assertCan
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.util.*
@KtorExperimentalLocationsAPI
@Location("/comments/{comment}")
class EditCommentRequest(val comment: CommentRef)
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
fun Route.editComment(repo: CommentRepository) {
put<EditCommentRequest> {
val comment = repo.findById(it.comment.id)!!
assertCan(CommentVoter.Action.UPDATE, comment)
comment.content = call.receiveText()
repo.edit(comment)
call.respond(HttpStatusCode.OK, comment)
}
}

View File

@@ -0,0 +1,42 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.component.comment.generic.CommentRepository
import fr.dcproject.component.comment.generic.CommentVoter
import fr.ktorVoter.assertCanAll
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.util.*
import java.util.*
@KtorExperimentalLocationsAPI
@Location("/comments/{comment}/children")
class CommentChildrenRequest(
val comment: UUID,
page: Int = 1,
limit: Int = 50,
val search: String? = null
) {
val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
}
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
fun Route.getChildrenComments(repo: CommentRepository) {
get<CommentChildrenRequest> {
val comments =
repo.findByParent(
it.comment,
it.page,
it.limit
)
assertCanAll(CommentVoter.Action.VIEW, comments.result)
call.respond(HttpStatusCode.OK, comments)
}
}

View File

@@ -0,0 +1,29 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.component.comment.generic.CommentRef
import fr.dcproject.component.comment.generic.CommentRepository
import fr.dcproject.component.comment.generic.CommentVoter
import fr.ktorVoter.assertCan
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.util.*
@KtorExperimentalLocationsAPI
@Location("/comments/{comment}")
class CommentRequest(val comment: CommentRef)
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
fun Route.getOneComment(repo: CommentRepository) {
get<CommentRequest> {
val comment = repo.findById(it.comment.id) ?: NotFoundException("Comment ${it.comment.id} not found")
assertCan(CommentVoter.Action.VIEW, comment)
call.respond(HttpStatusCode.OK, comment)
}
}