diff --git a/migrations/2020-02-14-142048_groupslist/down.sql b/migrations/2020-02-14-142048_groupslist/down.sql new file mode 100644 index 0000000..daeacbc --- /dev/null +++ b/migrations/2020-02-14-142048_groupslist/down.sql @@ -0,0 +1 @@ +DROP VIEW groups_list; diff --git a/migrations/2020-02-14-142048_groupslist/up.sql b/migrations/2020-02-14-142048_groupslist/up.sql new file mode 100644 index 0000000..9bc408c --- /dev/null +++ b/migrations/2020-02-14-142048_groupslist/up.sql @@ -0,0 +1 @@ +CREATE VIEW groups_list AS SELECT groups.name, groups.typ, groups.trust, count(memberships.user_uuid) as members_count FROM groups JOIN memberships ON groups.group_id = memberships.group_id GROUP BY groups.group_id; \ No newline at end of file diff --git a/openapi.yml b/openapi.yml index 984826d..c6c8d1d 100644 --- a/openapi.yml +++ b/openapi.yml @@ -35,6 +35,65 @@ paths: application/json: schema: $ref: "#/components/schemas/GenericError" + get: + summary: paginated groups + description: | + get paginated list of a groups + parameters: + - in: query + name: f + description: filter results based on name prefix + required: false + schema: + type: string + - in: query + name: by + description: order by + required: false + schema: + type: string + enum: ["MemberCount", "NameAsc", "NameDesc"] + - in: query + name: "n" + description: next page id (offset) + required: false + schema: + type: integer + default: 0 + - in: query + name: s + description: size of the result + required: false + schema: + type: integer + default: 20 + responses: + "200": + description: a page of members + content: + application/json: + schema: + type: object + properties: + members: + type: array + items: + $ref: "#/components/schemas/DisplayMember" + next: + type: integer + example: 20 + "400": + description: bad request + content: + application/json: + schema: + $ref: "#/components/schemas/GenericError" + "403": + description: operation forbidden + content: + application/json: + schema: + $ref: "#/components/schemas/GenericError" "/groups/api/v1/groups/{groupName}": get: summary: group information diff --git a/src/api/groups.rs b/src/api/groups.rs index 6c64869..22971c2 100644 --- a/src/api/groups.rs +++ b/src/api/groups.rs @@ -4,6 +4,7 @@ use crate::api::models::GroupInfo; use crate::db::operations; use crate::db::operations::models::GroupUpdate; use crate::db::operations::models::NewGroup; +use crate::db::operations::models::SortGroupsBy; use crate::db::types::*; use crate::db::Pool; use crate::utils::valid_group_name; @@ -16,11 +17,34 @@ use actix_web::Responder; use cis_client::CisClient; use dino_park_gate::scope::ScopeAndUser; use log::info; +use serde_derive::Deserialize; use std::sync::Arc; +#[derive(Deserialize)] +struct ListGroupsQuery { + f: Option, + #[serde(default)] + n: i64, + #[serde(default = "default_groups_list_size")] + s: i64, + #[serde(default)] + by: SortGroupsBy, +} + +fn default_groups_list_size() -> i64 { + 20 +} + +#[guard(Authenticated)] async fn get_group(pool: web::Data, group_name: web::Path) -> impl Responder { - operations::groups::get_group(&pool, &group_name) - .map(|group| HttpResponse::Ok().json(group)) + operations::groups::get_group(&pool, &group_name).map(|group| HttpResponse::Ok().json(group)) +} + +#[guard(Authenticated)] +async fn list_groups(pool: web::Data, query: web::Query) -> impl Responder { + let query = query.into_inner(); + operations::groups::list_groups(&pool, query.f, query.by, query.s, query.n) + .map(|groups| HttpResponse::Ok().json(groups)) .map_err(ApiError::GenericBadRequest) } @@ -142,7 +166,11 @@ pub fn groups_app() -> impl HttpServiceFactory { .max_age(3600) .finish(), ) - .service(web::resource("").route(web::post().to(add_group))) + .service( + web::resource("") + .route(web::post().to(add_group)) + .route(web::get().to(list_groups)), + ) .service( web::resource("/{group_name}") .route(web::get().to(get_group)) diff --git a/src/db/internal/group.rs b/src/db/internal/group.rs index e529198..e2488ca 100644 --- a/src/db/internal/group.rs +++ b/src/db/internal/group.rs @@ -5,14 +5,17 @@ use crate::db::model::*; use crate::db::operations::models::GroupUpdate; use crate::db::operations::models::GroupWithTermsFlag; use crate::db::operations::models::NewGroup; +use crate::db::operations::models::PaginatedGroupsLists; +use crate::db::operations::models::SortGroupsBy; use crate::db::schema; use crate::db::types::*; - +use crate::db::views; use diesel::dsl::exists; use diesel::dsl::select; use diesel::prelude::*; use failure::Error; use serde_json::Value; +use std::convert::TryFrom; use uuid::Uuid; pub fn get_group_with_terms_flag( @@ -182,3 +185,27 @@ pub fn groups_for_user(connection: &PgConnection, user_uuid: &Uuid) -> Result(connection) .map_err(Into::into) } + +pub fn list_groups( + connection: &PgConnection, + filter: Option, + sort_by: SortGroupsBy, + limit: i64, + offset: i64, +) -> Result { + let mut query = views::groups_list::table.into_boxed(); + if let Some(filter) = filter { + query = query.filter(views::groups_list::name.ilike(format!("{}%", filter))) + }; + query = match sort_by { + SortGroupsBy::MembersCount => query.order(views::groups_list::members_count.desc()), + SortGroupsBy::NameAsc => query.order(views::groups_list::name.asc()), + SortGroupsBy::NameDesc => query.order(views::groups_list::name.desc()), + }; + let groups: Vec = query.offset(offset).limit(limit).get_results(connection)?; + let next = match i64::try_from(groups.len()) { + Ok(x) if x == limit => Some(offset + x), + _ => None, + }; + Ok(PaginatedGroupsLists { groups, next }) +} diff --git a/src/db/model.rs b/src/db/model.rs index bfa9b0b..ec95c4f 100644 --- a/src/db/model.rs +++ b/src/db/model.rs @@ -60,6 +60,14 @@ pub struct Invitation { pub added_by: Uuid, } +#[derive(Queryable, Serialize)] +pub struct GroupsList { + pub name: String, + pub typ: GroupType, + pub trust: TrustType, + pub member_count: i64, +} + #[derive(Insertable)] #[table_name = "groups"] pub struct InsertGroup { diff --git a/src/db/operations/groups.rs b/src/db/operations/groups.rs index 6bee13c..e01e585 100644 --- a/src/db/operations/groups.rs +++ b/src/db/operations/groups.rs @@ -6,6 +6,8 @@ use crate::db::operations; use crate::db::operations::models::GroupUpdate; use crate::db::operations::models::GroupWithTermsFlag; use crate::db::operations::models::NewGroup; +use crate::db::operations::models::PaginatedGroupsLists; +use crate::db::operations::models::SortGroupsBy; use crate::db::Pool; use crate::error::PacksError; use crate::rules::engine::CREATE_GROUP; @@ -123,3 +125,14 @@ pub fn get_group_with_terms_flag( let connection = pool.get()?; internal::group::get_group_with_terms_flag(&connection, group_name) } + +pub fn list_groups( + pool: &Pool, + filter: Option, + sort_by: SortGroupsBy, + limit: i64, + offset: i64, +) -> Result { + let connection = pool.get()?; + internal::group::list_groups(&connection, filter, sort_by, limit, offset) +} diff --git a/src/db/operations/models.rs b/src/db/operations/models.rs index aa7f114..ae920bc 100644 --- a/src/db/operations/models.rs +++ b/src/db/operations/models.rs @@ -1,10 +1,24 @@ use crate::db::model::Group; +use crate::db::model::GroupsList; use crate::db::types::*; use chrono::NaiveDateTime; use serde_derive::Deserialize; use serde_derive::Serialize; use uuid::Uuid; +#[derive(Deserialize)] +pub enum SortGroupsBy { + MembersCount, + NameAsc, + NameDesc, +} + +impl Default for SortGroupsBy { + fn default() -> Self { + Self::MembersCount + } +} + #[derive(Deserialize)] pub struct GroupUpdate { pub description: Option, @@ -215,6 +229,12 @@ pub struct PaginatedDisplayMembersAndHost { pub next: Option, } +#[derive(Serialize)] +pub struct PaginatedGroupsLists { + pub groups: Vec, + pub next: Option, +} + #[cfg(test)] mod test { use super::*; diff --git a/src/db/views.rs b/src/db/views.rs index 407a9f0..d8c5e5e 100644 --- a/src/db/views.rs +++ b/src/db/views.rs @@ -26,3 +26,15 @@ hosts_table!(hosts_ndaed, users_ndaed); hosts_table!(hosts_vouched, users_vouched); hosts_table!(hosts_authenticated, users_authenticated); hosts_table!(hosts_public, users_public); + +table! { + use diesel::sql_types::*; + use crate::db::types::*; + + groups_list (name) { + name -> VarChar, + typ -> Group_type, + trust -> Trust_type, + members_count -> BigInt, + } +}