diff --git a/frontend/.vscode/tasks.json b/frontend/.vscode/tasks.json index d11a08c3e3..321db03922 100644 --- a/frontend/.vscode/tasks.json +++ b/frontend/.vscode/tasks.json @@ -234,5 +234,13 @@ "cwd": "${workspaceFolder}/appflowy_tauri" } }, + { + "label": "AF: Generate Env", + "type": "shell", + "command": "dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs ", + "options": { + "cwd": "${workspaceFolder}/appflowy_flutter" + } + }, ] } \ No newline at end of file diff --git a/frontend/appflowy_flutter/.gitignore b/frontend/appflowy_flutter/.gitignore index 504d82329b..52bcfb1538 100644 --- a/frontend/appflowy_flutter/.gitignore +++ b/frontend/appflowy_flutter/.gitignore @@ -70,6 +70,7 @@ windows/flutter/dart_ffi/ **/.vscode/ *.env +*.env.* coverage/ diff --git a/frontend/appflowy_flutter/lib/core/config/config.dart b/frontend/appflowy_flutter/lib/core/config/config.dart index 1c727f78f2..b90f395250 100644 --- a/frontend/appflowy_flutter/lib/core/config/config.dart +++ b/frontend/appflowy_flutter/lib/core/config/config.dart @@ -7,32 +7,24 @@ class Config { 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, + ..jwtSecret = secret + ..postgresConfig = postgresConfig, ).send(); } - - static Future setSupabaseCollabPluginConfig({ - required String url, - required String key, - required String jwtSecret, - required String collabTable, - }) async { - final payload = CollabPluginConfigPB.create(); - final collabTableConfig = CollabTableConfigPB.create() - ..tableName = collabTable; - - payload.supabaseConfig = SupabaseDBConfigPB.create() - ..supabaseUrl = url - ..key = key - ..jwtSecret = jwtSecret - ..collabTableConfig = collabTableConfig; - - await ConfigEventSetCollabPluginConfig(payload).send(); - } } diff --git a/frontend/appflowy_flutter/lib/env/env.dart b/frontend/appflowy_flutter/lib/env/env.dart index c52f2f0b13..d45e6ac1d3 100644 --- a/frontend/appflowy_flutter/lib/env/env.dart +++ b/frontend/appflowy_flutter/lib/env/env.dart @@ -3,6 +3,17 @@ import 'package:envied/envied.dart'; part 'env.g.dart'; +/// The environment variables are defined in `.env` file that is located in the +/// appflowy_flutter. +/// Run `dart run build_runner build --delete-conflicting-outputs` +/// to generate the keys from the env file. +/// +/// If you want to regenerate the keys, you need to run `dart run +/// build_runner clean` before running `dart run build_runner build +/// --delete-conflicting-outputs`. + +/// Follow the guide on https://supabase.com/docs/guides/auth/social-login/auth-google to setup the auth provider. +/// @Envied(path: '.env') abstract class Env { @EnviedField( @@ -32,14 +43,39 @@ abstract class Env { @EnviedField( obfuscate: true, - varName: 'SUPABASE_COLLAB_TABLE', + varName: 'SUPABASE_DB', defaultValue: '', ) - static final String supabaseCollabTable = _Env.supabaseCollabTable; + static final String supabaseDb = _Env.supabaseDb; + + @EnviedField( + obfuscate: true, + varName: 'SUPABASE_DB_USER', + defaultValue: '', + ) + static final String supabaseDbUser = _Env.supabaseDbUser; + + @EnviedField( + obfuscate: true, + varName: 'SUPABASE_DB_PASSWORD', + defaultValue: '', + ) + static final String supabaseDbPassword = _Env.supabaseDbPassword; + + @EnviedField( + obfuscate: true, + varName: 'SUPABASE_DB_PORT', + defaultValue: '5432', + ) + static final String supabaseDbPort = _Env.supabaseDbPort; } bool get isSupabaseEnable => Env.supabaseUrl.isNotEmpty && Env.supabaseAnonKey.isNotEmpty && Env.supabaseKey.isNotEmpty && - Env.supabaseJwtSecret.isNotEmpty; + Env.supabaseJwtSecret.isNotEmpty && + Env.supabaseDb.isNotEmpty && + Env.supabaseDbUser.isNotEmpty && + Env.supabaseDbPassword.isNotEmpty && + Env.supabaseDbPort.isNotEmpty; diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index d31544a4c7..877345123b 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:appflowy/env/env.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; import 'package:flutter/foundation.dart'; @@ -62,13 +61,7 @@ class FlowyRunner { // ignore in test mode if (!mode.isUnitTest) ...[ const HotKeyTask(), - InitSupabaseTask( - url: Env.supabaseUrl, - anonKey: Env.supabaseAnonKey, - key: Env.supabaseKey, - jwtSecret: Env.supabaseJwtSecret, - collabTable: Env.supabaseCollabTable, - ), + InitSupabaseTask(), const InitAppWidgetTask(), const InitPlatformServiceTask() ], diff --git a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart index da2575343d..ec80400beb 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart @@ -23,31 +23,29 @@ class InitRustSDKTask extends LaunchTask { Future initialize(LaunchContext context) async { final dir = directory ?? await appFlowyApplicationDataDirectory(); - context.getIt().setEnv(getAppFlowyEnv()); + final env = getAppFlowyEnv(); + context.getIt().setEnv(env); await context.getIt().init(dir); } } AppFlowyEnv getAppFlowyEnv() { + final postgresConfig = PostgresConfiguration( + url: Env.supabaseDb, + password: Env.supabaseDbPassword, + port: int.parse(Env.supabaseDbPort), + user_name: Env.supabaseDbUser, + ); + final supabaseConfig = SupabaseConfiguration( url: Env.supabaseUrl, key: Env.supabaseKey, jwt_secret: Env.supabaseJwtSecret, - ); - - final collabTableConfig = - CollabTableConfig(enable: true, table_name: Env.supabaseCollabTable); - - final supabaseDBConfig = SupabaseDBConfig( - url: Env.supabaseUrl, - key: Env.supabaseKey, - jwt_secret: Env.supabaseJwtSecret, - collab_table_config: collabTableConfig, + postgres_config: postgresConfig, ); return AppFlowyEnv( supabase_config: supabaseConfig, - supabase_db_config: supabaseDBConfig, ); } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart index f915fd883e..46dfbf191e 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart @@ -1,49 +1,37 @@ import 'package:appflowy/core/config/config.dart'; -import 'package:appflowy_backend/log.dart'; +import 'package:appflowy/env/env.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../startup.dart'; -bool isSupabaseEnable = false; bool isSupabaseInitialized = false; class InitSupabaseTask extends LaunchTask { - const InitSupabaseTask({ - required this.url, - required this.anonKey, - required this.key, - required this.jwtSecret, - this.collabTable = "", - }); - - final String url; - final String anonKey; - final String key; - final String jwtSecret; - final String collabTable; - @override Future initialize(LaunchContext context) async { - if (url.isEmpty || anonKey.isEmpty || jwtSecret.isEmpty || key.isEmpty) { - isSupabaseEnable = false; - Log.info('Supabase config is empty, skip init supabase.'); + if (!isSupabaseEnable) { return; } + if (isSupabaseInitialized) { return; } await Supabase.initialize( - url: url, - anonKey: anonKey, + url: Env.supabaseUrl, + anonKey: Env.supabaseAnonKey, debug: false, ); + await Config.setSupabaseConfig( - url: url, - key: key, - secret: jwtSecret, - anonKey: anonKey, + url: Env.supabaseUrl, + key: Env.supabaseKey, + secret: Env.supabaseJwtSecret, + anonKey: Env.supabaseAnonKey, + pgPassword: Env.supabaseDbPassword, + pgPort: Env.supabaseDbPort, + pgUrl: Env.supabaseDb, + pgUser: Env.supabaseDbUser, ); - isSupabaseEnable = true; isSupabaseInitialized = true; } } diff --git a/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart b/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart index f9629e1547..859f187b46 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart @@ -14,6 +14,6 @@ class AuthError { ..code = -10003; static final supabaseGetUserError = FlowyError() - ..msg = 'supabase sign in with oauth error' - ..code = -10003; + ..msg = 'unable to get user from supabase' + ..code = -10004; } 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 6a28a44981..8b506572d5 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 @@ -1,11 +1,9 @@ import 'dart:async'; -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/core/config/kv_keys.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/startup/tasks/prelude.dart'; +import 'package:appflowy/env/env.dart'; import 'package:appflowy/user/application/auth/appflowy_auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -112,13 +110,12 @@ class SupabaseAuthService implements AuthService { final completer = Completer>(); late final StreamSubscription subscription; subscription = _auth.onAuthStateChange.listen((event) async { - if (event.event != AuthChangeEvent.signedIn) { + final user = event.session?.user; + if (event.event != AuthChangeEvent.signedIn || user == null) { completer.complete(left(AuthError.supabaseSignInWithOauthError)); } else { - final user = await getSupabaseUser(); - final Either response = await user.fold( - (l) => left(l), - (r) async => await setupAuth(map: {AuthServiceMapKeys.uuid: r.id}), + final Either response = await setupAuth( + map: {AuthServiceMapKeys.uuid: user.id}, ); completer.complete(response); } @@ -164,16 +161,21 @@ class SupabaseAuthService implements AuthService { return _appFlowyAuthService.signUpAsGuest(); } + // @override + // Future> getUser() async { + // final loginType = await getIt() + // .get(KVKeys.loginType) + // .then((value) => value.toOption().toNullable()); + // if (!isSupabaseEnable || (loginType != null && loginType != 'supabase')) { + // return _appFlowyAuthService.getUser(); + // } + // final user = await getSupabaseUser(); + // return user.map((r) => r.toUserProfile()); + // } + @override Future> getUser() async { - final loginType = await getIt() - .get(KVKeys.loginType) - .then((value) => value.toOption().toNullable()); - if (!isSupabaseEnable || (loginType != null && loginType != 'supabase')) { - return _appFlowyAuthService.getUser(); - } - final user = await getSupabaseUser(); - return user.map((r) => r.toUserProfile()); + return UserBackendService.getCurrentUserProfile(); } Future> getSupabaseUser() async { @@ -197,14 +199,6 @@ class SupabaseAuthService implements AuthService { } } -extension on User { - UserProfilePB toUserProfile() { - return UserProfilePB() - ..email = email ?? '' - ..token = this.id; - } -} - extension on String { Provider toProvider() { switch (this) { diff --git a/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart index 74870d445a..f586ce4005 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/splash_screen.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/startup/tasks/supabase_task.dart'; +import 'package:appflowy/env/env.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; 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 a2fd1f5f02..f29f6e776e 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 @@ -2,10 +2,12 @@ import 'dart:convert'; import 'dart:async'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/entry_point.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/util/debounce.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/size.dart'; @@ -26,24 +28,27 @@ class SettingsUserView extends StatelessWidget { create: (context) => getIt(param1: user) ..add(const SettingsUserEvent.initial()), child: BlocBuilder( - builder: (context, state) => SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _renderUserNameInput(context), - const VSpace(20), - _renderCurrentIcon(context), - const VSpace(20), - _renderCurrentOpenaiKey(context) - ], - ), + builder: (context, state) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + _renderUserNameInput(context), + const VSpace(20), + _renderCurrentIcon(context), + const VSpace(20), + _renderCurrentOpenaiKey(context), + const Spacer(), + _renderLogoutButton(context), + const VSpace(20), + ], ), ), ); } Widget _renderUserNameInput(BuildContext context) { - final String name = context.read().state.userProfile.name; + final String name = + context.read().state.userProfile.name; return UserNameInput(name); } @@ -61,6 +66,23 @@ class SettingsUserView extends StatelessWidget { context.read().state.userProfile.openaiKey; return _OpenaiKeyInput(openAIKey); } + + Widget _renderLogoutButton(BuildContext context) { + return FlowyButton( + useIntrinsicWidth: true, + text: const FlowyText( + 'Logout', + ), + onTap: () async { + await getIt().signOut(authType: AuthTypePB.Supabase); + await getIt().signOut(authType: AuthTypePB.Local); + await FlowyRunner.run( + FlowyApp(), + integrationEnv(), + ); + }, + ); + } } @visibleForTesting 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 957476dbd2..30cb4b57be 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.dart @@ -1,14 +1,15 @@ import 'package:json_annotation/json_annotation.dart'; -part 'env_serde.l.dart'; +// Run `dart run build_runner build` to generate the json serialization +// 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'; @JsonSerializable() class AppFlowyEnv { final SupabaseConfiguration supabase_config; - final SupabaseDBConfig supabase_db_config; - AppFlowyEnv( - {required this.supabase_config, required this.supabase_db_config}); + AppFlowyEnv({required this.supabase_config}); factory AppFlowyEnv.fromJson(Map json) => _$AppFlowyEnvFromJson(json); @@ -21,9 +22,14 @@ class SupabaseConfiguration { final String url; final String key; final String jwt_secret; + final PostgresConfiguration postgres_config; - SupabaseConfiguration( - {required this.url, required this.key, required this.jwt_secret}); + SupabaseConfiguration({ + required this.url, + required this.key, + required this.jwt_secret, + required this.postgres_config, + }); factory SupabaseConfiguration.fromJson(Map json) => _$SupabaseConfigurationFromJson(json); @@ -32,33 +38,21 @@ class SupabaseConfiguration { } @JsonSerializable() -class SupabaseDBConfig { +class PostgresConfiguration { final String url; - final String key; - final String jwt_secret; - final CollabTableConfig collab_table_config; + final String user_name; + final String password; + final int port; - SupabaseDBConfig( - {required this.url, - required this.key, - required this.jwt_secret, - required this.collab_table_config}); + PostgresConfiguration({ + required this.url, + required this.user_name, + required this.password, + required this.port, + }); - factory SupabaseDBConfig.fromJson(Map json) => - _$SupabaseDBConfigFromJson(json); + factory PostgresConfiguration.fromJson(Map json) => + _$PostgresConfigurationFromJson(json); - Map toJson() => _$SupabaseDBConfigToJson(this); -} - -@JsonSerializable() -class CollabTableConfig { - final String table_name; - final bool enable; - - CollabTableConfig({required this.table_name, required this.enable}); - - factory CollabTableConfig.fromJson(Map json) => - _$CollabTableConfigFromJson(json); - - Map toJson() => _$CollabTableConfigToJson(this); + Map toJson() => _$PostgresConfigurationToJson(this); } diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.l.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.i.dart similarity index 52% rename from frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.l.dart rename to frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.i.dart index 8fe70f71d9..96fe48b08b 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.l.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/env_serde.i.dart @@ -9,14 +9,11 @@ part of 'env_serde.dart'; AppFlowyEnv _$AppFlowyEnvFromJson(Map json) => AppFlowyEnv( supabase_config: SupabaseConfiguration.fromJson( json['supabase_config'] as Map), - supabase_db_config: SupabaseDBConfig.fromJson( - json['supabase_db_config'] as Map), ); Map _$AppFlowyEnvToJson(AppFlowyEnv instance) => { 'supabase_config': instance.supabase_config, - 'supabase_db_config': instance.supabase_db_config, }; SupabaseConfiguration _$SupabaseConfigurationFromJson( @@ -25,6 +22,8 @@ SupabaseConfiguration _$SupabaseConfigurationFromJson( url: json['url'] as String, key: json['key'] as String, jwt_secret: json['jwt_secret'] as String, + postgres_config: PostgresConfiguration.fromJson( + json['postgres_config'] as Map), ); Map _$SupabaseConfigurationToJson( @@ -33,33 +32,23 @@ Map _$SupabaseConfigurationToJson( 'url': instance.url, 'key': instance.key, 'jwt_secret': instance.jwt_secret, + 'postgres_config': instance.postgres_config, }; -SupabaseDBConfig _$SupabaseDBConfigFromJson(Map json) => - SupabaseDBConfig( +PostgresConfiguration _$PostgresConfigurationFromJson( + Map json) => + PostgresConfiguration( url: json['url'] as String, - key: json['key'] as String, - jwt_secret: json['jwt_secret'] as String, - collab_table_config: CollabTableConfig.fromJson( - json['collab_table_config'] as Map), + user_name: json['user_name'] as String, + password: json['password'] as String, + port: json['port'] as int, ); -Map _$SupabaseDBConfigToJson(SupabaseDBConfig instance) => +Map _$PostgresConfigurationToJson( + PostgresConfiguration instance) => { 'url': instance.url, - 'key': instance.key, - 'jwt_secret': instance.jwt_secret, - 'collab_table_config': instance.collab_table_config, - }; - -CollabTableConfig _$CollabTableConfigFromJson(Map json) => - CollabTableConfig( - table_name: json['table_name'] as String, - enable: json['enable'] as bool, - ); - -Map _$CollabTableConfigToJson(CollabTableConfig instance) => - { - 'table_name': instance.table_name, - 'enable': instance.enable, + 'user_name': instance.user_name, + 'password': instance.password, + 'port': instance.port, }; diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index c92f34d3f1..684f8c01b1 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -576,10 +576,10 @@ packages: dependency: transitive description: name: functions_client - sha256: "578537de508c62c2875a6fdaa5dc71033283551ac7a32b8b8ef405c6c5823273" + sha256: "3b157b4d3ae9e38614fd80fab76d1ef1e0e39ff3412a45de2651f27cecb9d2d2" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" get_it: dependency: "direct main" description: @@ -608,10 +608,10 @@ packages: dependency: transitive description: name: gotrue - sha256: "3306606658484a05fc885aea15f9fa65bcc28194f35ef294de3a34d01393b928" + sha256: "214d5050a68ce68a55da1a6d9d7a2e07e039b359f99f1a17ec685320c9101aa6" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.8.4" graphs: dependency: transitive description: @@ -1029,10 +1029,10 @@ packages: dependency: transitive description: name: postgrest - sha256: "42abd4bf3322af3eb0d286ca2fca7cc28baae52b805761dfa7ab0d206ee072a3" + sha256: "78fd180ecd2274df7b04c406746495b5c627248856458f8f537bf5348de9c817" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" process: dependency: transitive description: @@ -1077,10 +1077,10 @@ packages: dependency: transitive description: name: realtime_client - sha256: "13f6a62244bca7562b47658e3f92e5eeeb79a46d58ad4a97ad536e4ba5e97086" + sha256: "0342f73f42345f3547e3cdcc804a0ed108fcd9142d1537d159aead94a213e248" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" reorderables: dependency: "direct main" description: @@ -1362,10 +1362,10 @@ packages: dependency: transitive description: name: storage_client - sha256: e14434a4cc16b01f2e96f3c646e43fb0bb16624b279a65a34da889cffe4b083c + sha256: a3024569213b064587d616827747b766f9bc796e80cec99bd5ffb597b8aeb018 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.1" stream_channel: dependency: transitive description: @@ -1402,18 +1402,18 @@ packages: dependency: transitive description: name: supabase - sha256: "8f89e406d1c0101409a9c5d5560ed391d6d3636d2e077336905f3eee18622073" + sha256: "5f5e47fcac99a496e15274d5f6944e1323519df9f8929b4ab9eef8711abeb5f3" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.4" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: "809c67c296d4a0690fdc8e5f952a5e18b3ebd145867f1cb3f8f80248b22a56ae" + sha256: "1ebe89b83b992123d40dcf5aa88b87d6c2d0a3c62052380cfc94de2337aac469" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.4" sync_http: dependency: transitive description: @@ -1762,10 +1762,10 @@ packages: dependency: transitive description: name: yet_another_json_isolate - sha256: "7809f6517bafd0a7b3d0be63cd5f952ae5c030d682250e8aa9ed7002eaac5ff8" + sha256: "86fad76026c4241a32831d6c7febd8f9bded5019e2cd36c5b148499808d8307d" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" sdks: dart: ">=3.0.0 <4.0.0" flutter: ">=3.10.1" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 7b5a7f835d..2cda3a2c96 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -99,7 +99,7 @@ dependencies: archive: ^3.3.7 flutter_svg: ^2.0.6 nanoid: ^1.0.0 - supabase_flutter: ^1.10.0 + supabase_flutter: ^1.10.4 envied: ^0.3.0+3 dotted_border: ^2.0.0+3 diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 75efed88bf..f9c7001856 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 = "d1882d" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" } -collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" } -appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" } +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 = { path = "../../AppFlowy-Collab/collab" } #collab-folder = { path = "../../AppFlowy-Collab/collab-folder" } diff --git a/frontend/rust-lib/.gitignore b/frontend/rust-lib/.gitignore index 5e19a3e66b..6cb8157cf5 100644 --- a/frontend/rust-lib/.gitignore +++ b/frontend/rust-lib/.gitignore @@ -14,4 +14,5 @@ bin/ **/resources/proto .idea/ AppFlowy-Collab/ -.env \ No newline at end of file +.env +.env.** \ No newline at end of file diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 842e939b6d..8d9ebca719 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -78,14 +78,13 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "appflowy-integrate" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50" dependencies = [ "anyhow", "collab", @@ -112,6 +111,16 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -887,7 +896,6 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50" dependencies = [ "anyhow", "bytes", @@ -905,7 +913,6 @@ dependencies = [ [[package]] name = "collab-client-ws" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50" dependencies = [ "bytes", "collab-sync", @@ -923,7 +930,6 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50" dependencies = [ "anyhow", "async-trait", @@ -942,6 +948,7 @@ dependencies = [ "serde_repr", "thiserror", "tokio", + "tokio-stream", "tracing", "uuid", ] @@ -949,7 +956,6 @@ dependencies = [ [[package]] name = "collab-derive" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50" dependencies = [ "proc-macro2", "quote", @@ -961,7 +967,6 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50" dependencies = [ "anyhow", "collab", @@ -973,13 +978,13 @@ dependencies = [ "serde_json", "thiserror", "tokio", + "tokio-stream", "tracing", ] [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50" dependencies = [ "anyhow", "chrono", @@ -999,7 +1004,6 @@ dependencies = [ [[package]] name = "collab-persistence" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50" dependencies = [ "bincode", "chrono", @@ -1019,7 +1023,6 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50" dependencies = [ "anyhow", "async-trait", @@ -1035,13 +1038,16 @@ dependencies = [ "parking_lot 0.12.1", "postgrest", "rand 0.8.5", + "refinery", "rusoto_credential", "serde", "serde_json", "similar 2.2.1", "thiserror", "tokio", + "tokio-postgres", "tokio-retry", + "tokio-stream", "tracing", "y-sync", "yrs", @@ -1050,7 +1056,6 @@ dependencies = [ [[package]] name = "collab-sync" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d1882d#d1882d6784a8863419727be92c29923cd175fd50" dependencies = [ "bytes", "collab", @@ -1323,6 +1328,40 @@ dependencies = [ "parking_lot_core 0.9.7", ] +[[package]] +name = "deadpool" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "retain_mut", + "tokio", +] + +[[package]] +name = "deadpool-postgres" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a24a9d49deefe610b8b60c767a7412e9a931d79a89415cd2d2d71630ca8d7" +dependencies = [ + "deadpool", + "log", + "tokio", + "tokio-postgres", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaa37046cc0f6c3cc6090fbdbf73ef0b8ef4cfcc37f6befc0020f63e8cf121e1" +dependencies = [ + "tokio", +] + [[package]] name = "derivative" version = "2.2.0" @@ -1456,6 +1495,12 @@ dependencies = [ "regex", ] +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + [[package]] name = "errno" version = "0.3.1" @@ -1506,6 +1551,12 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fancy-regex" version = "0.10.0" @@ -1574,7 +1625,7 @@ dependencies = [ "similar 1.3.0", "syn 1.0.109", "tera", - "toml", + "toml 0.5.11", "walkdir", ] @@ -1649,7 +1700,7 @@ dependencies = [ "flowy-task", "flowy-test", "futures", - "indexmap", + "indexmap 1.9.3", "lazy_static", "lib-dispatch", "lib-infra", @@ -1697,8 +1748,10 @@ dependencies = [ "flowy-derive", "flowy-error", "flowy-notification", - "indexmap", + "futures", + "indexmap 1.9.3", "lib-dispatch", + "lib-infra", "nanoid", "parking_lot 0.12.1", "protobuf", @@ -1708,6 +1761,7 @@ dependencies = [ "strum_macros", "tempfile", "tokio", + "tokio-stream", "tracing", "tracing-subscriber 0.3.16", "uuid", @@ -1800,13 +1854,19 @@ name = "flowy-server" version = "0.1.0" dependencies = [ "anyhow", + "appflowy-integrate", + "async-stream", "bytes", "chrono", "config", + "deadpool-postgres", "dotenv", + "flowy-database2", + "flowy-document2", "flowy-error", "flowy-folder2", "flowy-user", + "futures", "futures-util", "hyper", "lazy_static", @@ -1814,14 +1874,17 @@ dependencies = [ "nanoid", "parking_lot 0.12.1", "postgrest", + "refinery", "reqwest", "serde", "serde-aux", "serde_json", "thiserror", "tokio", + "tokio-postgres", "tokio-retry", "tracing", + "tracing-subscriber 0.3.16", "uuid", ] @@ -1864,7 +1927,13 @@ dependencies = [ name = "flowy-test" version = "0.1.0" dependencies = [ + "anyhow", + "assert-json-diff", "bytes", + "collab", + "collab-database", + "collab-document", + "collab-folder", "dotenv", "flowy-core", "flowy-database2", @@ -2164,7 +2233,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -2189,6 +2258,12 @@ dependencies = [ "ahash 0.8.3", ] +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "hdrhistogram" version = "7.5.2" @@ -2317,7 +2392,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -2437,6 +2512,16 @@ dependencies = [ "serde", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + [[package]] name = "indextree" version = "4.6.0" @@ -2588,7 +2673,7 @@ name = "lib-ot" version = "0.1.0" dependencies = [ "bytes", - "indexmap", + "indexmap 1.9.3", "indextree", "lazy_static", "log", @@ -2774,6 +2859,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest 0.10.6", +] + [[package]] name = "md5" version = "0.7.0" @@ -3305,6 +3399,37 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +[[package]] +name = "postgres-protocol" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b7fa9f396f51dffd61546fd8573ee20592287996568e6175ceb0f8699ad75d" +dependencies = [ + "base64 0.21.0", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.8.5", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f028f05971fe20f512bcc679e2c10227e57809a3af86a7606304435bc8896cd6" +dependencies = [ + "bytes", + "chrono", + "fallible-iterator", + "postgres-protocol", + "uuid", +] + [[package]] name = "postgrest" version = "1.5.0" @@ -3336,7 +3461,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "toml", + "toml 0.5.11", ] [[package]] @@ -3734,6 +3859,51 @@ dependencies = [ "thiserror", ] +[[package]] +name = "refinery" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb0436d0dd7bd8d4fce1e828751fa79742b08e35f27cfea7546f8a322b5ef24" +dependencies = [ + "refinery-core", + "refinery-macros", +] + +[[package]] +name = "refinery-core" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19206547cd047e8f4dfa6b20c30d3ecaf24be05841b6aa0aa926a47a3d0662bb" +dependencies = [ + "async-trait", + "cfg-if", + "lazy_static", + "log", + "regex", + "serde", + "siphasher", + "thiserror", + "time 0.3.21", + "tokio", + "tokio-postgres", + "toml 0.7.5", + "url", + "walkdir", +] + +[[package]] +name = "refinery-macros" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d94d4b9241859ba19eaa5c04c86e782eb3aa0aae2c5868e0cfa90c856e58a174" +dependencies = [ + "proc-macro2", + "quote", + "refinery-core", + "regex", + "syn 2.0.16", +] + [[package]] name = "regex" version = "1.7.3" @@ -3820,6 +3990,12 @@ dependencies = [ "winreg", ] +[[package]] +name = "retain_mut" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" + [[package]] name = "ring" version = "0.16.20" @@ -4143,6 +4319,15 @@ dependencies = [ "syn 2.0.16", ] +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4303,6 +4488,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -4315,6 +4510,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strum" version = "0.21.0" @@ -4489,6 +4694,7 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ + "itoa", "serde", "time-core", "time-macros", @@ -4538,7 +4744,7 @@ dependencies = [ "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.4.9", "tokio-macros", "tracing", "windows-sys 0.45.0", @@ -4575,6 +4781,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-postgres" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e89f6234aa8fd43779746012fcf53603cdb91fdd8399aa0de868c2d56b6dde1" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot 0.12.1", + "percent-encoding", + "phf 0.11.1", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "socket2 0.5.3", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-retry" version = "0.3.0" @@ -4657,6 +4887,40 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.8.3" @@ -4697,7 +4961,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 1.9.3", "pin-project", "pin-project-lite", "rand 0.8.5", @@ -5038,6 +5302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" dependencies = [ "getrandom 0.2.9", + "serde", "sha1_smol", ] @@ -5411,6 +5676,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winnow" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index cd4e680cb6..3241b2722a 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -33,11 +33,11 @@ opt-level = 3 incremental = false [patch.crates-io] -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" } -appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d1882d" } +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 = { path = "../AppFlowy-Collab/collab" } #collab-folder = { path = "../AppFlowy-Collab/collab-folder" } diff --git a/frontend/rust-lib/dart-ffi/src/env_serde.rs b/frontend/rust-lib/dart-ffi/src/env_serde.rs index 6c2436fd5f..a8d8c9734c 100644 --- a/frontend/rust-lib/dart-ffi/src/env_serde.rs +++ b/frontend/rust-lib/dart-ffi/src/env_serde.rs @@ -1,11 +1,10 @@ -use appflowy_integrate::SupabaseDBConfig; -use flowy_server::supabase::SupabaseConfiguration; use serde::Deserialize; +use flowy_server::supabase::SupabaseConfiguration; + #[derive(Deserialize, Debug)] pub struct AppFlowyEnv { supabase_config: SupabaseConfiguration, - supabase_db_config: SupabaseDBConfig, } impl AppFlowyEnv { @@ -13,7 +12,6 @@ impl AppFlowyEnv { if let Ok(env) = serde_json::from_str::(env_str) { tracing::trace!("{:?}", env); env.supabase_config.write_env(); - env.supabase_db_config.write_env(); } } } diff --git a/frontend/rust-lib/flowy-config/src/entities.rs b/frontend/rust-lib/flowy-config/src/entities.rs index d1f963cd0c..dc4505ae23 100644 --- a/frontend/rust-lib/flowy-config/src/entities.rs +++ b/frontend/rust-lib/flowy-config/src/entities.rs @@ -1,8 +1,8 @@ use appflowy_integrate::config::AWSDynamoDBConfig; -use appflowy_integrate::{CollabTableConfig, SupabaseDBConfig}; + use flowy_derive::ProtoBuf; use flowy_error::FlowyError; -use flowy_server::supabase::SupabaseConfiguration; +use flowy_server::supabase::{PostgresConfiguration, SupabaseConfiguration}; #[derive(Default, ProtoBuf)] pub struct KeyValuePB { @@ -32,16 +32,21 @@ pub struct SupabaseConfigPB { #[pb(index = 4)] jwt_secret: String, + + #[pb(index = 5)] + pub postgres_config: PostgresConfigurationPB, } impl TryFrom for SupabaseConfiguration { type Error = FlowyError; - fn try_from(value: SupabaseConfigPB) -> Result { - Ok(Self { - url: value.supabase_url, - key: value.key, - jwt_secret: value.jwt_secret, + 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, }) } } @@ -50,9 +55,6 @@ impl TryFrom for SupabaseConfiguration { pub struct CollabPluginConfigPB { #[pb(index = 1, one_of)] pub aws_config: Option, - - #[pb(index = 2, one_of)] - pub supabase_config: Option, } #[derive(Default, ProtoBuf)] @@ -81,50 +83,29 @@ impl TryFrom for AWSDynamoDBConfig { } #[derive(Default, ProtoBuf)] -pub struct SupabaseDBConfigPB { +pub struct PostgresConfigurationPB { #[pb(index = 1)] - pub supabase_url: String, + pub url: String, #[pb(index = 2)] - pub key: String, + pub user_name: String, #[pb(index = 3)] - pub jwt_secret: String, + pub password: String, #[pb(index = 4)] - pub collab_table_config: CollabTableConfigPB, + pub port: u32, } -impl TryFrom for SupabaseDBConfig { +impl TryFrom for PostgresConfiguration { type Error = FlowyError; - fn try_from(config: SupabaseDBConfigPB) -> Result { - let update_table_config = CollabTableConfig::try_from(config.collab_table_config)?; - Ok(SupabaseDBConfig { - url: config.supabase_url, - key: config.key, - jwt_secret: config.jwt_secret, - collab_table_config: update_table_config, - }) - } -} - -#[derive(Default, ProtoBuf)] -pub struct CollabTableConfigPB { - #[pb(index = 1)] - pub table_name: String, -} - -impl TryFrom for CollabTableConfig { - type Error = FlowyError; - - fn try_from(config: CollabTableConfigPB) -> Result { - if config.table_name.is_empty() { - return Err(FlowyError::internal().context("table_name is empty")); - } - Ok(CollabTableConfig { - table_name: config.table_name, - enable: true, + 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 c5b7ef5fb8..4b815b87bb 100644 --- a/frontend/rust-lib/flowy-config/src/event_handler.rs +++ b/frontend/rust-lib/flowy-config/src/event_handler.rs @@ -1,5 +1,5 @@ use appflowy_integrate::config::AWSDynamoDBConfig; -use appflowy_integrate::SupabaseDBConfig; + use flowy_error::{FlowyError, FlowyResult}; use flowy_server::supabase::SupabaseConfiguration; use flowy_sqlite::kv::KV; @@ -52,10 +52,5 @@ pub(crate) async fn set_collab_plugin_config_handler( aws_config.write_env(); } } - if let Some(supabase_config_pb) = config.supabase_config { - if let Ok(supabase_config) = SupabaseDBConfig::try_from(supabase_config_pb) { - supabase_config.write_env(); - } - } Ok(()) } 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 aa3b7674bb..a6791e7952 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 @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::sync::Weak; use appflowy_integrate::{ calculate_snapshot_diff, CollabSnapshot, PersistenceError, SnapshotPersistence, @@ -14,19 +14,21 @@ use flowy_sqlite::{ use flowy_user::services::UserSession; use lib_infra::util::timestamp; -pub struct SnapshotDBImpl(pub Arc); +pub struct SnapshotDBImpl(pub Weak); impl SnapshotPersistence for SnapshotDBImpl { fn get_snapshots(&self, _uid: i64, object_id: &str) -> Vec { - self - .0 - .db_pool() - .and_then(|pool| Ok(pool.get()?)) - .and_then(|conn| { - CollabSnapshotTableSql::get_all_snapshots(object_id, &conn) - .map(|rows| rows.into_iter().map(|row| row.into()).collect()) - }) - .unwrap_or_else(|_| vec![]) + match self.0.upgrade() { + None => vec![], + Some(user_session) => user_session + .db_pool() + .and_then(|pool| Ok(pool.get()?)) + .and_then(|conn| { + CollabSnapshotTableSql::get_all_snapshots(object_id, &conn) + .map(|rows| rows.into_iter().map(|row| row.into()).collect()) + }) + .unwrap_or_else(|_| vec![]), + } } fn create_snapshot( @@ -34,19 +36,15 @@ impl SnapshotPersistence for SnapshotDBImpl { uid: i64, object_id: &str, title: String, - collab_type: String, snapshot_data: Vec, ) -> Result<(), PersistenceError> { let object_id = object_id.to_string(); - let weak_pool = Arc::downgrade( - &self - .0 - .db_pool() - .map_err(|e| PersistenceError::Internal(Box::new(e)))?, - ); - + let weak_user_session = self.0.clone(); tokio::task::spawn_blocking(move || { - if let Some(pool) = weak_pool.upgrade() { + if let Some(pool) = weak_user_session + .upgrade() + .and_then(|user_session| user_session.db_pool().ok()) + { let conn = pool .get() .map_err(|e| PersistenceError::Internal(Box::new(e)))?; @@ -66,7 +64,7 @@ impl SnapshotPersistence for SnapshotDBImpl { object_id: object_id.clone(), title, desc, - collab_type, + collab_type: "".to_string(), timestamp: timestamp(), data: snapshot_data, }, @@ -75,7 +73,7 @@ impl SnapshotPersistence for SnapshotDBImpl { .map_err(|e| PersistenceError::Internal(Box::new(e))); if let Err(e) = result { - tracing::error!("create snapshot error: {:?}", e); + tracing::warn!("create snapshot error: {:?}", e); } } Ok::<(), PersistenceError>(()) 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 d388be3b1e..f5fddae086 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 @@ -1,10 +1,11 @@ -use std::sync::Arc; +use std::sync::{Arc, Weak}; use appflowy_integrate::collab_builder::AppFlowyCollabBuilder; use appflowy_integrate::RocksCollabDB; use tokio::sync::RwLock; -use flowy_database2::{DatabaseManager2, DatabaseUser2}; +use flowy_database2::deps::{DatabaseCloudService, DatabaseUser2}; +use flowy_database2::DatabaseManager2; use flowy_error::FlowyError; use flowy_task::TaskDispatcher; use flowy_user::services::UserSession; @@ -13,32 +14,44 @@ pub struct Database2DepsResolver(); impl Database2DepsResolver { pub async fn resolve( - user_session: Arc, + user_session: Weak, task_scheduler: Arc>, collab_builder: Arc, + cloud_service: Arc, ) -> Arc { let user = Arc::new(DatabaseUserImpl(user_session)); - Arc::new(DatabaseManager2::new(user, task_scheduler, collab_builder)) + Arc::new(DatabaseManager2::new( + user, + task_scheduler, + collab_builder, + cloud_service, + )) } } -struct DatabaseUserImpl(Arc); +struct DatabaseUserImpl(Weak); impl DatabaseUser2 for DatabaseUserImpl { fn user_id(&self) -> Result { self .0 + .upgrade() + .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? .user_id() - .map_err(|e| FlowyError::internal().context(e)) } fn token(&self) -> Result, FlowyError> { self .0 + .upgrade() + .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? .token() - .map_err(|e| FlowyError::internal().context(e)) } fn collab_db(&self) -> Result, FlowyError> { - self.0.get_collab_db() + self + .0 + .upgrade() + .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? + .get_collab_db() } } 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 5ad0a81dae..4a109ce969 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 @@ -1,42 +1,54 @@ -use std::sync::Arc; +use std::sync::{Arc, Weak}; use appflowy_integrate::collab_builder::AppFlowyCollabBuilder; use appflowy_integrate::RocksCollabDB; use flowy_database2::DatabaseManager2; -use flowy_document2::manager::{DocumentManager, DocumentUser}; +use flowy_document2::deps::{DocumentCloudService, DocumentUser}; +use flowy_document2::manager::DocumentManager; use flowy_error::FlowyError; use flowy_user::services::UserSession; pub struct Document2DepsResolver(); impl Document2DepsResolver { pub fn resolve( - user_session: Arc, + user_session: Weak, _database_manager: &Arc, collab_builder: Arc, + cloud_service: Arc, ) -> Arc { let user: Arc = Arc::new(DocumentUserImpl(user_session)); - Arc::new(DocumentManager::new(user.clone(), collab_builder)) + Arc::new(DocumentManager::new( + user.clone(), + collab_builder, + cloud_service, + )) } } -struct DocumentUserImpl(Arc); +struct DocumentUserImpl(Weak); impl DocumentUser for DocumentUserImpl { fn user_id(&self) -> Result { self .0 + .upgrade() + .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? .user_id() - .map_err(|e| FlowyError::internal().context(e)) } fn token(&self) -> Result, FlowyError> { self .0 + .upgrade() + .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? .token() - .map_err(|e| FlowyError::internal().context(e)) } fn collab_db(&self) -> Result, FlowyError> { - self.0.get_collab_db() + self + .0 + .upgrade() + .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? + .get_collab_db() } } 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 cb4b3710f4..2007542e44 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 @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::convert::TryFrom; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use appflowy_integrate::collab_builder::AppFlowyCollabBuilder; use appflowy_integrate::RocksCollabDB; @@ -17,7 +17,7 @@ use flowy_document2::parser::json::parser::JsonToDocumentParser; use flowy_error::FlowyError; use flowy_folder2::deps::{FolderCloudService, FolderUser}; use flowy_folder2::entities::ViewLayoutPB; -use flowy_folder2::manager::Folder2Manager; +use flowy_folder2::manager::FolderManager; use flowy_folder2::share::ImportType; use flowy_folder2::view_operation::{ FolderOperationHandler, FolderOperationHandlers, View, WorkspaceViewBuilder, @@ -30,17 +30,17 @@ use lib_infra::future::FutureResult; pub struct Folder2DepsResolver(); impl Folder2DepsResolver { pub async fn resolve( - user_session: Arc, + user_session: Weak, document_manager: &Arc, database_manager: &Arc, collab_builder: Arc, folder_cloud: Arc, - ) -> Arc { + ) -> Arc { let user: Arc = Arc::new(FolderUserImpl(user_session.clone())); let handlers = folder_operation_handlers(document_manager.clone(), database_manager.clone()); Arc::new( - Folder2Manager::new(user.clone(), collab_builder, handlers, folder_cloud) + FolderManager::new(user.clone(), collab_builder, handlers, folder_cloud) .await .unwrap(), ) @@ -63,24 +63,30 @@ fn folder_operation_handlers( Arc::new(map) } -struct FolderUserImpl(Arc); +struct FolderUserImpl(Weak); impl FolderUser for FolderUserImpl { fn user_id(&self) -> Result { self .0 + .upgrade() + .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? .user_id() - .map_err(|e| FlowyError::internal().context(e)) } fn token(&self) -> Result, FlowyError> { self .0 + .upgrade() + .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? .token() - .map_err(|e| FlowyError::internal().context(e)) } fn collab_db(&self) -> Result, FlowyError> { - self.0.get_collab_db() + self + .0 + .upgrade() + .ok_or(FlowyError::internal().context("Unexpected error: UserSession is None"))? + .get_collab_db() } } @@ -143,8 +149,7 @@ impl FolderOperationHandler for DocumentFolderOperation { let manager = self.0.clone(); let view_id = view_id.to_string(); FutureResult::new(async move { - let document = manager.get_document_from_disk(&view_id)?; - let data: DocumentDataPB = document.lock().get_document()?.into(); + let data: DocumentDataPB = manager.get_document_data(&view_id)?.into(); let data_bytes = data.into_bytes().map_err(|_| FlowyError::invalid_data())?; Ok(data_bytes) }) diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs index 273490dd10..83ead3d9cd 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/server.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/server.rs @@ -1,11 +1,15 @@ -use lib_infra::future::FutureResult; use std::collections::HashMap; use std::sync::Arc; +use appflowy_integrate::collab_builder::{CollabStorageProvider, CollabStorageType}; +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_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_folder2::deps::{FolderCloudService, Workspace}; +use flowy_folder2::deps::{FolderCloudService, FolderSnapshot, Workspace}; use flowy_server::local_server::LocalServer; use flowy_server::self_host::configuration::self_host_server_configuration; use flowy_server::self_host::SelfHostServer; @@ -14,8 +18,7 @@ use flowy_server::AppFlowyServer; use flowy_sqlite::kv::KV; use flowy_user::event_map::{UserAuthService, UserCloudServiceProvider}; use flowy_user::services::AuthType; - -use serde_repr::*; +use lib_infra::future::FutureResult; const SERVER_PROVIDER_TYPE_KEY: &str = "server_provider_type"; @@ -115,6 +118,102 @@ impl FolderCloudService for AppFlowyServerProvider { let name = name.to_string(); FutureResult::new(async move { server?.folder_service().create_workspace(uid, &name).await }) } + + fn get_folder_latest_snapshot( + &self, + workspace_id: &str, + ) -> 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_latest_snapshot(&workspace_id) + .await + }) + } + + fn get_folder_updates(&self, workspace_id: &str) -> 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) + .await + }) + } +} + +impl DatabaseCloudService for AppFlowyServerProvider { + fn get_database_updates(&self, database_id: &str) -> FutureResult>, FlowyError> { + 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_updates(&database_id) + .await + }) + } + + fn get_database_latest_snapshot( + &self, + database_id: &str, + ) -> FutureResult, FlowyError> { + 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) + .await + }) + } +} + +impl DocumentCloudService for AppFlowyServerProvider { + fn get_document_updates(&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_updates(&document_id) + .await + }) + } + + fn get_document_latest_snapshot( + &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_latest_snapshot(&document_id) + .await + }) + } +} + +impl CollabStorageProvider for AppFlowyServerProvider { + fn storage_type(&self) -> CollabStorageType { + self.provider_type().into() + } + + fn get_storage(&self, storage_type: &CollabStorageType) -> Option> { + match storage_type { + CollabStorageType::Local => None, + CollabStorageType::AWS => None, + CollabStorageType::Supabase => self + .get_provider(&ServerProviderType::Supabase) + .ok() + .and_then(|provider| provider.collab_storage()), + } + } } fn server_from_auth_type( @@ -137,8 +236,7 @@ fn server_from_auth_type( }, ServerProviderType::Supabase => { let config = SupabaseConfiguration::from_env()?; - let server = Arc::new(SupabaseServer::new(config)); - Ok(server) + Ok(Arc::new(SupabaseServer::new(config))) }, } } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 3bc2c0ddff..e1b7c3eb03 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -9,14 +9,14 @@ use std::{ }, }; -use appflowy_integrate::collab_builder::{AppFlowyCollabBuilder, CloudStorageType}; +use appflowy_integrate::collab_builder::{AppFlowyCollabBuilder, CollabStorageType}; use tokio::sync::RwLock; use tracing::debug; use flowy_database2::DatabaseManager2; use flowy_document2::manager::DocumentManager as DocumentManager2; use flowy_error::FlowyResult; -use flowy_folder2::manager::Folder2Manager; +use flowy_folder2::manager::FolderManager; use flowy_sqlite::kv::KV; use flowy_task::{TaskDispatcher, TaskRunner}; use flowy_user::entities::UserProfile; @@ -46,7 +46,7 @@ pub struct AppFlowyCoreConfig { /// Different `AppFlowyCoreConfig` instance should have different name name: String, /// Panics if the `root` path is not existing - storage_path: String, + pub storage_path: String, log_filter: String, } @@ -81,6 +81,7 @@ fn create_log_filter(level: String, with_crates: Vec) -> String { .collect::>(); filters.push(format!("flowy_core={}", level)); filters.push(format!("flowy_folder2={}", level)); + filters.push(format!("collab_sync={}", level)); filters.push(format!("collab_folder={}", level)); filters.push(format!("collab_persistence={}", level)); filters.push(format!("collab_database={}", level)); @@ -90,6 +91,7 @@ fn create_log_filter(level: String, with_crates: Vec) -> String { filters.push(format!("flowy_user={}", level)); filters.push(format!("flowy_document2={}", level)); filters.push(format!("flowy_database2={}", level)); + filters.push(format!("flowy_server={}", level)); filters.push(format!("flowy_notification={}", "info")); filters.push(format!("lib_infra={}", level)); filters.push(format!("flowy_task={}", level)); @@ -112,7 +114,7 @@ pub struct AppFlowyCore { pub config: AppFlowyCoreConfig, pub user_session: Arc, pub document_manager2: Arc, - pub folder_manager: Arc, + pub folder_manager: Arc, pub database_manager: Arc, pub event_dispatcher: Arc, pub server_provider: Arc, @@ -141,64 +143,60 @@ impl AppFlowyCore { let server_provider = Arc::new(AppFlowyServerProvider::new()); - let ( - user_session, - folder_manager, - server_provider, - database_manager, - document_manager2, - collab_builder, - ) = runtime.block_on(async { - let user_session = mk_user_session(&config, server_provider.clone()); - /// The shared collab builder is used to build the [Collab] instance. The plugins will be loaded - /// on demand based on the [CollabPluginConfig]. - let collab_builder = Arc::new(AppFlowyCollabBuilder::new( - server_provider.provider_type().into(), - Some(Arc::new(SnapshotDBImpl(user_session.clone()))), - )); + let (user_session, folder_manager, server_provider, database_manager, document_manager2) = + runtime.block_on(async { + let user_session = mk_user_session(&config, server_provider.clone()); + /// The shared collab builder is used to build the [Collab] instance. The plugins will be loaded + /// on demand based on the [CollabPluginConfig]. + let collab_builder = Arc::new(AppFlowyCollabBuilder::new( + server_provider.clone(), + Some(Arc::new(SnapshotDBImpl(Arc::downgrade(&user_session)))), + )); - let database_manager2 = Database2DepsResolver::resolve( - user_session.clone(), - task_dispatcher.clone(), - collab_builder.clone(), - ) - .await; + let database_manager2 = Database2DepsResolver::resolve( + Arc::downgrade(&user_session), + task_dispatcher.clone(), + collab_builder.clone(), + server_provider.clone(), + ) + .await; - let document_manager2 = Document2DepsResolver::resolve( - user_session.clone(), - &database_manager2, - collab_builder.clone(), - ); + let document_manager2 = Document2DepsResolver::resolve( + Arc::downgrade(&user_session), + &database_manager2, + collab_builder.clone(), + server_provider.clone(), + ); - let folder_manager = Folder2DepsResolver::resolve( - user_session.clone(), - &document_manager2, - &database_manager2, - collab_builder.clone(), - server_provider.clone(), - ) - .await; + let folder_manager = Folder2DepsResolver::resolve( + Arc::downgrade(&user_session), + &document_manager2, + &database_manager2, + collab_builder, + server_provider.clone(), + ) + .await; - ( - user_session, - folder_manager, - server_provider, - database_manager2, - document_manager2, - collab_builder, - ) - }); + ( + user_session, + folder_manager, + server_provider, + database_manager2, + document_manager2, + ) + }); let user_status_listener = UserStatusCallbackImpl { - collab_builder, folder_manager: folder_manager.clone(), database_manager: database_manager.clone(), config: config.clone(), }; - let cloned_user_session = user_session.clone(); + let cloned_user_session = Arc::downgrade(&user_session); runtime.block_on(async move { - cloned_user_session.clone().init(user_status_listener).await; + if let Some(user_session) = cloned_user_session.upgrade() { + user_session.init(user_status_listener).await; + } }); let event_dispatcher = Arc::new(AFPluginDispatcher::construct(runtime, || { @@ -253,20 +251,14 @@ fn mk_user_session( } struct UserStatusCallbackImpl { - collab_builder: Arc, - folder_manager: Arc, + folder_manager: Arc, database_manager: Arc, #[allow(dead_code)] config: AppFlowyCoreConfig, } impl UserStatusCallback for UserStatusCallbackImpl { - fn auth_type_did_changed(&self, auth_type: AuthType) { - let provider_type: ServerProviderType = auth_type.into(); - self - .collab_builder - .set_cloud_storage_type(provider_type.into()); - } + fn auth_type_did_changed(&self, _auth_type: AuthType) {} fn did_sign_in(&self, user_id: i64, workspace_id: &str) -> Fut> { let user_id = user_id.to_owned(); @@ -281,7 +273,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { }) } - fn did_sign_up(&self, user_profile: &UserProfile) -> Fut> { + fn did_sign_up(&self, is_new: bool, user_profile: &UserProfile) -> Fut> { let user_profile = user_profile.clone(); let folder_manager = self.folder_manager.clone(); let database_manager = self.database_manager.clone(); @@ -290,6 +282,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { .initialize_with_new_user( user_profile.id, &user_profile.token, + is_new, &user_profile.workspace_id, ) .await?; @@ -311,12 +304,12 @@ impl UserStatusCallback for UserStatusCallbackImpl { } } -impl From for CloudStorageType { +impl From for CollabStorageType { fn from(server_provider: ServerProviderType) -> Self { match server_provider { - ServerProviderType::Local => CloudStorageType::Local, - ServerProviderType::SelfHosted => CloudStorageType::Local, - ServerProviderType::Supabase => CloudStorageType::Supabase, + ServerProviderType::Local => CollabStorageType::Local, + ServerProviderType::SelfHosted => CollabStorageType::Local, + ServerProviderType::Supabase => CollabStorageType::Supabase, } } } diff --git a/frontend/rust-lib/flowy-core/src/module.rs b/frontend/rust-lib/flowy-core/src/module.rs index 46f1a7efcd..9297aa5c98 100644 --- a/frontend/rust-lib/flowy-core/src/module.rs +++ b/frontend/rust-lib/flowy-core/src/module.rs @@ -2,12 +2,12 @@ use std::sync::Arc; use flowy_database2::DatabaseManager2; use flowy_document2::manager::DocumentManager as DocumentManager2; -use flowy_folder2::manager::Folder2Manager; +use flowy_folder2::manager::FolderManager; use flowy_user::services::UserSession; use lib_dispatch::prelude::AFPlugin; pub fn make_plugins( - folder_manager: &Arc, + folder_manager: &Arc, database_manager: &Arc, user_session: &Arc, document_manager2: &Arc, diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index 16415613f4..825faba6b4 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -45,7 +45,7 @@ strum = "0.21" strum_macros = "0.21" [dev-dependencies] -flowy-test = { path = "../flowy-test" } +flowy-test = { path = "../flowy-test", default-features = false } [build-dependencies] flowy-codegen = { path = "../../../shared-lib/flowy-codegen"} diff --git a/frontend/rust-lib/flowy-database2/src/deps.rs b/frontend/rust-lib/flowy-database2/src/deps.rs new file mode 100644 index 0000000000..7223672fa9 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/deps.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use appflowy_integrate::RocksCollabDB; + +use flowy_error::FlowyError; +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>; +} + +/// 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_database_latest_snapshot( + &self, + database_id: &str, + ) -> FutureResult, FlowyError>; +} + +pub struct DatabaseSnapshot { + pub snapshot_id: i64, + pub database_id: String, + pub data: Vec, + pub created_at: i64, +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs index 792a19b803..7faf2e2558 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs @@ -1,3 +1,4 @@ +use collab::core::collab_state::SyncState; use collab_database::rows::RowId; use collab_database::user::DatabaseRecord; use collab_database::views::DatabaseLayout; @@ -105,7 +106,7 @@ impl TryInto for MoveFieldPayloadPB { fn try_into(self) -> Result { let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; - let item_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::InvalidData)?; + let item_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::InvalidParams)?; Ok(MoveFieldParams { view_id: view_id.0, field_id: item_id.0, @@ -264,3 +265,48 @@ impl TryInto for DatabaseLayoutMetaPB { }) } } + +#[derive(Debug, Default, ProtoBuf)] +pub struct DatabaseSyncStatePB { + #[pb(index = 1)] + pub is_syncing: bool, + + #[pb(index = 2)] + pub is_finish: bool, +} + +impl From for DatabaseSyncStatePB { + fn from(value: SyncState) -> Self { + Self { + is_syncing: value.is_syncing(), + is_finish: value.is_sync_finished(), + } + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct DatabaseSnapshotStatePB { + #[pb(index = 1)] + pub new_snapshot_id: i64, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct RepeatedDatabaseSnapshotPB { + #[pb(index = 1)] + pub items: Vec, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct DatabaseSnapshotPB { + #[pb(index = 1)] + pub snapshot_id: i64, + + #[pb(index = 2)] + pub snapshot_desc: String, + + #[pb(index = 3)] + pub created_at: i64, + + #[pb(index = 4)] + pub data: Vec, +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checkbox_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checkbox_filter.rs index 6e2a1fdc2d..4b2f9fb888 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checkbox_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checkbox_filter.rs @@ -1,7 +1,8 @@ -use crate::services::filter::{Filter, FromFilterString}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; +use crate::services::filter::{Filter, FromFilterString}; + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct CheckboxFilterPB { #[pb(index = 1)] @@ -30,7 +31,7 @@ impl std::convert::TryFrom for CheckboxFilterConditionPB { match value { 0 => Ok(CheckboxFilterConditionPB::IsChecked), 1 => Ok(CheckboxFilterConditionPB::IsUnChecked), - _ => Err(ErrorCode::InvalidData), + _ => Err(ErrorCode::InvalidParams), } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs index e27a4e5ec0..0c2e7fc037 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs @@ -1,7 +1,8 @@ -use crate::services::filter::{Filter, FromFilterString}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; +use crate::services::filter::{Filter, FromFilterString}; + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct ChecklistFilterPB { #[pb(index = 1)] @@ -30,7 +31,7 @@ impl std::convert::TryFrom for ChecklistFilterConditionPB { match value { 0 => Ok(ChecklistFilterConditionPB::IsComplete), 1 => Ok(ChecklistFilterConditionPB::IsIncomplete), - _ => Err(ErrorCode::InvalidData), + _ => Err(ErrorCode::InvalidParams), } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs index 09958c5a58..3c94efd701 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs @@ -1,8 +1,11 @@ -use crate::services::filter::{Filter, FromFilterString}; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; + +use crate::services::filter::{Filter, FromFilterString}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct DateFilterPB { @@ -73,7 +76,7 @@ impl std::convert::TryFrom for DateFilterConditionPB { 4 => Ok(DateFilterConditionPB::DateOnOrAfter), 5 => Ok(DateFilterConditionPB::DateWithIn), 6 => Ok(DateFilterConditionPB::DateIsEmpty), - _ => Err(ErrorCode::InvalidData), + _ => Err(ErrorCode::InvalidParams), } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/number_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/number_filter.rs index eddeac9f42..d51d0e4726 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/number_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/number_filter.rs @@ -1,7 +1,8 @@ -use crate::services::filter::{Filter, FromFilterString}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; +use crate::services::filter::{Filter, FromFilterString}; + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct NumberFilterPB { #[pb(index = 1)] @@ -44,7 +45,7 @@ impl std::convert::TryFrom for NumberFilterConditionPB { 5 => Ok(NumberFilterConditionPB::LessThanOrEqualTo), 6 => Ok(NumberFilterConditionPB::NumberIsEmpty), 7 => Ok(NumberFilterConditionPB::NumberIsNotEmpty), - _ => Err(ErrorCode::InvalidData), + _ => Err(ErrorCode::InvalidParams), } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs index 6f512cf0b0..86698b3904 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs @@ -1,8 +1,9 @@ -use crate::services::field::SelectOptionIds; -use crate::services::filter::{Filter, FromFilterString}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; +use crate::services::field::SelectOptionIds; +use crate::services::filter::{Filter, FromFilterString}; + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct SelectOptionFilterPB { #[pb(index = 1)] @@ -38,7 +39,7 @@ impl std::convert::TryFrom for SelectOptionConditionPB { 1 => Ok(SelectOptionConditionPB::OptionIsNot), 2 => Ok(SelectOptionConditionPB::OptionIsEmpty), 3 => Ok(SelectOptionConditionPB::OptionIsNotEmpty), - _ => Err(ErrorCode::InvalidData), + _ => Err(ErrorCode::InvalidParams), } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/text_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/text_filter.rs index 58d07ec725..0956fdb894 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/text_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/text_filter.rs @@ -1,7 +1,8 @@ -use crate::services::filter::{Filter, FromFilterString}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; +use crate::services::filter::{Filter, FromFilterString}; + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct TextFilterPB { #[pb(index = 1)] @@ -45,7 +46,7 @@ impl std::convert::TryFrom for TextFilterConditionPB { 5 => Ok(TextFilterConditionPB::EndsWith), 6 => Ok(TextFilterConditionPB::TextIsEmpty), 7 => Ok(TextFilterConditionPB::TextIsNotEmpty), - _ => Err(ErrorCode::InvalidData), + _ => Err(ErrorCode::InvalidParams), } } } diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 6730246f1f..7a8c38ab69 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -28,6 +28,19 @@ pub(crate) async fn get_database_data_handler( data_result_ok(data) } +#[tracing::instrument(level = "trace", skip_all, err)] +pub(crate) async fn open_database_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let view_id: DatabaseViewIdPB = data.into_inner(); + let database_id = manager + .get_database_id_with_view_id(view_id.as_ref()) + .await?; + let _ = manager.open_database(&database_id).await?; + Ok(()) +} + #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn get_database_id_handler( data: AFPluginData, @@ -807,3 +820,13 @@ pub(crate) async fn export_csv_handler( data, }) } + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_snapshots_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let view_id = data.into_inner().value; + let snapshots = manager.get_database_snapshots(&view_id).await?; + data_result_ok(RepeatedDatabaseSnapshotPB { items: snapshots }) +} diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index eaf644daec..8ec138981b 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -14,6 +14,7 @@ pub fn init(database_manager: Arc) -> AFPlugin { .state(database_manager); plugin .event(DatabaseEvent::GetDatabase, get_database_data_handler) + .event(DatabaseEvent::OpenDatabase, get_database_data_handler) .event(DatabaseEvent::GetDatabaseId, get_database_id_handler) .event(DatabaseEvent::GetDatabaseSetting, get_database_setting_handler) .event(DatabaseEvent::UpdateDatabaseSetting, update_database_setting_handler) @@ -72,6 +73,7 @@ pub fn init(database_manager: Arc) -> AFPlugin { .event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler) .event(DatabaseEvent::CreateDatabaseView, create_database_view) .event(DatabaseEvent::ExportCSV, export_csv_handler) + .event(DatabaseEvent::GetDatabaseSnapshots, get_snapshots_handler) } /// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf) @@ -110,6 +112,9 @@ pub enum DatabaseEvent { #[event(input = "DatabaseViewIdPB")] DeleteAllSorts = 6, + #[event(input = "DatabaseViewIdPB")] + OpenDatabase = 7, + /// [GetFields] event is used to get the database's fields. /// /// The event handler accepts a [GetFieldPayloadPB] and returns a [RepeatedFieldPB] @@ -306,4 +311,8 @@ pub enum DatabaseEvent { #[event(input = "DatabaseViewIdPB", output = "DatabaseExportDataPB")] ExportCSV = 141, + + /// Returns all the snapshots of the database view. + #[event(input = "DatabaseViewIdPB", output = "RepeatedDatabaseSnapshotPB")] + GetDatabaseSnapshots = 150, } diff --git a/frontend/rust-lib/flowy-database2/src/lib.rs b/frontend/rust-lib/flowy-database2/src/lib.rs index 5e9c988c86..aa941c5010 100644 --- a/frontend/rust-lib/flowy-database2/src/lib.rs +++ b/frontend/rust-lib/flowy-database2/src/lib.rs @@ -1,5 +1,6 @@ pub use manager::*; +pub mod deps; pub mod entities; mod event_handler; pub mod event_map; diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 96f9d7f907..4e887942b6 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -14,23 +14,21 @@ use tokio::sync::RwLock; use flowy_error::{internal_error, FlowyError, FlowyResult}; use flowy_task::TaskDispatcher; -use crate::entities::{DatabaseDescriptionPB, DatabaseLayoutPB, RepeatedDatabaseDescriptionPB}; +use crate::deps::{DatabaseCloudService, DatabaseUser2}; +use crate::entities::{ + DatabaseDescriptionPB, DatabaseLayoutPB, DatabaseSnapshotPB, RepeatedDatabaseDescriptionPB, +}; use crate::services::database::{DatabaseEditor, MutexDatabase}; use crate::services::database_view::DatabaseLayoutDepsResolver; use crate::services::share::csv::{CSVFormat, CSVImporter, ImportResult}; -pub trait DatabaseUser2: Send + Sync { - fn user_id(&self) -> Result; - fn token(&self) -> Result, FlowyError>; - fn collab_db(&self) -> Result, FlowyError>; -} - pub struct DatabaseManager2 { user: Arc, user_database: UserDatabase, task_scheduler: Arc>, editors: RwLock>>, collab_builder: Arc, + cloud_service: Arc, } impl DatabaseManager2 { @@ -38,6 +36,7 @@ impl DatabaseManager2 { database_user: Arc, task_scheduler: Arc>, collab_builder: Arc, + cloud_service: Arc, ) -> Self { Self { user: database_user, @@ -45,6 +44,7 @@ impl DatabaseManager2 { task_scheduler, editors: Default::default(), collab_builder, + cloud_service, } } @@ -98,7 +98,10 @@ impl DatabaseManager2 { if let Some(editor) = self.editors.read().await.get(database_id) { return Ok(editor.clone()); } + self.open_database(database_id).await + } + 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( @@ -117,9 +120,14 @@ impl DatabaseManager2 { #[tracing::instrument(level = "debug", skip_all)] 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, |database| { - database.get_database_id_with_view_id(view_id) + 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 }); if let Some(database_id) = database_id { @@ -151,6 +159,7 @@ impl DatabaseManager2 { Ok(database_data) } + /// Create a new database with the given data that can be deserialized to [DatabaseData]. #[tracing::instrument(level = "trace", skip_all, err)] pub async fn create_database_with_database_data( &self, @@ -251,6 +260,29 @@ impl DatabaseManager2 { database.update_view_layout(view_id, layout.into()).await } + pub async fn get_database_snapshots( + &self, + view_id: &str, + ) -> FlowyResult> { + let database_id = self.get_database_id_with_view_id(view_id).await?; + let mut snapshots = vec![]; + if let Some(snapshot) = self + .cloud_service + .get_database_latest_snapshot(&database_id) + .await? + .map(|snapshot| DatabaseSnapshotPB { + snapshot_id: snapshot.snapshot_id, + snapshot_desc: "".to_string(), + created_at: snapshot.created_at, + data: snapshot.data, + }) + { + snapshots.push(snapshot); + } + + Ok(snapshots) + } + fn with_user_database(&self, default_value: Output, f: F) -> Output where F: FnOnce(&InnerUserDatabase) -> Output, @@ -261,6 +293,12 @@ impl DatabaseManager2 { Some(folder) => f(folder), } } + + /// Only expose this method for testing + #[cfg(debug_assertions)] + pub fn get_cloud_service(&self) -> &Arc { + &self.cloud_service + } } #[derive(Clone, Default)] diff --git a/frontend/rust-lib/flowy-database2/src/notification.rs b/frontend/rust-lib/flowy-database2/src/notification.rs index f170e14da9..36cb0072f0 100644 --- a/frontend/rust-lib/flowy-database2/src/notification.rs +++ b/frontend/rust-lib/flowy-database2/src/notification.rs @@ -1,7 +1,7 @@ use flowy_derive::ProtoBuf_Enum; use flowy_notification::NotificationBuilder; -const OBSERVABLE_CATEGORY: &str = "Grid"; +const DATABASE_OBSERVABLE_SOURCE: &str = "Database"; #[derive(ProtoBuf_Enum, Debug, Default)] pub enum DatabaseNotification { @@ -45,6 +45,8 @@ pub enum DatabaseNotification { DidDeleteDatabaseView = 83, // Trigger when the database view is moved to trash DidMoveDatabaseViewToTrash = 84, + DidUpdateDatabaseSyncUpdate = 85, + DidUpdateDatabaseSnapshotState = 86, } impl std::convert::From for i32 { @@ -53,7 +55,34 @@ impl std::convert::From for i32 { } } +impl std::convert::From for DatabaseNotification { + fn from(notification: i32) -> Self { + match notification { + 20 => DatabaseNotification::DidUpdateViewRows, + 21 => DatabaseNotification::DidUpdateViewRowsVisibility, + 22 => DatabaseNotification::DidUpdateFields, + 40 => DatabaseNotification::DidUpdateCell, + 50 => DatabaseNotification::DidUpdateField, + 60 => DatabaseNotification::DidUpdateNumOfGroups, + 61 => DatabaseNotification::DidUpdateGroupRow, + 62 => DatabaseNotification::DidGroupByField, + 63 => DatabaseNotification::DidUpdateFilter, + 64 => DatabaseNotification::DidUpdateSort, + 65 => DatabaseNotification::DidReorderRows, + 66 => DatabaseNotification::DidReorderSingleRow, + 67 => DatabaseNotification::DidUpdateRowMeta, + 70 => DatabaseNotification::DidUpdateSettings, + 80 => DatabaseNotification::DidUpdateLayoutSettings, + 81 => DatabaseNotification::DidSetNewLayoutField, + 82 => DatabaseNotification::DidUpdateDatabaseLayout, + 83 => DatabaseNotification::DidDeleteDatabaseView, + 84 => DatabaseNotification::DidMoveDatabaseViewToTrash, + _ => DatabaseNotification::Unknown, + } + } +} + #[tracing::instrument(level = "trace")] pub fn send_notification(id: &str, ty: DatabaseNotification) -> NotificationBuilder { - NotificationBuilder::new(id, ty, OBSERVABLE_CATEGORY) + NotificationBuilder::new(id, ty, DATABASE_OBSERVABLE_SOURCE) } diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs index 5403d1fca6..107c573e23 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs @@ -277,7 +277,7 @@ pub struct AnyCellChangeset(pub Option); impl AnyCellChangeset { pub fn try_into_inner(self) -> FlowyResult { match self.0 { - None => Err(ErrorCode::InvalidData.into()), + None => Err(ErrorCode::InvalidParams.into()), Some(data) => Ok(data), } } 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 6a27a4e4eb..099f48fab4 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 @@ -7,6 +7,7 @@ use collab_database::database::Database as InnerDatabase; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{Cell, Cells, CreateRowParams, Row, RowCell, RowId}; use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; +use futures::StreamExt; use parking_lot::Mutex; use tokio::sync::{broadcast, RwLock}; @@ -54,6 +55,38 @@ impl DatabaseEditor { cell_cache: cell_cache.clone(), }); + let database_id = database.lock().get_database_id(); + + // Receive database sync state and send to frontend via the notification + let mut sync_state = database.lock().subscribe_sync_state(); + let cloned_database_id = database_id.clone(); + tokio::spawn(async move { + while let Some(sync_state) = sync_state.next().await { + send_notification( + &cloned_database_id, + DatabaseNotification::DidUpdateDatabaseSyncUpdate, + ) + .payload(DatabaseSyncStatePB::from(sync_state)) + .send(); + } + }); + + // Receive database snapshot state and send to frontend via the notification + let mut snapshot_state = database.lock().subscribe_snapshot_state(); + 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); + send_notification( + &database_id, + DatabaseNotification::DidUpdateDatabaseSnapshotState, + ) + .payload(DatabaseSnapshotStatePB { new_snapshot_id }) + .send(); + } + } + }); + let database_views = Arc::new(DatabaseViews::new(database.clone(), cell_cache.clone(), database_view_data).await?); Ok(Self { @@ -1090,6 +1123,12 @@ impl DatabaseEditor { .filter(|f| FieldType::from(f.field_type).is_auto_update()) .collect::>() } + + /// Only expose this method for testing + #[cfg(debug_assertions)] + pub fn get_mutex_database(&self) -> &MutexDatabase { + &self.database + } } pub(crate) async fn notify_did_update_cell(changesets: Vec) { diff --git a/frontend/rust-lib/flowy-database2/src/services/snapshot/entities.rs b/frontend/rust-lib/flowy-database2/src/services/snapshot/entities.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/snapshot/entities.rs @@ -0,0 +1 @@ + diff --git a/frontend/rust-lib/flowy-database2/src/services/snapshot/mod.rs b/frontend/rust-lib/flowy-database2/src/services/snapshot/mod.rs index 8b13789179..0b8f0b5a5a 100644 --- a/frontend/rust-lib/flowy-database2/src/services/snapshot/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/snapshot/mod.rs @@ -1 +1 @@ - +pub mod entities; diff --git a/frontend/rust-lib/flowy-document2/Cargo.toml b/frontend/rust-lib/flowy-document2/Cargo.toml index 25cd8d5d58..16d9dba124 100644 --- a/frontend/rust-lib/flowy-document2/Cargo.toml +++ b/frontend/rust-lib/flowy-document2/Cargo.toml @@ -14,6 +14,7 @@ flowy-derive = { path = "../../../shared-lib/flowy-derive" } flowy-notification = { path = "../flowy-notification" } flowy-error = { path = "../flowy-error", features = ["adaptor_serde", "adaptor_database", "adaptor_dispatch", "collab"] } lib-dispatch = { path = "../lib-dispatch" } +lib-infra = { path = "../../../shared-lib/lib-infra" } protobuf = {version = "2.28.0"} bytes = { version = "1.4" } @@ -28,6 +29,8 @@ tokio = { version = "1.26", features = ["full"] } anyhow = "1.0" indexmap = {version = "1.9.2", features = ["serde"]} uuid = { version = "1.3.3", features = ["v4"] } +futures = "0.3.26" +tokio-stream = { version = "0.1.14", features = ["sync"] } [dev-dependencies] tempfile = "3.4.0" diff --git a/frontend/rust-lib/flowy-document2/src/deps.rs b/frontend/rust-lib/flowy-document2/src/deps.rs new file mode 100644 index 0000000000..f3c659321b --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/deps.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use appflowy_integrate::RocksCollabDB; + +use flowy_error::FlowyError; +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>; +} + +/// A trait for document cloud service. +/// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of +/// [flowy-server] crate for more information. +pub trait DocumentCloudService: Send + Sync + 'static { + fn get_document_updates(&self, document_id: &str) -> FutureResult>, FlowyError>; + + fn get_document_latest_snapshot( + &self, + document_id: &str, + ) -> FutureResult, FlowyError>; +} + +pub struct DocumentSnapshot { + pub snapshot_id: i64, + pub document_id: String, + pub data: Vec, + pub created_at: i64, +} diff --git a/frontend/rust-lib/flowy-document2/src/document.rs b/frontend/rust-lib/flowy-document2/src/document.rs index b44fe181b9..7428442f72 100644 --- a/frontend/rust-lib/flowy-document2/src/document.rs +++ b/frontend/rust-lib/flowy-document2/src/document.rs @@ -4,26 +4,33 @@ use std::{ }; use collab::core::collab::MutexCollab; -use collab_document::{blocks::DocumentData, document::Document as InnerDocument}; +use collab_document::{blocks::DocumentData, document::Document}; +use futures::StreamExt; use parking_lot::Mutex; +use tokio_stream::wrappers::WatchStream; use flowy_error::FlowyResult; +use crate::entities::{DocEventPB, DocumentSnapshotStatePB, DocumentSyncStatePB}; +use crate::notification::{send_notification, DocumentNotification}; + /// This struct wrap the document::Document #[derive(Clone)] -pub struct Document(Arc>); +pub struct MutexDocument(Arc>); -impl Document { - /// Creates and returns a new Document object. +impl MutexDocument { + /// Open a document with the given collab. /// # Arguments /// * `collab` - the identifier of the collaboration instance /// /// # Returns /// * `Result` - a Result containing either a new Document object or an Error if the document creation failed - pub fn new(collab: Arc) -> FlowyResult { - InnerDocument::create(collab) - .map(|inner| Self(Arc::new(Mutex::new(inner)))) - .map_err(|err| err.into()) + pub fn open(doc_id: &str, collab: Arc) -> FlowyResult { + let document = Document::open(collab.clone()).map(|inner| Self(Arc::new(Mutex::new(inner))))?; + subscribe_document_changed(doc_id, &document); + subscribe_document_snapshot_state(&collab); + subscribe_document_sync_state(&collab); + Ok(document) } /// Creates and returns a new Document object with initial data. @@ -34,24 +41,73 @@ impl Document { /// # Returns /// * `Result` - a Result containing either a new Document object or an Error if the document creation failed pub fn create_with_data(collab: Arc, data: DocumentData) -> FlowyResult { - InnerDocument::create_with_data(collab, data) - .map(|inner| Self(Arc::new(Mutex::new(inner)))) - .map_err(|err| err.into()) + let document = + Document::create_with_data(collab, data).map(|inner| Self(Arc::new(Mutex::new(inner))))?; + Ok(document) } } -unsafe impl Sync for Document {} -unsafe impl Send for Document {} +fn subscribe_document_changed(doc_id: &str, document: &MutexDocument) { + let doc_id = doc_id.to_string(); + document + .lock() + .subscribe_block_changed(move |events, is_remote| { + tracing::trace!( + "document changed: {:?}, from remote: {}", + &events, + is_remote + ); + // send notification to the client. + send_notification(&doc_id, DocumentNotification::DidReceiveUpdate) + .payload::((events, is_remote).into()) + .send(); + }); +} -impl Deref for Document { - type Target = Arc>; +fn subscribe_document_snapshot_state(collab: &Arc) { + let document_id = collab.lock().object_id.clone(); + let mut snapshot_state = WatchStream::new(collab.lock().subscribe_snapshot_state()); + 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); + send_notification( + &document_id, + DocumentNotification::DidUpdateDocumentSnapshotState, + ) + .payload(DocumentSnapshotStatePB { new_snapshot_id }) + .send(); + } + } + }); +} + +fn subscribe_document_sync_state(collab: &Arc) { + let document_id = collab.lock().object_id.clone(); + let mut sync_state_stream = WatchStream::new(collab.lock().subscribe_sync_state()); + tokio::spawn(async move { + while let Some(sync_state) = sync_state_stream.next().await { + send_notification( + &document_id, + DocumentNotification::DidUpdateDocumentSyncState, + ) + .payload(DocumentSyncStatePB::from(sync_state)) + .send(); + } + }); +} +unsafe impl Sync for MutexDocument {} +unsafe impl Send for MutexDocument {} + +impl Deref for MutexDocument { + type Target = Arc>; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for Document { +impl DerefMut for MutexDocument { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } diff --git a/frontend/rust-lib/flowy-document2/src/document_block_keys.rs b/frontend/rust-lib/flowy-document2/src/document_block_keys.rs deleted file mode 100644 index 92ff17e91a..0000000000 --- a/frontend/rust-lib/flowy-document2/src/document_block_keys.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub const PAGE: &str = "page"; -pub const PARAGRAPH_BLOCK_TYPE: &str = "paragraph"; diff --git a/frontend/rust-lib/flowy-document2/src/document_data.rs b/frontend/rust-lib/flowy-document2/src/document_data.rs index 242139c8ad..31607926bb 100644 --- a/frontend/rust-lib/flowy-document2/src/document_data.rs +++ b/frontend/rust-lib/flowy-document2/src/document_data.rs @@ -3,10 +3,10 @@ use std::{collections::HashMap, vec}; use collab_document::blocks::{Block, DocumentData, DocumentMeta}; use nanoid::nanoid; -use crate::{ - document_block_keys::{PAGE, PARAGRAPH_BLOCK_TYPE}, - entities::{BlockPB, ChildrenPB, DocumentDataPB, MetaPB}, -}; +use crate::entities::{BlockPB, ChildrenPB, DocumentDataPB, MetaPB}; + +pub const PAGE: &str = "page"; +pub const PARAGRAPH_BLOCK_TYPE: &str = "paragraph"; impl From for DocumentDataPB { fn from(data: DocumentData) -> Self { diff --git a/frontend/rust-lib/flowy-document2/src/entities.rs b/frontend/rust-lib/flowy-document2/src/entities.rs index 715bc7165f..6f7a41ba90 100644 --- a/frontend/rust-lib/flowy-document2/src/entities.rs +++ b/frontend/rust-lib/flowy-document2/src/entities.rs @@ -1,3 +1,4 @@ +use collab::core::collab_state::SyncState; use collab_document::blocks::{BlockAction, DocumentData}; use std::collections::HashMap; @@ -336,3 +337,48 @@ impl TryInto for ConvertDataPayloadPB { Ok(ConvertDataParams { convert_type, data }) } } + +#[derive(Debug, Default, ProtoBuf)] +pub struct RepeatedDocumentSnapshotPB { + #[pb(index = 1)] + pub items: Vec, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct DocumentSnapshotPB { + #[pb(index = 1)] + pub snapshot_id: i64, + + #[pb(index = 2)] + pub snapshot_desc: String, + + #[pb(index = 3)] + pub created_at: i64, + + #[pb(index = 4)] + pub data: Vec, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct DocumentSnapshotStatePB { + #[pb(index = 1)] + pub new_snapshot_id: i64, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct DocumentSyncStatePB { + #[pb(index = 1)] + pub is_syncing: bool, + + #[pb(index = 2)] + pub is_finish: bool, +} + +impl From for DocumentSyncStatePB { + fn from(value: SyncState) -> Self { + Self { + is_syncing: value.is_syncing(), + is_finish: value.is_sync_finished(), + } + } +} diff --git a/frontend/rust-lib/flowy-document2/src/event_handler.rs b/frontend/rust-lib/flowy-document2/src/event_handler.rs index 98f49b584f..c36c794655 100644 --- a/frontend/rust-lib/flowy-document2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document2/src/event_handler.rs @@ -14,20 +14,8 @@ use collab_document::blocks::{ use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; -use crate::entities::{ - ApplyActionParams, CloseDocumentParams, ConvertDataParams, CreateDocumentParams, - DocumentRedoUndoParams, OpenDocumentParams, -}; -use crate::{ - entities::{ - ApplyActionPayloadPB, BlockActionPB, BlockActionPayloadPB, BlockActionTypePB, BlockEventPB, - BlockEventPayloadPB, BlockPB, CloseDocumentPayloadPB, ConvertDataPayloadPB, ConvertType, - CreateDocumentPayloadPB, DeltaTypePB, DocEventPB, DocumentDataPB, DocumentRedoUndoPayloadPB, - DocumentRedoUndoResponsePB, OpenDocumentPayloadPB, - }, - manager::DocumentManager, - parser::json::parser::JsonToDocumentParser, -}; +use crate::entities::*; +use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser}; // Handler for creating a new document pub(crate) async fn create_document_handler( @@ -46,8 +34,8 @@ 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_or_open_document(&doc_id)?; - let document_data = document.lock().get_document()?; + let document = manager.get_document(&doc_id)?; + let document_data = document.lock().get_document_data()?; data_result_ok(DocumentDataPB::from(document_data)) } @@ -69,8 +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 = manager.get_document_from_disk(&doc_id)?; - let document_data = document.lock().get_document()?; + let document_data = manager.get_document_data(&doc_id)?; data_result_ok(DocumentDataPB::from(document_data)) } @@ -81,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_or_open_document(&doc_id)?; + let document = manager.get_document(&doc_id)?; let actions = params.actions; document.lock().apply_action(actions); Ok(()) @@ -117,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_or_open_document(&doc_id)?; + let document = manager.get_document(&doc_id)?; let document = document.lock(); let redo = document.redo(); let can_redo = document.can_redo(); @@ -135,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_or_open_document(&doc_id)?; + let document = manager.get_document(&doc_id)?; let document = document.lock(); let undo = document.undo(); let can_redo = document.can_redo(); @@ -153,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_or_open_document(&doc_id)?; + let document = manager.get_document(&doc_id)?; let document = document.lock(); let can_redo = document.can_redo(); let can_undo = document.can_undo(); @@ -165,6 +152,16 @@ pub(crate) async fn can_undo_redo_handler( }) } +pub(crate) async fn get_snapshot_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let params: OpenDocumentParams = data.into_inner().try_into()?; + let doc_id = params.document_id; + let snapshots = manager.get_document_snapshots(&doc_id).await?; + data_result_ok(RepeatedDocumentSnapshotPB { items: snapshots }) +} + impl From for BlockAction { fn from(pb: BlockActionPB) -> Self { Self { diff --git a/frontend/rust-lib/flowy-document2/src/event_map.rs b/frontend/rust-lib/flowy-document2/src/event_map.rs index 2a3c398f92..b5ce9a863c 100644 --- a/frontend/rust-lib/flowy-document2/src/event_map.rs +++ b/frontend/rust-lib/flowy-document2/src/event_map.rs @@ -4,34 +4,26 @@ use strum_macros::Display; use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use lib_dispatch::prelude::AFPlugin; -use crate::{ - event_handler::{ - apply_action_handler, can_undo_redo_handler, close_document_handler, convert_data_to_document, - create_document_handler, get_document_data_handler, open_document_handler, redo_handler, - undo_handler, - }, - manager::DocumentManager, -}; +use crate::event_handler::get_snapshot_handler; +use crate::{event_handler::*, manager::DocumentManager}; pub fn init(document_manager: Arc) -> AFPlugin { - let mut plugin = AFPlugin::new() + AFPlugin::new() .name(env!("CARGO_PKG_NAME")) - .state(document_manager); - - plugin = plugin.event(DocumentEvent::CreateDocument, create_document_handler); - plugin = plugin.event(DocumentEvent::OpenDocument, open_document_handler); - plugin = plugin.event(DocumentEvent::CloseDocument, close_document_handler); - plugin = plugin.event(DocumentEvent::ApplyAction, apply_action_handler); - plugin = plugin.event(DocumentEvent::GetDocumentData, get_document_data_handler); - plugin = plugin.event( - DocumentEvent::ConvertDataToDocument, - convert_data_to_document, - ); - plugin = plugin.event(DocumentEvent::Redo, redo_handler); - plugin = plugin.event(DocumentEvent::Undo, undo_handler); - plugin = plugin.event(DocumentEvent::CanUndoRedo, can_undo_redo_handler); - - plugin + .state(document_manager) + .event(DocumentEvent::CreateDocument, create_document_handler) + .event(DocumentEvent::OpenDocument, open_document_handler) + .event(DocumentEvent::CloseDocument, close_document_handler) + .event(DocumentEvent::ApplyAction, apply_action_handler) + .event(DocumentEvent::GetDocumentData, get_document_data_handler) + .event( + DocumentEvent::ConvertDataToDocument, + convert_data_to_document, + ) + .event(DocumentEvent::Redo, redo_handler) + .event(DocumentEvent::Undo, undo_handler) + .event(DocumentEvent::CanUndoRedo, can_undo_redo_handler) + .event(DocumentEvent::GetDocumentSnapshots, get_snapshot_handler) } #[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)] @@ -49,7 +41,7 @@ pub enum DocumentEvent { #[event(input = "ApplyActionPayloadPB")] ApplyAction = 3, - #[event(input = "OpenDocumentPayloadPB")] + #[event(input = "OpenDocumentPayloadPB", output = "DocumentDataPB")] GetDocumentData = 4, #[event(input = "ConvertDataPayloadPB", output = "DocumentDataPB")] @@ -72,4 +64,7 @@ pub enum DocumentEvent { output = "DocumentRedoUndoResponsePB" )] CanUndoRedo = 8, + + #[event(input = "OpenDocumentPayloadPB", output = "RepeatedDocumentSnapshotPB")] + GetDocumentSnapshots = 9, } diff --git a/frontend/rust-lib/flowy-document2/src/lib.rs b/frontend/rust-lib/flowy-document2/src/lib.rs index 602fd8da8a..ee0c15ffd6 100644 --- a/frontend/rust-lib/flowy-document2/src/lib.rs +++ b/frontend/rust-lib/flowy-document2/src/lib.rs @@ -1,5 +1,4 @@ pub mod document; -pub mod document_block_keys; pub mod document_data; pub mod entities; pub mod event_handler; @@ -8,5 +7,6 @@ pub mod manager; pub mod parser; pub mod protobuf; +pub mod deps; mod notification; mod parse; diff --git a/frontend/rust-lib/flowy-document2/src/manager.rs b/frontend/rust-lib/flowy-document2/src/manager.rs index cac0d2d071..1dee80d4cf 100644 --- a/frontend/rust-lib/flowy-document2/src/manager.rs +++ b/frontend/rust-lib/flowy-document2/src/manager.rs @@ -1,39 +1,37 @@ use std::{collections::HashMap, sync::Arc}; use appflowy_integrate::collab_builder::AppFlowyCollabBuilder; -use appflowy_integrate::RocksCollabDB; +use collab::core::collab::MutexCollab; use collab_document::blocks::DocumentData; -use collab_document::error::DocumentError; +use collab_document::document::Document; use collab_document::YrsDocAction; use parking_lot::RwLock; -use flowy_error::{FlowyError, FlowyResult}; +use flowy_error::{internal_error, FlowyError, FlowyResult}; -use crate::{ - document::Document, - document_data::default_document_data, - entities::DocEventPB, - notification::{send_notification, DocumentNotification}, -}; - -pub trait DocumentUser: Send + Sync { - fn user_id(&self) -> Result; - fn token(&self) -> Result, FlowyError>; // unused now. - fn collab_db(&self) -> Result, FlowyError>; -} +use crate::deps::{DocumentCloudService, DocumentUser}; +use crate::entities::DocumentSnapshotPB; +use crate::{document::MutexDocument, document_data::default_document_data}; pub struct DocumentManager { user: Arc, collab_builder: Arc, - documents: Arc>>>, + documents: Arc>>>, + #[allow(dead_code)] + cloud_service: Arc, } impl DocumentManager { - pub fn new(user: Arc, collab_builder: Arc) -> Self { + pub fn new( + user: Arc, + collab_builder: Arc, + cloud_service: Arc, + ) -> Self { Self { user, collab_builder, documents: Default::default(), + cloud_service, } } @@ -45,67 +43,52 @@ impl DocumentManager { &self, doc_id: &str, data: Option, - ) -> FlowyResult> { - tracing::debug!("create a 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); + ) -> FlowyResult> { + tracing::trace!("create a document: {:?}", doc_id); + let collab = self.collab_for_document(doc_id)?; let data = data.unwrap_or_else(default_document_data); - let document = Arc::new(Document::create_with_data(collab, data)?); + let document = Arc::new(MutexDocument::create_with_data(collab, data)?); Ok(document) } - /// get document - /// read the existing document from the map if it exists, otherwise read it from the disk and write it to the map. - pub fn get_or_open_document(&self, doc_id: &str) -> FlowyResult> { + /// Return the document + pub 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. + if !self.is_doc_exist(doc_id)? { + return Err( + FlowyError::record_not_found().context(format!("document: {} is not exist", doc_id)), + ); + } + tracing::debug!("open_document: {:?}", doc_id); - // read the existing document from the disk. - let document = self.get_document_from_disk(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 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. // and we don't want to subscribe to the document changes if we open the same document again. self .documents .write() .insert(doc_id.to_string(), document.clone()); - - // subscribe to the document changes. - self.subscribe_document_changes(document.clone(), doc_id)?; - Ok(document) } - pub fn subscribe_document_changes( - &self, - document: Arc, - doc_id: &str, - ) -> Result { - let mut document = document.lock(); - let doc_id = doc_id.to_string(); - document.open(move |events, is_remote| { - tracing::trace!( - "document changed: {:?}, from remote: {}", - &events, - is_remote + pub fn get_document_data(&self, doc_id: &str) -> FlowyResult { + if !self.is_doc_exist(doc_id)? { + return Err( + FlowyError::record_not_found().context(format!("document: {} is not exist", doc_id)), ); - // send notification to the client. - send_notification(&doc_id, DocumentNotification::DidReceiveUpdate) - .payload::((events, is_remote).into()) - .send(); - }) - } + } - /// get document - /// read the existing document from the disk. - pub fn get_document_from_disk(&self, doc_id: &str) -> FlowyResult> { - let uid = self.user.user_id()?; - let db = self.user.collab_db()?; - let collab = self.collab_builder.build(uid, doc_id, "document", db); - // read the existing document from the disk. - let document = Arc::new(Document::new(collab)?); - Ok(document) + let collab = self.collab_for_document(doc_id)?; + Document::open(collab)? + .get_document_data() + .map_err(internal_error) } pub fn close_document(&self, doc_id: &str) -> FlowyResult<()> { @@ -123,4 +106,46 @@ impl DocumentManager { self.documents.write().remove(doc_id); Ok(()) } + + /// Return the list of snapshots of the document. + pub async fn get_document_snapshots( + &self, + document_id: &str, + ) -> FlowyResult> { + let mut snapshots = vec![]; + if let Some(snapshot) = self + .cloud_service + .get_document_latest_snapshot(document_id) + .await? + .map(|snapshot| DocumentSnapshotPB { + snapshot_id: snapshot.snapshot_id, + snapshot_desc: "".to_string(), + created_at: snapshot.created_at, + data: snapshot.data, + }) + { + snapshots.push(snapshot); + } + + Ok(snapshots) + } + + fn collab_for_document(&self, doc_id: &str) -> FlowyResult> { + let uid = self.user.user_id()?; + let db = self.user.collab_db()?; + Ok(self.collab_builder.build(uid, doc_id, "document", db)) + } + + fn is_doc_exist(&self, doc_id: &str) -> FlowyResult { + let uid = self.user.user_id()?; + let db = self.user.collab_db()?; + let read_txn = db.read_txn(); + Ok(read_txn.is_exist(uid, doc_id)) + } + + /// Only expose this method for testing + #[cfg(debug_assertions)] + pub fn get_cloud_service(&self) -> &Arc { + &self.cloud_service + } } diff --git a/frontend/rust-lib/flowy-document2/src/notification.rs b/frontend/rust-lib/flowy-document2/src/notification.rs index bef774374f..b05e1a2669 100644 --- a/frontend/rust-lib/flowy-document2/src/notification.rs +++ b/frontend/rust-lib/flowy-document2/src/notification.rs @@ -1,7 +1,7 @@ use flowy_derive::ProtoBuf_Enum; use flowy_notification::NotificationBuilder; -const OBSERVABLE_CATEGORY: &str = "Document"; +const DOCUMENT_OBSERVABLE_SOURCE: &str = "Document"; #[derive(ProtoBuf_Enum, Debug, Default)] pub(crate) enum DocumentNotification { @@ -9,6 +9,8 @@ pub(crate) enum DocumentNotification { Unknown = 0, DidReceiveUpdate = 1, + DidUpdateDocumentSnapshotState = 2, + DidUpdateDocumentSyncState = 3, } impl std::convert::From for i32 { @@ -16,7 +18,17 @@ impl std::convert::From for i32 { notification as i32 } } +impl std::convert::From for DocumentNotification { + fn from(notification: i32) -> Self { + match notification { + 1 => DocumentNotification::DidReceiveUpdate, + 2 => DocumentNotification::DidUpdateDocumentSnapshotState, + 3 => DocumentNotification::DidUpdateDocumentSyncState, + _ => DocumentNotification::Unknown, + } + } +} pub(crate) fn send_notification(id: &str, ty: DocumentNotification) -> NotificationBuilder { - NotificationBuilder::new(id, ty, OBSERVABLE_CATEGORY) + NotificationBuilder::new(id, ty, DOCUMENT_OBSERVABLE_SOURCE) } 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 96eea2f372..060f52f001 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 @@ -1,12 +1,14 @@ use std::{collections::HashMap, vec}; +use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType}; + +use flowy_document2::document_data::PARAGRAPH_BLOCK_TYPE; + use crate::document::util; use crate::document::util::gen_id; -use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType}; -use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE; -#[test] -fn document_apply_insert_block_with_empty_parent_id() { +#[tokio::test] +async fn document_apply_insert_block_with_empty_parent_id() { let (_, document, page_id) = util::create_and_open_empty_document(); // create a text block with no parent 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 e7bc4e98ef..c3095e8cba 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 @@ -1,27 +1,23 @@ use std::collections::HashMap; -use std::sync::Arc; use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType}; -use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE; -use flowy_document2::document_data::default_document_data; -use flowy_document2::manager::DocumentManager; +use flowy_document2::document_data::{default_document_data, PARAGRAPH_BLOCK_TYPE}; -use crate::document::util::{default_collab_builder, gen_document_id, gen_id, FakeUser}; +use crate::document::util::{gen_document_id, gen_id, DocumentTest}; #[tokio::test] async fn undo_redo_test() { - let user = FakeUser::new(); - let manager = DocumentManager::new(Arc::new(user), default_collab_builder()); + let test = DocumentTest::new(); let doc_id: String = gen_document_id(); let data = default_document_data(); // create a document - _ = manager.create_document(&doc_id, Some(data.clone())); + _ = test.create_document(&doc_id, Some(data.clone())); // open a document - let document = manager.get_or_open_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).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 7b09c24ad6..06a9c64208 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/document_test.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/document_test.rs @@ -1,69 +1,60 @@ -use std::{collections::HashMap, sync::Arc, vec}; +use std::{collections::HashMap, vec}; use collab_document::blocks::{Block, BlockAction, BlockActionPayload, BlockActionType}; use serde_json::{json, to_value, Value}; -use flowy_document2::document_block_keys::PARAGRAPH_BLOCK_TYPE; -use flowy_document2::document_data::default_document_data; -use flowy_document2::manager::DocumentManager; +use flowy_document2::document_data::{default_document_data, PARAGRAPH_BLOCK_TYPE}; -use crate::document::util::{default_collab_builder, gen_document_id, gen_id}; +use crate::document::util::{gen_document_id, gen_id, DocumentTest}; -use super::util::FakeUser; - -#[test] -fn restore_document() { - let user = FakeUser::new(); - let manager = DocumentManager::new(Arc::new(user), default_collab_builder()); +#[tokio::test] +async fn restore_document() { + let test = DocumentTest::new(); // create a document let doc_id: String = gen_document_id(); let data = default_document_data(); - let document_a = manager - .create_document(&doc_id, Some(data.clone())) - .unwrap(); - let data_a = document_a.lock().get_document().unwrap(); + let document_a = test.create_document(&doc_id, Some(data.clone())).unwrap(); + let data_a = document_a.lock().get_document_data().unwrap(); assert_eq!(data_a, data); // open a document - let data_b = manager - .get_or_open_document(&doc_id) + let data_b = test + .get_document(&doc_id) .unwrap() .lock() - .get_document() + .get_document_data() .unwrap(); // close a document - _ = manager.close_document(&doc_id); + _ = test.close_document(&doc_id); assert_eq!(data_b, data); // restore - _ = manager.create_document(&doc_id, Some(data.clone())); + _ = test.create_document(&doc_id, Some(data.clone())); // open a document - let data_b = manager - .get_or_open_document(&doc_id) + let data_b = test + .get_document(&doc_id) .unwrap() .lock() - .get_document() + .get_document_data() .unwrap(); // close a document - _ = manager.close_document(&doc_id); + _ = test.close_document(&doc_id); assert_eq!(data_b, data); } -#[test] -fn document_apply_insert_action() { - let user = FakeUser::new(); - let manager = DocumentManager::new(Arc::new(user), default_collab_builder()); - +#[tokio::test] +async fn document_apply_insert_action() { + let test = DocumentTest::new(); let doc_id: String = gen_document_id(); let data = default_document_data(); // create a document - _ = manager.create_document(&doc_id, Some(data.clone())); + _ = test.create_document(&doc_id, Some(data.clone())); // open a document - let document = manager.get_or_open_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).unwrap(); let page_block = document.lock().get_block(&data.page_id).unwrap(); // insert a text block @@ -85,36 +76,34 @@ fn document_apply_insert_action() { }, }; document.lock().apply_action(vec![insert_text_action]); - let data_a = document.lock().get_document().unwrap(); + let data_a = document.lock().get_document_data().unwrap(); // close the original document - _ = manager.close_document(&doc_id); + _ = test.close_document(&doc_id); // re-open the document - let data_b = manager - .get_or_open_document(&doc_id) + let data_b = test + .get_document(&doc_id) .unwrap() .lock() - .get_document() + .get_document_data() .unwrap(); // close a document - _ = manager.close_document(&doc_id); + _ = test.close_document(&doc_id); assert_eq!(data_b, data_a); } -#[test] -fn document_apply_update_page_action() { - let user = FakeUser::new(); - let manager = DocumentManager::new(Arc::new(user), default_collab_builder()); - +#[tokio::test] +async fn document_apply_update_page_action() { + let test = DocumentTest::new(); let doc_id: String = gen_document_id(); let data = default_document_data(); // create a document - _ = manager.create_document(&doc_id, Some(data.clone())); + _ = test.create_document(&doc_id, Some(data.clone())); // open a document - let document = manager.get_or_open_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).unwrap(); let page_block = document.lock().get_block(&data.page_id).unwrap(); let mut page_block_clone = page_block; @@ -135,28 +124,26 @@ fn document_apply_update_page_action() { tracing::trace!("{:?}", &actions); document.lock().apply_action(actions); let page_block_old = document.lock().get_block(&data.page_id).unwrap(); - _ = manager.close_document(&doc_id); + _ = test.close_document(&doc_id); // re-open the document - let document = manager.get_or_open_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).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")); } -#[test] -fn document_apply_update_action() { - let user = FakeUser::new(); - let manager = DocumentManager::new(Arc::new(user), default_collab_builder()); - +#[tokio::test] +async fn document_apply_update_action() { + let test = DocumentTest::new(); let doc_id: String = gen_document_id(); let data = default_document_data(); // create a document - _ = manager.create_document(&doc_id, Some(data.clone())); + _ = test.create_document(&doc_id, Some(data.clone())); // open a document - let document = manager.get_or_open_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).unwrap(); let page_block = document.lock().get_block(&data.page_id).unwrap(); // insert a text block @@ -203,12 +190,12 @@ fn document_apply_update_action() { }; document.lock().apply_action(vec![update_text_action]); // close the original document - _ = manager.close_document(&doc_id); + _ = test.close_document(&doc_id); // re-open the document - let document = manager.get_or_open_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).unwrap(); let block = document.lock().get_block(&text_block_id).unwrap(); assert_eq!(block.data, updated_text_block_data); // close a document - _ = manager.close_document(&doc_id); + _ = test.close_document(&doc_id); } diff --git a/frontend/rust-lib/flowy-document2/tests/document/util.rs b/frontend/rust-lib/flowy-document2/tests/document/util.rs index d822e34cca..46a1cc72d5 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/util.rs @@ -1,16 +1,40 @@ -use appflowy_integrate::collab_builder::{AppFlowyCollabBuilder, CloudStorageType}; - +use std::ops::Deref; use std::sync::Arc; +use appflowy_integrate::collab_builder::{AppFlowyCollabBuilder, DefaultCollabStorageProvider}; use appflowy_integrate::RocksCollabDB; -use flowy_document2::document::Document; +use nanoid::nanoid; use parking_lot::Once; use tempfile::TempDir; use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter}; +use flowy_document2::deps::{DocumentCloudService, DocumentSnapshot, DocumentUser}; +use flowy_document2::document::MutexDocument; use flowy_document2::document_data::default_document_data; -use flowy_document2::manager::{DocumentManager, DocumentUser}; -use nanoid::nanoid; +use flowy_document2::manager::DocumentManager; +use flowy_error::FlowyError; +use lib_infra::future::FutureResult; + +pub struct DocumentTest { + inner: DocumentManager, +} + +impl DocumentTest { + pub fn new() -> Self { + let user = FakeUser::new(); + let cloud_service = Arc::new(LocalTestDocumentCloudServiceImpl()); + let manager = DocumentManager::new(Arc::new(user), default_collab_builder(), cloud_service); + Self { inner: manager } + } +} + +impl Deref for DocumentTest { + type Target = DocumentManager; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} pub struct FakeUser { kv: Arc, @@ -53,25 +77,21 @@ pub fn db() -> Arc { } pub fn default_collab_builder() -> Arc { - let builder = AppFlowyCollabBuilder::new(CloudStorageType::Local, None); + let builder = AppFlowyCollabBuilder::new(DefaultCollabStorageProvider(), None); Arc::new(builder) } -pub fn create_and_open_empty_document() -> (DocumentManager, Arc, String) { - let user = FakeUser::new(); - let manager = DocumentManager::new(Arc::new(user), default_collab_builder()); - +pub 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(); // create a document - _ = manager - .create_document(&doc_id, Some(data.clone())) - .unwrap(); + _ = test.create_document(&doc_id, Some(data.clone())).unwrap(); - let document = manager.get_or_open_document(&doc_id).unwrap(); + let document = test.get_document(&doc_id).unwrap(); - (manager, document, data.page_id) + (test, document, data.page_id) } pub fn gen_document_id() -> String { @@ -82,3 +102,17 @@ pub fn gen_document_id() -> String { pub fn gen_id() -> String { nanoid!(10) } + +pub struct LocalTestDocumentCloudServiceImpl(); +impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { + fn get_document_updates(&self, _document_id: &str) -> FutureResult>, FlowyError> { + FutureResult::new(async move { Ok(vec![]) }) + } + + fn get_document_latest_snapshot( + &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 4ecc2f492b..24ad9e37f0 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -149,8 +149,8 @@ pub enum ErrorCode { #[error("Invalid date time format")] InvalidDateTimeFormat = 47, - #[error("Invalid data")] - InvalidData = 49, + #[error("Invalid params")] + InvalidParams = 49, #[error("Serde")] Serde = 50, @@ -208,6 +208,12 @@ pub enum ErrorCode { #[error("Apply actions is empty")] ApplyActionsIsEmpty = 68, + + #[error("Connect postgres database failed")] + PgConnectError = 69, + + #[error("Postgres database error")] + PgDatabaseError = 70, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index fe8240ee58..411ffa1ae7 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -79,7 +79,7 @@ impl FlowyError { static_flowy_error!(user_id, ErrorCode::UserIdInvalid); static_flowy_error!(user_not_exist, ErrorCode::UserNotExist); static_flowy_error!(text_too_long, ErrorCode::TextTooLong); - static_flowy_error!(invalid_data, ErrorCode::InvalidData); + static_flowy_error!(invalid_data, ErrorCode::InvalidParams); static_flowy_error!(out_of_bounds, ErrorCode::OutOfBounds); static_flowy_error!(serde, ErrorCode::Serde); static_flowy_error!(field_record_not_found, ErrorCode::FieldRecordNotFound); diff --git a/frontend/rust-lib/flowy-folder2/Cargo.toml b/frontend/rust-lib/flowy-folder2/Cargo.toml index e4939fefa7..86d7fb74fd 100644 --- a/frontend/rust-lib/flowy-folder2/Cargo.toml +++ b/frontend/rust-lib/flowy-folder2/Cargo.toml @@ -31,7 +31,7 @@ tokio-stream = { version = "0.1.14", features = ["sync"] } [dev-dependencies] flowy-folder2 = { path = "../flowy-folder2"} -flowy-test = { path = "../flowy-test" } +flowy-test = { path = "../flowy-test", default-features = false } [build-dependencies] flowy-codegen = { path = "../../../shared-lib/flowy-codegen"} diff --git a/frontend/rust-lib/flowy-folder2/src/deps.rs b/frontend/rust-lib/flowy-folder2/src/deps.rs index 3e46e4d127..ecb4eccf48 100644 --- a/frontend/rust-lib/flowy-folder2/src/deps.rs +++ b/frontend/rust-lib/flowy-folder2/src/deps.rs @@ -14,4 +14,18 @@ pub trait FolderUser: Send + Sync { /// [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_latest_snapshot( + &self, + workspace_id: &str, + ) -> FutureResult, FlowyError>; + + fn get_folder_updates(&self, workspace_id: &str) -> FutureResult>, FlowyError>; +} + +pub struct FolderSnapshot { + pub snapshot_id: i64, + pub database_id: String, + pub data: Vec, + pub created_at: i64, } diff --git a/frontend/rust-lib/flowy-folder2/src/entities/workspace.rs b/frontend/rust-lib/flowy-folder2/src/entities/workspace.rs index 538fc29bbd..2d5e2ff29a 100644 --- a/frontend/rust-lib/flowy-folder2/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-folder2/src/entities/workspace.rs @@ -2,6 +2,7 @@ use crate::{ entities::parser::workspace::{WorkspaceDesc, WorkspaceIdentify, WorkspaceName}, entities::view::ViewPB, }; +use collab::core::collab_state::SyncState; use collab_folder::core::Workspace; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; @@ -151,3 +152,48 @@ impl TryInto for UpdateWorkspacePayloadPB { }) } } + +#[derive(Debug, Default, ProtoBuf)] +pub struct RepeatedFolderSnapshotPB { + #[pb(index = 1)] + pub items: Vec, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct FolderSnapshotPB { + #[pb(index = 1)] + pub snapshot_id: i64, + + #[pb(index = 2)] + pub snapshot_desc: String, + + #[pb(index = 3)] + pub created_at: i64, + + #[pb(index = 4)] + pub data: Vec, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct FolderSnapshotStatePB { + #[pb(index = 1)] + pub new_snapshot_id: i64, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct FolderSyncStatePB { + #[pb(index = 1)] + pub is_syncing: bool, + + #[pb(index = 2)] + pub is_finish: bool, +} + +impl From for FolderSyncStatePB { + fn from(value: SyncState) -> Self { + Self { + is_syncing: value.is_syncing(), + is_finish: value.is_sync_finished(), + } + } +} diff --git a/frontend/rust-lib/flowy-folder2/src/event_handler.rs b/frontend/rust-lib/flowy-folder2/src/event_handler.rs index 20a3c6d0c6..1d982d1936 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_handler.rs @@ -4,13 +4,13 @@ use flowy_error::FlowyError; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; use crate::entities::*; -use crate::manager::Folder2Manager; +use crate::manager::FolderManager; use crate::share::ImportParams; #[tracing::instrument(level = "debug", skip(data, folder), err)] pub(crate) async fn create_workspace_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> DataResult { let params: CreateWorkspaceParams = data.into_inner().try_into()?; let workspace = folder.create_workspace(params).await?; @@ -19,7 +19,7 @@ pub(crate) async fn create_workspace_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_workspace_views_handler( - folder: AFPluginState>, + folder: AFPluginState>, ) -> DataResult { let child_views = folder.get_current_workspace_views().await?; let repeated_view: RepeatedViewPB = child_views.into(); @@ -29,7 +29,7 @@ pub(crate) async fn read_workspace_views_handler( #[tracing::instrument(level = "debug", skip(data, folder), err)] pub(crate) async fn open_workspace_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> DataResult { let params: WorkspaceIdPB = data.into_inner(); match params.value { @@ -50,7 +50,7 @@ pub(crate) async fn open_workspace_handler( #[tracing::instrument(level = "debug", skip(data, folder), err)] pub(crate) async fn read_workspaces_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> DataResult { let params: WorkspaceIdPB = data.into_inner(); let workspaces = match params.value { @@ -67,7 +67,7 @@ pub(crate) async fn read_workspaces_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub async fn read_current_workspace_setting_handler( - folder: AFPluginState>, + folder: AFPluginState>, ) -> DataResult { let workspace = folder.get_current_workspace().await?; let latest_view: Option = folder.get_current_view().await; @@ -79,7 +79,7 @@ pub async fn read_current_workspace_setting_handler( pub(crate) async fn create_view_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> DataResult { let params: CreateViewParams = data.into_inner().try_into()?; let set_as_current = params.set_as_current; @@ -92,7 +92,7 @@ pub(crate) async fn create_view_handler( pub(crate) async fn create_orphan_view_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> DataResult { let params: CreateViewParams = data.into_inner().try_into()?; let set_as_current = params.set_as_current; @@ -105,7 +105,7 @@ pub(crate) async fn create_orphan_view_handler( pub(crate) async fn read_view_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> DataResult { let view_id: ViewIdPB = data.into_inner(); let view_pb = folder.get_view(&view_id.value).await?; @@ -115,7 +115,7 @@ pub(crate) async fn read_view_handler( #[tracing::instrument(level = "debug", skip(data, folder), err)] pub(crate) async fn update_view_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> Result<(), FlowyError> { let params: UpdateViewParams = data.into_inner().try_into()?; folder.update_view_with_params(params).await?; @@ -124,7 +124,7 @@ pub(crate) async fn update_view_handler( pub(crate) async fn delete_view_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> Result<(), FlowyError> { let params: RepeatedViewIdPB = data.into_inner(); for view_id in ¶ms.items { @@ -135,7 +135,7 @@ pub(crate) async fn delete_view_handler( pub(crate) async fn set_latest_view_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> Result<(), FlowyError> { let view_id: ViewIdPB = data.into_inner(); let _ = folder.set_current_view(&view_id.value).await; @@ -144,7 +144,7 @@ pub(crate) async fn set_latest_view_handler( pub(crate) async fn close_view_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> Result<(), FlowyError> { let view_id: ViewIdPB = data.into_inner(); let _ = folder.close_view(&view_id.value).await; @@ -154,7 +154,7 @@ pub(crate) async fn close_view_handler( #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn move_view_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> Result<(), FlowyError> { let params: MoveViewParams = data.into_inner().try_into()?; folder @@ -166,7 +166,7 @@ pub(crate) async fn move_view_handler( #[tracing::instrument(level = "debug", skip(data, folder), err)] pub(crate) async fn duplicate_view_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> Result<(), FlowyError> { let view: ViewPB = data.into_inner(); folder.duplicate_view(&view.id).await?; @@ -175,7 +175,7 @@ pub(crate) async fn duplicate_view_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_trash_handler( - folder: AFPluginState>, + folder: AFPluginState>, ) -> DataResult { let trash = folder.get_all_trash().await; data_result_ok(trash.into()) @@ -184,7 +184,7 @@ pub(crate) async fn read_trash_handler( #[tracing::instrument(level = "debug", skip(identifier, folder), err)] pub(crate) async fn putback_trash_handler( identifier: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> Result<(), FlowyError> { folder.restore_trash(&identifier.id).await; Ok(()) @@ -193,7 +193,7 @@ pub(crate) async fn putback_trash_handler( #[tracing::instrument(level = "debug", skip(identifiers, folder), err)] pub(crate) async fn delete_trash_handler( identifiers: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> Result<(), FlowyError> { let trash_ids = identifiers.into_inner().items; for trash_id in trash_ids { @@ -204,7 +204,7 @@ pub(crate) async fn delete_trash_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn restore_all_trash_handler( - folder: AFPluginState>, + folder: AFPluginState>, ) -> Result<(), FlowyError> { folder.restore_all_trash().await; Ok(()) @@ -212,7 +212,7 @@ pub(crate) async fn restore_all_trash_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn delete_all_trash_handler( - folder: AFPluginState>, + folder: AFPluginState>, ) -> Result<(), FlowyError> { folder.delete_all_trash().await; Ok(()) @@ -221,9 +221,22 @@ pub(crate) async fn delete_all_trash_handler( #[tracing::instrument(level = "debug", skip(data, folder), err)] pub(crate) async fn import_data_handler( data: AFPluginData, - folder: AFPluginState>, + folder: AFPluginState>, ) -> Result<(), FlowyError> { let params: ImportParams = data.into_inner().try_into()?; folder.import(params).await?; Ok(()) } + +#[tracing::instrument(level = "debug", skip(folder), err)] +pub(crate) async fn get_folder_snapshots_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> DataResult { + if let Some(workspace_id) = &data.value { + let snapshots = folder.get_folder_snapshots(workspace_id).await?; + data_result_ok(RepeatedFolderSnapshotPB { items: snapshots }) + } else { + data_result_ok(RepeatedFolderSnapshotPB { items: vec![] }) + } +} diff --git a/frontend/rust-lib/flowy-folder2/src/event_map.rs b/frontend/rust-lib/flowy-folder2/src/event_map.rs index 652fef9b76..d4f05ed708 100644 --- a/frontend/rust-lib/flowy-folder2/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder2/src/event_map.rs @@ -6,9 +6,9 @@ use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use lib_dispatch::prelude::*; use crate::event_handler::*; -use crate::manager::Folder2Manager; +use crate::manager::FolderManager; -pub fn init(folder: Arc) -> AFPlugin { +pub fn init(folder: Arc) -> AFPlugin { AFPlugin::new().name("Flowy-Folder").state(folder) // Workspace .event(FolderEvent::CreateWorkspace, create_workspace_handler) @@ -36,6 +36,7 @@ pub fn init(folder: Arc) -> AFPlugin { .event(FolderEvent::RestoreAllTrash, restore_all_trash_handler) .event(FolderEvent::DeleteAllTrash, delete_all_trash_handler) .event(FolderEvent::ImportData, import_data_handler) + .event(FolderEvent::GetFolderSnapshots, get_folder_snapshots_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -128,4 +129,7 @@ pub enum FolderEvent { #[event(input = "ImportPB")] ImportData = 30, + + #[event()] + GetFolderSnapshots = 31, } diff --git a/frontend/rust-lib/flowy-folder2/src/manager.rs b/frontend/rust-lib/flowy-folder2/src/manager.rs index 7c6f82a5d8..84f227fb98 100644 --- a/frontend/rust-lib/flowy-folder2/src/manager.rs +++ b/frontend/rust-lib/flowy-folder2/src/manager.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Weak}; use appflowy_integrate::collab_builder::AppFlowyCollabBuilder; use appflowy_integrate::CollabPersistenceConfig; -use collab::core::collab_state::CollabState; +use collab::core::collab_state::SyncState; use collab_folder::core::{ Folder, FolderContext, TrashChange, TrashChangeReceiver, TrashInfo, View, ViewChange, ViewChangeReceiver, ViewLayout, Workspace, @@ -19,8 +19,8 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use crate::deps::{FolderCloudService, FolderUser}; use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, CreateViewParams, - CreateWorkspaceParams, DeletedViewPB, RepeatedTrashPB, RepeatedViewPB, RepeatedWorkspacePB, - UpdateViewParams, ViewPB, WorkspacePB, + CreateWorkspaceParams, DeletedViewPB, FolderSnapshotPB, FolderSnapshotStatePB, FolderSyncStatePB, + RepeatedTrashPB, RepeatedViewPB, RepeatedWorkspacePB, UpdateViewParams, ViewPB, WorkspacePB, }; use crate::notification::{ send_notification, send_workspace_notification, send_workspace_setting_notification, @@ -32,7 +32,7 @@ use crate::view_operation::{ create_view, gen_view_id, FolderOperationHandler, FolderOperationHandlers, }; -pub struct Folder2Manager { +pub struct FolderManager { mutex_folder: Arc, collab_builder: Arc, user: Arc, @@ -40,10 +40,10 @@ pub struct Folder2Manager { cloud_service: Arc, } -unsafe impl Send for Folder2Manager {} -unsafe impl Sync for Folder2Manager {} +unsafe impl Send for FolderManager {} +unsafe impl Sync for FolderManager {} -impl Folder2Manager { +impl FolderManager { pub async fn new( user: Arc, collab_builder: Arc, @@ -134,13 +134,18 @@ impl Folder2Manager { trash_change_tx: trash_tx, }; let folder = Folder::get_or_create(collab, folder_context); - let folder_state_rx = folder.subscribe_state_change(); + let folder_state_rx = folder.subscribe_sync_state(); *self.mutex_folder.lock() = Some(folder); let weak_mutex_folder = Arc::downgrade(&self.mutex_folder); - listen_on_folder_state_change(workspace_id, folder_state_rx, &weak_mutex_folder); - listen_on_trash_change(trash_rx, &weak_mutex_folder); - listen_on_view_change(view_rx, &weak_mutex_folder); + subscribe_folder_sync_state_changed( + workspace_id.clone(), + folder_state_rx, + &weak_mutex_folder, + ); + subscribe_folder_snapshot_state_changed(workspace_id, &weak_mutex_folder); + subscribe_folder_trash_changed(trash_rx, &weak_mutex_folder); + subscribe_folder_view_changed(view_rx, &weak_mutex_folder); } Ok(()) @@ -151,24 +156,30 @@ impl Folder2Manager { &self, user_id: i64, token: &str, + is_new: bool, workspace_id: &str, ) -> FlowyResult<()> { self.initialize(user_id, workspace_id).await?; - 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); - }); - send_notification(token, FolderNotification::DidCreateWorkspace) - .payload(RepeatedWorkspacePB { - items: vec![workspace_pb], - }) - .send(); + // Create the default workspace if the user is new + tracing::info!("initialize_with_user: 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); + }); + + send_notification(token, FolderNotification::DidCreateWorkspace) + .payload(RepeatedWorkspacePB { + items: vec![workspace_pb], + }) + .send(); + } Ok(()) } @@ -540,7 +551,7 @@ impl Folder2Manager { pub(crate) async fn import(&self, import_data: ImportParams) -> FlowyResult { if import_data.data.is_none() && import_data.file_path.is_none() { return Err(FlowyError::new( - ErrorCode::InvalidData, + ErrorCode::InvalidParams, "data or file_path is required", )); } @@ -626,10 +637,47 @@ impl Folder2Manager { } }) } + + pub async fn get_folder_snapshots( + &self, + workspace_id: &str, + ) -> FlowyResult> { + let mut snapshots = vec![]; + if let Some(snapshot) = self + .cloud_service + .get_folder_latest_snapshot(workspace_id) + .await? + .map(|snapshot| FolderSnapshotPB { + snapshot_id: snapshot.snapshot_id, + snapshot_desc: "".to_string(), + created_at: snapshot.created_at, + data: snapshot.data, + }) + { + snapshots.push(snapshot); + } + + Ok(snapshots) + } + + /// Only expose this method for testing + #[cfg(debug_assertions)] + pub fn get_mutex_folder(&self) -> &Arc { + &self.mutex_folder + } + + /// Only expose this method for testing + #[cfg(debug_assertions)] + pub fn get_cloud_service(&self) -> &Arc { + &self.cloud_service + } } /// Listen on the [ViewChange] after create/delete/update events happened -fn listen_on_view_change(mut rx: ViewChangeReceiver, weak_mutex_folder: &Weak) { +fn subscribe_folder_view_changed( + mut rx: ViewChangeReceiver, + weak_mutex_folder: &Weak, +) { let weak_mutex_folder = weak_mutex_folder.clone(); tokio::spawn(async move { while let Ok(value) = rx.recv().await { @@ -664,15 +712,43 @@ fn listen_on_view_change(mut rx: ViewChangeReceiver, weak_mutex_folder: &Weak, + weak_mutex_folder: &Weak, +) { + let weak_mutex_folder = weak_mutex_folder.clone(); + tokio::spawn(async move { + if let Some(mutex_folder) = weak_mutex_folder.upgrade() { + let stream = mutex_folder + .lock() + .as_ref() + .map(|folder| folder.subscribe_snapshot_state()); + 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); + send_notification( + &workspace_id, + FolderNotification::DidUpdateFolderSnapshotState, + ) + .payload(FolderSnapshotStatePB { new_snapshot_id }) + .send(); + } + } + } + } + }); +} + +fn subscribe_folder_sync_state_changed( + workspace_id: String, + mut folder_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_root_changed() { + 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 { @@ -683,12 +759,19 @@ fn listen_on_folder_state_change( } } } + + send_notification(&workspace_id, FolderNotification::DidUpdateFolderSyncUpdate) + .payload(FolderSyncStatePB::from(state)) + .send(); } }); } /// Listen on the [TrashChange]s and notify the frontend some views were changed. -fn listen_on_trash_change(mut rx: TrashChangeReceiver, weak_mutex_folder: &Weak) { +fn subscribe_folder_trash_changed( + mut rx: TrashChangeReceiver, + weak_mutex_folder: &Weak, +) { let weak_mutex_folder = weak_mutex_folder.clone(); tokio::spawn(async move { while let Ok(value) = rx.recv().await { diff --git a/frontend/rust-lib/flowy-folder2/src/notification.rs b/frontend/rust-lib/flowy-folder2/src/notification.rs index efdde7de6b..e531fa2fcf 100644 --- a/frontend/rust-lib/flowy-folder2/src/notification.rs +++ b/frontend/rust-lib/flowy-folder2/src/notification.rs @@ -8,7 +8,7 @@ use lib_dispatch::prelude::ToBytes; use crate::entities::{view_pb_without_child_views, WorkspacePB, WorkspaceSettingPB}; -const OBSERVABLE_CATEGORY: &str = "Workspace"; +const FOLDER_OBSERVABLE_SOURCE: &str = "Workspace"; #[derive(ProtoBuf_Enum, Debug, Default)] pub(crate) enum FolderNotification { @@ -22,16 +22,18 @@ pub(crate) enum FolderNotification { DidUpdateWorkspaceViews = 3, /// Trigger when the settings of the workspace are changed. The changes including the latest visiting view, etc DidUpdateWorkspaceSetting = 4, - DidUpdateView = 29, - DidUpdateChildViews = 30, + DidUpdateView = 10, + DidUpdateChildViews = 11, /// Trigger after deleting the view - DidDeleteView = 31, + DidDeleteView = 12, /// Trigger when restore the view from trash - DidRestoreView = 32, + DidRestoreView = 13, /// Trigger after moving the view to trash - DidMoveViewToTrash = 33, + DidMoveViewToTrash = 14, /// Trigger when the number of trash is changed - DidUpdateTrash = 34, + DidUpdateTrash = 15, + DidUpdateFolderSnapshotState = 16, + DidUpdateFolderSyncUpdate = 17, } impl std::convert::From for i32 { @@ -40,9 +42,29 @@ impl std::convert::From for i32 { } } +impl std::convert::From for FolderNotification { + fn from(value: i32) -> Self { + match value { + 1 => FolderNotification::DidCreateWorkspace, + 2 => FolderNotification::DidUpdateWorkspace, + 3 => FolderNotification::DidUpdateWorkspaceViews, + 4 => FolderNotification::DidUpdateWorkspaceSetting, + 10 => FolderNotification::DidUpdateView, + 11 => FolderNotification::DidUpdateChildViews, + 12 => FolderNotification::DidDeleteView, + 13 => FolderNotification::DidRestoreView, + 14 => FolderNotification::DidMoveViewToTrash, + 15 => FolderNotification::DidUpdateTrash, + 16 => FolderNotification::DidUpdateFolderSnapshotState, + 17 => FolderNotification::DidUpdateFolderSyncUpdate, + _ => FolderNotification::Unknown, + } + } +} + #[tracing::instrument(level = "trace")] pub(crate) fn send_notification(id: &str, ty: FolderNotification) -> NotificationBuilder { - NotificationBuilder::new(id, ty, OBSERVABLE_CATEGORY) + NotificationBuilder::new(id, ty, FOLDER_OBSERVABLE_SOURCE) } /// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the diff --git a/frontend/rust-lib/flowy-folder2/src/test_helper.rs b/frontend/rust-lib/flowy-folder2/src/test_helper.rs index 9418974a4d..fbd95ee832 100644 --- a/frontend/rust-lib/flowy-folder2/src/test_helper.rs +++ b/frontend/rust-lib/flowy-folder2/src/test_helper.rs @@ -1,10 +1,10 @@ use crate::entities::{CreateViewParams, ViewLayoutPB}; -use crate::manager::Folder2Manager; +use crate::manager::FolderManager; use crate::view_operation::gen_view_id; use std::collections::HashMap; #[cfg(feature = "test_helper")] -impl Folder2Manager { +impl FolderManager { pub async fn create_test_grid_view( &self, app_id: &str, diff --git a/frontend/rust-lib/flowy-notification/src/lib.rs b/frontend/rust-lib/flowy-notification/src/lib.rs index 40b722af4a..5ce28e68c9 100644 --- a/frontend/rust-lib/flowy-notification/src/lib.rs +++ b/frontend/rust-lib/flowy-notification/src/lib.rs @@ -1,11 +1,14 @@ -pub mod entities; -mod protobuf; +use std::sync::RwLock; -use crate::entities::SubscribeObject; use bytes::Bytes; use lazy_static::lazy_static; + use lib_dispatch::prelude::ToBytes; -use std::sync::RwLock; + +use crate::entities::SubscribeObject; + +pub mod entities; +mod protobuf; lazy_static! { static ref NOTIFICATION_SENDER: RwLock>> = RwLock::new(vec![]); @@ -14,10 +17,7 @@ lazy_static! { pub fn register_notification_sender(sender: T) { let box_sender = Box::new(sender); match NOTIFICATION_SENDER.write() { - Ok(mut write_guard) => { - write_guard.pop(); - write_guard.push(box_sender) - }, + Ok(mut write_guard) => write_guard.push(box_sender), Err(err) => tracing::error!("Failed to push notification sender: {:?}", err), } } diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index 062cbb0f55..3b390b8f7b 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -20,17 +20,31 @@ tokio = { version = "1.26", features = ["sync"]} parking_lot = "0.12" lazy_static = "1.4.0" bytes = "1.0.1" -postgrest = "1.0" tokio-retry = "0.3" anyhow = "1.0" uuid = { version = "1.3.3", features = ["v4"] } chrono = { version = "0.4.22", default-features = false, features = ["clock"] } +appflowy-integrate = { version = "0.1.0" } + +postgrest = "1.0" +tokio-postgres = { version = "0.7.8", optional = true, features = ["with-uuid-1","with-chrono-0_4"] } +deadpool-postgres = "0.10.5" +refinery= { version = "0.8.10", optional = true, features = ["tokio-postgres"] } +async-stream = "0.3.4" +futures = "0.3.26" lib-infra = { path = "../../../shared-lib/lib-infra" } flowy-user = { path = "../flowy-user" } flowy-folder2 = { path = "../flowy-folder2" } +flowy-database2 = { path = "../flowy-database2" } +flowy-document2 = { path = "../flowy-document2" } flowy-error = { path = "../flowy-error" } [dev-dependencies] uuid = { version = "1.3.3", features = ["v4"] } +tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } dotenv = "0.15.0" + +[features] +default = ["postgres_storage"] +postgres_storage = ["tokio-postgres", "refinery", ] diff --git a/frontend/rust-lib/flowy-server/src/lib.rs b/frontend/rust-lib/flowy-server/src/lib.rs index 34bf7b9b23..25cb9c448f 100644 --- a/frontend/rust-lib/flowy-server/src/lib.rs +++ b/frontend/rust-lib/flowy-server/src/lib.rs @@ -1,6 +1,10 @@ -use flowy_folder2::deps::FolderCloudService; use std::sync::Arc; +use appflowy_integrate::RemoteCollabStorage; + +use flowy_database2::deps::DatabaseCloudService; +use flowy_document2::deps::DocumentCloudService; +use flowy_folder2::deps::FolderCloudService; use flowy_user::event_map::UserAuthService; pub mod local_server; @@ -8,6 +12,7 @@ mod request; mod response; pub mod self_host; pub mod supabase; +pub mod util; /// In order to run this the supabase test, you need to create a .env file in the root directory of this project /// and add the following environment variables: @@ -26,4 +31,7 @@ pub mod supabase; pub trait AppFlowyServer: Send + Sync + 'static { fn user_service(&self) -> Arc; fn folder_service(&self) -> Arc; + fn database_service(&self) -> Arc; + fn document_service(&self) -> Arc; + fn collab_storage(&self) -> Option>; } 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 new file mode 100644 index 0000000000..e01222dffd --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -0,0 +1,18 @@ +use flowy_database2::deps::{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> { + FutureResult::new(async move { Ok(vec![]) }) + } + + fn get_database_latest_snapshot( + &self, + _database_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 new file mode 100644 index 0000000000..6a52c1f0c9 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs @@ -0,0 +1,18 @@ +use flowy_document2::deps::{DocumentCloudService, DocumentSnapshot}; +use flowy_error::FlowyError; +use lib_infra::future::FutureResult; + +pub(crate) struct LocalServerDocumentCloudServiceImpl(); + +impl DocumentCloudService for LocalServerDocumentCloudServiceImpl { + fn get_document_updates(&self, _document_id: &str) -> FutureResult>, FlowyError> { + FutureResult::new(async move { Ok(vec![]) }) + } + + fn get_document_latest_snapshot( + &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 39ee81d691..026246aa27 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,5 +1,5 @@ use flowy_error::FlowyError; -use flowy_folder2::deps::{FolderCloudService, Workspace}; +use flowy_folder2::deps::{FolderCloudService, FolderSnapshot, Workspace}; use flowy_folder2::gen_workspace_id; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; @@ -18,4 +18,15 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { }) }) } + + fn get_folder_latest_snapshot( + &self, + _workspace_id: &str, + ) -> FutureResult, FlowyError> { + FutureResult::new(async move { Ok(None) }) + } + + fn get_folder_updates(&self, _workspace_id: &str) -> FutureResult>, FlowyError> { + FutureResult::new(async move { Ok(vec![]) }) + } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs index 8a05c27f6b..0280cfbefb 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs @@ -1,5 +1,9 @@ -mod folder; -mod user; - +pub(crate) use database::*; +pub(crate) use document::*; pub(crate) use folder::*; pub(crate) use user::*; + +mod database; +mod document; +mod folder; +mod user; 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 95077bbcc5..606b3f4e78 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 @@ -5,7 +5,7 @@ use flowy_error::FlowyError; use flowy_user::entities::{ SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfile, }; -use flowy_user::event_map::UserAuthService; +use flowy_user::event_map::{UserAuthService, UserCredentials}; use lib_infra::box_any::BoxAny; use lib_infra::future::FutureResult; @@ -27,6 +27,7 @@ impl UserAuthService for LocalServerUserAuthServiceImpl { user_id: uid, name: params.name, workspace_id, + is_new: true, email: Some(params.email), token: None, }) @@ -54,8 +55,7 @@ impl UserAuthService for LocalServerUserAuthServiceImpl { fn update_user( &self, - _uid: i64, - _token: &Option, + _credential: UserCredentials, _params: UpdateUserProfileParams, ) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) @@ -63,9 +63,12 @@ impl UserAuthService for LocalServerUserAuthServiceImpl { fn get_user_profile( &self, - _token: Option, - _uid: i64, + _credential: UserCredentials, ) -> FutureResult, FlowyError> { FutureResult::new(async { Ok(None) }) } + + fn check_user(&self, _credential: UserCredentials) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) + } } 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 added89985..b9254674a9 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,12 +1,16 @@ use std::sync::Arc; -use flowy_folder2::deps::FolderCloudService; +use appflowy_integrate::RemoteCollabStorage; use parking_lot::RwLock; use tokio::sync::mpsc; +use flowy_database2::deps::DatabaseCloudService; +use flowy_document2::deps::DocumentCloudService; +use flowy_folder2::deps::FolderCloudService; use flowy_user::event_map::UserAuthService; use crate::local_server::impls::{ + LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, LocalServerFolderCloudServiceImpl, LocalServerUserAuthServiceImpl, }; use crate::AppFlowyServer; @@ -38,4 +42,16 @@ impl AppFlowyServer for LocalServer { fn folder_service(&self) -> Arc { Arc::new(LocalServerFolderCloudServiceImpl()) } + + fn database_service(&self) -> Arc { + Arc::new(LocalServerDatabaseCloudServiceImpl()) + } + + fn document_service(&self) -> Arc { + Arc::new(LocalServerDocumentCloudServiceImpl()) + } + + fn collab_storage(&self) -> Option> { + None + } } 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 new file mode 100644 index 0000000000..dbe65ed266 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/self_host/impls/database.rs @@ -0,0 +1,18 @@ +use flowy_database2::deps::{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> { + FutureResult::new(async move { Ok(vec![]) }) + } + + fn get_database_latest_snapshot( + &self, + _database_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 new file mode 100644 index 0000000000..52aac894b7 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/self_host/impls/document.rs @@ -0,0 +1,18 @@ +use flowy_document2::deps::{DocumentCloudService, DocumentSnapshot}; +use flowy_error::FlowyError; +use lib_infra::future::FutureResult; + +pub(crate) struct SelfHostedDocumentCloudServiceImpl(); + +impl DocumentCloudService for SelfHostedDocumentCloudServiceImpl { + fn get_document_updates(&self, _document_id: &str) -> FutureResult>, FlowyError> { + FutureResult::new(async move { Ok(vec![]) }) + } + + fn get_document_latest_snapshot( + &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 842f96c213..eafb7c1061 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, Workspace}; +use flowy_folder2::deps::{FolderCloudService, FolderSnapshot, Workspace}; use flowy_folder2::gen_workspace_id; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; @@ -18,4 +18,15 @@ impl FolderCloudService for SelfHostedServerFolderCloudServiceImpl { }) }) } + + fn get_folder_latest_snapshot( + &self, + _workspace_id: &str, + ) -> FutureResult, FlowyError> { + FutureResult::new(async move { Ok(None) }) + } + + fn get_folder_updates(&self, _workspace_id: &str) -> FutureResult>, FlowyError> { + FutureResult::new(async move { Ok(vec![]) }) + } } diff --git a/frontend/rust-lib/flowy-server/src/self_host/impls/mod.rs b/frontend/rust-lib/flowy-server/src/self_host/impls/mod.rs index 8a05c27f6b..0280cfbefb 100644 --- a/frontend/rust-lib/flowy-server/src/self_host/impls/mod.rs +++ b/frontend/rust-lib/flowy-server/src/self_host/impls/mod.rs @@ -1,5 +1,9 @@ -mod folder; -mod user; - +pub(crate) use database::*; +pub(crate) use document::*; pub(crate) use folder::*; pub(crate) use user::*; + +mod database; +mod document; +mod folder; +mod user; diff --git a/frontend/rust-lib/flowy-server/src/self_host/impls/user.rs b/frontend/rust-lib/flowy-server/src/self_host/impls/user.rs index 74148403b4..c359fae27e 100644 --- a/frontend/rust-lib/flowy-server/src/self_host/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/self_host/impls/user.rs @@ -2,7 +2,7 @@ use flowy_error::{ErrorCode, FlowyError}; use flowy_user::entities::{ SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfile, }; -use flowy_user::event_map::UserAuthService; +use flowy_user::event_map::{UserAuthService, UserCredentials}; use lib_infra::box_any::BoxAny; use lib_infra::future::FutureResult; @@ -42,7 +42,7 @@ impl UserAuthService for SelfHostedUserAuthServiceImpl { match token { None => FutureResult::new(async { Err(FlowyError::new( - ErrorCode::InvalidData, + ErrorCode::InvalidParams, "Token should not be empty", )) }), @@ -59,19 +59,18 @@ impl UserAuthService for SelfHostedUserAuthServiceImpl { fn update_user( &self, - _uid: i64, - token: &Option, + credential: UserCredentials, params: UpdateUserProfileParams, ) -> FutureResult<(), FlowyError> { - match token { + match credential.token { None => FutureResult::new(async { Err(FlowyError::new( - ErrorCode::InvalidData, + ErrorCode::InvalidParams, "Token should not be empty", )) }), Some(token) => { - let token = token.to_owned(); + let token = token; let url = self.config.user_profile_url(); FutureResult::new(async move { update_user_profile_request(&token, params, &url).await?; @@ -83,13 +82,11 @@ impl UserAuthService for SelfHostedUserAuthServiceImpl { fn get_user_profile( &self, - token: Option, - _uid: i64, + credential: UserCredentials, ) -> FutureResult, FlowyError> { - let token = token; let url = self.config.user_profile_url(); FutureResult::new(async move { - match token { + match credential.token { None => Err(FlowyError::new( ErrorCode::UnexpectedEmpty, "Token should not be empty", @@ -101,6 +98,11 @@ impl UserAuthService for SelfHostedUserAuthServiceImpl { } }) } + + fn check_user(&self, _credential: UserCredentials) -> FutureResult<(), FlowyError> { + // TODO(nathan): implement the OpenAPI for this + FutureResult::new(async { Ok(()) }) + } } pub async fn user_sign_up_request( diff --git a/frontend/rust-lib/flowy-server/src/self_host/server.rs b/frontend/rust-lib/flowy-server/src/self_host/server.rs index 0b9cf29582..3a381c7036 100644 --- a/frontend/rust-lib/flowy-server/src/self_host/server.rs +++ b/frontend/rust-lib/flowy-server/src/self_host/server.rs @@ -1,10 +1,15 @@ -use flowy_folder2::deps::FolderCloudService; use std::sync::Arc; +use appflowy_integrate::RemoteCollabStorage; + +use flowy_database2::deps::DatabaseCloudService; +use flowy_document2::deps::DocumentCloudService; +use flowy_folder2::deps::FolderCloudService; use flowy_user::event_map::UserAuthService; use crate::self_host::configuration::SelfHostedConfiguration; use crate::self_host::impls::{ + SelfHostedDatabaseCloudServiceImpl, SelfHostedDocumentCloudServiceImpl, SelfHostedServerFolderCloudServiceImpl, SelfHostedUserAuthServiceImpl, }; use crate::AppFlowyServer; @@ -27,4 +32,16 @@ impl AppFlowyServer for SelfHostServer { fn folder_service(&self) -> Arc { Arc::new(SelfHostedServerFolderCloudServiceImpl()) } + + fn database_service(&self) -> Arc { + Arc::new(SelfHostedDatabaseCloudServiceImpl()) + } + + fn document_service(&self) -> Arc { + Arc::new(SelfHostedDocumentCloudServiceImpl()) + } + + fn collab_storage(&self) -> Option> { + None + } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/configuration.rs b/frontend/rust-lib/flowy-server/src/supabase/configuration.rs new file mode 100644 index 0000000000..250333fe92 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/configuration.rs @@ -0,0 +1,90 @@ +use serde::Deserialize; + +use flowy_error::{ErrorCode, FlowyError}; + +pub const SUPABASE_URL: &str = "SUPABASE_URL"; +pub const SUPABASE_ANON_KEY: &str = "SUPABASE_ANON_KEY"; +pub const SUPABASE_KEY: &str = "SUPABASE_KEY"; +pub const SUPABASE_JWT_SECRET: &str = "SUPABASE_JWT_SECRET"; + +pub const SUPABASE_DB: &str = "SUPABASE_DB"; +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)] +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 postgres_config: PostgresConfiguration, +} + +impl SupabaseConfiguration { + /// Load the configuration from the environment variables. + /// SUPABASE_URL=https://.supabase.co + /// SUPABASE_KEY= + /// SUPABASE_JWT_SECRET= + /// + pub fn from_env() -> Result { + let postgres_config = PostgresConfiguration::from_env()?; + Ok(Self { + url: std::env::var(SUPABASE_URL) + .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_URL"))?, + key: std::env::var(SUPABASE_KEY) + .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_KEY"))?, + jwt_secret: std::env::var(SUPABASE_JWT_SECRET).map_err(|_| { + FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_JWT_SECRET") + })?, + postgres_config, + }) + } + + pub fn write_env(&self) { + 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); + self.postgres_config.write_env(); + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PostgresConfiguration { + pub url: String, + pub user_name: String, + pub password: String, + pub port: u16, +} + +impl PostgresConfiguration { + pub fn from_env() -> Result { + let url = std::env::var(SUPABASE_DB) + .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_DB"))?; + let user_name = std::env::var(SUPABASE_DB_USER) + .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_DB_USER"))?; + let password = std::env::var(SUPABASE_DB_PASSWORD) + .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_DB_PASSWORD"))?; + let port = std::env::var(SUPABASE_DB_PORT) + .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_DB_PORT"))? + .parse::() + .map_err(|_e| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_DB_PORT"))?; + + Ok(Self { + url, + user_name, + password, + port, + }) + } + + pub fn write_env(&self) { + std::env::set_var(SUPABASE_DB, &self.url); + std::env::set_var(SUPABASE_DB_USER, &self.user_name); + std::env::set_var(SUPABASE_DB_PASSWORD, &self.password); + std::env::set_var(SUPABASE_DB_PORT, self.port.to_string()); + } +} diff --git a/frontend/rust-lib/flowy-server/src/supabase/entities.rs b/frontend/rust-lib/flowy-server/src/supabase/entities.rs new file mode 100644 index 0000000000..1eff97ed09 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/entities.rs @@ -0,0 +1,39 @@ +use serde::Deserialize; +use uuid::Uuid; + +use crate::supabase::impls::WORKSPACE_ID; +use crate::util::deserialize_null_or_default; + +pub enum GetUserProfileParams { + Uid(i64), + Uuid(Uuid), +} + +#[allow(dead_code)] +#[derive(Debug, Deserialize, Clone)] +pub(crate) struct UserProfileResponse { + pub uid: i64, + #[serde(deserialize_with = "deserialize_null_or_default")] + pub name: String, + + #[serde(deserialize_with = "deserialize_null_or_default")] + pub email: String, + + #[serde(deserialize_with = "deserialize_null_or_default")] + pub workspace_id: String, +} + +impl From for UserProfileResponse { + fn from(row: tokio_postgres::Row) -> Self { + let workspace_id: Uuid = row.get(WORKSPACE_ID); + Self { + uid: row.get("uid"), + name: row.try_get("name").unwrap_or_default(), + email: row.try_get("email").unwrap_or_default(), + workspace_id: workspace_id.to_string(), + } + } +} + +#[derive(Debug, Deserialize)] +pub(crate) struct UserProfileResponseList(pub Vec); 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 new file mode 100644 index 0000000000..9576afd1a6 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/impls/collab_storage.rs @@ -0,0 +1,304 @@ +use std::sync::{Arc, Weak}; + +use anyhow::Error; +use appflowy_integrate::{ + merge_updates_v1, CollabObject, Decode, MsgId, RemoteCollabSnapshot, RemoteCollabState, + RemoteCollabStorage, YrsUpdate, +}; +use chrono::{DateTime, Utc}; +use deadpool_postgres::GenericClient; +use futures_util::TryStreamExt; +use tokio::task::spawn_blocking; +use tokio_postgres::types::ToSql; +use tokio_postgres::Row; + +use flowy_error::FlowyError; +use lib_infra::async_trait::async_trait; + +use crate::supabase::sql_builder::{ + DeleteSqlBuilder, InsertSqlBuilder, SelectSqlBuilder, WhereCondition, +}; +use crate::supabase::PostgresServer; + +pub struct PgCollabStorageImpl { + server: Arc, +} + +const AF_COLLAB_KEY_COLUMN: &str = "key"; +const AF_COLLAB_SNAPSHOT_OID_COLUMN: &str = "oid"; +const AF_COLLAB_SNAPSHOT_ID_COLUMN: &str = "sid"; +const AF_COLLAB_SNAPSHOT_BLOB_COLUMN: &str = "blob"; +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 { + Self { server } + } +} + +#[async_trait] +impl RemoteCollabStorage for PgCollabStorageImpl { + async fn get_all_updates(&self, object_id: &str) -> Result>, Error> { + get_updates_from_server(object_id, Arc::downgrade(&self.server)).await + } + + async fn get_latest_snapshot( + &self, + object_id: &str, + ) -> Result, Error> { + get_latest_snapshot_from_server(object_id, Arc::downgrade(&self.server)).await + } + + async fn get_collab_state(&self, object_id: &str) -> Result, Error> { + let client = self.server.get_pg_client().await.recv().await?; + let (sql, params) = SelectSqlBuilder::new("af_collab_state") + .column("*") + .where_clause("oid", object_id.to_string()) + .order_by("snapshot_created_at", false) + .limit(1) + .build(); + let stmt = client.prepare_cached(&sql).await?; + if let Some(row) = client + .query_raw(&stmt, params) + .await? + .try_collect::>() + .await? + .first() + { + let created_at = row.try_get::<&str, DateTime>("snapshot_created_at")?; + let current_edit_count = row.try_get::<_, i64>("current_edit_count")?; + let last_snapshot_edit_count = row.try_get::<_, i64>("snapshot_edit_count")?; + + let state = RemoteCollabState { + current_edit_count, + last_snapshot_edit_count, + last_snapshot_created_at: created_at.timestamp(), + }; + return Ok(Some(state)); + } + + Ok(None) + } + + async fn create_snapshot(&self, object: &CollabObject, snapshot: Vec) -> Result { + let client = self.server.get_pg_client().await.recv().await?; + let value_size = snapshot.len() as i32; + let (sql, params) = InsertSqlBuilder::new("af_collab_snapshot") + .value(AF_COLLAB_SNAPSHOT_OID_COLUMN, object.id.clone()) + .value("name", object.name.clone()) + .value(AF_COLLAB_SNAPSHOT_BLOB_COLUMN, snapshot) + .value(AF_COLLAB_SNAPSHOT_BLOB_SIZE_COLUMN, value_size) + .returning(AF_COLLAB_SNAPSHOT_ID_COLUMN) + .build(); + let stmt = client.prepare_cached(&sql).await?; + + let all_rows = client + .query_raw(&stmt, params) + .await? + .try_collect::>() + .await?; + let row = all_rows + .first() + .ok_or(anyhow::anyhow!("Create snapshot failed. No row returned"))?; + let sid = row.try_get::<&str, i64>(AF_COLLAB_SNAPSHOT_ID_COLUMN)?; + return Ok(sid); + } + + async fn send_update( + &self, + object: &CollabObject, + _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(); + + let stmt = client.prepare_cached(&sql).await?; + client.execute_raw(&stmt, params).await?; + Ok(()) + } + + async fn send_init_sync( + &self, + object: &CollabObject, + _id: MsgId, + init_update: Vec, + ) -> Result<(), Error> { + let mut client = self.server.get_pg_client().await.recv().await?; + let txn = client.transaction().await?; + + // 1.Get all updates + 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()) + .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 insert_builder = InsertSqlBuilder::new("af_collab") + .value("oid", object.id.clone()) + .value("name", object.name.clone()); + + let (sql, params) = if !remote_updates.is_empty() { + let remoted_keys = remote_updates + .iter() + .map(|row| row.get::<_, i64>(AF_COLLAB_KEY_COLUMN)) + .collect::>(); + let last_row_key = remoted_keys.last().cloned().unwrap(); + + // 2.Merge all updates + let merged_update = + spawn_blocking(move || merge_update_from_rows(remote_updates, init_update)).await??; + + // 3. Delete all updates + let (sql, params) = DeleteSqlBuilder::new("af_collab") + .where_condition(WhereCondition::Equals( + "oid".to_string(), + Box::new(object.id.clone()), + )) + .where_condition(WhereCondition::In( + AF_COLLAB_KEY_COLUMN.to_string(), + remoted_keys + .into_iter() + .map(|key| Box::new(key) as Box) + .collect::>(), + )) + .build(); + 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 + insert_builder + .value("value", merged_update) + .value("value_size", value_size) + .value(AF_COLLAB_KEY_COLUMN, last_row_key) + .overriding_system_value() + .build() + } else { + let value_size = init_update.len() as i32; + insert_builder + .value("value", init_update) + .value("value_size", value_size) + .build() + }; + + // 4.Insert the merged update + let stmt = txn.prepare_cached(&sql).await?; + txn.execute_raw(&stmt, params).await?; + + // 4.commit the transaction + txn.commit().await?; + tracing::trace!("{} init sync done", object.id); + Ok(()) + } +} + +pub async fn get_updates_from_server( + object_id: &str, + server: Weak, +) -> Result>, Error> { + match 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.to_string()) + .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 async fn get_latest_snapshot_from_server( + object_id: &str, + server: Weak, +) -> Result, Error> { + match server.upgrade() { + None => Ok(None), + Some(server) => { + let client = server.get_pg_client().await.recv().await?; + let (sql, params) = SelectSqlBuilder::new(AF_COLLAB_SNAPSHOT_TABLE) + .column(AF_COLLAB_SNAPSHOT_ID_COLUMN) + .column(AF_COLLAB_SNAPSHOT_BLOB_COLUMN) + .column(AF_COLLAB_SNAPSHOT_CREATED_AT_COLUMN) + .order_by(AF_COLLAB_SNAPSHOT_ID_COLUMN, false) + .limit(1) + .where_clause(AF_COLLAB_SNAPSHOT_OID_COLUMN, object_id.to_string()) + .build(); + + let stmt = client.prepare_cached(&sql).await?; + let all_rows = client + .query_raw(&stmt, params) + .await? + .try_collect::>() + .await?; + + let row = all_rows.first().ok_or(anyhow::anyhow!( + "Get latest snapshot failed. No row returned" + ))?; + let snapshot_id = row.try_get::<_, i64>(AF_COLLAB_SNAPSHOT_ID_COLUMN)?; + let update = row.try_get::<_, Vec>(AF_COLLAB_SNAPSHOT_BLOB_COLUMN)?; + let created_at = row + .try_get::<_, DateTime>(AF_COLLAB_SNAPSHOT_CREATED_AT_COLUMN)? + .timestamp(); + + Ok(Some(RemoteCollabSnapshot { + snapshot_id, + oid: object_id.to_string(), + data: update, + created_at, + })) + }, + } +} + +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))) +} + +#[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> { + let mut updates = vec![]; + for row in rows { + 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")) +} diff --git a/frontend/rust-lib/flowy-server/src/supabase/impls/database.rs b/frontend/rust-lib/flowy-server/src/supabase/impls/database.rs new file mode 100644 index 0000000000..10bf7fd725 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/impls/database.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; + +use tokio::sync::oneshot::channel; + +use flowy_database2::deps::{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; + +pub(crate) struct SupabaseDatabaseCloudServiceImpl { + server: Arc, +} + +impl SupabaseDatabaseCloudServiceImpl { + pub fn new(server: Arc) -> Self { + Self { server } + } +} + +impl DatabaseCloudService for SupabaseDatabaseCloudServiceImpl { + fn get_database_updates(&self, database_id: &str) -> FutureResult>, FlowyError> { + let server = Arc::downgrade(&self.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) }); + FutureResult::new(async { rx.await.map_err(internal_error)?.map_err(internal_error) }) + } + + fn get_database_latest_snapshot( + &self, + database_id: &str, + ) -> FutureResult, FlowyError> { + let server = Arc::downgrade(&self.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) }, + ); + FutureResult::new(async { + Ok( + rx.await + .map_err(internal_error)? + .map_err(internal_error)? + .map(|snapshot| DatabaseSnapshot { + snapshot_id: snapshot.snapshot_id, + database_id: snapshot.oid, + data: snapshot.data, + created_at: snapshot.created_at, + }), + ) + }) + } +} diff --git a/frontend/rust-lib/flowy-server/src/supabase/impls/document.rs b/frontend/rust-lib/flowy-server/src/supabase/impls/document.rs new file mode 100644 index 0000000000..68b7274af6 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/impls/document.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; + +use tokio::sync::oneshot::channel; + +use flowy_document2::deps::{DocumentCloudService, 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; + +pub(crate) struct SupabaseDocumentCloudServiceImpl { + server: Arc, +} + +impl SupabaseDocumentCloudServiceImpl { + pub fn new(server: Arc) -> Self { + Self { server } + } +} + +impl DocumentCloudService for SupabaseDocumentCloudServiceImpl { + fn get_document_updates(&self, document_id: &str) -> FutureResult>, FlowyError> { + let server = Arc::downgrade(&self.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) }) + } + + fn get_document_latest_snapshot( + &self, + document_id: &str, + ) -> FutureResult, FlowyError> { + let server = Arc::downgrade(&self.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) }, + ); + + FutureResult::new(async { + { + Ok( + rx.await + .map_err(internal_error)? + .map_err(internal_error)? + .map(|snapshot| DocumentSnapshot { + snapshot_id: snapshot.snapshot_id, + document_id: snapshot.oid, + data: snapshot.data, + created_at: snapshot.created_at, + }), + ) + } + }) + } +} 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 f90b2e9788..f8a1258277 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/impls/folder.rs @@ -1,54 +1,184 @@ -use crate::supabase::request::create_workspace_with_uid; -use flowy_error::FlowyError; -use flowy_folder2::deps::{FolderCloudService, Workspace}; -use lib_infra::future::FutureResult; -use postgrest::Postgrest; use std::sync::Arc; +use chrono::{DateTime, Utc}; +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 lib_infra::future::FutureResult; + +use crate::supabase::pg_db::PostgresObject; +use crate::supabase::sql_builder::{InsertSqlBuilder, SelectSqlBuilder}; +use crate::supabase::PostgresServer; + pub(crate) const WORKSPACE_TABLE: &str = "af_workspace"; -pub(crate) const WORKSPACE_NAME_COLUMN: &str = "workspace_name"; +pub(crate) const WORKSPACE_ID: &str = "workspace_id"; +const WORKSPACE_NAME: &str = "workspace_name"; +const CREATED_AT: &str = "created_at"; + pub(crate) struct SupabaseFolderCloudServiceImpl { - postgrest: Arc, + server: Arc, +} + +impl SupabaseFolderCloudServiceImpl { + pub fn new(server: Arc) -> Self { + Self { server } + } } impl FolderCloudService for SupabaseFolderCloudServiceImpl { fn create_workspace(&self, uid: i64, name: &str) -> FutureResult { + let server = self.server.clone(); + let (tx, rx) = channel(); let name = name.to_string(); - let postgrest = self.postgrest.clone(); - FutureResult::new(async move { create_workspace_with_uid(postgrest, uid, &name).await }) + tokio::spawn(async move { + tx.send( + async move { + let client = server.get_pg_client().await.recv().await?; + create_workspace(&client, uid, &name).await + } + .await, + ) + }); + FutureResult::new(async { rx.await.map_err(internal_error)? }) + } + + fn get_folder_latest_snapshot( + &self, + workspace_id: &str, + ) -> FutureResult, FlowyError> { + let server = Arc::downgrade(&self.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) }, + ); + FutureResult::new(async { + Ok( + rx.await + .map_err(internal_error)? + .map_err(internal_error)? + .map(|snapshot| FolderSnapshot { + snapshot_id: snapshot.snapshot_id, + database_id: snapshot.oid, + data: snapshot.data, + created_at: snapshot.created_at, + }), + ) + }) + } + + fn get_folder_updates(&self, workspace_id: &str) -> FutureResult>, FlowyError> { + let server = Arc::downgrade(&self.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) }); + FutureResult::new(async { rx.await.map_err(internal_error)?.map_err(internal_error) }) + } +} + +async fn create_workspace( + client: &PostgresObject, + uid: i64, + name: &str, +) -> Result { + let new_workspace_id = Uuid::new_v4(); + + // Create workspace + let (sql, params) = InsertSqlBuilder::new(WORKSPACE_TABLE) + .value("uid", uid) + .value(WORKSPACE_ID, new_workspace_id) + .value(WORKSPACE_NAME, name.to_string()) + .build(); + let stmt = client + .prepare_cached(&sql) + .await + .map_err(|e| FlowyError::new(ErrorCode::PgDatabaseError, e))?; + client + .execute_raw(&stmt, params) + .await + .map_err(|e| FlowyError::new(ErrorCode::PgDatabaseError, e))?; + + // Read the workspace + let (sql, params) = SelectSqlBuilder::new(WORKSPACE_TABLE) + .column(WORKSPACE_ID) + .column(WORKSPACE_NAME) + .column(CREATED_AT) + .where_clause(WORKSPACE_ID, new_workspace_id) + .build(); + let stmt = client + .prepare_cached(&sql) + .await + .map_err(|e| FlowyError::new(ErrorCode::PgDatabaseError, e))?; + + let rows = Box::pin( + client + .query_raw(&stmt, params) + .await + .map_err(|e| FlowyError::new(ErrorCode::PgDatabaseError, e))?, + ); + pin_mut!(rows); + + if let Some(Ok(row)) = rows.next().await { + let created_at = row + .try_get::<&str, DateTime>(CREATED_AT) + .unwrap_or_default(); + let workspace_id: Uuid = row.get(WORKSPACE_ID); + + Ok(Workspace { + id: workspace_id.to_string(), + name: row.get(WORKSPACE_NAME), + child_views: Default::default(), + created_at: created_at.timestamp(), + }) + } else { + Err(FlowyError::new( + ErrorCode::PgDatabaseError, + "Create workspace failed", + )) } } #[cfg(test)] mod tests { - use crate::supabase::request::{ - create_user_with_uuid, create_workspace_with_uid, get_user_workspace_with_uid, - }; - use crate::supabase::{SupabaseConfiguration, SupabaseServer}; - use dotenv::dotenv; + use std::collections::HashMap; use std::sync::Arc; + use uuid::Uuid; + + use flowy_folder2::deps::FolderCloudService; + 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}; + #[tokio::test] async fn create_user_workspace() { - dotenv().ok(); - if let Ok(config) = SupabaseConfiguration::from_env() { - let server = Arc::new(SupabaseServer::new(config)); - let uuid = uuid::Uuid::new_v4(); - let uid = create_user_with_uuid(server.postgres.clone(), uuid.to_string()) - .await - .unwrap() - .uid; - - create_workspace_with_uid(server.postgres.clone(), uid, "test") - .await - .unwrap(); - - let workspaces = get_user_workspace_with_uid(server.postgres.clone(), uid) - .await - .unwrap(); - assert_eq!(workspaces.len(), 2); - assert_eq!(workspaces[0].name, "My workspace"); - assert_eq!(workspaces[1].name, "test"); + if dotenv::from_filename("./.env.workspace.test").is_err() { + return; } + let server = Arc::new(PostgresServer::new( + PostgresConfiguration::from_env().unwrap(), + )); + let user_service = SupabaseUserAuthServiceImpl::new(server.clone()); + + // create user + let mut params = HashMap::new(); + params.insert("uuid".to_string(), Uuid::new_v4().to_string()); + let user = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + + // create workspace + let folder_service = SupabaseFolderCloudServiceImpl::new(server); + let workspace = folder_service + .create_workspace(user.user_id, "my test workspace") + .await + .unwrap(); + + assert_eq!(workspace.name, "my test workspace"); } } 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 8a05c27f6b..74c1a9f500 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/impls/mod.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/impls/mod.rs @@ -1,5 +1,11 @@ +pub use collab_storage::*; +pub(crate) use database::*; +pub(crate) use document::*; +pub(crate) use folder::*; +pub use user::*; + +mod collab_storage; +mod database; +mod document; mod folder; mod user; - -pub(crate) use folder::*; -pub(crate) use user::*; 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 cf55424120..62a83e14b1 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/impls/user.rs @@ -1,54 +1,75 @@ +use std::str::FromStr; use std::sync::Arc; -use postgrest::Postgrest; +use deadpool_postgres::GenericClient; +use futures::pin_mut; +use futures_util::StreamExt; +use tokio::sync::oneshot::channel; +use tokio_postgres::error::SqlState; +use uuid::Uuid; -use flowy_error::FlowyError; +use flowy_error::{internal_error, ErrorCode, FlowyError}; use flowy_user::entities::{SignInResponse, SignUpResponse, UpdateUserProfileParams, UserProfile}; -use flowy_user::event_map::UserAuthService; +use flowy_user::event_map::{UserAuthService, UserCredentials}; use lib_infra::box_any::BoxAny; use lib_infra::future::FutureResult; -use crate::supabase::request::*; +use crate::supabase::entities::{GetUserProfileParams, UserProfileResponse}; +use crate::supabase::pg_db::PostgresObject; +use crate::supabase::sql_builder::{SelectSqlBuilder, UpdateSqlBuilder}; +use crate::supabase::PostgresServer; +use crate::util::uuid_from_box_any; pub(crate) const USER_TABLE: &str = "af_user"; pub(crate) const USER_PROFILE_TABLE: &str = "af_user_profile"; -#[allow(dead_code)] -pub(crate) const USER_WORKSPACE_TABLE: &str = "af_workspace"; -pub(crate) struct PostgrestUserAuthServiceImpl { - postgrest: Arc, +pub const USER_UUID: &str = "uuid"; + +pub struct SupabaseUserAuthServiceImpl { + server: Arc, } -impl PostgrestUserAuthServiceImpl { - pub(crate) fn new(postgrest: Arc) -> Self { - Self { postgrest } +impl SupabaseUserAuthServiceImpl { + pub fn new(server: Arc) -> Self { + Self { server } } } -impl UserAuthService for PostgrestUserAuthServiceImpl { +impl UserAuthService for SupabaseUserAuthServiceImpl { fn sign_up(&self, params: BoxAny) -> FutureResult { - let postgrest = self.postgrest.clone(); - FutureResult::new(async move { - let uuid = uuid_from_box_any(params)?; - let user = create_user_with_uuid(postgrest, uuid).await?; - Ok(SignUpResponse { - user_id: user.uid, - workspace_id: user.workspace_id, - ..Default::default() - }) - }) + let server = self.server.clone(); + 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 + } + .await, + ) + }); + FutureResult::new(async { rx.await.map_err(internal_error)? }) } fn sign_in(&self, params: BoxAny) -> FutureResult { - let postgrest = self.postgrest.clone(); - FutureResult::new(async move { - let uuid = uuid_from_box_any(params)?; - let user_profile = get_user_profile(postgrest, GetUserProfileParams::Uuid(uuid)).await?; - Ok(SignInResponse { - user_id: user_profile.uid, - workspace_id: user_profile.workspace_id, - ..Default::default() - }) - }) + let server = self.server.clone(); + 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() + }) + } + .await, + ) + }); + FutureResult::new(async { rx.await.map_err(internal_error)? }) } fn sign_out(&self, _token: Option) -> FutureResult<(), FlowyError> { @@ -57,111 +78,222 @@ impl UserAuthService for PostgrestUserAuthServiceImpl { fn update_user( &self, - _uid: i64, - _token: &Option, + _credential: UserCredentials, params: UpdateUserProfileParams, ) -> FutureResult<(), FlowyError> { - let postgrest = self.postgrest.clone(); - FutureResult::new(async move { - let _ = update_user_profile(postgrest, params).await?; - Ok(()) - }) + let server = self.server.clone(); + 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 + } + .await, + ) + }); + FutureResult::new(async { rx.await.map_err(internal_error)? }) } fn get_user_profile( &self, - _token: Option, - uid: i64, + credential: UserCredentials, ) -> FutureResult, FlowyError> { - let postgrest = self.postgrest.clone(); - FutureResult::new(async move { - let user_profile_resp = get_user_profile(postgrest, GetUserProfileParams::Uid(uid)).await?; - - let profile = UserProfile { - id: user_profile_resp.uid, - email: user_profile_resp.email, - name: user_profile_resp.name, - token: "".to_string(), - icon_url: "".to_string(), - openai_key: "".to_string(), - workspace_id: user_profile_resp.workspace_id, - }; - - Ok(Some(profile)) - }) - } -} - -#[cfg(test)] -mod tests { - use std::sync::Arc; - - use dotenv::dotenv; - - use flowy_user::entities::UpdateUserProfileParams; - - use crate::supabase::request::{ - create_user_with_uuid, get_user_id_with_uuid, get_user_profile, get_user_workspace_with_uid, - update_user_profile, GetUserProfileParams, - }; - use crate::supabase::{SupabaseConfiguration, SupabaseServer}; - - #[tokio::test] - async fn read_user_table_test() { - dotenv().ok(); - if let Ok(config) = SupabaseConfiguration::from_env() { - let server = Arc::new(SupabaseServer::new(config)); - let uid = get_user_id_with_uuid( - server.postgres.clone(), - "c8c674fc-506f-403c-b052-209e09817f6e".to_string(), + let server = self.server.clone(); + 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) + } + .await, ) - .await - .unwrap(); - println!("uid: {:?}", uid); - } + }); + FutureResult::new(async { rx.await.map_err(internal_error)? }) } - #[tokio::test] - async fn insert_user_table_test() { - dotenv().ok(); - if let Ok(config) = SupabaseConfiguration::from_env() { - let server = Arc::new(SupabaseServer::new(config)); - let uuid = uuid::Uuid::new_v4(); - // let uuid = "c8c674fc-506f-403c-b052-209e09817f6e"; - let uid = create_user_with_uuid(server.postgres.clone(), uuid.to_string()).await; - println!("uid: {:?}", uid); - } - } - - #[tokio::test] - async fn create_and_then_update_user_profile_test() { - dotenv().ok(); - if let Ok(config) = SupabaseConfiguration::from_env() { - let server = Arc::new(SupabaseServer::new(config)); - let uuid = uuid::Uuid::new_v4(); - let uid = create_user_with_uuid(server.postgres.clone(), uuid.to_string()) - .await - .unwrap() - .uid; - let params = UpdateUserProfileParams { - id: uid, - name: Some("nathan".to_string()), - ..Default::default() - }; - let result = update_user_profile(server.postgres.clone(), params) - .await - .unwrap(); - println!("result: {:?}", result); - - let result = get_user_profile(server.postgres.clone(), GetUserProfileParams::Uid(uid)) - .await - .unwrap(); - assert_eq!(result.name, "nathan".to_string()); - - let result = get_user_workspace_with_uid(server.postgres.clone(), uid) - .await - .unwrap(); - assert!(!result.is_empty()); - } + 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 (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 + } + .await, + ) + }); + FutureResult::new(async { rx.await.map_err(internal_error)? }) + } +} + +async fn create_user_with_uuid( + client: &PostgresObject, + uuid: Uuid, +) -> Result { + let mut is_new = true; + if let Err(e) = client + .execute( + &format!("INSERT INTO {} (uuid) VALUES ($1);", USER_TABLE), + &[&uuid], + ) + .await + { + if let Some(code) = e.code() { + if code == &SqlState::UNIQUE_VIOLATION { + is_new = false; + } else { + return Err(FlowyError::new(ErrorCode::PgDatabaseError, e)); + } + } + }; + + let user_profile = get_user_profile(client, GetUserProfileParams::Uuid(uuid)).await?; + Ok(SignUpResponse { + user_id: user_profile.uid, + name: user_profile.name, + workspace_id: user_profile.workspace_id, + is_new, + email: Some(user_profile.email), + token: None, + }) +} + +async fn get_user_profile( + client: &PostgresObject, + params: GetUserProfileParams, +) -> Result { + let rows = match params { + GetUserProfileParams::Uid(uid) => { + let stmt = client + .prepare_cached(&format!( + "SELECT * FROM {} WHERE uid = $1", + USER_PROFILE_TABLE + )) + .await + .map_err(|e| FlowyError::new(ErrorCode::PgDatabaseError, e))?; + + client + .query(&stmt, &[&uid]) + .await + .map_err(|e| FlowyError::new(ErrorCode::PgDatabaseError, e))? + }, + GetUserProfileParams::Uuid(uuid) => { + let stmt = client + .prepare_cached(&format!( + "SELECT * FROM {} WHERE uuid = $1", + USER_PROFILE_TABLE + )) + .await + .map_err(|e| FlowyError::new(ErrorCode::PgDatabaseError, e))?; + + client + .query(&stmt, &[&uuid]) + .await + .map_err(|e| FlowyError::new(ErrorCode::PgDatabaseError, e))? + }, + }; + + let mut user_profiles = rows + .into_iter() + .map(UserProfileResponse::from) + .collect::>(); + if user_profiles.is_empty() { + Err(FlowyError::record_not_found()) + } else { + Ok(user_profiles.remove(0)) + } +} + +async fn update_user_profile( + client: &PostgresObject, + params: UpdateUserProfileParams, +) -> Result<(), FlowyError> { + if params.is_empty() { + return Err(FlowyError::new( + ErrorCode::InvalidParams, + format!("Update user profile params is empty: {:?}", params), + )); + } + let (sql, pg_params) = UpdateSqlBuilder::new(USER_PROFILE_TABLE) + .set("name", params.name) + .set("email", params.email) + .where_clause("uid", params.id) + .build(); + + let stmt = client.prepare_cached(&sql).await.map_err(|e| { + FlowyError::new( + ErrorCode::PgDatabaseError, + format!("Prepare update user profile sql error: {}", e), + ) + })?; + + let affect_rows = client + .execute_raw(&stmt, pg_params) + .await + .map_err(|e| FlowyError::new(ErrorCode::PgDatabaseError, e))?; + tracing::trace!("Update user profile affect rows: {}", affect_rows); + Ok(()) +} + +async fn check_user( + client: &PostgresObject, + uid: Option, + uuid: Option, +) -> Result<(), FlowyError> { + if uid.is_none() && uuid.is_none() { + return Err(FlowyError::new( + ErrorCode::InvalidParams, + "uid and uuid can't be both empty", + )); + } + + let (sql, params) = match uid { + None => SelectSqlBuilder::new(USER_TABLE) + .where_clause("uuid", uuid.unwrap()) + .build(), + Some(uid) => SelectSqlBuilder::new(USER_TABLE) + .where_clause("uid", uid) + .build(), + }; + + let stmt = client + .prepare_cached(&sql) + .await + .map_err(|e| FlowyError::new(ErrorCode::PgDatabaseError, e))?; + let rows = Box::pin( + client + .query_raw(&stmt, params) + .await + .map_err(|e| FlowyError::new(ErrorCode::PgDatabaseError, e))?, + ); + pin_mut!(rows); + + // TODO(nathan): would it be better to use token. + if rows.next().await.is_some() { + Ok(()) + } else { + Err(FlowyError::new( + ErrorCode::UserNotExist, + "Can't find the user in pg database", + )) } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/migration.rs b/frontend/rust-lib/flowy-server/src/supabase/migration.rs new file mode 100644 index 0000000000..d7c219ca76 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/migration.rs @@ -0,0 +1,100 @@ +use refinery::embed_migrations; +use tokio_postgres::Client; + +embed_migrations!("./src/supabase/migrations"); + +const AF_MIGRATION_HISTORY: &str = "af_migration_history"; + +pub(crate) async fn run_migrations(client: &mut Client) -> Result<(), anyhow::Error> { + match migrations::runner() + .set_migration_table_name(AF_MIGRATION_HISTORY) + .run_async(client) + .await + { + Ok(report) => { + if !report.applied_migrations().is_empty() { + tracing::info!("Run postgres db migration: {:?}", report); + } + Ok(()) + }, + Err(e) => { + tracing::error!("postgres db migration error: {}", e); + Err(anyhow::anyhow!("postgres db migration error: {}", e)) + }, + } +} + +/// 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; +"#; + client.batch_execute(sql).await.unwrap(); + client + .batch_execute("DROP TABLE IF EXISTS af_migration_history") + .await + .unwrap(); +} + +#[cfg(test)] +mod tests { + use tokio_postgres::NoTls; + + 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() { + 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-server/src/supabase/migrations/initial/Initial_down.sql b/frontend/rust-lib/flowy-server/src/supabase/migrations/initial/Initial_down.sql new file mode 100644 index 0000000000..6c2590311d --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/migrations/initial/Initial_down.sql @@ -0,0 +1,23 @@ +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; + 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 new file mode 100644 index 0000000000..980d1953bf --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/migrations/initial/V1__Initial_Up.sql @@ -0,0 +1,127 @@ +-- user table +CREATE TABLE IF NOT EXISTS af_user ( + uuid UUID PRIMARY KEY, + uid BIGINT GENERATED ALWAYS AS IDENTITY, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); +-- user profile table +CREATE TABLE IF NOT EXISTS af_user_profile ( + uid BIGINT PRIMARY KEY, + uuid UUID, + name TEXT, + email TEXT, + workspace_id UUID DEFAULT uuid_generate_v4() +); +-- 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); +RETURN NEW; +END $$ LANGUAGE plpgsql; +CREATE TRIGGER create_af_user_profile_trigger BEFORE +INSERT ON af_user FOR EACH ROW EXECUTE FUNCTION create_af_user_profile_trigger_func(); +-- workspace table +CREATE TABLE IF NOT EXISTS af_workspace ( + workspace_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + uid BIGINT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + workspace_name TEXT DEFAULT 'My Workspace' +); +-- workspace trigger +CREATE OR REPLACE FUNCTION create_af_workspace_trigger_func() RETURNS TRIGGER AS $$ BEGIN +INSERT INTO af_workspace (uid, workspace_id) +VALUES (NEW.uid, NEW.workspace_id); +RETURN NEW; +END $$ LANGUAGE plpgsql; +CREATE TRIGGER create_af_workspace_trigger BEFORE +INSERT ON af_user_profile FOR EACH ROW EXECUTE FUNCTION create_af_workspace_trigger_func(); +-- collab table. +CREATE TABLE IF NOT EXISTS af_collab ( + oid TEXT NOT NULL, + name TEXT DEFAULT '', + key BIGINT GENERATED ALWAYS AS IDENTITY, + value BYTEA NOT NULL, + value_size INTEGER, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (oid, key) +); +-- 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 +); +-- 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( + SELECT 1 + FROM af_collab_statistics + WHERE oid = NEW.oid + ) THEN +UPDATE af_collab_statistics +SET edit_count = edit_count + 1 +WHERE oid = NEW.oid; +ELSE +INSERT INTO af_collab_statistics (oid, edit_count) +VALUES (NEW.oid, 1); +END IF; +RETURN NEW; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER af_collab_insert_trigger +AFTER +INSERT ON af_collab FOR EACH ROW EXECUTE FUNCTION increment_af_collab_edit_count(); +-- collab snapshot. It will be used to store the snapshots of the collab. +CREATE TABLE IF NOT EXISTS af_collab_snapshot ( + sid BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + oid TEXT NOT NULL, + name TEXT DEFAULT '', + blob BYTEA NOT NULL, + blob_size INTEGER NOT NULL, + edit_count BIGINT 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 + FROM af_collab_statistics + WHERE oid = NEW.oid + ); +RETURN NEW; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER af_collab_snapshot_update_edit_count_trigger BEFORE +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. +CREATE OR REPLACE FUNCTION check_and_delete_snapshots() RETURNS TRIGGER AS $$ +DECLARE row_count INT; +BEGIN +SELECT COUNT(*) INTO row_count +FROM af_collab_snapshot +WHERE oid = NEW.oid; +IF row_count > 20 THEN +DELETE FROM af_collab_snapshot +WHERE id IN ( + SELECT id + FROM af_collab_snapshot + WHERE created_at < NOW() - INTERVAL '10 days' + AND oid = NEW.oid + ORDER BY created_at ASC + LIMIT row_count - 20 + ); +END IF; +RETURN NEW; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER check_and_delete_snapshots_trigger +AFTER +INSERT + OR +UPDATE ON af_collab_snapshot FOR EACH ROW EXECUTE FUNCTION check_and_delete_snapshots(); +-- collab state view. It will be used to get the current state of the collab. +CREATE VIEW af_collab_state AS +SELECT a.oid, + a.created_at AS snapshot_created_at, + a.edit_count AS snapshot_edit_count, + b.edit_count AS current_edit_count +FROM af_collab_snapshot AS a + JOIN af_collab_statistics AS b ON a.oid = b.oid; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-server/src/supabase/mod.rs b/frontend/rust-lib/flowy-server/src/supabase/mod.rs index 37ef0a6fdb..6bc21685da 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/mod.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/mod.rs @@ -1,7 +1,12 @@ +pub use configuration::*; pub use server::*; +mod entities; pub mod impls; -mod request; -mod response; -mod retry; +mod pg_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/pg_db.rs new file mode 100644 index 0000000000..9e9989ec36 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/pg_db.rs @@ -0,0 +1,123 @@ +use std::cmp::Ordering; +use std::fmt::{Debug, Formatter}; +use std::sync::Arc; + +use deadpool_postgres::{Manager, ManagerConfig, Object, Pool, RecyclingMethod}; +use tokio_postgres::NoTls; + +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; + +use crate::supabase::migration::run_migrations; +use crate::supabase::queue::RequestPayload; +use crate::supabase::PostgresConfiguration; + +pub type PostgresObject = Object; +pub struct PostgresDB { + pub configuration: PostgresConfiguration, + pub client: Arc, +} + +impl PostgresDB { + #[allow(dead_code)] + pub async fn from_env() -> Result { + let configuration = PostgresConfiguration::from_env()?; + Self::new(configuration).await + } + + pub async fn new(configuration: PostgresConfiguration) -> Result { + let mut pg_config = tokio_postgres::Config::new(); + pg_config + .host(&configuration.url) + .user(&configuration.user_name) + .password(&configuration.password) + .port(configuration.port); + + let mgr_config = ManagerConfig { + recycling_method: RecyclingMethod::Fast, + }; + + // Using the https://docs.rs/postgres-openssl/latest/postgres_openssl/ to enable tls connection. + let mgr = Manager::from_config(pg_config, NoTls, mgr_config); + let pool = Pool::builder(mgr).max_size(16).build()?; + let mut client = pool.get().await?; + // Run migrations + run_migrations(&mut client).await?; + + Ok(Self { + configuration, + client: Arc::new(pool), + }) + } +} + +pub type PgClientSender = tokio::sync::mpsc::Sender; + +pub struct PgClientReceiver(pub tokio::sync::mpsc::Receiver); +impl PgClientReceiver { + pub async fn recv(&mut self) -> FlowyResult { + match self.0.recv().await { + None => Err(FlowyError::new( + ErrorCode::PgConnectError, + "Can't connect to the postgres db".to_string(), + )), + Some(object) => Ok(object), + } + } +} + +#[derive(Clone)] +pub enum PostgresEvent { + ConnectDB, + /// The ID is utilized to sequence the events within the priority queue. + /// The sender is employed for transmitting the PostgresObject back to the original sender. + /// At present, the sender is invoked subsequent to the processing of the previous PostgresObject. + /// For future optimizations, we could potentially perform batch processing of the [GetPgClient] events utilizing the [Pool]. + GetPgClient { + id: u32, + sender: PgClientSender, + }, +} + +impl Debug for PostgresEvent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PostgresEvent::ConnectDB => f.write_str("ConnectDB"), + PostgresEvent::GetPgClient { id, .. } => f.write_fmt(format_args!("GetPgClient({})", id)), + } + } +} + +impl Ord for PostgresEvent { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (PostgresEvent::ConnectDB, PostgresEvent::ConnectDB) => Ordering::Equal, + (PostgresEvent::ConnectDB, PostgresEvent::GetPgClient { .. }) => Ordering::Greater, + (PostgresEvent::GetPgClient { .. }, PostgresEvent::ConnectDB) => Ordering::Less, + (PostgresEvent::GetPgClient { id: id1, .. }, PostgresEvent::GetPgClient { id: id2, .. }) => { + id1.cmp(id2).reverse() + }, + } + } +} + +impl Eq for PostgresEvent {} + +impl PartialEq for PostgresEvent { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (PostgresEvent::ConnectDB, PostgresEvent::ConnectDB) => true, + (PostgresEvent::GetPgClient { id: id1, .. }, PostgresEvent::GetPgClient { id: id2, .. }) => { + id1 == id2 + }, + _ => false, + } + } +} + +impl PartialOrd for PostgresEvent { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl RequestPayload for PostgresEvent {} diff --git a/frontend/rust-lib/flowy-server/src/supabase/postgres_deprecated/mod.rs b/frontend/rust-lib/flowy-server/src/supabase/postgres_deprecated/mod.rs new file mode 100644 index 0000000000..1daeed872d --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/postgres_deprecated/mod.rs @@ -0,0 +1,3 @@ +mod postgres_conn; +mod request; +mod response; diff --git a/frontend/rust-lib/flowy-server/src/supabase/postgres_deprecated/postgres_conn.rs b/frontend/rust-lib/flowy-server/src/supabase/postgres_deprecated/postgres_conn.rs new file mode 100644 index 0000000000..224a106049 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/postgres_deprecated/postgres_conn.rs @@ -0,0 +1,21 @@ +use std::sync::Arc; + +use postgrest::Postgrest; + +use crate::supabase::SupabaseConfiguration; + +pub struct PostgresHttp { + pub postgres: Arc, +} + +impl PostgresHttp { + pub fn new(config: SupabaseConfiguration) -> Self { + let url = format!("{}/rest/v1/", config.url); + let auth = format!("Bearer {}", config.key); + let postgrest = Postgrest::new(url) + .insert_header("apikey", config.key) + .insert_header("Authorization", auth); + let postgres = Arc::new(postgrest); + Self { postgres } + } +} diff --git a/frontend/rust-lib/flowy-server/src/supabase/request.rs b/frontend/rust-lib/flowy-server/src/supabase/postgres_deprecated/request.rs similarity index 90% rename from frontend/rust-lib/flowy-server/src/supabase/request.rs rename to frontend/rust-lib/flowy-server/src/supabase/postgres_deprecated/request.rs index 1e87407295..44dc3a8f35 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/request.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/postgres_deprecated/request.rs @@ -1,30 +1,30 @@ -use std::collections::HashMap; use std::sync::Arc; use postgrest::Postgrest; use serde_json::json; +use uuid::Uuid; use flowy_error::{ErrorCode, FlowyError}; use flowy_folder2::deps::Workspace; use flowy_user::entities::UpdateUserProfileParams; -use lib_infra::box_any::BoxAny; +use crate::supabase::entities::{ + GetUserProfileParams, UserProfileResponse, UserProfileResponseList, +}; use crate::supabase::impls::{ - USER_PROFILE_TABLE, USER_TABLE, USER_WORKSPACE_TABLE, WORKSPACE_NAME_COLUMN, WORKSPACE_TABLE, -}; -use crate::supabase::response::{ - InsertResponse, PostgrestError, UserProfileResponse, UserProfileResponseList, UserWorkspaceList, + USER_PROFILE_TABLE, USER_TABLE, WORKSPACE_NAME_COLUMN, WORKSPACE_TABLE, }; +use crate::supabase::postgres_http::response::{InsertResponse, PostgrestError, UserWorkspaceList}; const USER_ID: &str = "uid"; const USER_UUID: &str = "uuid"; pub(crate) async fn create_user_with_uuid( postgrest: Arc, - uuid: String, + uuid: Uuid, ) -> Result { let mut insert = serde_json::Map::new(); - insert.insert(USER_UUID.to_string(), json!(&uuid)); + insert.insert(USER_UUID.to_string(), json!(&uuid.to_string())); let insert_query = serde_json::to_string(&insert).unwrap(); // Create a new user with uuid. @@ -103,19 +103,6 @@ pub(crate) async fn get_user_id_with_uuid( } } -pub(crate) fn uuid_from_box_any(any: BoxAny) -> Result { - let map: HashMap = any.unbox_or_error()?; - let uuid = map - .get(USER_UUID) - .ok_or_else(|| FlowyError::new(ErrorCode::MissingAuthField, "Missing uuid field"))?; - Ok(uuid.to_string()) -} - -pub enum GetUserProfileParams { - Uid(i64), - Uuid(String), -} - pub(crate) async fn get_user_profile( postgrest: Arc, params: GetUserProfileParams, @@ -123,7 +110,7 @@ pub(crate) async fn get_user_profile( let mut builder = postgrest.from(USER_PROFILE_TABLE); match params { GetUserProfileParams::Uid(uid) => builder = builder.eq(USER_ID, uid.to_string()), - GetUserProfileParams::Uuid(uuid) => builder = builder.eq(USER_UUID, uuid), + GetUserProfileParams::Uuid(uuid) => builder = builder.eq(USER_UUID, uuid.to_string()), } let resp = builder .select("*") @@ -198,7 +185,7 @@ pub(crate) async fn get_user_workspace_with_uid( uid: i64, ) -> Result, FlowyError> { let resp = postgrest - .from(USER_WORKSPACE_TABLE) + .from(WORKSPACE_TABLE) .eq(USER_ID, uid.to_string()) .select("*") .execute() diff --git a/frontend/rust-lib/flowy-server/src/supabase/response.rs b/frontend/rust-lib/flowy-server/src/supabase/postgres_deprecated/response.rs similarity index 66% rename from frontend/rust-lib/flowy-server/src/supabase/response.rs rename to frontend/rust-lib/flowy-server/src/supabase/postgres_deprecated/response.rs index 07c5c41c7c..c4ec4751ea 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/response.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/postgres_deprecated/response.rs @@ -1,10 +1,12 @@ use chrono::{DateTime, Utc}; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; use flowy_error::{ErrorCode, FlowyError}; +use crate::util::deserialize_null_or_default; + #[derive(Debug, Error, Serialize, Deserialize)] #[error( "PostgrestException(message: {message}, code: {code:?}, details: {details:?}, hint: {hint:?})" @@ -55,23 +57,6 @@ pub(crate) struct InsertRecord { pub(crate) uuid: String, } -#[allow(dead_code)] -#[derive(Debug, Deserialize, Clone)] -pub(crate) struct UserProfileResponse { - pub uid: i64, - #[serde(deserialize_with = "deserialize_null_or_default")] - pub name: String, - - #[serde(deserialize_with = "deserialize_null_or_default")] - pub email: String, - - #[serde(deserialize_with = "deserialize_null_or_default")] - pub workspace_id: String, -} - -#[derive(Debug, Deserialize)] -pub(crate) struct UserProfileResponseList(pub Vec); - #[derive(Debug, Deserialize, Clone)] pub(crate) struct UserWorkspace { #[allow(dead_code)] @@ -90,14 +75,3 @@ impl UserWorkspaceList { self.0 } } - -/// Handles the case where the value is null. If the value is null, return the default value of the -/// type. Otherwise, deserialize the value. -fn deserialize_null_or_default<'de, D, T>(deserializer: D) -> Result -where - T: Default + Deserialize<'de>, - D: Deserializer<'de>, -{ - let opt = Option::deserialize(deserializer)?; - Ok(opt.unwrap_or_default()) -} diff --git a/frontend/rust-lib/flowy-server/src/supabase/queue.rs b/frontend/rust-lib/flowy-server/src/supabase/queue.rs new file mode 100644 index 0000000000..87ad3fb29e --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/queue.rs @@ -0,0 +1,199 @@ +use std::cmp::Ordering; +use std::collections::BinaryHeap; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, Weak}; + +use tokio::sync::watch; + +use lib_infra::async_trait::async_trait; + +pub trait RequestPayload: Clone + Ord {} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum RequestState { + Pending, + Processing, + Done, +} + +#[derive(Debug)] +pub struct PendingRequest { + pub payload: Payload, + pub state: RequestState, +} + +impl PendingRequest { + pub fn new(payload: Payload) -> Self { + Self { + payload, + state: RequestState::Pending, + } + } + + #[allow(dead_code)] + pub fn state(&self) -> &RequestState { + &self.state + } + + pub fn set_state(&mut self, new_state: RequestState) + where + Payload: Debug, + { + if self.state != new_state { + // tracing::trace!( + // "PgRequest {:?} from {:?} to {:?}", + // self.payload, + // self.state, + // new_state, + // ); + self.state = new_state; + } + } + + pub fn is_processing(&self) -> bool { + self.state == RequestState::Processing + } + + pub fn is_done(&self) -> bool { + self.state == RequestState::Done + } +} + +impl Clone for PendingRequest +where + Payload: Clone + Debug, +{ + fn clone(&self) -> Self { + Self { + payload: self.payload.clone(), + state: self.state.clone(), + } + } +} + +pub(crate) struct RequestQueue(BinaryHeap>); + +impl RequestQueue +where + Payload: Ord, +{ + pub(crate) fn new() -> Self { + Self(BinaryHeap::new()) + } +} + +impl Deref for RequestQueue +where + Payload: Ord, +{ + type Target = BinaryHeap>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for RequestQueue +where + Payload: Ord, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Eq for PendingRequest where Payload: Eq {} + +impl PartialEq for PendingRequest +where + Payload: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.payload == other.payload + } +} + +impl PartialOrd for PendingRequest +where + Payload: PartialOrd + Ord, +{ + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PendingRequest +where + Payload: Ord, +{ + fn cmp(&self, other: &Self) -> Ordering { + self.payload.cmp(&other.payload) + } +} + +#[async_trait] +pub trait RequestHandler: Send + Sync + 'static { + async fn prepare_request(&self) -> Option>; + async fn handle_request(&self, request: PendingRequest) -> Option<()>; + fn notify(&self); +} + +#[async_trait] +impl RequestHandler for Arc +where + T: RequestHandler, + Payload: 'static + Send + Sync, +{ + async fn prepare_request(&self) -> Option> { + (**self).prepare_request().await + } + + async fn handle_request(&self, request: PendingRequest) -> Option<()> { + (**self).handle_request(request).await + } + + fn notify(&self) { + (**self).notify() + } +} + +pub struct RequestRunner(PhantomData); +impl RequestRunner +where + Payload: 'static + Send + Sync, +{ + pub async fn run(mut notifier: watch::Receiver, server: Weak>) { + server.upgrade().unwrap().notify(); + loop { + // stops the runner if the notifier was closed. + if notifier.changed().await.is_err() { + break; + } + + // stops the runner if the value of notifier is `true` + if *notifier.borrow() { + break; + } + + if let Some(server) = server.upgrade() { + if let Some(request) = server.prepare_request().await { + if request.is_done() { + server.notify(); + continue; + } + + if request.is_processing() { + continue; + } + + let _ = server.handle_request(request).await; + server.notify(); + } + } else { + break; + } + } + } +} diff --git a/frontend/rust-lib/flowy-server/src/supabase/retry.rs b/frontend/rust-lib/flowy-server/src/supabase/retry.rs deleted file mode 100644 index 25a0598862..0000000000 --- a/frontend/rust-lib/flowy-server/src/supabase/retry.rs +++ /dev/null @@ -1,11 +0,0 @@ -// pub(crate) struct SupabaseRetryAction {} -// -// impl Action for SupabaseRetryAction { -// type Future = (); -// type Item = (); -// type Error = (); -// -// fn run(&mut self) -> Self::Future { -// todo!() -// } -// } diff --git a/frontend/rust-lib/flowy-server/src/supabase/server.rs b/frontend/rust-lib/flowy-server/src/supabase/server.rs index 2650b28a8e..788f65e2f8 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/server.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/server.rs @@ -1,77 +1,198 @@ -use std::sync::Arc; +use std::ops::Deref; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{Arc, Weak}; +use std::time::Duration; -use postgrest::Postgrest; -use serde::Deserialize; +use appflowy_integrate::RemoteCollabStorage; +use tokio::spawn; +use tokio::sync::{watch, Mutex}; +use tokio::time::interval; -use flowy_error::{ErrorCode, FlowyError}; +use flowy_database2::deps::DatabaseCloudService; +use flowy_document2::deps::DocumentCloudService; use flowy_folder2::deps::FolderCloudService; use flowy_user::event_map::UserAuthService; +use lib_infra::async_trait::async_trait; -use crate::supabase::impls::PostgrestUserAuthServiceImpl; +use crate::supabase::impls::{ + PgCollabStorageImpl, SupabaseDatabaseCloudServiceImpl, SupabaseDocumentCloudServiceImpl, + SupabaseFolderCloudServiceImpl, SupabaseUserAuthServiceImpl, +}; +use crate::supabase::pg_db::{PgClientReceiver, PostgresDB, PostgresEvent}; +use crate::supabase::queue::{ + PendingRequest, RequestHandler, RequestQueue, RequestRunner, RequestState, +}; +use crate::supabase::{PostgresConfiguration, SupabaseConfiguration}; use crate::AppFlowyServer; -pub const SUPABASE_URL: &str = "SUPABASE_URL"; -pub const SUPABASE_ANON_KEY: &str = "SUPABASE_ANON_KEY"; -pub const SUPABASE_KEY: &str = "SUPABASE_KEY"; -pub const SUPABASE_JWT_SECRET: &str = "SUPABASE_JWT_SECRET"; - -#[derive(Debug, Deserialize)] -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, -} - -impl SupabaseConfiguration { - /// Load the configuration from the environment variables. - /// SUPABASE_URL=https://.supabase.co - /// SUPABASE_KEY= - /// SUPABASE_JWT_SECRET= - /// - pub fn from_env() -> Result { - Ok(Self { - url: std::env::var(SUPABASE_URL) - .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_URL"))?, - key: std::env::var(SUPABASE_KEY) - .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_KEY"))?, - jwt_secret: std::env::var(SUPABASE_JWT_SECRET).map_err(|_| { - FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_JWT_SECRET") - })?, - }) - } - - pub fn write_env(&self) { - 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); - } -} - +/// Supabase server is used to provide the implementation of the [AppFlowyServer] trait. +/// It contains the configuration of the supabase server and the postgres server. pub struct SupabaseServer { - pub postgres: Arc, + #[allow(dead_code)] + config: SupabaseConfiguration, + postgres: Arc, } impl SupabaseServer { pub fn new(config: SupabaseConfiguration) -> Self { - let url = format!("{}/rest/v1/", config.url); - let auth = format!("Bearer {}", config.key); - let postgrest = Postgrest::new(url) - .insert_header("apikey", config.key) - .insert_header("Authorization", auth); - let postgres = Arc::new(postgrest); - Self { postgres } + let postgres = PostgresServer::new(config.postgres_config.clone()); + Self { + config, + postgres: Arc::new(postgres), + } } } impl AppFlowyServer for SupabaseServer { fn user_service(&self) -> Arc { - Arc::new(PostgrestUserAuthServiceImpl::new(self.postgres.clone())) + Arc::new(SupabaseUserAuthServiceImpl::new(self.postgres.clone())) } fn folder_service(&self) -> Arc { - todo!() + Arc::new(SupabaseFolderCloudServiceImpl::new(self.postgres.clone())) + } + + fn database_service(&self) -> Arc { + Arc::new(SupabaseDatabaseCloudServiceImpl::new(self.postgres.clone())) + } + + fn document_service(&self) -> Arc { + Arc::new(SupabaseDocumentCloudServiceImpl::new(self.postgres.clone())) + } + + fn collab_storage(&self) -> Option> { + Some(Arc::new(PgCollabStorageImpl::new(self.postgres.clone()))) + } +} + +pub struct PostgresServer { + inner: Arc, +} + +impl Deref for PostgresServer { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +pub struct PostgresServerInner { + config: PostgresConfiguration, + db: Arc>>>, + queue: parking_lot::Mutex>, + notifier: Arc>, + sequence: AtomicU32, +} + +impl PostgresServerInner { + pub fn new(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); + Self { + db, + queue, + notifier, + config, + sequence: Default::default(), + } + } + + pub async fn get_pg_client(&self) -> PgClientReceiver { + let (tx, rx) = tokio::sync::mpsc::channel(1); + let mut queue = self.queue.lock(); + + let event = PostgresEvent::GetPgClient { + id: self.sequence.fetch_add(1, Ordering::SeqCst), + sender: tx, + }; + let request = PendingRequest::new(event); + queue.push(request); + self.notify(); + PgClientReceiver(rx) + } +} + +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 { + 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); + spawn(async move { + interval(Duration::from_millis(300)).tick().await; + if let Some(notifier) = weak_notifier.upgrade() { + let _ = notifier.send(false); + } + }); + None + }, + Some(queue) => queue.peek().cloned(), + } + } + + async fn handle_request(&self, request: PendingRequest) -> Option<()> { + debug_assert!(Some(&request) == self.queue.lock().peek()); + + match request.payload { + PostgresEvent::ConnectDB => { + let is_connected = self.db.lock().await.is_some(); + if is_connected { + tracing::warn!("Already connect to postgres db"); + } else { + tracing::info!("Start connecting to postgres db"); + match PostgresDB::new(self.config.clone()).await { + Ok(db) => { + *self.db.lock().await = Some(Arc::new(db)); + if let Some(mut request) = self.queue.lock().pop() { + request.set_state(RequestState::Done); + } + }, + Err(e) => tracing::error!("Error connecting to the postgres db: {}", e), + } + } + }, + PostgresEvent::GetPgClient { id: _, sender } => { + match self.db.lock().await.as_ref().map(|db| db.client.clone()) { + None => tracing::error!("Can't get the postgres client"), + Some(pool) => { + match pool.get().await { + Ok(object) => { + if let Err(e) = sender.send(object).await { + tracing::error!("Error sending the postgres client: {}", e); + } + }, + Err(e) => tracing::error!("Get postgres client failed: {}", e), + } + + if let Some(mut request) = self.queue.lock().pop() { + request.set_state(RequestState::Done); + } + }, + } + }, + } + None + } + + fn notify(&self) { + let _ = self.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 new file mode 100644 index 0000000000..1d16abf5d5 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/supabase/sql_builder.rs @@ -0,0 +1,234 @@ +use tokio_postgres::types::ToSql; + +pub struct UpdateSqlBuilder { + table: String, + sets: Vec<(String, Box)>, + where_clause: Option<(String, Box)>, +} + +impl UpdateSqlBuilder { + pub fn new(table: &str) -> Self { + Self { + table: table.to_string(), + sets: Vec::new(), + where_clause: None, + } + } + + pub fn set(mut self, column: &str, value: Option) -> Self { + if let Some(value) = value { + self.sets.push((column.to_string(), Box::new(value))); + } + self + } + + pub fn where_clause(mut self, clause: &str, value: T) -> Self { + self.where_clause = Some((clause.to_string(), Box::new(value))); + self + } + + pub fn build(self) -> (String, Vec>) { + let mut sql = format!("UPDATE {} SET ", self.table); + + for i in 0..self.sets.len() { + if i > 0 { + sql.push_str(", "); + } + sql.push_str(&format!("{} = ${}", self.sets[i].0, i + 1)); + } + + let mut params: Vec<_> = self.sets.into_iter().map(|(_, value)| value).collect(); + + if let Some((clause, value)) = self.where_clause { + sql.push_str(&format!(" WHERE {} = ${}", clause, params.len() + 1)); + params.push(value); + } + + (sql, params) + } +} + +pub struct SelectSqlBuilder { + table: String, + columns: Vec, + where_clause: Option<(String, Box)>, + order_by: Option<(String, bool)>, + limit: Option, +} + +impl SelectSqlBuilder { + pub fn new(table: &str) -> Self { + Self { + table: table.to_string(), + columns: Vec::new(), + where_clause: None, + order_by: None, + limit: None, + } + } + + pub fn column(mut self, column: &str) -> Self { + self.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 + } + + pub fn where_clause(mut self, clause: &str, value: T) -> Self { + self.where_clause = Some((clause.to_string(), Box::new(value))); + 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 mut params: Vec<_> = Vec::new(); + if let Some((clause, value)) = self.where_clause { + sql.push_str(&format!(" WHERE {} = ${}", clause, params.len() + 1)); + params.push(value); + } + + 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)); + } + + if let Some(limit) = self.limit { + sql.push_str(&format!(" LIMIT {}", limit)); + } + + (sql, params) + } +} + +pub struct InsertSqlBuilder { + table: String, + columns: Vec, + values: Vec>, + override_system_value: bool, + returning: Vec, // Vec for returning multiple columns +} + +impl InsertSqlBuilder { + pub fn new(table: &str) -> Self { + Self { + table: table.to_string(), + columns: Vec::new(), + values: Vec::new(), + override_system_value: false, + returning: vec![], + } + } + + pub fn value(mut self, column: &str, value: T) -> Self { + self.columns.push(column.to_string()); + self.values.push(Box::new(value)); + self + } + + pub fn overriding_system_value(mut self) -> Self { + self.override_system_value = true; + self + } + + pub fn returning(mut self, column: &str) -> Self { + // add column to return + self.returning.push(column.to_string()); + self + } + + pub fn build(self) -> (String, Vec>) { + let mut query = format!("INSERT INTO {} (", self.table); + query.push_str(&self.columns.join(", ")); + query.push(')'); + + if self.override_system_value { + query.push_str(" OVERRIDING SYSTEM VALUE"); + } + + query.push_str(" VALUES ("); + query.push_str( + &(0..self.columns.len()) + .map(|i| format!("${}", i + 1)) + .collect::>() + .join(", "), + ); + query.push(')'); + + if !self.returning.is_empty() { + // add RETURNING clause if there are columns to return + query.push_str(&format!(" RETURNING {}", self.returning.join(", "))); + } + + (query, self.values) + } +} + +pub enum WhereCondition { + Equals(String, Box), + In(String, Vec>), +} + +pub struct DeleteSqlBuilder { + table: String, + conditions: Vec, +} + +impl DeleteSqlBuilder { + pub fn new(table: &str) -> Self { + Self { + table: table.to_string(), + conditions: Vec::new(), + } + } + + pub fn where_condition(mut self, condition: WhereCondition) -> Self { + self.conditions.push(condition); + self + } + + pub fn build(self) -> (String, Vec>) { + let mut sql = format!("DELETE FROM {}", self.table); + let mut params: Vec> = Vec::new(); + + if !self.conditions.is_empty() { + sql.push_str(" WHERE "); + let condition_len = self.conditions.len(); + for (i, condition) in self.conditions.into_iter().enumerate() { + match condition { + WhereCondition::Equals(column, value) => { + sql.push_str(&format!( + "{} = ${}{}", + column, + params.len() + 1, + if i < condition_len - 1 { " AND " } else { "" }, + )); + params.push(value); + }, + WhereCondition::In(column, values) => { + let placeholders: Vec = (1..=values.len()) + .map(|i| format!("${}", i + params.len())) + .collect(); + sql.push_str(&format!( + "{} IN ({}){}", + column, + placeholders.join(", "), + if i < condition_len - 1 { " AND " } else { "" }, + )); + params.extend(values); + }, + } + } + } + + (sql, params) + } +} diff --git a/frontend/rust-lib/flowy-server/src/util.rs b/frontend/rust-lib/flowy-server/src/util.rs new file mode 100644 index 0000000000..b19d381ca6 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/util.rs @@ -0,0 +1,27 @@ +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. +pub(crate) fn deserialize_null_or_default<'de, D, T>(deserializer: D) -> Result +where + T: Default + Deserialize<'de>, + D: Deserializer<'de>, +{ + 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/main.rs b/frontend/rust-lib/flowy-server/tests/main.rs new file mode 100644 index 0000000000..edbbb44472 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/main.rs @@ -0,0 +1,23 @@ +use std::sync::Once; + +use tracing_subscriber::fmt::Subscriber; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; + +mod supabase_test; + +pub fn setup_log() { + static START: Once = Once::new(); + START.call_once(|| { + let level = "trace"; + let mut filters = vec![]; + filters.push(format!("flowy_server={}", level)); + std::env::set_var("RUST_LOG", filters.join(",")); + + let subscriber = Subscriber::builder() + .with_env_filter(EnvFilter::from_default_env()) + .with_ansi(true) + .finish(); + subscriber.try_init().unwrap(); + }); +} diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs new file mode 100644 index 0000000000..3e9a615104 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs @@ -0,0 +1 @@ +mod user_test; 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 new file mode 100644 index 0000000000..ff9ce37586 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs @@ -0,0 +1,159 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use uuid::Uuid; + +use flowy_server::supabase::impls::{SupabaseUserAuthServiceImpl, USER_UUID}; +use flowy_server::supabase::{PostgresConfiguration, PostgresServer}; +use flowy_user::entities::{SignUpResponse, UpdateUserProfileParams}; +use flowy_user::event_map::{UserAuthService, UserCredentials}; +use lib_infra::box_any::BoxAny; + +use crate::setup_log; + +// ‼️‼️‼️ Warning: this test will create a table in the database +#[tokio::test] +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 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()); +} + +#[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 uuid = Uuid::new_v4(); + + let mut params = HashMap::new(); + params.insert(USER_UUID.to_string(), uuid.to_string()); + let _user: SignUpResponse = user_service + .sign_up(BoxAny::new(params.clone())) + .await + .unwrap(); + let user: SignUpResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + assert!(!user.workspace_id.is_empty()); +} + +#[tokio::test] +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 uuid = Uuid::new_v4(); + + let mut params = HashMap::new(); + params.insert(USER_UUID.to_string(), uuid.to_string()); + let user: SignUpResponse = user_service + .sign_up(BoxAny::new(params.clone())) + .await + .unwrap(); + + user_service + .update_user( + UserCredentials::from_uid(user.user_id), + UpdateUserProfileParams { + id: user.user_id, + auth_type: Default::default(), + name: Some("123".to_string()), + email: Some("123@appflowy.io".to_string()), + password: None, + icon_url: None, + openai_key: None, + }, + ) + .await + .unwrap(); + + let user_profile = user_service + .get_user_profile(UserCredentials::from_uid(user.user_id)) + .await + .unwrap() + .unwrap(); + + assert_eq!(user_profile.name, "123"); +} + +#[tokio::test] +async fn get_user_profile_test() { + if dotenv::from_filename("./.env.test").is_err() { + return; + } + setup_log(); + let server = Arc::new(PostgresServer::new( + PostgresConfiguration::from_env().unwrap(), + )); + let user_service = SupabaseUserAuthServiceImpl::new(server); + let uuid = Uuid::new_v4(); + + let mut params = HashMap::new(); + params.insert(USER_UUID.to_string(), uuid.to_string()); + let user: SignUpResponse = user_service + .sign_up(BoxAny::new(params.clone())) + .await + .unwrap(); + + let credential = UserCredentials::from_uid(user.user_id); + + user_service + .get_user_profile(credential.clone()) + .await + .unwrap() + .unwrap(); + user_service + .get_user_profile(credential.clone()) + .await + .unwrap() + .unwrap(); + user_service + .get_user_profile(credential.clone()) + .await + .unwrap() + .unwrap(); + user_service + .get_user_profile(credential.clone()) + .await + .unwrap() + .unwrap(); + user_service + .get_user_profile(credential) + .await + .unwrap() + .unwrap(); +} + +#[tokio::test] +async fn get_not_exist_user_profile_test() { + if dotenv::from_filename("./.env.test").is_err() { + return; + } + setup_log(); + let server = Arc::new(PostgresServer::new( + PostgresConfiguration::from_env().unwrap(), + )); + let user_service = SupabaseUserAuthServiceImpl::new(server); + let result = user_service + .get_user_profile(UserCredentials::from_uid(i64::MAX)) + .await + .unwrap(); + // user not found + assert!(result.is_none()); +} diff --git a/frontend/rust-lib/flowy-test/Cargo.toml b/frontend/rust-lib/flowy-test/Cargo.toml index b3a05f2926..cd26010e2f 100644 --- a/frontend/rust-lib/flowy-test/Cargo.toml +++ b/frontend/rust-lib/flowy-test/Cargo.toml @@ -17,6 +17,7 @@ lib-ot = { path = "../../../shared-lib/lib-ot" } lib-infra = { path = "../../../shared-lib/lib-infra" } flowy-server = { path = "../flowy-server" } flowy-notification = { path = "../flowy-notification" } +anyhow = "1.0.71" serde = { version = "1.0", features = ["derive"] } serde_json = {version = "1.0"} @@ -30,9 +31,17 @@ tempdir = "0.3.7" tracing = { version = "0.1.27" } parking_lot = "0.12.1" dotenv = "0.15.0" +uuid = { version = "1.3.3", features = ["serde", "v4"] } [dev-dependencies] uuid = { version = "1.3.3", features = ["v4"] } +collab = { version = "0.1.0" } +collab-document = { version = "0.1.0" } +collab-folder = { version = "0.1.0" } +collab-database = { version = "0.1.0" } +assert-json-diff = "2.0.2" [features] +default = ["cloud_test"] dart = ["flowy-core/dart"] +cloud_test = [] \ No newline at end of file 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 121517a129..29e88d7559 100644 --- a/frontend/rust-lib/flowy-test/src/document/document_event.rs +++ b/frontend/rust-lib/flowy-test/src/document/document_event.rs @@ -1,3 +1,4 @@ +use crate::document::utils::{gen_id, gen_text_block_data}; use crate::event_builder::EventBuilder; use crate::FlowyCoreTest; use flowy_document2::entities::*; @@ -5,6 +6,8 @@ use flowy_document2::event_map::DocumentEvent; use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; use flowy_folder2::event_map::FolderEvent; +const TEXT_BLOCK_TY: &str = "paragraph"; + pub struct DocumentEventTest { inner: FlowyCoreTest, } @@ -20,6 +23,10 @@ impl DocumentEventTest { Self { inner: sdk } } + pub fn new_with_core(core: FlowyCoreTest) -> Self { + Self { inner: core } + } + pub async fn create_document(&self) -> ViewPB { let core = &self.inner; let current_workspace = core.get_current_workspace().await.workspace; @@ -128,4 +135,102 @@ impl DocumentEventTest { .await .parse::() } + + /// Insert a new text block at the index of parent's children. + pub async fn insert_index( + &self, + document_id: &str, + text: &str, + index: usize, + parent_id: Option<&str>, + ) -> String { + let text = text.to_string(); + let page_id = self.get_page_id(document_id).await; + let parent_id = parent_id + .map(|id| id.to_string()) + .unwrap_or_else(|| page_id); + let parent_children = self.get_block_children(document_id, &parent_id).await; + + let prev_id = { + // If index is 0, then the new block will be the first child of parent. + if index == 0 { + None + } else { + parent_children.and_then(|children| { + // If index is greater than the length of children, then the new block will be the last child of parent. + if index >= children.len() { + children.last().cloned() + } else { + children.get(index - 1).cloned() + } + }) + } + }; + + let new_block_id = gen_id(); + let data = gen_text_block_data(&text); + + let new_block = BlockPB { + id: new_block_id.clone(), + ty: TEXT_BLOCK_TY.to_string(), + data, + parent_id: parent_id.clone(), + children_id: gen_id(), + }; + let action = BlockActionPB { + action: BlockActionTypePB::Insert, + payload: BlockActionPayloadPB { + block: new_block, + prev_id, + parent_id: Some(parent_id), + }, + }; + let payload = ApplyActionPayloadPB { + document_id: document_id.to_string(), + actions: vec![action], + }; + self.apply_actions(payload).await; + new_block_id + } + + pub async fn update(&self, document_id: &str, block_id: &str, text: &str) { + let block = self.get_block(document_id, block_id).await.unwrap(); + let data = gen_text_block_data(text); + let new_block = { + let mut new_block = block.clone(); + new_block.data = data; + new_block + }; + let action = BlockActionPB { + action: BlockActionTypePB::Update, + payload: BlockActionPayloadPB { + block: new_block, + prev_id: None, + parent_id: Some(block.parent_id.clone()), + }, + }; + let payload = ApplyActionPayloadPB { + document_id: document_id.to_string(), + actions: vec![action], + }; + self.apply_actions(payload).await; + } + + pub async fn delete(&self, document_id: &str, block_id: &str) { + let block = self.get_block(document_id, block_id).await.unwrap(); + let parent_id = block.parent_id.clone(); + let action = BlockActionPB { + action: BlockActionTypePB::Delete, + payload: BlockActionPayloadPB { + block, + prev_id: None, + parent_id: Some(parent_id), + }, + }; + let payload = ApplyActionPayloadPB { + document_id: document_id.to_string(), + actions: vec![action], + }; + self.apply_actions(payload).await; + } } diff --git a/frontend/rust-lib/flowy-test/src/document/mod.rs b/frontend/rust-lib/flowy-test/src/document/mod.rs index 89fffd26bb..ffdc5ac089 100644 --- a/frontend/rust-lib/flowy-test/src/document/mod.rs +++ b/frontend/rust-lib/flowy-test/src/document/mod.rs @@ -1,3 +1,2 @@ pub mod document_event; -pub mod text_block_event; pub mod utils; diff --git a/frontend/rust-lib/flowy-test/src/document/text_block_event.rs b/frontend/rust-lib/flowy-test/src/document/text_block_event.rs deleted file mode 100644 index dba583957b..0000000000 --- a/frontend/rust-lib/flowy-test/src/document/text_block_event.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::document::document_event::DocumentEventTest; -use crate::document::utils::{gen_id, gen_text_block_data}; -use flowy_document2::entities::*; -use std::sync::Arc; - -const TEXT_BLOCK_TY: &str = "paragraph"; - -pub struct TextBlockEventTest { - doc: Arc, - doc_id: String, -} - -impl TextBlockEventTest { - pub async fn new() -> Self { - let doc = DocumentEventTest::new().await; - let doc_id = doc.create_document().await.id; - Self { - doc: Arc::new(doc), - doc_id, - } - } - - pub async fn get(&self, block_id: &str) -> Option { - let doc = self.doc.clone(); - let doc_id = self.doc_id.clone(); - doc.get_block(&doc_id, block_id).await - } - - /// Insert a new text block at the index of parent's children. - pub async fn insert_index(&self, text: String, index: usize, parent_id: Option<&str>) -> String { - let doc = self.doc.clone(); - let doc_id = self.doc_id.clone(); - let page_id = self.doc.get_page_id(&doc_id).await; - let parent_id = parent_id - .map(|id| id.to_string()) - .unwrap_or_else(|| page_id); - let parent_children = self.doc.get_block_children(&doc_id, &parent_id).await; - - let prev_id = { - // If index is 0, then the new block will be the first child of parent. - if index == 0 { - None - } else { - parent_children.and_then(|children| { - // If index is greater than the length of children, then the new block will be the last child of parent. - if index >= children.len() { - children.last().cloned() - } else { - children.get(index - 1).cloned() - } - }) - } - }; - - let new_block_id = gen_id(); - let data = gen_text_block_data(text); - - let new_block = BlockPB { - id: new_block_id.clone(), - ty: TEXT_BLOCK_TY.to_string(), - data, - parent_id: parent_id.clone(), - children_id: gen_id(), - }; - let action = BlockActionPB { - action: BlockActionTypePB::Insert, - payload: BlockActionPayloadPB { - block: new_block, - prev_id, - parent_id: Some(parent_id), - }, - }; - let payload = ApplyActionPayloadPB { - document_id: doc_id, - actions: vec![action], - }; - doc.apply_actions(payload).await; - new_block_id - } - - pub async fn update(&self, block_id: &str, text: String) { - let doc = self.doc.clone(); - let doc_id = self.doc_id.clone(); - let block = self.get(block_id).await.unwrap(); - let data = gen_text_block_data(text); - let new_block = { - let mut new_block = block.clone(); - new_block.data = data; - new_block - }; - let action = BlockActionPB { - action: BlockActionTypePB::Update, - payload: BlockActionPayloadPB { - block: new_block, - prev_id: None, - parent_id: Some(block.parent_id.clone()), - }, - }; - let payload = ApplyActionPayloadPB { - document_id: doc_id, - actions: vec![action], - }; - doc.apply_actions(payload).await; - } - - pub async fn delete(&self, block_id: &str) { - let doc = self.doc.clone(); - let doc_id = self.doc_id.clone(); - let block = self.get(block_id).await.unwrap(); - let parent_id = block.parent_id.clone(); - let action = BlockActionPB { - action: BlockActionTypePB::Delete, - payload: BlockActionPayloadPB { - block, - prev_id: None, - parent_id: Some(parent_id), - }, - }; - let payload = ApplyActionPayloadPB { - document_id: doc_id, - actions: vec![action], - }; - doc.apply_actions(payload).await; - } -} diff --git a/frontend/rust-lib/flowy-test/src/document/utils.rs b/frontend/rust-lib/flowy-test/src/document/utils.rs index b94be77fc5..fdc6959162 100644 --- a/frontend/rust-lib/flowy-test/src/document/utils.rs +++ b/frontend/rust-lib/flowy-test/src/document/utils.rs @@ -8,7 +8,7 @@ pub fn gen_id() -> String { nanoid!(10) } -pub fn gen_text_block_data(text: String) -> String { +pub fn gen_text_block_data(text: &str) -> String { json!({ "delta": [{ "insert": text diff --git a/frontend/rust-lib/flowy-test/src/event_builder.rs b/frontend/rust-lib/flowy-test/src/event_builder.rs index 826262f416..943507add3 100644 --- a/frontend/rust-lib/flowy-test/src/event_builder.rs +++ b/frontend/rust-lib/flowy-test/src/event_builder.rs @@ -1,8 +1,3 @@ -use crate::FlowyCoreTest; -use flowy_user::errors::FlowyError; -use lib_dispatch::prelude::{ - AFPluginDispatcher, AFPluginEventResponse, AFPluginFromBytes, AFPluginRequest, ToBytes, *, -}; use std::{ convert::TryFrom, fmt::{Debug, Display}, @@ -10,6 +5,13 @@ use std::{ sync::Arc, }; +use flowy_user::errors::{internal_error, FlowyError}; +use lib_dispatch::prelude::{ + AFPluginDispatcher, AFPluginEventResponse, AFPluginFromBytes, AFPluginRequest, ToBytes, *, +}; + +use crate::FlowyCoreTest; + #[derive(Clone)] pub struct EventBuilder { context: TestContext, @@ -84,6 +86,14 @@ impl EventBuilder { } } + pub fn try_parse(self) -> Result + where + R: AFPluginFromBytes, + { + let response = self.get_response(); + response.parse::().map_err(internal_error)? + } + pub fn error(self) -> Option { let response = self.get_response(); >::try_from(response.payload) diff --git a/frontend/rust-lib/flowy-test/src/lib.rs b/frontend/rust-lib/flowy-test/src/lib.rs index cfd967ba77..b1871314fb 100644 --- a/frontend/rust-lib/flowy-test/src/lib.rs +++ b/frontend/rust-lib/flowy-test/src/lib.rs @@ -1,19 +1,30 @@ +use std::collections::HashMap; use std::convert::TryFrom; use std::env::temp_dir; +use std::path::PathBuf; use std::sync::Arc; use bytes::Bytes; use nanoid::nanoid; use parking_lot::RwLock; +use protobuf::ProtobufError; +use tokio::sync::broadcast::{channel, Sender}; use flowy_core::{AppFlowyCore, AppFlowyCoreConfig}; use flowy_database2::entities::*; +use flowy_database2::event_map::DatabaseEvent; +use flowy_document2::entities::{DocumentDataPB, OpenDocumentPayloadPB}; +use flowy_document2::event_map::DocumentEvent; use flowy_folder2::entities::*; -use flowy_user::entities::{AuthTypePB, UserProfilePB}; +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::errors::FlowyError; +use flowy_user::event_map::UserEvent::*; use crate::event_builder::EventBuilder; -use crate::user_event::{async_sign_up, init_user_setting, SignUpContext}; +use crate::user_event::{async_sign_up, SignUpContext}; pub mod document; pub mod event_builder; @@ -24,19 +35,30 @@ pub mod user_event; pub struct FlowyCoreTest { auth_type: Arc>, inner: AppFlowyCore, + cleaner: Arc>>, + pub notification_sender: TestNotificationSender, } impl Default for FlowyCoreTest { fn default() -> Self { let temp_dir = temp_dir(); - let config = - AppFlowyCoreConfig::new(temp_dir.to_str().unwrap(), nanoid!(6)).log_filter("info", vec![]); + let config = AppFlowyCoreConfig::new(temp_dir.to_str().unwrap(), nanoid!(6)) + .log_filter("debug", vec!["flowy_test".to_string()]); + let inner = std::thread::spawn(|| AppFlowyCore::new(config)) .join() .unwrap(); let auth_type = Arc::new(RwLock::new(AuthTypePB::Local)); + let notification_sender = TestNotificationSender::new(); + register_notification_sender(notification_sender.clone()); + std::mem::forget(inner.dispatcher()); - Self { inner, auth_type } + Self { + inner, + auth_type, + notification_sender, + cleaner: Arc::new(RwLock::new(None)), + } } } @@ -53,23 +75,51 @@ impl FlowyCoreTest { pub async fn sign_up(&self) -> SignUpContext { let auth_type = self.auth_type.read().clone(); + async_sign_up(self.inner.dispatcher(), auth_type).await } + 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; + } + pub fn set_auth_type(&self, auth_type: AuthTypePB) { *self.auth_type.write() = auth_type; } pub async fn init_user(&self) -> UserProfilePB { - let auth_type = self.auth_type.read().clone(); - let context = async_sign_up(self.inner.dispatcher(), auth_type).await; - init_user_setting(self.inner.dispatcher()).await; - context.user_profile + self.sign_up().await.user_profile } + pub async fn sign_up_with_uuid(&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, + }; + + let user_profile = EventBuilder::new(self.clone()) + .event(ThirdPartyAuth) + .payload(payload) + .async_send() + .await + .parse::(); + + let user_path = PathBuf::from(&self.config.storage_path).join(user_profile.id.to_string()); + *self.cleaner.write() = Some(Cleaner::new(user_path)); + user_profile + } + + // Must sign up/ sign in first pub async fn get_current_workspace(&self) -> WorkspaceSettingPB { EventBuilder::new(self.clone()) - .event(flowy_folder2::event_map::FolderEvent::GetCurrentWorkspace) + .event(FolderEvent::GetCurrentWorkspace) .async_send() .await .parse::() @@ -77,7 +127,7 @@ impl FlowyCoreTest { pub async fn get_all_workspace_views(&self) -> Vec { EventBuilder::new(self.clone()) - .event(flowy_folder2::event_map::FolderEvent::ReadWorkspaceViews) + .event(FolderEvent::ReadWorkspaceViews) .async_send() .await .parse::() @@ -91,7 +141,7 @@ impl FlowyCoreTest { // delete the view. the view will be moved to trash EventBuilder::new(self.clone()) - .event(flowy_folder2::event_map::FolderEvent::DeleteView) + .event(FolderEvent::DeleteView) .payload(payload) .async_send() .await; @@ -100,7 +150,7 @@ impl FlowyCoreTest { pub async fn update_view(&self, changeset: UpdateViewPayloadPB) -> Option { // delete the view. the view will be moved to trash EventBuilder::new(self.clone()) - .event(flowy_folder2::event_map::FolderEvent::UpdateView) + .event(FolderEvent::UpdateView) .payload(changeset) .async_send() .await @@ -119,13 +169,50 @@ impl FlowyCoreTest { set_as_current: false, }; EventBuilder::new(self.clone()) - .event(flowy_folder2::event_map::FolderEvent::CreateView) + .event(FolderEvent::CreateView) .payload(payload) .async_send() .await .parse::() } + pub async fn create_document( + &self, + parent_id: &str, + name: &str, + initial_data: Vec, + ) -> ViewPB { + let payload = CreateViewPayloadPB { + parent_view_id: parent_id.to_string(), + name: name.to_string(), + desc: "".to_string(), + thumbnail: None, + layout: ViewLayoutPB::Document, + initial_data, + meta: Default::default(), + set_as_current: true, + }; + let view = EventBuilder::new(self.clone()) + .event(FolderEvent::CreateView) + .payload(payload) + .async_send() + .await + .parse::(); + + let payload = OpenDocumentPayloadPB { + document_id: view.id.clone(), + }; + + let _ = EventBuilder::new(self.clone()) + .event(DocumentEvent::OpenDocument) + .payload(payload) + .async_send() + .await + .parse::(); + + view + } + pub async fn create_grid(&self, parent_id: &str, name: String, initial_data: Vec) -> ViewPB { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), @@ -138,13 +225,23 @@ impl FlowyCoreTest { set_as_current: true, }; EventBuilder::new(self.clone()) - .event(flowy_folder2::event_map::FolderEvent::CreateView) + .event(FolderEvent::CreateView) .payload(payload) .async_send() .await .parse::() } + pub async fn open_database(&self, view_id: &str) { + EventBuilder::new(self.clone()) + .event(DatabaseEvent::GetDatabase) + .payload(DatabaseViewIdPB { + value: view_id.to_string(), + }) + .async_send() + .await; + } + pub async fn create_board(&self, parent_id: &str, name: String, initial_data: Vec) -> ViewPB { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), @@ -157,7 +254,7 @@ impl FlowyCoreTest { set_as_current: true, }; EventBuilder::new(self.clone()) - .event(flowy_folder2::event_map::FolderEvent::CreateView) + .event(FolderEvent::CreateView) .payload(payload) .async_send() .await @@ -181,7 +278,7 @@ impl FlowyCoreTest { set_as_current: true, }; EventBuilder::new(self.clone()) - .event(flowy_folder2::event_map::FolderEvent::CreateView) + .event(FolderEvent::CreateView) .payload(payload) .async_send() .await @@ -190,7 +287,7 @@ impl FlowyCoreTest { pub async fn get_database(&self, view_id: &str) -> DatabasePB { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::GetDatabase) + .event(DatabaseEvent::GetDatabase) .payload(DatabaseViewIdPB { value: view_id.to_string(), }) @@ -201,7 +298,7 @@ impl FlowyCoreTest { pub async fn get_all_database_fields(&self, view_id: &str) -> RepeatedFieldPB { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::GetFields) + .event(DatabaseEvent::GetFields) .payload(GetFieldPayloadPB { view_id: view_id.to_string(), field_ids: None, @@ -213,7 +310,7 @@ impl FlowyCoreTest { pub async fn create_field(&self, view_id: &str, field_type: FieldType) -> FieldPB { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::CreateTypeOption) + .event(DatabaseEvent::CreateTypeOption) .payload(CreateFieldPayloadPB { view_id: view_id.to_string(), field_type, @@ -227,7 +324,7 @@ impl FlowyCoreTest { pub async fn update_field(&self, changeset: FieldChangesetPB) { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::UpdateField) + .event(DatabaseEvent::UpdateField) .payload(changeset) .async_send() .await; @@ -235,7 +332,7 @@ impl FlowyCoreTest { pub async fn delete_field(&self, view_id: &str, field_id: &str) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::DeleteField) + .event(DatabaseEvent::DeleteField) .payload(DeleteFieldPayloadPB { view_id: view_id.to_string(), field_id: field_id.to_string(), @@ -252,7 +349,7 @@ impl FlowyCoreTest { field_type: FieldType, ) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::UpdateFieldType) + .event(DatabaseEvent::UpdateFieldType) .payload(UpdateFieldTypePayloadPB { view_id: view_id.to_string(), field_id: field_id.to_string(), @@ -265,7 +362,7 @@ impl FlowyCoreTest { pub async fn duplicate_field(&self, view_id: &str, field_id: &str) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::DuplicateField) + .event(DatabaseEvent::DuplicateField) .payload(DuplicateFieldPayloadPB { view_id: view_id.to_string(), field_id: field_id.to_string(), @@ -277,7 +374,7 @@ impl FlowyCoreTest { pub async fn get_primary_field(&self, database_view_id: &str) -> FieldPB { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::GetPrimaryField) + .event(DatabaseEvent::GetPrimaryField) .payload(DatabaseViewIdPB { value: database_view_id.to_string(), }) @@ -293,7 +390,7 @@ impl FlowyCoreTest { data: Option, ) -> RowMetaPB { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::CreateRow) + .event(DatabaseEvent::CreateRow) .payload(CreateRowPayloadPB { view_id: view_id.to_string(), start_row_id, @@ -307,7 +404,7 @@ impl FlowyCoreTest { pub async fn delete_row(&self, view_id: &str, row_id: &str) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::DeleteRow) + .event(DatabaseEvent::DeleteRow) .payload(RowIdPB { view_id: view_id.to_string(), row_id: row_id.to_string(), @@ -320,7 +417,7 @@ impl FlowyCoreTest { pub async fn get_row(&self, view_id: &str, row_id: &str) -> OptionalRowPB { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::GetRow) + .event(DatabaseEvent::GetRow) .payload(RowIdPB { view_id: view_id.to_string(), row_id: row_id.to_string(), @@ -333,7 +430,7 @@ impl FlowyCoreTest { pub async fn get_row_meta(&self, view_id: &str, row_id: &str) -> RowMetaPB { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::GetRowMeta) + .event(DatabaseEvent::GetRowMeta) .payload(RowIdPB { view_id: view_id.to_string(), row_id: row_id.to_string(), @@ -346,7 +443,7 @@ impl FlowyCoreTest { pub async fn update_row_meta(&self, changeset: UpdateRowMetaChangesetPB) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::UpdateRowMeta) + .event(DatabaseEvent::UpdateRowMeta) .payload(changeset) .async_send() .await @@ -355,7 +452,7 @@ impl FlowyCoreTest { pub async fn duplicate_row(&self, view_id: &str, row_id: &str) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::DuplicateRow) + .event(DatabaseEvent::DuplicateRow) .payload(RowIdPB { view_id: view_id.to_string(), row_id: row_id.to_string(), @@ -368,7 +465,7 @@ impl FlowyCoreTest { pub async fn move_row(&self, view_id: &str, row_id: &str, to_row_id: &str) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::MoveRow) + .event(DatabaseEvent::MoveRow) .payload(MoveRowPayloadPB { view_id: view_id.to_string(), from_row_id: row_id.to_string(), @@ -381,7 +478,7 @@ impl FlowyCoreTest { pub async fn update_cell(&self, changeset: CellChangesetPB) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::UpdateCell) + .event(DatabaseEvent::UpdateCell) .payload(changeset) .async_send() .await @@ -390,7 +487,7 @@ impl FlowyCoreTest { pub async fn update_date_cell(&self, changeset: DateChangesetPB) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::UpdateDateCell) + .event(DatabaseEvent::UpdateDateCell) .payload(changeset) .async_send() .await @@ -399,7 +496,7 @@ impl FlowyCoreTest { pub async fn get_cell(&self, view_id: &str, row_id: &str, field_id: &str) -> CellPB { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::GetCell) + .event(DatabaseEvent::GetCell) .payload(CellIdPB { view_id: view_id.to_string(), row_id: row_id.to_string(), @@ -422,7 +519,7 @@ impl FlowyCoreTest { row_id: &str, ) -> ChecklistCellDataPB { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::GetChecklistCellData) + .event(DatabaseEvent::GetChecklistCellData) .payload(CellIdPB { view_id: view_id.to_string(), row_id: row_id.to_string(), @@ -438,7 +535,7 @@ impl FlowyCoreTest { changeset: ChecklistCellDataChangesetPB, ) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::UpdateChecklistCell) + .event(DatabaseEvent::UpdateChecklistCell) .payload(changeset) .async_send() .await @@ -453,7 +550,7 @@ impl FlowyCoreTest { name: &str, ) -> Option { let option = EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::CreateSelectOption) + .event(DatabaseEvent::CreateSelectOption) .payload(CreateSelectOptionPayloadPB { field_id: field_id.to_string(), view_id: view_id.to_string(), @@ -464,7 +561,7 @@ impl FlowyCoreTest { .parse::(); EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::InsertOrUpdateSelectOption) + .event(DatabaseEvent::InsertOrUpdateSelectOption) .payload(RepeatedSelectOptionPayload { view_id: view_id.to_string(), field_id: field_id.to_string(), @@ -478,7 +575,7 @@ impl FlowyCoreTest { pub async fn get_groups(&self, view_id: &str) -> Vec { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::GetGroups) + .event(DatabaseEvent::GetGroups) .payload(DatabaseViewIdPB { value: view_id.to_string(), }) @@ -490,7 +587,7 @@ impl FlowyCoreTest { pub async fn move_group(&self, view_id: &str, from_id: &str, to_id: &str) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::MoveGroup) + .event(DatabaseEvent::MoveGroup) .payload(MoveGroupPayloadPB { view_id: view_id.to_string(), from_group_id: from_id.to_string(), @@ -503,7 +600,7 @@ impl FlowyCoreTest { pub async fn set_group_by_field(&self, view_id: &str, field_id: &str) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::SetGroupByField) + .event(DatabaseEvent::SetGroupByField) .payload(GroupByFieldPayloadPB { field_id: field_id.to_string(), view_id: view_id.to_string(), @@ -521,7 +618,7 @@ impl FlowyCoreTest { visible: Option, ) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::UpdateGroup) + .event(DatabaseEvent::UpdateGroup) .payload(UpdateGroupPB { view_id: view_id.to_string(), group_id: group_id.to_string(), @@ -535,7 +632,7 @@ impl FlowyCoreTest { pub async fn update_setting(&self, changeset: DatabaseSettingChangesetPB) -> Option { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::UpdateDatabaseSetting) + .event(DatabaseEvent::UpdateDatabaseSetting) .payload(changeset) .async_send() .await @@ -544,7 +641,7 @@ impl FlowyCoreTest { pub async fn get_all_calendar_events(&self, view_id: &str) -> Vec { EventBuilder::new(self.clone()) - .event(flowy_database2::event_map::DatabaseEvent::GetAllCalendarEvents) + .event(DatabaseEvent::GetAllCalendarEvents) .payload(CalendarEventRequestPB { view_id: view_id.to_string(), }) @@ -556,7 +653,7 @@ impl FlowyCoreTest { pub async fn get_view(&self, view_id: &str) -> ViewPB { EventBuilder::new(self.clone()) - .event(flowy_folder2::event_map::FolderEvent::ReadView) + .event(FolderEvent::ReadView) .payload(ViewIdPB { value: view_id.to_string(), }) @@ -574,12 +671,92 @@ impl std::ops::Deref for FlowyCoreTest { } } -// pub struct TestNotificationSender { -// pub(crate) sender: tokio::sync::mpsc::Sender<()>, -// } -// -// impl NotificationSender for TestNotificationSender { -// fn send_subject(&self, subject: SubscribeObject) -> Result<(), String> { -// todo!() -// } -// } +#[derive(Clone)] +pub struct TestNotificationSender { + sender: Arc>, +} + +impl Default for TestNotificationSender { + fn default() -> Self { + let (sender, _) = channel(1000); + Self { + sender: Arc::new(sender), + } + } +} + +impl TestNotificationSender { + pub fn new() -> Self { + Self::default() + } + + pub fn subscribe(&self, id: &str) -> tokio::sync::mpsc::Receiver + where + T: TryFrom + Send + 'static, + { + let id = id.to_string(); + let (tx, rx) = tokio::sync::mpsc::channel::(10); + let mut receiver = self.sender.subscribe(); + tokio::spawn(async move { + while let Ok(value) = receiver.recv().await { + if value.id == id { + if let Some(payload) = value.payload { + if let Ok(object) = T::try_from(Bytes::from(payload)) { + let _ = tx.send(object).await; + } + } + } + } + }); + rx + } + + pub fn subscribe_with_condition(&self, id: &str, when: F) -> tokio::sync::mpsc::Receiver + where + T: TryFrom + Send + 'static, + F: Fn(&T) -> bool + Send + 'static, + { + let id = id.to_string(); + let (tx, rx) = tokio::sync::mpsc::channel::(10); + let mut receiver = self.sender.subscribe(); + tokio::spawn(async move { + while let Ok(value) = receiver.recv().await { + if value.id == id { + if let Some(payload) = value.payload { + if let Ok(object) = T::try_from(Bytes::from(payload)) { + if when(&object) { + let _ = tx.send(object).await; + } + } + } + } + } + }); + rx + } +} + +impl NotificationSender for TestNotificationSender { + fn send_subject(&self, subject: SubscribeObject) -> Result<(), String> { + let _ = self.sender.send(subject); + Ok(()) + } +} + +struct Cleaner(PathBuf); + +impl Cleaner { + fn new(dir: PathBuf) -> Self { + Cleaner(dir) + } + + fn cleanup(dir: &PathBuf) { + let _ = std::fs::remove_dir_all(dir); + } +} + +impl Drop for Cleaner { + fn drop(&mut self) { + Self::cleanup(&self.0) + } +} diff --git a/frontend/rust-lib/flowy-test/src/user_event.rs b/frontend/rust-lib/flowy-test/src/user_event.rs index 5bba21115b..f445c174e0 100644 --- a/frontend/rust-lib/flowy-test/src/user_event.rs +++ b/frontend/rust-lib/flowy-test/src/user_event.rs @@ -1,9 +1,11 @@ +use std::sync::Arc; + +use nanoid::nanoid; + use flowy_user::entities::{AuthTypePB, SignInPayloadPB, SignUpPayloadPB, UserProfilePB}; use flowy_user::errors::FlowyError; use flowy_user::event_map::UserEvent::*; use lib_dispatch::prelude::{AFPluginDispatcher, AFPluginRequest, ToBytes}; -use nanoid::nanoid; -use std::sync::Arc; pub fn random_email() -> String { format!("{}@appflowy.io", nanoid!(20)) @@ -96,8 +98,3 @@ fn sign_in(dispatch: Arc) -> UserProfilePB { .unwrap() .unwrap() } - -#[allow(dead_code)] -fn logout(dispatch: Arc) { - let _ = AFPluginDispatcher::sync_send(dispatch, AFPluginRequest::new(SignOut)); -} diff --git a/frontend/rust-lib/flowy-test/tests/asset/database_template_1.afdb b/frontend/rust-lib/flowy-test/tests/asset/database_template_1.afdb new file mode 100644 index 0000000000..04a49dead3 --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/asset/database_template_1.afdb @@ -0,0 +1,11 @@ +"{""id"":""2_OVWb"",""name"":""Name"",""field_type"":0,""visibility"":true,""width"":150,""type_options"":{""0"":{""data"":""""}},""is_primary"":true}","{""id"":""xjmOSi"",""name"":""Type"",""field_type"":3,""visibility"":true,""width"":150,""type_options"":{""3"":{""content"":""{\""options\"":[{\""id\"":\""t1WZ\"",\""name\"":\""s6\"",\""color\"":\""Lime\""},{\""id\"":\""GzNa\"",\""name\"":\""s5\"",\""color\"":\""Yellow\""},{\""id\"":\""l_8w\"",\""name\"":\""s4\"",\""color\"":\""Orange\""},{\""id\"":\""TzVT\"",\""name\"":\""s3\"",\""color\"":\""LightPink\""},{\""id\"":\""b5WF\"",\""name\"":\""s2\"",\""color\"":\""Pink\""},{\""id\"":\""AcHA\"",\""name\"":\""s1\"",\""color\"":\""Purple\""}],\""disable_color\"":false}""}},""is_primary"":false}","{""id"":""Hpbiwr"",""name"":""Done"",""field_type"":5,""visibility"":true,""width"":150,""type_options"":{""5"":{""is_selected"":false}},""is_primary"":false}","{""id"":""F7WLnw"",""name"":""checklist"",""field_type"":7,""visibility"":true,""width"":120,""type_options"":{""0"":{""data"":""""},""7"":{}},""is_primary"":false}","{""id"":""KABhMe"",""name"":""number"",""field_type"":1,""visibility"":true,""width"":120,""type_options"":{""1"":{""format"":0,""symbol"":""RUB"",""scale"":0,""name"":""Number""},""0"":{""scale"":0,""data"":"""",""format"":0,""name"":""Number"",""symbol"":""RUB""}},""is_primary"":false}","{""id"":""lEn6Bv"",""name"":""date"",""field_type"":2,""visibility"":true,""width"":120,""type_options"":{""2"":{""field_type"":2,""time_format"":1,""timezone_id"":"""",""date_format"":3},""0"":{""field_type"":2,""date_format"":3,""time_format"":1,""data"":"""",""timezone_id"":""""}},""is_primary"":false}","{""id"":""B8Prnx"",""name"":""url"",""field_type"":6,""visibility"":true,""width"":120,""type_options"":{""6"":{""content"":"""",""url"":""""},""0"":{""content"":"""",""data"":"""",""url"":""""}},""is_primary"":false}","{""id"":""MwUow4"",""name"":""multi-select"",""field_type"":4,""visibility"":true,""width"":240,""type_options"":{""0"":{""content"":""{\""options\"":[],\""disable_color\"":false}"",""data"":""""},""4"":{""content"":""{\""options\"":[{\""id\"":\""__Us\"",\""name\"":\""m7\"",\""color\"":\""Green\""},{\""id\"":\""n9-g\"",\""name\"":\""m6\"",\""color\"":\""Lime\""},{\""id\"":\""KFYu\"",\""name\"":\""m5\"",\""color\"":\""Yellow\""},{\""id\"":\""KftP\"",\""name\"":\""m4\"",\""color\"":\""Orange\""},{\""id\"":\""5lWo\"",\""name\"":\""m3\"",\""color\"":\""LightPink\""},{\""id\"":\""Djrz\"",\""name\"":\""m2\"",\""color\"":\""Pink\""},{\""id\"":\""2uRu\"",\""name\"":\""m1\"",\""color\"":\""Purple\""}],\""disable_color\"":false}""}},""is_primary"":false}" +"{""field_type"":0,""created_at"":1686793246,""data"":""A"",""last_modified"":1686793246}","{""last_modified"":1686793275,""created_at"":1686793261,""data"":""AcHA"",""field_type"":3}","{""created_at"":1686793241,""field_type"":5,""last_modified"":1686793241,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""pi1A\"",\""name\"":\""t1\"",\""color\"":\""Purple\""},{\""id\"":\""6Pym\"",\""name\"":\""t2\"",\""color\"":\""Purple\""},{\""id\"":\""erEe\"",\""name\"":\""t3\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""pi1A\"",\""6Pym\""]}"",""created_at"":1686793302,""field_type"":7,""last_modified"":1686793308}","{""created_at"":1686793333,""field_type"":1,""data"":""-1"",""last_modified"":1686793333}","{""last_modified"":1686793370,""field_type"":2,""data"":""1685583770"",""include_time"":false,""created_at"":1686793370}","{""created_at"":1686793395,""data"":""appflowy.io"",""field_type"":6,""last_modified"":1686793399,""url"":""https://appflowy.io""}","{""last_modified"":1686793446,""field_type"":4,""data"":""2uRu"",""created_at"":1686793428}" +"{""last_modified"":1686793247,""data"":""B"",""field_type"":0,""created_at"":1686793247}","{""created_at"":1686793278,""data"":""b5WF"",""field_type"":3,""last_modified"":1686793278}","{""created_at"":1686793292,""last_modified"":1686793292,""data"":""Yes"",""field_type"":5}","{""data"":""{\""options\"":[{\""id\"":\""YHDO\"",\""name\"":\""t1\"",\""color\"":\""Purple\""},{\""id\"":\""QjtW\"",\""name\"":\""t2\"",\""color\"":\""Purple\""},{\""id\"":\""K2nM\"",\""name\"":\""t3\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""YHDO\""]}"",""field_type"":7,""last_modified"":1686793318,""created_at"":1686793311}","{""data"":""-2"",""last_modified"":1686793335,""created_at"":1686793335,""field_type"":1}","{""field_type"":2,""data"":""1685670174"",""include_time"":false,""created_at"":1686793374,""last_modified"":1686793374}","{""last_modified"":1686793403,""field_type"":6,""created_at"":1686793399,""url"":"""",""data"":""no url""}","{""data"":""2uRu,Djrz"",""field_type"":4,""last_modified"":1686793449,""created_at"":1686793449}" +"{""data"":""C"",""created_at"":1686793248,""last_modified"":1686793248,""field_type"":0}","{""created_at"":1686793280,""field_type"":3,""data"":""TzVT"",""last_modified"":1686793280}","{""data"":""Yes"",""last_modified"":1686793292,""field_type"":5,""created_at"":1686793292}","{""last_modified"":1686793329,""field_type"":7,""created_at"":1686793322,""data"":""{\""options\"":[{\""id\"":\""iWM1\"",\""name\"":\""t1\"",\""color\"":\""Purple\""},{\""id\"":\""WDvF\"",\""name\"":\""t2\"",\""color\"":\""Purple\""},{\""id\"":\""w3k7\"",\""name\"":\""t3\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""iWM1\"",\""WDvF\"",\""w3k7\""]}""}","{""field_type"":1,""last_modified"":1686793339,""data"":""0.1"",""created_at"":1686793339}","{""last_modified"":1686793377,""data"":""1685756577"",""created_at"":1686793377,""include_time"":false,""field_type"":2}","{""created_at"":1686793403,""field_type"":6,""data"":""appflowy.io"",""last_modified"":1686793408,""url"":""https://appflowy.io""}","{""data"":""2uRu,Djrz,5lWo"",""created_at"":1686793453,""last_modified"":1686793454,""field_type"":4}" +"{""data"":""D"",""last_modified"":1686793249,""created_at"":1686793249,""field_type"":0}","{""data"":""l_8w"",""created_at"":1686793284,""last_modified"":1686793284,""field_type"":3}","{""data"":""Yes"",""created_at"":1686793293,""last_modified"":1686793293,""field_type"":5}",,"{""field_type"":1,""last_modified"":1686793341,""created_at"":1686793341,""data"":""0.2""}","{""created_at"":1686793379,""last_modified"":1686793379,""field_type"":2,""data"":""1685842979"",""include_time"":false}","{""last_modified"":1686793419,""field_type"":6,""created_at"":1686793408,""data"":""https://github.com/AppFlowy-IO/"",""url"":""https://github.com/AppFlowy-IO/""}","{""data"":""2uRu,Djrz,5lWo"",""last_modified"":1686793459,""field_type"":4,""created_at"":1686793459}" +"{""field_type"":0,""last_modified"":1686793250,""created_at"":1686793250,""data"":""E""}","{""field_type"":3,""last_modified"":1686793290,""created_at"":1686793290,""data"":""GzNa""}","{""last_modified"":1686793294,""created_at"":1686793294,""data"":""Yes"",""field_type"":5}",,"{""created_at"":1686793346,""field_type"":1,""last_modified"":1686793346,""data"":""1""}","{""last_modified"":1686793383,""data"":""1685929383"",""field_type"":2,""include_time"":false,""created_at"":1686793383}","{""field_type"":6,""url"":"""",""data"":"""",""last_modified"":1686793421,""created_at"":1686793419}","{""field_type"":4,""last_modified"":1686793465,""data"":""2uRu,Djrz,5lWo,KFYu,KftP"",""created_at"":1686793463}" +"{""field_type"":0,""created_at"":1686793251,""data"":"""",""last_modified"":1686793289}",,,,"{""data"":""2"",""field_type"":1,""created_at"":1686793347,""last_modified"":1686793347}","{""include_time"":false,""data"":""1685929385"",""last_modified"":1686793385,""field_type"":2,""created_at"":1686793385}",, +"{""created_at"":1686793254,""field_type"":0,""last_modified"":1686793288,""data"":""""}",,,,"{""created_at"":1686793351,""last_modified"":1686793351,""data"":""10"",""field_type"":1}","{""include_time"":false,""data"":""1686879792"",""field_type"":2,""created_at"":1686793392,""last_modified"":1686793392}",, +,,,,"{""last_modified"":1686793354,""created_at"":1686793354,""field_type"":1,""data"":""11""}",,, +,,,,"{""field_type"":1,""last_modified"":1686793356,""data"":""12"",""created_at"":1686793356}",,, +,,,,,,, diff --git a/frontend/rust-lib/flowy-test/tests/database/local_test/mod.rs b/frontend/rust-lib/flowy-test/tests/database/local_test/mod.rs new file mode 100644 index 0000000000..585722915d --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/database/local_test/mod.rs @@ -0,0 +1 @@ +mod test; diff --git a/frontend/rust-lib/flowy-test/tests/database/test.rs b/frontend/rust-lib/flowy-test/tests/database/local_test/test.rs similarity index 100% rename from frontend/rust-lib/flowy-test/tests/database/test.rs rename to frontend/rust-lib/flowy-test/tests/database/local_test/test.rs diff --git a/frontend/rust-lib/flowy-test/tests/database/mod.rs b/frontend/rust-lib/flowy-test/tests/database/mod.rs index 585722915d..cf818e3251 100644 --- a/frontend/rust-lib/flowy-test/tests/database/mod.rs +++ b/frontend/rust-lib/flowy-test/tests/database/mod.rs @@ -1 +1,4 @@ -mod test; +mod local_test; + +#[cfg(feature = "cloud_test")] +mod supabase_test; 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 new file mode 100644 index 0000000000..453d89b1f6 --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/database/supabase_test/helper.rs @@ -0,0 +1,112 @@ +use std::ops::Deref; + +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::{merge_updates_v1, JsonValue, Update}; + +use flowy_database2::entities::{DatabasePB, DatabaseViewIdPB, RepeatedDatabaseSnapshotPB}; +use flowy_database2::event_map::DatabaseEvent::*; +use flowy_folder2::entities::ViewPB; +use flowy_test::event_builder::EventBuilder; + +use crate::util::FlowySupabaseTest; + +pub struct FlowySupabaseDatabaseTest { + pub uuid: String, + inner: FlowySupabaseTest, +} + +impl FlowySupabaseDatabaseTest { + pub async fn new_with_user(uuid: String) -> Option { + let inner = FlowySupabaseTest::new()?; + inner.sign_up_with_uuid(&uuid).await; + Some(Self { uuid, inner }) + } + + pub async fn new_with_new_user() -> Option { + let inner = FlowySupabaseTest::new()?; + let uuid = uuid::Uuid::new_v4().to_string(); + let _ = inner.sign_up_with_uuid(&uuid).await; + Some(Self { uuid, inner }) + } + + pub async fn create_database(&self) -> (ViewPB, DatabasePB) { + let current_workspace = self.inner.get_current_workspace().await; + let view = self + .inner + .create_grid( + ¤t_workspace.workspace.id, + "my database".to_string(), + vec![], + ) + .await; + let database = self.inner.get_database(&view.id).await; + (view, database) + } + + pub async fn get_collab_json(&self, database_id: &str) -> JsonValue { + let database_editor = self + .database_manager + .get_database(database_id) + .await + .unwrap(); + // let address = Arc::into_raw(database_editor.clone()); + let database = database_editor.get_mutex_database().lock(); + database.get_mutex_collab().to_json_value() + } + + pub async fn get_database_snapshots(&self, view_id: &str) -> RepeatedDatabaseSnapshotPB { + EventBuilder::new(self.inner.deref().clone()) + .event(GetDatabaseSnapshots) + .payload(DatabaseViewIdPB { + value: view_id.to_string(), + }) + .async_send() + .await + .parse::() + } + + 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(); + + if remote_updates.is_empty() { + return vec![]; + } + + let updates = remote_updates + .iter() + .map(|update| update.as_ref()) + .collect::>(); + + merge_updates_v1(&updates).unwrap() + } +} + +pub fn assert_database_collab_content( + database_id: &str, + collab_update: &[u8], + expected: JsonValue, +) { + let collab = MutexCollab::new(CollabOrigin::Server, database_id, vec![]); + collab.lock().with_transact_mut(|txn| { + let update = Update::decode_v1(collab_update).unwrap(); + txn.apply_update(update); + }); + + let json = collab.to_json_value(); + assert_json_eq!(json, expected); +} + +impl Deref for FlowySupabaseDatabaseTest { + type Target = FlowySupabaseTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/frontend/rust-lib/flowy-test/tests/database/supabase_test/mod.rs b/frontend/rust-lib/flowy-test/tests/database/supabase_test/mod.rs new file mode 100644 index 0000000000..05fa1b00ed --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/database/supabase_test/mod.rs @@ -0,0 +1,2 @@ +mod helper; +mod test; diff --git a/frontend/rust-lib/flowy-test/tests/database/supabase_test/test.rs b/frontend/rust-lib/flowy-test/tests/database/supabase_test/test.rs new file mode 100644 index 0000000000..0330de4be7 --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/database/supabase_test/test.rs @@ -0,0 +1,105 @@ +use std::time::Duration; + +use flowy_database2::entities::{ + DatabaseSnapshotStatePB, DatabaseSyncStatePB, FieldChangesetPB, FieldType, +}; + +use crate::database::supabase_test::helper::{ + assert_database_collab_content, FlowySupabaseDatabaseTest, +}; +use crate::util::receive_with_timeout; + +#[tokio::test] +async fn cloud_test_supabase_initial_database_snapshot_test() { + if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { + let (view, database) = test.create_database().await; + let mut rx = test + .notification_sender + .subscribe::(&database.id); + + receive_with_timeout(&mut rx, Duration::from_secs(30)) + .await + .unwrap(); + + let expected = test.get_collab_json(&database.id).await; + let snapshots = test.get_database_snapshots(&view.id).await; + assert_eq!(snapshots.items.len(), 1); + assert_database_collab_content(&database.id, &snapshots.items[0].data, expected); + } +} + +#[tokio::test] +async fn cloud_test_supabase_edit_database_test() { + if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { + let (view, database) = test.create_database().await; + let existing_fields = test.get_all_database_fields(&view.id).await; + for field in existing_fields.items { + if !field.is_primary { + test.delete_field(&view.id, &field.id).await; + } + } + + let field = test.create_field(&view.id, FieldType::Checklist).await; + test + .update_field(FieldChangesetPB { + field_id: field.id.clone(), + view_id: view.id.clone(), + name: Some("hello world".to_string()), + ..Default::default() + }) + .await; + + // wait all updates are send to the remote + let mut rx = test + .notification_sender + .subscribe_with_condition::(&database.id, |pb| pb.is_finish); + receive_with_timeout(&mut rx, Duration::from_secs(30)) + .await + .unwrap(); + + assert_eq!(test.get_all_database_fields(&view.id).await.items.len(), 2); + let expected = test.get_collab_json(&database.id).await; + let update = test.get_collab_update(&database.id).await; + assert_database_collab_content(&database.id, &update, expected); + } +} + +// #[tokio::test] +// async fn cloud_test_supabase_login_sync_database_test() { +// if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { +// let uuid = test.uuid.clone(); +// let (view, database) = test.create_database().await; +// // wait all updates are send to the remote +// let mut rx = test +// .notification_sender +// .subscribe_with_condition::(&database.id, |pb| pb.is_finish); +// receive_with_timeout(&mut rx, Duration::from_secs(30)) +// .await +// .unwrap(); +// let expected = test.get_collab_json(&database.id).await; +// test.sign_out().await; +// // Drop the test will cause the test resources to be dropped, which will +// // delete the user data folder. +// drop(test); +// +// let new_test = FlowySupabaseDatabaseTest::new_with_user(uuid) +// .await +// .unwrap(); +// // let actual = new_test.get_collab_json(&database.id).await; +// // assert_json_eq!(actual, json!("")); +// +// new_test.open_database(&view.id).await; +// +// // wait all updates are synced from the remote +// let mut rx = new_test +// .notification_sender +// .subscribe_with_condition::(&database.id, |pb| pb.is_finish); +// receive_with_timeout(&mut rx, Duration::from_secs(30)) +// .await +// .unwrap(); +// +// // when the new sync is finished, the database should be the same as the old one +// let actual = new_test.get_collab_json(&database.id).await; +// assert_json_eq!(actual, expected); +// } +// } diff --git a/frontend/rust-lib/flowy-test/tests/document/block_test.rs b/frontend/rust-lib/flowy-test/tests/document/block_test.rs deleted file mode 100644 index dad47ee58f..0000000000 --- a/frontend/rust-lib/flowy-test/tests/document/block_test.rs +++ /dev/null @@ -1,28 +0,0 @@ -use flowy_test::document::text_block_event::TextBlockEventTest; -use flowy_test::document::utils::gen_text_block_data; - -#[tokio::test] -async fn insert_text_block_test() { - let test = TextBlockEventTest::new().await; - let text = "Hello World".to_string(); - let block_id = test.insert_index(text.clone(), 1, None).await; - let block = test.get(&block_id).await; - assert!(block.is_some()); - let block = block.unwrap(); - let data = gen_text_block_data(text); - assert_eq!(block.data, data); -} - -#[tokio::test] -async fn update_text_block_test() { - let test = TextBlockEventTest::new().await; - let insert_text = "Hello World".to_string(); - let block_id = test.insert_index(insert_text.clone(), 1, None).await; - let update_text = "Hello World 2".to_string(); - test.update(&block_id, update_text.clone()).await; - let block = test.get(&block_id).await; - assert!(block.is_some()); - let block = block.unwrap(); - let update_data = gen_text_block_data(update_text); - assert_eq!(block.data, update_data); -} diff --git a/frontend/rust-lib/flowy-test/tests/document/local_test/mod.rs b/frontend/rust-lib/flowy-test/tests/document/local_test/mod.rs new file mode 100644 index 0000000000..585722915d --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/document/local_test/mod.rs @@ -0,0 +1 @@ +mod test; diff --git a/frontend/rust-lib/flowy-test/tests/document/document_test.rs b/frontend/rust-lib/flowy-test/tests/document/local_test/test.rs similarity index 70% rename from frontend/rust-lib/flowy-test/tests/document/document_test.rs rename to frontend/rust-lib/flowy-test/tests/document/local_test/test.rs index 8bbf812c7e..232e28eb9a 100644 --- a/frontend/rust-lib/flowy-test/tests/document/document_test.rs +++ b/frontend/rust-lib/flowy-test/tests/document/local_test/test.rs @@ -60,3 +60,30 @@ async fn undo_redo_event_test() { let block_count_after_redo = test.open_document(doc_id.clone()).await.data.blocks.len(); assert_eq!(block_count_after_redo, block_count_after_insert); } + +#[tokio::test] +async fn insert_text_block_test() { + let test = DocumentEventTest::new().await; + let view = test.create_document().await; + let text = "Hello World"; + let block_id = test.insert_index(&view.id, text, 1, None).await; + let block = test.get_block(&view.id, &block_id).await; + assert!(block.is_some()); + let block = block.unwrap(); + let data = gen_text_block_data(text); + assert_eq!(block.data, data); +} + +#[tokio::test] +async fn update_text_block_test() { + let test = DocumentEventTest::new().await; + let view = test.create_document().await; + let block_id = test.insert_index(&view.id, "Hello World", 1, None).await; + let update_text = "Hello World 2"; + test.update(&view.id, &block_id, update_text).await; + let block = test.get_block(&view.id, &block_id).await; + assert!(block.is_some()); + let block = block.unwrap(); + let update_data = gen_text_block_data(update_text); + assert_eq!(block.data, update_data); +} diff --git a/frontend/rust-lib/flowy-test/tests/document/mod.rs b/frontend/rust-lib/flowy-test/tests/document/mod.rs index 9907638bcc..cf818e3251 100644 --- a/frontend/rust-lib/flowy-test/tests/document/mod.rs +++ b/frontend/rust-lib/flowy-test/tests/document/mod.rs @@ -1,2 +1,4 @@ -mod block_test; -mod document_test; +mod local_test; + +#[cfg(feature = "cloud_test")] +mod supabase_test; diff --git a/frontend/rust-lib/flowy-test/tests/document/supabase_test/helper.rs b/frontend/rust-lib/flowy-test/tests/document/supabase_test/helper.rs new file mode 100644 index 0000000000..fc00f2e89f --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/document/supabase_test/helper.rs @@ -0,0 +1,99 @@ +use crate::util::FlowySupabaseTest; + +use collab::core::collab::MutexCollab; +use collab::core::origin::CollabOrigin; +use collab::preclude::updates::decoder::Decode; +use collab::preclude::{merge_updates_v1, Update}; +use collab_document::blocks::DocumentData; +use collab_document::document::Document; +use flowy_document2::entities::{ + DocumentDataPB, OpenDocumentPayloadPB, RepeatedDocumentSnapshotPB, +}; +use flowy_document2::event_map::DocumentEvent::{GetDocumentData, GetDocumentSnapshots}; +use flowy_folder2::entities::ViewPB; +use flowy_test::event_builder::EventBuilder; +use std::ops::Deref; +use std::sync::Arc; + +pub struct FlowySupabaseDocumentTest { + inner: FlowySupabaseTest, +} + +impl FlowySupabaseDocumentTest { + pub async fn new() -> Option { + let inner = FlowySupabaseTest::new()?; + let uuid = uuid::Uuid::new_v4().to_string(); + let _ = inner.sign_up_with_uuid(&uuid).await; + Some(Self { inner }) + } + + pub async fn create_document(&self) -> ViewPB { + let current_workspace = self.inner.get_current_workspace().await; + self + .inner + .create_document(¤t_workspace.workspace.id, "my document", vec![]) + .await + } + + pub async fn get_document_snapshots(&self, view_id: &str) -> RepeatedDocumentSnapshotPB { + EventBuilder::new(self.inner.deref().clone()) + .event(GetDocumentSnapshots) + .payload(OpenDocumentPayloadPB { + document_id: view_id.to_string(), + }) + .async_send() + .await + .parse::() + } + + pub async fn get_document_data(&self, view_id: &str) -> DocumentData { + let pb = EventBuilder::new(self.inner.deref().clone()) + .event(GetDocumentData) + .payload(OpenDocumentPayloadPB { + document_id: view_id.to_string(), + }) + .async_send() + .await + .parse::(); + + DocumentData::from(pb) + } + + pub async fn get_collab_update(&self, document_id: &str) -> Vec { + let cloud_service = self.document_manager2.get_cloud_service().clone(); + let remote_updates = cloud_service + .get_document_updates(document_id) + .await + .unwrap(); + + if remote_updates.is_empty() { + return vec![]; + } + + let updates = remote_updates + .iter() + .map(|update| update.as_ref()) + .collect::>(); + + merge_updates_v1(&updates).unwrap() + } +} + +impl Deref for FlowySupabaseDocumentTest { + type Target = FlowySupabaseTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +pub fn assert_document_data_equal(collab_update: &[u8], doc_id: &str, expected: DocumentData) { + let collab = MutexCollab::new(CollabOrigin::Server, doc_id, vec![]); + collab.lock().with_transact_mut(|txn| { + let update = Update::decode_v1(collab_update).unwrap(); + txn.apply_update(update); + }); + let document = Document::open(Arc::new(collab)).unwrap(); + let actual = document.get_document_data().unwrap(); + assert_eq!(actual, expected); +} diff --git a/frontend/rust-lib/flowy-test/tests/document/supabase_test/mod.rs b/frontend/rust-lib/flowy-test/tests/document/supabase_test/mod.rs new file mode 100644 index 0000000000..05fa1b00ed --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/document/supabase_test/mod.rs @@ -0,0 +1,2 @@ +mod helper; +mod test; diff --git a/frontend/rust-lib/flowy-test/tests/document/supabase_test/test.rs b/frontend/rust-lib/flowy-test/tests/document/supabase_test/test.rs new file mode 100644 index 0000000000..6572e34119 --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/document/supabase_test/test.rs @@ -0,0 +1,85 @@ +use std::ops::Deref; +use std::time::Duration; + +use flowy_document2::entities::{DocumentSnapshotStatePB, DocumentSyncStatePB}; +use flowy_test::document::document_event::DocumentEventTest; + +use crate::document::supabase_test::helper::{ + assert_document_data_equal, FlowySupabaseDocumentTest, +}; +use crate::util::receive_with_timeout; + +#[tokio::test] +async fn cloud_test_supabase_initial_document_snapshot_test() { + if let Some(test) = FlowySupabaseDocumentTest::new().await { + let view = test.create_document().await; + + let mut rx = test + .notification_sender + .subscribe::(&view.id); + + receive_with_timeout(&mut rx, Duration::from_secs(30)) + .await + .unwrap(); + + let snapshots = test.get_document_snapshots(&view.id).await; + assert_eq!(snapshots.items.len(), 1); + + let document_data = test.get_document_data(&view.id).await; + assert_document_data_equal(&snapshots.items[0].data, &view.id, document_data); + } +} + +#[tokio::test] +async fn cloud_test_supabase_document_edit_sync_test() { + if let Some(test) = FlowySupabaseDocumentTest::new().await { + let view = test.create_document().await; + let document_id = view.id.clone(); + + let core = test.deref().deref().clone(); + let document_event = DocumentEventTest::new_with_core(core); + document_event + .insert_index(&document_id, "hello world", 0, None) + .await; + + // wait all update are send to the remote + let mut rx = test + .notification_sender + .subscribe_with_condition::(&document_id, |pb| pb.is_finish); + receive_with_timeout(&mut rx, Duration::from_secs(30)) + .await + .unwrap(); + + let document_data = test.get_document_data(&document_id).await; + let update = test.get_collab_update(&document_id).await; + assert_document_data_equal(&update, &document_id, document_data); + } +} + +#[tokio::test] +async fn cloud_test_supabase_document_edit_sync_test2() { + if let Some(test) = FlowySupabaseDocumentTest::new().await { + let view = test.create_document().await; + let document_id = view.id.clone(); + let core = test.deref().deref().clone(); + let document_event = DocumentEventTest::new_with_core(core); + + for i in 0..10 { + document_event + .insert_index(&document_id, "hello world", i, None) + .await; + } + + // wait all update are send to the remote + let mut rx = test + .notification_sender + .subscribe_with_condition::(&document_id, |pb| pb.is_finish); + receive_with_timeout(&mut rx, Duration::from_secs(30)) + .await + .unwrap(); + + let document_data = test.get_document_data(&document_id).await; + let update = test.get_collab_update(&document_id).await; + assert_document_data_equal(&update, &document_id, document_data); + } +} diff --git a/frontend/rust-lib/flowy-test/tests/folder/local_test/mod.rs b/frontend/rust-lib/flowy-test/tests/folder/local_test/mod.rs new file mode 100644 index 0000000000..585722915d --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/folder/local_test/mod.rs @@ -0,0 +1 @@ +mod test; diff --git a/frontend/rust-lib/flowy-test/tests/folder/test.rs b/frontend/rust-lib/flowy-test/tests/folder/local_test/test.rs similarity index 100% rename from frontend/rust-lib/flowy-test/tests/folder/test.rs rename to frontend/rust-lib/flowy-test/tests/folder/local_test/test.rs diff --git a/frontend/rust-lib/flowy-test/tests/folder/mod.rs b/frontend/rust-lib/flowy-test/tests/folder/mod.rs index 585722915d..cf818e3251 100644 --- a/frontend/rust-lib/flowy-test/tests/folder/mod.rs +++ b/frontend/rust-lib/flowy-test/tests/folder/mod.rs @@ -1 +1,4 @@ -mod test; +mod local_test; + +#[cfg(feature = "cloud_test")] +mod supabase_test; 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 new file mode 100644 index 0000000000..7542609992 --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/folder/supabase_test/helper.rs @@ -0,0 +1,85 @@ +use std::ops::Deref; + +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::{merge_updates_v1, JsonValue, Update}; + +use flowy_folder2::entities::{FolderSnapshotPB, RepeatedFolderSnapshotPB, WorkspaceIdPB}; +use flowy_folder2::event_map::FolderEvent::GetFolderSnapshots; +use flowy_test::event_builder::EventBuilder; + +use crate::util::FlowySupabaseTest; + +pub struct FlowySupabaseFolderTest { + inner: FlowySupabaseTest, +} + +impl FlowySupabaseFolderTest { + pub async fn new() -> Option { + let inner = FlowySupabaseTest::new()?; + let uuid = uuid::Uuid::new_v4().to_string(); + let _ = inner.sign_up_with_uuid(&uuid).await; + Some(Self { inner }) + } + + pub async fn get_collab_json(&self) -> JsonValue { + let folder = self.folder_manager.get_mutex_folder().lock(); + folder.as_ref().unwrap().to_json_value() + } + + pub async fn get_folder_snapshots(&self, workspace_id: &str) -> Vec { + EventBuilder::new(self.inner.deref().clone()) + .event(GetFolderSnapshots) + .payload(WorkspaceIdPB { + value: Some(workspace_id.to_string()), + }) + .async_send() + .await + .parse::() + .items + } + + 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) + .await + .unwrap(); + + if remote_updates.is_empty() { + return vec![]; + } + + let updates = remote_updates + .iter() + .map(|update| update.as_ref()) + .collect::>(); + + merge_updates_v1(&updates).unwrap() + } +} + +pub fn assert_folder_collab_content(workspace_id: &str, collab_update: &[u8], expected: JsonValue) { + if collab_update.is_empty() { + panic!("collab update is empty"); + } + + let collab = MutexCollab::new(CollabOrigin::Server, workspace_id, vec![]); + collab.lock().with_transact_mut(|txn| { + let update = Update::decode_v1(collab_update).unwrap(); + txn.apply_update(update); + }); + + let json = collab.to_json_value(); + assert_json_eq!(json["folder"], expected); +} + +impl Deref for FlowySupabaseFolderTest { + type Target = FlowySupabaseTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/frontend/rust-lib/flowy-test/tests/folder/supabase_test/mod.rs b/frontend/rust-lib/flowy-test/tests/folder/supabase_test/mod.rs new file mode 100644 index 0000000000..05fa1b00ed --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/folder/supabase_test/mod.rs @@ -0,0 +1,2 @@ +mod helper; +mod test; diff --git a/frontend/rust-lib/flowy-test/tests/folder/supabase_test/test.rs b/frontend/rust-lib/flowy-test/tests/folder/supabase_test/test.rs new file mode 100644 index 0000000000..54ac8ca34e --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/folder/supabase_test/test.rs @@ -0,0 +1,54 @@ +use std::time::Duration; + +use flowy_folder2::entities::{FolderSnapshotStatePB, FolderSyncStatePB}; + +use crate::folder::supabase_test::helper::{assert_folder_collab_content, FlowySupabaseFolderTest}; +use crate::util::receive_with_timeout; + +#[tokio::test] +async fn cloud_test_supabase_initial_folder_snapshot_test() { + if let Some(test) = FlowySupabaseFolderTest::new().await { + let workspace_id = test.get_current_workspace().await.workspace.id; + let mut rx = test + .notification_sender + .subscribe::(&workspace_id); + + receive_with_timeout(&mut rx, Duration::from_secs(30)) + .await + .unwrap(); + + let expected = test.get_collab_json().await; + let snapshots = test.get_folder_snapshots(&workspace_id).await; + assert_eq!(snapshots.len(), 1); + assert_folder_collab_content(&workspace_id, &snapshots[0].data, expected); + } +} + +#[tokio::test] +async fn cloud_test_supabase_initial_folder_snapshot_test2() { + if let Some(test) = FlowySupabaseFolderTest::new().await { + let workspace_id = test.get_current_workspace().await.workspace.id; + + test + .create_view(&workspace_id, "supabase test view1".to_string()) + .await; + test + .create_view(&workspace_id, "supabase test view2".to_string()) + .await; + test + .create_view(&workspace_id, "supabase test view3".to_string()) + .await; + + let mut rx = test + .notification_sender + .subscribe_with_condition::(&workspace_id, |pb| pb.is_finish); + + receive_with_timeout(&mut rx, Duration::from_secs(30)) + .await + .unwrap(); + + let expected = test.get_collab_json().await; + let update = test.get_collab_update(&workspace_id).await; + assert_folder_collab_content(&workspace_id, &update, expected); + } +} diff --git a/frontend/rust-lib/flowy-test/tests/main.rs b/frontend/rust-lib/flowy-test/tests/main.rs index ae80f04a9e..91d1d2a44f 100644 --- a/frontend/rust-lib/flowy-test/tests/main.rs +++ b/frontend/rust-lib/flowy-test/tests/main.rs @@ -2,3 +2,4 @@ mod database; mod document; mod folder; mod user; +pub mod util; diff --git a/frontend/rust-lib/flowy-test/tests/user/mod.rs b/frontend/rust-lib/flowy-test/tests/user/mod.rs index 0d7ec93eed..cf818e3251 100644 --- a/frontend/rust-lib/flowy-test/tests/user/mod.rs +++ b/frontend/rust-lib/flowy-test/tests/user/mod.rs @@ -1,2 +1,4 @@ mod local_test; + +#[cfg(feature = "cloud_test")] mod supabase_test; 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 2ee5182a52..fdb35e6372 100644 --- a/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs +++ b/frontend/rust-lib/flowy-test/tests/user/supabase_test/auth_test.rs @@ -1,11 +1,15 @@ -use crate::user::supabase_test::helper::get_supabase_config; - -use flowy_test::{event_builder::EventBuilder, FlowyCoreTest}; -use flowy_user::entities::{AuthTypePB, ThirdPartyAuthPB, UserProfilePB}; - -use flowy_user::event_map::UserEvent::*; use std::collections::HashMap; +use flowy_test::event_builder::EventBuilder; +use flowy_test::FlowyCoreTest; +use flowy_user::entities::{ + AuthTypePB, ThirdPartyAuthPB, UpdateUserProfilePayloadPB, UserProfilePB, +}; +use flowy_user::errors::ErrorCode; +use flowy_user::event_map::UserEvent::*; + +use crate::util::*; + #[tokio::test] async fn sign_up_test() { if get_supabase_config().is_some() { @@ -26,3 +30,39 @@ async fn sign_up_test() { dbg!(&response); } } + +#[tokio::test] +async fn check_not_exist_user_test() { + if let Some(test) = FlowySupabaseTest::new() { + let err = test + .check_user_with_uuid(&uuid::Uuid::new_v4().to_string()) + .await + .unwrap_err(); + assert_eq!(err.code, ErrorCode::UserNotExist.value()); + } +} + +#[tokio::test] +async fn get_user_profile_test() { + if let Some(test) = FlowySupabaseTest::new() { + let uuid = uuid::Uuid::new_v4().to_string(); + test.sign_up_with_uuid(&uuid).await; + + let result = test.get_user_profile().await; + assert!(result.is_ok()); + } +} + +#[tokio::test] +async fn update_user_profile_test() { + if let Some(test) = FlowySupabaseTest::new() { + let uuid = uuid::Uuid::new_v4().to_string(); + let profile = test.sign_up_with_uuid(&uuid).await; + test + .update_user_profile(UpdateUserProfilePayloadPB::new(profile.id).name("lucas")) + .await; + + let new_profile = test.get_user_profile().await.unwrap(); + assert_eq!(new_profile.name, "lucas") + } +} diff --git a/frontend/rust-lib/flowy-test/tests/user/supabase_test/helper.rs b/frontend/rust-lib/flowy-test/tests/user/supabase_test/helper.rs deleted file mode 100644 index fc78aafa90..0000000000 --- a/frontend/rust-lib/flowy-test/tests/user/supabase_test/helper.rs +++ /dev/null @@ -1,20 +0,0 @@ -use dotenv::dotenv; -use flowy_server::supabase::SupabaseConfiguration; - -/// In order to run this test, you need to create a .env file in the root directory of this project -/// and add the following environment variables: -/// - SUPABASE_URL -/// - SUPABASE_ANON_KEY -/// - SUPABASE_KEY -/// - SUPABASE_JWT_SECRET -/// -/// the .env file should look like this: -/// SUPABASE_URL=https://.supabase.co -/// SUPABASE_ANON_KEY= -/// SUPABASE_KEY= -/// SUPABASE_JWT_SECRET= -/// -pub fn get_supabase_config() -> Option { - dotenv().ok()?; - SupabaseConfiguration::from_env().ok() -} diff --git a/frontend/rust-lib/flowy-test/tests/user/supabase_test/mod.rs b/frontend/rust-lib/flowy-test/tests/user/supabase_test/mod.rs index 35aaffc467..b31fdaa002 100644 --- a/frontend/rust-lib/flowy-test/tests/user/supabase_test/mod.rs +++ b/frontend/rust-lib/flowy-test/tests/user/supabase_test/mod.rs @@ -1,3 +1,2 @@ mod auth_test; -mod helper; mod workspace_test; diff --git a/frontend/rust-lib/flowy-test/tests/user/supabase_test/workspace_test.rs b/frontend/rust-lib/flowy-test/tests/user/supabase_test/workspace_test.rs index 5117a6923b..17b4663e9b 100644 --- a/frontend/rust-lib/flowy-test/tests/user/supabase_test/workspace_test.rs +++ b/frontend/rust-lib/flowy-test/tests/user/supabase_test/workspace_test.rs @@ -1,12 +1,12 @@ -use crate::user::supabase_test::helper::get_supabase_config; +use std::collections::HashMap; + use flowy_folder2::entities::WorkspaceSettingPB; use flowy_folder2::event_map::FolderEvent::GetCurrentWorkspace; - use flowy_test::{event_builder::EventBuilder, FlowyCoreTest}; use flowy_user::entities::{AuthTypePB, ThirdPartyAuthPB, UserProfilePB}; - use flowy_user::event_map::UserEvent::*; -use std::collections::HashMap; + +use crate::util::*; #[tokio::test] async fn initial_workspace_test() { diff --git a/frontend/rust-lib/flowy-test/tests/util.rs b/frontend/rust-lib/flowy-test/tests/util.rs new file mode 100644 index 0000000000..8431edd35d --- /dev/null +++ b/frontend/rust-lib/flowy-test/tests/util.rs @@ -0,0 +1,104 @@ +use std::ops::Deref; +use std::time::Duration; + +use tokio::sync::mpsc::Receiver; +use tokio::time::timeout; + +use flowy_server::supabase::SupabaseConfiguration; +use flowy_test::event_builder::EventBuilder; +use flowy_test::FlowyCoreTest; +use flowy_user::entities::{ + AuthTypePB, UpdateUserProfilePayloadPB, UserCredentialsPB, UserProfilePB, +}; +use flowy_user::errors::FlowyError; +use flowy_user::event_map::UserCloudServiceProvider; +use flowy_user::event_map::UserEvent::*; +use flowy_user::services::AuthType; + +/// In order to run this test, you need to create a .env.test file in the root directory of this project +/// and add the following environment variables: +/// - SUPABASE_URL +/// - SUPABASE_ANON_KEY +/// - SUPABASE_KEY +/// - SUPABASE_JWT_SECRET +/// - SUPABASE_DB +/// - SUPABASE_DB_USER +/// - SUPABASE_DB_PORT +/// - SUPABASE_DB_PASSWORD +/// +/// the .env.test file should look like this: +/// SUPABASE_URL=https://.supabase.co +/// SUPABASE_ANON_KEY= +/// SUPABASE_KEY= +/// SUPABASE_JWT_SECRET= +/// SUPABASE_DB=db.xxx.supabase.co +/// SUPABASE_DB_USER= +/// SUPABASE_DB_PORT= +/// SUPABASE_DB_PASSWORD= +/// +pub fn get_supabase_config() -> Option { + dotenv::from_path(".env.test").ok()?; + SupabaseConfiguration::from_env().ok() +} + +pub struct FlowySupabaseTest { + inner: FlowyCoreTest, +} + +impl FlowySupabaseTest { + pub fn new() -> Option { + let _ = get_supabase_config()?; + let test = FlowyCoreTest::new(); + test.set_auth_type(AuthTypePB::Supabase); + test.server_provider.set_auth_type(AuthType::Supabase); + + Some(Self { inner: test }) + } + + pub async fn check_user_with_uuid(&self, uuid: &str) -> Result<(), FlowyError> { + match EventBuilder::new(self.inner.clone()) + .event(CheckUser) + .payload(UserCredentialsPB::from_uuid(uuid)) + .async_send() + .await + .error() + { + None => Ok(()), + Some(error) => Err(error), + } + } + + pub async fn get_user_profile(&self) -> Result { + EventBuilder::new(self.inner.clone()) + .event(GetUserProfile) + .async_send() + .await + .try_parse::() + } + + pub async fn update_user_profile(&self, payload: UpdateUserProfilePayloadPB) { + EventBuilder::new(self.inner.clone()) + .event(UpdateUserProfile) + .payload(payload) + .async_send() + .await; + } +} + +impl Deref for FlowySupabaseTest { + type Target = FlowyCoreTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +pub async fn receive_with_timeout( + receiver: &mut Receiver, + duration: Duration, +) -> Result> { + let res = timeout(duration, receiver.recv()) + .await? + .ok_or(anyhow::anyhow!("recv timeout"))?; + Ok(res) +} diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index d663d10dcc..ec1b96598f 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -7,6 +7,7 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use crate::entities::parser::*; use crate::errors::ErrorCode; +use crate::event_map::UserCredentials; use crate::services::AuthType; #[derive(ProtoBuf, Default)] @@ -101,6 +102,7 @@ pub struct SignUpResponse { pub user_id: i64, pub name: String, pub workspace_id: String, + pub is_new: bool, pub email: Option, pub token: Option, } @@ -194,3 +196,47 @@ pub struct SignOutPB { #[pb(index = 1)] pub auth_type: AuthTypePB, } + +#[derive(Debug, ProtoBuf, Default)] +pub struct UserCredentialsPB { + #[pb(index = 1, one_of)] + pub uid: Option, + + #[pb(index = 2, one_of)] + pub uuid: Option, + + #[pb(index = 3, one_of)] + pub token: Option, +} + +impl UserCredentialsPB { + pub fn from_uid(uid: i64) -> Self { + Self { + uid: Some(uid), + uuid: None, + token: None, + } + } + + pub fn from_token(token: &str) -> Self { + Self { + uid: None, + uuid: None, + token: Some(token.to_owned()), + } + } + + pub fn from_uuid(uuid: &str) -> Self { + Self { + uid: None, + uuid: Some(uuid.to_owned()), + token: None, + } + } +} + +impl From for UserCredentials { + fn from(value: UserCredentialsPB) -> Self { + Self::new(value.token, value.uid, value.uuid) + } +} diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 2e0a6ef193..77f5cc9c21 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -7,6 +7,7 @@ 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}; #[tracing::instrument(level = "debug", name = "sign_in", skip(data, session), fields(email = %data.email), err)] @@ -55,10 +56,12 @@ 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>, -) -> DataResult { - let user_profile: UserProfilePB = session.check_user().await?.into(); - data_result_ok(user_profile) +) -> Result<(), FlowyError> { + let credential = UserCredentials::from(data.into_inner()); + session.check_user(credential).await?; + Ok(()) } #[tracing::instrument(level = "debug", skip(session))] diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index b12e20fa68..64dee7f5e0 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -4,7 +4,6 @@ use strum_macros::Display; use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use flowy_error::FlowyResult; - use lib_dispatch::prelude::*; use lib_infra::box_any::BoxAny; use lib_infra::future::{to_fut, Fut, FutureResult}; @@ -39,7 +38,7 @@ impl UserStatusCallback for DefaultUserStatusCallback { to_fut(async { Ok(()) }) } - fn did_sign_up(&self, _user_profile: &UserProfile) -> Fut> { + fn did_sign_up(&self, _is_new: bool, _user_profile: &UserProfile) -> Fut> { to_fut(async { Ok(()) }) } @@ -51,7 +50,7 @@ impl UserStatusCallback for DefaultUserStatusCallback { pub trait UserStatusCallback: Send + Sync + 'static { fn auth_type_did_changed(&self, auth_type: AuthType); fn did_sign_in(&self, user_id: i64, workspace_id: &str) -> Fut>; - fn did_sign_up(&self, user_profile: &UserProfile) -> Fut>; + fn did_sign_up(&self, is_new: bool, user_profile: &UserProfile) -> Fut>; fn did_expired(&self, token: &str, user_id: i64) -> Fut>; } @@ -75,6 +74,40 @@ where } } +#[derive(Clone, Debug)] +pub struct UserCredentials { + /// Currently, the token is only used when the [AuthType] is SelfHosted + pub token: Option, + + /// The user id + pub uid: Option, + + /// The user id + pub uuid: Option, +} + +impl UserCredentials { + pub fn from_uid(uid: i64) -> Self { + Self { + token: None, + uid: Some(uid), + uuid: None, + } + } + + pub fn from_uuid(uuid: String) -> Self { + Self { + token: None, + uid: None, + uuid: Some(uuid), + } + } + + pub fn new(token: Option, uid: Option, uuid: Option) -> Self { + Self { token, uid, uuid } + } +} + /// Provide the generic interface for the user cloud service /// The user cloud service is responsible for the user authentication and user profile management pub trait UserAuthService: Send + Sync { @@ -93,17 +126,18 @@ pub trait UserAuthService: Send + Sync { /// Using the user's token to update the user information fn update_user( &self, - uid: i64, - token: &Option, + credential: UserCredentials, params: UpdateUserProfileParams, ) -> FutureResult<(), FlowyError>; - /// Get the user information using the user's token + /// Get the user information using the user's token or uid + /// return None if the user is not found fn get_user_profile( &self, - token: Option, - uid: i64, + credential: UserCredentials, ) -> FutureResult, FlowyError>; + + fn check_user(&self, credential: UserCredentials) -> FutureResult<(), FlowyError>; } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] diff --git a/frontend/rust-lib/flowy-user/src/notification.rs b/frontend/rust-lib/flowy-user/src/notification.rs index 425453805b..f8cc06f3d0 100644 --- a/frontend/rust-lib/flowy-user/src/notification.rs +++ b/frontend/rust-lib/flowy-user/src/notification.rs @@ -1,6 +1,6 @@ use flowy_derive::ProtoBuf_Enum; use flowy_notification::NotificationBuilder; -const OBSERVABLE_CATEGORY: &str = "User"; +const USER_OBSERVABLE_SOURCE: &str = "User"; #[derive(ProtoBuf_Enum, Debug, Default)] pub(crate) enum UserNotification { @@ -17,9 +17,9 @@ impl std::convert::From for i32 { } pub(crate) fn send_notification(id: &str, ty: UserNotification) -> NotificationBuilder { - NotificationBuilder::new(id, ty, OBSERVABLE_CATEGORY) + NotificationBuilder::new(id, ty, USER_OBSERVABLE_SOURCE) } pub(crate) fn send_sign_in_notification() -> NotificationBuilder { - NotificationBuilder::new("", UserNotification::DidUserSignIn, OBSERVABLE_CATEGORY) + NotificationBuilder::new("", UserNotification::DidUserSignIn, USER_OBSERVABLE_SOURCE) } diff --git a/frontend/rust-lib/flowy-user/src/services/database.rs b/frontend/rust-lib/flowy-user/src/services/database.rs index fc4dcbcb08..d65816c304 100644 --- a/frontend/rust-lib/flowy-user/src/services/database.rs +++ b/frontend/rust-lib/flowy-user/src/services/database.rs @@ -39,7 +39,7 @@ impl UserDB { dir.push(user_id.to_string()); let dir = dir.to_str().unwrap().to_owned(); - tracing::trace!("open user db {} at path: {}", user_id, dir); + 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) @@ -83,13 +83,16 @@ impl UserDB { } pub(crate) fn close_user_db(&self, user_id: i64) -> Result<(), FlowyError> { - match DB_MAP.try_write_for(Duration::from_millis(300)) { - None => Err(FlowyError::internal().context("Acquire write lock to close user db failed")), - Some(mut write_guard) => { - write_guard.remove(&user_id); - Ok(()) - }, + if let Some(mut sqlite_dbs) = DB_MAP.try_write_for(Duration::from_millis(300)) { + sqlite_dbs.remove(&user_id); } + + if let Some(mut collab_dbs) = COLLAB_DB_MAP.try_write_for(Duration::from_millis(300)) { + if let Some(db) = collab_dbs.remove(&user_id) { + drop(db); + } + } + Ok(()) } pub(crate) fn get_connection(&self, user_id: i64) -> Result { 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 920c84fa11..2978db0879 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_session.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_session.rs @@ -19,7 +19,9 @@ use crate::entities::{ AuthTypePB, SignInResponse, SignUpResponse, UpdateUserProfileParams, UserProfile, }; use crate::entities::{UserProfilePB, UserSettingPB}; -use crate::event_map::{DefaultUserStatusCallback, UserCloudServiceProvider, UserStatusCallback}; +use crate::event_map::{ + DefaultUserStatusCallback, UserCloudServiceProvider, UserCredentials, UserStatusCallback, +}; use crate::{ errors::FlowyError, event_map::UserAuthService, @@ -147,12 +149,10 @@ impl UserSession { .auth_type_did_changed(auth_type.clone()); self.cloud_services.set_auth_type(auth_type.clone()); - let resp = self - .cloud_services - .get_auth_service()? - .sign_up(params) - .await?; + 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(); self.set_session(Some(session))?; let user_table = self.save_user(resp.into()).await?; @@ -161,7 +161,7 @@ impl UserSession { .user_status_callback .read() .await - .did_sign_up(&user_profile) + .did_sign_up(is_new, &user_profile) .await; Ok(user_profile) } @@ -172,6 +172,7 @@ impl UserSession { 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.set_session(None)?; @@ -215,13 +216,9 @@ impl UserSession { Ok(()) } - pub async fn check_user(&self) -> Result { - let (user_id, _token) = self.get_session()?.into_part(); - let user_id = user_id.to_string(); - let user = dsl::user_table - .filter(user_table::id.eq(&user_id)) - .first::(&*(self.db_connection()?))?; - Ok(user.into()) + pub async fn check_user(&self, credential: UserCredentials) -> Result<(), FlowyError> { + let auth_service = self.cloud_services.get_auth_service()?; + auth_service.check_user(credential).await } pub async fn get_user_profile(&self) -> Result { @@ -273,10 +270,10 @@ impl UserSession { let server = self.cloud_services.get_auth_service()?; let token = token.to_owned(); let _ = tokio::spawn(async move { - match server.update_user(uid, &token, params).await { + let credentials = UserCredentials::new(token, Some(uid), None); + match server.update_user(credentials, params).await { Ok(_) => {}, Err(e) => { - // TODO: retry? tracing::error!("update user profile failed: {:?}", e); }, } @@ -287,9 +284,16 @@ impl UserSession { async fn save_user(&self, user: UserTable) -> Result { let conn = self.db_connection()?; - let _ = diesel::insert_into(user_table::table) - .values(user.clone()) - .execute(&*conn)?; + conn.immediate_transaction(|| { + // delete old user if exists + diesel::delete(dsl::user_table.filter(dsl::id.eq(&user.id))).execute(&*conn)?; + + let _ = diesel::insert_into(user_table::table) + .values(user.clone()) + .execute(&*conn)?; + Ok::<(), FlowyError>(()) + })?; + Ok(user) } diff --git a/frontend/rust-lib/lib-dispatch/src/data.rs b/frontend/rust-lib/lib-dispatch/src/data.rs index c6381aa7fb..17c50da12a 100644 --- a/frontend/rust-lib/lib-dispatch/src/data.rs +++ b/frontend/rust-lib/lib-dispatch/src/data.rs @@ -1,3 +1,8 @@ +use std::fmt::{Debug, Formatter}; +use std::ops; + +use bytes::Bytes; + use crate::{ byte_trait::*, errors::{DispatchError, InternalError}, @@ -5,8 +10,6 @@ use crate::{ response::{AFPluginEventResponse, AFPluginResponder, ResponseBuilder}, util::ready::{ready, Ready}, }; -use bytes::Bytes; -use std::ops; pub struct AFPluginData(pub T); @@ -126,3 +129,12 @@ impl ToBytes for AFPluginData { Ok(Bytes::from(self.0)) } } + +impl Debug for AFPluginData +where + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} diff --git a/frontend/rust-lib/lib-log/src/layer.rs b/frontend/rust-lib/lib-log/src/layer.rs index 9a0af79f9e..870223c0cf 100644 --- a/frontend/rust-lib/lib-log/src/layer.rs +++ b/frontend/rust-lib/lib-log/src/layer.rs @@ -1,11 +1,12 @@ +use std::{fmt, io::Write}; + use serde::ser::{SerializeMap, Serializer}; use serde_json::Value; -use std::{fmt, io::Write}; use tracing::{Event, Id, Subscriber}; use tracing_bunyan_formatter::JsonStorage; use tracing_core::{metadata::Level, span::Attributes}; - use tracing_subscriber::{fmt::MakeWriter, layer::Context, registry::SpanRef, Layer}; + const LEVEL: &str = "level"; const TIME: &str = "time"; const MESSAGE: &str = "msg"; @@ -22,6 +23,7 @@ pub struct FlowyFormattingLayer { } impl FlowyFormattingLayer { + #[allow(dead_code)] pub fn new(make_writer: W) -> Self { Self { make_writer, diff --git a/frontend/rust-lib/lib-log/src/lib.rs b/frontend/rust-lib/lib-log/src/lib.rs index 39d28fd8af..a69f552d93 100644 --- a/frontend/rust-lib/lib-log/src/lib.rs +++ b/frontend/rust-lib/lib-log/src/lib.rs @@ -1,14 +1,13 @@ -mod layer; -use crate::layer::*; +use std::sync::RwLock; + use lazy_static::lazy_static; use log::LevelFilter; -use std::sync::RwLock; 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}; +mod layer; lazy_static! { static ref LOG_GUARD: RwLock> = RwLock::new(None); } @@ -40,22 +39,22 @@ 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(false) + .with_target(true) .with_max_level(tracing::Level::TRACE) .with_writer(std::io::stderr) .with_thread_ids(true) .json() - // .with_current_span(true) - // .with_span_list(true) + .with_current_span(true) + .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() @@ -71,6 +70,7 @@ impl Builder { #[cfg(test)] mod tests { use super::*; + // run cargo test --features="use_bunyan" or cargo test #[test] fn test_log() { diff --git a/frontend/scripts/makefile/tests.toml b/frontend/scripts/makefile/tests.toml index 3d150ea120..15d419cd62 100644 --- a/frontend/scripts/makefile/tests.toml +++ b/frontend/scripts/makefile/tests.toml @@ -39,12 +39,20 @@ flutter test --dart-define=RUST_LOG=${RUST_LOG} -j, --concurrency=1 --coverage [tasks.rust_unit_test] run_task = { name = ["rust_lib_unit_test", "shared_lib_unit_test"] } +[tasks.rust_cloud_unit_test] +env = { RUST_LOG = "debug" } +description = "Run cloud unit tests" +script = ''' +cd rust-lib/flowy-test +RUST_BACKTRACE=1 cargo test cloud_test_ --features "cloud_test" -- --test-threads=1 +''' + [tasks.rust_lib_unit_test] env = { RUST_LOG = "info" } description = "Run rust-lib unit tests" script = ''' cd rust-lib -RUST_LOG=info RUST_BACKTRACE=1 cargo test --no-default-features --features="rev-sqlite" +RUST_LOG=info RUST_BACKTRACE=1 cargo test --no-default-features --features "rev-sqlite" ''' [tasks.shared_lib_unit_test]