From b775aa9d4cf69e3796764e08273d7c5257349acd Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Thu, 24 Oct 2024 22:06:52 +0800 Subject: [PATCH 1/7] feat: patching of publish name --- libs/client-api/src/http_publish.rs | 21 ++++++++++- libs/database-entity/src/dto.rs | 6 +++ libs/database/src/publish.rs | 44 +++++++++++++++++++++- src/api/workspace.rs | 24 ++++++++++-- src/biz/workspace/publish.rs | 58 +++++++++++++++++++++++++++-- tests/workspace/publish.rs | 52 +++++++++++++++++++++++++- 6 files changed, 194 insertions(+), 11 deletions(-) diff --git a/libs/client-api/src/http_publish.rs b/libs/client-api/src/http_publish.rs index 13b318ef..e8d29247 100644 --- a/libs/client-api/src/http_publish.rs +++ b/libs/client-api/src/http_publish.rs @@ -3,7 +3,8 @@ use client_api_entity::workspace_dto::PublishInfoView; use client_api_entity::{workspace_dto::PublishedDuplicate, PublishInfo, UpdatePublishNamespace}; use client_api_entity::{ CreateGlobalCommentParams, CreateReactionParams, DeleteGlobalCommentParams, DeleteReactionParams, - GetReactionQueryParams, GlobalComments, PublishInfoMeta, Reactions, UpdateDefaultPublishView, + GetReactionQueryParams, GlobalComments, PatchPublishedCollab, PublishInfoMeta, Reactions, + UpdateDefaultPublishView, }; use reqwest::Method; use shared_entity::response::{AppResponse, AppResponseError}; @@ -75,6 +76,22 @@ impl Client { .into_data() } + pub async fn patch_published_collabs( + &self, + workspace_id: &str, + patches: &[PatchPublishedCollab], + ) -> Result<(), AppResponseError> { + let url = format!("{}/api/workspace/{}/publish", self.base_url, workspace_id); + let resp = self + .http_client_with_auth(Method::PATCH, &url) + .await? + .json(patches) + .send() + .await?; + log_request_id(&resp); + AppResponse::<()>::from_response(resp).await?.into_error() + } + pub async fn unpublish_collabs( &self, workspace_id: &str, @@ -251,7 +268,7 @@ impl Client { &self, view_id: &uuid::Uuid, ) -> Result { - let url = format!("{}/api/workspace/published-info/{}", self.base_url, view_id,); + let url = format!("{}/api/workspace/published-info/{}", self.base_url, view_id); let resp = self.cloud_client.get(&url).send().await?; AppResponse::::from_response(resp) diff --git a/libs/database-entity/src/dto.rs b/libs/database-entity/src/dto.rs index 00a5a67d..28badb73 100644 --- a/libs/database-entity/src/dto.rs +++ b/libs/database-entity/src/dto.rs @@ -1133,6 +1133,12 @@ pub struct PublishCollabItem { pub data: Data, } +#[derive(Debug, Serialize, Deserialize)] +pub struct PatchPublishedCollab { + pub view_id: Uuid, + pub publish_name: Option, +} + #[derive(Serialize, Deserialize, Debug)] pub struct GlobalComments { pub comments: Vec, diff --git a/libs/database/src/publish.rs b/libs/database/src/publish.rs index ffd77023..4f64d115 100644 --- a/libs/database/src/publish.rs +++ b/libs/database/src/publish.rs @@ -1,5 +1,7 @@ use app_error::AppError; -use database_entity::dto::{PublishCollabItem, PublishCollabKey, PublishInfo}; +use database_entity::dto::{ + PatchPublishedCollab, PublishCollabItem, PublishCollabKey, PublishInfo, +}; use sqlx::{Executor, PgPool, Postgres}; use uuid::Uuid; @@ -238,6 +240,46 @@ pub async fn delete_published_collabs<'a, E: Executor<'a, Database = Postgres>>( Ok(()) } +#[inline] +pub async fn update_published_collabs( + txn: &mut sqlx::Transaction<'_, Postgres>, + workspace_id: &Uuid, + patches: &[PatchPublishedCollab], +) -> Result<(), AppError> { + for patch in patches { + let new_publish_name = match &patch.publish_name { + Some(new_publish_name) => new_publish_name, + None => continue, + }; + + let res = sqlx::query!( + r#" + UPDATE af_published_collab + SET publish_name = $1 + WHERE workspace_id = $2 + AND view_id = $3 + "#, + patch.publish_name, + workspace_id, + patch.view_id, + ) + .execute(txn.as_mut()) + .await?; + + if res.rows_affected() != 1 { + tracing::error!( + "Failed to update published collab publish name, workspace_id: {}, view_id: {}, new_publish_name: {}, rows_affected: {}", + workspace_id, + patch.view_id, + new_publish_name, + res.rows_affected() + ); + } + } + + Ok(()) +} + #[inline] pub async fn select_published_metadata_for_view_id( pg_pool: &PgPool, diff --git a/src/api/workspace.rs b/src/api/workspace.rs index e7470ebf..d545c639 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -204,7 +204,8 @@ pub fn workspace_scope() -> Scope { .service( web::resource("/{workspace_id}/publish") .route(web::post().to(post_publish_collabs_handler)) - .route(web::delete().to(delete_published_collabs_handler)), + .route(web::delete().to(delete_published_collabs_handler)) + .route(web::patch().to(patch_published_collabs_handler)), ) .service( web::resource("/{workspace_id}/folder").route(web::get().to(get_workspace_folder_handler)), @@ -1440,6 +1441,23 @@ async fn post_publish_collabs_handler( Ok(Json(AppResponse::Ok())) } +async fn patch_published_collabs_handler( + workspace_id: web::Path, + user_uuid: UserUuid, + state: Data, + patches: Json>, +) -> Result>> { + let workspace_id = workspace_id.into_inner(); + if patches.is_empty() { + return Err(AppError::InvalidRequest("No patches provided".to_string()).into()); + } + state + .published_collab_store + .patch_collabs(&workspace_id, &user_uuid, &patches) + .await?; + Ok(Json(AppResponse::Ok())) +} + async fn delete_published_collabs_handler( workspace_id: web::Path, user_uuid: UserUuid, @@ -1449,11 +1467,11 @@ async fn delete_published_collabs_handler( let workspace_id = workspace_id.into_inner(); let view_ids = view_ids.into_inner(); if view_ids.is_empty() { - return Ok(Json(AppResponse::Ok())); + return Err(AppError::InvalidRequest("No view_ids provided".to_string()).into()); } state .published_collab_store - .delete_collab(&workspace_id, &view_ids, &user_uuid) + .delete_collabs(&workspace_id, &view_ids, &user_uuid) .await?; Ok(Json(AppResponse::Ok())) } diff --git a/src/biz/workspace/publish.rs b/src/biz/workspace/publish.rs index 12735cb3..3ba13fce 100644 --- a/src/biz/workspace/publish.rs +++ b/src/biz/workspace/publish.rs @@ -3,9 +3,11 @@ use database::{ collab::GetCollabOrigin, publish::{ select_all_published_collab_info, select_default_published_view_id, - select_default_published_view_id_for_namespace, update_workspace_default_publish_view, + select_default_published_view_id_for_namespace, update_published_collabs, + update_workspace_default_publish_view, }, }; +use database_entity::dto::PatchPublishedCollab; use std::sync::Arc; use app_error::AppError; @@ -251,12 +253,19 @@ pub trait PublishedCollabStore: Sync + Send + 'static { publish_name: &str, ) -> Result, AppError>; - async fn delete_collab( + async fn delete_collabs( &self, workspace_id: &Uuid, view_ids: &[Uuid], user_uuid: &Uuid, ) -> Result<(), AppError>; + + async fn patch_collabs( + &self, + workspace_id: &Uuid, + user_uuid: &Uuid, + patches: &[PatchPublishedCollab], + ) -> Result<(), AppError>; } pub struct PublishedCollabPostgresStore { @@ -351,7 +360,7 @@ impl PublishedCollabStore for PublishedCollabPostgresStore { result } - async fn delete_collab( + async fn delete_collabs( &self, workspace_id: &Uuid, view_ids: &[Uuid], @@ -361,6 +370,15 @@ impl PublishedCollabStore for PublishedCollabPostgresStore { delete_published_collabs(&self.pg_pool, workspace_id, view_ids).await?; Ok(()) } + + async fn patch_collabs( + &self, + workspace_id: &Uuid, + user_uuid: &Uuid, + patches: &[PatchPublishedCollab], + ) -> Result<(), AppError> { + patch_collabs(&self.pg_pool, workspace_id, user_uuid, patches).await + } } pub struct PublishedCollabS3StoreWithPostgresFallback { @@ -519,7 +537,7 @@ impl PublishedCollabStore for PublishedCollabS3StoreWithPostgresFallback { } } - async fn delete_collab( + async fn delete_collabs( &self, workspace_id: &Uuid, view_ids: &[Uuid], @@ -534,4 +552,36 @@ impl PublishedCollabStore for PublishedCollabS3StoreWithPostgresFallback { delete_published_collabs(&self.pg_pool, workspace_id, view_ids).await?; Ok(()) } + + async fn patch_collabs( + &self, + workspace_id: &Uuid, + user_uuid: &Uuid, + patches: &[PatchPublishedCollab], + ) -> Result<(), AppError> { + patch_collabs(&self.pg_pool, workspace_id, user_uuid, patches).await + } +} + +async fn patch_collabs( + pg_pool: &PgPool, + workspace_id: &Uuid, + user_uuid: &Uuid, + patches: &[PatchPublishedCollab], +) -> Result<(), AppError> { + let view_ids = patches + .iter() + .map(|patch| patch.view_id) + .collect::>(); + for patch in patches { + if let Some(new_publish_name) = patch.publish_name.as_deref() { + check_collab_publish_name(new_publish_name)?; + } + } + check_workspace_owner_or_publisher(pg_pool, user_uuid, workspace_id, &view_ids).await?; + + let mut txn = pg_pool.begin().await?; + update_published_collabs(&mut txn, workspace_id, patches).await?; + txn.commit().await?; + Ok(()) } diff --git a/tests/workspace/publish.rs b/tests/workspace/publish.rs index ec20dc94..79b3b54a 100644 --- a/tests/workspace/publish.rs +++ b/tests/workspace/publish.rs @@ -2,7 +2,8 @@ use app_error::ErrorCode; use appflowy_cloud::biz::collab::folder_view::collab_folder_to_folder_view; use appflowy_cloud::biz::workspace::ops::collab_from_doc_state; use client_api::entity::{ - AFRole, GlobalComment, PublishCollabItem, PublishCollabMetadata, PublishInfoMeta, + AFRole, GlobalComment, PatchPublishedCollab, PublishCollabItem, PublishCollabMetadata, + PublishInfoMeta, }; use client_api_test::TestClient; use client_api_test::{generate_unique_registered_user_client, localhost_client}; @@ -255,6 +256,55 @@ async fn test_publish_doc() { .await .unwrap(); + { + let new_publish_name_1 = "new-publish-name-1".to_string(); + + // User change publish name + c.patch_published_collabs( + &workspace_id, + &[PatchPublishedCollab { + view_id: view_id_1, + publish_name: Some(new_publish_name_1.to_string()), + }], + ) + .await + .unwrap(); + + // Guest now cannot access the collab using old publish name + let guest_client = localhost_client(); + let err = guest_client + .get_published_collab::(&my_namespace, publish_name_1) + .await + .err() + .unwrap(); + assert_eq!(err.code, ErrorCode::RecordNotFound, "{:?}", err); + + // Guest now access the collab using new publish name + let guest_client = localhost_client(); + let _ = guest_client + .get_published_collab::(&my_namespace, &new_publish_name_1) + .await + .unwrap(); + + // Switch back to old publish name + c.patch_published_collabs( + &workspace_id, + &[PatchPublishedCollab { + view_id: view_id_1, + publish_name: Some(publish_name_1.to_string()), + }], + ) + .await + .unwrap(); + + // Guest can access the collab using the orginal publish name + let guest_client = localhost_client(); + let _ = guest_client + .get_published_collab::(&my_namespace, publish_name_1) + .await + .unwrap(); + } + { // Deleted collab should not be accessible let guest_client = localhost_client(); From 90a644eaade405966cd22f4c67e49d4509c4e80f Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 25 Oct 2024 09:32:13 +0800 Subject: [PATCH 2/7] chore: cargo sqlx --- ...7af17e8dcb59d13b644bc336da5351184ec4c9e1.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .sqlx/query-2ba7fdabdd71d2e3ac6dc1b67af17e8dcb59d13b644bc336da5351184ec4c9e1.json diff --git a/.sqlx/query-2ba7fdabdd71d2e3ac6dc1b67af17e8dcb59d13b644bc336da5351184ec4c9e1.json b/.sqlx/query-2ba7fdabdd71d2e3ac6dc1b67af17e8dcb59d13b644bc336da5351184ec4c9e1.json new file mode 100644 index 00000000..926c6e56 --- /dev/null +++ b/.sqlx/query-2ba7fdabdd71d2e3ac6dc1b67af17e8dcb59d13b644bc336da5351184ec4c9e1.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE af_published_collab\n SET publish_name = $1\n WHERE workspace_id = $2\n AND view_id = $3\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Uuid", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "2ba7fdabdd71d2e3ac6dc1b67af17e8dcb59d13b644bc336da5351184ec4c9e1" +} From 5c7a12cc95914caa3b4b345b36941db76eac3cdd Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 25 Oct 2024 11:49:03 +0800 Subject: [PATCH 3/7] test: fix test case --- tests/workspace/publish.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/workspace/publish.rs b/tests/workspace/publish.rs index 79b3b54a..3be1f5e2 100644 --- a/tests/workspace/publish.rs +++ b/tests/workspace/publish.rs @@ -252,10 +252,6 @@ async fn test_publish_doc() { assert_eq!(default_info_meta.meta.title, "my_title_1"); } - c.unpublish_collabs(&workspace_id, &[view_id_1, view_id_2]) - .await - .unwrap(); - { let new_publish_name_1 = "new-publish-name-1".to_string(); @@ -305,6 +301,11 @@ async fn test_publish_doc() { .unwrap(); } + // user unpublish collabs + c.unpublish_collabs(&workspace_id, &[view_id_1, view_id_2]) + .await + .unwrap(); + { // Deleted collab should not be accessible let guest_client = localhost_client(); From 920cc6972918a422bb0c42b3e08990fa6352e437 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 25 Oct 2024 14:48:09 +0800 Subject: [PATCH 4/7] feat: add more cases to disambiguate update custom namespace error --- libs/app-error/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/app-error/src/lib.rs b/libs/app-error/src/lib.rs index 11f49c28..287aeced 100644 --- a/libs/app-error/src/lib.rs +++ b/libs/app-error/src/lib.rs @@ -352,6 +352,9 @@ pub enum ErrorCode { AccessRequestAlreadyExists = 1043, CustomNamespaceDisabled = 1044, CustomNamespaceDisallowed = 1045, + CustomNamespaceTooShort = 1046, + CustomNamespaceTooLong = 1047, + CustomNamespaceReserved = 1048, } impl ErrorCode { From a1e9d56bcf1b6410158dec876097eba211459a6a Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 25 Oct 2024 17:23:35 +0800 Subject: [PATCH 5/7] chore: make pin project workspace dependency --- Cargo.toml | 3 ++- libs/client-api/Cargo.toml | 2 +- libs/infra/Cargo.toml | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f15dbc05..00492ca2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,7 +144,7 @@ tonic-build = "0.11.0" log = "0.4.20" lettre = { version = "0.11.7", features = ["tokio1", "tokio1-native-tls"] } handlebars = "5.1.2" -pin-project = "1.1.5" +pin-project.workspace = true byteorder = "1.5.0" sha2 = "0.10.8" rayon.workspace = true @@ -281,6 +281,7 @@ async_zip = { version = "0.0.17", features = ["full"] } sanitize-filename = "0.5.0" base64 = "0.22" md5 = "0.7.0" +pin-project = "1.1.5" # collaboration yrs = { version = "0.21.2", features = ["sync"] } diff --git a/libs/client-api/Cargo.toml b/libs/client-api/Cargo.toml index d8f8a584..758397be 100644 --- a/libs/client-api/Cargo.toml +++ b/libs/client-api/Cargo.toml @@ -45,7 +45,7 @@ collab-rt-entity = { workspace = true } client-api-entity.workspace = true serde_urlencoded = "0.7.1" futures.workspace = true -pin-project = "1.1.5" +pin-project.workspace = true percent-encoding = "2.3.1" lazy_static = { workspace = true } mime_guess = "2.0.5" diff --git a/libs/infra/Cargo.toml b/libs/infra/Cargo.toml index ce1714e8..bc2d152e 100644 --- a/libs/infra/Cargo.toml +++ b/libs/infra/Cargo.toml @@ -13,9 +13,9 @@ serde_json.workspace = true tracing.workspace = true bytes = { workspace = true } tokio = { workspace = true, optional = true } -pin-project = "1.1.6" +pin-project.workspace = true futures = "0.3.30" [features] file_util = ["tokio/fs"] -request_util = ["reqwest"] \ No newline at end of file +request_util = ["reqwest"] From dc44d0ae4ef634978a263435238e135b76f3a6da Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Sun, 27 Oct 2024 13:36:17 +0800 Subject: [PATCH 6/7] fix: remove assert that might cause panic --- libs/database/src/workspace.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/libs/database/src/workspace.rs b/libs/database/src/workspace.rs index 8e320772..aba61af9 100644 --- a/libs/database/src/workspace.rs +++ b/libs/database/src/workspace.rs @@ -19,16 +19,24 @@ use app_error::AppError; #[inline] pub async fn delete_from_workspace(pg_pool: &PgPool, workspace_id: &Uuid) -> Result<(), AppError> { - let pg_row = sqlx::query!( + let res = sqlx::query!( r#" - DELETE FROM public.af_workspace where workspace_id = $1 + DELETE FROM public.af_workspace + WHERE workspace_id = $1 "#, workspace_id ) .execute(pg_pool) .await?; - assert!(pg_row.rows_affected() == 1); + if res.rows_affected() != 1 { + tracing::error!( + "Failed to delete workspace, workspace_id: {}, rows_affected: {}", + workspace_id, + res.rows_affected() + ); + } + Ok(()) } From dc53e163a05d7bf471b2de059b67bdce1c081620 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Sun, 27 Oct 2024 14:07:08 +0800 Subject: [PATCH 7/7] chore: cargo sqlx --- ...f329a4768ca0372a7b8cac54d83e3277ea0ad5ed9d.json | 14 -------------- ...3be5c6816fc46e3de33f926c6343bcbfa90a387b07.json | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 .sqlx/query-0eae8461a1caa6a609bfc4f329a4768ca0372a7b8cac54d83e3277ea0ad5ed9d.json create mode 100644 .sqlx/query-f58a2f05efbda0698d27d83be5c6816fc46e3de33f926c6343bcbfa90a387b07.json diff --git a/.sqlx/query-0eae8461a1caa6a609bfc4f329a4768ca0372a7b8cac54d83e3277ea0ad5ed9d.json b/.sqlx/query-0eae8461a1caa6a609bfc4f329a4768ca0372a7b8cac54d83e3277ea0ad5ed9d.json deleted file mode 100644 index 0231b748..00000000 --- a/.sqlx/query-0eae8461a1caa6a609bfc4f329a4768ca0372a7b8cac54d83e3277ea0ad5ed9d.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n DELETE FROM public.af_workspace where workspace_id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "0eae8461a1caa6a609bfc4f329a4768ca0372a7b8cac54d83e3277ea0ad5ed9d" -} diff --git a/.sqlx/query-f58a2f05efbda0698d27d83be5c6816fc46e3de33f926c6343bcbfa90a387b07.json b/.sqlx/query-f58a2f05efbda0698d27d83be5c6816fc46e3de33f926c6343bcbfa90a387b07.json new file mode 100644 index 00000000..8eb8b64b --- /dev/null +++ b/.sqlx/query-f58a2f05efbda0698d27d83be5c6816fc46e3de33f926c6343bcbfa90a387b07.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n DELETE FROM public.af_workspace\n WHERE workspace_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "f58a2f05efbda0698d27d83be5c6816fc46e3de33f926c6343bcbfa90a387b07" +}