feature #12: Add constitution Entity, repository and route
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
32
src/main/kotlin/fr/dcproject/entity/Constitution.kt
Normal file
32
src/main/kotlin/fr/dcproject/entity/Constitution.kt
Normal 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)
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
src/main/kotlin/fr/dcproject/repository/Constitution.kt
Normal file
41
src/main/kotlin/fr/dcproject/repository/Constitution.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/main/kotlin/fr/dcproject/routes/Constitution.kt
Normal file
30
src/main/kotlin/fr/dcproject/routes/Constitution.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
102
src/test/kotlin/ConstitutionTest.kt
Normal file
102
src/test/kotlin/ConstitutionTest.kt
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user