Selaa lähdekoodia

add folder migration & add folder unit test

appflowy 3 vuotta sitten
vanhempi
commit
324dc53e5f
22 muutettua tiedostoa jossa 479 lisäystä ja 341 poistoa
  1. 1 1
      frontend/rust-lib/flowy-core/Cargo.toml
  2. 43 55
      frontend/rust-lib/flowy-core/src/controller.rs
  3. 81 0
      frontend/rust-lib/flowy-core/src/services/persistence/migration.rs
  4. 37 3
      frontend/rust-lib/flowy-core/src/services/persistence/mod.rs
  5. 0 2
      frontend/rust-lib/flowy-core/src/services/persistence/version_1/app_sql.rs
  6. 2 2
      frontend/rust-lib/flowy-core/src/services/persistence/version_1/v1_impl.rs
  7. 1 1
      frontend/rust-lib/flowy-core/src/services/persistence/version_1/workspace_sql.rs
  8. 7 3
      frontend/rust-lib/flowy-core/src/services/persistence/version_2/v2_impl.rs
  9. 21 0
      frontend/rust-lib/flowy-core/src/services/view/controller.rs
  10. 1 5
      frontend/rust-lib/flowy-core/src/services/workspace/controller.rs
  11. 0 78
      frontend/rust-lib/flowy-core/tests/workspace/app_test.rs
  12. 244 0
      frontend/rust-lib/flowy-core/tests/workspace/folder_test.rs
  13. 1 4
      frontend/rust-lib/flowy-core/tests/workspace/main.rs
  14. 0 96
      frontend/rust-lib/flowy-core/tests/workspace/view_test.rs
  15. 0 84
      frontend/rust-lib/flowy-core/tests/workspace/workspace_test.rs
  16. 4 2
      frontend/rust-lib/flowy-sdk/src/lib.rs
  17. 7 0
      frontend/rust-lib/flowy-sync/src/cache/mod.rs
  18. 14 1
      frontend/rust-lib/flowy-test/src/lib.rs
  19. 2 2
      frontend/rust-lib/flowy-user/src/services/database.rs
  20. 1 1
      frontend/rust-lib/flowy-user/src/services/mod.rs
  21. 11 0
      shared-lib/flowy-collaboration/src/folder/folder_pad.rs
  22. 1 1
      shared-lib/flowy-collaboration/src/server_document/document_pad.rs

+ 1 - 1
frontend/rust-lib/flowy-core/Cargo.toml

@@ -44,8 +44,8 @@ crossbeam-utils = "0.8"
 chrono = "0.4"
 
 [dev-dependencies]
-flowy-test = { path = "../flowy-test" }
 serial_test = "0.5.1"
+flowy-test = { path = "../flowy-test" }
 
 [features]
 default = []

+ 43 - 55
frontend/rust-lib/flowy-core/src/controller.rs

@@ -1,13 +1,12 @@
 use bytes::Bytes;
 use chrono::Utc;
 use flowy_collaboration::client_document::default::{initial_delta, initial_read_me};
-use flowy_core_data_model::{entities::view::CreateViewParams, user_default};
+use flowy_core_data_model::user_default;
 use flowy_document::context::DocumentContext;
 use flowy_sync::RevisionWebSocket;
 use lazy_static::lazy_static;
 
-use futures_core::future::BoxFuture;
-
+use flowy_collaboration::folder::FolderPad;
 use parking_lot::RwLock;
 use std::{collections::HashMap, sync::Arc};
 
@@ -16,11 +15,18 @@ use crate::{
     entities::workspace::RepeatedWorkspace,
     errors::FlowyResult,
     module::{FolderCouldServiceV1, WorkspaceUser},
-    services::{persistence::FolderPersistence, AppController, TrashController, ViewController, WorkspaceController},
+    services::{
+        persistence::FolderPersistence,
+        set_current_workspace,
+        AppController,
+        TrashController,
+        ViewController,
+        WorkspaceController,
+    },
 };
 
 lazy_static! {
-    static ref INIT_WORKSPACE: RwLock<HashMap<String, bool>> = RwLock::new(HashMap::new());
+    static ref INIT_FOLDER_FLAG: RwLock<HashMap<String, bool>> = RwLock::new(HashMap::new());
 }
 
 pub struct FolderManager {
@@ -43,7 +49,7 @@ impl FolderManager {
         ws_sender: Arc<dyn RevisionWebSocket>,
     ) -> Self {
         if let Ok(token) = user.token() {
-            INIT_WORKSPACE.write().insert(token, false);
+            INIT_FOLDER_FLAG.write().insert(token, false);
         }
 
         let trash_controller = Arc::new(TrashController::new(
@@ -97,74 +103,56 @@ impl FolderManager {
 
     pub async fn did_receive_ws_data(&self, _data: Bytes) {}
 
-    pub async fn initialize(&self, token: &str) -> FlowyResult<()> {
-        self.initialize_with_fn(token, || Box::pin(async { Ok(()) })).await?;
-        Ok(())
-    }
-
-    pub async fn clear(&self) { self.persistence.user_did_logout() }
-
-    pub async fn initialize_with_new_user(&self, token: &str) -> FlowyResult<()> {
-        self.initialize_with_fn(token, || Box::pin(self.initial_default_workspace()))
-            .await
-    }
-
-    async fn initialize_with_fn<'a, F>(&'a self, token: &str, f: F) -> FlowyResult<()>
-    where
-        F: FnOnce() -> BoxFuture<'a, FlowyResult<()>>,
-    {
-        if let Some(is_init) = INIT_WORKSPACE.read().get(token) {
+    pub async fn initialize(&self, user_id: &str) -> FlowyResult<()> {
+        if let Some(is_init) = INIT_FOLDER_FLAG.read().get(user_id) {
             if *is_init {
                 return Ok(());
             }
         }
-        INIT_WORKSPACE.write().insert(token.to_owned(), true);
-
-        self.persistence.initialize().await?;
-        f().await?;
+        let _ = self.persistence.initialize(user_id).await?;
         let _ = self.app_controller.initialize()?;
         let _ = self.view_controller.initialize()?;
+        INIT_FOLDER_FLAG.write().insert(user_id.to_owned(), true);
         Ok(())
     }
 
-    async fn initial_default_workspace(&self) -> FlowyResult<()> {
+    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).await
+    }
+
+    pub async fn clear(&self) { self.persistence.user_did_logout() }
+}
+
+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);
-        let apps = workspace.apps.clone().into_inner();
-        let cloned_workspace = workspace.clone();
-
-        let _ = self.workspace_controller.create_workspace_on_local(workspace).await?;
-        for app in apps {
-            let app_id = app.id.clone();
-            let views = app.belongings.clone().into_inner();
-            let _ = self.app_controller.create_app_on_local(app).await?;
-            for (index, view) in views.into_iter().enumerate() {
+        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()
                 };
-                self.view_controller.set_latest_view(&view);
-                let params = CreateViewParams {
-                    belong_to_id: app_id.clone(),
-                    name: view.name,
-                    desc: view.desc,
-                    thumbnail: "".to_string(),
-                    view_type: view.view_type,
-                    view_data,
-                    view_id: view.id.clone(),
-                };
-                let _ = self.view_controller.create_view_from_params(params).await?;
+                view_controller.set_latest_view(&view);
+                let _ = view_controller
+                    .create_view_document_content(&view.id, view_data)
+                    .await?;
             }
         }
-
-        let token = self.user.token()?;
-        let repeated_workspace = RepeatedWorkspace {
-            items: vec![cloned_workspace],
-        };
-
-        send_dart_notification(&token, WorkspaceNotification::UserCreateWorkspace)
+        let folder = FolderPad::new(vec![workspace.clone()], vec![])?;
+        let _ = persistence.save_folder(user_id, folder).await?;
+        let repeated_workspace = RepeatedWorkspace { items: vec![workspace] };
+        send_dart_notification(token, WorkspaceNotification::UserCreateWorkspace)
             .payload(repeated_workspace)
             .send();
         Ok(())

+ 81 - 0
frontend/rust-lib/flowy-core/src/services/persistence/migration.rs

@@ -0,0 +1,81 @@
+use crate::{
+    module::WorkspaceDatabase,
+    services::persistence::{AppTableSql, TrashTableSql, ViewTableSql, WorkspaceTableSql, FOLDER_ID},
+};
+use flowy_collaboration::{
+    entities::revision::{md5, Revision},
+    folder::FolderPad,
+};
+use flowy_core_data_model::entities::{
+    app::{App, RepeatedApp},
+    view::{RepeatedView, View},
+    workspace::Workspace,
+};
+use flowy_database::kv::KV;
+use flowy_error::{FlowyError, FlowyResult};
+use flowy_sync::{RevisionCache, RevisionManager};
+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() {
+            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))
+    }
+}

+ 37 - 3
frontend/rust-lib/flowy-core/src/services/persistence/mod.rs

@@ -1,13 +1,18 @@
+mod migration;
 pub mod version_1;
 mod version_2;
 
+use flowy_collaboration::{
+    entities::revision::{Revision, RevisionState},
+    folder::FolderPad,
+};
 use parking_lot::RwLock;
 use std::sync::Arc;
 pub use version_1::{app_sql::*, trash_sql::*, v1_impl::V1Transaction, view_sql::*, workspace_sql::*};
 
 use crate::{
     module::{WorkspaceDatabase, WorkspaceUser},
-    services::persistence::version_2::v2_impl::FolderEditor,
+    services::persistence::{migration::FolderMigration, version_2::v2_impl::FolderEditor},
 };
 use flowy_core_data_model::entities::{
     app::App,
@@ -17,6 +22,9 @@ use flowy_core_data_model::entities::{
     workspace::Workspace,
 };
 use flowy_error::{FlowyError, FlowyResult};
+use flowy_sync::{mk_revision_disk_cache, RevisionCache, RevisionManager, RevisionRecord};
+
+pub const FOLDER_ID: &str = "flowy_folder";
 
 pub trait FolderPersistenceTransaction {
     fn create_workspace(&self, user_id: &str, workspace: Workspace) -> FlowyResult<()>;
@@ -57,8 +65,12 @@ impl FolderPersistence {
         }
     }
 
+    #[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_transaction2<F, O>(&self, f: F) -> FlowyResult<O>
+    pub fn begin_transaction_v_1<F, O>(&self, f: F) -> FlowyResult<O>
     where
         F: for<'a> FnOnce(Box<dyn FolderPersistenceTransaction + 'a>) -> FlowyResult<O>,
     {
@@ -93,7 +105,13 @@ impl FolderPersistence {
 
     pub fn user_did_logout(&self) { *self.folder_editor.write() = None; }
 
-    pub async fn initialize(&self) -> FlowyResult<()> {
+    pub async fn initialize(&self, user_id: &str) -> 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, migrated_folder).await?;
+        }
+
         let _ = self.init_folder_editor().await?;
         Ok(())
     }
@@ -107,4 +125,20 @@ impl FolderPersistence {
         *self.folder_editor.write() = Some(editor.clone());
         Ok(editor)
     }
+
+    pub async fn save_folder(&self, user_id: &str, 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, 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.write_revision_records(vec![record], &conn)
+    }
 }

+ 0 - 2
frontend/rust-lib/flowy-core/src/services/persistence/version_1/app_sql.rs

@@ -41,12 +41,10 @@ impl AppTableSql {
 
     pub(crate) fn read_workspace_apps(
         workspace_id: &str,
-        is_trash: bool,
         conn: &SqliteConnection,
     ) -> Result<Vec<AppTable>, FlowyError> {
         let app_table = dsl::app_table
             .filter(app_table::workspace_id.eq(workspace_id))
-            .filter(app_table::is_trash.eq(is_trash))
             .order(app_table::create_time.asc())
             .load::<AppTable>(conn)?;
 

+ 2 - 2
frontend/rust-lib/flowy-core/src/services/persistence/version_1/v1_impl.rs

@@ -23,7 +23,7 @@ impl<'a> FolderPersistenceTransaction for V1Transaction<'a> {
     }
 
     fn read_workspaces(&self, user_id: &str, workspace_id: Option<String>) -> FlowyResult<Vec<Workspace>> {
-        let tables = WorkspaceTableSql::read_workspaces(workspace_id, user_id, &*self.0)?;
+        let tables = WorkspaceTableSql::read_workspaces(user_id, workspace_id, &*self.0)?;
         let workspaces = tables.into_iter().map(Workspace::from).collect::<Vec<_>>();
         Ok(workspaces)
     }
@@ -52,7 +52,7 @@ impl<'a> FolderPersistenceTransaction for V1Transaction<'a> {
     }
 
     fn read_workspace_apps(&self, workspace_id: &str) -> FlowyResult<Vec<App>> {
-        let tables = AppTableSql::read_workspace_apps(workspace_id, false, &*self.0)?;
+        let tables = AppTableSql::read_workspace_apps(workspace_id, &*self.0)?;
         let apps = tables.into_iter().map(App::from).collect::<Vec<_>>();
         Ok(apps)
     }

+ 1 - 1
frontend/rust-lib/flowy-core/src/services/persistence/version_1/workspace_sql.rs

@@ -29,8 +29,8 @@ impl WorkspaceTableSql {
     }
 
     pub(crate) fn read_workspaces(
-        workspace_id: Option<String>,
         user_id: &str,
+        workspace_id: Option<String>,
         conn: &SqliteConnection,
     ) -> Result<Vec<WorkspaceTable>, FlowyError> {
         let mut filter = dsl::workspace_table

+ 7 - 3
frontend/rust-lib/flowy-core/src/services/persistence/version_2/v2_impl.rs

@@ -1,4 +1,10 @@
-use crate::services::persistence::{AppChangeset, FolderPersistenceTransaction, ViewChangeset, WorkspaceChangeset};
+use crate::services::persistence::{
+    AppChangeset,
+    FolderPersistenceTransaction,
+    ViewChangeset,
+    WorkspaceChangeset,
+    FOLDER_ID,
+};
 use flowy_collaboration::{
     entities::revision::Revision,
     folder::{FolderChange, FolderPad},
@@ -14,8 +20,6 @@ use lib_sqlite::ConnectionPool;
 use parking_lot::RwLock;
 use std::sync::Arc;
 
-const FOLDER_ID: &str = "flowy_folder";
-
 pub struct FolderEditor {
     user_id: String,
     folder_pad: Arc<RwLock<FolderPad>>,

+ 21 - 0
frontend/rust-lib/flowy-core/src/services/view/controller.rs

@@ -83,6 +83,27 @@ impl ViewController {
         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_ctx
+            .controller
+            .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| {

+ 1 - 5
frontend/rust-lib/flowy-core/src/services/workspace/controller.rs

@@ -39,10 +39,6 @@ impl WorkspaceController {
         params: CreateWorkspaceParams,
     ) -> Result<Workspace, FlowyError> {
         let workspace = self.create_workspace_on_server(params.clone()).await?;
-        self.create_workspace_on_local(workspace).await
-    }
-
-    pub(crate) async fn create_workspace_on_local(&self, workspace: Workspace) -> Result<Workspace, FlowyError> {
         let user_id = self.user.user_id()?;
         let token = self.user.token()?;
         let workspaces = self.persistence.begin_transaction(|transaction| {
@@ -184,7 +180,7 @@ impl WorkspaceController {
 
 const CURRENT_WORKSPACE_ID: &str = "current_workspace_id";
 
-fn set_current_workspace(workspace_id: &str) { KV::set_str(CURRENT_WORKSPACE_ID, workspace_id.to_owned()); }
+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) {

+ 0 - 78
frontend/rust-lib/flowy-core/tests/workspace/app_test.rs

@@ -1,78 +0,0 @@
-use flowy_core::entities::{
-    app::QueryAppRequest,
-    trash::{TrashId, TrashType},
-    view::*,
-};
-use flowy_test::helper::*;
-
-#[tokio::test]
-#[should_panic]
-async fn app_delete() {
-    let test = AppTest::new().await;
-    delete_app(&test.sdk, &test.app.id).await;
-    let query = QueryAppRequest {
-        app_ids: vec![test.app.id.clone()],
-    };
-    let _ = read_app(&test.sdk, query).await;
-}
-
-#[tokio::test]
-async fn app_delete_then_putback() {
-    let test = AppTest::new().await;
-    delete_app(&test.sdk, &test.app.id).await;
-    putback_trash(
-        &test.sdk,
-        TrashId {
-            id: test.app.id.clone(),
-            ty: TrashType::App,
-        },
-    )
-    .await;
-
-    let query = QueryAppRequest {
-        app_ids: vec![test.app.id.clone()],
-    };
-    let app = read_app(&test.sdk, query).await;
-    assert_eq!(&app, &test.app);
-}
-
-#[tokio::test]
-async fn app_read() {
-    let test = AppTest::new().await;
-    let query = QueryAppRequest {
-        app_ids: vec![test.app.id.clone()],
-    };
-    let app_from_db = read_app(&test.sdk, query).await;
-    assert_eq!(app_from_db, test.app);
-}
-
-#[tokio::test]
-async fn app_create_with_view() {
-    let test = AppTest::new().await;
-    let request_a = CreateViewRequest {
-        belong_to_id: test.app.id.clone(),
-        name: "View A".to_string(),
-        desc: "".to_string(),
-        thumbnail: Some("http://1.png".to_string()),
-        view_type: ViewType::Doc,
-    };
-
-    let request_b = CreateViewRequest {
-        belong_to_id: test.app.id.clone(),
-        name: "View B".to_string(),
-        desc: "".to_string(),
-        thumbnail: Some("http://1.png".to_string()),
-        view_type: ViewType::Doc,
-    };
-
-    let view_a = create_view_with_request(&test.sdk, request_a).await;
-    let view_b = create_view_with_request(&test.sdk, request_b).await;
-
-    let query = QueryAppRequest {
-        app_ids: vec![test.app.id.clone()],
-    };
-    let view_from_db = read_app(&test.sdk, query).await;
-
-    assert_eq!(view_from_db.belongings[0], view_a);
-    assert_eq!(view_from_db.belongings[1], view_b);
-}

+ 244 - 0
frontend/rust-lib/flowy-core/tests/workspace/folder_test.rs

@@ -0,0 +1,244 @@
+use flowy_core::{
+    entities::workspace::{CreateWorkspaceRequest, QueryWorkspaceRequest},
+    event::WorkspaceEvent::*,
+    prelude::*,
+};
+use flowy_test::{event_builder::*, helper::*, FlowySDKTest};
+
+#[tokio::test]
+async fn workspace_read_all() {
+    let test = WorkspaceTest::new().await;
+    let workspace = read_workspace(&test.sdk, QueryWorkspaceRequest::new(None)).await;
+    assert_eq!(workspace.len(), 2);
+}
+
+#[tokio::test]
+async fn workspace_read() {
+    let test = WorkspaceTest::new().await;
+    let request = QueryWorkspaceRequest::new(Some(test.workspace.id.clone()));
+    let workspace_from_db = read_workspace(&test.sdk, request)
+        .await
+        .drain(..1)
+        .collect::<Vec<Workspace>>()
+        .pop()
+        .unwrap();
+    assert_eq!(test.workspace, workspace_from_db);
+}
+
+#[tokio::test]
+async fn workspace_create_with_apps() {
+    let test = WorkspaceTest::new().await;
+    let app = create_app(&test.sdk, "App A", "AppFlowy GitHub Project", &test.workspace.id).await;
+    let request = QueryWorkspaceRequest::new(Some(test.workspace.id.clone()));
+    let workspace_from_db = read_workspace(&test.sdk, request)
+        .await
+        .drain(..1)
+        .collect::<Vec<Workspace>>()
+        .pop()
+        .unwrap();
+    assert_eq!(&app, workspace_from_db.apps.first_or_crash());
+}
+
+#[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!(
+            CoreModuleEventBuilder::new(sdk)
+                .event(CreateWorkspace)
+                .request(request)
+                .async_send()
+                .await
+                .error()
+                .code,
+            code.value()
+        )
+    }
+}
+
+#[tokio::test]
+async fn workspace_update_with_invalid_name() {
+    let sdk = FlowySDKTest::default();
+    for (name, code) in invalid_workspace_name_test_case() {
+        let request = CreateWorkspaceRequest {
+            name,
+            desc: "".to_owned(),
+        };
+        assert_eq!(
+            CoreModuleEventBuilder::new(sdk.clone())
+                .event(CreateWorkspace)
+                .request(request)
+                .async_send()
+                .await
+                .error()
+                .code,
+            code.value()
+        )
+    }
+}
+
+#[tokio::test]
+#[should_panic]
+async fn app_delete() {
+    let test = AppTest::new().await;
+    delete_app(&test.sdk, &test.app.id).await;
+    let query = QueryAppRequest {
+        app_ids: vec![test.app.id.clone()],
+    };
+    let _ = read_app(&test.sdk, query).await;
+}
+
+#[tokio::test]
+async fn app_delete_then_putback() {
+    let test = AppTest::new().await;
+    delete_app(&test.sdk, &test.app.id).await;
+    putback_trash(
+        &test.sdk,
+        TrashId {
+            id: test.app.id.clone(),
+            ty: TrashType::App,
+        },
+    )
+    .await;
+
+    let query = QueryAppRequest {
+        app_ids: vec![test.app.id.clone()],
+    };
+    let app = read_app(&test.sdk, query).await;
+    assert_eq!(&app, &test.app);
+}
+
+#[tokio::test]
+async fn app_read() {
+    let test = AppTest::new().await;
+    let query = QueryAppRequest {
+        app_ids: vec![test.app.id.clone()],
+    };
+    let app_from_db = read_app(&test.sdk, query).await;
+    assert_eq!(app_from_db, test.app);
+}
+
+#[tokio::test]
+async fn app_create_with_view() {
+    let test = AppTest::new().await;
+    let request_a = CreateViewRequest {
+        belong_to_id: test.app.id.clone(),
+        name: "View A".to_string(),
+        desc: "".to_string(),
+        thumbnail: Some("http://1.png".to_string()),
+        view_type: ViewType::Doc,
+    };
+
+    let request_b = CreateViewRequest {
+        belong_to_id: test.app.id.clone(),
+        name: "View B".to_string(),
+        desc: "".to_string(),
+        thumbnail: Some("http://1.png".to_string()),
+        view_type: ViewType::Doc,
+    };
+
+    let view_a = create_view_with_request(&test.sdk, request_a).await;
+    let view_b = create_view_with_request(&test.sdk, request_b).await;
+
+    let query = QueryAppRequest {
+        app_ids: vec![test.app.id.clone()],
+    };
+    let view_from_db = read_app(&test.sdk, query).await;
+
+    assert_eq!(view_from_db.belongings[0], view_a);
+    assert_eq!(view_from_db.belongings[1], view_b);
+}
+
+#[tokio::test]
+#[should_panic]
+async fn view_delete() {
+    let test = FlowySDKTest::default();
+    let _ = test.init_user().await;
+
+    let test = ViewTest::new(&test).await;
+    test.delete_views(vec![test.view.id.clone()]).await;
+    let query = QueryViewRequest {
+        view_ids: vec![test.view.id.clone()],
+    };
+    let _ = read_view(&test.sdk, query).await;
+}
+
+#[tokio::test]
+async fn view_delete_then_putback() {
+    let test = FlowySDKTest::default();
+    let _ = test.init_user().await;
+
+    let test = ViewTest::new(&test).await;
+    test.delete_views(vec![test.view.id.clone()]).await;
+    putback_trash(
+        &test.sdk,
+        TrashId {
+            id: test.view.id.clone(),
+            ty: TrashType::View,
+        },
+    )
+    .await;
+
+    let query = QueryViewRequest {
+        view_ids: vec![test.view.id.clone()],
+    };
+    let view = read_view(&test.sdk, query).await;
+    assert_eq!(&view, &test.view);
+}
+
+#[tokio::test]
+async fn view_delete_all() {
+    let test = FlowySDKTest::default();
+    let _ = test.init_user().await;
+
+    let test = ViewTest::new(&test).await;
+    let view1 = test.view.clone();
+    let view2 = create_view(&test.sdk, &test.app.id).await;
+    let view3 = create_view(&test.sdk, &test.app.id).await;
+    let view_ids = vec![view1.id.clone(), view2.id.clone(), view3.id.clone()];
+
+    let query = QueryAppRequest {
+        app_ids: vec![test.app.id.clone()],
+    };
+    let app = read_app(&test.sdk, query.clone()).await;
+    assert_eq!(app.belongings.len(), view_ids.len());
+    test.delete_views(view_ids.clone()).await;
+
+    assert_eq!(read_app(&test.sdk, query).await.belongings.len(), 0);
+    assert_eq!(read_trash(&test.sdk).await.len(), view_ids.len());
+}
+
+#[tokio::test]
+async fn view_delete_all_permanent() {
+    let test = FlowySDKTest::default();
+    let _ = test.init_user().await;
+
+    let test = ViewTest::new(&test).await;
+    let view1 = test.view.clone();
+    let view2 = create_view(&test.sdk, &test.app.id).await;
+
+    let view_ids = vec![view1.id.clone(), view2.id.clone()];
+    test.delete_views_permanent(view_ids).await;
+
+    let query = QueryAppRequest {
+        app_ids: vec![test.app.id.clone()],
+    };
+    assert_eq!(read_app(&test.sdk, query).await.belongings.len(), 0);
+    assert_eq!(read_trash(&test.sdk).await.len(), 0);
+}
+
+#[tokio::test]
+async fn view_open_doc() {
+    let test = FlowySDKTest::default();
+    let _ = test.init_user().await;
+
+    let test = ViewTest::new(&test).await;
+    let request = QueryViewRequest {
+        view_ids: vec![test.view.id.clone()],
+    };
+    let _ = open_view(&test.sdk, request).await;
+}

+ 1 - 4
frontend/rust-lib/flowy-core/tests/workspace/main.rs

@@ -1,4 +1 @@
-mod app_test;
-// mod helper;
-mod view_test;
-mod workspace_test;
+mod folder_test;

+ 0 - 96
frontend/rust-lib/flowy-core/tests/workspace/view_test.rs

@@ -1,96 +0,0 @@
-use flowy_core::entities::{
-    app::QueryAppRequest,
-    trash::{TrashId, TrashType},
-    view::*,
-};
-use flowy_test::{helper::*, FlowySDKTest};
-
-#[tokio::test]
-#[should_panic]
-async fn view_delete() {
-    let test = FlowySDKTest::default();
-    let _ = test.init_user().await;
-
-    let test = ViewTest::new(&test).await;
-    test.delete_views(vec![test.view.id.clone()]).await;
-    let query = QueryViewRequest {
-        view_ids: vec![test.view.id.clone()],
-    };
-    let _ = read_view(&test.sdk, query).await;
-}
-
-#[tokio::test]
-async fn view_delete_then_putback() {
-    let test = FlowySDKTest::default();
-    let _ = test.init_user().await;
-
-    let test = ViewTest::new(&test).await;
-    test.delete_views(vec![test.view.id.clone()]).await;
-    putback_trash(
-        &test.sdk,
-        TrashId {
-            id: test.view.id.clone(),
-            ty: TrashType::View,
-        },
-    )
-    .await;
-
-    let query = QueryViewRequest {
-        view_ids: vec![test.view.id.clone()],
-    };
-    let view = read_view(&test.sdk, query).await;
-    assert_eq!(&view, &test.view);
-}
-
-#[tokio::test]
-async fn view_delete_all() {
-    let test = FlowySDKTest::default();
-    let _ = test.init_user().await;
-
-    let test = ViewTest::new(&test).await;
-    let view1 = test.view.clone();
-    let view2 = create_view(&test.sdk, &test.app.id).await;
-    let view3 = create_view(&test.sdk, &test.app.id).await;
-    let view_ids = vec![view1.id.clone(), view2.id.clone(), view3.id.clone()];
-
-    let query = QueryAppRequest {
-        app_ids: vec![test.app.id.clone()],
-    };
-    let app = read_app(&test.sdk, query.clone()).await;
-    assert_eq!(app.belongings.len(), view_ids.len());
-    test.delete_views(view_ids.clone()).await;
-
-    assert_eq!(read_app(&test.sdk, query).await.belongings.len(), 0);
-    assert_eq!(read_trash(&test.sdk).await.len(), view_ids.len());
-}
-
-#[tokio::test]
-async fn view_delete_all_permanent() {
-    let test = FlowySDKTest::default();
-    let _ = test.init_user().await;
-
-    let test = ViewTest::new(&test).await;
-    let view1 = test.view.clone();
-    let view2 = create_view(&test.sdk, &test.app.id).await;
-
-    let view_ids = vec![view1.id.clone(), view2.id.clone()];
-    test.delete_views_permanent(view_ids).await;
-
-    let query = QueryAppRequest {
-        app_ids: vec![test.app.id.clone()],
-    };
-    assert_eq!(read_app(&test.sdk, query).await.belongings.len(), 0);
-    assert_eq!(read_trash(&test.sdk).await.len(), 0);
-}
-
-#[tokio::test]
-async fn view_open_doc() {
-    let test = FlowySDKTest::default();
-    let _ = test.init_user().await;
-
-    let test = ViewTest::new(&test).await;
-    let request = QueryViewRequest {
-        view_ids: vec![test.view.id.clone()],
-    };
-    let _ = open_view(&test.sdk, request).await;
-}

+ 0 - 84
frontend/rust-lib/flowy-core/tests/workspace/workspace_test.rs

@@ -1,84 +0,0 @@
-use flowy_core::{
-    entities::workspace::{CreateWorkspaceRequest, QueryWorkspaceRequest},
-    event::WorkspaceEvent::*,
-    prelude::*,
-};
-use flowy_test::{event_builder::*, helper::*, FlowySDKTest};
-
-#[tokio::test]
-async fn workspace_read_all() {
-    let test = WorkspaceTest::new().await;
-    let workspace = read_workspace(&test.sdk, QueryWorkspaceRequest::new(None)).await;
-    assert_eq!(workspace.len(), 2);
-}
-
-#[tokio::test]
-async fn workspace_read() {
-    let test = WorkspaceTest::new().await;
-    let request = QueryWorkspaceRequest::new(Some(test.workspace.id.clone()));
-    let workspace_from_db = read_workspace(&test.sdk, request)
-        .await
-        .drain(..1)
-        .collect::<Vec<Workspace>>()
-        .pop()
-        .unwrap();
-    assert_eq!(test.workspace, workspace_from_db);
-}
-
-#[tokio::test]
-async fn workspace_create_with_apps() {
-    let test = WorkspaceTest::new().await;
-    let app = create_app(&test.sdk, "App A", "AppFlowy GitHub Project", &test.workspace.id).await;
-    let request = QueryWorkspaceRequest::new(Some(test.workspace.id.clone()));
-    let workspace_from_db = read_workspace(&test.sdk, request)
-        .await
-        .drain(..1)
-        .collect::<Vec<Workspace>>()
-        .pop()
-        .unwrap();
-    assert_eq!(&app, workspace_from_db.apps.first_or_crash());
-}
-
-#[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!(
-            CoreModuleEventBuilder::new(sdk)
-                .event(CreateWorkspace)
-                .request(request)
-                .async_send()
-                .await
-                .error()
-                .code,
-            code.value()
-        )
-    }
-}
-
-#[tokio::test]
-async fn workspace_update_with_invalid_name() {
-    let sdk = FlowySDKTest::default();
-    for (name, code) in invalid_workspace_name_test_case() {
-        let request = CreateWorkspaceRequest {
-            name,
-            desc: "".to_owned(),
-        };
-        assert_eq!(
-            CoreModuleEventBuilder::new(sdk.clone())
-                .event(CreateWorkspace)
-                .request(request)
-                .async_send()
-                .await
-                .error()
-                .code,
-            code.value()
-        )
-    }
-}
-
-// TODO 1) delete workspace, but can't delete the last workspace

+ 4 - 2
frontend/rust-lib/flowy-sdk/src/lib.rs

@@ -177,7 +177,7 @@ async fn _listen_user_status(
             match status {
                 UserStatus::Login { token, user_id } => {
                     tracing::trace!("User did login");
-                    let _ = folder_manager.initialize(&token).await?;
+                    let _ = folder_manager.initialize(&user_id).await?;
                     let _ = ws_conn.start(token, user_id).await?;
                 },
                 UserStatus::Logout { .. } => {
@@ -192,7 +192,9 @@ async fn _listen_user_status(
                 },
                 UserStatus::SignUp { profile, ret } => {
                     tracing::trace!("User did sign up");
-                    let _ = folder_manager.initialize_with_new_user(&profile.token).await?;
+                    let _ = folder_manager
+                        .initialize_with_new_user(&profile.id, &profile.token)
+                        .await?;
                     let _ = ws_conn.start(profile.token.clone(), profile.id.clone()).await?;
                     let _ = ret.send(());
                 },

+ 7 - 0
frontend/rust-lib/flowy-sync/src/cache/mod.rs

@@ -24,6 +24,13 @@ pub struct RevisionCache {
     latest_rev_id: AtomicI64,
 }
 
+pub fn mk_revision_disk_cache(
+    user_id: &str,
+    pool: Arc<ConnectionPool>,
+) -> Arc<dyn RevisionDiskCache<Error = FlowyError>> {
+    Arc::new(SQLitePersistence::new(user_id, pool))
+}
+
 impl RevisionCache {
     pub fn new(user_id: &str, object_id: &str, pool: Arc<ConnectionPool>) -> RevisionCache {
         let disk_cache = Arc::new(SQLitePersistence::new(user_id, pool));

+ 14 - 1
frontend/rust-lib/flowy-test/src/lib.rs

@@ -4,8 +4,9 @@ pub mod helper;
 use crate::helper::*;
 use backend_service::configuration::{get_client_server_configuration, ClientServerConfiguration};
 use flowy_sdk::{FlowySDK, FlowySDKConfig};
-use flowy_user::entities::UserProfile;
+use flowy_user::{entities::UserProfile, services::database::UserDB};
 use lib_infra::uuid_string;
+use std::sync::Arc;
 
 pub mod prelude {
     pub use crate::{event_builder::*, helper::*, *};
@@ -51,3 +52,15 @@ impl FlowySDKTest {
         context.user_profile
     }
 }
+
+pub struct MigrationTest {
+    pub db: UserDB,
+}
+
+impl MigrationTest {
+    pub fn new() -> Self {
+        let dir = root_dir();
+        let db = UserDB::new(&dir);
+        Self { db }
+    }
+}

+ 2 - 2
frontend/rust-lib/flowy-user/src/services/database.rs

@@ -11,12 +11,12 @@ lazy_static! {
     static ref DB: RwLock<Option<Database>> = RwLock::new(None);
 }
 
-pub(crate) struct UserDB {
+pub struct UserDB {
     db_dir: String,
 }
 
 impl UserDB {
-    pub(crate) fn new(db_dir: &str) -> Self {
+    pub fn new(db_dir: &str) -> Self {
         Self {
             db_dir: db_dir.to_owned(),
         }

+ 1 - 1
frontend/rust-lib/flowy-user/src/services/mod.rs

@@ -1,4 +1,4 @@
-mod database;
+pub mod database;
 pub mod notifier;
 mod user_session;
 pub use user_session::*;

+ 11 - 0
shared-lib/flowy-collaboration/src/folder/folder_pad.rs

@@ -39,6 +39,15 @@ pub struct FolderChange {
 }
 
 impl FolderPad {
+    pub fn new(workspaces: Vec<Workspace>, trash: Vec<Trash>) -> CollaborateResult<Self> {
+        let mut pad = FolderPad::default();
+        pad.workspaces = workspaces.into_iter().map(Arc::new).collect::<Vec<_>>();
+        pad.trash = trash.into_iter().map(Arc::new).collect::<Vec<_>>();
+        let json = pad.to_json()?;
+        pad.root = PlainDeltaBuilder::new().insert(&json).build();
+        Ok(pad)
+    }
+
     pub fn from_revisions(revisions: Vec<Revision>) -> CollaborateResult<Self> {
         let mut folder_delta = PlainDelta::new();
         for revision in revisions {
@@ -65,6 +74,8 @@ impl FolderPad {
         Ok(folder)
     }
 
+    pub fn delta(&self) -> &PlainDelta { &self.root }
+
     pub fn create_workspace(&mut self, workspace: Workspace) -> CollaborateResult<Option<FolderChange>> {
         let workspace = Arc::new(workspace);
         if self.workspaces.contains(&workspace) {

+ 1 - 1
shared-lib/flowy-collaboration/src/server_document/document_pad.rs

@@ -23,7 +23,7 @@ impl RevisionSyncObject<RichTextAttributes> for ServerDocument {
     fn id(&self) -> &str { &self.doc_id }
 
     fn compose(&mut self, other: &RichTextDelta) -> Result<(), CollaborateError> {
-        tracing::trace!("{} compose {}", &self.delta.to_json(), other.to_json());
+        // tracing::trace!("{} compose {}", &self.delta.to_json(), other.to_json());
         let new_delta = self.delta.compose(other)?;
         self.delta = new_delta;
         Ok(())