feat: integrate postgres storage (#2604)

* chore: env config

* chore: get user workspace

* feat: enable postgres storage

* chore: add new env

* chore: add set env ffi

* chore: pass env before backend init

* chore: update

* fix: ci tests

* chore: commit the generate env file

* chore: remove unused import
This commit is contained in:
Nathan.fooo 2023-05-23 23:55:21 +08:00 committed by GitHub
parent 51a7954af7
commit 056e2d49d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 1421 additions and 1131 deletions

View File

@ -16,4 +16,23 @@ class Config {
..jwtSecret = secret,
).send();
}
static Future<void> 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();
}
}

View File

@ -29,6 +29,13 @@ abstract class Env {
defaultValue: '',
)
static final supabaseJwtSecret = _Env.supabaseJwtSecret;
@EnviedField(
obfuscate: true,
varName: 'SUPABASE_COLLAB_TABLE',
defaultValue: '',
)
static final supabaseCollabTable = _Env.supabaseCollabTable;
}
bool get isSupabaseEnable =>

View File

@ -6,7 +6,6 @@ import 'package:appflowy/util/json_print.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy/workspace/application/doc/doc_listener.dart';
import 'package:appflowy/plugins/document/application/doc_service.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart';
import 'package:appflowy_editor/appflowy_editor.dart'
@ -153,7 +152,7 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
editorState.logConfiguration
..level = LogLevel.all
..handler = (log) {
Log.debug(log);
// Log.debug(log);
};
}
}

View File

@ -62,6 +62,7 @@ class FlowyRunner {
anonKey: Env.supabaseAnonKey,
key: Env.supabaseKey,
jwtSecret: Env.supabaseJwtSecret,
collabTable: Env.supabaseCollabTable,
),
const InitAppWidgetTask(),
const InitPlatformServiceTask()

View File

@ -1,6 +1,8 @@
import 'dart:io';
import 'package:appflowy/env/env.dart';
import 'package:appflowy_backend/appflowy_backend.dart';
import 'package:appflowy_backend/env_serde.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
@ -20,10 +22,35 @@ class InitRustSDKTask extends LaunchTask {
@override
Future<void> initialize(LaunchContext context) async {
final dir = directory ?? await appFlowyDocumentDirectory();
context.getIt<FlowySDK>().setEnv(getAppFlowyEnv());
await context.getIt<FlowySDK>().init(dir);
}
}
AppFlowyEnv getAppFlowyEnv() {
final supabaseConfig = SupabaseConfiguration(
url: Env.supabaseUrl,
key: Env.supabaseKey,
jwt_secret: Env.supabaseJwtSecret,
);
final collabTableConfig =
CollabTableConfig(enable: true, table_name: Env.supabaseCollabTable);
final supbaseDBConfig = SupabaseDBConfig(
url: Env.supabaseUrl,
key: Env.supabaseKey,
jwt_secret: Env.supabaseJwtSecret,
collab_table_config: collabTableConfig,
);
return AppFlowyEnv(
supabase_config: supabaseConfig,
supabase_db_config: supbaseDBConfig,
);
}
Future<Directory> appFlowyDocumentDirectory() async {
switch (integrationEnv()) {
case IntegrationMode.develop:

View File

@ -13,12 +13,14 @@ class InitSupabaseTask extends LaunchTask {
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<void> initialize(LaunchContext context) async {
@ -33,6 +35,7 @@ class InitSupabaseTask extends LaunchTask {
await Supabase.initialize(
url: url,
anonKey: anonKey,
debug: false,
);
await Config.setSupabaseConfig(
url: url,

View File

@ -14,3 +14,5 @@ int32_t set_stream_port(int64_t port);
void link_me_please(void);
void backend_log(int64_t level, const char *data);
void set_env(const char *data);

View File

@ -1,9 +1,11 @@
export 'package:async/async.dart';
import 'dart:convert';
import 'dart:io';
import 'dart:async';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:flutter/services.dart';
import 'dart:ffi';
import 'env_serde.dart';
import 'ffi.dart' as ffi;
import 'package:ffi/ffi.dart';
@ -34,4 +36,9 @@ class FlowySDK {
ffi.store_dart_post_cobject(NativeApi.postCObject);
ffi.init_sdk(sdkDir.path.toNativeUtf8());
}
void setEnv(AppFlowyEnv env) {
final jsonStr = jsonEncode(env.toJson());
ffi.set_env(jsonStr.toNativeUtf8());
}
}

View File

@ -0,0 +1,64 @@
import 'package:json_annotation/json_annotation.dart';
part 'env_serde.l.dart';
@JsonSerializable()
class AppFlowyEnv {
final SupabaseConfiguration supabase_config;
final SupabaseDBConfig supabase_db_config;
AppFlowyEnv(
{required this.supabase_config, required this.supabase_db_config});
factory AppFlowyEnv.fromJson(Map<String, dynamic> json) =>
_$AppFlowyEnvFromJson(json);
Map<String, dynamic> toJson() => _$AppFlowyEnvToJson(this);
}
@JsonSerializable()
class SupabaseConfiguration {
final String url;
final String key;
final String jwt_secret;
SupabaseConfiguration(
{required this.url, required this.key, required this.jwt_secret});
factory SupabaseConfiguration.fromJson(Map<String, dynamic> json) =>
_$SupabaseConfigurationFromJson(json);
Map<String, dynamic> toJson() => _$SupabaseConfigurationToJson(this);
}
@JsonSerializable()
class SupabaseDBConfig {
final String url;
final String key;
final String jwt_secret;
final CollabTableConfig collab_table_config;
SupabaseDBConfig(
{required this.url,
required this.key,
required this.jwt_secret,
required this.collab_table_config});
factory SupabaseDBConfig.fromJson(Map<String, dynamic> json) =>
_$SupabaseDBConfigFromJson(json);
Map<String, dynamic> 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<String, dynamic> json) =>
_$CollabTableConfigFromJson(json);
Map<String, dynamic> toJson() => _$CollabTableConfigToJson(this);
}

View File

@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'env_serde.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AppFlowyEnv _$AppFlowyEnvFromJson(Map<String, dynamic> json) => AppFlowyEnv(
supabase_config: SupabaseConfiguration.fromJson(
json['supabase_config'] as Map<String, dynamic>),
supabase_db_config: SupabaseDBConfig.fromJson(
json['supabase_db_config'] as Map<String, dynamic>),
);
Map<String, dynamic> _$AppFlowyEnvToJson(AppFlowyEnv instance) =>
<String, dynamic>{
'supabase_config': instance.supabase_config,
'supabase_db_config': instance.supabase_db_config,
};
SupabaseConfiguration _$SupabaseConfigurationFromJson(
Map<String, dynamic> json) =>
SupabaseConfiguration(
url: json['url'] as String,
key: json['key'] as String,
jwt_secret: json['jwt_secret'] as String,
);
Map<String, dynamic> _$SupabaseConfigurationToJson(
SupabaseConfiguration instance) =>
<String, dynamic>{
'url': instance.url,
'key': instance.key,
'jwt_secret': instance.jwt_secret,
};
SupabaseDBConfig _$SupabaseDBConfigFromJson(Map<String, dynamic> json) =>
SupabaseDBConfig(
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<String, dynamic>),
);
Map<String, dynamic> _$SupabaseDBConfigToJson(SupabaseDBConfig instance) =>
<String, dynamic>{
'url': instance.url,
'key': instance.key,
'jwt_secret': instance.jwt_secret,
'collab_table_config': instance.collab_table_config,
};
CollabTableConfig _$CollabTableConfigFromJson(Map<String, dynamic> json) =>
CollabTableConfig(
table_name: json['table_name'] as String,
enable: json['enable'] as bool,
);
Map<String, dynamic> _$CollabTableConfigToJson(CollabTableConfig instance) =>
<String, dynamic>{
'table_name': instance.table_name,
'enable': instance.enable,
};

View File

@ -151,3 +151,19 @@ typedef _invoke_log_Dart = void Function(
int level,
Pointer<ffi.Utf8>,
);
/// C function `set_env`.
void set_env(
Pointer<ffi.Utf8> data,
) {
_set_env(data);
}
final _set_env_Dart _set_env =
_dart_ffi_lib.lookupFunction<_set_env_C, _set_env_Dart>('set_env');
typedef _set_env_C = Void Function(
Pointer<ffi.Utf8> data,
);
typedef _set_env_Dart = void Function(
Pointer<ffi.Utf8> data,
);

View File

@ -15,3 +15,5 @@ int32_t set_stream_port(int64_t port);
void link_me_please(void);
void backend_log(int64_t level, const char *data);
void set_env(const char *data);

View File

@ -14,3 +14,5 @@ int32_t set_stream_port(int64_t port);
void link_me_please(void);
void backend_log(int64_t level, const char *data);
void set_env(const char *data);

View File

@ -18,6 +18,7 @@ dependencies:
freezed_annotation:
logger: ^1.0.0
plugin_platform_interface: ^2.1.3
json_annotation: ^4.7.0
dev_dependencies:
flutter_test:
@ -25,6 +26,7 @@ dev_dependencies:
build_runner:
freezed:
flutter_lints: ^2.0.1
json_serializable: ^6.6.2
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View File

@ -99,8 +99,9 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "appflowy-integrate"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"anyhow",
"collab",
"collab-database",
"collab-document",
@ -109,6 +110,7 @@ dependencies = [
"collab-plugins",
"serde",
"serde_json",
"tracing",
]
[[package]]
@ -1021,7 +1023,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"anyhow",
"bytes",
@ -1038,7 +1040,7 @@ dependencies = [
[[package]]
name = "collab-client-ws"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"bytes",
"collab-sync",
@ -1056,7 +1058,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"anyhow",
"async-trait",
@ -1075,12 +1077,13 @@ dependencies = [
"thiserror",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "collab-derive"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"proc-macro2",
"quote",
@ -1092,7 +1095,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"anyhow",
"collab",
@ -1109,7 +1112,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"anyhow",
"collab",
@ -1127,7 +1130,7 @@ dependencies = [
[[package]]
name = "collab-persistence"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"bincode",
"chrono",
@ -1147,21 +1150,25 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"anyhow",
"async-trait",
"aws-config",
"aws-credential-types",
"aws-sdk-dynamodb",
"base64 0.21.0",
"collab",
"collab-client-ws",
"collab-persistence",
"collab-sync",
"futures-util",
"parking_lot 0.12.1",
"postgrest",
"rand 0.8.5",
"rusoto_credential",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-retry",
@ -1173,7 +1180,7 @@ dependencies = [
[[package]]
name = "collab-sync"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"bytes",
"collab",
@ -1773,6 +1780,7 @@ dependencies = [
"flowy-codegen",
"flowy-derive",
"flowy-error",
"flowy-server",
"flowy-sqlite",
"lib-dispatch",
"protobuf",
@ -1803,6 +1811,7 @@ dependencies = [
"parking_lot 0.12.1",
"serde",
"serde_json",
"serde_repr",
"tokio",
"tracing",
]
@ -1972,9 +1981,10 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bytes",
"chrono",
"config",
"flowy-config",
"flowy-error",
"flowy-folder2",
"flowy-user",
"futures-util",
"hyper",

View File

@ -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 = "7a2e97" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7a2e97" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7a2e97" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7a2e97" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7a2e97" }
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7a2e97" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bff164" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bff164" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bff164" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bff164" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bff164" }
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bff164" }
#collab = { path = "../../AppFlowy-Collab/collab" }
#collab-folder = { path = "../../AppFlowy-Collab/collab-folder" }

View File

@ -85,8 +85,9 @@ checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
[[package]]
name = "appflowy-integrate"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"anyhow",
"collab",
"collab-database",
"collab-document",
@ -95,6 +96,7 @@ dependencies = [
"collab-plugins",
"serde",
"serde_json",
"tracing",
]
[[package]]
@ -884,7 +886,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"anyhow",
"bytes",
@ -901,7 +903,7 @@ dependencies = [
[[package]]
name = "collab-client-ws"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"bytes",
"collab-sync",
@ -919,7 +921,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"anyhow",
"async-trait",
@ -938,12 +940,13 @@ dependencies = [
"thiserror",
"tokio",
"tracing",
"uuid",
]
[[package]]
name = "collab-derive"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"proc-macro2",
"quote",
@ -955,7 +958,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"anyhow",
"collab",
@ -972,7 +975,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"anyhow",
"collab",
@ -990,7 +993,7 @@ dependencies = [
[[package]]
name = "collab-persistence"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"bincode",
"chrono",
@ -1010,21 +1013,25 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"anyhow",
"async-trait",
"aws-config",
"aws-credential-types",
"aws-sdk-dynamodb",
"base64 0.21.0",
"collab",
"collab-client-ws",
"collab-persistence",
"collab-sync",
"futures-util",
"parking_lot 0.12.1",
"postgrest",
"rand 0.8.5",
"rusoto_credential",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-retry",
@ -1036,7 +1043,7 @@ dependencies = [
[[package]]
name = "collab-sync"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=7a2e97#7a2e97d9bfe746f5db18753ab0b59347ec5bf23f"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=bff164#bff1647076b2370c34f19d57d8d0da4bbb80ee55"
dependencies = [
"bytes",
"collab",
@ -1254,6 +1261,7 @@ name = "dart-ffi"
version = "0.1.0"
dependencies = [
"allo-isolate",
"appflowy-integrate",
"byteorder",
"bytes",
"crossbeam-utils",
@ -1463,11 +1471,12 @@ dependencies = [
[[package]]
name = "fake"
version = "2.5.0"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d68f517805463f3a896a9d29c1d6ff09d3579ded64a7201b4069f8f9c0d52fd"
checksum = "0a44c765350db469b774425ff1c833890b16ceb9612fb5d7c4bbdf4a1b55f876"
dependencies = [
"rand 0.8.5",
"unidecode",
]
[[package]]
@ -1551,6 +1560,7 @@ dependencies = [
"flowy-codegen",
"flowy-derive",
"flowy-error",
"flowy-server",
"flowy-sqlite",
"lib-dispatch",
"protobuf",
@ -1582,6 +1592,7 @@ dependencies = [
"parking_lot 0.12.1",
"serde",
"serde_json",
"serde_repr",
"tokio",
"tracing",
]
@ -1757,10 +1768,11 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bytes",
"chrono",
"config",
"dotenv",
"flowy-config",
"flowy-error",
"flowy-folder2",
"flowy-user",
"futures-util",
"hyper",
@ -1820,28 +1832,26 @@ name = "flowy-test"
version = "0.1.0"
dependencies = [
"bytes",
"fake",
"dotenv",
"flowy-core",
"flowy-folder2",
"flowy-net",
"flowy-server",
"flowy-user",
"futures",
"futures-util",
"lib-dispatch",
"lib-infra",
"lib-ot",
"log",
"nanoid",
"parking_lot 0.12.1",
"protobuf",
"quickcheck",
"quickcheck_macros 0.9.1",
"serde",
"serde_json",
"serial_test",
"tempdir",
"thread-id",
"tokio",
"tracing",
"uuid",
]
[[package]]
@ -1859,7 +1869,6 @@ dependencies = [
"flowy-error",
"flowy-notification",
"flowy-sqlite",
"flowy-test",
"lazy_static",
"lib-dispatch",
"lib-infra",
@ -1869,7 +1878,7 @@ dependencies = [
"parking_lot 0.12.1",
"protobuf",
"quickcheck",
"quickcheck_macros 1.0.0",
"quickcheck_macros",
"rand 0.8.5",
"rand_core 0.6.4",
"serde",
@ -3483,17 +3492,6 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "quickcheck_macros"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608c156fd8e97febc07dc9c2e2c80bf74cfc6ef26893eae3daf8bc2bc94a4b7f"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "quickcheck_macros"
version = "1.0.0"
@ -4121,28 +4119,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serial_test"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d"
dependencies = [
"lazy_static",
"parking_lot 0.11.2",
"serial_test_derive",
]
[[package]]
name = "serial_test_derive"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "sha-1"
version = "0.9.8"
@ -4978,6 +4954,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unidecode"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc"
[[package]]
name = "untrusted"
version = "0.7.1"

View File

@ -33,11 +33,11 @@ opt-level = 3
incremental = false
[patch.crates-io]
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7a2e97" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7a2e97" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7a2e97" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7a2e97" }
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "7a2e97" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bff164" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bff164" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bff164" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bff164" }
appflowy-integrate = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "bff164" }
#collab = { path = "../AppFlowy-Collab/collab" }
#collab-folder = { path = "../AppFlowy-Collab/collab-folder" }

View File

@ -24,7 +24,7 @@ crossbeam-utils = "0.8.15"
lazy_static = "1.4.0"
parking_lot = "0.12.1"
tracing = { version = "0.1", features = ["log"] }
appflowy-integrate = {version = "0.1.0" }
lib-dispatch = { path = "../lib-dispatch" }
flowy-core = { path = "../flowy-core" }

View File

@ -14,3 +14,5 @@ int32_t set_stream_port(int64_t port);
void link_me_please(void);
void backend_log(int64_t level, const char *data);
void set_env(const char *data);

View File

@ -0,0 +1,19 @@
use appflowy_integrate::SupabaseDBConfig;
use flowy_server::supabase::SupabaseConfiguration;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
pub struct AppFlowyEnv {
supabase_config: SupabaseConfiguration,
supabase_db_config: SupabaseDBConfig,
}
impl AppFlowyEnv {
pub fn parser(env_str: &str) {
if let Ok(env) = serde_json::from_str::<AppFlowyEnv>(env_str) {
dbg!(&env);
env.supabase_config.write_env();
env.supabase_db_config.write_env();
}
}
}

View File

@ -10,6 +10,7 @@ use flowy_notification::register_notification_sender;
use lib_dispatch::prelude::ToBytes;
use lib_dispatch::prelude::*;
use crate::env_serde::AppFlowyEnv;
use crate::notification::DartNotificationSender;
use crate::{
c::{extend_front_four_bytes_into_bytes, forget_rust},
@ -17,6 +18,7 @@ use crate::{
};
mod c;
mod env_serde;
mod model;
mod notification;
mod protobuf;
@ -134,3 +136,10 @@ pub extern "C" fn backend_log(level: i64, data: *const c_char) {
_ => (),
}
}
#[no_mangle]
pub extern "C" fn set_env(data: *const c_char) {
let c_str = unsafe { CStr::from_ptr(data) };
let serde_str = c_str.to_str().unwrap();
AppFlowyEnv::parser(serde_str);
}

View File

@ -14,6 +14,7 @@ bytes = { version = "1.4" }
flowy-error = { path = "../flowy-error" }
strum_macros = "0.21"
appflowy-integrate = {version = "0.1.0" }
flowy-server = { path = "../flowy-server" }
[build-dependencies]
flowy-codegen = { path = "../../../shared-lib/flowy-codegen"}

View File

@ -1,4 +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;
#[derive(Default, ProtoBuf)]
pub struct KeyValuePB {
@ -15,11 +19,6 @@ pub struct KeyPB {
pub key: String,
}
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(Default, ProtoBuf)]
pub struct SupabaseConfigPB {
#[pb(index = 1)]
@ -35,28 +34,97 @@ pub struct SupabaseConfigPB {
jwt_secret: String,
}
impl SupabaseConfigPB {
pub(crate) fn write_to_env(self) {
std::env::set_var(SUPABASE_URL, self.supabase_url);
std::env::set_var(SUPABASE_ANON_KEY, self.anon_key);
std::env::set_var(SUPABASE_KEY, self.key);
std::env::set_var(SUPABASE_JWT_SECRET, self.jwt_secret);
impl TryFrom<SupabaseConfigPB> for SupabaseConfiguration {
type Error = FlowyError;
fn try_from(value: SupabaseConfigPB) -> Result<Self, Self::Error> {
Ok(Self {
url: value.supabase_url,
key: value.key,
jwt_secret: value.jwt_secret,
})
}
}
#[derive(Default, ProtoBuf)]
pub struct AppFlowyCollabConfigPB {
pub struct CollabPluginConfigPB {
#[pb(index = 1, one_of)]
aws_config: Option<AWSDynamoDBConfigPB>,
pub aws_config: Option<AWSDynamoDBConfigPB>,
#[pb(index = 2, one_of)]
pub supabase_config: Option<SupabaseDBConfigPB>,
}
#[derive(Default, ProtoBuf)]
pub struct AWSDynamoDBConfigPB {
#[pb(index = 1)]
pub access_key_id: String,
#[pb(index = 2)]
pub secret_access_key: String,
// Region list: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html
#[pb(index = 3)]
pub region: String,
}
impl TryFrom<AWSDynamoDBConfigPB> for AWSDynamoDBConfig {
type Error = FlowyError;
fn try_from(config: AWSDynamoDBConfigPB) -> Result<Self, Self::Error> {
Ok(AWSDynamoDBConfig {
access_key_id: config.access_key_id,
secret_access_key: config.secret_access_key,
region: config.region,
enable: true,
})
}
}
#[derive(Default, ProtoBuf)]
pub struct SupabaseDBConfigPB {
#[pb(index = 1)]
pub supabase_url: String,
#[pb(index = 2)]
pub key: String,
#[pb(index = 3)]
pub jwt_secret: String,
#[pb(index = 4)]
pub collab_table_config: CollabTableConfigPB,
}
impl TryFrom<SupabaseDBConfigPB> for SupabaseDBConfig {
type Error = FlowyError;
fn try_from(config: SupabaseDBConfigPB) -> Result<Self, Self::Error> {
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<CollabTableConfigPB> for CollabTableConfig {
type Error = FlowyError;
fn try_from(config: CollabTableConfigPB) -> Result<Self, Self::Error> {
if config.table_name.is_empty() {
return Err(FlowyError::internal().context("table_name is empty"));
}
Ok(CollabTableConfig {
table_name: config.table_name,
enable: true,
})
}
}

View File

@ -1,8 +1,11 @@
use appflowy_integrate::config::AWSDynamoDBConfig;
use appflowy_integrate::SupabaseDBConfig;
use flowy_error::{FlowyError, FlowyResult};
use flowy_server::supabase::SupabaseConfiguration;
use flowy_sqlite::kv::KV;
use lib_dispatch::prelude::{data_result_ok, AFPluginData, DataResult};
use crate::entities::{KeyPB, KeyValuePB, SupabaseConfigPB};
use crate::entities::{CollabPluginConfigPB, KeyPB, KeyValuePB, SupabaseConfigPB};
pub(crate) async fn set_key_value_handler(data: AFPluginData<KeyValuePB>) -> FlowyResult<()> {
let data = data.into_inner();
@ -35,7 +38,24 @@ pub(crate) async fn remove_key_value_handler(data: AFPluginData<KeyPB>) -> Flowy
pub(crate) async fn set_supabase_config_handler(
data: AFPluginData<SupabaseConfigPB>,
) -> FlowyResult<()> {
let config = data.into_inner();
config.write_to_env();
let config = SupabaseConfiguration::try_from(data.into_inner())?;
config.write_env();
Ok(())
}
pub(crate) async fn set_collab_plugin_config_handler(
data: AFPluginData<CollabPluginConfigPB>,
) -> FlowyResult<()> {
let config = data.into_inner();
if let Some(aws_config_pb) = config.aws_config {
if let Ok(aws_config) = AWSDynamoDBConfig::try_from(aws_config_pb) {
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(())
}

View File

@ -12,6 +12,10 @@ pub fn init() -> AFPlugin {
.event(ConfigEvent::GetKeyValue, get_key_value_handler)
.event(ConfigEvent::RemoveKeyValue, remove_key_value_handler)
.event(ConfigEvent::SetSupabaseConfig, set_supabase_config_handler)
.event(
ConfigEvent::SetCollabPluginConfig,
set_collab_plugin_config_handler,
)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
@ -30,4 +34,7 @@ pub enum ConfigEvent {
/// Check out the `write_to_env` of [SupabaseConfigPB].
#[event(input = "SupabaseConfigPB")]
SetSupabaseConfig = 3,
#[event(input = "CollabPluginConfigPB")]
SetCollabPluginConfig = 4,
}

View File

@ -11,12 +11,9 @@ lib-log = { path = "../lib-log" }
flowy-user = { path = "../flowy-user" }
flowy-net = { path = "../flowy-net" }
flowy-folder2 = { path = "../flowy-folder2" }
#flowy-database = { path = "../flowy-database" }
flowy-database2 = { path = "../flowy-database2" }
flowy-sqlite = { path = "../flowy-sqlite", optional = true }
#flowy-document = { path = "../flowy-document" }
flowy-document2 = { path = "../flowy-document2" }
#flowy-revision = { path = "../flowy-revision" }
flowy-error = { path = "../flowy-error" }
flowy-task = { path = "../flowy-task" }
flowy-server = { path = "../flowy-server" }
@ -34,6 +31,7 @@ lib-ws = { path = "../../../shared-lib/lib-ws" }
lib-infra = { path = "../../../shared-lib/lib-infra" }
serde = "1.0"
serde_json = "1.0"
serde_repr = "0.1"
[features]
default = ["rev-sqlite"]

View File

@ -13,8 +13,9 @@ use flowy_document2::document_data::DocumentDataWrapper;
use flowy_document2::entities::DocumentDataPB;
use flowy_document2::manager::DocumentManager;
use flowy_error::FlowyError;
use flowy_folder2::deps::{FolderCloudService, FolderUser};
use flowy_folder2::entities::ViewLayoutPB;
use flowy_folder2::manager::{Folder2Manager, FolderUser};
use flowy_folder2::manager::Folder2Manager;
use flowy_folder2::view_ext::{ViewDataProcessor, ViewDataProcessorMap};
use flowy_folder2::ViewLayout;
use flowy_user::services::UserSession;
@ -27,13 +28,14 @@ impl Folder2DepsResolver {
document_manager: &Arc<DocumentManager>,
database_manager: &Arc<DatabaseManager2>,
collab_builder: Arc<AppFlowyCollabBuilder>,
folder_cloud: Arc<dyn FolderCloudService>,
) -> Arc<Folder2Manager> {
let user: Arc<dyn FolderUser> = Arc::new(FolderUserImpl(user_session.clone()));
let view_data_processor =
let view_processors =
make_view_data_processor(document_manager.clone(), database_manager.clone());
Arc::new(
Folder2Manager::new(user.clone(), collab_builder, view_data_processor)
Folder2Manager::new(user.clone(), collab_builder, view_processors, folder_cloud)
.await
.unwrap(),
)

View File

@ -1,64 +1,134 @@
use lib_infra::future::FutureResult;
use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use flowy_error::{ErrorCode, FlowyError};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_folder2::deps::{FolderCloudService, Workspace};
use flowy_server::local_server::LocalServer;
use flowy_server::self_host::configuration::self_host_server_configuration;
use flowy_server::self_host::SelfHostServer;
use flowy_server::supabase::{SupabaseConfiguration, SupabaseServer};
use flowy_server::AppFlowyServer;
use flowy_sqlite::kv::KV;
use flowy_user::event_map::{UserAuthService, UserCloudServiceProvider};
use flowy_user::services::AuthType;
use serde_repr::*;
const SERVER_PROVIDER_TYPE_KEY: &str = "server_provider_type";
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
pub enum ServerProviderType {
/// Local server provider.
/// Offline mode, no user authentication and the data is stored locally.
Local = 0,
/// Self-hosted server provider.
/// The [AppFlowy-Server](https://github.com/AppFlowy-IO/AppFlowy-Server) is still a work in
/// progress.
SelfHosted = 1,
/// Supabase server provider.
/// It uses supabase's postgresql database to store data and user authentication.
Supabase = 2,
}
/// The [AppFlowyServerProvider] provides list of [AppFlowyServer] base on the [AuthType]. Using
/// the auth type, the [AppFlowyServerProvider] will create a new [AppFlowyServer] if it doesn't
/// exist.
/// Each server implements the [AppFlowyServer] trait, which provides the [UserAuthService], etc.
#[derive(Default)]
pub struct AppFlowyServerProvider {
providers: RwLock<HashMap<AuthType, Arc<dyn AppFlowyServer>>>,
provider_type: RwLock<ServerProviderType>,
providers: RwLock<HashMap<ServerProviderType, Arc<dyn AppFlowyServer>>>,
}
impl AppFlowyServerProvider {
pub fn new() -> Self {
Self::default()
}
}
impl UserCloudServiceProvider for AppFlowyServerProvider {
/// Returns the [UserAuthService] base on the current [AuthType].
/// Creates a new [AppFlowyServer] if it doesn't exist.
fn get_auth_service(&self, auth_type: &AuthType) -> Result<Arc<dyn UserAuthService>, FlowyError> {
if let Some(provider) = self.providers.read().get(auth_type) {
return Ok(provider.user_service());
pub fn provider_type(&self) -> ServerProviderType {
self.provider_type.read().clone()
}
/// Returns a [AppFlowyServer] trait implementation base on the provider_type.
fn get_provider(
&self,
provider_type: &ServerProviderType,
) -> FlowyResult<Arc<dyn AppFlowyServer>> {
if let Some(provider) = self.providers.read().get(provider_type) {
return Ok(provider.clone());
}
let server = server_from_auth_type(auth_type)?;
let user_service = server.user_service();
self.providers.write().insert(auth_type.clone(), server);
Ok(user_service)
let server = server_from_auth_type(provider_type)?;
self
.providers
.write()
.insert(provider_type.clone(), server.clone());
Ok(server)
}
}
fn server_from_auth_type(auth_type: &AuthType) -> Result<Arc<dyn AppFlowyServer>, FlowyError> {
match auth_type {
AuthType::Local => {
impl Default for AppFlowyServerProvider {
fn default() -> Self {
Self {
provider_type: RwLock::new(current_server_provider()),
providers: RwLock::new(HashMap::new()),
}
}
}
impl UserCloudServiceProvider for AppFlowyServerProvider {
/// When user login, the provider type is set by the [AuthType].
/// Each [AuthType] has a corresponding [ServerProviderType]. The [ServerProviderType] is used
/// to create a new [AppFlowyServer] if it doesn't exist. Once the [ServerProviderType] is set,
/// it will be used when user open the app again.
fn set_auth_type(&self, auth_type: AuthType) {
let provider_type: ServerProviderType = auth_type.into();
match KV::set_object(SERVER_PROVIDER_TYPE_KEY, provider_type.clone()) {
Ok(_) => tracing::trace!("Update server provider type to: {:?}", provider_type),
Err(e) => {
tracing::error!("🔴Failed to update server provider type: {:?}", e);
},
}
}
/// Returns the [UserAuthService] base on the current [ServerProviderType].
/// Creates a new [AppFlowyServer] if it doesn't exist.
fn get_auth_service(&self, auth_type: &AuthType) -> Result<Arc<dyn UserAuthService>, FlowyError> {
let provider_type: ServerProviderType = auth_type.into();
Ok(self.get_provider(&provider_type)?.user_service())
}
}
impl FolderCloudService for AppFlowyServerProvider {
fn create_workspace(&self, uid: i64, name: &str) -> FutureResult<Workspace, FlowyError> {
let server = self.get_provider(&self.provider_type.read());
let name = name.to_string();
FutureResult::new(async move { server?.folder_service().create_workspace(uid, &name).await })
}
}
fn server_from_auth_type(
provider: &ServerProviderType,
) -> Result<Arc<dyn AppFlowyServer>, FlowyError> {
match provider {
ServerProviderType::Local => {
let server = Arc::new(LocalServer::new());
Ok(server)
},
AuthType::SelfHosted => {
ServerProviderType::SelfHosted => {
let config = self_host_server_configuration().map_err(|e| {
FlowyError::new(
ErrorCode::InvalidAuthConfig,
format!("Missing self host config: {:?}. Error: {:?}", auth_type, e),
format!("Missing self host config: {:?}. Error: {:?}", provider, e),
)
})?;
let server = Arc::new(SelfHostServer::new(config));
Ok(server)
},
AuthType::Supabase => {
ServerProviderType::Supabase => {
// init the SupabaseServerConfiguration from the environment variables.
let config = SupabaseConfiguration::from_env()?;
let server = Arc::new(SupabaseServer::new(config));
@ -66,3 +136,26 @@ fn server_from_auth_type(auth_type: &AuthType) -> Result<Arc<dyn AppFlowyServer>
},
}
}
impl From<AuthType> for ServerProviderType {
fn from(auth_provider: AuthType) -> Self {
match auth_provider {
AuthType::Local => ServerProviderType::Local,
AuthType::SelfHosted => ServerProviderType::SelfHosted,
AuthType::Supabase => ServerProviderType::Supabase,
}
}
}
impl From<&AuthType> for ServerProviderType {
fn from(auth_provider: &AuthType) -> Self {
Self::from(auth_provider.clone())
}
}
fn current_server_provider() -> ServerProviderType {
match KV::get_object::<ServerProviderType>(SERVER_PROVIDER_TYPE_KEY) {
None => ServerProviderType::Local,
Some(provider_type) => provider_type,
}
}

View File

@ -1,6 +1,5 @@
#![allow(unused_doc_comments)]
use std::str::FromStr;
use std::time::Duration;
use std::{
fmt,
@ -10,8 +9,8 @@ use std::{
},
};
use appflowy_integrate::collab_builder::AppFlowyCollabBuilder;
use appflowy_integrate::config::{AWSDynamoDBConfig, AppFlowyCollabConfig};
use appflowy_integrate::collab_builder::{AppFlowyCollabBuilder, CloudStorageType};
use tokio::sync::RwLock;
use flowy_database2::DatabaseManager2;
@ -28,9 +27,10 @@ use lib_dispatch::runtime::tokio_default_runtime;
use lib_infra::future::{to_fut, Fut};
use module::make_plugins;
pub use module::*;
use tracing::debug;
use crate::deps_resolve::*;
use crate::integrate::server::AppFlowyServerProvider;
use crate::integrate::server::{AppFlowyServerProvider, ServerProviderType};
mod deps_resolve;
mod integrate;
@ -92,10 +92,8 @@ fn create_log_filter(level: String, with_crates: Vec<String>) -> String {
filters.push(format!("flowy_document2={}", level));
filters.push(format!("flowy_database2={}", level));
filters.push(format!("flowy_notification={}", "info"));
filters.push(format!("lib_ot={}", level));
filters.push(format!("lib_infra={}", level));
filters.push(format!("flowy_task={}", level));
// filters.push(format!("lib_dispatch={}", level));
filters.push(format!("dart_ffi={}", "info"));
filters.push(format!("flowy_sqlite={}", "info"));
@ -136,22 +134,19 @@ impl AppFlowyCore {
// Init the key value database
init_kv(&config.storage_path);
// The collab config is used to build the [Collab] instance that used in document,
// database, folder, etc.
let collab_config = get_collab_config();
inject_aws_env(collab_config.aws_config());
/// The shared collab builder is used to build the [Collab] instance. The plugins will be loaded
/// on demand based on the [AppFlowyCollabConfig].
let collab_builder = Arc::new(AppFlowyCollabBuilder::new(collab_config));
tracing::debug!("🔥 {:?}", config);
debug!("🔥 {:?}", &config);
let runtime = tokio_default_runtime().unwrap();
let task_scheduler = TaskDispatcher::new(Duration::from_secs(2));
let task_dispatcher = Arc::new(RwLock::new(task_scheduler));
runtime.spawn(TaskRunner::run(task_dispatcher.clone()));
let server_provider = Arc::new(AppFlowyServerProvider::new());
/// The shared collab builder is used to build the [Collab] instance. The plugins will be loaded
/// on demand based on the [CollabPluginConfig].
let cloud_storage_type =
collab_storage_type_from_server_provider_type(&server_provider.provider_type());
let collab_builder = Arc::new(AppFlowyCollabBuilder::new(cloud_storage_type));
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());
@ -173,6 +168,7 @@ impl AppFlowyCore {
&document_manager2,
&database_manager2,
collab_builder.clone(),
server_provider.clone(),
)
.await;
@ -233,23 +229,6 @@ fn init_kv(root: &str) {
}
}
fn get_collab_config() -> AppFlowyCollabConfig {
match KV::get_str("collab_config") {
None => AppFlowyCollabConfig::default(),
Some(s) => AppFlowyCollabConfig::from_str(&s).unwrap_or_default(),
}
}
fn inject_aws_env(aws_config: Option<&AWSDynamoDBConfig>) {
if let Some(aws_config) = aws_config {
std::env::set_var("AWS_ACCESS_KEY_ID", aws_config.access_key_id.clone());
std::env::set_var(
"AWS_SECRET_ACCESS_KEY",
aws_config.secret_access_key.clone(),
);
}
}
fn init_log(config: &AppFlowyCoreConfig) {
if !INIT_LOG.load(Ordering::SeqCst) {
INIT_LOG.store(true, Ordering::SeqCst);
@ -334,3 +313,13 @@ impl UserStatusCallback for UserStatusCallbackImpl {
to_fut(async move { listener.did_expired(&token, user_id).await })
}
}
fn collab_storage_type_from_server_provider_type(
server_provider_type: &ServerProviderType,
) -> CloudStorageType {
match server_provider_type {
ServerProviderType::Local => CloudStorageType::Local,
ServerProviderType::SelfHosted => CloudStorageType::Local,
ServerProviderType::Supabase => CloudStorageType::Supabase,
}
}

View File

@ -334,7 +334,7 @@ pub(crate) async fn create_row_handler(
}
}
#[tracing::instrument(level = "trace", skip_all, err)]
// #[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn get_cell_handler(
data: AFPluginData<CellIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,
@ -560,7 +560,6 @@ pub(crate) async fn set_layout_setting_handler(
Ok(())
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
pub(crate) async fn get_layout_setting_handler(
data: AFPluginData<DatabaseLayoutIdPB>,
manager: AFPluginState<Arc<DatabaseManager2>>,

View File

@ -3,7 +3,7 @@ use std::ops::Deref;
use std::sync::Arc;
use appflowy_integrate::collab_builder::AppFlowyCollabBuilder;
use appflowy_integrate::{RocksCollabDB, RocksDBConfig};
use appflowy_integrate::{CollabPersistenceConfig, RocksCollabDB};
use collab::core::collab::MutexCollab;
use collab_database::database::DatabaseData;
use collab_database::user::{UserDatabase as InnerUserDatabase, UserDatabaseCollabBuilder};
@ -51,7 +51,7 @@ impl DatabaseManager2 {
*self.user_database.lock() = Some(InnerUserDatabase::new(
user_id,
db,
RocksDBConfig::default(),
CollabPersistenceConfig::default(),
UserDatabaseCollabBuilderImpl(self.collab_builder.clone()),
));
// do nothing
@ -229,7 +229,7 @@ impl UserDatabaseCollabBuilder for UserDatabaseCollabBuilderImpl {
uid: i64,
object_id: &str,
db: Arc<RocksCollabDB>,
config: &RocksDBConfig,
config: &CollabPersistenceConfig,
) -> Arc<MutexCollab> {
self.0.build_with_config(uid, object_id, db, config)
}

View File

@ -3,8 +3,9 @@ use std::sync::Arc;
use collab_database::fields::Field;
use collab_database::rows::RowId;
use flowy_error::FlowyResult;
use flowy_error::{FlowyError, FlowyResult};
use lib_infra::future::{to_fut, Fut};
use tracing::trace;
use crate::entities::FieldType;
use crate::services::database_view::DatabaseViewData;
@ -42,9 +43,10 @@ pub async fn new_group_controller(
let fields = delegate.get_fields(&view_id, None).await;
let rows = delegate.get_rows(&view_id).await;
let layout = delegate.get_layout_for_view(&view_id);
trace!(?fields, ?rows, ?layout, "new_group_controller");
// Read the grouping field or find a new grouping field
let grouping_field = setting_reader
let mut grouping_field = setting_reader
.get_group_setting(&view_id)
.await
.and_then(|setting| {
@ -52,17 +54,25 @@ pub async fn new_group_controller(
.iter()
.find(|field| field.id == setting.field_id)
.cloned()
})
.unwrap_or_else(|| find_new_grouping_field(&fields, &layout).unwrap());
});
make_group_controller(
view_id,
grouping_field,
rows,
setting_reader,
setting_writer,
)
.await
if grouping_field.is_none() {
grouping_field = find_new_grouping_field(&fields, &layout);
}
match grouping_field {
None => Err(FlowyError::internal().context("No grouping field found".to_owned())),
Some(_) => {
make_group_controller(
view_id,
grouping_field.unwrap(),
rows,
setting_reader,
setting_writer,
)
.await
},
}
}
pub(crate) struct GroupSettingReaderImpl(pub Arc<dyn DatabaseViewData>);

View File

@ -7,15 +7,9 @@ use strum::IntoEnumIterator;
use strum_macros::EnumIter;
lazy_static! {
pub static ref CURRENCY_SYMBOL: Vec<String> = sorted_symbol();
}
fn sorted_symbol() -> Vec<String> {
let mut symbols = NumberFormat::iter()
pub static ref CURRENCY_SYMBOL: Vec<String> = NumberFormat::iter()
.map(|format| format.symbol())
.collect::<Vec<String>>();
symbols.sort_by(|a, b| b.len().cmp(&a.len()));
symbols
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, Serialize, Deserialize)]

View File

@ -137,9 +137,7 @@ impl NumberTypeOption {
};
match Decimal::from_str(&num_str) {
Ok(decimal, ..) => {
return Ok(NumberCellFormat::from_decimal(decimal));
},
Ok(decimal, ..) => Ok(NumberCellFormat::from_decimal(decimal)),
Err(_) => Ok(NumberCellFormat::new()),
}
}

View File

@ -27,7 +27,7 @@ impl NumberCellFormat {
return Ok(Self::default());
}
// If the first char is not '-', then it is a sign.
let sign_positive = match num_str.find("-") {
let sign_positive = match num_str.find('-') {
None => true,
Some(offset) => offset != 0,
};

View File

@ -162,7 +162,7 @@ where
) {
if let Some(cell_data_cache) = self.cell_data_cache.as_ref() {
let field_type = FieldType::from(field.field_type);
let key = CellDataCacheKey::new(field, field_type.clone(), cell);
let key = CellDataCacheKey::new(field, field_type, cell);
// tracing::trace!(
// "Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}",
// field_type,

View File

@ -85,7 +85,7 @@ impl SortController {
self.gen_task(task_type, QualityOfService::Background).await;
}
#[tracing::instrument(name = "process_sort_task", level = "debug", skip_all, err)]
// #[tracing::instrument(name = "process_sort_task", level = "trace", skip_all, err)]
pub async fn process(&mut self, predicate: &str) -> FlowyResult<()> {
let event_type = SortEvent::from_str(predicate).unwrap();
let mut rows = self.delegate.get_rows(&self.view_id).await;

View File

@ -13,13 +13,13 @@ use flowy_database2::services::field::{
SelectOptionCellChangeset, SingleSelectTypeOption,
};
use flowy_error::FlowyResult;
use flowy_test::helper::ViewTest;
use flowy_test::FlowySDKTest;
use flowy_test::folder_event::ViewTest;
use flowy_test::FlowyCoreTest;
use crate::database::mock_data::{make_test_board, make_test_calendar, make_test_grid};
pub struct DatabaseEditorTest {
pub sdk: FlowySDKTest,
pub sdk: FlowyCoreTest,
pub app_id: String,
pub view_id: String,
pub editor: Arc<DatabaseEditor>,
@ -43,7 +43,7 @@ impl DatabaseEditorTest {
}
pub async fn new(layout: DatabaseLayoutPB) -> Self {
let sdk = FlowySDKTest::default();
let sdk = FlowyCoreTest::new();
let _ = sdk.init_user().await;
let test = match layout {
DatabaseLayoutPB::Grid => {

View File

@ -1,425 +0,0 @@
use bytes::Bytes;
use database_model::entities::{
BuildGridContext, CellChangeset, Field, FieldChangesetParams, FieldMeta, FieldOrder, FieldType,
GridBlockInfoChangeset, GridBlockMetaSnapshot, InsertFieldParams, RowMeta, RowMetaChangeset,
RowOrder, TypeOptionDataFormat,
};
use flowy_client_sync::client_grid::GridBuilder;
use flowy_database::services::field::*;
use flowy_database::services::grid_meta_editor::{GridMetaEditor, GridPadBuilder};
use flowy_database::services::row::CreateRowMetaPayload;
use flowy_revision::REVISION_WRITE_INTERVAL_IN_MILLIS;
use flowy_test::helper::ViewTest;
use flowy_test::FlowySDKTest;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use strum::EnumCount;
use tokio::time::sleep;
pub enum EditorScript {
CreateField {
params: InsertFieldParams,
},
UpdateField {
changeset: FieldChangesetParams,
},
DeleteField {
field_meta: FieldMeta,
},
AssertFieldCount(usize),
AssertFieldEqual {
field_index: usize,
field_meta: FieldMeta,
},
CreateBlock {
block: GridBlockMetaSnapshot,
},
UpdateBlock {
changeset: GridBlockInfoChangeset,
},
AssertBlockCount(usize),
AssertBlock {
block_index: usize,
row_count: i32,
start_row_index: i32,
},
AssertBlockEqual {
block_index: usize,
block: GridBlockMetaSnapshot,
},
CreateEmptyRow,
CreateRow {
context: CreateRowMetaPayload,
},
UpdateRow {
changeset: RowMetaChangeset,
},
AssertRow {
changeset: RowMetaChangeset,
},
DeleteRow {
row_ids: Vec<String>,
},
UpdateCell {
changeset: CellChangeset,
is_err: bool,
},
AssertRowCount(usize),
// AssertRowEqual{ row_index: usize, row: RowMeta},
AssertGridMetaPad,
}
pub struct GridEditorTest {
pub sdk: FlowySDKTest,
pub grid_id: String,
pub editor: Arc<GridMetaEditor>,
pub field_metas: Vec<FieldMeta>,
pub grid_blocks: Vec<GridBlockMetaSnapshot>,
pub row_metas: Vec<Arc<RowMeta>>,
pub field_count: usize,
pub row_order_by_row_id: HashMap<String, RowOrder>,
}
impl GridEditorTest {
pub async fn new() -> Self {
let sdk = FlowySDKTest::default();
let _ = sdk.init_user().await;
let build_context = make_template_1_grid();
let view_data: Bytes = build_context.into();
let test = ViewTest::new_grid_view(&sdk, view_data.to_vec()).await;
let editor = sdk.grid_manager.open_grid(&test.view.id).await.unwrap();
let field_metas = editor.get_field_metas::<FieldOrder>(None).await.unwrap();
let grid_blocks = editor.get_block_metas().await.unwrap();
let row_metas = get_row_metas(&editor).await;
let grid_id = test.view.id;
Self {
sdk,
grid_id,
editor,
field_metas,
grid_blocks,
row_metas,
field_count: FieldType::COUNT,
row_order_by_row_id: HashMap::default(),
}
}
pub async fn run_scripts(&mut self, scripts: Vec<EditorScript>) {
for script in scripts {
self.run_script(script).await;
}
}
pub async fn run_script(&mut self, script: EditorScript) {
let grid_manager = self.sdk.grid_manager.clone();
let pool = self.sdk.user_session.db_pool().unwrap();
let rev_manager = self.editor.rev_manager();
let _cache = rev_manager.revision_cache().await;
match script {
EditorScript::CreateField { params } => {
if !self.editor.contain_field(&params.field.id).await {
self.field_count += 1;
}
self.editor.insert_field(params).await.unwrap();
self.field_metas = self
.editor
.get_field_metas::<FieldOrder>(None)
.await
.unwrap();
assert_eq!(self.field_count, self.field_metas.len());
},
EditorScript::UpdateField { changeset: change } => {
self.editor.update_field(change).await.unwrap();
self.field_metas = self
.editor
.get_field_metas::<FieldOrder>(None)
.await
.unwrap();
},
EditorScript::DeleteField { field_meta } => {
if self.editor.contain_field(&field_meta.id).await {
self.field_count -= 1;
}
self.editor.delete_field(&field_meta.id).await.unwrap();
self.field_metas = self
.editor
.get_field_metas::<FieldOrder>(None)
.await
.unwrap();
assert_eq!(self.field_count, self.field_metas.len());
},
EditorScript::AssertFieldCount(count) => {
assert_eq!(
self
.editor
.get_field_metas::<FieldOrder>(None)
.await
.unwrap()
.len(),
count
);
},
EditorScript::AssertFieldEqual {
field_index,
field_meta,
} => {
let field_metas = self
.editor
.get_field_metas::<FieldOrder>(None)
.await
.unwrap();
assert_eq!(field_metas[field_index].clone(), field_meta);
},
EditorScript::CreateBlock { block } => {
self.editor.create_block(block).await.unwrap();
self.grid_blocks = self.editor.get_block_metas().await.unwrap();
},
EditorScript::UpdateBlock { changeset: change } => {
self.editor.update_block(change).await.unwrap();
},
EditorScript::AssertBlockCount(count) => {
assert_eq!(self.editor.get_block_metas().await.unwrap().len(), count);
},
EditorScript::AssertBlock {
block_index,
row_count,
start_row_index,
} => {
assert_eq!(self.grid_blocks[block_index].row_count, row_count);
assert_eq!(
self.grid_blocks[block_index].start_row_index,
start_row_index
);
},
EditorScript::AssertBlockEqual { block_index, block } => {
let blocks = self.editor.get_block_metas().await.unwrap();
let compared_block = blocks[block_index].clone();
assert_eq!(compared_block, block);
},
EditorScript::CreateEmptyRow => {
let row_order = self.editor.create_row(None).await.unwrap();
self
.row_order_by_row_id
.insert(row_order.row_id.clone(), row_order);
self.row_metas = self.get_row_metas().await;
self.grid_blocks = self.editor.get_block_metas().await.unwrap();
},
EditorScript::CreateRow { context } => {
let row_orders = self.editor.insert_rows(vec![context]).await.unwrap();
for row_order in row_orders {
self
.row_order_by_row_id
.insert(row_order.row_id.clone(), row_order);
}
self.row_metas = self.get_row_metas().await;
self.grid_blocks = self.editor.get_block_metas().await.unwrap();
},
EditorScript::UpdateRow { changeset: change } => {
self.editor.update_row(change).await.unwrap()
},
EditorScript::DeleteRow { row_ids } => {
let row_orders = row_ids
.into_iter()
.map(|row_id| self.row_order_by_row_id.get(&row_id).unwrap().clone())
.collect::<Vec<RowOrder>>();
self.editor.delete_rows(row_orders).await.unwrap();
self.row_metas = self.get_row_metas().await;
self.grid_blocks = self.editor.get_block_metas().await.unwrap();
},
EditorScript::AssertRow { changeset } => {
let row = self
.row_metas
.iter()
.find(|row| row.id == changeset.row_id)
.unwrap();
if let Some(visibility) = changeset.visibility {
assert_eq!(row.visibility, visibility);
}
if let Some(height) = changeset.height {
assert_eq!(row.height, height);
}
},
EditorScript::UpdateCell { changeset, is_err } => {
let result = self.editor.update_cell(changeset).await;
if is_err {
assert!(result.is_err())
} else {
let _ = result.unwrap();
self.row_metas = self.get_row_metas().await;
}
},
EditorScript::AssertRowCount(count) => {
assert_eq!(self.row_metas.len(), count);
},
EditorScript::AssertGridMetaPad => {
sleep(Duration::from_millis(2 * REVISION_WRITE_INTERVAL_IN_MILLIS)).await;
let mut grid_rev_manager = grid_manager
.make_grid_rev_manager(&self.grid_id, pool.clone())
.unwrap();
let grid_pad = grid_rev_manager.load::<GridPadBuilder>(None).await.unwrap();
println!("{}", grid_pad.delta_str());
},
}
}
async fn get_row_metas(&self) -> Vec<Arc<RowMeta>> {
get_row_metas(&self.editor).await
}
}
async fn get_row_metas(editor: &Arc<GridMetaEditor>) -> Vec<Arc<RowMeta>> {
editor
.grid_block_snapshots(None)
.await
.unwrap()
.pop()
.unwrap()
.row_metas
}
pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldMeta) {
let field_meta = FieldBuilder::new(RichTextTypeOptionBuilder::default())
.name("Name")
.visibility(true)
.build();
let cloned_field_meta = field_meta.clone();
let type_option_data = field_meta
.get_type_option_entry::<RichTextTypeOptionPB>(&field_meta.field_type)
.unwrap()
.protobuf_bytes()
.to_vec();
let field = Field {
id: field_meta.id,
name: field_meta.name,
desc: field_meta.desc,
field_type: field_meta.field_type,
frozen: field_meta.frozen,
visibility: field_meta.visibility,
width: field_meta.width,
is_primary: false,
};
let params = InsertFieldParams {
grid_id: grid_id.to_owned(),
field,
type_option_data,
start_field_id: None,
};
(params, cloned_field_meta)
}
pub fn create_single_select_field(grid_id: &str) -> (InsertFieldParams, FieldMeta) {
let single_select = SingleSelectTypeOptionBuilder::default()
.option(SelectOption::new("Done"))
.option(SelectOption::new("Progress"));
let field_meta = FieldBuilder::new(single_select)
.name("Name")
.visibility(true)
.build();
let cloned_field_meta = field_meta.clone();
let type_option_data = field_meta
.get_type_option_entry::<SingleSelectTypeOption>(&field_meta.field_type)
.unwrap()
.protobuf_bytes()
.to_vec();
let field = Field {
id: field_meta.id,
name: field_meta.name,
desc: field_meta.desc,
field_type: field_meta.field_type,
frozen: field_meta.frozen,
visibility: field_meta.visibility,
width: field_meta.width,
is_primary: false,
};
let params = InsertFieldParams {
grid_id: grid_id.to_owned(),
field,
type_option_data,
start_field_id: None,
};
(params, cloned_field_meta)
}
fn make_template_1_grid() -> BuildGridContext {
let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
.name("Name")
.visibility(true)
.build();
// Single Select
let single_select = SingleSelectTypeOptionBuilder::default()
.option(SelectOption::new("Live"))
.option(SelectOption::new("Completed"))
.option(SelectOption::new("Planned"))
.option(SelectOption::new("Paused"));
let single_select_field = FieldBuilder::new(single_select)
.name("Status")
.visibility(true)
.build();
// MultiSelect
let multi_select = MultiSelectTypeOptionBuilder::default()
.option(SelectOption::new("Google"))
.option(SelectOption::new("Facebook"))
.option(SelectOption::new("Twitter"));
let multi_select_field = FieldBuilder::new(multi_select)
.name("Platform")
.visibility(true)
.build();
// Number
let number = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD);
let number_field = FieldBuilder::new(number)
.name("Price")
.visibility(true)
.build();
// Date
let date = DateTypeOptionBuilder::default()
.date_format(DateFormat::US)
.time_format(TimeFormat::TwentyFourHour);
let date_field = FieldBuilder::new(date)
.name("Time")
.visibility(true)
.build();
// Checkbox
let checkbox = CheckboxTypeOptionBuilder::default();
let checkbox_field = FieldBuilder::new(checkbox)
.name("is done")
.visibility(true)
.build();
// URL
let url = URLTypeOptionBuilder::default();
let url_field = FieldBuilder::new(url).name("link").visibility(true).build();
GridBuilder::default()
.add_field(text_field)
.add_field(single_select_field)
.add_field(multi_select_field)
.add_field(number_field)
.add_field(date_field)
.add_field(checkbox_field)
.add_field(url_field)
.add_empty_row()
.add_empty_row()
.add_empty_row()
.build()
}

View File

@ -1,5 +1,5 @@
use appflowy_integrate::collab_builder::AppFlowyCollabBuilder;
use appflowy_integrate::config::AppFlowyCollabConfig;
use appflowy_integrate::collab_builder::{AppFlowyCollabBuilder, CloudStorageType};
use std::sync::Arc;
use appflowy_integrate::RocksCollabDB;
@ -50,6 +50,6 @@ pub fn db() -> Arc<RocksCollabDB> {
}
pub fn default_collab_builder() -> Arc<AppFlowyCollabBuilder> {
let builder = AppFlowyCollabBuilder::new(AppFlowyCollabConfig::default());
let builder = AppFlowyCollabBuilder::new(CloudStorageType::Local);
Arc::new(builder)
}

View File

@ -0,0 +1,17 @@
use appflowy_integrate::RocksCollabDB;
pub use collab_folder::core::Workspace;
use flowy_error::FlowyError;
use lib_infra::future::FutureResult;
use std::sync::Arc;
/// [FolderUser] represents the user for folder.
pub trait FolderUser: Send + Sync {
fn user_id(&self) -> Result<i64, FlowyError>;
fn token(&self) -> Result<Option<String>, FlowyError>;
fn collab_db(&self) -> Result<Arc<RocksCollabDB>, FlowyError>;
}
/// [FolderCloudService] represents the cloud service for folder.
pub trait FolderCloudService: Send + Sync + 'static {
fn create_workspace(&self, uid: i64, name: &str) -> FutureResult<Workspace, FlowyError>;
}

View File

@ -106,7 +106,7 @@ impl WorkspaceIdPB {
}
}
#[derive(Default, ProtoBuf, Clone)]
#[derive(Default, ProtoBuf, Debug, Clone)]
pub struct WorkspaceSettingPB {
#[pb(index = 1)]
pub workspace: WorkspacePB,

View File

@ -3,6 +3,7 @@ use crate::manager::Folder2Manager;
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
use lib_dispatch::prelude::*;
use std::sync::Arc;
use strum_macros::Display;

View File

@ -7,7 +7,9 @@ pub mod protobuf;
mod user_default;
pub mod view_ext;
pub mod deps;
#[cfg(feature = "test_helper")]
mod test_helper;
pub use collab_folder::core::ViewLayout;
pub use user_default::gen_workspace_id;

View File

@ -3,7 +3,7 @@ use std::ops::Deref;
use std::sync::Arc;
use appflowy_integrate::collab_builder::AppFlowyCollabBuilder;
use appflowy_integrate::RocksCollabDB;
use collab_folder::core::{
Folder as InnerFolder, FolderContext, TrashChange, TrashChangeReceiver, TrashInfo, TrashRecord,
View, ViewChange, ViewChangeReceiver, ViewLayout, Workspace,
@ -11,6 +11,7 @@ use collab_folder::core::{
use parking_lot::Mutex;
use tracing::{event, Level};
use crate::deps::{FolderCloudService, FolderUser};
use flowy_error::{FlowyError, FlowyResult};
use lib_infra::util::timestamp;
@ -22,22 +23,17 @@ use crate::notification::{
send_notification, send_workspace_notification, send_workspace_setting_notification,
FolderNotification,
};
use crate::user_default::{gen_workspace_id, DefaultFolderBuilder};
use crate::user_default::DefaultFolderBuilder;
use crate::view_ext::{
gen_view_id, view_from_create_view_params, ViewDataProcessor, ViewDataProcessorMap,
};
pub trait FolderUser: Send + Sync {
fn user_id(&self) -> Result<i64, FlowyError>;
fn token(&self) -> Result<Option<String>, FlowyError>;
fn collab_db(&self) -> Result<Arc<RocksCollabDB>, FlowyError>;
}
pub struct Folder2Manager {
folder: Folder,
collab_builder: Arc<AppFlowyCollabBuilder>,
user: Arc<dyn FolderUser>,
view_processors: ViewDataProcessorMap,
cloud_service: Arc<dyn FolderCloudService>,
}
unsafe impl Send for Folder2Manager {}
@ -48,6 +44,7 @@ impl Folder2Manager {
user: Arc<dyn FolderUser>,
collab_builder: Arc<AppFlowyCollabBuilder>,
view_processors: ViewDataProcessorMap,
cloud_service: Arc<dyn FolderCloudService>,
) -> FlowyResult<Self> {
let folder = Folder::default();
let manager = Self {
@ -55,6 +52,7 @@ impl Folder2Manager {
folder,
collab_builder,
view_processors,
cloud_service,
};
Ok(manager)
@ -90,7 +88,7 @@ impl Folder2Manager {
}
/// Called immediately after the application launched fi the user already sign in/sign up.
#[tracing::instrument(level = "trace", skip(self), err)]
#[tracing::instrument(level = "debug", skip(self), err)]
pub async fn initialize(&self, uid: i64, workspace_id: &str) -> FlowyResult<()> {
if let Ok(collab_db) = self.user.collab_db() {
let collab = self.collab_builder.build(uid, workspace_id, collab_db);
@ -139,13 +137,10 @@ impl Folder2Manager {
pub async fn clear(&self, _user_id: i64) {}
pub async fn create_workspace(&self, params: CreateWorkspaceParams) -> FlowyResult<Workspace> {
let workspace = Workspace {
id: gen_workspace_id(),
name: params.name,
belongings: Default::default(),
created_at: timestamp(),
};
let workspace = self
.cloud_service
.create_workspace(self.user.user_id()?, &params.name)
.await?;
self.with_folder((), |folder| {
folder.workspaces.create_workspace(workspace.clone());
folder.set_current_workspace(&workspace.id);

View File

@ -1,7 +1,7 @@
use crate::script::{invalid_workspace_name_test_case, FolderScript::*, FolderTest};
use collab_folder::core::ViewLayout;
use flowy_folder2::entities::CreateWorkspacePayloadPB;
use flowy_test::{event_builder::*, FlowySDKTest};
use flowy_test::{event_builder::*, FlowyCoreTest};
#[tokio::test]
async fn workspace_read_all() {
@ -63,18 +63,19 @@ async fn workspace_create_with_apps() {
#[tokio::test]
async fn workspace_create_with_invalid_name() {
for (name, code) in invalid_workspace_name_test_case() {
let sdk = FlowySDKTest::default();
let sdk = FlowyCoreTest::new();
let request = CreateWorkspacePayloadPB {
name,
desc: "".to_owned(),
};
assert_eq!(
Folder2EventBuilder::new(sdk)
EventBuilder::new(sdk)
.event(flowy_folder2::event_map::FolderEvent::CreateWorkspace)
.payload(request)
.async_send()
.await
.error()
.unwrap()
.code,
code.value()
)

View File

@ -2,8 +2,8 @@ use collab_folder::core::ViewLayout;
use flowy_error::ErrorCode;
use flowy_folder2::entities::*;
use flowy_folder2::event_map::FolderEvent::*;
use flowy_test::event_builder::Folder2EventBuilder;
use flowy_test::FlowySDKTest;
use flowy_test::event_builder::EventBuilder;
use flowy_test::FlowyCoreTest;
pub enum FolderScript {
// Workspace
@ -51,7 +51,7 @@ pub enum FolderScript {
}
pub struct FolderTest {
pub sdk: FlowySDKTest,
pub sdk: FlowyCoreTest,
pub all_workspace: Vec<WorkspacePB>,
pub workspace: WorkspacePB,
pub parent_view: ViewPB,
@ -61,7 +61,7 @@ pub struct FolderTest {
impl FolderTest {
pub async fn new() -> Self {
let sdk = FlowySDKTest::default();
let sdk = FlowyCoreTest::new();
let _ = sdk.init_user().await;
let workspace = create_workspace(&sdk, "FolderWorkspace", "Folder test workspace").await;
let parent_view = create_app(&sdk, &workspace.id, "Folder App", "Folder test app").await;
@ -170,13 +170,13 @@ pub fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> {
]
}
pub async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> WorkspacePB {
pub async fn create_workspace(sdk: &FlowyCoreTest, name: &str, desc: &str) -> WorkspacePB {
let request = CreateWorkspacePayloadPB {
name: name.to_owned(),
desc: desc.to_owned(),
};
Folder2EventBuilder::new(sdk.clone())
EventBuilder::new(sdk.clone())
.event(CreateWorkspace)
.payload(request)
.async_send()
@ -184,11 +184,11 @@ pub async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> Wor
.parse::<WorkspacePB>()
}
pub async fn read_workspace(sdk: &FlowySDKTest, workspace_id: Option<String>) -> Vec<WorkspacePB> {
pub async fn read_workspace(sdk: &FlowyCoreTest, workspace_id: Option<String>) -> Vec<WorkspacePB> {
let request = WorkspaceIdPB {
value: workspace_id,
};
let repeated_workspace = Folder2EventBuilder::new(sdk.clone())
let repeated_workspace = EventBuilder::new(sdk.clone())
.event(ReadWorkspaces)
.payload(request.clone())
.async_send()
@ -210,7 +210,7 @@ pub async fn read_workspace(sdk: &FlowySDKTest, workspace_id: Option<String>) ->
workspaces
}
pub async fn create_app(sdk: &FlowySDKTest, workspace_id: &str, name: &str, desc: &str) -> ViewPB {
pub async fn create_app(sdk: &FlowyCoreTest, workspace_id: &str, name: &str, desc: &str) -> ViewPB {
let create_view_request = CreateViewPayloadPB {
belong_to_id: workspace_id.to_owned(),
name: name.to_string(),
@ -221,7 +221,7 @@ pub async fn create_app(sdk: &FlowySDKTest, workspace_id: &str, name: &str, desc
ext: Default::default(),
};
Folder2EventBuilder::new(sdk.clone())
EventBuilder::new(sdk.clone())
.event(CreateView)
.payload(create_view_request)
.async_send()
@ -230,7 +230,7 @@ pub async fn create_app(sdk: &FlowySDKTest, workspace_id: &str, name: &str, desc
}
pub async fn create_view(
sdk: &FlowySDKTest,
sdk: &FlowyCoreTest,
app_id: &str,
name: &str,
desc: &str,
@ -245,7 +245,7 @@ pub async fn create_view(
initial_data: vec![],
ext: Default::default(),
};
Folder2EventBuilder::new(sdk.clone())
EventBuilder::new(sdk.clone())
.event(CreateView)
.payload(request)
.async_send()
@ -253,9 +253,9 @@ pub async fn create_view(
.parse::<ViewPB>()
}
pub async fn read_view(sdk: &FlowySDKTest, view_id: &str) -> ViewPB {
pub async fn read_view(sdk: &FlowyCoreTest, view_id: &str) -> ViewPB {
let view_id: ViewIdPB = view_id.into();
Folder2EventBuilder::new(sdk.clone())
EventBuilder::new(sdk.clone())
.event(ReadView)
.payload(view_id)
.async_send()
@ -264,7 +264,7 @@ pub async fn read_view(sdk: &FlowySDKTest, view_id: &str) -> ViewPB {
}
pub async fn update_view(
sdk: &FlowySDKTest,
sdk: &FlowyCoreTest,
view_id: &str,
name: Option<String>,
desc: Option<String>,
@ -275,54 +275,54 @@ pub async fn update_view(
desc,
thumbnail: None,
};
Folder2EventBuilder::new(sdk.clone())
EventBuilder::new(sdk.clone())
.event(UpdateView)
.payload(request)
.async_send()
.await;
}
pub async fn delete_view(sdk: &FlowySDKTest, view_ids: Vec<String>) {
pub async fn delete_view(sdk: &FlowyCoreTest, view_ids: Vec<String>) {
let request = RepeatedViewIdPB { items: view_ids };
Folder2EventBuilder::new(sdk.clone())
EventBuilder::new(sdk.clone())
.event(DeleteView)
.payload(request)
.async_send()
.await;
}
pub async fn read_trash(sdk: &FlowySDKTest) -> RepeatedTrashPB {
Folder2EventBuilder::new(sdk.clone())
pub async fn read_trash(sdk: &FlowyCoreTest) -> RepeatedTrashPB {
EventBuilder::new(sdk.clone())
.event(ReadTrash)
.async_send()
.await
.parse::<RepeatedTrashPB>()
}
pub async fn restore_app_from_trash(sdk: &FlowySDKTest, app_id: &str) {
pub async fn restore_app_from_trash(sdk: &FlowyCoreTest, app_id: &str) {
let id = TrashIdPB {
id: app_id.to_owned(),
};
Folder2EventBuilder::new(sdk.clone())
EventBuilder::new(sdk.clone())
.event(PutbackTrash)
.payload(id)
.async_send()
.await;
}
pub async fn restore_view_from_trash(sdk: &FlowySDKTest, view_id: &str) {
pub async fn restore_view_from_trash(sdk: &FlowyCoreTest, view_id: &str) {
let id = TrashIdPB {
id: view_id.to_owned(),
};
Folder2EventBuilder::new(sdk.clone())
EventBuilder::new(sdk.clone())
.event(PutbackTrash)
.payload(id)
.async_send()
.await;
}
pub async fn delete_all_trash(sdk: &FlowySDKTest) {
Folder2EventBuilder::new(sdk.clone())
pub async fn delete_all_trash(sdk: &FlowyCoreTest) {
EventBuilder::new(sdk.clone())
.event(DeleteAllTrash)
.async_send()
.await;

View File

@ -24,11 +24,12 @@ postgrest = "1.0"
tokio-retry = "0.3"
anyhow = "1.0"
uuid = { version = "1.3.3", features = ["v4"] }
chrono = "0.4.24"
lib-infra = { path = "../../../shared-lib/lib-infra" }
flowy-user = { path = "../flowy-user" }
flowy-folder2 = { path = "../flowy-folder2" }
flowy-error = { path = "../flowy-error" }
flowy-config = { path = "../flowy-config" }
[dev-dependencies]
uuid = { version = "1.3.3", features = ["v4"] }

View File

@ -1,3 +1,4 @@
use flowy_folder2::deps::FolderCloudService;
use std::sync::Arc;
use flowy_user::event_map::UserAuthService;
@ -8,6 +9,21 @@ mod response;
pub mod self_host;
pub mod supabase;
/// 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:
/// - SUPABASE_URL
/// - SUPABASE_ANON_KEY
/// - SUPABASE_KEY
/// - SUPABASE_JWT_SECRET
///
/// the .env file should look like this:
/// SUPABASE_URL=https://<your-supabase-url>.supabase.co
/// SUPABASE_ANON_KEY=<your-supabase-anon-key>
/// SUPABASE_KEY=<your-supabase-key>
/// SUPABASE_JWT_SECRET=<your-supabase-jwt-secret>
///
pub trait AppFlowyServer: Send + Sync + 'static {
fn user_service(&self) -> Arc<dyn UserAuthService>;
fn folder_service(&self) -> Arc<dyn FolderCloudService>;
}

View File

@ -0,0 +1,21 @@
use flowy_error::FlowyError;
use flowy_folder2::deps::{FolderCloudService, Workspace};
use flowy_folder2::gen_workspace_id;
use lib_infra::future::FutureResult;
use lib_infra::util::timestamp;
pub(crate) struct LocalServerFolderCloudServiceImpl();
impl FolderCloudService for LocalServerFolderCloudServiceImpl {
fn create_workspace(&self, _uid: i64, name: &str) -> FutureResult<Workspace, FlowyError> {
let name = name.to_string();
FutureResult::new(async move {
Ok(Workspace {
id: gen_workspace_id(),
name: name.to_string(),
belongings: Default::default(),
created_at: timestamp(),
})
})
}
}

View File

@ -0,0 +1,5 @@
mod folder;
mod user;
pub(crate) use folder::*;
pub(crate) use user::*;

View File

@ -1,5 +1,5 @@
pub use server::*;
pub mod impls;
mod server;
pub(crate) mod uid;
mod user;

View File

@ -1,11 +1,14 @@
use std::sync::Arc;
use flowy_folder2::deps::FolderCloudService;
use parking_lot::RwLock;
use tokio::sync::mpsc;
use flowy_user::event_map::UserAuthService;
use crate::local_server::user::LocalServerUserAuthServiceImpl;
use crate::local_server::impls::{
LocalServerFolderCloudServiceImpl, LocalServerUserAuthServiceImpl,
};
use crate::AppFlowyServer;
#[derive(Default)]
@ -31,4 +34,8 @@ impl AppFlowyServer for LocalServer {
fn user_service(&self) -> Arc<dyn UserAuthService> {
Arc::new(LocalServerUserAuthServiceImpl())
}
fn folder_service(&self) -> Arc<dyn FolderCloudService> {
Arc::new(LocalServerFolderCloudServiceImpl())
}
}

View File

@ -0,0 +1,21 @@
use flowy_error::FlowyError;
use flowy_folder2::deps::{FolderCloudService, Workspace};
use flowy_folder2::gen_workspace_id;
use lib_infra::future::FutureResult;
use lib_infra::util::timestamp;
pub(crate) struct SelfHostedServerFolderCloudServiceImpl();
impl FolderCloudService for SelfHostedServerFolderCloudServiceImpl {
fn create_workspace(&self, _uid: i64, name: &str) -> FutureResult<Workspace, FlowyError> {
let name = name.to_string();
FutureResult::new(async move {
Ok(Workspace {
id: gen_workspace_id(),
name: name.to_string(),
belongings: Default::default(),
created_at: timestamp(),
})
})
}
}

View File

@ -0,0 +1,5 @@
mod folder;
mod user;
pub(crate) use folder::*;
pub(crate) use user::*;

View File

@ -1,6 +1,5 @@
pub use server::*;
pub use user::*;
pub mod configuration;
pub mod impls;
mod server;
mod user;

View File

@ -1,9 +1,12 @@
use flowy_folder2::deps::FolderCloudService;
use std::sync::Arc;
use flowy_user::event_map::UserAuthService;
use crate::self_host::configuration::SelfHostedConfiguration;
use crate::self_host::SelfHostedUserAuthServiceImpl;
use crate::self_host::impls::{
SelfHostedServerFolderCloudServiceImpl, SelfHostedUserAuthServiceImpl,
};
use crate::AppFlowyServer;
pub struct SelfHostServer {
@ -20,4 +23,8 @@ impl AppFlowyServer for SelfHostServer {
fn user_service(&self) -> Arc<dyn UserAuthService> {
Arc::new(SelfHostedUserAuthServiceImpl::new(self.config.clone()))
}
fn folder_service(&self) -> Arc<dyn FolderCloudService> {
Arc::new(SelfHostedServerFolderCloudServiceImpl())
}
}

View File

@ -0,0 +1,54 @@
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;
pub(crate) const WORKSPACE_TABLE: &str = "af_workspace";
pub(crate) const WORKSPACE_NAME_COLUMN: &str = "workspace_name";
pub(crate) struct SupabaseFolderCloudServiceImpl {
postgrest: Arc<Postgrest>,
}
impl FolderCloudService for SupabaseFolderCloudServiceImpl {
fn create_workspace(&self, uid: i64, name: &str) -> FutureResult<Workspace, FlowyError> {
let name = name.to_string();
let postgrest = self.postgrest.clone();
FutureResult::new(async move { create_workspace_with_uid(postgrest, uid, &name).await })
}
}
#[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::sync::Arc;
#[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");
}
}
}

View File

@ -0,0 +1,5 @@
mod folder;
mod user;
pub(crate) use folder::*;
pub(crate) use user::*;

View File

@ -12,7 +12,8 @@ use crate::supabase::request::*;
pub(crate) const USER_TABLE: &str = "af_user";
pub(crate) const USER_PROFILE_TABLE: &str = "af_user_profile";
pub(crate) const USER_WORKSPACE_TABLE: &str = "af_user_workspace_view";
#[allow(dead_code)]
pub(crate) const USER_WORKSPACE_TABLE: &str = "af_workspace";
pub(crate) struct PostgrestUserAuthServiceImpl {
postgrest: Arc<Postgrest>,
}
@ -41,14 +42,12 @@ impl UserAuthService for PostgrestUserAuthServiceImpl {
let postgrest = self.postgrest.clone();
FutureResult::new(async move {
let uuid = uuid_from_box_any(params)?;
match get_user_workspace_with_uuid(postgrest, uuid).await? {
None => Err(FlowyError::user_not_exist()),
Some(user) => Ok(SignInResponse {
user_id: user.uid,
workspace_id: user.workspace_id,
..Default::default()
}),
}
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()
})
})
}
@ -76,18 +75,19 @@ impl UserAuthService for PostgrestUserAuthServiceImpl {
) -> FutureResult<Option<UserProfile>, FlowyError> {
let postgrest = self.postgrest.clone();
FutureResult::new(async move {
let profile = get_user_workspace_with_uid(postgrest, uid)
.await?
.map(|user_workspace| UserProfile {
id: user_workspace.uid,
email: "".to_string(),
name: user_workspace.name,
token: "".to_string(),
icon_url: "".to_string(),
openai_key: "".to_string(),
workspace_id: user_workspace.workspace_id,
});
Ok(profile)
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))
})
}
}
@ -100,8 +100,10 @@ mod tests {
use flowy_user::entities::UpdateUserProfileParams;
use crate::supabase::request::{get_user_profile, get_user_workspace_with_uid};
use crate::supabase::user::{create_user_with_uuid, get_user_id_with_uuid, update_user_profile};
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]
@ -151,17 +153,15 @@ mod tests {
.unwrap();
println!("result: {:?}", result);
let result = get_user_profile(server.postgres.clone(), uid)
let result = get_user_profile(server.postgres.clone(), GetUserProfileParams::Uid(uid))
.await
.unwrap()
.unwrap();
assert_eq!(result.name, "nathan".to_string());
let result = get_user_workspace_with_uid(server.postgres.clone(), uid)
.await
.unwrap()
.unwrap();
assert!(!result.workspace_id.is_empty());
assert!(!result.is_empty());
}
}
}

View File

@ -1,7 +1,7 @@
pub use server::*;
pub mod impls;
mod request;
mod response;
mod retry;
mod server;
pub mod user;

View File

@ -5,13 +5,16 @@ use postgrest::Postgrest;
use serde_json::json;
use flowy_error::{ErrorCode, FlowyError};
use flowy_folder2::deps::Workspace;
use flowy_user::entities::UpdateUserProfileParams;
use lib_infra::box_any::BoxAny;
use crate::supabase::response::{
InsertResponse, PostgrestError, UserProfile, UserProfileList, UserWorkspace, UserWorkspaceList,
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,
};
use crate::supabase::user::{USER_PROFILE_TABLE, USER_TABLE, USER_WORKSPACE_TABLE};
const USER_ID: &str = "uid";
const USER_UUID: &str = "uuid";
@ -19,13 +22,15 @@ const USER_UUID: &str = "uuid";
pub(crate) async fn create_user_with_uuid(
postgrest: Arc<Postgrest>,
uuid: String,
) -> Result<UserWorkspace, FlowyError> {
let insert = format!("{{\"{}\": \"{}\"}}", USER_UUID, &uuid);
) -> Result<UserProfileResponse, FlowyError> {
let mut insert = serde_json::Map::new();
insert.insert(USER_UUID.to_string(), json!(&uuid));
let insert_query = serde_json::to_string(&insert).unwrap();
// Create a new user with uuid.
let resp = postgrest
.from(USER_TABLE)
.insert(insert)
.insert(insert_query)
.execute()
.await
.map_err(|e| FlowyError::new(ErrorCode::HttpError, e))?;
@ -44,13 +49,7 @@ pub(crate) async fn create_user_with_uuid(
.map_err(|e| FlowyError::serde().context(e))?
.first_or_error()?;
match get_user_workspace_with_uid(postgrest, record.uid).await {
Ok(Some(user)) => Ok(user),
_ => Err(FlowyError::new(
ErrorCode::Internal,
"Failed to get user workspace",
)),
}
get_user_profile(postgrest, GetUserProfileParams::Uid(record.uid)).await
} else {
let err = serde_json::from_str::<PostgrestError>(&content)
.map_err(|e| FlowyError::serde().context(e))?;
@ -58,8 +57,8 @@ pub(crate) async fn create_user_with_uuid(
// If there is a unique violation, try to get the user id with uuid. At this point, the user
// should exist.
if err.is_unique_violation() {
match get_user_workspace_with_uuid(postgrest, uuid).await {
Ok(Some(user)) => Ok(user),
match get_user_profile(postgrest, GetUserProfileParams::Uuid(uuid)).await {
Ok(user) => Ok(user),
_ => Err(FlowyError::new(
ErrorCode::Internal,
"Failed to get user workspace",
@ -112,14 +111,21 @@ pub(crate) fn uuid_from_box_any(any: BoxAny) -> Result<String, FlowyError> {
Ok(uuid.to_string())
}
#[allow(dead_code)]
pub enum GetUserProfileParams {
Uid(i64),
Uuid(String),
}
pub(crate) async fn get_user_profile(
postgrest: Arc<Postgrest>,
uid: i64,
) -> Result<Option<UserProfile>, FlowyError> {
let resp = postgrest
.from(USER_PROFILE_TABLE)
.eq(USER_ID, uid.to_string())
params: GetUserProfileParams,
) -> Result<UserProfileResponse, FlowyError> {
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),
}
let resp = builder
.select("*")
.execute()
.await
@ -129,19 +135,35 @@ pub(crate) async fn get_user_profile(
.text()
.await
.map_err(|e| FlowyError::new(ErrorCode::UnexpectedEmpty, e))?;
let resp = serde_json::from_str::<UserProfileList>(&content)
.map_err(|_e| FlowyError::new(ErrorCode::Serde, "Deserialize UserProfileList failed"))?;
Ok(resp.0.first().cloned())
let mut user_profiles =
serde_json::from_str::<UserProfileResponseList>(&content).map_err(|_e| {
FlowyError::new(
ErrorCode::Serde,
"Deserialize UserProfileResponseList failed",
)
})?;
if user_profiles.0.is_empty() {
return Err(FlowyError::new(
ErrorCode::Internal,
"Failed to get user profile",
));
}
Ok(user_profiles.0.remove(0))
}
pub(crate) async fn get_user_workspace_with_uuid(
pub(crate) async fn create_workspace_with_uid(
postgrest: Arc<Postgrest>,
uuid: String,
) -> Result<Option<UserWorkspace>, FlowyError> {
uid: i64,
name: &str,
) -> Result<Workspace, FlowyError> {
let mut insert = serde_json::Map::new();
insert.insert(USER_ID.to_string(), json!(uid));
insert.insert(WORKSPACE_NAME_COLUMN.to_string(), json!(name));
let insert_query = serde_json::to_string(&insert).unwrap();
let resp = postgrest
.from(USER_WORKSPACE_TABLE)
.eq(USER_UUID, uuid)
.select("*")
.from(WORKSPACE_TABLE)
.insert(insert_query)
.execute()
.await
.map_err(|e| FlowyError::new(ErrorCode::HttpError, e))?;
@ -150,15 +172,31 @@ pub(crate) async fn get_user_workspace_with_uuid(
.text()
.await
.map_err(|e| FlowyError::new(ErrorCode::UnexpectedEmpty, e))?;
let resp = serde_json::from_str::<UserWorkspaceList>(&content)
.map_err(|_e| FlowyError::new(ErrorCode::Serde, "Deserialize UserWorkspaceList failed"))?;
Ok(resp.0.first().cloned())
let mut workspace_list = serde_json::from_str::<UserWorkspaceList>(&content)
.map_err(|_e| FlowyError::new(ErrorCode::Serde, "Deserialize UserWorkspaceList failed"))?
.into_inner();
debug_assert!(workspace_list.len() == 1);
if workspace_list.is_empty() {
return Err(FlowyError::new(
ErrorCode::Internal,
"Failed to create workspace",
));
}
let user_workspace = workspace_list.remove(0);
Ok(Workspace {
id: user_workspace.workspace_id,
name: user_workspace.workspace_name,
belongings: Default::default(),
created_at: user_workspace.created_at.timestamp(),
})
}
#[allow(dead_code)]
pub(crate) async fn get_user_workspace_with_uid(
postgrest: Arc<Postgrest>,
uid: i64,
) -> Result<Option<UserWorkspace>, FlowyError> {
) -> Result<Vec<Workspace>, FlowyError> {
let resp = postgrest
.from(USER_WORKSPACE_TABLE)
.eq(USER_ID, uid.to_string())
@ -171,16 +209,27 @@ pub(crate) async fn get_user_workspace_with_uid(
.text()
.await
.map_err(|e| FlowyError::new(ErrorCode::UnexpectedEmpty, e))?;
let resp = serde_json::from_str::<UserWorkspaceList>(&content)
.map_err(|_e| FlowyError::new(ErrorCode::Serde, "Deserialize UserWorkspaceList failed"))?;
Ok(resp.0.first().cloned())
let user_workspaces = serde_json::from_str::<UserWorkspaceList>(&content)
.map_err(|_e| FlowyError::new(ErrorCode::Serde, "Deserialize UserWorkspaceList failed"))?
.0;
Ok(
user_workspaces
.into_iter()
.map(|user_workspace| Workspace {
id: user_workspace.workspace_id,
name: user_workspace.workspace_name,
belongings: Default::default(),
created_at: user_workspace.created_at.timestamp(),
})
.collect(),
)
}
#[allow(dead_code)]
pub(crate) async fn update_user_profile(
postgrest: Arc<Postgrest>,
params: UpdateUserProfileParams,
) -> Result<Option<UserProfile>, FlowyError> {
) -> Result<Option<UserProfileResponse>, FlowyError> {
if params.is_empty() {
return Err(FlowyError::new(
ErrorCode::UnexpectedEmpty,
@ -206,7 +255,7 @@ pub(crate) async fn update_user_profile(
.await
.map_err(|e| FlowyError::new(ErrorCode::UnexpectedEmpty, e))?;
let resp = serde_json::from_str::<UserProfileList>(&content)
let resp = serde_json::from_str::<UserProfileResponseList>(&content)
.map_err(|_e| FlowyError::new(ErrorCode::Serde, "Deserialize UserProfileList failed"))?;
Ok(resp.0.first().cloned())
}

View File

@ -1,3 +1,4 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use thiserror::Error;
@ -56,27 +57,39 @@ pub(crate) struct InsertRecord {
#[allow(dead_code)]
#[derive(Debug, Deserialize, Clone)]
pub(crate) struct UserProfile {
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,
}
#[derive(Debug, Deserialize)]
pub(crate) struct UserProfileList(pub Vec<UserProfile>);
#[derive(Debug, Deserialize, Clone)]
pub(crate) struct UserWorkspace {
pub uid: i64,
#[serde(deserialize_with = "deserialize_null_or_default")]
pub name: String,
pub workspace_id: String,
}
#[derive(Debug, Deserialize)]
pub(crate) struct UserWorkspaceList(pub Vec<UserWorkspace>);
pub(crate) struct UserProfileResponseList(pub Vec<UserProfileResponse>);
#[derive(Debug, Deserialize, Clone)]
pub(crate) struct UserWorkspace {
#[allow(dead_code)]
pub uid: i64,
#[serde(deserialize_with = "deserialize_null_or_default")]
pub workspace_name: String,
pub created_at: DateTime<Utc>,
pub workspace_id: String,
}
#[derive(Debug, Deserialize)]
pub(crate) struct UserWorkspaceList(pub(crate) Vec<UserWorkspace>);
impl UserWorkspaceList {
pub(crate) fn into_inner(self) -> Vec<UserWorkspace> {
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.

View File

@ -1,15 +1,21 @@
use std::sync::Arc;
use postgrest::Postgrest;
use serde::Deserialize;
use flowy_config::entities::{SUPABASE_JWT_SECRET, SUPABASE_KEY, SUPABASE_URL};
use flowy_error::{ErrorCode, FlowyError};
use flowy_folder2::deps::FolderCloudService;
use flowy_user::event_map::UserAuthService;
use crate::supabase::user::PostgrestUserAuthServiceImpl;
use crate::supabase::impls::PostgrestUserAuthServiceImpl;
use crate::AppFlowyServer;
#[derive(Debug)]
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,
@ -20,6 +26,11 @@ pub struct SupabaseConfiguration {
}
impl SupabaseConfiguration {
/// Load the configuration from the environment variables.
/// SUPABASE_URL=https://<your-supabase-url>.supabase.co
/// SUPABASE_KEY=<your-supabase-key>
/// SUPABASE_JWT_SECRET=<your-supabase-jwt-secret>
///
pub fn from_env() -> Result<Self, FlowyError> {
Ok(Self {
url: std::env::var(SUPABASE_URL)
@ -31,6 +42,12 @@ impl SupabaseConfiguration {
})?,
})
}
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);
}
}
pub struct SupabaseServer {
@ -53,4 +70,8 @@ impl AppFlowyServer for SupabaseServer {
fn user_service(&self) -> Arc<dyn UserAuthService> {
Arc::new(PostgrestUserAuthServiceImpl::new(self.postgres.clone()))
}
fn folder_service(&self) -> Arc<dyn FolderCloudService> {
todo!()
}
}

View File

@ -10,7 +10,6 @@ flowy-core = { path = "../flowy-core" }
flowy-user = { path = "../flowy-user"}
flowy-net = { path = "../flowy-net"}
flowy-folder2 = { path = "../flowy-folder2", features = ["test_helper"] }
#flowy-document= { path = "../flowy-document" }
lib-dispatch = { path = "../lib-dispatch" }
lib-ot = { path = "../../../shared-lib/lib-ot" }
lib-infra = { path = "../../../shared-lib/lib-infra" }
@ -19,21 +18,18 @@ flowy-server = { path = "../flowy-server" }
serde = { version = "1.0", features = ["derive"] }
serde_json = {version = "1.0"}
protobuf = {version = "2.28.0"}
#claim = "0.5.0"
tokio = { version = "1.26", features = ["full"]}
futures-util = "0.3.26"
thread-id = "3.3.0"
log = "0.4"
bytes = "1.4"
nanoid = "0.4.0"
tempdir = "0.3.7"
tracing = { version = "0.1.27" }
parking_lot = "0.12.1"
dotenv = "0.15.0"
[dev-dependencies]
quickcheck = "1.0.3"
quickcheck_macros = "0.9.1"
fake = "2.5.0"
futures = "0.3.26"
serial_test = "0.5.1"
uuid = { version = "1.3.3", features = ["v4"] }
[features]
dart = ["flowy-core/dart"]

View File

@ -1,5 +1,5 @@
use crate::FlowySDKTest;
use flowy_user::{entities::UserProfilePB, errors::FlowyError};
use crate::FlowyCoreTest;
use flowy_user::errors::FlowyError;
use lib_dispatch::prelude::{
AFPluginDispatcher, AFPluginEventResponse, AFPluginFromBytes, AFPluginRequest, StatusCode,
ToBytes, *,
@ -8,38 +8,18 @@ use std::{
convert::TryFrom,
fmt::{Debug, Display},
hash::Hash,
marker::PhantomData,
sync::Arc,
};
pub type Folder2EventBuilder = EventBuilder<FlowyError>;
impl Folder2EventBuilder {
pub fn new(sdk: FlowySDKTest) -> Self {
EventBuilder::test(TestContext::new(sdk))
}
pub fn user_profile(&self) -> &Option<UserProfilePB> {
&self.user_profile
}
}
pub type UserModuleEventBuilder = Folder2EventBuilder;
#[derive(Clone)]
pub struct EventBuilder<E> {
pub struct EventBuilder {
context: TestContext,
user_profile: Option<UserProfilePB>,
err_phantom: PhantomData<E>,
}
impl<E> EventBuilder<E>
where
E: AFPluginFromBytes + Debug,
{
fn test(context: TestContext) -> Self {
impl EventBuilder {
pub fn new(sdk: FlowyCoreTest) -> Self {
Self {
context,
user_profile: None,
err_phantom: PhantomData,
context: TestContext::new(sdk),
}
}
@ -53,7 +33,7 @@ where
self.context.request = Some(module_request.payload(bytes))
},
Err(e) => {
log::error!("Set payload failed: {:?}", e);
tracing::error!("Set payload failed: {:?}", e);
},
}
self
@ -86,7 +66,7 @@ where
R: AFPluginFromBytes,
{
let response = self.get_response();
match response.clone().parse::<R, E>() {
match response.clone().parse::<R, FlowyError>() {
Ok(Ok(data)) => data,
Ok(Err(e)) => {
panic!(
@ -105,22 +85,12 @@ where
}
}
pub fn error(self) -> E {
pub fn error(self) -> Option<FlowyError> {
let response = self.get_response();
assert_eq!(response.status_code, StatusCode::Err);
<AFPluginData<E>>::try_from(response.payload)
.unwrap()
.into_inner()
}
pub fn assert_error(self) -> Self {
// self.context.assert_error();
self
}
pub fn assert_success(self) -> Self {
// self.context.assert_success();
self
<AFPluginData<FlowyError>>::try_from(response.payload)
.ok()
.map(|data| data.into_inner())
}
fn dispatch(&self) -> Arc<AFPluginDispatcher> {
@ -132,7 +102,7 @@ where
.context
.response
.as_ref()
.expect("must call sync_send first")
.expect("must call sync_send/async_send first")
.clone()
}
@ -143,13 +113,13 @@ where
#[derive(Clone)]
pub struct TestContext {
pub sdk: FlowySDKTest,
pub sdk: FlowyCoreTest,
request: Option<AFPluginRequest>,
response: Option<AFPluginEventResponse>,
}
impl TestContext {
pub fn new(sdk: FlowySDKTest) -> Self {
pub fn new(sdk: FlowyCoreTest) -> Self {
Self {
sdk,
request: None,

View File

@ -0,0 +1,111 @@
use crate::event_builder::EventBuilder;
use crate::FlowyCoreTest;
use flowy_folder2::entities::*;
use flowy_folder2::event_map::FolderEvent::*;
pub struct ViewTest {
pub sdk: FlowyCoreTest,
pub workspace: WorkspacePB,
pub parent_view: ViewPB,
pub child_view: ViewPB,
}
impl ViewTest {
#[allow(dead_code)]
pub async fn new(sdk: &FlowyCoreTest, layout: ViewLayoutPB, data: Vec<u8>) -> Self {
let workspace = create_workspace(sdk, "Workspace", "").await;
open_workspace(sdk, &workspace.id).await;
let app = create_app(sdk, "App", "AppFlowy GitHub Project", &workspace.id).await;
let view = create_view(sdk, &app.id, layout, data).await;
Self {
sdk: sdk.clone(),
workspace,
parent_view: app,
child_view: view,
}
}
pub async fn new_grid_view(sdk: &FlowyCoreTest, data: Vec<u8>) -> Self {
Self::new(sdk, ViewLayoutPB::Grid, data).await
}
pub async fn new_board_view(sdk: &FlowyCoreTest, data: Vec<u8>) -> Self {
Self::new(sdk, ViewLayoutPB::Board, data).await
}
pub async fn new_calendar_view(sdk: &FlowyCoreTest, data: Vec<u8>) -> Self {
Self::new(sdk, ViewLayoutPB::Calendar, data).await
}
pub async fn new_document_view(sdk: &FlowyCoreTest) -> Self {
Self::new(sdk, ViewLayoutPB::Document, vec![]).await
}
}
async fn create_workspace(sdk: &FlowyCoreTest, name: &str, desc: &str) -> WorkspacePB {
let request = CreateWorkspacePayloadPB {
name: name.to_owned(),
desc: desc.to_owned(),
};
EventBuilder::new(sdk.clone())
.event(CreateWorkspace)
.payload(request)
.async_send()
.await
.parse::<WorkspacePB>()
}
async fn open_workspace(sdk: &FlowyCoreTest, workspace_id: &str) {
let payload = WorkspaceIdPB {
value: Some(workspace_id.to_owned()),
};
let _ = EventBuilder::new(sdk.clone())
.event(OpenWorkspace)
.payload(payload)
.async_send()
.await;
}
async fn create_app(sdk: &FlowyCoreTest, name: &str, desc: &str, workspace_id: &str) -> ViewPB {
let create_app_request = CreateViewPayloadPB {
belong_to_id: workspace_id.to_owned(),
name: name.to_string(),
desc: desc.to_string(),
thumbnail: None,
layout: ViewLayoutPB::Document,
initial_data: vec![],
ext: Default::default(),
};
EventBuilder::new(sdk.clone())
.event(CreateView)
.payload(create_app_request)
.async_send()
.await
.parse::<ViewPB>()
}
async fn create_view(
sdk: &FlowyCoreTest,
app_id: &str,
layout: ViewLayoutPB,
data: Vec<u8>,
) -> ViewPB {
let payload = CreateViewPayloadPB {
belong_to_id: app_id.to_string(),
name: "View A".to_string(),
desc: "".to_string(),
thumbnail: Some("http://1.png".to_string()),
layout,
initial_data: data,
ext: Default::default(),
};
EventBuilder::new(sdk.clone())
.event(CreateView)
.payload(payload)
.async_send()
.await
.parse::<ViewPB>()
}

View File

@ -1,216 +0,0 @@
use std::sync::Arc;
use flowy_folder2::entities::{
CreateViewPayloadPB, CreateWorkspacePayloadPB, ViewLayoutPB, ViewPB, WorkspaceIdPB, WorkspacePB,
};
use flowy_folder2::event_map::FolderEvent::{CreateView, CreateWorkspace, OpenWorkspace};
use flowy_user::entities::AuthTypePB;
use flowy_user::{
entities::{SignInPayloadPB, SignUpPayloadPB, UserProfilePB},
errors::FlowyError,
event_map::UserEvent::{InitUser, SignIn, SignOut, SignUp},
};
use lib_dispatch::prelude::{AFPluginDispatcher, AFPluginRequest, ToBytes};
use crate::prelude::*;
pub struct ViewTest {
pub sdk: FlowySDKTest,
pub workspace: WorkspacePB,
pub parent_view: ViewPB,
pub child_view: ViewPB,
}
impl ViewTest {
#[allow(dead_code)]
pub async fn new(sdk: &FlowySDKTest, layout: ViewLayoutPB, data: Vec<u8>) -> Self {
let workspace = create_workspace(sdk, "Workspace", "").await;
open_workspace(sdk, &workspace.id).await;
let app = create_app(sdk, "App", "AppFlowy GitHub Project", &workspace.id).await;
let view = create_view(sdk, &app.id, layout, data).await;
Self {
sdk: sdk.clone(),
workspace,
parent_view: app,
child_view: view,
}
}
pub async fn new_grid_view(sdk: &FlowySDKTest, data: Vec<u8>) -> Self {
Self::new(sdk, ViewLayoutPB::Grid, data).await
}
pub async fn new_board_view(sdk: &FlowySDKTest, data: Vec<u8>) -> Self {
Self::new(sdk, ViewLayoutPB::Board, data).await
}
pub async fn new_calendar_view(sdk: &FlowySDKTest, data: Vec<u8>) -> Self {
Self::new(sdk, ViewLayoutPB::Calendar, data).await
}
pub async fn new_document_view(sdk: &FlowySDKTest) -> Self {
Self::new(sdk, ViewLayoutPB::Document, vec![]).await
}
}
async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> WorkspacePB {
let request = CreateWorkspacePayloadPB {
name: name.to_owned(),
desc: desc.to_owned(),
};
Folder2EventBuilder::new(sdk.clone())
.event(CreateWorkspace)
.payload(request)
.async_send()
.await
.parse::<WorkspacePB>()
}
async fn open_workspace(sdk: &FlowySDKTest, workspace_id: &str) {
let payload = WorkspaceIdPB {
value: Some(workspace_id.to_owned()),
};
let _ = Folder2EventBuilder::new(sdk.clone())
.event(OpenWorkspace)
.payload(payload)
.async_send()
.await;
}
async fn create_app(sdk: &FlowySDKTest, name: &str, desc: &str, workspace_id: &str) -> ViewPB {
let create_app_request = CreateViewPayloadPB {
belong_to_id: workspace_id.to_owned(),
name: name.to_string(),
desc: desc.to_string(),
thumbnail: None,
layout: ViewLayoutPB::Document,
initial_data: vec![],
ext: Default::default(),
};
Folder2EventBuilder::new(sdk.clone())
.event(CreateView)
.payload(create_app_request)
.async_send()
.await
.parse::<ViewPB>()
}
async fn create_view(
sdk: &FlowySDKTest,
app_id: &str,
layout: ViewLayoutPB,
data: Vec<u8>,
) -> ViewPB {
let payload = CreateViewPayloadPB {
belong_to_id: app_id.to_string(),
name: "View A".to_string(),
desc: "".to_string(),
thumbnail: Some("http://1.png".to_string()),
layout,
initial_data: data,
ext: Default::default(),
};
Folder2EventBuilder::new(sdk.clone())
.event(CreateView)
.payload(payload)
.async_send()
.await
.parse::<ViewPB>()
}
pub fn random_email() -> String {
format!("{}@appflowy.io", nanoid!(20))
}
pub fn login_email() -> String {
"annie2@appflowy.io".to_string()
}
pub fn login_password() -> String {
"HelloWorld!123".to_string()
}
pub struct SignUpContext {
pub user_profile: UserProfilePB,
pub password: String,
}
pub fn sign_up(dispatch: Arc<AFPluginDispatcher>) -> SignUpContext {
let password = login_password();
let payload = SignUpPayloadPB {
email: random_email(),
name: "app flowy".to_string(),
password: password.clone(),
auth_type: AuthTypePB::Local,
}
.into_bytes()
.unwrap();
let request = AFPluginRequest::new(SignUp).payload(payload);
let user_profile = AFPluginDispatcher::sync_send(dispatch, request)
.parse::<UserProfilePB, FlowyError>()
.unwrap()
.unwrap();
SignUpContext {
user_profile,
password,
}
}
pub async fn async_sign_up(dispatch: Arc<AFPluginDispatcher>) -> SignUpContext {
let password = login_password();
let email = random_email();
let payload = SignUpPayloadPB {
email,
name: "app flowy".to_string(),
password: password.clone(),
auth_type: AuthTypePB::Local,
}
.into_bytes()
.unwrap();
let request = AFPluginRequest::new(SignUp).payload(payload);
let user_profile = AFPluginDispatcher::async_send(dispatch.clone(), request)
.await
.parse::<UserProfilePB, FlowyError>()
.unwrap()
.unwrap();
// let _ = create_default_workspace_if_need(dispatch.clone(), &user_profile.id);
SignUpContext {
user_profile,
password,
}
}
pub async fn init_user_setting(dispatch: Arc<AFPluginDispatcher>) {
let request = AFPluginRequest::new(InitUser);
let _ = AFPluginDispatcher::async_send(dispatch.clone(), request).await;
}
#[allow(dead_code)]
fn sign_in(dispatch: Arc<AFPluginDispatcher>) -> UserProfilePB {
let payload = SignInPayloadPB {
email: login_email(),
password: login_password(),
name: "rust".to_owned(),
auth_type: AuthTypePB::Local,
}
.into_bytes()
.unwrap();
let request = AFPluginRequest::new(SignIn).payload(payload);
AFPluginDispatcher::sync_send(dispatch, request)
.parse::<UserProfilePB, FlowyError>()
.unwrap()
.unwrap()
}
#[allow(dead_code)]
fn logout(dispatch: Arc<AFPluginDispatcher>) {
let _ = AFPluginDispatcher::sync_send(dispatch, AFPluginRequest::new(SignOut));
}

View File

@ -1,57 +1,62 @@
use nanoid::nanoid;
use parking_lot::RwLock;
use std::env::temp_dir;
use std::sync::Arc;
use flowy_core::{AppFlowyCore, AppFlowyCoreConfig};
use flowy_user::entities::UserProfilePB;
use crate::helper::*;
use flowy_user::entities::{AuthTypePB, UserProfilePB};
use crate::user_event::{async_sign_up, init_user_setting, SignUpContext};
pub mod event_builder;
pub mod helper;
pub mod prelude {
pub use lib_dispatch::prelude::*;
pub use crate::{event_builder::*, helper::*, *};
}
pub mod folder_event;
pub mod user_event;
#[derive(Clone)]
pub struct FlowySDKTest {
pub inner: AppFlowyCore,
pub struct FlowyCoreTest {
auth_type: Arc<RwLock<AuthTypePB>>,
inner: AppFlowyCore,
}
impl std::ops::Deref for FlowySDKTest {
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 inner = std::thread::spawn(|| AppFlowyCore::new(config))
.join()
.unwrap();
let auth_type = Arc::new(RwLock::new(AuthTypePB::Local));
std::mem::forget(inner.dispatcher());
Self { inner, auth_type }
}
}
impl FlowyCoreTest {
pub fn new() -> Self {
Self::default()
}
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 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
}
}
impl std::ops::Deref for FlowyCoreTest {
type Target = AppFlowyCore;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::default::Default for FlowySDKTest {
fn default() -> Self {
Self::new()
}
}
impl FlowySDKTest {
pub fn new() -> Self {
let config =
AppFlowyCoreConfig::new(temp_dir().to_str().unwrap(), nanoid!(6)).log_filter("info", vec![]);
let sdk = std::thread::spawn(|| AppFlowyCore::new(config))
.join()
.unwrap();
std::mem::forget(sdk.dispatcher());
Self { inner: sdk }
}
pub async fn sign_up(&self) -> SignUpContext {
async_sign_up(self.inner.dispatcher()).await
}
pub async fn init_user(&self) -> UserProfilePB {
let context = async_sign_up(self.inner.dispatcher()).await;
init_user_setting(self.inner.dispatcher()).await;
context.user_profile
}
}

View File

@ -0,0 +1,103 @@
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))
}
pub fn login_email() -> String {
"annie2@appflowy.io".to_string()
}
pub fn login_password() -> String {
"HelloWorld!123".to_string()
}
pub struct SignUpContext {
pub user_profile: UserProfilePB,
pub password: String,
}
pub fn sign_up(dispatch: Arc<AFPluginDispatcher>) -> SignUpContext {
let password = login_password();
let payload = SignUpPayloadPB {
email: random_email(),
name: "app flowy".to_string(),
password: password.clone(),
auth_type: AuthTypePB::Local,
}
.into_bytes()
.unwrap();
let request = AFPluginRequest::new(SignUp).payload(payload);
let user_profile = AFPluginDispatcher::sync_send(dispatch, request)
.parse::<UserProfilePB, FlowyError>()
.unwrap()
.unwrap();
SignUpContext {
user_profile,
password,
}
}
pub async fn async_sign_up(
dispatch: Arc<AFPluginDispatcher>,
auth_type: AuthTypePB,
) -> SignUpContext {
let password = login_password();
let email = random_email();
let payload = SignUpPayloadPB {
email,
name: "app flowy".to_string(),
password: password.clone(),
auth_type,
}
.into_bytes()
.unwrap();
let request = AFPluginRequest::new(SignUp).payload(payload);
let user_profile = AFPluginDispatcher::async_send(dispatch.clone(), request)
.await
.parse::<UserProfilePB, FlowyError>()
.unwrap()
.unwrap();
// let _ = create_default_workspace_if_need(dispatch.clone(), &user_profile.id);
SignUpContext {
user_profile,
password,
}
}
pub async fn init_user_setting(dispatch: Arc<AFPluginDispatcher>) {
let request = AFPluginRequest::new(InitUser);
let _ = AFPluginDispatcher::async_send(dispatch.clone(), request).await;
}
#[allow(dead_code)]
fn sign_in(dispatch: Arc<AFPluginDispatcher>) -> UserProfilePB {
let payload = SignInPayloadPB {
email: login_email(),
password: login_password(),
name: "rust".to_owned(),
auth_type: AuthTypePB::Local,
}
.into_bytes()
.unwrap();
let request = AFPluginRequest::new(SignIn).payload(payload);
AFPluginDispatcher::sync_send(dispatch, request)
.parse::<UserProfilePB, FlowyError>()
.unwrap()
.unwrap()
}
#[allow(dead_code)]
fn logout(dispatch: Arc<AFPluginDispatcher>) {
let _ = AFPluginDispatcher::sync_send(dispatch, AFPluginRequest::new(SignOut));
}

View File

@ -0,0 +1 @@
mod user;

View File

@ -1,13 +1,15 @@
use flowy_test::{event_builder::UserModuleEventBuilder, FlowySDKTest};
use flowy_test::user_event::*;
use flowy_test::{event_builder::EventBuilder, FlowyCoreTest};
use flowy_user::entities::{AuthTypePB, SignInPayloadPB, SignUpPayloadPB, UserProfilePB};
use flowy_user::{errors::ErrorCode, event_map::UserEvent::*};
use flowy_user::errors::ErrorCode;
use flowy_user::event_map::UserEvent::*;
use crate::helper::*;
use crate::user::local_test::helper::*;
#[tokio::test]
async fn sign_up_with_invalid_email() {
for email in invalid_email_test_case() {
let sdk = FlowySDKTest::default();
let sdk = FlowyCoreTest::new();
let request = SignUpPayloadPB {
email: email.to_string(),
name: valid_name(),
@ -16,43 +18,45 @@ async fn sign_up_with_invalid_email() {
};
assert_eq!(
UserModuleEventBuilder::new(sdk)
EventBuilder::new(sdk)
.event(SignUp)
.payload(request)
.async_send()
.await
.error()
.unwrap()
.code,
ErrorCode::EmailFormatInvalid.value()
);
}
}
#[tokio::test]
async fn sign_up_with_invalid_password() {
for password in invalid_password_test_case() {
let sdk = FlowySDKTest::default();
let request = SignUpPayloadPB {
email: random_email(),
name: valid_name(),
password,
auth_type: AuthTypePB::Local,
};
async fn sign_up_with_long_password() {
let sdk = FlowyCoreTest::new();
let request = SignUpPayloadPB {
email: random_email(),
name: valid_name(),
password: "1234".repeat(100).as_str().to_string(),
auth_type: AuthTypePB::Local,
};
UserModuleEventBuilder::new(sdk)
assert_eq!(
EventBuilder::new(sdk)
.event(SignUp)
.payload(request)
.async_send()
.await
.assert_error();
}
.error()
.unwrap()
.code,
ErrorCode::PasswordTooLong.value()
);
}
#[tokio::test]
async fn sign_in_success() {
let test = FlowySDKTest::default();
let _ = UserModuleEventBuilder::new(test.clone())
.event(SignOut)
.sync_send();
let test = FlowyCoreTest::new();
let _ = EventBuilder::new(test.clone()).event(SignOut).sync_send();
let sign_up_context = test.sign_up().await;
let request = SignInPayloadPB {
@ -62,7 +66,7 @@ async fn sign_in_success() {
auth_type: AuthTypePB::Local,
};
let response = UserModuleEventBuilder::new(test.clone())
let response = EventBuilder::new(test.clone())
.event(SignIn)
.payload(request)
.async_send()
@ -74,7 +78,7 @@ async fn sign_in_success() {
#[tokio::test]
async fn sign_in_with_invalid_email() {
for email in invalid_email_test_case() {
let sdk = FlowySDKTest::default();
let sdk = FlowyCoreTest::new();
let request = SignInPayloadPB {
email: email.to_string(),
password: login_password(),
@ -83,12 +87,13 @@ async fn sign_in_with_invalid_email() {
};
assert_eq!(
UserModuleEventBuilder::new(sdk)
EventBuilder::new(sdk)
.event(SignIn)
.payload(request)
.async_send()
.await
.error()
.unwrap()
.code,
ErrorCode::EmailFormatInvalid.value()
);
@ -98,7 +103,7 @@ async fn sign_in_with_invalid_email() {
#[tokio::test]
async fn sign_in_with_invalid_password() {
for password in invalid_password_test_case() {
let sdk = FlowySDKTest::default();
let sdk = FlowyCoreTest::new();
let request = SignInPayloadPB {
email: random_email(),
@ -107,11 +112,12 @@ async fn sign_in_with_invalid_password() {
auth_type: AuthTypePB::Local,
};
UserModuleEventBuilder::new(sdk)
assert!(EventBuilder::new(sdk)
.event(SignIn)
.payload(request)
.async_send()
.await
.assert_error();
.error()
.is_some())
}
}

View File

@ -1,8 +1,3 @@
pub use flowy_test::{
event_builder::*,
prelude::{login_password, random_email},
};
pub(crate) fn invalid_email_test_case() -> Vec<String> {
// https://gist.github.com/cjaoude/fd9910626629b53c4d25
vec![

View File

@ -1,5 +1,5 @@
use crate::helper::*;
use flowy_test::{event_builder::UserModuleEventBuilder, FlowySDKTest};
use crate::user::local_test::helper::*;
use flowy_test::{event_builder::EventBuilder, FlowyCoreTest};
use flowy_user::entities::{UpdateUserProfilePayloadPB, UserProfilePB};
use flowy_user::{errors::ErrorCode, event_map::UserEvent::*};
use nanoid::nanoid;
@ -8,20 +8,20 @@ use nanoid::nanoid;
#[tokio::test]
async fn user_profile_get_failed() {
let sdk = FlowySDKTest::default();
let result = UserModuleEventBuilder::new(sdk)
let sdk = FlowyCoreTest::new();
let result = EventBuilder::new(sdk)
.event(GetUserProfile)
.assert_error()
.async_send()
.await;
assert!(result.user_profile().is_none())
.await
.error();
assert!(result.is_some())
}
#[tokio::test]
async fn user_profile_get() {
let test = FlowySDKTest::default();
let test = FlowyCoreTest::new();
let user_profile = test.init_user().await;
let user = UserModuleEventBuilder::new(test.clone())
let user = EventBuilder::new(test.clone())
.event(GetUserProfile)
.sync_send()
.parse::<UserProfilePB>();
@ -30,18 +30,17 @@ async fn user_profile_get() {
#[tokio::test]
async fn user_update_with_name() {
let sdk = FlowySDKTest::default();
let sdk = FlowyCoreTest::new();
let user = sdk.init_user().await;
let new_name = "hello_world".to_owned();
let request = UpdateUserProfilePayloadPB::new(user.id).name(&new_name);
let _ = UserModuleEventBuilder::new(sdk.clone())
let _ = EventBuilder::new(sdk.clone())
.event(UpdateUserProfile)
.payload(request)
.sync_send();
let user_profile = UserModuleEventBuilder::new(sdk.clone())
let user_profile = EventBuilder::new(sdk.clone())
.event(GetUserProfile)
.assert_error()
.sync_send()
.parse::<UserProfilePB>();
@ -50,49 +49,35 @@ async fn user_update_with_name() {
#[tokio::test]
async fn user_update_with_email() {
let sdk = FlowySDKTest::default();
let sdk = FlowyCoreTest::new();
let user = sdk.init_user().await;
let new_email = format!("{}@gmail.com", nanoid!(6));
let request = UpdateUserProfilePayloadPB::new(user.id).email(&new_email);
let _ = UserModuleEventBuilder::new(sdk.clone())
let _ = EventBuilder::new(sdk.clone())
.event(UpdateUserProfile)
.payload(request)
.sync_send();
let user_profile = UserModuleEventBuilder::new(sdk.clone())
let user_profile = EventBuilder::new(sdk.clone())
.event(GetUserProfile)
.assert_error()
.sync_send()
.parse::<UserProfilePB>();
assert_eq!(user_profile.email, new_email,);
}
#[tokio::test]
async fn user_update_with_password() {
let sdk = FlowySDKTest::default();
let user = sdk.init_user().await;
let new_password = "H123world!".to_owned();
let request = UpdateUserProfilePayloadPB::new(user.id).password(&new_password);
let _ = UserModuleEventBuilder::new(sdk.clone())
.event(UpdateUserProfile)
.payload(request)
.sync_send()
.assert_success();
}
#[tokio::test]
async fn user_update_with_invalid_email() {
let test = FlowySDKTest::default();
let test = FlowyCoreTest::new();
let user = test.init_user().await;
for email in invalid_email_test_case() {
let request = UpdateUserProfilePayloadPB::new(user.id).email(&email);
assert_eq!(
UserModuleEventBuilder::new(test.clone())
EventBuilder::new(test.clone())
.event(UpdateUserProfile)
.payload(request)
.sync_send()
.error()
.unwrap()
.code,
ErrorCode::EmailFormatInvalid.value()
);
@ -101,27 +86,30 @@ async fn user_update_with_invalid_email() {
#[tokio::test]
async fn user_update_with_invalid_password() {
let test = FlowySDKTest::default();
let test = FlowyCoreTest::new();
let user = test.init_user().await;
for password in invalid_password_test_case() {
let request = UpdateUserProfilePayloadPB::new(user.id).password(&password);
UserModuleEventBuilder::new(test.clone())
assert!(EventBuilder::new(test.clone())
.event(UpdateUserProfile)
.payload(request)
.sync_send()
.assert_error();
.async_send()
.await
.error()
.is_some());
}
}
#[tokio::test]
async fn user_update_with_invalid_name() {
let test = FlowySDKTest::default();
let test = FlowyCoreTest::new();
let user = test.init_user().await;
let request = UpdateUserProfilePayloadPB::new(user.id).name("");
UserModuleEventBuilder::new(test.clone())
assert!(EventBuilder::new(test.clone())
.event(UpdateUserProfile)
.payload(request)
.sync_send()
.assert_error();
.error()
.is_some())
}

View File

@ -0,0 +1,2 @@
mod local_test;
mod supabase_test;

View File

@ -0,0 +1,28 @@
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;
#[tokio::test]
async fn sign_up_test() {
if get_supabase_config().is_some() {
let test = FlowyCoreTest::new();
let mut map = HashMap::new();
map.insert("uuid".to_string(), uuid::Uuid::new_v4().to_string());
let payload = ThirdPartyAuthPB {
map,
auth_type: AuthTypePB::Supabase,
};
let response = EventBuilder::new(test.clone())
.event(ThirdPartyAuth)
.payload(payload)
.async_send()
.await
.parse::<UserProfilePB>();
dbg!(&response);
}
}

View File

@ -0,0 +1,20 @@
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://<your-supabase-url>.supabase.co
/// SUPABASE_ANON_KEY=<your-supabase-anon-key>
/// SUPABASE_KEY=<your-supabase-key>
/// SUPABASE_JWT_SECRET=<your-supabase-jwt-secret>
///
pub fn get_supabase_config() -> Option<SupabaseConfiguration> {
dotenv().ok()?;
SupabaseConfiguration::from_env().ok()
}

View File

@ -0,0 +1,3 @@
mod auth_test;
mod helper;
mod workspace_test;

View File

@ -0,0 +1,38 @@
use crate::user::supabase_test::helper::get_supabase_config;
use flowy_folder2::entities::WorkspaceSettingPB;
use flowy_folder2::event_map::FolderEvent::ReadCurrentWorkspace;
use flowy_test::{event_builder::EventBuilder, FlowyCoreTest};
use flowy_user::entities::{AuthTypePB, ThirdPartyAuthPB, UserProfilePB};
use flowy_user::event_map::UserEvent::*;
use std::collections::HashMap;
#[tokio::test]
async fn initial_workspace_test() {
if get_supabase_config().is_some() {
let test = FlowyCoreTest::new();
let mut map = HashMap::new();
map.insert("uuid".to_string(), uuid::Uuid::new_v4().to_string());
let payload = ThirdPartyAuthPB {
map,
auth_type: AuthTypePB::Supabase,
};
let _ = EventBuilder::new(test.clone())
.event(ThirdPartyAuth)
.payload(payload)
.async_send()
.await
.parse::<UserProfilePB>();
let workspace_settings = EventBuilder::new(test.clone())
.event(ReadCurrentWorkspace)
.async_send()
.await
.parse::<WorkspaceSettingPB>();
assert!(workspace_settings.latest_view.is_some());
dbg!(&workspace_settings);
}
}

View File

@ -34,7 +34,6 @@ unicode-segmentation = "1.10"
fancy-regex = "0.11.0"
[dev-dependencies]
flowy-test = { path = "../flowy-test" }
nanoid = "0.4.0"
fake = "2.0.0"
rand = "0.8.4"

View File

@ -16,6 +16,7 @@ pub async fn sign_in(
) -> DataResult<UserProfilePB, FlowyError> {
let params: SignInParams = data.into_inner().try_into()?;
let auth_type = params.auth_type.clone();
let user_profile: UserProfilePB = session
.sign_in(&auth_type, BoxAny::new(params))
.await?

View File

@ -40,6 +40,7 @@ pub trait UserStatusCallback: Send + Sync + 'static {
/// The user cloud service provider.
/// The provider can be supabase, firebase, aws, or any other cloud service.
pub trait UserCloudServiceProvider: Send + Sync + 'static {
fn set_auth_type(&self, auth_type: AuthType);
fn get_auth_service(&self, auth_type: &AuthType) -> Result<Arc<dyn UserAuthService>, FlowyError>;
}
@ -47,6 +48,10 @@ impl<T> UserCloudServiceProvider for Arc<T>
where
T: UserCloudServiceProvider,
{
fn set_auth_type(&self, auth_type: AuthType) {
(**self).set_auth_type(auth_type)
}
fn get_auth_service(&self, auth_type: &AuthType) -> Result<Arc<dyn UserAuthService>, FlowyError> {
(**self).get_auth_service(auth_type)
}

View File

@ -104,6 +104,7 @@ impl UserSession {
auth_type: &AuthType,
params: BoxAny,
) -> Result<UserProfile, FlowyError> {
self.cloud_services.set_auth_type(auth_type.clone());
let resp = self
.cloud_services
.get_auth_service(auth_type)?
@ -134,6 +135,7 @@ impl UserSession {
auth_type: &AuthType,
params: BoxAny,
) -> Result<UserProfile, FlowyError> {
self.cloud_services.set_auth_type(auth_type.clone());
let resp = self
.cloud_services
.get_auth_service(auth_type)?
@ -163,6 +165,7 @@ impl UserSession {
.execute(&*(self.db_connection()?))?;
self.database.close_user_db(session.user_id)?;
self.set_session(None)?;
let server = self.cloud_services.get_auth_service(auth_type)?;
let token = session.token;
let _ = tokio::spawn(async move {