From 4e67282f2bbe3fd1de9a4466d42fbf3bc644d9d9 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Mon, 28 Aug 2023 13:28:24 +0800 Subject: [PATCH] feat: Data sync on signup (#3283) * chore: sync all data * chore: sync database row and document in the row * chore: sync inline view id * chore: sync row and document in row * fix: tests * fix: migrate document in row * chore: retry when create collab fail * fix: invalid secret cause rerun application * chore: fix clippy warnnings --- .../lib/user/presentation/sign_in_screen.dart | 34 +- .../widgets/setting_third_party_login.dart | 11 + .../lib/style_widget/snap_bar.dart | 2 +- frontend/appflowy_tauri/src-tauri/Cargo.toml | 18 +- frontend/resources/translations/en.json | 1 + frontend/rust-lib/Cargo.lock | 31 +- frontend/rust-lib/Cargo.toml | 16 +- .../src/deps_resolve/folder_deps.rs | 11 +- frontend/rust-lib/flowy-core/src/lib.rs | 28 +- .../src/entities/database_entities.rs | 6 +- .../src/entities/group_entities/group.rs | 6 +- .../src/entities/row_entities.rs | 32 +- .../src/entities/view_entities.rs | 2 +- .../flowy-database2/src/event_handler.rs | 2 +- .../rust-lib/flowy-database2/src/manager.rs | 16 +- .../src/services/database/database_editor.rs | 48 +- .../src/services/database/entities.rs | 12 +- .../src/services/database_view/view_editor.rs | 14 +- .../src/services/field_settings/mod.rs | 9 +- .../src/services/filter/controller.rs | 8 +- .../src/services/group/controller.rs | 6 +- .../controller_impls/checkbox_controller.rs | 4 +- .../group/controller_impls/date_controller.rs | 4 +- .../select_option_controller/util.rs | 4 +- .../group/controller_impls/url_controller.rs | 4 +- .../tests/database/block_test/script.rs | 2 +- .../database/field_settings_test/script.rs | 1 + .../flowy-document2/src/event_handler.rs | 4 +- .../rust-lib/flowy-document2/src/manager.rs | 27 +- .../tests/document/document_redo_undo_test.rs | 4 +- .../tests/document/document_test.rs | 9 +- .../flowy-document2/tests/document/util.rs | 1 + .../rust-lib/flowy-folder-deps/src/cloud.rs | 4 + .../flowy-folder2/src/entities/view.rs | 6 +- .../rust-lib/flowy-folder2/src/manager.rs | 108 ++--- .../rust-lib/flowy-folder2/src/test_helper.rs | 8 +- .../flowy-folder2/src/view_operation.rs | 7 +- .../src/local_server/impls/user.rs | 2 +- .../src/supabase/api/collab_storage.rs | 14 +- .../flowy-server/src/supabase/api/document.rs | 2 + .../flowy-server/src/supabase/api/request.rs | 2 +- .../flowy-server/src/supabase/api/user.rs | 93 +++- .../tests/supabase_test/user_test.rs | 4 +- .../flowy-test/src/document/document_event.rs | 8 +- frontend/rust-lib/flowy-test/src/lib.rs | 6 +- .../tests/document/supabase_test/helper.rs | 6 +- .../user/migration_test/document_test.rs | 8 +- .../migration_test/history_user_db/README.md | 4 + .../tests/user/migration_test/mod.rs | 1 - .../tests/user/migration_test/util.rs | 47 -- .../tests/user/migration_test/version_test.rs | 8 +- .../tests/user/supabase_test/auth_test.rs | 271 ++++++++++- .../supabase_test/history_user_db/README.md | 4 + .../history_user_db/workspace_sync.zip | Bin 0 -> 44001 bytes frontend/rust-lib/flowy-test/tests/util.rs | 41 ++ .../rust-lib/flowy-user-deps/src/cloud.rs | 2 +- .../rust-lib/flowy-user-deps/src/entities.rs | 5 +- frontend/rust-lib/flowy-user/Cargo.toml | 3 + frontend/rust-lib/flowy-user/src/event_map.rs | 4 +- frontend/rust-lib/flowy-user/src/manager.rs | 45 +- .../src/migrations/local_user_to_cloud.rs | 143 ------ .../src/migrations/migrate_to_new_user.rs | 430 ++++++++++++++++++ .../rust-lib/flowy-user/src/migrations/mod.rs | 7 +- .../src/migrations/sync_new_user.rs | 327 +++++++++++++ .../flowy-user/src/services/entities.rs | 2 +- .../src/services/user_workspace_sql.rs | 6 +- 66 files changed, 1492 insertions(+), 513 deletions(-) create mode 100644 frontend/rust-lib/flowy-test/tests/user/migration_test/history_user_db/README.md delete mode 100644 frontend/rust-lib/flowy-test/tests/user/migration_test/util.rs create mode 100644 frontend/rust-lib/flowy-test/tests/user/supabase_test/history_user_db/README.md create mode 100644 frontend/rust-lib/flowy-test/tests/user/supabase_test/history_user_db/workspace_sync.zip delete mode 100644 frontend/rust-lib/flowy-user/src/migrations/local_user_to_cloud.rs create mode 100644 frontend/rust-lib/flowy-user/src/migrations/migrate_to_new_user.rs create mode 100644 frontend/rust-lib/flowy-user/src/migrations/sync_new_user.rs diff --git a/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart index 94f34f048c..359458f372 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/sign_in_screen.dart @@ -73,19 +73,27 @@ class SignInScreen extends StatelessWidget { } void handleOpenWorkspaceError(BuildContext context, FlowyError error) { - if (error.code == ErrorCode.WorkspaceDataNotSync) { - final userFolder = UserFolderPB.fromBuffer(error.payload); - getIt().pushWorkspaceErrorScreen(context, userFolder, error); - } else { - Log.error(error); - showSnapBar( - context, - error.msg, - onClosed: () { - getIt().signOut(); - runAppFlowy(); - }, - ); + Log.error(error); + switch (error.code) { + case ErrorCode.WorkspaceDataNotSync: + final userFolder = UserFolderPB.fromBuffer(error.payload); + getIt().pushWorkspaceErrorScreen(context, userFolder, error); + break; + case ErrorCode.InvalidEncryptSecret: + showSnapBar( + context, + error.msg, + ); + break; + default: + showSnapBar( + context, + error.msg, + onClosed: () { + getIt().signOut(); + runAppFlowy(); + }, + ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart index be5c08e8a4..443851f819 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart @@ -31,6 +31,14 @@ class SettingThirdPartyLogin extends StatelessWidget { final indicator = state.isSubmitting ? const CircularProgressIndicator.adaptive() : const SizedBox.shrink(); + + final promptMessage = state.isSubmitting + ? FlowyText.medium( + LocaleKeys.signIn_syncPromptMessage.tr(), + maxLines: null, + ) + : const SizedBox.shrink(); + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -45,9 +53,12 @@ class SettingThirdPartyLogin extends StatelessWidget { ], ), const VSpace(6), + promptMessage, + const VSpace(6), const ThirdPartySignInButtons( mainAxisAlignment: MainAxisAlignment.start, ), + const VSpace(6), ], ); }, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart index 63683a0ad4..6caf35a857 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart @@ -7,7 +7,7 @@ void showSnapBar(BuildContext context, String title, {VoidCallback? onClosed}) { ScaffoldMessenger.of(context) .showSnackBar( SnackBar( - duration: const Duration(milliseconds: 10000), + duration: const Duration(milliseconds: 8000), content: WillPopScope( onWillPop: () async { ScaffoldMessenger.of(context).removeCurrentSnackBar(); diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 6cf136f4e9..328de68e02 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -34,15 +34,15 @@ default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] [patch.crates-io] -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } #collab = { path = "../../../../AppFlowy-Collab/collab" } #collab-folder = { path = "../../../../AppFlowy-Collab/collab-folder" } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 3e02821916..2af12f660c 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -41,6 +41,7 @@ "dontHaveAnAccount": "Don't have an account?", "repeatPasswordEmptyError": "Repeat password can't be empty", "unmatchedPasswordError": "Repeat password is not the same as password", + "syncPromptMessage": "Syncing the data might take a while. Please don't close this page", "signInWith": "Sign in with:" }, "workspace": { diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 0860c39fb9..9175764671 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -113,14 +113,14 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "appflowy-integrate" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b1f6737#b1f67375e39c67e32c502b2968749bedf61d6a46" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "collab", @@ -611,7 +611,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b1f6737#b1f67375e39c67e32c502b2968749bedf61d6a46" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "bytes", @@ -629,7 +629,7 @@ dependencies = [ [[package]] name = "collab-client-ws" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b1f6737#b1f67375e39c67e32c502b2968749bedf61d6a46" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "bytes", "collab-sync", @@ -647,7 +647,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b1f6737#b1f67375e39c67e32c502b2968749bedf61d6a46" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "async-trait", @@ -676,7 +676,7 @@ dependencies = [ [[package]] name = "collab-define" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b1f6737#b1f67375e39c67e32c502b2968749bedf61d6a46" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "uuid", ] @@ -684,7 +684,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b1f6737#b1f67375e39c67e32c502b2968749bedf61d6a46" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "proc-macro2", "quote", @@ -696,7 +696,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b1f6737#b1f67375e39c67e32c502b2968749bedf61d6a46" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "collab", @@ -715,7 +715,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b1f6737#b1f67375e39c67e32c502b2968749bedf61d6a46" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "chrono", @@ -735,7 +735,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b1f6737#b1f67375e39c67e32c502b2968749bedf61d6a46" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "bincode", "chrono", @@ -755,7 +755,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b1f6737#b1f67375e39c67e32c502b2968749bedf61d6a46" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "async-trait", @@ -784,7 +784,7 @@ dependencies = [ [[package]] name = "collab-sync" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b1f6737#b1f67375e39c67e32c502b2968749bedf61d6a46" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "bytes", "collab", @@ -806,7 +806,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b1f6737#b1f67375e39c67e32c502b2968749bedf61d6a46" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=1b297c#1b297c2ed75aa33b964f0da546d771b00805be62" dependencies = [ "anyhow", "collab", @@ -1738,11 +1738,13 @@ dependencies = [ name = "flowy-user" version = "0.1.0" dependencies = [ + "anyhow", "appflowy-integrate", "base64 0.21.2", "bytes", "chrono", "collab", + "collab-database", "collab-document", "collab-folder", "collab-user", @@ -1754,6 +1756,7 @@ dependencies = [ "flowy-derive", "flowy-encrypt", "flowy-error", + "flowy-folder-deps", "flowy-notification", "flowy-server-config", "flowy-sqlite", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index eb2f5a9a49..f842cc248d 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -39,14 +39,14 @@ opt-level = 3 incremental = false [patch.crates-io] -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } -collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b1f6737" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } +collab-define = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "1b297c" } #collab = { path = "../AppFlowy-Collab/collab" } #collab-folder = { path = "../AppFlowy-Collab/collab-folder" } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index f08477931b..18b26124ca 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -111,6 +111,7 @@ impl FolderOperationHandler for DocumentFolderOperation { let document_pb = JsonToDocumentParser::json_str_to_document(json_str).unwrap(); manager .create_document(uid, &view.parent_view.id, Some(document_pb.into())) + .await .unwrap(); view }) @@ -165,7 +166,9 @@ impl FolderOperationHandler for DocumentFolderOperation { let manager = self.0.clone(); FutureResult::new(async move { let data = DocumentDataPB::try_from(Bytes::from(data))?; - manager.create_document(user_id, &view_id, Some(data.into()))?; + manager + .create_document(user_id, &view_id, Some(data.into())) + .await?; Ok(()) }) } @@ -182,7 +185,7 @@ impl FolderOperationHandler for DocumentFolderOperation { let view_id = view_id.to_string(); let manager = self.0.clone(); FutureResult::new(async move { - manager.create_document(user_id, &view_id, None)?; + manager.create_document(user_id, &view_id, None).await?; Ok(()) }) } @@ -199,7 +202,9 @@ impl FolderOperationHandler for DocumentFolderOperation { let manager = self.0.clone(); FutureResult::new(async move { let data = DocumentDataPB::try_from(Bytes::from(bytes))?; - manager.create_document(uid, &view_id, Some(data.into()))?; + manager + .create_document(uid, &view_id, Some(data.into())) + .await?; Ok(()) }) } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 09a68b6baf..905eed318f 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -16,10 +16,10 @@ use tokio::sync::RwLock; use flowy_database2::DatabaseManager; use flowy_document2::manager::DocumentManager; use flowy_error::FlowyResult; -use flowy_folder2::manager::{FolderInitializeData, FolderManager}; +use flowy_folder2::manager::{FolderInitializeDataSource, FolderManager}; use flowy_sqlite::kv::StorePreferences; use flowy_task::{TaskDispatcher, TaskRunner}; -use flowy_user::event_map::{SignUpContext, UserCloudServiceProvider, UserStatusCallback}; +use flowy_user::event_map::{UserCloudServiceProvider, UserStatusCallback}; use flowy_user::manager::{UserManager, UserSessionConfig}; use flowy_user_deps::cloud::UserCloudConfig; use flowy_user_deps::entities::{AuthType, UserProfile, UserWorkspace}; @@ -316,13 +316,19 @@ impl UserStatusCallback for UserStatusCallbackImpl { to_fut(async move { collab_builder.initialize(user_workspace.id.clone()); folder_manager - .initialize(user_id, &user_workspace.id, FolderInitializeData::Empty) + .initialize( + user_id, + &user_workspace.id, + FolderInitializeDataSource::LocalDisk { + create_if_not_exist: false, + }, + ) .await?; database_manager .initialize( user_id, user_workspace.id.clone(), - user_workspace.database_storage_id, + user_workspace.database_views_aggregate_id, ) .await?; document_manager @@ -352,7 +358,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .initialize( user_id, user_workspace.id.clone(), - user_workspace.database_storage_id, + user_workspace.database_views_aggregate_id, ) .await?; document_manager @@ -364,7 +370,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { fn did_sign_up( &self, - context: SignUpContext, + is_new_user: bool, user_profile: &UserProfile, user_workspace: &UserWorkspace, _device_id: &str, @@ -380,8 +386,10 @@ impl UserStatusCallback for UserStatusCallbackImpl { .initialize_with_new_user( user_profile.uid, &user_profile.token, - context.is_new, - context.local_folder, + is_new_user, + FolderInitializeDataSource::LocalDisk { + create_if_not_exist: true, + }, &user_workspace.id, ) .await?; @@ -389,7 +397,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .initialize_with_new_user( user_profile.uid, user_workspace.id.clone(), - user_workspace.database_storage_id, + user_workspace.database_views_aggregate_id, ) .await?; @@ -425,7 +433,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .initialize( user_id, user_workspace.id.clone(), - user_workspace.database_storage_id, + user_workspace.database_views_aggregate_id, ) .await?; document_manager diff --git a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs index 7faf2e2558..197d335cb9 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs @@ -1,6 +1,6 @@ use collab::core::collab_state::SyncState; use collab_database::rows::RowId; -use collab_database::user::DatabaseRecord; +use collab_database::user::DatabaseWithViews; use collab_database::views::DatabaseLayout; use flowy_derive::ProtoBuf; @@ -197,8 +197,8 @@ pub struct DatabaseDescriptionPB { pub database_id: String, } -impl From for DatabaseDescriptionPB { - fn from(data: DatabaseRecord) -> Self { +impl From for DatabaseDescriptionPB { + fn from(data: DatabaseWithViews) -> Self { Self { name: data.name, database_id: data.database_id, diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs index b2f5e6aa8b..69280c66fc 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs @@ -94,11 +94,7 @@ impl std::convert::From for GroupPB { field_id: group_data.field_id, group_id: group_data.id, group_name: group_data.name, - rows: group_data - .rows - .into_iter() - .map(|row_detail| RowMetaPB::from(row_detail.meta)) - .collect(), + rows: group_data.rows.into_iter().map(RowMetaPB::from).collect(), is_default: group_data.is_default, is_visible: group_data.is_visible, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs index 6ef2fe9039..2e9e4859e5 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use collab_database::rows::{Row, RowId, RowMeta}; +use collab_database::rows::{Row, RowDetail, RowId}; use collab_database::views::RowOrder; use flowy_derive::ProtoBuf; @@ -61,27 +61,27 @@ pub struct RowMetaPB { pub cover: Option, } -impl std::convert::From<&RowMeta> for RowMetaPB { - fn from(row_meta: &RowMeta) -> Self { +impl std::convert::From<&RowDetail> for RowMetaPB { + fn from(row_detail: &RowDetail) -> Self { Self { - id: row_meta.row_id.clone(), - document_id: row_meta.document_id.clone(), - icon: row_meta.icon_url.clone(), - cover: row_meta.cover_url.clone(), + id: row_detail.row.id.to_string(), + document_id: row_detail.document_id.clone(), + icon: row_detail.meta.icon_url.clone(), + cover: row_detail.meta.cover_url.clone(), } } } - -impl std::convert::From for RowMetaPB { - fn from(row_meta: RowMeta) -> Self { +impl std::convert::From for RowMetaPB { + fn from(row_detail: RowDetail) -> Self { Self { - id: row_meta.row_id, - document_id: row_meta.document_id, - icon: row_meta.icon_url, - cover: row_meta.cover_url, + id: row_detail.row.id.to_string(), + document_id: row_detail.document_id, + icon: row_detail.meta.icon_url, + cover: row_detail.meta.cover_url, } } } +// #[derive(Debug, Default, Clone, ProtoBuf)] pub struct UpdateRowMetaChangesetPB { @@ -251,7 +251,7 @@ impl std::convert::From for InsertedRowPB { impl From for InsertedRowPB { fn from(data: InsertedRow) -> Self { Self { - row_meta: data.row_meta.into(), + row_meta: data.row_detail.into(), index: data.index, is_new: data.is_new, } @@ -274,7 +274,7 @@ pub struct UpdatedRowPB { impl From for UpdatedRowPB { fn from(data: UpdatedRow) -> Self { - let row_meta = data.row_meta.map(RowMetaPB::from); + let row_meta = data.row_detail.map(RowMetaPB::from); Self { row_id: data.row_id, field_ids: data.field_ids, diff --git a/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs index 513e2e9e31..c32dc4b124 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs @@ -88,7 +88,7 @@ impl From for DidFetchRowPB { visibility: value.row.visibility, created_at: value.row.created_at, modified_at: value.row.modified_at, - meta: RowMetaPB::from(value.meta), + meta: RowMetaPB::from(value), } } } diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 3376716965..b7194b6288 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -461,7 +461,7 @@ pub(crate) async fn create_row_handler( .await? { None => Err(FlowyError::internal().with_context("Create row fail")), - Some(row) => data_result_ok(RowMetaPB::from(row.meta)), + Some(row) => data_result_ok(RowMetaPB::from(row)), } } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 577f21b524..abae5fa5da 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -76,7 +76,7 @@ impl DatabaseManager { &self, uid: i64, _workspace_id: String, - database_storage_id: String, + database_views_aggregate_id: String, ) -> FlowyResult<()> { let collab_db = self.user.collab_db(uid)?; let collab_builder = UserDatabaseCollabServiceImpl { @@ -87,11 +87,11 @@ impl DatabaseManager { let mut collab_raw_data = CollabRawData::default(); // If the workspace database not exist in disk, try to fetch from remote. - if !self.is_collab_exist(uid, &collab_db, &database_storage_id) { + if !self.is_collab_exist(uid, &collab_db, &database_views_aggregate_id) { tracing::trace!("workspace database not exist, try to fetch from remote"); match self .cloud_service - .get_collab_update(&database_storage_id, CollabType::WorkspaceDatabase) + .get_collab_update(&database_views_aggregate_id, CollabType::WorkspaceDatabase) .await { Ok(updates) => { @@ -100,17 +100,17 @@ impl DatabaseManager { Err(err) => { return Err(FlowyError::record_not_found().with_context(format!( "get workspace database :{} failed: {}", - database_storage_id, err, + database_views_aggregate_id, err, ))); }, } } // Construct the workspace database. - tracing::trace!("open workspace database: {}", &database_storage_id); + tracing::trace!("open workspace database: {}", &database_views_aggregate_id); let collab = collab_builder.build_collab_with_config( uid, - &database_storage_id, + &database_views_aggregate_id, CollabType::WorkspaceDatabase, collab_db.clone(), collab_raw_data, @@ -130,10 +130,10 @@ impl DatabaseManager { &self, user_id: i64, workspace_id: String, - database_storage_id: String, + database_views_aggregate_id: String, ) -> FlowyResult<()> { self - .initialize(user_id, workspace_id, database_storage_id) + .initialize(user_id, workspace_id, database_views_aggregate_id) .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 34d10a7cee..9d7374752d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -411,8 +411,8 @@ impl DatabaseEditor { pub async fn move_row(&self, view_id: &str, from: RowId, to: RowId) { let database = self.database.lock(); - if let (Some(row_meta), Some(from_index), Some(to_index)) = ( - database.get_row_meta(&from), + if let (Some(row_detail), Some(from_index), Some(to_index)) = ( + database.get_row_detail(&from), database.index_of_row(view_id, &from), database.index_of_row(view_id, &to), ) { @@ -422,7 +422,7 @@ impl DatabaseEditor { drop(database); let delete_row_id = from.into_inner(); - let insert_row = InsertedRowPB::new(RowMetaPB::from(&row_meta)).with_index(to_index as i32); + let insert_row = InsertedRowPB::new(RowMetaPB::from(row_detail)).with_index(to_index as i32); let changes = RowsChangePB::from_move(vec![delete_row_id], vec![insert_row]); send_notification(view_id, DatabaseNotification::DidUpdateViewRows) .payload(changes) @@ -442,10 +442,8 @@ impl DatabaseEditor { let result = self.database.lock().create_row_in_view(view_id, params); if let Some((index, row_order)) = result { tracing::trace!("create row: {:?} at {}", row_order, index); - let row = self.database.lock().get_row(&row_order.id); - let row_meta = self.database.lock().get_row_meta(&row_order.id); - if let Some(meta) = row_meta { - let row_detail = RowDetail { row, meta }; + let row_detail = self.database.lock().get_row_detail(&row_order.id); + if let Some(row_detail) = row_detail { for view in self.database_views.editors().await { view.v_did_create_row(&row_detail, &group_id, index).await; } @@ -545,9 +543,10 @@ impl DatabaseEditor { pub fn get_row_meta(&self, view_id: &str, row_id: &RowId) -> Option { if self.database.lock().views.is_row_exist(view_id, row_id) { let row_meta = self.database.lock().get_row_meta(row_id)?; + let row_document_id = self.database.lock().get_row_document_id(row_id)?; Some(RowMetaPB { id: row_id.clone().into_inner(), - document_id: row_meta.document_id, + document_id: row_document_id, icon: row_meta.icon_url, cover: row_meta.cover_url, }) @@ -559,9 +558,7 @@ impl DatabaseEditor { pub fn get_row_detail(&self, view_id: &str, row_id: &RowId) -> Option { if self.database.lock().views.is_row_exist(view_id, row_id) { - let meta = self.database.lock().get_row_meta(row_id)?; - let row = self.database.lock().get_row(row_id); - Some(RowDetail { row, meta }) + self.database.lock().get_row_detail(row_id) } else { tracing::warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id); None @@ -587,15 +584,15 @@ impl DatabaseEditor { }); // Use the temporary row meta to get rid of the lock that not implement the `Send` or 'Sync' trait. - let row_meta = self.database.lock().get_row_meta(row_id); - if let Some(row_meta) = row_meta { + let row_detail = self.database.lock().get_row_detail(row_id); + if let Some(row_detail) = row_detail { for view in self.database_views.editors().await { - view.v_did_update_row_meta(row_id, &row_meta).await; + view.v_did_update_row_meta(row_id, &row_detail).await; } // Notifies the client that the row meta has been updated. send_notification(row_id.as_str(), DatabaseNotification::DidUpdateRowMeta) - .payload(RowMetaPB::from(&row_meta)) + .payload(RowMetaPB::from(&row_detail)) .send(); } } @@ -1084,7 +1081,7 @@ impl DatabaseEditor { let rows = rows .into_iter() - .map(|row_detail| RowMetaPB::from(&row_detail.meta)) + .map(|row_detail| RowMetaPB::from(row_detail.as_ref())) .collect::>(); Ok(DatabasePB { id: database_id, @@ -1245,17 +1242,10 @@ impl DatabaseViewData for DatabaseViewDataImpl { fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut)>> { let index = self.database.lock().index_of_row(view_id, row_id); - let row = self.database.lock().get_row(row_id); - let row_meta = self.database.lock().get_row_meta(row_id); + let row_detail = self.database.lock().get_row_detail(row_id); to_fut(async move { - match (index, row_meta) { - (Some(index), Some(row_meta)) => { - let row_detail = RowDetail { - row, - meta: row_meta, - }; - Some((index, Arc::new(row_detail))) - }, + match (index, row_detail) { + (Some(index), Some(row_detail)) => Some((index, Arc::new(row_detail))), _ => None, } }) @@ -1266,11 +1256,7 @@ impl DatabaseViewData for DatabaseViewDataImpl { let rows = database.get_rows_for_view(view_id); let row_details = rows .into_iter() - .flat_map(|row| { - database - .get_row_meta(&row.id) - .map(|meta| RowDetail { row, meta }) - }) + .flat_map(|row| database.get_row_detail(&row.id)) .collect::>(); to_fut(async move { row_details.into_iter().map(Arc::new).collect() }) diff --git a/frontend/rust-lib/flowy-database2/src/services/database/entities.rs b/frontend/rust-lib/flowy-database2/src/services/database/entities.rs index edf48c352e..86acb5d7b2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/entities.rs @@ -1,4 +1,4 @@ -use collab_database::rows::{RowId, RowMeta}; +use collab_database::rows::{RowDetail, RowId}; use collab_database::views::DatabaseLayout; #[derive(Debug, Clone)] @@ -14,7 +14,7 @@ pub enum DatabaseRowEvent { #[derive(Debug, Clone)] pub struct InsertedRow { - pub row_meta: RowMeta, + pub row_detail: RowDetail, pub index: Option, pub is_new: bool, } @@ -29,7 +29,7 @@ pub struct UpdatedRow { pub field_ids: Vec, /// The meta of row was updated if this is Some. - pub row_meta: Option, + pub row_detail: Option, } impl UpdatedRow { @@ -38,7 +38,7 @@ impl UpdatedRow { row_id: row_id.to_string(), height: None, field_ids: vec![], - row_meta: None, + row_detail: None, } } @@ -52,8 +52,8 @@ impl UpdatedRow { self } - pub fn with_row_meta(mut self, row_meta: RowMeta) -> Self { - self.row_meta = Some(row_meta); + pub fn with_row_meta(mut self, row_detail: RowDetail) -> Self { + self.row_detail = Some(row_detail); self } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index af71d28d94..f9dffa7392 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use collab_database::database::{gen_database_filter_id, gen_database_sort_id, MutexDatabase}; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cells, Row, RowCell, RowDetail, RowId, RowMeta}; +use collab_database::rows::{Cells, Row, RowCell, RowDetail, RowId}; use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; use tokio::sync::{broadcast, RwLock}; @@ -216,8 +216,8 @@ impl DatabaseViewEditor { .await; } - pub async fn v_did_update_row_meta(&self, row_id: &RowId, row_meta: &RowMeta) { - let update_row = UpdatedRow::new(row_id.as_str()).with_row_meta(row_meta.clone()); + pub async fn v_did_update_row_meta(&self, row_id: &RowId, row_detail: &RowDetail) { + let update_row = UpdatedRow::new(row_id.as_str()).with_row_meta(row_detail.clone()); let changeset = RowsChangePB::from_update(update_row.into()); send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) .payload(changeset) @@ -234,7 +234,7 @@ impl DatabaseViewEditor { // Send the group notification if the current view has groups match group_id.as_ref() { None => { - let row = InsertedRowPB::new(RowMetaPB::from(&row_detail.meta)).with_index(index as i32); + let row = InsertedRowPB::new(RowMetaPB::from(row_detail)).with_index(index as i32); changes = RowsChangePB::from_insert(row); }, Some(group_id) => { @@ -246,7 +246,7 @@ impl DatabaseViewEditor { .await; let inserted_row = InsertedRowPB { - row_meta: RowMetaPB::from(&row_detail.meta), + row_meta: RowMetaPB::from(row_detail), index: Some(index as i32), is_new: true, }; @@ -790,7 +790,7 @@ impl DatabaseViewEditor { let (_, row_detail) = self.delegate.get_row(&self.view_id, &row_id).await?; Some(CalendarEventPB { - row_meta: RowMetaPB::from(&row_detail.meta), + row_meta: RowMetaPB::from(row_detail.as_ref()), date_field_id: date_field.id.clone(), title, timestamp, @@ -853,7 +853,7 @@ impl DatabaseViewEditor { let (_, row_detail) = self.delegate.get_row(&self.view_id, &row_id).await?; let event = CalendarEventPB { - row_meta: RowMetaPB::from(&row_detail.meta), + row_meta: RowMetaPB::from(row_detail.as_ref()), date_field_id: calendar_setting.field_id.clone(), title, timestamp, diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/mod.rs index 506779948a..4e84cc3450 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/mod.rs @@ -1,7 +1,8 @@ -mod entities; -mod field_settings; -mod field_settings_builder; - pub use entities::*; pub use field_settings::*; pub use field_settings_builder::*; + +mod entities; +#[allow(clippy::module_inception)] +mod field_settings; +mod field_settings_builder; diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs index fe676fdba3..6d25e24c0c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs @@ -161,9 +161,9 @@ impl FilterController { ) { if is_visible { if let Some((index, _row)) = self.delegate.get_row(&self.view_id, &row_id).await { - notification - .visible_rows - .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta)).with_index(index as i32)) + notification.visible_rows.push( + InsertedRowPB::new(RowMetaPB::from(row_detail.as_ref())).with_index(index as i32), + ) } } else { notification.invisible_rows.push(row_id); @@ -197,7 +197,7 @@ impl FilterController { &self.cell_filter_cache, ) { if is_visible { - let row_meta = RowMetaPB::from(&row_detail.meta); + let row_meta = RowMetaPB::from(row_detail.as_ref()); visible_rows.push(InsertedRowPB::new(row_meta).with_index(index as i32)) } else { invisible_rows.push(row_id); diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs index ea2474de28..827a1c3cd5 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -165,7 +165,7 @@ where if !no_status_group_rows.is_empty() { changeset .inserted_rows - .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta))); + .push(InsertedRowPB::new(RowMetaPB::from(row_detail))); no_status_group.add_row(row_detail.clone()); } @@ -190,7 +190,7 @@ where let mut deleted_row_ids = vec![]; for row_detail in &no_status_group.rows { - let row_id = row_detail.meta.row_id.clone(); + let row_id = row_detail.row.id.to_string(); if default_group_deleted_rows .iter() .any(|deleted_row| deleted_row.row_meta.id == row_id) @@ -200,7 +200,7 @@ where } no_status_group .rows - .retain(|row_detail| !deleted_row_ids.contains(&row_detail.meta.row_id)); + .retain(|row_detail| !deleted_row_ids.contains(&row_detail.row.id)); changeset.deleted_rows.extend(deleted_row_ids); Some(changeset) } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs index 75645c17dd..221bdbbea0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs @@ -69,7 +69,7 @@ impl GroupCustomize for CheckboxGroupController { if is_not_contained { changeset .inserted_rows - .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta))); + .push(InsertedRowPB::new(RowMetaPB::from(row_detail))); group.add_row(row_detail.clone()); } } @@ -87,7 +87,7 @@ impl GroupCustomize for CheckboxGroupController { if is_not_contained { changeset .inserted_rows - .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta))); + .push(InsertedRowPB::new(RowMetaPB::from(row_detail))); group.add_row(row_detail.clone()); } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs index b260076818..349d0f7c63 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs @@ -112,7 +112,7 @@ impl GroupCustomize for DateGroupController { &setting_content, ); let mut new_group = self.context.add_new_group(group)?; - new_group.group.rows.push(RowMetaPB::from(&row_detail.meta)); + new_group.group.rows.push(RowMetaPB::from(row_detail)); inserted_group = Some(new_group); } @@ -164,7 +164,7 @@ impl GroupCustomize for DateGroupController { if !group.contains_row(&row_detail.row.id) { changeset .inserted_rows - .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta))); + .push(InsertedRowPB::new(RowMetaPB::from(row_detail))); group.add_row(row_detail.clone()); } } else if group.contains_row(&row_detail.row.id) { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs index 5a1ccabb71..2251a4ae06 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs @@ -31,7 +31,7 @@ pub fn add_or_remove_select_option_row( if !group.contains_row(&row_detail.row.id) { changeset .inserted_rows - .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta))); + .push(InsertedRowPB::new(RowMetaPB::from(row_detail))); group.add_row(row_detail.clone()); } } else if group.contains_row(&row_detail.row.id) { @@ -104,7 +104,7 @@ pub fn move_group_row( } if group.id == *to_group_id { - let mut inserted_row = InsertedRowPB::new(RowMetaPB::from(&row_detail.meta)); + let mut inserted_row = InsertedRowPB::new(RowMetaPB::from((*row_detail).clone())); match to_index { None => { changeset.inserted_rows.push(inserted_row); diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs index d2fa44bc30..f08ab426d5 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs @@ -58,7 +58,7 @@ impl GroupCustomize for URLGroupController { let cell_data: URLCellData = _cell_data.clone().into(); let group = make_group_from_url_cell(&cell_data); let mut new_group = self.context.add_new_group(group)?; - new_group.group.rows.push(RowMetaPB::from(&row_detail.meta)); + new_group.group.rows.push(RowMetaPB::from(row_detail)); inserted_group = Some(new_group); } @@ -99,7 +99,7 @@ impl GroupCustomize for URLGroupController { if !group.contains_row(&row_detail.row.id) { changeset .inserted_rows - .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta))); + .push(InsertedRowPB::new(RowMetaPB::from(row_detail))); group.add_row(row_detail.clone()); } } else if group.contains_row(&row_detail.row.id) { diff --git a/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs index abe22c353f..c253422242 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs @@ -43,7 +43,7 @@ impl DatabaseRowTest { .unwrap(); self .row_by_row_id - .insert(row_detail.row.id.to_string(), row_detail.meta.into()); + .insert(row_detail.row.id.to_string(), row_detail.into()); self.row_details = self.get_rows().await; }, RowScript::UpdateTextCell { row_id, content } => { diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/script.rs index 6e6d53dfa4..4ca25eb89f 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/script.rs @@ -3,6 +3,7 @@ use flowy_database2::services::field_settings::FieldSettingsChangesetParams; use crate::database::database_editor::DatabaseEditorTest; +#[allow(clippy::enum_variant_names)] pub enum FieldSettingsScript { AssertFieldSettings { field_id: String, diff --git a/frontend/rust-lib/flowy-document2/src/event_handler.rs b/frontend/rust-lib/flowy-document2/src/event_handler.rs index 89b8895500..c01a86ab6b 100644 --- a/frontend/rust-lib/flowy-document2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document2/src/event_handler.rs @@ -34,7 +34,9 @@ pub(crate) async fn create_document_handler( let manager = upgrade_document(manager)?; let params: CreateDocumentParams = data.into_inner().try_into()?; let uid = manager.user.user_id()?; - manager.create_document(uid, ¶ms.document_id, params.initial_data)?; + manager + .create_document(uid, ¶ms.document_id, params.initial_data) + .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-document2/src/manager.rs b/frontend/rust-lib/flowy-document2/src/manager.rs index 5b64ee2205..6d4e2e784e 100644 --- a/frontend/rust-lib/flowy-document2/src/manager.rs +++ b/frontend/rust-lib/flowy-document2/src/manager.rs @@ -57,7 +57,7 @@ impl DocumentManager { /// /// if the document already exists, return the existing document. /// if the data is None, will create a document with default data. - pub fn create_document( + pub async fn create_document( &self, uid: i64, doc_id: &str, @@ -65,13 +65,19 @@ impl DocumentManager { ) -> FlowyResult> { tracing::trace!("create a document: {:?}", doc_id); let collab = self.collab_for_document(uid, doc_id, vec![])?; - let data = data.unwrap_or_else(default_document_data); - let document = Arc::new(MutexDocument::create_with_data(collab, data)?); - Ok(document) + + match self.get_document(doc_id).await { + Ok(document) => Ok(document), + Err(_) => { + let data = data.unwrap_or_else(default_document_data); + let document = Arc::new(MutexDocument::create_with_data(collab, data)?); + Ok(document) + }, + } } /// Return the document - #[tracing::instrument(level = "debug", skip_all)] + #[tracing::instrument(level = "debug", skip(self), err)] pub async fn get_document(&self, doc_id: &str) -> FlowyResult> { if let Some(doc) = self.documents.read().get(doc_id) { return Ok(doc.clone()); @@ -83,10 +89,7 @@ impl DocumentManager { } let uid = self.user.user_id()?; - let db = self.user.collab_db(uid)?; - let collab = self - .collab_builder - .build(uid, doc_id, CollabType::Document, updates, db)?; + let collab = self.collab_for_document(uid, doc_id, updates)?; let document = Arc::new(MutexDocument::open(doc_id, collab)?); // save the document to the memory and read it from the memory if we open the same document again. @@ -101,11 +104,7 @@ impl DocumentManager { pub async fn get_document_data(&self, doc_id: &str) -> FlowyResult { let mut updates = vec![]; if !self.is_doc_exist(doc_id)? { - if let Ok(document_updates) = self.cloud_service.get_document_updates(doc_id).await { - updates = document_updates; - } else { - return Err(FlowyError::collab_not_sync()); - } + updates = self.cloud_service.get_document_updates(doc_id).await?; } let uid = self.user.user_id()?; let collab = self.collab_for_document(uid, doc_id, updates)?; diff --git a/frontend/rust-lib/flowy-document2/tests/document/document_redo_undo_test.rs b/frontend/rust-lib/flowy-document2/tests/document/document_redo_undo_test.rs index fee0220fd8..443a2fac30 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/document_redo_undo_test.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/document_redo_undo_test.rs @@ -13,7 +13,9 @@ async fn undo_redo_test() { let data = default_document_data(); // create a document - _ = test.create_document(test.user.user_id().unwrap(), &doc_id, Some(data.clone())); + _ = test + .create_document(test.user.user_id().unwrap(), &doc_id, Some(data.clone())) + .await; // open a document let document = test.get_document(&doc_id).await.unwrap(); diff --git a/frontend/rust-lib/flowy-document2/tests/document/document_test.rs b/frontend/rust-lib/flowy-document2/tests/document/document_test.rs index 763e3add2f..2a6629c24d 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/document_test.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/document_test.rs @@ -16,6 +16,7 @@ async fn restore_document() { let uid = test.user.user_id().unwrap(); let document_a = test .create_document(uid, &doc_id, Some(data.clone())) + .await .unwrap(); let data_a = document_a.lock().get_document_data().unwrap(); assert_eq!(data_a, data); @@ -33,7 +34,7 @@ async fn restore_document() { assert_eq!(data_b, data); // restore - _ = test.create_document(uid, &doc_id, Some(data.clone())); + _ = test.create_document(uid, &doc_id, Some(data.clone())).await; // open a document let data_b = test .get_document(&doc_id) @@ -56,7 +57,7 @@ async fn document_apply_insert_action() { let data = default_document_data(); // create a document - _ = test.create_document(uid, &doc_id, Some(data.clone())); + _ = test.create_document(uid, &doc_id, Some(data.clone())).await; // open a document let document = test.get_document(&doc_id).await.unwrap(); @@ -107,7 +108,7 @@ async fn document_apply_update_page_action() { let data = default_document_data(); // create a document - _ = test.create_document(uid, &doc_id, Some(data.clone())); + _ = test.create_document(uid, &doc_id, Some(data.clone())).await; // open a document let document = test.get_document(&doc_id).await.unwrap(); @@ -148,7 +149,7 @@ async fn document_apply_update_action() { let data = default_document_data(); // create a document - _ = test.create_document(uid, &doc_id, Some(data.clone())); + _ = test.create_document(uid, &doc_id, Some(data.clone())).await; // open a document let document = test.get_document(&doc_id).await.unwrap(); diff --git a/frontend/rust-lib/flowy-document2/tests/document/util.rs b/frontend/rust-lib/flowy-document2/tests/document/util.rs index 1fa4a5615d..08aaac660c 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/util.rs @@ -94,6 +94,7 @@ pub async fn create_and_open_empty_document() -> (DocumentTest, Arc Uuid { uuid::Uuid::new_v4() } + +pub fn gen_view_id() -> Uuid { + uuid::Uuid::new_v4() +} diff --git a/frontend/rust-lib/flowy-folder2/src/entities/view.rs b/frontend/rust-lib/flowy-folder2/src/entities/view.rs index de9947101d..f75f06980d 100644 --- a/frontend/rust-lib/flowy-folder2/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder2/src/entities/view.rs @@ -5,12 +5,12 @@ use std::sync::Arc; use collab_folder::core::{View, ViewLayout}; -use crate::entities::icon::ViewIconPB; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; +use flowy_folder_deps::cloud::gen_view_id; +use crate::entities::icon::ViewIconPB; use crate::entities::parser::view::{ViewDesc, ViewIdentify, ViewName, ViewThumbnail}; -use crate::view_operation::gen_view_id; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct ChildViewUpdatePB { @@ -226,7 +226,7 @@ impl TryInto for CreateViewPayloadPB { fn try_into(self) -> Result { let name = ViewName::parse(self.name)?.0; let parent_view_id = ViewIdentify::parse(self.parent_view_id)?.0; - let view_id = gen_view_id(); + let view_id = gen_view_id().to_string(); Ok(CreateViewParams { parent_view_id, diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index 243f200c02..fb71cb990a 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -16,7 +16,7 @@ use tokio_stream::StreamExt; use tracing::{event, Level}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_folder_deps::cloud::FolderCloudService; +use flowy_folder_deps::cloud::{gen_view_id, FolderCloudService}; use crate::entities::icon::UpdateViewIconParams; use crate::entities::{ @@ -31,9 +31,7 @@ use crate::notification::{ }; use crate::share::ImportParams; use crate::user_default::DefaultFolderBuilder; -use crate::view_operation::{ - create_view, gen_view_id, FolderOperationHandler, FolderOperationHandlers, -}; +use crate::view_operation::{create_view, FolderOperationHandler, FolderOperationHandlers}; /// [FolderUser] represents the user for folder. pub trait FolderUser: Send + Sync { @@ -129,10 +127,9 @@ impl FolderManager { } pub async fn get_workspace_views(&self, workspace_id: &str) -> FlowyResult> { - let views = self.with_folder( - || vec![], - |folder| get_workspace_view_pbs(workspace_id, folder), - ); + let views = self.with_folder(std::vec::Vec::new, |folder| { + get_workspace_view_pbs(workspace_id, folder) + }); Ok(views) } @@ -143,7 +140,7 @@ impl FolderManager { &self, uid: i64, workspace_id: &str, - initial_data: FolderInitializeData, + initial_data: FolderInitializeDataSource, ) -> FlowyResult<()> { *self.workspace_id.write() = Some(workspace_id.to_string()); let workspace_id = workspace_id.to_string(); @@ -156,25 +153,34 @@ impl FolderManager { }; let folder = match initial_data { - FolderInitializeData::Empty => { + FolderInitializeDataSource::LocalDisk { + create_if_not_exist, + } => { let is_exist = is_exist_in_local_disk(&self.user, &workspace_id).unwrap_or(false); - if !is_exist { + if is_exist { + let collab = self.collab_for_folder(uid, &workspace_id, collab_db, vec![])?; + Folder::open(collab, Some(folder_notifier)) + } else if create_if_not_exist { + let folder_data = + DefaultFolderBuilder::build(uid, workspace_id.to_string(), &self.operation_handlers) + .await; + let collab = self.collab_for_folder(uid, &workspace_id, collab_db, vec![])?; + Folder::create(collab, Some(folder_notifier), Some(folder_data)) + } else { return Err(FlowyError::new( ErrorCode::RecordNotFound, "Can't find any workspace data", )); } - let collab = self.collab_for_folder(uid, &workspace_id, collab_db, vec![])?; - Folder::open(collab, Some(folder_notifier)) }, - FolderInitializeData::Raw(raw_data) => { + FolderInitializeDataSource::Cloud(raw_data) => { if raw_data.is_empty() { return Err(workspace_data_not_sync_error(uid, &workspace_id)); } let collab = self.collab_for_folder(uid, &workspace_id, collab_db, raw_data)?; Folder::open(collab, Some(folder_notifier)) }, - FolderInitializeData::Data(folder_data) => { + FolderInitializeDataSource::FolderData(folder_data) => { let collab = self.collab_for_folder(uid, &workspace_id, collab_db, vec![])?; Folder::create(collab, Some(folder_notifier), Some(folder_data)) }, @@ -239,7 +245,7 @@ impl FolderManager { .initialize( user_id, workspace_id, - FolderInitializeData::Raw(folder_updates), + FolderInitializeDataSource::Cloud(folder_updates), ) .await?; Ok(()) @@ -252,27 +258,13 @@ impl FolderManager { user_id: i64, _token: &str, is_new: bool, - folder_data: Option, + data_source: FolderInitializeDataSource, workspace_id: &str, ) -> FlowyResult<()> { // Create the default workspace if the user is new tracing::info!("initialize_when_sign_up: is_new: {}", is_new); if is_new { - let folder_data = match folder_data { - None => { - DefaultFolderBuilder::build(user_id, workspace_id.to_string(), &self.operation_handlers) - .await - }, - Some(folder_data) => folder_data, - }; - - self - .initialize( - user_id, - workspace_id, - FolderInitializeData::Data(folder_data), - ) - .await?; + self.initialize(user_id, workspace_id, data_source).await?; } else { // The folder updates should not be empty, as the folder data is stored // when the user signs up for the first time. @@ -290,7 +282,7 @@ impl FolderManager { .initialize( user_id, workspace_id, - FolderInitializeData::Raw(folder_updates), + FolderInitializeDataSource::Cloud(folder_updates), ) .await?; } @@ -376,7 +368,9 @@ impl FolderManager { } pub async fn get_all_workspaces(&self) -> Vec { - self.with_folder(|| vec![], |folder| folder.workspaces.get_all_workspaces()) + self.with_folder(std::vec::Vec::new, |folder| { + folder.workspaces.get_all_workspaces() + }) } pub async fn create_view_with_params(&self, params: CreateViewParams) -> FlowyResult { @@ -628,10 +622,9 @@ impl FolderManager { /// Return a list of views that belong to the given parent view id. #[tracing::instrument(level = "debug", skip(self, parent_view_id), err)] pub async fn get_views_belong_to(&self, parent_view_id: &str) -> FlowyResult>> { - let views = self.with_folder( - || vec![], - |folder| folder.views.get_views_belong_to(parent_view_id), - ); + let views = self.with_folder(std::vec::Vec::new, |folder| { + folder.views.get_views_belong_to(parent_view_id) + }); Ok(views) } @@ -686,7 +679,7 @@ impl FolderManager { desc: view.desc.clone(), layout: view.layout.clone().into(), initial_data: view_data.to_vec(), - view_id: gen_view_id(), + view_id: gen_view_id().to_string(), meta: Default::default(), set_as_current: true, index, @@ -757,25 +750,22 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_all_favorites(&self) -> Vec { - self.with_folder( - || vec![], - |folder| { - let trash_ids = folder - .get_all_trash() - .into_iter() - .map(|trash| trash.id) - .collect::>(); + self.with_folder(std::vec::Vec::new, |folder| { + let trash_ids = folder + .get_all_trash() + .into_iter() + .map(|trash| trash.id) + .collect::>(); - let mut views = folder.get_all_favorites(); - views.retain(|view| !trash_ids.contains(&view.id)); - views - }, - ) + let mut views = folder.get_all_favorites(); + views.retain(|view| !trash_ids.contains(&view.id)); + views + }) } #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_all_trash(&self) -> Vec { - self.with_folder(|| vec![], |folder| folder.get_all_trash()) + self.with_folder(std::vec::Vec::new, |folder| folder.get_all_trash()) } #[tracing::instrument(level = "trace", skip(self))] @@ -804,7 +794,7 @@ impl FolderManager { /// Delete all the trash permanently. #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn delete_all_trash(&self) { - let deleted_trash = self.with_folder(|| vec![], |folder| folder.get_all_trash()); + let deleted_trash = self.with_folder(std::vec::Vec::new, |folder| folder.get_all_trash()); for trash in deleted_trash { let _ = self.delete_trash(&trash.id).await; } @@ -843,7 +833,7 @@ impl FolderManager { } let handler = self.get_handler(&import_data.view_layout)?; - let view_id = gen_view_id(); + let view_id = gen_view_id().to_string(); let uid = self.user.user_id()?; if let Some(data) = import_data.data { handler @@ -1238,13 +1228,13 @@ impl Deref for MutexFolder { unsafe impl Sync for MutexFolder {} unsafe impl Send for MutexFolder {} -pub enum FolderInitializeData { +pub enum FolderInitializeDataSource { /// It means using the data stored on local disk to initialize the folder - Empty, + LocalDisk { create_if_not_exist: bool }, /// If there is no data stored on local disk, we will use the data from the server to initialize the folder - Raw(CollabRawData), + Cloud(CollabRawData), /// If the user is new, we use the [DefaultFolderBuilder] to create the default folder. - Data(FolderData), + FolderData(FolderData), } fn is_exist_in_local_disk(user: &Arc, doc_id: &str) -> FlowyResult { diff --git a/frontend/rust-lib/flowy-folder2/src/test_helper.rs b/frontend/rust-lib/flowy-folder2/src/test_helper.rs index 18872cc7d9..7cfa2fd0cd 100644 --- a/frontend/rust-lib/flowy-folder2/src/test_helper.rs +++ b/frontend/rust-lib/flowy-folder2/src/test_helper.rs @@ -1,7 +1,9 @@ +use std::collections::HashMap; + +use flowy_folder_deps::cloud::gen_view_id; + use crate::entities::{CreateViewParams, ViewLayoutPB}; use crate::manager::FolderManager; -use crate::view_operation::gen_view_id; -use std::collections::HashMap; #[cfg(feature = "test_helper")] impl FolderManager { @@ -34,7 +36,7 @@ impl FolderManager { layout: ViewLayoutPB, ext: HashMap, ) -> String { - let view_id = gen_view_id(); + let view_id = gen_view_id().to_string(); let params = CreateViewParams { parent_view_id: app_id.to_string(), name: name.to_string(), diff --git a/frontend/rust-lib/flowy-folder2/src/view_operation.rs b/frontend/rust-lib/flowy-folder2/src/view_operation.rs index e303c934c8..be1d935cf3 100644 --- a/frontend/rust-lib/flowy-folder2/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder2/src/view_operation.rs @@ -8,6 +8,7 @@ use collab_folder::core::{RepeatedViewIdentifier, ViewIcon, ViewIdentifier, View use tokio::sync::RwLock; use flowy_error::FlowyError; +use flowy_folder_deps::cloud::gen_view_id; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; @@ -62,7 +63,7 @@ impl ViewBuilder { pub fn new(parent_view_id: String) -> Self { Self { parent_view_id, - view_id: gen_view_id(), + view_id: gen_view_id().to_string(), name: Default::default(), desc: Default::default(), layout: ViewLayout::Document, @@ -260,10 +261,6 @@ pub(crate) fn create_view(params: CreateViewParams, layout: ViewLayout) -> View } } -pub fn gen_view_id() -> String { - uuid::Uuid::new_v4().to_string() -} - #[cfg(test)] mod tests { use crate::view_operation::{FlattedViews, WorkspaceViewBuilder}; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index 3ca5c4d0c3..cb15d1a644 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -136,6 +136,6 @@ fn make_user_workspace() -> UserWorkspace { id: uuid::Uuid::new_v4().to_string(), name: "My Workspace".to_string(), created_at: Default::default(), - database_storage_id: uuid::Uuid::new_v4().to_string(), + database_views_aggregate_id: uuid::Uuid::new_v4().to_string(), } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs index 94b56ce0ca..bef726d874 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs @@ -141,9 +141,9 @@ where update: Vec, ) -> Result<(), Error> { if let Some(postgrest) = self.server.get_postgrest() { - let workspace_id = object - .get_workspace_id() - .ok_or(anyhow::anyhow!("Invalid workspace id"))?; + let workspace_id = object.get_workspace_id().ok_or(anyhow::anyhow!( + "Can't get the workspace id in CollabObject" + ))?; send_update(workspace_id, object, update, &postgrest, &self.secret()).await?; } @@ -198,7 +198,6 @@ pub(crate) async fn flush_collab_with_update( ) -> Result<(), Error> { // 2.Merge the updates into one and then delete the merged updates let merge_result = spawn_blocking(move || merge_updates(update_items, update)).await??; - tracing::trace!("Merged updates count: {}", merge_result.merged_keys.len()); let workspace_id = object .get_workspace_id() @@ -207,7 +206,12 @@ pub(crate) async fn flush_collab_with_update( let value_size = merge_result.new_update.len() as i32; let md5 = md5(&merge_result.new_update); - tracing::trace!("Flush collab id:{} type:{}", object.object_id, object.ty); + tracing::trace!( + "Flush collab id:{} type:{} is_encrypt: {}", + object.object_id, + object.ty, + secret.is_some() + ); let (new_update, encrypt) = SupabaseBinaryColumnEncoder::encode(merge_result.new_update, &secret)?; let params = InsertParamsBuilder::new() diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs index 3817cb0865..85a74d0109 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs @@ -26,6 +26,7 @@ impl DocumentCloudService for SupabaseDocumentServiceImpl where T: SupabaseServerService, { + #[tracing::instrument(level = "debug", skip(self))] fn get_document_updates(&self, document_id: &str) -> FutureResult>, Error> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let document_id = document_id.to_string(); @@ -70,6 +71,7 @@ where }) } + #[tracing::instrument(level = "debug", skip(self))] fn get_document_data(&self, document_id: &str) -> FutureResult, Error> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let document_id = document_id.to_string(); diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/request.rs b/frontend/rust-lib/flowy-server/src/supabase/api/request.rs index 690deb4722..998a8eeb6c 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/request.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/request.rs @@ -415,7 +415,7 @@ pub struct UpdateItem { pub value: Vec, } -pub struct RetryCondition(Weak); +pub struct RetryCondition(pub Weak); impl Condition for RetryCondition { fn should_retry(&mut self, _error: &anyhow::Error) -> bool { self.0.upgrade().is_some() diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index 04f2e692fa..ba3c9b0951 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -1,5 +1,9 @@ +use std::future::Future; +use std::iter::Take; +use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, Weak}; +use std::time::Duration; use anyhow::Error; use collab::core::collab::MutexCollab; @@ -8,6 +12,8 @@ use collab_plugins::cloud_storage::CollabObject; use parking_lot::RwLock; use serde_json::Value; use tokio::sync::oneshot::channel; +use tokio_retry::strategy::FixedInterval; +use tokio_retry::{Action, RetryIf}; use uuid::Uuid; use flowy_folder_deps::cloud::{Folder, Workspace}; @@ -18,13 +24,13 @@ use lib_infra::box_any::BoxAny; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; -use crate::supabase::api::request::{get_updates_from_server, FetchObjectUpdateAction}; +use crate::supabase::api::request::{ + get_updates_from_server, FetchObjectUpdateAction, RetryCondition, +}; use crate::supabase::api::util::{ ExtendedResponse, InsertParamsBuilder, RealtimeBinaryColumnDecoder, SupabaseBinaryColumnDecoder, }; -use crate::supabase::api::{ - flush_collab_with_update, send_update, PostgresWrapper, SupabaseServerService, -}; +use crate::supabase::api::{flush_collab_with_update, PostgresWrapper, SupabaseServerService}; use crate::supabase::define::*; use crate::supabase::entities::UserProfileResponse; use crate::supabase::entities::{GetUserProfileParams, RealtimeUserEvent}; @@ -273,7 +279,7 @@ where } fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), Error> { - let collab_object = collab_object.clone(); + let collab_object = collab_object; let try_get_postgrest = self.server.try_get_weak_postgrest(); let (tx, rx) = channel(); @@ -308,7 +314,7 @@ where fn create_collab_object( &self, collab_object: &CollabObject, - data: Vec, + update: Vec, ) -> FutureResult<(), Error> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let cloned_collab_object = collab_object.clone(); @@ -316,23 +322,9 @@ where tokio::spawn(async move { tx.send( async move { - let workspace_id = cloned_collab_object - .get_workspace_id() - .ok_or(anyhow::anyhow!("Invalid workspace id"))?; - - let postgrest = try_get_postgrest? - .upgrade() - .ok_or(anyhow::anyhow!("postgrest is not available"))?; - - let encryption_secret = postgrest.secret(); - send_update( - workspace_id, - &cloned_collab_object, - data, - &postgrest, - &encryption_secret, - ) - .await?; + CreateCollabAction::new(cloned_collab_object, try_get_postgrest?, update) + .run() + .await?; Ok(()) } .await, @@ -342,6 +334,61 @@ where } } +pub struct CreateCollabAction { + collab_object: CollabObject, + postgrest: Weak, + update: Vec, +} + +impl CreateCollabAction { + pub fn new( + collab_object: CollabObject, + postgrest: Weak, + update: Vec, + ) -> Self { + Self { + collab_object, + postgrest, + update, + } + } + + pub fn run(self) -> RetryIf, CreateCollabAction, RetryCondition> { + let postgrest = self.postgrest.clone(); + let retry_strategy = FixedInterval::new(Duration::from_secs(2)).take(3); + RetryIf::spawn(retry_strategy, self, RetryCondition(postgrest)) + } +} + +impl Action for CreateCollabAction { + type Future = Pin> + Send>>; + type Item = (); + type Error = anyhow::Error; + + fn run(&mut self) -> Self::Future { + let weak_postgres = self.postgrest.clone(); + let cloned_collab_object = self.collab_object.clone(); + let cloned_update = self.update.clone(); + Box::pin(async move { + match weak_postgres.upgrade() { + None => Ok(()), + Some(postgrest) => { + let secret = postgrest.secret(); + flush_collab_with_update( + &cloned_collab_object, + vec![], + &postgrest, + cloned_update, + secret, + ) + .await?; + Ok(()) + }, + } + }) + } +} + async fn get_user_profile( postgrest: Arc, params: GetUserProfileParams, diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs index 4f0effb715..83ca62a919 100644 --- a/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs @@ -20,7 +20,7 @@ async fn supabase_user_sign_up_test() { let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); assert!(!user.latest_workspace.id.is_empty()); assert!(!user.user_workspaces.is_empty()); - assert!(!user.latest_workspace.database_storage_id.is_empty()); + assert!(!user.latest_workspace.database_views_aggregate_id.is_empty()); } #[tokio::test] @@ -37,7 +37,7 @@ async fn supabase_user_sign_up_with_existing_uuid_test() { .unwrap(); let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); assert!(!user.latest_workspace.id.is_empty()); - assert!(!user.latest_workspace.database_storage_id.is_empty()); + assert!(!user.latest_workspace.database_views_aggregate_id.is_empty()); assert!(!user.user_workspaces.is_empty()); } diff --git a/frontend/rust-lib/flowy-test/src/document/document_event.rs b/frontend/rust-lib/flowy-test/src/document/document_event.rs index 8b6704303a..d00f5c0e47 100644 --- a/frontend/rust-lib/flowy-test/src/document/document_event.rs +++ b/frontend/rust-lib/flowy-test/src/document/document_event.rs @@ -1,11 +1,12 @@ -use crate::document::utils::{gen_id, gen_text_block_data}; -use crate::event_builder::EventBuilder; -use crate::FlowyCoreTest; use flowy_document2::entities::*; use flowy_document2::event_map::DocumentEvent; use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; use flowy_folder2::event_map::FolderEvent; +use crate::document::utils::{gen_id, gen_text_block_data}; +use crate::event_builder::EventBuilder; +use crate::FlowyCoreTest; + const TEXT_BLOCK_TY: &str = "paragraph"; pub struct DocumentEventTest { @@ -138,6 +139,7 @@ impl DocumentEventTest { } /// Insert a new text block at the index of parent's children. + /// return the new block id. pub async fn insert_index( &self, document_id: &str, diff --git a/frontend/rust-lib/flowy-test/src/lib.rs b/frontend/rust-lib/flowy-test/src/lib.rs index 708f9cd6a4..ab71ad8dbf 100644 --- a/frontend/rust-lib/flowy-test/src/lib.rs +++ b/frontend/rust-lib/flowy-test/src/lib.rs @@ -61,7 +61,7 @@ impl FlowyCoreTest { pub fn new_with_user_data_path(path: PathBuf, name: String) -> Self { let config = AppFlowyCoreConfig::new(path.to_str().unwrap(), name).log_filter( - "info", + "debug", vec!["flowy_test".to_string(), "lib_dispatch".to_string()], ); @@ -259,12 +259,12 @@ impl FlowyCoreTest { pub async fn create_document( &self, parent_id: &str, - name: &str, + name: String, initial_data: Vec, ) -> ViewPB { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), - name: name.to_string(), + name, desc: "".to_string(), thumbnail: None, layout: ViewLayoutPB::Document, diff --git a/frontend/rust-lib/flowy-test/tests/document/supabase_test/helper.rs b/frontend/rust-lib/flowy-test/tests/document/supabase_test/helper.rs index 8fcdd64e45..651670961c 100644 --- a/frontend/rust-lib/flowy-test/tests/document/supabase_test/helper.rs +++ b/frontend/rust-lib/flowy-test/tests/document/supabase_test/helper.rs @@ -33,7 +33,11 @@ impl FlowySupabaseDocumentTest { let current_workspace = self.inner.get_current_workspace().await; self .inner - .create_document(¤t_workspace.workspace.id, "my document", vec![]) + .create_document( + ¤t_workspace.workspace.id, + "my document".to_string(), + vec![], + ) .await } diff --git a/frontend/rust-lib/flowy-test/tests/user/migration_test/document_test.rs b/frontend/rust-lib/flowy-test/tests/user/migration_test/document_test.rs index b8093d80e6..190ec70cf0 100644 --- a/frontend/rust-lib/flowy-test/tests/user/migration_test/document_test.rs +++ b/frontend/rust-lib/flowy-test/tests/user/migration_test/document_test.rs @@ -2,11 +2,15 @@ use flowy_core::DEFAULT_NAME; use flowy_folder2::entities::ViewLayoutPB; use flowy_test::FlowyCoreTest; -use crate::user::migration_test::util::unzip_history_user_db; +use crate::util::unzip_history_user_db; #[tokio::test] async fn migrate_historical_empty_document_test() { - let (cleaner, user_db_path) = unzip_history_user_db("historical_empty_document").unwrap(); + let (cleaner, user_db_path) = unzip_history_user_db( + "./tests/user/migration_test/history_user_db", + "historical_empty_document", + ) + .unwrap(); let test = FlowyCoreTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()); let views = test.get_all_workspace_views().await; diff --git a/frontend/rust-lib/flowy-test/tests/user/migration_test/history_user_db/README.md b/frontend/rust-lib/flowy-test/tests/user/migration_test/history_user_db/README.md new file mode 100644 index 0000000000..426255b00d --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/user/migration_test/history_user_db/README.md @@ -0,0 +1,4 @@ + +## Don't modify the zip files in this folder + +The zip files in this folder are used for integration tests. If the tests fail, it means users upgrading to this version of AppFlowy will encounter issues \ No newline at end of file diff --git a/frontend/rust-lib/flowy-test/tests/user/migration_test/mod.rs b/frontend/rust-lib/flowy-test/tests/user/migration_test/mod.rs index f268e440f9..940f03e64f 100644 --- a/frontend/rust-lib/flowy-test/tests/user/migration_test/mod.rs +++ b/frontend/rust-lib/flowy-test/tests/user/migration_test/mod.rs @@ -1,3 +1,2 @@ mod document_test; -mod util; mod version_test; diff --git a/frontend/rust-lib/flowy-test/tests/user/migration_test/util.rs b/frontend/rust-lib/flowy-test/tests/user/migration_test/util.rs deleted file mode 100644 index 55c529ec31..0000000000 --- a/frontend/rust-lib/flowy-test/tests/user/migration_test/util.rs +++ /dev/null @@ -1,47 +0,0 @@ -use flowy_test::Cleaner; -use nanoid::nanoid; -use std::fs::{create_dir_all, File}; -use std::io::copy; -use std::path::{Path, PathBuf}; -use zip::ZipArchive; - -pub fn unzip_history_user_db(folder_name: &str) -> std::io::Result<(Cleaner, PathBuf)> { - // Open the zip file - let zip_file_path = format!( - "./tests/user/migration_test/history_user_db/{}.zip", - folder_name - ); - let reader = File::open(zip_file_path)?; - let output_folder_path = format!( - "./tests/user/migration_test/history_user_db/unit_test_{}", - nanoid!(6) - ); - - // Create a ZipArchive from the file - let mut archive = ZipArchive::new(reader)?; - - // Iterate through each file in the zip - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - let output_path = Path::new(&output_folder_path).join(file.mangled_name()); - - if file.name().ends_with('/') { - // Create directory - create_dir_all(&output_path)?; - } else { - // Write file - if let Some(p) = output_path.parent() { - if !p.exists() { - create_dir_all(p)?; - } - } - let mut outfile = File::create(&output_path)?; - copy(&mut file, &mut outfile)?; - } - } - let path = format!("{}/{}", output_folder_path, folder_name); - Ok(( - Cleaner::new(PathBuf::from(output_folder_path)), - PathBuf::from(path), - )) -} diff --git a/frontend/rust-lib/flowy-test/tests/user/migration_test/version_test.rs b/frontend/rust-lib/flowy-test/tests/user/migration_test/version_test.rs index aa339301df..2b327770d3 100644 --- a/frontend/rust-lib/flowy-test/tests/user/migration_test/version_test.rs +++ b/frontend/rust-lib/flowy-test/tests/user/migration_test/version_test.rs @@ -2,11 +2,15 @@ use flowy_core::DEFAULT_NAME; use flowy_folder2::entities::ViewLayoutPB; use flowy_test::FlowyCoreTest; -use crate::user::migration_test::util::unzip_history_user_db; +use crate::util::unzip_history_user_db; #[tokio::test] async fn migrate_020_historical_empty_document_test() { - let (cleaner, user_db_path) = unzip_history_user_db("020_historical_user_data").unwrap(); + let (cleaner, user_db_path) = unzip_history_user_db( + "./tests/user/migration_test/history_user_db", + "020_historical_user_data", + ) + .unwrap(); let test = FlowyCoreTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()); let mut views = test.get_all_workspace_views().await; diff --git a/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs b/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs index cb539883c7..4980cc3521 100644 --- a/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs +++ b/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs @@ -1,9 +1,16 @@ use std::collections::HashMap; +use assert_json_diff::assert_json_eq; +use collab_database::rows::database_row_document_id_from_row_id; +use collab_document::blocks::DocumentData; +use collab_folder::core::FolderData; use nanoid::nanoid; +use serde_json::json; +use flowy_core::DEFAULT_NAME; use flowy_encrypt::decrypt_text; -use flowy_server::supabase::define::{USER_EMAIL, USER_UUID}; +use flowy_server::supabase::define::{CollabType, USER_EMAIL, USER_UUID}; +use flowy_test::document::document_event::DocumentEventTest; use flowy_test::event_builder::EventBuilder; use flowy_test::FlowyCoreTest; use flowy_user::entities::{ @@ -259,3 +266,265 @@ async fn update_user_profile_with_existing_email_test() { assert_eq!(error.code, ErrorCode::Conflict); } } + +#[tokio::test] +async fn migrate_anon_document_on_cloud_signup() { + if get_supabase_config().is_some() { + let test = FlowyCoreTest::new(); + let user_profile = test.sign_up_as_guest().await.user_profile; + + let view = test + .create_view(&user_profile.workspace_id, "My first view".to_string()) + .await; + let document_event = DocumentEventTest::new_with_core(test.clone()); + let block_id = document_event + .insert_index(&view.id, "hello world", 1, None) + .await; + + let _ = test.supabase_party_sign_up().await; + + // After sign up, the documents should be migrated to the cloud + // So, we can get the document data from the cloud + let data: DocumentData = test + .document_manager + .get_cloud_service() + .get_document_data(&view.id) + .await + .unwrap() + .unwrap(); + let block = data.blocks.get(&block_id).unwrap(); + assert_json_eq!( + block.data, + json!({ + "delta": [ + { + "insert": "hello world" + } + ] + }) + ); + } +} + +#[tokio::test] +async fn migrate_anon_data_on_cloud_signup() { + if get_supabase_config().is_some() { + let (cleaner, user_db_path) = unzip_history_user_db( + "./tests/user/supabase_test/history_user_db", + "workspace_sync", + ) + .unwrap(); + let test = FlowyCoreTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()); + let user_profile = test.supabase_party_sign_up().await; + + // Get the folder data from remote + let folder_data: FolderData = test + .folder_manager + .get_cloud_service() + .get_folder_data(&user_profile.workspace_id) + .await + .unwrap() + .unwrap(); + + let expected_folder_data = expected_workspace_sync_folder_data(); + + if folder_data.workspaces.len() != expected_folder_data.workspaces.len() { + dbg!(&folder_data.workspaces); + } + + assert_eq!( + folder_data.workspaces.len(), + expected_folder_data.workspaces.len() + ); + assert_eq!(folder_data.views.len(), expected_folder_data.views.len()); + + // After migration, the ids of the folder_data should be different from the expected_folder_data + for i in 0..folder_data.views.len() { + let left_view = &folder_data.views[i]; + let right_view = &expected_folder_data.views[i]; + assert_ne!(left_view.id, right_view.id); + assert_ne!(left_view.parent_view_id, right_view.parent_view_id); + assert_eq!(left_view.name, right_view.name); + } + + assert_ne!( + folder_data.current_workspace_id, + expected_folder_data.current_workspace_id + ); + assert_ne!(folder_data.current_view, expected_folder_data.current_view); + + let database_views = folder_data + .views + .iter() + .filter(|view| view.layout.is_database()) + .collect::>(); + + // Try to load the database from the cloud. + for (i, database_view) in database_views.iter().enumerate() { + let cloud_service = test.database_manager.get_cloud_service(); + let database_id = test + .database_manager + .get_database_id_with_view_id(&database_view.id) + .await + .unwrap(); + let editor = test + .database_manager + .get_database(&database_id) + .await + .unwrap(); + + // The database view setting should be loaded by the view id + let _ = editor + .get_database_view_setting(&database_view.id) + .await + .unwrap(); + + let rows = editor.get_rows(&database_view.id).await.unwrap(); + assert_eq!(rows.len(), 3); + + if i == 0 { + let first_row = rows.first().unwrap().as_ref(); + let icon_url = first_row.meta.icon_url.clone().unwrap(); + assert_eq!(icon_url, "😄"); + + let document_id = database_row_document_id_from_row_id(&first_row.row.id); + let document_data: DocumentData = test + .document_manager + .get_cloud_service() + .get_document_data(&document_id) + .await + .unwrap() + .unwrap(); + + let editor = test + .document_manager + .get_document(&document_id) + .await + .unwrap(); + let expected_document_data = editor.lock().get_document_data().unwrap(); + + // let expected_document_data = test + // .document_manager + // .get_document_data(&document_id) + // .await + // .unwrap(); + assert_eq!(document_data, expected_document_data); + let json = json!(document_data); + assert_eq!( + json["blocks"]["LPMpo0Qaab"]["data"]["delta"][0]["insert"], + json!("Row document") + ); + } + + assert!(cloud_service + .get_collab_update(&database_id, CollabType::Database) + .await + .is_ok()); + } + + drop(cleaner); + } +} + +fn expected_workspace_sync_folder_data() -> FolderData { + serde_json::from_value::(json!({ + "current_view": "e0811131-9928-4541-a174-20b7553d9e4c", + "current_workspace_id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3", + "views": [ + { + "children": { + "items": [ + { + "id": "e0811131-9928-4541-a174-20b7553d9e4c" + }, + { + "id": "53333949-c262-447b-8597-107589697059" + } + ] + }, + "created_at": 1693147093, + "desc": "", + "icon": null, + "id": "e203afb3-de5d-458a-8380-33cd788a756e", + "is_favorite": false, + "layout": 0, + "name": "⭐️ Getting started", + "parent_view_id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3" + }, + { + "children": { + "items": [ + { + "id": "11c697ba-5ed1-41c0-adfc-576db28ad27b" + }, + { + "id": "4a5c25e2-a734-440c-973b-4c0e7ab0039c" + } + ] + }, + "created_at": 1693147096, + "desc": "", + "icon": null, + "id": "e0811131-9928-4541-a174-20b7553d9e4c", + "is_favorite": false, + "layout": 1, + "name": "database", + "parent_view_id": "e203afb3-de5d-458a-8380-33cd788a756e" + }, + { + "children": { + "items": [] + }, + "created_at": 1693147124, + "desc": "", + "icon": null, + "id": "11c697ba-5ed1-41c0-adfc-576db28ad27b", + "is_favorite": false, + "layout": 3, + "name": "calendar", + "parent_view_id": "e0811131-9928-4541-a174-20b7553d9e4c" + }, + { + "children": { + "items": [] + }, + "created_at": 1693147125, + "desc": "", + "icon": null, + "id": "4a5c25e2-a734-440c-973b-4c0e7ab0039c", + "is_favorite": false, + "layout": 2, + "name": "board", + "parent_view_id": "e0811131-9928-4541-a174-20b7553d9e4c" + }, + { + "children": { + "items": [] + }, + "created_at": 1693147133, + "desc": "", + "icon": null, + "id": "53333949-c262-447b-8597-107589697059", + "is_favorite": false, + "layout": 0, + "name": "document", + "parent_view_id": "e203afb3-de5d-458a-8380-33cd788a756e" + } + ], + "workspaces": [ + { + "child_views": { + "items": [ + { + "id": "e203afb3-de5d-458a-8380-33cd788a756e" + } + ] + }, + "created_at": 1693147093, + "id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3", + "name": "Workspace" + } + ] + })) + .unwrap() +} diff --git a/frontend/rust-lib/flowy-test/tests/user/supabase_test/history_user_db/README.md b/frontend/rust-lib/flowy-test/tests/user/supabase_test/history_user_db/README.md new file mode 100644 index 0000000000..426255b00d --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/user/supabase_test/history_user_db/README.md @@ -0,0 +1,4 @@ + +## Don't modify the zip files in this folder + +The zip files in this folder are used for integration tests. If the tests fail, it means users upgrading to this version of AppFlowy will encounter issues \ No newline at end of file diff --git a/frontend/rust-lib/flowy-test/tests/user/supabase_test/history_user_db/workspace_sync.zip b/frontend/rust-lib/flowy-test/tests/user/supabase_test/history_user_db/workspace_sync.zip new file mode 100644 index 0000000000000000000000000000000000000000..6fd5ca087169d72d4edb9ec3edc8f6da927077c9 GIT binary patch literal 44001 zcmeFY1yE&8vMx%~xI2xzyE`=Q?(XjH?%KFZTFUzxVyuHf6mPOa_-!7--{P9 zH)2-owQI41m09^!RaREkS2E(Cfsg<`uKqd<<=-Cu>kAeD7r@!t&fMNcSKm{?XAu_)S0|Nm2gRs?ifoSi-w6!I9`K9Gm zXsEUSDEALyDgG)}Uk6#=SVvFiTv9^m99Ahr#s@I^%V!cej87M4xF8p2^l-qK{Cs}j zC8DFSzLtd*1b~x|jsCz*P0uP#Qjv*{D^Zq-*G^1LN={0LkhA|1k}vZMQXDZH;Yy7D zN)GLduT%wuYs}j7u6=fguWw*X{U;<94iu!OonJ}HY777XGUA|M+JoL%-tTsN*zz|k zKTw+c6J+>*g^Zqoo{o-@k(P~)iJ6X`mWhS=Zz$fpqxfO!NBO@*@%QpSG*kW_#Xn;B z4(eZ3{RZscHT~ZNZ1Fo z(?4kW4bHzS`M(9ukI{JWJ2=U)0RgD~3Ma3PjgW=4vnv(9g{h&H1GR;zPN8@!)@@eFa`Pg!-evVyKU0Keoo*-&ZY3tx>IBY4+QY$Bl}q z;_FqV_bbh`7B!7=-<5bjZGBXA^5M4;SR+F;VwahCqN+@_5zn2~RWSzTvrHyUCX{c} zH?1Aq%)SqoFt0xW9|1jC>2|#VhIf#l8$9VU%BjQ|)-?ejExdqr$F9Mv9Q3=O=7;lj z)j+)}er8dW+@5s&d@g%_uEe1R)_!}}{Q`TH%^`OhwCZce(bRl=(>m0Ddn4G%p`&7C z@@d+wNP&VO#2|kbsU)G8+rN_h_Wl@s>c+}YYRz=LE-;_dH;{dLgeRGTC))c}8!7sf zPZB6cZPG}mc}l^sV%+FaEbR-VqoS!TE8uct6}v zTxu8~z007yU!fj)mwdYSgW;ciliOOC!*7d9HJpXDtw6HkH^JH=k!5A2sB=MaA4DQQ z2gioZ57x4<6^WnGhal@%dXkGR!F{{332YrO{ zikk+iClkkqRPfc$zc3hLCYDUVk}hnc{H&P3bkfO!Eyc1Sjj9u+qhZ?G2Hqj1qJ6R271M`SA1_U!w!;02YB6hV^|$>6 zFfLl}XT&ST*c~G*5IsO;RCB&K)HJ@MjvDD+&zH(>w!XNV5+a89L(e9-aOCo*@Dhh3FJb^ra4lbgXybrK{dMWpT(zvz&26!Ksm zSyvLL^=%0!-VeV+;d2l|ME{yugfM>jHKRsOGeH%1h<-U5NHLkwAZFW@5q;~^BexqD z9a9l|2_R(%HAx8NyiVX_NRpb~=ZdpeTz-gvM{T?{8>v2>e6n?i6W4s7eqnj{03JuO zVsOL?Y7uhvO36dGTLpuDHT$0sOi|W=<;0(4_!q%;jV}@nN|T4K;u>U>9=G{aY1AV+ zaJED@162%lPXAt6IFNou2DQ^lvb*y>3OvP_FX*dhl7TNCb2~*A6#vR< z*hf03d_e1KVjVRO-OG`CO&Fl(UEkZlrS!eZge;SGI#&d@| zs9KZqKfdbhji%Iw)NT}dm_>K+gHAE86G_^-*XhC79Wjwa;*TeKX)bli!=Mcw?I6Vx zgg>B8f_42F$qM-EXc6yBc3eP5T zR09<%xeuz+>?q)ObP?yEEO{2VX=cjiA_YsqLT#Z@L{93Ma4)ikPvvb0S1xqSj)6UL_vvArew5$JzOZI z;Kz~Fj@jo}9AXc72Yy*Wvb1Dn{L=3)nT}v?qRk=75U4xatw;sGtm@E#(m%arp`W~` z%E3H~8OUpQ`xYAB`o(e3h{e3Cz5d0>9ocSh3iVrX5v!3WW?InaA|W_Y6DS+LT5%nx zSaAxman`l=rh^qt-V}V=(=`}cp_~oj!Lo59G3WCTr_b;z0mLP-IrFy6oiQD><1IHm z5%Z4{hBtv3g+Ci?^H5!_RpMOlu~HY{kD#T>1~m{ZVIp0vR)E?D#3Ut+hyVnfD8u9% z*nqbLfc5y}fjtn+IV6ZEimInX_p`y)!TqeI&BD_fi}_=RFPR;uUYWo*pE{#{m4JO> z0jfKomL#em_5)c?2h&%YB8;&tk&gn7+jZfD@6Ra-9M9*FFp}9GH^h34$t{cpa=JGl zd~zgI&r^$xV}+>^Ob0|8;2e)4VfrD4sAZUTnXJ$BLwY7Ck7?$D)q#FRm)<|sg#OtP z_34<<7mFKh&`a_M3U+>_FS!pHkKs-zl%sTfn#^_&-7?Vk?Ac~I&^Kot`umaZ*bHbM zMKJ_ESh~LuUeL?!uYv6hqwEEj``Xxx`5OzWp=pCaqyrNXd~7q2o7qWqW5g|TRjlbun20E*qf|(kxjO#tF$mp&ydp%ou^IgG$fNVe5UC` zj|>x&QbxQM(I%5WAEk97HkBX@*C-^>cIqdt8E|w{BjuB#E1+m<*t~(leg2_(>>(uW-E_FqVN0+8OrZ$!9{9I z9O!n8uD=+s|DyisJ^_7!y1%J=aIefX}5<4+}@yO{&*p?~y_9DR6;7 zx-~)~h+)aGm*4@e6#lNIIj@`uMnT~K_yvK|)ByN3suR34j_pz}u-Ee10@zND^CrFz z;`es0({4Zqu%M%!<4q*zCZ%C4NWTWfQA^5{FGqpE7Xxc(L__uXbc1sA{Iy~S*=-bY zoASG;;&R!fZuY-=x_QtYdS^q@Ln*u`91jO|tqZ?%nRQyjftB^uqF|-O?>^YEwz9T)cjIc??K|f(jK(bl; z+qS6E7lh{rGZHuWT#pb9rgrF|VJ+_&ES(ZF0ctm=iBR%K0rhtObzZKWXv|?oBf@CF z1kwHu&o=Xx1~p$**tHshJ{5u%TQ7s>m%seY@szH{to_ov>HGD#G;`jS;sx+%?Q6PVYG#G-^?TDrKZfQ>#vcN?yTBxeYC;l@bK% zhs+cax1`Ds>3|(l28=JEQzOxh$kWM8T;zJl%!fzS5U4IA;O2&q2UZzv$5%`A;^w-o zI+xMNSQa2m@iCTM;Pnr)()fDY5kmiX({KdX{VP3};Q$`3QSqmne{sdA!X9dt z0C0lQgWSNyL{i#;0hE0Keid0d40N^Y8{zX(DU))CpY>`X=gK$dGBW%7>B8DHI+^%^CD9 z)AP+=TYnyW7txT^K3CK5*a@Fj7CLpU>^7SpITC; zHlxZ!lr(NZ;TiP2a2UUKVS3sUVCS3Pa?s`^dLGL5h_8Qt8U%b{oZG5b?qJRTmC4Yo zk`Q)!S#qMyfY3Nhfq;3E=XtZE6JNh`kVj>jugSOK?B0=69zFir$K=XbE!}=}M9}!$ z1!Rdk4%8_lFNrZQ9#bzMxzL*k(Ze4TJtzzP3m6E|J=md5%9($BoZ~@ora0g5y>G-_ zyym|dcf6I)pjo2|Zbdj}8pj@&iwx^3ipSqjxHGyQ_QT808TF~vZ0~X&DOAK|PW80k z$7K%bDw;kLNI#nt_PEw09z(U{l$o)UCekZGb0ow+H51=kNn{IyXyLH6I#CbRL!qZU6&BDVI@*P4H z0Swvh-f{^A8#P!kfMS|yhw!T@h7-3F&FQ12x>ka$`#m^=IYLG~!iZY^WvlKLJpPQj zN-8)pN*&pNs_(4z?GNhLeP*jPpqmYaC-dS1fh6~wZWhB3LICSrP1Hac9VdN~o&C+@}c)AKK@HCEvRPovNmgRB_LtFYX=OGzzHMnBqjGa#&py@&*6Jmz*+{C#CC zxtbS8^j2K}5%;x$@S%olTOD7I2~@4ms2ZQckCkg)3B_S77!dR3iHxK3sv^;#l(^YD z^<*}F?o;$>(4f4gEp~Sl^SvtgseYqm(FIM_ElG79*>w^^wZaM4U8zAxRS+wgX&_j4 zT(SmNc;@41#gs4c7{J>Rz$|7{T1HhhL~PqcjUeF+Uo`%XkJ7&>ZpnVtJK_-rUR;}b zH_^KT4k@sBDNjh2cWtlIy5bs=^pf2`YB(uQqwPp;?-J#duYpWth5u{hURB{Ge0bCz zX;~vy9gk>&WV8w2UUN%yw-mYq6X1cP;3YV1wh-RMP%i2xhY2an^{7cO^7;l)6Eey)WB)`?C-F^4F z1n^|uRp4DuVD8l~WbUbF6ohP)Pr(f5dS$W_v$ny@SQhT*Tb_bQGcBYNg=M(kHmmfJ ztG-%6$b<;Mf1R8p_P~m2;^#@c*14&o-8}TmtGQXq8xy-B>yh`DBb#CE0Ix&uX=@m* z&j39^6n5KKc`GR9;L9@`PSCHNY#HEwv#zClJVL&Ek`qM1xd7m5_R$CGmd}O=RzJ}9 zEKsMR>Z6~hMev$aJSy%z2UIqkm`BnNEdvbj&Dq4*8%@KxLmr5K@AhpPzC>>3{p0f8 z>7OAz&$YY*AYWMUuPN@3#k2Iew@=0!z)m~}Exc~AQVj-_uNNoiv(bXJAl*u{tYt;Y zE93Kv$9=)8v-}nzf@WZi8O{bThYR^zE#ohs7V7&2)mo!Z>M0lLnT#gn>BlbXd#{i4 zOF$AHgSmO>RA%RFVs$NVlKrfMiq%s~aWc8i7vv2w#Wq3|=O>CFe5_rY$u8wGQ1i$c z-UDX7Pj!8ol4>0r6LEUkpMHgsUr@r%kkze}i~!0eE(Q@6{mjD)#~eSq^Zc>kOScMd zF}!yHyn|xtAj{Q(f!FxuE>i#Vpo+Gk&PJq$o8On^+H!T>D^?#1B!m4pZu!j(+Vr_Z zW@~sqE@VIdNtATMkdpk-)uEzu@E@bOEY=yL66}|ogeZO&Z0wt>Z2R?B)$`6smixq7 zHihU{qefp3Fq>E8n_Ws@-Kt={oo5)8i?MoKdyZW^2?aNVS9ampd|yYc2{2kb4XXSBq*DAgx}hPacGq8L>U%7;#Qv zu+=3ZKx+ppRF?2!ANlUiwhzNYp49$Y#PI`(EJTCRH_ZV1IVwNJcQjB$gqs^1GFs}uwaDLNt&qIG#adyi7?WsO_XQjX={>>}Y$*v5K6|hP^1lBFtw5aLL#rtA3Q~AT zyTO%)(Qg`_&ii8Lv?Yuc99K=$)fN*?6^!C^nXr)9e!O8k4j~vha6i4NvMqOIb-ZjX zPx1W~(a}$*Y?6Xn8CUW*siB=s(XS?KGl2tJ7gWBXSQ{y|r8L=?%7Zd7`xHM?G_U0f z>)wcss(F^#ZKDs{-uTf7%024d(|zfaGl zjyqm0M3YlNl@^ya)lIYN&R`m;^%I>7PxzrOd_jyXh1zIwkg$9*m&uK&WTojC&(g2P zq;Lo>!-*XyNr+?wmpir3?ciCl&m3#?J=*%3^TZlLnBHOLeaLP?nvr%ixl!=Vu+4Es^Fc2lBGn69$?g)JcMAms{L2B*iqpn~^2zUZt?r2iEEI24SSRe&6L| zkGR`Sh?H(D$4r{L%}ekNtFfhv@gR4Yq1zZw5m9{JusHYujcIE47q6y*&;XKAi>fO{ zQ~ojcY70Ta-9#J$p2n!|es~ypbzX2Ef`!W+EyiI^U*zt^nx88)>C(|Ki{q|u_JrF_ zdmLl#fyC3-rDCH9cX-r?OG%~Z;FO1-fE z?xC{EKIQS|>hfrYzf%ZS)5EV9F=PJ9OL=1au|sjWk?&synAMZh#0gi8*`LouZXUfAn0bPvdh<-$;KF* zEQYhT{z+!!CT8D#57dW?O`hKqlwWGmzVQ4bMVS%o4V+CppixUaL?s7-tu_K!pNbxb zVw5bVB(q8{36!L&3c$qRIi^!SeW0iw>LGk?> zjqJ+ptt+&r);emER&a(h20YZ<8AzHIj0PefEeKe`o~1G&ILq@Wt-DE6>wjf69S@t^ zy#(}){Us2haDqjbUc~8aZ7~8@z;&{quNr3&B;XWZy>e;RNNhSN-%eB>GK9PU@~?|v z9|6;Q?$gX`rtth)ru5ZJ2Q4@&5)ymtP8+7&nCZf;)RuFE9HzCJCV!Y)6cMEq`k|W* zUgRcerR(xrHWS7bWiwL`<(m}c<85%$qdA}(m$N?vaF|l4S|Gt9V3lV#_t|9^VG<;( z7GPI_+O0yje%^7iv0prL?vAT>=U_8$Y9SsE3({LNh;!2gF42h=xk?~ThN1>0&Yv%4IYmada+ zo5?4N0M|nY+v}CgH>HEQEYKUW`y0J$%dpc#EgFdvS#8zS;$l@@8`H+SldY=f%VBGp zmCDCET`g9P+EKE z;}Y=xo}Dk(FV^dnn+G*q(!tXw(nvd5kttwj!Pnwhk|{PmI7ZW4MA)w^*@{nkky)9kUhOdx88xFl54% z`fPca7hxySa$QvN!2$ug)i7jCNVPU1WcZ+1Q>!Py?8Iv*KIb(SbY}LH{6*AXZ++>n?`VKu5d=gQodY+7LmG@LK^`W=LA7}h3dLX5mD;zu=AMzkks$*$ z#2vO|mB*4tQE3`%mo~_5n>&Sf)0)R&FLc@&V^HH^|Jn~T@0#odXO^Xyp|Ni#B7JUt zoGq0R@Wx8}F>e&Gw0*R@Hd7nlupE_BvS6(@w~I|jbmN&=DDQ-R&cHzD)OcytRLJUj zTmHmG-y;@0GtuSYTeF*vLz9v%s*^Vz|4}D=*!?L*TyeLW52qwrVI>^(?oyCG=BI}W zsvWYk{}4yQ9l=h9fKIM{>UB>_*Py-K8$WO+ga^H^v}|6MEq%r;grAtl1%h@5*ejT+ zxIBE-Bt$5)TudQ;mTwGAxLPu6`*(V6yA&F29eaZuV$Bq(Sns@HW%6s=KyJfgMO!_L z6UeABugr|%@qLlQd&|;a!g7cW_C~%V)s3-vlcQ;TL~?%OLQ@6!?b};)ML`InHWCgR zTbFEuYOFB58%!CemWJ@CfW)1!=K#3MaVt+<6HKaODYPRX znTdZ=#LGDP1f1?usJfZ-HrtSbis;s#*Q%VoRTPq$F*+zUhNp1WU@QY{vJ{Nely&!o zb+CeXY-)E0x*RuyB5+HXAf~^3LS@?iwvo@+8caLab!fBVW!K+{L>}spGdlu zG1-lt!9KZ-o>^m->&9pD2adaRl?>yvrX2`b3I6@0oB-L>;Voqv8cMz>?VpX7_<)J$ zLL8S~PHzO=zdP_aSLRBnzI^St_@26V$X|6GCy{6z0qD_X5RNga>*ld7L;i^@tG{0t zja&Uy!4=&ed3YqCG{M`RqC&3sa;4FKP0{_l6tPzbh|KP~_)Y+Z6Dn~%us={i2J+47 z=gSXU_*7GLquw=Z7KxjsD(Xn4rDEqb2FPx`f_+guQBP+AXj(1TAWbjalIH;H2vyUe zVT*EE)X9_zNu?cgJ;`{sa$?3a3NMehmv2#YX}%YquljlDw24%$=3GUsY%poUB8~W? z)$+Ev>yACSLS!Hr-jnxeg?d0;hDs=3ff2Lnl327K$Unag$|b? zHN9c4>4i~<_?)3CvtBi``4k!J2Z;&`HjFX;OTIH}T>l0ojB$xB+cP_i zaScnlD6ziv8+*TfEciw49wDkNt9-+}0AbR28LV*_w<8^LytqB9{6PJ6@S*chnu2~{ zeY~$Z=2ky>;Sj&r@w{l+h5QP7#MQ*p2Swl|BtUh=MvkF1)*|;zM+K`HCcHPM+(uIPGxQnRigGw6NR&iCLK=>_fz=Ec!Qk%Sx{H}$X%UR}m ztGT(8taNW(Z;9J?nBuBjC8t!Y$3|sw|0iJd?yAg`l%n}8ZYQfJC6~QXN9~zjt*hL6 zETV+nY~JsNCWZROpchq98LO3;^VM;b#R7(0II7aZGcZ{9{*y=uT&!i1K*D7*x`jWZ z&IF`PGHTD1f^0=719PUd+Ujt)&GCud*hHQ#P}os5!LU08d)r-XX|im`g^{Ww?+<$D zXWEif)`pGOL<6Xoe(dGZE%fr{vCH_E;GS;%{E9BddHP}Mkocl_79;)QWzf(wy(8mtgDjZO_XaeA(2mpe#VpZtF=J?8uLUrHG)flk z@ett=bytY;9Zq34Jn4af{}DUzgLT#SJJuBdEdT)OUt!eO)i*JuHqiUc#@To9{(k=n#FOMzt8D=! zpA*8J!kjNw`w&G{9>8)d_dGnx2FnV3c@CzvQ`BJ4^6u#oX6_MOQY>nA)sP}>JsqId zKd35wT|QB>Ii+cSN(kEk#2X`#q_-bSXD}X15lCfwau_0-)|bswIIOq~$|)HNZmA$0 zLaA%G1VmQbgWXP z!Ltr^(89P2i=6R3=R!nJg4y-;xf{raDdRX$hL<=*le) z(cVC&^C%9+iRuSact$d*Sp+D;w8 zFn+jTzkLRHb`DxI#?F^nOf&mKn>1o#6NpgJz)hQUzD1T80B^?7*=h7QaE+rcdFgN@ zuEVwn^(1l{L@rgGvHPC1e7>@=Szvkb_tl3vrt^U68mUevxEo+bxZj2;kUoW>lfH+>}RE4qqH3z~_@wux&)wG46;QJ|y$m4@<50wun}++5;z>f8^9 zM}}pLX8)LzQ&6~y={KkXA*2eWI)l9UobOjhE}kTHk5Q;8o;=6hVb$iDwYnzk9fq)E zS6pJA`t4qihOOIPci54Ge)G4SdEmt?vRXR@j6zxKvH3!-SPyjmJ{y2dxONHtKMv7} zvWx?a1uo+gVhft-YW%a!-963s{T|u((}#dr_g5=XPc-SVLoKkC+sSxnOsWsDLL}re zXNP3+p3>Yp_IpH$h$!5GqoekewmGGB#1Y6VvJ_pZAzZ^1fp^pfHmp#4<Jta zHWy3cDS=I)RDCbFV)i&Gc%#E47=^!Sl#WGR>qq;N!^#s6FfLy?l;o{JhBiNu$=NZVym& zH}P5jXa4{9Hou$v8}0Z{rv8;{OrvjYVWF$1ZJ_tJDdF#)?4$gXH~U|s%lt3U{clvh z1O9iVfAFCH1~cyec$WA*#~a`W006^ZXNmvDa--k4x(2!qzj2`dK7-u6WBu}ef0Td1 z{3Ju&0&^D4=gwum^%lg~AF>(_&ay$)G8k&akp^CbHOdysi5jC-2TpBlB)vNLxOXw* zrcab06|Ag5qz)PE=GMnYtjN;@XQ_}|xx}SB+5`d_RwSmV2T{b+%zf9+=$5{95de?x zIm=`5>g}!h?JDb{?D=ewVYonWN~m0`OGz#4I2;$rl`$hq03Y%CDN5w4pI86V5UHI$ zF+ObOW0%0C7-32iN6j5haU+8BnWy`>J7V)i)(IEK?ctj4(nmk^sSGYn$_Xv@2b~nR zM^PHiTg1JQn=f7Pu+_D4#A|1Fws6J68klAntSc^5M$J@{%;FU6oKH-eE({l^uPjy)i+h^F@6hWs{KPE}b@SXkVlSej*Sp=98)prAppWPvh+fWyD6 zY>7<{%it^d!tuKFr3gAp`Dko?t>Bb6i83k*YFN9|a|~m#*-Zx3aNf3)MK{KXfl;Lf zrnJQ9cy4KuAJcqwpg6L9Zd3-yp+M>EO&@?_jkEVp=o9E#=Q87c%e2Hyb#<#t2QY6Ne3W%X( zg1pMwV8mw}0>zaUWzH>{DB1&b#Z{sdSn6td!_(BWhd}PrM{*68uJo2P&0!dMGgW4e zZMOeHR*)vC2EVHZ)e zCvxn{@4KVq&Qd;d@>wLQv$eBaPKl-opcNc~B+w@y#A_j-J@MbWAxu@8YM3UBPqd1c z3%*fhvCo;)+3UKwF|$31aB@5DenO%vWpAEf)h_VnPuQQENw+IDhZpBd!mYkziAlmL z) zH*OGZ=he>dFg~_NG_}3HRVzLaJ-ns^wZzgt8z|cY|UVt~+umhJ= zmG@VIhErgOYx=B%>IP65Mtb^mEoNekDm6`bOVLg(0u2}^{0*N{B5PSVCrN2)*G|3P zSaA@ZL9NPsZ2)IjV?%^pdHvOQ)#3Cqs{6OFr0VrI;bLk0Ysi1<_Oa%477n~vGlqJv zSG5WieRU=yBF8l_0Kkv(%|@D@zWKS+tmKe(ceNq&Al-8a5)9yV1Gi;~)u#2fv7?lZ%2V(^lmjs0@j#UNj{ch`l)<*k6lZw<>$z%n2Bv z_a0Mn>r;M4P7Q4egO?XB=kKoLIGGK>ASOG4?itEmCN`}{rs(+zJ?BMf#*PKnk{E{^ zYU5fq1#|7!r(3qm8ajTFQ}L@Rlfs26qGz0vhnmv5mUwFnjteiY_xPH<2$@6Fn@*K} z;AbH9QmMG|hG#)K>2{D}$&5oBjP}_q$*7QNsg;tAC#dGRdxMjPI0iAMWlBr^!Lat7 z`T{dXfmx!Y+JPTS(mgf@tbFkG8F{qjn##Dei@oUVNqRN7O!h@O2bP4;XFN_Jd6)#`;DkyvHG z*%ZYl$@=txZ&`_JAcaYogr=PA0R>yXE-w+_4XPiJY*$MNbk|0WT4GCiJe7=bLJ?k{ zYwWgS=Bu7EU-ewhbbHjP=iN8&ha=Wjl$CsA;AL}FUzjqv?9aLCr*k$mvAQh=Oj~I^ zD+_fKJHaV=`k@}W_YKWMUdzlU2f|Ep)GQMmQe>M)m9CG~_h`d}d*k?(?FK5w^B(#3 ziX5ih&nH-hV&meuM|oWWo*>SnSffoe_X{|YXNyv<+6yM!w+VQ;q4fDIsSAf5M|8wT z4C&uiH}gEYZggx_qHTwsx`XUjm4l#y+4{Y#Sw-~yxb>T(FdZ3v`(z>770@nTH0duF zIp++cLOe3#4E=*dMZfbnEDjht^3(llGuWUUg8(cvgV;5YoM`4#+F|npmK+}@tSmMA z*7AFB;CZ!t^YMvRvj+X-^;E3o zc@;6}z23)XXBC<6bv`_I7M}Wj6|i1U!yX@7b77ueK6^8EbaL#-c)$Hn{_UNJvx>5D z8*ZCm#QUeJ#orHO&FTOIfG_GT@-kNUg+Z;UCF^{~XpR>vU5_{#XU zyPkyqblmv3E-w%{9$GWDgG42K0v#$Z%DXKPhzgNRO5YduF68WwLe^=I@7{u4wRAe) za2B0gg@5Oaev|{fzJmTkp^r6aIe!wp)!dfQ?mojf_TawJ=TbwtDYG3lF8i4uJ0Q)N z2Bk)-UotK~(WuuG7|vy$T;E)AxNbwo=+XYtr4yK%Cxx1Ktt(b|b1t!r@iRD>hFL=8GUnF_R?AS3*(@7alRcy|3rU7mB zpigjDo2BZq#yUv7I))Ic)*{BO8?}e!uzpub2HSGxP)Vk(8_Tu4VmYBt!xXP2a-X;+ z;#jNeB+E1%uU0AcMy<3oQaERspkYUbO}Sm8)Pfn-T`^cxdZqA?Hl?k#v|}5}!(07O zR@jEeth_EbTpUe)9CLJ`S)4(bzTs&y_N%sA7lD0?>NYmnM# zwAV3VAuo@fJCD(g#_nZIr3vTNtIO*SL{#ov8mYn8poJO#sJw=##FL$4%-MOS$KdBl z?^qiw+VP5?i24=Z?!5YbgjOBDk))q9&9S!7dFuumXW#WSDU}`;ATqz8Tg``i&1bdv z^|w+bK1Z!yB3hob#u|1)T=ec@<3~$bIt);r{(U87CLRiF{@8?R3mQ?SYraijm zoV}1LHi}mZNQbO8wV0wcJQABV63p)!dczF4al&zR3*_nSg^9fZ??lNkm&sl5{1Q=B ze9Z=xiQ)34P8FIJ3b2$n_>+38-kq+@F*{UPMldhkHSI#vVE6Ygc3;wSgL$3nupVvh z<$`%lD!VaZP92*OTn@isc6AS=)gBQ}AKN&ETX>H(uDd$|`;)Y$d2rVa?zN;^1Tlp^ ztItSn&xusNvq;qy+(PLWzf&~K?xD=i2e0NdEb$+LAv&$*Xq~HrH49n>uI9YL0o!gs z)bOK{q1C$qgU)L+%BUOV3uDc|3`=lNslM}W!=3ebd%pDn6BQn|mDm8%T%=P%0!xh| z6OKiTD)r{zA-lX0I6K8)L4;gR#fwdaMkH{Sb&k$MJdTKMEn<*3&6!|*U=M>vBkiVk zZc%U7y_(-|SQYRdYuh40~UgU}JU2gD%h6OxTBzNe3(<|)9Qh#UA`v`_TXK`=$QxS7IMa zeeVC$#pu6Yl>HklO8=Gy2rK?`8W`s}LGbr!V2JnZB>=$hPs+ak3BdQ)mFXXj$I|H9 z*!%{8_8)$JKTT??uj^oHZS{Vr^xeaKoFV@>M*8>rPYczL_W98E_v@d@g5THYqkSa* zMf?1r2JdI1|Ie%TZ_`}=`>g%{U(ID=>SXwD+FXCB+W&RU_50nBkG}qK4*gF_wtsAa z(EQ_x+RWbC>bExe*lPLv{XZT;zgPM(gy{d(5cnH* z8q&L4`{TCEGdchO`(J_oZxU|*NHo$&%P5FSOUYBweq4V$8UK;v{Ai2+Cf7NuvTV1_ zjNo~vTtr`qAslNcs)dZRmD0qlXfGOROj4XGmW_)LUxg1&4Ys5t_i&ePL|ghWw>Jk2>CBrl&+>F_ZzZQ5o@ua5$A%kJ)KL@h<0Tdb6i)YCP8pp< z%j2oU!oO>shB(gZ(f=*JjK(;L!Q*Ziqq74eFFV}&%!=oMS513}q&ne1*H}{B5T9nr zs514J`w{_@A(cFHH+jk+*DDG7OR!kM7t8e7V#MWK`N*OK3^_Ozc2Q?;|eE% zEp!LItjk}B;n#sbmo8}_H8Td!&Y}H=A14K&32s>@*LgBTIRIzOKn#aFwXwU> zLS+^tHA1;X7;)ulU8&xzX$Xy&lOcgk0Rg{QN9l&3Whjz#L<;GU7kO|T=Zrh_j>q%m zz|)(zu@RFsU{|&jACYSEyd?Mw34)Y*{k-CwbvnOHz*sM7EZVu3n6SsVy-xDM?!>p( z$8Gmb4c>GX#W`AOX$_+U{(*xbvL{@~^W5zi7O11CIW-kRGtw=g8BrVzW?`+HGdy z%-=8vo$j&Dn-|3)VY-Mki!K1=(CqSsaeT#dS=E0LWSCLtGZ?l`oOEt$rH}*?ms#!9w8{pCiogOKTgUMJq4Rf{o z-TVwBkse@_cEV5SYZqI75%01~2Z@wA!N6>@Xw^lsUQ!H3!CFOo7aF@b(3^tjglM!b zHl57kWRxsSm+l-94v**SIRP-JRnZlJ`jQelUFfKYi5nIesy)gO=>`nE5Og8Dm7^|ydJKRE_x6jS26bT#;7c7_YT+y=nns* zfl+dIK{pnO+_X{xel%e|haRV^M4B$j6EkeVZI=+?F%}&hp8EuJKISe3lrd-!5!r~( zV15w$R>TaFrKp_tAzx{AGH9x3(kbvx_L4B|UQ5Us67S3G@LX{re6cP@9ibS07a%Lq z7dqU)vgj$n60B2y1z|h>kY+7mPPq4@U)F0nw%`2u+oCU3<&h#l(UMK!P=7H38Uq<7 z<@RtCQNn^4-Qe%Zs|L7BXMcKLba1R!8=1T6EuNb86WKSOMr>`vjcgxH!i4><;O>qp zRzK*RGGwUUbk<~YbX-<$J0v_W6OTDFY2I!-@SP~|fKdz!1qW(h)U~c zE3y%V)0EXt)Hzw04fWd+d)(gOwVfB%CFpom1Kg=f>E0kZ&s>2OWKM`SU&&h97wc;~ zyrEEM2v<5RNjMj`V2LCRTQMJ{bLMUca0!m+YHIR#+? z$=3tTAbxB{R&Bnr_GxAd{_QAWX7^=l+i#V3*B6kARd-c-{LBeMfUUwA&7Sn@Jvz3( zy2_AJP?5KSj$|H%PA8jT0Ov-Nr3*1M@utL{L7v*EB$oF>dY_GKYc)o#D_Gi+@+D&z zcWI=GlTl+Z*e8k%no}j`kBGvzVO*zBmhPzh^e7N0Sn~U%Ae0LxwmZ*aFFndo zG3sc=UYTju69guu>=BRB{7n~2?08oJ@{5#IH?MQfHW*k1bLOXdYg-XXk{isMc2E-e zM77Jj%1s|fyzF#5%k3P$Sds1FMm+F#xN86N?^SW!pE8t~_VWGD&0={(2VYGzK-m0~ zd8=hN?kOyv2eX4#WgJ7~!?Qt_&)I{b69bR8ct; zew%3pNvp{Gp#>d8CSsRD+*CyQYJ9ziOm+<(NxkfIkG6Eu zkr}1ArM}J`2Ydr~S9HgzSmi3bh>DR}#jBjz8*s#|>QX#nKhYt#ldLj2erC%5oC^J%)hQ+rJ)=Yt z4BPS;(55la0&<&mkbF_baPS3ZZ5p@_(KE{JWx)8x5>~hI7~9$F+wJz)+Zt8>3%os) zdP92~aP&9Q<@HclkJAACsEH^IGctNAB`OgD+G`;$%Zd^TQ*CN>^*DjY5Uk@4y)j3Nt zRo$_itu#8nR7?oP_KJS{VG*lX=xbYNAk3I|?K0CpZ#p!KJmOpwxw6JmS_$?md^wnw zwXYofRy3TMf0|PvahuHZo6Xj=EyGutb0*c>FZ?JQ9df(q zMG8_i$git}3fvR(W?(F&$rbyPglsFk;s=6u)88`Da`oodVZ(|t$42hMb2D0YYNFVY zL`ZVsmMU1=Z#>a>vhQf-=V>N^Ul}JkR&}hB8QV6<4&z(isn>q+WV0-D4L3!!Q}3sW z(YyCe>oQkI_~X+tGHLc0lxo9tdTgN!t+KGeLy`JWgal{xrnSz=X5QSmw9hgkI z$hKx?Ktv&)9-+VEwvtoVw<1{6wX5C+KV%*&DNkXaCDa|f`e_G&e_ujk=ZXrThTDIz zN&v_4ykq_$5{!GGQpibj`Z9h;9-_sM+?#@}>wv0MDv>>_M>2Zlmeri>z~E2+aMxBVb_;N`iADdzyr0RZq3g8p-j z`}?i;j{sfpUj*p?GD@Y^{ugoTzfa8mzfDE_HD-VL{~EKi{evs;pP2nWG5ddF_W#7} z|B2cE6SMy(X8(T^v(KXyPOjVyPRFIadRIrLM!)5&*faK(&W<{`)~ZAxNBgAH%!ijA z_1;XeIGJ2bKKJd|&-C4RD$n%88lEFk-8K7&rMBb~(oWFZ87qj;a_At04Uls6*pF8Q zObb}BmL~GiX|st%t{4EgMhD$}Mx9$xn&|`7D{+%vGBpD=8vX|>38`i%Y3pYhhN9`!`9|E0mg z-4zlSN4Bx^B$yKq%DkDf?urR>7TkCWy)#KhNQVd`Ynk)U2UIwdGTb0j1$-q8W-%AXr<;2GGUuFq`?S_wxMAUy zwQ5>(Ko?Nf;a)t%FIAkTM{(aJOzNIhISp?iMXNquDs0o_n=lc(S%AyMATV9gj*BGC zm7aWxD#%eksE=Jw8YLlsf39xrv5(2%!FQZlQK^{&eQ%r-_~FwMt+7+5>=d~@uS6cm z8yRw|GV^_D{XB}{Gp2y?Nh7Y0$$_M7ES#DnhGj?i8?IXG&<^F!PHy6`?GKlg6x?wJ zw>B`g#iS7$Uw2H#d}O+FR#esTA?tWd;op> z3X!Lt%+3W1QXOyX+cG&K_wA**S2Qf|M74yo#?oEpu{?ZhfJg{~c;^mvnL_fr${vZ7 z)4NG6A-8zP>jPbVw?d;^jU3V|s4;f+b7za``U-AtiM5MTL0%lkiihRC@_l7RYF`@n zaRJv!y6C$bdsCRLQUQC6hDBKc#)KF_p0O*u6@=q)Zpo$bDDZtDu3Z{VPQCKb|H%;s zygkoEc@ElNV$lEpgZAGCs{eIC`;P}|x(xsTUXpwNu*31UuQ0zYurt0?qWvvkecc!1tlJ1FCm2_HB$#*&V#!&^PhKGh%%#t^tG zJ4{G5K#HPjOVwJrH^3kNExWG7lNuJtaYHXGvp%$WQ^1?9JgPH zKHfeSOeWXO-CnghUO9eozk1vJ_0hi_4G13SY&7^W6l$UOT&cIwW6hL{j|`C`Az`?o z3;L+WP+@}LYBLl}B5Mqqtf!|o@ae)XjDjS*nl(M74XY@DE13AG32gwoWNg9q3*w;IXQqHe4Ffl@`P3;qU0=W_4*#=hoeD zrDMa0g0=!e2}d^O(Hng-qDoa+m@K|d`nc5{ut615uqA6FEelPR8qwHY8?sO%Jcv4t zh}nHodLs0=lfc-RAh&FzFj=#4AKi!ckmcYeAOV1l^-XojS|hkVcy%2thWK zdmYOm;Cu79hQ=@|qJ!hTYPHN+kw;Eu#Kn;1@2!CGijl0ZCR&IrV+<_;gH+EX)57?j zz35Rce|=VdO|s%!cPQhUY@%|BqTK{>kL=hi3cCd+v{T%jG1#MpVLKH*I|K+=mT}{7 ze+kntcI;#`w38O?DG`Q!@{I}hmV;aDlk*c(DfE(%1Mf<$ zHBbAGE?dv^P$x~e#zpX0u`1cAO7#RMih*jcU7%ZMFfZ6=8YM4Z9cD(6pe5{Gpo2q= zw$9y76$QJZ(I9osx(CwiUt$;$7hGX7c;k9BFvvmE8ola9Q{9z%izrn=CWt+iH- zyDoD?VwoM!eSKRKt6h8rM&*D&#vm_3pu4*1nWr?i1S&qDJDAmNP+p-$IzZU^~7S{o1pq=nMrqzA>)&V3~y#Ef{O45G|qPUi#azZ2}~iuVH=N zXW$SecbMSJnhPor6k>{_2<$};&N#_$DV4rSVtA}AC7;J} zru43oQp+UMV`^n^$|=ClExfSz{r-0y_hu6 zkfYNy782r4s3#)gs$@>NyJ$y2qR}8F7)rJb2zCd#>;KRq=+^TXec8&d5q$po?iq;< zbq?z6@SJ|iSwrn+?%XY6gk0a~@lUNba9uSr!@&psHefN=Px%R zPqR87zF-fW7xv1v#cOUIb*muD}pkzH1^H5TqfoqtOe4vG*!s>}# zOrZCy!XW*6jG13>Qp^$`D0y@Uc|d%+#AU692;S@H`=?JN8e8s*6VHW&kHfQpzXFAy zhkQC;e}rP84{=3h$N4a5?(^07wn0tdAlLnsxt_BlU^$uc!2Udxxe=+nKK$#4FlCKk z2zftzLHF!dR}NyNQ4NEBZ5^R!-g zVG01cm7boiP^w5H`$xq1(T0$eRJE=IHd$C@5_!BIcAvX$20b6s+G9J zbmX6X`4~#X1n7oo)C(orWe%;_e`KVB%k{4F<0TzsxLlt#eioV@tC>xWSckMAe_~*l zJ)r4%kQO{OwibM@*kS&TgLzeLE}wxC`6SdNRohU4P6fPu6X`2I$73vM2YP9S_j1qb zQkEPsfmPG$^^)2V)Y4$Dd}n5yo_ziWo#cXG&zUx)QssL24u@lF^1 z+>3V{zQLbH?xlj2D~;w)5W4)U!1c^OTAG#6438$`5_Up?1S*#Ey*0u=0UsPm;cz$? z69;o3z7ZooA^XN|$1@N~HZSvaRN6g4c0r#FV|)rO`1A}&E{F=okuY(0AsrWMPd<6g z!l&B?=nZ@U=7#uKrv(dd?wWwChb1A}Wn2xGIMtP-L}}~zSzH)ww+#>VWIG2_ir5C2 z_6>9Xaqz9ryjNsck55QqcuC&51s7X!s%03N_`8p1=h#_U;`!4dP0e zL$;x}roMLMx;0O6XG$l@#z~f%pUs5Ri^_zp`#SvI19F8GtI%gsMGzNGioDUg=_Pif z$}uOWtLl0KcX2gHxnNN^PD~4=og#117}Bsfm_s$D2~{M^txq~Io)F9*hKjK5(0g@G zap-;SKzyn$r{nz^LN@rgCg!?=J~-Zc?(xuKDk&zos3n8-r z0XmE)sm@1I^|R{-u@wz-nT$OM#{v9^g0rK|Wv%vI50(d`wok({=*sExAK-oRrPyMz zSY5OvpXBV|WN%>Pm9;=R-)}J}kY|#~vZquw+~9b^8c8p7Fyi8y z=+6~L%Ybc&Z^ejE(7b8mueB+Haj9dKo+o6pR91&7LtH^s5z1a)7a&Yc3pak9Ff`rN z$tG-!)Y)Ft*S!qQr%J|COG=%XUu-c@3#*+CD7C`eg>-LB!az4DC^r_lb*Bx?sL>~r zPUQ>e@O?=h*E7D50GJisc770t>Wj}B-H46zBSDky#36%+bsbxiKJ8oV1gsnt)Ks!$ zYROtrIqh_z@X&rJ|Ej3D_|?x=?D_Z8&)@tVcYCWLH2144g?n>%dmlg+bf!7gz#*Ed zu7fIL8EaV{AxHCD6bMfK)ETD$gz z2ESI}aL{5fp`U09(eZqX7>keb852eS{lqHWGSEP!fQJZwQEd+pKQqoU2+4VT;AV|= zwwI079LCUjhUi5Do&Ru_3ytf=8i(gYILDIuW)6t)QYjSXR-B>fSTj?IO3(%ak#mKp zXi-cy^VAbvHw;*V?Tt^gi3qZQ(o_c-b?}NLa?BKU80t3{X3RBwHjZhNZCW6g`-M7| z9kz4HVWaVp@&+Tm$0n9_IPwf3QXF0&LLeOF@LsORs9DtbDJiL#cXs0#789I`*0EpN zY+}C_XmUTn=rm+uFwAeq=f=>iLJCY*fkz7r0eSn8q(lK(^F!5vc6tmQjH_yX?LIXG zX7KS{4Ncqxs|2yCHVzo8Jmf6YTx^E#WbwRePBc;SF&gvCQb(n-w0cTLRBQoTMWBF! zJ#mNFr-iIrfh0q^0vYQLTKh<4dRWv~Faavn&fpB8P1pakuDZgJhWA}YpBvYl%*)_ zpWcsQLJ1l5h2QB*O1%Q(lqOoc5IQZQ>;typkv860YT)q;d7MXMM+5%^`rtTw3~1$_ z%yj-+j1OS9Vt)%wvdv%`72?Ac#pTV}v_5bBKnmM2Sw8WF8+uiQVgUApA4w*u&Xx<0 zGD!W9^s~Vfk^F-mw4|9qRBx~xK2hNWnVh(|O_p%V9Xj8LxG1Q(9%pG@R4kIWrr>G4 zHB-94i1XL#<5_}jHKk!TZ$G4jPVP*>h(JO~+9O%c@v4K1q8OTrI?!om{)A%NB{0J# ziZs58l2&_j+Q76)Gn01@B@5;j>uGNvulkNn)52{nirq~>*tQ8M2D;gj7;Rv+dDCY0#}o5aWGftL((=XQr3uiXo%b(C0TI5w9fZU6Dk)%^jHQhrvuOA60-T)of${z0dT+yz08iR(sB?soHx%ucT_ zAvY}={nZ5u;7o!{NW$icqz#n7qOAH}%DF1ln|UniS~4r{bNXeJu`cY@4_BZ9Av;+@ zr=dYdApKfmjt@YfzRJ68MqtaR==PcGX|FzTpjU+)g$~(i>J{Ai({|XYZsjPtN;zR! z;&(ya?mF;=#*hoysOYOpJ52lg2PViw9%<{P7&~d<`O+cU>oE+yA66hX$@uhs*nrrC zIVA|x%@!GvDK)uhe(3$aR?u#P22DTowF5GONeHaff{J)p?-uMLbkwku<}_Y1dn*Q^ z6~%_;O80?AK>mUTcP5rwH?|$K`9g5ea8e*ey)oiFWwqF|5y)p7Gka!Xa(CV_;r0+( z6Ui8hw-b&6*yrAu(UD}BLeIAl<7~r!u61^+ZOh=vm9RO=U!Mf;%+#*x4-?VlRxQb@ z?qOT&Ff`}-)i^#mDy9KpC ztkk&xa(RihKwQ-t1P3}dv4FWT1KbUzEM?^LswN}gKkA8K8s}uGskXDCsPpa z9bZNV!2NAH)6)|q3&dcJjHC(b9jE1?^Sx=aIAV;j6Nsc)hc#QW-AP5e~|qoOa~;1pg~iHR1Y%!be%M`{#CqM|f# zVl$J{D>WhJw7&L%sSbEwjO*qU3?$ULN`B4x_w)?Qtg0Co+IaT3O%edI>8m zb}Ni!2b?FBUdUDqqFDFdp~(Be_mNyfDi;+rXCkIz6}{@4)a0;9NCS*SA3p0z9JMh{FuS-jtb?k6-9k zktcXW_qqe8l{7z%lK`G8DSrS<^PieJ?5l8tti5PXgUeM&`EilbIipiW*s2j#Z)aA`@#_ABuxiaM|h)v-N zdu|97L+h{TN2lEm*9<40e-ccokZ)_KDd2Kl9%D~61HAqG zFUrtd8XKQe_PsO3FPy^P3}4{&pJBbzl0SC}H`oVbJ%(#ve!vC*R`6a0$~0)UVmdv5 zlGwGE2CBjV_F(41Y8#0rb^!>;bKwaFEFnE*e^r8f(ooxO!7Bn%?o)z7;l11#2NExY zXBhrJ?4l>- zF*$S`W)T4k9m_rpy9h5|<4J4IF)nbkt?Ik`AA!j!>?<}&Sq~eIsm(#9$DGZT0|=Dq zId~Jg1Jd)nKUybv;s88{8UPvV0wo@G2Wy7N(@di^vY!BTHL2iOz2@igGbdg@QIwQ8 z0du|B{NUg6plz9?xw3APhrnW_smV$LDfx9!euSfvsNw9Vrs85(! zCWJ$Vb&-4O?d2y>Y^@0*Q@&TBaWSNrQRXjAaXjT)D?Sz~j@+iTc|PB3@T|;T)+L_4B+Aq^|6nnw^aW8v3A7e|OW{-}ZAVqs@i*R1g)uNJcmx*; zFmqz6!i+*Q)hoEpdXH=-W`_!S(+}~4kx4O}YP>Z@0tjcYPB*JDx-NK$PT*=2O5iPe z^*W;R%%Tx2`(BA*sVTmI6zr?9X+FZwf0+Sn#|h!I0&YvqiXg1Hu@)|STgYQtd}?e& zw^*iQ6(`A^FdS7U#G>2>umso6ICHlN3~xcdJlhbHpb&1vt?@23(p2f&xP;BuVZqeT zxRK9QG(Tq!C1reqOk>719PewX%eiqf*4MD3_cJa#{hEut%LX_3btAv!#;NbE$Ich# zcTV~8pfbSRds)c+oHE``N}%+|4~lr-O62gq4N*inKfeJJ7#f>1B1I!tVx3*L09KNq zo%3_q5MrDV`j{vs+z0bal|G{HAtjnV=48tTrdid>r%wRjRQigZ#IQm-XX^;+HQYsZ7EKa+DN6 zX%9zhrE^?vlO-krwCdGvS5s_e76A~mxj%~| zLG6D+PJx}$!66nHSOjYb!=Ba^1&*g`y*)$SXeIPY^qln*l^#;M)1xSVFC}RSi!i z@-&11O9N3^>oUnKJKrcEKW{@iZ}8UD>1P%WZ;Lx2H4mf0kjco!FP!Ap9IJs}6Vcz# zTNQ_`;#~*1etK@9q5U!V%?W&;J^RVNMAN1}riI&>@nz#K#JhWQ_Ol`Cqm4pi&(w%8 zy64MLqqQA6NkzK&9N8?uZsM_aaT0hei|YUeZ?yI`F+6=ul%sfkCe9Chi+xqSmg>4e zle7?4$$7xl_tz2XYZbi2h!2Q%&r5Lenq~M}r>!Z*CUFmt9~!tm#vNrOHIzqci@!(m z9V73kDxg>H*i5(N_VuamBF~-m#+*xD6^R*FtO~OrnQ{_nWJuCKA12IQOzt$0YmTgP zzPx(PtVnnXYmG1WeI6*-%*Mjiat90dIG&BJ4ppifmJSWcW75=eb{@0StTCiIQR_Uw3 zL0BMGE&eZ{%+j~@ejcwV)#cLL7CW-Q+HTHkM@pj+%h0BLDad@;JF`#{mQY|sz+mf7 zzM2xFU;|kdA_sz1CEHlNhPAJ%>kaW87qLGn_C6ie1YV`4Knd4^)|Zx4YSJq1iQ!-A z=mMQZ)@XWE0dQ13H3GuT;>3W6bW!>+ml8;qwzsx8OYSOl8=_2?oLfl4nH^1Q+%aoJ zl}rjo{pzF~Q_}^_dR{l)4#PR-0AIM!)*-QheQnp3`vIz?J%BXP_#zQW%FS96ll>!+ zo3>;T6~dV5O_AVf_5%t0WeL6~(yXTBiX(1iyhbrx>k zBm3R*aYnkKg6C_$qV|4<()N?dy4%n&x+%?F9`cw2i!ala<78t1I*3LOM9<^4s2!qP z$@6tkbyILpUJMe1IY(8xc6iUL0M=K&0Exw(vpm%M%i(E#dN~(~JXTKn5BwcDk0}KL z$`4F)xI&;VIm;L%Mg%!%dF=#~+T!{(Ud7*@&JvX&;FS2y5U-e1P^qak+<#4tl>G@Hw#|l6!dY!Z!sGTHkF>YsYf5?E-K7as)n=c0uscaS@29!t8q{?8u@kAB zd}5_4RT}X2EP$QCTTT-!!?J3&R4C4x%swHSK-`q*he~MBjX8FH-F&MSI*{<1;`X<{ zKt7b{Ll;=qdAz!J-te(4GGjCA0eHUKJ8W&KdNX{O_2*GJb~U80$)_Kx*8}*%C)BD) zr9ldoA2^g9?XgSpGWUW=wYuY3HYrIZIVx4OP-|c~RI`KmT8MZmYYLnfHqRsSnay@K zM$%N*T3@|^oD`{onAN-w2_pir{2qVP*t)-;?Dfn3Dn8K zp$1x$5Rz|iovtbY#FVO&!7~D?a7L55Sfq?#KMviV?6Lplh(6r?FIWz*Nf; z5XDQ>Hdp$O`F@=6YYf9rauCZJJyuo}U{utIP)lh|N9u9Dg5``EqqZjqI&Knh-1 zzC*Ld<^-DEzSKJ&O{)Zp3g@;j66f1jjz+n~=0v1j)j2XAZKVW=%la7JC#NW`F+8&h{1TI1>P%EcI``@}V){ z9PXLp3)i3f$}bi-_@`jhntvtQf3;M30e%Vz7|6v>4|CgEchhx}>YNnQp0*R0J zw~8=TN_b>tNi5MZ+~tR63t0{rbC5NQ1bC!2gto(~&ei2tv&U=d##lvajn6fSl27mn2il}BE!r;NB+%q5sdf8=y4ThWXrQBknY zDDPiSzZv1I6~N8FgkD?Q=cHQqiyEiP8?5KSU zZU_!H3ppau&8?lF`x0woq^ad$tE>FsX6E7WTjdNmdfcuHU(wwijUMo^R5a@wR80!4 zt{#T)T+T&{l+aWsb+E0Yb@tIcypa=~q{3sLoH@dqZx)_a!YLmLwtDWeU~dblLkWVr zvU;soSoot8FS<86`a2?bCP86_8L|7GlYM6E!kV``V}%jQ9*o3unm9=`&)9|f3yIZV zZ=|(%h6rgcAbvemMep)c=N`i&V7W=)dbo1-oHkYInvHw#ps*5}=1@s+-tEK8Z0nYP zb-=LMF~VpCj+uaT4ro1z=Et~L5hRx_X6Z2y6q*H7!x?dZ;_b6S?yZh)1r8=wiQ^`9 z|J(;bHf81$d$vasi*h;0rJVp}Ovxag+qp00UOyMO@K#@PCYW+dwtV^KgN~n8;L^Ih z{2`0-y&{RUu_k)^(^+0@?(0Q*ODGA`R$UQ2 zj1eFm88)_J@n#w8USyfvuJ-im9l6I1WQ%L2OcrJ}m)?^BTi4VGrw zQ+^!~6>%!3pCFo6lkSW>Xfpo9X> zAT0?A^pv95JO>P528Pa@2eocN`T-%YOT!NUtzM%tdc$z7@8yChNNUm`Dr*STO})cn za@2tbYItil5aW|_-fRF(ISao(H-_E9qPU+>-U@N7lk#3f%Q%T@AruA8K2rPkRV;%n zPKipxP(TEMVt*=r9ToTtxkXT&$R_9QCGt(2peXd_)EO|;DBuiDrCp*!Z6UY zv>%laifF~d*Y~6$OOfoQh4B$0;AL~g^w~sQxrR!lPkq4)9uUXFOBtu#^C6ZADj}GQ z+w+r#{p=$KzA?EDoo$PW@TkM=K$JD5n4a?%0jTfyqe7}|l$o$RI z=Dr9rnedqn+bVYg*i!OT^5R*G8-yB@DWH^hq46Y0pgb3&JxECbJm6v!#NpUZRZ+n! zJ4xtkbLX}%qG;ki>Zs#4XuOD&!!8+ypP}m-IG1tH8hb&>z77(XrBF%sf^X!}^1v6R zVOMQY!F=1m@~LK}w2fn&WP+*3SUh{LM%dpfIF-jQ=ntw}0i}}c7ut~?JJGA4;LU#k z4lpuann6G=8~oURuM=|IxE}_kpEh*PzsUL83OcD^M z`q~}YMw4U=7W^Q%TUAFOIXNYTiC41XN}3Qd#vFEL9luJK=mGoMK-XytKzN{DA2XRTPBZreBry)cCwMgnP|Q`m&A9;eGk}wGZ1pNYLXZ@XTJZ+$sYrMyTa6+#F2AAU`jl>(AB7}qqJSPu(#)Mt1uGx z@!R8In6q)Rm21LWnU@$k_25AiRfWC>3B&2EGA9r(^_nF7BZ$_R_!!}ZV{$oA4ipJR zl5tS%nGHX}?fY1xha)Jg4qLVH{l}CzKeL%%?$x_zRPcWdtP<;5Y9V<6tg(^jcmWmAi`z8%ySUte@Nh zL>u{|k*!xNDJ^la!jw5bHDNe;x5CrhBn4IW4=>M}-hW%wVaYdSUDO6B z0lxR`HqoX*si@W}tR=pfUnq$TE%k#7iefG}z*!F^^n(kBhqnnkSiIn^vMwK3t+%M* zu{rxx`t6Ds#1yfn)STHT|N61#s>LV~FFcpCyHtkP z7WTABqTVK!lq%!K{7Lm-V;~vX%1MH)LUj|L<&8kgBw})?*E+ue=S4ogS*~Y&Ks2A7 z3%9^%mclOzz{*LJ<5u% zStw)4kh%f}BcY?eDjSFxkKzPB6F}I53|_ClOY-kN)9c0*F3ay2LV>C(I8-VC_x8u~ zAm2norMIKZa@11qNcxtMmQ1F|EW3=H!xuVy7%~SRMNy#m#zNsWU?%ui^HL?A6Eo8~ z?tI?}M3f7A45Dyp>)Gtn(UPyGohQV2Ba?A)*o5RFNm_)E?`7`3~;6<$Dr?WoQNQ(Rw2*ygGt!mViuUxUDASmM)DZI}S+ zM^xvyD**<1>#O16i7m&D-_VlI8jEyQhWNSnA-1QqSgF?ID6QU=#)u1}-l@Ojj<4Bj?>U6pHmZO#5x5Q29OZyfu0O@--NX>UD}}vM4c`ZsmLu^*`w|bM*@iJE z2NXJpP6DWEqV;XMqXA>igQ+SP%t7N7ry_79R4qzXDHu+)VRpIq6tbzuwkqr4|C7m=C4FNw| z&XRs&fbu84xMl4Soj4@_vP$lOa$5cd+}CiLqiP;{RE-_Jl5EMxG#pWjsFMsRVVjcE z(zr=f@CZ#UeHr(4rOpXPw1R7xW0eidCzotok3dny$k~TtS58}B6?~$C#$3lRHR)cS z1LhGA*BpHWBv7k!*y(o?rc-+`c#H37ju?CBwW3;dAM2$S3Mn+*>p2WN`#=tEoQ46V6I` zI<_(|d09WAvMMX?x5=tN0dwdcCl`cRb zd1j1*kJ^uEjdm4$N?LTm2ROX=SoK0Z)-2BUUjvjOa;~b#E8m%#ZEinY4OE+$V<3}F z*#l|lwACT)tqL)RUvlbmB}VbU@avOq-$la`$gIiifRaky{!gE@sdY)OF%d0;?*ptDwqhYnk$cR>vQRQ z0Z-gyU2Q~bf@7uAsmhl?W~EcMHp+0=wiYR%`{3a@>hQKxjV9d4j@qNtk7gvMiQp^S z=bgcEjXSWZ{ zbZPk()pB#4fo3^h^;yf0-sb3DNJh`R(01`ILX)?vGLs7B(}ad!FQpPk z?@+d=BZ?CyJ9Ot5)M1ZHHB2Hxj3^)FbYyFGGi@)s8j*93`C@{@x3cE*Ri>j-jCL!# z!AAR7R^a#^vOa^ddX~&UTgX1InX{8xEB+BRQdd?Lv#)XmNex`6qO_iFA6xFu)9=00 z_2>%7QJK1Rv5aBzW8Aq zcTU@OULDwc6Y*$c-v)0~M4gUWcz&siMzZ)^w2%ru=X9HvUUbTVY$6r)_A>j8uK!cE zGf8`yh2oW4x4HP*f$rVeQwmEPGT7|o?fyz*;FPQCV^4=`OJa-mM@C*Ov`^@o$Z7SN z`bpIYTQH)8$anPkB2#Sw6CyJ!0zP5YKzJ_4a0eV=ep*<@m#)rn^t{y zM1Og#u8?%#6&`}Cf1ZrJ?jrq#N+$sreNkFd8)QIAX-W^V=8nZUC~#%rI!*2Vb4DedM<$m382PBuIr3 zG%#=KsNPh9?Bpo29HX?ZQ&w~ViD4dQZ3r*I4{X6{mQYvbvaSmZ{2!*`Chh0 ztO6DXlY8Qehwjc7XoTz*%T)*eP6kJ%r7oDI1S_Bl-38W`r6=))h0iOa52mYv4?0cB zM7=OQdc6|$+|lN36$W$J9KMvW1k;CNXw%IH!e7PX;RbevX^z$H6c@QXj#dXAqaFNh zXv5G|v$41-TMgm5Hr=mb-d4AJA9}61D&8)mKGDq;oLW|Z16s820=?B7CJtqkui7eU z1mnGCXG`sUZAY02%e#V;hC>=KXP$9m5WaQ^}%M zWFl8&9#b~EGoZ4_QRedG#J@%DUB@KPn4rfnUGy)=)D%)?rtDV{y5} z%yXKD!|V>%?`Z2E>|B+^%jXprCe4saE`ABd)QKv|&`=3|U1Gyf=p@S3S{EL%qB$7> z5@*h}TzALz5PBOIRhQwh;F@{|EZ!|7DamCn9jCFV8U}K4KFOVsGh2uChL;P}9wxY^ zrD6>Za^JXkQe+(N3XnipGBU4Qx4mQ^ zZ6VY_vJ{<6nrO#P0%pJ>c+BCJdni-AVixBIc+MZhnV6E%nv|2PJSm**4Lq;2o5rHCJ8wt)co$WX zAj={iJn8B_i92>#N9kuA40sB64n!c%<&KS{=F!3r|9Nk>w5NNQW)JIqjmmC~v(%om zJ|F(XWGzfCe@5&bOms$uepMe;*C%kFndK|*1)JEkl8lOljRx)Kb02*D0nr|^^c9aZ zFZ(Tfyp*N_&+R5&7OH=~r2kUL@@K7y{`R0PwWgTl?^m<`tC#a7|KHxzubed&=amo^ z;Fp#ApQ@fr|5)1p$tIZY^o*5(mKMxi^F|;H@S9vY2@_=#36Yo#S$$y>X9MdJTMr9f z9cdk5k8xCk3{n&@0b($efVb=>Z|Bj_*wB;amC!C{>Je{SKQDfnXP{~Bup0wDn-JtG7F@M9s{mut9ZWWLiO@5@hru>NlreW~|^_pC6{ z^YxJKsYEk~k&pY;+-#^s)y;9Hh zv;WNccbxx)raxBhe$f#7N6x%qK!4Ko_vO2PWA#!l{DsZS=iey#!sCL?)P%mzZv#rCioY6y`cM&((q$nz?T-D-zm>qn(R+{{iy-qPaYsYn(&3mk0$(a z9?0`Xq531NJDD@)zk9is|*T0(wc%i8Oq|~3C|CgHm{nLIgoPTuAmx>_Y>CoHm zne)%=_ZK4kLi4{L(SDf)f9(J8l9=(G^1K~s{vGZAzR|;Ze-EmTxBpwH{?yUne+88k&;KD*e-iCqjH*9A4SswE=XqQ5{u!$8bMr4ZI{4?@ z^1}4Tg8whA4Zc&J_vSOxpAMaWaR1dt2S3g$FLT?EHC-W&4N&GLM`F)A3 z{}nXs%Ktae{9dG=xXR0W%=hr^$GqeppNP)^hRUx&^A`!oFOd8jA>@xE<|PCAJLP#_ zIsM15{DnCGB0TvXbN!g_`;vY9o$|b)Tz(CbzfA1?#c}eISNxswylb8%_zzv^FNOKZ z2>Jfp`D3*9lGFR0^1QoTe+`a5CD#5T6fZfw-zm?V*X=)s;!lG71cvX9@&dz;nXSJM z+a%n74Tir;Vf{rUejm22JPYt24vN2$<=+6~$2Gy1*V-x>lKz=6E3ow5P4k*|Q RU*hL4JVXG1(xjKG{|^(kLQDVv literal 0 HcmV?d00001 diff --git a/frontend/rust-lib/flowy-test/tests/util.rs b/frontend/rust-lib/flowy-test/tests/util.rs index 074d025ee1..04c69c5fdc 100644 --- a/frontend/rust-lib/flowy-test/tests/util.rs +++ b/frontend/rust-lib/flowy-test/tests/util.rs @@ -1,12 +1,17 @@ +use std::fs::{create_dir_all, File}; +use std::io::copy; use std::ops::Deref; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use anyhow::Error; use collab_folder::core::FolderData; use collab_plugins::cloud_storage::RemoteCollabStorage; +use nanoid::nanoid; use tokio::sync::mpsc::Receiver; use tokio::time::timeout; +use zip::ZipArchive; use flowy_database_deps::cloud::DatabaseCloudService; use flowy_folder_deps::cloud::{FolderCloudService, FolderSnapshot}; @@ -14,6 +19,7 @@ use flowy_server::supabase::api::*; use flowy_server::{AppFlowyEncryption, EncryptionImpl}; use flowy_server_config::supabase_config::SupabaseConfiguration; use flowy_test::event_builder::EventBuilder; +use flowy_test::Cleaner; use flowy_test::FlowyCoreTest; use flowy_user::entities::{AuthTypePB, UpdateUserProfilePayloadPB, UserCredentialsPB}; use flowy_user::errors::FlowyError; @@ -170,3 +176,38 @@ pub fn appflowy_server( let server = Arc::new(RESTfulPostgresServer::new(config, encryption)); (SupabaseServerServiceImpl::new(server), encryption_impl) } + +pub fn unzip_history_user_db(root: &str, folder_name: &str) -> std::io::Result<(Cleaner, PathBuf)> { + // Open the zip file + let zip_file_path = format!("{}/{}.zip", root, folder_name); + let reader = File::open(zip_file_path)?; + let output_folder_path = format!("{}/unit_test_{}", root, nanoid!(6)); + + // Create a ZipArchive from the file + let mut archive = ZipArchive::new(reader)?; + + // Iterate through each file in the zip + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let output_path = Path::new(&output_folder_path).join(file.mangled_name()); + + if file.name().ends_with('/') { + // Create directory + create_dir_all(&output_path)?; + } else { + // Write file + if let Some(p) = output_path.parent() { + if !p.exists() { + create_dir_all(p)?; + } + } + let mut outfile = File::create(&output_path)?; + copy(&mut file, &mut outfile)?; + } + } + let path = format!("{}/{}", output_folder_path, folder_name); + Ok(( + Cleaner::new(PathBuf::from(output_folder_path)), + PathBuf::from(path), + )) +} diff --git a/frontend/rust-lib/flowy-user-deps/src/cloud.rs b/frontend/rust-lib/flowy-user-deps/src/cloud.rs index 012a0d3303..8fb7f39f5c 100644 --- a/frontend/rust-lib/flowy-user-deps/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-deps/src/cloud.rs @@ -58,7 +58,7 @@ impl Display for UserCloudConfig { /// Provide the generic interface for the user cloud service /// The user cloud service is responsible for the user authentication and user profile management -pub trait UserCloudService: Send + Sync { +pub trait UserCloudService: Send + Sync + 'static { /// Sign up a new account. /// The type of the params is defined the this trait's implementation. /// Use the `unbox_or_error` of the [BoxAny] to get the params. diff --git a/frontend/rust-lib/flowy-user-deps/src/entities.rs b/frontend/rust-lib/flowy-user-deps/src/entities.rs index 7a3883fe20..87d1f6004c 100644 --- a/frontend/rust-lib/flowy-user-deps/src/entities.rs +++ b/frontend/rust-lib/flowy-user-deps/src/entities.rs @@ -167,7 +167,8 @@ pub struct UserWorkspace { pub name: String, pub created_at: DateTime, /// The database storage id is used indexing all the database in current workspace. - pub database_storage_id: String, + #[serde(rename = "database_storage_id")] + pub database_views_aggregate_id: String, } impl UserWorkspace { @@ -176,7 +177,7 @@ impl UserWorkspace { id: workspace_id.to_string(), name: "".to_string(), created_at: Utc::now(), - database_storage_id: uuid::Uuid::new_v4().to_string(), + database_views_aggregate_id: uuid::Uuid::new_v4().to_string(), } } } diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index ebdbbc2026..52a6b561f3 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -10,6 +10,7 @@ flowy-derive = { path = "../../../shared-lib/flowy-derive" } flowy-sqlite = { path = "../flowy-sqlite", optional = true } flowy-encrypt = { path = "../flowy-encrypt" } flowy-error = { path = "../flowy-error", features = ["impl_from_sqlite", "impl_from_dispatch_error"] } +flowy-folder-deps = { path = "../flowy-folder-deps" } lib-infra = { path = "../../../shared-lib/lib-infra" } flowy-notification = { path = "../flowy-notification" } flowy-server-config = { path = "../flowy-server-config" } @@ -18,8 +19,10 @@ appflowy-integrate = { version = "0.1.0" } collab = { version = "0.1.0" } collab-folder = { version = "0.1.0" } collab-document = { version = "0.1.0" } +collab-database = { version = "0.1.0" } collab-user = { version = "0.1.0" } flowy-user-deps = { path = "../flowy-user-deps" } +anyhow = "1.0.75" tracing = { version = "0.1", features = ["log"] } bytes = "1.4" diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 093580d948..7e4a64243a 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -88,7 +88,7 @@ pub trait UserStatusCallback: Send + Sync + 'static { /// Will be called after the user signed up. fn did_sign_up( &self, - context: SignUpContext, + is_new_user: bool, user_profile: &UserProfile, user_workspace: &UserWorkspace, device_id: &str, @@ -163,7 +163,7 @@ impl UserStatusCallback for DefaultUserStatusCallback { fn did_sign_up( &self, - _context: SignUpContext, + _is_new_user: bool, _user_profile: &UserProfile, _user_workspace: &UserWorkspace, _device_id: &str, diff --git a/frontend/rust-lib/flowy-user/src/manager.rs b/frontend/rust-lib/flowy-user/src/manager.rs index 766658e5ee..a372e075d9 100644 --- a/frontend/rust-lib/flowy-user/src/manager.rs +++ b/frontend/rust-lib/flowy-user/src/manager.rs @@ -3,7 +3,6 @@ use std::sync::{Arc, Weak}; use appflowy_integrate::collab_builder::AppFlowyCollabBuilder; use appflowy_integrate::RocksCollabDB; -use collab_folder::core::FolderData; use collab_user::core::MutexUserAwareness; use serde_json::Value; use tokio::sync::{Mutex, RwLock}; @@ -19,12 +18,11 @@ use flowy_user_deps::entities::*; use lib_infra::box_any::BoxAny; use crate::entities::{AuthStateChangedPB, AuthStatePB, UserProfilePB, UserSettingPB}; -use crate::event_map::{ - DefaultUserStatusCallback, SignUpContext, UserCloudServiceProvider, UserStatusCallback, -}; +use crate::event_map::{DefaultUserStatusCallback, UserCloudServiceProvider, UserStatusCallback}; use crate::migrations::historical_document::HistoricalEmptyDocumentMigration; -use crate::migrations::local_user_to_cloud::migration_user_to_cloud; +use crate::migrations::migrate_to_new_user::migration_local_user_on_sign_up; use crate::migrations::migration::UserLocalDataMigration; +use crate::migrations::sync_new_user::sync_user_data_to_cloud; use crate::migrations::MigrationUser; use crate::services::cloud_config::get_cloud_config; use crate::services::database::UserDB; @@ -305,10 +303,7 @@ impl UserManager { } else { UserAwarenessDataSource::Remote }; - let mut sign_up_context = SignUpContext { - is_new: response.is_new_user, - local_folder: None, - }; + if response.is_new_user { if let Some(old_user) = migration_user { let new_user = MigrationUser { @@ -320,10 +315,9 @@ impl UserManager { old_user.user_profile.uid, new_user.user_profile.uid ); - match self.migrate_local_user_to_cloud(&old_user, &new_user).await { - Ok(folder_data) => sign_up_context.local_folder = folder_data, - Err(e) => tracing::error!("{:?}", e), - } + self + .migrate_local_user_to_cloud(&old_user, &new_user) + .await?; let _ = self.database.close(old_user.session.user_id); } } @@ -331,20 +325,20 @@ impl UserManager { .initialize_user_awareness(&new_session, user_awareness_source) .await; + self + .save_auth_data(&response, auth_type, &new_session) + .await?; self .user_status_callback .read() .await .did_sign_up( - sign_up_context, + response.is_new_user, user_profile, &new_session.user_workspace, &new_session.device_id, ) .await?; - self - .save_auth_data(&response, auth_type, &new_session) - .await?; send_auth_state_notification(AuthStateChangedPB { state: AuthStatePB::AuthStateSignIn, @@ -596,17 +590,28 @@ impl UserManager { &self, old_user: &MigrationUser, new_user: &MigrationUser, - ) -> Result, FlowyError> { + ) -> Result<(), FlowyError> { let old_collab_db = self.database.get_collab_db(old_user.session.user_id)?; let new_collab_db = self.database.get_collab_db(new_user.session.user_id)?; - let folder_data = migration_user_to_cloud(old_user, &old_collab_db, new_user, &new_collab_db)?; + migration_local_user_on_sign_up(old_user, &old_collab_db, new_user, &new_collab_db)?; + + if let Err(err) = sync_user_data_to_cloud( + self.cloud_services.get_user_service()?, + new_user, + &new_collab_db, + ) + .await + { + tracing::error!("Sync user data to cloud failed: {:?}", err); + } + // Save the old user workspace setting. save_user_workspaces( old_user.session.user_id, self.database.get_pool(old_user.session.user_id)?, &[old_user.session.user_workspace.clone()], )?; - Ok(folder_data) + Ok(()) } } diff --git a/frontend/rust-lib/flowy-user/src/migrations/local_user_to_cloud.rs b/frontend/rust-lib/flowy-user/src/migrations/local_user_to_cloud.rs deleted file mode 100644 index 65af5a520c..0000000000 --- a/frontend/rust-lib/flowy-user/src/migrations/local_user_to_cloud.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::sync::Arc; - -use appflowy_integrate::{PersistenceError, RocksCollabDB, YrsDocAction}; -use collab::core::collab::{CollabRawData, MutexCollab}; -use collab::core::origin::{CollabClient, CollabOrigin}; -use collab::preclude::Collab; -use collab_folder::core::{Folder, FolderData}; - -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; - -use crate::migrations::MigrationUser; - -/// Migration the collab objects of the old user to new user. Currently, it only happens when -/// the user is a local user and try to use AppFlowy cloud service. -pub fn migration_user_to_cloud( - old_user: &MigrationUser, - old_collab_db: &Arc, - new_user: &MigrationUser, - new_collab_db: &Arc, -) -> FlowyResult> { - let mut folder_data = None; - new_collab_db - .with_write_txn(|w_txn| { - let read_txn = old_collab_db.read_txn(); - if let Ok(object_ids) = read_txn.get_all_docs() { - // Migration of all objects - for object_id in object_ids { - tracing::debug!("migrate object: {:?}", object_id); - if let Ok(updates) = read_txn.get_all_updates(old_user.session.user_id, &object_id) { - // If the object is a folder, migrate the folder data - if object_id == old_user.session.user_workspace.id { - folder_data = migrate_folder( - old_user.session.user_id, - &object_id, - &new_user.session.user_workspace.id, - updates, - ); - } else if object_id == old_user.session.user_workspace.database_storage_id { - migrate_database_storage( - old_user.session.user_id, - &object_id, - new_user.session.user_id, - &new_user.session.user_workspace.database_storage_id, - updates, - w_txn, - ); - } else { - migrate_object( - old_user.session.user_id, - new_user.session.user_id, - &object_id, - updates, - w_txn, - ); - } - } - } - } - Ok(()) - }) - .map_err(|err| FlowyError::new(ErrorCode::Internal, err))?; - Ok(folder_data) -} - -fn migrate_database_storage<'a, W>( - old_uid: i64, - old_object_id: &str, - new_uid: i64, - new_object_id: &str, - updates: CollabRawData, - w_txn: &'a W, -) where - W: YrsDocAction<'a>, - PersistenceError: From, -{ - let origin = CollabOrigin::Client(CollabClient::new(old_uid, "phantom")); - match Collab::new_with_raw_data(origin, old_object_id, updates, vec![]) { - Ok(collab) => { - let txn = collab.transact(); - if let Err(err) = w_txn.create_new_doc(new_uid, new_object_id, &txn) { - tracing::error!("🔴migrate database storage failed: {:?}", err); - } - }, - Err(err) => tracing::error!("🔴construct migration database storage failed: {:?} ", err), - } -} - -fn migrate_object<'a, W>( - old_uid: i64, - new_uid: i64, - object_id: &str, - updates: CollabRawData, - w_txn: &'a W, -) where - W: YrsDocAction<'a>, - PersistenceError: From, -{ - let origin = CollabOrigin::Client(CollabClient::new(old_uid, "phantom")); - match Collab::new_with_raw_data(origin, object_id, updates, vec![]) { - Ok(collab) => { - let txn = collab.transact(); - if let Err(err) = w_txn.create_new_doc(new_uid, object_id, &txn) { - tracing::error!("🔴migrate collab failed: {:?}", err); - } - }, - Err(err) => tracing::error!("🔴construct migration collab failed: {:?} ", err), - } -} - -fn migrate_folder( - old_uid: i64, - old_object_id: &str, - new_workspace_id: &str, - updates: CollabRawData, -) -> Option { - let origin = CollabOrigin::Client(CollabClient::new(old_uid, "phantom")); - let old_folder_collab = Collab::new_with_raw_data(origin, old_object_id, updates, vec![]).ok()?; - let mutex_collab = Arc::new(MutexCollab::from_collab(old_folder_collab)); - let old_folder = Folder::open(mutex_collab, None); - - let mut folder_data = old_folder.get_folder_data()?; - let old_workspace_id = folder_data.current_workspace_id; - folder_data.current_workspace_id = new_workspace_id.to_string(); - - let mut workspace = folder_data.workspaces.pop()?; - if folder_data.workspaces.len() > 1 { - tracing::error!("🔴migrate folder: more than one workspace"); - } - workspace.id = new_workspace_id.to_string(); - - // Only take one workspace - folder_data.workspaces.clear(); - folder_data.workspaces.push(workspace); - - // Update the view's parent view id to new workspace id - folder_data.views.iter_mut().for_each(|view| { - if view.parent_view_id == old_workspace_id { - view.parent_view_id = new_workspace_id.to_string(); - } - }); - - Some(folder_data) -} diff --git a/frontend/rust-lib/flowy-user/src/migrations/migrate_to_new_user.rs b/frontend/rust-lib/flowy-user/src/migrations/migrate_to_new_user.rs new file mode 100644 index 0000000000..cd2fed82d6 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/migrations/migrate_to_new_user.rs @@ -0,0 +1,430 @@ +use std::collections::{HashMap, HashSet}; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; + +use anyhow::anyhow; +use appflowy_integrate::{PersistenceError, RocksCollabDB, YrsDocAction}; +use collab::core::collab::MutexCollab; +use collab::core::origin::{CollabClient, CollabOrigin}; +use collab::preclude::Collab; +use collab_database::database::{ + is_database_collab, mut_database_views_with_collab, reset_inline_view_id, +}; +use collab_database::rows::{database_row_document_id_from_row_id, mut_row_with_collab, RowId}; +use collab_database::user::DatabaseWithViewsArray; +use collab_folder::core::Folder; +use parking_lot::{Mutex, RwLock}; + +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_folder_deps::cloud::gen_view_id; + +use crate::migrations::MigrationUser; + +/// Migration the collab objects of the old user to new user. Currently, it only happens when +/// the user is a local user and try to use AppFlowy cloud service. +pub fn migration_local_user_on_sign_up( + old_user: &MigrationUser, + old_collab_db: &Arc, + new_user: &MigrationUser, + new_collab_db: &Arc, +) -> FlowyResult<()> { + new_collab_db + .with_write_txn(|new_collab_w_txn| { + let old_collab_r_txn = old_collab_db.read_txn(); + let old_to_new_id_map = Arc::new(Mutex::new(OldToNewIdMap::new())); + + migrate_user_awareness(old_to_new_id_map.lock().deref_mut(), old_user, new_user)?; + + migrate_database_with_views_object( + &mut old_to_new_id_map.lock(), + old_user, + &old_collab_r_txn, + new_user, + new_collab_w_txn, + )?; + + let mut object_ids = old_collab_r_txn + .get_all_docs() + .map(|iter| iter.collect::>()) + .unwrap_or_default(); + + // Migration of all objects except the folder and database_with_views + object_ids.retain(|id| { + id != &old_user.session.user_workspace.id + && id != &old_user.session.user_workspace.database_views_aggregate_id + }); + + tracing::info!("migrate collab objects: {:?}", object_ids.len()); + let collab_by_oid = make_collab_by_oid(old_user, &old_collab_r_txn, &object_ids); + migrate_databases( + &old_to_new_id_map, + new_user, + new_collab_w_txn, + &mut object_ids, + &collab_by_oid, + )?; + + // Migrates the folder, replacing all existing view IDs with new ones. + // This function handles the process of migrating folder data between two users. As a part of this migration, + // all existing view IDs associated with the old user will be replaced by new IDs relevant to the new user. + migrate_workspace_folder( + &mut old_to_new_id_map.lock(), + old_user, + &old_collab_r_txn, + new_user, + new_collab_w_txn, + )?; + + // Migrate other collab objects + for object_id in &object_ids { + if let Some(collab) = collab_by_oid.get(object_id) { + let new_object_id = old_to_new_id_map.lock().get_new_id(object_id); + tracing::debug!("migrate from: {}, to: {}", object_id, new_object_id,); + migrate_collab_object( + collab, + new_user.session.user_id, + &new_object_id, + new_collab_w_txn, + ); + } + } + + Ok(()) + }) + .map_err(|err| FlowyError::new(ErrorCode::Internal, err))?; + + Ok(()) +} + +#[derive(Default)] +pub struct OldToNewIdMap(HashMap); + +impl OldToNewIdMap { + fn new() -> Self { + Self::default() + } + fn get_new_id(&mut self, old_id: &str) -> String { + let view_id = self + .0 + .entry(old_id.to_string()) + .or_insert(gen_view_id().to_string()); + (*view_id).clone() + } +} + +impl Deref for OldToNewIdMap { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for OldToNewIdMap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +fn migrate_database_with_views_object<'a, W>( + old_to_new_id_map: &mut OldToNewIdMap, + old_user: &MigrationUser, + old_collab_r_txn: &'a W, + new_user: &MigrationUser, + new_collab_w_txn: &'a W, +) -> Result<(), PersistenceError> +where + W: YrsDocAction<'a>, + PersistenceError: From, +{ + let database_with_views_collab = Collab::new( + old_user.session.user_id, + &old_user.session.user_workspace.database_views_aggregate_id, + "phantom", + vec![], + ); + database_with_views_collab.with_origin_transact_mut(|txn| { + old_collab_r_txn.load_doc( + old_user.session.user_id, + &old_user.session.user_workspace.database_views_aggregate_id, + txn, + ) + })?; + + let new_uid = new_user.session.user_id; + let new_object_id = &new_user.session.user_workspace.database_views_aggregate_id; + + let array = DatabaseWithViewsArray::from_collab(&database_with_views_collab); + for database_view in array.get_all_databases() { + array.update_database(&database_view.database_id, |update| { + let new_linked_views = update + .linked_views + .iter() + .map(|view_id| old_to_new_id_map.get_new_id(view_id)) + .collect(); + update.database_id = old_to_new_id_map.get_new_id(&update.database_id); + update.linked_views = new_linked_views; + }) + } + + let txn = database_with_views_collab.transact(); + if let Err(err) = new_collab_w_txn.create_new_doc(new_uid, new_object_id, &txn) { + tracing::error!("🔴migrate database storage failed: {:?}", err); + } + drop(txn); + Ok(()) +} + +fn migrate_collab_object<'a, W>(collab: &Collab, new_uid: i64, new_object_id: &str, w_txn: &'a W) +where + W: YrsDocAction<'a>, + PersistenceError: From, +{ + let txn = collab.transact(); + if let Err(err) = w_txn.create_new_doc(new_uid, &new_object_id, &txn) { + tracing::error!("🔴migrate collab failed: {:?}", err); + } +} + +fn migrate_workspace_folder<'a, W>( + old_to_new_id_map: &mut OldToNewIdMap, + old_user: &MigrationUser, + old_collab_r_txn: &'a W, + new_user: &MigrationUser, + new_collab_w_txn: &'a W, +) -> Result<(), PersistenceError> +where + W: YrsDocAction<'a>, + PersistenceError: From, +{ + let old_uid = old_user.session.user_id; + let old_workspace_id = &old_user.session.user_workspace.id; + let new_uid = new_user.session.user_id; + let new_workspace_id = &new_user.session.user_workspace.id; + + let old_folder_collab = Collab::new(old_uid, old_workspace_id, "phantom", vec![]); + old_folder_collab + .with_origin_transact_mut(|txn| old_collab_r_txn.load_doc(old_uid, old_workspace_id, txn))?; + let old_folder = Folder::open(Arc::new(MutexCollab::from_collab(old_folder_collab)), None); + let mut folder_data = old_folder + .get_folder_data() + .ok_or(PersistenceError::Internal( + anyhow!("Can't migrate the folder data").into(), + ))?; + + old_to_new_id_map + .0 + .insert(old_workspace_id.to_string(), new_workspace_id.to_string()); + + // 1. Replace the workspace views id to new id + debug_assert!(folder_data.workspaces.len() == 1); + + folder_data + .workspaces + .iter_mut() + .enumerate() + .for_each(|(index, workspace)| { + if index == 0 { + workspace.id = new_workspace_id.to_string(); + } else { + tracing::warn!("🔴migrate folder: more than one workspace"); + workspace.id = old_to_new_id_map.get_new_id(&workspace.id); + } + workspace + .child_views + .iter_mut() + .for_each(|view_identifier| { + view_identifier.id = old_to_new_id_map.get_new_id(&view_identifier.id); + }); + }); + + folder_data.views.iter_mut().for_each(|view| { + // 2. replace the old parent view id of the view + view.parent_view_id = old_to_new_id_map.get_new_id(&view.parent_view_id); + + // 3. replace the old id of the view + view.id = old_to_new_id_map.get_new_id(&view.id); + + // 4. replace the old id of the children views + view.children.iter_mut().for_each(|view_identifier| { + view_identifier.id = old_to_new_id_map.get_new_id(&view_identifier.id); + }); + }); + + match old_to_new_id_map.get(&folder_data.current_workspace_id) { + Some(new_workspace_id) => { + folder_data.current_workspace_id = new_workspace_id.clone(); + }, + None => { + tracing::error!("🔴migrate folder: current workspace id not found"); + }, + } + + match old_to_new_id_map.get(&folder_data.current_view) { + Some(new_view_id) => { + folder_data.current_view = new_view_id.clone(); + }, + None => { + tracing::error!("🔴migrate folder: current view id not found"); + folder_data.current_view = "".to_string(); + }, + } + + let origin = CollabOrigin::Client(CollabClient::new(new_uid, "phantom")); + let new_folder_collab = Collab::new_with_raw_data(origin, new_workspace_id, vec![], vec![]) + .map_err(|err| PersistenceError::Internal(Box::new(err)))?; + let mutex_collab = Arc::new(MutexCollab::from_collab(new_folder_collab)); + let _ = Folder::create(mutex_collab.clone(), None, Some(folder_data)); + + { + let mutex_collab = mutex_collab.lock(); + let txn = mutex_collab.transact(); + if let Err(err) = new_collab_w_txn.create_new_doc(new_uid, new_workspace_id, &txn) { + tracing::error!("🔴migrate folder failed: {:?}", err); + } + } + Ok(()) +} + +fn migrate_user_awareness( + old_to_new_id_map: &mut OldToNewIdMap, + old_user: &MigrationUser, + new_user: &MigrationUser, +) -> Result<(), PersistenceError> { + let old_uid = old_user.session.user_id; + let new_uid = new_user.session.user_id; + tracing::debug!("migrate user awareness from: {}, to: {}", old_uid, new_uid); + old_to_new_id_map.insert(old_uid.to_string(), new_uid.to_string()); + Ok(()) +} + +fn migrate_databases<'a, W>( + old_to_new_id_map: &Arc>, + new_user: &MigrationUser, + new_collab_w_txn: &'a W, + object_ids: &mut Vec, + collab_by_oid: &HashMap, +) -> Result<(), PersistenceError> +where + W: YrsDocAction<'a>, + PersistenceError: From, +{ + // Migrate databases + let mut database_object_ids = vec![]; + let database_row_object_ids = RwLock::new(HashSet::new()); + + for object_id in &mut *object_ids { + if let Some(collab) = collab_by_oid.get(object_id) { + if !is_database_collab(collab) { + continue; + } + + database_object_ids.push(object_id.clone()); + reset_inline_view_id(collab, |old_inline_view_id| { + old_to_new_id_map.lock().get_new_id(&old_inline_view_id) + }); + + mut_database_views_with_collab(collab, |database_view| { + let new_view_id = old_to_new_id_map.lock().get_new_id(&database_view.id); + let new_database_id = old_to_new_id_map + .lock() + .get_new_id(&database_view.database_id); + + tracing::trace!( + "migrate database view id from: {}, to: {}", + database_view.id, + new_view_id, + ); + tracing::trace!( + "migrate database view database id from: {}, to: {}", + database_view.database_id, + new_database_id, + ); + + database_view.id = new_view_id; + database_view.database_id = new_database_id; + database_view.row_orders.iter_mut().for_each(|row_order| { + let old_row_id = String::from(row_order.id.clone()); + let old_row_document_id = database_row_document_id_from_row_id(&old_row_id); + let new_row_id = old_to_new_id_map.lock().get_new_id(&old_row_id); + let new_row_document_id = database_row_document_id_from_row_id(&new_row_id); + tracing::debug!("migrate row id: {} to {}", row_order.id, new_row_id); + tracing::debug!( + "migrate row document id: {} to {}", + old_row_document_id, + new_row_document_id + ); + old_to_new_id_map + .lock() + .insert(old_row_document_id, new_row_document_id); + + row_order.id = RowId::from(new_row_id); + database_row_object_ids.write().insert(old_row_id); + }); + }); + + let new_object_id = old_to_new_id_map.lock().get_new_id(object_id); + tracing::debug!( + "migrate database from: {}, to: {}", + object_id, + new_object_id, + ); + migrate_collab_object( + collab, + new_user.session.user_id, + &new_object_id, + new_collab_w_txn, + ); + } + } + object_ids.retain(|id| !database_object_ids.contains(id)); + + let database_row_object_ids = database_row_object_ids.read(); + for object_id in &*database_row_object_ids { + if let Some(collab) = collab_by_oid.get(object_id) { + let new_object_id = old_to_new_id_map.lock().get_new_id(object_id); + tracing::info!( + "migrate database row from: {}, to: {}", + object_id, + new_object_id, + ); + mut_row_with_collab(collab, |row_update| { + row_update.set_row_id(RowId::from(new_object_id.clone())); + }); + migrate_collab_object( + collab, + new_user.session.user_id, + &new_object_id, + new_collab_w_txn, + ); + } + } + object_ids.retain(|id| !database_row_object_ids.contains(id)); + + Ok(()) +} + +fn make_collab_by_oid<'a, R>( + old_user: &MigrationUser, + old_collab_r_txn: &R, + object_ids: &[String], +) -> HashMap +where + R: YrsDocAction<'a>, + PersistenceError: From, +{ + let mut collab_by_oid = HashMap::new(); + for object_id in object_ids { + let collab = Collab::new(old_user.session.user_id, object_id, "phantom", vec![]); + match collab.with_origin_transact_mut(|txn| { + old_collab_r_txn.load_doc(old_user.session.user_id, &object_id, txn) + }) { + Ok(_) => { + collab_by_oid.insert(object_id.clone(), collab); + }, + Err(err) => tracing::error!("🔴Initialize migration collab failed: {:?} ", err), + } + } + + collab_by_oid +} diff --git a/frontend/rust-lib/flowy-user/src/migrations/mod.rs b/frontend/rust-lib/flowy-user/src/migrations/mod.rs index 6929f360df..b35e4377dc 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/mod.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/mod.rs @@ -1,6 +1,7 @@ +pub use define::*; + mod define; pub mod historical_document; -pub mod local_user_to_cloud; +pub mod migrate_to_new_user; pub mod migration; - -pub use define::*; +pub mod sync_new_user; diff --git a/frontend/rust-lib/flowy-user/src/migrations/sync_new_user.rs b/frontend/rust-lib/flowy-user/src/migrations/sync_new_user.rs new file mode 100644 index 0000000000..9b195e5f9e --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/migrations/sync_new_user.rs @@ -0,0 +1,327 @@ +use std::future::Future; +use std::ops::Deref; +use std::pin::Pin; +use std::sync::Arc; + +use anyhow::{anyhow, Error}; +use appflowy_integrate::{CollabObject, CollabType, PersistenceError, RocksCollabDB, YrsDocAction}; +use collab::core::collab::MutexCollab; +use collab::preclude::Collab; +use collab_database::database::get_database_row_ids; +use collab_database::rows::database_row_document_id_from_row_id; +use collab_database::user::{get_database_with_views, DatabaseWithViews}; +use collab_folder::core::{Folder, View, ViewLayout}; +use parking_lot::Mutex; + +use flowy_error::FlowyResult; +use flowy_user_deps::cloud::UserCloudService; + +use crate::migrations::MigrationUser; + +#[tracing::instrument(level = "info", skip_all, err)] +pub async fn sync_user_data_to_cloud( + user_service: Arc, + new_user: &MigrationUser, + collab_db: &Arc, +) -> FlowyResult<()> { + let workspace_id = new_user.session.user_workspace.id.clone(); + let uid = new_user.session.user_id; + let folder = Arc::new(sync_folder(uid, &workspace_id, collab_db, user_service.clone()).await?); + + let database_records = sync_database_views( + uid, + &workspace_id, + &new_user.session.user_workspace.database_views_aggregate_id, + collab_db, + user_service.clone(), + ) + .await; + + let views = folder.lock().get_current_workspace_views(); + for view in views { + let view_id = view.id.clone(); + if let Err(err) = sync_views( + uid, + folder.clone(), + database_records.clone(), + workspace_id.to_string(), + view, + collab_db.clone(), + user_service.clone(), + ) + .await + { + tracing::error!("🔴sync {} failed: {:?}", view_id, err); + } + } + Ok(()) +} + +fn sync_views( + uid: i64, + folder: Arc, + database_records: Vec>, + workspace_id: String, + view: Arc, + collab_db: Arc, + user_service: Arc, +) -> Pin> + Send + Sync>> { + Box::pin(async move { + let collab_type = collab_type_from_view_layout(&view.layout); + let object_id = object_id_from_view(&view, &database_records)?; + tracing::debug!( + "sync view: {:?}:{} with object_id: {}", + view.layout, + view.id, + object_id + ); + + let collab_object = + CollabObject::new(uid, object_id, collab_type).with_workspace_id(workspace_id.to_string()); + + match view.layout { + ViewLayout::Document => { + let update = get_collab_init_update(uid, &collab_object, &collab_db)?; + tracing::info!( + "sync object: {} with update: {}", + collab_object, + update.len() + ); + user_service + .create_collab_object(&collab_object, update) + .await?; + }, + ViewLayout::Grid | ViewLayout::Board | ViewLayout::Calendar => { + let (database_update, row_ids) = get_database_init_update(uid, &collab_object, &collab_db)?; + tracing::info!( + "sync object: {} with update: {}", + collab_object, + database_update.len() + ); + user_service + .create_collab_object(&collab_object, database_update) + .await?; + + // sync database's row + for row_id in row_ids { + tracing::debug!("sync row: {}", row_id); + let document_id = database_row_document_id_from_row_id(&row_id); + + let database_row_collab_object = CollabObject::new(uid, row_id, CollabType::DatabaseRow) + .with_workspace_id(workspace_id.to_string()); + let database_row_update = + get_collab_init_update(uid, &database_row_collab_object, &collab_db)?; + tracing::info!( + "sync object: {} with update: {}", + database_row_collab_object, + database_row_update.len() + ); + + let _ = user_service + .create_collab_object(&database_row_collab_object, database_row_update) + .await; + + let database_row_document = CollabObject::new(uid, document_id, CollabType::Document) + .with_workspace_id(workspace_id.to_string()); + // sync document in the row if exist + if let Ok(document_update) = + get_collab_init_update(uid, &database_row_document, &collab_db) + { + tracing::info!( + "sync database row document: {} with update: {}", + database_row_document, + document_update.len() + ); + let _ = user_service + .create_collab_object(&database_row_document, document_update) + .await; + } + } + }, + } + + let child_views = folder.lock().views.get_views_belong_to(&view.id); + for child_view in child_views { + let cloned_child_view = child_view.clone(); + if let Err(err) = Box::pin(sync_views( + uid, + folder.clone(), + database_records.clone(), + workspace_id.clone(), + child_view, + collab_db.clone(), + user_service.clone(), + )) + .await + { + tracing::error!( + "🔴sync {:?}:{} failed: {:?}", + cloned_child_view.layout, + cloned_child_view.id, + err + ) + } + } + Ok(()) + }) +} + +fn get_collab_init_update( + uid: i64, + collab_object: &CollabObject, + collab_db: &Arc, +) -> Result, PersistenceError> { + let collab = Collab::new(uid, &collab_object.object_id, "phantom", vec![]); + let _ = collab.with_origin_transact_mut(|txn| { + collab_db + .read_txn() + .load_doc(uid, &collab_object.object_id, txn) + })?; + let update = collab.encode_as_update_v1().0; + if update.is_empty() { + return Err(PersistenceError::UnexpectedEmptyUpdates); + } + + Ok(update) +} + +fn get_database_init_update( + uid: i64, + collab_object: &CollabObject, + collab_db: &Arc, +) -> Result<(Vec, Vec), PersistenceError> { + let collab = Collab::new(uid, &collab_object.object_id, "phantom", vec![]); + let _ = collab.with_origin_transact_mut(|txn| { + collab_db + .read_txn() + .load_doc(uid, &collab_object.object_id, txn) + })?; + + let row_ids = get_database_row_ids(&collab).unwrap_or_default(); + let update = collab.encode_as_update_v1().0; + if update.is_empty() { + return Err(PersistenceError::UnexpectedEmptyUpdates); + } + + Ok((update, row_ids)) +} + +async fn sync_folder( + uid: i64, + workspace_id: &str, + collab_db: &Arc, + user_service: Arc, +) -> Result { + let (folder, update) = { + let collab = Collab::new(uid, workspace_id, "phantom", vec![]); + // Use the temporary result to short the lifetime of the TransactionMut + collab.with_origin_transact_mut(|txn| collab_db.read_txn().load_doc(uid, workspace_id, txn))?; + let update = collab.encode_as_update_v1().0; + ( + MutexFolder::new(Folder::open( + Arc::new(MutexCollab::from_collab(collab)), + None, + )), + update, + ) + }; + + let collab_object = CollabObject::new(uid, workspace_id.to_string(), CollabType::Folder) + .with_workspace_id(workspace_id.to_string()); + tracing::info!( + "sync object: {} with update: {}", + collab_object, + update.len() + ); + if let Err(err) = user_service + .create_collab_object(&collab_object, update) + .await + { + tracing::error!("🔴sync folder failed: {:?}", err); + } + + Ok(folder) +} + +async fn sync_database_views( + uid: i64, + workspace_id: &str, + database_views_aggregate_id: &str, + collab_db: &Arc, + user_service: Arc, +) -> Vec> { + let collab_object = CollabObject::new( + uid, + database_views_aggregate_id.to_string(), + CollabType::WorkspaceDatabase, + ) + .with_workspace_id(workspace_id.to_string()); + + // Use the temporary result to short the lifetime of the TransactionMut + let result = { + let collab = Collab::new(uid, database_views_aggregate_id, "phantom", vec![]); + collab + .with_origin_transact_mut(|txn| { + collab_db + .read_txn() + .load_doc(uid, database_views_aggregate_id, txn) + }) + .map(|_| { + ( + get_database_with_views(&collab), + collab.encode_as_update_v1().0, + ) + }) + }; + + if let Ok((records, update)) = result { + let _ = user_service + .create_collab_object(&collab_object, update) + .await; + records.into_iter().map(Arc::new).collect() + } else { + vec![] + } +} + +struct MutexFolder(Mutex); +impl MutexFolder { + pub fn new(folder: Folder) -> Self { + Self(Mutex::new(folder)) + } +} +impl Deref for MutexFolder { + type Target = Mutex; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +unsafe impl Sync for MutexFolder {} +unsafe impl Send for MutexFolder {} + +fn collab_type_from_view_layout(view_layout: &ViewLayout) -> CollabType { + match view_layout { + ViewLayout::Document => CollabType::Document, + ViewLayout::Grid | ViewLayout::Board | ViewLayout::Calendar => CollabType::Database, + } +} + +fn object_id_from_view( + view: &Arc, + database_records: &[Arc], +) -> Result { + if view.layout.is_database() { + match database_records + .iter() + .find(|record| record.linked_views.contains(&view.id)) + { + None => Err(anyhow!( + "🔴sync view: {} failed: no database for this view", + view.id + )), + Some(record) => Ok(record.database_id.clone()), + } + } else { + Ok(view.id.clone()) + } +} diff --git a/frontend/rust-lib/flowy-user/src/services/entities.rs b/frontend/rust-lib/flowy-user/src/services/entities.rs index 409d492cf4..eba4304c0a 100644 --- a/frontend/rust-lib/flowy-user/src/services/entities.rs +++ b/frontend/rust-lib/flowy-user/src/services/entities.rs @@ -66,7 +66,7 @@ impl<'de> Visitor<'de> for SessionVisitor { name: "My Workspace".to_string(), created_at: Utc::now(), // For historical reasons, the database_storage_id is constructed by the user_id. - database_storage_id: STANDARD.encode(format!("{}:user:database", user_id)), + database_views_aggregate_id: STANDARD.encode(format!("{}:user:database", user_id)), }) } } diff --git a/frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs index 457fd24d9a..0bfe89f79f 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_workspace_sql.rs @@ -23,7 +23,7 @@ impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable { if value.1.id.is_empty() { return Err(FlowyError::invalid_data().with_context("The id is empty")); } - if value.1.database_storage_id.is_empty() { + if value.1.database_views_aggregate_id.is_empty() { return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); } @@ -32,7 +32,7 @@ impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable { name: value.1.name.clone(), uid: value.0, created_at: value.1.created_at.timestamp(), - database_storage_id: value.1.database_storage_id.clone(), + database_storage_id: value.1.database_views_aggregate_id.clone(), }) } } @@ -46,7 +46,7 @@ impl From for UserWorkspace { .timestamp_opt(value.created_at, 0) .single() .unwrap_or_default(), - database_storage_id: value.database_storage_id, + database_views_aggregate_id: value.database_storage_id, } } }