From 6777ed8f5d667e802aea7464254b8fc663e2e320 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Wed, 25 Sep 2024 14:46:26 +0800 Subject: [PATCH 1/4] feat: add database listings --- libs/client-api/src/http_collab.rs | 19 +++++ libs/shared-entity/src/dto/workspace_dto.rs | 13 +++ src/api/workspace.rs | 18 ++++ src/biz/collab/ops.rs | 95 +++++++++++++++++++++ tests/workspace/workspace_crud.rs | 33 +++++++ 5 files changed, 178 insertions(+) diff --git a/libs/client-api/src/http_collab.rs b/libs/client-api/src/http_collab.rs index cdeb6f063..52106271c 100644 --- a/libs/client-api/src/http_collab.rs +++ b/libs/client-api/src/http_collab.rs @@ -1,6 +1,7 @@ use crate::http::log_request_id; use crate::{blocking_brotli_compress, Client}; use app_error::AppError; +use client_api_entity::workspace_dto::AFDatabase; use client_api_entity::{ BatchQueryCollabParams, BatchQueryCollabResult, CreateCollabParams, DeleteCollabParams, QueryCollab, @@ -103,6 +104,7 @@ impl Client { .await? .into_data() } + #[instrument(level = "info", skip_all, err)] pub async fn delete_collab(&self, params: DeleteCollabParams) -> Result<(), AppResponseError> { let url = format!( @@ -118,4 +120,21 @@ impl Client { log_request_id(&resp); AppResponse::<()>::from_response(resp).await?.into_error() } + + #[instrument(level = "info", skip_all, err)] + pub async fn list_databases( + &self, + workspace_id: &str, + ) -> Result, AppResponseError> { + let url = format!("{}/api/workspace/{}/database", self.base_url, workspace_id); + let resp = self + .http_client_with_auth(Method::GET, &url) + .await? + .send() + .await?; + log_request_id(&resp); + AppResponse::from_response(resp) + .await? + .into_data() + } } diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index ff6d2c672..11ef99788 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -215,3 +215,16 @@ pub struct PublishedView { pub extra: Option, pub children: Vec, } + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct AFDatabase { + pub id: String, + pub name: String, + pub fields: Vec, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct AFDatabaseField { + pub name: String, + pub field_type: String, +} diff --git a/src/api/workspace.rs b/src/api/workspace.rs index d7e371a08..b0e12f8c9 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -205,6 +205,7 @@ pub fn workspace_scope() -> Scope { // Web browser can't carry payload when using GET method, so for browser compatibility, we use POST method .route(web::post().to(batch_get_collab_handler)), ) + .service(web::resource("/{workspace_id}/database").route(web::get().to(list_database_handler))) } pub fn collab_scope() -> Scope { @@ -1450,6 +1451,23 @@ async fn get_workspace_publish_outline_handler( Ok(Json(AppResponse::Ok().with_data(published_view))) } +async fn list_database_handler( + user_uuid: UserUuid, + workspace_id: web::Path, + state: Data, +) -> Result>>> { + let uid = state.user_cache.get_user_uid(&user_uuid).await?; + let workspace_id = workspace_id.into_inner(); + let dbs = biz::collab::ops::list_database( + &state.pg_pool, + &state.collab_access_control_storage, + uid, + workspace_id, + ) + .await?; + Ok(Json(AppResponse::Ok().with_data(dbs))) +} + #[inline] async fn parser_realtime_msg( payload: Bytes, diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index f81dea247..ec3459016 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -2,14 +2,22 @@ use std::sync::Arc; use app_error::AppError; use appflowy_collaborate::collab::storage::CollabAccessControlStorage; +use collab::preclude::Collab; +use collab_database::database::DatabaseBody; +use collab_database::entity::FieldType; +use collab_database::workspace_database::WorkspaceDatabaseBody; use collab_entity::CollabType; use collab_entity::EncodedCollab; use collab_folder::SectionItem; use collab_folder::{CollabOrigin, Folder}; +use database::collab::select_workspace_database_oid; use database::collab::{CollabStorage, GetCollabOrigin}; use database::publish::select_published_view_ids_for_workspace; use database::publish::select_workspace_id_for_publish_namespace; +use database_entity::dto::QueryCollabResult; use database_entity::dto::{QueryCollab, QueryCollabParams}; +use shared_entity::dto::workspace_dto::AFDatabase; +use shared_entity::dto::workspace_dto::AFDatabaseField; use sqlx::PgPool; use std::ops::DerefMut; @@ -347,3 +355,90 @@ pub async fn get_published_view( collab_folder_to_published_outline(&workspace_id.to_string(), &folder, &publish_view_ids)?; Ok(published_view) } + +pub async fn list_database( + pg_pool: &PgPool, + collab_storage: &Arc, + uid: i64, + workspace_uuid_str: String, +) -> Result, AppError> { + let workspace_uuid: Uuid = workspace_uuid_str.as_str().parse()?; + let ws_db_oid = select_workspace_database_oid(pg_pool, &workspace_uuid).await?; + + let ec = get_latest_collab_encoded( + collab_storage.clone(), + GetCollabOrigin::Server, + &workspace_uuid_str, + &ws_db_oid, + CollabType::WorkspaceDatabase, + ) + .await?; + let mut collab: Collab = + Collab::new_with_source(CollabOrigin::Server, &ws_db_oid, ec.into(), vec![], false).map_err( + |e| { + AppError::Internal(anyhow::anyhow!( + "Failed to create collab from encoded collab: {:?}", + e + )) + }, + )?; + + let ws_body = WorkspaceDatabaseBody::open(&mut collab); + let db_metas = ws_body.get_all_database_meta(&collab.transact()); + let query_collabs: Vec = db_metas + .into_iter() + .map(|meta| QueryCollab { + object_id: meta.database_id.clone(), + collab_type: CollabType::Database, + }) + .collect(); + let results = collab_storage.batch_get_collab(&uid, query_collabs).await; + + let txn = collab.transact(); + let mut af_databases: Vec = Vec::with_capacity(results.len()); + for (oid, result) in results { + match result { + QueryCollabResult::Success { encode_collab_v1 } => { + match EncodedCollab::decode_from_bytes(&encode_collab_v1) { + Ok(ec) => { + match Collab::new_with_source(CollabOrigin::Server, &oid, ec.into(), vec![], false) { + Ok(db_collab) => match DatabaseBody::from_collab(&db_collab) { + Some(db_body) => match db_body.metas.get_inline_view_id(&txn) { + Some(iid) => match db_body.views.get_view(&txn, &iid) { + Some(iview) => { + let name = iview.name; + + let db_fields = db_body.fields.get_all_fields(&txn); + let mut af_fields: Vec = Vec::with_capacity(db_fields.len()); + for db_field in db_fields { + af_fields.push(AFDatabaseField { + name: db_field.name, + field_type: format!("{:?}", FieldType::from(db_field.field_type)), + }); + } + af_databases.push(AFDatabase { + id: db_body.get_database_id(&txn), + name, + fields: af_fields, + }); + }, + None => tracing::warn!("Failed to get inline view: {}", iid), + }, + None => tracing::error!("Failed to get inline view id for database: {}", oid), + }, + None => tracing::error!("Failed to create db_body from db_collab, oid: {}", oid), + }, + Err(err) => tracing::error!("Failed to create db_collab: {:?}", err), + } + }, + Err(err) => tracing::error!("Failed to decode collab: {:?}", err), + } + }, + QueryCollabResult::Failed { error } => { + tracing::warn!("Failed to get collab: {:?}", error) + }, + } + } + + Ok(af_databases) +} diff --git a/tests/workspace/workspace_crud.rs b/tests/workspace/workspace_crud.rs index 5f484ae7b..da1a668f5 100644 --- a/tests/workspace/workspace_crud.rs +++ b/tests/workspace/workspace_crud.rs @@ -1,9 +1,42 @@ use client_api_test::generate_unique_registered_user_client; use collab_entity::CollabType; use database_entity::dto::QueryCollabParams; +use shared_entity::dto::workspace_dto::AFDatabaseField; use shared_entity::dto::workspace_dto::CreateWorkspaceParam; use shared_entity::dto::workspace_dto::PatchWorkspaceParam; +#[tokio::test] +async fn workspace_list_database() { + let (c, _user) = generate_unique_registered_user_client().await; + let workspace_id = c.get_workspaces().await.unwrap()[0].workspace_id; + let dbs = c.list_databases(&workspace_id.to_string()).await.unwrap(); + assert_eq!(dbs.len(), 1); + + let db = &dbs[0]; + + assert_eq!(db.name, "Untitled"); + assert!(db.fields.contains(&AFDatabaseField { + name: "Last modified".to_string(), + field_type: "LastEditedTime".to_string(), + })); + assert!(db.fields.contains(&AFDatabaseField { + name: "Multiselect".to_string(), + field_type: "MultiSelect".to_string(), + })); + assert!(db.fields.contains(&AFDatabaseField { + name: "Tasks".to_string(), + field_type: "Checklist".to_string(), + })); + assert!(db.fields.contains(&AFDatabaseField { + name: "Status".to_string(), + field_type: "SingleSelect".to_string(), + })); + assert!(db.fields.contains(&AFDatabaseField { + name: "Description".to_string(), + field_type: "RichText".to_string(), + })); +} + #[tokio::test] async fn add_and_delete_workspace_for_user() { let (c, _user) = generate_unique_registered_user_client().await; From 30dff6c25bd645217d1d2938df9c10ef31b2aff3 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Thu, 7 Nov 2024 16:33:27 +0800 Subject: [PATCH 2/4] chore: add missing imports --- libs/client-api/src/http_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/client-api/src/http_view.rs b/libs/client-api/src/http_view.rs index d2c7656b8..25e513311 100644 --- a/libs/client-api/src/http_view.rs +++ b/libs/client-api/src/http_view.rs @@ -1,4 +1,4 @@ -use client_api_entity::workspace_dto::{CreatePageParams, Page, PageCollab}; +use client_api_entity::workspace_dto::{CreatePageParams, Page, PageCollab, UpdatePageParams}; use reqwest::Method; use serde_json::json; use shared_entity::response::{AppResponse, AppResponseError}; From 867ee8eeffaec98126d660e19e50ccf2ee42d033 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Thu, 7 Nov 2024 17:08:17 +0800 Subject: [PATCH 3/4] chore: cargo fmt --- libs/client-api/src/http_collab.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libs/client-api/src/http_collab.rs b/libs/client-api/src/http_collab.rs index 0d44d4682..bafa05405 100644 --- a/libs/client-api/src/http_collab.rs +++ b/libs/client-api/src/http_collab.rs @@ -153,8 +153,6 @@ impl Client { .send() .await?; log_request_id(&resp); - AppResponse::from_response(resp) - .await? - .into_data() + AppResponse::from_response(resp).await?.into_data() } } From fa771815c9230ffe9a2eb688d76875143271b540 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Thu, 7 Nov 2024 20:19:45 +0800 Subject: [PATCH 4/4] chore: fix test case --- tests/workspace/workspace_crud.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/workspace/workspace_crud.rs b/tests/workspace/workspace_crud.rs index da1a668f5..76e01385e 100644 --- a/tests/workspace/workspace_crud.rs +++ b/tests/workspace/workspace_crud.rs @@ -14,7 +14,7 @@ async fn workspace_list_database() { let db = &dbs[0]; - assert_eq!(db.name, "Untitled"); + assert_eq!(db.name, ""); assert!(db.fields.contains(&AFDatabaseField { name: "Last modified".to_string(), field_type: "LastEditedTime".to_string(),