From 2da78b351fb48e267a44ac6faa5dfa02c2ab7fd2 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Tue, 26 Nov 2024 14:20:10 +0800 Subject: [PATCH 1/3] feat: api to get database fields --- libs/client-api/src/http_collab.rs | 20 +++++- libs/shared-entity/src/dto/workspace_dto.rs | 14 ++-- src/api/workspace.rs | 26 ++++++++ src/biz/collab/ops.rs | 71 +++++++++++++++------ tests/workspace/workspace_crud.rs | 44 ++++++++++++- 5 files changed, 147 insertions(+), 28 deletions(-) diff --git a/libs/client-api/src/http_collab.rs b/libs/client-api/src/http_collab.rs index d02b9c54..d1841167 100644 --- a/libs/client-api/src/http_collab.rs +++ b/libs/client-api/src/http_collab.rs @@ -4,7 +4,7 @@ use app_error::AppError; use bytes::Bytes; use chrono::{DateTime, Utc}; use client_api_entity::workspace_dto::{ - AFDatabase, AFDatabaseRow, AFDatabaseRowDetail, DatabaseRowUpdatedItem, + AFDatabase, AFDatabaseField, AFDatabaseRow, AFDatabaseRowDetail, DatabaseRowUpdatedItem, ListDatabaseRowDetailParam, ListDatabaseRowUpdatedParam, }; use client_api_entity::{ @@ -192,6 +192,24 @@ impl Client { AppResponse::from_response(resp).await?.into_data() } + pub async fn get_database_fields( + &self, + workspace_id: &str, + database_id: &str, + ) -> Result, AppResponseError> { + let url = format!( + "{}/api/workspace/{}/database/{}/fields", + 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() + } + pub async fn list_database_row_ids_updated( &self, workspace_id: &str, diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index fb071bee..c0fc989b 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -344,12 +344,6 @@ pub struct AFDatabase { pub views: Vec, } -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct AFDatabaseField { - pub name: String, - pub field_type: String, -} - #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct AFDatabaseRow { pub id: String, @@ -360,3 +354,11 @@ pub struct AFDatabaseRowDetail { pub id: String, pub cells: HashMap>, } + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +pub struct AFDatabaseField { + pub id: String, + pub name: String, + pub field_type: String, + pub is_primary: bool, +} diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 03229f5c..4484c166 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -261,6 +261,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}/fields") + .route(web::get().to(get_database_fields_handler)), + ) .service( web::resource("/{workspace_id}/database/{database_id}/row/updated") .route(web::get().to(list_database_row_id_updated_handler)), @@ -1906,6 +1910,28 @@ async fn list_database_row_id_handler( Ok(Json(AppResponse::Ok().with_data(db_rows))) } +async fn get_database_fields_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_fields = biz::collab::ops::get_database_fields( + &state.collab_access_control_storage, + &workspace_id, + &db_id, + ) + .await?; + + Ok(Json(AppResponse::Ok().with_data(db_fields))) +} + async fn list_database_row_id_updated_handler( user_uuid: UserUuid, path_param: web::Path<(String, String)>, diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index c9140706..0f5512cf 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -25,6 +25,7 @@ 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::DatabaseRowUpdatedItem; @@ -444,26 +445,8 @@ pub async fn list_database_row_ids( workspace_uuid_str: &str, database_uuid_str: &str, ) -> 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, - )) - })?; - + let (db_collab, db_body) = + get_database_body(collab_storage, workspace_uuid_str, database_uuid_str).await?; // get any view_id let txn = db_collab.transact(); let iid = db_body.get_inline_view_id(&txn); @@ -483,6 +466,27 @@ pub async fn list_database_row_ids( Ok(db_rows) } +pub async fn get_database_fields( + collab_storage: &CollabAccessControlStorage, + workspace_uuid_str: &str, + database_uuid_str: &str, +) -> Result, AppError> { + let (db_collab, db_body) = + get_database_body(collab_storage, workspace_uuid_str, database_uuid_str).await?; + + let all_fields = db_body.fields.get_all_fields(&db_collab.transact()); + let mut acc = Vec::with_capacity(all_fields.len()); + for field in all_fields { + acc.push(AFDatabaseField { + id: field.id, + name: field.name, + field_type: format!("{:?}", FieldType::from(field.field_type)), + is_primary: field.is_primary, + }); + } + Ok(acc) +} + pub async fn list_database_row_ids_updated( collab_storage: &CollabAccessControlStorage, pg_pool: &PgPool, @@ -736,3 +740,30 @@ fn add_to_selection_from_type_options( } }; } + +async fn get_database_body( + collab_storage: &CollabAccessControlStorage, + workspace_uuid_str: &str, + database_uuid_str: &str, +) -> Result<(Collab, DatabaseBody), 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, + )) + })?; + Ok((db_collab, db_body)) +} diff --git a/tests/workspace/workspace_crud.rs b/tests/workspace/workspace_crud.rs index 4f23afa0..91dbe50d 100644 --- a/tests/workspace/workspace_crud.rs +++ b/tests/workspace/workspace_crud.rs @@ -1,6 +1,7 @@ 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; @@ -24,6 +25,48 @@ async fn workspace_list_database() { .unwrap(); assert_eq!(db_row_ids.len(), 5, "{:?}", db_row_ids); } + { + let mut db_fields = c + .get_database_fields(&workspace_id, &todos_db.id) + .await + .unwrap(); + db_fields.sort(); + + let expected = vec![ + AFDatabaseField { + id: "3AE6iK".to_string(), + name: "Last modified".to_string(), + field_type: "LastEditedTime".to_string(), + is_primary: false, + }, + AFDatabaseField { + id: "KinVda".to_string(), + name: "Tasks".to_string(), + field_type: "Checklist".to_string(), + is_primary: false, + }, + AFDatabaseField { + id: "SqwRg1".to_string(), + name: "Status".to_string(), + field_type: "SingleSelect".to_string(), + is_primary: false, + }, + AFDatabaseField { + id: "phVRgL".to_string(), + name: "Description".to_string(), + field_type: "RichText".to_string(), + is_primary: true, + }, + AFDatabaseField { + id: "wdX8DG".to_string(), + name: "Multiselect".to_string(), + field_type: "MultiSelect".to_string(), + is_primary: false, + }, + ]; + + assert_eq!(db_fields, expected, "{:#?}", db_fields); + } { let db_row_ids = c .list_database_row_ids_updated(&workspace_id, &todos_db.id, None) @@ -31,7 +74,6 @@ async fn workspace_list_database() { .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) From ed83fba2420d78d1a56c04d44a121e6384eadb84 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Wed, 27 Nov 2024 17:24:14 +0800 Subject: [PATCH 2/3] feat: add type options --- libs/shared-entity/src/dto/workspace_dto.rs | 3 +- src/biz/collab/ops.rs | 32 +++++++- tests/workspace/workspace_crud.rs | 87 +++++++++++++++++---- 3 files changed, 104 insertions(+), 18 deletions(-) diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index fd335e73..84ca4708 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -368,10 +368,11 @@ pub struct AFDatabaseRowDetail { pub cells: HashMap>, } -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct AFDatabaseField { pub id: String, pub name: String, pub field_type: String, + pub type_option: HashMap, pub is_primary: bool, } diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index 171d9063..3a41b80d 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -503,10 +503,12 @@ pub async fn get_database_fields( let all_fields = db_body.fields.get_all_fields(&db_collab.transact()); let mut acc = Vec::with_capacity(all_fields.len()); for field in all_fields { + let field_type = FieldType::from(field.field_type); acc.push(AFDatabaseField { id: field.id, name: field.name, - field_type: format!("{:?}", FieldType::from(field.field_type)), + field_type: format!("{:?}", field_type), + type_option: type_options_serde(&field.type_options, &field_type), is_primary: field.is_primary, }); } @@ -805,3 +807,31 @@ pub fn collab_from_doc_state(doc_state: Vec, object_id: &str) -> Result HashMap { + let type_option = match type_options.get(&field_type.type_id()) { + Some(type_option) => type_option, + None => return HashMap::new(), + }; + + let mut result = HashMap::with_capacity(type_option.len()); + for (key, value) in type_option { + match field_type { + FieldType::SingleSelect | FieldType::MultiSelect => { + if let yrs::Any::String(arc_str) = value { + if let Ok(serde_value) = serde_json::from_str::(&arc_str) { + result.insert(key.clone(), serde_value); + } + } + }, + _ => { + result.insert(key.clone(), serde_json::to_value(value).unwrap_or_default()); + }, + } + } + + result +} diff --git a/tests/workspace/workspace_crud.rs b/tests/workspace/workspace_crud.rs index 91dbe50d..c094a7a8 100644 --- a/tests/workspace/workspace_crud.rs +++ b/tests/workspace/workspace_crud.rs @@ -1,6 +1,9 @@ +use std::collections::HashMap; + use client_api_test::generate_unique_registered_user_client; use collab_entity::CollabType; use database_entity::dto::QueryCollabParams; +use serde_json::json; use shared_entity::dto::workspace_dto::AFDatabaseField; use shared_entity::dto::workspace_dto::CreateWorkspaceParam; use shared_entity::dto::workspace_dto::PatchWorkspaceParam; @@ -30,42 +33,94 @@ async fn workspace_list_database() { .get_database_fields(&workspace_id, &todos_db.id) .await .unwrap(); - db_fields.sort(); + // convert to hashset to check for equeality + let actual = db_fields.sort_by(|a, b| a.id.cmp(&b.id)); let expected = vec![ AFDatabaseField { - id: "3AE6iK".to_string(), - name: "Last modified".to_string(), - field_type: "LastEditedTime".to_string(), - is_primary: false, - }, - AFDatabaseField { - id: "KinVda".to_string(), - name: "Tasks".to_string(), - field_type: "Checklist".to_string(), + id: "wdX8DG".to_string(), + name: "Multiselect".to_string(), + field_type: "MultiSelect".to_string(), + type_option: { + let mut options = HashMap::new(); + options.insert( + "content".to_string(), + json!({ + "disable_color": false, + "options": [ + {"color": "Purple", "id": "4PDn", "name": "get things done"}, + {"color": "Blue", "id": "Bpyg", "name": "self-host"}, + {"color": "Aqua", "id": "GOQj", "name": "open source"}, + {"color": "Green", "id": "BD-T", "name": "looks great"}, + {"color": "Lime", "id": "6UxM", "name": "fast"}, + {"color": "Yellow", "id": "g2Uq", "name": "Claude 3"}, + {"color": "Orange", "id": "Tt-J", "name": "GPT-4o"}, + {"color": "LightPink", "id": "5QDY", "name": "Q&A"}, + {"color": "Pink", "id": "XYUx", "name": "news"}, + {"color": "Purple", "id": "hoZx", "name": "social"}, + ], + }), + ); + options + }, is_primary: false, }, AFDatabaseField { id: "SqwRg1".to_string(), name: "Status".to_string(), field_type: "SingleSelect".to_string(), + type_option: { + let mut options = HashMap::new(); + options.insert( + "content".to_string(), + json!({ + "disable_color": false, + "options": [ + {"color": "Purple", "id": "CEZD", "name": "To Do"}, + {"color": "Orange", "id": "TznH", "name": "Doing"}, + {"color": "Yellow", "id": "__n6", "name": "✅ Done"}, + ], + }), + ); + options + }, is_primary: false, }, AFDatabaseField { id: "phVRgL".to_string(), name: "Description".to_string(), field_type: "RichText".to_string(), + type_option: { + let mut options = HashMap::new(); + options.insert("data".to_string(), json!("")); + options + }, is_primary: true, }, AFDatabaseField { - id: "wdX8DG".to_string(), - name: "Multiselect".to_string(), - field_type: "MultiSelect".to_string(), + id: "KinVda".to_string(), + name: "Tasks".to_string(), + field_type: "Checklist".to_string(), + type_option: HashMap::new(), is_primary: false, }, - ]; - - assert_eq!(db_fields, expected, "{:#?}", db_fields); + AFDatabaseField { + id: "3AE6iK".to_string(), + name: "Last modified".to_string(), + field_type: "LastEditedTime".to_string(), + type_option: { + let mut options = HashMap::new(); + options.insert("date_format".to_string(), json!(3)); + options.insert("field_type".to_string(), json!(8)); + options.insert("include_time".to_string(), json!(true)); + options.insert("time_format".to_string(), json!(1)); + options + }, + is_primary: false, + }, + ] + .sort_by(|a, b| a.id.cmp(&b.id)); + assert_eq!(actual, expected, "{:#?}", db_fields); } { let db_row_ids = c From 50586b1ea7ce6331f7e1b737900d24f16011f72e Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Wed, 27 Nov 2024 18:46:09 +0800 Subject: [PATCH 3/3] fix: filter database that is in trash --- src/biz/collab/ops.rs | 30 ++++++++++++++++++------------ tests/workspace/workspace_crud.rs | 10 +++++----- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index 26b1d74e..a4639782 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -447,20 +447,26 @@ pub async fn list_database( ) .await?; + let trash = folder + .get_all_trash_sections() + .into_iter() + .map(|s| s.id) + .collect::>(); + let mut af_databases = Vec::with_capacity(db_metas.len()); for db_meta in db_metas { let id = db_meta.database_id; - let views: Vec = db_meta - .linked_views - .into_iter() - .map(|view_id| { - folder - .get_view(&view_id) - .map(|view| to_dto_folder_view_miminal(&view)) - .unwrap_or_default() - }) - .collect(); - af_databases.push(AFDatabase { id, views }); + let mut views: Vec = Vec::new(); + for linked_view_id in db_meta.linked_views { + if !trash.contains(&linked_view_id) { + if let Some(folder_view) = folder.get_view(&linked_view_id) { + views.push(to_dto_folder_view_miminal(&folder_view)); + }; + } + } + if !views.is_empty() { + af_databases.push(AFDatabase { id, views }); + } } Ok(af_databases) @@ -822,7 +828,7 @@ fn type_options_serde( match field_type { FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Media => { if let yrs::Any::String(arc_str) = value { - if let Ok(serde_value) = serde_json::from_str::(&arc_str) { + if let Ok(serde_value) = serde_json::from_str::(arc_str) { result.insert(key.clone(), serde_value); } } diff --git a/tests/workspace/workspace_crud.rs b/tests/workspace/workspace_crud.rs index c094a7a8..822bc85b 100644 --- a/tests/workspace/workspace_crud.rs +++ b/tests/workspace/workspace_crud.rs @@ -35,8 +35,8 @@ async fn workspace_list_database() { .unwrap(); // convert to hashset to check for equeality - let actual = db_fields.sort_by(|a, b| a.id.cmp(&b.id)); - let expected = vec![ + db_fields.sort_by(|a, b| a.id.cmp(&b.id)); + let mut expected = vec![ AFDatabaseField { id: "wdX8DG".to_string(), name: "Multiselect".to_string(), @@ -118,9 +118,9 @@ async fn workspace_list_database() { }, is_primary: false, }, - ] - .sort_by(|a, b| a.id.cmp(&b.id)); - assert_eq!(actual, expected, "{:#?}", db_fields); + ]; + expected.sort_by(|a, b| a.id.cmp(&b.id)); + assert_eq!(db_fields, expected, "{:#?}", db_fields); } { let db_row_ids = c