From f9e7b5ffa49b1ced1c5f801ad40ca3ce97029915 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Fri, 14 Jul 2023 13:37:13 +0800 Subject: [PATCH] feat: reload UI (#2999) * chore: reload folder * chore: reload folder * chore: init sync * chore: update tables * chore: update database * chore: load row * chore: update * chore: reload row * test: fit test * chore: retry * chore: support batch fetch * chore: enable sync * chore: sync switch * chore: sync switch * chore: migration user data * chore: migrate data * chore: migrate folder * chore: save user email * chore: refresh user profile * chore: fix test * chore: delete translation files * test: clippy format --- .../lib/core/config/config.dart | 30 -- frontend/appflowy_flutter/lib/env/env.dart | 7 + .../application/cell/cell_cache.dart | 20 +- .../application/cell/cell_controller.dart | 19 +- .../cell/cell_controller_builder.dart | 4 +- .../database_view/application/defines.dart | 4 +- .../application/field/field_controller.dart | 22 +- .../application/row/row_cache.dart | 142 +++---- ...ta_controller.dart => row_controller.dart} | 6 +- .../application/row/row_listener.dart | 68 ++++ .../application/view/view_cache.dart | 6 +- .../board/presentation/board_page.dart | 7 +- .../calendar/application/calendar_bloc.dart | 2 +- .../application/unschedule_event_bloc.dart | 2 +- .../calendar/presentation/calendar_page.dart | 2 +- .../grid/application/grid_bloc.dart | 4 +- .../grid/application/row/row_bloc.dart | 33 +- .../grid/application/row/row_detail_bloc.dart | 2 +- .../grid/presentation/grid_page.dart | 2 +- .../grid/presentation/widgets/row/row.dart | 7 +- .../database_view/widgets/card/card_bloc.dart | 8 +- .../widgets/card/card_cell_builder.dart | 2 +- .../widgets/row/cell_builder.dart | 2 +- .../database_view/widgets/row/row_action.dart | 2 +- .../database_view/widgets/row/row_detail.dart | 2 +- .../lib/plugins/document/document_page.dart | 14 +- .../lib/startup/tasks/rust_sdk.dart | 1 + .../lib/startup/tasks/supabase_task.dart | 11 - .../auth/appflowy_auth_service.dart | 5 +- .../user/application/auth/auth_service.dart | 5 +- .../auth/supabase_auth_service.dart | 5 +- .../lib/user/application/user_service.dart | 5 +- .../workspace/application/menu/menu_bloc.dart | 4 +- .../settings/setting_supabase_bloc.dart | 73 ++++ .../settings/settings_dialog_bloc.dart | 1 + .../workspace/workspace_service.dart | 2 +- .../settings/settings_dialog.dart | 3 + .../widgets/setting_supabase_view.dart | 30 ++ .../widgets/setting_third_party_login.dart | 41 ++ .../settings/widgets/settings_menu.dart | 15 + .../settings/widgets/settings_user_view.dart | 23 +- .../appflowy_backend/lib/env_serde.dart | 11 +- .../appflowy_backend/lib/env_serde.i.dart | 2 + .../test/bloc_test/board_test/util.dart | 2 +- .../test/bloc_test/grid_test/util.dart | 2 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 12 +- frontend/appflowy_tauri/src-tauri/Cargo.toml | 12 +- .../stores/effects/user/user_bd_svc.ts | 9 +- frontend/flowy-server-config/Cargo.toml | 8 + frontend/flowy-server-config/src/lib.rs | 14 + frontend/resources/translations/en.json | 3 +- frontend/rust-lib/Cargo.lock | 39 +- frontend/rust-lib/Cargo.toml | 11 +- frontend/rust-lib/dart-ffi/Cargo.toml | 1 + frontend/rust-lib/dart-ffi/src/env_serde.rs | 3 +- frontend/rust-lib/dart-ffi/src/lib.rs | 1 - .../rust-lib/flowy-config/src/entities.rs | 61 --- .../flowy-config/src/event_handler.rs | 11 +- .../rust-lib/flowy-config/src/event_map.rs | 6 - frontend/rust-lib/flowy-core/Cargo.toml | 1 + .../src/deps_resolve/collab_deps.rs | 6 +- .../src/deps_resolve/database_deps.rs | 4 +- .../src/deps_resolve/document2_deps.rs | 4 +- .../src/deps_resolve/folder2_deps.rs | 10 +- .../flowy-core/src/deps_resolve/mod.rs | 1 + .../flowy-core/src/deps_resolve/user_deps.rs | 1 + .../flowy-core/src/integrate/server.rs | 165 +++++--- frontend/rust-lib/flowy-core/src/lib.rs | 41 +- frontend/rust-lib/flowy-database2/src/deps.rs | 15 +- .../flowy-database2/src/entities/macros.rs | 2 +- .../src/entities/row_entities.rs | 9 - .../src/entities/view_entities.rs | 61 ++- .../rust-lib/flowy-database2/src/manager.rs | 295 ++++++++------ .../flowy-database2/src/notification.rs | 4 + .../src/services/database/database_editor.rs | 94 ++--- .../src/services/database/entities.rs | 8 +- .../src/services/database_view/layout_deps.rs | 15 +- .../src/services/database_view/view_editor.rs | 38 +- .../src/services/database_view/view_filter.rs | 3 +- .../src/services/database_view/view_sort.rs | 2 +- .../src/services/database_view/views.rs | 9 +- .../select_type_option.rs | 3 +- .../src/services/filter/controller.rs | 3 +- .../src/services/group/action.rs | 3 +- .../src/services/group/controller.rs | 3 +- .../controller_impls/checkbox_controller.rs | 3 +- .../controller_impls/default_controller.rs | 3 +- .../multi_select_controller.rs | 3 +- .../single_select_controller.rs | 3 +- .../select_option_controller/util.rs | 3 +- .../group/controller_impls/url_controller.rs | 3 +- .../src/services/group/entities.rs | 4 +- .../src/services/group/group_builder.rs | 2 +- .../src/services/sort/controller.rs | 3 +- .../tests/database/database_editor.rs | 4 +- frontend/rust-lib/flowy-document2/src/deps.rs | 5 +- .../rust-lib/flowy-document2/src/document.rs | 2 +- .../rust-lib/flowy-document2/src/entities.rs | 12 +- .../flowy-document2/src/event_handler.rs | 12 +- .../rust-lib/flowy-document2/src/manager.rs | 55 ++- .../tests/document/document_insert_test.rs | 2 +- .../tests/document/document_redo_undo_test.rs | 2 +- .../tests/document/document_test.rs | 13 +- .../flowy-document2/tests/document/util.rs | 14 +- frontend/rust-lib/flowy-error/src/code.rs | 3 + frontend/rust-lib/flowy-folder2/src/deps.rs | 17 +- .../flowy-folder2/src/event_handler.rs | 4 +- .../rust-lib/flowy-folder2/src/event_map.rs | 4 +- .../rust-lib/flowy-folder2/src/manager.rs | 231 +++++++---- .../flowy-folder2/src/user_default.rs | 28 +- .../rust-lib/flowy-server-config/Cargo.toml | 10 + .../rust-lib/flowy-server-config/src/lib.rs | 1 + .../src/supabase_config.rs} | 18 +- frontend/rust-lib/flowy-server/Cargo.toml | 3 + frontend/rust-lib/flowy-server/src/lib.rs | 1 + .../src/local_server/impls/database.rs | 17 +- .../src/local_server/impls/document.rs | 9 +- .../src/local_server/impls/folder.rs | 36 +- .../src/local_server/impls/user.rs | 24 +- .../flowy-server/src/local_server/server.rs | 51 ++- .../src/self_host/impls/database.rs | 17 +- .../src/self_host/impls/document.rs | 9 +- .../src/self_host/impls/folder.rs | 16 +- .../src/supabase/impls/collab_storage.rs | 314 ++++++++++++--- .../src/supabase/impls/database.rs | 100 +++-- .../src/supabase/impls/document.rs | 88 ++++- .../flowy-server/src/supabase/impls/folder.rs | 125 ++++-- .../flowy-server/src/supabase/impls/mod.rs | 6 +- .../flowy-server/src/supabase/impls/user.rs | 135 ++++--- .../flowy-server/src/supabase/migration.rs | 5 +- .../migrations/initial/Initial_down.sql | 3 + .../migrations/initial/V1__Initial_Up.sql | 26 +- .../rust-lib/flowy-server/src/supabase/mod.rs | 4 +- .../src/supabase/{pg_db.rs => postgres_db.rs} | 5 +- .../flowy-server/src/supabase/queue.rs | 19 +- .../flowy-server/src/supabase/server.rs | 139 +++++-- .../flowy-server/src/supabase/sql_builder.rs | 76 +++- frontend/rust-lib/flowy-server/src/util.rs | 15 - .../tests/supabase_test/user_test.rs | 38 +- .../2023-07-12-135810_user_auth_type/down.sql | 3 + .../2023-07-12-135810_user_auth_type/up.sql | 3 + frontend/rust-lib/flowy-sqlite/src/lib.rs | 9 +- frontend/rust-lib/flowy-sqlite/src/schema.rs | 1 + .../rust-lib/flowy-sqlite/src/sqlite/pool.rs | 17 +- frontend/rust-lib/flowy-test/Cargo.toml | 2 + .../flowy-test/src/document/document_event.rs | 2 +- frontend/rust-lib/flowy-test/src/lib.rs | 30 +- .../rust-lib/flowy-test/src/user_event.rs | 20 +- .../tests/database/local_test/test.rs | 80 ++-- .../tests/database/supabase_test/helper.rs | 6 +- .../tests/folder/local_test/test.rs | 28 +- .../tests/folder/supabase_test/helper.rs | 2 +- frontend/rust-lib/flowy-test/tests/main.rs | 2 + .../rust-lib/flowy-test/tests/tool/mod.rs | 2 + .../flowy-test/tests/tool/pg_migration.rs | 74 ++++ .../rust-lib/flowy-test/tests/tool/pg_row.rs | 172 +++++++++ .../tests/user/local_test/auth_test.rs | 5 +- .../tests/user/supabase_test/auth_test.rs | 96 ++++- frontend/rust-lib/flowy-test/tests/util.rs | 2 +- frontend/rust-lib/flowy-user/Cargo.toml | 4 + .../rust-lib/flowy-user/src/entities/auth.rs | 22 +- .../flowy-user/src/entities/user_profile.rs | 6 +- .../flowy-user/src/entities/user_setting.rs | 88 ++++- .../rust-lib/flowy-user/src/event_handler.rs | 60 +-- frontend/rust-lib/flowy-user/src/event_map.rs | 76 +++- .../flowy-user/src/services/database.rs | 181 +++++---- .../rust-lib/flowy-user/src/services/mod.rs | 6 +- .../flowy-user/src/services/user_data.rs | 91 +++++ .../flowy-user/src/services/user_session.rs | 363 ++++++++++++------ frontend/rust-lib/lib-log/src/lib.rs | 13 +- 170 files changed, 3380 insertions(+), 1482 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/core/config/config.dart rename frontend/appflowy_flutter/lib/plugins/database_view/application/row/{row_data_controller.dart => row_controller.dart} (85%) create mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_listener.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/application/settings/setting_supabase_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_view.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart create mode 100644 frontend/flowy-server-config/Cargo.toml create mode 100644 frontend/flowy-server-config/src/lib.rs create mode 100644 frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs create mode 100644 frontend/rust-lib/flowy-server-config/Cargo.toml create mode 100644 frontend/rust-lib/flowy-server-config/src/lib.rs rename frontend/rust-lib/{flowy-server/src/supabase/configuration.rs => flowy-server-config/src/supabase_config.rs} (85%) rename frontend/rust-lib/flowy-server/src/supabase/{pg_db.rs => postgres_db.rs} (93%) create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2023-07-12-135810_user_auth_type/down.sql create mode 100644 frontend/rust-lib/flowy-sqlite/migrations/2023-07-12-135810_user_auth_type/up.sql create mode 100644 frontend/rust-lib/flowy-test/tests/tool/mod.rs create mode 100644 frontend/rust-lib/flowy-test/tests/tool/pg_migration.rs create mode 100644 frontend/rust-lib/flowy-test/tests/tool/pg_row.rs create mode 100644 frontend/rust-lib/flowy-user/src/services/user_data.rs diff --git a/frontend/appflowy_flutter/lib/core/config/config.dart b/frontend/appflowy_flutter/lib/core/config/config.dart deleted file mode 100644 index b90f395250..0000000000 --- a/frontend/appflowy_flutter/lib/core/config/config.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-config/entities.pb.dart'; - -class Config { - static Future setSupabaseConfig({ - required String url, - required String anonKey, - required String key, - required String secret, - required String pgUrl, - required String pgUser, - required String pgPassword, - required String pgPort, - }) async { - final postgresConfig = PostgresConfigurationPB.create() - ..url = pgUrl - ..userName = pgUser - ..password = pgPassword - ..port = int.parse(pgPort); - - await ConfigEventSetSupabaseConfig( - SupabaseConfigPB.create() - ..supabaseUrl = url - ..key = key - ..anonKey = anonKey - ..jwtSecret = secret - ..postgresConfig = postgresConfig, - ).send(); - } -} diff --git a/frontend/appflowy_flutter/lib/env/env.dart b/frontend/appflowy_flutter/lib/env/env.dart index d45e6ac1d3..f5b86f782c 100644 --- a/frontend/appflowy_flutter/lib/env/env.dart +++ b/frontend/appflowy_flutter/lib/env/env.dart @@ -68,6 +68,13 @@ abstract class Env { defaultValue: '5432', ) static final String supabaseDbPort = _Env.supabaseDbPort; + + @EnviedField( + obfuscate: true, + varName: 'ENABLE_SUPABASE_SYNC', + defaultValue: true, + ) + static final bool enableSupabaseSync = _Env.enableSupabaseSync; } bool get isSupabaseEnable => diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart index f66fe1f3bb..86810aa7ee 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_cache.dart @@ -24,38 +24,38 @@ class CellCacheKey { /// We use GridCellCacheKey to index the cell in the cache. /// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid /// for more information -class CellCache { +class CellMemCache { final String viewId; /// fieldId: {cacheKey: GridCell} - final Map> _cellDataByFieldId = {}; - CellCache({ + final Map> _cellByFieldId = {}; + CellMemCache({ required this.viewId, }); void removeCellWithFieldId(String fieldId) { - _cellDataByFieldId.remove(fieldId); + _cellByFieldId.remove(fieldId); } void remove(CellCacheKey key) { - final map = _cellDataByFieldId[key.fieldId]; + final map = _cellByFieldId[key.fieldId]; if (map != null) { map.remove(key.rowId); } } void insert(CellCacheKey key, T value) { - var map = _cellDataByFieldId[key.fieldId]; + var map = _cellByFieldId[key.fieldId]; if (map == null) { - _cellDataByFieldId[key.fieldId] = {}; - map = _cellDataByFieldId[key.fieldId]; + _cellByFieldId[key.fieldId] = {}; + map = _cellByFieldId[key.fieldId]; } map![key.rowId] = value.object; } T? get(CellCacheKey key) { - final map = _cellDataByFieldId[key.fieldId]; + final map = _cellByFieldId[key.fieldId]; if (map == null) { return null; } else { @@ -72,6 +72,6 @@ class CellCache { } Future dispose() async { - _cellDataByFieldId.clear(); + _cellByFieldId.clear(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart index 586734a96e..6a5d55b813 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller.dart @@ -24,7 +24,7 @@ import 'cell_service.dart'; // ignore: must_be_immutable class CellController extends Equatable { DatabaseCellContext _cellContext; - final CellCache _cellCache; + final CellMemCache _cellCache; final CellCacheKey _cacheKey; final FieldBackendService _fieldBackendSvc; final CellDataLoader _cellDataLoader; @@ -54,7 +54,7 @@ class CellController extends Equatable { CellController({ required DatabaseCellContext cellContext, - required CellCache cellCache, + required CellMemCache cellCache, required CellDataLoader cellDataLoader, required CellDataPersistence cellDataPersistence, }) : _cellContext = cellContext, @@ -103,12 +103,15 @@ class CellController extends Equatable { }, ); - _rowMetaListener?.start( - callback: (newRowMeta) { - _cellContext = _cellContext.copyWith(rowMeta: newRowMeta); - _onRowMetaChanged?.call(); - }, - ); + // Only the primary can listen on the row meta changes. + if (_cellContext.fieldInfo.isPrimary) { + _rowMetaListener?.start( + callback: (newRowMeta) { + _cellContext = _cellContext.copyWith(rowMeta: newRowMeta); + _onRowMetaChanged?.call(); + }, + ); + } } /// Listen on the cell content or field changes diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart index 70fee21e35..73f995117e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart @@ -18,11 +18,11 @@ typedef URLCellController = CellController; class CellControllerBuilder { final DatabaseCellContext _cellContext; - final CellCache _cellCache; + final CellMemCache _cellCache; CellControllerBuilder({ required DatabaseCellContext cellContext, - required CellCache cellCache, + required CellMemCache cellCache, }) : _cellCache = cellCache, _cellContext = cellContext; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart index 4ec8fbc0dc..6fe6908476 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/defines.dart @@ -17,13 +17,13 @@ typedef OnDatabaseChanged = void Function(DatabasePB); typedef OnRowsCreated = void Function(List ids); typedef OnRowsUpdated = void Function( List ids, - RowsChangedReason reason, + ChangedReason reason, ); typedef OnRowsDeleted = void Function(List ids); typedef OnNumOfRowsChanged = void Function( UnmodifiableListView rows, UnmodifiableMapView rowByRowId, - RowsChangedReason reason, + ChangedReason reason, ); typedef OnError = void Function(FlowyError); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart index 7d277553ed..ed77f9943d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_controller.dart @@ -687,27 +687,29 @@ class FieldController { } } -class RowDelegatesImpl extends RowFieldsDelegate with RowCacheDelegate { - final FieldController _cache; +class RowCacheDependenciesImpl extends RowFieldsDelegate with RowLifeCycle { + final FieldController _fieldController; OnReceiveFields? _onFieldFn; - RowDelegatesImpl(FieldController cache) : _cache = cache; + RowCacheDependenciesImpl(FieldController cache) : _fieldController = cache; @override UnmodifiableListView get fields => - UnmodifiableListView(_cache.fieldInfos); + UnmodifiableListView(_fieldController.fieldInfos); @override void onFieldsChanged(void Function(List) callback) { - _onFieldFn = (fieldInfos) { - callback(fieldInfos); - }; - _cache.addListener(onReceiveFields: _onFieldFn); + if (_onFieldFn != null) { + _fieldController.removeListener(onFieldsListener: _onFieldFn!); + } + + _onFieldFn = (fieldInfos) => callback(fieldInfos); + _fieldController.addListener(onReceiveFields: _onFieldFn); } @override - void onRowDispose() { + void onRowDisposed() { if (_onFieldFn != null) { - _cache.removeListener(onFieldsListener: _onFieldFn!); + _fieldController.removeListener(onFieldsListener: _onFieldFn!); _onFieldFn = null; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart index 83656a7ff9..63173c4c98 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_cache.dart @@ -13,30 +13,25 @@ part 'row_cache.freezed.dart'; typedef RowUpdateCallback = void Function(); +/// A delegate that provides the fields of the row. abstract class RowFieldsDelegate { + UnmodifiableListView get fields; void onFieldsChanged(void Function(List) callback); } -abstract mixin class RowCacheDelegate { - UnmodifiableListView get fields; - void onRowDispose(); +abstract mixin class RowLifeCycle { + void onRowDisposed(); } -/// Cache the rows in memory -/// Insert / delete / update row -/// /// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information. class RowCache { final String viewId; - - /// _rows contains the current block's rows - /// Use List to reverse the order of the GridRow. final RowList _rowList = RowList(); - - final CellCache _cellCache; - final RowCacheDelegate _delegate; - final RowChangesetNotifier _rowChangeReasonNotifier; + final CellMemCache _cellMemCache; + final RowLifeCycle _rowLifeCycle; + final RowFieldsDelegate _fieldDelegate; + final RowChangesetNotifier _changedNotifier; /// Returns a unmodifiable list of RowInfo UnmodifiableListView get rowInfos { @@ -44,29 +39,29 @@ class RowCache { return UnmodifiableListView(visibleRows); } - /// Returns a unmodifiable map of rowId to RowInfo + /// Returns a unmodifiable map of RowInfo UnmodifiableMapView get rowByRowId { return UnmodifiableMapView(_rowList.rowInfoByRowId); } - CellCache get cellCache => _cellCache; - - RowsChangedReason get changeReason => _rowChangeReasonNotifier.reason; + CellMemCache get cellCache => _cellMemCache; + ChangedReason get changeReason => _changedNotifier.reason; RowCache({ required this.viewId, required RowFieldsDelegate fieldsDelegate, - required RowCacheDelegate cacheDelegate, - }) : _cellCache = CellCache(viewId: viewId), - _rowChangeReasonNotifier = RowChangesetNotifier(), - _delegate = cacheDelegate { - // + required RowLifeCycle rowLifeCycle, + }) : _cellMemCache = CellMemCache(viewId: viewId), + _changedNotifier = RowChangesetNotifier(), + _rowLifeCycle = rowLifeCycle, + _fieldDelegate = fieldsDelegate { + // Listen on the changed of the fields. If the fields changed, we need to + // clear the cell cache with the given field id. fieldsDelegate.onFieldsChanged((fieldInfos) { for (final fieldInfo in fieldInfos) { - _cellCache.removeCellWithFieldId(fieldInfo.id); + _cellMemCache.removeCellWithFieldId(fieldInfo.id); } - _rowChangeReasonNotifier - .receive(const RowsChangedReason.fieldDidChange()); + _changedNotifier.receive(const ChangedReason.fieldDidChange()); }); } @@ -82,9 +77,9 @@ class RowCache { } Future dispose() async { - _delegate.onRowDispose(); - _rowChangeReasonNotifier.dispose(); - await _cellCache.dispose(); + _rowLifeCycle.onRowDisposed(); + _changedNotifier.dispose(); + await _cellMemCache.dispose(); } void applyRowsChanged(RowsChangePB changeset) { @@ -100,7 +95,7 @@ class RowCache { void reorderAllRows(List rowIds) { _rowList.reorderWithRowIds(rowIds); - _rowChangeReasonNotifier.receive(const RowsChangedReason.reorderRows()); + _changedNotifier.receive(const ChangedReason.reorderRows()); } void reorderSingleRow(ReorderSingleRowPB reorderRow) { @@ -111,8 +106,8 @@ class RowCache { reorderRow.oldIndex, reorderRow.newIndex, ); - _rowChangeReasonNotifier.receive( - RowsChangedReason.reorderSingleRow( + _changedNotifier.receive( + ChangedReason.reorderSingleRow( reorderRow, rowInfo, ), @@ -124,7 +119,7 @@ class RowCache { for (final rowId in deletedRowIds) { final deletedRow = _rowList.remove(rowId); if (deletedRow != null) { - _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRow)); + _changedNotifier.receive(ChangedReason.delete(deletedRow)); } } } @@ -134,8 +129,7 @@ class RowCache { final insertedIndex = _rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta)); if (insertedIndex != null) { - _rowChangeReasonNotifier - .receive(RowsChangedReason.insert(insertedIndex)); + _changedNotifier.receive(ChangedReason.insert(insertedIndex)); } } } @@ -149,7 +143,7 @@ class RowCache { fieldId: fieldId, rowId: updatedRow.rowId, ); - _cellCache.remove(key); + _cellMemCache.remove(key); } if (updatedRow.hasRowMeta()) { updatedList.add(updatedRow.rowMeta); @@ -160,7 +154,7 @@ class RowCache { _rowList.updateRows(updatedList, (rowId) => buildGridRow(rowId)); if (updatedIndexs.isNotEmpty) { - _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs)); + _changedNotifier.receive(ChangedReason.update(updatedIndexs)); } } @@ -168,7 +162,7 @@ class RowCache { for (final rowId in invisibleRows) { final deletedRow = _rowList.remove(rowId); if (deletedRow != null) { - _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRow)); + _changedNotifier.receive(ChangedReason.delete(deletedRow)); } } } @@ -178,62 +172,45 @@ class RowCache { final insertedIndex = _rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta)); if (insertedIndex != null) { - _rowChangeReasonNotifier - .receive(RowsChangedReason.insert(insertedIndex)); + _changedNotifier.receive(ChangedReason.insert(insertedIndex)); } } } - void onRowsChanged(void Function(RowsChangedReason) onRowChanged) { - _rowChangeReasonNotifier.addListener(() { - onRowChanged(_rowChangeReasonNotifier.reason); + void onRowsChanged(void Function(ChangedReason) onRowChanged) { + _changedNotifier.addListener(() { + onRowChanged(_changedNotifier.reason); }); } RowUpdateCallback addListener({ required RowId rowId, - void Function(CellContextByFieldId, RowsChangedReason)? onCellUpdated, - bool Function()? listenWhen, + void Function(CellContextByFieldId, ChangedReason)? onRowChanged, }) { listenerHandler() async { - if (listenWhen != null && listenWhen() == false) { - return; - } - - notifyUpdate() { - if (onCellUpdated != null) { - final rowInfo = _rowList.get(rowId); - if (rowInfo != null) { - final CellContextByFieldId cellDataMap = _makeGridCells( - rowInfo.rowMeta, - ); - onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason); - } + if (onRowChanged != null) { + final rowInfo = _rowList.get(rowId); + if (rowInfo != null) { + final cellDataMap = _makeCells(rowInfo.rowMeta); + onRowChanged(cellDataMap, _changedNotifier.reason); } } - - _rowChangeReasonNotifier.reason.whenOrNull( - update: (indexs) { - if (indexs[rowId] != null) notifyUpdate(); - }, - fieldDidChange: () => notifyUpdate(), - ); } - _rowChangeReasonNotifier.addListener(listenerHandler); + _changedNotifier.addListener(listenerHandler); return listenerHandler; } void removeRowListener(VoidCallback callback) { - _rowChangeReasonNotifier.removeListener(callback); + _changedNotifier.removeListener(callback); } - CellContextByFieldId loadGridCells(RowMetaPB rowMeta) { + CellContextByFieldId loadCells(RowMetaPB rowMeta) { final rowInfo = _rowList.get(rowMeta.id); if (rowInfo == null) { _loadRow(rowMeta.id); } - return _makeGridCells(rowMeta); + return _makeCells(rowMeta); } Future _loadRow(RowId rowId) async { @@ -257,18 +234,17 @@ class RowCache { rowId: rowMetaPB.id, ); - _rowChangeReasonNotifier - .receive(RowsChangedReason.update(updatedIndexs)); + _changedNotifier.receive(ChangedReason.update(updatedIndexs)); } }, (err) => Log.error(err), ); } - CellContextByFieldId _makeGridCells(RowMetaPB rowMeta) { + CellContextByFieldId _makeCells(RowMetaPB rowMeta) { // ignore: prefer_collection_literals final cellContextMap = CellContextByFieldId(); - for (final field in _delegate.fields) { + for (final field in _fieldDelegate.fields) { if (field.visibility) { cellContextMap[field.id] = DatabaseCellContext( rowMeta: rowMeta, @@ -283,7 +259,7 @@ class RowCache { RowInfo buildGridRow(RowMetaPB rowMetaPB) { return RowInfo( viewId: viewId, - fields: _delegate.fields, + fields: _fieldDelegate.fields, rowId: rowMetaPB.id, rowMeta: rowMetaPB, ); @@ -291,11 +267,11 @@ class RowCache { } class RowChangesetNotifier extends ChangeNotifier { - RowsChangedReason reason = const InitialListState(); + ChangedReason reason = const InitialListState(); RowChangesetNotifier(); - void receive(RowsChangedReason newReason) { + void receive(ChangedReason newReason) { reason = newReason; reason.map( insert: (_) => notifyListeners(), @@ -326,14 +302,14 @@ typedef DeletedIndexs = List; typedef UpdatedIndexMap = LinkedHashMap; @freezed -class RowsChangedReason with _$RowsChangedReason { - const factory RowsChangedReason.insert(InsertedIndex item) = _Insert; - const factory RowsChangedReason.delete(DeletedIndex item) = _Delete; - const factory RowsChangedReason.update(UpdatedIndexMap indexs) = _Update; - const factory RowsChangedReason.fieldDidChange() = _FieldDidChange; - const factory RowsChangedReason.initial() = InitialListState; - const factory RowsChangedReason.reorderRows() = _ReorderRows; - const factory RowsChangedReason.reorderSingleRow( +class ChangedReason with _$ChangedReason { + const factory ChangedReason.insert(InsertedIndex item) = _Insert; + const factory ChangedReason.delete(DeletedIndex item) = _Delete; + const factory ChangedReason.update(UpdatedIndexMap indexs) = _Update; + const factory ChangedReason.fieldDidChange() = _FieldDidChange; + const factory ChangedReason.initial() = InitialListState; + const factory ChangedReason.reorderRows() = _ReorderRows; + const factory ChangedReason.reorderSingleRow( ReorderSingleRowPB reorderRow, RowInfo rowInfo, ) = _ReorderSingleRow; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_controller.dart similarity index 85% rename from frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart rename to frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_controller.dart index 4dacd7310a..b7102cfe4f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_data_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_controller.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import '../cell/cell_service.dart'; import 'row_cache.dart'; -typedef OnRowChanged = void Function(CellContextByFieldId, RowsChangedReason); +typedef OnRowChanged = void Function(CellContextByFieldId, ChangedReason); class RowController { final RowMetaPB rowMeta; @@ -24,13 +24,13 @@ class RowController { }) : _rowCache = rowCache; CellContextByFieldId loadData() { - return _rowCache.loadGridCells(rowMeta); + return _rowCache.loadCells(rowMeta); } void addListener({OnRowChanged? onRowChanged}) { final fn = _rowCache.addListener( rowId: rowMeta.id, - onCellUpdated: onRowChanged, + onRowChanged: onRowChanged, ); // Add the listener to the list so that we can remove it later. diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_listener.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_listener.dart new file mode 100644 index 0000000000..3854fff56b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/row/row_listener.dart @@ -0,0 +1,68 @@ +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:dartz/dartz.dart'; + +typedef DidFetchRowCallback = void Function(DidFetchRowPB); +typedef RowMetaCallback = void Function(RowMetaPB); + +class RowListener { + final String rowId; + DidFetchRowCallback? _onRowFetchedCallback; + RowMetaCallback? _onMetaChangedCallback; + DatabaseNotificationListener? _listener; + RowListener(this.rowId); + + /// OnMetaChanged will be called when the row meta is changed. + /// OnRowFetched will be called when the row is fetched from remote storage + void start({ + RowMetaCallback? onMetaChanged, + DidFetchRowCallback? onRowFetched, + }) { + _onMetaChangedCallback = onMetaChanged; + _onRowFetchedCallback = onRowFetched; + _listener = DatabaseNotificationListener( + objectId: rowId, + handler: _handler, + ); + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidUpdateRowMeta: + result.fold( + (payload) { + if (_onMetaChangedCallback != null) { + _onMetaChangedCallback!(RowMetaPB.fromBuffer(payload)); + } + }, + (error) => Log.error(error), + ); + break; + case DatabaseNotification.DidFetchRow: + result.fold( + (payload) { + if (_onRowFetchedCallback != null) { + _onRowFetchedCallback!(DidFetchRowPB.fromBuffer(payload)); + } + }, + (error) => Log.error(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _onMetaChangedCallback = null; + _onRowFetchedCallback = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart index b961750068..d99f1b222e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/view/view_cache.dart @@ -46,11 +46,11 @@ class DatabaseViewCache { required this.viewId, required FieldController fieldController, }) : _databaseViewListener = DatabaseViewListener(viewId: viewId) { - final delegate = RowDelegatesImpl(fieldController); + final depsImpl = RowCacheDependenciesImpl(fieldController); _rowCache = RowCache( viewId: viewId, - fieldsDelegate: delegate, - cacheDelegate: delegate, + fieldsDelegate: depsImpl, + rowLifeCycle: depsImpl, ); _databaseViewListener.start( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index d3482b7675..33b091d9f5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -6,7 +6,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/application/field/field_controller.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; -import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database_view/tar_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; @@ -96,7 +96,10 @@ class BoardPage extends StatelessWidget { (_) => BoardContent( onEditStateChanged: onEditStateChanged, ), - (err) => FlowyErrorPage.message(err.toString(), howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(),), + (err) => FlowyErrorPage.message( + err.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart index cf9bba21c3..b2ade97700 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/calendar_bloc.dart @@ -24,7 +24,7 @@ class CalendarBloc extends Bloc { // Getters String get viewId => databaseController.viewId; FieldController get fieldController => databaseController.fieldController; - CellCache get cellCache => databaseController.rowCache.cellCache; + CellMemCache get cellCache => databaseController.rowCache.cellCache; RowCache get rowCache => databaseController.rowCache; CalendarBloc({required ViewPB view, required this.databaseController}) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/unschedule_event_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/unschedule_event_bloc.dart index 761fdeb367..de7ef21652 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/unschedule_event_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/application/unschedule_event_bloc.dart @@ -21,7 +21,7 @@ class UnscheduleEventsBloc // Getters String get viewId => databaseController.viewId; FieldController get fieldController => databaseController.fieldController; - CellCache get cellCache => databaseController.rowCache.cellCache; + CellMemCache get cellCache => databaseController.rowCache.cellCache; RowCache get rowCache => databaseController.rowCache; UnscheduleEventsBloc({ diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart index 8dab7e9d34..efe57786d2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_page.dart @@ -14,7 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../application/row/row_cache.dart'; -import '../../application/row/row_data_controller.dart'; +import '../../application/row/row_controller.dart'; import '../../widgets/row/cell_builder.dart'; import '../../widgets/row/row_detail.dart'; import 'calendar_day.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart index 4907611004..518c489dfb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/grid_bloc.dart @@ -153,7 +153,7 @@ class GridEvent with _$GridEvent { const factory GridEvent.moveRow(int from, int to) = _MoveRow; const factory GridEvent.didLoadRows( List rows, - RowsChangedReason reason, + ChangedReason reason, ) = _DidReceiveRowUpdate; const factory GridEvent.didReceiveFieldUpdate( List fields, @@ -179,7 +179,7 @@ class GridState with _$GridState { required int rowCount, required GridLoadingState loadingState, required bool reorderable, - required RowsChangedReason reason, + required ChangedReason reason, required List sorts, required List filters, }) = _GridState; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart index 0a108abbdf..35c514a715 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_bloc.dart @@ -1,4 +1,6 @@ import 'dart:collection'; +import 'package:appflowy/plugins/database_view/application/row/row_listener.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -7,7 +9,7 @@ import 'dart:async'; import '../../../application/cell/cell_service.dart'; import '../../../application/field/field_controller.dart'; import '../../../application/row/row_cache.dart'; -import '../../../application/row/row_data_controller.dart'; +import '../../../application/row/row_controller.dart'; import '../../../application/row/row_service.dart'; part 'row_bloc.freezed.dart'; @@ -15,6 +17,7 @@ part 'row_bloc.freezed.dart'; class RowBloc extends Bloc { final RowBackendService _rowBackendSvc; final RowController _dataController; + final RowListener _rowListener; final String viewId; final String rowId; @@ -24,6 +27,7 @@ class RowBloc extends Bloc { required RowController dataController, }) : _rowBackendSvc = RowBackendService(viewId: viewId), _dataController = dataController, + _rowListener = RowListener(rowId), super(RowState.initial(dataController.loadData())) { on( (event, emit) async { @@ -46,6 +50,9 @@ class RowBloc extends Bloc { ), ); }, + reloadRow: (DidFetchRowPB row) { + emit(state.copyWith(rowSource: RowSourece.remote(row))); + }, ); }, ); @@ -54,6 +61,7 @@ class RowBloc extends Bloc { @override Future close() async { _dataController.dispose(); + await _rowListener.stop(); return super.close(); } @@ -65,6 +73,14 @@ class RowBloc extends Bloc { } }, ); + + _rowListener.start( + onRowFetched: (fetchRow) { + if (!isClosed) { + add(RowEvent.reloadRow(fetchRow)); + } + }, + ); } } @@ -72,9 +88,10 @@ class RowBloc extends Bloc { class RowEvent with _$RowEvent { const factory RowEvent.initial() = _InitialRow; const factory RowEvent.createRow() = _CreateRow; + const factory RowEvent.reloadRow(DidFetchRowPB row) = _ReloadRow; const factory RowEvent.didReceiveCells( CellContextByFieldId cellsByFieldId, - RowsChangedReason reason, + ChangedReason reason, ) = _DidReceiveCells; } @@ -83,7 +100,8 @@ class RowState with _$RowState { const factory RowState({ required CellContextByFieldId cellByFieldId, required UnmodifiableListView cells, - RowsChangedReason? changeReason, + required RowSourece rowSource, + ChangedReason? changeReason, }) = _RowState; factory RowState.initial( @@ -96,6 +114,7 @@ class RowState with _$RowState { .map((e) => GridCellEquatable(e.fieldInfo)) .toList(), ), + rowSource: const RowSourece.disk(), ); } @@ -112,3 +131,11 @@ class GridCellEquatable extends Equatable { _fieldContext.width, ]; } + +@freezed +class RowSourece with _$RowSourece { + const factory RowSourece.disk() = _Disk; + const factory RowSourece.remote( + DidFetchRowPB row, + ) = _Remote; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart index 812a98b837..4cd382afc5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/application/row/row_detail_bloc.dart @@ -5,7 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; import '../../../application/cell/cell_service.dart'; import '../../../application/field/field_service.dart'; -import '../../../application/row/row_data_controller.dart'; +import '../../../application/row/row_controller.dart'; part 'row_detail_bloc.freezed.dart'; class RowDetailBloc extends Bloc { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart index 7b557688fd..15f4f539bd 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/grid_page.dart @@ -18,7 +18,7 @@ import 'package:flutter/material.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; import '../../application/field/field_controller.dart'; import '../../application/row/row_cache.dart'; -import '../../application/row/row_data_controller.dart'; +import '../../application/row/row_controller.dart'; import '../application/grid_bloc.dart'; import '../../application/database_controller.dart'; import 'grid_scroll.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart index 5c54665e46..9776728bd6 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/row/row.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; -import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/application/row/row_service.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; @@ -65,7 +65,9 @@ class _GridRowState extends State { child: _RowEnterRegion( child: BlocBuilder( // The row need to rebuild when the cell count changes. - buildWhen: (p, c) => p.cellByFieldId.length != c.cellByFieldId.length, + buildWhen: (p, c) => + p.cellByFieldId.length != c.cellByFieldId.length || + p.rowSource != c.rowSource, builder: (context, state) { final content = Expanded( child: RowContent( @@ -78,6 +80,7 @@ class _GridRowState extends State { ); return Row( + key: ValueKey(state.rowSource), children: [ _RowLeading( index: widget.index, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart index f2af6128c2..d3e8c91ba6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_bloc.dart @@ -29,7 +29,7 @@ class CardBloc extends Bloc { _rowCache = rowCache, super( RowCardState.initial( - _makeCells(groupFieldId, rowCache.loadGridCells(rowMeta)), + _makeCells(groupFieldId, rowCache.loadCells(rowMeta)), isEditing, ), ) { @@ -78,7 +78,7 @@ class CardBloc extends Bloc { Future _startListening() async { _rowCallback = _rowCache.addListener( rowId: rowMeta.id, - onCellUpdated: (cellMap, reason) { + onRowChanged: (cellMap, reason) { if (!isClosed) { final cells = _makeCells(groupFieldId, cellMap); add(RowCardEvent.didReceiveCells(cells, reason)); @@ -112,7 +112,7 @@ class RowCardEvent with _$RowCardEvent { const factory RowCardEvent.setIsEditing(bool isEditing) = _IsEditing; const factory RowCardEvent.didReceiveCells( List cells, - RowsChangedReason reason, + ChangedReason reason, ) = _DidReceiveCells; } @@ -121,7 +121,7 @@ class RowCardState with _$RowCardState { const factory RowCardState({ required List cells, required bool isEditing, - RowsChangedReason? changeReason, + ChangedReason? changeReason, }) = _RowCardState; factory RowCardState.initial( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart index 676e5e5cc4..3570643a5c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart @@ -14,7 +14,7 @@ import 'cells/url_card_cell.dart'; // T represents as the Generic card data class CardCellBuilder { - final CellCache cellCache; + final CellMemCache cellCache; final Map? styles; CardCellBuilder(this.cellCache, {this.styles}); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart index c7a6cd7d33..0b9de32497 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart @@ -16,7 +16,7 @@ import 'cells/url_cell/url_cell.dart'; /// Build the cell widget in Grid style. class GridCellBuilder { - final CellCache cellCache; + final CellMemCache cellCache; GridCellBuilder({ required this.cellCache, }); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart index e9bc70b5a3..e9518c7eec 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_action.dart @@ -1,7 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart'; -import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart index 93eb9582db..ac3cd959eb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart'; -import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_document.dart'; import 'package:collection/collection.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index c35955b7f6..0136de42b6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -10,10 +10,11 @@ import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/plugins/document/presentation/export_page_widget.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/base64_string.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart' hide DocumentEvent; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -66,10 +67,13 @@ class _DocumentPageState extends State { return state.loadingState.when( loading: () => const SizedBox.shrink(), finish: (result) => result.fold( - (error) => FlowyErrorPage.message( - error.toString(), - howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), - ), + (error) { + Log.error(error); + return FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ); + }, (data) { if (state.forceClose) { widget.onDeleted(); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart index ec80400beb..2630068a39 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart @@ -38,6 +38,7 @@ AppFlowyEnv getAppFlowyEnv() { ); final supabaseConfig = SupabaseConfiguration( + enable_sync: Env.enableSupabaseSync, url: Env.supabaseUrl, key: Env.supabaseKey, jwt_secret: Env.supabaseJwtSecret, diff --git a/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart index 46dfbf191e..623b2fe059 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/core/config/config.dart'; import 'package:appflowy/env/env.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -22,16 +21,6 @@ class InitSupabaseTask extends LaunchTask { debug: false, ); - await Config.setSupabaseConfig( - url: Env.supabaseUrl, - key: Env.supabaseKey, - secret: Env.supabaseJwtSecret, - anonKey: Env.supabaseAnonKey, - pgPassword: Env.supabaseDbPassword, - pgPort: Env.supabaseDbPort, - pgUrl: Env.supabaseDb, - pgUser: Env.supabaseDbUser, - ); isSupabaseInitialized = true; } } diff --git a/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart index 302d31a75c..99feb10b60 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/appflowy_auth_service.dart @@ -51,8 +51,7 @@ class AppFlowyAuthService implements AuthService { AuthTypePB authType = AuthTypePB.Local, Map map = const {}, }) async { - final payload = SignOutPB()..authType = authType; - await UserEventSignOut(payload).send(); + await UserEventSignOut().send(); return; } @@ -61,7 +60,7 @@ class AppFlowyAuthService implements AuthService { AuthTypePB authType = AuthTypePB.Local, Map map = const {}, }) { - const password = "AppFlowy123@"; + const password = "Guest!@123456"; final uid = uuid(); final userEmail = "$uid@appflowy.io"; return signUp( diff --git a/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart index 70c7fd73f9..82f44fb113 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart @@ -8,6 +8,7 @@ class AuthServiceMapKeys { // for supabase auth use only. static const String uuid = 'uuid'; + static const String email = 'email'; } abstract class AuthService { @@ -42,9 +43,7 @@ abstract class AuthService { }); /// - Future signOut({ - AuthTypePB authType, - }); + Future signOut(); /// Returns [UserProfilePB] if the user has sign in, otherwise returns null. Future> getUser(); diff --git a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart index 8b506572d5..e982194e08 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart @@ -115,7 +115,10 @@ class SupabaseAuthService implements AuthService { completer.complete(left(AuthError.supabaseSignInWithOauthError)); } else { final Either response = await setupAuth( - map: {AuthServiceMapKeys.uuid: user.id}, + map: { + AuthServiceMapKeys.uuid: user.id, + AuthServiceMapKeys.email: user.email ?? user.newEmail ?? '' + }, ); completer.complete(response); } diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 3fb1312b85..77c26ccf1c 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -58,9 +58,8 @@ class UserBackendService { throw UnimplementedError(); } - Future> signOut(AuthTypePB authType) { - final payload = SignOutPB()..authType = authType; - return UserEventSignOut(payload).send(); + Future> signOut() { + return UserEventSignOut().send(); } Future> initUser() async { diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart index aa876fae61..ac5e02c9ce 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_bloc.dart @@ -85,9 +85,9 @@ class MenuBloc extends Bloc { // ignore: unused_element Future _fetchApps(Emitter emit) async { - final appsOrFail = await _workspaceService.getViews(); + final viewsOrError = await _workspaceService.getViews(); emit( - appsOrFail.fold( + viewsOrError.fold( (views) => state.copyWith(views: views), (error) { Log.error(error); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/setting_supabase_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/setting_supabase_bloc.dart new file mode 100644 index 0000000000..e87e2dc623 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/setting_supabase_bloc.dart @@ -0,0 +1,73 @@ +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; +import 'package:protobuf/protobuf.dart'; + +part 'setting_supabase_bloc.freezed.dart'; + +class SettingSupabaseBloc + extends Bloc { + SettingSupabaseBloc() : super(SettingSupabaseState.initial()) { + on((event, emit) async { + await event.when( + initial: () async { + await getSupabaseConfig(); + }, + enableSync: (bool enable) async { + final oldConfig = state.config; + if (oldConfig != null) { + oldConfig.freeze(); + final newConfig = oldConfig.rebuild((config) { + config.enableSync = enable; + }); + updateSupabaseConfig(newConfig); + emit(state.copyWith(config: newConfig)); + } + }, + didReceiveSupabseConfig: (SupabaseConfigPB config) { + emit(state.copyWith(config: config)); + }, + ); + }); + } + + Future updateSupabaseConfig(SupabaseConfigPB config) async { + await UserEventSetSupabaseConfig(config).send(); + } + + Future getSupabaseConfig() async { + final result = await UserEventGetSupabaseConfig().send(); + result.fold( + (config) { + if (!isClosed) { + add(SettingSupabaseEvent.didReceiveSupabseConfig(config)); + } + }, + (r) => Log.error(r), + ); + } +} + +@freezed +class SettingSupabaseEvent with _$SettingSupabaseEvent { + const factory SettingSupabaseEvent.initial() = _Initial; + const factory SettingSupabaseEvent.didReceiveSupabseConfig( + SupabaseConfigPB config, + ) = _DidReceiveSupabaseConfig; + const factory SettingSupabaseEvent.enableSync(bool enable) = _EnableSync; +} + +@freezed +class SettingSupabaseState with _$SettingSupabaseState { + const factory SettingSupabaseState({ + SupabaseConfigPB? config, + required Either successOrFailure, + }) = _SettingSupabaseState; + + factory SettingSupabaseState.initial() => SettingSupabaseState( + successOrFailure: left(unit), + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index 4b0f3de9df..bd2dbe1662 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -13,6 +13,7 @@ enum SettingsPage { language, files, user, + supabaseSetting, } class SettingsDialogBloc diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart index 8b01f4c684..8fe3cff50f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart @@ -53,7 +53,7 @@ class WorkspaceService { final payload = WorkspaceIdPB.create()..value = workspaceId; return FolderEventReadWorkspaceViews(payload).send().then((result) { return result.fold( - (apps) => left(apps.items), + (views) => left(views.items), (error) => right(error), ); }); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 09e719c2e4..c7fc281918 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -1,5 +1,6 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_file_system_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_language_view.dart'; @@ -85,6 +86,8 @@ class SettingsDialog extends StatelessWidget { return const SettingsFileSystemView(); case SettingsPage.user: return SettingsUserView(user); + case SettingsPage.supabaseSetting: + return const SupabaseSettingView(); default: return Container(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_view.dart new file mode 100644 index 0000000000..c25031bfe4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_view.dart @@ -0,0 +1,30 @@ +import 'package:appflowy/workspace/application/settings/setting_supabase_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SupabaseSettingView extends StatelessWidget { + const SupabaseSettingView({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + SettingSupabaseBloc()..add(const SettingSupabaseEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + return Align( + alignment: Alignment.topRight, + child: Switch( + onChanged: (bool value) { + context.read().add( + SettingSupabaseEvent.enableSync(value), + ); + }, + value: state.config?.enableSync ?? false, + ), + ); + }, + ), + ); + } +} 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 new file mode 100644 index 0000000000..3a1e59cfa8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart @@ -0,0 +1,41 @@ +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/sign_in_bloc.dart'; +import 'package:appflowy/user/presentation/sign_in_screen.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SettingThirdPartyLogin extends StatelessWidget { + const SettingThirdPartyLogin({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(), + child: BlocConsumer( + listener: (context, state) { + state.successOrFail.fold( + () => null, + (result) => _handleSuccessOrFail(result, context), + ); + }, + builder: (_, __) => const ThirdPartySignInButtons(), + ), + ); + } + + void _handleSuccessOrFail( + Either result, + BuildContext context, + ) { + result.fold( + (user) { + // TODO(Lucas): push to home screen + }, + (error) => showSnapBar(context, error.msg), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index ecdb4c6696..ff2a98213c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -1,8 +1,11 @@ +import 'package:appflowy/env/env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsMenu extends StatelessWidget { const SettingsMenu({ @@ -55,6 +58,18 @@ class SettingsMenu extends StatelessWidget { icon: Icons.account_box_outlined, changeSelectedPage: changeSelectedPage, ), + + // Only show supabase setting if supabase is enabled and the current auth type is not local + if (isSupabaseEnable && + context.read().state.userProfile.authType != + AuthTypePB.Local) + SettingsMenuElement( + page: SettingsPage.supabaseSetting, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_supabaseSetting.tr(), + icon: Icons.sync, + changeSelectedPage: changeSelectedPage, + ), ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart index f29f6e776e..bbc11ad727 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:async'; +import 'package:appflowy/env/env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/entry_point.dart'; import 'package:appflowy/startup/startup.dart'; @@ -15,6 +16,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'setting_third_party_login.dart'; + const defaultUserAvatar = '1F600'; const _iconSize = Size(60, 60); @@ -38,7 +41,7 @@ class SettingsUserView extends StatelessWidget { const VSpace(20), _renderCurrentOpenaiKey(context), const Spacer(), - _renderLogoutButton(context), + _renderLoginOrLogoutButton(context, state), const VSpace(20), ], ), @@ -46,6 +49,21 @@ class SettingsUserView extends StatelessWidget { ); } + Widget _renderLoginOrLogoutButton( + BuildContext context, + SettingsUserState state, + ) { + if (!isSupabaseEnable) { + return _renderLogoutButton(context); + } + + if (state.userProfile.authType == AuthTypePB.Local) { + return const SettingThirdPartyLogin(); + } else { + return _renderLogoutButton(context); + } + } + Widget _renderUserNameInput(BuildContext context) { final String name = context.read().state.userProfile.name; @@ -74,8 +92,7 @@ class SettingsUserView extends StatelessWidget { 'Logout', ), onTap: () async { - await getIt().signOut(authType: AuthTypePB.Supabase); - await getIt().signOut(authType: AuthTypePB.Local); + await getIt().signOut(); await FlowyRunner.run( FlowyApp(), integrationEnv(), diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart index 30cb4b57be..7081177224 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart @@ -1,6 +1,8 @@ import 'package:json_annotation/json_annotation.dart'; -// Run `dart run build_runner build` to generate the json serialization +// Run `dart run build_runner build` to generate the json serialization If the +// file `env_serde.i.dart` is existed, delete it first. +// // the file `env_serde.g.dart` will be generated in the same directory. Rename // the file to `env_serde.i.dart` because the file is ignored by default. part 'env_serde.i.dart'; @@ -9,7 +11,9 @@ part 'env_serde.i.dart'; class AppFlowyEnv { final SupabaseConfiguration supabase_config; - AppFlowyEnv({required this.supabase_config}); + AppFlowyEnv({ + required this.supabase_config, + }); factory AppFlowyEnv.fromJson(Map json) => _$AppFlowyEnvFromJson(json); @@ -19,12 +23,15 @@ class AppFlowyEnv { @JsonSerializable() class SupabaseConfiguration { + /// Indicates whether the sync feature is enabled. + final bool enable_sync; final String url; final String key; final String jwt_secret; final PostgresConfiguration postgres_config; SupabaseConfiguration({ + this.enable_sync = true, required this.url, required this.key, required this.jwt_secret, diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.i.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.i.dart index 96fe48b08b..4dcae3b5bc 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.i.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.i.dart @@ -19,6 +19,7 @@ Map _$AppFlowyEnvToJson(AppFlowyEnv instance) => SupabaseConfiguration _$SupabaseConfigurationFromJson( Map json) => SupabaseConfiguration( + enable_sync: json['enable_sync'] as bool? ?? true, url: json['url'] as String, key: json['key'] as String, jwt_secret: json['jwt_secret'] as String, @@ -29,6 +30,7 @@ SupabaseConfiguration _$SupabaseConfigurationFromJson( Map _$SupabaseConfigurationToJson( SupabaseConfiguration instance) => { + 'enable_sync': instance.enable_sync, 'url': instance.url, 'key': instance.key, 'jwt_secret': instance.jwt_secret, diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart index 33880ba2bd..f293829ec5 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart @@ -5,7 +5,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_editor_bl import 'package:appflowy/plugins/database_view/application/field/field_service.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; -import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/board/board.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart'; diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart index c3ca8408eb..accb4aa7cf 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart @@ -6,7 +6,7 @@ import 'package:appflowy/plugins/database_view/application/field/field_service.d import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_service.dart'; import 'package:appflowy/plugins/database_view/application/row/row_cache.dart'; -import 'package:appflowy/plugins/database_view/application/row/row_data_controller.dart'; +import 'package:appflowy/plugins/database_view/application/row/row_controller.dart'; import 'package:appflowy/plugins/database_view/application/database_controller.dart'; import 'package:appflowy/plugins/database_view/grid/application/row/row_bloc.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 58ee0124f5..a6911c7f3e 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -105,7 +105,6 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "appflowy-integrate" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" dependencies = [ "anyhow", "collab", @@ -1030,7 +1029,6 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" dependencies = [ "anyhow", "bytes", @@ -1048,7 +1046,6 @@ dependencies = [ [[package]] name = "collab-client-ws" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" dependencies = [ "bytes", "collab-sync", @@ -1066,7 +1063,6 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" dependencies = [ "anyhow", "async-trait", @@ -1093,7 +1089,6 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" dependencies = [ "proc-macro2", "quote", @@ -1105,7 +1100,6 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" dependencies = [ "anyhow", "collab", @@ -1124,7 +1118,6 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" dependencies = [ "anyhow", "chrono", @@ -1144,7 +1137,6 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" dependencies = [ "bincode", "chrono", @@ -1164,7 +1156,6 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" dependencies = [ "anyhow", "async-trait", @@ -1198,7 +1189,6 @@ dependencies = [ [[package]] name = "collab-sync" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" dependencies = [ "bytes", "collab", @@ -2072,6 +2062,8 @@ dependencies = [ "async-stream", "bytes", "chrono", + "collab-document", + "collab-folder", "config", "deadpool-postgres", "flowy-database2", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index f9c7001856..aec70946d2 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -34,12 +34,12 @@ default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] [patch.crates-io] -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2134c0" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2134c0" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2134c0" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2134c0" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2134c0" } -appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2134c0" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" } +collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" } +appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" } #collab = { path = "../../AppFlowy-Collab/collab" } #collab-folder = { path = "../../AppFlowy-Collab/collab-folder" } diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts index 3804e42010..4ed80be729 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/user/user_bd_svc.ts @@ -2,7 +2,6 @@ import { nanoid } from '@reduxjs/toolkit'; import { AppearanceSettingsPB, AuthTypePB, - SignOutPB, ThemeModePB, UserEventGetAppearanceSetting, UserEventGetUserProfile, @@ -91,9 +90,7 @@ export class UserBackendService { }; signOut = () => { - const payload = SignOutPB.fromObject({ auth_type: AuthTypePB.Local }); - - return UserEventSignOut(payload); + return UserEventSignOut(); }; setAppearanceSettings = (params: ReturnType) => { @@ -125,9 +122,7 @@ export class AuthBackendService { }; signOut = () => { - const payload = SignOutPB.fromObject({ auth_type: AuthTypePB.Local }); - - return UserEventSignOut(payload); + return UserEventSignOut(); }; autoSignUp = () => { diff --git a/frontend/flowy-server-config/Cargo.toml b/frontend/flowy-server-config/Cargo.toml new file mode 100644 index 0000000000..ca045c8267 --- /dev/null +++ b/frontend/flowy-server-config/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "flowy-server-config" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/frontend/flowy-server-config/src/lib.rs b/frontend/flowy-server-config/src/lib.rs new file mode 100644 index 0000000000..7d12d9af81 --- /dev/null +++ b/frontend/flowy-server-config/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 2939393b09..a103666955 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -198,7 +198,8 @@ "language": "Language", "user": "User", "files": "Files", - "open": "Open Settings" + "open": "Open Settings", + "supabaseSetting": "Supabase Setting" }, "appearance": { "fontFamily": { diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a97f55a076..673d89cbc4 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -85,7 +85,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "appflowy-integrate" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3" dependencies = [ "anyhow", "collab", @@ -897,7 +897,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3" dependencies = [ "anyhow", "bytes", @@ -915,7 +915,7 @@ dependencies = [ [[package]] name = "collab-client-ws" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3" dependencies = [ "bytes", "collab-sync", @@ -933,7 +933,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3" dependencies = [ "anyhow", "async-trait", @@ -960,7 +960,7 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3" dependencies = [ "proc-macro2", "quote", @@ -972,7 +972,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3" dependencies = [ "anyhow", "collab", @@ -991,7 +991,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3" dependencies = [ "anyhow", "chrono", @@ -1011,7 +1011,7 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3" dependencies = [ "bincode", "chrono", @@ -1031,7 +1031,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3" dependencies = [ "anyhow", "async-trait", @@ -1065,7 +1065,7 @@ dependencies = [ [[package]] name = "collab-sync" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2134c0#2134c0f27b8a9f3077e25ae928f2420c926506cc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2eb044#2eb044356ba49b26382c4aa9f1fd03d7663e84d3" dependencies = [ "bytes", "collab", @@ -1314,6 +1314,7 @@ dependencies = [ "flowy-net", "flowy-notification", "flowy-server", + "flowy-server-config", "lazy_static", "lib-dispatch", "log", @@ -1670,6 +1671,7 @@ dependencies = [ "flowy-folder2", "flowy-net", "flowy-server", + "flowy-server-config", "flowy-sqlite", "flowy-task", "flowy-user", @@ -1868,6 +1870,8 @@ dependencies = [ "async-stream", "bytes", "chrono", + "collab-document", + "collab-folder", "config", "deadpool-postgres", "dotenv", @@ -1875,6 +1879,7 @@ dependencies = [ "flowy-document2", "flowy-error", "flowy-folder2", + "flowy-server-config", "flowy-user", "futures", "futures-util", @@ -1898,6 +1903,14 @@ dependencies = [ "uuid", ] +[[package]] +name = "flowy-server-config" +version = "0.1.0" +dependencies = [ + "flowy-error", + "serde", +] + [[package]] name = "flowy-sqlite" version = "0.1.0" @@ -1952,6 +1965,7 @@ dependencies = [ "flowy-net", "flowy-notification", "flowy-server", + "flowy-server-config", "flowy-user", "futures-util", "lib-dispatch", @@ -1965,6 +1979,7 @@ dependencies = [ "tempdir", "thread-id", "tokio", + "tokio-postgres", "tracing", "uuid", ] @@ -1975,6 +1990,8 @@ version = "0.1.0" dependencies = [ "appflowy-integrate", "bytes", + "collab", + "collab-folder", "diesel", "diesel_derives", "fake", @@ -1983,6 +2000,7 @@ dependencies = [ "flowy-derive", "flowy-error", "flowy-notification", + "flowy-server-config", "flowy-sqlite", "lazy_static", "lib-dispatch", @@ -2004,6 +2022,7 @@ dependencies = [ "tokio", "tracing", "unicode-segmentation", + "uuid", "validator", ] diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 3241b2722a..c5ef7a8fba 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -15,6 +15,7 @@ members = [ "flowy-database2", "flowy-task", "flowy-server", + "flowy-server-config", "flowy-config", ] @@ -33,11 +34,11 @@ opt-level = 3 incremental = false [patch.crates-io] -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2134c0" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2134c0" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2134c0" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2134c0" } -appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2134c0" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" } +appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2eb044" } #collab = { path = "../AppFlowy-Collab/collab" } #collab-folder = { path = "../AppFlowy-Collab/collab-folder" } diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 824b205e25..38e86087d0 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -32,6 +32,7 @@ flowy-notification = { path = "../flowy-notification" } flowy-net = { path = "../flowy-net" } flowy-derive = { path = "../../../shared-lib/flowy-derive" } flowy-server = { path = "../flowy-server" } +flowy-server-config = { path = "../flowy-server-config" } [features] default = ["dart", "rev-sqlite"] diff --git a/frontend/rust-lib/dart-ffi/src/env_serde.rs b/frontend/rust-lib/dart-ffi/src/env_serde.rs index a8d8c9734c..187d23e361 100644 --- a/frontend/rust-lib/dart-ffi/src/env_serde.rs +++ b/frontend/rust-lib/dart-ffi/src/env_serde.rs @@ -1,6 +1,6 @@ use serde::Deserialize; -use flowy_server::supabase::SupabaseConfiguration; +use flowy_server_config::supabase_config::SupabaseConfiguration; #[derive(Deserialize, Debug)] pub struct AppFlowyEnv { @@ -10,7 +10,6 @@ pub struct AppFlowyEnv { impl AppFlowyEnv { pub fn parser(env_str: &str) { if let Ok(env) = serde_json::from_str::(env_str) { - tracing::trace!("{:?}", env); env.supabase_config.write_env(); } } diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index b249a2756c..256ad1d8f2 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -37,7 +37,6 @@ pub extern "C" fn init_sdk(path: *mut c_char) -> i64 { let config = AppFlowyCoreConfig::new(path, DEFAULT_NAME.to_string()).log_filter("info", log_crates); *APPFLOWY_CORE.write() = Some(AppFlowyCore::new(config)); - 0 } diff --git a/frontend/rust-lib/flowy-config/src/entities.rs b/frontend/rust-lib/flowy-config/src/entities.rs index dc4505ae23..c4791b35aa 100644 --- a/frontend/rust-lib/flowy-config/src/entities.rs +++ b/frontend/rust-lib/flowy-config/src/entities.rs @@ -2,7 +2,6 @@ use appflowy_integrate::config::AWSDynamoDBConfig; use flowy_derive::ProtoBuf; use flowy_error::FlowyError; -use flowy_server::supabase::{PostgresConfiguration, SupabaseConfiguration}; #[derive(Default, ProtoBuf)] pub struct KeyValuePB { @@ -19,38 +18,6 @@ pub struct KeyPB { pub key: String, } -#[derive(Default, ProtoBuf)] -pub struct SupabaseConfigPB { - #[pb(index = 1)] - supabase_url: String, - - #[pb(index = 2)] - anon_key: String, - - #[pb(index = 3)] - key: String, - - #[pb(index = 4)] - jwt_secret: String, - - #[pb(index = 5)] - pub postgres_config: PostgresConfigurationPB, -} - -impl TryFrom for SupabaseConfiguration { - type Error = FlowyError; - - fn try_from(config: SupabaseConfigPB) -> Result { - let postgres_config = PostgresConfiguration::try_from(config.postgres_config)?; - Ok(SupabaseConfiguration { - url: config.supabase_url, - key: config.key, - jwt_secret: config.jwt_secret, - postgres_config, - }) - } -} - #[derive(Default, ProtoBuf)] pub struct CollabPluginConfigPB { #[pb(index = 1, one_of)] @@ -81,31 +48,3 @@ impl TryFrom for AWSDynamoDBConfig { }) } } - -#[derive(Default, ProtoBuf)] -pub struct PostgresConfigurationPB { - #[pb(index = 1)] - pub url: String, - - #[pb(index = 2)] - pub user_name: String, - - #[pb(index = 3)] - pub password: String, - - #[pb(index = 4)] - pub port: u32, -} - -impl TryFrom for PostgresConfiguration { - type Error = FlowyError; - - fn try_from(config: PostgresConfigurationPB) -> Result { - Ok(Self { - url: config.url, - user_name: config.user_name, - password: config.password, - port: config.port as u16, - }) - } -} diff --git a/frontend/rust-lib/flowy-config/src/event_handler.rs b/frontend/rust-lib/flowy-config/src/event_handler.rs index 4b815b87bb..1b9d713f3a 100644 --- a/frontend/rust-lib/flowy-config/src/event_handler.rs +++ b/frontend/rust-lib/flowy-config/src/event_handler.rs @@ -1,11 +1,10 @@ use appflowy_integrate::config::AWSDynamoDBConfig; use flowy_error::{FlowyError, FlowyResult}; -use flowy_server::supabase::SupabaseConfiguration; use flowy_sqlite::kv::KV; use lib_dispatch::prelude::{data_result_ok, AFPluginData, DataResult}; -use crate::entities::{CollabPluginConfigPB, KeyPB, KeyValuePB, SupabaseConfigPB}; +use crate::entities::{CollabPluginConfigPB, KeyPB, KeyValuePB}; pub(crate) async fn set_key_value_handler(data: AFPluginData) -> FlowyResult<()> { let data = data.into_inner(); @@ -35,14 +34,6 @@ pub(crate) async fn remove_key_value_handler(data: AFPluginData) -> Flowy Ok(()) } -pub(crate) async fn set_supabase_config_handler( - data: AFPluginData, -) -> FlowyResult<()> { - let config = SupabaseConfiguration::try_from(data.into_inner())?; - config.write_env(); - Ok(()) -} - pub(crate) async fn set_collab_plugin_config_handler( data: AFPluginData, ) -> FlowyResult<()> { diff --git a/frontend/rust-lib/flowy-config/src/event_map.rs b/frontend/rust-lib/flowy-config/src/event_map.rs index 24394f5678..933d358b8e 100644 --- a/frontend/rust-lib/flowy-config/src/event_map.rs +++ b/frontend/rust-lib/flowy-config/src/event_map.rs @@ -11,7 +11,6 @@ pub fn init() -> AFPlugin { .event(ConfigEvent::SetKeyValue, set_key_value_handler) .event(ConfigEvent::GetKeyValue, get_key_value_handler) .event(ConfigEvent::RemoveKeyValue, remove_key_value_handler) - .event(ConfigEvent::SetSupabaseConfig, set_supabase_config_handler) .event( ConfigEvent::SetCollabPluginConfig, set_collab_plugin_config_handler, @@ -30,11 +29,6 @@ pub enum ConfigEvent { #[event(input = "KeyPB")] RemoveKeyValue = 2, - /// Set the supabase config. It will be written to the environment variables. - /// Check out the `write_to_env` of [SupabaseConfigPB]. - #[event(input = "SupabaseConfigPB")] - SetSupabaseConfig = 3, - #[event(input = "CollabPluginConfigPB")] SetCollabPluginConfig = 4, } diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index ec24342afc..e5254fb21a 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -17,6 +17,7 @@ flowy-document2 = { path = "../flowy-document2" } flowy-error = { path = "../flowy-error" } flowy-task = { path = "../flowy-task" } flowy-server = { path = "../flowy-server" } +flowy-server-config = { path = "../flowy-server-config" } flowy-config = { path = "../flowy-config" } appflowy-integrate = { version = "0.1.0" } diesel = { version = "1.4.8", features = ["sqlite"] } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs index a6791e7952..411f1cda96 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs @@ -17,11 +17,11 @@ use lib_infra::util::timestamp; pub struct SnapshotDBImpl(pub Weak); impl SnapshotPersistence for SnapshotDBImpl { - fn get_snapshots(&self, _uid: i64, object_id: &str) -> Vec { + fn get_snapshots(&self, uid: i64, object_id: &str) -> Vec { match self.0.upgrade() { None => vec![], Some(user_session) => user_session - .db_pool() + .db_pool(uid) .and_then(|pool| Ok(pool.get()?)) .and_then(|conn| { CollabSnapshotTableSql::get_all_snapshots(object_id, &conn) @@ -43,7 +43,7 @@ impl SnapshotPersistence for SnapshotDBImpl { tokio::task::spawn_blocking(move || { if let Some(pool) = weak_user_session .upgrade() - .and_then(|user_session| user_session.db_pool().ok()) + .and_then(|user_session| user_session.db_pool(uid).ok()) { let conn = pool .get() diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs index f5fddae086..5614cf21df 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs @@ -47,11 +47,11 @@ impl DatabaseUser2 for DatabaseUserImpl { .token() } - fn collab_db(&self) -> Result, FlowyError> { + fn collab_db(&self, uid: i64) -> Result, FlowyError> { self .0 .upgrade() .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? - .get_collab_db() + .get_collab_db(uid) } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/document2_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/document2_deps.rs index 4a109ce969..9c1b27638d 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/document2_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/document2_deps.rs @@ -44,11 +44,11 @@ impl DocumentUser for DocumentUserImpl { .token() } - fn collab_db(&self) -> Result, FlowyError> { + fn collab_db(&self, uid: i64) -> Result, FlowyError> { self .0 .upgrade() .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? - .get_collab_db() + .get_collab_db(uid) } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs index 2007542e44..00377e7764 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder2_deps.rs @@ -81,12 +81,12 @@ impl FolderUser for FolderUserImpl { .token() } - fn collab_db(&self) -> Result, FlowyError> { + fn collab_db(&self, uid: i64) -> Result, FlowyError> { self .0 .upgrade() .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? - .get_collab_db() + .get_collab_db(uid) } } @@ -139,7 +139,7 @@ impl FolderOperationHandler for DocumentFolderOperation { FutureResult::new(async move { match manager.delete_document(&view_id) { Ok(_) => tracing::trace!("Delete document: {}", view_id), - Err(e) => tracing::error!("Failed to delete document: {}", e), + Err(e) => tracing::error!("🔴delete document failed: {}", e), } Ok(()) }) @@ -149,7 +149,7 @@ impl FolderOperationHandler for DocumentFolderOperation { let manager = self.0.clone(); let view_id = view_id.to_string(); FutureResult::new(async move { - let data: DocumentDataPB = manager.get_document_data(&view_id)?.into(); + let data: DocumentDataPB = manager.get_document_data(&view_id).await?.into(); let data_bytes = data.into_bytes().map_err(|_| FlowyError::invalid_data())?; Ok(data_bytes) }) @@ -235,7 +235,7 @@ impl FolderOperationHandler for DatabaseFolderOperation { FutureResult::new(async move { match database_manager.delete_database_view(&view_id).await { Ok(_) => tracing::trace!("Delete database view: {}", view_id), - Err(e) => tracing::error!("Failed to delete database: {}", e), + Err(e) => tracing::error!("🔴delete database failed: {}", e), } Ok(()) }) diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs index 0c4e7746c2..d14a275def 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs @@ -9,3 +9,4 @@ mod folder2_deps; mod util; mod database_deps; +mod user_deps; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs @@ -0,0 +1 @@ + diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs index 83ead3d9cd..9dea34fc87 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/server.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/server.rs @@ -6,20 +6,25 @@ use appflowy_integrate::RemoteCollabStorage; use parking_lot::RwLock; use serde_repr::*; -use flowy_database2::deps::{DatabaseCloudService, DatabaseSnapshot}; -use flowy_document2::deps::{DocumentCloudService, DocumentSnapshot}; +use flowy_database2::deps::{ + CollabObjectUpdate, CollabObjectUpdateByOid, DatabaseCloudService, DatabaseSnapshot, +}; +use flowy_document2::deps::{DocumentCloudService, DocumentData, DocumentSnapshot}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_folder2::deps::{FolderCloudService, FolderSnapshot, Workspace}; +use flowy_folder2::deps::{FolderCloudService, FolderData, FolderSnapshot, Workspace}; use flowy_server::local_server::LocalServer; use flowy_server::self_host::configuration::self_host_server_configuration; use flowy_server::self_host::SelfHostServer; -use flowy_server::supabase::{SupabaseConfiguration, SupabaseServer}; +use flowy_server::supabase::SupabaseServer; use flowy_server::AppFlowyServer; +use flowy_server_config::supabase_config::SupabaseConfiguration; use flowy_sqlite::kv::KV; use flowy_user::event_map::{UserAuthService, UserCloudServiceProvider}; use flowy_user::services::AuthType; use lib_infra::future::FutureResult; +use crate::AppFlowyCoreConfig; + const SERVER_PROVIDER_TYPE_KEY: &str = "server_provider_type"; #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] @@ -42,13 +47,20 @@ pub enum ServerProviderType { /// exist. /// Each server implements the [AppFlowyServer] trait, which provides the [UserAuthService], etc. pub struct AppFlowyServerProvider { + config: AppFlowyCoreConfig, provider_type: RwLock, providers: RwLock>>, + supabase_config: RwLock>, } impl AppFlowyServerProvider { - pub fn new() -> Self { - Self::default() + pub fn new(config: AppFlowyCoreConfig, supabase_config: Option) -> Self { + Self { + config, + provider_type: RwLock::new(current_server_provider()), + providers: RwLock::new(HashMap::new()), + supabase_config: RwLock::new(supabase_config), + } } pub fn provider_type(&self) -> ServerProviderType { @@ -64,7 +76,33 @@ impl AppFlowyServerProvider { return Ok(provider.clone()); } - let server = server_from_auth_type(provider_type)?; + let server = match provider_type { + ServerProviderType::Local => { + let server = Arc::new(LocalServer::new(&self.config.storage_path)); + Ok::, FlowyError>(server) + }, + ServerProviderType::SelfHosted => { + let config = self_host_server_configuration().map_err(|e| { + FlowyError::new( + ErrorCode::InvalidAuthConfig, + format!( + "Missing self host config: {:?}. Error: {:?}", + provider_type, e + ), + ) + })?; + let server = Arc::new(SelfHostServer::new(config)); + Ok::, FlowyError>(server) + }, + ServerProviderType::Supabase => { + let config = self.supabase_config.read().clone().ok_or(FlowyError::new( + ErrorCode::InvalidAuthConfig, + "Missing supabase config".to_string(), + ))?; + Ok::, FlowyError>(Arc::new(SupabaseServer::new(config))) + }, + }?; + self .providers .write() @@ -73,16 +111,19 @@ impl AppFlowyServerProvider { } } -impl Default for AppFlowyServerProvider { - fn default() -> Self { - Self { - provider_type: RwLock::new(current_server_provider()), - providers: RwLock::new(HashMap::new()), +impl UserCloudServiceProvider for AppFlowyServerProvider { + fn update_supabase_config(&self, supabase_config: &SupabaseConfiguration) { + self + .supabase_config + .write() + .replace(supabase_config.clone()); + + supabase_config.write_env(); + if let Ok(provider) = self.get_provider(&self.provider_type.read()) { + provider.enable_sync(supabase_config.enable_sync); } } -} -impl UserCloudServiceProvider for AppFlowyServerProvider { /// When user login, the provider type is set by the [AuthType] and save to disk for next use. /// /// Each [AuthType] has a corresponding [ServerProviderType]. The [ServerProviderType] is used @@ -119,6 +160,17 @@ impl FolderCloudService for AppFlowyServerProvider { FutureResult::new(async move { server?.folder_service().create_workspace(uid, &name).await }) } + fn get_folder_data(&self, workspace_id: &str) -> FutureResult, FlowyError> { + let server = self.get_provider(&self.provider_type.read()); + let workspace_id = workspace_id.to_string(); + FutureResult::new(async move { + server? + .folder_service() + .get_folder_data(&workspace_id) + .await + }) + } + fn get_folder_latest_snapshot( &self, workspace_id: &str, @@ -133,40 +185,64 @@ impl FolderCloudService for AppFlowyServerProvider { }) } - fn get_folder_updates(&self, workspace_id: &str) -> FutureResult>, FlowyError> { + fn get_folder_updates( + &self, + workspace_id: &str, + uid: i64, + ) -> FutureResult>, FlowyError> { let workspace_id = workspace_id.to_string(); let server = self.get_provider(&self.provider_type.read()); FutureResult::new(async move { server? .folder_service() - .get_folder_updates(&workspace_id) + .get_folder_updates(&workspace_id, uid) .await }) } + + fn service_name(&self) -> String { + self + .get_provider(&self.provider_type.read()) + .map(|provider| provider.folder_service().service_name()) + .unwrap_or_default() + } } impl DatabaseCloudService for AppFlowyServerProvider { - fn get_database_updates(&self, database_id: &str) -> FutureResult>, FlowyError> { + fn get_collab_update(&self, object_id: &str) -> FutureResult { let server = self.get_provider(&self.provider_type.read()); - let database_id = database_id.to_string(); + let database_id = object_id.to_string(); FutureResult::new(async move { server? .database_service() - .get_database_updates(&database_id) + .get_collab_update(&database_id) .await }) } - fn get_database_latest_snapshot( + fn batch_get_collab_updates( &self, - database_id: &str, - ) -> FutureResult, FlowyError> { + object_ids: Vec, + ) -> FutureResult { let server = self.get_provider(&self.provider_type.read()); - let database_id = database_id.to_string(); FutureResult::new(async move { server? .database_service() - .get_database_latest_snapshot(&database_id) + .batch_get_collab_updates(object_ids) + .await + }) + } + + fn get_collab_latest_snapshot( + &self, + object_id: &str, + ) -> FutureResult, FlowyError> { + let server = self.get_provider(&self.provider_type.read()); + let database_id = object_id.to_string(); + FutureResult::new(async move { + server? + .database_service() + .get_collab_latest_snapshot(&database_id) .await }) } @@ -197,6 +273,17 @@ impl DocumentCloudService for AppFlowyServerProvider { .await }) } + + fn get_document_data(&self, document_id: &str) -> FutureResult, FlowyError> { + let server = self.get_provider(&self.provider_type.read()); + let document_id = document_id.to_string(); + FutureResult::new(async move { + server? + .document_service() + .get_document_data(&document_id) + .await + }) + } } impl CollabStorageProvider for AppFlowyServerProvider { @@ -214,30 +301,14 @@ impl CollabStorageProvider for AppFlowyServerProvider { .and_then(|provider| provider.collab_storage()), } } -} -fn server_from_auth_type( - provider: &ServerProviderType, -) -> Result, FlowyError> { - match provider { - ServerProviderType::Local => { - let server = Arc::new(LocalServer::new()); - Ok(server) - }, - ServerProviderType::SelfHosted => { - let config = self_host_server_configuration().map_err(|e| { - FlowyError::new( - ErrorCode::InvalidAuthConfig, - format!("Missing self host config: {:?}. Error: {:?}", provider, e), - ) - })?; - let server = Arc::new(SelfHostServer::new(config)); - Ok(server) - }, - ServerProviderType::Supabase => { - let config = SupabaseConfiguration::from_env()?; - Ok(Arc::new(SupabaseServer::new(config))) - }, + fn is_sync_enabled(&self) -> bool { + self + .supabase_config + .read() + .as_ref() + .map(|config| config.enable_sync) + .unwrap_or(false) } } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index e1b7c3eb03..61780822ff 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -16,12 +16,12 @@ use tracing::debug; use flowy_database2::DatabaseManager2; use flowy_document2::manager::DocumentManager as DocumentManager2; use flowy_error::FlowyResult; -use flowy_folder2::manager::FolderManager; +use flowy_folder2::manager::{FolderInitializeData, FolderManager}; use flowy_sqlite::kv::KV; use flowy_task::{TaskDispatcher, TaskRunner}; use flowy_user::entities::UserProfile; -use flowy_user::event_map::{UserCloudServiceProvider, UserStatusCallback}; -use flowy_user::services::{AuthType, UserSession, UserSessionConfig}; +use flowy_user::event_map::{SignUpContext, UserCloudServiceProvider, UserStatusCallback}; +use flowy_user::services::{get_supabase_config, AuthType, UserSession, UserSessionConfig}; use lib_dispatch::prelude::*; use lib_dispatch::runtime::tokio_default_runtime; use lib_infra::future::{to_fut, Fut}; @@ -141,7 +141,10 @@ impl AppFlowyCore { let task_dispatcher = Arc::new(RwLock::new(task_scheduler)); runtime.spawn(TaskRunner::run(task_dispatcher.clone())); - let server_provider = Arc::new(AppFlowyServerProvider::new()); + let server_provider = Arc::new(AppFlowyServerProvider::new( + config.clone(), + get_supabase_config(), + )); let (user_session, folder_manager, server_provider, database_manager, document_manager2) = runtime.block_on(async { @@ -260,6 +263,21 @@ struct UserStatusCallbackImpl { impl UserStatusCallback for UserStatusCallbackImpl { fn auth_type_did_changed(&self, _auth_type: AuthType) {} + fn did_init(&self, user_id: i64, workspace_id: &str) -> Fut> { + let user_id = user_id.to_owned(); + let workspace_id = workspace_id.to_owned(); + let folder_manager = self.folder_manager.clone(); + let database_manager = self.database_manager.clone(); + + to_fut(async move { + folder_manager + .initialize(user_id, &workspace_id, FolderInitializeData::Empty) + .await?; + database_manager.initialize(user_id).await?; + Ok(()) + }) + } + fn did_sign_in(&self, user_id: i64, workspace_id: &str) -> Fut> { let user_id = user_id.to_owned(); let workspace_id = workspace_id.to_owned(); @@ -267,22 +285,29 @@ impl UserStatusCallback for UserStatusCallbackImpl { let database_manager = self.database_manager.clone(); to_fut(async move { - folder_manager.initialize(user_id, &workspace_id).await?; + folder_manager + .initialize_when_sign_in(user_id, &workspace_id) + .await?; database_manager.initialize(user_id).await?; Ok(()) }) } - fn did_sign_up(&self, is_new: bool, user_profile: &UserProfile) -> Fut> { + fn did_sign_up( + &self, + context: SignUpContext, + user_profile: &UserProfile, + ) -> Fut> { let user_profile = user_profile.clone(); let folder_manager = self.folder_manager.clone(); let database_manager = self.database_manager.clone(); to_fut(async move { folder_manager - .initialize_with_new_user( + .initialize_when_sign_up( user_profile.id, &user_profile.token, - is_new, + context.is_new, + context.local_folder, &user_profile.workspace_id, ) .await?; diff --git a/frontend/rust-lib/flowy-database2/src/deps.rs b/frontend/rust-lib/flowy-database2/src/deps.rs index 7223672fa9..009ea664e3 100644 --- a/frontend/rust-lib/flowy-database2/src/deps.rs +++ b/frontend/rust-lib/flowy-database2/src/deps.rs @@ -1,6 +1,8 @@ use std::sync::Arc; use appflowy_integrate::RocksCollabDB; +pub use collab_database::user::CollabObjectUpdate; +pub use collab_database::user::CollabObjectUpdateByOid; use flowy_error::FlowyError; use lib_infra::future::FutureResult; @@ -8,18 +10,23 @@ use lib_infra::future::FutureResult; pub trait DatabaseUser2: Send + Sync { fn user_id(&self) -> Result; fn token(&self) -> Result, FlowyError>; - fn collab_db(&self) -> Result, FlowyError>; + fn collab_db(&self, uid: i64) -> Result, FlowyError>; } /// A trait for database cloud service. /// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of /// [flowy-server] crate for more information. pub trait DatabaseCloudService: Send + Sync { - fn get_database_updates(&self, database_id: &str) -> FutureResult>, FlowyError>; + fn get_collab_update(&self, object_id: &str) -> FutureResult; - fn get_database_latest_snapshot( + fn batch_get_collab_updates( &self, - database_id: &str, + object_ids: Vec, + ) -> FutureResult; + + fn get_collab_latest_snapshot( + &self, + object_id: &str, ) -> FutureResult, FlowyError>; } diff --git a/frontend/rust-lib/flowy-database2/src/entities/macros.rs b/frontend/rust-lib/flowy-database2/src/entities/macros.rs index fd70e1a8f5..86baf0a08d 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/macros.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/macros.rs @@ -15,7 +15,7 @@ macro_rules! impl_into_field_type { 8 => FieldType::LastEditedTime, 9 => FieldType::CreatedTime, _ => { - tracing::error!("Can't parser FieldType from value: {}", ty); + tracing::error!("🔴Can't parser FieldType from value: {}", ty); FieldType::RichText }, } 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 e506e35974..6ef2fe9039 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs @@ -324,15 +324,6 @@ impl TryInto for RowIdPB { } } -#[derive(Debug, Default, Clone, ProtoBuf)] -pub struct BlockRowIdPB { - #[pb(index = 1)] - pub block_id: String, - - #[pb(index = 2)] - pub row_id: String, -} - #[derive(ProtoBuf, Default)] pub struct CreateRowPayloadPB { #[pb(index = 1)] 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 7240b6e745..513e2e9e31 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs @@ -1,6 +1,8 @@ +use collab_database::rows::RowDetail; + use flowy_derive::ProtoBuf; -use crate::entities::{InsertedRowPB, UpdatedRowPB}; +use crate::entities::{InsertedRowPB, RowMetaPB, UpdatedRowPB}; #[derive(Debug, Default, Clone, ProtoBuf)] pub struct RowsVisibilityChangePB { @@ -17,53 +19,76 @@ pub struct RowsVisibilityChangePB { #[derive(Debug, Default, Clone, ProtoBuf)] pub struct RowsChangePB { #[pb(index = 1)] - pub view_id: String, - - #[pb(index = 2)] pub inserted_rows: Vec, - #[pb(index = 3)] + #[pb(index = 2)] pub deleted_rows: Vec, - #[pb(index = 4)] + #[pb(index = 3)] pub updated_rows: Vec, } impl RowsChangePB { - pub fn from_insert(view_id: String, inserted_row: InsertedRowPB) -> Self { + pub fn from_insert(inserted_row: InsertedRowPB) -> Self { Self { - view_id, inserted_rows: vec![inserted_row], ..Default::default() } } - pub fn from_delete(view_id: String, deleted_row: String) -> Self { + pub fn from_delete(deleted_row: String) -> Self { Self { - view_id, deleted_rows: vec![deleted_row], ..Default::default() } } - pub fn from_update(view_id: String, updated_row: UpdatedRowPB) -> Self { + pub fn from_update(updated_row: UpdatedRowPB) -> Self { Self { - view_id, updated_rows: vec![updated_row], ..Default::default() } } - pub fn from_move( - view_id: String, - deleted_rows: Vec, - inserted_rows: Vec, - ) -> Self { + pub fn from_move(deleted_rows: Vec, inserted_rows: Vec) -> Self { Self { - view_id, inserted_rows, deleted_rows, ..Default::default() } } } + +#[derive(Debug, Default, ProtoBuf)] +pub struct DidFetchRowPB { + #[pb(index = 1)] + pub row_id: String, + + #[pb(index = 2)] + pub height: i32, + + #[pb(index = 3)] + pub visibility: bool, + + #[pb(index = 4)] + pub created_at: i64, + + #[pb(index = 5)] + pub modified_at: i64, + + #[pb(index = 6)] + pub meta: RowMetaPB, +} + +impl From for DidFetchRowPB { + fn from(value: RowDetail) -> Self { + Self { + row_id: value.row.id.to_string(), + height: value.row.height, + visibility: value.row.visibility, + created_at: value.row.created_at, + modified_at: value.row.modified_at, + meta: RowMetaPB::from(value.meta), + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 4e887942b6..5bb586d7de 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -1,14 +1,17 @@ use std::collections::HashMap; -use std::ops::Deref; use std::sync::Arc; use appflowy_integrate::collab_builder::AppFlowyCollabBuilder; use appflowy_integrate::{CollabPersistenceConfig, RocksCollabDB}; -use collab::core::collab::MutexCollab; -use collab_database::database::DatabaseData; -use collab_database::user::{DatabaseCollabBuilder, UserDatabase as InnerUserDatabase}; +use collab::core::collab::{CollabRawData, MutexCollab}; +use collab_database::blocks::BlockEvent; +use collab_database::database::{DatabaseData, YrsDocAction}; +use collab_database::error::DatabaseError; +use collab_database::user::{ + make_workspace_database_id, CollabFuture, CollabObjectUpdate, CollabObjectUpdateByOid, + DatabaseCollabService, WorkspaceDatabase, +}; use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout}; -use parking_lot::Mutex; use tokio::sync::RwLock; use flowy_error::{internal_error, FlowyError, FlowyResult}; @@ -16,15 +19,17 @@ use flowy_task::TaskDispatcher; use crate::deps::{DatabaseCloudService, DatabaseUser2}; use crate::entities::{ - DatabaseDescriptionPB, DatabaseLayoutPB, DatabaseSnapshotPB, RepeatedDatabaseDescriptionPB, + DatabaseDescriptionPB, DatabaseLayoutPB, DatabaseSnapshotPB, DidFetchRowPB, + RepeatedDatabaseDescriptionPB, }; -use crate::services::database::{DatabaseEditor, MutexDatabase}; +use crate::notification::{send_notification, DatabaseNotification}; +use crate::services::database::DatabaseEditor; use crate::services::database_view::DatabaseLayoutDepsResolver; use crate::services::share::csv::{CSVFormat, CSVImporter, ImportResult}; pub struct DatabaseManager2 { user: Arc, - user_database: UserDatabase, + workspace_database: Arc>>>, task_scheduler: Arc>, editors: RwLock>>, collab_builder: Arc, @@ -40,7 +45,7 @@ impl DatabaseManager2 { ) -> Self { Self { user: database_user, - user_database: UserDatabase::default(), + workspace_database: Default::default(), task_scheduler, editors: Default::default(), collab_builder, @@ -48,16 +53,53 @@ impl DatabaseManager2 { } } - pub async fn initialize(&self, user_id: i64) -> FlowyResult<()> { + fn is_collab_exist(&self, uid: i64, collab_db: &Arc, object_id: &str) -> bool { + let read_txn = collab_db.read_txn(); + read_txn.is_exist(uid, object_id) + } + + pub async fn initialize(&self, uid: i64) -> FlowyResult<()> { + let collab_db = self.user.collab_db(uid)?; + let workspace_database_id = make_workspace_database_id(uid); + let collab_builder = UserDatabaseCollabServiceImpl { + collab_builder: self.collab_builder.clone(), + cloud_service: self.cloud_service.clone(), + }; let config = CollabPersistenceConfig::new().snapshot_per_update(10); - let db = self.user.collab_db()?; - *self.user_database.lock() = Some(InnerUserDatabase::new( - user_id, - db, - config, - UserDatabaseCollabBuilderImpl(self.collab_builder.clone()), - )); - // do nothing + 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, &workspace_database_id) { + tracing::trace!("workspace database not exist, try to fetch from remote"); + match self + .cloud_service + .get_collab_update(&workspace_database_id) + .await + { + Ok(updates) => collab_raw_data = updates, + Err(err) => { + return Err(FlowyError::record_not_found().context(format!( + "get workspace database :{} failed: {}", + workspace_database_id, err, + ))); + }, + } + } + + // Construct the workspace database. + tracing::trace!("open workspace database: {}", &workspace_database_id); + let collab = collab_builder.build_collab_with_config( + uid, + &workspace_database_id, + "databases", + collab_db.clone(), + collab_raw_data, + &config, + ); + let workspace_database = + WorkspaceDatabase::open(uid, collab, collab_db, config, collab_builder); + subscribe_block_event(&workspace_database); + *self.workspace_database.write().await = Some(Arc::new(workspace_database)); Ok(()) } @@ -67,17 +109,15 @@ impl DatabaseManager2 { } pub async fn get_all_databases_description(&self) -> RepeatedDatabaseDescriptionPB { - let databases_description = self.with_user_database(vec![], |database| { - database + let mut items = vec![]; + if let Ok(wdb) = self.get_workspace_database().await { + items = wdb .get_all_databases() .into_iter() .map(DatabaseDescriptionPB::from) - .collect() - }); - - RepeatedDatabaseDescriptionPB { - items: databases_description, + .collect(); } + RepeatedDatabaseDescriptionPB { items } } pub async fn get_database_with_view_id(&self, view_id: &str) -> FlowyResult> { @@ -86,12 +126,11 @@ impl DatabaseManager2 { } pub async fn get_database_id_with_view_id(&self, view_id: &str) -> FlowyResult { - let database_id = self.with_user_database(Err(FlowyError::internal()), |database| { - database - .get_database_id_with_view_id(view_id) - .ok_or_else(FlowyError::record_not_found) - })?; - Ok(database_id) + let wdb = self.get_workspace_database().await?; + wdb.get_database_id_with_view_id(view_id).ok_or_else(|| { + FlowyError::record_not_found() + .context(format!("The database for view id: {} not found", view_id)) + }) } pub async fn get_database(&self, database_id: &str) -> FlowyResult> { @@ -104,14 +143,12 @@ impl DatabaseManager2 { pub async fn open_database(&self, database_id: &str) -> FlowyResult> { tracing::trace!("create new editor for database {}", database_id); let mut editors = self.editors.write().await; - let database = MutexDatabase::new(self.with_user_database( - Err(FlowyError::record_not_found()), - |database| { - database - .get_database(database_id) - .ok_or_else(FlowyError::record_not_found) - }, - )?); + + let wdb = self.get_workspace_database().await?; + let database = wdb + .get_database(database_id) + .await + .ok_or_else(FlowyError::record_not_found)?; let editor = Arc::new(DatabaseEditor::new(database, self.task_scheduler.clone()).await?); editors.insert(database_id.to_string(), editor.clone()); @@ -122,13 +159,11 @@ impl DatabaseManager2 { pub async fn close_database_view>(&self, view_id: T) -> FlowyResult<()> { // TODO(natan): defer closing the database if the sync is not finished let view_id = view_id.as_ref(); - let database_id = self.with_user_database(None, |databases| { - let database_id = databases.get_database_id_with_view_id(view_id); - if database_id.is_some() { - databases.close_database(database_id.as_ref().unwrap()); - } - database_id - }); + let wdb = self.get_workspace_database().await?; + let database_id = wdb.get_database_id_with_view_id(view_id); + if database_id.is_some() { + wdb.close_database(database_id.as_ref().unwrap()); + } if let Some(database_id) = database_id { let mut editors = self.editors.write().await; @@ -150,13 +185,10 @@ impl DatabaseManager2 { } pub async fn duplicate_database(&self, view_id: &str) -> FlowyResult> { - let database_data = self.with_user_database(Err(FlowyError::internal()), |database| { - let data = database.get_database_duplicated_data(view_id)?; - let json_bytes = data.to_json_bytes()?; - Ok(json_bytes) - })?; - - Ok(database_data) + let wdb = self.get_workspace_database().await?; + let data = wdb.get_database_duplicated_data(view_id).await?; + let json_bytes = data.to_json_bytes()?; + Ok(json_bytes) } /// Create a new database with the given data that can be deserialized to [DatabaseData]. @@ -168,24 +200,15 @@ impl DatabaseManager2 { ) -> FlowyResult<()> { let mut database_data = DatabaseData::from_json_bytes(data)?; database_data.view.id = view_id.to_string(); - self.with_user_database( - Err(FlowyError::internal().context("Create database with data failed")), - |database| { - let database = database.create_database_with_data(database_data)?; - Ok(database) - }, - )?; + + let wdb = self.get_workspace_database().await?; + let _ = wdb.create_database_with_data(database_data)?; Ok(()) } pub async fn create_database_with_params(&self, params: CreateDatabaseParams) -> FlowyResult<()> { - let _ = self.with_user_database( - Err(FlowyError::internal().context("Create database with params failed")), - |user_database| { - let database = user_database.create_database(params)?; - Ok(database) - }, - )?; + let wdb = self.get_workspace_database().await?; + let _ = wdb.create_database(params)?; Ok(()) } @@ -198,23 +221,18 @@ impl DatabaseManager2 { database_id: String, database_view_id: String, ) -> FlowyResult<()> { - self.with_user_database( - Err(FlowyError::internal().context("Create database view failed")), - |user_database| { - let mut params = CreateViewParams::new(database_id.clone(), database_view_id, name, layout); - if let Some(database) = user_database.get_database(&database_id) { - if let Some((field, layout_setting)) = DatabaseLayoutDepsResolver::new(database, layout) - .resolve_deps_when_create_database_linked_view() - { - params = params - .with_deps_fields(vec![field]) - .with_layout_setting(layout_setting); - } - }; - user_database.create_database_linked_view(params)?; - Ok(()) - }, - )?; + let wdb = self.get_workspace_database().await?; + let mut params = CreateViewParams::new(database_id.clone(), database_view_id, name, layout); + if let Some(database) = wdb.get_database(&database_id).await { + if let Some((field, layout_setting)) = DatabaseLayoutDepsResolver::new(database, layout) + .resolve_deps_when_create_database_linked_view() + { + params = params + .with_deps_fields(vec![field]) + .with_layout_setting(layout_setting); + } + }; + wdb.create_database_linked_view(params).await?; Ok(()) } @@ -268,7 +286,7 @@ impl DatabaseManager2 { let mut snapshots = vec![]; if let Some(snapshot) = self .cloud_service - .get_database_latest_snapshot(&database_id) + .get_collab_latest_snapshot(&database_id) .await? .map(|snapshot| DatabaseSnapshotPB { snapshot_id: snapshot.snapshot_id, @@ -283,14 +301,11 @@ impl DatabaseManager2 { Ok(snapshots) } - fn with_user_database(&self, default_value: Output, f: F) -> Output - where - F: FnOnce(&InnerUserDatabase) -> Output, - { - let database = self.user_database.lock(); + async fn get_workspace_database(&self) -> FlowyResult> { + let database = self.workspace_database.read().await; match &*database { - None => default_value, - Some(folder) => f(folder), + None => Err(FlowyError::internal().context("Workspace database not initialized")), + Some(user_database) => Ok(user_database.clone()), } } @@ -301,33 +316,97 @@ impl DatabaseManager2 { } } -#[derive(Clone, Default)] -pub struct UserDatabase(Arc>>); - -impl Deref for UserDatabase { - type Target = Arc>>; - fn deref(&self) -> &Self::Target { - &self.0 - } +/// Send notification to all clients that are listening to the given object. +fn subscribe_block_event(workspace_database: &WorkspaceDatabase) { + let mut block_event_rx = workspace_database.subscribe_block_event(); + tokio::spawn(async move { + while let Ok(event) = block_event_rx.recv().await { + match event { + BlockEvent::DidFetchRow(row_details) => { + for row_detail in row_details { + tracing::trace!("Did fetch row: {:?}", row_detail.row.id); + let row_id = row_detail.row.id.clone(); + let pb = DidFetchRowPB::from(row_detail); + send_notification(&row_id, DatabaseNotification::DidFetchRow) + .payload(pb) + .send(); + } + }, + } + } + }); } -unsafe impl Sync for UserDatabase {} +struct UserDatabaseCollabServiceImpl { + collab_builder: Arc, + cloud_service: Arc, +} -unsafe impl Send for UserDatabase {} +impl DatabaseCollabService for UserDatabaseCollabServiceImpl { + fn get_collab_update( + &self, + object_id: &str, + ) -> CollabFuture> { + let object_id = object_id.to_string(); + let weak_cloud_service = Arc::downgrade(&self.cloud_service); + Box::pin(async move { + match weak_cloud_service.upgrade() { + None => { + tracing::warn!("Cloud service is dropped"); + Ok(vec![]) + }, + Some(cloud_service) => { + let updates = cloud_service + .get_collab_update(&object_id) + .await + .map_err(|e| DatabaseError::Internal(Box::new(e)))?; + Ok(updates) + }, + } + }) + } -struct UserDatabaseCollabBuilderImpl(Arc); + fn batch_get_collab_update( + &self, + object_ids: Vec, + ) -> CollabFuture> { + let weak_cloud_service = Arc::downgrade(&self.cloud_service); + Box::pin(async move { + match weak_cloud_service.upgrade() { + None => { + tracing::warn!("Cloud service is dropped"); + Ok(CollabObjectUpdateByOid::default()) + }, + Some(cloud_service) => { + let updates = cloud_service + .batch_get_collab_updates(object_ids) + .await + .map_err(|e| DatabaseError::Internal(Box::new(e)))?; + Ok(updates) + }, + } + }) + } -impl DatabaseCollabBuilder for UserDatabaseCollabBuilderImpl { - fn build_with_config( + fn build_collab_with_config( &self, uid: i64, object_id: &str, object_name: &str, - db: Arc, + collab_db: Arc, + collab_raw_data: CollabRawData, config: &CollabPersistenceConfig, ) -> Arc { self - .0 - .build_with_config(uid, object_id, object_name, db, config) + .collab_builder + .build_with_config( + uid, + object_id, + object_name, + collab_db, + collab_raw_data, + config, + ) + .unwrap() } } diff --git a/frontend/rust-lib/flowy-database2/src/notification.rs b/frontend/rust-lib/flowy-database2/src/notification.rs index 36cb0072f0..3cc6001610 100644 --- a/frontend/rust-lib/flowy-database2/src/notification.rs +++ b/frontend/rust-lib/flowy-database2/src/notification.rs @@ -7,6 +7,9 @@ const DATABASE_OBSERVABLE_SOURCE: &str = "Database"; pub enum DatabaseNotification { #[default] Unknown = 0, + /// Fetch row data from the remote server. It will be triggered if the backend support remote + /// storage. + DidFetchRow = 19, /// Trigger after inserting/deleting/updating a row DidUpdateViewRows = 20, /// Trigger when the visibility of the row was changed. For example, updating the filter will trigger the notification @@ -58,6 +61,7 @@ impl std::convert::From for i32 { impl std::convert::From for DatabaseNotification { fn from(notification: i32) -> Self { match notification { + 19 => DatabaseNotification::DidFetchRow, 20 => DatabaseNotification::DidUpdateViewRows, 21 => DatabaseNotification::DidUpdateViewRowsVisibility, 22 => DatabaseNotification::DidUpdateFields, 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 099f48fab4..fc2d3e2d44 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 @@ -1,14 +1,12 @@ use std::collections::HashMap; -use std::ops::Deref; use std::sync::Arc; use bytes::Bytes; -use collab_database::database::Database as InnerDatabase; +use collab_database::database::MutexDatabase; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cell, Cells, CreateRowParams, Row, RowCell, RowId}; +use collab_database::rows::{Cell, Cells, CreateRowParams, Row, RowCell, RowDetail, RowId}; use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; use futures::StreamExt; -use parking_lot::Mutex; use tokio::sync::{broadcast, RwLock}; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; @@ -21,7 +19,7 @@ use crate::services::cell::{ apply_cell_changeset, get_cell_protobuf, AnyTypeCache, CellCache, ToCellChangeset, }; use crate::services::database::util::database_view_setting_pb_from_view; -use crate::services::database::{RowDetail, UpdatedRow}; +use crate::services::database::UpdatedRow; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, DatabaseViews}; use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData}; use crate::services::field::{ @@ -38,14 +36,14 @@ use crate::services::sort::Sort; #[derive(Clone)] pub struct DatabaseEditor { - database: MutexDatabase, + database: Arc, pub cell_cache: CellCache, database_views: Arc, } impl DatabaseEditor { pub async fn new( - database: MutexDatabase, + database: Arc, task_scheduler: Arc>, ) -> FlowyResult { let cell_cache = AnyTypeCache::::new(); @@ -76,7 +74,7 @@ impl DatabaseEditor { tokio::spawn(async move { while let Some(snapshot_state) = snapshot_state.next().await { if let Some(new_snapshot_id) = snapshot_state.snapshot_id() { - tracing::debug!("Did create database snapshot: {}", new_snapshot_id); + tracing::debug!("Did create database remote snapshot: {}", new_snapshot_id); send_notification( &database_id, DatabaseNotification::DidUpdateDatabaseSnapshotState, @@ -415,8 +413,7 @@ impl DatabaseEditor { let delete_row_id = from.into_inner(); let insert_row = InsertedRowPB::new(RowMetaPB::from(&row_meta)).with_index(to_index as i32); - let changes = - RowsChangePB::from_move(view_id.to_string(), vec![delete_row_id], vec![insert_row]); + let changes = RowsChangePB::from_move(vec![delete_row_id], vec![insert_row]); send_notification(view_id, DatabaseNotification::DidUpdateViewRows) .payload(changes) .send(); @@ -437,7 +434,7 @@ impl DatabaseEditor { 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(row), Some(meta)) = (row, row_meta) { + if let Some(meta) = row_meta { let row_detail = RowDetail { row, meta }; for view in self.database_views.editors().await { view.v_did_create_row(&row_detail, &group_id, index).await; @@ -527,7 +524,7 @@ impl DatabaseEditor { pub fn get_row(&self, view_id: &str, row_id: &RowId) -> Option { if self.database.lock().views.is_row_exist(view_id, row_id) { - self.database.lock().get_row(row_id) + Some(self.database.lock().get_row(row_id)) } else { None } @@ -551,7 +548,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)?; + let row = self.database.lock().get_row(row_id); Some(RowDetail { row, meta }) } else { tracing::warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id); @@ -597,40 +594,23 @@ impl DatabaseEditor { let field_type = FieldType::from(field.field_type); // If the cell data is referenced, return the reference data. Otherwise, return an empty cell. match field_type { - FieldType::LastEditedTime | FieldType::CreatedTime => database - .get_row(row_id) - .map(|row| { - if field_type.is_created_time() { - DateCellData::new(row.created_at, true) - } else { - DateCellData::new(row.modified_at, true) - } - }) - .map(Cell::from), + FieldType::LastEditedTime | FieldType::CreatedTime => { + let row = database.get_row(row_id); + let cell_data = if field_type.is_created_time() { + DateCellData::new(row.created_at, true) + } else { + DateCellData::new(row.modified_at, true) + }; + Some(Cell::from(cell_data)) + }, _ => database.get_cell(field_id, row_id).cell, } } pub async fn get_cell_pb(&self, field_id: &str, row_id: &RowId) -> Option { let (field, cell) = { - let database = self.database.lock(); - let field = database.fields.get_field(field_id)?; - let field_type = FieldType::from(field.field_type); - // If the cell data is referenced, return the reference data. Otherwise, return an empty cell. - let cell = match field_type { - FieldType::LastEditedTime | FieldType::CreatedTime => database - .get_row(row_id) - .map(|row| { - if field_type.is_created_time() { - DateCellData::new(row.created_at, true) - } else { - DateCellData::new(row.modified_at, true) - } - }) - .map(Cell::from), - _ => database.get_cell(field_id, row_id).cell, - }?; - + let cell = self.get_cell(field_id, row_id).await?; + let field = self.database.lock().fields.get_field(field_id)?; (field, cell) }; @@ -723,7 +703,7 @@ impl DatabaseEditor { if let Some(new_row_detail) = option_row { let updated_row = UpdatedRow::new(&new_row_detail.row.id).with_field_ids(vec![field_id.to_string()]); - let changes = RowsChangePB::from_update(view_id.to_string(), updated_row.into()); + let changes = RowsChangePB::from_update(updated_row.into()); send_notification(view_id, DatabaseNotification::DidUpdateViewRows) .payload(changes) .send(); @@ -1154,35 +1134,15 @@ fn cell_changesets_from_cell_by_field_id( .collect() } -#[derive(Clone)] -pub struct MutexDatabase(Arc>>); - -impl MutexDatabase { - pub(crate) fn new(database: Arc) -> Self { - Self(Arc::new(Mutex::new(database))) - } -} - -impl Deref for MutexDatabase { - type Target = Arc>>; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -unsafe impl Sync for MutexDatabase {} - -unsafe impl Send for MutexDatabase {} - struct DatabaseViewDataImpl { - database: MutexDatabase, + database: Arc, task_scheduler: Arc>, cell_cache: CellCache, } impl DatabaseViewData for DatabaseViewDataImpl { - fn get_database(&self) -> Arc { - self.database.lock().clone() + fn get_database(&self) -> Arc { + self.database.clone() } fn get_view(&self, view_id: &str) -> Fut> { @@ -1245,8 +1205,8 @@ impl DatabaseViewData for DatabaseViewDataImpl { let row = self.database.lock().get_row(row_id); let row_meta = self.database.lock().get_row_meta(row_id); to_fut(async move { - match (index, row, row_meta) { - (Some(index), Some(row), Some(row_meta)) => { + match (index, row_meta) { + (Some(index), Some(row_meta)) => { let row_detail = RowDetail { row, meta: row_meta, 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 b691dc7200..edf48c352e 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::{Row, RowId, RowMeta}; +use collab_database::rows::{RowId, RowMeta}; use collab_database::views::DatabaseLayout; #[derive(Debug, Clone)] @@ -64,9 +64,3 @@ pub struct CreateDatabaseViewParams { pub view_id: String, pub layout_type: DatabaseLayout, } - -#[derive(Debug, Clone)] -pub struct RowDetail { - pub row: Row, - pub meta: RowMeta, -} diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs index 8f75bf2ba0..77b9e92370 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs @@ -1,8 +1,7 @@ -use std::sync::Arc; - -use collab_database::database::{gen_field_id, Database}; +use collab_database::database::{gen_field_id, MutexDatabase}; use collab_database::fields::Field; use collab_database::views::{DatabaseLayout, LayoutSetting}; +use std::sync::Arc; use crate::entities::FieldType; use crate::services::field::DateTypeOption; @@ -11,13 +10,13 @@ use crate::services::setting::CalendarLayoutSetting; /// When creating a database, we need to resolve the dependencies of the views. Different database /// view has different dependencies. For example, a calendar view depends on a date field. pub struct DatabaseLayoutDepsResolver { - pub database: Arc, + pub database: Arc, /// The new database layout. pub database_layout: DatabaseLayout, } impl DatabaseLayoutDepsResolver { - pub fn new(database: Arc, database_layout: DatabaseLayout) -> Self { + pub fn new(database: Arc, database_layout: DatabaseLayout) -> Self { Self { database, database_layout, @@ -39,7 +38,7 @@ impl DatabaseLayoutDepsResolver { /// If the new layout type is a calendar and there is not date field in the database, it will add /// a new date field to the database and create the corresponding layout setting. pub fn resolve_deps_when_update_layout_type(&self, view_id: &str) { - let fields = self.database.get_fields(None); + let fields = self.database.lock().get_fields(None); // Insert the layout setting if it's not exist match &self.database_layout { DatabaseLayout::Grid => {}, @@ -53,7 +52,7 @@ impl DatabaseLayoutDepsResolver { tracing::trace!("Create a new date field after layout type change"); let field = self.create_date_field(); let field_id = field.id.clone(); - self.database.create_field(field); + self.database.lock().create_field(field); field_id }, Some(date_field) => date_field.id, @@ -66,12 +65,14 @@ impl DatabaseLayoutDepsResolver { fn create_calendar_layout_setting_if_need(&self, view_id: &str, field_id: &str) { if self .database + .lock() .get_layout_setting::(view_id, &self.database_layout) .is_none() { let layout_setting = CalendarLayoutSetting::new(field_id.to_string()); self .database + .lock() .insert_layout_setting(view_id, &self.database_layout, layout_setting); } } 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 1be0edfa3d..f675fb9180 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 @@ -2,9 +2,9 @@ use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; -use collab_database::database::{gen_database_filter_id, gen_database_sort_id, Database}; +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, RowId, RowMeta}; +use collab_database::rows::{Cells, Row, RowCell, RowDetail, RowId, RowMeta}; use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; use tokio::sync::{broadcast, RwLock}; @@ -20,9 +20,7 @@ use crate::entities::{ }; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::cell::CellCache; -use crate::services::database::{ - database_view_setting_pb_from_view, DatabaseRowEvent, RowDetail, UpdatedRow, -}; +use crate::services::database::{database_view_setting_pb_from_view, DatabaseRowEvent, UpdatedRow}; use crate::services::database_view::view_filter::make_filter_controller; use crate::services::database_view::view_group::{ get_cell_for_row, get_cells_for_field, new_group_controller, new_group_controller_with_field, @@ -44,7 +42,7 @@ use crate::services::setting::CalendarLayoutSetting; use crate::services::sort::{DeletedSortType, Sort, SortChangeset, SortController, SortType}; pub trait DatabaseViewData: Send + Sync + 'static { - fn get_database(&self) -> Arc; + fn get_database(&self) -> Arc; fn get_view(&self, view_id: &str) -> Fut>; /// If the field_ids is None, then it will return all the field revisions @@ -204,7 +202,7 @@ impl DatabaseViewEditor { 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()); - let changeset = RowsChangePB::from_update(self.view_id.clone(), update_row.into()); + let changeset = RowsChangePB::from_update(update_row.into()); send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) .payload(changeset) .send(); @@ -221,7 +219,7 @@ impl DatabaseViewEditor { match group_id.as_ref() { None => { let row = InsertedRowPB::new(RowMetaPB::from(&row_detail.meta)).with_index(index as i32); - changes = RowsChangePB::from_insert(self.view_id.clone(), row); + changes = RowsChangePB::from_insert(row); }, Some(group_id) => { self @@ -239,7 +237,7 @@ impl DatabaseViewEditor { let changeset = GroupRowsNotificationPB::insert(group_id.clone(), vec![inserted_row.clone()]); notify_did_update_group_rows(changeset).await; - changes = RowsChangePB::from_insert(self.view_id.clone(), inserted_row); + changes = RowsChangePB::from_insert(inserted_row); }, } @@ -263,7 +261,7 @@ impl DatabaseViewEditor { notify_did_update_group_rows(changeset).await; } } - let changes = RowsChangePB::from_delete(self.view_id.clone(), row.id.clone().into_inner()); + let changes = RowsChangePB::from_delete(row.id.clone().into_inner()); send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) .payload(changes) .send(); @@ -311,7 +309,7 @@ impl DatabaseViewEditor { } else { let update_row = UpdatedRow::new(&row_detail.row.id).with_field_ids(vec![field_id.to_string()]); - let changeset = RowsChangePB::from_update(self.view_id.clone(), update_row.into()); + let changeset = RowsChangePB::from_update(update_row.into()); send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) .payload(changeset) .send(); @@ -880,23 +878,13 @@ impl DatabaseViewEditor { pub async fn handle_row_event(&self, event: Cow<'_, DatabaseRowEvent>) { let changeset = match event.into_owned() { - DatabaseRowEvent::InsertRow(row) => { - RowsChangePB::from_insert(self.view_id.clone(), row.into()) - }, - DatabaseRowEvent::UpdateRow(row) => { - RowsChangePB::from_update(self.view_id.clone(), row.into()) - }, - DatabaseRowEvent::DeleteRow(row_id) => { - RowsChangePB::from_delete(self.view_id.clone(), row_id.into_inner()) - }, + DatabaseRowEvent::InsertRow(row) => RowsChangePB::from_insert(row.into()), + DatabaseRowEvent::UpdateRow(row) => RowsChangePB::from_update(row.into()), + DatabaseRowEvent::DeleteRow(row_id) => RowsChangePB::from_delete(row_id.into_inner()), DatabaseRowEvent::Move { deleted_row_id, inserted_row, - } => RowsChangePB::from_move( - self.view_id.clone(), - vec![deleted_row_id.into_inner()], - vec![inserted_row.into()], - ), + } => RowsChangePB::from_move(vec![deleted_row_id.into_inner()], vec![inserted_row.into()]), }; send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs index 88c65f76ac..eec723e6a0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs @@ -1,12 +1,11 @@ use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::RowId; +use collab_database::rows::{RowDetail, RowId}; use lib_infra::future::{to_fut, Fut}; use crate::services::cell::CellCache; -use crate::services::database::RowDetail; use crate::services::database_view::{ gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewData, }; diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs index b450a99fbc..6fec7d9ff9 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs @@ -1,12 +1,12 @@ use std::sync::Arc; use collab_database::fields::Field; +use collab_database::rows::RowDetail; use tokio::sync::RwLock; use lib_infra::future::{to_fut, Fut}; use crate::services::cell::CellCache; -use crate::services::database::RowDetail; use crate::services::database_view::{ gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewData, }; diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs index c0dd434a03..509ff5849b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; use std::sync::Arc; +use collab_database::database::MutexDatabase; use collab_database::fields::Field; -use collab_database::rows::RowId; +use collab_database::rows::{RowDetail, RowId}; use nanoid::nanoid; use tokio::sync::{broadcast, RwLock}; @@ -10,7 +11,7 @@ use flowy_error::FlowyResult; use lib_infra::future::Fut; use crate::services::cell::CellCache; -use crate::services::database::{DatabaseRowEvent, MutexDatabase, RowDetail}; +use crate::services::database::DatabaseRowEvent; use crate::services::database_view::{DatabaseViewData, DatabaseViewEditor}; use crate::services::group::RowChangeset; @@ -19,7 +20,7 @@ pub type RowEventReceiver = broadcast::Receiver; pub struct DatabaseViews { #[allow(dead_code)] - database: MutexDatabase, + database: Arc, cell_cache: CellCache, database_view_data: Arc, editor_map: Arc>>>, @@ -27,7 +28,7 @@ pub struct DatabaseViews { impl DatabaseViews { pub async fn new( - database: MutexDatabase, + database: Arc, cell_cache: CellCache, database_view_data: Arc, ) -> FlowyResult { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs index a5f316b5d9..356d8d6d96 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs @@ -9,7 +9,6 @@ use crate::entities::{FieldType, SelectOptionCellDataPB}; use crate::services::cell::{ CellDataDecoder, CellProtobufBlobParser, DecodedCellData, FromCellChangeset, ToCellChangeset, }; - use crate::services::field::selection_type_option::type_option_transform::SelectOptionTypeOptionTransformHelper; use crate::services::field::{ make_selected_options, CheckboxCellData, MultiSelectTypeOption, SelectOption, @@ -168,7 +167,7 @@ pub fn select_type_option_from_field( Ok(Box::new(type_option)) }, ty => { - tracing::error!("Unsupported field type: {:?} for this handler", ty); + tracing::error!("🔴Unsupported field type: {:?} for this handler", ty); Err(ErrorCode::FieldInvalidOperation.into()) }, } 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 6b60ed784f..fe676fdba3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::{Cell, Row, RowId}; +use collab_database::rows::{Cell, Row, RowDetail, RowId}; use dashmap::DashMap; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; @@ -15,7 +15,6 @@ use lib_infra::future::Fut; use crate::entities::filter_entities::*; use crate::entities::{FieldType, InsertedRowPB, RowMetaPB}; use crate::services::cell::{AnyTypeCache, CellCache, CellFilterCache}; -use crate::services::database::RowDetail; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; use crate::services::field::*; use crate::services::filter::{Filter, FilterChangeset, FilterResult, FilterResultNotification}; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/action.rs b/frontend/rust-lib/flowy-database2/src/services/group/action.rs index 15401267f0..015fd8f796 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/action.rs @@ -1,11 +1,10 @@ use collab_database::fields::Field; -use collab_database::rows::{Cell, Row}; +use collab_database::rows::{Cell, Row, RowDetail}; use flowy_error::FlowyResult; use crate::entities::{GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; use crate::services::cell::DecodedCellData; -use crate::services::database::RowDetail; use crate::services::group::controller::MoveGroupRowContext; use crate::services::group::{GroupData, GroupSettingChangeset}; 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 6bda5d74fb..ea2474de28 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use std::sync::Arc; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cell, Cells, Row, RowId}; +use collab_database::rows::{Cell, Cells, Row, RowDetail, RowId}; use serde::de::DeserializeOwned; use serde::Serialize; @@ -13,7 +13,6 @@ use crate::entities::{ FieldType, GroupChangesPB, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB, }; use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser, DecodedCellData}; -use crate::services::database::RowDetail; use crate::services::group::action::{ DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, GroupCustomize, }; 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 2b67c53f07..75645c17dd 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 @@ -1,12 +1,11 @@ use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; use crate::entities::{FieldType, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB}; use crate::services::cell::insert_checkbox_cell; -use crate::services::database::RowDetail; use crate::services::field::{ CheckboxCellData, CheckboxCellDataParser, CheckboxTypeOption, CHECK, UNCHECK, }; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs index dde8909047..3b2a0d40e6 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs @@ -1,12 +1,11 @@ use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::{Cells, Row}; +use collab_database::rows::{Cells, Row, RowDetail}; use flowy_error::FlowyResult; use crate::entities::GroupChangesPB; -use crate::services::database::RowDetail; use crate::services::group::action::{ DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, }; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index 4418a83bf0..84ea090e42 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -1,12 +1,11 @@ use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; use crate::entities::{FieldType, GroupRowsNotificationPB, SelectOptionCellDataPB}; use crate::services::cell::insert_select_option_cell; -use crate::services::database::RowDetail; use crate::services::field::{MultiSelectTypeOption, SelectOptionCellDataParser}; use crate::services::group::action::GroupCustomize; use crate::services::group::controller::{ diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index ba278e092a..dcfce42488 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -1,12 +1,11 @@ use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; use crate::entities::{FieldType, GroupRowsNotificationPB, SelectOptionCellDataPB}; use crate::services::cell::insert_select_option_cell; -use crate::services::database::RowDetail; use crate::services::field::{SelectOptionCellDataParser, SingleSelectTypeOption}; use crate::services::group::action::GroupCustomize; use crate::services::group::controller::{ 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 2cc8249566..d2face26c2 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 @@ -1,11 +1,10 @@ use collab_database::fields::Field; -use collab_database::rows::{Cell, Row}; +use collab_database::rows::{Cell, Row, RowDetail}; use crate::entities::{ FieldType, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB, SelectOptionCellDataPB, }; use crate::services::cell::{insert_checkbox_cell, insert_select_option_cell, insert_url_cell}; -use crate::services::database::RowDetail; use crate::services::field::{SelectOption, CHECK}; use crate::services::group::controller::MoveGroupRowContext; use crate::services::group::{GeneratedGroupConfig, Group, GroupData}; 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 b86683c057..d2fa44bc30 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 @@ -1,7 +1,7 @@ use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; use flowy_error::FlowyResult; @@ -11,7 +11,6 @@ use crate::entities::{ URLCellDataPB, }; use crate::services::cell::insert_url_cell; -use crate::services::database::RowDetail; use crate::services::field::{URLCellData, URLCellDataParser, URLTypeOption}; use crate::services::group::action::GroupCustomize; use crate::services::group::configuration::GroupContext; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs index b88b084aa6..f7b2e1982c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs @@ -1,12 +1,10 @@ use anyhow::bail; use collab::core::any_map::AnyMapExtension; use collab_database::database::gen_database_group_id; -use collab_database::rows::RowId; +use collab_database::rows::{RowDetail, RowId}; use collab_database::views::{GroupMap, GroupMapBuilder, GroupSettingBuilder, GroupSettingMap}; use serde::{Deserialize, Serialize}; -use crate::services::database::RowDetail; - #[derive(Debug, Clone, Default)] pub struct GroupSetting { pub id: String, diff --git a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs index 688e421e4d..f8b4601120 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs @@ -1,12 +1,12 @@ use std::sync::Arc; use collab_database::fields::Field; +use collab_database::rows::RowDetail; use collab_database::views::DatabaseLayout; use flowy_error::FlowyResult; use crate::entities::FieldType; -use crate::services::database::RowDetail; use crate::services::group::configuration::GroupSettingReader; use crate::services::group::controller::GroupController; use crate::services::group::{ diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs index c2c85eec86..12b78c6bee 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::{Cell, Row, RowId}; +use collab_database::rows::{Cell, Row, RowDetail, RowId}; use rayon::prelude::ParallelSliceMut; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; @@ -16,7 +16,6 @@ use lib_infra::future::Fut; use crate::entities::FieldType; use crate::entities::SortChangesetNotificationPB; use crate::services::cell::CellCache; -use crate::services::database::RowDetail; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; use crate::services::field::{default_order, TypeOptionCellExt}; use crate::services::sort::{ diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index eb549c589d..e1cc1ba34c 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -3,12 +3,12 @@ use std::sync::Arc; use collab_database::database::{gen_database_view_id, timestamp}; use collab_database::fields::Field; -use collab_database::rows::{CreateRowParams, RowId}; +use collab_database::rows::{CreateRowParams, RowDetail, RowId}; use strum::EnumCount; use flowy_database2::entities::{FieldType, FilterPB, RowMetaPB, SelectOptionPB}; use flowy_database2::services::cell::{CellBuilder, ToCellChangeset}; -use flowy_database2::services::database::{DatabaseEditor, RowDetail}; +use flowy_database2::services::database::DatabaseEditor; use flowy_database2::services::field::checklist_type_option::{ ChecklistCellChangeset, ChecklistTypeOption, }; diff --git a/frontend/rust-lib/flowy-document2/src/deps.rs b/frontend/rust-lib/flowy-document2/src/deps.rs index f3c659321b..f92aa9815f 100644 --- a/frontend/rust-lib/flowy-document2/src/deps.rs +++ b/frontend/rust-lib/flowy-document2/src/deps.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use appflowy_integrate::RocksCollabDB; +pub use collab_document::blocks::DocumentData; use flowy_error::FlowyError; use lib_infra::future::FutureResult; @@ -8,7 +9,7 @@ use lib_infra::future::FutureResult; pub trait DocumentUser: Send + Sync { fn user_id(&self) -> Result; fn token(&self) -> Result, FlowyError>; // unused now. - fn collab_db(&self) -> Result, FlowyError>; + fn collab_db(&self, uid: i64) -> Result, FlowyError>; } /// A trait for document cloud service. @@ -21,6 +22,8 @@ pub trait DocumentCloudService: Send + Sync + 'static { &self, document_id: &str, ) -> FutureResult, FlowyError>; + + fn get_document_data(&self, document_id: &str) -> FutureResult, FlowyError>; } pub struct DocumentSnapshot { diff --git a/frontend/rust-lib/flowy-document2/src/document.rs b/frontend/rust-lib/flowy-document2/src/document.rs index 7428442f72..8bfa68eddf 100644 --- a/frontend/rust-lib/flowy-document2/src/document.rs +++ b/frontend/rust-lib/flowy-document2/src/document.rs @@ -70,7 +70,7 @@ fn subscribe_document_snapshot_state(collab: &Arc) { tokio::spawn(async move { while let Some(snapshot_state) = snapshot_state.next().await { if let Some(new_snapshot_id) = snapshot_state.snapshot_id() { - tracing::debug!("Did create document snapshot: {}", new_snapshot_id); + tracing::debug!("Did create document remote snapshot: {}", new_snapshot_id); send_notification( &document_id, DocumentNotification::DidUpdateDocumentSnapshotState, diff --git a/frontend/rust-lib/flowy-document2/src/entities.rs b/frontend/rust-lib/flowy-document2/src/entities.rs index 6f7a41ba90..88e80db055 100644 --- a/frontend/rust-lib/flowy-document2/src/entities.rs +++ b/frontend/rust-lib/flowy-document2/src/entities.rs @@ -1,11 +1,13 @@ -use collab::core::collab_state::SyncState; -use collab_document::blocks::{BlockAction, DocumentData}; use std::collections::HashMap; -use crate::parse::{NotEmptyStr, NotEmptyVec}; +use collab::core::collab_state::SyncState; +use collab_document::blocks::{BlockAction, DocumentData}; + use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; +use crate::parse::{NotEmptyStr, NotEmptyVec}; + #[derive(Default, ProtoBuf)] pub struct OpenDocumentPayloadPB { #[pb(index = 1)] @@ -270,7 +272,7 @@ impl From for ExportType { 1 => ExportType::Markdown, 2 => ExportType::Link, _ => { - tracing::error!("Invalid export type: {}", val); + tracing::error!("🔴Invalid export type: {}", val); ExportType::Text }, } @@ -306,7 +308,7 @@ impl From for ConvertType { match val { 0 => ConvertType::Json, _ => { - tracing::error!("Invalid export type: {}", val); + tracing::error!("🔴Invalid export type: {}", val); ConvertType::Json }, } diff --git a/frontend/rust-lib/flowy-document2/src/event_handler.rs b/frontend/rust-lib/flowy-document2/src/event_handler.rs index c36c794655..a9b10dba4e 100644 --- a/frontend/rust-lib/flowy-document2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document2/src/event_handler.rs @@ -34,7 +34,7 @@ pub(crate) async fn open_document_handler( ) -> DataResult { let params: OpenDocumentParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.get_document(&doc_id)?; + let document = manager.get_document(&doc_id).await?; let document_data = document.lock().get_document_data()?; data_result_ok(DocumentDataPB::from(document_data)) } @@ -57,7 +57,7 @@ pub(crate) async fn get_document_data_handler( ) -> DataResult { let params: OpenDocumentParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document_data = manager.get_document_data(&doc_id)?; + let document_data = manager.get_document_data(&doc_id).await?; data_result_ok(DocumentDataPB::from(document_data)) } @@ -68,7 +68,7 @@ pub(crate) async fn apply_action_handler( ) -> FlowyResult<()> { let params: ApplyActionParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.get_document(&doc_id)?; + let document = manager.get_document(&doc_id).await?; let actions = params.actions; document.lock().apply_action(actions); Ok(()) @@ -104,7 +104,7 @@ pub(crate) async fn redo_handler( ) -> DataResult { let params: DocumentRedoUndoParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.get_document(&doc_id)?; + let document = manager.get_document(&doc_id).await?; let document = document.lock(); let redo = document.redo(); let can_redo = document.can_redo(); @@ -122,7 +122,7 @@ pub(crate) async fn undo_handler( ) -> DataResult { let params: DocumentRedoUndoParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.get_document(&doc_id)?; + let document = manager.get_document(&doc_id).await?; let document = document.lock(); let undo = document.undo(); let can_redo = document.can_redo(); @@ -140,7 +140,7 @@ pub(crate) async fn can_undo_redo_handler( ) -> DataResult { let params: DocumentRedoUndoParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.get_document(&doc_id)?; + let document = manager.get_document(&doc_id).await?; let document = document.lock(); let can_redo = document.can_redo(); let can_undo = document.can_undo(); diff --git a/frontend/rust-lib/flowy-document2/src/manager.rs b/frontend/rust-lib/flowy-document2/src/manager.rs index 1dee80d4cf..dd74a1d6e1 100644 --- a/frontend/rust-lib/flowy-document2/src/manager.rs +++ b/frontend/rust-lib/flowy-document2/src/manager.rs @@ -45,28 +45,35 @@ impl DocumentManager { data: Option, ) -> FlowyResult> { tracing::trace!("create a document: {:?}", doc_id); - let collab = self.collab_for_document(doc_id)?; + let collab = self.collab_for_document(doc_id, vec![])?; let data = data.unwrap_or_else(default_document_data); let document = Arc::new(MutexDocument::create_with_data(collab, data)?); Ok(document) } /// Return the document - pub fn get_document(&self, doc_id: &str) -> FlowyResult> { + pub async fn get_document(&self, doc_id: &str) -> FlowyResult> { if let Some(doc) = self.documents.read().get(doc_id) { return Ok(doc.clone()); } - // Check if the document exists. If not, return error. + let mut updates = vec![]; if !self.is_doc_exist(doc_id)? { - return Err( - FlowyError::record_not_found().context(format!("document: {} is not exist", doc_id)), - ); + // Try to get the document from the cloud service + if let Ok(document_updates) = self.cloud_service.get_document_updates(doc_id).await { + updates = document_updates; + } else { + return Err( + FlowyError::record_not_found().context(format!("document: {} is not exist", doc_id)), + ); + }; } tracing::debug!("open_document: {:?}", doc_id); let uid = self.user.user_id()?; - let db = self.user.collab_db()?; - let collab = self.collab_builder.build(uid, doc_id, "document", db); + let db = self.user.collab_db(uid)?; + let collab = self + .collab_builder + .build(uid, doc_id, "document", updates, db)?; 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. @@ -78,14 +85,19 @@ impl DocumentManager { Ok(document) } - pub fn get_document_data(&self, doc_id: &str) -> FlowyResult { + pub async fn get_document_data(&self, doc_id: &str) -> FlowyResult { + let mut updates = vec![]; if !self.is_doc_exist(doc_id)? { - return Err( - FlowyError::record_not_found().context(format!("document: {} is not exist", doc_id)), - ); + if let Ok(document_updates) = self.cloud_service.get_document_updates(doc_id).await { + updates = document_updates; + } else { + return Err( + FlowyError::record_not_found().context(format!("document: {} is not exist", doc_id)), + ); + } } - let collab = self.collab_for_document(doc_id)?; + let collab = self.collab_for_document(doc_id, updates)?; Document::open(collab)? .get_document_data() .map_err(internal_error) @@ -98,7 +110,7 @@ impl DocumentManager { pub fn delete_document(&self, doc_id: &str) -> FlowyResult<()> { let uid = self.user.user_id()?; - let db = self.user.collab_db()?; + let db = self.user.collab_db(uid)?; let _ = db.with_write_txn(|txn| { txn.delete_doc(uid, &doc_id)?; Ok(()) @@ -130,15 +142,22 @@ impl DocumentManager { Ok(snapshots) } - fn collab_for_document(&self, doc_id: &str) -> FlowyResult> { + fn collab_for_document( + &self, + doc_id: &str, + updates: Vec>, + ) -> FlowyResult> { let uid = self.user.user_id()?; - let db = self.user.collab_db()?; - Ok(self.collab_builder.build(uid, doc_id, "document", db)) + let db = self.user.collab_db(uid)?; + let collab = self + .collab_builder + .build(uid, doc_id, "document", updates, db)?; + Ok(collab) } fn is_doc_exist(&self, doc_id: &str) -> FlowyResult { let uid = self.user.user_id()?; - let db = self.user.collab_db()?; + let db = self.user.collab_db(uid)?; let read_txn = db.read_txn(); Ok(read_txn.is_exist(uid, doc_id)) } diff --git a/frontend/rust-lib/flowy-document2/tests/document/document_insert_test.rs b/frontend/rust-lib/flowy-document2/tests/document/document_insert_test.rs index 060f52f001..6a4d5d46a4 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/document_insert_test.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/document_insert_test.rs @@ -9,7 +9,7 @@ use crate::document::util::gen_id; #[tokio::test] async fn document_apply_insert_block_with_empty_parent_id() { - let (_, document, page_id) = util::create_and_open_empty_document(); + let (_, document, page_id) = util::create_and_open_empty_document().await; // create a text block with no parent let text_block_id = gen_id(); 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 c3095e8cba..d05dee8ca4 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 @@ -17,7 +17,7 @@ async fn undo_redo_test() { _ = test.create_document(&doc_id, Some(data.clone())); // open a document - let document = test.get_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); let document = document.lock(); let page_block = document.get_block(&data.page_id).unwrap(); let page_id = page_block.id; 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 06a9c64208..053448c789 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/document_test.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/document_test.rs @@ -21,6 +21,7 @@ async fn restore_document() { // open a document let data_b = test .get_document(&doc_id) + .await .unwrap() .lock() .get_document_data() @@ -34,6 +35,7 @@ async fn restore_document() { // open a document let data_b = test .get_document(&doc_id) + .await .unwrap() .lock() .get_document_data() @@ -54,7 +56,7 @@ async fn document_apply_insert_action() { _ = test.create_document(&doc_id, Some(data.clone())); // open a document - let document = test.get_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); let page_block = document.lock().get_block(&data.page_id).unwrap(); // insert a text block @@ -83,6 +85,7 @@ async fn document_apply_insert_action() { // re-open the document let data_b = test .get_document(&doc_id) + .await .unwrap() .lock() .get_document_data() @@ -103,7 +106,7 @@ async fn document_apply_update_page_action() { _ = test.create_document(&doc_id, Some(data.clone())); // open a document - let document = test.get_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); let page_block = document.lock().get_block(&data.page_id).unwrap(); let mut page_block_clone = page_block; @@ -127,7 +130,7 @@ async fn document_apply_update_page_action() { _ = test.close_document(&doc_id); // re-open the document - let document = test.get_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); let page_block_new = document.lock().get_block(&data.page_id).unwrap(); assert_eq!(page_block_old, page_block_new); assert!(page_block_new.data.contains_key("delta")); @@ -143,7 +146,7 @@ async fn document_apply_update_action() { _ = test.create_document(&doc_id, Some(data.clone())); // open a document - let document = test.get_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); let page_block = document.lock().get_block(&data.page_id).unwrap(); // insert a text block @@ -193,7 +196,7 @@ async fn document_apply_update_action() { _ = test.close_document(&doc_id); // re-open the document - let document = test.get_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); let block = document.lock().get_block(&text_block_id).unwrap(); assert_eq!(block.data, updated_text_block_data); // close a document diff --git a/frontend/rust-lib/flowy-document2/tests/document/util.rs b/frontend/rust-lib/flowy-document2/tests/document/util.rs index 46a1cc72d5..5f01b886fa 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/util.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use appflowy_integrate::collab_builder::{AppFlowyCollabBuilder, DefaultCollabStorageProvider}; use appflowy_integrate::RocksCollabDB; +use collab_document::blocks::DocumentData; use nanoid::nanoid; use parking_lot::Once; use tempfile::TempDir; @@ -55,7 +56,7 @@ impl DocumentUser for FakeUser { Ok(None) } - fn collab_db(&self) -> Result, flowy_error::FlowyError> { + fn collab_db(&self, _uid: i64) -> Result, flowy_error::FlowyError> { Ok(self.kv.clone()) } } @@ -81,7 +82,7 @@ pub fn default_collab_builder() -> Arc { Arc::new(builder) } -pub fn create_and_open_empty_document() -> (DocumentTest, Arc, String) { +pub async fn create_and_open_empty_document() -> (DocumentTest, Arc, String) { let test = DocumentTest::new(); let doc_id: String = gen_document_id(); let data = default_document_data(); @@ -89,7 +90,7 @@ pub fn create_and_open_empty_document() -> (DocumentTest, Arc, St // create a document _ = test.create_document(&doc_id, Some(data.clone())).unwrap(); - let document = test.get_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); (test, document, data.page_id) } @@ -115,4 +116,11 @@ impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { ) -> FutureResult, FlowyError> { FutureResult::new(async move { Ok(None) }) } + + fn get_document_data( + &self, + _document_id: &str, + ) -> FutureResult, FlowyError> { + FutureResult::new(async move { Ok(None) }) + } } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 24ad9e37f0..b4477a5d43 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -214,6 +214,9 @@ pub enum ErrorCode { #[error("Postgres database error")] PgDatabaseError = 70, + + #[error("Enable supabase sync")] + SupabaseSyncRequired = 71, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-folder2/src/deps.rs b/frontend/rust-lib/flowy-folder2/src/deps.rs index ecb4eccf48..23dbf01455 100644 --- a/frontend/rust-lib/flowy-folder2/src/deps.rs +++ b/frontend/rust-lib/flowy-folder2/src/deps.rs @@ -1,26 +1,37 @@ +use std::sync::Arc; + use appflowy_integrate::RocksCollabDB; +pub use collab_folder::core::FolderData; pub use collab_folder::core::Workspace; + use flowy_error::FlowyError; use lib_infra::future::FutureResult; -use std::sync::Arc; /// [FolderUser] represents the user for folder. pub trait FolderUser: Send + Sync { fn user_id(&self) -> Result; fn token(&self) -> Result, FlowyError>; - fn collab_db(&self) -> Result, FlowyError>; + fn collab_db(&self, uid: i64) -> Result, FlowyError>; } /// [FolderCloudService] represents the cloud service for folder. pub trait FolderCloudService: Send + Sync + 'static { fn create_workspace(&self, uid: i64, name: &str) -> FutureResult; + fn get_folder_data(&self, workspace_id: &str) -> FutureResult, FlowyError>; + fn get_folder_latest_snapshot( &self, workspace_id: &str, ) -> FutureResult, FlowyError>; - fn get_folder_updates(&self, workspace_id: &str) -> FutureResult>, FlowyError>; + fn get_folder_updates( + &self, + workspace_id: &str, + uid: i64, + ) -> FutureResult>, FlowyError>; + + fn service_name(&self) -> String; } pub struct FolderSnapshot { diff --git a/frontend/rust-lib/flowy-folder2/src/event_handler.rs b/frontend/rust-lib/flowy-folder2/src/event_handler.rs index 1d982d1936..56145118b2 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_handler.rs @@ -18,7 +18,7 @@ pub(crate) async fn create_workspace_handler( } #[tracing::instrument(level = "debug", skip(folder), err)] -pub(crate) async fn read_workspace_views_handler( +pub(crate) async fn get_workspace_views_handler( folder: AFPluginState>, ) -> DataResult { let child_views = folder.get_current_workspace_views().await?; @@ -66,7 +66,7 @@ pub(crate) async fn read_workspaces_handler( } #[tracing::instrument(level = "debug", skip(folder), err)] -pub async fn read_current_workspace_setting_handler( +pub async fn get_current_workspace_setting_handler( folder: AFPluginState>, ) -> DataResult { let workspace = folder.get_current_workspace().await?; diff --git a/frontend/rust-lib/flowy-folder2/src/event_map.rs b/frontend/rust-lib/flowy-folder2/src/event_map.rs index d4f05ed708..c1c661450a 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_map.rs @@ -14,11 +14,11 @@ pub fn init(folder: Arc) -> AFPlugin { .event(FolderEvent::CreateWorkspace, create_workspace_handler) .event( FolderEvent::GetCurrentWorkspace, - read_current_workspace_setting_handler, + get_current_workspace_setting_handler, ) .event(FolderEvent::ReadAllWorkspaces, read_workspaces_handler) .event(FolderEvent::OpenWorkspace, open_workspace_handler) - .event(FolderEvent::ReadWorkspaceViews, read_workspace_views_handler) + .event(FolderEvent::ReadWorkspaceViews, get_workspace_views_handler) // View .event(FolderEvent::CreateView, create_view_handler) .event(FolderEvent::CreateOrphanView, create_orphan_view_handler) diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index 84f227fb98..f7b43fcfdf 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -3,10 +3,11 @@ use std::ops::Deref; use std::sync::{Arc, Weak}; use appflowy_integrate::collab_builder::AppFlowyCollabBuilder; -use appflowy_integrate::CollabPersistenceConfig; +use appflowy_integrate::{CollabPersistenceConfig, RocksCollabDB}; +use collab::core::collab::{CollabRawData, MutexCollab}; use collab::core::collab_state::SyncState; use collab_folder::core::{ - Folder, FolderContext, TrashChange, TrashChangeReceiver, TrashInfo, View, ViewChange, + Folder, FolderData, FolderNotify, TrashChange, TrashChangeReceiver, TrashInfo, View, ViewChange, ViewChangeReceiver, ViewLayout, Workspace, }; use parking_lot::Mutex; @@ -63,30 +64,33 @@ impl FolderManager { } pub async fn get_current_workspace(&self) -> FlowyResult { - self.with_folder(Err(FlowyError::internal()), |folder| { - let workspace_pb_from_workspace = |workspace: Workspace, folder: &Folder| { - let views = get_workspace_view_pbs(&workspace.id, folder); - let workspace: WorkspacePB = (workspace, views).into(); - Ok::(workspace) - }; + self.with_folder( + Err(FlowyError::internal().context("Folder is not initialized".to_string())), + |folder| { + let workspace_pb_from_workspace = |workspace: Workspace, folder: &Folder| { + let views = get_workspace_view_pbs(&workspace.id, folder); + let workspace: WorkspacePB = (workspace, views).into(); + Ok::(workspace) + }; - match folder.get_current_workspace() { - None => { - // The current workspace should always exist. If not, try to find the first workspace. - // from the folder. Otherwise, return an error. - let mut workspaces = folder.workspaces.get_all_workspaces(); - if workspaces.is_empty() { - Err(FlowyError::record_not_found().context("Can not find the workspace")) - } else { - tracing::error!("Can't find the current workspace, use the first workspace"); - let workspace = workspaces.remove(0); - folder.set_current_workspace(&workspace.id); - workspace_pb_from_workspace(workspace, folder) - } - }, - Some(workspace) => workspace_pb_from_workspace(workspace, folder), - } - }) + match folder.get_current_workspace() { + None => { + // The current workspace should always exist. If not, try to find the first workspace. + // from the folder. Otherwise, return an error. + let mut workspaces = folder.workspaces.get_all_workspaces(); + if workspaces.is_empty() { + Err(FlowyError::record_not_found().context("Can not find the workspace")) + } else { + tracing::error!("Can't find the current workspace, use the first workspace"); + let workspace = workspaces.remove(0); + folder.set_current_workspace(&workspace.id); + workspace_pb_from_workspace(workspace, folder) + } + }, + Some(workspace) => workspace_pb_from_workspace(workspace, folder), + } + }, + ) } /// Return a list of views of the current workspace. @@ -101,6 +105,7 @@ impl FolderManager { if let Some(Some(workspace_id)) = workspace_id { self.get_workspace_views(&workspace_id).await } else { + tracing::warn!("Can't get current workspace views"); Ok(vec![]) } } @@ -114,26 +119,38 @@ impl FolderManager { } /// Called immediately after the application launched fi the user already sign in/sign up. - #[tracing::instrument(level = "info", skip(self), err)] - pub async fn initialize(&self, uid: i64, workspace_id: &str) -> FlowyResult<()> { + #[tracing::instrument(level = "info", skip(self, initial_data), err)] + pub async fn initialize( + &self, + uid: i64, + workspace_id: &str, + initial_data: FolderInitializeData, + ) -> FlowyResult<()> { let workspace_id = workspace_id.to_string(); - if let Ok(collab_db) = self.user.collab_db() { - let collab = self.collab_builder.build_with_config( - uid, - &workspace_id, - "workspace", - collab_db, - &CollabPersistenceConfig::new() - .enable_snapshot(true) - .snapshot_per_update(5), - ); + if let Ok(collab_db) = self.user.collab_db(uid) { let (view_tx, view_rx) = tokio::sync::broadcast::channel(100); let (trash_tx, trash_rx) = tokio::sync::broadcast::channel(100); - let folder_context = FolderContext { + let folder_notifier = FolderNotify { view_change_tx: view_tx, trash_change_tx: trash_tx, }; - let folder = Folder::get_or_create(collab, folder_context); + + let folder = match initial_data { + FolderInitializeData::Empty => { + let collab = self.collab_for_folder(uid, &workspace_id, collab_db, vec![])?; + Folder::open(collab, Some(folder_notifier)) + }, + FolderInitializeData::Raw(raw_data) => { + let collab = self.collab_for_folder(uid, &workspace_id, collab_db, raw_data)?; + Folder::open(collab, Some(folder_notifier)) + }, + FolderInitializeData::Data(folder_data) => { + let collab = self.collab_for_folder(uid, &workspace_id, collab_db, vec![])?; + Folder::create(collab, Some(folder_notifier), Some(folder_data)) + }, + }; + + tracing::debug!("Current workspace_id: {}", workspace_id); let folder_state_rx = folder.subscribe_sync_state(); *self.mutex_folder.lock() = Some(folder); @@ -151,34 +168,103 @@ impl FolderManager { Ok(()) } - /// Called after the user sign up / sign in - pub async fn initialize_with_new_user( + fn collab_for_folder( + &self, + uid: i64, + workspace_id: &str, + collab_db: Arc, + raw_data: CollabRawData, + ) -> Result, FlowyError> { + let collab = self.collab_builder.build_with_config( + uid, + workspace_id, + "workspace", + collab_db, + raw_data, + &CollabPersistenceConfig::new().enable_snapshot(true), + )?; + Ok(collab) + } + + #[tracing::instrument(level = "debug", skip(self, user_id), err)] + pub async fn initialize_when_sign_in(&self, user_id: i64, workspace_id: &str) -> FlowyResult<()> { + let folder_updates = self + .cloud_service + .get_folder_updates(workspace_id, user_id) + .await?; + + tracing::trace!( + "Get folder updates via {}, number of updates: {}", + self.cloud_service.service_name(), + folder_updates.len() + ); + + self + .initialize( + user_id, + workspace_id, + FolderInitializeData::Raw(folder_updates), + ) + .await?; + Ok(()) + } + + pub async fn initialize_when_sign_up( &self, user_id: i64, - token: &str, + _token: &str, is_new: bool, + folder_data: Option, workspace_id: &str, ) -> FlowyResult<()> { - self.initialize(user_id, workspace_id).await?; - // Create the default workspace if the user is new - tracing::info!("initialize_with_user: is_new: {}", is_new); + tracing::info!("initialize_when_sign_up: is_new: {}", is_new); if is_new { - let (folder_data, workspace_pb) = DefaultFolderBuilder::build( - self.user.user_id()?, - workspace_id.to_string(), - &self.operation_handlers, - ) - .await; - self.with_folder((), |folder| { - folder.create_with_data(folder_data); - }); + let folder_data = match folder_data { + None => { + DefaultFolderBuilder::build( + self.user.user_id()?, + workspace_id.to_string(), + &self.operation_handlers, + ) + .await + }, + Some(folder_data) => folder_data, + }; - send_notification(token, FolderNotification::DidCreateWorkspace) - .payload(RepeatedWorkspacePB { - items: vec![workspace_pb], - }) - .send(); + self + .initialize( + user_id, + workspace_id, + FolderInitializeData::Data(folder_data), + ) + .await?; + // send_notification(token, FolderNotification::DidCreateWorkspace) + // .payload(RepeatedWorkspacePB { + // items: vec![workspace_pb], + // }) + // .send(); + } else { + // The folder data is loaded through the [FolderCloudService]. If the cloud service in use is + // [LocalServerFolderCloudServiceImpl], the folder data will be None because the Folder will load + // the data directly from the disk. If any other cloud service is in use, the folder data will be loaded remotely. + let folder_updates = self + .cloud_service + .get_folder_updates(workspace_id, user_id) + .await?; + if !folder_updates.is_empty() { + tracing::trace!( + "Get folder updates via {}", + self.cloud_service.service_name() + ); + } + self + .initialize( + user_id, + workspace_id, + FolderInitializeData::Raw(folder_updates), + ) + .await?; } Ok(()) } @@ -726,7 +812,7 @@ fn subscribe_folder_snapshot_state_changed( if let Some(mut state_stream) = stream { while let Some(snapshot_state) = state_stream.next().await { if let Some(new_snapshot_id) = snapshot_state.snapshot_id() { - tracing::debug!("Did create folder snapshot: {}", new_snapshot_id); + tracing::debug!("Did create folder remote snapshot: {}", new_snapshot_id); send_notification( &workspace_id, FolderNotification::DidUpdateFolderSnapshotState, @@ -742,24 +828,11 @@ fn subscribe_folder_snapshot_state_changed( fn subscribe_folder_sync_state_changed( workspace_id: String, - mut folder_state_rx: WatchStream, - weak_mutex_folder: &Weak, + mut folder_sync_state_rx: WatchStream, + _weak_mutex_folder: &Weak, ) { - let weak_mutex_folder = weak_mutex_folder.clone(); tokio::spawn(async move { - while let Some(state) = folder_state_rx.next().await { - if state.is_full_sync() { - if let Some(mutex_folder) = weak_mutex_folder.upgrade() { - let folder = mutex_folder.lock().take(); - if let Some(folder) = folder { - tracing::trace!("🔥Reload folder"); - let reload_folder = folder.reload(); - notify_did_update_workspace(&workspace_id, &reload_folder); - *mutex_folder.lock() = Some(reload_folder); - } - } - } - + while let Some(state) = folder_sync_state_rx.next().await { send_notification(&workspace_id, FolderNotification::DidUpdateFolderSyncUpdate) .payload(FolderSyncStatePB::from(state)) .send(); @@ -922,3 +995,9 @@ impl Deref for MutexFolder { } unsafe impl Sync for MutexFolder {} unsafe impl Send for MutexFolder {} + +pub enum FolderInitializeData { + Empty, + Raw(CollabRawData), + Data(FolderData), +} diff --git a/frontend/rust-lib/flowy-folder2/src/user_default.rs b/frontend/rust-lib/flowy-folder2/src/user_default.rs index c25b31d541..f854123b7c 100644 --- a/frontend/rust-lib/flowy-folder2/src/user_default.rs +++ b/frontend/rust-lib/flowy-folder2/src/user_default.rs @@ -5,7 +5,7 @@ use tokio::sync::RwLock; use lib_infra::util::timestamp; -use crate::entities::{view_pb_with_child_views, ViewPB, WorkspacePB}; +use crate::entities::{view_pb_with_child_views, ViewPB}; use crate::view_operation::{ FlattedViews, FolderOperationHandlers, ParentChildViews, WorkspaceViewBuilder, }; @@ -16,7 +16,7 @@ impl DefaultFolderBuilder { _uid: i64, workspace_id: String, handlers: &FolderOperationHandlers, - ) -> (FolderData, WorkspacePB) { + ) -> FolderData { let workspace_view_builder = Arc::new(RwLock::new(WorkspaceViewBuilder::new(workspace_id.clone()))); for handler in handlers.values() { @@ -50,24 +50,12 @@ impl DefaultFolderBuilder { created_at: timestamp(), }; - let first_level_view_pbs = views.iter().map(ViewPB::from).collect::>(); - - let workspace_pb = WorkspacePB { - id: workspace.id.clone(), - name: workspace.name.clone(), - views: first_level_view_pbs, - create_time: workspace.created_at, - }; - - ( - FolderData { - current_workspace: workspace.id.clone(), - current_view: first_view.id, - workspaces: vec![workspace], - views: FlattedViews::flatten_views(views), - }, - workspace_pb, - ) + FolderData { + current_workspace_id: workspace.id.clone(), + current_view: first_view.id, + workspaces: vec![workspace], + views: FlattedViews::flatten_views(views), + } } } diff --git a/frontend/rust-lib/flowy-server-config/Cargo.toml b/frontend/rust-lib/flowy-server-config/Cargo.toml new file mode 100644 index 0000000000..97831957ee --- /dev/null +++ b/frontend/rust-lib/flowy-server-config/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "flowy-server-config" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +flowy-error = { path = "../flowy-error" } +serde = { version = "1.0", features = ["derive"] } \ No newline at end of file diff --git a/frontend/rust-lib/flowy-server-config/src/lib.rs b/frontend/rust-lib/flowy-server-config/src/lib.rs new file mode 100644 index 0000000000..8dfcc56877 --- /dev/null +++ b/frontend/rust-lib/flowy-server-config/src/lib.rs @@ -0,0 +1 @@ +pub mod supabase_config; diff --git a/frontend/rust-lib/flowy-server/src/supabase/configuration.rs b/frontend/rust-lib/flowy-server-config/src/supabase_config.rs similarity index 85% rename from frontend/rust-lib/flowy-server/src/supabase/configuration.rs rename to frontend/rust-lib/flowy-server-config/src/supabase_config.rs index 250333fe92..677643ea7b 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/configuration.rs +++ b/frontend/rust-lib/flowy-server-config/src/supabase_config.rs @@ -1,7 +1,8 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use flowy_error::{ErrorCode, FlowyError}; +pub const ENABLE_SUPABASE_SYNC: &str = "ENABLE_SUPABASE_SYNC"; pub const SUPABASE_URL: &str = "SUPABASE_URL"; pub const SUPABASE_ANON_KEY: &str = "SUPABASE_ANON_KEY"; pub const SUPABASE_KEY: &str = "SUPABASE_KEY"; @@ -12,15 +13,18 @@ pub const SUPABASE_DB_USER: &str = "SUPABASE_DB_USER"; pub const SUPABASE_DB_PASSWORD: &str = "SUPABASE_DB_PASSWORD"; pub const SUPABASE_DB_PORT: &str = "SUPABASE_DB_PORT"; -#[derive(Debug, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct SupabaseConfiguration { /// The url of the supabase server. pub url: String, + /// The key of the supabase server. pub key: String, /// The secret used to sign the JWT tokens. pub jwt_secret: String, + pub enable_sync: bool, + pub postgres_config: PostgresConfiguration, } @@ -33,6 +37,9 @@ impl SupabaseConfiguration { pub fn from_env() -> Result { let postgres_config = PostgresConfiguration::from_env()?; Ok(Self { + enable_sync: std::env::var(ENABLE_SUPABASE_SYNC) + .map(|v| v == "true") + .unwrap_or(false), url: std::env::var(SUPABASE_URL) .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_URL"))?, key: std::env::var(SUPABASE_KEY) @@ -45,6 +52,11 @@ impl SupabaseConfiguration { } pub fn write_env(&self) { + if self.enable_sync { + std::env::set_var(ENABLE_SUPABASE_SYNC, "true"); + } else { + std::env::set_var(ENABLE_SUPABASE_SYNC, "false"); + } std::env::set_var(SUPABASE_URL, &self.url); std::env::set_var(SUPABASE_KEY, &self.key); std::env::set_var(SUPABASE_JWT_SECRET, &self.jwt_secret); @@ -52,7 +64,7 @@ impl SupabaseConfiguration { } } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct PostgresConfiguration { pub url: String, pub user_name: String, diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index 3b390b8f7b..39a625f06b 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -39,6 +39,9 @@ flowy-folder2 = { path = "../flowy-folder2" } flowy-database2 = { path = "../flowy-database2" } flowy-document2 = { path = "../flowy-document2" } flowy-error = { path = "../flowy-error" } +flowy-server-config = { path = "../flowy-server-config" } +collab-folder = { version = "0.1.0" } +collab-document = { version = "0.1.0" } [dev-dependencies] uuid = { version = "1.3.3", features = ["v4"] } diff --git a/frontend/rust-lib/flowy-server/src/lib.rs b/frontend/rust-lib/flowy-server/src/lib.rs index 25cb9c448f..85d1fe81f2 100644 --- a/frontend/rust-lib/flowy-server/src/lib.rs +++ b/frontend/rust-lib/flowy-server/src/lib.rs @@ -29,6 +29,7 @@ pub mod util; /// pub trait AppFlowyServer: Send + Sync + 'static { + fn enable_sync(&self, _enable: bool) {} fn user_service(&self) -> Arc; fn folder_service(&self) -> Arc; fn database_service(&self) -> Arc; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs index e01222dffd..cc3ca3b1ad 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -1,17 +1,26 @@ -use flowy_database2::deps::{DatabaseCloudService, DatabaseSnapshot}; +use flowy_database2::deps::{ + CollabObjectUpdate, CollabObjectUpdateByOid, DatabaseCloudService, DatabaseSnapshot, +}; use flowy_error::FlowyError; use lib_infra::future::FutureResult; pub(crate) struct LocalServerDatabaseCloudServiceImpl(); impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { - fn get_database_updates(&self, _database_id: &str) -> FutureResult>, FlowyError> { + fn get_collab_update(&self, _object_id: &str) -> FutureResult { FutureResult::new(async move { Ok(vec![]) }) } - fn get_database_latest_snapshot( + fn batch_get_collab_updates( &self, - _database_id: &str, + _object_ids: Vec, + ) -> FutureResult { + FutureResult::new(async move { Ok(CollabObjectUpdateByOid::default()) }) + } + + fn get_collab_latest_snapshot( + &self, + _object_id: &str, ) -> FutureResult, FlowyError> { FutureResult::new(async move { Ok(None) }) } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs index 6a52c1f0c9..220b7e0311 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs @@ -1,4 +1,4 @@ -use flowy_document2::deps::{DocumentCloudService, DocumentSnapshot}; +use flowy_document2::deps::{DocumentCloudService, DocumentData, DocumentSnapshot}; use flowy_error::FlowyError; use lib_infra::future::FutureResult; @@ -15,4 +15,11 @@ impl DocumentCloudService for LocalServerDocumentCloudServiceImpl { ) -> FutureResult, FlowyError> { FutureResult::new(async move { Ok(None) }) } + + fn get_document_data( + &self, + _document_id: &str, + ) -> FutureResult, FlowyError> { + FutureResult::new(async move { Ok(None) }) + } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 026246aa27..5862f9000d 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -1,10 +1,16 @@ +use std::sync::Arc; + use flowy_error::FlowyError; -use flowy_folder2::deps::{FolderCloudService, FolderSnapshot, Workspace}; +use flowy_folder2::deps::{FolderCloudService, FolderData, FolderSnapshot, Workspace}; use flowy_folder2::gen_workspace_id; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; -pub(crate) struct LocalServerFolderCloudServiceImpl(); +use crate::local_server::LocalServerDB; + +pub(crate) struct LocalServerFolderCloudServiceImpl { + pub db: Arc, +} impl FolderCloudService for LocalServerFolderCloudServiceImpl { fn create_workspace(&self, _uid: i64, name: &str) -> FutureResult { @@ -19,6 +25,10 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { }) } + fn get_folder_data(&self, _workspace_id: &str) -> FutureResult, FlowyError> { + FutureResult::new(async move { Ok(None) }) + } + fn get_folder_latest_snapshot( &self, _workspace_id: &str, @@ -26,7 +36,25 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { FutureResult::new(async move { Ok(None) }) } - fn get_folder_updates(&self, _workspace_id: &str) -> FutureResult>, FlowyError> { - FutureResult::new(async move { Ok(vec![]) }) + fn get_folder_updates( + &self, + workspace_id: &str, + uid: i64, + ) -> FutureResult>, FlowyError> { + let weak_db = Arc::downgrade(&self.db); + let workspace_id = workspace_id.to_string(); + FutureResult::new(async move { + match weak_db.upgrade() { + None => Ok(vec![]), + Some(db) => { + let updates = db.get_collab_updates(uid, &workspace_id)?; + Ok(updates) + }, + } + }) + } + + fn service_name(&self) -> String { + "Local".to_string() } } 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 606b3f4e78..0b2888b535 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 @@ -1,3 +1,5 @@ +use std::sync::Arc; + use lazy_static::lazy_static; use parking_lot::Mutex; @@ -10,12 +12,15 @@ use lib_infra::box_any::BoxAny; use lib_infra::future::FutureResult; use crate::local_server::uid::UserIDGenerator; +use crate::local_server::LocalServerDB; lazy_static! { static ref ID_GEN: Mutex = Mutex::new(UserIDGenerator::new(1)); } -pub(crate) struct LocalServerUserAuthServiceImpl(); +pub(crate) struct LocalServerUserAuthServiceImpl { + pub db: Arc, +} impl UserAuthService for LocalServerUserAuthServiceImpl { fn sign_up(&self, params: BoxAny) -> FutureResult { @@ -35,10 +40,21 @@ impl UserAuthService for LocalServerUserAuthServiceImpl { } fn sign_in(&self, params: BoxAny) -> FutureResult { + let weak_db = Arc::downgrade(&self.db); FutureResult::new(async move { - let uid = ID_GEN.lock().next_id(); - let params = params.unbox_or_error::()?; - let workspace_id = uuid::Uuid::new_v4().to_string(); + let params: SignInParams = params.unbox_or_error::()?; + let uid = match params.uid { + None => ID_GEN.lock().next_id(), + Some(uid) => uid, + }; + + // Get the workspace id from the database if it exists, otherwise generate a new one. + let workspace_id = weak_db + .upgrade() + .and_then(|db| db.get_user_profile(uid).ok()) + .and_then(|user_profile| user_profile.map(|user_profile| user_profile.workspace_id)) + .unwrap_or(uuid::Uuid::new_v4().to_string()); + Ok(SignInResponse { user_id: uid, name: params.name, diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index b9254674a9..fe255900a0 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,13 +1,17 @@ use std::sync::Arc; use appflowy_integrate::RemoteCollabStorage; +use collab_document::YrsDocAction; use parking_lot::RwLock; use tokio::sync::mpsc; use flowy_database2::deps::DatabaseCloudService; use flowy_document2::deps::DocumentCloudService; +use flowy_error::FlowyError; use flowy_folder2::deps::FolderCloudService; +use flowy_user::entities::UserProfile; use flowy_user::event_map::UserAuthService; +use flowy_user::services::database::{get_user_profile, open_collab_db, open_user_db}; use crate::local_server::impls::{ LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, @@ -15,15 +19,22 @@ use crate::local_server::impls::{ }; use crate::AppFlowyServer; -#[derive(Default)] +pub trait LocalServerDB: Send + Sync + 'static { + fn get_user_profile(&self, uid: i64) -> Result, FlowyError>; + fn get_collab_updates(&self, uid: i64, object_id: &str) -> Result>, FlowyError>; +} + pub struct LocalServer { + storage_path: String, stop_tx: RwLock>>, } impl LocalServer { - pub fn new() -> Self { - // let _config = self_host_server_configuration().unwrap(); - Self::default() + pub fn new(storage_path: &str) -> Self { + Self { + storage_path: storage_path.to_string(), + stop_tx: Default::default(), + } } pub async fn stop(&self) { @@ -36,11 +47,17 @@ impl LocalServer { impl AppFlowyServer for LocalServer { fn user_service(&self) -> Arc { - Arc::new(LocalServerUserAuthServiceImpl()) + let db = LocalServerDBImpl { + storage_path: self.storage_path.clone(), + }; + Arc::new(LocalServerUserAuthServiceImpl { db: Arc::new(db) }) } fn folder_service(&self) -> Arc { - Arc::new(LocalServerFolderCloudServiceImpl()) + let db = LocalServerDBImpl { + storage_path: self.storage_path.clone(), + }; + Arc::new(LocalServerFolderCloudServiceImpl { db: Arc::new(db) }) } fn database_service(&self) -> Arc { @@ -55,3 +72,25 @@ impl AppFlowyServer for LocalServer { None } } + +struct LocalServerDBImpl { + storage_path: String, +} + +impl LocalServerDB for LocalServerDBImpl { + fn get_user_profile(&self, uid: i64) -> Result, FlowyError> { + let sqlite_db = open_user_db(&self.storage_path, uid)?; + let user_profile = get_user_profile(&sqlite_db, uid).ok(); + Ok(user_profile) + } + + fn get_collab_updates(&self, uid: i64, object_id: &str) -> Result>, FlowyError> { + let collab_db = open_collab_db(&self.storage_path, uid)?; + let read_txn = collab_db.read_txn(); + let updates = read_txn + .get_all_updates(uid, object_id) + .map_err(|e| FlowyError::internal().context(format!("Failed to open collab db: {:?}", e)))?; + + Ok(updates) + } +} diff --git a/frontend/rust-lib/flowy-server/src/self_host/impls/database.rs b/frontend/rust-lib/flowy-server/src/self_host/impls/database.rs index dbe65ed266..0ce0395394 100644 --- a/frontend/rust-lib/flowy-server/src/self_host/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/self_host/impls/database.rs @@ -1,17 +1,26 @@ -use flowy_database2::deps::{DatabaseCloudService, DatabaseSnapshot}; +use flowy_database2::deps::{ + CollabObjectUpdate, CollabObjectUpdateByOid, DatabaseCloudService, DatabaseSnapshot, +}; use flowy_error::FlowyError; use lib_infra::future::FutureResult; pub(crate) struct SelfHostedDatabaseCloudServiceImpl(); impl DatabaseCloudService for SelfHostedDatabaseCloudServiceImpl { - fn get_database_updates(&self, _database_id: &str) -> FutureResult>, FlowyError> { + fn get_collab_update(&self, _object_id: &str) -> FutureResult { FutureResult::new(async move { Ok(vec![]) }) } - fn get_database_latest_snapshot( + fn batch_get_collab_updates( &self, - _database_id: &str, + _object_ids: Vec, + ) -> FutureResult { + FutureResult::new(async move { Ok(CollabObjectUpdateByOid::default()) }) + } + + fn get_collab_latest_snapshot( + &self, + _object_id: &str, ) -> FutureResult, FlowyError> { FutureResult::new(async move { Ok(None) }) } diff --git a/frontend/rust-lib/flowy-server/src/self_host/impls/document.rs b/frontend/rust-lib/flowy-server/src/self_host/impls/document.rs index 52aac894b7..97e5e713d6 100644 --- a/frontend/rust-lib/flowy-server/src/self_host/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/self_host/impls/document.rs @@ -1,4 +1,4 @@ -use flowy_document2::deps::{DocumentCloudService, DocumentSnapshot}; +use flowy_document2::deps::{DocumentCloudService, DocumentData, DocumentSnapshot}; use flowy_error::FlowyError; use lib_infra::future::FutureResult; @@ -15,4 +15,11 @@ impl DocumentCloudService for SelfHostedDocumentCloudServiceImpl { ) -> FutureResult, FlowyError> { FutureResult::new(async move { Ok(None) }) } + + fn get_document_data( + &self, + _document_id: &str, + ) -> FutureResult, FlowyError> { + FutureResult::new(async move { Ok(None) }) + } } diff --git a/frontend/rust-lib/flowy-server/src/self_host/impls/folder.rs b/frontend/rust-lib/flowy-server/src/self_host/impls/folder.rs index eafb7c1061..9ee761a8fb 100644 --- a/frontend/rust-lib/flowy-server/src/self_host/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/self_host/impls/folder.rs @@ -1,5 +1,5 @@ use flowy_error::FlowyError; -use flowy_folder2::deps::{FolderCloudService, FolderSnapshot, Workspace}; +use flowy_folder2::deps::{FolderCloudService, FolderData, FolderSnapshot, Workspace}; use flowy_folder2::gen_workspace_id; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; @@ -19,6 +19,10 @@ impl FolderCloudService for SelfHostedServerFolderCloudServiceImpl { }) } + fn get_folder_data(&self, _workspace_id: &str) -> FutureResult, FlowyError> { + FutureResult::new(async move { Ok(None) }) + } + fn get_folder_latest_snapshot( &self, _workspace_id: &str, @@ -26,7 +30,15 @@ impl FolderCloudService for SelfHostedServerFolderCloudServiceImpl { FutureResult::new(async move { Ok(None) }) } - fn get_folder_updates(&self, _workspace_id: &str) -> FutureResult>, FlowyError> { + fn get_folder_updates( + &self, + _workspace_id: &str, + _uid: i64, + ) -> FutureResult>, FlowyError> { FutureResult::new(async move { Ok(vec![]) }) } + + fn service_name(&self) -> String { + "SelfHosted".to_string() + } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/impls/collab_storage.rs b/frontend/rust-lib/flowy-server/src/supabase/impls/collab_storage.rs index 9576afd1a6..16eeaada51 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/impls/collab_storage.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/impls/collab_storage.rs @@ -1,27 +1,36 @@ -use std::sync::{Arc, Weak}; +use std::future::Future; +use std::iter::Take; +use std::pin::Pin; +use std::sync::Weak; +use std::time::Duration; use anyhow::Error; use appflowy_integrate::{ - merge_updates_v1, CollabObject, Decode, MsgId, RemoteCollabSnapshot, RemoteCollabState, - RemoteCollabStorage, YrsUpdate, + merge_updates_v1, CollabObject, MsgId, RemoteCollabSnapshot, RemoteCollabState, + RemoteCollabStorage, RemoteUpdateReceiver, }; use chrono::{DateTime, Utc}; use deadpool_postgres::GenericClient; -use futures_util::TryStreamExt; +use futures::pin_mut; +use futures_util::{StreamExt, TryStreamExt}; use tokio::task::spawn_blocking; use tokio_postgres::types::ToSql; use tokio_postgres::Row; +use tokio_retry::strategy::FixedInterval; +use tokio_retry::{Action, Retry}; -use flowy_error::FlowyError; +use flowy_database2::deps::{CollabObjectUpdate, CollabObjectUpdateByOid}; use lib_infra::async_trait::async_trait; +use lib_infra::util::md5; +use crate::supabase::postgres_db::PostgresObject; use crate::supabase::sql_builder::{ DeleteSqlBuilder, InsertSqlBuilder, SelectSqlBuilder, WhereCondition, }; -use crate::supabase::PostgresServer; +use crate::supabase::{PostgresServer, SupabaseServerService}; -pub struct PgCollabStorageImpl { - server: Arc, +pub struct PgCollabStorageImpl { + server: T, } const AF_COLLAB_KEY_COLUMN: &str = "key"; @@ -32,27 +41,64 @@ const AF_COLLAB_SNAPSHOT_BLOB_SIZE_COLUMN: &str = "blob_size"; const AF_COLLAB_SNAPSHOT_CREATED_AT_COLUMN: &str = "created_at"; const AF_COLLAB_SNAPSHOT_TABLE: &str = "af_collab_snapshot"; -impl PgCollabStorageImpl { - pub fn new(server: Arc) -> Self { +impl PgCollabStorageImpl +where + T: SupabaseServerService, +{ + pub fn new(server: T) -> Self { Self { server } } + + pub async fn get_client(&self) -> Option { + self + .server + .get_pg_server()? + .upgrade()? + .get_pg_client() + .await + .recv() + .await + .ok() + } } #[async_trait] -impl RemoteCollabStorage for PgCollabStorageImpl { +impl RemoteCollabStorage for PgCollabStorageImpl +where + T: SupabaseServerService, +{ + fn is_enable(&self) -> bool { + self + .server + .get_pg_server() + .and_then(|server| server.upgrade()) + .is_some() + } + async fn get_all_updates(&self, object_id: &str) -> Result>, Error> { - get_updates_from_server(object_id, Arc::downgrade(&self.server)).await + let pg_server = self.server.try_get_pg_server()?; + let action = FetchObjectUpdateAction::new(object_id, pg_server); + let updates = action.run().await?; + Ok(updates) } async fn get_latest_snapshot( &self, object_id: &str, ) -> Result, Error> { - get_latest_snapshot_from_server(object_id, Arc::downgrade(&self.server)).await + match self.server.get_pg_server() { + None => Ok(None), + Some(weak_server) => get_latest_snapshot_from_server(object_id, weak_server).await, + } } async fn get_collab_state(&self, object_id: &str) -> Result, Error> { - let client = self.server.get_pg_client().await.recv().await?; + let client = self.get_client().await; + if client.is_none() { + return Ok(None); + } + + let client = client.unwrap(); let (sql, params) = SelectSqlBuilder::new("af_collab_state") .column("*") .where_clause("oid", object_id.to_string()) @@ -83,7 +129,10 @@ impl RemoteCollabStorage for PgCollabStorageImpl { } async fn create_snapshot(&self, object: &CollabObject, snapshot: Vec) -> Result { - let client = self.server.get_pg_client().await.recv().await?; + let client = self + .get_client() + .await + .ok_or_else(|| anyhow::anyhow!("Create snapshot failed. No client available"))?; let value_size = snapshot.len() as i32; let (sql, params) = InsertSqlBuilder::new("af_collab_snapshot") .value(AF_COLLAB_SNAPSHOT_OID_COLUMN, object.id.clone()) @@ -112,17 +161,21 @@ impl RemoteCollabStorage for PgCollabStorageImpl { _id: MsgId, update: Vec, ) -> Result<(), Error> { - let client = self.server.get_pg_client().await.recv().await?; - let value_size = update.len() as i32; - let (sql, params) = InsertSqlBuilder::new("af_collab") - .value("oid", object.id.clone()) - .value("name", object.name.clone()) - .value("value", update) - .value("value_size", value_size) - .build(); + if let Some(client) = self.get_client().await { + let value_size = update.len() as i32; + let md5 = md5(&update); + let (sql, params) = InsertSqlBuilder::new("af_collab") + .value("oid", object.id.clone()) + .value("name", object.name.clone()) + .value("value", update) + .value("uid", object.uid) + .value("md5", md5) + .value("value_size", value_size) + .build(); - let stmt = client.prepare_cached(&sql).await?; - client.execute_raw(&stmt, params).await?; + let stmt = client.prepare_cached(&sql).await?; + client.execute_raw(&stmt, params).await?; + } Ok(()) } @@ -132,36 +185,46 @@ impl RemoteCollabStorage for PgCollabStorageImpl { _id: MsgId, init_update: Vec, ) -> Result<(), Error> { - let mut client = self.server.get_pg_client().await.recv().await?; + let client = self.get_client().await; + if client.is_none() { + return Ok(()); + } + + let mut client = client.unwrap(); let txn = client.transaction().await?; - // 1.Get all updates + // 1.Get all updates and lock the table. It means that a subsequent UPDATE, DELETE, or SELECT + // FOR UPDATE by this transaction will not result in a lock wait. other transactions that try + // to update or lock these specific rows will be blocked until the current transaction ends let (sql, params) = SelectSqlBuilder::new("af_collab") .column(AF_COLLAB_KEY_COLUMN) .column("value") .order_by(AF_COLLAB_KEY_COLUMN, true) .where_clause("oid", object.id.clone()) + .lock() .build(); + let get_all_update_stmt = txn.prepare_cached(&sql).await?; let row_stream = txn.query_raw(&get_all_update_stmt, params).await?; - let remote_updates = row_stream.try_collect::>().await?; + let pg_rows = row_stream.try_collect::>().await?; let insert_builder = InsertSqlBuilder::new("af_collab") .value("oid", object.id.clone()) + .value("uid", object.uid) .value("name", object.name.clone()); - let (sql, params) = if !remote_updates.is_empty() { - let remoted_keys = remote_updates - .iter() + let (sql, params) = if !pg_rows.is_empty() { + let last_row_key = pg_rows + .last() .map(|row| row.get::<_, i64>(AF_COLLAB_KEY_COLUMN)) - .collect::>(); - let last_row_key = remoted_keys.last().cloned().unwrap(); + .unwrap(); - // 2.Merge all updates - let merged_update = - spawn_blocking(move || merge_update_from_rows(remote_updates, init_update)).await??; + // 2.Merge the updates into one and then delete the merged updates + let merge_result = + spawn_blocking(move || merge_update_from_rows(pg_rows, init_update)).await??; + tracing::trace!("Merged updates count: {}", merge_result.merged_keys.len()); - // 3. Delete all updates + // 3. Delete merged updates let (sql, params) = DeleteSqlBuilder::new("af_collab") .where_condition(WhereCondition::Equals( "oid".to_string(), @@ -169,7 +232,8 @@ impl RemoteCollabStorage for PgCollabStorageImpl { )) .where_condition(WhereCondition::In( AF_COLLAB_KEY_COLUMN.to_string(), - remoted_keys + merge_result + .merged_keys .into_iter() .map(|key| Box::new(key) as Box) .collect::>(), @@ -178,18 +242,25 @@ impl RemoteCollabStorage for PgCollabStorageImpl { let delete_stmt = txn.prepare_cached(&sql).await?; txn.execute_raw(&delete_stmt, params).await?; - let value_size = merged_update.len() as i32; - // Override the key with the last row key in case of concurrent init sync + // 4. Insert the merged update. The new_update contains the merged update and the + // init_update. + let new_update = merge_result.new_update; + + let value_size = new_update.len() as i32; + let md5 = md5(&new_update); insert_builder - .value("value", merged_update) + .value("value", new_update) .value("value_size", value_size) + .value("md5", md5) .value(AF_COLLAB_KEY_COLUMN, last_row_key) .overriding_system_value() .build() } else { let value_size = init_update.len() as i32; + let md5 = md5(&init_update); insert_builder .value("value", init_update) + .value("md5", md5) .value("value_size", value_size) .build() }; @@ -203,6 +274,11 @@ impl RemoteCollabStorage for PgCollabStorageImpl { tracing::trace!("{} init sync done", object.id); Ok(()) } + + async fn subscribe_remote_updates(&self, _object: &CollabObject) -> Option { + // using pg_notify to subscribe to updates + None + } } pub async fn get_updates_from_server( @@ -225,7 +301,7 @@ pub async fn get_updates_from_server( .try_collect::>() .await? .into_iter() - .flat_map(|row| update_from_row(row).ok()) + .flat_map(|row| update_from_row(&row).ok()) .collect(), ) }, @@ -275,30 +351,156 @@ pub async fn get_latest_snapshot_from_server( } } -fn update_from_row(row: Row) -> Result, FlowyError> { - row - .try_get::<_, Vec>("value") - .map_err(|e| FlowyError::internal().context(format!("Failed to get value from row: {}", e))) +fn update_from_row(row: &Row) -> Result, anyhow::Error> { + let update = row.try_get::<_, Vec>("value")?; + Ok(update) } -#[allow(dead_code)] -fn decode_update_from_row(row: Row) -> Result { - let update = update_from_row(row)?; - YrsUpdate::decode_v1(&update).map_err(|_| FlowyError::internal().context("Invalid yrs update")) -} - -fn merge_update_from_rows(rows: Vec, new_update: Vec) -> Result, FlowyError> { +fn merge_update_from_rows( + rows: Vec, + new_update: Vec, +) -> Result { let mut updates = vec![]; + let mut merged_keys = vec![]; for row in rows { - let update = update_from_row(row)?; + merged_keys.push(row.try_get::<_, i64>(AF_COLLAB_KEY_COLUMN)?); + let update = update_from_row(&row)?; updates.push(update); } updates.push(new_update); - let updates = updates .iter() .map(|update| update.as_ref()) .collect::>(); - merge_updates_v1(&updates).map_err(|_| FlowyError::internal().context("Failed to merge updates")) + let new_update = merge_updates_v1(&updates)?; + Ok(MergeResult { + merged_keys, + new_update, + }) +} + +struct MergeResult { + merged_keys: Vec, + new_update: Vec, +} + +pub struct FetchObjectUpdateAction { + object_id: String, + pg_server: Weak, +} + +impl FetchObjectUpdateAction { + pub fn new(object_id: &str, pg_server: Weak) -> Self { + Self { + pg_server, + object_id: object_id.to_string(), + } + } + + pub fn run(self) -> Retry, FetchObjectUpdateAction> { + let retry_strategy = FixedInterval::new(Duration::from_secs(5)).take(3); + Retry::spawn(retry_strategy, self) + } + + pub fn run_with_fix_interval( + self, + secs: u64, + times: usize, + ) -> Retry, FetchObjectUpdateAction> { + let retry_strategy = FixedInterval::new(Duration::from_secs(secs)).take(times); + Retry::spawn(retry_strategy, self) + } +} + +impl Action for FetchObjectUpdateAction { + type Future = Pin> + Send>>; + type Item = CollabObjectUpdate; + type Error = anyhow::Error; + + fn run(&mut self) -> Self::Future { + let weak_pb_server = self.pg_server.clone(); + let object_id = self.object_id.clone(); + Box::pin(async move { + match weak_pb_server.upgrade() { + None => Ok(vec![]), + Some(server) => { + let client = server.get_pg_client().await.recv().await?; + let (sql, params) = SelectSqlBuilder::new("af_collab") + .column("value") + .order_by(AF_COLLAB_KEY_COLUMN, true) + .where_clause("oid", object_id) + .build(); + let stmt = client.prepare_cached(&sql).await?; + let row_stream = client.query_raw(&stmt, params).await?; + Ok( + row_stream + .try_collect::>() + .await? + .into_iter() + .flat_map(|row| update_from_row(&row).ok()) + .collect(), + ) + }, + } + }) + } +} + +pub struct BatchFetchObjectUpdateAction { + object_ids: Vec, + pg_server: Weak, +} + +impl BatchFetchObjectUpdateAction { + pub fn new(object_ids: Vec, pg_server: Weak) -> Self { + Self { + pg_server, + object_ids, + } + } + + pub fn run(self) -> Retry, BatchFetchObjectUpdateAction> { + let retry_strategy = FixedInterval::new(Duration::from_secs(5)).take(3); + Retry::spawn(retry_strategy, self) + } +} + +impl Action for BatchFetchObjectUpdateAction { + type Future = Pin> + Send>>; + type Item = CollabObjectUpdateByOid; + type Error = anyhow::Error; + + fn run(&mut self) -> Self::Future { + let weak_pb_server = self.pg_server.clone(); + let object_ids = self.object_ids.clone(); + Box::pin(async move { + match weak_pb_server.upgrade() { + None => Ok(CollabObjectUpdateByOid::default()), + Some(server) => { + let client = server.get_pg_client().await.recv().await?; + let mut updates_by_oid = CollabObjectUpdateByOid::new(); + + // Group the updates by oid + let (sql, params) = SelectSqlBuilder::new("af_collab") + .column("oid") + .array_agg("value") + .group_by("oid") + .where_clause_in("oid", object_ids) + .build(); + let stmt = client.prepare_cached(&sql).await?; + + // Poll the rows + let rows = Box::pin(client.query_raw(&stmt, params).await?); + pin_mut!(rows); + while let Some(Ok(row)) = rows.next().await { + let oid = row.try_get::<_, String>("oid")?; + let updates = row.try_get::<_, Vec>>("value")?; + updates_by_oid.insert(oid, updates); + } + Ok(updates_by_oid) + }, + } + }) + } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/impls/database.rs b/frontend/rust-lib/flowy-server/src/supabase/impls/database.rs index 10bf7fd725..a2d89dd815 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/impls/database.rs @@ -1,48 +1,100 @@ -use std::sync::Arc; - use tokio::sync::oneshot::channel; -use flowy_database2::deps::{DatabaseCloudService, DatabaseSnapshot}; +use flowy_database2::deps::{ + CollabObjectUpdate, CollabObjectUpdateByOid, DatabaseCloudService, DatabaseSnapshot, +}; use flowy_error::{internal_error, FlowyError}; use lib_infra::future::FutureResult; -use crate::supabase::impls::{get_latest_snapshot_from_server, get_updates_from_server}; -use crate::supabase::PostgresServer; +use crate::supabase::impls::{ + get_latest_snapshot_from_server, BatchFetchObjectUpdateAction, FetchObjectUpdateAction, +}; +use crate::supabase::SupabaseServerService; -pub(crate) struct SupabaseDatabaseCloudServiceImpl { - server: Arc, +pub struct SupabaseDatabaseCloudServiceImpl { + server: T, } -impl SupabaseDatabaseCloudServiceImpl { - pub fn new(server: Arc) -> Self { +impl SupabaseDatabaseCloudServiceImpl { + pub fn new(server: T) -> Self { Self { server } } } -impl DatabaseCloudService for SupabaseDatabaseCloudServiceImpl { - fn get_database_updates(&self, database_id: &str) -> FutureResult>, FlowyError> { - let server = Arc::downgrade(&self.server); +impl DatabaseCloudService for SupabaseDatabaseCloudServiceImpl +where + T: SupabaseServerService, +{ + fn get_collab_update(&self, object_id: &str) -> FutureResult { + let weak_server = self.server.get_pg_server(); let (tx, rx) = channel(); - let database_id = database_id.to_string(); - tokio::spawn(async move { tx.send(get_updates_from_server(&database_id, server).await) }); + let database_id = object_id.to_string(); + tokio::spawn(async move { + tx.send( + async move { + match weak_server { + None => Ok(CollabObjectUpdate::default()), + Some(weak_server) => { + FetchObjectUpdateAction::new(&database_id, weak_server) + .run() + .await + }, + } + } + .await, + ) + }); FutureResult::new(async { rx.await.map_err(internal_error)?.map_err(internal_error) }) } - fn get_database_latest_snapshot( + fn batch_get_collab_updates( &self, - database_id: &str, - ) -> FutureResult, FlowyError> { - let server = Arc::downgrade(&self.server); + object_ids: Vec, + ) -> FutureResult { + let weak_server = self.server.get_pg_server(); let (tx, rx) = channel(); - let database_id = database_id.to_string(); - tokio::spawn( - async move { tx.send(get_latest_snapshot_from_server(&database_id, server).await) }, - ); + tokio::spawn(async move { + tx.send( + async move { + match weak_server { + None => Ok(CollabObjectUpdateByOid::default()), + Some(weak_server) => { + BatchFetchObjectUpdateAction::new(object_ids, weak_server) + .run() + .await + }, + } + } + .await, + ) + }); + FutureResult::new(async { rx.await.map_err(internal_error)?.map_err(internal_error) }) + } + + fn get_collab_latest_snapshot( + &self, + object_id: &str, + ) -> FutureResult, FlowyError> { + let weak_server = self.server.get_pg_server(); + let (tx, rx) = channel(); + let database_id = object_id.to_string(); + tokio::spawn(async move { + tx.send( + async move { + match weak_server { + None => Ok(None), + Some(weak_server) => get_latest_snapshot_from_server(&database_id, weak_server) + .await + .map_err(internal_error), + } + } + .await, + ) + }); FutureResult::new(async { Ok( rx.await - .map_err(internal_error)? - .map_err(internal_error)? + .map_err(internal_error)?? .map(|snapshot| DatabaseSnapshot { snapshot_id: snapshot.snapshot_id, database_id: snapshot.oid, diff --git a/frontend/rust-lib/flowy-server/src/supabase/impls/document.rs b/frontend/rust-lib/flowy-server/src/supabase/impls/document.rs index 68b7274af6..89520eb742 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/impls/document.rs @@ -1,50 +1,75 @@ -use std::sync::Arc; - +use collab_document::document::Document; +use collab_folder::core::CollabOrigin; use tokio::sync::oneshot::channel; -use flowy_document2::deps::{DocumentCloudService, DocumentSnapshot}; +use flowy_document2::deps::{DocumentCloudService, DocumentData, DocumentSnapshot}; use flowy_error::{internal_error, FlowyError}; use lib_infra::future::FutureResult; -use crate::supabase::impls::{get_latest_snapshot_from_server, get_updates_from_server}; -use crate::supabase::PostgresServer; +use crate::supabase::impls::{get_latest_snapshot_from_server, FetchObjectUpdateAction}; +use crate::supabase::SupabaseServerService; -pub(crate) struct SupabaseDocumentCloudServiceImpl { - server: Arc, +pub struct SupabaseDocumentCloudServiceImpl { + server: T, } -impl SupabaseDocumentCloudServiceImpl { - pub fn new(server: Arc) -> Self { +impl SupabaseDocumentCloudServiceImpl { + pub fn new(server: T) -> Self { Self { server } } } -impl DocumentCloudService for SupabaseDocumentCloudServiceImpl { +impl DocumentCloudService for SupabaseDocumentCloudServiceImpl +where + T: SupabaseServerService, +{ fn get_document_updates(&self, document_id: &str) -> FutureResult>, FlowyError> { - let server = Arc::downgrade(&self.server); + let weak_server = self.server.get_pg_server(); let (tx, rx) = channel(); let document_id = document_id.to_string(); - tokio::spawn(async move { tx.send(get_updates_from_server(&document_id, server).await) }); - FutureResult::new(async { rx.await.map_err(internal_error)?.map_err(internal_error) }) + tokio::spawn(async move { + tx.send( + async move { + match weak_server { + None => Ok(vec![]), + Some(weak_server) => FetchObjectUpdateAction::new(&document_id, weak_server) + .run_with_fix_interval(5, 5) + .await + .map_err(internal_error), + } + } + .await, + ) + }); + FutureResult::new(async { rx.await.map_err(internal_error)? }) } fn get_document_latest_snapshot( &self, document_id: &str, ) -> FutureResult, FlowyError> { - let server = Arc::downgrade(&self.server); + let weak_server = self.server.get_pg_server(); let (tx, rx) = channel(); let document_id = document_id.to_string(); - tokio::spawn( - async move { tx.send(get_latest_snapshot_from_server(&document_id, server).await) }, - ); + tokio::spawn(async move { + tx.send( + async move { + match weak_server { + None => Ok(None), + Some(weak_server) => get_latest_snapshot_from_server(&document_id, weak_server) + .await + .map_err(internal_error), + } + } + .await, + ) + }); FutureResult::new(async { { Ok( rx.await - .map_err(internal_error)? - .map_err(internal_error)? + .map_err(internal_error)?? .map(|snapshot| DocumentSnapshot { snapshot_id: snapshot.snapshot_id, document_id: snapshot.oid, @@ -55,4 +80,29 @@ impl DocumentCloudService for SupabaseDocumentCloudServiceImpl { } }) } + + fn get_document_data(&self, document_id: &str) -> FutureResult, FlowyError> { + let weak_server = self.server.get_pg_server(); + let (tx, rx) = channel(); + let document_id = document_id.to_string(); + tokio::spawn(async move { + tx.send( + async move { + match weak_server { + None => Ok(Ok(None)), + Some(weak_server) => { + let action = FetchObjectUpdateAction::new(&document_id, weak_server); + action.run().await.map(|updates| { + let document = + Document::from_updates(CollabOrigin::Empty, updates, &document_id, vec![])?; + Ok(document.get_document_data().ok()) + }) + }, + } + } + .await, + ) + }); + FutureResult::new(async { rx.await.map_err(internal_error)?.map_err(internal_error)? }) + } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/impls/folder.rs b/frontend/rust-lib/flowy-server/src/supabase/impls/folder.rs index f8a1258277..75afe466d1 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/impls/folder.rs @@ -1,44 +1,56 @@ -use std::sync::Arc; - use chrono::{DateTime, Utc}; +use collab_folder::core::{CollabOrigin, Folder}; use futures_util::{pin_mut, StreamExt}; use tokio::sync::oneshot::channel; use uuid::Uuid; -use crate::supabase::impls::{get_latest_snapshot_from_server, get_updates_from_server}; use flowy_error::{internal_error, ErrorCode, FlowyError}; -use flowy_folder2::deps::{FolderCloudService, FolderSnapshot, Workspace}; +use flowy_folder2::deps::{FolderCloudService, FolderData, FolderSnapshot, Workspace}; use lib_infra::future::FutureResult; -use crate::supabase::pg_db::PostgresObject; +use crate::supabase::impls::{ + get_latest_snapshot_from_server, get_updates_from_server, FetchObjectUpdateAction, +}; +use crate::supabase::postgres_db::PostgresObject; use crate::supabase::sql_builder::{InsertSqlBuilder, SelectSqlBuilder}; -use crate::supabase::PostgresServer; +use crate::supabase::SupabaseServerService; pub(crate) const WORKSPACE_TABLE: &str = "af_workspace"; pub(crate) const WORKSPACE_ID: &str = "workspace_id"; const WORKSPACE_NAME: &str = "workspace_name"; const CREATED_AT: &str = "created_at"; -pub(crate) struct SupabaseFolderCloudServiceImpl { - server: Arc, +pub struct SupabaseFolderCloudServiceImpl { + server: T, } -impl SupabaseFolderCloudServiceImpl { - pub fn new(server: Arc) -> Self { +impl SupabaseFolderCloudServiceImpl { + pub fn new(server: T) -> Self { Self { server } } } -impl FolderCloudService for SupabaseFolderCloudServiceImpl { +impl FolderCloudService for SupabaseFolderCloudServiceImpl +where + T: SupabaseServerService, +{ fn create_workspace(&self, uid: i64, name: &str) -> FutureResult { - let server = self.server.clone(); + let weak_server = self.server.try_get_pg_server(); let (tx, rx) = channel(); let name = name.to_string(); tokio::spawn(async move { tx.send( async move { - let client = server.get_pg_client().await.recv().await?; - create_workspace(&client, uid, &name).await + match weak_server?.upgrade() { + None => Err(FlowyError::new( + ErrorCode::PgDatabaseError, + "Server is close", + )), + Some(server) => { + let client = server.get_pg_client().await.recv().await?; + create_workspace(&client, uid, &name).await + }, + } } .await, ) @@ -46,21 +58,58 @@ impl FolderCloudService for SupabaseFolderCloudServiceImpl { FutureResult::new(async { rx.await.map_err(internal_error)? }) } + fn get_folder_data(&self, workspace_id: &str) -> FutureResult, FlowyError> { + let weak_server = self.server.get_pg_server(); + let (tx, rx) = channel(); + let workspace_id = workspace_id.to_string(); + tokio::spawn(async move { + tx.send( + async move { + match weak_server { + None => Ok(Ok(None)), + Some(weak_server) => get_updates_from_server(&workspace_id, weak_server) + .await + .map(|updates| { + let folder = Folder::from_collab_raw_data( + CollabOrigin::Empty, + updates, + &workspace_id, + vec![], + )?; + Ok(folder.get_folder_data()) + }), + } + } + .await, + ) + }); + FutureResult::new(async { rx.await.map_err(internal_error)?.map_err(internal_error)? }) + } + fn get_folder_latest_snapshot( &self, workspace_id: &str, ) -> FutureResult, FlowyError> { - let server = Arc::downgrade(&self.server); + let weak_server = self.server.get_pg_server(); let workspace_id = workspace_id.to_string(); let (tx, rx) = channel(); - tokio::spawn( - async move { tx.send(get_latest_snapshot_from_server(&workspace_id, server).await) }, - ); + tokio::spawn(async move { + tx.send( + async { + match weak_server { + None => Ok(None), + Some(weak_server) => get_latest_snapshot_from_server(&workspace_id, weak_server) + .await + .map_err(internal_error), + } + } + .await, + ) + }); FutureResult::new(async { Ok( rx.await - .map_err(internal_error)? - .map_err(internal_error)? + .map_err(internal_error)?? .map(|snapshot| FolderSnapshot { snapshot_id: snapshot.snapshot_id, database_id: snapshot.oid, @@ -71,13 +120,34 @@ impl FolderCloudService for SupabaseFolderCloudServiceImpl { }) } - fn get_folder_updates(&self, workspace_id: &str) -> FutureResult>, FlowyError> { - let server = Arc::downgrade(&self.server); + fn get_folder_updates( + &self, + workspace_id: &str, + _uid: i64, + ) -> FutureResult>, FlowyError> { + let weak_server = self.server.get_pg_server(); let (tx, rx) = channel(); let workspace_id = workspace_id.to_string(); - tokio::spawn(async move { tx.send(get_updates_from_server(&workspace_id, server).await) }); + tokio::spawn(async move { + tx.send( + async move { + match weak_server { + None => Ok(vec![]), + Some(weak_server) => { + let action = FetchObjectUpdateAction::new(&workspace_id, weak_server); + action.run_with_fix_interval(5, 10).await + }, + } + } + .await, + ) + }); FutureResult::new(async { rx.await.map_err(internal_error)?.map_err(internal_error) }) } + + fn service_name(&self) -> String { + "Supabase".to_string() + } } async fn create_workspace( @@ -147,15 +217,17 @@ mod tests { use std::collections::HashMap; use std::sync::Arc; + use parking_lot::RwLock; use uuid::Uuid; use flowy_folder2::deps::FolderCloudService; + use flowy_server_config::supabase_config::PostgresConfiguration; use flowy_user::event_map::UserAuthService; use lib_infra::box_any::BoxAny; use crate::supabase::impls::folder::SupabaseFolderCloudServiceImpl; use crate::supabase::impls::SupabaseUserAuthServiceImpl; - use crate::supabase::{PostgresConfiguration, PostgresServer}; + use crate::supabase::{PostgresServer, SupabaseServerServiceImpl}; #[tokio::test] async fn create_user_workspace() { @@ -165,7 +237,8 @@ mod tests { let server = Arc::new(PostgresServer::new( PostgresConfiguration::from_env().unwrap(), )); - let user_service = SupabaseUserAuthServiceImpl::new(server.clone()); + let weak_server = SupabaseServerServiceImpl(Arc::new(RwLock::new(Some(server.clone())))); + let user_service = SupabaseUserAuthServiceImpl::new(weak_server.clone()); // create user let mut params = HashMap::new(); @@ -173,7 +246,7 @@ mod tests { let user = user_service.sign_up(BoxAny::new(params)).await.unwrap(); // create workspace - let folder_service = SupabaseFolderCloudServiceImpl::new(server); + let folder_service = SupabaseFolderCloudServiceImpl::new(weak_server); let workspace = folder_service .create_workspace(user.user_id, "my test workspace") .await diff --git a/frontend/rust-lib/flowy-server/src/supabase/impls/mod.rs b/frontend/rust-lib/flowy-server/src/supabase/impls/mod.rs index 74c1a9f500..65257a5951 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/impls/mod.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/impls/mod.rs @@ -1,7 +1,7 @@ pub use collab_storage::*; -pub(crate) use database::*; -pub(crate) use document::*; -pub(crate) use folder::*; +pub use database::*; +pub use document::*; +pub use folder::*; pub use user::*; mod collab_storage; diff --git a/frontend/rust-lib/flowy-server/src/supabase/impls/user.rs b/frontend/rust-lib/flowy-server/src/supabase/impls/user.rs index 62a83e14b1..5a259e9c12 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/impls/user.rs @@ -1,5 +1,4 @@ use std::str::FromStr; -use std::sync::Arc; use deadpool_postgres::GenericClient; use futures::pin_mut; @@ -11,39 +10,50 @@ use uuid::Uuid; use flowy_error::{internal_error, ErrorCode, FlowyError}; use flowy_user::entities::{SignInResponse, SignUpResponse, UpdateUserProfileParams, UserProfile}; use flowy_user::event_map::{UserAuthService, UserCredentials}; +use flowy_user::services::{uuid_from_box_any, AuthType}; use lib_infra::box_any::BoxAny; use lib_infra::future::FutureResult; use crate::supabase::entities::{GetUserProfileParams, UserProfileResponse}; -use crate::supabase::pg_db::PostgresObject; +use crate::supabase::postgres_db::PostgresObject; use crate::supabase::sql_builder::{SelectSqlBuilder, UpdateSqlBuilder}; -use crate::supabase::PostgresServer; -use crate::util::uuid_from_box_any; +use crate::supabase::SupabaseServerService; pub(crate) const USER_TABLE: &str = "af_user"; pub(crate) const USER_PROFILE_TABLE: &str = "af_user_profile"; pub const USER_UUID: &str = "uuid"; -pub struct SupabaseUserAuthServiceImpl { - server: Arc, +pub struct SupabaseUserAuthServiceImpl { + server: T, } -impl SupabaseUserAuthServiceImpl { - pub fn new(server: Arc) -> Self { +impl SupabaseUserAuthServiceImpl { + pub fn new(server: T) -> Self { Self { server } } } -impl UserAuthService for SupabaseUserAuthServiceImpl { +impl UserAuthService for SupabaseUserAuthServiceImpl +where + T: SupabaseServerService, +{ fn sign_up(&self, params: BoxAny) -> FutureResult { - let server = self.server.clone(); + let weak_server = self.server.try_get_pg_server(); let (tx, rx) = channel(); tokio::spawn(async move { tx.send( - async { - let client = server.get_pg_client().await.recv().await?; - let uuid = uuid_from_box_any(params)?; - create_user_with_uuid(&client, uuid).await + async move { + match weak_server?.upgrade() { + Some(server) => { + let client = server.get_pg_client().await.recv().await?; + let params = uuid_from_box_any(params)?; + create_user_with_uuid(&client, params.uuid, params.email).await + }, + None => Err(FlowyError::new( + ErrorCode::PgDatabaseError, + "Server is close", + )), + } } .await, ) @@ -52,19 +62,28 @@ impl UserAuthService for SupabaseUserAuthServiceImpl { } fn sign_in(&self, params: BoxAny) -> FutureResult { - let server = self.server.clone(); + let server = self.server.try_get_pg_server(); let (tx, rx) = channel(); tokio::spawn(async move { tx.send( async { - let client = server.get_pg_client().await.recv().await?; - let uuid = uuid_from_box_any(params)?; - let user_profile = get_user_profile(&client, GetUserProfileParams::Uuid(uuid)).await?; - Ok(SignInResponse { - user_id: user_profile.uid, - workspace_id: user_profile.workspace_id, - ..Default::default() - }) + match server?.upgrade() { + None => Err(FlowyError::new( + ErrorCode::PgDatabaseError, + "Server is close", + )), + Some(server) => { + let client = server.get_pg_client().await.recv().await?; + let uuid = uuid_from_box_any(params)?.uuid; + let user_profile = + get_user_profile(&client, GetUserProfileParams::Uuid(uuid)).await?; + Ok(SignInResponse { + user_id: user_profile.uid, + workspace_id: user_profile.workspace_id, + ..Default::default() + }) + }, + } } .await, ) @@ -81,13 +100,17 @@ impl UserAuthService for SupabaseUserAuthServiceImpl { _credential: UserCredentials, params: UpdateUserProfileParams, ) -> FutureResult<(), FlowyError> { - let server = self.server.clone(); + let weak_server = self.server.try_get_pg_server(); let (tx, rx) = channel(); tokio::spawn(async move { tx.send( async move { - let client = server.get_pg_client().await.recv().await?; - update_user_profile(&client, params).await + if let Some(server) = weak_server?.upgrade() { + let client = server.get_pg_client().await.recv().await?; + update_user_profile(&client, params).await + } else { + Ok(()) + } } .await, ) @@ -99,28 +122,33 @@ impl UserAuthService for SupabaseUserAuthServiceImpl { &self, credential: UserCredentials, ) -> FutureResult, FlowyError> { - let server = self.server.clone(); + let weak_server = self.server.try_get_pg_server(); let (tx, rx) = channel(); tokio::spawn(async move { tx.send( async move { - let client = server.get_pg_client().await.recv().await?; - let uid = credential - .uid - .ok_or(FlowyError::new(ErrorCode::InvalidParams, "uid is required"))?; - let user_profile = get_user_profile(&client, GetUserProfileParams::Uid(uid)) - .await - .ok() - .map(|user_profile| UserProfile { - id: user_profile.uid, - email: user_profile.email, - name: user_profile.name, - token: "".to_string(), - icon_url: "".to_string(), - openai_key: "".to_string(), - workspace_id: user_profile.workspace_id, - }); - Ok(user_profile) + if let Some(server) = weak_server?.upgrade() { + let client = server.get_pg_client().await.recv().await?; + let uid = credential + .uid + .ok_or(FlowyError::new(ErrorCode::InvalidParams, "uid is required"))?; + let user_profile = get_user_profile(&client, GetUserProfileParams::Uid(uid)) + .await + .ok() + .map(|user_profile| UserProfile { + id: user_profile.uid, + email: user_profile.email, + name: user_profile.name, + token: "".to_string(), + icon_url: "".to_string(), + openai_key: "".to_string(), + workspace_id: user_profile.workspace_id, + auth_type: AuthType::Supabase, + }); + Ok(user_profile) + } else { + Ok(None) + } } .await, ) @@ -130,13 +158,21 @@ impl UserAuthService for SupabaseUserAuthServiceImpl { fn check_user(&self, credential: UserCredentials) -> FutureResult<(), FlowyError> { let uuid = credential.uuid.and_then(|uuid| Uuid::from_str(&uuid).ok()); - let server = self.server.clone(); + let weak_server = self.server.try_get_pg_server(); let (tx, rx) = channel(); tokio::spawn(async move { tx.send( async move { - let client = server.get_pg_client().await.recv().await?; - check_user(&client, credential.uid, uuid).await + match weak_server?.upgrade() { + None => Err(FlowyError::new( + ErrorCode::PgDatabaseError, + "Server is close", + )), + Some(server) => { + let client = server.get_pg_client().await.recv().await?; + check_user(&client, credential.uid, uuid).await + }, + } } .await, ) @@ -148,12 +184,13 @@ impl UserAuthService for SupabaseUserAuthServiceImpl { async fn create_user_with_uuid( client: &PostgresObject, uuid: Uuid, + email: String, ) -> Result { let mut is_new = true; if let Err(e) = client .execute( - &format!("INSERT INTO {} (uuid) VALUES ($1);", USER_TABLE), - &[&uuid], + &format!("INSERT INTO {} (uuid, email) VALUES ($1,$2);", USER_TABLE), + &[&uuid, &email], ) .await { diff --git a/frontend/rust-lib/flowy-server/src/supabase/migration.rs b/frontend/rust-lib/flowy-server/src/supabase/migration.rs index d7c219ca76..4668517406 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/migration.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/migration.rs @@ -65,13 +65,14 @@ DROP FUNCTION IF EXISTS check_and_delete_snapshots; mod tests { use tokio_postgres::NoTls; + use flowy_server_config::supabase_config::PostgresConfiguration; + use crate::supabase::migration::run_initial_drop; - use crate::supabase::*; // ‼️‼️‼️ Warning: this test will create a table in the database #[tokio::test] async fn test_postgres_db() -> Result<(), anyhow::Error> { - if dotenv::from_filename(".env.test.danger").is_err() { + if dotenv::from_filename(".env.test").is_err() { return Ok(()); } diff --git a/frontend/rust-lib/flowy-server/src/supabase/migrations/initial/Initial_down.sql b/frontend/rust-lib/flowy-server/src/supabase/migrations/initial/Initial_down.sql index 6c2590311d..5db804e3de 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/migrations/initial/Initial_down.sql +++ b/frontend/rust-lib/flowy-server/src/supabase/migrations/initial/Initial_down.sql @@ -21,3 +21,6 @@ DROP FUNCTION IF EXISTS af_collab_snapshot_update_edit_count; DROP TRIGGER IF EXISTS check_and_delete_snapshots_trigger ON af_collab_snapshot CASCADE; DROP FUNCTION IF EXISTS check_and_delete_snapshots; +DROP TRIGGER IF EXISTS new_af_collab_row_trigger ON af_collab CASCADE; +DROP FUNCTION IF EXISTS notify_on_insert_af_collab; + diff --git a/frontend/rust-lib/flowy-server/src/supabase/migrations/initial/V1__Initial_Up.sql b/frontend/rust-lib/flowy-server/src/supabase/migrations/initial/V1__Initial_Up.sql index 980d1953bf..76eb4a403f 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/migrations/initial/V1__Initial_Up.sql +++ b/frontend/rust-lib/flowy-server/src/supabase/migrations/initial/V1__Initial_Up.sql @@ -1,6 +1,7 @@ -- user table CREATE TABLE IF NOT EXISTS af_user ( uuid UUID PRIMARY KEY, + email TEXT DEFAULT '', uid BIGINT GENERATED ALWAYS AS IDENTITY, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); @@ -14,8 +15,8 @@ CREATE TABLE IF NOT EXISTS af_user_profile ( ); -- user_profile trigger CREATE OR REPLACE FUNCTION create_af_user_profile_trigger_func() RETURNS TRIGGER AS $$ BEGIN -INSERT INTO af_user_profile (uid, uuid) -VALUES (NEW.uid, NEW.uuid); +INSERT INTO af_user_profile (uid, uuid, email) +VALUES (NEW.uid, NEW.uuid, NEW.email); RETURN NEW; END $$ LANGUAGE plpgsql; CREATE TRIGGER create_af_user_profile_trigger BEFORE @@ -42,13 +43,26 @@ CREATE TABLE IF NOT EXISTS af_collab ( key BIGINT GENERATED ALWAYS AS IDENTITY, value BYTEA NOT NULL, value_size INTEGER, + uid BIGINT NOT NULL, + md5 TEXT DEFAULT '', created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (oid, key) ); +-- collab pg notify trigger. It will notify the frontend when a new row is inserted in the af_collab table. +CREATE OR REPLACE FUNCTION notify_on_insert_af_collab() RETURNS trigger AS $$ +BEGIN + -- use pg_notify to send a notification + PERFORM pg_notify('new_row_in_af_collab', NEW.oid::text); +RETURN NEW; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER new_af_collab_row_trigger + AFTER INSERT ON af_collab + FOR EACH ROW EXECUTE PROCEDURE notify_on_insert_af_collab(); -- collab statistics. It will be used to store the edit_count of the collab. CREATE TABLE IF NOT EXISTS af_collab_statistics ( oid TEXT PRIMARY KEY, - edit_count BIGINT DEFAULT 0 + edit_count BIGINT NOT NULL DEFAULT 0 ); -- collab statistics trigger. It will increment the edit_count of the collab when a new row is inserted in the af_collab table. CREATE OR REPLACE FUNCTION increment_af_collab_edit_count() RETURNS TRIGGER AS $$ BEGIN IF EXISTS( @@ -76,19 +90,19 @@ CREATE TABLE IF NOT EXISTS af_collab_snapshot ( name TEXT DEFAULT '', blob BYTEA NOT NULL, blob_size INTEGER NOT NULL, - edit_count BIGINT DEFAULT 0, + edit_count BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- auto insert edit_count in the snapshot table. CREATE OR REPLACE FUNCTION af_collab_snapshot_update_edit_count() RETURNS TRIGGER AS $$ BEGIN NEW.edit_count := ( - SELECT edit_count + SELECT COALESCE(edit_count, 0) FROM af_collab_statistics WHERE oid = NEW.oid ); RETURN NEW; END; $$ LANGUAGE plpgsql; -CREATE TRIGGER af_collab_snapshot_update_edit_count_trigger BEFORE +CREATE TRIGGER af_collab_snapshot_update_edit_count_trigger AFTER INSERT ON af_collab_snapshot FOR EACH ROW EXECUTE FUNCTION af_collab_snapshot_update_edit_count(); -- collab snapshot trigger. It will delete the oldest snapshot if the number of snapshots is greater than 20. -- It can use the PG_CRON extension to run this trigger periodically. diff --git a/frontend/rust-lib/flowy-server/src/supabase/mod.rs b/frontend/rust-lib/flowy-server/src/supabase/mod.rs index 6bc21685da..90f09e3a6b 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/mod.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/mod.rs @@ -1,12 +1,10 @@ -pub use configuration::*; pub use server::*; mod entities; pub mod impls; -mod pg_db; +mod postgres_db; mod sql_builder; // mod postgres_http; -mod configuration; mod migration; mod queue; mod server; diff --git a/frontend/rust-lib/flowy-server/src/supabase/pg_db.rs b/frontend/rust-lib/flowy-server/src/supabase/postgres_db.rs similarity index 93% rename from frontend/rust-lib/flowy-server/src/supabase/pg_db.rs rename to frontend/rust-lib/flowy-server/src/supabase/postgres_db.rs index 9e9989ec36..f97bee3b4e 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/pg_db.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/postgres_db.rs @@ -6,10 +6,10 @@ use deadpool_postgres::{Manager, ManagerConfig, Object, Pool, RecyclingMethod}; use tokio_postgres::NoTls; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_server_config::supabase_config::PostgresConfiguration; use crate::supabase::migration::run_migrations; use crate::supabase::queue::RequestPayload; -use crate::supabase::PostgresConfiguration; pub type PostgresObject = Object; pub struct PostgresDB { @@ -25,6 +25,9 @@ impl PostgresDB { } pub async fn new(configuration: PostgresConfiguration) -> Result { + // TODO(nathan): Handling connection surges using + // https://supabase.com/blog/supabase-pgbouncer + // https://supabase.com/docs/guides/database/connecting-to-postgres let mut pg_config = tokio_postgres::Config::new(); pg_config .host(&configuration.url) diff --git a/frontend/rust-lib/flowy-server/src/supabase/queue.rs b/frontend/rust-lib/flowy-server/src/supabase/queue.rs index 87ad3fb29e..15785f335e 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/queue.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/queue.rs @@ -164,8 +164,13 @@ impl RequestRunner where Payload: 'static + Send + Sync, { - pub async fn run(mut notifier: watch::Receiver, server: Weak>) { - server.upgrade().unwrap().notify(); + pub async fn run( + mut notifier: watch::Receiver, + handler: Weak>, + ) { + if let Some(handler) = handler.upgrade() { + handler.notify(); + } loop { // stops the runner if the notifier was closed. if notifier.changed().await.is_err() { @@ -177,10 +182,10 @@ where break; } - if let Some(server) = server.upgrade() { - if let Some(request) = server.prepare_request().await { + if let Some(handler) = handler.upgrade() { + if let Some(request) = handler.prepare_request().await { if request.is_done() { - server.notify(); + handler.notify(); continue; } @@ -188,8 +193,8 @@ where continue; } - let _ = server.handle_request(request).await; - server.notify(); + let _ = handler.handle_request(request).await; + handler.notify(); } } else { break; diff --git a/frontend/rust-lib/flowy-server/src/supabase/server.rs b/frontend/rust-lib/flowy-server/src/supabase/server.rs index 788f65e2f8..669456c596 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/server.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/server.rs @@ -4,13 +4,16 @@ use std::sync::{Arc, Weak}; use std::time::Duration; use appflowy_integrate::RemoteCollabStorage; +use parking_lot::RwLock; use tokio::spawn; use tokio::sync::{watch, Mutex}; use tokio::time::interval; use flowy_database2::deps::DatabaseCloudService; use flowy_document2::deps::DocumentCloudService; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_folder2::deps::FolderCloudService; +use flowy_server_config::supabase_config::{PostgresConfiguration, SupabaseConfiguration}; use flowy_user::event_map::UserAuthService; use lib_infra::async_trait::async_trait; @@ -18,11 +21,10 @@ use crate::supabase::impls::{ PgCollabStorageImpl, SupabaseDatabaseCloudServiceImpl, SupabaseDocumentCloudServiceImpl, SupabaseFolderCloudServiceImpl, SupabaseUserAuthServiceImpl, }; -use crate::supabase::pg_db::{PgClientReceiver, PostgresDB, PostgresEvent}; +use crate::supabase::postgres_db::{PgClientReceiver, PostgresDB, PostgresEvent}; use crate::supabase::queue::{ PendingRequest, RequestHandler, RequestQueue, RequestRunner, RequestState, }; -use crate::supabase::{PostgresConfiguration, SupabaseConfiguration}; use crate::AppFlowyServer; /// Supabase server is used to provide the implementation of the [AppFlowyServer] trait. @@ -30,70 +32,148 @@ use crate::AppFlowyServer; pub struct SupabaseServer { #[allow(dead_code)] config: SupabaseConfiguration, - postgres: Arc, + postgres: Arc>>>, } impl SupabaseServer { pub fn new(config: SupabaseConfiguration) -> Self { - let postgres = PostgresServer::new(config.postgres_config.clone()); + let postgres = if config.enable_sync { + Some(Arc::new(PostgresServer::new( + config.postgres_config.clone(), + ))) + } else { + None + }; Self { config, - postgres: Arc::new(postgres), + postgres: Arc::new(RwLock::new(postgres)), + } + } + + pub fn set_enable_sync(&self, enable: bool) { + if enable { + if self.postgres.read().is_some() { + return; + } + *self.postgres.write() = Some(Arc::new(PostgresServer::new( + self.config.postgres_config.clone(), + ))); + } else { + *self.postgres.write() = None; } } } impl AppFlowyServer for SupabaseServer { + fn enable_sync(&self, enable: bool) { + tracing::info!("supabase sync: {}", enable); + self.set_enable_sync(enable); + } + fn user_service(&self) -> Arc { - Arc::new(SupabaseUserAuthServiceImpl::new(self.postgres.clone())) + Arc::new(SupabaseUserAuthServiceImpl::new(SupabaseServerServiceImpl( + self.postgres.clone(), + ))) } fn folder_service(&self) -> Arc { - Arc::new(SupabaseFolderCloudServiceImpl::new(self.postgres.clone())) + Arc::new(SupabaseFolderCloudServiceImpl::new( + SupabaseServerServiceImpl(self.postgres.clone()), + )) } fn database_service(&self) -> Arc { - Arc::new(SupabaseDatabaseCloudServiceImpl::new(self.postgres.clone())) + Arc::new(SupabaseDatabaseCloudServiceImpl::new( + SupabaseServerServiceImpl(self.postgres.clone()), + )) } fn document_service(&self) -> Arc { - Arc::new(SupabaseDocumentCloudServiceImpl::new(self.postgres.clone())) + Arc::new(SupabaseDocumentCloudServiceImpl::new( + SupabaseServerServiceImpl(self.postgres.clone()), + )) } fn collab_storage(&self) -> Option> { - Some(Arc::new(PgCollabStorageImpl::new(self.postgres.clone()))) + Some(Arc::new(PgCollabStorageImpl::new( + SupabaseServerServiceImpl(self.postgres.clone()), + ))) + } +} + +/// [SupabaseServerService] is used to provide supabase services. The caller can using this trait +/// to get the services and it might need to handle the situation when the services is unavailable. +/// For example, when user stop syncing, the services will be unavailable or when the user is logged +/// out. +pub trait SupabaseServerService: Send + Sync + 'static { + fn get_pg_server(&self) -> Option>; + + fn try_get_pg_server(&self) -> FlowyResult>; +} + +#[derive(Clone)] +pub struct SupabaseServerServiceImpl(pub Arc>>>); +impl SupabaseServerService for SupabaseServerServiceImpl { + /// Get the postgres server, if the postgres server is not available, return None. + fn get_pg_server(&self) -> Option> { + self.0.read().as_ref().map(Arc::downgrade) + } + + /// Try to get the postgres server, if the postgres server is not available, return an error. + fn try_get_pg_server(&self) -> FlowyResult> { + self.0.read().as_ref().map(Arc::downgrade).ok_or_else(|| { + FlowyError::new( + ErrorCode::SupabaseSyncRequired, + "Supabase sync is disabled, please enable it first", + ) + }) } } pub struct PostgresServer { - inner: Arc, + request_handler: Arc, } impl Deref for PostgresServer { - type Target = Arc; + type Target = Arc; fn deref(&self) -> &Self::Target { - &self.inner + &self.request_handler } } -pub struct PostgresServerInner { +impl PostgresServer { + pub fn new(config: PostgresConfiguration) -> Self { + let (runner_notifier_tx, runner_notifier) = watch::channel(false); + let request_handler = Arc::new(PostgresRequestHandler::new(runner_notifier_tx, config)); + + // Initialize the connection to the database + let conn = PendingRequest::new(PostgresEvent::ConnectDB); + request_handler.queue.lock().push(conn); + let handler = Arc::downgrade(&request_handler) as Weak>; + spawn(RequestRunner::run(runner_notifier, handler)); + + Self { request_handler } + } +} + +pub struct PostgresRequestHandler { config: PostgresConfiguration, db: Arc>>>, queue: parking_lot::Mutex>, - notifier: Arc>, + runner_notifier: Arc>, sequence: AtomicU32, } -impl PostgresServerInner { - pub fn new(notifier: watch::Sender, config: PostgresConfiguration) -> Self { +impl PostgresRequestHandler { + pub fn new(runner_notifier: watch::Sender, config: PostgresConfiguration) -> Self { let db = Arc::new(Default::default()); let queue = parking_lot::Mutex::new(RequestQueue::new()); - let notifier = Arc::new(notifier); + let runner_notifier = Arc::new(runner_notifier); Self { db, queue, - notifier, + runner_notifier, config, sequence: Default::default(), } @@ -114,28 +194,13 @@ impl PostgresServerInner { } } -impl PostgresServer { - pub fn new(config: PostgresConfiguration) -> Self { - let (notifier, notifier_rx) = watch::channel(false); - let inner = Arc::new(PostgresServerInner::new(notifier, config)); - - // Initialize the connection to the database - let conn = PendingRequest::new(PostgresEvent::ConnectDB); - inner.queue.lock().push(conn); - let handler = Arc::downgrade(&inner) as Weak>; - spawn(RequestRunner::run(notifier_rx, handler)); - - Self { inner } - } -} - #[async_trait] -impl RequestHandler for PostgresServerInner { +impl RequestHandler for PostgresRequestHandler { async fn prepare_request(&self) -> Option> { match self.queue.try_lock() { None => { // If acquire the lock failed, try after 300ms - let weak_notifier = Arc::downgrade(&self.notifier); + let weak_notifier = Arc::downgrade(&self.runner_notifier); spawn(async move { interval(Duration::from_millis(300)).tick().await; if let Some(notifier) = weak_notifier.upgrade() { @@ -193,6 +258,6 @@ impl RequestHandler for PostgresServerInner { } fn notify(&self) { - let _ = self.notifier.send(false); + let _ = self.runner_notifier.send(false); } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/sql_builder.rs b/frontend/rust-lib/flowy-server/src/supabase/sql_builder.rs index 1d16abf5d5..f29f7769b0 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/sql_builder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/sql_builder.rs @@ -52,8 +52,12 @@ pub struct SelectSqlBuilder { table: String, columns: Vec, where_clause: Option<(String, Box)>, + where_clause_in: Option<(String, Vec>)>, + group_by_column: Option, order_by: Option<(String, bool)>, limit: Option, + lock: bool, + array_agg_columns: Vec, } impl SelectSqlBuilder { @@ -62,16 +66,35 @@ impl SelectSqlBuilder { table: table.to_string(), columns: Vec::new(), where_clause: None, + where_clause_in: None, + group_by_column: None, order_by: None, limit: None, + lock: false, + array_agg_columns: vec![], } } + pub fn lock(mut self) -> Self { + self.lock = true; + self + } + pub fn column(mut self, column: &str) -> Self { self.columns.push(column.to_string()); self } + pub fn group_by(mut self, column: &str) -> Self { + self.group_by_column = Some(column.to_string()); + self + } + + pub fn array_agg(mut self, column: &str) -> Self { + self.array_agg_columns.push(column.to_string()); + self + } + pub fn order_by(mut self, column: &str, asc: bool) -> Self { self.order_by = Some((column.to_string(), asc)); self @@ -82,13 +105,33 @@ impl SelectSqlBuilder { self } + pub fn where_clause_in( + mut self, + clause: &str, + values: Vec, + ) -> Self { + let boxed_values: Vec<_> = values + .into_iter() + .map(|value| Box::new(value) as Box) + .collect(); + self.where_clause_in = Some((clause.to_string(), boxed_values)); + self + } + pub fn limit(mut self, limit: i64) -> Self { self.limit = Some(limit); self } pub fn build(self) -> (String, Vec>) { - let mut sql = format!("SELECT {} FROM {}", self.columns.join(", "), self.table); + let all_columns = self + .columns + .iter() + .chain(self.array_agg_columns.iter()) + .cloned() + .collect::>() + .join(", "); + let mut sql = format!("SELECT {} FROM {}", all_columns, self.table); let mut params: Vec<_> = Vec::new(); if let Some((clause, value)) = self.where_clause { @@ -96,15 +139,46 @@ impl SelectSqlBuilder { params.push(value); } + if let Some((clause, values)) = self.where_clause_in { + let placeholders: Vec = values + .iter() + .enumerate() + .map(|(i, _)| format!("${}", i + 1)) + .collect(); + sql.push_str(&format!( + " WHERE {} IN ({})", + clause, + placeholders.join(",") + )); + params.extend(values); + } + + if let Some(group_by_column) = self.group_by_column { + sql.push_str(&format!(" GROUP BY {}", group_by_column)); + } + if let Some((order_by_column, asc)) = self.order_by { let order = if asc { "ASC" } else { "DESC" }; sql.push_str(&format!(" ORDER BY {} {}", order_by_column, order)); } + // ARRAY_AGG is an aggregate function that concatenates the values from column_name + // into an array. + for array_agg_column in self.array_agg_columns { + sql = sql.replace( + &array_agg_column, + &format!("ARRAY_AGG({}) as {}", array_agg_column, array_agg_column), + ); + } + if let Some(limit) = self.limit { sql.push_str(&format!(" LIMIT {}", limit)); } + if self.lock { + sql.push_str(" FOR UPDATE"); + } + (sql, params) } } diff --git a/frontend/rust-lib/flowy-server/src/util.rs b/frontend/rust-lib/flowy-server/src/util.rs index b19d381ca6..fd9e2a999c 100644 --- a/frontend/rust-lib/flowy-server/src/util.rs +++ b/frontend/rust-lib/flowy-server/src/util.rs @@ -1,11 +1,4 @@ -use std::collections::HashMap; -use std::str::FromStr; - use serde::{Deserialize, Deserializer}; -use uuid::Uuid; - -use flowy_error::{internal_error, ErrorCode, FlowyError}; -use lib_infra::box_any::BoxAny; /// Handles the case where the value is null. If the value is null, return the default value of the /// type. Otherwise, deserialize the value. @@ -17,11 +10,3 @@ where let opt = Option::deserialize(deserializer)?; Ok(opt.unwrap_or_default()) } - -pub(crate) fn uuid_from_box_any(any: BoxAny) -> Result { - let map: HashMap = any.unbox_or_error()?; - let uuid = map - .get("uuid") - .ok_or_else(|| FlowyError::new(ErrorCode::MissingAuthField, "Missing uuid field"))?; - Uuid::from_str(uuid).map_err(internal_error) -} 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 ff9ce37586..cbe92a25d6 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 @@ -1,10 +1,12 @@ use std::collections::HashMap; use std::sync::Arc; +use parking_lot::RwLock; use uuid::Uuid; use flowy_server::supabase::impls::{SupabaseUserAuthServiceImpl, USER_UUID}; -use flowy_server::supabase::{PostgresConfiguration, PostgresServer}; +use flowy_server::supabase::{PostgresServer, SupabaseServerServiceImpl}; +use flowy_server_config::supabase_config::PostgresConfiguration; use flowy_user::entities::{SignUpResponse, UpdateUserProfileParams}; use flowy_user::event_map::{UserAuthService, UserCredentials}; use lib_infra::box_any::BoxAny; @@ -17,26 +19,27 @@ async fn user_sign_up_test() { if dotenv::from_filename("./.env.test").is_err() { return; } - let server = Arc::new(PostgresServer::new( - PostgresConfiguration::from_env().unwrap(), - )); - let user_service = SupabaseUserAuthServiceImpl::new(server); - + let user_service = user_auth_service_impl(); let mut params = HashMap::new(); params.insert(USER_UUID.to_string(), Uuid::new_v4().to_string()); let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); assert!(!user.workspace_id.is_empty()); } +fn user_auth_service_impl() -> SupabaseUserAuthServiceImpl { + let server = Arc::new(PostgresServer::new( + PostgresConfiguration::from_env().unwrap(), + )); + let weak_server = SupabaseServerServiceImpl(Arc::new(RwLock::new(Some(server)))); + SupabaseUserAuthServiceImpl::new(weak_server) +} + #[tokio::test] async fn user_sign_up_with_existing_uuid_test() { if dotenv::from_filename("./.env.test").is_err() { return; } - let server = Arc::new(PostgresServer::new( - PostgresConfiguration::from_env().unwrap(), - )); - let user_service = SupabaseUserAuthServiceImpl::new(server); + let user_service = user_auth_service_impl(); let uuid = Uuid::new_v4(); let mut params = HashMap::new(); @@ -54,10 +57,7 @@ async fn update_user_profile_test() { if dotenv::from_filename("./.env.test").is_err() { return; } - let server = Arc::new(PostgresServer::new( - PostgresConfiguration::from_env().unwrap(), - )); - let user_service = SupabaseUserAuthServiceImpl::new(server); + let user_service = user_auth_service_impl(); let uuid = Uuid::new_v4(); let mut params = HashMap::new(); @@ -98,10 +98,7 @@ async fn get_user_profile_test() { return; } setup_log(); - let server = Arc::new(PostgresServer::new( - PostgresConfiguration::from_env().unwrap(), - )); - let user_service = SupabaseUserAuthServiceImpl::new(server); + let user_service = user_auth_service_impl(); let uuid = Uuid::new_v4(); let mut params = HashMap::new(); @@ -146,10 +143,7 @@ async fn get_not_exist_user_profile_test() { return; } setup_log(); - let server = Arc::new(PostgresServer::new( - PostgresConfiguration::from_env().unwrap(), - )); - let user_service = SupabaseUserAuthServiceImpl::new(server); + let user_service = user_auth_service_impl(); let result = user_service .get_user_profile(UserCredentials::from_uid(i64::MAX)) .await diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2023-07-12-135810_user_auth_type/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2023-07-12-135810_user_auth_type/down.sql new file mode 100644 index 0000000000..4352905722 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2023-07-12-135810_user_auth_type/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE user_table +DROP COLUMN auth_type; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2023-07-12-135810_user_auth_type/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2023-07-12-135810_user_auth_type/up.sql new file mode 100644 index 0000000000..1a493bd4e5 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2023-07-12-135810_user_auth_type/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE user_table +ADD COLUMN auth_type INTEGER NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/src/lib.rs b/frontend/rust-lib/flowy-sqlite/src/lib.rs index ec552afe20..fbdc5431d1 100644 --- a/frontend/rust-lib/flowy-sqlite/src/lib.rs +++ b/frontend/rust-lib/flowy-sqlite/src/lib.rs @@ -27,14 +27,13 @@ pub mod prelude { pub use diesel::{query_dsl::*, BelongingToDsl, ExpressionMethods, RunQueryDsl}; pub use crate::*; - - pub use super::UserDatabaseConnection; } embed_migrations!("../flowy-sqlite/migrations/"); pub const DB_NAME: &str = "flowy-database.db"; -pub fn init(storage_path: &str) -> Result { +pub fn init>(storage_path: P) -> Result { + let storage_path = storage_path.as_ref().to_str().unwrap(); if !Path::new(storage_path).exists() { std::fs::create_dir_all(storage_path)?; } @@ -52,7 +51,3 @@ where let msg = format!("{:?}", e); io::Error::new(io::ErrorKind::NotConnected, msg) } - -pub trait UserDatabaseConnection: Send + Sync { - fn get_connection(&self) -> Result; -} diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index 6b4825744c..6034714aa2 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -21,6 +21,7 @@ diesel::table! { openai_key -> Text, token -> Text, email -> Text, + auth_type -> Integer, } } diff --git a/frontend/rust-lib/flowy-sqlite/src/sqlite/pool.rs b/frontend/rust-lib/flowy-sqlite/src/sqlite/pool.rs index 3858804b1d..e14ea9d7a8 100644 --- a/frontend/rust-lib/flowy-sqlite/src/sqlite/pool.rs +++ b/frontend/rust-lib/flowy-sqlite/src/sqlite/pool.rs @@ -1,14 +1,10 @@ -use crate::sqlite::{errors::*, pragma::*}; +use std::{sync::Arc, time::Duration}; + use diesel::{connection::Connection, SqliteConnection}; use r2d2::{CustomizeConnection, ManageConnection, Pool}; use scheduled_thread_pool::ScheduledThreadPool; -use std::{sync::Arc, time::Duration}; -lazy_static::lazy_static! { - static ref DB_POOL: Arc = Arc::new( - ScheduledThreadPool::builder().num_threads(4).thread_name_pattern("db-pool-{}:").build() - ); -} +use crate::sqlite::{errors::*, pragma::*}; pub struct ConnectionPool { pub(crate) inner: Pool, @@ -28,7 +24,12 @@ impl ConnectionPool { T: Into, { let manager = ConnectionManager::new(uri); - let thread_pool = DB_POOL.clone(); + let thread_pool = Arc::new( + ScheduledThreadPool::builder() + .num_threads(4) + .thread_name_pattern("db-pool-{}:") + .build(), + ); let config = Arc::new(config); let customizer_config = DatabaseCustomizerConfig::default(); diff --git a/frontend/rust-lib/flowy-test/Cargo.toml b/frontend/rust-lib/flowy-test/Cargo.toml index cd26010e2f..8a4b3d4e30 100644 --- a/frontend/rust-lib/flowy-test/Cargo.toml +++ b/frontend/rust-lib/flowy-test/Cargo.toml @@ -16,6 +16,7 @@ lib-dispatch = { path = "../lib-dispatch" } lib-ot = { path = "../../../shared-lib/lib-ot" } lib-infra = { path = "../../../shared-lib/lib-infra" } flowy-server = { path = "../flowy-server" } +flowy-server-config = { path = "../flowy-server-config" } flowy-notification = { path = "../flowy-notification" } anyhow = "1.0.71" @@ -40,6 +41,7 @@ collab-document = { version = "0.1.0" } collab-folder = { version = "0.1.0" } collab-database = { version = "0.1.0" } assert-json-diff = "2.0.2" +tokio-postgres = { version = "0.7.8" } [features] default = ["cloud_test"] 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 29e88d7559..2cb04d741f 100644 --- a/frontend/rust-lib/flowy-test/src/document/document_event.rs +++ b/frontend/rust-lib/flowy-test/src/document/document_event.rs @@ -19,7 +19,7 @@ pub struct OpenDocumentData { impl DocumentEventTest { pub async fn new() -> Self { - let sdk = FlowyCoreTest::new_with_user().await; + let sdk = FlowyCoreTest::new_with_guest_user().await; Self { inner: sdk } } diff --git a/frontend/rust-lib/flowy-test/src/lib.rs b/frontend/rust-lib/flowy-test/src/lib.rs index b1871314fb..91ae74e9b6 100644 --- a/frontend/rust-lib/flowy-test/src/lib.rs +++ b/frontend/rust-lib/flowy-test/src/lib.rs @@ -19,7 +19,7 @@ use flowy_folder2::entities::*; use flowy_folder2::event_map::FolderEvent; use flowy_notification::entities::SubscribeObject; use flowy_notification::{register_notification_sender, NotificationSender}; -use flowy_user::entities::{AuthTypePB, SignOutPB, ThirdPartyAuthPB, UserProfilePB}; +use flowy_user::entities::{AuthTypePB, ThirdPartyAuthPB, UserProfilePB}; use flowy_user::errors::FlowyError; use flowy_user::event_map::UserEvent::*; @@ -67,23 +67,35 @@ impl FlowyCoreTest { Self::default() } - pub async fn new_with_user() -> Self { + pub async fn new_with_guest_user() -> Self { let test = Self::default(); - test.sign_up().await; + test.sign_up_as_guest().await; test } - pub async fn sign_up(&self) -> SignUpContext { - let auth_type = self.auth_type.read().clone(); + pub async fn sign_up_as_guest(&self) -> SignUpContext { + async_sign_up(self.inner.dispatcher(), AuthTypePB::Local).await + } - async_sign_up(self.inner.dispatcher(), auth_type).await + pub async fn supabase_party_sign_up(&self, uuid: &str) -> UserProfilePB { + let mut map = HashMap::new(); + map.insert("uuid".to_string(), uuid.to_string()); + let payload = ThirdPartyAuthPB { + map, + auth_type: AuthTypePB::Supabase, + }; + + EventBuilder::new(self.clone()) + .event(ThirdPartyAuth) + .payload(payload) + .async_send() + .await + .parse::() } pub async fn sign_out(&self) { - let auth_type = self.auth_type.read().clone(); EventBuilder::new(self.clone()) .event(SignOut) - .payload(SignOutPB { auth_type }) .async_send() .await; } @@ -93,7 +105,7 @@ impl FlowyCoreTest { } pub async fn init_user(&self) -> UserProfilePB { - self.sign_up().await.user_profile + self.sign_up_as_guest().await.user_profile } pub async fn sign_up_with_uuid(&self, uuid: &str) -> UserProfilePB { diff --git a/frontend/rust-lib/flowy-test/src/user_event.rs b/frontend/rust-lib/flowy-test/src/user_event.rs index f445c174e0..6d9cac0e6b 100644 --- a/frontend/rust-lib/flowy-test/src/user_event.rs +++ b/frontend/rust-lib/flowy-test/src/user_event.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use nanoid::nanoid; -use flowy_user::entities::{AuthTypePB, SignInPayloadPB, SignUpPayloadPB, UserProfilePB}; +use flowy_user::entities::{AuthTypePB, SignUpPayloadPB, UserProfilePB}; use flowy_user::errors::FlowyError; use flowy_user::event_map::UserEvent::*; use lib_dispatch::prelude::{AFPluginDispatcher, AFPluginRequest, ToBytes}; @@ -80,21 +80,3 @@ pub async fn init_user_setting(dispatch: Arc) { let request = AFPluginRequest::new(InitUser); let _ = AFPluginDispatcher::async_send(dispatch.clone(), request).await; } - -#[allow(dead_code)] -fn sign_in(dispatch: Arc) -> UserProfilePB { - let payload = SignInPayloadPB { - email: login_email(), - password: login_password(), - name: "rust".to_owned(), - auth_type: AuthTypePB::Local, - } - .into_bytes() - .unwrap(); - - let request = AFPluginRequest::new(SignIn).payload(payload); - AFPluginDispatcher::sync_send(dispatch, request) - .parse::() - .unwrap() - .unwrap() -} diff --git a/frontend/rust-lib/flowy-test/tests/database/local_test/test.rs b/frontend/rust-lib/flowy-test/tests/database/local_test/test.rs index 38c28deac0..9cceb95def 100644 --- a/frontend/rust-lib/flowy-test/tests/database/local_test/test.rs +++ b/frontend/rust-lib/flowy-test/tests/database/local_test/test.rs @@ -13,7 +13,7 @@ use lib_infra::util::timestamp; #[tokio::test] async fn get_database_id_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -35,7 +35,7 @@ async fn get_database_id_event_test() { #[tokio::test] async fn get_database_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -49,7 +49,7 @@ async fn get_database_event_test() { #[tokio::test] async fn get_field_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -64,7 +64,7 @@ async fn get_field_event_test() { #[tokio::test] async fn create_field_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -78,7 +78,7 @@ async fn create_field_event_test() { #[tokio::test] async fn delete_field_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -99,7 +99,7 @@ async fn delete_field_event_test() { // The primary field is not allowed to be deleted. #[tokio::test] async fn delete_primary_field_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -114,7 +114,7 @@ async fn delete_primary_field_event_test() { #[tokio::test] async fn update_field_type_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -132,7 +132,7 @@ async fn update_field_type_event_test() { #[tokio::test] async fn update_primary_field_type_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -151,7 +151,7 @@ async fn update_primary_field_type_event_test() { #[tokio::test] async fn duplicate_field_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -169,7 +169,7 @@ async fn duplicate_field_event_test() { // The primary field is not allowed to be duplicated. So this test should return an error. #[tokio::test] async fn duplicate_primary_field_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -183,7 +183,7 @@ async fn duplicate_primary_field_test() { #[tokio::test] async fn get_primary_field_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -196,7 +196,7 @@ async fn get_primary_field_event_test() { #[tokio::test] async fn create_row_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -209,7 +209,7 @@ async fn create_row_event_test() { #[tokio::test] async fn delete_row_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -232,7 +232,7 @@ async fn delete_row_event_test() { #[tokio::test] async fn get_row_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -248,7 +248,7 @@ async fn get_row_event_test() { #[tokio::test] async fn update_row_meta_event_with_url_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -276,7 +276,7 @@ async fn update_row_meta_event_with_url_test() { #[tokio::test] async fn update_row_meta_event_with_cover_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -304,7 +304,7 @@ async fn update_row_meta_event_with_cover_test() { #[tokio::test] async fn delete_row_event_with_invalid_row_id_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -317,7 +317,7 @@ async fn delete_row_event_with_invalid_row_id_test() { #[tokio::test] async fn duplicate_row_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -334,7 +334,7 @@ async fn duplicate_row_event_test() { #[tokio::test] async fn duplicate_row_event_with_invalid_row_id_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -351,7 +351,7 @@ async fn duplicate_row_event_with_invalid_row_id_test() { #[tokio::test] async fn move_row_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -371,7 +371,7 @@ async fn move_row_event_test() { #[tokio::test] async fn move_row_event_test2() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -391,7 +391,7 @@ async fn move_row_event_test2() { #[tokio::test] async fn move_row_event_with_invalid_row_id_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -418,7 +418,7 @@ async fn move_row_event_with_invalid_row_id_test() { #[tokio::test] async fn update_text_cell_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -448,7 +448,7 @@ async fn update_text_cell_event_test() { #[tokio::test] async fn update_checkbox_cell_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -479,7 +479,7 @@ async fn update_checkbox_cell_event_test() { #[tokio::test] async fn update_single_select_cell_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -506,7 +506,7 @@ async fn update_single_select_cell_event_test() { #[tokio::test] async fn update_date_cell_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -545,7 +545,7 @@ async fn update_date_cell_event_test() { #[tokio::test] async fn update_date_cell_event_with_empty_time_str_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -581,7 +581,7 @@ async fn update_date_cell_event_with_empty_time_str_test() { #[tokio::test] async fn create_checklist_field_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -602,7 +602,7 @@ async fn create_checklist_field_test() { #[tokio::test] async fn update_checklist_cell_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -659,7 +659,7 @@ async fn update_checklist_cell_test() { // The number of groups should be 0 if there is no group by field in grid #[tokio::test] async fn get_groups_event_with_grid_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -671,7 +671,7 @@ async fn get_groups_event_with_grid_test() { #[tokio::test] async fn get_groups_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -683,7 +683,7 @@ async fn get_groups_event_test() { #[tokio::test] async fn move_group_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -717,7 +717,7 @@ async fn move_group_event_test() { #[tokio::test] async fn move_group_event_with_invalid_id_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -739,7 +739,7 @@ async fn move_group_event_with_invalid_id_test() { #[tokio::test] async fn rename_group_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -763,7 +763,7 @@ async fn rename_group_event_test() { #[tokio::test] async fn hide_group_event_test2() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -785,7 +785,7 @@ async fn hide_group_event_test2() { // Update the database layout type from grid to board #[tokio::test] async fn update_database_layout_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -807,7 +807,7 @@ async fn update_database_layout_event_test() { // Update the database layout type from grid to board. Set the checkbox field as the grouping field #[tokio::test] async fn update_database_layout_event_test2() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let grid_view = test .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) @@ -839,7 +839,7 @@ async fn update_database_layout_event_test2() { // Create a checkbox field in the default board and then set it as the grouping field. #[tokio::test] async fn set_group_by_checkbox_field_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) @@ -856,7 +856,7 @@ async fn set_group_by_checkbox_field_test() { #[tokio::test] async fn get_all_calendar_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let calendar_view = test .create_calendar(¤t_workspace.id, "my calendar view".to_owned(), vec![]) @@ -869,7 +869,7 @@ async fn get_all_calendar_event_test() { #[tokio::test] async fn create_calendar_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let calendar_view = test .create_calendar(¤t_workspace.id, "my calendar view".to_owned(), vec![]) diff --git a/frontend/rust-lib/flowy-test/tests/database/supabase_test/helper.rs b/frontend/rust-lib/flowy-test/tests/database/supabase_test/helper.rs index 453d89b1f6..4c96cf34dd 100644 --- a/frontend/rust-lib/flowy-test/tests/database/supabase_test/helper.rs +++ b/frontend/rust-lib/flowy-test/tests/database/supabase_test/helper.rs @@ -19,6 +19,7 @@ pub struct FlowySupabaseDatabaseTest { } impl FlowySupabaseDatabaseTest { + #[allow(dead_code)] pub async fn new_with_user(uuid: String) -> Option { let inner = FlowySupabaseTest::new()?; inner.sign_up_with_uuid(&uuid).await; @@ -70,10 +71,7 @@ impl FlowySupabaseDatabaseTest { pub async fn get_collab_update(&self, database_id: &str) -> Vec { let cloud_service = self.database_manager.get_cloud_service().clone(); - let remote_updates = cloud_service - .get_database_updates(database_id) - .await - .unwrap(); + let remote_updates = cloud_service.get_collab_update(database_id).await.unwrap(); if remote_updates.is_empty() { return vec![]; diff --git a/frontend/rust-lib/flowy-test/tests/folder/local_test/test.rs b/frontend/rust-lib/flowy-test/tests/folder/local_test/test.rs index 1afc4f1d0e..2c3f4f35cd 100644 --- a/frontend/rust-lib/flowy-test/tests/folder/local_test/test.rs +++ b/frontend/rust-lib/flowy-test/tests/folder/local_test/test.rs @@ -5,7 +5,7 @@ use flowy_user::errors::ErrorCode; #[tokio::test] async fn create_workspace_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let request = CreateWorkspacePayloadPB { name: "my second workspace".to_owned(), desc: "".to_owned(), @@ -21,7 +21,7 @@ async fn create_workspace_event_test() { #[tokio::test] async fn open_workspace_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let payload = CreateWorkspacePayloadPB { name: "my second workspace".to_owned(), desc: "".to_owned(), @@ -51,7 +51,7 @@ async fn open_workspace_event_test() { #[tokio::test] async fn create_view_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -63,7 +63,7 @@ async fn create_view_event_test() { #[tokio::test] async fn update_view_event_with_name_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -84,7 +84,7 @@ async fn update_view_event_with_name_test() { #[tokio::test] async fn update_view_event_with_icon_url_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -105,7 +105,7 @@ async fn update_view_event_with_icon_url_test() { #[tokio::test] async fn update_view_event_with_cover_url_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -126,7 +126,7 @@ async fn update_view_event_with_cover_url_test() { #[tokio::test] async fn delete_view_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -149,7 +149,7 @@ async fn delete_view_event_test() { #[tokio::test] async fn put_back_trash_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -192,7 +192,7 @@ async fn put_back_trash_event_test() { #[tokio::test] async fn delete_view_permanently_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let view = test .create_view(¤t_workspace.id, "My first view".to_string()) @@ -241,7 +241,7 @@ async fn delete_view_permanently_event_test() { #[tokio::test] async fn delete_all_trash_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; for i in 0..3 { @@ -285,7 +285,7 @@ async fn delete_all_trash_test() { #[tokio::test] async fn multiple_hierarchy_view_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; for i in 1..4 { let parent = test @@ -361,7 +361,7 @@ async fn multiple_hierarchy_view_test() { #[tokio::test] async fn move_view_event_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; for i in 1..4 { let parent = test @@ -399,7 +399,7 @@ async fn move_view_event_test() { #[tokio::test] async fn move_view_event_after_delete_view_test() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; for i in 1..6 { let _ = test @@ -441,7 +441,7 @@ async fn move_view_event_after_delete_view_test() { #[tokio::test] async fn move_view_event_after_delete_view_test2() { - let test = FlowyCoreTest::new_with_user().await; + let test = FlowyCoreTest::new_with_guest_user().await; let current_workspace = test.get_current_workspace().await.workspace; let parent = test .create_view(¤t_workspace.id, "My view".to_string()) diff --git a/frontend/rust-lib/flowy-test/tests/folder/supabase_test/helper.rs b/frontend/rust-lib/flowy-test/tests/folder/supabase_test/helper.rs index 7542609992..b1522f407c 100644 --- a/frontend/rust-lib/flowy-test/tests/folder/supabase_test/helper.rs +++ b/frontend/rust-lib/flowy-test/tests/folder/supabase_test/helper.rs @@ -44,7 +44,7 @@ impl FlowySupabaseFolderTest { pub async fn get_collab_update(&self, workspace_id: &str) -> Vec { let cloud_service = self.folder_manager.get_cloud_service().clone(); let remote_updates = cloud_service - .get_folder_updates(workspace_id) + .get_folder_updates(workspace_id, self.user_session.user_id().unwrap()) .await .unwrap(); diff --git a/frontend/rust-lib/flowy-test/tests/main.rs b/frontend/rust-lib/flowy-test/tests/main.rs index 91d1d2a44f..e0e50b0932 100644 --- a/frontend/rust-lib/flowy-test/tests/main.rs +++ b/frontend/rust-lib/flowy-test/tests/main.rs @@ -3,3 +3,5 @@ mod document; mod folder; mod user; pub mod util; + +mod tool; diff --git a/frontend/rust-lib/flowy-test/tests/tool/mod.rs b/frontend/rust-lib/flowy-test/tests/tool/mod.rs new file mode 100644 index 0000000000..4478fbe55e --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/tool/mod.rs @@ -0,0 +1,2 @@ +mod pg_migration; +// mod pg_row; diff --git a/frontend/rust-lib/flowy-test/tests/tool/pg_migration.rs b/frontend/rust-lib/flowy-test/tests/tool/pg_migration.rs new file mode 100644 index 0000000000..f7ba3464a1 --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/tool/pg_migration.rs @@ -0,0 +1,74 @@ +use tokio_postgres::{Client, NoTls}; + +use flowy_server_config::supabase_config::PostgresConfiguration; + +/// Drop all tables and dependencies defined in the v1_initial_up.sql. +/// Be careful when using this function. It will drop all tables and dependencies. +/// Mostly used for testing. +#[allow(dead_code)] +#[cfg(debug_assertions)] +pub(crate) async fn run_initial_drop(client: &Client) { + // let sql = include_str!("migrations/initial/initial_down.sql"); + let sql = r#"DROP TABLE IF EXISTS af_user; +DROP TABLE IF EXISTS af_workspace; +DROP TABLE IF EXISTS af_user_profile; +DROP TABLE IF EXISTS af_collab; +DROP VIEW IF EXISTS af_collab_state; +DROP TABLE IF EXISTS af_collab_snapshot; +DROP TABLE IF EXISTS af_collab_statistics; + +DROP TRIGGER IF EXISTS create_af_user_profile_trigger ON af_user_profile CASCADE; +DROP FUNCTION IF EXISTS create_af_user_profile_trigger_func; + +DROP TRIGGER IF EXISTS create_af_workspace_trigger ON af_workspace CASCADE; +DROP FUNCTION IF EXISTS create_af_workspace_trigger_func; + +DROP TRIGGER IF EXISTS af_collab_insert_trigger ON af_collab CASCADE; +DROP FUNCTION IF EXISTS increment_af_collab_update_count; + +DROP TRIGGER IF EXISTS af_collab_snapshot_update_edit_count_trigger ON af_collab_snapshot; +DROP FUNCTION IF EXISTS af_collab_snapshot_update_edit_count; + +DROP TRIGGER IF EXISTS check_and_delete_snapshots_trigger ON af_collab_snapshot CASCADE; +DROP FUNCTION IF EXISTS check_and_delete_snapshots; + +DROP TRIGGER IF EXISTS new_af_collab_row_trigger ON af_collab CASCADE; +DROP FUNCTION IF EXISTS notify_on_insert_af_collab; +"#; + client.batch_execute(sql).await.unwrap(); + client + .batch_execute("DROP TABLE IF EXISTS af_migration_history") + .await + .unwrap(); +} + +// ‼️‼️‼️ Warning: this test will create a table in the database +#[tokio::test] +async fn run_initial_drop_test() -> Result<(), anyhow::Error> { + // rename the `.evn.test.danger` to the actual env file name. + if dotenv::from_filename(".env.test.danger").is_err() { + return Ok(()); + } + + let configuration = PostgresConfiguration::from_env().unwrap(); + let mut config = tokio_postgres::Config::new(); + config + .host(&configuration.url) + .user(&configuration.user_name) + .password(&configuration.password) + .port(configuration.port); + + // Using the https://docs.rs/postgres-openssl/latest/postgres_openssl/ to enable tls connection. + let (client, connection) = config.connect(NoTls).await?; + tokio::spawn(async move { + if let Err(e) = connection.await { + tracing::error!("postgres db connection error: {}", e); + } + }); + + #[cfg(debug_assertions)] + { + run_initial_drop(&client).await; + } + Ok(()) +} diff --git a/frontend/rust-lib/flowy-test/tests/tool/pg_row.rs b/frontend/rust-lib/flowy-test/tests/tool/pg_row.rs new file mode 100644 index 0000000000..1b3e7e57d0 --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/tool/pg_row.rs @@ -0,0 +1,172 @@ +use std::sync::Arc; + +use assert_json_diff::assert_json_eq; +use collab::core::collab::MutexCollab; +use collab::core::origin::CollabOrigin; +use collab::preclude::updates::decoder::Decode; +use collab::preclude::Update; +use parking_lot::RwLock; +use serde_json::json; + +use flowy_database2::deps::DatabaseCloudService; +use flowy_document2::deps::DocumentCloudService; +use flowy_folder2::deps::{FolderCloudService, FolderSnapshot}; +use flowy_server::supabase::impls::{ + SupabaseDatabaseCloudServiceImpl, SupabaseDocumentCloudServiceImpl, + SupabaseFolderCloudServiceImpl, +}; +use flowy_server::supabase::{PostgresServer, SupabaseServerServiceImpl}; + +use crate::util::get_supabase_config; + +pub struct PostgresConnect { + inner: Arc, +} + +impl PostgresConnect { + pub fn new() -> Option { + let config = get_supabase_config()?; + let inner = PostgresServer::new(config.postgres_config); + Some(Self { + inner: Arc::new(inner), + }) + } + + fn server_provider_impl(&self) -> SupabaseServerServiceImpl { + SupabaseServerServiceImpl(Arc::new(RwLock::new(Some(self.inner.clone())))) + } + + async fn get_folder(&self, workspace_id: &str) -> MutexCollab { + let folder_service = SupabaseFolderCloudServiceImpl::new(self.server_provider_impl()); + let updates = folder_service + .get_folder_updates(workspace_id, 0) + .await + .unwrap(); + let collab = MutexCollab::new(CollabOrigin::Server, workspace_id, vec![]); + collab.lock().with_transact_mut(|txn| { + for update in updates { + txn.apply_update(Update::decode_v1(&update).unwrap()); + } + }); + collab + } + + async fn get_folder_snapshot(&self, workspace_id: &str) -> MutexCollab { + let folder_service = SupabaseFolderCloudServiceImpl::new(self.server_provider_impl()); + let snapshot: FolderSnapshot = folder_service + .get_folder_latest_snapshot(workspace_id) + .await + .unwrap() + .unwrap(); + let collab = MutexCollab::new(CollabOrigin::Server, workspace_id, vec![]); + collab.lock().with_transact_mut(|txn| { + txn.apply_update(Update::decode_v1(&snapshot.data).unwrap()); + }); + collab + } + + async fn get_database_collab_object(&self, object_id: &str) -> MutexCollab { + let database_service = SupabaseDatabaseCloudServiceImpl::new(self.server_provider_impl()); + let updates = database_service.get_collab_update(object_id).await.unwrap(); + let collab = MutexCollab::new(CollabOrigin::Server, object_id, vec![]); + collab.lock().with_transact_mut(|txn| { + for update in updates { + txn.apply_update(Update::decode_v1(&update).unwrap()); + } + }); + collab + } + + async fn get_database_rows_object(&self, row_ids: Vec) -> Vec { + let database_service = SupabaseDatabaseCloudServiceImpl::new(self.server_provider_impl()); + let updates_by_oid = database_service + .batch_get_collab_updates(row_ids) + .await + .unwrap(); + let mut collabs = vec![]; + for (oid, updates) in updates_by_oid { + let collab = MutexCollab::new(CollabOrigin::Server, &oid, vec![]); + collab.lock().with_transact_mut(|txn| { + for update in updates { + txn.apply_update(Update::decode_v1(&update).unwrap()); + } + }); + collabs.push(collab); + } + collabs + } + + async fn get_document(&self, document_id: &str) -> MutexCollab { + let document_service = SupabaseDocumentCloudServiceImpl::new(self.server_provider_impl()); + let updates = document_service + .get_document_updates(document_id) + .await + .unwrap(); + let collab = MutexCollab::new(CollabOrigin::Server, document_id, vec![]); + collab.lock().with_transact_mut(|txn| { + for update in updates { + txn.apply_update(Update::decode_v1(&update).unwrap()); + } + }); + collab + } +} + +#[tokio::test] +async fn get_folder_test() { + if let Some(conn) = PostgresConnect::new() { + let collab = conn + .get_folder("2ddf790f-18bb-4e9c-aacb-f29ca755f72a") + .await; + let value = collab.to_json_value(); + assert_json_eq!(value, json!("")); + } +} + +#[tokio::test] +async fn get_folder_snapshot() { + if let Some(conn) = PostgresConnect::new() { + let collab = conn + .get_folder_snapshot("17f5e820-dcc8-4ca9-ab93-b45f17ca0948") + .await; + let value = collab.to_json_value(); + assert_json_eq!(value, json!("")); + } +} + +#[tokio::test] +async fn get_document_test() { + if let Some(conn) = PostgresConnect::new() { + let collab = conn + .get_document("158c8275-ff6d-49e1-a2ed-82c71dea1126") + .await; + let value = collab.to_json_value(); + assert_json_eq!(value, json!("")); + } +} + +#[tokio::test] +async fn get_workspace_database_test() { + if let Some(conn) = PostgresConnect::new() { + let collab = conn + .get_database_collab_object("MTp1c2VyOmRhdGFiYXNl") + .await; + let value = collab.to_json_value(); + assert_json_eq!(value, json!("")); + } +} + +#[tokio::test] +async fn batch_get_database_rows_test() { + if let Some(conn) = PostgresConnect::new() { + let row_ids = vec![ + "93cebb2d-4831-496c-adde-1a82bd745099".to_string(), + "7989a12f-23b2-48ff-8d5f-9bdf651ad7aa".to_string(), + ]; + let collabs = conn.get_database_rows_object(row_ids).await; + for collab in collabs { + let value = collab.to_json_value(); + println!("{}", value); + } + } +} diff --git a/frontend/rust-lib/flowy-test/tests/user/local_test/auth_test.rs b/frontend/rust-lib/flowy-test/tests/user/local_test/auth_test.rs index e02f55e271..f39ddaa210 100644 --- a/frontend/rust-lib/flowy-test/tests/user/local_test/auth_test.rs +++ b/frontend/rust-lib/flowy-test/tests/user/local_test/auth_test.rs @@ -57,13 +57,14 @@ async fn sign_up_with_long_password() { async fn sign_in_success() { let test = FlowyCoreTest::new(); let _ = EventBuilder::new(test.clone()).event(SignOut).sync_send(); - let sign_up_context = test.sign_up().await; + let sign_up_context = test.sign_up_as_guest().await; let request = SignInPayloadPB { email: sign_up_context.user_profile.email.clone(), password: sign_up_context.password.clone(), name: "".to_string(), auth_type: AuthTypePB::Local, + uid: Some(sign_up_context.user_profile.id), }; let response = EventBuilder::new(test.clone()) @@ -84,6 +85,7 @@ async fn sign_in_with_invalid_email() { password: login_password(), name: "".to_string(), auth_type: AuthTypePB::Local, + uid: None, }; assert_eq!( @@ -110,6 +112,7 @@ async fn sign_in_with_invalid_password() { password, name: "".to_string(), auth_type: AuthTypePB::Local, + uid: None, }; assert!(EventBuilder::new(sdk) 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 fdb35e6372..f8ff710713 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 @@ -11,7 +11,7 @@ use flowy_user::event_map::UserEvent::*; use crate::util::*; #[tokio::test] -async fn sign_up_test() { +async fn third_party_sign_up_test() { if get_supabase_config().is_some() { let test = FlowyCoreTest::new(); let mut map = HashMap::new(); @@ -31,6 +31,100 @@ async fn sign_up_test() { } } +#[tokio::test] +async fn sign_up_as_guest_and_then_update_to_new_cloud_user_test() { + if get_supabase_config().is_some() { + let test = FlowyCoreTest::new_with_guest_user().await; + let old_views = test + .folder_manager + .get_current_workspace_views() + .await + .unwrap(); + let old_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + + let uuid = uuid::Uuid::new_v4().to_string(); + test.supabase_party_sign_up(&uuid).await; + let new_views = test + .folder_manager + .get_current_workspace_views() + .await + .unwrap(); + let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + + assert_eq!(old_views.len(), new_views.len()); + assert_eq!(old_workspace.name, new_workspace.name); + assert_eq!(old_workspace.views.len(), new_workspace.views.len()); + for (index, view) in old_views.iter().enumerate() { + assert_eq!(view.name, new_views[index].name); + assert_eq!(view.id, new_views[index].id); + assert_eq!(view.layout, new_views[index].layout); + assert_eq!(view.create_time, new_views[index].create_time); + } + } +} + +#[tokio::test] +async fn sign_up_as_guest_and_then_update_to_existing_cloud_user_test() { + if get_supabase_config().is_some() { + let test = FlowyCoreTest::new_with_guest_user().await; + let historical_users = test.user_session.sign_in_history(); + assert_eq!(historical_users.len(), 1); + let uuid = uuid::Uuid::new_v4().to_string(); + + // The workspace of the guest will be migrated to the new user with given uuid + let user_profile = test.supabase_party_sign_up(&uuid).await; + // let historical_users = test.user_session.sign_in_history(user_profile.id); + // assert_eq!(historical_users.len(), 2); + let old_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + let old_cloud_views = test + .folder_manager + .get_current_workspace_views() + .await + .unwrap(); + assert_eq!(old_cloud_views.len(), 1); + assert_eq!(old_cloud_views.first().unwrap().child_views.len(), 1); + + // sign out and then sign in as a guest + test.sign_out().await; + // when sign out, the user profile will be not found + let error = test + .user_session + .get_user_profile(user_profile.id, false) + .await + .err() + .unwrap(); + assert_eq!(error.code, ErrorCode::RecordNotFound.value()); + + let _sign_up_context = test.sign_up_as_guest().await; + // assert_eq!( + // test + // .user_session + // .sign_in_history(sign_up_context.user_profile.id) + // .len(), + // 3 + // ); + let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + test + .create_view(&new_workspace.id, "new workspace child view".to_string()) + .await; + let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + assert_eq!(new_workspace.views.len(), 2); + + // upload to cloud user with given uuid. This time the workspace of the guest will not be merged + // because the cloud user already has a workspace + test.supabase_party_sign_up(&uuid).await; + // assert_eq!(test.user_session.sign_in_history().len(), 3); + let new_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + let new_cloud_views = test + .folder_manager + .get_current_workspace_views() + .await + .unwrap(); + assert_eq!(new_cloud_workspace, old_cloud_workspace); + assert_eq!(new_cloud_views, old_cloud_views); + } +} + #[tokio::test] async fn check_not_exist_user_test() { if let Some(test) = FlowySupabaseTest::new() { diff --git a/frontend/rust-lib/flowy-test/tests/util.rs b/frontend/rust-lib/flowy-test/tests/util.rs index 8431edd35d..675d008c65 100644 --- a/frontend/rust-lib/flowy-test/tests/util.rs +++ b/frontend/rust-lib/flowy-test/tests/util.rs @@ -4,7 +4,7 @@ use std::time::Duration; use tokio::sync::mpsc::Receiver; use tokio::time::timeout; -use flowy_server::supabase::SupabaseConfiguration; +use flowy_server_config::supabase_config::SupabaseConfiguration; use flowy_test::event_builder::EventBuilder; use flowy_test::FlowyCoreTest; use flowy_user::entities::{ diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index 0cb42cb8f4..3db06ae91c 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -11,8 +11,11 @@ flowy-sqlite = { path = "../flowy-sqlite", optional = true } flowy-error = { path = "../flowy-error", features = ["adaptor_database", "adaptor_dispatch"] } lib-infra = { path = "../../../shared-lib/lib-infra" } flowy-notification = { path = "../flowy-notification" } +flowy-server-config = { path = "../flowy-server-config" } lib-dispatch = { path = "../lib-dispatch" } appflowy-integrate = { version = "0.1.0" } +collab = { version = "0.1.0" } +collab-folder = { version = "0.1.0" } tracing = { version = "0.1", features = ["log"] } bytes = "1.4" @@ -32,6 +35,7 @@ tokio = { version = "1.26", features = ["rt"] } validator = "0.16.0" unicode-segmentation = "1.10" fancy-regex = "0.11.0" +uuid = { version = "1.3.3", features = [ "v4"] } [dev-dependencies] nanoid = "0.4.0" diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index ec1b96598f..eb4d2d066c 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -23,6 +23,10 @@ pub struct SignInPayloadPB { #[pb(index = 4)] pub auth_type: AuthTypePB, + + // Only used in local sign in. + #[pb(index = 5, one_of)] + pub uid: Option, } impl TryInto for SignInPayloadPB { @@ -37,6 +41,7 @@ impl TryInto for SignInPayloadPB { password: password.0, name: self.name, auth_type: self.auth_type.into(), + uid: self.uid, }) } } @@ -78,6 +83,8 @@ pub struct SignInParams { pub password: String, pub name: String, pub auth_type: AuthType, + // Currently, the uid only used in local sign in. + pub uid: Option, } #[derive(Debug, Default, Serialize, Deserialize, Clone)] @@ -121,7 +128,7 @@ pub struct ThirdPartyAuthPB { pub auth_type: AuthTypePB, } -#[derive(ProtoBuf_Enum, Debug, Clone)] +#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] pub enum AuthTypePB { Local = 0, SelfHosted = 1, @@ -143,6 +150,7 @@ pub struct UserProfile { pub icon_url: String, pub openai_key: String, pub workspace_id: String, + pub auth_type: AuthType, } #[derive(Serialize, Deserialize, Default, Clone, Debug)] @@ -191,12 +199,6 @@ impl UpdateUserProfileParams { } } -#[derive(ProtoBuf, Default)] -pub struct SignOutPB { - #[pb(index = 1)] - pub auth_type: AuthTypePB, -} - #[derive(Debug, ProtoBuf, Default)] pub struct UserCredentialsPB { #[pb(index = 1, one_of)] @@ -240,3 +242,9 @@ impl From for UserCredentials { Self::new(value.token, value.uid, value.uuid) } } + +#[derive(Default, ProtoBuf)] +pub struct UserStatePB { + #[pb(index = 1)] + pub auth_type: AuthTypePB, +} diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index acec2f0d8d..c684fe7bce 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -18,7 +18,7 @@ pub struct UserSettingPB { pub(crate) user_folder: String, } -#[derive(ProtoBuf, Default, Debug, PartialEq, Eq, Clone)] +#[derive(ProtoBuf, Default, Eq, PartialEq, Debug, Clone)] pub struct UserProfilePB { #[pb(index = 1)] pub id: i64, @@ -37,6 +37,9 @@ pub struct UserProfilePB { #[pb(index = 6)] pub openai_key: String, + + #[pb(index = 7)] + pub auth_type: AuthTypePB, } impl std::convert::From for UserProfilePB { @@ -48,6 +51,7 @@ impl std::convert::From for UserProfilePB { token: user_profile.token, icon_url: user_profile.icon_url, openai_key: user_profile.openai_key, + auth_type: user_profile.auth_type.into(), } } } diff --git a/frontend/rust-lib/flowy-user/src/entities/user_setting.rs b/frontend/rust-lib/flowy-user/src/entities/user_setting.rs index e2fe152cf5..436e6de21a 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_setting.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_setting.rs @@ -1,6 +1,11 @@ -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::convert::TryFrom; + +use serde::{Deserialize, Serialize}; + +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::FlowyError; +use flowy_server_config::supabase_config::{PostgresConfiguration, SupabaseConfiguration}; #[derive(ProtoBuf, Default, Debug, Clone)] pub struct UserPreferencesPB { @@ -97,3 +102,82 @@ impl std::default::Default for AppearanceSettingsPB { } } } + +#[derive(Default, ProtoBuf)] +pub struct SupabaseConfigPB { + #[pb(index = 1)] + supabase_url: String, + + #[pb(index = 2)] + key: String, + + #[pb(index = 3)] + jwt_secret: String, + + #[pb(index = 4)] + pub postgres_config: PostgresConfigurationPB, + + #[pb(index = 5)] + enable_sync: bool, +} + +impl TryFrom for SupabaseConfiguration { + type Error = FlowyError; + + fn try_from(config: SupabaseConfigPB) -> Result { + let postgres_config = PostgresConfiguration::try_from(config.postgres_config)?; + Ok(SupabaseConfiguration { + url: config.supabase_url, + key: config.key, + jwt_secret: config.jwt_secret, + enable_sync: config.enable_sync, + postgres_config, + }) + } +} + +impl From for SupabaseConfigPB { + fn from(value: SupabaseConfiguration) -> Self { + let postgres_config = PostgresConfigurationPB { + url: value.postgres_config.url, + user_name: value.postgres_config.user_name, + password: value.postgres_config.password, + port: value.postgres_config.port as u32, + }; + Self { + supabase_url: value.url, + key: value.key, + jwt_secret: value.jwt_secret, + postgres_config, + enable_sync: value.enable_sync, + } + } +} + +#[derive(Default, ProtoBuf)] +pub struct PostgresConfigurationPB { + #[pb(index = 1)] + pub url: String, + + #[pb(index = 2)] + pub user_name: String, + + #[pb(index = 3)] + pub password: String, + + #[pb(index = 4)] + pub port: u32, +} + +impl TryFrom for PostgresConfiguration { + type Error = FlowyError; + + fn try_from(config: PostgresConfigurationPB) -> Result { + Ok(Self { + url: config.url, + user_name: config.user_name, + password: config.password, + port: config.port as u16, + }) + } +} diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 77f5cc9c21..b38b197088 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -1,14 +1,15 @@ +use std::convert::TryFrom; use std::{convert::TryInto, sync::Arc}; use flowy_error::FlowyError; +use flowy_server_config::supabase_config::SupabaseConfiguration; use flowy_sqlite::kv::KV; use lib_dispatch::prelude::*; use lib_infra::box_any::BoxAny; use crate::entities::*; use crate::entities::{SignInParams, SignUpParams, UpdateUserProfileParams}; -use crate::event_map::UserCredentials; -use crate::services::{AuthType, UserSession}; +use crate::services::{get_supabase_config, AuthType, UserSession}; #[tracing::instrument(level = "debug", name = "sign_in", skip(data, session), fields(email = %data.email), err)] pub async fn sign_in( @@ -17,9 +18,10 @@ pub async fn sign_in( ) -> DataResult { let params: SignInParams = data.into_inner().try_into()?; let auth_type = params.auth_type.clone(); + session.update_auth_type(&auth_type).await; let user_profile: UserProfilePB = session - .sign_in(&auth_type, BoxAny::new(params)) + .sign_in(BoxAny::new(params), auth_type) .await? .into(); data_result_ok(user_profile) @@ -41,11 +43,10 @@ pub async fn sign_up( ) -> DataResult { let params: SignUpParams = data.into_inner().try_into()?; let auth_type = params.auth_type.clone(); - let user_profile: UserProfilePB = session - .sign_up(&auth_type, BoxAny::new(params)) - .await? - .into(); - data_result_ok(user_profile) + session.update_auth_type(&auth_type).await; + + let user_profile = session.sign_up(auth_type, BoxAny::new(params)).await?; + data_result_ok(user_profile.into()) } #[tracing::instrument(level = "debug", skip(session))] @@ -56,11 +57,9 @@ pub async fn init_user_handler(session: AFPluginState>) -> Resu #[tracing::instrument(level = "debug", skip(session))] pub async fn check_user_handler( - data: AFPluginData, session: AFPluginState>, ) -> Result<(), FlowyError> { - let credential = UserCredentials::from(data.into_inner()); - session.check_user(credential).await?; + session.check_user().await?; Ok(()) } @@ -68,17 +67,14 @@ pub async fn check_user_handler( pub async fn get_user_profile_handler( session: AFPluginState>, ) -> DataResult { - let user_profile: UserProfilePB = session.get_user_profile().await?.into(); + let uid = session.get_session()?.user_id; + let user_profile: UserProfilePB = session.get_user_profile(uid, true).await?.into(); data_result_ok(user_profile) } -#[tracing::instrument(level = "debug", skip(data, session))] -pub async fn sign_out( - data: AFPluginData, - session: AFPluginState>, -) -> Result<(), FlowyError> { - let auth_type: AuthType = data.into_inner().auth_type.into(); - session.sign_out(&auth_type).await?; +#[tracing::instrument(level = "debug", skip(session))] +pub async fn sign_out(session: AFPluginState>) -> Result<(), FlowyError> { + session.sign_out().await?; Ok(()) } @@ -144,9 +140,25 @@ pub async fn third_party_auth_handler( ) -> DataResult { let params = data.into_inner(); let auth_type: AuthType = params.auth_type.into(); - let user_profile: UserProfilePB = session - .sign_up(&auth_type, BoxAny::new(params.map)) - .await? - .into(); - data_result_ok(user_profile) + session.update_auth_type(&auth_type).await; + let user_profile = session.sign_up(auth_type, BoxAny::new(params.map)).await?; + data_result_ok(user_profile.into()) +} + +#[tracing::instrument(level = "debug", skip(data, session), err)] +pub async fn set_supabase_config_handler( + data: AFPluginData, + session: AFPluginState>, +) -> Result<(), FlowyError> { + let config = SupabaseConfiguration::try_from(data.into_inner())?; + session.save_supabase_config(config); + Ok(()) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn get_supabase_config_handler( + _session: AFPluginState>, +) -> DataResult { + let config = get_supabase_config().unwrap_or_default(); + data_result_ok(config.into()) } diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 64dee7f5e0..9aa4e891aa 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -1,9 +1,11 @@ use std::sync::Arc; +use collab_folder::core::FolderData; use strum_macros::Display; use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use flowy_error::FlowyResult; +use flowy_server_config::supabase_config::SupabaseConfiguration; use lib_dispatch::prelude::*; use lib_infra::box_any::BoxAny; use lib_infra::future::{to_fut, Fut, FutureResult}; @@ -27,36 +29,38 @@ pub fn init(user_session: Arc) -> AFPlugin { .event(UserEvent::SetAppearanceSetting, set_appearance_setting) .event(UserEvent::GetAppearanceSetting, get_appearance_setting) .event(UserEvent::GetUserSetting, get_user_setting) + .event(UserEvent::SetSupabaseConfig, set_supabase_config_handler) + .event(UserEvent::GetSupabaseConfig, get_supabase_config_handler) .event(UserEvent::ThirdPartyAuth, third_party_auth_handler) } -pub(crate) struct DefaultUserStatusCallback; -impl UserStatusCallback for DefaultUserStatusCallback { - fn auth_type_did_changed(&self, _auth_type: AuthType) {} - - fn did_sign_in(&self, _user_id: i64, _workspace_id: &str) -> Fut> { - to_fut(async { Ok(()) }) - } - - fn did_sign_up(&self, _is_new: bool, _user_profile: &UserProfile) -> Fut> { - to_fut(async { Ok(()) }) - } - - fn did_expired(&self, _token: &str, _user_id: i64) -> Fut> { - to_fut(async { Ok(()) }) - } +pub struct SignUpContext { + /// Indicate whether the user is new or not. + pub is_new: bool, + /// If the user is sign in as guest, and the is_new is true, then the folder data will be not + /// None. + pub local_folder: Option, } pub trait UserStatusCallback: Send + Sync + 'static { + /// When the [AuthType] changed, this method will be called. Currently, the auth type + /// will be changed when the user sign in or sign up. fn auth_type_did_changed(&self, auth_type: AuthType); + /// This will be called after the application launches if the user is already signed in. + /// If the user is not signed in, this method will not be called + fn did_init(&self, user_id: i64, workspace_id: &str) -> Fut>; + /// Will be called after the user signed in. fn did_sign_in(&self, user_id: i64, workspace_id: &str) -> Fut>; - fn did_sign_up(&self, is_new: bool, user_profile: &UserProfile) -> Fut>; + /// Will be called after the user signed up. + fn did_sign_up(&self, context: SignUpContext, user_profile: &UserProfile) + -> Fut>; fn did_expired(&self, token: &str, user_id: i64) -> Fut>; } /// The user cloud service provider. /// The provider can be supabase, firebase, aws, or any other cloud service. pub trait UserCloudServiceProvider: Send + Sync + 'static { + fn update_supabase_config(&self, supabase_config: &SupabaseConfiguration); fn set_auth_type(&self, auth_type: AuthType); fn get_auth_service(&self) -> Result, FlowyError>; } @@ -65,6 +69,10 @@ impl UserCloudServiceProvider for Arc where T: UserCloudServiceProvider, { + fn update_supabase_config(&self, supabase_config: &SupabaseConfiguration) { + (**self).update_supabase_config(supabase_config) + } + fn set_auth_type(&self, auth_type: AuthType) { (**self).set_auth_type(auth_type) } @@ -140,6 +148,32 @@ pub trait UserAuthService: Send + Sync { fn check_user(&self, credential: UserCredentials) -> FutureResult<(), FlowyError>; } +/// Acts as a placeholder [UserStatusCallback] for the user session, but does not perform any function +pub(crate) struct DefaultUserStatusCallback; +impl UserStatusCallback for DefaultUserStatusCallback { + fn auth_type_did_changed(&self, _auth_type: AuthType) {} + + fn did_init(&self, _user_id: i64, _workspace_id: &str) -> Fut> { + to_fut(async { Ok(()) }) + } + + fn did_sign_in(&self, _user_id: i64, _workspace_id: &str) -> Fut> { + to_fut(async { Ok(()) }) + } + + fn did_sign_up( + &self, + _context: SignUpContext, + _user_profile: &UserProfile, + ) -> Fut> { + to_fut(async { Ok(()) }) + } + + fn did_expired(&self, _token: &str, _user_id: i64) -> Fut> { + to_fut(async { Ok(()) }) + } +} + #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum UserEvent { @@ -154,7 +188,7 @@ pub enum UserEvent { SignUp = 1, /// Logging out fo an account - #[event(input = "SignOutPB")] + #[event()] SignOut = 2, /// Update the user information @@ -187,4 +221,12 @@ pub enum UserEvent { #[event(input = "ThirdPartyAuthPB", output = "UserProfilePB")] ThirdPartyAuth = 10, + + /// Set the supabase config. It will be written to the environment variables. + /// Check out the `write_to_env` of [SupabaseConfigPB]. + #[event(input = "SupabaseConfigPB")] + SetSupabaseConfig = 13, + + #[event(output = "SupabaseConfigPB")] + GetSupabaseConfig = 14, } diff --git a/frontend/rust-lib/flowy-user/src/services/database.rs b/frontend/rust-lib/flowy-user/src/services/database.rs index d65816c304..5084309faa 100644 --- a/frontend/rust-lib/flowy-user/src/services/database.rs +++ b/frontend/rust-lib/flowy-user/src/services/database.rs @@ -7,82 +7,28 @@ use parking_lot::RwLock; use flowy_error::{ErrorCode, FlowyError}; use flowy_sqlite::ConnectionPool; -use flowy_sqlite::{schema::user_table, DBConnection, Database}; +use flowy_sqlite::{ + query_dsl::*, + schema::{user_table, user_table::dsl}, + DBConnection, Database, ExpressionMethods, +}; use crate::entities::{SignInResponse, SignUpResponse, UpdateUserProfileParams, UserProfile}; +use crate::services::AuthType; pub struct UserDB { - db_dir: String, + root: String, } impl UserDB { pub fn new(db_dir: &str) -> Self { Self { - db_dir: db_dir.to_owned(), + root: db_dir.to_owned(), } } - fn open_user_db_if_need(&self, user_id: i64) -> Result, FlowyError> { - if let Some(database) = DB_MAP.read().get(&user_id) { - return Ok(database.get_pool()); - } - - let mut write_guard = DB_MAP.write(); - // The Write guard acquire exclusive access that will guarantee the user db only initialize once. - match write_guard.get(&user_id) { - None => {}, - Some(database) => return Ok(database.get_pool()), - } - - let mut dir = PathBuf::new(); - dir.push(&self.db_dir); - dir.push(user_id.to_string()); - let dir = dir.to_str().unwrap().to_owned(); - - tracing::debug!("open sqlite db {} at path: {}", user_id, dir); - let db = flowy_sqlite::init(&dir).map_err(|e| { - tracing::error!("open user db failed, {:?}", e); - FlowyError::new(ErrorCode::MultipleDBInstance, e) - })?; - let pool = db.get_pool(); - write_guard.insert(user_id.to_owned(), db); - drop(write_guard); - Ok(pool) - } - - fn open_collab_db_if_need(&self, user_id: i64) -> Result, FlowyError> { - if let Some(kv) = COLLAB_DB_MAP.read().get(&user_id) { - return Ok(kv.clone()); - } - - let mut write_guard = COLLAB_DB_MAP.write(); - // The Write guard acquire exclusive access that will guarantee the user db only initialize once. - match write_guard.get(&user_id) { - None => {}, - Some(kv) => return Ok(kv.clone()), - } - - let mut dir = PathBuf::new(); - dir.push(&self.db_dir); - dir.push(user_id.to_string()); - dir.push("collab_db"); - - tracing::trace!("open collab db {} at path: {:?}", user_id, dir); - let db = match RocksCollabDB::open(dir) { - Ok(db) => Ok(db), - Err(err) => { - tracing::error!("open collab db failed, {:?}", err); - Err(FlowyError::new(ErrorCode::MultipleDBInstance, err)) - }, - }?; - - let db = Arc::new(db); - write_guard.insert(user_id.to_owned(), db.clone()); - drop(write_guard); - Ok(db) - } - - pub(crate) fn close_user_db(&self, user_id: i64) -> Result<(), FlowyError> { + /// Close the database connection for the user. + pub(crate) fn close(&self, user_id: i64) -> Result<(), FlowyError> { if let Some(mut sqlite_dbs) = DB_MAP.try_write_for(Duration::from_millis(300)) { sqlite_dbs.remove(&user_id); } @@ -101,16 +47,81 @@ impl UserDB { } pub(crate) fn get_pool(&self, user_id: i64) -> Result, FlowyError> { - let pool = self.open_user_db_if_need(user_id)?; + let pool = open_user_db(&self.root, user_id)?; Ok(pool) } pub(crate) fn get_collab_db(&self, user_id: i64) -> Result, FlowyError> { - let collab_db = self.open_collab_db_if_need(user_id)?; + let collab_db = open_collab_db(&self.root, user_id)?; Ok(collab_db) } } +pub fn open_user_db(root: &str, user_id: i64) -> Result, FlowyError> { + if let Some(database) = DB_MAP.read().get(&user_id) { + return Ok(database.get_pool()); + } + + let mut write_guard = DB_MAP.write(); + let dir = user_db_path_from_uid(root, user_id); + tracing::debug!("open sqlite db {} at path: {:?}", user_id, dir); + let db = flowy_sqlite::init(&dir) + .map_err(|e| FlowyError::internal().context(format!("open user db failed, {:?}", e)))?; + let pool = db.get_pool(); + write_guard.insert(user_id.to_owned(), db); + drop(write_guard); + Ok(pool) +} + +pub fn get_user_profile(pool: &Arc, uid: i64) -> Result { + let uid = uid.to_string(); + let conn = pool.get()?; + let user = dsl::user_table + .filter(user_table::id.eq(&uid)) + .first::(&*conn)?; + + Ok(user.into()) +} + +pub fn user_db_path_from_uid(root: &str, uid: i64) -> PathBuf { + let mut dir = PathBuf::new(); + dir.push(root); + dir.push(uid.to_string()); + dir +} + +/// Open a collab db for the user. If the db is already opened, return the opened db. +/// +pub fn open_collab_db(root: &str, uid: i64) -> Result, FlowyError> { + if let Some(collab_db) = COLLAB_DB_MAP.read().get(&uid) { + return Ok(collab_db.clone()); + } + + let mut write_guard = COLLAB_DB_MAP.write(); + let dir = collab_db_path_from_uid(root, uid); + tracing::trace!("open collab db {} at path: {:?}", uid, dir); + let db = match RocksCollabDB::open(dir) { + Ok(db) => Ok(db), + Err(err) => { + tracing::error!("open collab db failed, {:?}", err); + Err(FlowyError::new(ErrorCode::MultipleDBInstance, err)) + }, + }?; + + let db = Arc::new(db); + write_guard.insert(uid.to_owned(), db.clone()); + drop(write_guard); + Ok(db) +} + +pub fn collab_db_path_from_uid(root: &str, uid: i64) -> PathBuf { + let mut dir = PathBuf::new(); + dir.push(root); + dir.push(uid.to_string()); + dir.push("collab_db"); + dir +} + lazy_static! { static ref DB_MAP: RwLock> = RwLock::new(HashMap::new()); static ref COLLAB_DB_MAP: RwLock>> = RwLock::new(HashMap::new()); @@ -128,29 +139,19 @@ pub struct UserTable { pub(crate) openai_key: String, pub(crate) token: String, pub(crate) email: String, + pub(crate) auth_type: i32, } impl UserTable { - pub fn new(id: String, name: String, email: String, token: String, workspace_id: String) -> Self { - Self { - id, - name, - email, - token, - icon_url: "".to_owned(), - workspace: workspace_id, - openai_key: "".to_owned(), - } - } - pub fn set_workspace(mut self, workspace: String) -> Self { self.workspace = workspace; self } } -impl From for UserTable { - fn from(resp: SignUpResponse) -> Self { +impl From<(SignUpResponse, AuthType)> for UserTable { + fn from(params: (SignUpResponse, AuthType)) -> Self { + let resp = params.0; UserTable { id: resp.user_id.to_string(), name: resp.name, @@ -159,12 +160,15 @@ impl From for UserTable { workspace: resp.workspace_id, icon_url: "".to_string(), openai_key: "".to_string(), + auth_type: params.1 as i32, } } } -impl From for UserTable { - fn from(resp: SignInResponse) -> Self { +impl From<(SignInResponse, AuthType)> for UserTable { + fn from(params: (SignInResponse, AuthType)) -> Self { + let resp = params.0; + let auth_type = params.1; UserTable { id: resp.user_id.to_string(), name: resp.name, @@ -173,6 +177,7 @@ impl From for UserTable { workspace: resp.workspace_id, icon_url: "".to_string(), openai_key: "".to_string(), + auth_type: auth_type as i32, } } } @@ -187,6 +192,7 @@ impl From for UserProfile { icon_url: table.icon_url, openai_key: table.openai_key, workspace_id: table.workspace, + auth_type: AuthType::from(table.auth_type), } } } @@ -213,4 +219,15 @@ impl UserTableChangeset { openai_key: params.openai_key, } } + + pub fn from_user_profile(user_profile: UserProfile) -> Self { + UserTableChangeset { + id: user_profile.id.to_string(), + workspace: None, + name: Some(user_profile.name), + email: Some(user_profile.email), + icon_url: Some(user_profile.icon_url), + openai_key: Some(user_profile.openai_key), + } + } } diff --git a/frontend/rust-lib/flowy-user/src/services/mod.rs b/frontend/rust-lib/flowy-user/src/services/mod.rs index 11b6128fd7..428866a33a 100644 --- a/frontend/rust-lib/flowy-user/src/services/mod.rs +++ b/frontend/rust-lib/flowy-user/src/services/mod.rs @@ -1,3 +1,5 @@ -pub mod database; -mod user_session; pub use user_session::*; + +pub mod database; +mod user_data; +mod user_session; diff --git a/frontend/rust-lib/flowy-user/src/services/user_data.rs b/frontend/rust-lib/flowy-user/src/services/user_data.rs new file mode 100644 index 0000000000..4d3d9dbe25 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/services/user_data.rs @@ -0,0 +1,91 @@ +use std::sync::Arc; + +use appflowy_integrate::{RocksCollabDB, YrsDocAction}; +use collab::core::collab::MutexCollab; +use collab::core::origin::{CollabClient, CollabOrigin}; +use collab::preclude::Collab; + +use collab_folder::core::{Folder, FolderData}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; + +pub struct UserDataMigration(); + +impl UserDataMigration { + pub fn migration( + old_uid: i64, + old_collab_db: &Arc, + old_workspace_id: &str, + new_uid: i64, + new_collab_db: &Arc, + new_workspace_id: &str, + ) -> 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_uid, &object_id) { + // If the object is a folder, migrate the folder data + if object_id == old_workspace_id { + let origin = CollabOrigin::Client(CollabClient::new(old_uid, "")); + if let Ok(old_folder_collab) = + Collab::new_with_raw_data(origin, &object_id, updates, vec![]) + { + let mutex_collab = Arc::new(MutexCollab::from_collab(old_folder_collab)); + let old_folder = Folder::open(mutex_collab, None); + folder_data = migrate_folder(new_workspace_id, old_folder); + } + } else { + let origin = CollabOrigin::Client(CollabClient::new(new_uid, "")); + 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), + } + } + } + } + } + Ok(()) + }) + .map_err(|err| FlowyError::new(ErrorCode::Internal, err))?; + Ok(folder_data) + } +} + +fn migrate_folder(new_workspace_id: &str, old_folder: Folder) -> Option { + 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) +} + +// fn open_collab_db(uid: i64, root: String) -> FlowyResult { +// let dir = collab_db_path_from_uid(&root, uid); +// RocksCollabDB::open(dir).map_err(|err| FlowyError::new(ErrorCode::Internal, err)) +// } diff --git a/frontend/rust-lib/flowy-user/src/services/user_session.rs b/frontend/rust-lib/flowy-user/src/services/user_session.rs index 2978db0879..141120d40b 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_session.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_session.rs @@ -1,17 +1,22 @@ +use std::collections::HashMap; +use std::str::FromStr; use std::sync::Arc; use appflowy_integrate::RocksCollabDB; +use collab_folder::core::FolderData; use serde::{Deserialize, Serialize}; use serde_repr::*; use tokio::sync::RwLock; +use uuid::Uuid; -use flowy_error::internal_error; +use flowy_error::{internal_error, ErrorCode}; +use flowy_server_config::supabase_config::SupabaseConfiguration; use flowy_sqlite::ConnectionPool; use flowy_sqlite::{ kv::KV, query_dsl::*, schema::{user_table, user_table::dsl}, - DBConnection, ExpressionMethods, UserDatabaseConnection, + DBConnection, ExpressionMethods, }; use lib_infra::box_any::BoxAny; @@ -20,15 +25,17 @@ use crate::entities::{ }; use crate::entities::{UserProfilePB, UserSettingPB}; use crate::event_map::{ - DefaultUserStatusCallback, UserCloudServiceProvider, UserCredentials, UserStatusCallback, + DefaultUserStatusCallback, SignUpContext, UserCloudServiceProvider, UserCredentials, + UserStatusCallback, }; +use crate::services::user_data::UserDataMigration; use crate::{ errors::FlowyError, - event_map::UserAuthService, notification::*, services::database::{UserDB, UserTable, UserTableChangeset}, }; +pub(crate) const SUPABASE_CONFIG_CACHE_KEY: &str = "supabase_config_cache_key"; pub struct UserSessionConfig { root_dir: String, @@ -73,16 +80,18 @@ impl UserSession { pub async fn init(&self, user_status_callback: C) { if let Ok(session) = self.get_session() { - let _ = user_status_callback - .did_sign_in(session.user_id, &session.workspace_id) - .await; + if let Err(e) = user_status_callback + .did_init(session.user_id, &session.workspace_id) + .await + { + tracing::error!("Failed to call did_sign_in callback: {:?}", e); + } } *self.user_status_callback.write().await = Arc::new(user_status_callback); } - pub fn db_connection(&self) -> Result { - let user_id = self.get_session()?.user_id; - self.database.get_connection(user_id) + pub fn db_connection(&self, uid: i64) -> Result { + self.database.get_connection(uid) } // The caller will be not 'Sync' before of the return value, @@ -91,29 +100,44 @@ impl UserSession { // // let pool = self.db_connection_pool()?; // let conn: PooledConnection = pool.get()?; - pub fn db_pool(&self) -> Result, FlowyError> { - let user_id = self.get_session()?.user_id; - self.database.get_pool(user_id) + pub fn db_pool(&self, uid: i64) -> Result, FlowyError> { + self.database.get_pool(uid) } - pub fn get_collab_db(&self) -> Result, FlowyError> { - let user_id = self.get_session()?.user_id; - self.database.get_collab_db(user_id) + pub fn get_collab_db(&self, uid: i64) -> Result, FlowyError> { + self.database.get_collab_db(uid) + } + + pub async fn migrate_old_user_data( + &self, + old_uid: i64, + old_workspace_id: &str, + new_uid: i64, + new_workspace_id: &str, + ) -> Result, FlowyError> { + let old_collab_db = self.database.get_collab_db(old_uid)?; + let new_collab_db = self.database.get_collab_db(new_uid)?; + let folder_data = UserDataMigration::migration( + old_uid, + &old_collab_db, + old_workspace_id, + new_uid, + &new_collab_db, + new_workspace_id, + )?; + Ok(folder_data) + } + + pub fn clear_old_user(&self, old_uid: i64) { + let _ = self.database.close(old_uid); } #[tracing::instrument(level = "debug", skip(self, params))] pub async fn sign_in( &self, - auth_type: &AuthType, params: BoxAny, + auth_type: AuthType, ) -> Result { - self - .user_status_callback - .read() - .await - .auth_type_did_changed(auth_type.clone()); - - self.cloud_services.set_auth_type(auth_type.clone()); let resp = self .cloud_services .get_auth_service()? @@ -121,14 +145,18 @@ impl UserSession { .await?; let session: Session = resp.clone().into(); + let uid = session.user_id; self.set_session(Some(session))?; - let user_profile: UserProfile = self.save_user(resp.into()).await?.into(); - let _ = self + let user_profile: UserProfile = self.save_user(uid, (resp, auth_type).into()).await?.into(); + if let Err(e) = self .user_status_callback .read() .await .did_sign_in(user_profile.id, &user_profile.workspace_id) - .await; + .await + { + tracing::error!("Failed to call did_sign_in callback: {:?}", e); + } send_sign_in_notification() .payload::(user_profile.clone().into()) .send(); @@ -136,12 +164,7 @@ impl UserSession { Ok(user_profile) } - #[tracing::instrument(level = "debug", skip(self, params))] - pub async fn sign_up( - &self, - auth_type: &AuthType, - params: BoxAny, - ) -> Result { + pub async fn update_auth_type(&self, auth_type: &AuthType) { self .user_status_callback .read() @@ -149,42 +172,86 @@ impl UserSession { .auth_type_did_changed(auth_type.clone()); self.cloud_services.set_auth_type(auth_type.clone()); - let auth_service = self.cloud_services.get_auth_service()?; - let resp = auth_service.sign_up(params).await?; + } - let is_new = resp.is_new; - let session: Session = resp.clone().into(); + #[tracing::instrument(level = "debug", skip(self, params))] + pub async fn sign_up( + &self, + auth_type: AuthType, + params: BoxAny, + ) -> Result { + let old_user_profile = { + if let Ok(old_session) = self.get_session() { + self.get_user_profile(old_session.user_id, false).await.ok() + } else { + None + } + }; + + let auth_service = self.cloud_services.get_auth_service()?; + let response: SignUpResponse = auth_service.sign_up(params).await?; + let mut sign_up_context = SignUpContext { + is_new: response.is_new, + local_folder: None, + }; + let session = Session { + user_id: response.user_id, + workspace_id: response.workspace_id.clone(), + }; + let uid = session.user_id; self.set_session(Some(session))?; - let user_table = self.save_user(resp.into()).await?; - let user_profile: UserProfile = user_table.into(); + let user_table = self + .save_user(uid, (response, auth_type.clone()).into()) + .await?; + let new_user_profile: UserProfile = user_table.into(); + + // Only migrate the data if the user is login in as a guest and sign up as a new user + if sign_up_context.is_new { + if let Some(old_user_profile) = old_user_profile { + if old_user_profile.auth_type == AuthType::Local && !auth_type.is_local() { + tracing::info!( + "Migrate old user data from {:?} to {:?}", + old_user_profile.id, + new_user_profile.id + ); + match self + .migrate_old_user_data( + old_user_profile.id, + &old_user_profile.workspace_id, + new_user_profile.id, + &new_user_profile.workspace_id, + ) + .await + { + Ok(folder_data) => sign_up_context.local_folder = folder_data, + Err(e) => tracing::error!("{:?}", e), + } + } + } + } + let _ = self .user_status_callback .read() .await - .did_sign_up(is_new, &user_profile) + .did_sign_up(sign_up_context, &new_user_profile) .await; - Ok(user_profile) + Ok(new_user_profile) } #[tracing::instrument(level = "debug", skip(self))] - pub async fn sign_out(&self, auth_type: &AuthType) -> Result<(), FlowyError> { + pub async fn sign_out(&self) -> Result<(), FlowyError> { let session = self.get_session()?; - let uid = session.user_id.to_string(); - let _ = diesel::delete(dsl::user_table.filter(dsl::id.eq(&uid))) - .execute(&*(self.db_connection()?))?; - - self.database.close_user_db(session.user_id)?; + self.database.close(session.user_id)?; self.set_session(None)?; let server = self.cloud_services.get_auth_service()?; - let token = session.token; tokio::spawn(async move { - match server.sign_out(token).await { + match server.sign_out(None).await { Ok(_) => {}, Err(e) => tracing::error!("Sign out failed: {:?}", e), } }); - Ok(()) } @@ -196,9 +263,14 @@ impl UserSession { let auth_type = params.auth_type.clone(); let session = self.get_session()?; let changeset = UserTableChangeset::new(params.clone()); - diesel_update_table!(user_table, changeset, &*self.db_connection()?); + diesel_update_table!( + user_table, + changeset, + &*self.db_connection(session.user_id)? + ); - let user_profile = self.get_user_profile().await?; + let session = self.get_session()?; + let user_profile = self.get_user_profile(session.user_id, false).await?; let profile_pb: UserProfilePB = user_profile.into(); send_notification( &session.user_id.to_string(), @@ -207,7 +279,7 @@ impl UserSession { .payload(profile_pb) .send(); self - .update_user(&auth_type, session.user_id, &session.token, params) + .update_user(&auth_type, session.user_id, None, params) .await?; Ok(()) } @@ -216,17 +288,52 @@ impl UserSession { Ok(()) } - pub async fn check_user(&self, credential: UserCredentials) -> Result<(), FlowyError> { + pub async fn check_user(&self) -> Result<(), FlowyError> { + let user_id = self.get_session()?.user_id; + let credential = UserCredentials::from_uid(user_id); let auth_service = self.cloud_services.get_auth_service()?; auth_service.check_user(credential).await } - pub async fn get_user_profile(&self) -> Result { - let (user_id, _) = self.get_session()?.into_part(); - let user_id = user_id.to_string(); + pub async fn check_user_with_uuid(&self, uuid: &Uuid) -> Result<(), FlowyError> { + let credential = UserCredentials::from_uuid(uuid.to_string()); + let auth_service = self.cloud_services.get_auth_service()?; + auth_service.check_user(credential).await + } + + /// Get the user profile from the database + /// If the refresh is true, it will try to get the user profile from the server + pub async fn get_user_profile(&self, uid: i64, refresh: bool) -> Result { + let user_id = uid.to_string(); let user = dsl::user_table .filter(user_table::id.eq(&user_id)) - .first::(&*(self.db_connection()?))?; + .first::(&*(self.db_connection(uid)?))?; + + if refresh { + let weak_auth_service = Arc::downgrade(&self.cloud_services.get_auth_service()?); + let weak_pool = Arc::downgrade(&self.database.get_pool(uid)?); + tokio::spawn(async move { + if let (Some(auth_service), Some(pool)) = (weak_auth_service.upgrade(), weak_pool.upgrade()) + { + if let Ok(Some(user_profile)) = auth_service + .get_user_profile(UserCredentials::from_uid(uid)) + .await + { + let changeset = UserTableChangeset::from_user_profile(user_profile.clone()); + if let Ok(conn) = pool.get() { + let filter = dsl::user_table.filter(dsl::id.eq(changeset.id.clone())); + let _ = diesel::update(filter).set(changeset).execute(&*conn); + + // Send notification to the client + let user_profile_pb: UserProfilePB = user_profile.into(); + send_notification(&uid.to_string(), UserNotification::DidUpdateUserProfile) + .payload(user_profile_pb) + .send(); + } + } + } + }); + } Ok(user.into()) } @@ -250,21 +357,28 @@ impl UserSession { Ok(self.get_session()?.user_id) } - pub fn user_name(&self) -> Result { - Ok(self.get_session()?.name) + pub fn token(&self) -> Result, FlowyError> { + Ok(None) } - pub fn token(&self) -> Result, FlowyError> { - Ok(self.get_session()?.token) + pub fn save_supabase_config(&self, config: SupabaseConfiguration) { + self.cloud_services.update_supabase_config(&config); + let _ = KV::set_object(SUPABASE_CONFIG_CACHE_KEY, config); } } +pub fn get_supabase_config() -> Option { + KV::get_str(SUPABASE_CONFIG_CACHE_KEY) + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_else(|| SupabaseConfiguration::from_env().ok()) +} + impl UserSession { async fn update_user( &self, _auth_type: &AuthType, uid: i64, - token: &Option, + token: Option, params: UpdateUserProfileParams, ) -> Result<(), FlowyError> { let server = self.cloud_services.get_auth_service()?; @@ -282,8 +396,8 @@ impl UserSession { Ok(()) } - async fn save_user(&self, user: UserTable) -> Result { - let conn = self.db_connection()?; + async fn save_user(&self, uid: i64, user: UserTable) -> Result { + let conn = self.db_connection(uid)?; conn.immediate_transaction(|| { // delete old user if exists diesel::delete(dsl::user_table.filter(dsl::id.eq(&user.id))).execute(&*conn)?; @@ -309,77 +423,47 @@ impl UserSession { Ok(()) } - fn get_session(&self) -> Result { + /// Returns the current user session. + pub fn get_session(&self) -> Result { match KV::get_object::(&self.session_config.session_cache_key) { - None => Err(FlowyError::unauthorized()), + None => Err(FlowyError::new( + ErrorCode::RecordNotFound, + "User is not logged in".to_string(), + )), Some(session) => Ok(session), } } -} -pub async fn update_user( - _cloud_service: Arc, - pool: Arc, - params: UpdateUserProfileParams, -) -> Result<(), FlowyError> { - let changeset = UserTableChangeset::new(params); - let conn = pool.get()?; - diesel_update_table!(user_table, changeset, &*conn); - Ok(()) -} - -impl UserDatabaseConnection for UserSession { - fn get_connection(&self) -> Result { - self.db_connection().map_err(|e| format!("{:?}", e)) + pub fn sign_in_history(&self) -> Vec { + // match self.db_connection(uid) { + // Ok(conn) => match dsl::user_table.load::(&*conn) { + // Ok(users) => users.into_iter().map(|u| u.into()).collect(), + // Err(_) => vec![], + // }, + // Err(e) => { + // tracing::error!("get user sign in history failed: {:?}", e); + // vec![] + // }, + // } + vec![] } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] -struct Session { - user_id: i64, - - workspace_id: String, - - #[serde(default)] - name: String, - - #[serde(default)] - token: Option, - - #[serde(default)] - email: Option, +pub struct Session { + pub user_id: i64, + pub workspace_id: String, } impl std::convert::From for Session { fn from(resp: SignInResponse) -> Self { Session { user_id: resp.user_id, - token: resp.token, - email: resp.email, - name: resp.name, workspace_id: resp.workspace_id, } } } -impl std::convert::From for Session { - fn from(resp: SignUpResponse) -> Self { - Session { - user_id: resp.user_id, - token: resp.token, - email: resp.email, - name: resp.name, - workspace_id: resp.workspace_id, - } - } -} - -impl Session { - pub fn into_part(self) -> (i64, Option) { - (self.user_id, self.token) - } -} - impl std::convert::From for Session { fn from(s: String) -> Self { match serde_json::from_str(&s) { @@ -415,6 +499,12 @@ pub enum AuthType { Supabase = 2, } +impl AuthType { + pub fn is_local(&self) -> bool { + matches!(self, AuthType::Local) + } +} + impl Default for AuthType { fn default() -> Self { Self::Local @@ -430,3 +520,44 @@ impl From for AuthType { } } } + +impl From for AuthTypePB { + fn from(auth_type: AuthType) -> Self { + match auth_type { + AuthType::Supabase => AuthTypePB::Supabase, + AuthType::Local => AuthTypePB::Local, + AuthType::SelfHosted => AuthTypePB::SelfHosted, + } + } +} + +impl From for AuthType { + fn from(value: i32) -> Self { + match value { + 0 => AuthType::Local, + 1 => AuthType::SelfHosted, + 2 => AuthType::Supabase, + _ => AuthType::Local, + } + } +} + +pub struct ThirdPartyParams { + pub uuid: Uuid, + pub email: String, +} + +pub fn uuid_from_box_any(any: BoxAny) -> Result { + let map: HashMap = any.unbox_or_error()?; + let uuid = uuid_from_map(&map)?; + let email = map.get("email").cloned().unwrap_or_default(); + Ok(ThirdPartyParams { uuid, email }) +} + +pub fn uuid_from_map(map: &HashMap) -> Result { + let uuid = map + .get("uuid") + .ok_or_else(|| FlowyError::new(ErrorCode::MissingAuthField, "Missing uuid field"))? + .as_str(); + Uuid::from_str(uuid).map_err(internal_error) +} diff --git a/frontend/rust-lib/lib-log/src/lib.rs b/frontend/rust-lib/lib-log/src/lib.rs index a69f552d93..86203e6378 100644 --- a/frontend/rust-lib/lib-log/src/lib.rs +++ b/frontend/rust-lib/lib-log/src/lib.rs @@ -4,9 +4,12 @@ use lazy_static::lazy_static; use log::LevelFilter; use tracing::subscriber::set_global_default; use tracing_appender::{non_blocking::WorkerGuard, rolling::RollingFileAppender}; +use tracing_bunyan_formatter::JsonStorageLayer; use tracing_log::LogTracer; use tracing_subscriber::{layer::SubscriberExt, EnvFilter}; +use crate::layer::FlowyFormattingLayer; + mod layer; lazy_static! { static ref LOG_GUARD: RwLock> = RwLock::new(None); @@ -39,7 +42,7 @@ impl Builder { pub fn build(self) -> std::result::Result<(), String> { let env_filter = EnvFilter::new(self.env_filter); - let (_non_blocking, guard) = tracing_appender::non_blocking(self.file_appender); + let (non_blocking, guard) = tracing_appender::non_blocking(self.file_appender); let subscriber = tracing_subscriber::fmt() .with_ansi(true) .with_target(true) @@ -51,10 +54,10 @@ impl Builder { .with_span_list(true) .compact() .finish() - .with(env_filter); - // .with(JsonStorageLayer) - // .with(FlowyFormattingLayer::new(std::io::stdout)) - // .with(FlowyFormattingLayer::new(non_blocking)); + .with(env_filter) + .with(JsonStorageLayer) + .with(FlowyFormattingLayer::new(std::io::stdout)) + .with(FlowyFormattingLayer::new(non_blocking)); set_global_default(subscriber).map_err(|e| format!("{:?}", e))?; LogTracer::builder()