use anyhow::anyhow; use app_error::AppError; use appflowy_collaborate::collab::storage::CollabAccessControlStorage; use chrono::DateTime; use collab::core::collab::Collab; use collab_database::workspace_database::{NoPersistenceDatabaseCollabService, WorkspaceDatabase}; use collab_database::{database::DatabaseBody, rows::RowId}; use collab_document::document::Document; use collab_document::document_data::default_document_data; use collab_entity::{CollabType, EncodedCollab}; use collab_folder::hierarchy_builder::NestedChildViewBuilder; use collab_folder::{CollabOrigin, Folder}; use database::collab::{select_workspace_database_oid, CollabStorage, GetCollabOrigin}; use database::publish::select_published_view_ids_for_workspace; use database::user::select_web_user_from_uid; use database_entity::dto::{CollabParams, QueryCollab, QueryCollabParams, QueryCollabResult}; use itertools::Itertools; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use shared_entity::dto::workspace_dto::{FolderView, Page, PageCollab, PageCollabData, ViewLayout}; use sqlx::{PgPool, Transaction}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; use uuid::Uuid; use yrs::updates::decoder::Decode; use yrs::Update; use crate::api::metrics::AppFlowyWebMetrics; use crate::biz::collab::folder_view::{ parse_extra_field_as_json, to_dto_view_icon, to_dto_view_layout, }; use crate::biz::collab::{ folder_view::view_is_space, ops::{get_latest_collab_encoded, get_latest_collab_folder}, }; use super::ops::{broadcast_update, collab_from_doc_state}; struct FolderUpdate { pub updated_encoded_collab: Vec, pub encoded_updates: Vec, } pub async fn create_page( pg_pool: &PgPool, collab_storage: &CollabAccessControlStorage, uid: i64, workspace_id: Uuid, parent_view_id: &str, view_layout: &ViewLayout, ) -> Result { if *view_layout != ViewLayout::Document { return Err(AppError::InvalidRequest( "Only document layout is supported for page creation".to_string(), )); } create_document_page(pg_pool, collab_storage, uid, workspace_id, parent_view_id).await } fn prepare_default_document_collab_param() -> Result { let object_id = Uuid::new_v4().to_string(); let document_data = default_document_data(&object_id); let document = Document::create(&object_id, document_data) .map_err(|err| AppError::Internal(anyhow!("Failed to create default document: {}", err)))?; let encoded_collab_v1 = document .encode_collab() .map_err(|err| AppError::Internal(anyhow!("Failed to encode default document: {}", err)))? .encode_to_bytes()?; Ok(CollabParams { object_id: object_id.clone(), encoded_collab_v1: encoded_collab_v1.into(), collab_type: CollabType::Document, embeddings: None, }) } async fn add_new_view_to_folder( uid: i64, parent_view_id: &str, view_id: &str, folder: &mut Folder, ) -> Result { let encoded_update = { let view = NestedChildViewBuilder::new(uid, parent_view_id.to_string()) .with_view_id(view_id) .build() .view; let mut txn = folder.collab.transact_mut(); folder.body.views.insert(&mut txn, view, None); txn.encode_update_v1() }; Ok(FolderUpdate { updated_encoded_collab: folder_to_encoded_collab(folder)?, encoded_updates: encoded_update, }) } async fn move_view_to_trash(view_id: &str, folder: &mut Folder) -> Result { let mut current_view_and_descendants = folder .get_views_belong_to(view_id) .iter() .map(|v| v.id.clone()) .collect_vec(); current_view_and_descendants.push(view_id.to_string()); let encoded_update = { let mut txn = folder.collab.transact_mut(); current_view_and_descendants.iter().for_each(|view_id| { folder.body.views.update_view(&mut txn, view_id, |update| { update.set_favorite(false).set_trash(true).done() }); }); txn.encode_update_v1() }; Ok(FolderUpdate { updated_encoded_collab: folder_to_encoded_collab(folder)?, encoded_updates: encoded_update, }) } fn folder_to_encoded_collab(folder: &Folder) -> Result, AppError> { let collab_type = CollabType::Folder; let encoded_folder_collab = folder .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) .map_err(|err| AppError::Internal(anyhow!("Failed to encode workspace folder: {}", err)))?; encoded_folder_collab.encode_to_bytes().map_err(|err| { AppError::Internal(anyhow!( "Failed to encode workspace folder to bytes: {}", err )) }) } async fn insert_and_broadcast_workspace_folder_update( uid: i64, workspace_id: Uuid, folder_update: FolderUpdate, collab_storage: &CollabAccessControlStorage, transaction: &mut Transaction<'_, sqlx::Postgres>, ) -> Result<(), AppError> { let params = CollabParams { object_id: workspace_id.to_string(), encoded_collab_v1: folder_update.updated_encoded_collab.into(), collab_type: CollabType::Folder, embeddings: None, }; let action_description = format!("Update workspace folder: {}", workspace_id); collab_storage .insert_new_collab_with_transaction( &workspace_id.to_string(), &uid, params, transaction, &action_description, ) .await?; broadcast_update( collab_storage, &workspace_id.to_string(), folder_update.encoded_updates.clone(), ) .await?; Ok(()) } async fn create_document_page( pg_pool: &PgPool, collab_storage: &CollabAccessControlStorage, uid: i64, workspace_id: Uuid, parent_view_id: &str, ) -> Result { let default_document_collab_params = prepare_default_document_collab_param()?; let view_id = default_document_collab_params.object_id.clone(); let collab_origin = GetCollabOrigin::User { uid }; let mut folder = get_latest_collab_folder(collab_storage, collab_origin, &workspace_id.to_string()).await?; let folder_update = add_new_view_to_folder(uid, parent_view_id, &view_id, &mut folder).await?; let mut transaction = pg_pool.begin().await?; let action = format!("Create new collab: {}", view_id); collab_storage .insert_new_collab_with_transaction( &workspace_id.to_string(), &uid, default_document_collab_params, &mut transaction, &action, ) .await?; insert_and_broadcast_workspace_folder_update( uid, workspace_id, folder_update, collab_storage, &mut transaction, ) .await?; transaction.commit().await?; Ok(Page { view_id }) } pub async fn move_page_to_trash( pg_pool: &PgPool, collab_storage: &CollabAccessControlStorage, uid: i64, workspace_id: Uuid, view_id: &str, ) -> Result<(), AppError> { let collab_origin = GetCollabOrigin::User { uid }; let mut folder = get_latest_collab_folder(collab_storage, collab_origin, &workspace_id.to_string()).await?; let trash_info = folder.get_my_trash_info(); if trash_info.into_iter().any(|info| info.id == view_id) { return Ok(()); } let folder_update = move_view_to_trash(view_id, &mut folder).await?; let mut transaction = pg_pool.begin().await?; insert_and_broadcast_workspace_folder_update( uid, workspace_id, folder_update, collab_storage, &mut transaction, ) .await?; transaction.commit().await?; Ok(()) } pub async fn get_page_view_collab( pg_pool: &PgPool, collab_access_control_storage: &CollabAccessControlStorage, uid: i64, workspace_id: Uuid, view_id: &str, ) -> Result { let folder = get_latest_collab_folder( collab_access_control_storage, GetCollabOrigin::User { uid }, &workspace_id.to_string(), ) .await?; let view = folder .get_view(view_id) .ok_or(AppError::InvalidFolderView(format!( "View {} not found", view_id )))?; let owner = match view.created_by { Some(uid) => Some(select_web_user_from_uid(pg_pool, uid).await?), None => None, }; let last_editor = match view.last_edited_by { Some(uid) => Some(select_web_user_from_uid(pg_pool, uid).await?), None => None, }; let publish_view_ids = select_published_view_ids_for_workspace(pg_pool, workspace_id).await?; let publish_view_ids: HashSet = publish_view_ids .into_iter() .map(|id| id.to_string()) .collect(); let folder_view = FolderView { view_id: view_id.to_string(), name: view.name.clone(), icon: view .icon .as_ref() .map(|icon| to_dto_view_icon(icon.clone())), is_space: view_is_space(&view), is_private: false, is_published: publish_view_ids.contains(view_id), layout: to_dto_view_layout(&view.layout), created_at: DateTime::from_timestamp(view.created_at, 0).unwrap_or_default(), last_edited_time: DateTime::from_timestamp(view.last_edited_time, 0).unwrap_or_default(), extra: view.extra.as_ref().map(|e| parse_extra_field_as_json(e)), children: vec![], }; let page_collab_data = match view.layout { collab_folder::ViewLayout::Document => { get_page_collab_data_for_document(collab_access_control_storage, uid, workspace_id, view_id) .await }, collab_folder::ViewLayout::Grid | collab_folder::ViewLayout::Board | collab_folder::ViewLayout::Calendar => { get_page_collab_data_for_database( pg_pool, collab_access_control_storage, uid, workspace_id, view_id, ) .await }, collab_folder::ViewLayout::Chat => Err(AppError::InvalidRequest( "Page view for AI chat is not supported at the moment".to_string(), )), }?; let page_collab = PageCollab { view: folder_view, data: page_collab_data, owner, last_editor, }; Ok(page_collab) } async fn get_page_collab_data_for_database( pg_pool: &PgPool, collab_access_control_storage: &CollabAccessControlStorage, uid: i64, workspace_id: Uuid, view_id: &str, ) -> Result { let ws_db_oid = select_workspace_database_oid(pg_pool, &workspace_id).await?; let ws_db = get_latest_collab_encoded( collab_access_control_storage, GetCollabOrigin::User { uid }, &workspace_id.to_string(), &ws_db_oid, CollabType::WorkspaceDatabase, ) .await?; let ws_db_collab = collab_from_doc_state(ws_db.doc_state.to_vec(), &ws_db_oid)?; let ws_db_body = WorkspaceDatabase::open(ws_db_collab).map_err(|err| { AppError::Internal(anyhow!("Failed to open workspace database body: {}", err)) })?; let db_oid = { ws_db_body .get_database_meta_with_view_id(view_id) .ok_or(AppError::NoRequiredData(format!( "Database view {} not found", view_id )))? .database_id }; let db = get_latest_collab_encoded( collab_access_control_storage, GetCollabOrigin::User { uid }, &workspace_id.to_string(), &db_oid, CollabType::Database, ) .await?; let db_collab = Collab::new_with_source( CollabOrigin::Server, &db_oid, db.clone().into(), vec![], false, ) .map_err(|err| { AppError::Internal(anyhow!( "Unable to create collab from object id {}: {}", &db_oid, err )) })?; let db_body = DatabaseBody::from_collab( &db_collab, Arc::new(NoPersistenceDatabaseCollabService), None, ) .ok_or_else(|| AppError::RecordNotFound("no database body found".to_string()))?; let inline_view_id = { let txn = db_collab.transact(); db_body.get_inline_view_id(&txn) }; let row_ids: Vec = { let txn = db_collab.transact(); db_body .views .get_row_orders(&txn, &inline_view_id) .iter() .map(|ro| ro.id.clone()) .collect() }; let queries: Vec = row_ids .iter() .map(|row_id| QueryCollab { object_id: row_id.to_string(), collab_type: CollabType::DatabaseRow, }) .collect(); let row_query_collab_results = collab_access_control_storage .batch_get_collab(&uid, queries, true) .await; let row_data = tokio::task::spawn_blocking(move || { let row_collabs: HashMap> = row_query_collab_results .into_par_iter() .filter_map(|(row_id, query_collab_result)| match query_collab_result { QueryCollabResult::Success { encode_collab_v1 } => { let decoded_result = EncodedCollab::decode_from_bytes(&encode_collab_v1); match decoded_result { Ok(decoded) => Some((row_id, decoded.doc_state.to_vec())), Err(err) => { tracing::error!("Failed to decode collab for row {}: {}", row_id, err); None }, } }, QueryCollabResult::Failed { error } => { tracing::error!("Failed to get collab: {:?}", error); None }, }) .collect(); row_collabs }) .await?; Ok(PageCollabData { encoded_collab: db.doc_state.to_vec(), row_data, }) } async fn get_page_collab_data_for_document( collab_access_control_storage: &CollabAccessControlStorage, uid: i64, workspace_id: Uuid, view_id: &str, ) -> Result { let collab = get_latest_collab_encoded( collab_access_control_storage, GetCollabOrigin::User { uid }, &workspace_id.to_string(), view_id, CollabType::Document, ) .await?; Ok(PageCollabData { encoded_collab: collab.doc_state.clone().to_vec(), row_data: HashMap::default(), }) } pub async fn update_page_collab_data( collab_access_control_storage: Arc, appflowy_web_metrics: Arc, uid: i64, workspace_id: Uuid, object_id: Uuid, collab_type: CollabType, doc_state: &[u8], ) -> Result<(), AppError> { let param = QueryCollabParams { workspace_id: workspace_id.to_string(), inner: QueryCollab { object_id: object_id.to_string(), collab_type: collab_type.clone(), }, }; 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| { appflowy_web_metrics.incr_decoding_failure_count(1); AppError::InvalidRequest(format!("Failed to decode update: {}", e)) })?; collab.apply_update(update).map_err(|e| { appflowy_web_metrics.incr_apply_update_failure_count(1); AppError::InvalidRequest(format!("Failed to apply update: {}", e)) })?; let updated_encoded_collab = collab .encode_collab_v1(|c| collab_type.validate_require_data(c)) .map_err(|e| AppError::Internal(anyhow!("Failed to encode collab: {}", e)))? .encode_to_bytes()?; let params = CollabParams { object_id: object_id.to_string(), collab_type: collab_type.clone(), encoded_collab_v1: updated_encoded_collab.into(), embeddings: None, }; collab_access_control_storage .queue_insert_or_update_collab(&workspace_id.to_string(), &uid, params, true) .await?; broadcast_update( &collab_access_control_storage, &object_id.to_string(), doc_state.to_vec(), ) .await?; Ok(()) }