From c5a2f92b19daebe0dacb958757c84d50f3dd6825 Mon Sep 17 00:00:00 2001 From: Fabrice Lecomte Date: Thu, 3 Oct 2019 15:43:39 +0200 Subject: [PATCH] create SQL function for get citizen votes with multiple target ids add updated_at field on vote table --- .../kotlin/fr/dcproject/repository/Vote.kt | 34 ++++++------ .../kotlin/fr/dcproject/routes/VoteArticle.kt | 16 ++++++ src/main/resources/openApi.yaml | 53 ++++++++++++++++--- .../vote/find_citizen_votes_by_target_ids.sql | 32 +++++++++++ .../resources/sql/functions/vote/vote.sql | 12 +++-- .../sql/migrations/0000-init_schema.up.sql | 5 +- src/test/resources/feature/vote.feature | 7 +++ src/test/sql/vote.sql | 6 +++ 8 files changed, 134 insertions(+), 31 deletions(-) create mode 100644 src/main/resources/sql/functions/vote/find_citizen_votes_by_target_ids.sql diff --git a/src/main/kotlin/fr/dcproject/repository/Vote.kt b/src/main/kotlin/fr/dcproject/repository/Vote.kt index 34bfa59..df98ea6 100644 --- a/src/main/kotlin/fr/dcproject/repository/Vote.kt +++ b/src/main/kotlin/fr/dcproject/repository/Vote.kt @@ -31,21 +31,6 @@ open class Vote (override var requester: Requester): RepositoryI< )!! } - fun findByCitizen( - citizen: CitizenEntity, - target: String, - typeReference: TypeReference>>, - page: Int = 1, - limit: Int = 50 - ): Paginated> = - findByCitizen( - citizen.id ?: error("The citizen must have an id"), - target, - typeReference, - page, - limit - ) - fun findByCitizen( citizenId: UUID, target: String, @@ -61,6 +46,21 @@ open class Vote (override var requester: Requester): RepositoryI< )) } } + + fun findCitizenVotesByTargets( + citizen: CitizenEntity, + targets: List + ): List> { + val typeReference = object: TypeReference>>() {} + return requester.run { + val citizenId = citizen.id ?: error("The citizen must have an id") + getFunction("find_citizen_votes_by_target_ids") + .select(typeReference, mapOf( + "citizen_id" to citizenId, + "ids" to targets + )) + } + } } class VoteArticle (requester: Requester): Vote
(requester) { @@ -70,7 +70,7 @@ class VoteArticle (requester: Requester): Vote
(requester) { limit: Int = 50 ): Paginated> = findByCitizen( - citizen, + citizen.id ?: error("The citizen must have an id"), "article", object: TypeReference>>() {}, page, @@ -85,7 +85,7 @@ class VoteConstitution (requester: Requester): Vote(requester) { limit: Int = 50 ): Paginated> = findByCitizen( - citizen, + citizen.id ?: error("The citizen must have an id"), "constitution", object: TypeReference>>() {}, page, diff --git a/src/main/kotlin/fr/dcproject/routes/VoteArticle.kt b/src/main/kotlin/fr/dcproject/routes/VoteArticle.kt index 0459719..4fbca9e 100644 --- a/src/main/kotlin/fr/dcproject/routes/VoteArticle.kt +++ b/src/main/kotlin/fr/dcproject/routes/VoteArticle.kt @@ -15,6 +15,7 @@ import io.ktor.locations.put import io.ktor.request.receive import io.ktor.response.respond import io.ktor.routing.Route +import java.util.* import fr.dcproject.entity.Article as ArticleEntity import fr.dcproject.entity.Vote as VoteEntity import fr.dcproject.repository.VoteArticle as VoteArticleRepository @@ -33,6 +34,14 @@ object VoteArticlePaths { limit: Int = 50, val search: String? = null ): PaginatedRequestI by PaginatedRequest(page, limit) + + @Location("/citizens/{citizen}/votes") + class CitizenVotesByIdsRequest(val citizen: Citizen, id: List) { + val id: List = id + .map { it.trim() } + .filter { it.isNotBlank() } + .map { UUID.fromString(it) } + } } @KtorExperimentalLocationsAPI @@ -55,4 +64,11 @@ fun Route.voteArticle(repo: VoteArticleRepository) { call.respond(votes) } + + get { + val votes = repo.findCitizenVotesByTargets(it.citizen, it.id) + assertCan(VIEW, votes) + + call.respond(votes) + } } \ No newline at end of file diff --git a/src/main/resources/openApi.yaml b/src/main/resources/openApi.yaml index 73d46dc..feda5ea 100644 --- a/src/main/resources/openApi.yaml +++ b/src/main/resources/openApi.yaml @@ -62,14 +62,7 @@ paths: - citizen operationId: getCitizen parameters: - - name: citizen - in: path - description: ID of citizen - example: e74be8e4-6823-47c4-bd1b-789725b2fa8e - required: true - schema: - type: string - format: uuid + - $ref: '#/components/parameters/citizen' responses: 200: description: The Citizen object @@ -79,6 +72,40 @@ paths: $ref: '#/components/schemas/CitizenResponse' 404: description: Citizen not found + /citizens/{citizen}/votes: + get: + security: + - JWTAuth: [] + summary: Get Citizen + tags: + - citizen + operationId: getCitizenVotes + parameters: + - $ref: '#/components/parameters/citizen' + - name: id + in: query + required: true + example: + - 1329ab90-edae-cfed-f863-c8cb069fa327 + - cab54e50-ce85-bba0-da23-fc9f75feeaf5 + schema: + type: array + items: + type: string + format: uuid + + responses: + 200: + description: The Votes objects + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/VoteResponse' + 404: + description: Citizen not found + /citizens: get: security: @@ -609,6 +636,16 @@ components: schema: type: string + citizen: + name: citizen + in: path + description: ID of citizen + example: 770d46e8-c458-417d-beaf-0f2faa109b26 + required: true + schema: + type: string + format: uuid + securitySchemes: JWTAuth: type: http diff --git a/src/main/resources/sql/functions/vote/find_citizen_votes_by_target_ids.sql b/src/main/resources/sql/functions/vote/find_citizen_votes_by_target_ids.sql new file mode 100644 index 0000000..3ef7015 --- /dev/null +++ b/src/main/resources/sql/functions/vote/find_citizen_votes_by_target_ids.sql @@ -0,0 +1,32 @@ +create or replace function find_citizen_votes_by_target_ids( + _citizen_id uuid, + _ids uuid[], + _reference regclass default null, + out resource json +) language plpgsql as +$$ +begin + select + json_agg(t) + into resource + from ( + select + v.*, + find_reference_by_id(v.target_id, _reference) as target, + find_citizen_by_id(v.created_by_id) as created_by + + from vote as v + + where + (_reference is null or _reference = target_reference) + and target_id = any(_ids) + and created_by_id = _citizen_id + + order by + _ids + limit 100 + ) as t; +end; +$$; + +-- drop function if exists find_citizen_votes_by_target_ids(uuid, uuid[], regclass); diff --git a/src/main/resources/sql/functions/vote/vote.sql b/src/main/resources/sql/functions/vote/vote.sql index 9e27589..11abe20 100644 --- a/src/main/resources/sql/functions/vote/vote.sql +++ b/src/main/resources/sql/functions/vote/vote.sql @@ -7,25 +7,29 @@ begin values (_created_by_id, _target_id, _note, _anonymous) on conflict (created_by_id, target_id) do update set note = excluded.note, - anonymous = excluded.anonymous; + anonymous = excluded.anonymous, + updated_at = now(); elseif reference = 'constitution'::regclass then insert into vote_for_constitution (created_by_id, target_id, note, anonymous) values (_created_by_id, _target_id, _note, _anonymous) on conflict (created_by_id, target_id) do update set note = excluded.note, - anonymous = excluded.anonymous; + anonymous = excluded.anonymous, + updated_at = now(); elseif reference = 'comment_on_article'::regclass then insert into vote_for_comment_on_article (created_by_id, target_id, note, anonymous) values (_created_by_id, _target_id, _note, _anonymous) on conflict (created_by_id, target_id) do update set note = excluded.note, - anonymous = excluded.anonymous; + anonymous = excluded.anonymous, + updated_at = now(); elseif reference = 'comment_on_constitution'::regclass then insert into vote_for_comment_on_constitution (created_by_id, target_id, note, anonymous) values (_created_by_id, _target_id, _note, _anonymous) on conflict (created_by_id, target_id) do update set note = excluded.note, - anonymous = excluded.anonymous; + anonymous = excluded.anonymous, + updated_at = now(); else raise exception '% no implemented', reference::text; end if; diff --git a/src/main/resources/sql/migrations/0000-init_schema.up.sql b/src/main/resources/sql/migrations/0000-init_schema.up.sql index 0b0fc48..3ed5fa0 100644 --- a/src/main/resources/sql/migrations/0000-init_schema.up.sql +++ b/src/main/resources/sql/migrations/0000-init_schema.up.sql @@ -450,8 +450,9 @@ execute procedure set_comment_parents_ids(); create table vote ( - anonymous boolean default true not null, - note int not null check ( note >= -1 and note <= 1 ), + updated_at timestamptz default now() not null check ( updated_at >= created_at ), + anonymous boolean default true not null, + note int not null check ( note >= -1 and note <= 1 ), foreign key (created_by_id) references citizen (id), primary key (id), unique (created_by_id, target_id) diff --git a/src/test/resources/feature/vote.feature b/src/test/resources/feature/vote.feature index 20e3433..b4a24ec 100644 --- a/src/test/resources/feature/vote.feature +++ b/src/test/resources/feature/vote.feature @@ -29,3 +29,10 @@ Feature: vote Article | limit | 50 | | total | 1 | | result[0].note | 1 | + + Scenario: Can get votes of current citizen by target ids + Given I am authenticated as John Doe with id "64b7b379-2298-43ec-b428-ba134930cabd" + When I send a GET request to "/citizens/64b7b379-2298-43ec-b428-ba134930cabd/votes?id=9226c1a3-8091-c3fa-7d0d-c2e98c9bee7b&id=64b1f265-bfb3-332b-eef9-d00f63a3beaa" + Then the response status code should be 200 + And the response should contain object: + | [0].note | 1 | diff --git a/src/test/sql/vote.sql b/src/test/sql/vote.sql index 8731e9a..5dccebd 100644 --- a/src/test/sql/vote.sql +++ b/src/test/sql/vote.sql @@ -38,6 +38,7 @@ declare $json$; votes jsonb; votes_of_citizen json; + votes_of_citizen_for_targets json; votes_total int; begin -- insert user for context @@ -97,6 +98,11 @@ begin assert (votes_total = 1), format('votes count for user %s must be 1, instead of %s', _citizen_id, votes_total); assert ((votes_of_citizen#>>'{0,note}')::int = -1), format('the note must be -1, instead of %s', (votes_of_citizen#>>'{0,note}')); + -- test "find_citizen_votes_by_target_ids" + select find_citizen_votes_by_target_ids(_citizen_id, array[(created_article->>'id')]::uuid[]) into votes_of_citizen_for_targets; + assert (json_array_length(votes_of_citizen_for_targets) = 1), format('the function must be return 1 vote, instead of %s', json_array_length(votes_of_citizen_for_targets)); + assert (votes_of_citizen_for_targets#>>'{0,target_id}' = created_article->>'id'), format('target_id of vote must be %s, instead of %s', created_article->>'id', votes_of_citizen_for_targets#>>'{0,target_id}'); + -- delete vote and context delete from vote; delete from article;