mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
add folder migration & add folder unit test
This commit is contained in:
parent
44ff74a37c
commit
324dc53e5f
@ -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 = []
|
||||
|
@ -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(())
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)?;
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>>,
|
||||
|
@ -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| {
|
||||
|
@ -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) {
|
||||
|
@ -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
frontend/rust-lib/flowy-core/tests/workspace/folder_test.rs
Normal file
244
frontend/rust-lib/flowy-core/tests/workspace/folder_test.rs
Normal file
@ -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 +1 @@
|
||||
mod app_test;
|
||||
// mod helper;
|
||||
mod view_test;
|
||||
mod workspace_test;
|
||||
mod folder_test;
|
||||
|
@ -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;
|
||||
}
|
@ -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
|
@ -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(());
|
||||
},
|
||||
|
@ -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));
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
@ -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,4 +1,4 @@
|
||||
mod database;
|
||||
pub mod database;
|
||||
pub mod notifier;
|
||||
mod user_session;
|
||||
pub use user_session::*;
|
||||
|
@ -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) {
|
||||
|
@ -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(())
|
||||
|
Loading…
Reference in New Issue
Block a user