From 86d699c9c00de2f3f4414862f9db36b65ce631af Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Tue, 6 Aug 2019 18:20:21 +0200 Subject: [PATCH] feature #12: Add constitution Entity, repository and route --- src/main/kotlin/fr/dcproject/Application.kt | 18 ++++ src/main/kotlin/fr/dcproject/Module.kt | 4 + .../kotlin/fr/dcproject/entity/Article.kt | 7 +- .../fr/dcproject/entity/Constitution.kt | 32 ++++++ .../kotlin/fr/dcproject/repository/Article.kt | 18 +--- .../fr/dcproject/repository/Constitution.kt | 41 +++++++ .../fr/dcproject/routes/Constitution.kt | 30 ++++++ src/main/kotlin/fr/dcproject/routes/Paths.kt | 11 +- .../sql/functions/article/upsert_article.sql | 3 + .../constitution/find_constitutions.sql | 42 ++++++++ src/test/kotlin/ConstitutionTest.kt | 102 ++++++++++++++++++ 11 files changed, 288 insertions(+), 20 deletions(-) create mode 100644 src/main/kotlin/fr/dcproject/entity/Constitution.kt create mode 100644 src/main/kotlin/fr/dcproject/repository/Constitution.kt create mode 100644 src/main/kotlin/fr/dcproject/routes/Constitution.kt create mode 100644 src/main/resources/sql/functions/constitution/find_constitutions.sql create mode 100644 src/test/kotlin/ConstitutionTest.kt diff --git a/src/main/kotlin/fr/dcproject/Application.kt b/src/main/kotlin/fr/dcproject/Application.kt index 3dbb4c0..19ea113 100644 --- a/src/main/kotlin/fr/dcproject/Application.kt +++ b/src/main/kotlin/fr/dcproject/Application.kt @@ -7,7 +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.Constitution import fr.dcproject.routes.article +import fr.dcproject.routes.constitution +import fr.postgresjson.migration.Migrations import io.ktor.application.Application import io.ktor.application.install import io.ktor.auth.Authentication @@ -23,6 +26,7 @@ import org.koin.ktor.ext.Koin import org.koin.ktor.ext.get import java.util.* import fr.dcproject.repository.Article as RepositoryArticle +import fr.dcproject.repository.Constitution as RepositoryConstitution fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) @@ -36,6 +40,7 @@ fun Application.module() { } install(DataConversion) { + // TODO move to postgresJson lib convert { decode { values, _ -> values.singleOrNull()?.let { UUID.fromString(it) } @@ -49,6 +54,8 @@ fun Application.module() { } } } + + // create generic convert for entityI convert
{ decode { values, _ -> val id = values.singleOrNull()?.let { UUID.fromString(it) } @@ -56,6 +63,13 @@ fun Application.module() { get().findById(id) ?: throw InternalError("Article $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("Constitution $values not found") + } + } } install(Locations) { @@ -84,5 +98,9 @@ fun Application.module() { install(Routing) { article(get()) + constitution(get()) } + + // TODO move to postgresJson lib + get().run() } diff --git a/src/main/kotlin/fr/dcproject/Module.kt b/src/main/kotlin/fr/dcproject/Module.kt index 1d47780..bf1e31a 100644 --- a/src/main/kotlin/fr/dcproject/Module.kt +++ b/src/main/kotlin/fr/dcproject/Module.kt @@ -6,6 +6,7 @@ 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.Constitution as ConstitutionRepository val config = Config() @@ -21,6 +22,9 @@ val Module = module { functionsDirectory = config.sqlFiles.resolve("functions") ).createRequester() } + // create generic declaration single { ArticleRepository(get()) } + single { ConstitutionRepository(get()) } + single { Migrations(connection = get(), directory = config.sqlFiles) } } diff --git a/src/main/kotlin/fr/dcproject/entity/Article.kt b/src/main/kotlin/fr/dcproject/entity/Article.kt index a169818..2036298 100644 --- a/src/main/kotlin/fr/dcproject/entity/Article.kt +++ b/src/main/kotlin/fr/dcproject/entity/Article.kt @@ -5,15 +5,14 @@ import java.util.* class Article( id: UUID = UUID.randomUUID(), - var versionId: UUID = UUID.randomUUID(), - var versionNumber: Int? = null, var title: String?, var annonymous: Boolean? = true, var content: String?, var description: String?, var tags: List = emptyList(), - override var createdBy: Citizen? + createdBy: Citizen? ): UuidEntity(id), + EntityVersioning by UuidEntityVersioning(), EntityCreatedAt by EntityCreatedAtImp(), - CreatedBy by EntityCreatedByImp() \ No newline at end of file + CreatedBy by EntityCreatedByImp(createdBy) \ No newline at end of file diff --git a/src/main/kotlin/fr/dcproject/entity/Constitution.kt b/src/main/kotlin/fr/dcproject/entity/Constitution.kt new file mode 100644 index 0000000..91bf89c --- /dev/null +++ b/src/main/kotlin/fr/dcproject/entity/Constitution.kt @@ -0,0 +1,32 @@ +package fr.dcproject.entity + +import fr.postgresjson.entity.* +import java.util.* + +class Constitution( + id: UUID = UUID.randomUUID(), + var title: String?, + var annonymous: Boolean?, + var titles: List, + createdBy: Citizen? +): UuidEntity(id), + EntityVersioning<UUID, Int> by UuidEntityVersioning(), + EntityCreatedAt by EntityCreatedAtImp(), + CreatedBy<Citizen> by EntityCreatedByImp(createdBy) { + + init{ + titles.forEachIndexed { index, title -> + title.createdBy = this.createdBy + title.rank = index + } + } + + class Title( + id: UUID = UUID.randomUUID(), + var name: String?, + var rank: Int? = null, + createdBy: Citizen? = null + ): UuidEntity(id), + EntityCreatedAt by EntityCreatedAtImp(), + CreatedBy<Citizen> by EntityCreatedByImp(createdBy) +} diff --git a/src/main/kotlin/fr/dcproject/repository/Article.kt b/src/main/kotlin/fr/dcproject/repository/Article.kt index c9b38e0..0a229dc 100644 --- a/src/main/kotlin/fr/dcproject/repository/Article.kt +++ b/src/main/kotlin/fr/dcproject/repository/Article.kt @@ -2,8 +2,8 @@ package fr.dcproject.repository import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Requester -import fr.postgresjson.entity.EntitiesCollections import fr.postgresjson.repository.RepositoryI +import fr.postgresjson.repository.RepositoryI.Direction import net.pearx.kasechange.toSnakeCase import java.util.* import fr.dcproject.entity.Article as ArticleEntity @@ -13,12 +13,7 @@ class Article(override var requester: Requester) : RepositoryI<ArticleEntity> { fun findById(id: UUID): ArticleEntity? { val function = requester.getFunction("find_article_by_id") - return when (val e = EntitiesCollections().get(id) as ArticleEntity?) { - null -> { - function.selectOne("id" to id) - } - else -> e - } + return function.selectOne("id" to id) } fun find( @@ -41,13 +36,6 @@ class Article(override var requester: Requester) : RepositoryI<ArticleEntity> { fun upsert(article: ArticleEntity): ArticleEntity? { return requester .getFunction("upsert_article") - .selectOne<ArticleEntity>("resource" to article)?.also { - EntitiesCollections().set(it) - } - } - - enum class Direction { - asc, - desc + .selectOne("resource" to article) } } diff --git a/src/main/kotlin/fr/dcproject/repository/Constitution.kt b/src/main/kotlin/fr/dcproject/repository/Constitution.kt new file mode 100644 index 0000000..eadff39 --- /dev/null +++ b/src/main/kotlin/fr/dcproject/repository/Constitution.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.Constitution as ConstitutionEntity + +class Constitution(override var requester: Requester) : RepositoryI<ConstitutionEntity> { + override val entityName = ConstitutionEntity::class + + fun findById(id: UUID): ConstitutionEntity? { + val function = requester.getFunction("find_constitution_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<ConstitutionEntity> { + return requester + .getFunction("find_constitutions") + .select( + page, limit, + "sort" to sort?.toSnakeCase(), + "direction" to direction, + "search" to search + ) + } + + fun upsert(article: ConstitutionEntity): ConstitutionEntity? { + return requester + .getFunction("upsert_constitution") + .selectOne("resource" to article) + } +} diff --git a/src/main/kotlin/fr/dcproject/routes/Constitution.kt b/src/main/kotlin/fr/dcproject/routes/Constitution.kt new file mode 100644 index 0000000..9076cec --- /dev/null +++ b/src/main/kotlin/fr/dcproject/routes/Constitution.kt @@ -0,0 +1,30 @@ +package fr.dcproject.routes + +import Paths +import io.ktor.application.call +import io.ktor.locations.KtorExperimentalLocationsAPI +import io.ktor.locations.get +import io.ktor.locations.post +import io.ktor.request.receive +import io.ktor.response.respond +import io.ktor.routing.Route +import fr.dcproject.entity.Constitution as ConstitutionEntity +import fr.dcproject.repository.Constitution as ConstitutionRepository + +@KtorExperimentalLocationsAPI +fun Route.constitution(repo: ConstitutionRepository) { + get<Paths.ConstitutionsRequest> { + val constitutions = repo.find(it.page, it.limit, it.sort, it.direction, it.search) + call.respond(constitutions) + } + + get<Paths.ConstitutionRequest> { + call.respond(it.constitution) + } + + post<Paths.PostConstitutionRequest>() { + val constitution = call.receive<ConstitutionEntity>() + repo.upsert(constitution) + call.respond(constitution) + } +} \ 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 085b779..744c7b8 100644 --- a/src/main/kotlin/fr/dcproject/routes/Paths.kt +++ b/src/main/kotlin/fr/dcproject/routes/Paths.kt @@ -1,5 +1,6 @@ import fr.dcproject.entity.Article -import fr.dcproject.repository.Article.Direction +import fr.dcproject.entity.Constitution +import fr.postgresjson.repository.RepositoryI.Direction import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Location @@ -11,4 +12,12 @@ object Paths { } @Location("/articles/{article}") class ArticleRequest(val article: Article) @Location("/articles") class PostArticleRequest + + + @Location("/constitutions") class ConstitutionsRequest(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("/constitutions/{constitution}") class ConstitutionRequest(val constitution: Constitution) + @Location("/constitutions") class PostConstitutionRequest } \ No newline at end of file diff --git a/src/main/resources/sql/functions/article/upsert_article.sql b/src/main/resources/sql/functions/article/upsert_article.sql index 12fd182..36f7c5d 100644 --- a/src/main/resources/sql/functions/article/upsert_article.sql +++ b/src/main/resources/sql/functions/article/upsert_article.sql @@ -17,6 +17,9 @@ begin returning id into new_id; if resource->>'relations' is not null then + delete from article_relations + where source_id = (resource->>'id')::uuid; + insert into article_relations (source_id, target_id, created_by_id) select (resource->>'id')::uuid, diff --git a/src/main/resources/sql/functions/constitution/find_constitutions.sql b/src/main/resources/sql/functions/constitution/find_constitutions.sql new file mode 100644 index 0000000..ea1156a --- /dev/null +++ b/src/main/resources/sql/functions/constitution/find_constitutions.sql @@ -0,0 +1,42 @@ +create or replace function find_constitutions( + 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 constitution) + into resource, total + from ( + select + c.*, + find_citizen_by_id(c.created_by_id) as created_by, + find_constitution_titles_by_id(c.id) as titles + from constitution as c + where "search" is null or title ilike '%'||"search"||'%' + order by + case direction when 'asc' then + case sort + when 'title' then c.title + when 'created_at' then c.created_at::text + else null + end + end, + case direction when 'desc' then + case sort + when 'title' then c.title + when 'created_at' then c.created_at::text + end + end + desc, + c.created_at desc + limit "limit" offset "offset" + ) as t; +end; +$$; + +-- drop function if exists find_constitutions(json, int, int); diff --git a/src/test/kotlin/ConstitutionTest.kt b/src/test/kotlin/ConstitutionTest.kt new file mode 100644 index 0000000..bfc8f6c --- /dev/null +++ b/src/test/kotlin/ConstitutionTest.kt @@ -0,0 +1,102 @@ +import fr.dcproject.entity.Citizen +import fr.dcproject.entity.Constitution +import fr.dcproject.entity.User +import fr.postgresjson.serializer.deserialize +import fr.postgresjson.serializer.serialize +import io.ktor.locations.KtorExperimentalLocationsAPI +import io.ktor.util.KtorExperimentalAPI +import org.amshove.kluent.`should equal` +import org.amshove.kluent.shouldBe +import org.intellij.lang.annotations.Language +import org.joda.time.DateTime +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS + +@KtorExperimentalLocationsAPI +@KtorExperimentalAPI +@TestInstance(PER_CLASS) +class ConstitutionTest { + @Language("JSON") + private val constitutionJson: String = """{ + "id":"15814bb6-8d90-4c6a-a456-c3939a8ec75e", + "title":"Hello world!", + "annonymous":true, + "titles":[ + { + "id":"8156b66f-a9c8-4fd9-8375-a8a1f42ccfd2", + "name":"plop", + "rank":0, + "created_by":{ + "id":"18902d22-245d-4d44-b23d-9f0e82688612", + "name":{ + "first_name":"Jaque", + "last_name":"Bono", + "civility":null + }, + "birthday":"2019-08-07T20:34:08.013Z", + "user_id":null, + "vote_annonymous":null, + "follow_annonymous":null, + "user":{ + "id":"257abe9f-be17-4ad3-ae6a-b1dc9706d5d7", + "username":"jaque", + "blocked_at":null, + "plain_password":"azerty", + "created_at":null, + "updated_at":null + }, + "created_at":null + }, + "created_at":null + } + ], + "created_by":{ + "id":"18902d22-245d-4d44-b23d-9f0e82688612", + "name":{ + "first_name":"Jaque", + "last_name":"Bono", + "civility":null + }, + "birthday":"2019-08-07T20:34:08.013Z", + "user_id":null, + "vote_annonymous":null, + "follow_annonymous":null, + "user":{ + "id":"257abe9f-be17-4ad3-ae6a-b1dc9706d5d7", + "username":"jaque", + "plain_password":"azerty" + } + }, + "created_at":null, + "version_id":"3311a7af-2a62-4e31-b4cd-889f8ead9737", + "version_number":null + }""".trimIndent() + + @Test + fun `test Constitution serialize`() { + val user = User(username = "jaque", plainPassword = "azerty") + val citizen = Citizen( + name = Citizen.Name("Jaque", "Bono"), + birthday = DateTime.now(), + user = user + ) + val title1 = Constitution.Title( + name = "plop" + ) + val constitution = Constitution( + title = "Hello world!", + annonymous = true, + titles = listOf(title1), + createdBy = citizen + ) + println(constitution.serialize()) + constitution.serialize().contains("""Hello world!""") shouldBe true + } + + @Test + fun `test Constitution Deserialize`() { + val constitution2: Constitution = constitutionJson.deserialize()!! + constitution2.id.toString() `should equal` "15814bb6-8d90-4c6a-a456-c3939a8ec75e" + } +}