From f3a24446157779e81a2326a8678f7eeb2a859b15 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 22 Nov 2024 20:21:31 +0800 Subject: [PATCH 1/5] feat: list database row ids --- libs/client-api/src/http_collab.rs | 20 +++++- libs/shared-entity/src/dto/workspace_dto.rs | 5 ++ src/api/workspace.rs | 23 ++++++ src/biz/collab/ops.rs | 80 +++++++++++++++++---- tests/workspace/workspace_crud.rs | 16 +++++ 5 files changed, 130 insertions(+), 14 deletions(-) diff --git a/libs/client-api/src/http_collab.rs b/libs/client-api/src/http_collab.rs index 76ed47b4..6d4c8ec7 100644 --- a/libs/client-api/src/http_collab.rs +++ b/libs/client-api/src/http_collab.rs @@ -2,7 +2,7 @@ use crate::http::log_request_id; use crate::{blocking_brotli_compress, brotli_compress, Client}; use app_error::AppError; use bytes::Bytes; -use client_api_entity::workspace_dto::{AFDatabase, ListDatabaseParam}; +use client_api_entity::workspace_dto::{AFDatabase, AFDatabaseRow, ListDatabaseParam}; use client_api_entity::{ BatchQueryCollabParams, BatchQueryCollabResult, CollabParams, CreateCollabParams, DeleteCollabParams, PublishCollabItem, QueryCollab, QueryCollabParams, UpdateCollabWebParams, @@ -172,6 +172,24 @@ impl Client { AppResponse::from_response(resp).await?.into_data() } + pub async fn list_database_row_ids( + &self, + workspace_id: &str, + database_id: &str, + ) -> Result, AppResponseError> { + let url = format!( + "{}/api/workspace/{}/database/{}/row", + self.base_url, workspace_id, database_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() + } + #[instrument(level = "debug", skip_all, err)] pub async fn post_realtime_msg( &self, diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index b07973b3..4382e91e 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -334,3 +334,8 @@ pub struct AFDatabaseMeta { pub name: String, pub icon: String, } + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct AFDatabaseRow { + pub id: String, +} diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 1b0e68d2..222f11af 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -256,6 +256,10 @@ pub fn workspace_scope() -> Scope { .route(web::post().to(batch_get_collab_handler)), ) .service(web::resource("/{workspace_id}/database").route(web::get().to(list_database_handler))) + .service( + web::resource("/{workspace_id}/database/{database_id}/row") + .route(web::get().to(list_database_row_id_handler)), + ) } pub fn collab_scope() -> Scope { @@ -1874,6 +1878,25 @@ async fn list_database_handler( Ok(Json(AppResponse::Ok().with_data(dbs))) } +async fn list_database_row_id_handler( + user_uuid: UserUuid, + path_param: web::Path<(String, String)>, + state: Data, +) -> Result>>> { + let (workspace_id, db_id) = path_param.into_inner(); + let uid = state.user_cache.get_user_uid(&user_uuid).await?; + + state + .workspace_access_control + .enforce_action(&uid, &workspace_id, Action::Read) + .await?; + + let db_rows = + biz::collab::ops::list_database_row(&state.collab_access_control_storage, workspace_id, db_id) + .await?; + Ok(Json(AppResponse::Ok().with_data(db_rows))) +} + #[inline] async fn parser_realtime_msg( payload: Bytes, diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index adc929bd..d085ef6b 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -19,6 +19,7 @@ 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 shared_entity::dto::workspace_dto::AFDatabaseRow; use shared_entity::dto::workspace_dto::FavoriteFolderView; use shared_entity::dto::workspace_dto::RecentFolderView; use shared_entity::dto::workspace_dto::TrashFolderView; @@ -336,6 +337,24 @@ pub async fn get_latest_collab_encoded( .await } +pub async fn get_latest_collab( + storage: &CollabAccessControlStorage, + origin: GetCollabOrigin, + workspace_id: &str, + oid: &str, + collab_type: CollabType, +) -> Result { + let ec = get_latest_collab_encoded(storage, origin, workspace_id, oid, collab_type).await?; + let collab: Collab = Collab::new_with_source(CollabOrigin::Server, oid, ec.into(), vec![], false) + .map_err(|e| { + AppError::Internal(anyhow::anyhow!( + "Failed to create collab from encoded collab: {:?}", + e + )) + })?; + Ok(collab) +} + pub async fn get_published_view( collab_storage: &CollabAccessControlStorage, publish_namespace: String, @@ -368,7 +387,7 @@ pub async fn list_database( 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( + let mut ws_body_collab = get_latest_collab( collab_storage, GetCollabOrigin::Server, &workspace_uuid_str, @@ -376,23 +395,14 @@ pub async fn list_database( 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).map_err(|e| { + let ws_body = WorkspaceDatabaseBody::open(&mut ws_body_collab).map_err(|e| { AppError::Internal(anyhow::anyhow!( "Failed to open workspace database body: {:?}", e )) })?; - let db_metas = ws_body.get_all_meta(&collab.transact()); + let db_metas = ws_body.get_all_meta(&ws_body_collab.transact()); let query_collabs: Vec = db_metas .into_iter() .map(|meta| QueryCollab { @@ -404,7 +414,7 @@ pub async fn list_database( .batch_get_collab(&uid, query_collabs, true) .await; - let txn = collab.transact(); + let txn = ws_body_collab.transact(); let mut af_databases: Vec = Vec::with_capacity(results.len()); for (oid, result) in results { match result { @@ -463,3 +473,47 @@ pub async fn list_database( Ok(af_databases) } + +pub async fn list_database_row( + collab_storage: &CollabAccessControlStorage, + workspace_uuid_str: String, + database_uuid_str: String, +) -> Result, AppError> { + let db_collab = get_latest_collab( + collab_storage, + GetCollabOrigin::Server, + &workspace_uuid_str, + &database_uuid_str, + CollabType::Database, + ) + .await?; + let db_body = DatabaseBody::from_collab( + &db_collab, + Arc::new(NoPersistenceDatabaseCollabService), + None, + ) + .ok_or_else(|| { + AppError::Internal(anyhow::anyhow!( + "Failed to create database body from collab, db_collab_id: {}", + database_uuid_str, + )) + })?; + + // get any view_id + let txn = db_collab.transact(); + let iid = db_body.get_inline_view_id(&txn); + + let iview = db_body.views.get_view(&txn, &iid).ok_or_else(|| { + AppError::Internal(anyhow::anyhow!("Failed to get inline view, iid: {}", iid)) + })?; + + let db_rows = iview + .row_orders + .into_iter() + .map(|row_order| AFDatabaseRow { + id: row_order.id.to_string(), + }) + .collect(); + + Ok(db_rows) +} diff --git a/tests/workspace/workspace_crud.rs b/tests/workspace/workspace_crud.rs index 6e1835dd..7633311a 100644 --- a/tests/workspace/workspace_crud.rs +++ b/tests/workspace/workspace_crud.rs @@ -57,6 +57,14 @@ async fn workspace_list_database() { .await .unwrap(); assert_eq!(dbs.len(), 1); + { + let untitled_db = &dbs[0]; + let db_row_ids = c + .list_database_row_ids(&workspace_id, &untitled_db.id) + .await + .unwrap(); + assert_eq!(db_row_ids.len(), 5, "{:?}", db_row_ids); + } } { let dbs = c @@ -64,6 +72,14 @@ async fn workspace_list_database() { .await .unwrap(); assert_eq!(dbs.len(), 1); + { + let grid_db = &dbs[0]; + let db_row_ids = c + .list_database_row_ids(&workspace_id, &grid_db.id) + .await + .unwrap(); + assert_eq!(db_row_ids.len(), 5, "{:?}", db_row_ids); + } } } From 6d4200d1e7ef70cbc3633ae846c2b59aefc7b5f1 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Sat, 23 Nov 2024 02:40:26 +0800 Subject: [PATCH 2/5] feat: add row details --- libs/client-api/src/http_collab.rs | 24 +++- libs/shared-entity/src/dto/workspace_dto.rs | 22 ++++ src/api/workspace.rs | 31 +++++ src/biz/collab/ops.rs | 131 ++++++++++++++++++++ tests/workspace/workspace_crud.rs | 45 +++++++ 5 files changed, 252 insertions(+), 1 deletion(-) diff --git a/libs/client-api/src/http_collab.rs b/libs/client-api/src/http_collab.rs index 6d4c8ec7..aec30ec9 100644 --- a/libs/client-api/src/http_collab.rs +++ b/libs/client-api/src/http_collab.rs @@ -2,7 +2,9 @@ use crate::http::log_request_id; use crate::{blocking_brotli_compress, brotli_compress, Client}; use app_error::AppError; use bytes::Bytes; -use client_api_entity::workspace_dto::{AFDatabase, AFDatabaseRow, ListDatabaseParam}; +use client_api_entity::workspace_dto::{ + AFDatabase, AFDatabaseRow, AFDatabaseRowDetail, ListDatabaseParam, ListDatabaseRowDetailParam, +}; use client_api_entity::{ BatchQueryCollabParams, BatchQueryCollabResult, CollabParams, CreateCollabParams, DeleteCollabParams, PublishCollabItem, QueryCollab, QueryCollabParams, UpdateCollabWebParams, @@ -190,6 +192,26 @@ impl Client { AppResponse::from_response(resp).await?.into_data() } + pub async fn list_database_row_details( + &self, + workspace_id: &str, + database_id: &str, + row_ids: &[&str], + ) -> Result, AppResponseError> { + let url = format!( + "{}/api/workspace/{}/database/{}/row/detail", + self.base_url, workspace_id, database_id + ); + let resp = self + .http_client_with_auth(Method::GET, &url) + .await? + .query(&ListDatabaseRowDetailParam::from(row_ids)) + .send() + .await?; + log_request_id(&resp); + AppResponse::from_response(resp).await?.into_data() + } + #[instrument(level = "debug", skip_all, err)] pub async fn post_realtime_msg( &self, diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index 4382e91e..c82b0111 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -298,6 +298,22 @@ pub struct ListDatabaseParam { pub name_filter: Option, // logic: if database name contains } +#[derive(Default, Debug, Deserialize, Serialize)] +pub struct ListDatabaseRowDetailParam { + // Comma separated database row ids + // e.g. ",," + pub ids: String, +} + +impl ListDatabaseRowDetailParam { + pub fn from(ids: &[&str]) -> Self { + Self { ids: ids.join(",") } + } + pub fn into_ids(&self) -> Vec<&str> { + self.ids.split(',').collect() + } +} + #[derive(Default, Debug, Deserialize, Serialize)] pub struct QueryWorkspaceFolder { pub depth: Option, @@ -339,3 +355,9 @@ pub struct AFDatabaseMeta { pub struct AFDatabaseRow { pub id: String, } + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct AFDatabaseRowDetail { + pub id: String, + pub cells: HashMap>, +} diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 222f11af..667bf79e 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -260,6 +260,10 @@ pub fn workspace_scope() -> Scope { web::resource("/{workspace_id}/database/{database_id}/row") .route(web::get().to(list_database_row_id_handler)), ) + .service( + web::resource("/{workspace_id}/database/{database_id}/row/detail") + .route(web::get().to(list_database_row_details_handler)), + ) } pub fn collab_scope() -> Scope { @@ -1897,6 +1901,33 @@ async fn list_database_row_id_handler( Ok(Json(AppResponse::Ok().with_data(db_rows))) } +async fn list_database_row_details_handler( + user_uuid: UserUuid, + path_param: web::Path<(String, String)>, + state: Data, + param: web::Query, +) -> Result>>> { + let (workspace_id, db_id) = path_param.into_inner(); + let uid = state.user_cache.get_user_uid(&user_uuid).await?; + let list_db_row_query = param.into_inner(); + let row_ids = list_db_row_query.into_ids(); + + state + .workspace_access_control + .enforce_action(&uid, &workspace_id, Action::Read) + .await?; + + let db_rows = biz::collab::ops::list_database_row_details( + &state.collab_access_control_storage, + uid, + workspace_id, + db_id, + &row_ids, + ) + .await?; + Ok(Json(AppResponse::Ok().with_data(db_rows))) +} + #[inline] async fn parser_realtime_msg( payload: Bytes, diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index d085ef6b..c771939f 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::sync::Arc; use app_error::AppError; @@ -5,6 +6,8 @@ use appflowy_collaborate::collab::storage::CollabAccessControlStorage; use collab::preclude::Collab; use collab_database::database::DatabaseBody; use collab_database::entity::FieldType; +use collab_database::fields::Field; +use collab_database::rows::RowDetail; use collab_database::workspace_database::NoPersistenceDatabaseCollabService; use collab_database::workspace_database::WorkspaceDatabaseBody; use collab_entity::CollabType; @@ -20,6 +23,7 @@ use database_entity::dto::{QueryCollab, QueryCollabParams}; use shared_entity::dto::workspace_dto::AFDatabase; use shared_entity::dto::workspace_dto::AFDatabaseField; use shared_entity::dto::workspace_dto::AFDatabaseRow; +use shared_entity::dto::workspace_dto::AFDatabaseRowDetail; use shared_entity::dto::workspace_dto::FavoriteFolderView; use shared_entity::dto::workspace_dto::RecentFolderView; use shared_entity::dto::workspace_dto::TrashFolderView; @@ -517,3 +521,130 @@ pub async fn list_database_row( Ok(db_rows) } + +pub async fn list_database_row_details( + collab_storage: &CollabAccessControlStorage, + uid: i64, + workspace_uuid_str: String, + database_uuid_str: String, + row_ids: &[&str], +) -> Result, AppError> { + let query_collabs: Vec = row_ids + .iter() + .map(|id| QueryCollab { + object_id: id.to_string(), + collab_type: CollabType::DatabaseRow, + }) + .collect(); + + let database_collab = get_latest_collab( + collab_storage, + GetCollabOrigin::User { uid }, + &workspace_uuid_str, + &database_uuid_str, + CollabType::Database, + ) + .await?; + let db_body = DatabaseBody::from_collab( + &database_collab, + Arc::new(NoPersistenceDatabaseCollabService), + None, + ) + .ok_or_else(|| { + AppError::Internal(anyhow::anyhow!( + "Failed to create database body from collab, db_collab_id: {}", + database_uuid_str, + )) + })?; + + // create a map of field id to field. + // ensure that the field name is unique. + // if the field name is repeated, it will be appended with the field id, + // under practical usage circumstances, no other collision should occur + let field_by_id: HashMap = { + let all_fields = db_body.fields.get_all_fields(&database_collab.transact()); + + let mut uniq_name_set: HashSet = HashSet::with_capacity(all_fields.len()); + let mut field_by_id: HashMap = HashMap::with_capacity(all_fields.len()); + + for mut field in all_fields { + // if the name already exists, append the field id to the name + if uniq_name_set.contains(&field.name) { + let new_name = format!("{}-{}", field.name, field.id); + field.name = new_name.clone(); + } + uniq_name_set.insert(field.name.clone()); + field_by_id.insert(field.id.clone(), field); + } + field_by_id + }; + + let database_row_details = collab_storage + .batch_get_collab(&uid, query_collabs, true) + .await + .into_iter() + .flat_map(|(id, result)| match result { + QueryCollabResult::Success { encode_collab_v1 } => { + let ec = EncodedCollab::decode_from_bytes(&encode_collab_v1).unwrap(); + let collab = + Collab::new_with_source(CollabOrigin::Server, &id, ec.into(), vec![], false).unwrap(); + let row_detail = RowDetail::from_collab(&collab).unwrap(); + let cells = convert_database_cells_human_readable(row_detail.row.cells, &field_by_id); + Some(AFDatabaseRowDetail { id, cells }) + }, + QueryCollabResult::Failed { error } => { + tracing::warn!("Failed to get collab: {:?}", error); + None + }, + }) + .collect::>(); + + Ok(database_row_details) +} + +fn convert_database_cells_human_readable( + db_cells: HashMap>, + field_by_id: &HashMap, +) -> HashMap> { + let mut human_readable_records: HashMap> = + HashMap::with_capacity(db_cells.len()); + + for (field_id, cell) in db_cells { + let field = match field_by_id.get(&field_id) { + Some(field) => field, + None => { + tracing::error!("Failed to get field by id: {}", field_id); + continue; + }, + }; + + let mut human_readable_cell: HashMap = HashMap::with_capacity(cell.len()); + for (key, value) in cell { + let serde_value: String = match key.as_str() { + "created_at" | "last_modified" => match value.cast::() { + Ok(timestamp) => chrono::DateTime::from_timestamp(timestamp, 0) + .unwrap_or_default() + .to_rfc3339(), + Err(err) => { + tracing::error!("Failed to cast timestamp: {:?}", err); + "".to_string() + }, + }, + "field_type" => match value.cast::() { + Ok(field_type_int) => { + let field_type = FieldType::from(field_type_int); + format!("{:?}", field_type) + }, + Err(err) => { + tracing::error!("Failed to cast field type: {:?}", err); + String::from("Unknown") + }, + }, + _ => value.to_string(), + }; + human_readable_cell.insert(key, serde_value); + } + human_readable_records.insert(field.name.clone(), human_readable_cell); + } + human_readable_records +} diff --git a/tests/workspace/workspace_crud.rs b/tests/workspace/workspace_crud.rs index 7633311a..6eaef515 100644 --- a/tests/workspace/workspace_crud.rs +++ b/tests/workspace/workspace_crud.rs @@ -64,6 +64,51 @@ async fn workspace_list_database() { .await .unwrap(); assert_eq!(db_row_ids.len(), 5, "{:?}", db_row_ids); + { + let db_row_ids: Vec<&str> = db_row_ids.iter().map(|s| s.id.as_str()).collect(); + let db_row_ids: &[&str] = &db_row_ids; + let db_row_details = c + .list_database_row_details(&workspace_id, &untitled_db.id, db_row_ids) + .await + .unwrap(); + assert_eq!(db_row_details.len(), 5, "{:#?}", db_row_details); + + // cells: { + // "Multiselect": { + // "field_type": "MultiSelect", + // "last_modified": "2024-08-16T07:23:57+00:00", + // "created_at": "2024-08-16T07:23:35+00:00", + // "data": "BD-T,6UxM", + // }, + // "Description": { + // "field_type": "RichText", + // "last_modified": "2024-08-16T07:17:03+00:00", + // "created_at": "2024-08-16T07:16:51+00:00", + // "data": "Install AppFlowy Mobile", + // }, + // "Status": { + // "data": "CEZD", + // "field_type": "SingleSelect", + // }, + // }, + let _ = db_row_details + .into_iter() + .find(|row| { + row.cells["Multiselect"]["field_type"] == "MultiSelect" + && row.cells["Multiselect"]["last_modified"] == "2024-08-16T07:23:57+00:00" + && row.cells["Multiselect"]["created_at"] == "2024-08-16T07:23:35+00:00" + && row.cells["Multiselect"]["data"] == "BD-T,6UxM" + // Description + && row.cells["Description"]["field_type"] == "RichText" + && row.cells["Description"]["last_modified"] == "2024-08-16T07:17:03+00:00" + && row.cells["Description"]["created_at"] == "2024-08-16T07:16:51+00:00" + && row.cells["Description"]["data"] == "Install AppFlowy Mobile" + // Status + && row.cells["Status"]["data"] == "CEZD" + && row.cells["Status"]["field_type"] == "SingleSelect" + }) + .unwrap(); + } } } { From a6bd54f4471bd352fe6beeb6fdb22eccfb42417f Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Sat, 23 Nov 2024 12:11:27 +0800 Subject: [PATCH 3/5] feat: simplify listing databases --- libs/client-api/src/http_collab.rs | 4 +- libs/shared-entity/src/dto/workspace_dto.rs | 6 -- src/api/workspace.rs | 3 - src/biz/collab/ops.rs | 86 +++++---------------- tests/workspace/workspace_crud.rs | 78 ++++--------------- 5 files changed, 36 insertions(+), 141 deletions(-) diff --git a/libs/client-api/src/http_collab.rs b/libs/client-api/src/http_collab.rs index aec30ec9..0269c9cc 100644 --- a/libs/client-api/src/http_collab.rs +++ b/libs/client-api/src/http_collab.rs @@ -3,7 +3,7 @@ use crate::{blocking_brotli_compress, brotli_compress, Client}; use app_error::AppError; use bytes::Bytes; use client_api_entity::workspace_dto::{ - AFDatabase, AFDatabaseRow, AFDatabaseRowDetail, ListDatabaseParam, ListDatabaseRowDetailParam, + AFDatabase, AFDatabaseRow, AFDatabaseRowDetail, ListDatabaseRowDetailParam, }; use client_api_entity::{ BatchQueryCollabParams, BatchQueryCollabResult, CollabParams, CreateCollabParams, @@ -161,13 +161,11 @@ impl Client { pub async fn list_databases( &self, workspace_id: &str, - name_filter: Option, ) -> Result, AppResponseError> { let url = format!("{}/api/workspace/{}/database", self.base_url, workspace_id); let resp = self .http_client_with_auth(Method::GET, &url) .await? - .query(&ListDatabaseParam { name_filter }) .send() .await?; log_request_id(&resp); diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index c82b0111..c09beef8 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -293,11 +293,6 @@ pub struct QueryWorkspaceParam { pub include_role: Option, } -#[derive(Default, Debug, Deserialize, Serialize)] -pub struct ListDatabaseParam { - pub name_filter: Option, // logic: if database name contains -} - #[derive(Default, Debug, Deserialize, Serialize)] pub struct ListDatabaseRowDetailParam { // Comma separated database row ids @@ -336,7 +331,6 @@ pub struct PublishedView { pub struct AFDatabase { pub id: String, pub names: Vec, - pub fields: Vec, } #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 667bf79e..fd07b492 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -1866,9 +1866,7 @@ async fn list_database_handler( user_uuid: UserUuid, workspace_id: web::Path, state: Data, - query: web::Query, ) -> Result>>> { - let name_filter = query.into_inner().name_filter; 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( @@ -1876,7 +1874,6 @@ async fn list_database_handler( &state.collab_access_control_storage, uid, workspace_id, - name_filter, ) .await?; Ok(Json(AppResponse::Ok().with_data(dbs))) diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index c771939f..0301cd81 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -21,7 +21,6 @@ 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 shared_entity::dto::workspace_dto::AFDatabaseRow; use shared_entity::dto::workspace_dto::AFDatabaseRowDetail; use shared_entity::dto::workspace_dto::FavoriteFolderView; @@ -386,7 +385,6 @@ pub async fn list_database( collab_storage: &CollabAccessControlStorage, uid: i64, workspace_uuid_str: String, - name_filter: Option, ) -> 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?; @@ -407,72 +405,28 @@ pub async fn list_database( )) })?; let db_metas = ws_body.get_all_meta(&ws_body_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, true) - .await; - let txn = ws_body_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, - Arc::new(NoPersistenceDatabaseCollabService), - None, - ) { - Some(db_body) => { - let db_views = db_body.views.get_all_views_meta(&txn); - let names = db_views - .iter() - .map(|v| v.name.clone()) - .filter(|name| !name.is_empty()) - .collect::>(); + let folder = get_latest_collab_folder( + collab_storage, + GetCollabOrigin::User { uid }, + &workspace_uuid_str, + ) + .await?; - // if there exists a name filter, - // there must be at least one view name that contains the filter - if let Some(name_filter) = &name_filter { - if !names.iter().any(|name| name.contains(name_filter)) { - continue; - } - } - - 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), - names, - fields: af_fields, - }); - }, - 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) - }, - } + let mut af_databases = Vec::with_capacity(db_metas.len()); + for db_meta in db_metas { + let id = db_meta.database_id; + let names: Vec = db_meta + .linked_views + .into_iter() + .map(|view_id| { + folder + .get_view(&view_id) + .map(|v| v.name.clone()) + .unwrap_or_default() + }) + .collect(); + af_databases.push(AFDatabase { id, names }); } Ok(af_databases) diff --git a/tests/workspace/workspace_crud.rs b/tests/workspace/workspace_crud.rs index 6eaef515..d003efd5 100644 --- a/tests/workspace/workspace_crud.rs +++ b/tests/workspace/workspace_crud.rs @@ -1,7 +1,6 @@ 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; @@ -13,54 +12,22 @@ async fn workspace_list_database() { .to_string(); { - let dbs = c.list_databases(&workspace_id, None).await.unwrap(); - assert_eq!(dbs.len(), 1); - - let db = &dbs[0]; - - assert_eq!(db.names.len(), 2); - assert!(db.names.contains(&String::from("Untitled"))); - assert!(db.names.contains(&String::from("Grid"))); - - 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(), - })); - } - - { - let dbs = c - .list_databases(&workspace_id, Some(String::from("nomatch"))) - .await - .unwrap(); - assert_eq!(dbs.len(), 0); - } - { - let dbs = c - .list_databases(&workspace_id, Some(String::from("ntitle"))) - .await - .unwrap(); - assert_eq!(dbs.len(), 1); + let dbs = c.list_databases(&workspace_id).await.unwrap(); + assert_eq!(dbs.len(), 1, "{:?}", dbs); + let todos_db = &dbs[0]; + assert_eq!(todos_db.names.len(), 1); + assert!(todos_db.names.contains(&String::from("To-dos"))); { - let untitled_db = &dbs[0]; let db_row_ids = c - .list_database_row_ids(&workspace_id, &untitled_db.id) + .list_database_row_ids(&workspace_id, &todos_db.id) + .await + .unwrap(); + assert_eq!(db_row_ids.len(), 5, "{:?}", db_row_ids); + } + + { + let db_row_ids = c + .list_database_row_ids(&workspace_id, &todos_db.id) .await .unwrap(); assert_eq!(db_row_ids.len(), 5, "{:?}", db_row_ids); @@ -68,7 +35,7 @@ async fn workspace_list_database() { let db_row_ids: Vec<&str> = db_row_ids.iter().map(|s| s.id.as_str()).collect(); let db_row_ids: &[&str] = &db_row_ids; let db_row_details = c - .list_database_row_details(&workspace_id, &untitled_db.id, db_row_ids) + .list_database_row_details(&workspace_id, &todos_db.id, db_row_ids) .await .unwrap(); assert_eq!(db_row_details.len(), 5, "{:#?}", db_row_details); @@ -111,21 +78,6 @@ async fn workspace_list_database() { } } } - { - let dbs = c - .list_databases(&workspace_id, Some(String::from("rid"))) - .await - .unwrap(); - assert_eq!(dbs.len(), 1); - { - let grid_db = &dbs[0]; - let db_row_ids = c - .list_database_row_ids(&workspace_id, &grid_db.id) - .await - .unwrap(); - assert_eq!(db_row_ids.len(), 5, "{:?}", db_row_ids); - } - } } #[tokio::test] From ddecd8457ced585a96d1f27d526d8b798b13e744 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Sat, 23 Nov 2024 19:24:08 +0800 Subject: [PATCH 4/5] feat: use folder view minimal for database list --- libs/shared-entity/src/dto/workspace_dto.rs | 8 +------- src/biz/collab/ops.rs | 15 ++++++++++----- tests/workspace/workspace_crud.rs | 4 ++-- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index c09beef8..94e08c2b 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -330,7 +330,7 @@ pub struct PublishedView { #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct AFDatabase { pub id: String, - pub names: Vec, + pub views: Vec, } #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -339,12 +339,6 @@ pub struct AFDatabaseField { pub field_type: String, } -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct AFDatabaseMeta { - pub name: String, - pub icon: String, -} - #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct AFDatabaseRow { pub id: String, diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index 0301cd81..051bea7e 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -24,6 +24,7 @@ use shared_entity::dto::workspace_dto::AFDatabase; use shared_entity::dto::workspace_dto::AFDatabaseRow; use shared_entity::dto::workspace_dto::AFDatabaseRowDetail; use shared_entity::dto::workspace_dto::FavoriteFolderView; +use shared_entity::dto::workspace_dto::FolderViewMinimal; use shared_entity::dto::workspace_dto::RecentFolderView; use shared_entity::dto::workspace_dto::TrashFolderView; use sqlx::PgPool; @@ -47,6 +48,7 @@ use super::folder_view::collab_folder_to_folder_view; use super::folder_view::section_items_to_favorite_folder_view; use super::folder_view::section_items_to_recent_folder_view; use super::folder_view::section_items_to_trash_folder_view; +use super::folder_view::to_dto_folder_view_miminal; use super::publish_outline::collab_folder_to_published_outline; /// Create a new collab member @@ -416,17 +418,17 @@ pub async fn list_database( let mut af_databases = Vec::with_capacity(db_metas.len()); for db_meta in db_metas { let id = db_meta.database_id; - let names: Vec = db_meta + let views: Vec = db_meta .linked_views .into_iter() .map(|view_id| { folder .get_view(&view_id) - .map(|v| v.name.clone()) + .map(|view| to_dto_folder_view_miminal(&view)) .unwrap_or_default() }) .collect(); - af_databases.push(AFDatabase { id, names }); + af_databases.push(AFDatabase { id, views }); } Ok(af_databases) @@ -572,9 +574,12 @@ fn convert_database_cells_human_readable( }, }; + println!("field_type: {:#?}", field.field_type); + println!("type_options: {:#?}", field.type_options); + let mut human_readable_cell: HashMap = HashMap::with_capacity(cell.len()); for (key, value) in cell { - let serde_value: String = match key.as_str() { + let value_str: String = match key.as_str() { "created_at" | "last_modified" => match value.cast::() { Ok(timestamp) => chrono::DateTime::from_timestamp(timestamp, 0) .unwrap_or_default() @@ -596,7 +601,7 @@ fn convert_database_cells_human_readable( }, _ => value.to_string(), }; - human_readable_cell.insert(key, serde_value); + human_readable_cell.insert(key, value_str); } human_readable_records.insert(field.name.clone(), human_readable_cell); } diff --git a/tests/workspace/workspace_crud.rs b/tests/workspace/workspace_crud.rs index d003efd5..3e9be5b3 100644 --- a/tests/workspace/workspace_crud.rs +++ b/tests/workspace/workspace_crud.rs @@ -15,8 +15,8 @@ async fn workspace_list_database() { let dbs = c.list_databases(&workspace_id).await.unwrap(); assert_eq!(dbs.len(), 1, "{:?}", dbs); let todos_db = &dbs[0]; - assert_eq!(todos_db.names.len(), 1); - assert!(todos_db.names.contains(&String::from("To-dos"))); + assert_eq!(todos_db.views.len(), 1); + assert_eq!(todos_db.views[0].name, "To-dos"); { let db_row_ids = c .list_database_row_ids(&workspace_id, &todos_db.id) From 298698ac0ee6de2f688b63c1e5e2acda0e3fe837 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Sat, 23 Nov 2024 20:56:47 +0800 Subject: [PATCH 5/5] feat: format selection --- libs/shared-entity/src/dto/workspace_dto.rs | 2 +- src/biz/collab/ops.rs | 148 +++++++++++++++++--- tests/workspace/workspace_crud.rs | 8 +- 3 files changed, 132 insertions(+), 26 deletions(-) diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index 94e08c2b..72cf0b27 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -347,5 +347,5 @@ pub struct AFDatabaseRow { #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct AFDatabaseRowDetail { pub id: String, - pub cells: HashMap>, + pub cells: HashMap>, } diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index 051bea7e..379ac845 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -7,6 +7,7 @@ use collab::preclude::Collab; use collab_database::database::DatabaseBody; use collab_database::entity::FieldType; use collab_database::fields::Field; +use collab_database::fields::TypeOptions; use collab_database::rows::RowDetail; use collab_database::workspace_database::NoPersistenceDatabaseCollabService; use collab_database::workspace_database::WorkspaceDatabaseBody; @@ -535,6 +536,11 @@ pub async fn list_database_row_details( field_by_id }; + let mut selection_name_by_id: HashMap = HashMap::new(); + for field in field_by_id.values() { + add_to_selection_from_field(&mut selection_name_by_id, field); + } + let database_row_details = collab_storage .batch_get_collab(&uid, query_collabs, true) .await @@ -545,7 +551,11 @@ pub async fn list_database_row_details( let collab = Collab::new_with_source(CollabOrigin::Server, &id, ec.into(), vec![], false).unwrap(); let row_detail = RowDetail::from_collab(&collab).unwrap(); - let cells = convert_database_cells_human_readable(row_detail.row.cells, &field_by_id); + let cells = convert_database_cells_human_readable( + row_detail.row.cells, + &field_by_id, + &selection_name_by_id, + ); Some(AFDatabaseRowDetail { id, cells }) }, QueryCollabResult::Failed { error } => { @@ -561,8 +571,9 @@ pub async fn list_database_row_details( fn convert_database_cells_human_readable( db_cells: HashMap>, field_by_id: &HashMap, -) -> HashMap> { - let mut human_readable_records: HashMap> = + selection_name_by_id: &HashMap, +) -> HashMap> { + let mut human_readable_records: HashMap> = HashMap::with_capacity(db_cells.len()); for (field_id, cell) in db_cells { @@ -573,37 +584,132 @@ fn convert_database_cells_human_readable( continue; }, }; + let field_type = FieldType::from(field.field_type); - println!("field_type: {:#?}", field.field_type); - println!("type_options: {:#?}", field.type_options); - - let mut human_readable_cell: HashMap = HashMap::with_capacity(cell.len()); + let mut human_readable_cell: HashMap = + HashMap::with_capacity(cell.len()); for (key, value) in cell { - let value_str: String = match key.as_str() { + let serde_value: serde_json::Value = match key.as_str() { "created_at" | "last_modified" => match value.cast::() { Ok(timestamp) => chrono::DateTime::from_timestamp(timestamp, 0) .unwrap_or_default() - .to_rfc3339(), + .to_rfc3339() + .into(), Err(err) => { tracing::error!("Failed to cast timestamp: {:?}", err); - "".to_string() + serde_json::Value::Null }, }, - "field_type" => match value.cast::() { - Ok(field_type_int) => { - let field_type = FieldType::from(field_type_int); - format!("{:?}", field_type) - }, - Err(err) => { - tracing::error!("Failed to cast field type: {:?}", err); - String::from("Unknown") - }, + "field_type" => format!("{:?}", field_type).into(), + "data" => { + match field_type { + FieldType::DateTime => { + if let yrs::any::Any::String(value_str) = value { + let int_value = value_str.parse::().unwrap_or_default(); + chrono::DateTime::from_timestamp(int_value, 0) + .unwrap_or_default() + .to_rfc3339() + .into() + } else { + serde_json::to_value(value).unwrap_or_default() + } + }, + FieldType::Checklist => { + if let yrs::any::Any::String(value_str) = value { + serde_json::from_str(&value_str).unwrap_or_default() + } else { + serde_json::to_value(value).unwrap_or_default() + } + }, + FieldType::Media => { + if let yrs::any::Any::Array(arr) = value { + let mut acc = Vec::with_capacity(arr.len()); + for v in arr.as_ref() { + if let yrs::any::Any::String(value_str) = v { + let serde_value = serde_json::from_str(value_str).unwrap_or_default(); + acc.push(serde_value); + } + } + serde_json::Value::Array(acc) + } else { + serde_json::to_value(value).unwrap_or_default() + } + }, + FieldType::SingleSelect => { + if let yrs::any::Any::String(ref value_str) = value { + selection_name_by_id + .get(value_str.as_ref()) + .map(|v| v.to_string()) + .map(serde_json::Value::String) + .unwrap_or_else(|| value.to_string().into()) + } else { + serde_json::to_value(value).unwrap_or_default() + } + }, + FieldType::MultiSelect => { + if let yrs::any::Any::String(value_str) = value { + value_str + .split(',') + .filter_map(|v| selection_name_by_id.get(v).map(|v| v.to_string())) + .fold(String::new(), |mut acc, s| { + if !acc.is_empty() { + acc.push(','); + } + acc.push_str(&s); + acc + }) + .into() + } else { + serde_json::to_value(value).unwrap_or_default() + } + }, + // Handle different field types formatting as needed + _ => serde_json::to_value(value).unwrap_or_default(), + } }, - _ => value.to_string(), + _ => serde_json::to_value(value).unwrap_or_default(), }; - human_readable_cell.insert(key, value_str); + human_readable_cell.insert(key, serde_value); } human_readable_records.insert(field.name.clone(), human_readable_cell); } human_readable_records } + +fn add_to_selection_from_field(name_by_id: &mut HashMap, field: &Field) { + let field_type = FieldType::from(field.field_type); + match field_type { + FieldType::SingleSelect => { + add_to_selection_from_type_options(name_by_id, &field.type_options, &field_type); + }, + FieldType::MultiSelect => { + add_to_selection_from_type_options(name_by_id, &field.type_options, &field_type) + }, + _ => (), + } +} + +fn add_to_selection_from_type_options( + name_by_id: &mut HashMap, + type_options: &TypeOptions, + field_type: &FieldType, +) { + if let Some(type_opt) = type_options.get(&field_type.type_id()) { + if let Some(yrs::Any::String(arc_str)) = type_opt.get("content") { + if let Ok(serde_value) = serde_json::from_str::(arc_str) { + if let Some(selections) = serde_value.get("options").and_then(|v| v.as_array()) { + for selection in selections { + if let serde_json::Value::Object(selection) = selection { + if let (Some(id), Some(name)) = ( + selection.get("id").and_then(|v| v.as_str()), + selection.get("name").and_then(|v| v.as_str()), + ) { + name_by_id.insert(id.to_owned(), name.to_owned()); + } + } + } + } + } + } + }; +} diff --git a/tests/workspace/workspace_crud.rs b/tests/workspace/workspace_crud.rs index 3e9be5b3..b798b29a 100644 --- a/tests/workspace/workspace_crud.rs +++ b/tests/workspace/workspace_crud.rs @@ -45,7 +45,7 @@ async fn workspace_list_database() { // "field_type": "MultiSelect", // "last_modified": "2024-08-16T07:23:57+00:00", // "created_at": "2024-08-16T07:23:35+00:00", - // "data": "BD-T,6UxM", + // "data": "looks great,fast", // }, // "Description": { // "field_type": "RichText", @@ -54,7 +54,7 @@ async fn workspace_list_database() { // "data": "Install AppFlowy Mobile", // }, // "Status": { - // "data": "CEZD", + // "data": "To Do", // "field_type": "SingleSelect", // }, // }, @@ -64,14 +64,14 @@ async fn workspace_list_database() { row.cells["Multiselect"]["field_type"] == "MultiSelect" && row.cells["Multiselect"]["last_modified"] == "2024-08-16T07:23:57+00:00" && row.cells["Multiselect"]["created_at"] == "2024-08-16T07:23:35+00:00" - && row.cells["Multiselect"]["data"] == "BD-T,6UxM" + && row.cells["Multiselect"]["data"] == "looks great,fast" // Description && row.cells["Description"]["field_type"] == "RichText" && row.cells["Description"]["last_modified"] == "2024-08-16T07:17:03+00:00" && row.cells["Description"]["created_at"] == "2024-08-16T07:16:51+00:00" && row.cells["Description"]["data"] == "Install AppFlowy Mobile" // Status - && row.cells["Status"]["data"] == "CEZD" + && row.cells["Status"]["data"] == "To Do" && row.cells["Status"]["field_type"] == "SingleSelect" }) .unwrap();