feature #12: Add constitution Entity, repository and route

This commit is contained in:
2019-08-06 18:20:21 +02:00
parent 6131935036
commit 86d699c9c0
11 changed files with 288 additions and 20 deletions

View File

@@ -7,7 +7,10 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.joda.JodaModule import com.fasterxml.jackson.datatype.joda.JodaModule
import fr.dcproject.entity.Article import fr.dcproject.entity.Article
import fr.dcproject.entity.Constitution
import fr.dcproject.routes.article import fr.dcproject.routes.article
import fr.dcproject.routes.constitution
import fr.postgresjson.migration.Migrations
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.install import io.ktor.application.install
import io.ktor.auth.Authentication import io.ktor.auth.Authentication
@@ -23,6 +26,7 @@ import org.koin.ktor.ext.Koin
import org.koin.ktor.ext.get import org.koin.ktor.ext.get
import java.util.* import java.util.*
import fr.dcproject.repository.Article as RepositoryArticle import fr.dcproject.repository.Article as RepositoryArticle
import fr.dcproject.repository.Constitution as RepositoryConstitution
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
@@ -36,6 +40,7 @@ fun Application.module() {
} }
install(DataConversion) { install(DataConversion) {
// TODO move to postgresJson lib
convert<UUID> { convert<UUID> {
decode { values, _ -> decode { values, _ ->
values.singleOrNull()?.let { UUID.fromString(it) } values.singleOrNull()?.let { UUID.fromString(it) }
@@ -49,6 +54,8 @@ fun Application.module() {
} }
} }
} }
// create generic convert for entityI
convert<Article> { convert<Article> {
decode { values, _ -> decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) } val id = values.singleOrNull()?.let { UUID.fromString(it) }
@@ -56,6 +63,13 @@ fun Application.module() {
get<RepositoryArticle>().findById(id) ?: throw InternalError("Article $values not found") get<RepositoryArticle>().findById(id) ?: throw InternalError("Article $values not found")
} }
} }
convert<Constitution> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<RepositoryConstitution>().findById(id) ?: throw InternalError("Constitution $values not found")
}
}
} }
install(Locations) { install(Locations) {
@@ -84,5 +98,9 @@ fun Application.module() {
install(Routing) { install(Routing) {
article(get()) article(get())
constitution(get())
} }
// TODO move to postgresJson lib
get<Migrations>().run()
} }

View File

@@ -6,6 +6,7 @@ import fr.postgresjson.migration.Migrations
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import org.koin.dsl.module import org.koin.dsl.module
import fr.dcproject.repository.Article as ArticleRepository import fr.dcproject.repository.Article as ArticleRepository
import fr.dcproject.repository.Constitution as ConstitutionRepository
val config = Config() val config = Config()
@@ -21,6 +22,9 @@ val Module = module {
functionsDirectory = config.sqlFiles.resolve("functions") functionsDirectory = config.sqlFiles.resolve("functions")
).createRequester() } ).createRequester() }
// create generic declaration
single { ArticleRepository(get()) } single { ArticleRepository(get()) }
single { ConstitutionRepository(get()) }
single { Migrations(connection = get(), directory = config.sqlFiles) } single { Migrations(connection = get(), directory = config.sqlFiles) }
} }

View File

@@ -5,15 +5,14 @@ import java.util.*
class Article( class Article(
id: UUID = UUID.randomUUID(), id: UUID = UUID.randomUUID(),
var versionId: UUID = UUID.randomUUID(),
var versionNumber: Int? = null,
var title: String?, var title: String?,
var annonymous: Boolean? = true, var annonymous: Boolean? = true,
var content: String?, var content: String?,
var description: String?, var description: String?,
var tags: List<String> = emptyList(), var tags: List<String> = emptyList(),
override var createdBy: Citizen? createdBy: Citizen?
): ):
UuidEntity(id), UuidEntity(id),
EntityVersioning<UUID, Int> by UuidEntityVersioning(),
EntityCreatedAt by EntityCreatedAtImp(), EntityCreatedAt by EntityCreatedAtImp(),
CreatedBy<Citizen> by EntityCreatedByImp() CreatedBy<Citizen> by EntityCreatedByImp(createdBy)

View File

@@ -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<Title>,
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)
}

View File

@@ -2,8 +2,8 @@ package fr.dcproject.repository
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.EntitiesCollections
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
import fr.postgresjson.repository.RepositoryI.Direction
import net.pearx.kasechange.toSnakeCase import net.pearx.kasechange.toSnakeCase
import java.util.* import java.util.*
import fr.dcproject.entity.Article as ArticleEntity import fr.dcproject.entity.Article as ArticleEntity
@@ -13,12 +13,7 @@ class Article(override var requester: Requester) : RepositoryI<ArticleEntity> {
fun findById(id: UUID): ArticleEntity? { fun findById(id: UUID): ArticleEntity? {
val function = requester.getFunction("find_article_by_id") val function = requester.getFunction("find_article_by_id")
return when (val e = EntitiesCollections().get(id) as ArticleEntity?) { return function.selectOne("id" to id)
null -> {
function.selectOne("id" to id)
}
else -> e
}
} }
fun find( fun find(
@@ -41,13 +36,6 @@ class Article(override var requester: Requester) : RepositoryI<ArticleEntity> {
fun upsert(article: ArticleEntity): ArticleEntity? { fun upsert(article: ArticleEntity): ArticleEntity? {
return requester return requester
.getFunction("upsert_article") .getFunction("upsert_article")
.selectOne<ArticleEntity>("resource" to article)?.also { .selectOne("resource" to article)
EntitiesCollections().set(it)
}
}
enum class Direction {
asc,
desc
} }
} }

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -1,5 +1,6 @@
import fr.dcproject.entity.Article 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.KtorExperimentalLocationsAPI
import io.ktor.locations.Location import io.ktor.locations.Location
@@ -11,4 +12,12 @@ object Paths {
} }
@Location("/articles/{article}") class ArticleRequest(val article: Article) @Location("/articles/{article}") class ArticleRequest(val article: Article)
@Location("/articles") class PostArticleRequest @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
} }

View File

@@ -17,6 +17,9 @@ begin
returning id into new_id; returning id into new_id;
if resource->>'relations' is not null then 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) insert into article_relations (source_id, target_id, created_by_id)
select select
(resource->>'id')::uuid, (resource->>'id')::uuid,

View File

@@ -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);

View File

@@ -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"
}
}