From 18da7b873b53d57d527e7772bc0d84e56140b601 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 14 Jun 2024 21:35:39 +0800 Subject: [PATCH] feat: publish namespace and metadata --- libs/database-entity/src/dto.rs | 5 ++ libs/database/src/workspace.rs | 61 ++++++++++++++ migrations/20240613112820_publish_collab.sql | 28 +++++++ src/api/workspace.rs | 45 +++++++++++ src/biz/workspace/ops.rs | 84 +++++++++++++++++--- 5 files changed, 211 insertions(+), 12 deletions(-) create mode 100644 migrations/20240613112820_publish_collab.sql diff --git a/libs/database-entity/src/dto.rs b/libs/database-entity/src/dto.rs index 66e554ff..099d1aac 100644 --- a/libs/database-entity/src/dto.rs +++ b/libs/database-entity/src/dto.rs @@ -278,6 +278,11 @@ pub struct CollabMemberIdentify { pub object_id: String, } +#[derive(Deserialize)] +pub struct UpdatePublishNamespace { + pub new_namespace: String, +} + #[derive(Debug, Clone, Validate, Serialize, Deserialize)] pub struct QueryCollabMembers { #[validate(custom = "validate_not_empty_str")] diff --git a/libs/database/src/workspace.rs b/libs/database/src/workspace.rs index 886a413e..5c2c6af9 100644 --- a/libs/database/src/workspace.rs +++ b/libs/database/src/workspace.rs @@ -790,3 +790,64 @@ pub async fn upsert_workspace_settings( Ok(()) } + +#[inline] +pub async fn update_workspace_publish_namespace<'a, E: Executor<'a, Database = Postgres>>( + executor: E, + workspace_id: &Uuid, + new_namespace: &str, +) -> Result<(), AppError> { + let res = sqlx::query!( + r#" + UPDATE af_workspace + SET publish_namespace = $1 + WHERE workspace_id = $2 + "#, + new_namespace, + workspace_id, + ) + .execute(executor) + .await?; + + if res.rows_affected() != 1 { + tracing::error!( + "Failed to update workspace publish namespace, workspace_id: {}, new_namespace: {}, rows_affected: {}", + workspace_id, new_namespace, res.rows_affected() + ); + } + + Ok(()) +} + +#[inline] +pub async fn insert_or_replace_publish_collab_meta<'a, E: Executor<'a, Database = Postgres>>( + executor: E, + workspace_id: &Uuid, + doc_name: &str, + publisher_uuid: &Uuid, + metadata: &serde_json::Value, +) -> Result<(), AppError> { + let res = sqlx::query!( + r#" + INSERT INTO af_published_collab (doc_name, published_by, workspace_id, metadata) + VALUES ($1, (SELECT uid FROM af_user WHERE uuid = $2), $3, $4) + ON CONFLICT (workspace_id, doc_name) DO UPDATE + SET metadata = $4 + "#, + doc_name, + publisher_uuid, + workspace_id, + metadata + ) + .execute(executor) + .await?; + + if res.rows_affected() != 1 { + tracing::error!( + "Failed to insert or replace publish collab meta, workspace_id: {}, doc_name: {}, publisher_uuid: {}, rows_affected: {}", + workspace_id, doc_name, publisher_uuid, res.rows_affected() + ); + } + + Ok(()) +} diff --git a/migrations/20240613112820_publish_collab.sql b/migrations/20240613112820_publish_collab.sql new file mode 100644 index 00000000..24ad69dd --- /dev/null +++ b/migrations/20240613112820_publish_collab.sql @@ -0,0 +1,28 @@ +-- stores the published view of a workspace by a user of workspace +CREATE TABLE IF NOT EXISTS af_published_collab ( + doc_name TEXT NOT NULL, + published_by BIGINT NOT NULL REFERENCES af_user(uid) ON DELETE CASCADE, + workspace_id UUID NOT NULL REFERENCES af_workspace(workspace_id) ON DELETE CASCADE, + metadata JSONB NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY (workspace_id, doc_name) +); + +-- trigger to update updated_at column +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER af_published_collab_update_updated_at +BEFORE UPDATE ON af_published_collab +FOR EACH ROW +EXECUTE FUNCTION update_updated_at(); + +-- every workspace have a prefix for published view +ALTER TABLE af_workspace ADD COLUMN publish_namespace TEXT UNIQUE; diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 2fb3fa6f..fd019163 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -128,6 +128,14 @@ pub fn workspace_scope() -> Scope { .route(web::put().to(update_collab_member_handler)) .route(web::delete().to(remove_collab_member_handler)), ) + .service( + web::resource("/{workspace_id}/publish-namespace") + .route(web::put().to(put_publish_namespace_handler)) + ) + .service( + web::resource("/{workspace_id}/collab/{object_id}/publish") + .route(web::post().to(post_publish_collab_handler)) + ) .service( web::resource("/{workspace_id}/collab/{object_id}/member/list") .route(web::get().to(get_collab_member_list_handler)), @@ -937,6 +945,43 @@ async fn remove_collab_member_handler( Ok(Json(AppResponse::Ok())) } +async fn put_publish_namespace_handler( + user_uuid: UserUuid, + workspace_id: web::Path, + payload: Json, + state: Data, +) -> Result>> { + let workspace_id = workspace_id.into_inner(); + let new_namespace = payload.into_inner().new_namespace; + biz::workspace::ops::update_workspace_namespace( + &state.pg_pool, + &user_uuid, + &workspace_id, + &new_namespace, + ) + .await?; + Ok(Json(AppResponse::Ok())) +} + +async fn post_publish_collab_handler( + path_param: web::Path<(Uuid, String)>, + user_uuid: UserUuid, + payload: String, + state: Data, +) -> Result>> { + let (workspace_id, doc_name) = path_param.into_inner(); + let metadata = serde_json::Value::from(payload); + biz::workspace::ops::publish_workspace_collab( + &state.pg_pool, + &workspace_id, + &doc_name, + &user_uuid, + &metadata, + ) + .await?; + Ok(Json(AppResponse::Ok())) +} + #[instrument(level = "debug", skip(state, payload), err)] async fn get_collab_member_list_handler( payload: Json, diff --git a/src/biz/workspace/ops.rs b/src/biz/workspace/ops.rs index 1ba5618a..ce94cdfc 100644 --- a/src/biz/workspace/ops.rs +++ b/src/biz/workspace/ops.rs @@ -18,12 +18,12 @@ use database::resource_usage::get_all_workspace_blob_metadata; use database::user::select_uid_from_email; use database::workspace::{ change_workspace_icon, delete_from_workspace, delete_workspace_members, get_invitation_by_id, - insert_user_workspace, insert_workspace_invitation, rename_workspace, select_all_user_workspaces, - select_user_is_workspace_owner, select_workspace, select_workspace_invitations_for_user, - select_workspace_member, select_workspace_member_list, select_workspace_settings, - select_workspace_total_collab_bytes, update_updated_at_of_workspace, - update_workspace_invitation_set_status_accepted, upsert_workspace_member, - upsert_workspace_member_with_txn, upsert_workspace_settings, + insert_or_replace_publish_collab_meta, insert_user_workspace, insert_workspace_invitation, + rename_workspace, select_all_user_workspaces, select_user_is_workspace_owner, select_workspace, + select_workspace_invitations_for_user, select_workspace_member, select_workspace_member_list, + select_workspace_settings, select_workspace_total_collab_bytes, update_updated_at_of_workspace, + update_workspace_invitation_set_status_accepted, update_workspace_publish_namespace, + upsert_workspace_member, upsert_workspace_member_with_txn, upsert_workspace_settings, }; use database_entity::dto::{ AFAccessLevel, AFRole, AFWorkspace, AFWorkspaceInvitation, AFWorkspaceInvitationStatus, @@ -114,6 +114,38 @@ pub async fn patch_workspace( Ok(()) } +pub async fn update_workspace_namespace( + pg_pool: &PgPool, + user_uuid: &Uuid, + workspace_id: &Uuid, + new_namespace: &str, +) -> Result<(), AppError> { + check_workspace_owner(pg_pool, user_uuid, workspace_id).await?; + check_workspace_namespace(new_namespace).await?; + update_workspace_publish_namespace(pg_pool, workspace_id, new_namespace).await?; + Ok(()) +} + +pub async fn publish_workspace_collab( + pg_pool: &PgPool, + workspace_id: &Uuid, + doc_name: &str, + publisher_uuid: &Uuid, + metadata: &serde_json::Value, +) -> Result<(), AppError> { + insert_or_replace_publish_collab_meta(pg_pool, workspace_id, doc_name, publisher_uuid, metadata) + .await?; + Ok(()) +} + +pub async fn publish_workspace_collab_data( + _pg_pool: &PgPool, + _workspace_id: &Uuid, + _view_id: &Uuid, +) -> Result<(), AppError> { + Ok(()) +} + pub async fn get_all_user_workspaces( pg_pool: &PgPool, user_uuid: &Uuid, @@ -453,12 +485,7 @@ pub async fn get_workspace_document_total_bytes( user_uuid: &Uuid, workspace_id: &Uuid, ) -> Result { - let is_owner = select_user_is_workspace_owner(pg_pool, user_uuid, workspace_id).await?; - if !is_owner { - return Err(AppError::UserUnAuthorized( - "User is not the owner of the workspace".to_string(), - )); - } + check_workspace_owner(pg_pool, user_uuid, workspace_id).await?; let byte_count = select_workspace_total_collab_bytes(pg_pool, workspace_id).await?; Ok(WorkspaceUsage { @@ -510,3 +537,36 @@ pub async fn update_workspace_settings( tx.commit().await?; Ok(()) } + +async fn check_workspace_owner( + pg_pool: &PgPool, + user_uuid: &Uuid, + workspace_id: &Uuid, +) -> Result<(), AppError> { + match select_user_is_workspace_owner(pg_pool, user_uuid, workspace_id).await? { + true => Ok(()), + false => Err(AppError::UserUnAuthorized( + "User is not the owner of the workspace".to_string(), + )), + } +} + +async fn check_workspace_namespace(new_namespace: &str) -> Result<(), AppError> { + // Check len + if new_namespace.len() < 8 { + return Err(AppError::InvalidRequest( + "Namespace must be at least 8 characters long".to_string(), + )); + } + + // Only contain alphanumeric characters and hyphens + for c in new_namespace.chars() { + if !c.is_alphanumeric() && c != '-' { + return Err(AppError::InvalidRequest( + "Namespace must only contain alphanumeric characters and hyphens".to_string(), + )); + } + } + + Ok(()) +}