From b7de5e9059953c081ce7dea926ccb1044210fd53 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Thu, 28 Nov 2024 15:03:33 +0800 Subject: [PATCH 01/28] feat: insert database row into selected database --- src/api/workspace.rs | 27 +++++++++++- src/biz/collab/ops.rs | 99 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 4484c1664..dc11e636e 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -259,7 +259,8 @@ pub fn workspace_scope() -> Scope { .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)), + .route(web::get().to(list_database_row_id_handler)) + .route(web::post().to(post_database_row_handler)), ) .service( web::resource("/{workspace_id}/database/{database_id}/fields") @@ -1910,6 +1911,30 @@ async fn list_database_row_id_handler( Ok(Json(AppResponse::Ok().with_data(db_rows))) } +async fn post_database_row_handler( + user_uuid: UserUuid, + path_param: web::Path<(String, String)>, + state: Data, + data: Json, +) -> 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::Write) + .await?; + + biz::collab::ops::insert_database_row( + &state.collab_access_control_storage, + &workspace_id, + &db_id, + uid, + data.into_inner(), + ) + .await?; + Ok(Json(AppResponse::Ok())) +} + async fn get_database_fields_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 07e11d33f..112f1b5c3 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -11,7 +11,11 @@ use collab_database::database::DatabaseBody; use collab_database::entity::FieldType; use collab_database::fields::Field; use collab_database::fields::TypeOptions; +use collab_database::rows::CreateRowParams; +use collab_database::rows::DatabaseRowBody; +use collab_database::rows::Row; use collab_database::rows::RowDetail; +use collab_database::views::OrderObjectPosition; use collab_database::workspace_database::NoPersistenceDatabaseCollabService; use collab_database::workspace_database::WorkspaceDatabase; use collab_database::workspace_database::WorkspaceDatabaseBody; @@ -24,6 +28,7 @@ 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::CollabParams; use database_entity::dto::QueryCollabResult; use database_entity::dto::{QueryCollab, QueryCollabParams}; use shared_entity::dto::workspace_dto::AFDatabase; @@ -52,6 +57,8 @@ use database_entity::dto::{ UpdateCollabMemberParams, }; +use crate::biz::workspace::ops::broadcast_update; + 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; @@ -498,6 +505,90 @@ pub async fn list_database_row_ids( Ok(db_rows) } +pub async fn insert_database_row( + collab_storage: &CollabAccessControlStorage, + workspace_uuid_str: &str, + database_uuid_str: &str, + uid: i64, + src_values: serde_json::Value, +) -> Result<(), AppError> { + let new_db_row_id = uuid::Uuid::new_v4().to_string(); + let mut new_row = Row::new(new_db_row_id.clone(), database_uuid_str); + { + // TODO: add values from row_data into new_row + _ = src_values; + } + + let mut new_db_row_collab = + Collab::new_with_origin(CollabOrigin::Empty, new_db_row_id.clone(), vec![], false); + let new_db_row_body = DatabaseRowBody::create( + new_db_row_id.clone().into(), + &mut new_db_row_collab, + new_row, + ); + let db_row_ec_v1 = encode_collab_v1_bytes(&new_db_row_collab, CollabType::DatabaseRow)?; + + // insert row + collab_storage + .queue_insert_or_update_collab( + workspace_uuid_str, + &uid, + CollabParams { + object_id: new_db_row_id.clone(), + encoded_collab_v1: db_row_ec_v1.into(), + collab_type: CollabType::Database, + embeddings: None, + }, + true, + ) + .await?; + + let (mut db_collab, db_body) = + get_database_body(collab_storage, workspace_uuid_str, database_uuid_str).await?; + + // create a new row + let db_collab_update = { + let txn = db_collab.transact_mut(); + let ts_now = chrono::Utc::now().timestamp(); + let _ = db_body + .create_row(CreateRowParams { + id: uuid::Uuid::new_v4().to_string().into(), + database_id: database_uuid_str.to_string(), + cells: new_db_row_body + .cells(&new_db_row_collab.transact()) + .unwrap_or_default(), + height: 30, + visibility: true, + row_position: OrderObjectPosition::End, + created_at: ts_now, + modified_at: ts_now, + }) + .await + .map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to create row: {:?}", e)))?; + + txn.encode_update_v1() + }; + let db_ec_v1 = encode_collab_v1_bytes(&db_collab, CollabType::Database)?; + + // insert database with new row + collab_storage + .queue_insert_or_update_collab( + workspace_uuid_str, + &uid, + CollabParams { + object_id: database_uuid_str.to_string(), + encoded_collab_v1: db_ec_v1.into(), + collab_type: CollabType::Database, + embeddings: None, + }, + true, + ) + .await?; + broadcast_update(&collab_storage, workspace_uuid_str, db_collab_update).await?; + + Ok(()) +} + pub async fn get_database_fields( collab_storage: &CollabAccessControlStorage, workspace_uuid_str: &str, @@ -841,3 +932,11 @@ fn type_options_serde( result } + +fn encode_collab_v1_bytes(collab: &Collab, collab_type: CollabType) -> Result, AppError> { + let bs = collab + .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) + .map_err(|e| AppError::Unhandled(e.to_string()))? + .encode_to_bytes()?; + Ok(bs) +} From 1600baf680dece888e3de382ceff386c3e651391 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Thu, 28 Nov 2024 15:34:41 +0800 Subject: [PATCH 02/28] feat: inserting database row --- src/api/workspace.rs | 5 ++- src/biz/collab/ops.rs | 100 ++++++++++++++++++++++++++++-------------- 2 files changed, 70 insertions(+), 35 deletions(-) diff --git a/src/api/workspace.rs b/src/api/workspace.rs index dc11e636e..fae130cea 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -12,6 +12,7 @@ use futures_util::future::try_join_all; use prost::Message as ProstMessage; use rayon::prelude::*; use sqlx::types::uuid; +use std::collections::HashMap; use std::time::Instant; use tokio_stream::StreamExt; @@ -1915,7 +1916,7 @@ async fn post_database_row_handler( user_uuid: UserUuid, path_param: web::Path<(String, String)>, state: Data, - data: Json, + cells_by_id: Json>, ) -> Result>> { let (workspace_id, db_id) = path_param.into_inner(); let uid = state.user_cache.get_user_uid(&user_uuid).await?; @@ -1929,7 +1930,7 @@ async fn post_database_row_handler( &workspace_id, &db_id, uid, - data.into_inner(), + cells_by_id.into_inner(), ) .await?; Ok(Json(AppResponse::Ok())) diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index 112f1b5c3..f3c30bc41 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -11,6 +11,7 @@ use collab_database::database::DatabaseBody; use collab_database::entity::FieldType; use collab_database::fields::Field; use collab_database::fields::TypeOptions; +use collab_database::rows::Cell; use collab_database::rows::CreateRowParams; use collab_database::rows::DatabaseRowBody; use collab_database::rows::Row; @@ -505,18 +506,72 @@ pub async fn list_database_row_ids( Ok(db_rows) } +fn new_cell_from_value(cell_value: serde_json::Value, field: &Field) -> Cell { + // Based on the type of the field, handle each value differently + // This should be as forgiving/generic/all-purpose as much as possible + // to support different use cases. + let field_type = FieldType::from(field.field_type); + match field_type { + FieldType::RichText => { + // + todo!() + }, + FieldType::Number => todo!(), + FieldType::DateTime => todo!(), + FieldType::SingleSelect => todo!(), + FieldType::MultiSelect => todo!(), + FieldType::Checkbox => todo!(), + FieldType::URL => todo!(), + FieldType::Checklist => todo!(), + FieldType::LastEditedTime => todo!(), + FieldType::CreatedTime => todo!(), + FieldType::Relation => todo!(), + FieldType::Summary => todo!(), + FieldType::Translate => todo!(), + FieldType::Time => todo!(), + FieldType::Media => todo!(), + } +} + pub async fn insert_database_row( collab_storage: &CollabAccessControlStorage, workspace_uuid_str: &str, database_uuid_str: &str, uid: i64, - src_values: serde_json::Value, + cell_value_by_id: HashMap, ) -> Result<(), AppError> { + // get database types and type options + let (mut db_collab, db_body) = + get_database_body(collab_storage, workspace_uuid_str, database_uuid_str).await?; + + let field_by_id = db_body + .fields + .get_all_fields(&db_collab.transact()) + .into_iter() + .fold(HashMap::new(), |mut acc, field| { + acc.insert(field.id.clone(), field); + acc + }); + let new_db_row_id = uuid::Uuid::new_v4().to_string(); let mut new_row = Row::new(new_db_row_id.clone(), database_uuid_str); { - // TODO: add values from row_data into new_row - _ = src_values; + for (id, cell_value) in cell_value_by_id { + let field = match field_by_id.get(&id) { + Some(f) => f, + None => { + tracing::warn!( + "field not found: {} for database: {}", + id, + database_uuid_str + ); + continue; + }, + }; + + let new_cell: Cell = new_cell_from_value(cell_value, field); + new_row.cells.insert(id.clone(), new_cell); + } } let mut new_db_row_collab = @@ -543,9 +598,6 @@ pub async fn insert_database_row( ) .await?; - let (mut db_collab, db_body) = - get_database_body(collab_storage, workspace_uuid_str, database_uuid_str).await?; - // create a new row let db_collab_update = { let txn = db_collab.transact_mut(); @@ -638,33 +690,8 @@ pub async fn list_database_row_details( 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, - )) - })?; + let (database_collab, db_body) = + get_database_body(collab_storage, &workspace_uuid_str, &database_uuid_str).await?; // create a map of field id to field. // ensure that the field name is unique. @@ -693,6 +720,13 @@ pub async fn list_database_row_details( add_to_selection_from_field(&mut selection_name_by_id, field); } + let query_collabs: Vec = row_ids + .iter() + .map(|id| QueryCollab { + object_id: id.to_string(), + collab_type: CollabType::DatabaseRow, + }) + .collect(); let database_row_details = collab_storage .batch_get_collab(&uid, query_collabs, true) .await From fc0698d7905637cff751d89dd0955f61701562ea Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 29 Nov 2024 09:16:20 +0800 Subject: [PATCH 03/28] feat: add impl for cell --- .../appflowy-collaborate/src/group/manager.rs | 2 +- src/api/workspace.rs | 1 + src/biz/collab/mod.rs | 1 + src/biz/collab/ops.rs | 498 ++++-------------- src/biz/collab/utils.rs | 414 +++++++++++++++ src/biz/workspace/page_view.rs | 8 +- src/biz/workspace/publish_dup.rs | 4 +- tests/workspace/publish.rs | 2 +- 8 files changed, 539 insertions(+), 391 deletions(-) create mode 100644 src/biz/collab/utils.rs diff --git a/services/appflowy-collaborate/src/group/manager.rs b/services/appflowy-collaborate/src/group/manager.rs index 20b38ab0a..ebcfd1c4d 100644 --- a/services/appflowy-collaborate/src/group/manager.rs +++ b/services/appflowy-collaborate/src/group/manager.rs @@ -207,7 +207,7 @@ where ) .await?, ); - self.state.insert_group(object_id, group.clone()).await; + self.state.insert_group(object_id, group).await; Ok(()) } } diff --git a/src/api/workspace.rs b/src/api/workspace.rs index fae130cea..cb850521c 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -1927,6 +1927,7 @@ async fn post_database_row_handler( biz::collab::ops::insert_database_row( &state.collab_access_control_storage, + &state.pg_pool, &workspace_id, &db_id, uid, diff --git a/src/biz/collab/mod.rs b/src/biz/collab/mod.rs index dc877de24..8378f840f 100644 --- a/src/biz/collab/mod.rs +++ b/src/biz/collab/mod.rs @@ -1,3 +1,4 @@ pub mod folder_view; pub mod ops; pub mod publish_outline; +pub mod utils; diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index f3c30bc41..807d6e788 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -5,19 +5,13 @@ use app_error::AppError; use appflowy_collaborate::collab::storage::CollabAccessControlStorage; use chrono::DateTime; use chrono::Utc; -use collab::core::collab::DataSource; 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::Cell; use collab_database::rows::CreateRowParams; use collab_database::rows::DatabaseRowBody; use collab_database::rows::Row; use collab_database::rows::RowDetail; use collab_database::views::OrderObjectPosition; -use collab_database::workspace_database::NoPersistenceDatabaseCollabService; use collab_database::workspace_database::WorkspaceDatabase; use collab_database::workspace_database::WorkspaceDatabaseBody; use collab_entity::CollabType; @@ -30,8 +24,8 @@ 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::CollabParams; +use database_entity::dto::QueryCollab; 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; @@ -58,6 +52,7 @@ use database_entity::dto::{ UpdateCollabMemberParams, }; +use crate::biz::collab::utils::field_by_name_uniq; use crate::biz::workspace::ops::broadcast_update; use super::folder_view::collab_folder_to_folder_view; @@ -66,6 +61,16 @@ 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; +use super::utils::collab_from_doc_state; +use super::utils::convert_database_cells_human_readable; +use super::utils::encode_collab_v1_bytes; +use super::utils::field_by_id_name_uniq; +use super::utils::get_database_body; +use super::utils::get_latest_collab; +use super::utils::get_latest_collab_encoded; +use super::utils::new_cell_from_value; +use super::utils::selection_name_by_id; +use super::utils::type_options_serde; /// Create a new collab member /// If the collab member already exists, return [AppError::RecordAlreadyExists] @@ -360,46 +365,6 @@ pub async fn get_latest_collab_folder( Ok(folder) } -pub async fn get_latest_collab_encoded( - collab_storage: &CollabAccessControlStorage, - collab_origin: GetCollabOrigin, - workspace_id: &str, - oid: &str, - collab_type: CollabType, -) -> Result { - collab_storage - .get_encode_collab( - collab_origin, - QueryCollabParams { - workspace_id: workspace_id.to_string(), - inner: QueryCollab { - object_id: oid.to_string(), - collab_type, - }, - }, - true, - ) - .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, @@ -506,138 +471,133 @@ pub async fn list_database_row_ids( Ok(db_rows) } -fn new_cell_from_value(cell_value: serde_json::Value, field: &Field) -> Cell { - // Based on the type of the field, handle each value differently - // This should be as forgiving/generic/all-purpose as much as possible - // to support different use cases. - let field_type = FieldType::from(field.field_type); - match field_type { - FieldType::RichText => { - // - todo!() - }, - FieldType::Number => todo!(), - FieldType::DateTime => todo!(), - FieldType::SingleSelect => todo!(), - FieldType::MultiSelect => todo!(), - FieldType::Checkbox => todo!(), - FieldType::URL => todo!(), - FieldType::Checklist => todo!(), - FieldType::LastEditedTime => todo!(), - FieldType::CreatedTime => todo!(), - FieldType::Relation => todo!(), - FieldType::Summary => todo!(), - FieldType::Translate => todo!(), - FieldType::Time => todo!(), - FieldType::Media => todo!(), - } -} - pub async fn insert_database_row( collab_storage: &CollabAccessControlStorage, + pg_pool: &PgPool, workspace_uuid_str: &str, database_uuid_str: &str, uid: i64, cell_value_by_id: HashMap, ) -> Result<(), AppError> { + let mut db_txn = pg_pool.begin().await?; + // get database types and type options let (mut db_collab, db_body) = get_database_body(collab_storage, workspace_uuid_str, database_uuid_str).await?; - let field_by_id = db_body - .fields - .get_all_fields(&db_collab.transact()) - .into_iter() - .fold(HashMap::new(), |mut acc, field| { - acc.insert(field.id.clone(), field); - acc - }); + let all_fields = db_body.fields.get_all_fields(&db_collab.transact()); + let field_by_id = all_fields.iter().fold(HashMap::new(), |mut acc, field| { + acc.insert(field.id.clone(), field.clone()); + acc + }); + let field_by_name = field_by_name_uniq(all_fields); let new_db_row_id = uuid::Uuid::new_v4().to_string(); - let mut new_row = Row::new(new_db_row_id.clone(), database_uuid_str); - { - for (id, cell_value) in cell_value_by_id { + + let mut new_db_row_collab = + Collab::new_with_origin(CollabOrigin::Empty, new_db_row_id.clone(), vec![], false); + let new_db_row_body = { + let database_body = DatabaseRowBody::create( + new_db_row_id.clone().into(), + &mut new_db_row_collab, + Row::empty(new_db_row_id.clone().into(), database_uuid_str), + ); + let mut txn = new_db_row_collab.transact_mut(); + for (id, serde_val) in cell_value_by_id { let field = match field_by_id.get(&id) { Some(f) => f, - None => { - tracing::warn!( - "field not found: {} for database: {}", - id, - database_uuid_str - ); - continue; + // try use field name if id not found + None => match field_by_name.get(&id) { + Some(f) => f, + None => { + tracing::warn!( + "field not found: {} for database: {}", + id, + database_uuid_str + ); + continue; + }, }, }; - - let new_cell: Cell = new_cell_from_value(cell_value, field); - new_row.cells.insert(id.clone(), new_cell); + let new_cell = new_cell_from_value(serde_val, field); + if let Some(new_cell) = new_cell { + database_body.update(&mut txn, |row_update| { + row_update.update_cells(|cells_update| { + cells_update.insert_cell(&field.id, new_cell); + }); + }); + } } - } - - let mut new_db_row_collab = - Collab::new_with_origin(CollabOrigin::Empty, new_db_row_id.clone(), vec![], false); - let new_db_row_body = DatabaseRowBody::create( - new_db_row_id.clone().into(), - &mut new_db_row_collab, - new_row, - ); + database_body + }; let db_row_ec_v1 = encode_collab_v1_bytes(&new_db_row_collab, CollabType::DatabaseRow)?; // insert row collab_storage - .queue_insert_or_update_collab( + .insert_new_collab_with_transaction( workspace_uuid_str, &uid, CollabParams { object_id: new_db_row_id.clone(), encoded_collab_v1: db_row_ec_v1.into(), - collab_type: CollabType::Database, + collab_type: CollabType::DatabaseRow, embeddings: None, }, - true, + &mut db_txn, + "inserting new database row from server", ) .await?; - // create a new row + let ts_now = chrono::Utc::now().timestamp(); + let row_order = db_body + .create_row(CreateRowParams { + id: new_db_row_id.into(), + database_id: database_uuid_str.to_string(), + cells: new_db_row_body + .cells(&new_db_row_collab.transact()) + .unwrap_or_default(), + height: 30, + visibility: true, + row_position: OrderObjectPosition::End, + created_at: ts_now, + modified_at: ts_now, + }) + .await + .map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to create row: {:?}", e)))?; + + // For each database view, add the new row order let db_collab_update = { - let txn = db_collab.transact_mut(); - let ts_now = chrono::Utc::now().timestamp(); - let _ = db_body - .create_row(CreateRowParams { - id: uuid::Uuid::new_v4().to_string().into(), - database_id: database_uuid_str.to_string(), - cells: new_db_row_body - .cells(&new_db_row_collab.transact()) - .unwrap_or_default(), - height: 30, - visibility: true, - row_position: OrderObjectPosition::End, - created_at: ts_now, - modified_at: ts_now, - }) - .await - .map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to create row: {:?}", e)))?; + let mut txn = db_collab.transact_mut(); + let mut db_views = db_body.views.get_all_views(&txn); + for db_view in db_views.iter_mut() { + db_view.row_orders.push(row_order.clone()); + } + db_body.views.clear(&mut txn); + for view in db_views { + db_body.views.insert_view(&mut txn, view); + } txn.encode_update_v1() }; - let db_ec_v1 = encode_collab_v1_bytes(&db_collab, CollabType::Database)?; + let updated_db_collab = encode_collab_v1_bytes(&db_collab, CollabType::Database)?; - // insert database with new row collab_storage - .queue_insert_or_update_collab( + .insert_new_collab_with_transaction( workspace_uuid_str, &uid, CollabParams { object_id: database_uuid_str.to_string(), - encoded_collab_v1: db_ec_v1.into(), + encoded_collab_v1: updated_db_collab.into(), collab_type: CollabType::Database, embeddings: None, }, - true, + &mut db_txn, + "inserting updated database from server", ) .await?; - broadcast_update(&collab_storage, workspace_uuid_str, db_collab_update).await?; + db_txn.commit().await?; + broadcast_update(collab_storage, database_uuid_str, db_collab_update).await?; Ok(()) } @@ -693,32 +653,9 @@ pub async fn list_database_row_details( let (database_collab, db_body) = get_database_body(collab_storage, &workspace_uuid_str, &database_uuid_str).await?; - // 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.clone_from(&new_name); - } - uniq_name_set.insert(field.name.clone()); - field_by_id.insert(field.id.clone(), field); - } - 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 all_fields = db_body.fields.get_all_fields(&database_collab.transact()); + let selection_name_by_id = selection_name_by_id(&all_fields); + let field_by_name_uniq = field_by_id_name_uniq(all_fields); let query_collabs: Vec = row_ids .iter() @@ -733,13 +670,31 @@ pub async fn list_database_row_details( .into_iter() .flat_map(|(id, result)| match result { QueryCollabResult::Success { encode_collab_v1 } => { - let ec = EncodedCollab::decode_from_bytes(&encode_collab_v1).unwrap(); + let ec = match EncodedCollab::decode_from_bytes(&encode_collab_v1) { + Ok(ec) => ec, + Err(err) => { + tracing::error!("Failed to decode encoded collab: {:?}", err); + return None; + }, + }; let collab = - Collab::new_with_source(CollabOrigin::Server, &id, ec.into(), vec![], false).unwrap(); - let row_detail = RowDetail::from_collab(&collab).unwrap(); + match Collab::new_with_source(CollabOrigin::Server, &id, ec.into(), vec![], false) { + Ok(collab) => collab, + Err(err) => { + tracing::error!("Failed to create collab: {:?}", err); + return None; + }, + }; + let row_detail = match RowDetail::from_collab(&collab) { + Some(row_detail) => row_detail, + None => { + tracing::error!("Failed to get row detail from collab: {:?}", collab); + return None; + }, + }; let cells = convert_database_cells_human_readable( row_detail.row.cells, - &field_by_id, + &field_by_name_uniq, &selection_name_by_id, ); Some(AFDatabaseRowDetail { id, cells }) @@ -753,224 +708,3 @@ pub async fn list_database_row_details( Ok(database_row_details) } - -fn convert_database_cells_human_readable( - db_cells: HashMap>, - field_by_id: &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 { - 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 field_type = FieldType::from(field.field_type); - - let mut human_readable_cell: HashMap = - HashMap::with_capacity(cell.len()); - for (key, value) in cell { - 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() - .into(), - Err(err) => { - tracing::error!("Failed to cast timestamp: {:?}", err); - serde_json::Value::Null - }, - }, - "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(), - } - }, - _ => serde_json::to_value(value).unwrap_or_default(), - }; - 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()); - } - } - } - } - } - } - }; -} - -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)) -} - -pub fn collab_from_doc_state(doc_state: Vec, object_id: &str) -> Result { - let collab = Collab::new_with_source( - CollabOrigin::Server, - object_id, - DataSource::DocStateV1(doc_state), - vec![], - false, - ) - .map_err(|e| AppError::Unhandled(e.to_string()))?; - Ok(collab) -} - -fn type_options_serde( - type_options: &TypeOptions, - field_type: &FieldType, -) -> 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 | FieldType::Media => { - 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 -} - -fn encode_collab_v1_bytes(collab: &Collab, collab_type: CollabType) -> Result, AppError> { - let bs = collab - .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) - .map_err(|e| AppError::Unhandled(e.to_string()))? - .encode_to_bytes()?; - Ok(bs) -} diff --git a/src/biz/collab/utils.rs b/src/biz/collab/utils.rs new file mode 100644 index 000000000..a0fd91fec --- /dev/null +++ b/src/biz/collab/utils.rs @@ -0,0 +1,414 @@ +use app_error::AppError; +use appflowy_collaborate::collab::storage::CollabAccessControlStorage; +use collab::core::collab::DataSource; +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::new_cell_builder; +use collab_database::rows::Cell; +use collab_database::template::entity::CELL_DATA; +use collab_database::workspace_database::NoPersistenceDatabaseCollabService; +use collab_entity::CollabType; +use collab_entity::EncodedCollab; +use collab_folder::CollabOrigin; +use database::collab::CollabStorage; +use database::collab::GetCollabOrigin; +use database_entity::dto::QueryCollab; +use database_entity::dto::QueryCollabParams; +use std::collections::HashMap; +use std::collections::HashSet; +use std::sync::Arc; + +pub fn convert_database_cells_human_readable( + db_cells: HashMap>, + field_by_id: &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 { + let field = match field_by_id.get(&field_id) { + Some(field) => field, + None => { + tracing::error!("Failed to get field by id: {}, cell: {:?}", field_id, cell); + continue; + }, + }; + let field_type = FieldType::from(field.field_type); + + let mut human_readable_cell: HashMap = + HashMap::with_capacity(cell.len()); + for (key, value) in cell { + 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() + .into(), + Err(err) => { + tracing::error!("Failed to cast timestamp: {:?}", err); + serde_json::Value::Null + }, + }, + "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(), + } + }, + _ => serde_json::to_value(value).unwrap_or_default(), + }; + human_readable_cell.insert(key, serde_value); + } + human_readable_records.insert(field.name.clone(), human_readable_cell); + } + human_readable_records +} + +pub fn selection_name_by_id(fields: &[Field]) -> HashMap { + let mut selection_name_by_id: HashMap = HashMap::new(); + for field in fields { + add_to_selection_from_field(&mut selection_name_by_id, field); + } + selection_name_by_id +} + +/// create a map of field name to field +/// if the field name is repeated, it will be appended with the field id, +pub fn field_by_name_uniq(mut fields: Vec) -> HashMap { + fields.sort_by_key(|a| a.id.clone()); + let mut uniq_name_set: HashSet = HashSet::with_capacity(fields.len()); + let mut field_by_name: HashMap = HashMap::with_capacity(fields.len()); + + for field in fields { + // if the name already exists, append the field id to the name + let name = if uniq_name_set.contains(&field.name) { + format!("{}-{}", field.name, field.id) + } else { + field.name.clone() + }; + uniq_name_set.insert(name.clone()); + field_by_name.insert(name, field); + } + field_by_name +} + +/// create a map of field id to field name, and 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 +pub fn field_by_id_name_uniq(mut fields: Vec) -> HashMap { + fields.sort_by_key(|a| a.id.clone()); + let mut uniq_name_set: HashSet = HashSet::with_capacity(fields.len()); + let mut field_by_id: HashMap = HashMap::with_capacity(fields.len()); + + for mut field in 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.clone_from(&new_name); + } + uniq_name_set.insert(field.name.clone()); + field_by_id.insert(field.id.clone(), field); + } + field_by_id +} + +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()); + } + } + } + } + } + } + }; +} + +pub fn type_options_serde( + type_options: &TypeOptions, + field_type: &FieldType, +) -> 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 | FieldType::Media => { + 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 +} + +pub fn collab_from_doc_state(doc_state: Vec, object_id: &str) -> Result { + let collab = Collab::new_with_source( + CollabOrigin::Server, + object_id, + DataSource::DocStateV1(doc_state), + vec![], + false, + ) + .map_err(|e| AppError::Unhandled(e.to_string()))?; + Ok(collab) +} + +pub 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)) +} + +pub fn encode_collab_v1_bytes( + collab: &Collab, + collab_type: CollabType, +) -> Result, AppError> { + let bs = collab + .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) + .map_err(|e| AppError::Unhandled(e.to_string()))? + .encode_to_bytes()?; + Ok(bs) +} + +pub async fn get_latest_collab_encoded( + collab_storage: &CollabAccessControlStorage, + collab_origin: GetCollabOrigin, + workspace_id: &str, + oid: &str, + collab_type: CollabType, +) -> Result { + collab_storage + .get_encode_collab( + collab_origin, + QueryCollabParams { + workspace_id: workspace_id.to_string(), + inner: QueryCollab { + object_id: oid.to_string(), + collab_type, + }, + }, + true, + ) + .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 fn new_cell_from_value(cell_value: serde_json::Value, field: &Field) -> Option { + let field_type = FieldType::from(field.field_type); + let cell_value: Option = match field_type { + FieldType::Relation | FieldType::Media => { + if let serde_json::Value::Array(arr) = cell_value { + let mut acc = Vec::with_capacity(arr.len()); + for v in arr { + if let serde_json::Value::String(value_str) = v { + acc.push(yrs::any::Any::String(value_str.into())); + } + } + Some(yrs::any::Any::Array(acc.into())) + } else { + tracing::warn!("invalid media/relation value: {:?}", cell_value); + None + } + }, + FieldType::RichText => { + if let serde_json::Value::String(value_str) = cell_value { + Some(yrs::any::Any::String(value_str.into())) + } else { + None + } + }, + FieldType::Checkbox => { + let is_yes = match cell_value { + serde_json::Value::Null => false, + serde_json::Value::Bool(b) => b, + serde_json::Value::Number(n) => n.is_i64() && n.as_i64().unwrap() >= 1, + serde_json::Value::String(s) => s.to_lowercase() == "yes", + _ => { + tracing::warn!("invalid checklist value: {:?}", cell_value); + false + }, + }; + if is_yes { + Some(yrs::any::Any::String("Yes".into())) + } else { + None + } + }, + FieldType::Number => match cell_value { + serde_json::Value::Number(n) => Some(yrs::any::Any::String(n.to_string().into())), + serde_json::Value::String(s) => Some(yrs::any::Any::String(s.into())), + _ => { + tracing::warn!("invalid number value: {:?}", cell_value); + None + }, + }, + FieldType::SingleSelect + | FieldType::MultiSelect + | FieldType::Checklist + | FieldType::URL + | FieldType::Summary + | FieldType::Translate + | FieldType::DateTime => match serde_json::to_string(&cell_value) { + Ok(s) => Some(yrs::any::Any::String(s.into())), + Err(err) => { + tracing::error!("Failed to serialize cell value: {:?}", err); + None + }, + }, + FieldType::LastEditedTime | FieldType::CreatedTime | FieldType::Time => { + // should not be possible + tracing::error!( + "attempt to insert into invalid field: {:?}, value: {}", + field_type, + cell_value + ); + None + }, + }; + + cell_value.map(|v| { + let mut new_cell = new_cell_builder(field_type); + new_cell.insert(CELL_DATA.to_string(), v); + new_cell + }) +} diff --git a/src/biz/workspace/page_view.rs b/src/biz/workspace/page_view.rs index 795d0d89d..ed03dcafb 100644 --- a/src/biz/workspace/page_view.rs +++ b/src/biz/workspace/page_view.rs @@ -46,11 +46,9 @@ use crate::biz::collab::folder_view::{ parse_extra_field_as_json, to_dto_view_icon, to_dto_view_layout, to_folder_view_icon, to_space_permission, }; -use crate::biz::collab::ops::{collab_from_doc_state, get_latest_workspace_database}; -use crate::biz::collab::{ - folder_view::view_is_space, - ops::{get_latest_collab_encoded, get_latest_collab_folder}, -}; +use crate::biz::collab::ops::get_latest_workspace_database; +use crate::biz::collab::utils::{collab_from_doc_state, get_latest_collab_encoded}; +use crate::biz::collab::{folder_view::view_is_space, ops::get_latest_collab_folder}; use super::ops::broadcast_update; diff --git a/src/biz/workspace/publish_dup.rs b/src/biz/workspace/publish_dup.rs index 6f5ee329b..a4dd70faf 100644 --- a/src/biz/workspace/publish_dup.rs +++ b/src/biz/workspace/publish_dup.rs @@ -43,8 +43,8 @@ use yrs::{Map, MapRef}; use crate::biz::collab::folder_view::to_folder_view_icon; use crate::biz::collab::folder_view::to_folder_view_layout; -use crate::biz::collab::ops::collab_from_doc_state; -use crate::biz::collab::ops::get_latest_collab_encoded; +use crate::biz::collab::utils::collab_from_doc_state; +use crate::biz::collab::utils::get_latest_collab_encoded; use super::ops::broadcast_update; diff --git a/tests/workspace/publish.rs b/tests/workspace/publish.rs index a48a3c486..7fa764df4 100644 --- a/tests/workspace/publish.rs +++ b/tests/workspace/publish.rs @@ -1,6 +1,6 @@ use app_error::ErrorCode; use appflowy_cloud::biz::collab::folder_view::collab_folder_to_folder_view; -use appflowy_cloud::biz::collab::ops::collab_from_doc_state; +use appflowy_cloud::biz::collab::utils::collab_from_doc_state; use client_api::entity::{ AFRole, GlobalComment, PatchPublishedCollab, PublishCollabItem, PublishCollabMetadata, PublishInfoMeta, From e2d68ae93bf4f0e947568af387852cf570b32f7d Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Sat, 30 Nov 2024 22:30:09 +0800 Subject: [PATCH 04/28] feat: insert row selection and mulitselection --- src/biz/collab/utils.rs | 146 ++++++++++++++++++++++++++++------------ 1 file changed, 104 insertions(+), 42 deletions(-) diff --git a/src/biz/collab/utils.rs b/src/biz/collab/utils.rs index a0fd91fec..0b3a15018 100644 --- a/src/biz/collab/utils.rs +++ b/src/biz/collab/utils.rs @@ -132,11 +132,39 @@ pub fn convert_database_cells_human_readable( pub fn selection_name_by_id(fields: &[Field]) -> HashMap { let mut selection_name_by_id: HashMap = HashMap::new(); for field in fields { - add_to_selection_from_field(&mut selection_name_by_id, field); + let field_type = FieldType::from(field.field_type); + match field_type { + FieldType::SingleSelect | FieldType::MultiSelect => { + selection_id_name_pairs(&field.type_options, &field_type) + .into_iter() + .for_each(|(id, name)| { + selection_name_by_id.insert(id, name); + }) + }, + _ => (), + } } selection_name_by_id } +pub fn selection_id_by_name(fields: &[Field]) -> HashMap { + let mut selection_id_by_name: HashMap = HashMap::new(); + for field in fields { + let field_type = FieldType::from(field.field_type); + match field_type { + FieldType::SingleSelect | FieldType::MultiSelect => { + selection_id_name_pairs(&field.type_options, &field_type) + .into_iter() + .for_each(|(id, name)| { + selection_id_by_name.insert(name, id); + }) + }, + _ => (), + } + } + selection_id_by_name +} + /// create a map of field name to field /// if the field name is repeated, it will be appended with the field id, pub fn field_by_name_uniq(mut fields: Vec) -> HashMap { @@ -177,44 +205,6 @@ pub fn field_by_id_name_uniq(mut fields: Vec) -> HashMap { field_by_id } -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()); - } - } - } - } - } - } - }; -} - pub fn type_options_serde( type_options: &TypeOptions, field_type: &FieldType, @@ -382,9 +372,53 @@ pub fn new_cell_from_value(cell_value: serde_json::Value, field: &Field) -> Opti None }, }, - FieldType::SingleSelect - | FieldType::MultiSelect - | FieldType::Checklist + FieldType::SingleSelect => match cell_value { + serde_json::Value::String(s) => { + let selection_name_by_id = selection_name_by_id(std::slice::from_ref(field)); + match selection_name_by_id.get(&s) { + Some(_name) => Some(yrs::any::Any::String(s.into())), + None => { + let selection_id_by_name = selection_id_by_name(std::slice::from_ref(field)); + match selection_id_by_name.get(&s) { + Some(id) => Some(yrs::any::Any::String(id.as_str().into())), + None => { + tracing::warn!("invalid single select value for field: {:?}", field.name); + None + }, + } + }, + } + }, + _ => { + tracing::warn!("invalid single value: {:?}", cell_value); + None + }, + }, + FieldType::MultiSelect => { + let selection_name_by_id = selection_name_by_id(std::slice::from_ref(field)); + let selection_id_by_name = selection_id_by_name(std::slice::from_ref(field)); + let input_ids: Vec<&str> = match cell_value { + serde_json::Value::String(ref s) => s.split(',').collect(), + serde_json::Value::Array(ref arr) => arr.iter().flat_map(|v| v.as_str()).collect(), + _ => { + tracing::warn!("invalid multi select value: {:?}", cell_value); + vec![] + }, + }; + + let mut sel_ids = Vec::with_capacity(input_ids.len()); + for input_id in input_ids { + if let Some(_name) = selection_name_by_id.get(input_id) { + sel_ids.push(input_id.to_owned()); + } else if let Some(id) = selection_id_by_name.get(input_id) { + sel_ids.push(id.to_owned()); + } else { + tracing::warn!("invalid multi select value: {:?}", cell_value); + } + } + yrs::any::Any::String(sel_ids.join(",").into()).into() + }, + FieldType::Checklist | FieldType::URL | FieldType::Summary | FieldType::Translate @@ -412,3 +446,31 @@ pub fn new_cell_from_value(cell_value: serde_json::Value, field: &Field) -> Opti new_cell }) } + +fn selection_id_name_pairs( + type_options: &TypeOptions, + field_type: &FieldType, +) -> Vec<(String, String)> { + 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()) { + let mut acc = Vec::with_capacity(selections.len()); + 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()), + ) { + acc.push((id.to_owned(), name.to_owned())); + } + } + } + + return acc; + } + } + } + }; + vec![] +} From 7a3edebf5fcf45a13f4b348426990aa14a5e74f9 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 2 Dec 2024 14:43:21 +0800 Subject: [PATCH 05/28] feat: support datetime field type --- src/biz/collab/utils.rs | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/biz/collab/utils.rs b/src/biz/collab/utils.rs index 0b3a15018..124ab123a 100644 --- a/src/biz/collab/utils.rs +++ b/src/biz/collab/utils.rs @@ -344,7 +344,7 @@ pub fn new_cell_from_value(cell_value: serde_json::Value, field: &Field) -> Opti if let serde_json::Value::String(value_str) = cell_value { Some(yrs::any::Any::String(value_str.into())) } else { - None + Some(yrs::any::Any::String(cell_value.to_string().into())) } }, FieldType::Checkbox => { @@ -418,17 +418,35 @@ pub fn new_cell_from_value(cell_value: serde_json::Value, field: &Field) -> Opti } yrs::any::Any::String(sel_ids.join(",").into()).into() }, - FieldType::Checklist - | FieldType::URL - | FieldType::Summary - | FieldType::Translate - | FieldType::DateTime => match serde_json::to_string(&cell_value) { - Ok(s) => Some(yrs::any::Any::String(s.into())), - Err(err) => { - tracing::error!("Failed to serialize cell value: {:?}", err); + FieldType::DateTime => match cell_value { + serde_json::Value::Number(number) => { + let int_value = number.as_i64().unwrap_or_default(); + Some(yrs::any::Any::String(int_value.to_string().into())) + }, + serde_json::Value::String(s) => match s.parse::() { + Ok(int_value) => Some(yrs::any::Any::String(int_value.to_string().into())), + Err(_err) => match chrono::DateTime::parse_from_rfc3339(&s) { + Ok(dt) => Some(yrs::any::Any::String(dt.timestamp().to_string().into())), + Err(err) => { + tracing::warn!("Failed to parse datetime string: {:?}", err); + None + }, + }, + }, + _ => { + tracing::warn!("invalid datetime value: {:?}", cell_value); None }, }, + FieldType::Checklist | FieldType::URL | FieldType::Summary | FieldType::Translate => { + match serde_json::to_string(&cell_value) { + Ok(s) => Some(yrs::any::Any::String(s.into())), + Err(err) => { + tracing::error!("Failed to serialize cell value: {:?}", err); + None + }, + } + }, FieldType::LastEditedTime | FieldType::CreatedTime | FieldType::Time => { // should not be possible tracing::error!( From 6d5e4edfadafc96c47ed60e1886a9f6f420e7a3d Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 2 Dec 2024 15:08:39 +0800 Subject: [PATCH 06/28] fix: other fields like rich text --- src/biz/collab/utils.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/biz/collab/utils.rs b/src/biz/collab/utils.rs index 124ab123a..05b6bfd37 100644 --- a/src/biz/collab/utils.rs +++ b/src/biz/collab/utils.rs @@ -340,7 +340,7 @@ pub fn new_cell_from_value(cell_value: serde_json::Value, field: &Field) -> Opti None } }, - FieldType::RichText => { + FieldType::RichText | FieldType::URL | FieldType::Summary | FieldType::Translate => { if let serde_json::Value::String(value_str) = cell_value { Some(yrs::any::Any::String(value_str.into())) } else { @@ -438,14 +438,12 @@ pub fn new_cell_from_value(cell_value: serde_json::Value, field: &Field) -> Opti None }, }, - FieldType::Checklist | FieldType::URL | FieldType::Summary | FieldType::Translate => { - match serde_json::to_string(&cell_value) { - Ok(s) => Some(yrs::any::Any::String(s.into())), - Err(err) => { - tracing::error!("Failed to serialize cell value: {:?}", err); - None - }, - } + FieldType::Checklist => match serde_json::to_string(&cell_value) { + Ok(s) => Some(yrs::any::Any::String(s.into())), + Err(err) => { + tracing::error!("Failed to serialize cell value: {:?}", err); + None + }, }, FieldType::LastEditedTime | FieldType::CreatedTime | FieldType::Time => { // should not be possible From d4986b403acf72f2f32eacc9a479e0dedc90ffc2 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 2 Dec 2024 22:35:12 +0800 Subject: [PATCH 07/28] feat: add database field server impl --- libs/shared-entity/src/dto/workspace_dto.rs | 7 +++ src/api/workspace.rs | 29 ++++++++- src/biz/collab/ops.rs | 69 +++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index 84ca4708f..37fbfb2c6 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -376,3 +376,10 @@ pub struct AFDatabaseField { pub type_option: HashMap, pub is_primary: bool, } + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct InsertAFDatabaseField { + pub name: String, + pub field_type: i64, // FieldType ID + pub type_option: serde_json::Value, +} diff --git a/src/api/workspace.rs b/src/api/workspace.rs index cb850521c..0cee6b84e 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -265,7 +265,8 @@ pub fn workspace_scope() -> Scope { ) .service( web::resource("/{workspace_id}/database/{database_id}/fields") - .route(web::get().to(get_database_fields_handler)), + .route(web::get().to(get_database_fields_handler)) + .route(web::post().to(post_database_fields_handler)), ) .service( web::resource("/{workspace_id}/database/{database_id}/row/updated") @@ -1959,6 +1960,32 @@ async fn get_database_fields_handler( Ok(Json(AppResponse::Ok().with_data(db_fields))) } +async fn post_database_fields_handler( + user_uuid: UserUuid, + path_param: web::Path<(String, String)>, + state: Data, + field: Json, +) -> 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::Write) + .await?; + + biz::collab::ops::add_database_field( + uid, + &state.collab_access_control_storage, + &state.pg_pool, + &workspace_id, + &db_id, + field.into_inner(), + ) + .await?; + + Ok(Json(AppResponse::Ok())) +} + 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 807d6e788..08ef45333 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -7,6 +7,9 @@ use chrono::DateTime; use chrono::Utc; use collab::preclude::Collab; use collab_database::entity::FieldType; +use collab_database::fields::Field; +use collab_database::fields::TypeOptionData; +use collab_database::fields::TypeOptions; use collab_database::rows::CreateRowParams; use collab_database::rows::DatabaseRowBody; use collab_database::rows::Row; @@ -33,6 +36,7 @@ use shared_entity::dto::workspace_dto::AFDatabaseRowDetail; use shared_entity::dto::workspace_dto::DatabaseRowUpdatedItem; use shared_entity::dto::workspace_dto::FavoriteFolderView; use shared_entity::dto::workspace_dto::FolderViewMinimal; +use shared_entity::dto::workspace_dto::InsertAFDatabaseField; use shared_entity::dto::workspace_dto::RecentFolderView; use shared_entity::dto::workspace_dto::TrashFolderView; use sqlx::PgPool; @@ -624,6 +628,71 @@ pub async fn get_database_fields( Ok(acc) } +// inserts a new field into the database +// returns the id of the field created +pub async fn add_database_field( + uid: i64, + collab_storage: &CollabAccessControlStorage, + pg_pool: &PgPool, + workspace_id: &str, + database_id: &str, + insert_field: InsertAFDatabaseField, +) -> Result { + let (mut db_collab, db_body) = + get_database_body(collab_storage, workspace_id, database_id).await?; + + let new_id = uuid::Uuid::new_v4().to_string(); + let mut type_options = TypeOptions::new(); + let tod: TypeOptionData = match serde_json::from_value(insert_field.type_option) { + Ok(tod) => tod, + Err(err) => { + return Err(AppError::InvalidRequest(format!( + "Failed to parse type option: {:?}", + err + ))); + }, + }; + type_options.insert(insert_field.field_type.to_string(), tod); + + let db_collab_update = { + let mut yrs_txn = db_collab.transact_mut(); + db_body.fields.insert_field( + &mut yrs_txn, + Field { + id: new_id.clone(), + name: insert_field.name, + field_type: insert_field.field_type, + type_options, + ..Default::default() + }, + ); + yrs_txn.encode_update_v1() + }; + + let updated_db_collab = encode_collab_v1_bytes(&db_collab, CollabType::Database)?; + + let mut pg_txn = pg_pool.begin().await?; + collab_storage + .insert_new_collab_with_transaction( + workspace_id, + &uid, + CollabParams { + object_id: database_id.to_string(), + encoded_collab_v1: updated_db_collab.into(), + collab_type: CollabType::Database, + embeddings: None, + }, + &mut pg_txn, + "inserting updated database from server", + ) + .await?; + + pg_txn.commit().await?; + broadcast_update(collab_storage, database_id, db_collab_update).await?; + + Ok(new_id) +} + pub async fn list_database_row_ids_updated( collab_storage: &CollabAccessControlStorage, pg_pool: &PgPool, From 5c2ac037468a766170d5871896aba1b60ff47d56 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Tue, 3 Dec 2024 15:58:58 +0800 Subject: [PATCH 08/28] feat: add client api and tests --- libs/client-api/src/http_collab.rs | 52 +++++++++++++- libs/shared-entity/src/dto/workspace_dto.rs | 6 +- src/api/workspace.rs | 14 ++-- src/biz/collab/ops.rs | 55 ++++++++------ tests/collab/database_crud.rs | 80 +++++++++++++++++++++ tests/collab/mod.rs | 1 + 6 files changed, 176 insertions(+), 32 deletions(-) create mode 100644 tests/collab/database_crud.rs diff --git a/libs/client-api/src/http_collab.rs b/libs/client-api/src/http_collab.rs index d1841167a..2214ad160 100644 --- a/libs/client-api/src/http_collab.rs +++ b/libs/client-api/src/http_collab.rs @@ -4,8 +4,8 @@ use app_error::AppError; use bytes::Bytes; use chrono::{DateTime, Utc}; use client_api_entity::workspace_dto::{ - AFDatabase, AFDatabaseField, AFDatabaseRow, AFDatabaseRowDetail, DatabaseRowUpdatedItem, - ListDatabaseRowDetailParam, ListDatabaseRowUpdatedParam, + AFDatabase, AFDatabaseField, AFDatabaseRow, AFDatabaseRowDetail, AFInsertDatabaseField, + DatabaseRowUpdatedItem, ListDatabaseRowDetailParam, ListDatabaseRowUpdatedParam, }; use client_api_entity::{ BatchQueryCollabParams, BatchQueryCollabResult, CollabParams, CreateCollabParams, @@ -210,6 +210,28 @@ impl Client { AppResponse::from_response(resp).await?.into_data() } + // Adds a database field to the specified database. + // Returns the field id of the newly created field. + pub async fn add_database_field( + &self, + workspace_id: &str, + database_id: &str, + insert_field: &AFInsertDatabaseField, + ) -> Result { + let url = format!( + "{}/api/workspace/{}/database/{}/fields", + self.base_url, workspace_id, database_id + ); + let resp = self + .http_client_with_auth(Method::POST, &url) + .await? + .json(insert_field) + .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, @@ -250,6 +272,32 @@ impl Client { AppResponse::from_response(resp).await?.into_data() } + /// Example payload: + /// { + /// "Name": "some_data", # using column name + /// "_pIkG": "some other data" # using field_id (can be obtained from [get_database_fields]) + /// } + /// Upon success, returns the row id for the newly created row. + pub async fn add_database_item( + &self, + workspace_id: &str, + database_id: &str, + payload: &serde_json::Value, + ) -> Result { + let url = format!( + "{}/api/workspace/{}/database/{}/row", + self.base_url, workspace_id, database_id + ); + let resp = self + .http_client_with_auth(Method::POST, &url) + .await? + .json(&payload) + .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 37fbfb2c6..89037d1c1 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -378,8 +378,8 @@ pub struct AFDatabaseField { } #[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct InsertAFDatabaseField { +pub struct AFInsertDatabaseField { pub name: String, - pub field_type: i64, // FieldType ID - pub type_option: serde_json::Value, + pub field_type: i64, // FieldType ID + pub type_option_data: Option, // TypeOptionData } diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 0cee6b84e..61a001d57 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -1918,7 +1918,7 @@ async fn post_database_row_handler( path_param: web::Path<(String, String)>, state: Data, cells_by_id: Json>, -) -> Result>> { +) -> Result>> { let (workspace_id, db_id) = path_param.into_inner(); let uid = state.user_cache.get_user_uid(&user_uuid).await?; state @@ -1926,7 +1926,7 @@ async fn post_database_row_handler( .enforce_action(&uid, &workspace_id, Action::Write) .await?; - biz::collab::ops::insert_database_row( + let new_db_row_id = biz::collab::ops::insert_database_row( &state.collab_access_control_storage, &state.pg_pool, &workspace_id, @@ -1935,7 +1935,7 @@ async fn post_database_row_handler( cells_by_id.into_inner(), ) .await?; - Ok(Json(AppResponse::Ok())) + Ok(Json(AppResponse::Ok().with_data(new_db_row_id))) } async fn get_database_fields_handler( @@ -1964,8 +1964,8 @@ async fn post_database_fields_handler( user_uuid: UserUuid, path_param: web::Path<(String, String)>, state: Data, - field: Json, -) -> Result>> { + field: Json, +) -> Result>> { let (workspace_id, db_id) = path_param.into_inner(); let uid = state.user_cache.get_user_uid(&user_uuid).await?; state @@ -1973,7 +1973,7 @@ async fn post_database_fields_handler( .enforce_action(&uid, &workspace_id, Action::Write) .await?; - biz::collab::ops::add_database_field( + let field_id = biz::collab::ops::add_database_field( uid, &state.collab_access_control_storage, &state.pg_pool, @@ -1983,7 +1983,7 @@ async fn post_database_fields_handler( ) .await?; - Ok(Json(AppResponse::Ok())) + Ok(Json(AppResponse::Ok().with_data(field_id))) } async fn list_database_row_id_updated_handler( diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index 08ef45333..3163465d6 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -8,12 +8,12 @@ use chrono::Utc; use collab::preclude::Collab; use collab_database::entity::FieldType; use collab_database::fields::Field; -use collab_database::fields::TypeOptionData; use collab_database::fields::TypeOptions; use collab_database::rows::CreateRowParams; use collab_database::rows::DatabaseRowBody; use collab_database::rows::Row; use collab_database::rows::RowDetail; +use collab_database::views::FieldOrder; use collab_database::views::OrderObjectPosition; use collab_database::workspace_database::WorkspaceDatabase; use collab_database::workspace_database::WorkspaceDatabaseBody; @@ -33,10 +33,10 @@ 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::AFInsertDatabaseField; use shared_entity::dto::workspace_dto::DatabaseRowUpdatedItem; use shared_entity::dto::workspace_dto::FavoriteFolderView; use shared_entity::dto::workspace_dto::FolderViewMinimal; -use shared_entity::dto::workspace_dto::InsertAFDatabaseField; use shared_entity::dto::workspace_dto::RecentFolderView; use shared_entity::dto::workspace_dto::TrashFolderView; use sqlx::PgPool; @@ -482,7 +482,7 @@ pub async fn insert_database_row( database_uuid_str: &str, uid: i64, cell_value_by_id: HashMap, -) -> Result<(), AppError> { +) -> Result { let mut db_txn = pg_pool.begin().await?; // get database types and type options @@ -555,7 +555,7 @@ pub async fn insert_database_row( let ts_now = chrono::Utc::now().timestamp(); let row_order = db_body .create_row(CreateRowParams { - id: new_db_row_id.into(), + id: new_db_row_id.clone().into(), database_id: database_uuid_str.to_string(), cells: new_db_row_body .cells(&new_db_row_collab.transact()) @@ -602,7 +602,7 @@ pub async fn insert_database_row( db_txn.commit().await?; broadcast_update(collab_storage, database_uuid_str, db_collab_update).await?; - Ok(()) + Ok(new_db_row_id) } pub async fn get_database_fields( @@ -636,15 +636,23 @@ pub async fn add_database_field( pg_pool: &PgPool, workspace_id: &str, database_id: &str, - insert_field: InsertAFDatabaseField, + insert_field: AFInsertDatabaseField, ) -> Result { let (mut db_collab, db_body) = get_database_body(collab_storage, workspace_id, database_id).await?; - let new_id = uuid::Uuid::new_v4().to_string(); + let new_id = uuid::Uuid::new_v4() + .to_string() + .chars() + .take(6) + .collect::(); + let mut type_options = TypeOptions::new(); - let tod: TypeOptionData = match serde_json::from_value(insert_field.type_option) { - Ok(tod) => tod, + let type_option_data = insert_field + .type_option_data + .unwrap_or(serde_json::json!({})); + match serde_json::from_value(type_option_data) { + Ok(tod) => type_options.insert(insert_field.field_type.to_string(), tod), Err(err) => { return Err(AppError::InvalidRequest(format!( "Failed to parse type option: {:?}", @@ -652,20 +660,27 @@ pub async fn add_database_field( ))); }, }; - type_options.insert(insert_field.field_type.to_string(), tod); + + let new_field = Field { + id: new_id.clone(), + name: insert_field.name, + field_type: insert_field.field_type, + type_options, + ..Default::default() + }; let db_collab_update = { let mut yrs_txn = db_collab.transact_mut(); - db_body.fields.insert_field( - &mut yrs_txn, - Field { - id: new_id.clone(), - name: insert_field.name, - field_type: insert_field.field_type, - type_options, - ..Default::default() - }, - ); + db_body.fields.insert_field(&mut yrs_txn, new_field); + let mut db_views = db_body.views.get_all_views(&yrs_txn); + for db_view in db_views.iter_mut() { + db_view.field_orders.push(FieldOrder { id: new_id.clone() }); + } + db_body.views.clear(&mut yrs_txn); + for view in db_views { + db_body.views.insert_view(&mut yrs_txn, view); + } + yrs_txn.encode_update_v1() }; diff --git a/tests/collab/database_crud.rs b/tests/collab/database_crud.rs new file mode 100644 index 000000000..eec819a9f --- /dev/null +++ b/tests/collab/database_crud.rs @@ -0,0 +1,80 @@ +use client_api_test::{generate_unique_registered_user_client, workspace_id_from_client}; +use collab_database::entity::FieldType; +use shared_entity::dto::workspace_dto::AFInsertDatabaseField; + +#[tokio::test] +async fn database_fields_crud() { + let (c, _user) = generate_unique_registered_user_client().await; + let workspace_id = workspace_id_from_client(&c).await; + let databases = c.list_databases(&workspace_id).await.unwrap(); + assert_eq!(databases.len(), 1); + let todo_db = &databases[0]; + + let my_num_field_id = { + c.add_database_field( + &workspace_id, + &todo_db.id, + &AFInsertDatabaseField { + name: "MyNumberColumn".to_string(), + field_type: FieldType::Number.into(), + ..Default::default() + }, + ) + .await + .unwrap() + }; + let my_datetime_field_id = { + c.add_database_field( + &workspace_id, + &todo_db.id, + &AFInsertDatabaseField { + name: "MyDateTimeColumn".to_string(), + field_type: FieldType::DateTime.into(), + ..Default::default() + }, + ) + .await + .unwrap() + }; + { + let my_description = "my task 123"; + let my_status = "To Do"; + let new_row_id = c + .add_database_item( + &workspace_id, + &todo_db.id, + &serde_json::json!({ + "Description": my_description, + "Status": my_status, + "Multiselect": ["social", "news"], + my_num_field_id: 123, + my_datetime_field_id: 1733210221, + }), + ) + .await + .unwrap(); + + let row_details = c + .list_database_row_details(&workspace_id, &todo_db.id, &[&new_row_id]) + .await + .unwrap(); + assert_eq!(row_details.len(), 1); + let new_row_detail = &row_details[0]; + assert_eq!(new_row_detail.cells["Description"]["data"], my_description); + assert_eq!(new_row_detail.cells["Status"]["data"], my_status); + assert_eq!(new_row_detail.cells["Multiselect"]["data"], "social,news"); + assert_eq!(new_row_detail.cells["MyNumberColumn"]["data"], "123"); + assert_eq!( + new_row_detail.cells["MyDateTimeColumn"]["data"], + "2024-12-03T07:17:01+00:00" + ) + } + + let db_fields = c + .get_database_fields(&workspace_id, &todo_db.id) + .await + .unwrap(); + if true { + panic!("{:#?}", db_fields); + } +} diff --git a/tests/collab/mod.rs b/tests/collab/mod.rs index b7b380152..06286b777 100644 --- a/tests/collab/mod.rs +++ b/tests/collab/mod.rs @@ -8,3 +8,4 @@ mod single_device_edit; mod storage_test; pub mod util; mod web_edit; +mod database_crud; From 5d24f6ffb86804f89c08d87c70135e3fdd1fbb3f Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Thu, 5 Dec 2024 05:38:31 +0800 Subject: [PATCH 09/28] feat: use to json value impl from collab --- Cargo.lock | 7 -- Cargo.toml | 22 +++-- src/biz/collab/ops.rs | 16 ++-- src/biz/collab/utils.rs | 157 ++++++++++++--------------------- src/biz/workspace/page_view.rs | 2 +- tests/collab/database_crud.rs | 61 ++++++++++--- 6 files changed, 128 insertions(+), 137 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1de49bb7..63e398fe9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2177,7 +2177,6 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "arc-swap", @@ -2202,7 +2201,6 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "async-trait", @@ -2241,7 +2239,6 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "arc-swap", @@ -2262,7 +2259,6 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "bytes", @@ -2282,7 +2278,6 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "arc-swap", @@ -2304,7 +2299,6 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "async-recursion", @@ -2407,7 +2401,6 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0efc824a6e1a56e4485646e6428c07fdccf6e918#0efc824a6e1a56e4485646e6428c07fdccf6e918" dependencies = [ "anyhow", "collab", diff --git a/Cargo.toml b/Cargo.toml index 7c6a93fc8..b1a0cbbc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -312,13 +312,21 @@ lto = false # Disable Link-Time Optimization [patch.crates-io] # It's diffcult to resovle different version with the same crate used in AppFlowy Frontend and the Client-API crate. # So using patch to workaround this issue. -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } -collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0efc824a6e1a56e4485646e6428c07fdccf6e918" } +# collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } +# collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } +# collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } +# collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } +# collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } +# collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } +# collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } + +collab = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab" } +collab-entity = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab-entity" } +collab-folder = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab-folder" } +collab-document = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab-document" } +collab-user = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab-user" } +collab-database = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab-database" } +collab-importer = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab-importer" } [features] history = [] diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index 8b88630ca..dae7d0b48 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -66,14 +66,14 @@ 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; use super::utils::collab_from_doc_state; -use super::utils::convert_database_cells_human_readable; use super::utils::encode_collab_v1_bytes; use super::utils::field_by_id_name_uniq; use super::utils::get_database_body; use super::utils::get_latest_collab; use super::utils::get_latest_collab_encoded; +use super::utils::get_row_details_by_id; use super::utils::new_cell_from_value; -use super::utils::selection_name_by_id; +use super::utils::type_option_reader_by_id; use super::utils::type_options_serde; /// Create a new collab member @@ -738,9 +738,8 @@ pub async fn list_database_row_details( get_database_body(collab_storage, &workspace_uuid_str, &database_uuid_str).await?; let all_fields = db_body.fields.get_all_fields(&database_collab.transact()); - let selection_name_by_id = selection_name_by_id(&all_fields); - let field_by_name_uniq = field_by_id_name_uniq(all_fields); - + let type_option_reader_by_id = type_option_reader_by_id(&all_fields); + let field_by_id = field_by_id_name_uniq(all_fields); let query_collabs: Vec = row_ids .iter() .map(|id| QueryCollab { @@ -776,10 +775,11 @@ pub async fn list_database_row_details( return None; }, }; - let cells = convert_database_cells_human_readable( + + let cells = get_row_details_by_id( row_detail.row.cells, - &field_by_name_uniq, - &selection_name_by_id, + &field_by_id, + &type_option_reader_by_id, ); Some(AFDatabaseRowDetail { id, cells }) }, diff --git a/src/biz/collab/utils.rs b/src/biz/collab/utils.rs index 05b6bfd37..f6d94ae66 100644 --- a/src/biz/collab/utils.rs +++ b/src/biz/collab/utils.rs @@ -4,10 +4,14 @@ use collab::core::collab::DataSource; use collab::preclude::Collab; use collab_database::database::DatabaseBody; use collab_database::entity::FieldType; +use collab_database::fields::type_option_cell_reader; use collab_database::fields::Field; +use collab_database::fields::TypeOptionCellReader; +use collab_database::fields::TypeOptionData; use collab_database::fields::TypeOptions; use collab_database::rows::new_cell_builder; use collab_database::rows::Cell; +use collab_database::rows::Cells; use collab_database::template::entity::CELL_DATA; use collab_database::workspace_database::NoPersistenceDatabaseCollabService; use collab_entity::CollabType; @@ -21,112 +25,44 @@ use std::collections::HashMap; use std::collections::HashSet; use std::sync::Arc; -pub fn convert_database_cells_human_readable( - db_cells: HashMap>, - field_by_id: &HashMap, - selection_name_by_id: &HashMap, +pub fn cell_data_to_serde( + cell_data: Cell, + field: &Field, + type_option_reader_by_id: &HashMap>, +) -> serde_json::Value { + match type_option_reader_by_id.get(&field.id) { + Some(tor) => tor.json_cell(&cell_data), + None => { + tracing::error!("Failed to get type option reader by id: {}", field.id); + serde_json::Value::Null + }, + } +} + +pub fn get_row_details_by_id( + cells: Cells, + field_by_id_name_uniq: &HashMap, + type_option_reader_by_id: &HashMap>, ) -> HashMap> { - let mut human_readable_records: HashMap> = - HashMap::with_capacity(db_cells.len()); + let mut row_details: HashMap> = + HashMap::with_capacity(cells.len()); - for (field_id, cell) in db_cells { - let field = match field_by_id.get(&field_id) { - Some(field) => field, + for (field_id, field) in field_by_id_name_uniq { + let cell: Cell = match cells.get(field_id) { + Some(cell) => cell.clone(), None => { - tracing::error!("Failed to get field by id: {}, cell: {:?}", field_id, cell); - continue; + tracing::error!("Failed to get cell by field id: {}", field.id); + Cell::new() }, }; - let field_type = FieldType::from(field.field_type); - - let mut human_readable_cell: HashMap = - HashMap::with_capacity(cell.len()); - for (key, value) in cell { - 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() - .into(), - Err(err) => { - tracing::error!("Failed to cast timestamp: {:?}", err); - serde_json::Value::Null - }, - }, - "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(), - } - }, - _ => serde_json::to_value(value).unwrap_or_default(), - }; - human_readable_cell.insert(key, serde_value); - } - human_readable_records.insert(field.name.clone(), human_readable_cell); + let cell_value = cell_data_to_serde(cell, field, type_option_reader_by_id); + row_details.insert( + field.name.clone(), + HashMap::from([(CELL_DATA.to_string(), cell_value)]), + ); } - human_readable_records + + row_details } pub fn selection_name_by_id(fields: &[Field]) -> HashMap { @@ -205,6 +141,27 @@ pub fn field_by_id_name_uniq(mut fields: Vec) -> HashMap { field_by_id } +/// create a map type option reader by field id +pub fn type_option_reader_by_id( + fields: &[Field], +) -> HashMap> { + let mut type_option_reader_by_id: HashMap> = + HashMap::with_capacity(fields.len()); + for field in fields { + let field_id: String = field.id.clone(); + let type_option_reader: Box = { + let field_type: &FieldType = &FieldType::from(field.field_type); + let type_option_data: TypeOptionData = match field.type_options.get(&field_type.type_id()) { + Some(tod) => tod.clone(), + None => HashMap::new(), + }; + type_option_cell_reader(type_option_data, field_type) + }; + type_option_reader_by_id.insert(field_id, type_option_reader); + } + type_option_reader_by_id +} + pub fn type_options_serde( type_options: &TypeOptions, field_type: &FieldType, diff --git a/src/biz/workspace/page_view.rs b/src/biz/workspace/page_view.rs index a9a1041fc..bc4ce7855 100644 --- a/src/biz/workspace/page_view.rs +++ b/src/biz/workspace/page_view.rs @@ -369,7 +369,7 @@ async fn prepare_default_board_encoded_database( let mut rows = vec![]; let card_status_select_option_ids = SelectOptionIds::from(vec![default_option_id.clone()]); for i in 0..3 { - let card_status_cell_data = card_status_select_option_ids.to_cell_data(FieldType::SingleSelect); + let card_status_cell_data = card_status_select_option_ids.to_cell(FieldType::SingleSelect); let mut description_cell = new_cell_builder(FieldType::RichText); let description_text = format!("Card {}", i + 1); description_cell.insert(CELL_DATA.into(), description_text.into()); diff --git a/tests/collab/database_crud.rs b/tests/collab/database_crud.rs index eec819a9f..0b1d4cc33 100644 --- a/tests/collab/database_crud.rs +++ b/tests/collab/database_crud.rs @@ -1,5 +1,6 @@ use client_api_test::{generate_unique_registered_user_client, workspace_id_from_client}; use collab_database::entity::FieldType; +use serde_json::json; use shared_entity::dto::workspace_dto::AFInsertDatabaseField; #[tokio::test] @@ -36,6 +37,32 @@ async fn database_fields_crud() { .await .unwrap() }; + let my_url_field_id = { + c.add_database_field( + &workspace_id, + &todo_db.id, + &AFInsertDatabaseField { + name: "MyUrlField".to_string(), + field_type: FieldType::URL.into(), + ..Default::default() + }, + ) + .await + .unwrap() + }; + let my_checkbox_field_id = { + c.add_database_field( + &workspace_id, + &todo_db.id, + &AFInsertDatabaseField { + name: "MyCheckboxColumn".to_string(), + field_type: FieldType::Checkbox.into(), + ..Default::default() + }, + ) + .await + .unwrap() + }; { let my_description = "my task 123"; let my_status = "To Do"; @@ -49,6 +76,8 @@ async fn database_fields_crud() { "Multiselect": ["social", "news"], my_num_field_id: 123, my_datetime_field_id: 1733210221, + my_url_field_id: "https://appflowy.io", + my_checkbox_field_id: true, }), ) .await @@ -60,21 +89,25 @@ async fn database_fields_crud() { .unwrap(); assert_eq!(row_details.len(), 1); let new_row_detail = &row_details[0]; - assert_eq!(new_row_detail.cells["Description"]["data"], my_description); - assert_eq!(new_row_detail.cells["Status"]["data"], my_status); - assert_eq!(new_row_detail.cells["Multiselect"]["data"], "social,news"); + assert_eq!(new_row_detail.cells["Description"]["data"], my_description,); + assert_eq!(new_row_detail.cells["Status"]["data"][0]["name"], my_status); + assert_eq!( + new_row_detail.cells["Multiselect"]["data"][0]["name"], + "social" + ); + assert_eq!( + new_row_detail.cells["Multiselect"]["data"][1]["name"], + "news" + ); assert_eq!(new_row_detail.cells["MyNumberColumn"]["data"], "123"); assert_eq!( - new_row_detail.cells["MyDateTimeColumn"]["data"], - "2024-12-03T07:17:01+00:00" - ) - } - - let db_fields = c - .get_database_fields(&workspace_id, &todo_db.id) - .await - .unwrap(); - if true { - panic!("{:#?}", db_fields); + new_row_detail.cells["MyDateTimeColumn"]["data"]["timestamp"], + 1733210221 + ); + assert_eq!( + new_row_detail.cells["MyUrlField"]["data"], + json!({"data": "https://appflowy.io"}).to_string() + ); + assert_eq!(new_row_detail.cells["MyCheckboxColumn"]["data"], "Yes"); } } From 6f1ba480c09e31d1263047bc6acaac057d4e33cc Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Thu, 5 Dec 2024 06:34:31 +0800 Subject: [PATCH 10/28] feat: use add database cell impl from collab --- Cargo.lock | 7 + Cargo.toml | 22 +- libs/shared-entity/src/dto/workspace_dto.rs | 2 +- src/biz/collab/ops.rs | 23 +- src/biz/collab/utils.rs | 228 ++------------------ tests/collab/database_crud.rs | 9 +- tests/collab/mod.rs | 2 +- 7 files changed, 60 insertions(+), 233 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63e398fe9..3c4180212 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2177,6 +2177,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" dependencies = [ "anyhow", "arc-swap", @@ -2201,6 +2202,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" dependencies = [ "anyhow", "async-trait", @@ -2239,6 +2241,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" dependencies = [ "anyhow", "arc-swap", @@ -2259,6 +2262,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" dependencies = [ "anyhow", "bytes", @@ -2278,6 +2282,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" dependencies = [ "anyhow", "arc-swap", @@ -2299,6 +2304,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" dependencies = [ "anyhow", "async-recursion", @@ -2401,6 +2407,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" dependencies = [ "anyhow", "collab", diff --git a/Cargo.toml b/Cargo.toml index b1a0cbbc1..c41e967f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -312,21 +312,13 @@ lto = false # Disable Link-Time Optimization [patch.crates-io] # It's diffcult to resovle different version with the same crate used in AppFlowy Frontend and the Client-API crate. # So using patch to workaround this issue. -# collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } -# collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } -# collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } -# collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } -# collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } -# collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } -# collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cc31b093d1cb178505aa0f6ba3ca2148ef065ee8" } - -collab = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab" } -collab-entity = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab-entity" } -collab-folder = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab-folder" } -collab-document = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab-document" } -collab-user = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab-user" } -collab-database = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab-database" } -collab-importer = { path = "/home/zack2827/github.com/AppFlowy-IO/AppFlowy-Collab/collab-importer" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } +collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } [features] history = [] diff --git a/libs/shared-entity/src/dto/workspace_dto.rs b/libs/shared-entity/src/dto/workspace_dto.rs index 89037d1c1..0d80fe58a 100644 --- a/libs/shared-entity/src/dto/workspace_dto.rs +++ b/libs/shared-entity/src/dto/workspace_dto.rs @@ -380,6 +380,6 @@ pub struct AFDatabaseField { #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct AFInsertDatabaseField { pub name: String, - pub field_type: i64, // FieldType ID + pub field_type: i64, // FieldType ID pub type_option_data: Option, // TypeOptionData } diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index dae7d0b48..b8f7938a4 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -9,6 +9,7 @@ use collab::preclude::Collab; use collab_database::entity::FieldType; use collab_database::fields::Field; use collab_database::fields::TypeOptions; +use collab_database::rows::Cell; use collab_database::rows::CreateRowParams; use collab_database::rows::DatabaseRowBody; use collab_database::rows::Row; @@ -72,8 +73,8 @@ use super::utils::get_database_body; use super::utils::get_latest_collab; use super::utils::get_latest_collab_encoded; use super::utils::get_row_details_by_id; -use super::utils::new_cell_from_value; use super::utils::type_option_reader_by_id; +use super::utils::type_option_writer_by_id; use super::utils::type_options_serde; /// Create a new collab member @@ -494,6 +495,7 @@ pub async fn insert_database_row( acc.insert(field.id.clone(), field.clone()); acc }); + let type_option_reader_by_id = type_option_writer_by_id(&all_fields); let field_by_name = field_by_name_uniq(all_fields); let new_db_row_id = uuid::Uuid::new_v4().to_string(); @@ -523,14 +525,19 @@ pub async fn insert_database_row( }, }, }; - let new_cell = new_cell_from_value(serde_val, field); - if let Some(new_cell) = new_cell { - database_body.update(&mut txn, |row_update| { - row_update.update_cells(|cells_update| { - cells_update.insert_cell(&field.id, new_cell); - }); + let cell_writer = match type_option_reader_by_id.get(&field.id) { + Some(cell_writer) => cell_writer, + None => { + tracing::error!("Failed to get type option writer for field: {}", field.id); + continue; + }, + }; + let new_cell: Cell = cell_writer.convert_json_to_cell(serde_val); + database_body.update(&mut txn, |row_update| { + row_update.update_cells(|cells_update| { + cells_update.insert_cell(&field.id, new_cell); }); - } + }); } database_body }; diff --git a/src/biz/collab/utils.rs b/src/biz/collab/utils.rs index f6d94ae66..396be2e15 100644 --- a/src/biz/collab/utils.rs +++ b/src/biz/collab/utils.rs @@ -5,11 +5,12 @@ use collab::preclude::Collab; use collab_database::database::DatabaseBody; use collab_database::entity::FieldType; use collab_database::fields::type_option_cell_reader; +use collab_database::fields::type_option_cell_writer; use collab_database::fields::Field; use collab_database::fields::TypeOptionCellReader; +use collab_database::fields::TypeOptionCellWriter; use collab_database::fields::TypeOptionData; use collab_database::fields::TypeOptions; -use collab_database::rows::new_cell_builder; use collab_database::rows::Cell; use collab_database::rows::Cells; use collab_database::template::entity::CELL_DATA; @@ -65,42 +66,6 @@ pub fn get_row_details_by_id( row_details } -pub fn selection_name_by_id(fields: &[Field]) -> HashMap { - let mut selection_name_by_id: HashMap = HashMap::new(); - for field in fields { - let field_type = FieldType::from(field.field_type); - match field_type { - FieldType::SingleSelect | FieldType::MultiSelect => { - selection_id_name_pairs(&field.type_options, &field_type) - .into_iter() - .for_each(|(id, name)| { - selection_name_by_id.insert(id, name); - }) - }, - _ => (), - } - } - selection_name_by_id -} - -pub fn selection_id_by_name(fields: &[Field]) -> HashMap { - let mut selection_id_by_name: HashMap = HashMap::new(); - for field in fields { - let field_type = FieldType::from(field.field_type); - match field_type { - FieldType::SingleSelect | FieldType::MultiSelect => { - selection_id_name_pairs(&field.type_options, &field_type) - .into_iter() - .for_each(|(id, name)| { - selection_id_by_name.insert(name, id); - }) - }, - _ => (), - } - } - selection_id_by_name -} - /// create a map of field name to field /// if the field name is repeated, it will be appended with the field id, pub fn field_by_name_uniq(mut fields: Vec) -> HashMap { @@ -141,6 +106,27 @@ pub fn field_by_id_name_uniq(mut fields: Vec) -> HashMap { field_by_id } +/// create a map type option writer by field id +pub fn type_option_writer_by_id( + fields: &[Field], +) -> HashMap> { + let mut type_option_reader_by_id: HashMap> = + HashMap::with_capacity(fields.len()); + for field in fields { + let field_id: String = field.id.clone(); + let type_option_reader: Box = { + let field_type: &FieldType = &FieldType::from(field.field_type); + let type_option_data: TypeOptionData = match field.type_options.get(&field_type.type_id()) { + Some(tod) => tod.clone(), + None => HashMap::new(), + }; + type_option_cell_writer(type_option_data, field_type) + }; + type_option_reader_by_id.insert(field_id, type_option_reader); + } + type_option_reader_by_id +} + /// create a map type option reader by field id pub fn type_option_reader_by_id( fields: &[Field], @@ -279,171 +265,3 @@ pub async fn get_latest_collab( })?; Ok(collab) } - -pub fn new_cell_from_value(cell_value: serde_json::Value, field: &Field) -> Option { - let field_type = FieldType::from(field.field_type); - let cell_value: Option = match field_type { - FieldType::Relation | FieldType::Media => { - if let serde_json::Value::Array(arr) = cell_value { - let mut acc = Vec::with_capacity(arr.len()); - for v in arr { - if let serde_json::Value::String(value_str) = v { - acc.push(yrs::any::Any::String(value_str.into())); - } - } - Some(yrs::any::Any::Array(acc.into())) - } else { - tracing::warn!("invalid media/relation value: {:?}", cell_value); - None - } - }, - FieldType::RichText | FieldType::URL | FieldType::Summary | FieldType::Translate => { - if let serde_json::Value::String(value_str) = cell_value { - Some(yrs::any::Any::String(value_str.into())) - } else { - Some(yrs::any::Any::String(cell_value.to_string().into())) - } - }, - FieldType::Checkbox => { - let is_yes = match cell_value { - serde_json::Value::Null => false, - serde_json::Value::Bool(b) => b, - serde_json::Value::Number(n) => n.is_i64() && n.as_i64().unwrap() >= 1, - serde_json::Value::String(s) => s.to_lowercase() == "yes", - _ => { - tracing::warn!("invalid checklist value: {:?}", cell_value); - false - }, - }; - if is_yes { - Some(yrs::any::Any::String("Yes".into())) - } else { - None - } - }, - FieldType::Number => match cell_value { - serde_json::Value::Number(n) => Some(yrs::any::Any::String(n.to_string().into())), - serde_json::Value::String(s) => Some(yrs::any::Any::String(s.into())), - _ => { - tracing::warn!("invalid number value: {:?}", cell_value); - None - }, - }, - FieldType::SingleSelect => match cell_value { - serde_json::Value::String(s) => { - let selection_name_by_id = selection_name_by_id(std::slice::from_ref(field)); - match selection_name_by_id.get(&s) { - Some(_name) => Some(yrs::any::Any::String(s.into())), - None => { - let selection_id_by_name = selection_id_by_name(std::slice::from_ref(field)); - match selection_id_by_name.get(&s) { - Some(id) => Some(yrs::any::Any::String(id.as_str().into())), - None => { - tracing::warn!("invalid single select value for field: {:?}", field.name); - None - }, - } - }, - } - }, - _ => { - tracing::warn!("invalid single value: {:?}", cell_value); - None - }, - }, - FieldType::MultiSelect => { - let selection_name_by_id = selection_name_by_id(std::slice::from_ref(field)); - let selection_id_by_name = selection_id_by_name(std::slice::from_ref(field)); - let input_ids: Vec<&str> = match cell_value { - serde_json::Value::String(ref s) => s.split(',').collect(), - serde_json::Value::Array(ref arr) => arr.iter().flat_map(|v| v.as_str()).collect(), - _ => { - tracing::warn!("invalid multi select value: {:?}", cell_value); - vec![] - }, - }; - - let mut sel_ids = Vec::with_capacity(input_ids.len()); - for input_id in input_ids { - if let Some(_name) = selection_name_by_id.get(input_id) { - sel_ids.push(input_id.to_owned()); - } else if let Some(id) = selection_id_by_name.get(input_id) { - sel_ids.push(id.to_owned()); - } else { - tracing::warn!("invalid multi select value: {:?}", cell_value); - } - } - yrs::any::Any::String(sel_ids.join(",").into()).into() - }, - FieldType::DateTime => match cell_value { - serde_json::Value::Number(number) => { - let int_value = number.as_i64().unwrap_or_default(); - Some(yrs::any::Any::String(int_value.to_string().into())) - }, - serde_json::Value::String(s) => match s.parse::() { - Ok(int_value) => Some(yrs::any::Any::String(int_value.to_string().into())), - Err(_err) => match chrono::DateTime::parse_from_rfc3339(&s) { - Ok(dt) => Some(yrs::any::Any::String(dt.timestamp().to_string().into())), - Err(err) => { - tracing::warn!("Failed to parse datetime string: {:?}", err); - None - }, - }, - }, - _ => { - tracing::warn!("invalid datetime value: {:?}", cell_value); - None - }, - }, - FieldType::Checklist => match serde_json::to_string(&cell_value) { - Ok(s) => Some(yrs::any::Any::String(s.into())), - Err(err) => { - tracing::error!("Failed to serialize cell value: {:?}", err); - None - }, - }, - FieldType::LastEditedTime | FieldType::CreatedTime | FieldType::Time => { - // should not be possible - tracing::error!( - "attempt to insert into invalid field: {:?}, value: {}", - field_type, - cell_value - ); - None - }, - }; - - cell_value.map(|v| { - let mut new_cell = new_cell_builder(field_type); - new_cell.insert(CELL_DATA.to_string(), v); - new_cell - }) -} - -fn selection_id_name_pairs( - type_options: &TypeOptions, - field_type: &FieldType, -) -> Vec<(String, String)> { - 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()) { - let mut acc = Vec::with_capacity(selections.len()); - 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()), - ) { - acc.push((id.to_owned(), name.to_owned())); - } - } - } - - return acc; - } - } - } - }; - vec![] -} diff --git a/tests/collab/database_crud.rs b/tests/collab/database_crud.rs index 0b1d4cc33..97ff70a5d 100644 --- a/tests/collab/database_crud.rs +++ b/tests/collab/database_crud.rs @@ -89,7 +89,10 @@ async fn database_fields_crud() { .unwrap(); assert_eq!(row_details.len(), 1); let new_row_detail = &row_details[0]; - assert_eq!(new_row_detail.cells["Description"]["data"], my_description,); + assert_eq!( + new_row_detail.cells["Description"]["data"], + json!(my_description).to_string() + ); assert_eq!(new_row_detail.cells["Status"]["data"][0]["name"], my_status); assert_eq!( new_row_detail.cells["Multiselect"]["data"][0]["name"], @@ -106,8 +109,8 @@ async fn database_fields_crud() { ); assert_eq!( new_row_detail.cells["MyUrlField"]["data"], - json!({"data": "https://appflowy.io"}).to_string() + json!({"data": json!("https://appflowy.io").to_string()}).to_string() ); - assert_eq!(new_row_detail.cells["MyCheckboxColumn"]["data"], "Yes"); + assert_eq!(new_row_detail.cells["MyCheckboxColumn"]["data"], "true"); } } diff --git a/tests/collab/mod.rs b/tests/collab/mod.rs index 06286b777..c37e150a8 100644 --- a/tests/collab/mod.rs +++ b/tests/collab/mod.rs @@ -1,5 +1,6 @@ mod awareness_test; mod collab_curd_test; +mod database_crud; mod member_crud; mod missing_update_test; mod multi_devices_edit; @@ -8,4 +9,3 @@ mod single_device_edit; mod storage_test; pub mod util; mod web_edit; -mod database_crud; From 67737cffabb574cf9b47064f66772cacca5c8fbc Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Thu, 5 Dec 2024 13:03:20 +0800 Subject: [PATCH 11/28] feat: update to latest collab --- Cargo.lock | 14 +++++----- Cargo.toml | 14 +++++----- tests/workspace/workspace_crud.rs | 45 ------------------------------- 3 files changed, 14 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3c4180212..61c2e6077 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2177,7 +2177,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" dependencies = [ "anyhow", "arc-swap", @@ -2202,7 +2202,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" dependencies = [ "anyhow", "async-trait", @@ -2241,7 +2241,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" dependencies = [ "anyhow", "arc-swap", @@ -2262,7 +2262,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" dependencies = [ "anyhow", "bytes", @@ -2282,7 +2282,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" dependencies = [ "anyhow", "arc-swap", @@ -2304,7 +2304,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" dependencies = [ "anyhow", "async-recursion", @@ -2407,7 +2407,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=257766bd3a2ace30a87cb5ba860977a59c999b62#257766bd3a2ace30a87cb5ba860977a59c999b62" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" dependencies = [ "anyhow", "collab", diff --git a/Cargo.toml b/Cargo.toml index c41e967f7..5759d60dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -312,13 +312,13 @@ lto = false # Disable Link-Time Optimization [patch.crates-io] # It's diffcult to resovle different version with the same crate used in AppFlowy Frontend and the Client-API crate. # So using patch to workaround this issue. -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } -collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "257766bd3a2ace30a87cb5ba860977a59c999b62" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } +collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } [features] history = [] diff --git a/tests/workspace/workspace_crud.rs b/tests/workspace/workspace_crud.rs index 822bc85b5..fd41d2778 100644 --- a/tests/workspace/workspace_crud.rs +++ b/tests/workspace/workspace_crud.rs @@ -135,51 +135,6 @@ 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, &todos_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": "looks great,fast", - // }, - // "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": "To Do", - // "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"] == "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"] == "To Do" - && row.cells["Status"]["field_type"] == "SingleSelect" - }) - .unwrap(); - } } } } From f681279e0365cdf736e6d335e5e4bd3287fde71c Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 6 Dec 2024 04:06:48 +0800 Subject: [PATCH 12/28] chore: upgrade collab and fix tests --- Cargo.lock | 14 +++++++------- Cargo.toml | 14 +++++++------- tests/collab/database_crud.rs | 22 ++++++---------------- 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 61c2e6077..d72a64815 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2177,7 +2177,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bd61aebe485c2c15328c008154021a286c6da8c3#bd61aebe485c2c15328c008154021a286c6da8c3" dependencies = [ "anyhow", "arc-swap", @@ -2202,7 +2202,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bd61aebe485c2c15328c008154021a286c6da8c3#bd61aebe485c2c15328c008154021a286c6da8c3" dependencies = [ "anyhow", "async-trait", @@ -2241,7 +2241,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bd61aebe485c2c15328c008154021a286c6da8c3#bd61aebe485c2c15328c008154021a286c6da8c3" dependencies = [ "anyhow", "arc-swap", @@ -2262,7 +2262,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bd61aebe485c2c15328c008154021a286c6da8c3#bd61aebe485c2c15328c008154021a286c6da8c3" dependencies = [ "anyhow", "bytes", @@ -2282,7 +2282,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bd61aebe485c2c15328c008154021a286c6da8c3#bd61aebe485c2c15328c008154021a286c6da8c3" dependencies = [ "anyhow", "arc-swap", @@ -2304,7 +2304,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bd61aebe485c2c15328c008154021a286c6da8c3#bd61aebe485c2c15328c008154021a286c6da8c3" dependencies = [ "anyhow", "async-recursion", @@ -2407,7 +2407,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=e114e8577626498f82b29c31dac29d10cab56bdd#e114e8577626498f82b29c31dac29d10cab56bdd" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bd61aebe485c2c15328c008154021a286c6da8c3#bd61aebe485c2c15328c008154021a286c6da8c3" dependencies = [ "anyhow", "collab", diff --git a/Cargo.toml b/Cargo.toml index 5759d60dd..717822c94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -312,13 +312,13 @@ lto = false # Disable Link-Time Optimization [patch.crates-io] # It's diffcult to resovle different version with the same crate used in AppFlowy Frontend and the Client-API crate. # So using patch to workaround this issue. -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } -collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "e114e8577626498f82b29c31dac29d10cab56bdd" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bd61aebe485c2c15328c008154021a286c6da8c3" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bd61aebe485c2c15328c008154021a286c6da8c3" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bd61aebe485c2c15328c008154021a286c6da8c3" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bd61aebe485c2c15328c008154021a286c6da8c3" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bd61aebe485c2c15328c008154021a286c6da8c3" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bd61aebe485c2c15328c008154021a286c6da8c3" } +collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bd61aebe485c2c15328c008154021a286c6da8c3" } [features] history = [] diff --git a/tests/collab/database_crud.rs b/tests/collab/database_crud.rs index 97ff70a5d..f3343b263 100644 --- a/tests/collab/database_crud.rs +++ b/tests/collab/database_crud.rs @@ -1,6 +1,5 @@ use client_api_test::{generate_unique_registered_user_client, workspace_id_from_client}; use collab_database::entity::FieldType; -use serde_json::json; use shared_entity::dto::workspace_dto::AFInsertDatabaseField; #[tokio::test] @@ -89,19 +88,10 @@ async fn database_fields_crud() { .unwrap(); assert_eq!(row_details.len(), 1); let new_row_detail = &row_details[0]; - assert_eq!( - new_row_detail.cells["Description"]["data"], - json!(my_description).to_string() - ); - assert_eq!(new_row_detail.cells["Status"]["data"][0]["name"], my_status); - assert_eq!( - new_row_detail.cells["Multiselect"]["data"][0]["name"], - "social" - ); - assert_eq!( - new_row_detail.cells["Multiselect"]["data"][1]["name"], - "news" - ); + assert_eq!(new_row_detail.cells["Description"]["data"], my_description); + assert_eq!(new_row_detail.cells["Status"]["data"], my_status); + assert_eq!(new_row_detail.cells["Multiselect"]["data"][0], "social"); + assert_eq!(new_row_detail.cells["Multiselect"]["data"][1], "news"); assert_eq!(new_row_detail.cells["MyNumberColumn"]["data"], "123"); assert_eq!( new_row_detail.cells["MyDateTimeColumn"]["data"]["timestamp"], @@ -109,8 +99,8 @@ async fn database_fields_crud() { ); assert_eq!( new_row_detail.cells["MyUrlField"]["data"], - json!({"data": json!("https://appflowy.io").to_string()}).to_string() + "https://appflowy.io" ); - assert_eq!(new_row_detail.cells["MyCheckboxColumn"]["data"], "true"); + assert_eq!(new_row_detail.cells["MyCheckboxColumn"]["data"], true); } } From c6b39811ed50a6e50ff3236dc254e88b7e6c9bb4 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 6 Dec 2024 12:14:24 +0800 Subject: [PATCH 13/28] chore: review issues --- src/biz/collab/ops.rs | 75 ++++++++++++++------------------ src/biz/collab/utils.rs | 35 ++++++++++----- src/biz/workspace/page_view.rs | 1 + src/biz/workspace/publish_dup.rs | 13 +----- 4 files changed, 58 insertions(+), 66 deletions(-) diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index b8f7938a4..09665198f 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -6,6 +6,8 @@ use appflowy_collaborate::collab::storage::CollabAccessControlStorage; use chrono::DateTime; use chrono::Utc; use collab::preclude::Collab; +use collab_database::database::gen_field_id; +use collab_database::database::gen_row_id; use collab_database::entity::FieldType; use collab_database::fields::Field; use collab_database::fields::TypeOptions; @@ -14,7 +16,6 @@ use collab_database::rows::CreateRowParams; use collab_database::rows::DatabaseRowBody; use collab_database::rows::Row; use collab_database::rows::RowDetail; -use collab_database::views::FieldOrder; use collab_database::views::OrderObjectPosition; use collab_database::workspace_database::WorkspaceDatabase; use collab_database::workspace_database::WorkspaceDatabaseBody; @@ -484,8 +485,6 @@ pub async fn insert_database_row( uid: i64, cell_value_by_id: HashMap, ) -> Result { - let mut db_txn = pg_pool.begin().await?; - // get database types and type options let (mut db_collab, db_body) = get_database_body(collab_storage, workspace_uuid_str, database_uuid_str).await?; @@ -498,15 +497,14 @@ pub async fn insert_database_row( let type_option_reader_by_id = type_option_writer_by_id(&all_fields); let field_by_name = field_by_name_uniq(all_fields); - let new_db_row_id = uuid::Uuid::new_v4().to_string(); - + let new_db_row_id = gen_row_id(); let mut new_db_row_collab = Collab::new_with_origin(CollabOrigin::Empty, new_db_row_id.clone(), vec![], false); let new_db_row_body = { let database_body = DatabaseRowBody::create( - new_db_row_id.clone().into(), + new_db_row_id.clone(), &mut new_db_row_collab, - Row::empty(new_db_row_id.clone().into(), database_uuid_str), + Row::empty(new_db_row_id.clone(), database_uuid_str), ); let mut txn = new_db_row_collab.transact_mut(); for (id, serde_val) in cell_value_by_id { @@ -543,26 +541,10 @@ pub async fn insert_database_row( }; let db_row_ec_v1 = encode_collab_v1_bytes(&new_db_row_collab, CollabType::DatabaseRow)?; - // insert row - collab_storage - .insert_new_collab_with_transaction( - workspace_uuid_str, - &uid, - CollabParams { - object_id: new_db_row_id.clone(), - encoded_collab_v1: db_row_ec_v1.into(), - collab_type: CollabType::DatabaseRow, - embeddings: None, - }, - &mut db_txn, - "inserting new database row from server", - ) - .await?; - let ts_now = chrono::Utc::now().timestamp(); let row_order = db_body .create_row(CreateRowParams { - id: new_db_row_id.clone().into(), + id: new_db_row_id.clone(), database_id: database_uuid_str.to_string(), cells: new_db_row_body .cells(&new_db_row_collab.transact()) @@ -592,6 +574,24 @@ pub async fn insert_database_row( }; let updated_db_collab = encode_collab_v1_bytes(&db_collab, CollabType::Database)?; + let mut db_txn = pg_pool.begin().await?; + // insert row + collab_storage + .insert_new_collab_with_transaction( + workspace_uuid_str, + &uid, + CollabParams { + object_id: new_db_row_id.to_string(), + encoded_collab_v1: db_row_ec_v1.into(), + collab_type: CollabType::DatabaseRow, + embeddings: None, + }, + &mut db_txn, + "inserting new database row from server", + ) + .await?; + + // update database collab_storage .insert_new_collab_with_transaction( workspace_uuid_str, @@ -609,7 +609,7 @@ pub async fn insert_database_row( db_txn.commit().await?; broadcast_update(collab_storage, database_uuid_str, db_collab_update).await?; - Ok(new_db_row_id) + Ok(new_db_row_id.to_string()) } pub async fn get_database_fields( @@ -648,12 +648,7 @@ pub async fn add_database_field( let (mut db_collab, db_body) = get_database_body(collab_storage, workspace_id, database_id).await?; - let new_id = uuid::Uuid::new_v4() - .to_string() - .chars() - .take(6) - .collect::(); - + let new_id = gen_field_id(); let mut type_options = TypeOptions::new(); let type_option_data = insert_field .type_option_data @@ -678,19 +673,15 @@ pub async fn add_database_field( let db_collab_update = { let mut yrs_txn = db_collab.transact_mut(); - db_body.fields.insert_field(&mut yrs_txn, new_field); - let mut db_views = db_body.views.get_all_views(&yrs_txn); - for db_view in db_views.iter_mut() { - db_view.field_orders.push(FieldOrder { id: new_id.clone() }); - } - db_body.views.clear(&mut yrs_txn); - for view in db_views { - db_body.views.insert_view(&mut yrs_txn, view); - } - + db_body.create_field( + &mut yrs_txn, + None, + new_field, + &OrderObjectPosition::End, + &HashMap::new(), + ); yrs_txn.encode_update_v1() }; - let updated_db_collab = encode_collab_v1_bytes(&db_collab, CollabType::Database)?; let mut pg_txn = pg_pool.begin().await?; diff --git a/src/biz/collab/utils.rs b/src/biz/collab/utils.rs index 396be2e15..5cceee8d9 100644 --- a/src/biz/collab/utils.rs +++ b/src/biz/collab/utils.rs @@ -176,18 +176,6 @@ pub fn type_options_serde( result } -pub fn collab_from_doc_state(doc_state: Vec, object_id: &str) -> Result { - let collab = Collab::new_with_source( - CollabOrigin::Server, - object_id, - DataSource::DocStateV1(doc_state), - vec![], - false, - ) - .map_err(|e| AppError::Unhandled(e.to_string()))?; - Ok(collab) -} - pub async fn get_database_body( collab_storage: &CollabAccessControlStorage, workspace_uuid_str: &str, @@ -265,3 +253,26 @@ pub async fn get_latest_collab( })?; Ok(collab) } + +pub async fn collab_to_bin(collab: Collab, collab_type: CollabType) -> Result, AppError> { + tokio::task::spawn_blocking(move || { + let bin = collab + .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) + .map_err(|e| AppError::Unhandled(e.to_string()))? + .encode_to_bytes()?; + Ok(bin) + }) + .await? +} + +pub fn collab_from_doc_state(doc_state: Vec, object_id: &str) -> Result { + let collab = Collab::new_with_source( + CollabOrigin::Server, + object_id, + DataSource::DocStateV1(doc_state), + vec![], + false, + ) + .map_err(|e| AppError::Unhandled(e.to_string()))?; + Ok(collab) +} diff --git a/src/biz/workspace/page_view.rs b/src/biz/workspace/page_view.rs index bc4ce7855..b62ef3017 100644 --- a/src/biz/workspace/page_view.rs +++ b/src/biz/workspace/page_view.rs @@ -1242,6 +1242,7 @@ pub async fn update_page_collab_data( let encode_collab = collab_access_control_storage .get_encode_collab(GetCollabOrigin::User { uid }, param, true) .await?; + let mut collab = collab_from_doc_state(encode_collab.doc_state.to_vec(), &object_id.to_string())?; appflowy_web_metrics.record_update_size_bytes(doc_state.len()); let update = Update::decode_v1(doc_state).map_err(|e| { diff --git a/src/biz/workspace/publish_dup.rs b/src/biz/workspace/publish_dup.rs index a4dd70faf..b0a9887f9 100644 --- a/src/biz/workspace/publish_dup.rs +++ b/src/biz/workspace/publish_dup.rs @@ -3,7 +3,6 @@ use appflowy_collaborate::collab::storage::CollabAccessControlStorage; use anyhow::anyhow; use bytes::Bytes; -use collab::preclude::Collab; use collab_database::database::gen_row_id; use collab_database::database::DatabaseBody; use collab_database::entity::FieldType; @@ -44,6 +43,7 @@ use yrs::{Map, MapRef}; use crate::biz::collab::folder_view::to_folder_view_icon; use crate::biz::collab::folder_view::to_folder_view_layout; use crate::biz::collab::utils::collab_from_doc_state; +use crate::biz::collab::utils::collab_to_bin; use crate::biz::collab::utils::get_latest_collab_encoded; use super::ops::broadcast_update; @@ -1171,14 +1171,3 @@ fn add_to_view_info(acc: &mut HashMap, view_infos: &[Pu } } } - -async fn collab_to_bin(collab: Collab, collab_type: CollabType) -> Result, AppError> { - tokio::task::spawn_blocking(move || { - let bin = collab - .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) - .map_err(|e| AppError::Unhandled(e.to_string()))? - .encode_to_bytes()?; - Ok(bin) - }) - .await? -} From 058e9408055c6dd97def3c05a8fe05b2027239fd Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 6 Dec 2024 14:07:03 +0800 Subject: [PATCH 14/28] fix: code review feedback --- src/biz/collab/ops.rs | 12 ++++++++---- src/biz/collab/utils.rs | 25 ++++++++----------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index c0ad74fc2..a515cb005 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -68,7 +68,7 @@ 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; use super::utils::collab_from_doc_state; -use super::utils::encode_collab_v1_bytes; +use super::utils::collab_to_bin; use super::utils::field_by_id_name_uniq; use super::utils::get_database_body; use super::utils::get_latest_collab; @@ -500,6 +500,7 @@ pub async fn insert_database_row( let new_db_row_id = gen_row_id(); let mut new_db_row_collab = Collab::new_with_origin(CollabOrigin::Empty, new_db_row_id.clone(), vec![], false); + let new_db_row_body = { let database_body = DatabaseRowBody::create( new_db_row_id.clone(), @@ -539,8 +540,8 @@ pub async fn insert_database_row( } database_body }; - let db_row_ec_v1 = encode_collab_v1_bytes(&new_db_row_collab, CollabType::DatabaseRow)?; + // Create new row order let ts_now = chrono::Utc::now().timestamp(); let row_order = db_body .create_row(CreateRowParams { @@ -558,6 +559,9 @@ pub async fn insert_database_row( .await .map_err(|e| AppError::Internal(anyhow::anyhow!("Failed to create row: {:?}", e)))?; + // Prepare new row collab binary to store in postgres + let db_row_ec_v1 = collab_to_bin(new_db_row_collab, CollabType::DatabaseRow).await?; + // For each database view, add the new row order let db_collab_update = { let mut txn = db_collab.transact_mut(); @@ -572,7 +576,7 @@ pub async fn insert_database_row( txn.encode_update_v1() }; - let updated_db_collab = encode_collab_v1_bytes(&db_collab, CollabType::Database)?; + let updated_db_collab = collab_to_bin(db_collab, CollabType::Database).await?; let mut db_txn = pg_pool.begin().await?; // insert row @@ -682,7 +686,7 @@ pub async fn add_database_field( ); yrs_txn.encode_update_v1() }; - let updated_db_collab = encode_collab_v1_bytes(&db_collab, CollabType::Database)?; + let updated_db_collab = collab_to_bin(db_collab, CollabType::Database).await?; let mut pg_txn = pg_pool.begin().await?; collab_storage diff --git a/src/biz/collab/utils.rs b/src/biz/collab/utils.rs index 5cceee8d9..d9124ecab 100644 --- a/src/biz/collab/utils.rs +++ b/src/biz/collab/utils.rs @@ -51,10 +51,7 @@ pub fn get_row_details_by_id( for (field_id, field) in field_by_id_name_uniq { let cell: Cell = match cells.get(field_id) { Some(cell) => cell.clone(), - None => { - tracing::error!("Failed to get cell by field id: {}", field.id); - Cell::new() - }, + None => Cell::new(), }; let cell_value = cell_data_to_serde(cell, field, type_option_reader_by_id); row_details.insert( @@ -116,7 +113,8 @@ pub fn type_option_writer_by_id( let field_id: String = field.id.clone(); let type_option_reader: Box = { let field_type: &FieldType = &FieldType::from(field.field_type); - let type_option_data: TypeOptionData = match field.type_options.get(&field_type.type_id()) { + let type_option_data: TypeOptionData = match field.get_any_type_option(&field_type.type_id()) + { Some(tod) => tod.clone(), None => HashMap::new(), }; @@ -137,7 +135,8 @@ pub fn type_option_reader_by_id( let field_id: String = field.id.clone(); let type_option_reader: Box = { let field_type: &FieldType = &FieldType::from(field.field_type); - let type_option_data: TypeOptionData = match field.type_options.get(&field_type.type_id()) { + let type_option_data: TypeOptionData = match field.get_any_type_option(&field_type.type_id()) + { Some(tod) => tod.clone(), None => HashMap::new(), }; @@ -161,6 +160,9 @@ pub fn type_options_serde( for (key, value) in type_option { match field_type { FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Media => { + // Certain type option are stored as stringified JSON + // We need to parse them back to JSON + // e.g. "{ \"key\": \"value\" }" -> { "key": "value" } 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); @@ -203,17 +205,6 @@ pub async fn get_database_body( Ok((db_collab, db_body)) } -pub fn encode_collab_v1_bytes( - collab: &Collab, - collab_type: CollabType, -) -> Result, AppError> { - let bs = collab - .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) - .map_err(|e| AppError::Unhandled(e.to_string()))? - .encode_to_bytes()?; - Ok(bs) -} - pub async fn get_latest_collab_encoded( collab_storage: &CollabAccessControlStorage, collab_origin: GetCollabOrigin, From 9cb8ea14a5a3d1cf4b38fc6b328b8899083f019d Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Fri, 6 Dec 2024 18:12:41 +0800 Subject: [PATCH 15/28] feat: filter only allow supported field types --- src/api/workspace.rs | 12 +++++++++ src/biz/collab/ops.rs | 12 ++++++++- tests/collab/database_crud.rs | 48 +++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/api/workspace.rs b/src/api/workspace.rs index aae30338f..7af9b9138 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -7,6 +7,7 @@ use anyhow::{anyhow, Context}; use bytes::BytesMut; use chrono::{DateTime, Duration, Utc}; use collab::entity::EncodedCollab; +use collab_database::entity::FieldType; use collab_entity::CollabType; use futures_util::future::try_join_all; use prost::Message as ProstMessage; @@ -2042,12 +2043,23 @@ async fn list_database_row_details_handler( .enforce_action(&uid, &workspace_id, Action::Read) .await?; + static SUPPORTED_FIELD_TYPES: &[FieldType] = &[ + FieldType::RichText, + FieldType::Number, + FieldType::DateTime, + FieldType::SingleSelect, + FieldType::MultiSelect, + FieldType::Checkbox, + FieldType::URL, + ]; + let db_rows = biz::collab::ops::list_database_row_details( &state.collab_access_control_storage, uid, workspace_id, db_id, &row_ids, + SUPPORTED_FIELD_TYPES, ) .await?; Ok(Json(AppResponse::Ok().with_data(db_rows))) diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index a515cb005..fff07add0 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -735,11 +735,21 @@ pub async fn list_database_row_details( workspace_uuid_str: String, database_uuid_str: String, row_ids: &[&str], + supported_field_types: &[FieldType], ) -> Result, AppError> { let (database_collab, db_body) = get_database_body(collab_storage, &workspace_uuid_str, &database_uuid_str).await?; - let all_fields = db_body.fields.get_all_fields(&database_collab.transact()); + let all_fields: Vec = db_body + .fields + .get_all_fields(&database_collab.transact()) + .into_iter() + .filter(|field| supported_field_types.contains(&FieldType::from(field.field_type))) + .collect(); + if all_fields.is_empty() { + return Ok(vec![]); + } + let type_option_reader_by_id = type_option_reader_by_id(&all_fields); let field_by_id = field_by_id_name_uniq(all_fields); let query_collabs: Vec = row_ids diff --git a/tests/collab/database_crud.rs b/tests/collab/database_crud.rs index f3343b263..cfaffd3c0 100644 --- a/tests/collab/database_crud.rs +++ b/tests/collab/database_crud.rs @@ -104,3 +104,51 @@ async fn database_fields_crud() { assert_eq!(new_row_detail.cells["MyCheckboxColumn"]["data"], true); } } + +#[tokio::test] +async fn database_fields_unsupported_field_type() { + let (c, _user) = generate_unique_registered_user_client().await; + let workspace_id = workspace_id_from_client(&c).await; + let databases = c.list_databases(&workspace_id).await.unwrap(); + assert_eq!(databases.len(), 1); + let todo_db = &databases[0]; + + let my_rel_field_id = { + c.add_database_field( + &workspace_id, + &todo_db.id, + &AFInsertDatabaseField { + name: "MyRelationCol".to_string(), + field_type: FieldType::Relation.into(), + ..Default::default() + }, + ) + .await + .unwrap() + }; + { + let my_description = "my task 123"; + let my_status = "To Do"; + let new_row_id = c + .add_database_item( + &workspace_id, + &todo_db.id, + &serde_json::json!({ + "Description": my_description, + "Status": my_status, + "Multiselect": ["social", "news"], + my_rel_field_id: "relation_data" + }), + ) + .await + .unwrap(); + + let row_details = c + .list_database_row_details(&workspace_id, &todo_db.id, &[&new_row_id]) + .await + .unwrap(); + assert_eq!(row_details.len(), 1); + let new_row_detail = &row_details[0]; + assert!(!new_row_detail.cells.contains_key("MyRelationCol")); + } +} From 11743f7a794a137ca66697dbeb5a4419c91ebaae Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Sat, 7 Dec 2024 22:52:10 +0800 Subject: [PATCH 16/28] feat: support more field types --- src/api/workspace.rs | 12 ++---------- src/biz/collab/ops.rs | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/api/workspace.rs b/src/api/workspace.rs index 7af9b9138..3f711047f 100644 --- a/src/api/workspace.rs +++ b/src/api/workspace.rs @@ -2043,15 +2043,7 @@ async fn list_database_row_details_handler( .enforce_action(&uid, &workspace_id, Action::Read) .await?; - static SUPPORTED_FIELD_TYPES: &[FieldType] = &[ - FieldType::RichText, - FieldType::Number, - FieldType::DateTime, - FieldType::SingleSelect, - FieldType::MultiSelect, - FieldType::Checkbox, - FieldType::URL, - ]; + static UNSUPPORTED_FIELD_TYPES: &[FieldType] = &[FieldType::Relation]; let db_rows = biz::collab::ops::list_database_row_details( &state.collab_access_control_storage, @@ -2059,7 +2051,7 @@ async fn list_database_row_details_handler( workspace_id, db_id, &row_ids, - SUPPORTED_FIELD_TYPES, + UNSUPPORTED_FIELD_TYPES, ) .await?; Ok(Json(AppResponse::Ok().with_data(db_rows))) diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index fff07add0..56a9fec94 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -735,7 +735,7 @@ pub async fn list_database_row_details( workspace_uuid_str: String, database_uuid_str: String, row_ids: &[&str], - supported_field_types: &[FieldType], + unsupported_field_types: &[FieldType], ) -> Result, AppError> { let (database_collab, db_body) = get_database_body(collab_storage, &workspace_uuid_str, &database_uuid_str).await?; @@ -744,7 +744,7 @@ pub async fn list_database_row_details( .fields .get_all_fields(&database_collab.transact()) .into_iter() - .filter(|field| supported_field_types.contains(&FieldType::from(field.field_type))) + .filter(|field| !unsupported_field_types.contains(&FieldType::from(field.field_type))) .collect(); if all_fields.is_empty() { return Ok(vec![]); From edb4769dedfc7e04badd4d1e4a06ff2d40810dae Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 9 Dec 2024 09:44:48 +0800 Subject: [PATCH 17/28] feat: support created at and last modified --- Cargo.lock | 14 +++++------ Cargo.toml | 14 +++++------ src/biz/collab/ops.rs | 9 ++----- src/biz/collab/utils.rs | 52 ++++++++++++++++++++++------------------- 4 files changed, 44 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19210a80e..b3d1adada 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2133,7 +2133,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=295508f3a8c13b39ad084ec34db423ece99fb182#295508f3a8c13b39ad084ec34db423ece99fb182" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" dependencies = [ "anyhow", "arc-swap", @@ -2158,7 +2158,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=295508f3a8c13b39ad084ec34db423ece99fb182#295508f3a8c13b39ad084ec34db423ece99fb182" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" dependencies = [ "anyhow", "async-trait", @@ -2197,7 +2197,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=295508f3a8c13b39ad084ec34db423ece99fb182#295508f3a8c13b39ad084ec34db423ece99fb182" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" dependencies = [ "anyhow", "arc-swap", @@ -2218,7 +2218,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=295508f3a8c13b39ad084ec34db423ece99fb182#295508f3a8c13b39ad084ec34db423ece99fb182" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" dependencies = [ "anyhow", "bytes", @@ -2238,7 +2238,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=295508f3a8c13b39ad084ec34db423ece99fb182#295508f3a8c13b39ad084ec34db423ece99fb182" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" dependencies = [ "anyhow", "arc-swap", @@ -2260,7 +2260,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=295508f3a8c13b39ad084ec34db423ece99fb182#295508f3a8c13b39ad084ec34db423ece99fb182" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" dependencies = [ "anyhow", "async-recursion", @@ -2363,7 +2363,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=295508f3a8c13b39ad084ec34db423ece99fb182#295508f3a8c13b39ad084ec34db423ece99fb182" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" dependencies = [ "anyhow", "collab", diff --git a/Cargo.toml b/Cargo.toml index 267054574..af20e6f23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -312,13 +312,13 @@ lto = false # Disable Link-Time Optimization [patch.crates-io] # It's diffcult to resovle different version with the same crate used in AppFlowy Frontend and the Client-API crate. # So using patch to workaround this issue. -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "295508f3a8c13b39ad084ec34db423ece99fb182" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "295508f3a8c13b39ad084ec34db423ece99fb182" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "295508f3a8c13b39ad084ec34db423ece99fb182" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "295508f3a8c13b39ad084ec34db423ece99fb182" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "295508f3a8c13b39ad084ec34db423ece99fb182" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "295508f3a8c13b39ad084ec34db423ece99fb182" } -collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "295508f3a8c13b39ad084ec34db423ece99fb182" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } +collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } [features] history = [] diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index 56a9fec94..7ebbd9b84 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -73,7 +73,7 @@ use super::utils::field_by_id_name_uniq; use super::utils::get_database_body; use super::utils::get_latest_collab; use super::utils::get_latest_collab_encoded; -use super::utils::get_row_details_by_id; +use super::utils::get_row_details_serde; use super::utils::type_option_reader_by_id; use super::utils::type_option_writer_by_id; use super::utils::type_options_serde; @@ -787,12 +787,7 @@ pub async fn list_database_row_details( return None; }, }; - - let cells = get_row_details_by_id( - row_detail.row.cells, - &field_by_id, - &type_option_reader_by_id, - ); + let cells = get_row_details_serde(row_detail, &field_by_id, &type_option_reader_by_id); Some(AFDatabaseRowDetail { id, cells }) }, QueryCollabResult::Failed { error } => { diff --git a/src/biz/collab/utils.rs b/src/biz/collab/utils.rs index d9124ecab..43c46ccfd 100644 --- a/src/biz/collab/utils.rs +++ b/src/biz/collab/utils.rs @@ -12,8 +12,9 @@ use collab_database::fields::TypeOptionCellWriter; use collab_database::fields::TypeOptionData; use collab_database::fields::TypeOptions; use collab_database::rows::Cell; -use collab_database::rows::Cells; +use collab_database::rows::RowDetail; use collab_database::template::entity::CELL_DATA; +use collab_database::template::timestamp_parse::TimestampCellData; use collab_database::workspace_database::NoPersistenceDatabaseCollabService; use collab_entity::CollabType; use collab_entity::EncodedCollab; @@ -26,41 +27,44 @@ use std::collections::HashMap; use std::collections::HashSet; use std::sync::Arc; -pub fn cell_data_to_serde( - cell_data: Cell, - field: &Field, - type_option_reader_by_id: &HashMap>, -) -> serde_json::Value { - match type_option_reader_by_id.get(&field.id) { - Some(tor) => tor.json_cell(&cell_data), - None => { - tracing::error!("Failed to get type option reader by id: {}", field.id); - serde_json::Value::Null - }, - } -} - -pub fn get_row_details_by_id( - cells: Cells, +pub fn get_row_details_serde( + row_detail: RowDetail, field_by_id_name_uniq: &HashMap, type_option_reader_by_id: &HashMap>, ) -> HashMap> { - let mut row_details: HashMap> = + let mut cells = row_detail.row.cells; + let mut row_details_serde: HashMap> = HashMap::with_capacity(cells.len()); - for (field_id, field) in field_by_id_name_uniq { - let cell: Cell = match cells.get(field_id) { + let cell: Cell = match cells.remove(field_id) { Some(cell) => cell.clone(), - None => Cell::new(), + None => { + let field_type = FieldType::from(field.field_type); + match field_type { + FieldType::CreatedTime => { + TimestampCellData::new(Some(row_detail.row.created_at)).to_cell(field_type) + }, + FieldType::LastEditedTime => { + TimestampCellData::new(Some(row_detail.row.modified_at)).to_cell(field_type) + }, + _ => Cell::new(), + } + }, + }; + let cell_value = match type_option_reader_by_id.get(&field.id) { + Some(tor) => tor.json_cell(&cell), + None => { + tracing::error!("Failed to get type option reader by id: {}", field.id); + serde_json::Value::Null + }, }; - let cell_value = cell_data_to_serde(cell, field, type_option_reader_by_id); - row_details.insert( + row_details_serde.insert( field.name.clone(), HashMap::from([(CELL_DATA.to_string(), cell_value)]), ); } - row_details + row_details_serde } /// create a map of field name to field From 974b697ba9fd325f335ca5787ca642eebb989d8d Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 9 Dec 2024 16:56:26 +0800 Subject: [PATCH 18/28] feat: add timestamp cell for created at and modified at fields --- Cargo.lock | 14 +++++++------- Cargo.toml | 14 +++++++------- src/biz/collab/ops.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3d1adada..b91f73494 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2133,7 +2133,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" dependencies = [ "anyhow", "arc-swap", @@ -2158,7 +2158,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" dependencies = [ "anyhow", "async-trait", @@ -2197,7 +2197,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" dependencies = [ "anyhow", "arc-swap", @@ -2218,7 +2218,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" dependencies = [ "anyhow", "bytes", @@ -2238,7 +2238,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" dependencies = [ "anyhow", "arc-swap", @@ -2260,7 +2260,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" dependencies = [ "anyhow", "async-recursion", @@ -2363,7 +2363,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ecb589280f67a07402fbaa053a08f847ccdc2a83#ecb589280f67a07402fbaa053a08f847ccdc2a83" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" dependencies = [ "anyhow", "collab", diff --git a/Cargo.toml b/Cargo.toml index af20e6f23..5841ef46e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -312,13 +312,13 @@ lto = false # Disable Link-Time Optimization [patch.crates-io] # It's diffcult to resovle different version with the same crate used in AppFlowy Frontend and the Client-API crate. # So using patch to workaround this issue. -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } -collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "ecb589280f67a07402fbaa053a08f847ccdc2a83" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } +collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } [features] history = [] diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index 2f0e84ec2..d023f43e7 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -1,3 +1,4 @@ +use std::cell::OnceCell; use std::collections::HashMap; use std::sync::Arc; @@ -9,7 +10,9 @@ use collab::preclude::Collab; use collab_database::database::gen_field_id; use collab_database::database::gen_row_id; use collab_database::entity::FieldType; +use collab_database::fields::timestamp_type_option::TimestampTypeOption; use collab_database::fields::Field; +use collab_database::fields::TypeOptionCellWriter; use collab_database::fields::TypeOptions; use collab_database::rows::Cell; use collab_database::rows::CreateRowParams; @@ -514,6 +517,33 @@ pub async fn insert_database_row( Row::empty(new_db_row_id.clone(), database_uuid_str), ); let mut txn = new_db_row_collab.transact_mut(); + + + // Create a OnceCell to hold the timestamp + let timestamp_cell: OnceCell = OnceCell::new(); + let get_timestamp = || timestamp_cell.get_or_init(|| Utc::now().timestamp().to_string()); + + // Insert `created_at` and `modified_at` fields (if any) + for (field_id, field) in &field_by_name { + let field_type = FieldType::from(field.field_type); + match field_type { + FieldType::LastEditedTime | FieldType::CreatedTime => { + let new_cell = match type_option_reader_by_id.get(field_id.as_str()) { + Some(cell_writer) => cell_writer.convert_json_to_cell(get_timestamp().as_str().into()), + None => { + TimestampTypeOption::default().convert_json_to_cell(get_timestamp().as_str().into()) + }, + }; + database_body.update(&mut txn, |row_update| { + row_update.update_cells(|cells_update| { + cells_update.insert_cell(&field.id, new_cell); + }); + }); + }, + _ => {}, + } + } + for (id, serde_val) in cell_value_by_id { let field = match field_by_id.get(&id) { Some(f) => f, From a9d061925153bd3d62ae3a968f99d70d5b21c920 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Tue, 10 Dec 2024 00:30:04 +0800 Subject: [PATCH 19/28] chore: linting and formatting --- libs/client-api/src/http_chat.rs | 4 ++-- libs/client-api/src/http_file.rs | 2 +- libs/collab-stream/src/pubsub.rs | 2 +- libs/collab-stream/src/stream.rs | 8 ++++---- libs/collab-stream/src/stream_group.rs | 16 ++++++++-------- libs/database/src/collab/collab_db_ops.rs | 1 - rust-toolchain.toml | 2 +- .../src/collab/cache/mem_cache.rs | 6 +++--- src/biz/collab/ops.rs | 1 - src/biz/collab/utils.rs | 6 ++---- 10 files changed, 22 insertions(+), 26 deletions(-) diff --git a/libs/client-api/src/http_chat.rs b/libs/client-api/src/http_chat.rs index cc5ea5834..36f015a8b 100644 --- a/libs/client-api/src/http_chat.rs +++ b/libs/client-api/src/http_chat.rs @@ -320,7 +320,7 @@ impl Stream for QuestionStream { fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.project(); - return match ready!(this.stream.as_mut().poll_next(cx)) { + match ready!(this.stream.as_mut().poll_next(cx)) { Some(Ok(value)) => match value { Value::Object(mut value) => { if let Some(metadata) = value.remove(STREAM_METADATA_KEY) { @@ -344,6 +344,6 @@ impl Stream for QuestionStream { }, Some(Err(err)) => Poll::Ready(Some(Err(err))), None => Poll::Ready(None), - }; + } } } diff --git a/libs/client-api/src/http_file.rs b/libs/client-api/src/http_file.rs index ad4fe5546..1ef9a7312 100644 --- a/libs/client-api/src/http_file.rs +++ b/libs/client-api/src/http_file.rs @@ -290,7 +290,7 @@ impl WSClientConnectURLProvider for Client { /// /// # Returns /// A `Result` containing the base64-encoded MD5 hash on success, or an error if the file cannot be read. - +/// /// Asynchronously calculates the MD5 hash of a file using efficient buffer handling and returns it as a base64-encoded string. /// /// # Arguments diff --git a/libs/collab-stream/src/pubsub.rs b/libs/collab-stream/src/pubsub.rs index abe88038f..1bbe479ab 100644 --- a/libs/collab-stream/src/pubsub.rs +++ b/libs/collab-stream/src/pubsub.rs @@ -50,7 +50,7 @@ impl CollabStreamPub { #[instrument(level = "debug", skip_all, err)] pub async fn publish(&mut self, message: PubSubMessage) -> Result<(), StreamError> { - self.conn.publish(ACTIVE_COLLAB_CHANNEL, message).await?; + let () = self.conn.publish(ACTIVE_COLLAB_CHANNEL, message).await?; Ok(()) } } diff --git a/libs/collab-stream/src/stream.rs b/libs/collab-stream/src/stream.rs index a69222dc5..148bceb24 100644 --- a/libs/collab-stream/src/stream.rs +++ b/libs/collab-stream/src/stream.rs @@ -2,7 +2,7 @@ use crate::error::StreamError; use crate::model::{MessageId, StreamBinary, StreamMessage, StreamMessageByStreamKey}; use redis::aio::ConnectionManager; use redis::streams::{StreamMaxlen, StreamReadOptions}; -use redis::{pipe, AsyncCommands, RedisError}; +use redis::{pipe, AsyncCommands, Pipeline, RedisError}; pub struct CollabStream { connection_manager: ConnectionManager, @@ -34,9 +34,9 @@ impl CollabStream { let mut pipe = pipe(); for message in messages { let tuple = message.into_tuple_array(); - pipe.xadd(&self.stream_key, "*", tuple.as_slice()); + let _: &mut Pipeline = pipe.xadd(&self.stream_key, "*", tuple.as_slice()); } - pipe.query_async(&mut self.connection_manager).await?; + let () = pipe.query_async(&mut self.connection_manager).await?; Ok(()) } @@ -90,7 +90,7 @@ impl CollabStream { } pub async fn clear(&mut self) -> Result<(), RedisError> { - self + let () = self .connection_manager .xtrim(&self.stream_key, StreamMaxlen::Equals(0)) .await?; diff --git a/libs/collab-stream/src/stream_group.rs b/libs/collab-stream/src/stream_group.rs index 55fe754ec..8361f9eb7 100644 --- a/libs/collab-stream/src/stream_group.rs +++ b/libs/collab-stream/src/stream_group.rs @@ -7,7 +7,7 @@ use redis::streams::{ StreamClaimOptions, StreamClaimReply, StreamMaxlen, StreamPendingData, StreamPendingReply, StreamReadOptions, }; -use redis::{pipe, AsyncCommands, ErrorKind, RedisResult}; +use redis::{pipe, AsyncCommands, ErrorKind, Pipeline, RedisResult}; use tokio_util::sync::CancellationToken; use tracing::{error, info, trace, warn}; @@ -119,7 +119,7 @@ impl StreamGroup { .into_iter() .map(|m| m.to_string()) .collect::>(); - self + let () = self .connection_manager .xack(&self.stream_key, &self.group_name, &message_ids) .await?; @@ -164,7 +164,7 @@ impl StreamGroup { let message = message.into(); let tuple = message.into_tuple_array(); if let Some(len) = self.config.max_len { - pipe + let _: &mut Pipeline = pipe .cmd("XADD") .arg(&self.stream_key) .arg("MAXLEN") @@ -177,7 +177,7 @@ impl StreamGroup { } } - pipe.query_async(&mut self.connection_manager).await?; + let () = pipe.query_async(&mut self.connection_manager).await?; if let Err(err) = self.set_expiration().await { error!("set expiration fail: {:?}", err); } @@ -197,13 +197,13 @@ impl StreamGroup { let tuple = message.into_tuple_array(); match self.config.max_len { Some(max_len) => { - self + let () = self .connection_manager .xadd_maxlen(&self.stream_key, StreamMaxlen::Approx(max_len), "*", &tuple) .await?; }, None => { - self + let () = self .connection_manager .xadd(&self.stream_key, "*", tuple.as_slice()) .await?; @@ -369,7 +369,7 @@ impl StreamGroup { /// Use the `XTRIM` command to truncate the Redis stream to a maximum length of zero, effectively /// removing all entries from the stream. pub async fn clear(&mut self) -> Result<(), StreamError> { - self + let () = self .connection_manager .xtrim(&self.stream_key, StreamMaxlen::Equals(0)) .await?; @@ -393,7 +393,7 @@ impl StreamGroup { }; if should_set_expiration { - self + let () = self .connection_manager .expire(&self.stream_key, expire_time) .await?; diff --git a/libs/database/src/collab/collab_db_ops.rs b/libs/database/src/collab/collab_db_ops.rs index 4fdc0653f..41013164a 100644 --- a/libs/database/src/collab/collab_db_ops.rs +++ b/libs/database/src/collab/collab_db_ops.rs @@ -43,7 +43,6 @@ use uuid::Uuid; /// * There's a database operation failure. /// * There's an attempt to insert a row with an existing `object_id` but a different `workspace_id`. /// - #[inline] #[instrument(level = "trace", skip(tx, params), fields(oid=%params.object_id), err)] pub async fn insert_into_af_collab( diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 51985806f..0193dee36 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.78.0" +channel = "1.83.0" diff --git a/services/appflowy-collaborate/src/collab/cache/mem_cache.rs b/services/appflowy-collaborate/src/collab/cache/mem_cache.rs index 69215a887..c79f87277 100644 --- a/services/appflowy-collaborate/src/collab/cache/mem_cache.rs +++ b/services/appflowy-collaborate/src/collab/cache/mem_cache.rs @@ -32,7 +32,7 @@ impl CollabMemCache { pub async fn insert_collab_meta(&self, meta: CollabMetadata) -> Result<(), AppError> { let key = collab_meta_key(&meta.object_id); let value = serde_json::to_string(&meta)?; - self + let () = self .connection_manager .clone() .set_ex(key, value, ONE_MONTH) @@ -188,7 +188,7 @@ impl CollabMemCache { // for executing a subsequent transaction (with MULTI/EXEC). If any of the watched keys are // altered by another client before the current client executes EXEC, the transaction will be // aborted by Redis (the EXEC will return nil indicating the transaction was not processed). - redis::cmd("WATCH") + let () = redis::cmd("WATCH") .arg(&cache_object_id) .query_async::<_, ()>(&mut conn) .await?; @@ -228,7 +228,7 @@ impl CollabMemCache { .ignore() .expire(&cache_object_id, expiration_seconds.unwrap_or(SEVEN_DAYS) as i64) // Setting the expiration to 7 days .ignore(); - pipeline.query_async(&mut conn).await?; + let () = pipeline.query_async(&mut conn).await?; } Ok::<(), redis::RedisError>(()) } diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index d023f43e7..1e2ccfb8d 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -518,7 +518,6 @@ pub async fn insert_database_row( ); let mut txn = new_db_row_collab.transact_mut(); - // Create a OnceCell to hold the timestamp let timestamp_cell: OnceCell = OnceCell::new(); let get_timestamp = || timestamp_cell.get_or_init(|| Utc::now().timestamp().to_string()); diff --git a/src/biz/collab/utils.rs b/src/biz/collab/utils.rs index 43c46ccfd..c89bceb7d 100644 --- a/src/biz/collab/utils.rs +++ b/src/biz/collab/utils.rs @@ -117,8 +117,7 @@ pub fn type_option_writer_by_id( let field_id: String = field.id.clone(); let type_option_reader: Box = { let field_type: &FieldType = &FieldType::from(field.field_type); - let type_option_data: TypeOptionData = match field.get_any_type_option(&field_type.type_id()) - { + let type_option_data: TypeOptionData = match field.get_any_type_option(field_type.type_id()) { Some(tod) => tod.clone(), None => HashMap::new(), }; @@ -139,8 +138,7 @@ pub fn type_option_reader_by_id( let field_id: String = field.id.clone(); let type_option_reader: Box = { let field_type: &FieldType = &FieldType::from(field.field_type); - let type_option_data: TypeOptionData = match field.get_any_type_option(&field_type.type_id()) - { + let type_option_data: TypeOptionData = match field.get_any_type_option(field_type.type_id()) { Some(tod) => tod.clone(), None => HashMap::new(), }; From fdb87d26ce84e612189d9f8e588b7342a9dff257 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Tue, 10 Dec 2024 01:48:36 +0800 Subject: [PATCH 20/28] fix: add created at and last modified --- src/biz/collab/ops.rs | 39 +++++++++------------------------------ 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/src/biz/collab/ops.rs b/src/biz/collab/ops.rs index 1e2ccfb8d..56703bc88 100644 --- a/src/biz/collab/ops.rs +++ b/src/biz/collab/ops.rs @@ -1,4 +1,3 @@ -use std::cell::OnceCell; use std::collections::HashMap; use std::sync::Arc; @@ -10,9 +9,7 @@ use collab::preclude::Collab; use collab_database::database::gen_field_id; use collab_database::database::gen_row_id; use collab_database::entity::FieldType; -use collab_database::fields::timestamp_type_option::TimestampTypeOption; use collab_database::fields::Field; -use collab_database::fields::TypeOptionCellWriter; use collab_database::fields::TypeOptions; use collab_database::rows::Cell; use collab_database::rows::CreateRowParams; @@ -511,37 +508,19 @@ pub async fn insert_database_row( Collab::new_with_origin(CollabOrigin::Empty, new_db_row_id.clone(), vec![], false); let new_db_row_body = { - let database_body = DatabaseRowBody::create( + let db_row_body = DatabaseRowBody::create( new_db_row_id.clone(), &mut new_db_row_collab, Row::empty(new_db_row_id.clone(), database_uuid_str), ); let mut txn = new_db_row_collab.transact_mut(); - // Create a OnceCell to hold the timestamp - let timestamp_cell: OnceCell = OnceCell::new(); - let get_timestamp = || timestamp_cell.get_or_init(|| Utc::now().timestamp().to_string()); - - // Insert `created_at` and `modified_at` fields (if any) - for (field_id, field) in &field_by_name { - let field_type = FieldType::from(field.field_type); - match field_type { - FieldType::LastEditedTime | FieldType::CreatedTime => { - let new_cell = match type_option_reader_by_id.get(field_id.as_str()) { - Some(cell_writer) => cell_writer.convert_json_to_cell(get_timestamp().as_str().into()), - None => { - TimestampTypeOption::default().convert_json_to_cell(get_timestamp().as_str().into()) - }, - }; - database_body.update(&mut txn, |row_update| { - row_update.update_cells(|cells_update| { - cells_update.insert_cell(&field.id, new_cell); - }); - }); - }, - _ => {}, - } - } + // set last_modified and created_at + db_row_body.update(&mut txn, |row_update| { + row_update + .set_last_modified(Utc::now().timestamp()) + .set_created_at(Utc::now().timestamp()); + }); for (id, serde_val) in cell_value_by_id { let field = match field_by_id.get(&id) { @@ -567,13 +546,13 @@ pub async fn insert_database_row( }, }; let new_cell: Cell = cell_writer.convert_json_to_cell(serde_val); - database_body.update(&mut txn, |row_update| { + db_row_body.update(&mut txn, |row_update| { row_update.update_cells(|cells_update| { cells_update.insert_cell(&field.id, new_cell); }); }); } - database_body + db_row_body }; // Create new row order From 1c767eafb5056a135eec2943dff4ddf46638e313 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 9 Dec 2024 20:50:09 +0800 Subject: [PATCH 21/28] fix: ci add service dependency of appflowy cloud on admin frontend --- docker-compose-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index d19ee7224..91510438a 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -151,6 +151,9 @@ services: context: . dockerfile: ./admin_frontend/Dockerfile image: appflowyinc/admin_frontend:${APPFLOWY_ADMIN_FRONTEND_VERSION:-latest} + depends_on: + appflowy_cloud: + condition: service_healthy ports: - 3000:3000 environment: From a85afa1107a30e1b7d5e933af0993b79cdc04bb6 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 9 Dec 2024 20:59:29 +0800 Subject: [PATCH 22/28] chore: trigger ci --- admin_frontend/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/admin_frontend/README.md b/admin_frontend/README.md index a2b8c75c1..59b256c83 100644 --- a/admin_frontend/README.md +++ b/admin_frontend/README.md @@ -15,3 +15,4 @@ - Go to [web server](localhost) - After editing source files, do `docker compose up -d --no-deps --build admin_frontend` - You might need to add `--force-recreate` flag for non build changes to take effect + From 85ea7eb44b89fd62ba421bd507cbecdea18e1046 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 9 Dec 2024 21:13:57 +0800 Subject: [PATCH 23/28] feat: add logging for admin frontend client signin --- admin_frontend/tests/utils/mod.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/admin_frontend/tests/utils/mod.rs b/admin_frontend/tests/utils/mod.rs index 1416fba22..dd2a8125c 100644 --- a/admin_frontend/tests/utils/mod.rs +++ b/admin_frontend/tests/utils/mod.rs @@ -42,6 +42,7 @@ impl AdminFrontendClient { .send() .await .unwrap(); + let resp = check_resp(resp).await; let c = resp.cookies().find(|c| c.name() == "session_id").unwrap(); self.session_id = Some(c.value().to_string()); } @@ -84,3 +85,12 @@ impl AdminFrontendClient { self.session_id.as_ref().unwrap() } } + +async fn check_resp(resp: reqwest::Response) -> reqwest::Response { + if resp.status() != 200 { + println!("resp: {:#?}", resp); + let payload = resp.text().await.unwrap(); + panic!("payload: {:#?}", payload) + } + resp +} From dca63458dd400005a411a35aad5c9476ac5f3215 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 9 Dec 2024 21:54:47 +0800 Subject: [PATCH 24/28] fix: server logs if error --- .github/workflows/integration_test.yml | 6 ++++++ docker-compose-ci.yml | 3 --- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index d67b4f056..3849b92f2 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -132,6 +132,12 @@ jobs: echo "Running tests for ${{ matrix.test_service }} with flags: ${{ matrix.test_cmd }}" RUST_LOG="info" DISABLE_CI_TEST_LOG="true" cargo test ${{ matrix.test_cmd }} + - name: Server Logs + if: failure() + run: | + docker ps -a + docker compose -f docker-compose-ci.yml logs + - name: Docker Logs if: always() run: | diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index 91510438a..d19ee7224 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -151,9 +151,6 @@ services: context: . dockerfile: ./admin_frontend/Dockerfile image: appflowyinc/admin_frontend:${APPFLOWY_ADMIN_FRONTEND_VERSION:-latest} - depends_on: - appflowy_cloud: - condition: service_healthy ports: - 3000:3000 environment: From 2a5b643ec5938fcb0ceffe9bc58dfde7d43a81db Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 9 Dec 2024 22:41:56 +0800 Subject: [PATCH 25/28] fix: create admin confirmation without email --- docker/gotrue/start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/gotrue/start.sh b/docker/gotrue/start.sh index 3a1aaec6b..226c1ae04 100755 --- a/docker/gotrue/start.sh +++ b/docker/gotrue/start.sh @@ -5,7 +5,7 @@ set -e if [ -n "${GOTRUE_ADMIN_EMAIL}" ] && [ -n "${GOTRUE_ADMIN_PASSWORD}" ]; then set +e echo "Creating admin user for gotrue..." - command_output=$(./auth admin createuser --admin "${GOTRUE_ADMIN_EMAIL}" "${GOTRUE_ADMIN_PASSWORD}" 2>&1) + command_output=$(./auth admin createuser --admin --confirm "${GOTRUE_ADMIN_EMAIL}" "${GOTRUE_ADMIN_PASSWORD}" 2>&1) command_status=$? # Check if the command failed if [ $command_status -ne 0 ]; then From fd015eef96d40e0b1aa523158474cf141ac6bd34 Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 9 Dec 2024 20:50:09 +0800 Subject: [PATCH 26/28] fix: ci add service dependency of appflowy cloud on admin frontend --- docker-compose-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index d19ee7224..91510438a 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -151,6 +151,9 @@ services: context: . dockerfile: ./admin_frontend/Dockerfile image: appflowyinc/admin_frontend:${APPFLOWY_ADMIN_FRONTEND_VERSION:-latest} + depends_on: + appflowy_cloud: + condition: service_healthy ports: - 3000:3000 environment: From ad733c2449960355d824a13ffd7752b1a18f67dd Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Mon, 9 Dec 2024 21:54:47 +0800 Subject: [PATCH 27/28] fix: server logs if error --- docker-compose-ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index 91510438a..d19ee7224 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -151,9 +151,6 @@ services: context: . dockerfile: ./admin_frontend/Dockerfile image: appflowyinc/admin_frontend:${APPFLOWY_ADMIN_FRONTEND_VERSION:-latest} - depends_on: - appflowy_cloud: - condition: service_healthy ports: - 3000:3000 environment: From 166d239d0370e058c037b9f4f93abec605786baf Mon Sep 17 00:00:00 2001 From: Zack Fu Zi Xiang Date: Tue, 10 Dec 2024 12:06:16 +0800 Subject: [PATCH 28/28] chore: update collab --- Cargo.lock | 20 ++++++++++---------- Cargo.toml | 18 +++++++++--------- admin_frontend/Cargo.toml | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b91f73494..0a056442c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "access-control" @@ -539,9 +539,9 @@ checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "app-error" @@ -2133,7 +2133,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=128f3a81ea86a58e355615b1c00edbf861867886#128f3a81ea86a58e355615b1c00edbf861867886" dependencies = [ "anyhow", "arc-swap", @@ -2158,7 +2158,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=128f3a81ea86a58e355615b1c00edbf861867886#128f3a81ea86a58e355615b1c00edbf861867886" dependencies = [ "anyhow", "async-trait", @@ -2197,7 +2197,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=128f3a81ea86a58e355615b1c00edbf861867886#128f3a81ea86a58e355615b1c00edbf861867886" dependencies = [ "anyhow", "arc-swap", @@ -2218,7 +2218,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=128f3a81ea86a58e355615b1c00edbf861867886#128f3a81ea86a58e355615b1c00edbf861867886" dependencies = [ "anyhow", "bytes", @@ -2238,7 +2238,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=128f3a81ea86a58e355615b1c00edbf861867886#128f3a81ea86a58e355615b1c00edbf861867886" dependencies = [ "anyhow", "arc-swap", @@ -2260,7 +2260,7 @@ dependencies = [ [[package]] name = "collab-importer" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=128f3a81ea86a58e355615b1c00edbf861867886#128f3a81ea86a58e355615b1c00edbf861867886" dependencies = [ "anyhow", "async-recursion", @@ -2363,7 +2363,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4a581a1c47740b476aa3beaf7190ed16b7376ec#a4a581a1c47740b476aa3beaf7190ed16b7376ec" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=128f3a81ea86a58e355615b1c00edbf861867886#128f3a81ea86a58e355615b1c00edbf861867886" dependencies = [ "anyhow", "collab", diff --git a/Cargo.toml b/Cargo.toml index 5841ef46e..674c07ce5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ chrono = { version = "0.4.37", features = [ derive_more = { version = "0.99" } secrecy.workspace = true rand = { version = "0.8", features = ["std_rng"] } -anyhow = "1.0.79" +anyhow.workspace = true thiserror = "1.0.56" reqwest = { workspace = true, features = [ "json", @@ -248,7 +248,7 @@ serde = { version = "1.0.195", features = ["derive"] } bytes = "1.5.0" workspace-template = { path = "libs/workspace-template" } uuid = { version = "1.6.1", features = ["v4", "v5"] } -anyhow = "1.0.79" +anyhow = "1.0.94" actix = "0.13.3" actix-web = { version = "4.5.1", default-features = false, features = [ "openssl", @@ -312,13 +312,13 @@ lto = false # Disable Link-Time Optimization [patch.crates-io] # It's diffcult to resovle different version with the same crate used in AppFlowy Frontend and the Client-API crate. # So using patch to workaround this issue. -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } -collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4a581a1c47740b476aa3beaf7190ed16b7376ec" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "128f3a81ea86a58e355615b1c00edbf861867886" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "128f3a81ea86a58e355615b1c00edbf861867886" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "128f3a81ea86a58e355615b1c00edbf861867886" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "128f3a81ea86a58e355615b1c00edbf861867886" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "128f3a81ea86a58e355615b1c00edbf861867886" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "128f3a81ea86a58e355615b1c00edbf861867886" } +collab-importer = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "128f3a81ea86a58e355615b1c00edbf861867886" } [features] history = [] diff --git a/admin_frontend/Cargo.toml b/admin_frontend/Cargo.toml index 3d1cd4079..e292ff1c1 100644 --- a/admin_frontend/Cargo.toml +++ b/admin_frontend/Cargo.toml @@ -12,7 +12,7 @@ gotrue-entity = { path = "../libs/gotrue-entity" } database-entity = { path = "../libs/database-entity" } shared-entity = { path = "../libs/shared-entity" } -anyhow = "1.0.79" +anyhow.workspace = true axum = { version = "0.7", features = ["json"] } tokio = { version = "1.36", features = ["rt-multi-thread", "macros"] } askama = "0.12"