rename flowy-core to flowy-folder that manages the workspace,app,view

This commit is contained in:
appflowy
2022-01-27 20:39:54 +08:00
parent 17955bcf43
commit 2fa99a563c
213 changed files with 1008 additions and 1044 deletions

View File

@ -0,0 +1,55 @@
[package]
name = "flowy-folder"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
flowy-folder-data-model = { path = "../../../shared-lib/flowy-folder-data-model" }
flowy-collaboration = { path = "../../../shared-lib/flowy-collaboration" }
flowy-derive = { path = "../../../shared-lib/flowy-derive" }
lib-ot = { path = "../../../shared-lib/lib-ot" }
lib-infra = { path = "../../../shared-lib/lib-infra" }
flowy-document = { path = "../flowy-document" }
flowy-database = { path = "../flowy-database" }
flowy-error = { path = "../flowy-error", features = ["db", "backend"]}
dart-notify = { path = "../dart-notify" }
lib-dispatch = { path = "../lib-dispatch" }
lib-sqlite = { path = "../lib-sqlite" }
flowy-sync = { path = "../flowy-sync" }
parking_lot = "0.11"
protobuf = {version = "2.18.0"}
log = "0.4.14"
diesel = {version = "1.4.8", features = ["sqlite"]}
diesel_derives = {version = "1.4.1", features = ["sqlite"]}
#diesel = { git = "https://github.com/diesel-rs/diesel.git", branch = "master", features = ["sqlite"] }
#diesel_derives = { git = "https://github.com/diesel-rs/diesel.git", branch = "master",features = ["sqlite"] }
futures-core = { version = "0.3", default-features = false }
futures = "0.3.15"
pin-project = "1.0.0"
strum = "0.21"
strum_macros = "0.21"
tokio = { version = "1", features = ["rt"] }
lazy_static = "1.4.0"
serde = { version = "1.0", features = ["derive"] }
derive_more = {version = "0.99", features = ["display"]}
bincode = { version = "1.3"}
tracing = { version = "0.1", features = ["log"] }
bytes = { version = "1.0" }
crossbeam = "0.8"
crossbeam-utils = "0.8"
chrono = "0.4"
[dev-dependencies]
serial_test = "0.5.1"
serde_json = "1.0"
flowy-folder = { path = "../flowy-folder", features = ["flowy_unit_test"]}
flowy-test = { path = "../flowy-test" }
[features]
default = []
http_server = []
flowy_unit_test = ["lib-ot/flowy_unit_test", "flowy-sync/flowy_unit_test"]

View File

@ -0,0 +1,3 @@
proto_crates = ["src/entities", "src/event.rs", "src/dart_notification.rs"]
event_files = ["src/event.rs"]

View File

@ -0,0 +1,225 @@
use bytes::Bytes;
use chrono::Utc;
use flowy_collaboration::client_document::default::{initial_delta, initial_read_me};
use flowy_folder_data_model::user_default;
use flowy_sync::RevisionWebSocket;
use lazy_static::lazy_static;
use flowy_collaboration::{client_folder::FolderPad, entities::ws_data::ServerRevisionWSData};
use flowy_document::FlowyDocumentManager;
use std::{collections::HashMap, convert::TryInto, fmt::Formatter, sync::Arc};
use tokio::sync::RwLock as TokioRwLock;
use crate::{
dart_notification::{send_dart_notification, FolderNotification},
entities::workspace::RepeatedWorkspace,
errors::FlowyResult,
module::{FolderCouldServiceV1, WorkspaceDatabase, WorkspaceUser},
services::{
folder_editor::FolderEditor, persistence::FolderPersistence, set_current_workspace, AppController,
TrashController, ViewController, WorkspaceController,
},
};
lazy_static! {
static ref INIT_FOLDER_FLAG: TokioRwLock<HashMap<String, bool>> = TokioRwLock::new(HashMap::new());
}
const FOLDER_ID: &str = "folder";
const FOLDER_ID_SPLIT: &str = ":";
#[derive(Clone)]
pub struct FolderId(String);
impl FolderId {
pub fn new(user_id: &str) -> Self {
Self(format!("{}{}{}", user_id, FOLDER_ID_SPLIT, FOLDER_ID))
}
}
impl std::fmt::Display for FolderId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(FOLDER_ID)
}
}
impl std::fmt::Debug for FolderId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(FOLDER_ID)
}
}
impl AsRef<str> for FolderId {
fn as_ref(&self) -> &str {
&self.0
}
}
pub struct FolderManager {
pub user: Arc<dyn WorkspaceUser>,
pub(crate) cloud_service: Arc<dyn FolderCouldServiceV1>,
pub(crate) persistence: Arc<FolderPersistence>,
pub(crate) workspace_controller: Arc<WorkspaceController>,
pub(crate) app_controller: Arc<AppController>,
pub(crate) view_controller: Arc<ViewController>,
pub(crate) trash_controller: Arc<TrashController>,
web_socket: Arc<dyn RevisionWebSocket>,
folder_editor: Arc<TokioRwLock<Option<Arc<FolderEditor>>>>,
}
impl FolderManager {
pub async fn new(
user: Arc<dyn WorkspaceUser>,
cloud_service: Arc<dyn FolderCouldServiceV1>,
database: Arc<dyn WorkspaceDatabase>,
document_manager: Arc<FlowyDocumentManager>,
web_socket: Arc<dyn RevisionWebSocket>,
) -> Self {
if let Ok(user_id) = user.user_id() {
// Reset the flag if the folder manager gets initialized, otherwise,
// the folder_editor will not be initialized after flutter hot reload.
INIT_FOLDER_FLAG.write().await.insert(user_id.to_owned(), false);
}
let folder_editor = Arc::new(TokioRwLock::new(None));
let persistence = Arc::new(FolderPersistence::new(database.clone(), folder_editor.clone()));
let trash_controller = Arc::new(TrashController::new(
persistence.clone(),
cloud_service.clone(),
user.clone(),
));
let view_controller = Arc::new(ViewController::new(
user.clone(),
persistence.clone(),
cloud_service.clone(),
trash_controller.clone(),
document_manager,
));
let app_controller = Arc::new(AppController::new(
user.clone(),
persistence.clone(),
trash_controller.clone(),
cloud_service.clone(),
));
let workspace_controller = Arc::new(WorkspaceController::new(
user.clone(),
persistence.clone(),
trash_controller.clone(),
cloud_service.clone(),
));
Self {
user,
cloud_service,
persistence,
workspace_controller,
app_controller,
view_controller,
trash_controller,
web_socket,
folder_editor,
}
}
// pub fn network_state_changed(&self, new_type: NetworkType) {
// match new_type {
// NetworkType::UnknownNetworkType => {},
// NetworkType::Wifi => {},
// NetworkType::Cell => {},
// NetworkType::Ethernet => {},
// }
// }
pub async fn did_receive_ws_data(&self, data: Bytes) {
let result: Result<ServerRevisionWSData, protobuf::ProtobufError> = data.try_into();
match result {
Ok(data) => match self.folder_editor.read().await.clone() {
None => {}
Some(editor) => match editor.receive_ws_data(data).await {
Ok(_) => {}
Err(e) => tracing::error!("Folder receive data error: {:?}", e),
},
},
Err(e) => {
tracing::error!("Folder ws data parser failed: {:?}", e);
}
}
}
#[tracing::instrument(level = "trace", skip(self), err)]
pub async fn initialize(&self, user_id: &str, token: &str) -> FlowyResult<()> {
let mut write_guard = INIT_FOLDER_FLAG.write().await;
if let Some(is_init) = write_guard.get(user_id) {
if *is_init {
return Ok(());
}
}
tracing::debug!("Initialize folder editor");
let folder_id = FolderId::new(user_id);
let _ = self.persistence.initialize(user_id, &folder_id).await?;
let pool = self.persistence.db_pool()?;
let folder_editor = FolderEditor::new(user_id, &folder_id, token, pool, self.web_socket.clone()).await?;
*self.folder_editor.write().await = Some(Arc::new(folder_editor));
let _ = self.app_controller.initialize()?;
let _ = self.view_controller.initialize()?;
write_guard.insert(user_id.to_owned(), true);
Ok(())
}
pub async fn initialize_with_new_user(&self, user_id: &str, token: &str) -> FlowyResult<()> {
DefaultFolderBuilder::build(token, user_id, self.persistence.clone(), self.view_controller.clone()).await?;
self.initialize(user_id, token).await
}
pub async fn clear(&self) {
*self.folder_editor.write().await = None;
}
}
struct DefaultFolderBuilder();
impl DefaultFolderBuilder {
async fn build(
token: &str,
user_id: &str,
persistence: Arc<FolderPersistence>,
view_controller: Arc<ViewController>,
) -> FlowyResult<()> {
log::debug!("Create user default workspace");
let time = Utc::now();
let workspace = user_default::create_default_workspace(time);
set_current_workspace(&workspace.id);
for app in workspace.apps.iter() {
for (index, view) in app.belongings.iter().enumerate() {
let view_data = if index == 0 {
initial_read_me().to_json()
} else {
initial_delta().to_json()
};
view_controller.set_latest_view(view);
let _ = view_controller
.create_view_document_content(&view.id, view_data)
.await?;
}
}
let folder = FolderPad::new(vec![workspace.clone()], vec![])?;
let folder_id = FolderId::new(user_id);
let _ = persistence.save_folder(user_id, &folder_id, folder).await?;
let repeated_workspace = RepeatedWorkspace { items: vec![workspace] };
send_dart_notification(token, FolderNotification::UserCreateWorkspace)
.payload(repeated_workspace)
.send();
Ok(())
}
}
#[cfg(feature = "flowy_unit_test")]
impl FolderManager {
pub async fn folder_editor(&self) -> Arc<FolderEditor> {
self.folder_editor.read().await.clone().unwrap()
}
}

View File

@ -0,0 +1,42 @@
use dart_notify::DartNotifyBuilder;
use flowy_derive::ProtoBuf_Enum;
const OBSERVABLE_CATEGORY: &str = "Workspace";
#[derive(ProtoBuf_Enum, Debug)]
pub(crate) enum FolderNotification {
Unknown = 0,
UserCreateWorkspace = 10,
UserDeleteWorkspace = 11,
WorkspaceUpdated = 12,
WorkspaceListUpdated = 13,
WorkspaceAppsChanged = 14,
AppUpdated = 21,
AppViewsChanged = 24,
ViewUpdated = 31,
ViewDeleted = 32,
ViewRestored = 33,
UserUnauthorized = 100,
TrashUpdated = 1000,
}
impl std::default::Default for FolderNotification {
fn default() -> Self {
FolderNotification::Unknown
}
}
impl std::convert::From<FolderNotification> for i32 {
fn from(notification: FolderNotification) -> Self {
notification as i32
}
}
#[tracing::instrument(level = "trace")]
pub(crate) fn send_dart_notification(id: &str, ty: FolderNotification) -> DartNotifyBuilder {
DartNotifyBuilder::new(id, ty, OBSERVABLE_CATEGORY)
}
#[tracing::instrument(level = "trace")]
pub(crate) fn send_anonymous_dart_notification(ty: FolderNotification) -> DartNotifyBuilder {
DartNotifyBuilder::new("", ty, OBSERVABLE_CATEGORY)
}

View File

@ -0,0 +1,81 @@
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
use strum_macros::Display;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
#[event_err = "FlowyError"]
pub enum FolderEvent {
#[event(input = "CreateWorkspaceRequest", output = "Workspace")]
CreateWorkspace = 0,
#[event(output = "CurrentWorkspaceSetting")]
ReadCurWorkspace = 1,
#[event(input = "QueryWorkspaceRequest", output = "RepeatedWorkspace")]
ReadWorkspaces = 2,
#[event(input = "QueryWorkspaceRequest")]
DeleteWorkspace = 3,
#[event(input = "QueryWorkspaceRequest", output = "Workspace")]
OpenWorkspace = 4,
#[event(input = "QueryWorkspaceRequest", output = "RepeatedApp")]
ReadWorkspaceApps = 5,
#[event(input = "CreateAppRequest", output = "App")]
CreateApp = 101,
#[event(input = "QueryAppRequest")]
DeleteApp = 102,
#[event(input = "QueryAppRequest", output = "App")]
ReadApp = 103,
#[event(input = "UpdateAppRequest")]
UpdateApp = 104,
#[event(input = "CreateViewRequest", output = "View")]
CreateView = 201,
#[event(input = "QueryViewRequest", output = "View")]
ReadView = 202,
#[event(input = "UpdateViewRequest", output = "View")]
UpdateView = 203,
#[event(input = "QueryViewRequest")]
DeleteView = 204,
#[event(input = "QueryViewRequest")]
DuplicateView = 205,
#[event()]
CopyLink = 206,
#[event(input = "QueryViewRequest", output = "DocumentDelta")]
OpenDocument = 207,
#[event(input = "QueryViewRequest")]
CloseView = 208,
#[event(output = "RepeatedTrash")]
ReadTrash = 300,
#[event(input = "TrashId")]
PutbackTrash = 301,
#[event(input = "RepeatedTrashId")]
DeleteTrash = 302,
#[event()]
RestoreAllTrash = 303,
#[event()]
DeleteAllTrash = 304,
#[event(input = "DocumentDelta", output = "DocumentDelta")]
ApplyDocDelta = 400,
#[event(input = "ExportRequest", output = "ExportData")]
ExportDocument = 500,
}

View File

@ -0,0 +1,26 @@
pub use flowy_folder_data_model::entities;
pub mod event;
pub mod module;
pub mod services;
#[macro_use]
mod macros;
#[macro_use]
extern crate flowy_database;
pub mod controller;
mod dart_notification;
pub mod protobuf;
mod util;
pub mod prelude {
pub use flowy_folder_data_model::entities::{app::*, trash::*, view::*, workspace::*};
pub use crate::{errors::*, module::*};
}
pub mod errors {
pub use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
}

View File

@ -0,0 +1,49 @@
// #[macro_export]
// macro_rules! impl_save_func {
// ($func_name:ident, $target:ident, $table_name:expr, $conn:ident) => {
// fn $func_name(object: $target) -> Result<(), FlowyError> {
// let _ = diesel::insert_into($table_name)
// .values($target)
// .execute(&*($conn))?;
// }
// };
// }
#[macro_export]
macro_rules! impl_def_and_def_mut {
($target:ident, $item: ident) => {
impl std::ops::Deref for $target {
type Target = Vec<$item>;
fn deref(&self) -> &Self::Target {
&self.items
}
}
impl std::ops::DerefMut for $target {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.items
}
}
impl $target {
#[allow(dead_code)]
pub fn into_inner(&mut self) -> Vec<$item> {
::std::mem::replace(&mut self.items, vec![])
}
#[allow(dead_code)]
pub fn push(&mut self, item: $item) {
if self.items.contains(&item) {
log::error!("add duplicate item: {:?}", item);
return;
}
self.items.push(item);
}
pub fn first_or_crash(&self) -> &$item {
self.items.first().unwrap()
}
}
};
}

View File

@ -0,0 +1,119 @@
use crate::{
controller::FolderManager,
entities::{
app::{App, AppId, CreateAppParams, UpdateAppParams},
trash::{RepeatedTrash, RepeatedTrashId},
view::{CreateViewParams, RepeatedViewId, UpdateViewParams, View, ViewId},
workspace::{CreateWorkspaceParams, RepeatedWorkspace, UpdateWorkspaceParams, Workspace, WorkspaceId},
},
errors::FlowyError,
event::FolderEvent,
services::{app::event_handler::*, trash::event_handler::*, view::event_handler::*, workspace::event_handler::*},
};
use flowy_database::DBConnection;
use lib_dispatch::prelude::*;
use lib_infra::future::FutureResult;
use lib_sqlite::ConnectionPool;
use std::sync::Arc;
pub trait WorkspaceDeps: WorkspaceUser + WorkspaceDatabase {}
pub trait WorkspaceUser: Send + Sync {
fn user_id(&self) -> Result<String, FlowyError>;
fn token(&self) -> Result<String, FlowyError>;
}
pub trait WorkspaceDatabase: Send + Sync {
fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>;
fn db_connection(&self) -> Result<DBConnection, FlowyError> {
let pool = self.db_pool()?;
let conn = pool.get().map_err(|e| FlowyError::internal().context(e))?;
Ok(conn)
}
}
pub fn create(folder: Arc<FolderManager>) -> Module {
let mut module = Module::new()
.name("Flowy-Workspace")
.data(folder.workspace_controller.clone())
.data(folder.app_controller.clone())
.data(folder.view_controller.clone())
.data(folder.trash_controller.clone())
.data(folder.clone());
module = module
.event(FolderEvent::CreateWorkspace, create_workspace_handler)
.event(FolderEvent::ReadCurWorkspace, read_cur_workspace_handler)
.event(FolderEvent::ReadWorkspaces, read_workspaces_handler)
.event(FolderEvent::OpenWorkspace, open_workspace_handler)
.event(FolderEvent::ReadWorkspaceApps, read_workspace_apps_handler);
module = module
.event(FolderEvent::CreateApp, create_app_handler)
.event(FolderEvent::ReadApp, read_app_handler)
.event(FolderEvent::UpdateApp, update_app_handler)
.event(FolderEvent::DeleteApp, delete_app_handler);
module = module
.event(FolderEvent::CreateView, create_view_handler)
.event(FolderEvent::ReadView, read_view_handler)
.event(FolderEvent::UpdateView, update_view_handler)
.event(FolderEvent::DeleteView, delete_view_handler)
.event(FolderEvent::DuplicateView, duplicate_view_handler)
.event(FolderEvent::OpenDocument, open_document_handler)
.event(FolderEvent::CloseView, close_view_handler)
.event(FolderEvent::ApplyDocDelta, document_delta_handler);
module = module
.event(FolderEvent::ReadTrash, read_trash_handler)
.event(FolderEvent::PutbackTrash, putback_trash_handler)
.event(FolderEvent::DeleteTrash, delete_trash_handler)
.event(FolderEvent::RestoreAllTrash, restore_all_trash_handler)
.event(FolderEvent::DeleteAllTrash, delete_all_trash_handler);
module = module.event(FolderEvent::ExportDocument, export_handler);
module
}
pub trait FolderCouldServiceV1: Send + Sync {
fn init(&self);
// Workspace
fn create_workspace(&self, token: &str, params: CreateWorkspaceParams) -> FutureResult<Workspace, FlowyError>;
fn read_workspace(&self, token: &str, params: WorkspaceId) -> FutureResult<RepeatedWorkspace, FlowyError>;
fn update_workspace(&self, token: &str, params: UpdateWorkspaceParams) -> FutureResult<(), FlowyError>;
fn delete_workspace(&self, token: &str, params: WorkspaceId) -> FutureResult<(), FlowyError>;
// View
fn create_view(&self, token: &str, params: CreateViewParams) -> FutureResult<View, FlowyError>;
fn read_view(&self, token: &str, params: ViewId) -> FutureResult<Option<View>, FlowyError>;
fn delete_view(&self, token: &str, params: RepeatedViewId) -> FutureResult<(), FlowyError>;
fn update_view(&self, token: &str, params: UpdateViewParams) -> FutureResult<(), FlowyError>;
// App
fn create_app(&self, token: &str, params: CreateAppParams) -> FutureResult<App, FlowyError>;
fn read_app(&self, token: &str, params: AppId) -> FutureResult<Option<App>, FlowyError>;
fn update_app(&self, token: &str, params: UpdateAppParams) -> FutureResult<(), FlowyError>;
fn delete_app(&self, token: &str, params: AppId) -> FutureResult<(), FlowyError>;
// Trash
fn create_trash(&self, token: &str, params: RepeatedTrashId) -> FutureResult<(), FlowyError>;
fn delete_trash(&self, token: &str, params: RepeatedTrashId) -> FutureResult<(), FlowyError>;
fn read_trash(&self, token: &str) -> FutureResult<RepeatedTrash, FlowyError>;
}
pub trait FolderCouldServiceV2: Send + Sync {}

View File

@ -0,0 +1,4 @@
#![cfg_attr(rustfmt, rustfmt::skip)]
// Auto-generated, do not edit
mod model;
pub use model::*;

View File

@ -0,0 +1,158 @@
// This file is generated by rust-protobuf 2.22.1. Do not edit
// @generated
// https://github.com/rust-lang/rust-clippy/issues/702
#![allow(unknown_lints)]
#![allow(clippy::all)]
#![allow(unused_attributes)]
#![cfg_attr(rustfmt, rustfmt::skip)]
#![allow(box_pointers)]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(trivial_casts)]
#![allow(unused_imports)]
#![allow(unused_results)]
//! Generated file from `dart_notification.proto`
/// Generated files are compatible only with the same version
/// of protobuf runtime.
// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_22_1;
#[derive(Clone,PartialEq,Eq,Debug,Hash)]
pub enum FolderNotification {
Unknown = 0,
UserCreateWorkspace = 10,
UserDeleteWorkspace = 11,
WorkspaceUpdated = 12,
WorkspaceListUpdated = 13,
WorkspaceAppsChanged = 14,
AppUpdated = 21,
AppViewsChanged = 24,
ViewUpdated = 31,
ViewDeleted = 32,
ViewRestored = 33,
UserUnauthorized = 100,
TrashUpdated = 1000,
}
impl ::protobuf::ProtobufEnum for FolderNotification {
fn value(&self) -> i32 {
*self as i32
}
fn from_i32(value: i32) -> ::std::option::Option<FolderNotification> {
match value {
0 => ::std::option::Option::Some(FolderNotification::Unknown),
10 => ::std::option::Option::Some(FolderNotification::UserCreateWorkspace),
11 => ::std::option::Option::Some(FolderNotification::UserDeleteWorkspace),
12 => ::std::option::Option::Some(FolderNotification::WorkspaceUpdated),
13 => ::std::option::Option::Some(FolderNotification::WorkspaceListUpdated),
14 => ::std::option::Option::Some(FolderNotification::WorkspaceAppsChanged),
21 => ::std::option::Option::Some(FolderNotification::AppUpdated),
24 => ::std::option::Option::Some(FolderNotification::AppViewsChanged),
31 => ::std::option::Option::Some(FolderNotification::ViewUpdated),
32 => ::std::option::Option::Some(FolderNotification::ViewDeleted),
33 => ::std::option::Option::Some(FolderNotification::ViewRestored),
100 => ::std::option::Option::Some(FolderNotification::UserUnauthorized),
1000 => ::std::option::Option::Some(FolderNotification::TrashUpdated),
_ => ::std::option::Option::None
}
}
fn values() -> &'static [Self] {
static values: &'static [FolderNotification] = &[
FolderNotification::Unknown,
FolderNotification::UserCreateWorkspace,
FolderNotification::UserDeleteWorkspace,
FolderNotification::WorkspaceUpdated,
FolderNotification::WorkspaceListUpdated,
FolderNotification::WorkspaceAppsChanged,
FolderNotification::AppUpdated,
FolderNotification::AppViewsChanged,
FolderNotification::ViewUpdated,
FolderNotification::ViewDeleted,
FolderNotification::ViewRestored,
FolderNotification::UserUnauthorized,
FolderNotification::TrashUpdated,
];
values
}
fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT;
descriptor.get(|| {
::protobuf::reflect::EnumDescriptor::new_pb_name::<FolderNotification>("FolderNotification", file_descriptor_proto())
})
}
}
impl ::std::marker::Copy for FolderNotification {
}
impl ::std::default::Default for FolderNotification {
fn default() -> Self {
FolderNotification::Unknown
}
}
impl ::protobuf::reflect::ProtobufValue for FolderNotification {
fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self))
}
}
static file_descriptor_proto_data: &'static [u8] = b"\
\n\x17dart_notification.proto*\x9f\x02\n\x12FolderNotification\x12\x0b\n\
\x07Unknown\x10\0\x12\x17\n\x13UserCreateWorkspace\x10\n\x12\x17\n\x13Us\
erDeleteWorkspace\x10\x0b\x12\x14\n\x10WorkspaceUpdated\x10\x0c\x12\x18\
\n\x14WorkspaceListUpdated\x10\r\x12\x18\n\x14WorkspaceAppsChanged\x10\
\x0e\x12\x0e\n\nAppUpdated\x10\x15\x12\x13\n\x0fAppViewsChanged\x10\x18\
\x12\x0f\n\x0bViewUpdated\x10\x1f\x12\x0f\n\x0bViewDeleted\x10\x20\x12\
\x10\n\x0cViewRestored\x10!\x12\x14\n\x10UserUnauthorized\x10d\x12\x11\n\
\x0cTrashUpdated\x10\xe8\x07J\xbf\x04\n\x06\x12\x04\0\0\x10\x01\n\x08\n\
\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x05\0\x12\x04\x02\0\x10\x01\n\n\n\x03\
\x05\0\x01\x12\x03\x02\x05\x17\n\x0b\n\x04\x05\0\x02\0\x12\x03\x03\x04\
\x10\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x03\x04\x0b\n\x0c\n\x05\x05\0\
\x02\0\x02\x12\x03\x03\x0e\x0f\n\x0b\n\x04\x05\0\x02\x01\x12\x03\x04\x04\
\x1d\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x04\x04\x17\n\x0c\n\x05\x05\0\
\x02\x01\x02\x12\x03\x04\x1a\x1c\n\x0b\n\x04\x05\0\x02\x02\x12\x03\x05\
\x04\x1d\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\x05\x04\x17\n\x0c\n\x05\
\x05\0\x02\x02\x02\x12\x03\x05\x1a\x1c\n\x0b\n\x04\x05\0\x02\x03\x12\x03\
\x06\x04\x1a\n\x0c\n\x05\x05\0\x02\x03\x01\x12\x03\x06\x04\x14\n\x0c\n\
\x05\x05\0\x02\x03\x02\x12\x03\x06\x17\x19\n\x0b\n\x04\x05\0\x02\x04\x12\
\x03\x07\x04\x1e\n\x0c\n\x05\x05\0\x02\x04\x01\x12\x03\x07\x04\x18\n\x0c\
\n\x05\x05\0\x02\x04\x02\x12\x03\x07\x1b\x1d\n\x0b\n\x04\x05\0\x02\x05\
\x12\x03\x08\x04\x1e\n\x0c\n\x05\x05\0\x02\x05\x01\x12\x03\x08\x04\x18\n\
\x0c\n\x05\x05\0\x02\x05\x02\x12\x03\x08\x1b\x1d\n\x0b\n\x04\x05\0\x02\
\x06\x12\x03\t\x04\x14\n\x0c\n\x05\x05\0\x02\x06\x01\x12\x03\t\x04\x0e\n\
\x0c\n\x05\x05\0\x02\x06\x02\x12\x03\t\x11\x13\n\x0b\n\x04\x05\0\x02\x07\
\x12\x03\n\x04\x19\n\x0c\n\x05\x05\0\x02\x07\x01\x12\x03\n\x04\x13\n\x0c\
\n\x05\x05\0\x02\x07\x02\x12\x03\n\x16\x18\n\x0b\n\x04\x05\0\x02\x08\x12\
\x03\x0b\x04\x15\n\x0c\n\x05\x05\0\x02\x08\x01\x12\x03\x0b\x04\x0f\n\x0c\
\n\x05\x05\0\x02\x08\x02\x12\x03\x0b\x12\x14\n\x0b\n\x04\x05\0\x02\t\x12\
\x03\x0c\x04\x15\n\x0c\n\x05\x05\0\x02\t\x01\x12\x03\x0c\x04\x0f\n\x0c\n\
\x05\x05\0\x02\t\x02\x12\x03\x0c\x12\x14\n\x0b\n\x04\x05\0\x02\n\x12\x03\
\r\x04\x16\n\x0c\n\x05\x05\0\x02\n\x01\x12\x03\r\x04\x10\n\x0c\n\x05\x05\
\0\x02\n\x02\x12\x03\r\x13\x15\n\x0b\n\x04\x05\0\x02\x0b\x12\x03\x0e\x04\
\x1b\n\x0c\n\x05\x05\0\x02\x0b\x01\x12\x03\x0e\x04\x14\n\x0c\n\x05\x05\0\
\x02\x0b\x02\x12\x03\x0e\x17\x1a\n\x0b\n\x04\x05\0\x02\x0c\x12\x03\x0f\
\x04\x18\n\x0c\n\x05\x05\0\x02\x0c\x01\x12\x03\x0f\x04\x10\n\x0c\n\x05\
\x05\0\x02\x0c\x02\x12\x03\x0f\x13\x17b\x06proto3\
";
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap()
}
pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
file_descriptor_proto_lazy.get(|| {
parse_descriptor_proto()
})
}

View File

@ -0,0 +1,224 @@
// This file is generated by rust-protobuf 2.22.1. Do not edit
// @generated
// https://github.com/rust-lang/rust-clippy/issues/702
#![allow(unknown_lints)]
#![allow(clippy::all)]
#![allow(unused_attributes)]
#![cfg_attr(rustfmt, rustfmt::skip)]
#![allow(box_pointers)]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(trivial_casts)]
#![allow(unused_imports)]
#![allow(unused_results)]
//! Generated file from `event.proto`
/// Generated files are compatible only with the same version
/// of protobuf runtime.
// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_22_1;
#[derive(Clone,PartialEq,Eq,Debug,Hash)]
pub enum FolderEvent {
CreateWorkspace = 0,
ReadCurWorkspace = 1,
ReadWorkspaces = 2,
DeleteWorkspace = 3,
OpenWorkspace = 4,
ReadWorkspaceApps = 5,
CreateApp = 101,
DeleteApp = 102,
ReadApp = 103,
UpdateApp = 104,
CreateView = 201,
ReadView = 202,
UpdateView = 203,
DeleteView = 204,
DuplicateView = 205,
CopyLink = 206,
OpenDocument = 207,
CloseView = 208,
ReadTrash = 300,
PutbackTrash = 301,
DeleteTrash = 302,
RestoreAllTrash = 303,
DeleteAllTrash = 304,
ApplyDocDelta = 400,
ExportDocument = 500,
}
impl ::protobuf::ProtobufEnum for FolderEvent {
fn value(&self) -> i32 {
*self as i32
}
fn from_i32(value: i32) -> ::std::option::Option<FolderEvent> {
match value {
0 => ::std::option::Option::Some(FolderEvent::CreateWorkspace),
1 => ::std::option::Option::Some(FolderEvent::ReadCurWorkspace),
2 => ::std::option::Option::Some(FolderEvent::ReadWorkspaces),
3 => ::std::option::Option::Some(FolderEvent::DeleteWorkspace),
4 => ::std::option::Option::Some(FolderEvent::OpenWorkspace),
5 => ::std::option::Option::Some(FolderEvent::ReadWorkspaceApps),
101 => ::std::option::Option::Some(FolderEvent::CreateApp),
102 => ::std::option::Option::Some(FolderEvent::DeleteApp),
103 => ::std::option::Option::Some(FolderEvent::ReadApp),
104 => ::std::option::Option::Some(FolderEvent::UpdateApp),
201 => ::std::option::Option::Some(FolderEvent::CreateView),
202 => ::std::option::Option::Some(FolderEvent::ReadView),
203 => ::std::option::Option::Some(FolderEvent::UpdateView),
204 => ::std::option::Option::Some(FolderEvent::DeleteView),
205 => ::std::option::Option::Some(FolderEvent::DuplicateView),
206 => ::std::option::Option::Some(FolderEvent::CopyLink),
207 => ::std::option::Option::Some(FolderEvent::OpenDocument),
208 => ::std::option::Option::Some(FolderEvent::CloseView),
300 => ::std::option::Option::Some(FolderEvent::ReadTrash),
301 => ::std::option::Option::Some(FolderEvent::PutbackTrash),
302 => ::std::option::Option::Some(FolderEvent::DeleteTrash),
303 => ::std::option::Option::Some(FolderEvent::RestoreAllTrash),
304 => ::std::option::Option::Some(FolderEvent::DeleteAllTrash),
400 => ::std::option::Option::Some(FolderEvent::ApplyDocDelta),
500 => ::std::option::Option::Some(FolderEvent::ExportDocument),
_ => ::std::option::Option::None
}
}
fn values() -> &'static [Self] {
static values: &'static [FolderEvent] = &[
FolderEvent::CreateWorkspace,
FolderEvent::ReadCurWorkspace,
FolderEvent::ReadWorkspaces,
FolderEvent::DeleteWorkspace,
FolderEvent::OpenWorkspace,
FolderEvent::ReadWorkspaceApps,
FolderEvent::CreateApp,
FolderEvent::DeleteApp,
FolderEvent::ReadApp,
FolderEvent::UpdateApp,
FolderEvent::CreateView,
FolderEvent::ReadView,
FolderEvent::UpdateView,
FolderEvent::DeleteView,
FolderEvent::DuplicateView,
FolderEvent::CopyLink,
FolderEvent::OpenDocument,
FolderEvent::CloseView,
FolderEvent::ReadTrash,
FolderEvent::PutbackTrash,
FolderEvent::DeleteTrash,
FolderEvent::RestoreAllTrash,
FolderEvent::DeleteAllTrash,
FolderEvent::ApplyDocDelta,
FolderEvent::ExportDocument,
];
values
}
fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor {
static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT;
descriptor.get(|| {
::protobuf::reflect::EnumDescriptor::new_pb_name::<FolderEvent>("FolderEvent", file_descriptor_proto())
})
}
}
impl ::std::marker::Copy for FolderEvent {
}
impl ::std::default::Default for FolderEvent {
fn default() -> Self {
FolderEvent::CreateWorkspace
}
}
impl ::protobuf::reflect::ProtobufValue for FolderEvent {
fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self))
}
}
static file_descriptor_proto_data: &'static [u8] = b"\
\n\x0bevent.proto*\xd6\x03\n\x0bFolderEvent\x12\x13\n\x0fCreateWorkspace\
\x10\0\x12\x14\n\x10ReadCurWorkspace\x10\x01\x12\x12\n\x0eReadWorkspaces\
\x10\x02\x12\x13\n\x0fDeleteWorkspace\x10\x03\x12\x11\n\rOpenWorkspace\
\x10\x04\x12\x15\n\x11ReadWorkspaceApps\x10\x05\x12\r\n\tCreateApp\x10e\
\x12\r\n\tDeleteApp\x10f\x12\x0b\n\x07ReadApp\x10g\x12\r\n\tUpdateApp\
\x10h\x12\x0f\n\nCreateView\x10\xc9\x01\x12\r\n\x08ReadView\x10\xca\x01\
\x12\x0f\n\nUpdateView\x10\xcb\x01\x12\x0f\n\nDeleteView\x10\xcc\x01\x12\
\x12\n\rDuplicateView\x10\xcd\x01\x12\r\n\x08CopyLink\x10\xce\x01\x12\
\x11\n\x0cOpenDocument\x10\xcf\x01\x12\x0e\n\tCloseView\x10\xd0\x01\x12\
\x0e\n\tReadTrash\x10\xac\x02\x12\x11\n\x0cPutbackTrash\x10\xad\x02\x12\
\x10\n\x0bDeleteTrash\x10\xae\x02\x12\x14\n\x0fRestoreAllTrash\x10\xaf\
\x02\x12\x13\n\x0eDeleteAllTrash\x10\xb0\x02\x12\x12\n\rApplyDocDelta\
\x10\x90\x03\x12\x13\n\x0eExportDocument\x10\xf4\x03J\xab\x08\n\x06\x12\
\x04\0\0\x1c\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x05\0\x12\x04\
\x02\0\x1c\x01\n\n\n\x03\x05\0\x01\x12\x03\x02\x05\x10\n\x0b\n\x04\x05\0\
\x02\0\x12\x03\x03\x04\x18\n\x0c\n\x05\x05\0\x02\0\x01\x12\x03\x03\x04\
\x13\n\x0c\n\x05\x05\0\x02\0\x02\x12\x03\x03\x16\x17\n\x0b\n\x04\x05\0\
\x02\x01\x12\x03\x04\x04\x19\n\x0c\n\x05\x05\0\x02\x01\x01\x12\x03\x04\
\x04\x14\n\x0c\n\x05\x05\0\x02\x01\x02\x12\x03\x04\x17\x18\n\x0b\n\x04\
\x05\0\x02\x02\x12\x03\x05\x04\x17\n\x0c\n\x05\x05\0\x02\x02\x01\x12\x03\
\x05\x04\x12\n\x0c\n\x05\x05\0\x02\x02\x02\x12\x03\x05\x15\x16\n\x0b\n\
\x04\x05\0\x02\x03\x12\x03\x06\x04\x18\n\x0c\n\x05\x05\0\x02\x03\x01\x12\
\x03\x06\x04\x13\n\x0c\n\x05\x05\0\x02\x03\x02\x12\x03\x06\x16\x17\n\x0b\
\n\x04\x05\0\x02\x04\x12\x03\x07\x04\x16\n\x0c\n\x05\x05\0\x02\x04\x01\
\x12\x03\x07\x04\x11\n\x0c\n\x05\x05\0\x02\x04\x02\x12\x03\x07\x14\x15\n\
\x0b\n\x04\x05\0\x02\x05\x12\x03\x08\x04\x1a\n\x0c\n\x05\x05\0\x02\x05\
\x01\x12\x03\x08\x04\x15\n\x0c\n\x05\x05\0\x02\x05\x02\x12\x03\x08\x18\
\x19\n\x0b\n\x04\x05\0\x02\x06\x12\x03\t\x04\x14\n\x0c\n\x05\x05\0\x02\
\x06\x01\x12\x03\t\x04\r\n\x0c\n\x05\x05\0\x02\x06\x02\x12\x03\t\x10\x13\
\n\x0b\n\x04\x05\0\x02\x07\x12\x03\n\x04\x14\n\x0c\n\x05\x05\0\x02\x07\
\x01\x12\x03\n\x04\r\n\x0c\n\x05\x05\0\x02\x07\x02\x12\x03\n\x10\x13\n\
\x0b\n\x04\x05\0\x02\x08\x12\x03\x0b\x04\x12\n\x0c\n\x05\x05\0\x02\x08\
\x01\x12\x03\x0b\x04\x0b\n\x0c\n\x05\x05\0\x02\x08\x02\x12\x03\x0b\x0e\
\x11\n\x0b\n\x04\x05\0\x02\t\x12\x03\x0c\x04\x14\n\x0c\n\x05\x05\0\x02\t\
\x01\x12\x03\x0c\x04\r\n\x0c\n\x05\x05\0\x02\t\x02\x12\x03\x0c\x10\x13\n\
\x0b\n\x04\x05\0\x02\n\x12\x03\r\x04\x15\n\x0c\n\x05\x05\0\x02\n\x01\x12\
\x03\r\x04\x0e\n\x0c\n\x05\x05\0\x02\n\x02\x12\x03\r\x11\x14\n\x0b\n\x04\
\x05\0\x02\x0b\x12\x03\x0e\x04\x13\n\x0c\n\x05\x05\0\x02\x0b\x01\x12\x03\
\x0e\x04\x0c\n\x0c\n\x05\x05\0\x02\x0b\x02\x12\x03\x0e\x0f\x12\n\x0b\n\
\x04\x05\0\x02\x0c\x12\x03\x0f\x04\x15\n\x0c\n\x05\x05\0\x02\x0c\x01\x12\
\x03\x0f\x04\x0e\n\x0c\n\x05\x05\0\x02\x0c\x02\x12\x03\x0f\x11\x14\n\x0b\
\n\x04\x05\0\x02\r\x12\x03\x10\x04\x15\n\x0c\n\x05\x05\0\x02\r\x01\x12\
\x03\x10\x04\x0e\n\x0c\n\x05\x05\0\x02\r\x02\x12\x03\x10\x11\x14\n\x0b\n\
\x04\x05\0\x02\x0e\x12\x03\x11\x04\x18\n\x0c\n\x05\x05\0\x02\x0e\x01\x12\
\x03\x11\x04\x11\n\x0c\n\x05\x05\0\x02\x0e\x02\x12\x03\x11\x14\x17\n\x0b\
\n\x04\x05\0\x02\x0f\x12\x03\x12\x04\x13\n\x0c\n\x05\x05\0\x02\x0f\x01\
\x12\x03\x12\x04\x0c\n\x0c\n\x05\x05\0\x02\x0f\x02\x12\x03\x12\x0f\x12\n\
\x0b\n\x04\x05\0\x02\x10\x12\x03\x13\x04\x17\n\x0c\n\x05\x05\0\x02\x10\
\x01\x12\x03\x13\x04\x10\n\x0c\n\x05\x05\0\x02\x10\x02\x12\x03\x13\x13\
\x16\n\x0b\n\x04\x05\0\x02\x11\x12\x03\x14\x04\x14\n\x0c\n\x05\x05\0\x02\
\x11\x01\x12\x03\x14\x04\r\n\x0c\n\x05\x05\0\x02\x11\x02\x12\x03\x14\x10\
\x13\n\x0b\n\x04\x05\0\x02\x12\x12\x03\x15\x04\x14\n\x0c\n\x05\x05\0\x02\
\x12\x01\x12\x03\x15\x04\r\n\x0c\n\x05\x05\0\x02\x12\x02\x12\x03\x15\x10\
\x13\n\x0b\n\x04\x05\0\x02\x13\x12\x03\x16\x04\x17\n\x0c\n\x05\x05\0\x02\
\x13\x01\x12\x03\x16\x04\x10\n\x0c\n\x05\x05\0\x02\x13\x02\x12\x03\x16\
\x13\x16\n\x0b\n\x04\x05\0\x02\x14\x12\x03\x17\x04\x16\n\x0c\n\x05\x05\0\
\x02\x14\x01\x12\x03\x17\x04\x0f\n\x0c\n\x05\x05\0\x02\x14\x02\x12\x03\
\x17\x12\x15\n\x0b\n\x04\x05\0\x02\x15\x12\x03\x18\x04\x1a\n\x0c\n\x05\
\x05\0\x02\x15\x01\x12\x03\x18\x04\x13\n\x0c\n\x05\x05\0\x02\x15\x02\x12\
\x03\x18\x16\x19\n\x0b\n\x04\x05\0\x02\x16\x12\x03\x19\x04\x19\n\x0c\n\
\x05\x05\0\x02\x16\x01\x12\x03\x19\x04\x12\n\x0c\n\x05\x05\0\x02\x16\x02\
\x12\x03\x19\x15\x18\n\x0b\n\x04\x05\0\x02\x17\x12\x03\x1a\x04\x18\n\x0c\
\n\x05\x05\0\x02\x17\x01\x12\x03\x1a\x04\x11\n\x0c\n\x05\x05\0\x02\x17\
\x02\x12\x03\x1a\x14\x17\n\x0b\n\x04\x05\0\x02\x18\x12\x03\x1b\x04\x19\n\
\x0c\n\x05\x05\0\x02\x18\x01\x12\x03\x1b\x04\x12\n\x0c\n\x05\x05\0\x02\
\x18\x02\x12\x03\x1b\x15\x18b\x06proto3\
";
static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;
fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto {
::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap()
}
pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto {
file_descriptor_proto_lazy.get(|| {
parse_descriptor_proto()
})
}

View File

@ -0,0 +1,8 @@
#![cfg_attr(rustfmt, rustfmt::skip)]
// Auto-generated, do not edit
mod dart_notification;
pub use dart_notification::*;
mod event;
pub use event::*;

View File

@ -0,0 +1,17 @@
syntax = "proto3";
enum FolderNotification {
Unknown = 0;
UserCreateWorkspace = 10;
UserDeleteWorkspace = 11;
WorkspaceUpdated = 12;
WorkspaceListUpdated = 13;
WorkspaceAppsChanged = 14;
AppUpdated = 21;
AppViewsChanged = 24;
ViewUpdated = 31;
ViewDeleted = 32;
ViewRestored = 33;
UserUnauthorized = 100;
TrashUpdated = 1000;
}

View File

@ -0,0 +1,29 @@
syntax = "proto3";
enum FolderEvent {
CreateWorkspace = 0;
ReadCurWorkspace = 1;
ReadWorkspaces = 2;
DeleteWorkspace = 3;
OpenWorkspace = 4;
ReadWorkspaceApps = 5;
CreateApp = 101;
DeleteApp = 102;
ReadApp = 103;
UpdateApp = 104;
CreateView = 201;
ReadView = 202;
UpdateView = 203;
DeleteView = 204;
DuplicateView = 205;
CopyLink = 206;
OpenDocument = 207;
CloseView = 208;
ReadTrash = 300;
PutbackTrash = 301;
DeleteTrash = 302;
RestoreAllTrash = 303;
DeleteAllTrash = 304;
ApplyDocDelta = 400;
ExportDocument = 500;
}

View File

@ -0,0 +1,246 @@
use crate::{
dart_notification::*,
entities::{
app::{App, CreateAppParams, *},
trash::TrashType,
},
errors::*,
module::{FolderCouldServiceV1, WorkspaceUser},
services::{
persistence::{AppChangeset, FolderPersistence, FolderPersistenceTransaction},
TrashController, TrashEvent,
},
};
use futures::{FutureExt, StreamExt};
use std::{collections::HashSet, sync::Arc};
pub(crate) struct AppController {
user: Arc<dyn WorkspaceUser>,
persistence: Arc<FolderPersistence>,
trash_controller: Arc<TrashController>,
cloud_service: Arc<dyn FolderCouldServiceV1>,
}
impl AppController {
pub(crate) fn new(
user: Arc<dyn WorkspaceUser>,
persistence: Arc<FolderPersistence>,
trash_can: Arc<TrashController>,
cloud_service: Arc<dyn FolderCouldServiceV1>,
) -> Self {
Self {
user,
persistence,
trash_controller: trash_can,
cloud_service,
}
}
pub fn initialize(&self) -> Result<(), FlowyError> {
self.listen_trash_controller_event();
Ok(())
}
#[tracing::instrument(level = "debug", skip(self, params), fields(name = %params.name) err)]
pub(crate) async fn create_app_from_params(&self, params: CreateAppParams) -> Result<App, FlowyError> {
let app = self.create_app_on_server(params).await?;
self.create_app_on_local(app).await
}
pub(crate) async fn create_app_on_local(&self, app: App) -> Result<App, FlowyError> {
let _ = self
.persistence
.begin_transaction(|transaction| {
let _ = transaction.create_app(app.clone())?;
let _ = notify_apps_changed(&app.workspace_id, self.trash_controller.clone(), &transaction)?;
Ok(())
})
.await?;
Ok(app)
}
pub(crate) async fn read_app(&self, params: AppId) -> Result<App, FlowyError> {
let app = self
.persistence
.begin_transaction(|transaction| {
let app = transaction.read_app(&params.app_id)?;
let trash_ids = self.trash_controller.read_trash_ids(&transaction)?;
if trash_ids.contains(&app.id) {
return Err(FlowyError::record_not_found());
}
Ok(app)
})
.await?;
let _ = self.read_app_on_server(params)?;
Ok(app)
}
pub(crate) async fn update_app(&self, params: UpdateAppParams) -> Result<(), FlowyError> {
let changeset = AppChangeset::new(params.clone());
let app_id = changeset.id.clone();
let app = self
.persistence
.begin_transaction(|transaction| {
let _ = transaction.update_app(changeset)?;
let app = transaction.read_app(&app_id)?;
Ok(app)
})
.await?;
send_dart_notification(&app_id, FolderNotification::AppUpdated)
.payload(app)
.send();
let _ = self.update_app_on_server(params)?;
Ok(())
}
pub(crate) async fn read_local_apps(&self, ids: Vec<String>) -> Result<Vec<App>, FlowyError> {
let apps = self
.persistence
.begin_transaction(|transaction| {
let mut apps = vec![];
for id in ids {
apps.push(transaction.read_app(&id)?);
}
Ok(apps)
})
.await?;
Ok(apps)
}
}
impl AppController {
#[tracing::instrument(level = "trace", skip(self), err)]
async fn create_app_on_server(&self, params: CreateAppParams) -> Result<App, FlowyError> {
let token = self.user.token()?;
let app = self.cloud_service.create_app(&token, params).await?;
Ok(app)
}
#[tracing::instrument(level = "trace", skip(self), err)]
fn update_app_on_server(&self, params: UpdateAppParams) -> Result<(), FlowyError> {
let token = self.user.token()?;
let server = self.cloud_service.clone();
tokio::spawn(async move {
match server.update_app(&token, params).await {
Ok(_) => {}
Err(e) => {
// TODO: retry?
log::error!("Update app failed: {:?}", e);
}
}
});
Ok(())
}
#[tracing::instrument(level = "trace", skip(self), err)]
fn read_app_on_server(&self, params: AppId) -> Result<(), FlowyError> {
let token = self.user.token()?;
let server = self.cloud_service.clone();
let persistence = self.persistence.clone();
tokio::spawn(async move {
match server.read_app(&token, params).await {
Ok(Some(app)) => {
match persistence
.begin_transaction(|transaction| transaction.create_app(app.clone()))
.await
{
Ok(_) => {
send_dart_notification(&app.id, FolderNotification::AppUpdated)
.payload(app)
.send();
}
Err(e) => log::error!("Save app failed: {:?}", e),
}
}
Ok(None) => {}
Err(e) => log::error!("Read app failed: {:?}", e),
}
});
Ok(())
}
fn listen_trash_controller_event(&self) {
let mut rx = self.trash_controller.subscribe();
let persistence = self.persistence.clone();
let trash_controller = self.trash_controller.clone();
let _ = tokio::spawn(async move {
loop {
let mut stream = Box::pin(rx.recv().into_stream().filter_map(|result| async move {
match result {
Ok(event) => event.select(TrashType::App),
Err(_e) => None,
}
}));
if let Some(event) = stream.next().await {
handle_trash_event(persistence.clone(), trash_controller.clone(), event).await
}
}
});
}
}
#[tracing::instrument(level = "trace", skip(persistence, trash_controller))]
async fn handle_trash_event(
persistence: Arc<FolderPersistence>,
trash_controller: Arc<TrashController>,
event: TrashEvent,
) {
match event {
TrashEvent::NewTrash(identifiers, ret) | TrashEvent::Putback(identifiers, ret) => {
let result = persistence
.begin_transaction(|transaction| {
for identifier in identifiers.items {
let app = transaction.read_app(&identifier.id)?;
let _ = notify_apps_changed(&app.workspace_id, trash_controller.clone(), &transaction)?;
}
Ok(())
})
.await;
let _ = ret.send(result).await;
}
TrashEvent::Delete(identifiers, ret) => {
let result = persistence
.begin_transaction(|transaction| {
let mut notify_ids = HashSet::new();
for identifier in identifiers.items {
let app = transaction.read_app(&identifier.id)?;
let _ = transaction.delete_app(&identifier.id)?;
notify_ids.insert(app.workspace_id);
}
for notify_id in notify_ids {
let _ = notify_apps_changed(&notify_id, trash_controller.clone(), &transaction)?;
}
Ok(())
})
.await;
let _ = ret.send(result).await;
}
}
}
#[tracing::instrument(skip(workspace_id, trash_controller, transaction), err)]
fn notify_apps_changed<'a>(
workspace_id: &str,
trash_controller: Arc<TrashController>,
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
) -> FlowyResult<()> {
let repeated_app = read_local_workspace_apps(workspace_id, trash_controller, transaction)?;
send_dart_notification(workspace_id, FolderNotification::WorkspaceAppsChanged)
.payload(repeated_app)
.send();
Ok(())
}
pub fn read_local_workspace_apps<'a>(
workspace_id: &str,
trash_controller: Arc<TrashController>,
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
) -> Result<RepeatedApp, FlowyError> {
let mut apps = transaction.read_workspace_apps(workspace_id)?;
let trash_ids = trash_controller.read_trash_ids(transaction)?;
apps.retain(|app| !trash_ids.contains(&app.id));
Ok(RepeatedApp { items: apps })
}

View File

@ -0,0 +1,60 @@
use crate::{
entities::{
app::{App, AppId, CreateAppParams, CreateAppRequest, QueryAppRequest, UpdateAppParams, UpdateAppRequest},
trash::Trash,
},
errors::FlowyError,
services::{AppController, TrashController, ViewController},
};
use lib_dispatch::prelude::{data_result, Data, DataResult, Unit};
use std::{convert::TryInto, sync::Arc};
pub(crate) async fn create_app_handler(
data: Data<CreateAppRequest>,
controller: Unit<Arc<AppController>>,
) -> DataResult<App, FlowyError> {
let params: CreateAppParams = data.into_inner().try_into()?;
let detail = controller.create_app_from_params(params).await?;
data_result(detail)
}
pub(crate) async fn delete_app_handler(
data: Data<QueryAppRequest>,
app_controller: Unit<Arc<AppController>>,
trash_controller: Unit<Arc<TrashController>>,
) -> Result<(), FlowyError> {
let params: AppId = data.into_inner().try_into()?;
let trash = app_controller
.read_local_apps(vec![params.app_id])
.await?
.into_iter()
.map(|app| app.into())
.collect::<Vec<Trash>>();
let _ = trash_controller.add(trash).await?;
Ok(())
}
#[tracing::instrument(skip(data, controller))]
pub(crate) async fn update_app_handler(
data: Data<UpdateAppRequest>,
controller: Unit<Arc<AppController>>,
) -> Result<(), FlowyError> {
let params: UpdateAppParams = data.into_inner().try_into()?;
let _ = controller.update_app(params).await?;
Ok(())
}
#[tracing::instrument(skip(data, app_controller, view_controller))]
pub(crate) async fn read_app_handler(
data: Data<QueryAppRequest>,
app_controller: Unit<Arc<AppController>>,
view_controller: Unit<Arc<ViewController>>,
) -> DataResult<App, FlowyError> {
let params: AppId = data.into_inner().try_into()?;
let mut app = app_controller.read_app(params.clone()).await?;
app.belongings = view_controller.read_views_belong_to(&params.app_id).await?;
data_result(app)
}

View File

@ -0,0 +1,2 @@
pub mod controller;
pub mod event_handler;

View File

@ -0,0 +1,152 @@
use crate::services::web_socket::make_folder_ws_manager;
use flowy_collaboration::{
client_folder::{FolderChange, FolderPad},
entities::{revision::Revision, ws_data::ServerRevisionWSData},
};
use crate::controller::FolderId;
use flowy_collaboration::util::make_delta_from_revisions;
use flowy_error::{FlowyError, FlowyResult};
use flowy_sync::{
RevisionCache, RevisionCloudService, RevisionCompact, RevisionManager, RevisionObjectBuilder, RevisionWebSocket,
RevisionWebSocketManager,
};
use lib_infra::future::FutureResult;
use lib_ot::core::PlainAttributes;
use lib_sqlite::ConnectionPool;
use parking_lot::RwLock;
use std::sync::Arc;
pub struct FolderEditor {
user_id: String,
pub(crate) folder_id: FolderId,
pub(crate) folder: Arc<RwLock<FolderPad>>,
rev_manager: Arc<RevisionManager>,
ws_manager: Arc<RevisionWebSocketManager>,
}
impl FolderEditor {
pub async fn new(
user_id: &str,
folder_id: &FolderId,
token: &str,
pool: Arc<ConnectionPool>,
web_socket: Arc<dyn RevisionWebSocket>,
) -> FlowyResult<Self> {
let cache = Arc::new(RevisionCache::new(user_id, folder_id.as_ref(), pool));
let mut rev_manager = RevisionManager::new(user_id, folder_id.as_ref(), cache);
let cloud = Arc::new(FolderRevisionCloudServiceImpl {
token: token.to_string(),
});
let folder = Arc::new(RwLock::new(
rev_manager
.load::<FolderPadBuilder, FolderRevisionCompact>(cloud)
.await?,
));
let rev_manager = Arc::new(rev_manager);
let ws_manager = make_folder_ws_manager(
user_id,
folder_id.as_ref(),
rev_manager.clone(),
web_socket,
folder.clone(),
)
.await;
let user_id = user_id.to_owned();
let folder_id = folder_id.to_owned();
Ok(Self {
user_id,
folder_id,
folder,
rev_manager,
ws_manager,
})
}
pub async fn receive_ws_data(&self, data: ServerRevisionWSData) -> FlowyResult<()> {
let _ = self.ws_manager.ws_passthrough_tx.send(data).await.map_err(|e| {
let err_msg = format!("{} passthrough error: {}", self.folder_id, e);
FlowyError::internal().context(err_msg)
})?;
Ok(())
}
pub(crate) fn apply_change(&self, change: FolderChange) -> FlowyResult<()> {
let FolderChange { delta, md5 } = change;
let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair();
let delta_data = delta.to_bytes();
let revision = Revision::new(
&self.rev_manager.object_id,
base_rev_id,
rev_id,
delta_data,
&self.user_id,
md5,
);
let _ = futures::executor::block_on(async {
self.rev_manager
.add_local_revision::<FolderRevisionCompact>(&revision)
.await
})?;
Ok(())
}
#[allow(dead_code)]
pub fn folder_json(&self) -> FlowyResult<String> {
let json = self.folder.read().to_json()?;
Ok(json)
}
}
struct FolderPadBuilder();
impl RevisionObjectBuilder for FolderPadBuilder {
type Output = FolderPad;
fn build_with_revisions(_object_id: &str, revisions: Vec<Revision>) -> FlowyResult<Self::Output> {
let pad = FolderPad::from_revisions(revisions)?;
Ok(pad)
}
}
struct FolderRevisionCloudServiceImpl {
#[allow(dead_code)]
token: String,
// server: Arc<dyn FolderCouldServiceV2>,
}
impl RevisionCloudService for FolderRevisionCloudServiceImpl {
#[tracing::instrument(level = "trace", skip(self))]
fn fetch_object(&self, _user_id: &str, _object_id: &str) -> FutureResult<Vec<Revision>, FlowyError> {
FutureResult::new(async move { Ok(vec![]) })
}
}
#[cfg(feature = "flowy_unit_test")]
impl FolderEditor {
pub fn rev_manager(&self) -> Arc<RevisionManager> {
self.rev_manager.clone()
}
}
struct FolderRevisionCompact();
impl RevisionCompact for FolderRevisionCompact {
fn compact_revisions(user_id: &str, object_id: &str, mut revisions: Vec<Revision>) -> FlowyResult<Revision> {
if revisions.is_empty() {
return Err(FlowyError::internal().context("Can't compact the empty folder's revisions"));
}
if revisions.len() == 1 {
return Ok(revisions.pop().unwrap());
}
let first_revision = revisions.first().unwrap();
let last_revision = revisions.last().unwrap();
let (base_rev_id, rev_id) = first_revision.pair_rev_id();
let md5 = last_revision.md5.clone();
let delta = make_delta_from_revisions::<PlainAttributes>(revisions)?;
let delta_data = delta.to_bytes();
Ok(Revision::new(object_id, base_rev_id, rev_id, delta_data, user_id, md5))
}
}

View File

@ -0,0 +1,14 @@
pub(crate) use app::controller::*;
pub(crate) use trash::controller::*;
pub(crate) use view::controller::*;
pub(crate) use workspace::controller::*;
pub(crate) mod app;
pub mod folder_editor;
pub(crate) mod persistence;
pub(crate) mod trash;
pub(crate) mod view;
mod web_socket;
pub(crate) mod workspace;
pub const FOLDER_SYNC_INTERVAL_IN_MILLIS: u64 = 5000;

View File

@ -0,0 +1,78 @@
use crate::{
module::WorkspaceDatabase,
services::persistence::{AppTableSql, TrashTableSql, ViewTableSql, WorkspaceTableSql},
};
use flowy_collaboration::{client_folder::FolderPad, entities::revision::md5};
use flowy_database::kv::KV;
use flowy_error::{FlowyError, FlowyResult};
use flowy_folder_data_model::entities::{
app::{App, RepeatedApp},
view::{RepeatedView, View},
workspace::Workspace,
};
use std::sync::Arc;
pub(crate) const V1_MIGRATION: &str = "FOLDER_V1_MIGRATION";
pub(crate) struct FolderMigration {
user_id: String,
database: Arc<dyn WorkspaceDatabase>,
}
impl FolderMigration {
pub fn new(user_id: &str, database: Arc<dyn WorkspaceDatabase>) -> Self {
Self {
user_id: user_id.to_owned(),
database,
}
}
pub fn run_v1_migration(&self) -> FlowyResult<Option<FolderPad>> {
let key = md5(format!("{}{}", self.user_id, V1_MIGRATION));
if KV::get_bool(&key).unwrap_or(false) {
return Ok(None);
}
tracing::trace!("Run folder version 1 migrations");
let pool = self.database.db_pool()?;
let conn = &*pool.get()?;
let workspaces = conn.immediate_transaction::<_, FlowyError, _>(|| {
let mut workspaces = WorkspaceTableSql::read_workspaces(&self.user_id, None, conn)?
.into_iter()
.map(Workspace::from)
.collect::<Vec<_>>();
for workspace in workspaces.iter_mut() {
let mut apps = AppTableSql::read_workspace_apps(&workspace.id, conn)?
.into_iter()
.map(App::from)
.collect::<Vec<_>>();
for app in apps.iter_mut() {
let views = ViewTableSql::read_views(&app.id, conn)?
.into_iter()
.map(View::from)
.collect::<Vec<_>>();
app.belongings = RepeatedView { items: views };
}
workspace.apps = RepeatedApp { items: apps };
}
Ok(workspaces)
})?;
if workspaces.is_empty() {
KV::set_bool(&key, true);
return Ok(None);
}
let trash = conn.immediate_transaction::<_, FlowyError, _>(|| {
let trash = TrashTableSql::read_all(conn)?.take_items();
Ok(trash)
})?;
let folder = FolderPad::new(workspaces, trash)?;
KV::set_bool(&key, true);
Ok(Some(folder))
}
}

View File

@ -0,0 +1,127 @@
mod migration;
pub mod version_1;
mod version_2;
use flowy_collaboration::{
client_folder::FolderPad,
entities::revision::{Revision, RevisionState},
};
use std::sync::Arc;
use tokio::sync::RwLock;
pub use version_1::{app_sql::*, trash_sql::*, v1_impl::V1Transaction, view_sql::*, workspace_sql::*};
use crate::{
controller::FolderId,
module::WorkspaceDatabase,
services::{folder_editor::FolderEditor, persistence::migration::FolderMigration},
};
use flowy_error::{FlowyError, FlowyResult};
use flowy_folder_data_model::entities::{
app::App,
trash::{RepeatedTrash, Trash},
view::View,
workspace::Workspace,
};
use flowy_sync::{mk_revision_disk_cache, RevisionRecord};
use lib_sqlite::ConnectionPool;
pub trait FolderPersistenceTransaction {
fn create_workspace(&self, user_id: &str, workspace: Workspace) -> FlowyResult<()>;
fn read_workspaces(&self, user_id: &str, workspace_id: Option<String>) -> FlowyResult<Vec<Workspace>>;
fn update_workspace(&self, changeset: WorkspaceChangeset) -> FlowyResult<()>;
fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()>;
fn create_app(&self, app: App) -> FlowyResult<()>;
fn update_app(&self, changeset: AppChangeset) -> FlowyResult<()>;
fn read_app(&self, app_id: &str) -> FlowyResult<App>;
fn read_workspace_apps(&self, workspace_id: &str) -> FlowyResult<Vec<App>>;
fn delete_app(&self, app_id: &str) -> FlowyResult<App>;
fn create_view(&self, view: View) -> FlowyResult<()>;
fn read_view(&self, view_id: &str) -> FlowyResult<View>;
fn read_views(&self, belong_to_id: &str) -> FlowyResult<Vec<View>>;
fn update_view(&self, changeset: ViewChangeset) -> FlowyResult<()>;
fn delete_view(&self, view_id: &str) -> FlowyResult<()>;
fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()>;
fn read_trash(&self, trash_id: Option<String>) -> FlowyResult<RepeatedTrash>;
fn delete_trash(&self, trash_ids: Option<Vec<String>>) -> FlowyResult<()>;
}
pub struct FolderPersistence {
database: Arc<dyn WorkspaceDatabase>,
folder_editor: Arc<RwLock<Option<Arc<FolderEditor>>>>,
}
impl FolderPersistence {
pub fn new(database: Arc<dyn WorkspaceDatabase>, folder_editor: Arc<RwLock<Option<Arc<FolderEditor>>>>) -> Self {
Self {
database,
folder_editor,
}
}
#[deprecated(
since = "0.0.3",
note = "please use `begin_transaction` instead, this interface will be removed in the future"
)]
#[allow(dead_code)]
pub fn begin_transaction_v_1<F, O>(&self, f: F) -> FlowyResult<O>
where
F: for<'a> FnOnce(Box<dyn FolderPersistenceTransaction + 'a>) -> FlowyResult<O>,
{
//[[immediate_transaction]]
// https://sqlite.org/lang_transaction.html
// IMMEDIATE cause the database connection to start a new write immediately,
// without waiting for a write statement. The BEGIN IMMEDIATE might fail
// with SQLITE_BUSY if another write transaction is already active on another
// database connection.
//
// EXCLUSIVE is similar to IMMEDIATE in that a write transaction is started
// immediately. EXCLUSIVE and IMMEDIATE are the same in WAL mode, but in
// other journaling modes, EXCLUSIVE prevents other database connections from
// reading the database while the transaction is underway.
let conn = self.database.db_connection()?;
conn.immediate_transaction::<_, FlowyError, _>(|| f(Box::new(V1Transaction(&conn))))
}
pub async fn begin_transaction<F, O>(&self, f: F) -> FlowyResult<O>
where
F: FnOnce(Arc<dyn FolderPersistenceTransaction>) -> FlowyResult<O>,
{
match self.folder_editor.read().await.clone() {
None => Err(FlowyError::internal().context("FolderEditor should be initialized after user login in.")),
Some(editor) => f(editor),
}
}
pub fn db_pool(&self) -> FlowyResult<Arc<ConnectionPool>> {
self.database.db_pool()
}
pub async fn initialize(&self, user_id: &str, folder_id: &FolderId) -> FlowyResult<()> {
let migrations = FolderMigration::new(user_id, self.database.clone());
if let Some(migrated_folder) = migrations.run_v1_migration()? {
tracing::trace!("Save migration folder");
self.save_folder(user_id, folder_id, migrated_folder).await?;
}
Ok(())
}
pub async fn save_folder(&self, user_id: &str, folder_id: &FolderId, folder: FolderPad) -> FlowyResult<()> {
let pool = self.database.db_pool()?;
let delta_data = folder.delta().to_bytes();
let md5 = folder.md5();
let revision = Revision::new(folder_id.as_ref(), 0, 0, delta_data, user_id, md5);
let record = RevisionRecord {
revision,
state: RevisionState::Sync,
write_to_disk: true,
};
let conn = pool.get()?;
let disk_cache = mk_revision_disk_cache(user_id, pool);
disk_cache.create_revision_records(vec![record], &conn)
}
}

View File

@ -0,0 +1,199 @@
use crate::entities::{
app::{App, ColorStyle, UpdateAppParams},
trash::{Trash, TrashType},
view::RepeatedView,
};
use diesel::sql_types::Binary;
use flowy_database::{
prelude::*,
schema::{app_table, app_table::dsl},
SqliteConnection,
};
use serde::{Deserialize, Serialize, __private::TryFrom};
use std::convert::TryInto;
use crate::{errors::FlowyError, services::persistence::version_1::workspace_sql::WorkspaceTable};
pub struct AppTableSql();
impl AppTableSql {
pub(crate) fn create_app(app: App, conn: &SqliteConnection) -> Result<(), FlowyError> {
let app_table = AppTable::new(app);
match diesel_record_count!(app_table, &app_table.id, conn) {
0 => diesel_insert_table!(app_table, &app_table, conn),
_ => {
let changeset = AppChangeset::from_table(app_table);
diesel_update_table!(app_table, changeset, conn)
}
}
Ok(())
}
pub(crate) fn update_app(changeset: AppChangeset, conn: &SqliteConnection) -> Result<(), FlowyError> {
diesel_update_table!(app_table, changeset, conn);
Ok(())
}
pub(crate) fn read_app(app_id: &str, conn: &SqliteConnection) -> Result<AppTable, FlowyError> {
let filter = dsl::app_table.filter(app_table::id.eq(app_id)).into_boxed();
let app_table = filter.first::<AppTable>(conn)?;
Ok(app_table)
}
pub(crate) fn read_workspace_apps(
workspace_id: &str,
conn: &SqliteConnection,
) -> Result<Vec<AppTable>, FlowyError> {
let app_table = dsl::app_table
.filter(app_table::workspace_id.eq(workspace_id))
.order(app_table::create_time.asc())
.load::<AppTable>(conn)?;
Ok(app_table)
}
pub(crate) fn delete_app(app_id: &str, conn: &SqliteConnection) -> Result<AppTable, FlowyError> {
let app_table = dsl::app_table
.filter(app_table::id.eq(app_id))
.first::<AppTable>(conn)?;
diesel_delete_table!(app_table, app_id, conn);
Ok(app_table)
}
// pub(crate) fn read_views_belong_to_app(
// &self,
// app_id: &str,
// ) -> Result<Vec<ViewTable>, FlowyError> {
// let conn = self.database.db_connection()?;
//
// let views = conn.immediate_transaction::<_, FlowyError, _>(|| {
// let app_table: AppTable = dsl::app_table
// .filter(app_table::id.eq(app_id))
// .first::<AppTable>(&*(conn))?;
// let views =
// ViewTable::belonging_to(&app_table).load::<ViewTable>(&*conn)?;
// Ok(views)
// })?;
//
// Ok(views)
// }
}
#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)]
#[belongs_to(WorkspaceTable, foreign_key = "workspace_id")]
#[table_name = "app_table"]
pub(crate) struct AppTable {
pub id: String,
pub workspace_id: String, // equal to #[belongs_to(Workspace, foreign_key = "workspace_id")].
pub name: String,
pub desc: String,
pub color_style: ColorStyleCol,
pub last_view_id: Option<String>,
pub modified_time: i64,
pub create_time: i64,
pub version: i64,
pub is_trash: bool,
}
impl AppTable {
pub fn new(app: App) -> Self {
Self {
id: app.id,
workspace_id: app.workspace_id,
name: app.name,
desc: app.desc,
color_style: ColorStyleCol::default(),
last_view_id: None,
modified_time: app.modified_time,
create_time: app.create_time,
version: 0,
is_trash: false,
}
}
}
impl std::convert::From<AppTable> for Trash {
fn from(table: AppTable) -> Self {
Trash {
id: table.id,
name: table.name,
modified_time: table.modified_time,
create_time: table.create_time,
ty: TrashType::App,
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug, Default, FromSqlRow, AsExpression)]
#[sql_type = "Binary"]
pub(crate) struct ColorStyleCol {
pub(crate) theme_color: String,
}
impl std::convert::From<ColorStyle> for ColorStyleCol {
fn from(s: ColorStyle) -> Self {
Self {
theme_color: s.theme_color,
}
}
}
impl std::convert::TryInto<Vec<u8>> for &ColorStyleCol {
type Error = String;
fn try_into(self) -> Result<Vec<u8>, Self::Error> {
bincode::serialize(self).map_err(|e| format!("{:?}", e))
}
}
impl std::convert::TryFrom<&[u8]> for ColorStyleCol {
type Error = String;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
bincode::deserialize(value).map_err(|e| format!("{:?}", e))
}
}
impl_sql_binary_expression!(ColorStyleCol);
#[derive(AsChangeset, Identifiable, Default, Debug)]
#[table_name = "app_table"]
pub struct AppChangeset {
pub id: String,
pub name: Option<String>,
pub desc: Option<String>,
pub is_trash: Option<bool>,
}
impl AppChangeset {
pub(crate) fn new(params: UpdateAppParams) -> Self {
AppChangeset {
id: params.app_id,
name: params.name,
desc: params.desc,
is_trash: params.is_trash,
}
}
pub(crate) fn from_table(table: AppTable) -> Self {
AppChangeset {
id: table.id,
name: Some(table.name),
desc: Some(table.desc),
is_trash: Some(table.is_trash),
}
}
}
impl std::convert::From<AppTable> for App {
fn from(table: AppTable) -> Self {
App {
id: table.id,
workspace_id: table.workspace_id,
name: table.name,
desc: table.desc,
belongings: RepeatedView::default(),
version: table.version,
modified_time: table.modified_time,
create_time: table.create_time,
}
}
}

View File

@ -0,0 +1,5 @@
pub mod app_sql;
pub mod trash_sql;
pub mod v1_impl;
pub mod view_sql;
pub mod workspace_sql;

View File

@ -0,0 +1,146 @@
use crate::{
entities::trash::{RepeatedTrash, Trash, TrashType},
errors::FlowyError,
};
use diesel::sql_types::Integer;
use flowy_database::{
prelude::*,
schema::{trash_table, trash_table::dsl},
SqliteConnection,
};
pub struct TrashTableSql();
impl TrashTableSql {
pub(crate) fn create_trash(trashes: Vec<Trash>, conn: &SqliteConnection) -> Result<(), FlowyError> {
for trash in trashes {
let trash_table: TrashTable = trash.into();
match diesel_record_count!(trash_table, &trash_table.id, conn) {
0 => diesel_insert_table!(trash_table, &trash_table, conn),
_ => {
let changeset = TrashChangeset::from(trash_table);
diesel_update_table!(trash_table, changeset, conn)
}
}
}
Ok(())
}
pub(crate) fn read_all(conn: &SqliteConnection) -> Result<RepeatedTrash, FlowyError> {
let trash_tables = dsl::trash_table.load::<TrashTable>(conn)?;
let items = trash_tables.into_iter().map(|t| t.into()).collect::<Vec<Trash>>();
Ok(RepeatedTrash { items })
}
pub(crate) fn delete_all(conn: &SqliteConnection) -> Result<(), FlowyError> {
let _ = diesel::delete(dsl::trash_table).execute(conn)?;
Ok(())
}
pub(crate) fn read(trash_id: &str, conn: &SqliteConnection) -> Result<TrashTable, FlowyError> {
let trash_table = dsl::trash_table
.filter(trash_table::id.eq(trash_id))
.first::<TrashTable>(conn)?;
Ok(trash_table)
}
pub(crate) fn delete_trash(trash_id: &str, conn: &SqliteConnection) -> Result<(), FlowyError> {
diesel_delete_table!(trash_table, trash_id, conn);
Ok(())
}
}
#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)]
#[table_name = "trash_table"]
pub(crate) struct TrashTable {
pub id: String,
pub name: String,
pub desc: String,
pub modified_time: i64,
pub create_time: i64,
pub ty: SqlTrashType,
}
impl std::convert::From<TrashTable> for Trash {
fn from(table: TrashTable) -> Self {
Trash {
id: table.id,
name: table.name,
modified_time: table.modified_time,
create_time: table.create_time,
ty: table.ty.into(),
}
}
}
impl std::convert::From<Trash> for TrashTable {
fn from(trash: Trash) -> Self {
TrashTable {
id: trash.id,
name: trash.name,
desc: "".to_owned(),
modified_time: trash.modified_time,
create_time: trash.create_time,
ty: trash.ty.into(),
}
}
}
#[derive(AsChangeset, Identifiable, Clone, Default, Debug)]
#[table_name = "trash_table"]
pub(crate) struct TrashChangeset {
pub id: String,
pub name: Option<String>,
pub modified_time: i64,
}
impl std::convert::From<TrashTable> for TrashChangeset {
fn from(trash: TrashTable) -> Self {
TrashChangeset {
id: trash.id,
name: Some(trash.name),
modified_time: trash.modified_time,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)]
#[repr(i32)]
#[sql_type = "Integer"]
pub(crate) enum SqlTrashType {
Unknown = 0,
View = 1,
App = 2,
}
impl std::convert::From<i32> for SqlTrashType {
fn from(value: i32) -> Self {
match value {
0 => SqlTrashType::Unknown,
1 => SqlTrashType::View,
2 => SqlTrashType::App,
_o => SqlTrashType::Unknown,
}
}
}
impl_sql_integer_expression!(SqlTrashType);
impl std::convert::From<SqlTrashType> for TrashType {
fn from(ty: SqlTrashType) -> Self {
match ty {
SqlTrashType::Unknown => TrashType::Unknown,
SqlTrashType::View => TrashType::View,
SqlTrashType::App => TrashType::App,
}
}
}
impl std::convert::From<TrashType> for SqlTrashType {
fn from(ty: TrashType) -> Self {
match ty {
TrashType::Unknown => SqlTrashType::Unknown,
TrashType::View => SqlTrashType::View,
TrashType::App => SqlTrashType::App,
}
}
}

View File

@ -0,0 +1,194 @@
use crate::services::persistence::{
version_1::{
app_sql::{AppChangeset, AppTableSql},
view_sql::{ViewChangeset, ViewTableSql},
workspace_sql::{WorkspaceChangeset, WorkspaceTableSql},
},
FolderPersistenceTransaction, TrashTableSql,
};
use flowy_error::FlowyResult;
use flowy_folder_data_model::entities::{
app::App,
trash::{RepeatedTrash, Trash},
view::View,
workspace::Workspace,
};
use lib_sqlite::DBConnection;
pub struct V1Transaction<'a>(pub &'a DBConnection);
impl<'a> FolderPersistenceTransaction for V1Transaction<'a> {
fn create_workspace(&self, user_id: &str, workspace: Workspace) -> FlowyResult<()> {
let _ = WorkspaceTableSql::create_workspace(user_id, workspace, &*self.0)?;
Ok(())
}
fn read_workspaces(&self, user_id: &str, workspace_id: Option<String>) -> FlowyResult<Vec<Workspace>> {
let tables = WorkspaceTableSql::read_workspaces(user_id, workspace_id, &*self.0)?;
let workspaces = tables.into_iter().map(Workspace::from).collect::<Vec<_>>();
Ok(workspaces)
}
fn update_workspace(&self, changeset: WorkspaceChangeset) -> FlowyResult<()> {
WorkspaceTableSql::update_workspace(changeset, &*self.0)
}
fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> {
WorkspaceTableSql::delete_workspace(workspace_id, &*self.0)
}
fn create_app(&self, app: App) -> FlowyResult<()> {
let _ = AppTableSql::create_app(app, &*self.0)?;
Ok(())
}
fn update_app(&self, changeset: AppChangeset) -> FlowyResult<()> {
let _ = AppTableSql::update_app(changeset, &*self.0)?;
Ok(())
}
fn read_app(&self, app_id: &str) -> FlowyResult<App> {
let table = AppTableSql::read_app(app_id, &*self.0)?;
Ok(App::from(table))
}
fn read_workspace_apps(&self, workspace_id: &str) -> FlowyResult<Vec<App>> {
let tables = AppTableSql::read_workspace_apps(workspace_id, &*self.0)?;
let apps = tables.into_iter().map(App::from).collect::<Vec<_>>();
Ok(apps)
}
fn delete_app(&self, app_id: &str) -> FlowyResult<App> {
let table = AppTableSql::delete_app(app_id, &*self.0)?;
Ok(App::from(table))
}
fn create_view(&self, view: View) -> FlowyResult<()> {
let _ = ViewTableSql::create_view(view, &*self.0)?;
Ok(())
}
fn read_view(&self, view_id: &str) -> FlowyResult<View> {
let table = ViewTableSql::read_view(view_id, &*self.0)?;
Ok(View::from(table))
}
fn read_views(&self, belong_to_id: &str) -> FlowyResult<Vec<View>> {
let tables = ViewTableSql::read_views(belong_to_id, &*self.0)?;
let views = tables.into_iter().map(View::from).collect::<Vec<_>>();
Ok(views)
}
fn update_view(&self, changeset: ViewChangeset) -> FlowyResult<()> {
let _ = ViewTableSql::update_view(changeset, &*self.0)?;
Ok(())
}
fn delete_view(&self, view_id: &str) -> FlowyResult<()> {
let _ = ViewTableSql::delete_view(view_id, &*self.0)?;
Ok(())
}
fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()> {
let _ = TrashTableSql::create_trash(trashes, &*self.0)?;
Ok(())
}
fn read_trash(&self, trash_id: Option<String>) -> FlowyResult<RepeatedTrash> {
match trash_id {
None => TrashTableSql::read_all(&*self.0),
Some(trash_id) => {
let table = TrashTableSql::read(&trash_id, &*self.0)?;
Ok(RepeatedTrash {
items: vec![Trash::from(table)],
})
}
}
}
fn delete_trash(&self, trash_ids: Option<Vec<String>>) -> FlowyResult<()> {
match trash_ids {
None => TrashTableSql::delete_all(&*self.0),
Some(trash_ids) => {
for trash_id in &trash_ids {
let _ = TrashTableSql::delete_trash(trash_id, &*self.0)?;
}
Ok(())
}
}
}
}
// https://www.reddit.com/r/rust/comments/droxdg/why_arent_traits_impld_for_boxdyn_trait/
impl<T> FolderPersistenceTransaction for Box<T>
where
T: FolderPersistenceTransaction + ?Sized,
{
fn create_workspace(&self, user_id: &str, workspace: Workspace) -> FlowyResult<()> {
(**self).create_workspace(user_id, workspace)
}
fn read_workspaces(&self, user_id: &str, workspace_id: Option<String>) -> FlowyResult<Vec<Workspace>> {
(**self).read_workspaces(user_id, workspace_id)
}
fn update_workspace(&self, changeset: WorkspaceChangeset) -> FlowyResult<()> {
(**self).update_workspace(changeset)
}
fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> {
(**self).delete_workspace(workspace_id)
}
fn create_app(&self, app: App) -> FlowyResult<()> {
(**self).create_app(app)
}
fn update_app(&self, changeset: AppChangeset) -> FlowyResult<()> {
(**self).update_app(changeset)
}
fn read_app(&self, app_id: &str) -> FlowyResult<App> {
(**self).read_app(app_id)
}
fn read_workspace_apps(&self, workspace_id: &str) -> FlowyResult<Vec<App>> {
(**self).read_workspace_apps(workspace_id)
}
fn delete_app(&self, app_id: &str) -> FlowyResult<App> {
(**self).delete_app(app_id)
}
fn create_view(&self, view: View) -> FlowyResult<()> {
(**self).create_view(view)
}
fn read_view(&self, view_id: &str) -> FlowyResult<View> {
(**self).read_view(view_id)
}
fn read_views(&self, belong_to_id: &str) -> FlowyResult<Vec<View>> {
(**self).read_views(belong_to_id)
}
fn update_view(&self, changeset: ViewChangeset) -> FlowyResult<()> {
(**self).update_view(changeset)
}
fn delete_view(&self, view_id: &str) -> FlowyResult<()> {
(**self).delete_view(view_id)
}
fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()> {
(**self).create_trash(trashes)
}
fn read_trash(&self, trash_id: Option<String>) -> FlowyResult<RepeatedTrash> {
(**self).read_trash(trash_id)
}
fn delete_trash(&self, trash_ids: Option<Vec<String>>) -> FlowyResult<()> {
(**self).delete_trash(trash_ids)
}
}

View File

@ -0,0 +1,245 @@
use crate::{
entities::{
trash::{Trash, TrashType},
view::{RepeatedView, UpdateViewParams, View, ViewType},
},
errors::FlowyError,
services::persistence::version_1::app_sql::AppTable,
};
use diesel::sql_types::Integer;
use flowy_database::{
prelude::*,
schema::{view_table, view_table::dsl},
SqliteConnection,
};
use lib_infra::timestamp;
pub struct ViewTableSql();
impl ViewTableSql {
pub(crate) fn create_view(view: View, conn: &SqliteConnection) -> Result<(), FlowyError> {
let view_table = ViewTable::new(view);
match diesel_record_count!(view_table, &view_table.id, conn) {
0 => diesel_insert_table!(view_table, &view_table, conn),
_ => {
let changeset = ViewChangeset::from_table(view_table);
diesel_update_table!(view_table, changeset, conn)
}
}
Ok(())
}
pub(crate) fn read_view(view_id: &str, conn: &SqliteConnection) -> Result<ViewTable, FlowyError> {
// https://docs.diesel.rs/diesel/query_builder/struct.UpdateStatement.html
// let mut filter =
// dsl::view_table.filter(view_table::id.eq(view_id)).into_boxed();
// if let Some(is_trash) = is_trash {
// filter = filter.filter(view_table::is_trash.eq(is_trash));
// }
// let repeated_view = filter.first::<ViewTable>(conn)?;
let view_table = dsl::view_table
.filter(view_table::id.eq(view_id))
.first::<ViewTable>(conn)?;
Ok(view_table)
}
// belong_to_id will be the app_id or view_id.
pub(crate) fn read_views(belong_to_id: &str, conn: &SqliteConnection) -> Result<Vec<ViewTable>, FlowyError> {
let view_tables = dsl::view_table
.filter(view_table::belong_to_id.eq(belong_to_id))
.order(view_table::create_time.asc())
.into_boxed()
.load::<ViewTable>(conn)?;
Ok(view_tables)
}
pub(crate) fn update_view(changeset: ViewChangeset, conn: &SqliteConnection) -> Result<(), FlowyError> {
diesel_update_table!(view_table, changeset, conn);
Ok(())
}
pub(crate) fn delete_view(view_id: &str, conn: &SqliteConnection) -> Result<(), FlowyError> {
diesel_delete_table!(view_table, view_id, conn);
Ok(())
}
}
// pub(crate) fn read_views(
// belong_to_id: &str,
// is_trash: Option<bool>,
// conn: &SqliteConnection,
// ) -> Result<RepeatedView, FlowyError> {
// let views = dsl::view_table
// .inner_join(trash_table::dsl::trash_table.on(trash_id.ne(view_table::
// id))) .filter(view_table::belong_to_id.eq(belong_to_id))
// .select((
// view_table::id,
// view_table::belong_to_id,
// view_table::name,
// view_table::desc,
// view_table::modified_time,
// view_table::create_time,
// view_table::thumbnail,
// view_table::view_type,
// view_table::version,
// ))
// .load(conn)?
// .into_iter()
// .map(
// |(id, belong_to_id, name, desc, create_time, modified_time,
// thumbnail, view_type, version)| { ViewTable {
// id,
// belong_to_id,
// name,
// desc,
// modified_time,
// create_time,
// thumbnail,
// view_type,
// version,
// is_trash: false,
// }
// .into()
// },
// )
// .collect::<Vec<View>>();
//
// Ok(RepeatedView { items: views })
// }
#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)]
#[belongs_to(AppTable, foreign_key = "belong_to_id")]
#[table_name = "view_table"]
pub(crate) struct ViewTable {
pub id: String,
pub belong_to_id: String,
pub name: String,
pub desc: String,
pub modified_time: i64,
pub create_time: i64,
pub thumbnail: String,
pub view_type: ViewTableType,
pub version: i64,
pub is_trash: bool,
}
impl ViewTable {
pub fn new(view: View) -> Self {
let view_type = match view.view_type {
ViewType::Blank => ViewTableType::Docs,
ViewType::Doc => ViewTableType::Docs,
};
ViewTable {
id: view.id,
belong_to_id: view.belong_to_id,
name: view.name,
desc: view.desc,
modified_time: view.modified_time,
create_time: view.create_time,
// TODO: thumbnail
thumbnail: "".to_owned(),
view_type,
version: 0,
is_trash: false,
}
}
}
impl std::convert::From<ViewTable> for View {
fn from(table: ViewTable) -> Self {
let view_type = match table.view_type {
ViewTableType::Docs => ViewType::Doc,
};
View {
id: table.id,
belong_to_id: table.belong_to_id,
name: table.name,
desc: table.desc,
view_type,
belongings: RepeatedView::default(),
modified_time: table.modified_time,
version: table.version,
create_time: table.create_time,
}
}
}
impl std::convert::From<ViewTable> for Trash {
fn from(table: ViewTable) -> Self {
Trash {
id: table.id,
name: table.name,
modified_time: table.modified_time,
create_time: table.create_time,
ty: TrashType::View,
}
}
}
#[derive(AsChangeset, Identifiable, Clone, Default, Debug)]
#[table_name = "view_table"]
pub struct ViewChangeset {
pub id: String,
pub name: Option<String>,
pub desc: Option<String>,
pub thumbnail: Option<String>,
pub modified_time: i64,
}
impl ViewChangeset {
pub(crate) fn new(params: UpdateViewParams) -> Self {
ViewChangeset {
id: params.view_id,
name: params.name,
desc: params.desc,
thumbnail: params.thumbnail,
modified_time: timestamp(),
}
}
pub(crate) fn from_table(table: ViewTable) -> Self {
ViewChangeset {
id: table.id,
name: Some(table.name),
desc: Some(table.desc),
thumbnail: Some(table.thumbnail),
modified_time: table.modified_time,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, FromSqlRow, AsExpression)]
#[repr(i32)]
#[sql_type = "Integer"]
pub enum ViewTableType {
Docs = 0,
}
impl std::default::Default for ViewTableType {
fn default() -> Self {
ViewTableType::Docs
}
}
impl std::convert::From<i32> for ViewTableType {
fn from(value: i32) -> Self {
match value {
0 => ViewTableType::Docs,
o => {
log::error!("Unsupported view type {}, fallback to ViewType::Docs", o);
ViewTableType::Docs
}
}
}
}
impl ViewTableType {
pub fn value(&self) -> i32 {
*self as i32
}
}
impl_sql_integer_expression!(ViewTableType);

View File

@ -0,0 +1,127 @@
use crate::{
entities::{
app::RepeatedApp,
workspace::{UpdateWorkspaceParams, Workspace},
},
errors::FlowyError,
};
use diesel::SqliteConnection;
use flowy_database::{
prelude::*,
schema::{workspace_table, workspace_table::dsl},
};
pub(crate) struct WorkspaceTableSql();
impl WorkspaceTableSql {
pub(crate) fn create_workspace(
user_id: &str,
workspace: Workspace,
conn: &SqliteConnection,
) -> Result<(), FlowyError> {
let table = WorkspaceTable::new(workspace, user_id);
match diesel_record_count!(workspace_table, &table.id, conn) {
0 => diesel_insert_table!(workspace_table, &table, conn),
_ => {
let changeset = WorkspaceChangeset::from_table(table);
diesel_update_table!(workspace_table, changeset, conn);
}
}
Ok(())
}
pub(crate) fn read_workspaces(
user_id: &str,
workspace_id: Option<String>,
conn: &SqliteConnection,
) -> Result<Vec<WorkspaceTable>, FlowyError> {
let mut filter = dsl::workspace_table
.filter(workspace_table::user_id.eq(user_id))
.order(workspace_table::create_time.asc())
.into_boxed();
if let Some(workspace_id) = workspace_id {
filter = filter.filter(workspace_table::id.eq(workspace_id));
};
let workspaces = filter.load::<WorkspaceTable>(conn)?;
Ok(workspaces)
}
#[allow(dead_code)]
pub(crate) fn update_workspace(changeset: WorkspaceChangeset, conn: &SqliteConnection) -> Result<(), FlowyError> {
diesel_update_table!(workspace_table, changeset, conn);
Ok(())
}
#[allow(dead_code)]
pub(crate) fn delete_workspace(workspace_id: &str, conn: &SqliteConnection) -> Result<(), FlowyError> {
diesel_delete_table!(workspace_table, workspace_id, conn);
Ok(())
}
}
#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable)]
#[table_name = "workspace_table"]
pub struct WorkspaceTable {
pub id: String,
pub name: String,
pub desc: String,
pub modified_time: i64,
pub create_time: i64,
pub user_id: String,
pub version: i64,
}
impl WorkspaceTable {
#[allow(dead_code)]
pub fn new(workspace: Workspace, user_id: &str) -> Self {
WorkspaceTable {
id: workspace.id,
name: workspace.name,
desc: workspace.desc,
modified_time: workspace.modified_time,
create_time: workspace.create_time,
user_id: user_id.to_owned(),
version: 0,
}
}
}
impl std::convert::From<WorkspaceTable> for Workspace {
fn from(table: WorkspaceTable) -> Self {
Workspace {
id: table.id,
name: table.name,
desc: table.desc,
apps: RepeatedApp::default(),
modified_time: table.modified_time,
create_time: table.create_time,
}
}
}
#[derive(AsChangeset, Identifiable, Clone, Default, Debug)]
#[table_name = "workspace_table"]
pub struct WorkspaceChangeset {
pub id: String,
pub name: Option<String>,
pub desc: Option<String>,
}
impl WorkspaceChangeset {
pub fn new(params: UpdateWorkspaceParams) -> Self {
WorkspaceChangeset {
id: params.id,
name: params.name,
desc: params.desc,
}
}
pub(crate) fn from_table(table: WorkspaceTable) -> Self {
WorkspaceChangeset {
id: table.id,
name: Some(table.name),
desc: Some(table.desc),
}
}
}

View File

@ -0,0 +1 @@
pub mod v2_impl;

View File

@ -0,0 +1,212 @@
use crate::services::{
folder_editor::FolderEditor,
persistence::{AppChangeset, FolderPersistenceTransaction, ViewChangeset, WorkspaceChangeset},
};
use flowy_error::{FlowyError, FlowyResult};
use flowy_folder_data_model::entities::{
app::App,
trash::{RepeatedTrash, Trash},
view::View,
workspace::Workspace,
};
use std::sync::Arc;
impl FolderPersistenceTransaction for FolderEditor {
fn create_workspace(&self, _user_id: &str, workspace: Workspace) -> FlowyResult<()> {
if let Some(change) = self.folder.write().create_workspace(workspace)? {
let _ = self.apply_change(change)?;
}
Ok(())
}
fn read_workspaces(&self, _user_id: &str, workspace_id: Option<String>) -> FlowyResult<Vec<Workspace>> {
let workspaces = self.folder.read().read_workspaces(workspace_id)?;
Ok(workspaces)
}
fn update_workspace(&self, changeset: WorkspaceChangeset) -> FlowyResult<()> {
if let Some(change) = self
.folder
.write()
.update_workspace(&changeset.id, changeset.name, changeset.desc)?
{
let _ = self.apply_change(change)?;
}
Ok(())
}
fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> {
if let Some(change) = self.folder.write().delete_workspace(workspace_id)? {
let _ = self.apply_change(change)?;
}
Ok(())
}
fn create_app(&self, app: App) -> FlowyResult<()> {
if let Some(change) = self.folder.write().create_app(app)? {
let _ = self.apply_change(change)?;
}
Ok(())
}
fn update_app(&self, changeset: AppChangeset) -> FlowyResult<()> {
if let Some(change) = self
.folder
.write()
.update_app(&changeset.id, changeset.name, changeset.desc)?
{
let _ = self.apply_change(change)?;
}
Ok(())
}
fn read_app(&self, app_id: &str) -> FlowyResult<App> {
let app = self.folder.read().read_app(app_id)?;
Ok(app)
}
fn read_workspace_apps(&self, workspace_id: &str) -> FlowyResult<Vec<App>> {
let workspaces = self.folder.read().read_workspaces(Some(workspace_id.to_owned()))?;
match workspaces.first() {
None => {
Err(FlowyError::record_not_found().context(format!("can't find workspace with id {}", workspace_id)))
}
Some(workspace) => Ok(workspace.apps.clone().take_items()),
}
}
fn delete_app(&self, app_id: &str) -> FlowyResult<App> {
let app = self.folder.read().read_app(app_id)?;
if let Some(change) = self.folder.write().delete_app(app_id)? {
let _ = self.apply_change(change)?;
}
Ok(app)
}
fn create_view(&self, view: View) -> FlowyResult<()> {
if let Some(change) = self.folder.write().create_view(view)? {
let _ = self.apply_change(change)?;
}
Ok(())
}
fn read_view(&self, view_id: &str) -> FlowyResult<View> {
let view = self.folder.read().read_view(view_id)?;
Ok(view)
}
fn read_views(&self, belong_to_id: &str) -> FlowyResult<Vec<View>> {
let views = self.folder.read().read_views(belong_to_id)?;
Ok(views)
}
fn update_view(&self, changeset: ViewChangeset) -> FlowyResult<()> {
if let Some(change) =
self.folder
.write()
.update_view(&changeset.id, changeset.name, changeset.desc, changeset.modified_time)?
{
let _ = self.apply_change(change)?;
}
Ok(())
}
fn delete_view(&self, view_id: &str) -> FlowyResult<()> {
if let Some(change) = self.folder.write().delete_view(view_id)? {
let _ = self.apply_change(change)?;
}
Ok(())
}
fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()> {
if let Some(change) = self.folder.write().create_trash(trashes)? {
let _ = self.apply_change(change)?;
}
Ok(())
}
fn read_trash(&self, trash_id: Option<String>) -> FlowyResult<RepeatedTrash> {
let trash = self.folder.read().read_trash(trash_id)?;
Ok(RepeatedTrash { items: trash })
}
fn delete_trash(&self, trash_ids: Option<Vec<String>>) -> FlowyResult<()> {
if let Some(change) = self.folder.write().delete_trash(trash_ids)? {
let _ = self.apply_change(change)?;
}
Ok(())
}
}
impl<T> FolderPersistenceTransaction for Arc<T>
where
T: FolderPersistenceTransaction + ?Sized,
{
fn create_workspace(&self, user_id: &str, workspace: Workspace) -> FlowyResult<()> {
(**self).create_workspace(user_id, workspace)
}
fn read_workspaces(&self, user_id: &str, workspace_id: Option<String>) -> FlowyResult<Vec<Workspace>> {
(**self).read_workspaces(user_id, workspace_id)
}
fn update_workspace(&self, changeset: WorkspaceChangeset) -> FlowyResult<()> {
(**self).update_workspace(changeset)
}
fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> {
(**self).delete_workspace(workspace_id)
}
fn create_app(&self, app: App) -> FlowyResult<()> {
(**self).create_app(app)
}
fn update_app(&self, changeset: AppChangeset) -> FlowyResult<()> {
(**self).update_app(changeset)
}
fn read_app(&self, app_id: &str) -> FlowyResult<App> {
(**self).read_app(app_id)
}
fn read_workspace_apps(&self, workspace_id: &str) -> FlowyResult<Vec<App>> {
(**self).read_workspace_apps(workspace_id)
}
fn delete_app(&self, app_id: &str) -> FlowyResult<App> {
(**self).delete_app(app_id)
}
fn create_view(&self, view: View) -> FlowyResult<()> {
(**self).create_view(view)
}
fn read_view(&self, view_id: &str) -> FlowyResult<View> {
(**self).read_view(view_id)
}
fn read_views(&self, belong_to_id: &str) -> FlowyResult<Vec<View>> {
(**self).read_views(belong_to_id)
}
fn update_view(&self, changeset: ViewChangeset) -> FlowyResult<()> {
(**self).update_view(changeset)
}
fn delete_view(&self, view_id: &str) -> FlowyResult<()> {
(**self).delete_view(view_id)
}
fn create_trash(&self, trashes: Vec<Trash>) -> FlowyResult<()> {
(**self).create_trash(trashes)
}
fn read_trash(&self, trash_id: Option<String>) -> FlowyResult<RepeatedTrash> {
(**self).read_trash(trash_id)
}
fn delete_trash(&self, trash_ids: Option<Vec<String>>) -> FlowyResult<()> {
(**self).delete_trash(trash_ids)
}
}

View File

@ -0,0 +1,331 @@
use crate::{
dart_notification::{send_anonymous_dart_notification, FolderNotification},
entities::trash::{RepeatedTrash, RepeatedTrashId, Trash, TrashId, TrashType},
errors::{FlowyError, FlowyResult},
module::{FolderCouldServiceV1, WorkspaceUser},
services::persistence::{FolderPersistence, FolderPersistenceTransaction},
};
use std::{fmt::Formatter, sync::Arc};
use tokio::sync::{broadcast, mpsc};
pub struct TrashController {
persistence: Arc<FolderPersistence>,
notify: broadcast::Sender<TrashEvent>,
cloud_service: Arc<dyn FolderCouldServiceV1>,
user: Arc<dyn WorkspaceUser>,
}
impl TrashController {
pub fn new(
persistence: Arc<FolderPersistence>,
cloud_service: Arc<dyn FolderCouldServiceV1>,
user: Arc<dyn WorkspaceUser>,
) -> Self {
let (tx, _) = broadcast::channel(10);
Self {
persistence,
notify: tx,
cloud_service,
user,
}
}
#[tracing::instrument(level = "debug", skip(self), fields(putback) err)]
pub async fn putback(&self, trash_id: &str) -> FlowyResult<()> {
let (tx, mut rx) = mpsc::channel::<FlowyResult<()>>(1);
let trash = self
.persistence
.begin_transaction(|transaction| {
let mut repeated_trash = transaction.read_trash(Some(trash_id.to_owned()))?;
let _ = transaction.delete_trash(Some(vec![trash_id.to_owned()]))?;
notify_trash_changed(transaction.read_trash(None)?);
if repeated_trash.is_empty() {
return Err(FlowyError::internal().context("Try to put back trash is not exists"));
}
Ok(repeated_trash.pop().unwrap())
})
.await?;
let identifier = TrashId {
id: trash.id,
ty: trash.ty,
};
let _ = self.delete_trash_on_server(RepeatedTrashId {
items: vec![identifier.clone()],
delete_all: false,
})?;
tracing::Span::current().record("putback", &format!("{:?}", &identifier).as_str());
let _ = self.notify.send(TrashEvent::Putback(vec![identifier].into(), tx));
let _ = rx.recv().await.unwrap()?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self) err)]
pub async fn restore_all_trash(&self) -> FlowyResult<()> {
let repeated_trash = self
.persistence
.begin_transaction(|transaction| {
let trash = transaction.read_trash(None);
let _ = transaction.delete_trash(None);
trash
})
.await?;
let identifiers: RepeatedTrashId = repeated_trash.items.clone().into();
let (tx, mut rx) = mpsc::channel::<FlowyResult<()>>(1);
let _ = self.notify.send(TrashEvent::Putback(identifiers, tx));
let _ = rx.recv().await;
notify_trash_changed(RepeatedTrash { items: vec![] });
let _ = self.delete_all_trash_on_server().await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self), err)]
pub async fn delete_all_trash(&self) -> FlowyResult<()> {
let repeated_trash = self
.persistence
.begin_transaction(|transaction| transaction.read_trash(None))
.await?;
let trash_identifiers: RepeatedTrashId = repeated_trash.items.clone().into();
let _ = self.delete_with_identifiers(trash_identifiers.clone()).await?;
notify_trash_changed(RepeatedTrash { items: vec![] });
let _ = self.delete_all_trash_on_server().await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self), err)]
pub async fn delete(&self, trash_identifiers: RepeatedTrashId) -> FlowyResult<()> {
let _ = self.delete_with_identifiers(trash_identifiers.clone()).await?;
let repeated_trash = self
.persistence
.begin_transaction(|transaction| transaction.read_trash(None))
.await?;
notify_trash_changed(repeated_trash);
let _ = self.delete_trash_on_server(trash_identifiers)?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self), fields(delete_trash_ids), err)]
pub async fn delete_with_identifiers(&self, trash_identifiers: RepeatedTrashId) -> FlowyResult<()> {
let (tx, mut rx) = mpsc::channel::<FlowyResult<()>>(1);
tracing::Span::current().record("delete_trash_ids", &format!("{}", trash_identifiers).as_str());
let _ = self.notify.send(TrashEvent::Delete(trash_identifiers.clone(), tx));
match rx.recv().await {
None => {}
Some(result) => match result {
Ok(_) => {}
Err(e) => log::error!("{}", e),
},
}
let _ = self
.persistence
.begin_transaction(|transaction| {
let ids = trash_identifiers
.items
.into_iter()
.map(|item| item.id)
.collect::<Vec<_>>();
transaction.delete_trash(Some(ids))
})
.await?;
Ok(())
}
// [[ transaction ]]
// https://www.tutlane.com/tutorial/sqlite/sqlite-transactions-begin-commit-rollback
// We can use these commands only when we are performing INSERT, UPDATE, and
// DELETE operations. Its not possible for us to use these commands to
// CREATE and DROP tables operations because those are auto-commit in the
// database.
#[tracing::instrument(name = "add_trash", level = "debug", skip(self, trash), fields(trash_ids), err)]
pub async fn add<T: Into<Trash>>(&self, trash: Vec<T>) -> Result<(), FlowyError> {
let (tx, mut rx) = mpsc::channel::<FlowyResult<()>>(1);
let repeated_trash = trash.into_iter().map(|t| t.into()).collect::<Vec<Trash>>();
let identifiers = repeated_trash.iter().map(|t| t.into()).collect::<Vec<TrashId>>();
tracing::Span::current().record(
"trash_ids",
&format!(
"{:?}",
identifiers
.iter()
.map(|identifier| format!("{:?}:{}", identifier.ty, identifier.id))
.collect::<Vec<_>>()
)
.as_str(),
);
let _ = self
.persistence
.begin_transaction(|transaction| {
let _ = transaction.create_trash(repeated_trash.clone())?;
let _ = self.create_trash_on_server(repeated_trash);
notify_trash_changed(transaction.read_trash(None)?);
Ok(())
})
.await?;
let _ = self.notify.send(TrashEvent::NewTrash(identifiers.into(), tx));
let _ = rx.recv().await.unwrap()?;
Ok(())
}
pub fn subscribe(&self) -> broadcast::Receiver<TrashEvent> {
self.notify.subscribe()
}
pub async fn read_trash(&self) -> Result<RepeatedTrash, FlowyError> {
let repeated_trash = self
.persistence
.begin_transaction(|transaction| transaction.read_trash(None))
.await?;
let _ = self.read_trash_on_server()?;
Ok(repeated_trash)
}
pub fn read_trash_ids<'a>(
&self,
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
) -> Result<Vec<String>, FlowyError> {
let ids = transaction
.read_trash(None)?
.into_inner()
.into_iter()
.map(|item| item.id)
.collect::<Vec<String>>();
Ok(ids)
}
}
impl TrashController {
#[tracing::instrument(level = "trace", skip(self, trash), err)]
fn create_trash_on_server<T: Into<RepeatedTrashId>>(&self, trash: T) -> FlowyResult<()> {
let token = self.user.token()?;
let trash_identifiers = trash.into();
let server = self.cloud_service.clone();
// TODO: retry?
let _ = tokio::spawn(async move {
match server.create_trash(&token, trash_identifiers).await {
Ok(_) => {}
Err(e) => log::error!("Create trash failed: {:?}", e),
}
});
Ok(())
}
#[tracing::instrument(level = "trace", skip(self, trash), err)]
fn delete_trash_on_server<T: Into<RepeatedTrashId>>(&self, trash: T) -> FlowyResult<()> {
let token = self.user.token()?;
let trash_identifiers = trash.into();
let server = self.cloud_service.clone();
let _ = tokio::spawn(async move {
match server.delete_trash(&token, trash_identifiers).await {
Ok(_) => {}
Err(e) => log::error!("Delete trash failed: {:?}", e),
}
});
Ok(())
}
#[tracing::instrument(level = "trace", skip(self), err)]
fn read_trash_on_server(&self) -> FlowyResult<()> {
let token = self.user.token()?;
let server = self.cloud_service.clone();
let persistence = self.persistence.clone();
tokio::spawn(async move {
match server.read_trash(&token).await {
Ok(repeated_trash) => {
tracing::debug!("Remote trash count: {}", repeated_trash.items.len());
let result = persistence
.begin_transaction(|transaction| {
let _ = transaction.create_trash(repeated_trash.items.clone())?;
transaction.read_trash(None)
})
.await;
match result {
Ok(repeated_trash) => {
notify_trash_changed(repeated_trash);
}
Err(e) => log::error!("Save trash failed: {:?}", e),
}
}
Err(e) => log::error!("Read trash failed: {:?}", e),
}
});
Ok(())
}
#[tracing::instrument(level = "trace", skip(self), err)]
async fn delete_all_trash_on_server(&self) -> FlowyResult<()> {
let token = self.user.token()?;
let server = self.cloud_service.clone();
server.delete_trash(&token, RepeatedTrashId::all()).await
}
}
#[tracing::instrument(skip(repeated_trash), fields(n_trash))]
fn notify_trash_changed(repeated_trash: RepeatedTrash) {
tracing::Span::current().record("n_trash", &repeated_trash.len());
send_anonymous_dart_notification(FolderNotification::TrashUpdated)
.payload(repeated_trash)
.send();
}
#[derive(Clone)]
pub enum TrashEvent {
NewTrash(RepeatedTrashId, mpsc::Sender<FlowyResult<()>>),
Putback(RepeatedTrashId, mpsc::Sender<FlowyResult<()>>),
Delete(RepeatedTrashId, mpsc::Sender<FlowyResult<()>>),
}
impl std::fmt::Debug for TrashEvent {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
TrashEvent::NewTrash(identifiers, _) => f.write_str(&format!("{:?}", identifiers)),
TrashEvent::Putback(identifiers, _) => f.write_str(&format!("{:?}", identifiers)),
TrashEvent::Delete(identifiers, _) => f.write_str(&format!("{:?}", identifiers)),
}
}
}
impl TrashEvent {
pub fn select(self, s: TrashType) -> Option<TrashEvent> {
match self {
TrashEvent::Putback(mut identifiers, sender) => {
identifiers.items.retain(|item| item.ty == s);
if identifiers.items.is_empty() {
None
} else {
Some(TrashEvent::Putback(identifiers, sender))
}
}
TrashEvent::Delete(mut identifiers, sender) => {
identifiers.items.retain(|item| item.ty == s);
if identifiers.items.is_empty() {
None
} else {
Some(TrashEvent::Delete(identifiers, sender))
}
}
TrashEvent::NewTrash(mut identifiers, sender) => {
identifiers.items.retain(|item| item.ty == s);
if identifiers.items.is_empty() {
None
} else {
Some(TrashEvent::NewTrash(identifiers, sender))
}
}
}
}
}

View File

@ -0,0 +1,45 @@
use crate::{
entities::trash::{RepeatedTrash, RepeatedTrashId, TrashId},
errors::FlowyError,
services::TrashController,
};
use lib_dispatch::prelude::{data_result, Data, DataResult, Unit};
use std::sync::Arc;
#[tracing::instrument(skip(controller), err)]
pub(crate) async fn read_trash_handler(
controller: Unit<Arc<TrashController>>,
) -> DataResult<RepeatedTrash, FlowyError> {
let repeated_trash = controller.read_trash().await?;
data_result(repeated_trash)
}
#[tracing::instrument(skip(identifier, controller), err)]
pub(crate) async fn putback_trash_handler(
identifier: Data<TrashId>,
controller: Unit<Arc<TrashController>>,
) -> Result<(), FlowyError> {
let _ = controller.putback(&identifier.id).await?;
Ok(())
}
#[tracing::instrument(skip(identifiers, controller), err)]
pub(crate) async fn delete_trash_handler(
identifiers: Data<RepeatedTrashId>,
controller: Unit<Arc<TrashController>>,
) -> Result<(), FlowyError> {
let _ = controller.delete(identifiers.into_inner()).await?;
Ok(())
}
#[tracing::instrument(skip(controller), err)]
pub(crate) async fn restore_all_trash_handler(controller: Unit<Arc<TrashController>>) -> Result<(), FlowyError> {
let _ = controller.restore_all_trash().await?;
Ok(())
}
#[tracing::instrument(skip(controller), err)]
pub(crate) async fn delete_all_trash_handler(controller: Unit<Arc<TrashController>>) -> Result<(), FlowyError> {
let _ = controller.delete_all_trash().await?;
Ok(())
}

View File

@ -0,0 +1,2 @@
pub mod controller;
pub mod event_handler;

View File

@ -0,0 +1,434 @@
use bytes::Bytes;
use flowy_collaboration::entities::{
document_info::{DocumentDelta, DocumentId},
revision::{RepeatedRevision, Revision},
};
use flowy_collaboration::client_document::default::initial_delta_string;
use futures::{FutureExt, StreamExt};
use std::{collections::HashSet, sync::Arc};
use crate::{
dart_notification::{send_dart_notification, FolderNotification},
entities::{
trash::{RepeatedTrashId, TrashType},
view::{CreateViewParams, RepeatedView, UpdateViewParams, View, ViewId},
},
errors::{FlowyError, FlowyResult},
module::{FolderCouldServiceV1, WorkspaceUser},
services::{
persistence::{FolderPersistence, FolderPersistenceTransaction, ViewChangeset},
TrashController, TrashEvent,
},
};
use flowy_database::kv::KV;
use flowy_document::FlowyDocumentManager;
use flowy_folder_data_model::entities::share::{ExportData, ExportParams};
use lib_infra::uuid_string;
const LATEST_VIEW_ID: &str = "latest_view_id";
pub(crate) struct ViewController {
user: Arc<dyn WorkspaceUser>,
cloud_service: Arc<dyn FolderCouldServiceV1>,
persistence: Arc<FolderPersistence>,
trash_controller: Arc<TrashController>,
document_manager: Arc<FlowyDocumentManager>,
}
impl ViewController {
pub(crate) fn new(
user: Arc<dyn WorkspaceUser>,
persistence: Arc<FolderPersistence>,
cloud_service: Arc<dyn FolderCouldServiceV1>,
trash_can: Arc<TrashController>,
document_manager: Arc<FlowyDocumentManager>,
) -> Self {
Self {
user,
cloud_service,
persistence,
trash_controller: trash_can,
document_manager,
}
}
pub(crate) fn initialize(&self) -> Result<(), FlowyError> {
let _ = self.document_manager.init()?;
self.listen_trash_can_event();
Ok(())
}
#[tracing::instrument(level = "trace", skip(self, params), fields(name = %params.name), err)]
pub(crate) async fn create_view_from_params(&self, params: CreateViewParams) -> Result<View, FlowyError> {
let view_data = if params.view_data.is_empty() {
initial_delta_string()
} else {
params.view_data.clone()
};
let delta_data = Bytes::from(view_data);
let user_id = self.user.user_id()?;
let repeated_revision: RepeatedRevision =
Revision::initial_revision(&user_id, &params.view_id, delta_data).into();
let _ = self
.document_manager
.save_document(&params.view_id, repeated_revision)
.await?;
let view = self.create_view_on_server(params).await?;
let _ = self.create_view_on_local(view.clone()).await?;
Ok(view)
}
#[tracing::instrument(level = "debug", skip(self, view_id, view_data), err)]
pub(crate) async fn create_view_document_content(
&self,
view_id: &str,
view_data: String,
) -> Result<(), FlowyError> {
if view_data.is_empty() {
return Err(FlowyError::internal().context("The content of the view should not be empty"));
}
let delta_data = Bytes::from(view_data);
let user_id = self.user.user_id()?;
let repeated_revision: RepeatedRevision = Revision::initial_revision(&user_id, view_id, delta_data).into();
let _ = self.document_manager.save_document(view_id, repeated_revision).await?;
Ok(())
}
pub(crate) async fn create_view_on_local(&self, view: View) -> Result<(), FlowyError> {
let trash_controller = self.trash_controller.clone();
self.persistence
.begin_transaction(|transaction| {
let belong_to_id = view.belong_to_id.clone();
let _ = transaction.create_view(view)?;
let _ = notify_views_changed(&belong_to_id, trash_controller, &transaction)?;
Ok(())
})
.await
}
#[tracing::instrument(skip(self, params), fields(view_id = %params.view_id), err)]
pub(crate) async fn read_view(&self, params: ViewId) -> Result<View, FlowyError> {
let view = self
.persistence
.begin_transaction(|transaction| {
let view = transaction.read_view(&params.view_id)?;
let trash_ids = self.trash_controller.read_trash_ids(&transaction)?;
if trash_ids.contains(&view.id) {
return Err(FlowyError::record_not_found());
}
Ok(view)
})
.await?;
let _ = self.read_view_on_server(params);
Ok(view)
}
pub(crate) async fn read_local_views(&self, ids: Vec<String>) -> Result<Vec<View>, FlowyError> {
self.persistence
.begin_transaction(|transaction| {
let mut views = vec![];
for view_id in ids {
views.push(transaction.read_view(&view_id)?);
}
Ok(views)
})
.await
}
#[tracing::instrument(level = "debug", skip(self), err)]
pub(crate) async fn open_document(&self, doc_id: &str) -> Result<DocumentDelta, FlowyError> {
let editor = self.document_manager.open_document(doc_id).await?;
KV::set_str(LATEST_VIEW_ID, doc_id.to_owned());
let document_json = editor.document_json().await?;
Ok(DocumentDelta {
doc_id: doc_id.to_string(),
delta_json: document_json,
})
}
#[tracing::instrument(level = "debug", skip(self), err)]
pub(crate) async fn close_view(&self, doc_id: &str) -> Result<(), FlowyError> {
let _ = self.document_manager.close_document(doc_id)?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self,params), fields(doc_id = %params.doc_id), err)]
pub(crate) async fn delete_view(&self, params: DocumentId) -> Result<(), FlowyError> {
if let Some(view_id) = KV::get_str(LATEST_VIEW_ID) {
if view_id == params.doc_id {
let _ = KV::remove(LATEST_VIEW_ID);
}
}
let _ = self.document_manager.close_document(&params.doc_id)?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self), err)]
pub(crate) async fn duplicate_view(&self, doc_id: &str) -> Result<(), FlowyError> {
let view = self
.persistence
.begin_transaction(|transaction| transaction.read_view(doc_id))
.await?;
let editor = self.document_manager.open_document(doc_id).await?;
let document_json = editor.document_json().await?;
let duplicate_params = CreateViewParams {
belong_to_id: view.belong_to_id.clone(),
name: format!("{} (copy)", &view.name),
desc: view.desc.clone(),
thumbnail: "".to_owned(),
view_type: view.view_type.clone(),
view_data: document_json,
view_id: uuid_string(),
};
let _ = self.create_view_from_params(duplicate_params).await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip(self, params), err)]
pub(crate) async fn export_doc(&self, params: ExportParams) -> Result<ExportData, FlowyError> {
let editor = self.document_manager.open_document(&params.doc_id).await?;
let delta_json = editor.document_json().await?;
Ok(ExportData {
data: delta_json,
export_type: params.export_type,
})
}
// belong_to_id will be the app_id or view_id.
#[tracing::instrument(level = "debug", skip(self), err)]
pub(crate) async fn read_views_belong_to(&self, belong_to_id: &str) -> Result<RepeatedView, FlowyError> {
self.persistence
.begin_transaction(|transaction| {
read_belonging_views_on_local(belong_to_id, self.trash_controller.clone(), &transaction)
})
.await
}
#[tracing::instrument(level = "debug", skip(self, params), err)]
pub(crate) async fn update_view(&self, params: UpdateViewParams) -> Result<View, FlowyError> {
let changeset = ViewChangeset::new(params.clone());
let view_id = changeset.id.clone();
let view = self
.persistence
.begin_transaction(|transaction| {
let _ = transaction.update_view(changeset)?;
let view = transaction.read_view(&view_id)?;
send_dart_notification(&view_id, FolderNotification::ViewUpdated)
.payload(view.clone())
.send();
let _ = notify_views_changed(&view.belong_to_id, self.trash_controller.clone(), &transaction)?;
Ok(view)
})
.await?;
let _ = self.update_view_on_server(params);
Ok(view)
}
pub(crate) async fn receive_document_delta(&self, params: DocumentDelta) -> Result<DocumentDelta, FlowyError> {
let doc = self.document_manager.receive_local_delta(params).await?;
Ok(doc)
}
pub(crate) async fn latest_visit_view(&self) -> FlowyResult<Option<View>> {
match KV::get_str(LATEST_VIEW_ID) {
None => Ok(None),
Some(view_id) => {
let view = self
.persistence
.begin_transaction(|transaction| transaction.read_view(&view_id))
.await?;
Ok(Some(view))
}
}
}
pub(crate) fn set_latest_view(&self, view: &View) {
KV::set_str(LATEST_VIEW_ID, view.id.clone());
}
}
impl ViewController {
#[tracing::instrument(skip(self), err)]
async fn create_view_on_server(&self, params: CreateViewParams) -> Result<View, FlowyError> {
let token = self.user.token()?;
let view = self.cloud_service.create_view(&token, params).await?;
Ok(view)
}
#[tracing::instrument(skip(self), err)]
fn update_view_on_server(&self, params: UpdateViewParams) -> Result<(), FlowyError> {
let token = self.user.token()?;
let server = self.cloud_service.clone();
tokio::spawn(async move {
match server.update_view(&token, params).await {
Ok(_) => {}
Err(e) => {
// TODO: retry?
log::error!("Update view failed: {:?}", e);
}
}
});
Ok(())
}
#[tracing::instrument(skip(self), err)]
fn read_view_on_server(&self, params: ViewId) -> Result<(), FlowyError> {
let token = self.user.token()?;
let server = self.cloud_service.clone();
let persistence = self.persistence.clone();
// TODO: Retry with RetryAction?
tokio::spawn(async move {
match server.read_view(&token, params).await {
Ok(Some(view)) => {
match persistence
.begin_transaction(|transaction| transaction.create_view(view.clone()))
.await
{
Ok(_) => {
send_dart_notification(&view.id, FolderNotification::ViewUpdated)
.payload(view.clone())
.send();
}
Err(e) => log::error!("Save view failed: {:?}", e),
}
}
Ok(None) => {}
Err(e) => log::error!("Read view failed: {:?}", e),
}
});
Ok(())
}
fn listen_trash_can_event(&self) {
let mut rx = self.trash_controller.subscribe();
let persistence = self.persistence.clone();
let document_manager = self.document_manager.clone();
let trash_controller = self.trash_controller.clone();
let _ = tokio::spawn(async move {
loop {
let mut stream = Box::pin(rx.recv().into_stream().filter_map(|result| async move {
match result {
Ok(event) => event.select(TrashType::View),
Err(_e) => None,
}
}));
if let Some(event) = stream.next().await {
handle_trash_event(
persistence.clone(),
document_manager.clone(),
trash_controller.clone(),
event,
)
.await
}
}
});
}
}
#[tracing::instrument(level = "trace", skip(persistence, document_manager, trash_can))]
async fn handle_trash_event(
persistence: Arc<FolderPersistence>,
document_manager: Arc<FlowyDocumentManager>,
trash_can: Arc<TrashController>,
event: TrashEvent,
) {
match event {
TrashEvent::NewTrash(identifiers, ret) => {
let result = persistence
.begin_transaction(|transaction| {
let views = read_local_views_with_transaction(identifiers, &transaction)?;
for view in views {
let _ = notify_views_changed(&view.belong_to_id, trash_can.clone(), &transaction)?;
notify_dart(view, FolderNotification::ViewDeleted);
}
Ok(())
})
.await;
let _ = ret.send(result).await;
}
TrashEvent::Putback(identifiers, ret) => {
let result = persistence
.begin_transaction(|transaction| {
let views = read_local_views_with_transaction(identifiers, &transaction)?;
for view in views {
let _ = notify_views_changed(&view.belong_to_id, trash_can.clone(), &transaction)?;
notify_dart(view, FolderNotification::ViewRestored);
}
Ok(())
})
.await;
let _ = ret.send(result).await;
}
TrashEvent::Delete(identifiers, ret) => {
let result = persistence
.begin_transaction(|transaction| {
let mut notify_ids = HashSet::new();
for identifier in identifiers.items {
let view = transaction.read_view(&identifier.id)?;
let _ = transaction.delete_view(&identifier.id)?;
let _ = document_manager.delete(&identifier.id)?;
notify_ids.insert(view.belong_to_id);
}
for notify_id in notify_ids {
let _ = notify_views_changed(&notify_id, trash_can.clone(), &transaction)?;
}
Ok(())
})
.await;
let _ = ret.send(result).await;
}
}
}
fn read_local_views_with_transaction<'a>(
identifiers: RepeatedTrashId,
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
) -> Result<Vec<View>, FlowyError> {
let mut views = vec![];
for identifier in identifiers.items {
let view = transaction.read_view(&identifier.id)?;
views.push(view);
}
Ok(views)
}
fn notify_dart(view: View, notification: FolderNotification) {
send_dart_notification(&view.id, notification).payload(view).send();
}
#[tracing::instrument(skip(belong_to_id, trash_controller, transaction), fields(view_count), err)]
fn notify_views_changed<'a>(
belong_to_id: &str,
trash_controller: Arc<TrashController>,
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
) -> FlowyResult<()> {
let repeated_view = read_belonging_views_on_local(belong_to_id, trash_controller.clone(), transaction)?;
tracing::Span::current().record("view_count", &format!("{}", repeated_view.len()).as_str());
send_dart_notification(belong_to_id, FolderNotification::AppViewsChanged)
.payload(repeated_view)
.send();
Ok(())
}
fn read_belonging_views_on_local<'a>(
belong_to_id: &str,
trash_controller: Arc<TrashController>,
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
) -> FlowyResult<RepeatedView> {
let mut views = transaction.read_views(belong_to_id)?;
let trash_ids = trash_controller.read_trash_ids(transaction)?;
views.retain(|view_table| !trash_ids.contains(&view_table.id));
Ok(RepeatedView { items: views })
}

View File

@ -0,0 +1,115 @@
use crate::{
entities::{
trash::Trash,
view::{
CreateViewParams, CreateViewRequest, QueryViewRequest, RepeatedViewId, UpdateViewParams, UpdateViewRequest,
View, ViewId,
},
},
errors::FlowyError,
services::{TrashController, ViewController},
};
use flowy_collaboration::entities::document_info::DocumentDelta;
use flowy_folder_data_model::entities::share::{ExportData, ExportParams, ExportRequest};
use lib_dispatch::prelude::{data_result, Data, DataResult, Unit};
use std::{convert::TryInto, sync::Arc};
pub(crate) async fn create_view_handler(
data: Data<CreateViewRequest>,
controller: Unit<Arc<ViewController>>,
) -> DataResult<View, FlowyError> {
let params: CreateViewParams = data.into_inner().try_into()?;
let view = controller.create_view_from_params(params).await?;
data_result(view)
}
pub(crate) async fn read_view_handler(
data: Data<QueryViewRequest>,
controller: Unit<Arc<ViewController>>,
) -> DataResult<View, FlowyError> {
let params: ViewId = data.into_inner().try_into()?;
let mut view = controller.read_view(params.clone()).await?;
// For the moment, app and view can contains lots of views. Reading the view
// belongings using the view_id.
view.belongings = controller.read_views_belong_to(&params.view_id).await?;
data_result(view)
}
#[tracing::instrument(skip(data, controller), err)]
pub(crate) async fn update_view_handler(
data: Data<UpdateViewRequest>,
controller: Unit<Arc<ViewController>>,
) -> Result<(), FlowyError> {
let params: UpdateViewParams = data.into_inner().try_into()?;
let _ = controller.update_view(params).await?;
Ok(())
}
pub(crate) async fn document_delta_handler(
data: Data<DocumentDelta>,
controller: Unit<Arc<ViewController>>,
) -> DataResult<DocumentDelta, FlowyError> {
let doc = controller.receive_document_delta(data.into_inner()).await?;
data_result(doc)
}
pub(crate) async fn delete_view_handler(
data: Data<QueryViewRequest>,
view_controller: Unit<Arc<ViewController>>,
trash_controller: Unit<Arc<TrashController>>,
) -> Result<(), FlowyError> {
let params: RepeatedViewId = data.into_inner().try_into()?;
for view_id in &params.items {
let _ = view_controller.delete_view(view_id.into()).await;
}
let trash = view_controller
.read_local_views(params.items)
.await?
.into_iter()
.map(|view| view.into())
.collect::<Vec<Trash>>();
let _ = trash_controller.add(trash).await?;
Ok(())
}
pub(crate) async fn open_document_handler(
data: Data<QueryViewRequest>,
controller: Unit<Arc<ViewController>>,
) -> DataResult<DocumentDelta, FlowyError> {
let params: ViewId = data.into_inner().try_into()?;
let doc = controller.open_document(&params.view_id).await?;
data_result(doc)
}
pub(crate) async fn close_view_handler(
data: Data<QueryViewRequest>,
controller: Unit<Arc<ViewController>>,
) -> Result<(), FlowyError> {
let params: ViewId = data.into_inner().try_into()?;
let _ = controller.close_view(&params.view_id).await?;
Ok(())
}
#[tracing::instrument(skip(data, controller), err)]
pub(crate) async fn duplicate_view_handler(
data: Data<QueryViewRequest>,
controller: Unit<Arc<ViewController>>,
) -> Result<(), FlowyError> {
let params: ViewId = data.into_inner().try_into()?;
let _ = controller.duplicate_view(&params.view_id).await?;
Ok(())
}
#[tracing::instrument(skip(data, controller), err)]
pub(crate) async fn export_handler(
data: Data<ExportRequest>,
controller: Unit<Arc<ViewController>>,
) -> DataResult<ExportData, FlowyError> {
let params: ExportParams = data.into_inner().try_into()?;
let data = controller.export_doc(params).await?;
data_result(data)
}

View File

@ -0,0 +1,2 @@
pub mod controller;
pub mod event_handler;

View File

@ -0,0 +1,128 @@
use crate::services::FOLDER_SYNC_INTERVAL_IN_MILLIS;
use bytes::Bytes;
use flowy_collaboration::{
client_folder::FolderPad,
entities::{
revision::RevisionRange,
ws_data::{ClientRevisionWSData, NewDocumentUser, ServerRevisionWSDataType},
},
};
use flowy_error::FlowyError;
use flowy_sync::*;
use lib_infra::future::{BoxResultFuture, FutureResult};
use lib_ot::core::{Delta, OperationTransformable, PlainAttributes, PlainDelta};
use parking_lot::RwLock;
use std::{sync::Arc, time::Duration};
pub(crate) async fn make_folder_ws_manager(
user_id: &str,
folder_id: &str,
rev_manager: Arc<RevisionManager>,
web_socket: Arc<dyn RevisionWebSocket>,
folder_pad: Arc<RwLock<FolderPad>>,
) -> Arc<RevisionWebSocketManager> {
let composite_sink_provider = Arc::new(CompositeWSSinkDataProvider::new(folder_id, rev_manager.clone()));
let resolve_target = Arc::new(FolderRevisionResolveTarget { folder_pad });
let resolver = RevisionConflictResolver::<PlainAttributes>::new(
user_id,
resolve_target,
Arc::new(composite_sink_provider.clone()),
rev_manager,
);
let ws_stream_consumer = Arc::new(FolderWSStreamConsumerAdapter {
resolver: Arc::new(resolver),
});
let sink_provider = Arc::new(FolderWSSinkDataProviderAdapter(composite_sink_provider));
let ping_duration = Duration::from_millis(FOLDER_SYNC_INTERVAL_IN_MILLIS);
Arc::new(RevisionWebSocketManager::new(
"Folder",
folder_id,
web_socket,
sink_provider,
ws_stream_consumer,
ping_duration,
))
}
pub(crate) struct FolderWSSinkDataProviderAdapter(Arc<CompositeWSSinkDataProvider>);
impl RevisionWSSinkDataProvider for FolderWSSinkDataProviderAdapter {
fn next(&self) -> FutureResult<Option<ClientRevisionWSData>, FlowyError> {
let sink_provider = self.0.clone();
FutureResult::new(async move { sink_provider.next().await })
}
}
struct FolderRevisionResolveTarget {
folder_pad: Arc<RwLock<FolderPad>>,
}
impl ResolverTarget<PlainAttributes> for FolderRevisionResolveTarget {
fn compose_delta(&self, delta: Delta<PlainAttributes>) -> BoxResultFuture<DeltaMD5, FlowyError> {
let folder_pad = self.folder_pad.clone();
Box::pin(async move {
let md5 = folder_pad.write().compose_remote_delta(delta)?;
Ok(md5)
})
}
fn transform_delta(
&self,
delta: Delta<PlainAttributes>,
) -> BoxResultFuture<TransformDeltas<PlainAttributes>, FlowyError> {
let folder_pad = self.folder_pad.clone();
Box::pin(async move {
let read_guard = folder_pad.read();
let mut server_prime: Option<PlainDelta> = None;
let client_prime: PlainDelta;
if read_guard.is_empty() {
// Do nothing
client_prime = delta;
} else {
let (s_prime, c_prime) = read_guard.delta().transform(&delta)?;
client_prime = c_prime;
server_prime = Some(s_prime);
}
drop(read_guard);
Ok(TransformDeltas {
client_prime,
server_prime,
})
})
}
fn reset_delta(&self, delta: Delta<PlainAttributes>) -> BoxResultFuture<DeltaMD5, FlowyError> {
let folder_pad = self.folder_pad.clone();
Box::pin(async move {
let md5 = folder_pad.write().reset_folder(delta)?;
Ok(md5)
})
}
}
struct FolderWSStreamConsumerAdapter {
resolver: Arc<RevisionConflictResolver<PlainAttributes>>,
}
impl RevisionWSSteamConsumer for FolderWSStreamConsumerAdapter {
fn receive_push_revision(&self, bytes: Bytes) -> BoxResultFuture<(), FlowyError> {
let resolver = self.resolver.clone();
Box::pin(async move { resolver.receive_bytes(bytes).await })
}
fn receive_ack(&self, id: String, ty: ServerRevisionWSDataType) -> BoxResultFuture<(), FlowyError> {
let resolver = self.resolver.clone();
Box::pin(async move { resolver.ack_revision(id, ty).await })
}
fn receive_new_user_connect(&self, _new_user: NewDocumentUser) -> BoxResultFuture<(), FlowyError> {
// Do nothing by now, just a placeholder for future extension.
Box::pin(async move { Ok(()) })
}
fn pull_revisions_in_range(&self, range: RevisionRange) -> BoxResultFuture<(), FlowyError> {
let resolver = self.resolver.clone();
Box::pin(async move { resolver.send_revisions(range).await })
}
}

View File

@ -0,0 +1,207 @@
use crate::{
dart_notification::*,
errors::*,
module::{FolderCouldServiceV1, WorkspaceUser},
services::{
persistence::{FolderPersistence, FolderPersistenceTransaction, WorkspaceChangeset},
read_local_workspace_apps, TrashController,
},
};
use flowy_database::kv::KV;
use flowy_folder_data_model::entities::{app::RepeatedApp, workspace::*};
use std::sync::Arc;
pub struct WorkspaceController {
pub user: Arc<dyn WorkspaceUser>,
persistence: Arc<FolderPersistence>,
pub(crate) trash_controller: Arc<TrashController>,
cloud_service: Arc<dyn FolderCouldServiceV1>,
}
impl WorkspaceController {
pub(crate) fn new(
user: Arc<dyn WorkspaceUser>,
persistence: Arc<FolderPersistence>,
trash_can: Arc<TrashController>,
cloud_service: Arc<dyn FolderCouldServiceV1>,
) -> Self {
Self {
user,
persistence,
trash_controller: trash_can,
cloud_service,
}
}
pub(crate) async fn create_workspace_from_params(
&self,
params: CreateWorkspaceParams,
) -> Result<Workspace, FlowyError> {
let workspace = self.create_workspace_on_server(params.clone()).await?;
let user_id = self.user.user_id()?;
let token = self.user.token()?;
let workspaces = self
.persistence
.begin_transaction(|transaction| {
let _ = transaction.create_workspace(&user_id, workspace.clone())?;
transaction.read_workspaces(&user_id, None)
})
.await?;
let repeated_workspace = RepeatedWorkspace { items: workspaces };
send_dart_notification(&token, FolderNotification::UserCreateWorkspace)
.payload(repeated_workspace)
.send();
set_current_workspace(&workspace.id);
Ok(workspace)
}
#[allow(dead_code)]
pub(crate) async fn update_workspace(&self, params: UpdateWorkspaceParams) -> Result<(), FlowyError> {
let changeset = WorkspaceChangeset::new(params.clone());
let workspace_id = changeset.id.clone();
let workspace = self
.persistence
.begin_transaction(|transaction| {
let _ = transaction.update_workspace(changeset)?;
let user_id = self.user.user_id()?;
self.read_local_workspace(workspace_id.clone(), &user_id, &transaction)
})
.await?;
send_dart_notification(&workspace_id, FolderNotification::WorkspaceUpdated)
.payload(workspace)
.send();
let _ = self.update_workspace_on_server(params)?;
Ok(())
}
#[allow(dead_code)]
pub(crate) async fn delete_workspace(&self, workspace_id: &str) -> Result<(), FlowyError> {
let user_id = self.user.user_id()?;
let token = self.user.token()?;
let repeated_workspace = self
.persistence
.begin_transaction(|transaction| {
let _ = transaction.delete_workspace(workspace_id)?;
self.read_local_workspaces(None, &user_id, &transaction)
})
.await?;
send_dart_notification(&token, FolderNotification::UserDeleteWorkspace)
.payload(repeated_workspace)
.send();
let _ = self.delete_workspace_on_server(workspace_id)?;
Ok(())
}
pub(crate) async fn open_workspace(&self, params: WorkspaceId) -> Result<Workspace, FlowyError> {
let user_id = self.user.user_id()?;
if let Some(workspace_id) = params.workspace_id {
let workspace = self
.persistence
.begin_transaction(|transaction| self.read_local_workspace(workspace_id, &user_id, &transaction))
.await?;
set_current_workspace(&workspace.id);
Ok(workspace)
} else {
Err(FlowyError::workspace_id().context("Opened workspace id should not be empty"))
}
}
pub(crate) async fn read_current_workspace_apps(&self) -> Result<RepeatedApp, FlowyError> {
let workspace_id = get_current_workspace()?;
let repeated_app = self
.persistence
.begin_transaction(|transaction| {
read_local_workspace_apps(&workspace_id, self.trash_controller.clone(), &transaction)
})
.await?;
// TODO: read from server
Ok(repeated_app)
}
#[tracing::instrument(level = "debug", skip(self, transaction), err)]
pub(crate) fn read_local_workspaces<'a>(
&self,
workspace_id: Option<String>,
user_id: &str,
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
) -> Result<RepeatedWorkspace, FlowyError> {
let workspace_id = workspace_id.to_owned();
let workspaces = transaction.read_workspaces(user_id, workspace_id)?;
Ok(RepeatedWorkspace { items: workspaces })
}
pub(crate) fn read_local_workspace<'a>(
&self,
workspace_id: String,
user_id: &str,
transaction: &'a (dyn FolderPersistenceTransaction + 'a),
) -> Result<Workspace, FlowyError> {
let mut workspaces = transaction.read_workspaces(user_id, Some(workspace_id.clone()))?;
if workspaces.is_empty() {
return Err(FlowyError::record_not_found().context(format!("{} workspace not found", workspace_id)));
}
debug_assert_eq!(workspaces.len(), 1);
let workspace = workspaces.drain(..1).collect::<Vec<Workspace>>().pop().unwrap();
Ok(workspace)
}
}
impl WorkspaceController {
#[tracing::instrument(level = "trace", skip(self), err)]
async fn create_workspace_on_server(&self, params: CreateWorkspaceParams) -> Result<Workspace, FlowyError> {
let token = self.user.token()?;
let workspace = self.cloud_service.create_workspace(&token, params).await?;
Ok(workspace)
}
#[tracing::instrument(level = "trace", skip(self), err)]
fn update_workspace_on_server(&self, params: UpdateWorkspaceParams) -> Result<(), FlowyError> {
let (token, server) = (self.user.token()?, self.cloud_service.clone());
tokio::spawn(async move {
match server.update_workspace(&token, params).await {
Ok(_) => {}
Err(e) => {
// TODO: retry?
log::error!("Update workspace failed: {:?}", e);
}
}
});
Ok(())
}
#[tracing::instrument(level = "trace", skip(self), err)]
fn delete_workspace_on_server(&self, workspace_id: &str) -> Result<(), FlowyError> {
let params = WorkspaceId {
workspace_id: Some(workspace_id.to_string()),
};
let (token, server) = (self.user.token()?, self.cloud_service.clone());
tokio::spawn(async move {
match server.delete_workspace(&token, params).await {
Ok(_) => {}
Err(e) => {
// TODO: retry?
log::error!("Delete workspace failed: {:?}", e);
}
}
});
Ok(())
}
}
const CURRENT_WORKSPACE_ID: &str = "current_workspace_id";
pub fn set_current_workspace(workspace_id: &str) {
KV::set_str(CURRENT_WORKSPACE_ID, workspace_id.to_owned());
}
pub fn get_current_workspace() -> Result<String, FlowyError> {
match KV::get_str(CURRENT_WORKSPACE_ID) {
None => {
Err(FlowyError::record_not_found()
.context("Current workspace not found or should call open workspace first"))
}
Some(workspace_id) => Ok(workspace_id),
}
}

View File

@ -0,0 +1,143 @@
use crate::{
controller::FolderManager,
dart_notification::{send_dart_notification, FolderNotification},
errors::FlowyError,
services::{get_current_workspace, read_local_workspace_apps, WorkspaceController},
};
use flowy_folder_data_model::entities::{
app::RepeatedApp,
view::View,
workspace::{CurrentWorkspaceSetting, QueryWorkspaceRequest, RepeatedWorkspace, WorkspaceId, *},
};
use lib_dispatch::prelude::{data_result, Data, DataResult, Unit};
use std::{convert::TryInto, sync::Arc};
#[tracing::instrument(skip(data, controller), err)]
pub(crate) async fn create_workspace_handler(
data: Data<CreateWorkspaceRequest>,
controller: Unit<Arc<WorkspaceController>>,
) -> DataResult<Workspace, FlowyError> {
let controller = controller.get_ref().clone();
let params: CreateWorkspaceParams = data.into_inner().try_into()?;
let detail = controller.create_workspace_from_params(params).await?;
data_result(detail)
}
#[tracing::instrument(skip(controller), err)]
pub(crate) async fn read_workspace_apps_handler(
controller: Unit<Arc<WorkspaceController>>,
) -> DataResult<RepeatedApp, FlowyError> {
let repeated_app = controller.read_current_workspace_apps().await?;
data_result(repeated_app)
}
#[tracing::instrument(skip(data, controller), err)]
pub(crate) async fn open_workspace_handler(
data: Data<QueryWorkspaceRequest>,
controller: Unit<Arc<WorkspaceController>>,
) -> DataResult<Workspace, FlowyError> {
let params: WorkspaceId = data.into_inner().try_into()?;
let workspaces = controller.open_workspace(params).await?;
data_result(workspaces)
}
#[tracing::instrument(skip(data, folder), err)]
pub(crate) async fn read_workspaces_handler(
data: Data<QueryWorkspaceRequest>,
folder: Unit<Arc<FolderManager>>,
) -> DataResult<RepeatedWorkspace, FlowyError> {
let params: WorkspaceId = data.into_inner().try_into()?;
let user_id = folder.user.user_id()?;
let workspace_controller = folder.workspace_controller.clone();
let trash_controller = folder.trash_controller.clone();
let workspaces = folder
.persistence
.begin_transaction(|transaction| {
let mut workspaces =
workspace_controller.read_local_workspaces(params.workspace_id.clone(), &user_id, &transaction)?;
for workspace in workspaces.iter_mut() {
let apps =
read_local_workspace_apps(&workspace.id, trash_controller.clone(), &transaction)?.into_inner();
workspace.apps.items = apps;
}
Ok(workspaces)
})
.await?;
let _ = read_workspaces_on_server(folder, user_id, params);
data_result(workspaces)
}
#[tracing::instrument(skip(folder), err)]
pub async fn read_cur_workspace_handler(
folder: Unit<Arc<FolderManager>>,
) -> DataResult<CurrentWorkspaceSetting, FlowyError> {
let workspace_id = get_current_workspace()?;
let user_id = folder.user.user_id()?;
let params = WorkspaceId {
workspace_id: Some(workspace_id.clone()),
};
let workspace = folder
.persistence
.begin_transaction(|transaction| {
folder
.workspace_controller
.read_local_workspace(workspace_id, &user_id, &transaction)
})
.await?;
let latest_view: Option<View> = folder.view_controller.latest_visit_view().await.unwrap_or(None);
let setting = CurrentWorkspaceSetting { workspace, latest_view };
let _ = read_workspaces_on_server(folder, user_id, params);
data_result(setting)
}
#[tracing::instrument(level = "trace", skip(folder_manager), err)]
fn read_workspaces_on_server(
folder_manager: Unit<Arc<FolderManager>>,
user_id: String,
params: WorkspaceId,
) -> Result<(), FlowyError> {
let (token, server) = (folder_manager.user.token()?, folder_manager.cloud_service.clone());
let persistence = folder_manager.persistence.clone();
tokio::spawn(async move {
let workspaces = server.read_workspace(&token, params).await?;
let _ = persistence
.begin_transaction(|transaction| {
tracing::debug!("Save {} workspace", workspaces.len());
for workspace in &workspaces.items {
let m_workspace = workspace.clone();
let apps = m_workspace.apps.clone().into_inner();
let _ = transaction.create_workspace(&user_id, m_workspace)?;
tracing::debug!("Save {} apps", apps.len());
for app in apps {
let views = app.belongings.clone().into_inner();
match transaction.create_app(app) {
Ok(_) => {}
Err(e) => log::error!("create app failed: {:?}", e),
}
tracing::debug!("Save {} views", views.len());
for view in views {
match transaction.create_view(view) {
Ok(_) => {}
Err(e) => log::error!("create view failed: {:?}", e),
}
}
}
}
Ok(())
})
.await?;
send_dart_notification(&token, FolderNotification::WorkspaceListUpdated)
.payload(workspaces)
.send();
Result::<(), FlowyError>::Ok(())
});
Ok(())
}

View File

@ -0,0 +1,2 @@
pub mod controller;
pub mod event_handler;

View File

@ -0,0 +1,75 @@
#![allow(clippy::type_complexity)]
use crate::module::{FolderCouldServiceV1, WorkspaceUser};
use lib_infra::retry::Action;
use pin_project::pin_project;
use std::{
future::Future,
marker::PhantomData,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
pub(crate) type Builder<Fut> = Box<dyn Fn(String, Arc<dyn FolderCouldServiceV1>) -> Fut + Send + Sync>;
#[allow(dead_code)]
pub(crate) struct RetryAction<Fut, T, E> {
token: String,
cloud_service: Arc<dyn FolderCouldServiceV1>,
user: Arc<dyn WorkspaceUser>,
builder: Builder<Fut>,
phantom: PhantomData<(T, E)>,
}
impl<Fut, T, E> RetryAction<Fut, T, E> {
#[allow(dead_code)]
pub(crate) fn new<F>(cloud_service: Arc<dyn FolderCouldServiceV1>, user: Arc<dyn WorkspaceUser>, builder: F) -> Self
where
Fut: Future<Output = Result<T, E>> + Send + Sync + 'static,
F: Fn(String, Arc<dyn FolderCouldServiceV1>) -> Fut + Send + Sync + 'static,
{
let token = user.token().unwrap_or_else(|_| "".to_owned());
Self {
token,
cloud_service,
user,
builder: Box::new(builder),
phantom: PhantomData,
}
}
}
impl<Fut, T, E> Action for RetryAction<Fut, T, E>
where
Fut: Future<Output = Result<T, E>> + Send + Sync + 'static,
T: Send + Sync + 'static,
E: Send + Sync + 'static,
{
type Future = Pin<Box<dyn Future<Output = Result<Self::Item, Self::Error>> + Send + Sync>>;
type Item = T;
type Error = E;
fn run(&mut self) -> Self::Future {
let fut = (self.builder)(self.token.clone(), self.cloud_service.clone());
Box::pin(RetryActionFut { fut: Box::pin(fut) })
}
}
#[pin_project]
struct RetryActionFut<T, E> {
#[pin]
fut: Pin<Box<dyn Future<Output = Result<T, E>> + Send + Sync>>,
}
impl<T, E> Future for RetryActionFut<T, E>
where
T: Send + Sync + 'static,
E: Send + Sync + 'static,
{
type Output = Result<T, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut this = self.project();
this.fut.as_mut().poll(cx)
}
}

View File

@ -0,0 +1,365 @@
use crate::script::{invalid_workspace_name_test_case, FolderScript::*, FolderTest};
use flowy_collaboration::{client_document::default::initial_delta_string, entities::revision::RevisionState};
use flowy_folder::entities::workspace::CreateWorkspaceRequest;
use flowy_test::{event_builder::*, FlowySDKTest};
#[tokio::test]
async fn workspace_read_all() {
let mut test = FolderTest::new().await;
test.run_scripts(vec![ReadAllWorkspaces]).await;
// The first workspace will be the default workspace
// The second workspace will be created by FolderTest
assert_eq!(test.all_workspace.len(), 2);
let new_name = "My new workspace".to_owned();
test.run_scripts(vec![
CreateWorkspace {
name: new_name.clone(),
desc: "Daily routines".to_owned(),
},
ReadAllWorkspaces,
])
.await;
assert_eq!(test.all_workspace.len(), 3);
assert_eq!(test.all_workspace[2].name, new_name);
}
#[tokio::test]
async fn workspace_create() {
let mut test = FolderTest::new().await;
let name = "My new workspace".to_owned();
let desc = "Daily routines".to_owned();
test.run_scripts(vec![CreateWorkspace {
name: name.clone(),
desc: desc.clone(),
}])
.await;
let workspace = test.workspace.clone();
assert_eq!(workspace.name, name);
assert_eq!(workspace.desc, desc);
test.run_scripts(vec![
ReadWorkspace(Some(workspace.id.clone())),
AssertWorkspace(workspace),
])
.await;
}
#[tokio::test]
async fn workspace_read() {
let mut test = FolderTest::new().await;
let workspace = test.workspace.clone();
let json = serde_json::to_string(&workspace).unwrap();
test.run_scripts(vec![
ReadWorkspace(Some(workspace.id.clone())),
AssertWorkspaceJson(json),
AssertWorkspace(workspace),
])
.await;
}
#[tokio::test]
async fn workspace_create_with_apps() {
let mut test = FolderTest::new().await;
test.run_scripts(vec![CreateApp {
name: "App".to_string(),
desc: "App description".to_string(),
}])
.await;
let app = test.app.clone();
let json = serde_json::to_string(&app).unwrap();
test.run_scripts(vec![ReadApp(app.id), AssertAppJson(json)]).await;
}
#[tokio::test]
async fn workspace_create_with_invalid_name() {
for (name, code) in invalid_workspace_name_test_case() {
let sdk = FlowySDKTest::default();
let request = CreateWorkspaceRequest {
name,
desc: "".to_owned(),
};
assert_eq!(
FolderEventBuilder::new(sdk)
.event(flowy_folder::event::FolderEvent::CreateWorkspace)
.request(request)
.async_send()
.await
.error()
.code,
code.value()
)
}
}
#[tokio::test]
#[should_panic]
async fn app_delete() {
let mut test = FolderTest::new().await;
let app = test.app.clone();
test.run_scripts(vec![DeleteApp, ReadApp(app.id)]).await;
}
#[tokio::test]
async fn app_delete_then_restore() {
let mut test = FolderTest::new().await;
let app = test.app.clone();
test.run_scripts(vec![
DeleteApp,
RestoreAppFromTrash,
ReadApp(app.id.clone()),
AssertApp(app),
])
.await;
}
#[tokio::test]
async fn app_read() {
let mut test = FolderTest::new().await;
let app = test.app.clone();
test.run_scripts(vec![ReadApp(app.id.clone()), AssertApp(app)]).await;
}
#[tokio::test]
async fn app_update() {
let mut test = FolderTest::new().await;
let app = test.app.clone();
let new_name = "😁 hell world".to_owned();
assert_ne!(app.name, new_name);
test.run_scripts(vec![
UpdateApp {
name: Some(new_name.clone()),
desc: None,
},
ReadApp(app.id),
])
.await;
assert_eq!(test.app.name, new_name);
}
#[tokio::test]
async fn app_create_with_view() {
let mut test = FolderTest::new().await;
let mut app = test.app.clone();
test.run_scripts(vec![
CreateView {
name: "View A".to_owned(),
desc: "View A description".to_owned(),
},
CreateView {
name: "View B".to_owned(),
desc: "View B description".to_owned(),
},
ReadApp(app.id),
])
.await;
app = test.app.clone();
assert_eq!(app.belongings.len(), 3);
assert_eq!(app.belongings[1].name, "View A");
assert_eq!(app.belongings[2].name, "View B")
}
#[tokio::test]
async fn view_update() {
let mut test = FolderTest::new().await;
let view = test.view.clone();
let new_name = "😁 123".to_owned();
assert_ne!(view.name, new_name);
test.run_scripts(vec![
UpdateView {
name: Some(new_name.clone()),
desc: None,
},
ReadView(view.id),
])
.await;
assert_eq!(test.view.name, new_name);
}
#[tokio::test]
async fn open_document_view() {
let mut test = FolderTest::new().await;
assert_eq!(test.document_info, None);
test.run_scripts(vec![OpenDocument]).await;
let document_info = test.document_info.unwrap();
assert_eq!(document_info.text, initial_delta_string());
}
#[tokio::test]
#[should_panic]
async fn view_delete() {
let mut test = FolderTest::new().await;
let view = test.view.clone();
test.run_scripts(vec![DeleteView, ReadView(view.id)]).await;
}
#[tokio::test]
async fn view_delete_then_restore() {
let mut test = FolderTest::new().await;
let view = test.view.clone();
test.run_scripts(vec![
DeleteView,
RestoreViewFromTrash,
ReadView(view.id.clone()),
AssertView(view),
])
.await;
}
#[tokio::test]
async fn view_delete_all() {
let mut test = FolderTest::new().await;
let app = test.app.clone();
test.run_scripts(vec![
CreateView {
name: "View A".to_owned(),
desc: "View A description".to_owned(),
},
CreateView {
name: "View B".to_owned(),
desc: "View B description".to_owned(),
},
ReadApp(app.id.clone()),
])
.await;
assert_eq!(test.app.belongings.len(), 3);
let view_ids = test
.app
.belongings
.iter()
.map(|view| view.id.clone())
.collect::<Vec<String>>();
test.run_scripts(vec![DeleteViews(view_ids), ReadApp(app.id), ReadTrash])
.await;
assert_eq!(test.app.belongings.len(), 0);
assert_eq!(test.trash.len(), 3);
}
#[tokio::test]
async fn view_delete_all_permanent() {
let mut test = FolderTest::new().await;
let app = test.app.clone();
test.run_scripts(vec![
CreateView {
name: "View A".to_owned(),
desc: "View A description".to_owned(),
},
ReadApp(app.id.clone()),
])
.await;
let view_ids = test
.app
.belongings
.iter()
.map(|view| view.id.clone())
.collect::<Vec<String>>();
test.run_scripts(vec![DeleteViews(view_ids), ReadApp(app.id), DeleteAllTrash, ReadTrash])
.await;
assert_eq!(test.app.belongings.len(), 0);
assert_eq!(test.trash.len(), 0);
}
#[tokio::test]
async fn folder_sync_revision_state() {
let mut test = FolderTest::new().await;
test.run_scripts(vec![
AssertRevisionState {
rev_id: 1,
state: RevisionState::Sync,
},
AssertNextSyncRevId(Some(1)),
AssertRevisionState {
rev_id: 1,
state: RevisionState::Ack,
},
])
.await;
}
#[tokio::test]
async fn folder_sync_revision_seq() {
let mut test = FolderTest::new().await;
test.run_scripts(vec![
AssertRevisionState {
rev_id: 1,
state: RevisionState::Sync,
},
AssertRevisionState {
rev_id: 2,
state: RevisionState::Sync,
},
AssertNextSyncRevId(Some(1)),
AssertNextSyncRevId(Some(2)),
AssertRevisionState {
rev_id: 1,
state: RevisionState::Ack,
},
AssertRevisionState {
rev_id: 2,
state: RevisionState::Ack,
},
])
.await;
}
#[tokio::test]
async fn folder_sync_revision_with_new_app() {
let mut test = FolderTest::new().await;
let app_name = "AppFlowy contributors".to_owned();
let app_desc = "Welcome to be a AppFlowy contributor".to_owned();
test.run_scripts(vec![
AssertNextSyncRevId(Some(1)),
AssertNextSyncRevId(Some(2)),
CreateApp {
name: app_name.clone(),
desc: app_desc.clone(),
},
AssertCurrentRevId(3),
AssertNextSyncRevId(Some(3)),
AssertNextSyncRevId(None),
])
.await;
let app = test.app.clone();
assert_eq!(app.name, app_name);
assert_eq!(app.desc, app_desc);
test.run_scripts(vec![ReadApp(app.id.clone()), AssertApp(app)]).await;
}
#[tokio::test]
async fn folder_sync_revision_with_new_view() {
let mut test = FolderTest::new().await;
let view_name = "AppFlowy features".to_owned();
let view_desc = "😁".to_owned();
test.run_scripts(vec![
AssertNextSyncRevId(Some(1)),
AssertNextSyncRevId(Some(2)),
CreateView {
name: view_name.clone(),
desc: view_desc.clone(),
},
AssertCurrentRevId(3),
AssertNextSyncRevId(Some(3)),
AssertNextSyncRevId(None),
])
.await;
let view = test.view.clone();
assert_eq!(view.name, view_name);
assert_eq!(view.desc, view_desc);
test.run_scripts(vec![ReadView(view.id.clone()), AssertView(view)])
.await;
}

View File

@ -0,0 +1,209 @@
use flowy_collaboration::entities::document_info::DocumentInfo;
use flowy_folder::event::FolderEvent::*;
use flowy_folder_data_model::entities::{
app::{App, AppId, CreateAppRequest, QueryAppRequest, UpdateAppRequest},
trash::{RepeatedTrash, TrashId, TrashType},
view::{CreateViewRequest, QueryViewRequest, UpdateViewRequest, View, ViewType},
workspace::{CreateWorkspaceRequest, QueryWorkspaceRequest, RepeatedWorkspace, Workspace},
};
use flowy_test::{event_builder::*, FlowySDKTest};
pub async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> Workspace {
let request = CreateWorkspaceRequest {
name: name.to_owned(),
desc: desc.to_owned(),
};
let workspace = FolderEventBuilder::new(sdk.clone())
.event(CreateWorkspace)
.request(request)
.async_send()
.await
.parse::<Workspace>();
workspace
}
pub async fn read_workspace(sdk: &FlowySDKTest, workspace_id: Option<String>) -> Vec<Workspace> {
let request = QueryWorkspaceRequest { workspace_id };
let repeated_workspace = FolderEventBuilder::new(sdk.clone())
.event(ReadWorkspaces)
.request(request.clone())
.async_send()
.await
.parse::<RepeatedWorkspace>();
let workspaces;
if let Some(workspace_id) = &request.workspace_id {
workspaces = repeated_workspace
.into_inner()
.into_iter()
.filter(|workspace| &workspace.id == workspace_id)
.collect::<Vec<Workspace>>();
debug_assert_eq!(workspaces.len(), 1);
} else {
workspaces = repeated_workspace.items;
}
workspaces
}
pub async fn create_app(sdk: &FlowySDKTest, workspace_id: &str, name: &str, desc: &str) -> App {
let create_app_request = CreateAppRequest {
workspace_id: workspace_id.to_owned(),
name: name.to_string(),
desc: desc.to_string(),
color_style: Default::default(),
};
let app = FolderEventBuilder::new(sdk.clone())
.event(CreateApp)
.request(create_app_request)
.async_send()
.await
.parse::<App>();
app
}
pub async fn read_app(sdk: &FlowySDKTest, app_id: &str) -> App {
let request = QueryAppRequest {
app_ids: vec![app_id.to_owned()],
};
let app = FolderEventBuilder::new(sdk.clone())
.event(ReadApp)
.request(request)
.async_send()
.await
.parse::<App>();
app
}
pub async fn update_app(sdk: &FlowySDKTest, app_id: &str, name: Option<String>, desc: Option<String>) {
let request = UpdateAppRequest {
app_id: app_id.to_string(),
name,
desc,
color_style: None,
is_trash: None,
};
FolderEventBuilder::new(sdk.clone())
.event(UpdateApp)
.request(request)
.async_send()
.await;
}
pub async fn delete_app(sdk: &FlowySDKTest, app_id: &str) {
let request = AppId {
app_id: app_id.to_string(),
};
FolderEventBuilder::new(sdk.clone())
.event(DeleteApp)
.request(request)
.async_send()
.await;
}
pub async fn create_view(sdk: &FlowySDKTest, app_id: &str, name: &str, desc: &str, view_type: ViewType) -> View {
let request = CreateViewRequest {
belong_to_id: app_id.to_string(),
name: name.to_string(),
desc: desc.to_string(),
thumbnail: None,
view_type,
};
let view = FolderEventBuilder::new(sdk.clone())
.event(CreateView)
.request(request)
.async_send()
.await
.parse::<View>();
view
}
pub async fn read_view(sdk: &FlowySDKTest, view_ids: Vec<String>) -> View {
let request = QueryViewRequest { view_ids };
FolderEventBuilder::new(sdk.clone())
.event(ReadView)
.request(request)
.async_send()
.await
.parse::<View>()
}
pub async fn update_view(sdk: &FlowySDKTest, view_id: &str, name: Option<String>, desc: Option<String>) {
let request = UpdateViewRequest {
view_id: view_id.to_string(),
name,
desc,
thumbnail: None,
};
FolderEventBuilder::new(sdk.clone())
.event(UpdateView)
.request(request)
.async_send()
.await;
}
pub async fn delete_view(sdk: &FlowySDKTest, view_ids: Vec<String>) {
let request = QueryViewRequest { view_ids };
FolderEventBuilder::new(sdk.clone())
.event(DeleteView)
.request(request)
.async_send()
.await;
}
pub async fn open_document(sdk: &FlowySDKTest, view_id: &str) -> DocumentInfo {
let request = QueryViewRequest {
view_ids: vec![view_id.to_owned()],
};
FolderEventBuilder::new(sdk.clone())
.event(OpenDocument)
.request(request)
.async_send()
.await
.parse::<DocumentInfo>()
}
pub async fn read_trash(sdk: &FlowySDKTest) -> RepeatedTrash {
FolderEventBuilder::new(sdk.clone())
.event(ReadTrash)
.async_send()
.await
.parse::<RepeatedTrash>()
}
pub async fn restore_app_from_trash(sdk: &FlowySDKTest, app_id: &str) {
let id = TrashId {
id: app_id.to_owned(),
ty: TrashType::App,
};
FolderEventBuilder::new(sdk.clone())
.event(PutbackTrash)
.request(id)
.async_send()
.await;
}
pub async fn restore_view_from_trash(sdk: &FlowySDKTest, view_id: &str) {
let id = TrashId {
id: view_id.to_owned(),
ty: TrashType::View,
};
FolderEventBuilder::new(sdk.clone())
.event(PutbackTrash)
.request(id)
.async_send()
.await;
}
pub async fn delete_all_trash(sdk: &FlowySDKTest) {
FolderEventBuilder::new(sdk.clone())
.event(DeleteAllTrash)
.async_send()
.await;
}

View File

@ -0,0 +1,3 @@
mod folder_test;
mod helper;
mod script;

View File

@ -0,0 +1,219 @@
use crate::helper::*;
use flowy_collaboration::entities::{document_info::DocumentInfo, revision::RevisionState};
use flowy_folder::{errors::ErrorCode, services::folder_editor::FolderEditor};
use flowy_folder_data_model::entities::{
app::{App, RepeatedApp},
trash::Trash,
view::{RepeatedView, View, ViewType},
workspace::Workspace,
};
use flowy_sync::REVISION_WRITE_INTERVAL_IN_MILLIS;
use flowy_test::FlowySDKTest;
use std::{sync::Arc, time::Duration};
use tokio::time::sleep;
pub enum FolderScript {
// Workspace
ReadAllWorkspaces,
CreateWorkspace { name: String, desc: String },
AssertWorkspaceJson(String),
AssertWorkspace(Workspace),
ReadWorkspace(Option<String>),
// App
CreateApp { name: String, desc: String },
AssertAppJson(String),
AssertApp(App),
ReadApp(String),
UpdateApp { name: Option<String>, desc: Option<String> },
DeleteApp,
// View
CreateView { name: String, desc: String },
AssertView(View),
ReadView(String),
UpdateView { name: Option<String>, desc: Option<String> },
DeleteView,
DeleteViews(Vec<String>),
// Trash
RestoreAppFromTrash,
RestoreViewFromTrash,
ReadTrash,
DeleteAllTrash,
// Document
OpenDocument,
// Sync
AssertCurrentRevId(i64),
AssertNextSyncRevId(Option<i64>),
AssertRevisionState { rev_id: i64, state: RevisionState },
}
pub struct FolderTest {
pub sdk: FlowySDKTest,
pub all_workspace: Vec<Workspace>,
pub workspace: Workspace,
pub app: App,
pub view: View,
pub trash: Vec<Trash>,
pub document_info: Option<DocumentInfo>,
// pub folder_editor:
}
impl FolderTest {
pub async fn new() -> Self {
let sdk = FlowySDKTest::default();
let _ = sdk.init_user().await;
let mut workspace = create_workspace(&sdk, "FolderWorkspace", "Folder test workspace").await;
let mut app = create_app(&sdk, &workspace.id, "Folder App", "Folder test app").await;
let view = create_view(&sdk, &app.id, "Folder View", "Folder test view", ViewType::Doc).await;
app.belongings = RepeatedView {
items: vec![view.clone()],
};
workspace.apps = RepeatedApp {
items: vec![app.clone()],
};
Self {
sdk,
all_workspace: vec![],
workspace,
app,
view,
trash: vec![],
document_info: None,
}
}
pub async fn run_scripts(&mut self, scripts: Vec<FolderScript>) {
for script in scripts {
self.run_script(script).await;
}
}
pub async fn run_script(&mut self, script: FolderScript) {
let sdk = &self.sdk;
let folder_editor: Arc<FolderEditor> = sdk.folder_manager.folder_editor().await;
let rev_manager = folder_editor.rev_manager();
let cache = rev_manager.revision_cache().await;
match script {
FolderScript::ReadAllWorkspaces => {
let all_workspace = read_workspace(sdk, None).await;
self.all_workspace = all_workspace;
}
FolderScript::CreateWorkspace { name, desc } => {
let workspace = create_workspace(sdk, &name, &desc).await;
self.workspace = workspace;
}
FolderScript::AssertWorkspaceJson(expected_json) => {
let workspace = read_workspace(sdk, Some(self.workspace.id.clone()))
.await
.pop()
.unwrap();
let json = serde_json::to_string(&workspace).unwrap();
assert_eq!(json, expected_json);
}
FolderScript::AssertWorkspace(workspace) => {
assert_eq!(self.workspace, workspace);
}
FolderScript::ReadWorkspace(workspace_id) => {
let workspace = read_workspace(sdk, workspace_id).await.pop().unwrap();
self.workspace = workspace;
}
FolderScript::CreateApp { name, desc } => {
let app = create_app(sdk, &self.workspace.id, &name, &desc).await;
self.app = app;
}
FolderScript::AssertAppJson(expected_json) => {
let json = serde_json::to_string(&self.app).unwrap();
assert_eq!(json, expected_json);
}
FolderScript::AssertApp(app) => {
assert_eq!(self.app, app);
}
FolderScript::ReadApp(app_id) => {
let app = read_app(sdk, &app_id).await;
self.app = app;
}
FolderScript::UpdateApp { name, desc } => {
update_app(sdk, &self.app.id, name, desc).await;
}
FolderScript::DeleteApp => {
delete_app(sdk, &self.app.id).await;
}
FolderScript::CreateView { name, desc } => {
let view = create_view(sdk, &self.app.id, &name, &desc, ViewType::Doc).await;
self.view = view;
}
FolderScript::AssertView(view) => {
assert_eq!(self.view, view);
}
FolderScript::ReadView(view_id) => {
let view = read_view(sdk, vec![view_id]).await;
self.view = view;
}
FolderScript::UpdateView { name, desc } => {
update_view(sdk, &self.view.id, name, desc).await;
}
FolderScript::DeleteView => {
delete_view(sdk, vec![self.view.id.clone()]).await;
}
FolderScript::DeleteViews(view_ids) => {
delete_view(sdk, view_ids).await;
}
FolderScript::RestoreAppFromTrash => {
restore_app_from_trash(sdk, &self.app.id).await;
}
FolderScript::RestoreViewFromTrash => {
restore_view_from_trash(sdk, &self.view.id).await;
}
FolderScript::ReadTrash => {
let trash = read_trash(sdk).await;
self.trash = trash.into_inner();
}
FolderScript::DeleteAllTrash => {
delete_all_trash(sdk).await;
self.trash = vec![];
}
FolderScript::OpenDocument => {
let document_info = open_document(sdk, &self.view.id).await;
self.document_info = Some(document_info);
}
FolderScript::AssertRevisionState { rev_id, state } => {
let record = cache.get(rev_id).await.unwrap();
assert_eq!(record.state, state);
if let RevisionState::Ack = state {
// There is a defer action that writes the revisions to disk, so we wait here.
// Make sure everything is written.
sleep(Duration::from_millis(2 * REVISION_WRITE_INTERVAL_IN_MILLIS)).await;
}
}
FolderScript::AssertCurrentRevId(rev_id) => {
assert_eq!(rev_manager.rev_id(), rev_id, "Current rev_id is not match");
}
FolderScript::AssertNextSyncRevId(rev_id) => {
let next_revision = rev_manager.next_sync_revision().await.unwrap();
if rev_id.is_none() {
assert!(next_revision.is_none(), "Next revision should be None");
return;
}
let next_revision = next_revision
.unwrap_or_else(|| panic!("Expected Next revision is {}, but receive None", rev_id.unwrap()));
let mut receiver = rev_manager.revision_ack_receiver();
let _ = receiver.recv().await;
assert_eq!(next_revision.rev_id, rev_id.unwrap());
}
}
}
}
pub fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> {
vec![
("".to_owned(), ErrorCode::WorkspaceNameInvalid),
("1234".repeat(100), ErrorCode::WorkspaceNameTooLong),
]
}