From 5fd455543586e905819a9f250445c79daab3985f Mon Sep 17 00:00:00 2001 From: K0nnyaku Date: Thu, 2 Jan 2025 19:04:40 +0800 Subject: [PATCH] feat(org): suport get, remove, add, update (#71) * feat: suport get, remove, add, update * fix: resolve merge conflict * refactor: modify var type * chore: delete .zsh_history * chore: delete 1000 * chore: add bump version * fix: correct the method used to destructure thing. --- .changes/organization.md | 5 ++ .cspell.json | 5 +- Cargo.lock | 2 +- src/models/organization.rs | 73 +++++++++++++++++++++++- src/routes/organization.rs | 110 ++++++++++++++++++++++++++++++++++++- src/utils/organization.rs | 74 ++++++++++++++++++++----- tests/organization.rs | 86 +++++++++++++++++++++++++++-- 7 files changed, 326 insertions(+), 29 deletions(-) create mode 100644 .changes/organization.md diff --git a/.changes/organization.md b/.changes/organization.md new file mode 100644 index 0000000..a920882 --- /dev/null +++ b/.changes/organization.md @@ -0,0 +1,5 @@ +--- +"algohub-server": patch:feat +--- + +Support remove, create, and update organizations and add members to organizations. \ No newline at end of file diff --git a/.cspell.json b/.cspell.json index b1e0852..0976e6a 100644 --- a/.cspell.json +++ b/.cspell.json @@ -3,12 +3,11 @@ "algohub", "chrono", "covector", - "farmfe", - "ICPC", - "serde", "dtolnay", "farmfe", + "ICPC", "jbolda", + "serde", "signin", "surrealdb" ] diff --git a/Cargo.lock b/Cargo.lock index becf7f1..eaf8a3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ dependencies = [ [[package]] name = "algohub-server" -version = "0.1.16" +version = "0.1.17" dependencies = [ "anyhow", "chrono", diff --git a/src/models/organization.rs b/src/models/organization.rs index deee650..68fb80c 100644 --- a/src/models/organization.rs +++ b/src/models/organization.rs @@ -11,8 +11,7 @@ pub struct Organization { pub owners: Vec, pub members: Vec, - - pub creator: String, + pub creator: Option, pub created_at: chrono::NaiveDateTime, pub updated_at: chrono::NaiveDateTime, @@ -22,6 +21,14 @@ pub struct Organization { #[serde(crate = "rocket::serde")] pub struct OrganizationData<'c> { pub name: &'c str, + pub display_name: Option<&'c str>, + pub description: Option<&'c str>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct UpdateOrg { + pub name: String, pub display_name: Option, pub description: Option, } @@ -34,3 +41,65 @@ pub struct CreateOrganization<'r> { pub org: OrganizationData<'r>, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct UserOrganization { + pub name: String, + pub display_name: Option, + pub description: Option, + + pub owners: Vec, + pub members: Vec, + + pub created_at: chrono::NaiveDateTime, + pub updated_at: chrono::NaiveDateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct ChangeMember<'r> { + pub id: &'r str, + pub token: &'r str, + pub members: Vec<&'r str>, +} + +impl From> for Organization { + fn from(val: CreateOrganization) -> Self { + Organization { + id: None, + name: val.org.name.to_string(), + display_name: val.org.display_name.map(|s| s.to_string()), + description: val.org.description.map(|s| s.to_string()), + owners: vec![("account", val.id).into()], + members: vec![], + creator: Some(("account".to_string(), val.id.to_string()).into()), + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + } + } +} + +impl From for UserOrganization { + fn from(val: Organization) -> Self { + UserOrganization { + name: val.name, + display_name: val.display_name, + description: val.description, + owners: val.owners.iter().map(|thing| thing.id.to_raw()).collect(), + members: val.members.iter().map(|thing| thing.id.to_raw()).collect(), + created_at: val.created_at, + updated_at: val.updated_at, + } + } +} + +impl From> for UpdateOrg { + fn from(val: OrganizationData) -> Self { + UpdateOrg { + name: val.name.to_string(), + display_name: val.display_name.map(|s| s.to_string()), + description: val.description.map(|s| s.to_string()), + } + } +} diff --git a/src/routes/organization.rs b/src/routes/organization.rs index 04b2e9c..e2bcb3c 100644 --- a/src/routes/organization.rs +++ b/src/routes/organization.rs @@ -5,7 +5,7 @@ use surrealdb::{engine::remote::ws::Client, Surreal}; use crate::{ models::{ error::Error, - organization::CreateOrganization, + organization::{ChangeMember, CreateOrganization, Organization, UserOrganization}, response::{Empty, Response}, Credentials, OwnedId, }, @@ -24,7 +24,7 @@ pub async fn create( ))); } - let org = organization::create(db, org.id, org.into_inner().org) + let org = organization::create(db, org.into_inner()) .await? .ok_or(Error::ServerError(Json( "Failed to create a new organization".into(), @@ -39,6 +39,110 @@ pub async fn create( })) } +#[post("/get/", data = "")] +pub async fn get( + db: &State>, + id: &str, + auth: Json>>, +) -> Result { + if let Some(auth) = auth.into_inner() { + if !session::verify(db, auth.id, auth.token).await { + return Err(Error::Unauthorized(Json( + "Failed to grant permission".into(), + ))); + } + } else { + return Err(Error::Unauthorized(Json( + "Failed to grant permission".into(), + ))); + } + + let org = organization::get_by_id::(db, id) + .await? + .ok_or(Error::NotFound(Json("Organization not found".into())))?; + + Ok(Json(Response { + success: true, + message: "Organization found".to_string(), + data: Some(org.into()), + })) +} + +#[post("/add/", data = "")] +pub async fn add( + db: &State>, + id: &str, + member: Json>, +) -> Result { + if !session::verify(db, member.id, member.token).await { + return Err(Error::Unauthorized(Json( + "Failed to grant permission".into(), + ))); + } + + organization::add(db, id, member.into_inner().members) + .await? + .ok_or(Error::ServerError(Json( + "Failed to add members to the organization".into(), + )))?; + + Ok(Json(Response { + success: true, + message: "Members added successfully".into(), + data: None, + })) +} + +#[post("/remove/", data = "")] +pub async fn remove( + db: &State>, + id: &str, + member: Json>, +) -> Result { + if !session::verify(db, member.id, member.token).await { + return Err(Error::Unauthorized(Json( + "Failed to grant permission".into(), + ))); + } + + organization::remove(db, id, member.into_inner().members) + .await? + .ok_or(Error::ServerError(Json( + "Failed to remove members to the organization".into(), + )))?; + + Ok(Json(Response { + success: true, + message: "Members removed successfully".into(), + data: None, + })) +} + +#[post("/update/", data = "")] +pub async fn update( + db: &State>, + id: &str, + org: Json>, +) -> Result { + if !session::verify(db, org.id, org.token).await { + return Err(Error::Unauthorized(Json( + "Failed to grant permission".into(), + ))); + } + + organization::update(db, id, org.into_inner().org) + .await? + .ok_or(Error::ServerError(Json( + "Failed to update the organization".into(), + )))?; + + Ok(Json(Response { + success: true, + message: "Organization updated successfully".to_string(), + data: None, + })) +} + #[post("/delete/", data = "")] pub async fn delete( db: &State>, @@ -64,5 +168,5 @@ pub async fn delete( pub fn routes() -> Vec { use rocket::routes; - routes![create, delete] + routes![create, add, update, get, delete, remove] } diff --git a/src/utils/organization.rs b/src/utils/organization.rs index 89a6079..6beb2fc 100644 --- a/src/utils/organization.rs +++ b/src/utils/organization.rs @@ -1,27 +1,16 @@ use anyhow::Result; use serde::Deserialize; -use surrealdb::{engine::remote::ws::Client, Surreal}; +use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal}; -use crate::models::organization::{Organization, OrganizationData}; +use crate::models::organization::{CreateOrganization, Organization, OrganizationData, UpdateOrg}; pub async fn create( db: &Surreal, - id: &str, - org: OrganizationData<'_>, + org: CreateOrganization<'_>, ) -> Result> { Ok(db .create("organization") - .content(Organization { - id: None, - name: org.name.to_string(), - display_name: org.display_name, - description: org.description, - owners: vec![("account", id).into()], - members: vec![], - creator: id.to_string(), - created_at: chrono::Local::now().naive_local(), - updated_at: chrono::Local::now().naive_local(), - }) + .content(Into::::into(org)) .await?) } @@ -32,6 +21,61 @@ where Ok(db.select(("organization", id)).await?) } +pub async fn update( + db: &Surreal, + id: &str, + org: OrganizationData<'_>, +) -> Result> { + Ok(db + .update(("organization", id)) + .merge(Into::::into(org)) + .await?) +} + +const ADD_MEMBERS_QUERY: &str = r#" +UPDATE type::thing("organization", $id) + SET members = array::union(members, $new_members) +"#; +pub async fn add( + db: &Surreal, + id: &str, + member: Vec<&str>, +) -> Result> { + let members_to_add: Vec = member + .into_iter() + .map(|id| ("account", id).into()) + .collect(); + + Ok(db + .query(ADD_MEMBERS_QUERY) + .bind(("id", id.to_string())) + .bind(("new_members", members_to_add)) + .await? + .take(0)?) +} + +const REMOVE_MEMBERS_QUERY: &str = r#" +UPDATE type::thing("organization", $id) + SET members = array::complement(members, $new_members) +"#; +pub async fn remove( + db: &Surreal, + id: &str, + member: Vec<&str>, +) -> Result> { + let members_to_remove: Vec = member + .into_iter() + .map(|id| ("account", id).into()) + .collect(); + + Ok(db + .query(REMOVE_MEMBERS_QUERY) + .bind(("id", id.to_string())) + .bind(("new_members", members_to_remove)) + .await? + .take(0)?) +} + pub async fn delete(db: &Surreal, id: &str) -> Result> { Ok(db.delete(("organization", id)).await?) } diff --git a/tests/organization.rs b/tests/organization.rs index 82a2a77..3f11c99 100644 --- a/tests/organization.rs +++ b/tests/organization.rs @@ -4,7 +4,7 @@ use std::path::Path; use algohub_server::models::{ account::Register, - organization::{CreateOrganization, OrganizationData}, + organization::{ChangeMember, CreateOrganization, OrganizationData, UserOrganization}, response::{Empty, Response}, Credentials, OwnedCredentials, OwnedId, Token, }; @@ -64,13 +64,13 @@ async fn test_organization() -> Result<()> { message: _, data, } = response.into_json().await.unwrap(); - let data: OwnedId = data.unwrap(); + let org_data: OwnedId = data.unwrap(); assert!(success); - println!("Created organization: {}", data.id); + println!("Created organization: {}", org_data.id); let response = client - .post(format!("/org/delete/{}", data.id)) + .post(format!("/org/get/{}", org_data.id)) .json(&Credentials { id: &id, token: &token, @@ -78,9 +78,85 @@ async fn test_organization() -> Result<()> { .dispatch() .await; + assert_eq!(response.status().code, 200); + + let Response { + success, + message: _, + data, + } = response.into_json().await.unwrap(); + let data: UserOrganization = data.unwrap(); + + assert!(success); + println!("Get organization: {}", data.name); + + let response = client + .post(format!("/org/add/{}", org_data.id)) + .json(&ChangeMember { + id: &id, + token: &token, + members: vec!["k0"], + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + response.into_json::>().await.unwrap(); - assert!(!Path::new("content").join(data.id.clone()).exists()); + assert!(success); + println!("add member: {}", "k0"); + + let response = client + .post(format!("/org/remove/{}", org_data.id)) + .json(&ChangeMember { + id: &id, + token: &token, + members: vec!["k0"], + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + + response.into_json::>().await.unwrap(); + + assert!(success); + println!("remove member: {}", "k0"); + + let response = client + .post(format!("/org/update/{}", org_data.id)) + .json(&CreateOrganization { + id: &id, + token: &token, + org: OrganizationData { + name: "test_organization_update", + display_name: None, + description: None, + }, + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + + assert!(success); + println!("updated organization: {}", org_data.id); + + let response = client + .post(format!("/org/delete/{}", org_data.id)) + .json(&Credentials { + id: &id, + token: &token, + }) + .dispatch() + .await; + + response.into_json::>().await.unwrap(); + + assert!(!Path::new("content").join(org_data.id.clone()).exists()); + + println!("Deleted organization: {}", org_data.id); client .post(format!("/account/delete/{}", id))