feature #14: Add routes for get follows of one citizen

This commit is contained in:
2019-08-21 13:37:16 +02:00
parent 9b2b5e681f
commit 4e9f737e00
11 changed files with 179 additions and 21 deletions

View File

@@ -1,7 +1,5 @@
package fr.dcproject package fr.dcproject
import fr.dcproject.repository.FollowArticleRepository
import fr.dcproject.repository.FollowConstitutionRepository
import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.migration.Migrations import fr.postgresjson.migration.Migrations
@@ -10,6 +8,8 @@ import org.koin.dsl.module
import fr.dcproject.repository.Article as ArticleRepository import fr.dcproject.repository.Article as ArticleRepository
import fr.dcproject.repository.Citizen as CitizenRepository import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.Constitution as ConstitutionRepository import fr.dcproject.repository.Constitution as ConstitutionRepository
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
import fr.dcproject.repository.FollowConstitution as FollowConstitutionRepository
val config = Config() val config = Config()

View File

@@ -1,12 +1,9 @@
package fr.dcproject.entity package fr.dcproject.entity
import fr.postgresjson.entity.EntityI import fr.postgresjson.entity.UuidEntity
import java.util.* import java.util.*
class Follow <T: EntityI<UUID>> ( class Follow <T: UuidEntity> (
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
citizen: Citizen, citizen: Citizen,
override var target: T override var target: T
): Extra<T>(id, citizen) ): Extra<T>(id, citizen)
typealias FollowArticleEntity = Follow<Article>
typealias FollowConstitutionEntity = Follow<Constitution>

View File

@@ -1,16 +1,37 @@
package fr.dcproject.repository package fr.dcproject.repository
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.EntityI import fr.postgresjson.entity.UuidEntity
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
import java.util.* import java.util.*
import kotlin.reflect.KClass import kotlin.reflect.KClass
import fr.dcproject.entity.Article as ArticleEntity import fr.dcproject.entity.Article as ArticleEntity
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.entity.Constitution as ConstitutionEntity import fr.dcproject.entity.Constitution as ConstitutionEntity
import fr.dcproject.entity.Follow as FollowEntity import fr.dcproject.entity.Follow as FollowEntity
open class Follow <T: EntityI<UUID>>(override var requester: Requester): RepositoryI<FollowEntity<T>> { open class Follow <T: UuidEntity>(override var requester: Requester): RepositoryI<FollowEntity<T>> {
override val entityName = FollowEntity::class as KClass<FollowEntity<T>> override val entityName = FollowEntity::class as KClass<FollowEntity<T>>
open fun findByCitizenId(
citizen: CitizenEntity,
page: Int = 1,
limit: Int = 50
): Paginated<FollowEntity<T>> =
findByCitizenId(citizen.id ?: error("The citizen must have an id"), page, limit)
open fun findByCitizenId(
citizenId: UUID,
page: Int = 1,
limit: Int = 50
): Paginated<FollowEntity<T>> {
return requester.run {
getFunction("find_follows_by_citizen")
.select(page, limit,
"citizen_id" to citizenId
)
}
}
fun follow(follow: FollowEntity<T>) { fun follow(follow: FollowEntity<T>) {
val reference = follow.target::class.simpleName!!.toLowerCase() val reference = follow.target::class.simpleName!!.toLowerCase()
@@ -34,5 +55,33 @@ open class Follow <T: EntityI<UUID>>(override var requester: Requester): Reposit
) )
} }
} }
class FollowArticleRepository(override var requester: Requester): Follow<ArticleEntity>(requester)
class FollowConstitutionRepository(override var requester: Requester): Follow<ConstitutionEntity>(requester) class FollowArticle (requester: Requester): Follow<ArticleEntity>(requester) {
override fun findByCitizenId(
citizenId: UUID,
page: Int,
limit: Int
): Paginated<FollowEntity<ArticleEntity>> {
return requester.run {
getFunction("find_follows_article_by_citizen")
.select(page, limit,
"citizen_id" to citizenId
)
}
}
}
class FollowConstitution (requester: Requester): Follow<ConstitutionEntity>(requester) {
override fun findByCitizenId(
citizenId: UUID,
page: Int,
limit: Int
): Paginated<FollowEntity<ConstitutionEntity>> {
return requester.run {
getFunction("find_follows_constitution_by_citizen")
.select(page, limit,
"citizen_id" to citizenId
)
}
}
}

View File

@@ -3,17 +3,18 @@ package fr.dcproject.routes
import Paths import Paths
import fr.dcproject.entity.Citizen import fr.dcproject.entity.Citizen
import fr.dcproject.entity.User import fr.dcproject.entity.User
import fr.dcproject.repository.FollowArticleRepository
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.delete import io.ktor.locations.delete
import io.ktor.locations.get
import io.ktor.locations.post import io.ktor.locations.post
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.* import java.util.*
import fr.dcproject.entity.Follow as FollowEntity import fr.dcproject.entity.Follow as FollowEntity
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
// TODO get current citizen // TODO get current citizen
val currentCitizen = Citizen( val currentCitizen = Citizen(
@@ -34,4 +35,9 @@ fun Route.followArticle(repo: FollowArticleRepository) {
repo.unfollow(FollowEntity(target = it.article, citizen = currentCitizen)) repo.unfollow(FollowEntity(target = it.article, citizen = currentCitizen))
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
get<Paths.CitizenFollowArticleRequest> {
val follows = repo.findByCitizenId(it.citizen)
call.respond(follows)
}
} }

View File

@@ -3,17 +3,18 @@ package fr.dcproject.routes
import Paths import Paths
import fr.dcproject.entity.Citizen import fr.dcproject.entity.Citizen
import fr.dcproject.entity.User import fr.dcproject.entity.User
import fr.dcproject.repository.FollowConstitutionRepository
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.delete import io.ktor.locations.delete
import io.ktor.locations.get
import io.ktor.locations.post import io.ktor.locations.post
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import org.joda.time.DateTime import org.joda.time.DateTime
import java.util.* import java.util.*
import fr.dcproject.entity.Follow as FollowEntity import fr.dcproject.entity.Follow as FollowEntity
import fr.dcproject.repository.FollowConstitution as FollowConstitutionRepository
// TODO get current citizen // TODO get current citizen
val currentCitizen2 = Citizen( val currentCitizen2 = Citizen(
@@ -34,4 +35,9 @@ fun Route.followConstitution(repo: FollowConstitutionRepository) {
repo.unfollow(FollowEntity(target = it.constitution, citizen = currentCitizen2)) repo.unfollow(FollowEntity(target = it.constitution, citizen = currentCitizen2))
call.respond(HttpStatusCode.NoContent) call.respond(HttpStatusCode.NoContent)
} }
get<Paths.CitizenFollowConstitutionRequest> {
val follows = repo.findByCitizenId(it.citizen)
call.respond(follows)
}
} }

View File

@@ -1,7 +1,6 @@
import fr.dcproject.entity.Article import fr.dcproject.entity.Article
import fr.dcproject.entity.Citizen import fr.dcproject.entity.Citizen
import fr.dcproject.entity.Constitution import fr.dcproject.entity.Constitution
import fr.dcproject.entity.Follow
import fr.postgresjson.repository.RepositoryI.Direction import fr.postgresjson.repository.RepositoryI.Direction
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location import io.ktor.locations.Location
@@ -31,4 +30,6 @@ object Paths {
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
} }
@Location("/citizens/{citizen}") class CitizenRequest(val citizen: Citizen) @Location("/citizens/{citizen}") class CitizenRequest(val citizen: Citizen)
@Location("/citizens/{citizen}/follows/articles") class CitizenFollowArticleRequest(val citizen: Citizen)
@Location("/citizens/{citizen}/follows/constitutions") class CitizenFollowConstitutionRequest(val citizen: Citizen)
} }

View File

@@ -0,0 +1,26 @@
create or replace function find_follows_article_by_citizen(
_citizen_id uuid,
"limit" int default 50,
"offset" int default 0,
out resource json,
out total int
) language plpgsql as
$$
begin
select json_agg(t), (select count(id) from follow)
into resource, total
from (
select
f.*,
find_article_by_id(f.target_id) as target,
find_citizen_by_id(f.citizen_id) as citizen
from follow as f
where citizen_id = _citizen_id
order by created_at desc,
f.created_at desc
limit "limit" offset "offset"
) as t;
end;
$$;
-- drop function if exists find_follows_article_by_citizen(uuid, int, int);

View File

@@ -0,0 +1,26 @@
create or replace function find_follows_by_citizen(
_citizen_id uuid,
"limit" int default 50,
"offset" int default 0,
out resource json,
out total int
) language plpgsql as
$$
begin
select json_agg(t), (select count(id) from follow)
into resource, total
from (
select
f.*,
json_build_object('id', f.target_id) as target,
find_citizen_by_id(f.citizen_id) as citizen
from follow as f
where citizen_id = _citizen_id
order by created_at desc,
f.created_at desc
limit "limit" offset "offset"
) as t;
end;
$$;
-- drop function if exists find_follows_by_citizen(uuid, int, int);

View File

@@ -0,0 +1,26 @@
create or replace function find_follows_constitution_by_citizen(
_citizen_id uuid,
"limit" int default 50,
"offset" int default 0,
out resource json,
out total int
) language plpgsql as
$$
begin
select json_agg(t), (select count(id) from follow)
into resource, total
from (
select
f.*,
find_constitution_by_id(f.target_id) as target,
find_citizen_by_id(f.citizen_id) as citizen
from follow as f
where citizen_id = _citizen_id
order by created_at desc,
f.created_at desc
limit "limit" offset "offset"
) as t;
end;
$$;
-- drop function if exists find_follows_constitution_by_citizen(uuid, int, int);

View File

@@ -1,6 +1,7 @@
package feature package feature
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonParser
import cucumber.api.java8.En import cucumber.api.java8.En
import fr.dcproject.entity.Citizen import fr.dcproject.entity.Citizen
import fr.dcproject.entity.User import fr.dcproject.entity.User
@@ -21,6 +22,7 @@ import org.koin.test.inject
import org.opentest4j.AssertionFailedError import org.opentest4j.AssertionFailedError
import java.util.* import java.util.*
import kotlin.random.Random import kotlin.random.Random
import kotlin.test.assertTrue
import kotlin.test.asserter import kotlin.test.asserter
import feature.Context.Companion.current as currentContext import feature.Context.Companion.current as currentContext
@@ -103,14 +105,11 @@ class Request: En, KoinTest {
And("the response should contain object:") { expected: DataTable -> And("the response should contain object:") { expected: DataTable ->
val call: TestApplicationCall = currentContext.call ?: throw AssertionFailedError("No call") val call: TestApplicationCall = currentContext.call ?: throw AssertionFailedError("No call")
val p = call.response val p = call.response
val response = Gson().fromJson<Map<String, String>>(p.content, Map::class.java) val response = JsonParser().parse(p.content).getAsJsonObject()
expected.asMap<String, String>(String::class.java, String::class.java).forEach { (key, value) -> expected.asMap<String, String>(String::class.java, String::class.java).forEach { (key, valueExpected) ->
if (response.containsKey(key)) { assertTrue(response.has(key))
assertEquals(value, response[key]) assertEquals(valueExpected, response.get(key).asString)
return@And
}
asserter.fail("The response not contain $key field")
} }
} }
} }

View File

@@ -9,6 +9,17 @@ Feature: follow Article and Constitution
When I send a "POST" request to "/articles/9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b/follow" When I send a "POST" request to "/articles/9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b/follow"
Then the response status code should be 201 Then the response status code should be 201
Scenario: The route for get follows of articles must response a 200 and return objects
Given I have citizen:
| id | 64b7b379-2298-43ec-b428-ba134930cabd |
| firstName | Jaque |
| lastName | Dupuis |
When I send a "GET" request to "/citizens/64b7b379-2298-43ec-b428-ba134930cabd/follows/articles"
Then the response status code should be 200
And the response should contain object:
| current_page | 1 |
| limit | 50 |
Scenario: The route for unfollow article must response a 204 Scenario: The route for unfollow article must response a 204
Given I have citizen: Given I have citizen:
| id | 64b7b379-2298-43ec-b428-ba134930cabd | | id | 64b7b379-2298-43ec-b428-ba134930cabd |
@@ -26,6 +37,17 @@ Feature: follow Article and Constitution
When I send a "POST" request to "/constitutions/72aa1ee1-4963-eb44-c9e0-5ce6e0f18f00/follow" When I send a "POST" request to "/constitutions/72aa1ee1-4963-eb44-c9e0-5ce6e0f18f00/follow"
Then the response status code should be 201 Then the response status code should be 201
Scenario: The route for get follows of constitutions must response a 200 and return objects
Given I have citizen:
| id | 64b7b379-2298-43ec-b428-ba134930cabd |
| firstName | Jaque |
| lastName | Dupuis |
When I send a "GET" request to "/citizens/64b7b379-2298-43ec-b428-ba134930cabd/follows/constitutions"
Then the response status code should be 200
And the response should contain object:
| current_page | 1 |
| limit | 50 |
Scenario: The route for unfollow constitution must response a 204 Scenario: The route for unfollow constitution must response a 204
Given I have citizen: Given I have citizen:
| id | 64b7b379-2298-43ec-b428-ba134930cabd | | id | 64b7b379-2298-43ec-b428-ba134930cabd |