Move Follow to a component

This commit is contained in:
2021-01-18 21:45:48 +01:00
parent 6cdc526335
commit 6a32895571
23 changed files with 280 additions and 181 deletions

View File

@@ -0,0 +1,46 @@
package fr.dcproject.component.follow
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenBasicI
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.ExtraI
import fr.dcproject.entity.HasTarget
import fr.dcproject.entity.TargetI
import fr.postgresjson.entity.EntityCreatedAt
import fr.postgresjson.entity.EntityCreatedAtImp
import fr.postgresjson.entity.EntityCreatedBy
import fr.postgresjson.entity.EntityCreatedByImp
import fr.postgresjson.entity.UuidEntityI
import java.util.UUID
@Deprecated("")
class Follow<T : TargetI>(
id: UUID = UUID.randomUUID(),
override val createdBy: CitizenBasic,
override var target: T
) : ExtraI<T, CitizenBasicI>,
FollowSimple<T, CitizenBasicI>(id, createdBy, target)
@Deprecated("")
open class FollowSimple<T : TargetI, C : CitizenI>(
id: UUID = UUID.randomUUID(),
override val createdBy: C,
override var target: T
) : ExtraI<T, C>,
FollowRef(id),
EntityCreatedAt by EntityCreatedAtImp(),
EntityCreatedBy<C> by EntityCreatedByImp(createdBy)
class FollowForUpdate<T : TargetI, C : CitizenI>(
id: UUID = UUID.randomUUID(),
override val target: T,
override val createdBy: C
) : FollowRef(id),
HasTarget<T>,
EntityCreatedBy<C> by EntityCreatedByImp<C>(createdBy)
open class FollowRef(
override val id: UUID
) : FollowI
interface FollowI : UuidEntityI

View File

@@ -0,0 +1,148 @@
package fr.dcproject.component.follow
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.entity.ConstitutionRef
import fr.dcproject.entity.TargetRef
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.UuidEntity
import fr.postgresjson.repository.RepositoryI
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.util.UUID
import fr.dcproject.component.follow.Follow as FollowEntity
import fr.dcproject.entity.Constitution as ConstitutionEntity
sealed class FollowRepository<IN : TargetRef, OUT : TargetRef>(override var requester: Requester) : RepositoryI {
open fun findByCitizen(
citizen: CitizenI,
page: Int = 1,
limit: Int = 50
): Paginated<FollowEntity<OUT>> =
findByCitizen(citizen.id, page, limit)
open fun findByCitizen(
citizenId: UUID,
page: Int = 1,
limit: Int = 50
): Paginated<FollowEntity<OUT>> {
return requester
.getFunction("find_follows_by_citizen")
.select(
page,
limit,
"created_by_id" to citizenId
)
}
fun follow(follow: FollowForUpdate<IN, *>) {
requester
.getFunction("follow")
.sendQuery(
"reference" to follow.target.reference,
"target_id" to follow.target.id,
"created_by_id" to follow.createdBy.id
)
}
fun unfollow(follow: FollowForUpdate<IN, *>) {
requester
.getFunction("unfollow")
.sendQuery(
"reference" to follow.target.reference,
"target_id" to follow.target.id,
"created_by_id" to follow.createdBy.id
)
}
open fun findFollow(
citizen: CitizenI,
target: TargetRef
): FollowEntity<OUT>? =
requester
.getFunction("find_follow")
.selectOne(
"citizen_id" to citizen.id,
"target_id" to target.id,
"target_reference" to target.reference
)
fun findFollowsByTarget(
target: UuidEntity,
bulkSize: Int = 300
): Flow<FollowSimple<IN, CitizenRef>> = flow {
var nextPage = 1
do {
val paginate = findFollowsByTarget(target, nextPage, bulkSize)
paginate.result.forEach {
emit(it)
}
nextPage = paginate.currentPage + 1
} while (!paginate.isLastPage())
}
abstract fun findFollowsByTarget(
target: UuidEntity,
page: Int = 1,
limit: Int = 300
): Paginated<FollowSimple<IN, CitizenRef>>
}
class FollowArticleRepository(requester: Requester) : FollowRepository<ArticleRef, ArticleForView>(requester) {
override fun findByCitizen(
citizenId: UUID,
page: Int,
limit: Int
): Paginated<FollowEntity<ArticleForView>> {
return requester.run {
getFunction("find_follows_article_by_citizen")
.select(
page,
limit,
"created_by_id" to citizenId
)
}
}
override fun findFollowsByTarget(
target: UuidEntity,
page: Int,
limit: Int
): Paginated<FollowSimple<ArticleRef, CitizenRef>> {
return requester
.getFunction("find_follows_article_by_target")
.select(
page,
limit,
"target_id" to target.id
)
}
}
class FollowConstitutionRepository(requester: Requester) : FollowRepository<ConstitutionRef, ConstitutionEntity>(requester) {
override fun findByCitizen(
citizenId: UUID,
page: Int,
limit: Int
): Paginated<FollowEntity<ConstitutionEntity>> {
return requester.run {
getFunction("find_follows_constitution_by_citizen")
.select(
page,
limit,
"created_by_id" to citizenId
)
}
}
override fun findFollowsByTarget(
target: UuidEntity,
page: Int,
limit: Int
): Paginated<FollowSimple<ConstitutionRef, CitizenRef>> {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,26 @@
package fr.dcproject.component.follow
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.voter.Voter
import fr.dcproject.voter.VoterResponse
import fr.dcproject.component.follow.Follow as FollowEntity
class FollowVoter : Voter() {
fun canCreate(subject: FollowI, citizen: CitizenI?): VoterResponse {
return if (citizen == null) denied("You must be connected to follow", "follow.create.notConnected")
else granted()
}
fun canDelete(subject: FollowI, citizen: CitizenI?): VoterResponse {
return if (citizen == null) denied("You must be connected to unfollow", "follow.delete.notConnected")
else granted()
}
fun <S : FollowEntity<*>> canView(subjects: List<S>, citizen: CitizenI?): VoterResponse =
canAll(subjects) { canView(it, citizen) }
fun canView(subject: FollowEntity<*>, citizen: CitizenI?): VoterResponse {
return if ((citizen != null && subject.createdBy.id == citizen.id) || !subject.createdBy.followAnonymous) granted()
else denied("You cannot view an anonymous follow", "follow.view.anonymous")
}
}

View File

@@ -0,0 +1,31 @@
package fr.dcproject.component.follow.routes.article
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.follow.FollowArticleRepository
import fr.dcproject.component.follow.FollowForUpdate
import fr.dcproject.component.follow.FollowVoter
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.routing.Route
@KtorExperimentalLocationsAPI
object FollowArticle {
@Location("/articles/{article}/follows")
class ArticleFollowRequest(val article: ArticleRef)
fun Route.followArticle(repo: FollowArticleRepository, voter: FollowVoter) {
post<ArticleFollowRequest> {
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
voter.assert { canCreate(follow, citizenOrNull) }
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}
}
}

View File

@@ -0,0 +1,30 @@
package fr.dcproject.component.follow.routes.article
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.follow.FollowArticleRepository
import fr.dcproject.component.follow.FollowVoter
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
@KtorExperimentalLocationsAPI
object GetFollowArticle {
@Location("/articles/{article}/follows")
class ArticleFollowRequest(val article: ArticleRef)
fun Route.getFollowArticle(repo: FollowArticleRepository, voter: FollowVoter) {
get<ArticleFollowRequest> {
repo.findFollow(citizen, it.article)?.let { follow ->
voter.assert { canView(follow, citizenOrNull) }
call.respond(follow)
} ?: call.respond(HttpStatusCode.NoContent)
}
}
}

View File

@@ -0,0 +1,27 @@
package fr.dcproject.component.follow.routes.article
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.follow.FollowArticleRepository
import fr.dcproject.component.follow.FollowVoter
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
@KtorExperimentalLocationsAPI
object GetMyFollowsArticle {
@Location("/citizens/{citizen}/follows/articles")
class CitizenFollowArticleRequest(val citizen: Citizen)
fun Route.getMyFollowsArticle(repo: FollowArticleRepository, voter: FollowVoter) {
get<CitizenFollowArticleRequest> {
val follows = repo.findByCitizen(it.citizen)
voter.assert { canView(follows.result, citizenOrNull) }
call.respond(follows)
}
}
}

View File

@@ -0,0 +1,31 @@
package fr.dcproject.component.follow.routes.article
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.follow.FollowArticleRepository
import fr.dcproject.component.follow.FollowForUpdate
import fr.dcproject.component.follow.FollowVoter
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.delete
import io.ktor.response.respond
import io.ktor.routing.Route
@KtorExperimentalLocationsAPI
object UnfollowArticle {
@Location("/articles/{article}/follows")
class ArticleFollowRequest(val article: ArticleRef)
fun Route.unfollowArticle(repo: FollowArticleRepository, voter: FollowVoter) {
delete<ArticleFollowRequest> {
val follow = FollowForUpdate(target = it.article, createdBy = this.citizen)
voter.assert { canDelete(follow, citizenOrNull) }
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)
}
}
}

View File

@@ -0,0 +1,31 @@
package fr.dcproject.component.follow.routes.constitution
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.follow.FollowConstitutionRepository
import fr.dcproject.component.follow.FollowForUpdate
import fr.dcproject.component.follow.FollowVoter
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
import io.ktor.response.respond
import io.ktor.routing.Route
@KtorExperimentalLocationsAPI
object FollowConstitution {
@Location("/constitutions/{constitution}/follows")
class ConstitutionFollowRequest(val constitution: ConstitutionRef)
fun Route.followConstitution(repo: FollowConstitutionRepository, voter: FollowVoter) {
post<ConstitutionFollowRequest> {
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
voter.assert { canCreate(follow, citizenOrNull) }
repo.follow(follow)
call.respond(HttpStatusCode.Created)
}
}
}

View File

@@ -0,0 +1,30 @@
package fr.dcproject.component.follow.routes.constitution
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.follow.FollowConstitutionRepository
import fr.dcproject.component.follow.FollowVoter
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
@KtorExperimentalLocationsAPI
object GetFollowConstitution {
@Location("/constitutions/{constitution}/follows")
class ConstitutionFollowRequest(val constitution: ConstitutionRef)
fun Route.getFollowConstitution(repo: FollowConstitutionRepository, voter: FollowVoter) {
get<ConstitutionFollowRequest> {
repo.findFollow(citizen, it.constitution)?.let { follow ->
voter.assert { canView(follow, citizenOrNull) }
call.respond(follow)
} ?: call.respond(HttpStatusCode.NotFound)
}
}
}

View File

@@ -0,0 +1,27 @@
package fr.dcproject.component.follow.routes.constitution
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.component.follow.FollowConstitutionRepository
import fr.dcproject.component.follow.FollowVoter
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.response.respond
import io.ktor.routing.Route
@KtorExperimentalLocationsAPI
object GetMyFollowsConstitution {
@Location("/citizens/{citizen}/follows/constitutions")
class CitizenFollowConstitutionRequest(val citizen: CitizenRef)
fun Route.getMyFollowsConstitution(repo: FollowConstitutionRepository, voter: FollowVoter) {
get<CitizenFollowConstitutionRequest> {
val follows = repo.findByCitizen(it.citizen)
voter.assert { canView(follows.result, citizenOrNull) }
call.respond(follows)
}
}
}

View File

@@ -0,0 +1,31 @@
package fr.dcproject.component.follow.routes.constitution
import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.follow.FollowConstitutionRepository
import fr.dcproject.component.follow.FollowForUpdate
import fr.dcproject.component.follow.FollowVoter
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.voter.assert
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.delete
import io.ktor.response.respond
import io.ktor.routing.Route
@KtorExperimentalLocationsAPI
object UnfollowConstitution {
@Location("/constitutions/{constitution}/follows")
class ConstitutionUnfollowRequest(val constitution: ConstitutionRef)
fun Route.unfollowConstitution(repo: FollowConstitutionRepository, voter: FollowVoter) {
delete<ConstitutionUnfollowRequest> {
val follow = FollowForUpdate(target = it.constitution, createdBy = this.citizen)
voter.assert { canDelete(follow, citizenOrNull) }
repo.unfollow(follow)
call.respond(HttpStatusCode.NoContent)
}
}
}