From 7ae30bd3cd7923515b3b874fc0e14c66fcc26d60 Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Thu, 15 Aug 2019 15:59:46 +0200 Subject: [PATCH] feature #23: create route for citizen --- src/main/kotlin/fr/dcproject/Application.kt | 15 ++++++- src/main/kotlin/fr/dcproject/Module.kt | 3 ++ .../kotlin/fr/dcproject/repository/Citizen.kt | 41 ++++++++++++++++++ .../kotlin/fr/dcproject/routes/Citizen.kt | 21 +++++++++ src/main/kotlin/fr/dcproject/routes/Paths.kt | 8 ++++ .../sql/functions/citizen/find_citizens.sql | 43 +++++++++++++++++++ src/test/resources/feature/citizen.feature | 11 +++++ 7 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/fr/dcproject/repository/Citizen.kt create mode 100644 src/main/kotlin/fr/dcproject/routes/Citizen.kt create mode 100644 src/main/resources/sql/functions/citizen/find_citizens.sql create mode 100644 src/test/resources/feature/citizen.feature diff --git a/src/main/kotlin/fr/dcproject/Application.kt b/src/main/kotlin/fr/dcproject/Application.kt index 9b62b00..e9ce188 100644 --- a/src/main/kotlin/fr/dcproject/Application.kt +++ b/src/main/kotlin/fr/dcproject/Application.kt @@ -7,8 +7,10 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.joda.JodaModule import fr.dcproject.entity.Article +import fr.dcproject.entity.Citizen import fr.dcproject.entity.Constitution import fr.dcproject.routes.article +import fr.dcproject.routes.citizen import fr.dcproject.routes.constitution import fr.dcproject.routes.followArticle import fr.postgresjson.migration.Migrations @@ -30,6 +32,7 @@ import org.koin.ktor.ext.get import org.slf4j.event.Level import java.util.* import fr.dcproject.repository.Article as RepositoryArticle +import fr.dcproject.repository.Citizen as RepositoryCitizen import fr.dcproject.repository.Constitution as RepositoryConstitution fun main(args: Array): Unit = io.ktor.server.jetty.EngineMain.main(args) @@ -63,7 +66,7 @@ fun Application.module() { } } - // create generic convert for entityI + // TODO: create generic convert for entityI convert
{ decode { values, _ -> val id = values.singleOrNull()?.let { UUID.fromString(it) } @@ -71,6 +74,7 @@ fun Application.module() { get().findById(id) ?: throw InternalError("Article $values not found") } } + convert { decode { values, _ -> val id = values.singleOrNull()?.let { UUID.fromString(it) } @@ -78,6 +82,14 @@ fun Application.module() { get().findById(id) ?: throw InternalError("Constitution $values not found") } } + + convert { + decode { values, _ -> + val id = values.singleOrNull()?.let { UUID.fromString(it) } + ?: throw InternalError("Cannot convert $values to UUID") + get().findById(id) ?: throw InternalError("Citizen $values not found") + } + } } install(Locations) { @@ -106,6 +118,7 @@ fun Application.module() { install(Routing) { article(get()) + citizen(get()) constitution(get()) followArticle(get()) } diff --git a/src/main/kotlin/fr/dcproject/Module.kt b/src/main/kotlin/fr/dcproject/Module.kt index 128c39b..75d4ad7 100644 --- a/src/main/kotlin/fr/dcproject/Module.kt +++ b/src/main/kotlin/fr/dcproject/Module.kt @@ -1,12 +1,14 @@ package fr.dcproject import fr.dcproject.repository.FollowArticleRepository +import fr.dcproject.repository.FollowConstitutionRepository import fr.postgresjson.connexion.Connection import fr.postgresjson.connexion.Requester import fr.postgresjson.migration.Migrations import io.ktor.util.KtorExperimentalAPI import org.koin.dsl.module import fr.dcproject.repository.Article as ArticleRepository +import fr.dcproject.repository.Citizen as CitizenRepository import fr.dcproject.repository.Constitution as ConstitutionRepository val config = Config() @@ -25,6 +27,7 @@ val Module = module { // TODO: create generic declaration single { ArticleRepository(get()) } + single { CitizenRepository(get()) } single { ConstitutionRepository(get()) } single { FollowArticleRepository(get()) } diff --git a/src/main/kotlin/fr/dcproject/repository/Citizen.kt b/src/main/kotlin/fr/dcproject/repository/Citizen.kt new file mode 100644 index 0000000..c88415c --- /dev/null +++ b/src/main/kotlin/fr/dcproject/repository/Citizen.kt @@ -0,0 +1,41 @@ +package fr.dcproject.repository + +import fr.postgresjson.connexion.Paginated +import fr.postgresjson.connexion.Requester +import fr.postgresjson.repository.RepositoryI +import fr.postgresjson.repository.RepositoryI.Direction +import net.pearx.kasechange.toSnakeCase +import java.util.* +import fr.dcproject.entity.Citizen as CitizenEntity + +class Citizen(override var requester: Requester) : RepositoryI { + override val entityName = CitizenEntity::class + + fun findById(id: UUID): CitizenEntity? { + val function = requester.getFunction("find_citizen_by_id") + return function.selectOne("id" to id) + } + + fun find( + page: Int = 1, + limit: Int = 50, + sort: String? = null, + direction: Direction? = null, + search: String? = null + ): Paginated { + return requester + .getFunction("find_citizens") + .select( + page, limit, + "sort" to sort?.toSnakeCase(), + "direction" to direction, + "search" to search + ) + } + + fun upsert(citizen: CitizenEntity): CitizenEntity? { + return requester + .getFunction("upsert_citizen") + .selectOne("resource" to citizen) + } +} diff --git a/src/main/kotlin/fr/dcproject/routes/Citizen.kt b/src/main/kotlin/fr/dcproject/routes/Citizen.kt new file mode 100644 index 0000000..e2e5a2d --- /dev/null +++ b/src/main/kotlin/fr/dcproject/routes/Citizen.kt @@ -0,0 +1,21 @@ +package fr.dcproject.routes + +import Paths +import io.ktor.application.call +import io.ktor.locations.KtorExperimentalLocationsAPI +import io.ktor.locations.get +import io.ktor.response.respond +import io.ktor.routing.Route +import fr.dcproject.repository.Citizen as CitizenRepository + +@KtorExperimentalLocationsAPI +fun Route.citizen(repo: CitizenRepository) { + get { + val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search) + call.respond(citizens) + } + + get { + call.respond(it.citizen) + } +} \ No newline at end of file diff --git a/src/main/kotlin/fr/dcproject/routes/Paths.kt b/src/main/kotlin/fr/dcproject/routes/Paths.kt index 0ff73b8..9e8e55f 100644 --- a/src/main/kotlin/fr/dcproject/routes/Paths.kt +++ b/src/main/kotlin/fr/dcproject/routes/Paths.kt @@ -1,4 +1,5 @@ import fr.dcproject.entity.Article +import fr.dcproject.entity.Citizen import fr.dcproject.entity.Constitution import fr.dcproject.entity.Follow import fr.postgresjson.repository.RepositoryI.Direction @@ -22,4 +23,11 @@ object Paths { } @Location("/constitutions/{constitution}") class ConstitutionRequest(val constitution: Constitution) @Location("/constitutions") class PostConstitutionRequest + + + @Location("/citizens") class CitizensRequest(page: Int = 1, limit: Int = 50, val sort: String? = null, val direction: Direction? = null, 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 + } + @Location("/citizens/{citizen}") class CitizenRequest(val citizen: Citizen) } \ No newline at end of file diff --git a/src/main/resources/sql/functions/citizen/find_citizens.sql b/src/main/resources/sql/functions/citizen/find_citizens.sql new file mode 100644 index 0000000..5f57222 --- /dev/null +++ b/src/main/resources/sql/functions/citizen/find_citizens.sql @@ -0,0 +1,43 @@ +create or replace function find_citizens( + search text default null, + direction text default 'desc', + sort text default 'created_at', + "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 citizen) + into resource, total + from ( + select + z.* + from citizen as z + where "search" is null or ( + (name->'first_name')::text ilike '%'||"search"||'%' or + (name->'last_name')::text ilike '%'||"search"||'%' + ) + order by + case direction when 'asc' then + case sort + when 'name' then (z.name->'first_name')::text + when 'created_at' then z.created_at::text + else null + end + end, + case direction when 'desc' then + case sort + when 'name' then (z.name->'first_name')::text + when 'created_at' then z.created_at::text + end + end + desc, + z.created_at desc + limit "limit" offset "offset" + ) as t; +end; +$$; + +-- drop function if exists find_citizens(text, text, text, int, int); diff --git a/src/test/resources/feature/citizen.feature b/src/test/resources/feature/citizen.feature new file mode 100644 index 0000000..6923db5 --- /dev/null +++ b/src/test/resources/feature/citizen.feature @@ -0,0 +1,11 @@ +Feature: citizens routes + + Scenario: The route for get citizens must response a 200 + When I send a "GET" request to "/citizens" + Then the response status code should be 200 + + Scenario: The route for get one citizen must response a 200 and return citizen + When I send a "GET" request to "/citizens/6434f4f9-f570-f22a-c134-8668350651ff" + Then the response status code should be 200 + And the response should contain object: + | id | 6434f4f9-f570-f22a-c134-8668350651ff |