mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-24 14:47:13 -04:00
Merge pull request #7797 from AppFlowy-IO/integrate_workspace_template
Integrate workspace template
This commit is contained in:
commit
7f74543125
81 changed files with 522 additions and 2494 deletions
|
@ -145,7 +145,7 @@ class _MobileHomePageState extends State<MobileHomePage> {
|
||||||
|
|
||||||
void _onLatestViewChange() async {
|
void _onLatestViewChange() async {
|
||||||
final id = getIt<MenuSharedState>().latestOpenView?.id;
|
final id = getIt<MenuSharedState>().latestOpenView?.id;
|
||||||
if (id == null) {
|
if (id == null || id.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await FolderEventSetLatestView(ViewIdPB(value: id)).send();
|
await FolderEventSetLatestView(ViewIdPB(value: id)).send();
|
||||||
|
|
|
@ -27,6 +27,7 @@ class ChatMemberBloc extends Bloc<ChatMemberEvent, ChatMemberState> {
|
||||||
final payload = WorkspaceMemberIdPB(
|
final payload = WorkspaceMemberIdPB(
|
||||||
uid: Int64.parseInt(userId),
|
uid: Int64.parseInt(userId),
|
||||||
);
|
);
|
||||||
|
|
||||||
await UserEventGetMemberInfo(payload).send().then((result) {
|
await UserEventGetMemberInfo(payload).send().then((result) {
|
||||||
result.fold(
|
result.fold(
|
||||||
(member) {
|
(member) {
|
||||||
|
|
|
@ -36,7 +36,7 @@ class BlankPagePlugin extends Plugin {
|
||||||
PluginWidgetBuilder get widgetBuilder => BlankPagePluginWidgetBuilder();
|
PluginWidgetBuilder get widgetBuilder => BlankPagePluginWidgetBuilder();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PluginId get id => "BlankStack";
|
PluginId get id => "";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PluginType get pluginType => PluginType.blank;
|
PluginType get pluginType => PluginType.blank;
|
||||||
|
|
|
@ -127,7 +127,7 @@ class UserBackendService implements IUserBackendService {
|
||||||
) {
|
) {
|
||||||
final payload = OpenUserWorkspacePB()
|
final payload = OpenUserWorkspacePB()
|
||||||
..workspaceId = workspaceId
|
..workspaceId = workspaceId
|
||||||
..authType = authType;
|
..workspaceAuthType = authType;
|
||||||
return UserEventOpenWorkspace(payload).send();
|
return UserEventOpenWorkspace(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,13 +244,6 @@ class UserBackendService implements IUserBackendService {
|
||||||
return UserEventGetWorkspaceSubscriptionInfo(params).send();
|
return UserEventGetWorkspaceSubscriptionInfo(params).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<FlowyResult<WorkspaceMemberPB, FlowyError>>
|
|
||||||
getWorkspaceMember() async {
|
|
||||||
final data = WorkspaceMemberIdPB.create()..uid = userId;
|
|
||||||
|
|
||||||
return UserEventGetMemberInfo(data).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
|
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
|
||||||
String workspaceId,
|
String workspaceId,
|
||||||
|
|
|
@ -30,6 +30,10 @@ class LocalAiPluginBloc extends Bloc<LocalAiPluginEvent, LocalAiPluginState> {
|
||||||
LocalAiPluginEvent event,
|
LocalAiPluginEvent event,
|
||||||
Emitter<LocalAiPluginState> emit,
|
Emitter<LocalAiPluginState> emit,
|
||||||
) async {
|
) async {
|
||||||
|
if (isClosed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await event.when(
|
await event.when(
|
||||||
didReceiveAiState: (aiState) {
|
didReceiveAiState: (aiState) {
|
||||||
emit(
|
emit(
|
||||||
|
@ -54,7 +58,9 @@ class LocalAiPluginBloc extends Bloc<LocalAiPluginEvent, LocalAiPluginState> {
|
||||||
emit(LocalAiPluginState.loading());
|
emit(LocalAiPluginState.loading());
|
||||||
await AIEventToggleLocalAI().send().fold(
|
await AIEventToggleLocalAI().send().fold(
|
||||||
(aiState) {
|
(aiState) {
|
||||||
add(LocalAiPluginEvent.didReceiveAiState(aiState));
|
if (!isClosed) {
|
||||||
|
add(LocalAiPluginEvent.didReceiveAiState(aiState));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Log.error,
|
Log.error,
|
||||||
);
|
);
|
||||||
|
@ -69,10 +75,14 @@ class LocalAiPluginBloc extends Bloc<LocalAiPluginEvent, LocalAiPluginState> {
|
||||||
void _startListening() {
|
void _startListening() {
|
||||||
listener.start(
|
listener.start(
|
||||||
stateCallback: (pluginState) {
|
stateCallback: (pluginState) {
|
||||||
add(LocalAiPluginEvent.didReceiveAiState(pluginState));
|
if (!isClosed) {
|
||||||
|
add(LocalAiPluginEvent.didReceiveAiState(pluginState));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resourceCallback: (data) {
|
resourceCallback: (data) {
|
||||||
add(LocalAiPluginEvent.didReceiveLackOfResources(data));
|
if (!isClosed) {
|
||||||
|
add(LocalAiPluginEvent.didReceiveLackOfResources(data));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -80,7 +90,9 @@ class LocalAiPluginBloc extends Bloc<LocalAiPluginEvent, LocalAiPluginState> {
|
||||||
void _getLocalAiState() {
|
void _getLocalAiState() {
|
||||||
AIEventGetLocalAIState().send().fold(
|
AIEventGetLocalAIState().send().fold(
|
||||||
(aiState) {
|
(aiState) {
|
||||||
add(LocalAiPluginEvent.didReceiveAiState(aiState));
|
if (!isClosed) {
|
||||||
|
add(LocalAiPluginEvent.didReceiveAiState(aiState));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Log.error,
|
Log.error,
|
||||||
);
|
);
|
||||||
|
|
|
@ -76,12 +76,6 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||||
|
|
||||||
final (spaces, publicViews, privateViews) = await _getSpaces();
|
final (spaces, publicViews, privateViews) = await _getSpaces();
|
||||||
|
|
||||||
final shouldShowUpgradeDialog = await this.shouldShowUpgradeDialog(
|
|
||||||
spaces: spaces,
|
|
||||||
publicViews: publicViews,
|
|
||||||
privateViews: privateViews,
|
|
||||||
);
|
|
||||||
|
|
||||||
final currentSpace = await _getLastOpenedSpace(spaces);
|
final currentSpace = await _getLastOpenedSpace(spaces);
|
||||||
final isExpanded = await _getSpaceExpandStatus(currentSpace);
|
final isExpanded = await _getSpaceExpandStatus(currentSpace);
|
||||||
emit(
|
emit(
|
||||||
|
@ -89,17 +83,11 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||||
spaces: spaces,
|
spaces: spaces,
|
||||||
currentSpace: currentSpace,
|
currentSpace: currentSpace,
|
||||||
isExpanded: isExpanded,
|
isExpanded: isExpanded,
|
||||||
shouldShowUpgradeDialog: shouldShowUpgradeDialog,
|
shouldShowUpgradeDialog: false,
|
||||||
isInitialized: true,
|
isInitialized: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldShowUpgradeDialog && !integrationMode().isTest) {
|
|
||||||
if (!isClosed) {
|
|
||||||
add(const SpaceEvent.migrate());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openFirstPage) {
|
if (openFirstPage) {
|
||||||
if (currentSpace != null) {
|
if (currentSpace != null) {
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
|
|
|
@ -404,7 +404,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (update.updateChildViews.isNotEmpty) {
|
if (update.updateChildViews.isNotEmpty && update.parentViewId.isNotEmpty) {
|
||||||
final view = await ViewBackendService.getView(update.parentViewId);
|
final view = await ViewBackendService.getView(update.parentViewId);
|
||||||
final childViews = view.fold((l) => l.childViews, (r) => []);
|
final childViews = view.fold((l) => l.childViews, (r) => []);
|
||||||
bool isSameOrder = true;
|
bool isSameOrder = true;
|
||||||
|
|
|
@ -111,6 +111,12 @@ class ViewBackendService {
|
||||||
static Future<FlowyResult<List<ViewPB>, FlowyError>> getChildViews({
|
static Future<FlowyResult<List<ViewPB>, FlowyError>> getChildViews({
|
||||||
required String viewId,
|
required String viewId,
|
||||||
}) {
|
}) {
|
||||||
|
if (viewId.isEmpty) {
|
||||||
|
return Future.value(
|
||||||
|
FlowyResult<List<ViewPB>, FlowyError>.success(<ViewPB>[]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final payload = ViewIdPB.create()..value = viewId;
|
final payload = ViewIdPB.create()..value = viewId;
|
||||||
|
|
||||||
return FolderEventGetView(payload).send().then((result) {
|
return FolderEventGetView(payload).send().then((result) {
|
||||||
|
@ -262,6 +268,9 @@ class ViewBackendService {
|
||||||
static Future<FlowyResult<ViewPB, FlowyError>> getView(
|
static Future<FlowyResult<ViewPB, FlowyError>> getView(
|
||||||
String viewId,
|
String viewId,
|
||||||
) async {
|
) async {
|
||||||
|
if (viewId.isEmpty) {
|
||||||
|
Log.error('ViewId is empty');
|
||||||
|
}
|
||||||
final payload = ViewIdPB.create()..value = viewId;
|
final payload = ViewIdPB.create()..value = viewId;
|
||||||
return FolderEventGetView(payload).send();
|
return FolderEventGetView(payload).send();
|
||||||
}
|
}
|
||||||
|
|
|
@ -631,7 +631,7 @@ class PageNotifier extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the plugin view as the latest view.
|
// Set the plugin view as the latest view.
|
||||||
if (setLatest) {
|
if (setLatest && newPlugin.id.isNotEmpty) {
|
||||||
FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send();
|
FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,6 @@ class _LocalAISettingState extends State<LocalAISetting> {
|
||||||
),
|
),
|
||||||
header: LocalAiSettingHeader(
|
header: LocalAiSettingHeader(
|
||||||
isEnabled: state.isEnabled,
|
isEnabled: state.isEnabled,
|
||||||
isToggleable: state is ReadyLocalAiPluginState,
|
|
||||||
),
|
),
|
||||||
collapsed: const SizedBox.shrink(),
|
collapsed: const SizedBox.shrink(),
|
||||||
expanded: Padding(
|
expanded: Padding(
|
||||||
|
@ -65,11 +64,9 @@ class LocalAiSettingHeader extends StatelessWidget {
|
||||||
const LocalAiSettingHeader({
|
const LocalAiSettingHeader({
|
||||||
super.key,
|
super.key,
|
||||||
required this.isEnabled,
|
required this.isEnabled,
|
||||||
required this.isToggleable,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool isEnabled;
|
final bool isEnabled;
|
||||||
final bool isToggleable;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -91,22 +88,20 @@ class LocalAiSettingHeader extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IgnorePointer(
|
Toggle(
|
||||||
ignoring: !isToggleable,
|
value: isEnabled,
|
||||||
child: Opacity(
|
onChanged: (value) {
|
||||||
opacity: isToggleable ? 1 : 0.5,
|
_onToggleChanged(value, context);
|
||||||
child: Toggle(
|
},
|
||||||
value: isEnabled,
|
|
||||||
onChanged: (_) => _onToggleChanged(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onToggleChanged(BuildContext context) {
|
void _onToggleChanged(bool value, BuildContext context) {
|
||||||
if (isEnabled) {
|
if (value) {
|
||||||
|
context.read<LocalAiPluginBloc>().add(const LocalAiPluginEvent.toggle());
|
||||||
|
} else {
|
||||||
showConfirmDialog(
|
showConfirmDialog(
|
||||||
context: context,
|
context: context,
|
||||||
title: LocaleKeys.settings_aiPage_keys_disableLocalAITitle.tr(),
|
title: LocaleKeys.settings_aiPage_keys_disableLocalAITitle.tr(),
|
||||||
|
@ -119,8 +114,6 @@ class LocalAiSettingHeader extends StatelessWidget {
|
||||||
.add(const LocalAiPluginEvent.toggle());
|
.add(const LocalAiPluginEvent.toggle());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
context.read<LocalAiPluginBloc>().add(const LocalAiPluginEvent.toggle());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -283,7 +283,7 @@ impl EventIntegrationTest {
|
||||||
pub async fn open_workspace(&self, workspace_id: &str, auth_type: AuthTypePB) {
|
pub async fn open_workspace(&self, workspace_id: &str, auth_type: AuthTypePB) {
|
||||||
let payload = OpenUserWorkspacePB {
|
let payload = OpenUserWorkspacePB {
|
||||||
workspace_id: workspace_id.to_string(),
|
workspace_id: workspace_id.to_string(),
|
||||||
auth_type,
|
workspace_auth_type: auth_type,
|
||||||
};
|
};
|
||||||
EventBuilder::new(self.clone())
|
EventBuilder::new(self.clone())
|
||||||
.event(UserEvent::OpenWorkspace)
|
.event(UserEvent::OpenWorkspace)
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use assert_json_diff::assert_json_eq;
|
|
||||||
use collab::core::collab::MutexCollab;
|
|
||||||
use collab::core::origin::CollabOrigin;
|
|
||||||
use collab::preclude::updates::decoder::Decode;
|
|
||||||
use collab::preclude::{Collab, JsonValue, Update};
|
|
||||||
use collab_entity::CollabType;
|
|
||||||
|
|
||||||
use event_integration_test::event_builder::EventBuilder;
|
|
||||||
use flowy_database2::entities::{DatabasePB, DatabaseViewIdPB, RepeatedDatabaseSnapshotPB};
|
|
||||||
use flowy_database2::event_map::DatabaseEvent::*;
|
|
||||||
use flowy_folder::entities::ViewPB;
|
|
||||||
|
|
||||||
use crate::util::FlowySupabaseTest;
|
|
||||||
|
|
||||||
pub struct FlowySupabaseDatabaseTest {
|
|
||||||
pub uuid: String,
|
|
||||||
inner: FlowySupabaseTest,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FlowySupabaseDatabaseTest {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn new_with_user(uuid: String) -> Option<Self> {
|
|
||||||
let inner = FlowySupabaseTest::new().await?;
|
|
||||||
inner.supabase_sign_up_with_uuid(&uuid, None).await.unwrap();
|
|
||||||
Some(Self { uuid, inner })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn new_with_new_user() -> Option<Self> {
|
|
||||||
let inner = FlowySupabaseTest::new().await?;
|
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
|
||||||
let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await.unwrap();
|
|
||||||
Some(Self { uuid, inner })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_database(&self) -> (ViewPB, DatabasePB) {
|
|
||||||
let current_workspace = self.inner.get_current_workspace().await;
|
|
||||||
let view = self
|
|
||||||
.inner
|
|
||||||
.create_grid(¤t_workspace.id, "my database".to_string(), vec![])
|
|
||||||
.await;
|
|
||||||
let database = self.inner.get_database(&view.id).await;
|
|
||||||
(view, database)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_collab_json(&self, database_id: &str) -> JsonValue {
|
|
||||||
let database_editor = self
|
|
||||||
.database_manager
|
|
||||||
.get_database(database_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
// let address = Arc::into_raw(database_editor.clone());
|
|
||||||
let database = database_editor.get_mutex_database().lock();
|
|
||||||
database.get_mutex_collab().to_json_value()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_database_snapshots(&self, view_id: &str) -> RepeatedDatabaseSnapshotPB {
|
|
||||||
EventBuilder::new(self.inner.deref().clone())
|
|
||||||
.event(GetDatabaseSnapshots)
|
|
||||||
.payload(DatabaseViewIdPB {
|
|
||||||
value: view_id.to_string(),
|
|
||||||
})
|
|
||||||
.async_send()
|
|
||||||
.await
|
|
||||||
.parse::<RepeatedDatabaseSnapshotPB>()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_database_collab_update(&self, database_id: &str) -> Vec<u8> {
|
|
||||||
let workspace_id = self.user_manager.workspace_id().unwrap();
|
|
||||||
let cloud_service = self.database_manager.get_cloud_service().clone();
|
|
||||||
cloud_service
|
|
||||||
.get_database_object_doc_state(database_id, CollabType::Database, &workspace_id)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_database_collab_content(
|
|
||||||
database_id: &str,
|
|
||||||
collab_update: &[u8],
|
|
||||||
expected: JsonValue,
|
|
||||||
) {
|
|
||||||
let collab = MutexCollab::new(Collab::new_with_origin(
|
|
||||||
CollabOrigin::Server,
|
|
||||||
database_id,
|
|
||||||
vec![],
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
collab.lock().with_origin_transact_mut(|txn| {
|
|
||||||
let update = Update::decode_v1(collab_update).unwrap();
|
|
||||||
txn.apply_update(update).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
let json = collab.to_json_value();
|
|
||||||
assert_json_eq!(json, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for FlowySupabaseDatabaseTest {
|
|
||||||
type Target = FlowySupabaseTest;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
mod helper;
|
|
||||||
mod test;
|
|
|
@ -1,108 +0,0 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use flowy_database2::entities::{
|
|
||||||
DatabaseSnapshotStatePB, DatabaseSyncState, DatabaseSyncStatePB, FieldChangesetPB, FieldType,
|
|
||||||
};
|
|
||||||
use flowy_database2::notification::DatabaseNotification::DidUpdateDatabaseSnapshotState;
|
|
||||||
|
|
||||||
use crate::database::supabase_test::helper::{
|
|
||||||
assert_database_collab_content, FlowySupabaseDatabaseTest,
|
|
||||||
};
|
|
||||||
use crate::util::receive_with_timeout;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_initial_database_snapshot_test() {
|
|
||||||
if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await {
|
|
||||||
let (view, database) = test.create_database().await;
|
|
||||||
let rx = test
|
|
||||||
.notification_sender
|
|
||||||
.subscribe::<DatabaseSnapshotStatePB>(&database.id, DidUpdateDatabaseSnapshotState);
|
|
||||||
|
|
||||||
receive_with_timeout(rx, Duration::from_secs(30))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let expected = test.get_collab_json(&database.id).await;
|
|
||||||
let snapshots = test.get_database_snapshots(&view.id).await;
|
|
||||||
assert_eq!(snapshots.items.len(), 1);
|
|
||||||
assert_database_collab_content(&database.id, &snapshots.items[0].data, expected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_edit_database_test() {
|
|
||||||
if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await {
|
|
||||||
let (view, database) = test.create_database().await;
|
|
||||||
let existing_fields = test.get_all_database_fields(&view.id).await;
|
|
||||||
for field in existing_fields.items {
|
|
||||||
if !field.is_primary {
|
|
||||||
test.delete_field(&view.id, &field.id).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let field = test.create_field(&view.id, FieldType::Checklist).await;
|
|
||||||
test
|
|
||||||
.update_field(FieldChangesetPB {
|
|
||||||
field_id: field.id.clone(),
|
|
||||||
view_id: view.id.clone(),
|
|
||||||
name: Some("hello world".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// wait all updates are send to the remote
|
|
||||||
let rx = test
|
|
||||||
.notification_sender
|
|
||||||
.subscribe_with_condition::<DatabaseSyncStatePB, _>(&database.id, |pb| {
|
|
||||||
pb.value == DatabaseSyncState::SyncFinished
|
|
||||||
});
|
|
||||||
receive_with_timeout(rx, Duration::from_secs(30))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(test.get_all_database_fields(&view.id).await.items.len(), 2);
|
|
||||||
let expected = test.get_collab_json(&database.id).await;
|
|
||||||
let update = test.get_database_collab_update(&database.id).await;
|
|
||||||
assert_database_collab_content(&database.id, &update, expected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[tokio::test]
|
|
||||||
// async fn cloud_test_supabase_login_sync_database_test() {
|
|
||||||
// if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await {
|
|
||||||
// let uuid = test.uuid.clone();
|
|
||||||
// let (view, database) = test.create_database().await;
|
|
||||||
// // wait all updates are send to the remote
|
|
||||||
// let mut rx = test
|
|
||||||
// .notification_sender
|
|
||||||
// .subscribe_with_condition::<DatabaseSyncStatePB, _>(&database.id, |pb| pb.is_finish);
|
|
||||||
// receive_with_timeout(&mut rx, Duration::from_secs(30))
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
// let expected = test.get_collab_json(&database.id).await;
|
|
||||||
// test.sign_out().await;
|
|
||||||
// // Drop the test will cause the test resources to be dropped, which will
|
|
||||||
// // delete the user data folder.
|
|
||||||
// drop(test);
|
|
||||||
//
|
|
||||||
// let new_test = FlowySupabaseDatabaseTest::new_with_user(uuid)
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
// // let actual = new_test.get_collab_json(&database.id).await;
|
|
||||||
// // assert_json_eq!(actual, json!(""));
|
|
||||||
//
|
|
||||||
// new_test.open_database(&view.id).await;
|
|
||||||
//
|
|
||||||
// // wait all updates are synced from the remote
|
|
||||||
// let mut rx = new_test
|
|
||||||
// .notification_sender
|
|
||||||
// .subscribe_with_condition::<DatabaseSyncStatePB, _>(&database.id, |pb| pb.is_finish);
|
|
||||||
// receive_with_timeout(&mut rx, Duration::from_secs(30))
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
// // when the new sync is finished, the database should be the same as the old one
|
|
||||||
// let actual = new_test.get_collab_json(&database.id).await;
|
|
||||||
// assert_json_eq!(actual, expected);
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -1,65 +0,0 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use event_integration_test::document_event::assert_document_data_equal;
|
|
||||||
use flowy_document::entities::{DocumentSyncState, DocumentSyncStatePB};
|
|
||||||
|
|
||||||
use crate::document::supabase_test::helper::FlowySupabaseDocumentTest;
|
|
||||||
use crate::util::receive_with_timeout;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_document_edit_sync_test() {
|
|
||||||
if let Some(test) = FlowySupabaseDocumentTest::new().await {
|
|
||||||
let view = test.create_document().await;
|
|
||||||
let document_id = view.id.clone();
|
|
||||||
|
|
||||||
let cloned_test = test.clone();
|
|
||||||
let cloned_document_id = document_id.clone();
|
|
||||||
test.appflowy_core.dispatcher().spawn(async move {
|
|
||||||
cloned_test
|
|
||||||
.insert_document_text(&cloned_document_id, "hello world", 0)
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
|
|
||||||
// wait all update are send to the remote
|
|
||||||
let rx = test
|
|
||||||
.notification_sender
|
|
||||||
.subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| {
|
|
||||||
pb.value != DocumentSyncState::Syncing
|
|
||||||
});
|
|
||||||
receive_with_timeout(rx, Duration::from_secs(30))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let document_data = test.get_document_data(&document_id).await;
|
|
||||||
let update = test.get_document_doc_state(&document_id).await;
|
|
||||||
assert_document_data_equal(&update, &document_id, document_data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_document_edit_sync_test2() {
|
|
||||||
if let Some(test) = FlowySupabaseDocumentTest::new().await {
|
|
||||||
let view = test.create_document().await;
|
|
||||||
let document_id = view.id.clone();
|
|
||||||
|
|
||||||
for i in 0..10 {
|
|
||||||
test
|
|
||||||
.insert_document_text(&document_id, "hello world", i)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait all update are send to the remote
|
|
||||||
let rx = test
|
|
||||||
.notification_sender
|
|
||||||
.subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| {
|
|
||||||
pb.value != DocumentSyncState::Syncing
|
|
||||||
});
|
|
||||||
receive_with_timeout(rx, Duration::from_secs(30))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let document_data = test.get_document_data(&document_id).await;
|
|
||||||
let update = test.get_document_doc_state(&document_id).await;
|
|
||||||
assert_document_data_equal(&update, &document_id, document_data);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
// use std::fs::File;
|
|
||||||
// use std::io::{Cursor, Read};
|
|
||||||
// use std::path::Path;
|
|
||||||
//
|
|
||||||
// use uuid::Uuid;
|
|
||||||
// use zip::ZipArchive;
|
|
||||||
//
|
|
||||||
// use flowy_storage::StorageObject;
|
|
||||||
//
|
|
||||||
// use crate::document::supabase_test::helper::FlowySupabaseDocumentTest;
|
|
||||||
//
|
|
||||||
// #[tokio::test]
|
|
||||||
// async fn supabase_document_upload_text_file_test() {
|
|
||||||
// if let Some(test) = FlowySupabaseDocumentTest::new().await {
|
|
||||||
// let workspace_id = test.get_current_workspace().await.id;
|
|
||||||
// let storage_service = test
|
|
||||||
// .document_manager
|
|
||||||
// .get_file_storage_service()
|
|
||||||
// .upgrade()
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
// let object = StorageObject::from_bytes(
|
|
||||||
// &workspace_id,
|
|
||||||
// &Uuid::new_v4().to_string(),
|
|
||||||
// "hello world".as_bytes(),
|
|
||||||
// "text/plain".to_string(),
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// let url = storage_service.create_object(object).await.unwrap();
|
|
||||||
//
|
|
||||||
// let bytes = storage_service
|
|
||||||
// .get_object(url.clone())
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
// let s = String::from_utf8(bytes.to_vec()).unwrap();
|
|
||||||
// assert_eq!(s, "hello world");
|
|
||||||
//
|
|
||||||
// // Delete the text file
|
|
||||||
// let _ = storage_service.delete_object(url).await;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// #[tokio::test]
|
|
||||||
// async fn supabase_document_upload_zip_file_test() {
|
|
||||||
// if let Some(test) = FlowySupabaseDocumentTest::new().await {
|
|
||||||
// let workspace_id = test.get_current_workspace().await.id;
|
|
||||||
// let storage_service = test
|
|
||||||
// .document_manager
|
|
||||||
// .get_file_storage_service()
|
|
||||||
// .upgrade()
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
// // Upload zip file
|
|
||||||
// let object = StorageObject::from_file(
|
|
||||||
// &workspace_id,
|
|
||||||
// &Uuid::new_v4().to_string(),
|
|
||||||
// "./tests/asset/test.txt.zip",
|
|
||||||
// );
|
|
||||||
// let url = storage_service.create_object(object).await.unwrap();
|
|
||||||
//
|
|
||||||
// // Read zip file
|
|
||||||
// let zip_data = storage_service
|
|
||||||
// .get_object(url.clone())
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
// let reader = Cursor::new(zip_data);
|
|
||||||
// let mut archive = ZipArchive::new(reader).unwrap();
|
|
||||||
// for i in 0..archive.len() {
|
|
||||||
// let mut file = archive.by_index(i).unwrap();
|
|
||||||
// let name = file.name().to_string();
|
|
||||||
// let mut out = Vec::new();
|
|
||||||
// file.read_to_end(&mut out).unwrap();
|
|
||||||
//
|
|
||||||
// if name.starts_with("__MACOSX/") {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
// assert_eq!(name, "test.txt");
|
|
||||||
// assert_eq!(String::from_utf8(out).unwrap(), "hello world");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Delete the zip file
|
|
||||||
// let _ = storage_service.delete_object(url).await;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// #[tokio::test]
|
|
||||||
// async fn supabase_document_upload_image_test() {
|
|
||||||
// if let Some(test) = FlowySupabaseDocumentTest::new().await {
|
|
||||||
// let workspace_id = test.get_current_workspace().await.id;
|
|
||||||
// let storage_service = test
|
|
||||||
// .document_manager
|
|
||||||
// .get_file_storage_service()
|
|
||||||
// .upgrade()
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
// // Upload zip file
|
|
||||||
// let object = StorageObject::from_file(
|
|
||||||
// &workspace_id,
|
|
||||||
// &Uuid::new_v4().to_string(),
|
|
||||||
// "./tests/asset/logo.png",
|
|
||||||
// );
|
|
||||||
// let url = storage_service.create_object(object).await.unwrap();
|
|
||||||
//
|
|
||||||
// let image_data = storage_service
|
|
||||||
// .get_object(url.clone())
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
// // Read the image file
|
|
||||||
// let mut file = File::open(Path::new("./tests/asset/logo.png")).unwrap();
|
|
||||||
// let mut local_data = Vec::new();
|
|
||||||
// file.read_to_end(&mut local_data).unwrap();
|
|
||||||
//
|
|
||||||
// assert_eq!(image_data, local_data);
|
|
||||||
//
|
|
||||||
// // Delete the image
|
|
||||||
// let _ = storage_service.delete_object(url).await;
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -1,49 +0,0 @@
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use event_integration_test::event_builder::EventBuilder;
|
|
||||||
use flowy_document::entities::{OpenDocumentPayloadPB, RepeatedDocumentSnapshotMetaPB};
|
|
||||||
use flowy_document::event_map::DocumentEvent::GetDocumentSnapshotMeta;
|
|
||||||
use flowy_folder::entities::ViewPB;
|
|
||||||
|
|
||||||
use crate::util::FlowySupabaseTest;
|
|
||||||
|
|
||||||
pub struct FlowySupabaseDocumentTest {
|
|
||||||
inner: FlowySupabaseTest,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FlowySupabaseDocumentTest {
|
|
||||||
pub async fn new() -> Option<Self> {
|
|
||||||
let inner = FlowySupabaseTest::new().await?;
|
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
|
||||||
let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await;
|
|
||||||
Some(Self { inner })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create_document(&self) -> ViewPB {
|
|
||||||
let current_workspace = self.inner.get_current_workspace().await;
|
|
||||||
self
|
|
||||||
.inner
|
|
||||||
.create_and_open_document(¤t_workspace.id, "my document".to_string(), vec![])
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn get_document_snapshots(&self, view_id: &str) -> RepeatedDocumentSnapshotMetaPB {
|
|
||||||
EventBuilder::new(self.inner.deref().clone())
|
|
||||||
.event(GetDocumentSnapshotMeta)
|
|
||||||
.payload(OpenDocumentPayloadPB {
|
|
||||||
document_id: view_id.to_string(),
|
|
||||||
})
|
|
||||||
.async_send()
|
|
||||||
.await
|
|
||||||
.parse::<RepeatedDocumentSnapshotMetaPB>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for FlowySupabaseDocumentTest {
|
|
||||||
type Target = FlowySupabaseTest;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
mod edit_test;
|
|
||||||
mod file_test;
|
|
||||||
mod helper;
|
|
|
@ -1,91 +0,0 @@
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use assert_json_diff::assert_json_eq;
|
|
||||||
use collab::core::collab::MutexCollab;
|
|
||||||
use collab::core::origin::CollabOrigin;
|
|
||||||
use collab::preclude::updates::decoder::Decode;
|
|
||||||
use collab::preclude::{Collab, JsonValue, Update};
|
|
||||||
use collab_entity::CollabType;
|
|
||||||
use collab_folder::FolderData;
|
|
||||||
|
|
||||||
use event_integration_test::event_builder::EventBuilder;
|
|
||||||
use flowy_folder::entities::{FolderSnapshotPB, RepeatedFolderSnapshotPB, WorkspaceIdPB};
|
|
||||||
use flowy_folder::event_map::FolderEvent::GetFolderSnapshots;
|
|
||||||
|
|
||||||
use crate::util::FlowySupabaseTest;
|
|
||||||
|
|
||||||
pub struct FlowySupabaseFolderTest {
|
|
||||||
inner: FlowySupabaseTest,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FlowySupabaseFolderTest {
|
|
||||||
pub async fn new() -> Option<Self> {
|
|
||||||
let inner = FlowySupabaseTest::new().await?;
|
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
|
||||||
let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await;
|
|
||||||
Some(Self { inner })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_collab_json(&self) -> JsonValue {
|
|
||||||
let folder = self.folder_manager.get_mutex_folder().lock();
|
|
||||||
folder.as_ref().unwrap().to_json_value()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_local_folder_data(&self) -> FolderData {
|
|
||||||
let folder = self.folder_manager.get_mutex_folder().lock();
|
|
||||||
folder.as_ref().unwrap().get_folder_data().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_folder_snapshots(&self, workspace_id: &str) -> Vec<FolderSnapshotPB> {
|
|
||||||
EventBuilder::new(self.inner.deref().clone())
|
|
||||||
.event(GetFolderSnapshots)
|
|
||||||
.payload(WorkspaceIdPB {
|
|
||||||
value: workspace_id.to_string(),
|
|
||||||
})
|
|
||||||
.async_send()
|
|
||||||
.await
|
|
||||||
.parse::<RepeatedFolderSnapshotPB>()
|
|
||||||
.items
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_collab_update(&self, workspace_id: &str) -> Vec<u8> {
|
|
||||||
let cloud_service = self.folder_manager.get_cloud_service().clone();
|
|
||||||
cloud_service
|
|
||||||
.get_folder_doc_state(
|
|
||||||
workspace_id,
|
|
||||||
self.user_manager.user_id().unwrap(),
|
|
||||||
CollabType::Folder,
|
|
||||||
workspace_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_folder_collab_content(workspace_id: &str, collab_update: &[u8], expected: JsonValue) {
|
|
||||||
if collab_update.is_empty() {
|
|
||||||
panic!("collab update is empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
let collab = MutexCollab::new(Collab::new_with_origin(
|
|
||||||
CollabOrigin::Server,
|
|
||||||
workspace_id,
|
|
||||||
vec![],
|
|
||||||
false,
|
|
||||||
));
|
|
||||||
collab.lock().with_origin_transact_mut(|txn| {
|
|
||||||
let update = Update::decode_v1(collab_update).unwrap();
|
|
||||||
txn.apply_update(update).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
let json = collab.to_json_value();
|
|
||||||
assert_json_eq!(json["folder"], expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for FlowySupabaseFolderTest {
|
|
||||||
type Target = FlowySupabaseTest;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
mod helper;
|
|
||||||
mod test;
|
|
|
@ -1,122 +0,0 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use assert_json_diff::assert_json_eq;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use flowy_folder::entities::{FolderSnapshotStatePB, FolderSyncStatePB};
|
|
||||||
use flowy_folder::notification::FolderNotification::DidUpdateFolderSnapshotState;
|
|
||||||
|
|
||||||
use crate::folder::supabase_test::helper::{assert_folder_collab_content, FlowySupabaseFolderTest};
|
|
||||||
use crate::util::{get_folder_data_from_server, receive_with_timeout};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_encrypt_folder_test() {
|
|
||||||
if let Some(test) = FlowySupabaseFolderTest::new().await {
|
|
||||||
let uid = test.user_manager.user_id().unwrap();
|
|
||||||
let secret = test.enable_encryption().await;
|
|
||||||
|
|
||||||
let local_folder_data = test.get_local_folder_data().await;
|
|
||||||
let workspace_id = test.get_current_workspace().await.id;
|
|
||||||
let remote_folder_data = get_folder_data_from_server(&uid, &workspace_id, Some(secret))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_json_eq!(json!(local_folder_data), json!(remote_folder_data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_decrypt_folder_data_test() {
|
|
||||||
if let Some(test) = FlowySupabaseFolderTest::new().await {
|
|
||||||
let uid = test.user_manager.user_id().unwrap();
|
|
||||||
let secret = Some(test.enable_encryption().await);
|
|
||||||
let workspace_id = test.get_current_workspace().await.id;
|
|
||||||
test
|
|
||||||
.create_view(&workspace_id, "encrypt view".to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let rx = test
|
|
||||||
.notification_sender
|
|
||||||
.subscribe_with_condition::<FolderSyncStatePB, _>(&workspace_id, |pb| pb.is_finish);
|
|
||||||
|
|
||||||
receive_with_timeout(rx, Duration::from_secs(10))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let folder_data = get_folder_data_from_server(&uid, &workspace_id, secret)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(folder_data.views.len(), 2);
|
|
||||||
assert_eq!(folder_data.views[1].name, "encrypt view");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[should_panic]
|
|
||||||
async fn supabase_decrypt_with_invalid_secret_folder_data_test() {
|
|
||||||
if let Some(test) = FlowySupabaseFolderTest::new().await {
|
|
||||||
let uid = test.user_manager.user_id().unwrap();
|
|
||||||
let _ = Some(test.enable_encryption().await);
|
|
||||||
let workspace_id = test.get_current_workspace().await.id;
|
|
||||||
test
|
|
||||||
.create_view(&workspace_id, "encrypt view".to_string())
|
|
||||||
.await;
|
|
||||||
let rx = test
|
|
||||||
.notification_sender
|
|
||||||
.subscribe_with_condition::<FolderSyncStatePB, _>(&workspace_id, |pb| pb.is_finish);
|
|
||||||
receive_with_timeout(rx, Duration::from_secs(10))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let _ = get_folder_data_from_server(&uid, &workspace_id, Some("invalid secret".to_string()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_folder_snapshot_test() {
|
|
||||||
if let Some(test) = FlowySupabaseFolderTest::new().await {
|
|
||||||
let workspace_id = test.get_current_workspace().await.id;
|
|
||||||
let rx = test
|
|
||||||
.notification_sender
|
|
||||||
.subscribe::<FolderSnapshotStatePB>(&workspace_id, DidUpdateFolderSnapshotState);
|
|
||||||
receive_with_timeout(rx, Duration::from_secs(10))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let expected = test.get_collab_json().await;
|
|
||||||
let snapshots = test.get_folder_snapshots(&workspace_id).await;
|
|
||||||
assert_eq!(snapshots.len(), 1);
|
|
||||||
assert_folder_collab_content(&workspace_id, &snapshots[0].data, expected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_initial_folder_snapshot_test2() {
|
|
||||||
if let Some(test) = FlowySupabaseFolderTest::new().await {
|
|
||||||
let workspace_id = test.get_current_workspace().await.id;
|
|
||||||
|
|
||||||
test
|
|
||||||
.create_view(&workspace_id, "supabase test view1".to_string())
|
|
||||||
.await;
|
|
||||||
test
|
|
||||||
.create_view(&workspace_id, "supabase test view2".to_string())
|
|
||||||
.await;
|
|
||||||
test
|
|
||||||
.create_view(&workspace_id, "supabase test view3".to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let rx = test
|
|
||||||
.notification_sender
|
|
||||||
.subscribe_with_condition::<FolderSyncStatePB, _>(&workspace_id, |pb| pb.is_finish);
|
|
||||||
|
|
||||||
receive_with_timeout(rx, Duration::from_secs(10))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let expected = test.get_collab_json().await;
|
|
||||||
let update = test.get_collab_update(&workspace_id).await;
|
|
||||||
assert_folder_collab_content(&workspace_id, &update, expected);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,6 +5,8 @@ use collab_entity::CollabType;
|
||||||
use collab_folder::Folder;
|
use collab_folder::Folder;
|
||||||
use event_integration_test::user_event::use_localhost_af_cloud;
|
use event_integration_test::user_event::use_localhost_af_cloud;
|
||||||
use event_integration_test::EventIntegrationTest;
|
use event_integration_test::EventIntegrationTest;
|
||||||
|
use flowy_user::entities::AFRolePB;
|
||||||
|
use flowy_user_pub::cloud::UserCloudServiceProvider;
|
||||||
use flowy_user_pub::entities::AuthType;
|
use flowy_user_pub::entities::AuthType;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::task::LocalSet;
|
use tokio::task::LocalSet;
|
||||||
|
@ -150,7 +152,7 @@ async fn af_cloud_open_workspace_test() {
|
||||||
test
|
test
|
||||||
.open_workspace(
|
.open_workspace(
|
||||||
&first_workspace.workspace_id,
|
&first_workspace.workspace_id,
|
||||||
first_workspace.workspace_auth_type.clone(),
|
first_workspace.workspace_auth_type,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
sleep(Duration::from_millis(300)).await;
|
sleep(Duration::from_millis(300)).await;
|
||||||
|
@ -161,7 +163,7 @@ async fn af_cloud_open_workspace_test() {
|
||||||
test
|
test
|
||||||
.open_workspace(
|
.open_workspace(
|
||||||
&second_workspace.workspace_id,
|
&second_workspace.workspace_id,
|
||||||
second_workspace.workspace_auth_type.clone(),
|
second_workspace.workspace_auth_type,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
sleep(Duration::from_millis(200)).await;
|
sleep(Duration::from_millis(200)).await;
|
||||||
|
@ -174,7 +176,7 @@ async fn af_cloud_open_workspace_test() {
|
||||||
test
|
test
|
||||||
.open_workspace(
|
.open_workspace(
|
||||||
&first_workspace.workspace_id,
|
&first_workspace.workspace_id,
|
||||||
first_workspace.workspace_auth_type.clone(),
|
first_workspace.workspace_auth_type,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let views_1 = test.get_all_workspace_views().await;
|
let views_1 = test.get_all_workspace_views().await;
|
||||||
|
@ -186,7 +188,7 @@ async fn af_cloud_open_workspace_test() {
|
||||||
test
|
test
|
||||||
.open_workspace(
|
.open_workspace(
|
||||||
&second_workspace.workspace_id,
|
&second_workspace.workspace_id,
|
||||||
second_workspace.workspace_auth_type.clone(),
|
second_workspace.workspace_auth_type,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let views_2 = test.get_all_workspace_views().await;
|
let views_2 = test.get_all_workspace_views().await;
|
||||||
|
@ -245,10 +247,7 @@ async fn af_cloud_different_open_same_workspace_test() {
|
||||||
let index = i % 2;
|
let index = i % 2;
|
||||||
let iter_workspace_id = &all_workspaces[index].workspace_id;
|
let iter_workspace_id = &all_workspaces[index].workspace_id;
|
||||||
client
|
client
|
||||||
.open_workspace(
|
.open_workspace(iter_workspace_id, all_workspaces[index].workspace_auth_type)
|
||||||
iter_workspace_id,
|
|
||||||
all_workspaces[index].workspace_auth_type.clone(),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
if iter_workspace_id == &cloned_shared_workspace_id {
|
if iter_workspace_id == &cloned_shared_workspace_id {
|
||||||
let views = client.get_all_workspace_views().await;
|
let views = client.get_all_workspace_views().await;
|
||||||
|
@ -295,36 +294,127 @@ async fn af_cloud_different_open_same_workspace_test() {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn af_cloud_create_local_workspace_test() {
|
async fn af_cloud_create_local_workspace_test() {
|
||||||
|
// Setup: Initialize test environment with AppFlowyCloud
|
||||||
use_localhost_af_cloud().await;
|
use_localhost_af_cloud().await;
|
||||||
let test = EventIntegrationTest::new().await;
|
let test = EventIntegrationTest::new().await;
|
||||||
let _ = test.af_cloud_sign_up().await;
|
let _ = test.af_cloud_sign_up().await;
|
||||||
|
|
||||||
let workspaces = test.get_all_workspaces().await.items;
|
// Verify initial state: User should have one default workspace
|
||||||
assert_eq!(workspaces.len(), 1);
|
let initial_workspaces = test.get_all_workspaces().await.items;
|
||||||
|
assert_eq!(
|
||||||
|
initial_workspaces.len(),
|
||||||
|
1,
|
||||||
|
"User should start with one default workspace"
|
||||||
|
);
|
||||||
|
|
||||||
let created_workspace = test
|
// make sure the workspaces order is consistent
|
||||||
|
// tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
|
||||||
|
|
||||||
|
// Test: Create a local workspace
|
||||||
|
let local_workspace = test
|
||||||
.create_workspace("my local workspace", AuthType::Local)
|
.create_workspace("my local workspace", AuthType::Local)
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(created_workspace.name, "my local workspace");
|
|
||||||
|
|
||||||
let workspaces = test.get_all_workspaces().await.items;
|
// Verify: Local workspace was created correctly
|
||||||
assert_eq!(workspaces.len(), 2);
|
assert_eq!(local_workspace.name, "my local workspace");
|
||||||
assert_eq!(workspaces[1].name, "my local workspace");
|
let updated_workspaces = test.get_all_workspaces().await.items;
|
||||||
|
assert_eq!(
|
||||||
|
updated_workspaces.len(),
|
||||||
|
2,
|
||||||
|
"Should now have two workspaces"
|
||||||
|
);
|
||||||
|
dbg!(&updated_workspaces);
|
||||||
|
|
||||||
|
// Find local workspace by name instead of using index
|
||||||
|
let found_local_workspace = updated_workspaces
|
||||||
|
.iter()
|
||||||
|
.find(|workspace| workspace.name == "my local workspace")
|
||||||
|
.expect("Local workspace should exist");
|
||||||
|
assert_eq!(found_local_workspace.name, "my local workspace");
|
||||||
|
|
||||||
|
// Test: Open the local workspace
|
||||||
test
|
test
|
||||||
.open_workspace(
|
.open_workspace(
|
||||||
&created_workspace.workspace_id,
|
&local_workspace.workspace_id,
|
||||||
created_workspace.workspace_auth_type,
|
local_workspace.workspace_auth_type,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
// Verify: Views in the local workspace
|
||||||
let views = test.get_all_views().await;
|
let views = test.get_all_views().await;
|
||||||
assert_eq!(views.len(), 2);
|
assert_eq!(
|
||||||
assert!(views
|
views.len(),
|
||||||
.iter()
|
2,
|
||||||
.any(|view| view.parent_view_id == workspaces[1].workspace_id));
|
"Local workspace should have 2 default views"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
views
|
||||||
|
.iter()
|
||||||
|
.any(|view| view.parent_view_id == local_workspace.workspace_id),
|
||||||
|
"Views should belong to the local workspace"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify: Can access all views
|
||||||
for view in views {
|
for view in views {
|
||||||
test.get_view(&view.id).await;
|
test.get_view(&view.id).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify: Local workspace members
|
||||||
|
let members = test
|
||||||
|
.get_workspace_members(&local_workspace.workspace_id)
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
members.len(),
|
||||||
|
1,
|
||||||
|
"Local workspace should have only one member"
|
||||||
|
);
|
||||||
|
assert_eq!(members[0].role, AFRolePB::Owner, "User should be the owner");
|
||||||
|
|
||||||
|
// Test: Create a server workspace
|
||||||
|
let server_workspace = test
|
||||||
|
.create_workspace("my server workspace", AuthType::AppFlowyCloud)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Verify: Server workspace was created correctly
|
||||||
|
assert_eq!(server_workspace.name, "my server workspace");
|
||||||
|
let final_workspaces = test.get_all_workspaces().await.items;
|
||||||
|
assert_eq!(
|
||||||
|
final_workspaces.len(),
|
||||||
|
3,
|
||||||
|
"Should now have three workspaces"
|
||||||
|
);
|
||||||
|
|
||||||
|
dbg!(&final_workspaces);
|
||||||
|
|
||||||
|
// Find workspaces by name instead of using indices
|
||||||
|
let found_local_workspace = final_workspaces
|
||||||
|
.iter()
|
||||||
|
.find(|workspace| workspace.name == "my local workspace")
|
||||||
|
.expect("Local workspace should exist");
|
||||||
|
assert_eq!(found_local_workspace.name, "my local workspace");
|
||||||
|
|
||||||
|
let found_server_workspace = final_workspaces
|
||||||
|
.iter()
|
||||||
|
.find(|workspace| workspace.name == "my server workspace")
|
||||||
|
.expect("Server workspace should exist");
|
||||||
|
assert_eq!(found_server_workspace.name, "my server workspace");
|
||||||
|
|
||||||
|
// Verify: Server-side only recognizes cloud workspaces (not local ones)
|
||||||
|
let user_profile = test.get_user_profile().await.unwrap();
|
||||||
|
test
|
||||||
|
.server_provider
|
||||||
|
.set_server_auth_type(&AuthType::AppFlowyCloud, Some(user_profile.token.clone()))
|
||||||
|
.unwrap();
|
||||||
|
test.server_provider.set_token(&user_profile.token).unwrap();
|
||||||
|
|
||||||
|
let user_service = test.server_provider.get_server().unwrap().user_service();
|
||||||
|
let server_workspaces = user_service
|
||||||
|
.get_all_workspace(user_profile.id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
server_workspaces.len(),
|
||||||
|
2,
|
||||||
|
"Server should only see 2 workspaces (the default and server workspace, not the local one)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,502 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use assert_json_diff::assert_json_eq;
|
|
||||||
use collab_database::rows::database_row_document_id_from_row_id;
|
|
||||||
use collab_document::blocks::DocumentData;
|
|
||||||
use collab_entity::CollabType;
|
|
||||||
use collab_folder::FolderData;
|
|
||||||
use nanoid::nanoid;
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use event_integration_test::document::document_event::DocumentEventTest;
|
|
||||||
use event_integration_test::event_builder::EventBuilder;
|
|
||||||
use event_integration_test::EventIntegrationTest;
|
|
||||||
use flowy_core::DEFAULT_NAME;
|
|
||||||
use flowy_encrypt::decrypt_text;
|
|
||||||
use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_UUID};
|
|
||||||
use flowy_user::entities::{
|
|
||||||
AuthenticatorPB, OauthSignInPB, UpdateUserProfilePayloadPB, UserProfilePB,
|
|
||||||
};
|
|
||||||
use flowy_user::errors::ErrorCode;
|
|
||||||
use flowy_user::event_map::UserEvent::*;
|
|
||||||
|
|
||||||
use crate::util::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn third_party_sign_up_test() {
|
|
||||||
if get_supabase_config().is_some() {
|
|
||||||
let test = EventIntegrationTest::new().await;
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string());
|
|
||||||
map.insert(
|
|
||||||
USER_EMAIL.to_string(),
|
|
||||||
format!("{}@appflowy.io", nanoid!(6)),
|
|
||||||
);
|
|
||||||
map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string());
|
|
||||||
let payload = OauthSignInPB {
|
|
||||||
map,
|
|
||||||
authenticator: AuthenticatorPB::Supabase,
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = EventBuilder::new(test.clone())
|
|
||||||
.event(OauthSignIn)
|
|
||||||
.payload(payload)
|
|
||||||
.async_send()
|
|
||||||
.await
|
|
||||||
.parse::<UserProfilePB>();
|
|
||||||
dbg!(&response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn third_party_sign_up_with_encrypt_test() {
|
|
||||||
if get_supabase_config().is_some() {
|
|
||||||
let test = EventIntegrationTest::new().await;
|
|
||||||
test.supabase_party_sign_up().await;
|
|
||||||
let user_profile = test.get_user_profile().await.unwrap();
|
|
||||||
assert!(user_profile.encryption_sign.is_empty());
|
|
||||||
|
|
||||||
let secret = test.enable_encryption().await;
|
|
||||||
let user_profile = test.get_user_profile().await.unwrap();
|
|
||||||
assert!(!user_profile.encryption_sign.is_empty());
|
|
||||||
|
|
||||||
let decryption_sign = decrypt_text(user_profile.encryption_sign, &secret).unwrap();
|
|
||||||
assert_eq!(decryption_sign, user_profile.id.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn third_party_sign_up_with_duplicated_uuid() {
|
|
||||||
if get_supabase_config().is_some() {
|
|
||||||
let test = EventIntegrationTest::new().await;
|
|
||||||
let email = format!("{}@appflowy.io", nanoid!(6));
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string());
|
|
||||||
map.insert(USER_EMAIL.to_string(), email.clone());
|
|
||||||
map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string());
|
|
||||||
|
|
||||||
let response_1 = EventBuilder::new(test.clone())
|
|
||||||
.event(OauthSignIn)
|
|
||||||
.payload(OauthSignInPB {
|
|
||||||
map: map.clone(),
|
|
||||||
authenticator: AuthenticatorPB::Supabase,
|
|
||||||
})
|
|
||||||
.async_send()
|
|
||||||
.await
|
|
||||||
.parse::<UserProfilePB>();
|
|
||||||
dbg!(&response_1);
|
|
||||||
|
|
||||||
let response_2 = EventBuilder::new(test.clone())
|
|
||||||
.event(OauthSignIn)
|
|
||||||
.payload(OauthSignInPB {
|
|
||||||
map: map.clone(),
|
|
||||||
authenticator: AuthenticatorPB::Supabase,
|
|
||||||
})
|
|
||||||
.async_send()
|
|
||||||
.await
|
|
||||||
.parse::<UserProfilePB>();
|
|
||||||
assert_eq!(response_1, response_2);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn third_party_sign_up_with_duplicated_email() {
|
|
||||||
if get_supabase_config().is_some() {
|
|
||||||
let test = EventIntegrationTest::new().await;
|
|
||||||
let email = format!("{}@appflowy.io", nanoid!(6));
|
|
||||||
test
|
|
||||||
.supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let error = test
|
|
||||||
.supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone()))
|
|
||||||
.await
|
|
||||||
.err()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(error.code, ErrorCode::Conflict);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn sign_up_as_guest_and_then_update_to_new_cloud_user_test() {
|
|
||||||
if get_supabase_config().is_some() {
|
|
||||||
let test = EventIntegrationTest::new_anon().await;
|
|
||||||
let old_views = test
|
|
||||||
.folder_manager
|
|
||||||
.get_current_workspace_public_views()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let old_workspace = test.folder_manager.get_current_workspace().await.unwrap();
|
|
||||||
|
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
|
||||||
test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap();
|
|
||||||
let new_views = test
|
|
||||||
.folder_manager
|
|
||||||
.get_current_workspace_public_views()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let new_workspace = test.folder_manager.get_current_workspace().await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(old_views.len(), new_views.len());
|
|
||||||
assert_eq!(old_workspace.name, new_workspace.name);
|
|
||||||
assert_eq!(old_workspace.views.len(), new_workspace.views.len());
|
|
||||||
for (index, view) in old_views.iter().enumerate() {
|
|
||||||
assert_eq!(view.name, new_views[index].name);
|
|
||||||
assert_eq!(view.layout, new_views[index].layout);
|
|
||||||
assert_eq!(view.create_time, new_views[index].create_time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn sign_up_as_guest_and_then_update_to_existing_cloud_user_test() {
|
|
||||||
if get_supabase_config().is_some() {
|
|
||||||
let test = EventIntegrationTest::new_anon().await;
|
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
|
||||||
|
|
||||||
let email = format!("{}@appflowy.io", nanoid!(6));
|
|
||||||
// The workspace of the guest will be migrated to the new user with given uuid
|
|
||||||
let _user_profile = test
|
|
||||||
.supabase_sign_up_with_uuid(&uuid, Some(email.clone()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let old_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap();
|
|
||||||
let old_cloud_views = test
|
|
||||||
.folder_manager
|
|
||||||
.get_current_workspace_public_views()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(old_cloud_views.len(), 1);
|
|
||||||
assert_eq!(old_cloud_views.first().unwrap().child_views.len(), 1);
|
|
||||||
|
|
||||||
// sign out and then sign in as a guest
|
|
||||||
test.sign_out().await;
|
|
||||||
|
|
||||||
let _sign_up_context = test.sign_up_as_anon().await;
|
|
||||||
let new_workspace = test.folder_manager.get_current_workspace().await.unwrap();
|
|
||||||
test
|
|
||||||
.create_view(&new_workspace.id, "new workspace child view".to_string())
|
|
||||||
.await;
|
|
||||||
let new_workspace = test.folder_manager.get_current_workspace().await.unwrap();
|
|
||||||
assert_eq!(new_workspace.views.len(), 2);
|
|
||||||
|
|
||||||
// upload to cloud user with given uuid. This time the workspace of the guest will not be merged
|
|
||||||
// because the cloud user already has a workspace
|
|
||||||
test
|
|
||||||
.supabase_sign_up_with_uuid(&uuid, Some(email))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let new_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap();
|
|
||||||
let new_cloud_views = test
|
|
||||||
.folder_manager
|
|
||||||
.get_current_workspace_public_views()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(new_cloud_workspace, old_cloud_workspace);
|
|
||||||
assert_eq!(new_cloud_views, old_cloud_views);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn get_user_profile_test() {
|
|
||||||
if let Some(test) = FlowySupabaseTest::new().await {
|
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
|
||||||
test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap();
|
|
||||||
|
|
||||||
let result = test.get_user_profile().await;
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn update_user_profile_test() {
|
|
||||||
if let Some(test) = FlowySupabaseTest::new().await {
|
|
||||||
let uuid = uuid::Uuid::new_v4().to_string();
|
|
||||||
let profile = test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap();
|
|
||||||
test
|
|
||||||
.update_user_profile(UpdateUserProfilePayloadPB::new(profile.id).name("lucas"))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let new_profile = test.get_user_profile().await.unwrap();
|
|
||||||
assert_eq!(new_profile.name, "lucas")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn update_user_profile_with_existing_email_test() {
|
|
||||||
if let Some(test) = FlowySupabaseTest::new().await {
|
|
||||||
let email = format!("{}@appflowy.io", nanoid!(6));
|
|
||||||
let _ = test
|
|
||||||
.supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone()))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let profile = test
|
|
||||||
.supabase_sign_up_with_uuid(
|
|
||||||
&uuid::Uuid::new_v4().to_string(),
|
|
||||||
Some(format!("{}@appflowy.io", nanoid!(6))),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let error = test
|
|
||||||
.update_user_profile(
|
|
||||||
UpdateUserProfilePayloadPB::new(profile.id)
|
|
||||||
.name("lucas")
|
|
||||||
.email(&email),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(error.code, ErrorCode::Conflict);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn migrate_anon_document_on_cloud_signup() {
|
|
||||||
if get_supabase_config().is_some() {
|
|
||||||
let test = EventIntegrationTest::new().await;
|
|
||||||
let user_profile = test.sign_up_as_anon().await.user_profile;
|
|
||||||
|
|
||||||
let view = test
|
|
||||||
.create_view(&user_profile.workspace_id, "My first view".to_string())
|
|
||||||
.await;
|
|
||||||
let document_event = DocumentEventTest::new_with_core(test.clone());
|
|
||||||
let block_id = document_event
|
|
||||||
.insert_index(&view.id, "hello world", 1, None)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let _ = test.supabase_party_sign_up().await;
|
|
||||||
|
|
||||||
let workspace_id = test.user_manager.workspace_id().unwrap();
|
|
||||||
// After sign up, the documents should be migrated to the cloud
|
|
||||||
// So, we can get the document data from the cloud
|
|
||||||
let data: DocumentData = test
|
|
||||||
.document_manager
|
|
||||||
.get_cloud_service()
|
|
||||||
.get_document_data(&view.id, &workspace_id)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
let block = data.blocks.get(&block_id).unwrap();
|
|
||||||
assert_json_eq!(
|
|
||||||
block.data,
|
|
||||||
json!({
|
|
||||||
"delta": [
|
|
||||||
{
|
|
||||||
"insert": "hello world"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn migrate_anon_data_on_cloud_signup() {
|
|
||||||
if get_supabase_config().is_some() {
|
|
||||||
let (cleaner, user_db_path) = unzip(
|
|
||||||
"./tests/user/supabase_test/history_user_db",
|
|
||||||
"workspace_sync",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let test =
|
|
||||||
EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await;
|
|
||||||
let user_profile = test.supabase_party_sign_up().await;
|
|
||||||
|
|
||||||
// Get the folder data from remote
|
|
||||||
let folder_data: FolderData = test
|
|
||||||
.folder_manager
|
|
||||||
.get_cloud_service()
|
|
||||||
.get_folder_data(&user_profile.workspace_id, &user_profile.id)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let expected_folder_data = expected_workspace_sync_folder_data();
|
|
||||||
assert_eq!(folder_data.views.len(), expected_folder_data.views.len());
|
|
||||||
|
|
||||||
// After migration, the ids of the folder_data should be different from the expected_folder_data
|
|
||||||
for i in 0..folder_data.views.len() {
|
|
||||||
let left_view = &folder_data.views[i];
|
|
||||||
let right_view = &expected_folder_data.views[i];
|
|
||||||
assert_ne!(left_view.id, right_view.id);
|
|
||||||
assert_ne!(left_view.parent_view_id, right_view.parent_view_id);
|
|
||||||
assert_eq!(left_view.name, right_view.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_ne!(folder_data.workspace.id, expected_folder_data.workspace.id);
|
|
||||||
assert_ne!(folder_data.current_view, expected_folder_data.current_view);
|
|
||||||
|
|
||||||
let database_views = folder_data
|
|
||||||
.views
|
|
||||||
.iter()
|
|
||||||
.filter(|view| view.layout.is_database())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// Try to load the database from the cloud.
|
|
||||||
for (i, database_view) in database_views.iter().enumerate() {
|
|
||||||
let cloud_service = test.database_manager.get_cloud_service();
|
|
||||||
let database_id = test
|
|
||||||
.database_manager
|
|
||||||
.get_database_id_with_view_id(&database_view.id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let editor = test
|
|
||||||
.database_manager
|
|
||||||
.get_database(&database_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// The database view setting should be loaded by the view id
|
|
||||||
let _ = editor
|
|
||||||
.get_database_view_setting(&database_view.id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let rows = editor.get_rows(&database_view.id).await.unwrap();
|
|
||||||
assert_eq!(rows.len(), 3);
|
|
||||||
|
|
||||||
let workspace_id = test.user_manager.workspace_id().unwrap();
|
|
||||||
if i == 0 {
|
|
||||||
let first_row = rows.first().unwrap().as_ref();
|
|
||||||
let icon_url = first_row.meta.icon_url.clone().unwrap();
|
|
||||||
assert_eq!(icon_url, "😄");
|
|
||||||
|
|
||||||
let document_id = database_row_document_id_from_row_id(&first_row.row.id);
|
|
||||||
let document_data: DocumentData = test
|
|
||||||
.document_manager
|
|
||||||
.get_cloud_service()
|
|
||||||
.get_document_data(&document_id, &workspace_id)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let editor = test
|
|
||||||
.document_manager
|
|
||||||
.get_document(&document_id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let expected_document_data = editor.lock().get_document_data().unwrap();
|
|
||||||
|
|
||||||
// let expected_document_data = test
|
|
||||||
// .document_manager
|
|
||||||
// .get_document_data(&document_id)
|
|
||||||
// .await
|
|
||||||
// .unwrap();
|
|
||||||
assert_eq!(document_data, expected_document_data);
|
|
||||||
let json = json!(document_data);
|
|
||||||
assert_eq!(
|
|
||||||
json["blocks"]["LPMpo0Qaab"]["data"]["delta"][0]["insert"],
|
|
||||||
json!("Row document")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert!(cloud_service
|
|
||||||
.get_database_object_doc_state(&database_id, CollabType::Database, &workspace_id)
|
|
||||||
.await
|
|
||||||
.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(cleaner);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expected_workspace_sync_folder_data() -> FolderData {
|
|
||||||
serde_json::from_value::<FolderData>(json!({
|
|
||||||
"current_view": "e0811131-9928-4541-a174-20b7553d9e4c",
|
|
||||||
"current_workspace_id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3",
|
|
||||||
"views": [
|
|
||||||
{
|
|
||||||
"children": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "e0811131-9928-4541-a174-20b7553d9e4c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "53333949-c262-447b-8597-107589697059"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"created_at": 1693147093,
|
|
||||||
"desc": "",
|
|
||||||
"icon": null,
|
|
||||||
"id": "e203afb3-de5d-458a-8380-33cd788a756e",
|
|
||||||
"is_favorite": false,
|
|
||||||
"layout": 0,
|
|
||||||
"name": "⭐️ Getting started",
|
|
||||||
"parent_view_id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "11c697ba-5ed1-41c0-adfc-576db28ad27b"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4a5c25e2-a734-440c-973b-4c0e7ab0039c"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"created_at": 1693147096,
|
|
||||||
"desc": "",
|
|
||||||
"icon": null,
|
|
||||||
"id": "e0811131-9928-4541-a174-20b7553d9e4c",
|
|
||||||
"is_favorite": false,
|
|
||||||
"layout": 1,
|
|
||||||
"name": "database",
|
|
||||||
"parent_view_id": "e203afb3-de5d-458a-8380-33cd788a756e"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": {
|
|
||||||
"items": []
|
|
||||||
},
|
|
||||||
"created_at": 1693147124,
|
|
||||||
"desc": "",
|
|
||||||
"icon": null,
|
|
||||||
"id": "11c697ba-5ed1-41c0-adfc-576db28ad27b",
|
|
||||||
"is_favorite": false,
|
|
||||||
"layout": 3,
|
|
||||||
"name": "calendar",
|
|
||||||
"parent_view_id": "e0811131-9928-4541-a174-20b7553d9e4c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": {
|
|
||||||
"items": []
|
|
||||||
},
|
|
||||||
"created_at": 1693147125,
|
|
||||||
"desc": "",
|
|
||||||
"icon": null,
|
|
||||||
"id": "4a5c25e2-a734-440c-973b-4c0e7ab0039c",
|
|
||||||
"is_favorite": false,
|
|
||||||
"layout": 2,
|
|
||||||
"name": "board",
|
|
||||||
"parent_view_id": "e0811131-9928-4541-a174-20b7553d9e4c"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"children": {
|
|
||||||
"items": []
|
|
||||||
},
|
|
||||||
"created_at": 1693147133,
|
|
||||||
"desc": "",
|
|
||||||
"icon": null,
|
|
||||||
"id": "53333949-c262-447b-8597-107589697059",
|
|
||||||
"is_favorite": false,
|
|
||||||
"layout": 0,
|
|
||||||
"name": "document",
|
|
||||||
"parent_view_id": "e203afb3-de5d-458a-8380-33cd788a756e"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"workspaces": [
|
|
||||||
{
|
|
||||||
"child_views": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "e203afb3-de5d-458a-8380-33cd788a756e"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"created_at": 1693147093,
|
|
||||||
"id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3",
|
|
||||||
"name": "Workspace"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}))
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
|
|
||||||
## Don't modify the zip files in this folder
|
|
||||||
|
|
||||||
The zip files in this folder are used for integration tests. If the tests fail, it means users upgrading to this version of AppFlowy will encounter issues
|
|
Binary file not shown.
|
@ -1,2 +0,0 @@
|
||||||
mod auth_test;
|
|
||||||
mod workspace_test;
|
|
|
@ -1,43 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use event_integration_test::{event_builder::EventBuilder, EventIntegrationTest};
|
|
||||||
use flowy_folder::entities::WorkspaceSettingPB;
|
|
||||||
use flowy_folder::event_map::FolderEvent::GetCurrentWorkspaceSetting;
|
|
||||||
use flowy_server::supabase::define::{USER_EMAIL, USER_UUID};
|
|
||||||
use flowy_user::entities::{AuthenticatorPB, OauthSignInPB, UserProfilePB};
|
|
||||||
use flowy_user::event_map::UserEvent::*;
|
|
||||||
|
|
||||||
use crate::util::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn initial_workspace_test() {
|
|
||||||
if get_supabase_config().is_some() {
|
|
||||||
let test = EventIntegrationTest::new().await;
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string());
|
|
||||||
map.insert(
|
|
||||||
USER_EMAIL.to_string(),
|
|
||||||
format!("{}@gmail.com", uuid::Uuid::new_v4()),
|
|
||||||
);
|
|
||||||
let payload = OauthSignInPB {
|
|
||||||
map,
|
|
||||||
authenticator: AuthenticatorPB::Supabase,
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = EventBuilder::new(test.clone())
|
|
||||||
.event(OauthSignIn)
|
|
||||||
.payload(payload)
|
|
||||||
.async_send()
|
|
||||||
.await
|
|
||||||
.parse::<UserProfilePB>();
|
|
||||||
|
|
||||||
let workspace_settings = EventBuilder::new(test.clone())
|
|
||||||
.event(GetCurrentWorkspaceSetting)
|
|
||||||
.async_send()
|
|
||||||
.await
|
|
||||||
.parse::<WorkspaceSettingPB>();
|
|
||||||
|
|
||||||
assert!(workspace_settings.latest_view.is_some());
|
|
||||||
dbg!(&workspace_settings);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod cloud;
|
pub mod cloud;
|
||||||
pub mod persistence;
|
pub mod persistence;
|
||||||
|
pub mod user_service;
|
||||||
|
|
14
frontend/rust-lib/flowy-ai-pub/src/user_service.rs
Normal file
14
frontend/rust-lib/flowy-ai-pub/src/user_service.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
|
use flowy_sqlite::DBConnection;
|
||||||
|
use lib_infra::async_trait::async_trait;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AIUserService: Send + Sync + 'static {
|
||||||
|
fn user_id(&self) -> Result<i64, FlowyError>;
|
||||||
|
async fn is_local_model(&self) -> FlowyResult<bool>;
|
||||||
|
fn workspace_id(&self) -> Result<Uuid, FlowyError>;
|
||||||
|
fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError>;
|
||||||
|
fn application_root_dir(&self) -> Result<PathBuf, FlowyError>;
|
||||||
|
}
|
|
@ -12,9 +12,8 @@ use dashmap::DashMap;
|
||||||
use flowy_ai_pub::cloud::{
|
use flowy_ai_pub::cloud::{
|
||||||
AIModel, ChatCloudService, ChatSettings, UpdateChatParams, DEFAULT_AI_MODEL_NAME,
|
AIModel, ChatCloudService, ChatSettings, UpdateChatParams, DEFAULT_AI_MODEL_NAME,
|
||||||
};
|
};
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||||
use flowy_sqlite::kv::KVStorePreferences;
|
use flowy_sqlite::kv::KVStorePreferences;
|
||||||
use flowy_sqlite::DBConnection;
|
|
||||||
|
|
||||||
use crate::notification::{chat_notification_builder, ChatNotification};
|
use crate::notification::{chat_notification_builder, ChatNotification};
|
||||||
use crate::util::ai_available_models_key;
|
use crate::util::ai_available_models_key;
|
||||||
|
@ -22,6 +21,7 @@ use collab_integrate::persistence::collab_metadata_sql::{
|
||||||
batch_insert_collab_metadata, batch_select_collab_metadata, AFCollabMetadata,
|
batch_insert_collab_metadata, batch_select_collab_metadata, AFCollabMetadata,
|
||||||
};
|
};
|
||||||
use flowy_ai_pub::cloud::ai_dto::AvailableModel;
|
use flowy_ai_pub::cloud::ai_dto::AvailableModel;
|
||||||
|
use flowy_ai_pub::user_service::AIUserService;
|
||||||
use flowy_storage_pub::storage::StorageService;
|
use flowy_storage_pub::storage::StorageService;
|
||||||
use lib_infra::async_trait::async_trait;
|
use lib_infra::async_trait::async_trait;
|
||||||
use lib_infra::util::timestamp;
|
use lib_infra::util::timestamp;
|
||||||
|
@ -33,15 +33,6 @@ use tokio::sync::RwLock;
|
||||||
use tracing::{error, info, instrument, trace};
|
use tracing::{error, info, instrument, trace};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AIUserService: Send + Sync + 'static {
|
|
||||||
fn user_id(&self) -> Result<i64, FlowyError>;
|
|
||||||
async fn is_local_model(&self) -> FlowyResult<bool>;
|
|
||||||
fn workspace_id(&self) -> Result<Uuid, FlowyError>;
|
|
||||||
fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError>;
|
|
||||||
fn application_root_dir(&self) -> Result<PathBuf, FlowyError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// AIExternalService is an interface for external services that AI plugin can interact with.
|
/// AIExternalService is an interface for external services that AI plugin can interact with.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait AIExternalService: Send + Sync + 'static {
|
pub trait AIExternalService: Send + Sync + 'static {
|
||||||
|
@ -113,36 +104,86 @@ impl AIManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all, err)]
|
async fn reload_with_workspace_id(&self, workspace_id: &str) {
|
||||||
pub async fn initialize(&self, _workspace_id: &str) -> Result<(), FlowyError> {
|
// Check if local AI is enabled for this workspace and if we're in local mode
|
||||||
let local_ai = self.local_ai.clone();
|
let result = self.user_service.is_local_model().await;
|
||||||
tokio::spawn(async move {
|
if let Err(err) = &result {
|
||||||
if let Err(err) = local_ai.destroy_plugin().await {
|
if matches!(err.code, ErrorCode::UserNotLogin) {
|
||||||
error!("Failed to destroy plugin: {}", err);
|
info!("[AI Manager] User not logged in, skipping local AI reload");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(err) = local_ai.reload().await {
|
let is_local = result.unwrap_or(false);
|
||||||
error!("[AI Manager] failed to reload local AI: {:?}", err);
|
let is_enabled = self.local_ai.is_enabled_on_workspace(workspace_id);
|
||||||
}
|
let is_running = self.local_ai.is_running();
|
||||||
});
|
info!(
|
||||||
|
"[AI Manager] Reloading workspace: {}, is_local: {}, is_enabled: {}, is_running: {}",
|
||||||
|
workspace_id, is_local, is_enabled, is_running
|
||||||
|
);
|
||||||
|
|
||||||
|
// Shutdown AI if it's running but shouldn't be (not enabled and not in local mode)
|
||||||
|
if is_running && !is_enabled && !is_local {
|
||||||
|
info!("[AI Manager] Local AI is running but not enabled, shutting it down");
|
||||||
|
let local_ai = self.local_ai.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// Wait for 5 seconds to allow other services to initialize
|
||||||
|
// TODO: pick a right time to start plugin service. Maybe [UserStatusCallback::did_launch]
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||||
|
|
||||||
|
if let Err(err) = local_ai.toggle_plugin(false).await {
|
||||||
|
error!("[AI Manager] failed to shutdown local AI: {:?}", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start AI if it's enabled but not running
|
||||||
|
if is_enabled && !is_running {
|
||||||
|
info!("[AI Manager] Local AI is enabled but not running, starting it now");
|
||||||
|
let local_ai = self.local_ai.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// Wait for 5 seconds to allow other services to initialize
|
||||||
|
// TODO: pick a right time to start plugin service. Maybe [UserStatusCallback::did_launch]
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||||
|
|
||||||
|
if let Err(err) = local_ai.toggle_plugin(true).await {
|
||||||
|
error!("[AI Manager] failed to start local AI: {:?}", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log status for other cases
|
||||||
|
if is_running {
|
||||||
|
info!("[AI Manager] Local AI is already running");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip_all, err)]
|
||||||
|
pub async fn on_launch_if_authenticated(&self, workspace_id: &str) -> Result<(), FlowyError> {
|
||||||
|
self.reload_with_workspace_id(workspace_id).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn initialize_after_sign_in(&self, workspace_id: &str) -> Result<(), FlowyError> {
|
||||||
|
self.reload_with_workspace_id(workspace_id).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn initialize_after_sign_up(&self, workspace_id: &str) -> Result<(), FlowyError> {
|
||||||
|
self.reload_with_workspace_id(workspace_id).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all, err)]
|
#[instrument(skip_all, err)]
|
||||||
pub async fn initialize_after_open_workspace(
|
pub async fn initialize_after_open_workspace(
|
||||||
&self,
|
&self,
|
||||||
_workspace_id: &Uuid,
|
workspace_id: &Uuid,
|
||||||
) -> Result<(), FlowyError> {
|
) -> Result<(), FlowyError> {
|
||||||
let local_ai = self.local_ai.clone();
|
self
|
||||||
tokio::spawn(async move {
|
.reload_with_workspace_id(&workspace_id.to_string())
|
||||||
if let Err(err) = local_ai.destroy_plugin().await {
|
.await;
|
||||||
error!("Failed to destroy plugin: {}", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(err) = local_ai.reload().await {
|
|
||||||
error!("[AI Manager] failed to reload local AI: {:?}", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -450,13 +491,9 @@ impl AIManager {
|
||||||
pub async fn get_available_models(&self, source: String) -> FlowyResult<AvailableModelsPB> {
|
pub async fn get_available_models(&self, source: String) -> FlowyResult<AvailableModelsPB> {
|
||||||
let is_local_mode = self.user_service.is_local_model().await?;
|
let is_local_mode = self.user_service.is_local_model().await?;
|
||||||
if is_local_mode {
|
if is_local_mode {
|
||||||
let mut selected_model = AIModel::default();
|
let setting = self.local_ai.get_local_ai_setting();
|
||||||
let mut models = vec![];
|
let selected_model = AIModel::local(setting.chat_model_name, "".to_string());
|
||||||
if let Some(local_model) = self.local_ai.get_plugin_chat_model() {
|
let models = vec![selected_model.clone()];
|
||||||
let model = AIModel::local(local_model, "".to_string());
|
|
||||||
selected_model = model.clone();
|
|
||||||
models.push(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AvailableModelsPB {
|
Ok(AvailableModelsPB {
|
||||||
models: models.into_iter().map(|m| m.into()).collect(),
|
models: models.into_iter().map(|m| m.into()).collect(),
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::ai_manager::AIUserService;
|
|
||||||
use crate::entities::{
|
use crate::entities::{
|
||||||
ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, PredefinedFormatPB,
|
ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, PredefinedFormatPB,
|
||||||
RepeatedRelatedQuestionPB, StreamMessageParams,
|
RepeatedRelatedQuestionPB, StreamMessageParams,
|
||||||
|
@ -14,6 +13,7 @@ use flowy_ai_pub::persistence::{
|
||||||
select_answer_where_match_reply_message_id, select_chat_messages, upsert_chat_messages,
|
select_answer_where_match_reply_message_id, select_chat_messages, upsert_chat_messages,
|
||||||
ChatMessageTable,
|
ChatMessageTable,
|
||||||
};
|
};
|
||||||
|
use flowy_ai_pub::user_service::AIUserService;
|
||||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||||
use flowy_sqlite::DBConnection;
|
use flowy_sqlite::DBConnection;
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::ai_manager::AIUserService;
|
|
||||||
use crate::entities::{CompleteTextPB, CompleteTextTaskPB, CompletionTypePB};
|
use crate::entities::{CompleteTextPB, CompleteTextTaskPB, CompletionTypePB};
|
||||||
use allo_isolate::Isolate;
|
use allo_isolate::Isolate;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -14,6 +13,7 @@ use futures::{SinkExt, StreamExt};
|
||||||
use lib_infra::isolate_stream::IsolateSink;
|
use lib_infra::isolate_stream::IsolateSink;
|
||||||
|
|
||||||
use crate::stream_message::StreamMessage;
|
use crate::stream_message::StreamMessage;
|
||||||
|
use flowy_ai_pub::user_service::AIUserService;
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::ai_manager::AIUserService;
|
|
||||||
use crate::entities::{LocalAIPB, RunningStatePB};
|
use crate::entities::{LocalAIPB, RunningStatePB};
|
||||||
use crate::local_ai::resource::{LLMResourceService, LocalAIResourceController};
|
use crate::local_ai::resource::{LLMResourceService, LocalAIResourceController};
|
||||||
use crate::notification::{
|
use crate::notification::{
|
||||||
|
@ -17,6 +16,7 @@ use af_local_ai::ollama_plugin::OllamaAIPlugin;
|
||||||
use af_plugin::core::path::is_plugin_ready;
|
use af_plugin::core::path::is_plugin_ready;
|
||||||
use af_plugin::core::plugin::RunningState;
|
use af_plugin::core::plugin::RunningState;
|
||||||
use arc_swap::ArcSwapOption;
|
use arc_swap::ArcSwapOption;
|
||||||
|
use flowy_ai_pub::user_service::AIUserService;
|
||||||
use futures_util::SinkExt;
|
use futures_util::SinkExt;
|
||||||
use lib_infra::util::get_operating_system;
|
use lib_infra::util::get_operating_system;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -99,7 +99,7 @@ impl LocalAIController {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let key = local_ai_enabled_key(&workspace_id);
|
let key = local_ai_enabled_key(&workspace_id.to_string());
|
||||||
info!("[AI Plugin] state: {:?}", state);
|
info!("[AI Plugin] state: {:?}", state);
|
||||||
|
|
||||||
// Read whether plugin is enabled from store; default to true
|
// Read whether plugin is enabled from store; default to true
|
||||||
|
@ -157,14 +157,15 @@ impl LocalAIController {
|
||||||
}
|
}
|
||||||
#[instrument(level = "debug", skip_all)]
|
#[instrument(level = "debug", skip_all)]
|
||||||
pub async fn observe_plugin_resource(&self) {
|
pub async fn observe_plugin_resource(&self) {
|
||||||
debug!(
|
|
||||||
"[AI Plugin] init plugin when first run. thread: {:?}",
|
|
||||||
std::thread::current().id()
|
|
||||||
);
|
|
||||||
let sys = get_operating_system();
|
let sys = get_operating_system();
|
||||||
if !sys.is_desktop() {
|
if !sys.is_desktop() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"[AI Plugin] observer plugin state. thread: {:?}",
|
||||||
|
std::thread::current().id()
|
||||||
|
);
|
||||||
async fn try_init_plugin(
|
async fn try_init_plugin(
|
||||||
resource: &Arc<LocalAIResourceController>,
|
resource: &Arc<LocalAIResourceController>,
|
||||||
ai_plugin: &Arc<OllamaAIPlugin>,
|
ai_plugin: &Arc<OllamaAIPlugin>,
|
||||||
|
@ -196,12 +197,6 @@ impl LocalAIController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reload(&self) -> FlowyResult<()> {
|
|
||||||
let is_enabled = self.is_enabled();
|
|
||||||
self.toggle_plugin(is_enabled).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn upgrade_store_preferences(&self) -> FlowyResult<Arc<KVStorePreferences>> {
|
fn upgrade_store_preferences(&self) -> FlowyResult<Arc<KVStorePreferences>> {
|
||||||
self
|
self
|
||||||
.store_preferences
|
.store_preferences
|
||||||
|
@ -211,9 +206,6 @@ impl LocalAIController {
|
||||||
|
|
||||||
/// Indicate whether the local AI plugin is running.
|
/// Indicate whether the local AI plugin is running.
|
||||||
pub fn is_running(&self) -> bool {
|
pub fn is_running(&self) -> bool {
|
||||||
if !self.is_enabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
self.ai_plugin.get_plugin_running_state().is_running()
|
self.ai_plugin.get_plugin_running_state().is_running()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,20 +217,25 @@ impl LocalAIController {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(key) = self
|
if let Ok(workspace_id) = self.user_service.workspace_id() {
|
||||||
.user_service
|
self.is_enabled_on_workspace(&workspace_id.to_string())
|
||||||
.workspace_id()
|
|
||||||
.map(|workspace_id| local_ai_enabled_key(&workspace_id))
|
|
||||||
{
|
|
||||||
match self.upgrade_store_preferences() {
|
|
||||||
Ok(store) => store.get_bool(&key).unwrap_or(false),
|
|
||||||
Err(_) => false,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_enabled_on_workspace(&self, workspace_id: &str) -> bool {
|
||||||
|
let key = local_ai_enabled_key(workspace_id);
|
||||||
|
if !get_operating_system().is_desktop() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.upgrade_store_preferences() {
|
||||||
|
Ok(store) => store.get_bool(&key).unwrap_or(false),
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_plugin_chat_model(&self) -> Option<String> {
|
pub fn get_plugin_chat_model(&self) -> Option<String> {
|
||||||
if !self.is_enabled() {
|
if !self.is_enabled() {
|
||||||
return None;
|
return None;
|
||||||
|
@ -298,7 +295,8 @@ impl LocalAIController {
|
||||||
);
|
);
|
||||||
|
|
||||||
if self.resource.set_llm_setting(setting).await.is_ok() {
|
if self.resource.set_llm_setting(setting).await.is_ok() {
|
||||||
self.reload().await?;
|
let is_enabled = self.is_enabled();
|
||||||
|
self.toggle_plugin(is_enabled).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -373,7 +371,7 @@ impl LocalAIController {
|
||||||
|
|
||||||
pub async fn toggle_local_ai(&self) -> FlowyResult<bool> {
|
pub async fn toggle_local_ai(&self) -> FlowyResult<bool> {
|
||||||
let workspace_id = self.user_service.workspace_id()?;
|
let workspace_id = self.user_service.workspace_id()?;
|
||||||
let key = local_ai_enabled_key(&workspace_id);
|
let key = local_ai_enabled_key(&workspace_id.to_string());
|
||||||
let store_preferences = self.upgrade_store_preferences()?;
|
let store_preferences = self.upgrade_store_preferences()?;
|
||||||
let enabled = !store_preferences.get_bool(&key).unwrap_or(true);
|
let enabled = !store_preferences.get_bool(&key).unwrap_or(true);
|
||||||
store_preferences.set_bool(&key, enabled)?;
|
store_preferences.set_bool(&key, enabled)?;
|
||||||
|
@ -482,7 +480,7 @@ impl LocalAIController {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip_all)]
|
#[instrument(level = "debug", skip_all)]
|
||||||
async fn toggle_plugin(&self, enabled: bool) -> FlowyResult<()> {
|
pub(crate) async fn toggle_plugin(&self, enabled: bool) -> FlowyResult<()> {
|
||||||
info!(
|
info!(
|
||||||
"[AI Plugin] enable: {}, thread id: {:?}",
|
"[AI Plugin] enable: {}, thread id: {:?}",
|
||||||
enabled,
|
enabled,
|
||||||
|
@ -618,6 +616,6 @@ impl LLMResourceService for LLMResourceServiceImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
const APPFLOWY_LOCAL_AI_ENABLED: &str = "appflowy_local_ai_enabled";
|
const APPFLOWY_LOCAL_AI_ENABLED: &str = "appflowy_local_ai_enabled";
|
||||||
fn local_ai_enabled_key(workspace_id: &Uuid) -> String {
|
fn local_ai_enabled_key(workspace_id: &str) -> String {
|
||||||
format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id)
|
format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::ai_manager::AIUserService;
|
|
||||||
use crate::local_ai::controller::LocalAISetting;
|
use crate::local_ai::controller::LocalAISetting;
|
||||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||||
use lib_infra::async_trait::async_trait;
|
use lib_infra::async_trait::async_trait;
|
||||||
|
@ -11,6 +10,7 @@ use crate::notification::{
|
||||||
};
|
};
|
||||||
use af_local_ai::ollama_plugin::OllamaPluginConfig;
|
use af_local_ai::ollama_plugin::OllamaPluginConfig;
|
||||||
use af_plugin::core::path::{is_plugin_ready, ollama_plugin_path};
|
use af_plugin::core::path::{is_plugin_ready, ollama_plugin_path};
|
||||||
|
use flowy_ai_pub::user_service::AIUserService;
|
||||||
use lib_infra::util::{get_operating_system, OperatingSystem};
|
use lib_infra::util::{get_operating_system, OperatingSystem};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::ai_manager::AIUserService;
|
|
||||||
use crate::entities::{ChatStatePB, ModelTypePB};
|
use crate::entities::{ChatStatePB, ModelTypePB};
|
||||||
use crate::local_ai::controller::LocalAIController;
|
use crate::local_ai::controller::LocalAIController;
|
||||||
use crate::notification::{
|
use crate::notification::{
|
||||||
|
@ -19,6 +18,7 @@ use futures::{stream, StreamExt, TryStreamExt};
|
||||||
use lib_infra::async_trait::async_trait;
|
use lib_infra::async_trait::async_trait;
|
||||||
|
|
||||||
use crate::local_ai::stream_util::QuestionStream;
|
use crate::local_ai::stream_util::QuestionStream;
|
||||||
|
use flowy_ai_pub::user_service::AIUserService;
|
||||||
use flowy_storage_pub::storage::StorageService;
|
use flowy_storage_pub::storage::StorageService;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::ai_manager::AIUserService;
|
|
||||||
use flowy_ai_pub::cloud::{
|
use flowy_ai_pub::cloud::{
|
||||||
AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams,
|
AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams,
|
||||||
MessageCursor, ModelList, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat,
|
MessageCursor, ModelList, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat,
|
||||||
|
@ -8,6 +7,7 @@ use flowy_ai_pub::persistence::{
|
||||||
update_chat_is_sync, update_chat_message_is_sync, upsert_chat, upsert_chat_messages,
|
update_chat_is_sync, update_chat_message_is_sync, upsert_chat, upsert_chat_messages,
|
||||||
ChatMessageTable, ChatTable,
|
ChatMessageTable, ChatTable,
|
||||||
};
|
};
|
||||||
|
use flowy_ai_pub::user_service::AIUserService;
|
||||||
use flowy_error::FlowyError;
|
use flowy_error::FlowyError;
|
||||||
use lib_infra::async_trait::async_trait;
|
use lib_infra::async_trait::async_trait;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
|
@ -5,9 +5,10 @@ use collab::preclude::{Collab, StateVector};
|
||||||
use collab::util::is_change_since_sv;
|
use collab::util::is_change_since_sv;
|
||||||
use collab_entity::CollabType;
|
use collab_entity::CollabType;
|
||||||
use collab_integrate::persistence::collab_metadata_sql::AFCollabMetadata;
|
use collab_integrate::persistence::collab_metadata_sql::AFCollabMetadata;
|
||||||
use flowy_ai::ai_manager::{AIExternalService, AIManager, AIUserService};
|
use flowy_ai::ai_manager::{AIExternalService, AIManager};
|
||||||
use flowy_ai::local_ai::controller::LocalAIController;
|
use flowy_ai::local_ai::controller::LocalAIController;
|
||||||
use flowy_ai_pub::cloud::ChatCloudService;
|
use flowy_ai_pub::cloud::ChatCloudService;
|
||||||
|
use flowy_ai_pub::user_service::AIUserService;
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use flowy_folder::ViewLayout;
|
use flowy_folder::ViewLayout;
|
||||||
use flowy_folder_pub::cloud::{FolderCloudService, FullSyncCollabParams};
|
use flowy_folder_pub::cloud::{FolderCloudService, FullSyncCollabParams};
|
||||||
|
@ -153,7 +154,7 @@ impl AIExternalService for ChatQueryServiceImpl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChatUserServiceImpl(Weak<AuthenticateUser>);
|
pub struct ChatUserServiceImpl(Weak<AuthenticateUser>);
|
||||||
impl ChatUserServiceImpl {
|
impl ChatUserServiceImpl {
|
||||||
fn upgrade_user(&self) -> Result<Arc<AuthenticateUser>, FlowyError> {
|
fn upgrade_user(&self) -> Result<Arc<AuthenticateUser>, FlowyError> {
|
||||||
let user = self
|
let user = self
|
||||||
|
|
|
@ -161,6 +161,7 @@ impl StorageCloudService for ServerProvider {
|
||||||
impl UserCloudServiceProvider for ServerProvider {
|
impl UserCloudServiceProvider for ServerProvider {
|
||||||
fn set_token(&self, token: &str) -> Result<(), FlowyError> {
|
fn set_token(&self, token: &str) -> Result<(), FlowyError> {
|
||||||
let server = self.get_server()?;
|
let server = self.get_server()?;
|
||||||
|
info!("Set token");
|
||||||
server.set_token(token)?;
|
server.set_token(token)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -191,8 +192,12 @@ impl UserCloudServiceProvider for ServerProvider {
|
||||||
/// to create a new [AppFlowyServer] if it doesn't exist. Once the [AuthType] is set,
|
/// to create a new [AppFlowyServer] if it doesn't exist. Once the [AuthType] is set,
|
||||||
/// it will be used when user open the app again.
|
/// it will be used when user open the app again.
|
||||||
///
|
///
|
||||||
fn set_server_auth_type(&self, auth_type: &AuthType) {
|
fn set_server_auth_type(&self, auth_type: &AuthType, token: Option<String>) -> FlowyResult<()> {
|
||||||
self.set_auth_type(*auth_type);
|
self.set_auth_type(*auth_type);
|
||||||
|
if let Some(token) = token {
|
||||||
|
self.set_token(&token)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_server_auth_type(&self) -> AuthType {
|
fn get_server_auth_type(&self) -> AuthType {
|
||||||
|
|
|
@ -5,10 +5,8 @@ use dashmap::mapref::one::Ref;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use flowy_ai::local_ai::controller::LocalAIController;
|
use flowy_ai::local_ai::controller::LocalAIController;
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use flowy_server::af_cloud::{
|
use flowy_server::af_cloud::define::AIUserServiceImpl;
|
||||||
define::{AIUserServiceImpl, LoggedUser},
|
use flowy_server::af_cloud::{define::LoggedUser, AppFlowyCloudServer};
|
||||||
AppFlowyCloudServer,
|
|
||||||
};
|
|
||||||
use flowy_server::local_server::LocalServer;
|
use flowy_server::local_server::LocalServer;
|
||||||
use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl};
|
use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl};
|
||||||
use flowy_server_pub::AuthenticatorType;
|
use flowy_server_pub::AuthenticatorType;
|
||||||
|
@ -117,12 +115,14 @@ impl ServerProvider {
|
||||||
.cloud_config
|
.cloud_config
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or_else(|| FlowyError::internal().with_context("Missing cloud config"))?;
|
.ok_or_else(|| FlowyError::internal().with_context("Missing cloud config"))?;
|
||||||
|
let ai_user_service = Arc::new(AIUserServiceImpl(Arc::downgrade(&self.logged_user)));
|
||||||
Arc::new(AppFlowyCloudServer::new(
|
Arc::new(AppFlowyCloudServer::new(
|
||||||
cfg,
|
cfg,
|
||||||
self.user_enable_sync.load(Ordering::Acquire),
|
self.user_enable_sync.load(Ordering::Acquire),
|
||||||
self.config.device_id.clone(),
|
self.config.device_id.clone(),
|
||||||
self.config.app_version.clone(),
|
self.config.app_version.clone(),
|
||||||
Arc::downgrade(&self.logged_user),
|
Arc::downgrade(&self.logged_user),
|
||||||
|
ai_user_service,
|
||||||
))
|
))
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -38,15 +38,6 @@ pub(crate) struct UserStatusCallbackImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserStatusCallbackImpl {
|
impl UserStatusCallbackImpl {
|
||||||
fn init_ai_component(&self, workspace_id: String) {
|
|
||||||
let cloned_ai_manager = self.ai_manager.clone();
|
|
||||||
self.runtime.spawn(async move {
|
|
||||||
if let Err(err) = cloned_ai_manager.initialize(&workspace_id).await {
|
|
||||||
error!("Failed to initialize AIManager: {:?}", err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn folder_init_data_source(
|
async fn folder_init_data_source(
|
||||||
&self,
|
&self,
|
||||||
user_id: i64,
|
user_id: i64,
|
||||||
|
@ -95,7 +86,6 @@ impl UserStatusCallback for UserStatusCallbackImpl {
|
||||||
auth_type: &AuthType,
|
auth_type: &AuthType,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
let workspace_id = user_workspace.workspace_id()?;
|
let workspace_id = user_workspace.workspace_id()?;
|
||||||
|
|
||||||
if let Some(cloud_config) = cloud_config {
|
if let Some(cloud_config) = cloud_config {
|
||||||
self
|
self
|
||||||
.server_provider
|
.server_provider
|
||||||
|
@ -124,7 +114,15 @@ impl UserStatusCallback for UserStatusCallbackImpl {
|
||||||
self.document_manager.initialize(user_id).await?;
|
self.document_manager.initialize(user_id).await?;
|
||||||
|
|
||||||
let workspace_id = user_workspace.id.clone();
|
let workspace_id = user_workspace.id.clone();
|
||||||
self.init_ai_component(workspace_id);
|
let cloned_ai_manager = self.ai_manager.clone();
|
||||||
|
self.runtime.spawn(async move {
|
||||||
|
if let Err(err) = cloned_ai_manager
|
||||||
|
.on_launch_if_authenticated(&workspace_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!("Failed to initialize AIManager: {:?}", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,8 +156,11 @@ impl UserStatusCallback for UserStatusCallbackImpl {
|
||||||
.initialize_after_sign_in(user_id)
|
.initialize_after_sign_in(user_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let workspace_id = user_workspace.id.clone();
|
self
|
||||||
self.init_ai_component(workspace_id);
|
.ai_manager
|
||||||
|
.initialize_after_sign_in(&user_workspace.id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,8 +208,10 @@ impl UserStatusCallback for UserStatusCallbackImpl {
|
||||||
.await
|
.await
|
||||||
.context("DocumentManager error")?;
|
.context("DocumentManager error")?;
|
||||||
|
|
||||||
let workspace_id = user_workspace.id.clone();
|
self
|
||||||
self.init_ai_component(workspace_id);
|
.ai_manager
|
||||||
|
.initialize_after_sign_up(&user_workspace.id)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,14 @@ use collab_folder::{View, ViewIcon, ViewLayout};
|
||||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||||
use flowy_error::ErrorCode;
|
use flowy_error::ErrorCode;
|
||||||
use flowy_folder_pub::cloud::gen_view_id;
|
use flowy_folder_pub::cloud::gen_view_id;
|
||||||
|
use lib_infra::validator_fn::required_not_empty_str;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
use crate::entities::icon::ViewIconPB;
|
use crate::entities::icon::ViewIconPB;
|
||||||
use crate::entities::parser::view::{ViewIdentify, ViewName, ViewThumbnail};
|
use crate::entities::parser::view::{ViewIdentify, ViewName, ViewThumbnail};
|
||||||
|
@ -394,9 +396,10 @@ impl TryInto<CreateViewParams> for CreateOrphanViewPayloadPB {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, ProtoBuf, Clone, Debug)]
|
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
|
||||||
pub struct ViewIdPB {
|
pub struct ViewIdPB {
|
||||||
#[pb(index = 1)]
|
#[pb(index = 1)]
|
||||||
|
#[validate(custom(function = "required_not_empty_str"))]
|
||||||
pub value: String,
|
pub value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ pub(crate) async fn get_view_handler(
|
||||||
folder: AFPluginState<Weak<FolderManager>>,
|
folder: AFPluginState<Weak<FolderManager>>,
|
||||||
) -> DataResult<ViewPB, FlowyError> {
|
) -> DataResult<ViewPB, FlowyError> {
|
||||||
let folder = upgrade_folder(folder)?;
|
let folder = upgrade_folder(folder)?;
|
||||||
let view_id: ViewIdPB = data.into_inner();
|
let view_id = data.try_into_inner()?;
|
||||||
let view_pb = folder.get_view_pb(&view_id.value).await?;
|
let view_pb = folder.get_view_pb(&view_id.value).await?;
|
||||||
data_result_ok(view_pb)
|
data_result_ok(view_pb)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ use collab_integrate::CollabKVDB;
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::task::spawn_blocking;
|
||||||
use tracing::{event, info, Level};
|
use tracing::{error, event, info, Level};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
impl FolderManager {
|
impl FolderManager {
|
||||||
|
@ -139,9 +139,12 @@ impl FolderManager {
|
||||||
);
|
);
|
||||||
|
|
||||||
let weak_folder_indexer = Arc::downgrade(&self.folder_indexer);
|
let weak_folder_indexer = Arc::downgrade(&self.folder_indexer);
|
||||||
|
let workspace_id = *workspace_id;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Some(folder_indexer) = weak_folder_indexer.upgrade() {
|
if let Some(folder_indexer) = weak_folder_indexer.upgrade() {
|
||||||
folder_indexer.initialize().await;
|
if let Err(err) = folder_indexer.initialize(&workspace_id).await {
|
||||||
|
error!("Failed to initialize folder indexer: {:?}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ pub trait IndexManager: Send + Sync {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait FolderIndexManager: IndexManager {
|
pub trait FolderIndexManager: IndexManager {
|
||||||
async fn initialize(&self);
|
async fn initialize(&self, workspace_id: &Uuid) -> Result<(), FlowyError>;
|
||||||
|
|
||||||
fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: Uuid);
|
fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: Uuid);
|
||||||
|
|
||||||
|
|
|
@ -35,8 +35,6 @@ impl Drop for TantivyState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const FOLDER_INDEX_DIR: &str = "folder_index";
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FolderIndexManagerImpl {
|
pub struct FolderIndexManagerImpl {
|
||||||
auth_user: Weak<AuthenticateUser>,
|
auth_user: Weak<AuthenticateUser>,
|
||||||
|
@ -64,7 +62,7 @@ impl FolderIndexManagerImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes the state using the workspace directory.
|
/// Initializes the state using the workspace directory.
|
||||||
async fn initialize(&self) -> FlowyResult<()> {
|
async fn initialize(&self, workspace_id: &Uuid) -> FlowyResult<()> {
|
||||||
if let Some(state) = self.state.write().await.take() {
|
if let Some(state) = self.state.write().await.take() {
|
||||||
info!("Re-initializing folder indexer");
|
info!("Re-initializing folder indexer");
|
||||||
drop(state);
|
drop(state);
|
||||||
|
@ -82,7 +80,7 @@ impl FolderIndexManagerImpl {
|
||||||
.upgrade()
|
.upgrade()
|
||||||
.ok_or_else(|| FlowyError::internal().with_context("AuthenticateUser is not available"))?;
|
.ok_or_else(|| FlowyError::internal().with_context("AuthenticateUser is not available"))?;
|
||||||
|
|
||||||
let index_path = auth_user.get_index_path()?.join(FOLDER_INDEX_DIR);
|
let index_path = auth_user.get_index_path()?.join(workspace_id.to_string());
|
||||||
if !index_path.exists() {
|
if !index_path.exists() {
|
||||||
fs::create_dir_all(&index_path).map_err(|e| {
|
fs::create_dir_all(&index_path).map_err(|e| {
|
||||||
error!("Failed to create folder index directory: {:?}", e);
|
error!("Failed to create folder index directory: {:?}", e);
|
||||||
|
@ -327,10 +325,9 @@ impl IndexManager for FolderIndexManagerImpl {
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl FolderIndexManager for FolderIndexManagerImpl {
|
impl FolderIndexManager for FolderIndexManagerImpl {
|
||||||
async fn initialize(&self) {
|
async fn initialize(&self, workspace_id: &Uuid) -> Result<(), FlowyError> {
|
||||||
if let Err(e) = self.initialize().await {
|
self.initialize(workspace_id).await?;
|
||||||
error!("Failed to initialize FolderIndexManager: {:?}", e);
|
Ok(())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: Uuid) {
|
fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: Uuid) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use collab_plugins::CollabKVDB;
|
use collab_plugins::CollabKVDB;
|
||||||
use flowy_ai::ai_manager::AIUserService;
|
use flowy_ai_pub::user_service::AIUserService;
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use flowy_sqlite::DBConnection;
|
use flowy_sqlite::DBConnection;
|
||||||
use lib_infra::async_trait::async_trait;
|
use lib_infra::async_trait::async_trait;
|
||||||
|
@ -28,6 +28,7 @@ pub trait LoggedUser: Send + Sync {
|
||||||
fn application_root_dir(&self) -> Result<PathBuf, FlowyError>;
|
fn application_root_dir(&self) -> Result<PathBuf, FlowyError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
pub struct AIUserServiceImpl(pub Weak<dyn LoggedUser>);
|
pub struct AIUserServiceImpl(pub Weak<dyn LoggedUser>);
|
||||||
|
|
||||||
impl AIUserServiceImpl {
|
impl AIUserServiceImpl {
|
||||||
|
|
|
@ -656,6 +656,7 @@ fn to_user_workspace(af_workspace: AFWorkspace) -> UserWorkspace {
|
||||||
icon: af_workspace.icon,
|
icon: af_workspace.icon,
|
||||||
member_count: af_workspace.member_count.unwrap_or(0),
|
member_count: af_workspace.member_count.unwrap_or(0),
|
||||||
role: af_workspace.role.map(|r| r.into()),
|
role: af_workspace.role.map(|r| r.into()),
|
||||||
|
workspace_type: AuthType::AppFlowyCloud,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::af_cloud::define::{AIUserServiceImpl, LoggedUser};
|
use crate::af_cloud::define::LoggedUser;
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use client_api::collab_sync::ServerCollabMessage;
|
use client_api::collab_sync::ServerCollabMessage;
|
||||||
|
@ -28,7 +28,9 @@ use crate::af_cloud::impls::{
|
||||||
AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, AFCloudFileStorageServiceImpl,
|
AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, AFCloudFileStorageServiceImpl,
|
||||||
AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl, CloudChatServiceImpl,
|
AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl, CloudChatServiceImpl,
|
||||||
};
|
};
|
||||||
|
use crate::AppFlowyServer;
|
||||||
use flowy_ai::offline::offline_message_sync::AutoSyncChatService;
|
use flowy_ai::offline::offline_message_sync::AutoSyncChatService;
|
||||||
|
use flowy_ai_pub::user_service::AIUserService;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
|
@ -39,8 +41,6 @@ use tokio_util::sync::CancellationToken;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::AppFlowyServer;
|
|
||||||
|
|
||||||
use super::impls::AFCloudSearchCloudServiceImpl;
|
use super::impls::AFCloudSearchCloudServiceImpl;
|
||||||
|
|
||||||
pub(crate) type AFCloudClient = Client;
|
pub(crate) type AFCloudClient = Client;
|
||||||
|
@ -54,6 +54,7 @@ pub struct AppFlowyCloudServer {
|
||||||
pub device_id: String,
|
pub device_id: String,
|
||||||
ws_client: Arc<WSClient>,
|
ws_client: Arc<WSClient>,
|
||||||
logged_user: Weak<dyn LoggedUser>,
|
logged_user: Weak<dyn LoggedUser>,
|
||||||
|
ai_user_service: Arc<dyn AIUserService>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppFlowyCloudServer {
|
impl AppFlowyCloudServer {
|
||||||
|
@ -63,6 +64,7 @@ impl AppFlowyCloudServer {
|
||||||
mut device_id: String,
|
mut device_id: String,
|
||||||
client_version: Version,
|
client_version: Version,
|
||||||
logged_user: Weak<dyn LoggedUser>,
|
logged_user: Weak<dyn LoggedUser>,
|
||||||
|
ai_user_service: Arc<dyn AIUserService>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// The device id can't be empty, so we generate a new one if it is.
|
// The device id can't be empty, so we generate a new one if it is.
|
||||||
if device_id.is_empty() {
|
if device_id.is_empty() {
|
||||||
|
@ -101,6 +103,7 @@ impl AppFlowyCloudServer {
|
||||||
device_id,
|
device_id,
|
||||||
ws_client,
|
ws_client,
|
||||||
logged_user,
|
logged_user,
|
||||||
|
ai_user_service,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,7 +225,7 @@ impl AppFlowyServer for AppFlowyCloudServer {
|
||||||
Arc::new(CloudChatServiceImpl {
|
Arc::new(CloudChatServiceImpl {
|
||||||
inner: self.get_server_impl(),
|
inner: self.get_server_impl(),
|
||||||
}),
|
}),
|
||||||
Arc::new(AIUserServiceImpl(self.logged_user.clone())),
|
self.ai_user_service.clone(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use crate::af_cloud::define::LoggedUser;
|
use crate::af_cloud::define::LoggedUser;
|
||||||
use crate::local_server::uid::UserIDGenerator;
|
use crate::local_server::uid::UserIDGenerator;
|
||||||
|
use anyhow::Context;
|
||||||
use client_api::entity::GotrueTokenResponse;
|
use client_api::entity::GotrueTokenResponse;
|
||||||
use collab::core::origin::CollabOrigin;
|
use collab::core::origin::CollabOrigin;
|
||||||
use collab::preclude::Collab;
|
use collab::preclude::Collab;
|
||||||
|
@ -13,10 +14,10 @@ use flowy_error::FlowyError;
|
||||||
use flowy_user_pub::cloud::{UserCloudService, UserCollabParams};
|
use flowy_user_pub::cloud::{UserCloudService, UserCollabParams};
|
||||||
use flowy_user_pub::entities::*;
|
use flowy_user_pub::entities::*;
|
||||||
use flowy_user_pub::sql::{
|
use flowy_user_pub::sql::{
|
||||||
select_all_user_workspace, select_user_profile, select_user_workspace, select_workspace_member,
|
insert_local_workspace, select_all_user_workspace, select_user_profile, select_user_workspace,
|
||||||
select_workspace_setting, update_user_profile, update_workspace_setting, upsert_workspace_member,
|
select_workspace_member, select_workspace_setting, update_user_profile, update_workspace_setting,
|
||||||
upsert_workspace_setting, UserTableChangeset, WorkspaceMemberTable, WorkspaceSettingsChangeset,
|
upsert_workspace_member, upsert_workspace_setting, UserTableChangeset, WorkspaceMemberTable,
|
||||||
WorkspaceSettingsTable,
|
WorkspaceSettingsChangeset, WorkspaceSettingsTable,
|
||||||
};
|
};
|
||||||
use flowy_user_pub::DEFAULT_USER_NAME;
|
use flowy_user_pub::DEFAULT_USER_NAME;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
@ -161,10 +162,11 @@ impl UserCloudService for LocalServerUserServiceImpl {
|
||||||
|
|
||||||
async fn create_workspace(&self, workspace_name: &str) -> Result<UserWorkspace, FlowyError> {
|
async fn create_workspace(&self, workspace_name: &str) -> Result<UserWorkspace, FlowyError> {
|
||||||
let workspace_id = Uuid::new_v4();
|
let workspace_id = Uuid::new_v4();
|
||||||
Ok(UserWorkspace::new_local(
|
let uid = self.logged_user.user_id()?;
|
||||||
workspace_id.to_string(),
|
let mut conn = self.logged_user.get_sqlite_db(uid)?;
|
||||||
workspace_name,
|
let user_workspace =
|
||||||
))
|
insert_local_workspace(uid, &workspace_id.to_string(), workspace_name, &mut conn)?;
|
||||||
|
Ok(user_workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn patch_workspace(
|
async fn patch_workspace(
|
||||||
|
@ -180,6 +182,15 @@ impl UserCloudService for LocalServerUserServiceImpl {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_workspace_members(
|
||||||
|
&self,
|
||||||
|
workspace_id: Uuid,
|
||||||
|
) -> Result<Vec<WorkspaceMember>, FlowyError> {
|
||||||
|
let uid = self.logged_user.user_id()?;
|
||||||
|
let member = self.get_workspace_member(&workspace_id, uid).await?;
|
||||||
|
Ok(vec![member])
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_user_awareness_doc_state(
|
async fn get_user_awareness_doc_state(
|
||||||
&self,
|
&self,
|
||||||
uid: i64,
|
uid: i64,
|
||||||
|
@ -227,15 +238,16 @@ impl UserCloudService for LocalServerUserServiceImpl {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if err.is_record_not_found() {
|
if err.is_record_not_found() {
|
||||||
let mut conn = self.logged_user.get_sqlite_db(uid)?;
|
let mut conn = self.logged_user.get_sqlite_db(uid)?;
|
||||||
let profile = select_user_profile(uid, &workspace_id.to_string(), &mut conn)?;
|
let profile = select_user_profile(uid, &workspace_id.to_string(), &mut conn)
|
||||||
|
.context("Can't find user profile when create workspace member")?;
|
||||||
let row = WorkspaceMemberTable {
|
let row = WorkspaceMemberTable {
|
||||||
email: profile.email.to_string(),
|
email: profile.email.to_string(),
|
||||||
role: 0,
|
role: Role::Owner as i32,
|
||||||
name: profile.name.to_string(),
|
name: profile.name.to_string(),
|
||||||
avatar_url: Some(profile.icon_url),
|
avatar_url: Some(profile.icon_url),
|
||||||
uid,
|
uid,
|
||||||
workspace_id: workspace_id.to_string(),
|
workspace_id: workspace_id.to_string(),
|
||||||
updated_at: Default::default(),
|
updated_at: chrono::Utc::now().naive_utc(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let member = WorkspaceMember::from(row.clone());
|
let member = WorkspaceMember::from(row.clone());
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
mod user_test;
|
|
||||||
mod util;
|
|
|
@ -1,21 +0,0 @@
|
||||||
use flowy_server::AppFlowyServer;
|
|
||||||
use flowy_user_pub::entities::AuthResponse;
|
|
||||||
use lib_infra::box_any::BoxAny;
|
|
||||||
|
|
||||||
use crate::af_cloud_test::util::{
|
|
||||||
af_cloud_server, af_cloud_sign_up_param, generate_test_email, get_af_cloud_config,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn sign_up_test() {
|
|
||||||
if let Some(config) = get_af_cloud_config() {
|
|
||||||
let server = af_cloud_server(config.clone());
|
|
||||||
let user_service = server.user_service();
|
|
||||||
let email = generate_test_email();
|
|
||||||
let params = af_cloud_sign_up_param(&email, &config).await;
|
|
||||||
let resp: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
|
|
||||||
assert_eq!(resp.email.unwrap(), email);
|
|
||||||
assert!(resp.is_new_user);
|
|
||||||
assert_eq!(resp.user_workspaces.len(), 1);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
use client_api::ClientConfiguration;
|
|
||||||
use collab_plugins::CollabKVDB;
|
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
|
||||||
use semver::Version;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::{Arc, Weak};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::setup_log;
|
|
||||||
use flowy_server::af_cloud::define::LoggedUser;
|
|
||||||
use flowy_server::af_cloud::AppFlowyCloudServer;
|
|
||||||
use flowy_server_pub::af_cloud_config::AFCloudConfiguration;
|
|
||||||
use flowy_sqlite::DBConnection;
|
|
||||||
use lib_infra::async_trait::async_trait;
|
|
||||||
|
|
||||||
/// To run the test, create a .env.ci file in the 'flowy-server' directory and set the following environment variables:
|
|
||||||
///
|
|
||||||
/// - `APPFLOWY_CLOUD_BASE_URL=http://localhost:8000`
|
|
||||||
/// - `APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws`
|
|
||||||
/// - `APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998`
|
|
||||||
///
|
|
||||||
/// - `GOTRUE_ADMIN_EMAIL=admin@example.com`
|
|
||||||
/// - `GOTRUE_ADMIN_PASSWORD=password`
|
|
||||||
pub fn get_af_cloud_config() -> Option<AFCloudConfiguration> {
|
|
||||||
dotenv::from_filename("./.env.ci").ok()?;
|
|
||||||
setup_log();
|
|
||||||
AFCloudConfiguration::from_env().ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc<AppFlowyCloudServer> {
|
|
||||||
let fake_device_id = uuid::Uuid::new_v4().to_string();
|
|
||||||
let logged_user = Arc::new(FakeServerUserImpl) as Arc<dyn LoggedUser>;
|
|
||||||
Arc::new(AppFlowyCloudServer::new(
|
|
||||||
config,
|
|
||||||
true,
|
|
||||||
fake_device_id,
|
|
||||||
Version::new(0, 5, 8),
|
|
||||||
// do nothing, just for test
|
|
||||||
Arc::downgrade(&logged_user),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FakeServerUserImpl;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl LoggedUser for FakeServerUserImpl {
|
|
||||||
fn workspace_id(&self) -> FlowyResult<Uuid> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn user_id(&self) -> FlowyResult<i64> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn is_local_mode(&self) -> FlowyResult<bool> {
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_sqlite_db(&self, _uid: i64) -> Result<DBConnection, FlowyError> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_collab_db(&self, _uid: i64) -> Result<Weak<CollabKVDB>, FlowyError> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn application_root_dir(&self) -> Result<PathBuf, FlowyError> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String {
|
|
||||||
let client = client_api::Client::new(
|
|
||||||
&config.base_url,
|
|
||||||
&config.ws_base_url,
|
|
||||||
&config.gotrue_url,
|
|
||||||
"fake_device_id",
|
|
||||||
ClientConfiguration::default(),
|
|
||||||
"test",
|
|
||||||
);
|
|
||||||
let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap();
|
|
||||||
let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap();
|
|
||||||
let admin_client = client_api::Client::new(
|
|
||||||
client.base_url(),
|
|
||||||
client.ws_addr(),
|
|
||||||
client.gotrue_url(),
|
|
||||||
"fake_device_id",
|
|
||||||
ClientConfiguration::default(),
|
|
||||||
&client.client_version.to_string(),
|
|
||||||
);
|
|
||||||
admin_client
|
|
||||||
.sign_in_password(&admin_email, &admin_password)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let action_link = admin_client
|
|
||||||
.generate_sign_in_action_link(user_email)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
client.extract_sign_in_url(&action_link).await.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn af_cloud_sign_up_param(
|
|
||||||
email: &str,
|
|
||||||
config: &AFCloudConfiguration,
|
|
||||||
) -> HashMap<String, String> {
|
|
||||||
let mut params = HashMap::new();
|
|
||||||
params.insert(
|
|
||||||
"sign_in_url".to_string(),
|
|
||||||
generate_sign_in_url(email, config).await,
|
|
||||||
);
|
|
||||||
params.insert("device_id".to_string(), Uuid::new_v4().to_string());
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_test_email() -> String {
|
|
||||||
format!("{}@test.com", Uuid::new_v4())
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
|
@ -1,24 +0,0 @@
|
||||||
use std::sync::Once;
|
|
||||||
|
|
||||||
use tracing_subscriber::fmt::Subscriber;
|
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
|
||||||
use tracing_subscriber::EnvFilter;
|
|
||||||
|
|
||||||
mod af_cloud_test;
|
|
||||||
// mod supabase_test;
|
|
||||||
|
|
||||||
pub fn setup_log() {
|
|
||||||
static START: Once = Once::new();
|
|
||||||
START.call_once(|| {
|
|
||||||
let level = "trace";
|
|
||||||
let mut filters = vec![];
|
|
||||||
filters.push(format!("flowy_server={}", level));
|
|
||||||
std::env::set_var("RUST_LOG", filters.join(","));
|
|
||||||
|
|
||||||
let subscriber = Subscriber::builder()
|
|
||||||
.with_env_filter(EnvFilter::from_default_env())
|
|
||||||
.with_ansi(true)
|
|
||||||
.finish();
|
|
||||||
subscriber.try_init().unwrap();
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
use collab::core::collab::DataSource;
|
|
||||||
use collab_entity::{CollabObject, CollabType};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use flowy_user_pub::entities::AuthResponse;
|
|
||||||
use lib_infra::box_any::BoxAny;
|
|
||||||
|
|
||||||
use crate::supabase_test::util::{
|
|
||||||
collab_service, database_service, get_supabase_ci_config, third_party_sign_up_param,
|
|
||||||
user_auth_service,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_create_database_test() {
|
|
||||||
if get_supabase_ci_config().is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_service = user_auth_service();
|
|
||||||
let uuid = Uuid::new_v4().to_string();
|
|
||||||
let params = third_party_sign_up_param(uuid);
|
|
||||||
let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
|
|
||||||
|
|
||||||
let collab_service = collab_service();
|
|
||||||
let database_service = database_service();
|
|
||||||
|
|
||||||
let mut row_ids = vec![];
|
|
||||||
for _i in 0..3 {
|
|
||||||
let row_id = uuid::Uuid::new_v4().to_string();
|
|
||||||
row_ids.push(row_id.clone());
|
|
||||||
let collab_object = CollabObject::new(
|
|
||||||
user.user_id,
|
|
||||||
row_id,
|
|
||||||
CollabType::DatabaseRow,
|
|
||||||
user.latest_workspace.id.clone(),
|
|
||||||
"fake_device_id".to_string(),
|
|
||||||
);
|
|
||||||
collab_service
|
|
||||||
.send_update(&collab_object, 0, vec![1, 2, 3])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
collab_service
|
|
||||||
.send_update(&collab_object, 0, vec![4, 5, 6])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let updates_by_oid = database_service
|
|
||||||
.batch_get_database_object_doc_state(row_ids, CollabType::DatabaseRow, "fake_workspace_id")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(updates_by_oid.len(), 3);
|
|
||||||
for (_, source) in updates_by_oid {
|
|
||||||
match source {
|
|
||||||
DataSource::Disk => panic!("should not be from disk"),
|
|
||||||
DataSource::DocStateV1(doc_state) => {
|
|
||||||
assert_eq!(doc_state.len(), 2);
|
|
||||||
},
|
|
||||||
DataSource::DocStateV2(_) => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
// use url::Url;
|
|
||||||
// use uuid::Uuid;
|
|
||||||
//
|
|
||||||
// use flowy_storage::StorageObject;
|
|
||||||
//
|
|
||||||
// use crate::supabase_test::util::{file_storage_service, get_supabase_ci_config};
|
|
||||||
//
|
|
||||||
// #[tokio::test]
|
|
||||||
// async fn supabase_get_object_test() {
|
|
||||||
// if get_supabase_ci_config().is_none() {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let service = file_storage_service();
|
|
||||||
// let file_name = format!("test-{}.txt", Uuid::new_v4());
|
|
||||||
// let object = StorageObject::from_file("1", &file_name, "tests/test.txt");
|
|
||||||
//
|
|
||||||
// // Upload a file
|
|
||||||
// let url = service
|
|
||||||
// .create_object(object)
|
|
||||||
// .await
|
|
||||||
// .unwrap()
|
|
||||||
// .parse::<Url>()
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
// // The url would be something like:
|
|
||||||
// // https://acfrqdbdtbsceyjbxsfc.supabase.co/storage/v1/object/data/test-1693472809.txt
|
|
||||||
// let name = url.path_segments().unwrap().last().unwrap();
|
|
||||||
// assert_eq!(name, &file_name);
|
|
||||||
//
|
|
||||||
// // Download the file
|
|
||||||
// let bytes = service.get_object(url.to_string()).await.unwrap();
|
|
||||||
// let s = String::from_utf8(bytes.to_vec()).unwrap();
|
|
||||||
// assert_eq!(s, "hello world");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// #[tokio::test]
|
|
||||||
// async fn supabase_upload_image_test() {
|
|
||||||
// if get_supabase_ci_config().is_none() {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let service = file_storage_service();
|
|
||||||
// let file_name = format!("image-{}.png", Uuid::new_v4());
|
|
||||||
// let object = StorageObject::from_file("1", &file_name, "tests/logo.png");
|
|
||||||
//
|
|
||||||
// // Upload a file
|
|
||||||
// let url = service
|
|
||||||
// .create_object(object)
|
|
||||||
// .await
|
|
||||||
// .unwrap()
|
|
||||||
// .parse::<Url>()
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
// // Download object by url
|
|
||||||
// let bytes = service.get_object(url.to_string()).await.unwrap();
|
|
||||||
// assert_eq!(bytes.len(), 15694);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// #[tokio::test]
|
|
||||||
// async fn supabase_delete_object_test() {
|
|
||||||
// if get_supabase_ci_config().is_none() {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// let service = file_storage_service();
|
|
||||||
// let file_name = format!("test-{}.txt", Uuid::new_v4());
|
|
||||||
// let object = StorageObject::from_file("1", &file_name, "tests/test.txt");
|
|
||||||
// let url = service.create_object(object).await.unwrap();
|
|
||||||
//
|
|
||||||
// let result = service.get_object(url.clone()).await;
|
|
||||||
// assert!(result.is_ok());
|
|
||||||
//
|
|
||||||
// let _ = service.delete_object(url.clone()).await;
|
|
||||||
//
|
|
||||||
// let result = service.get_object(url.clone()).await;
|
|
||||||
// assert!(result.is_err());
|
|
||||||
// }
|
|
|
@ -1,316 +0,0 @@
|
||||||
use assert_json_diff::assert_json_eq;
|
|
||||||
use collab_entity::{CollabObject, CollabType};
|
|
||||||
use serde_json::json;
|
|
||||||
use uuid::Uuid;
|
|
||||||
use yrs::types::ToJson;
|
|
||||||
use yrs::updates::decoder::Decode;
|
|
||||||
use yrs::{merge_updates_v1, Array, Doc, Map, MapPrelim, ReadTxn, StateVector, Transact, Update};
|
|
||||||
|
|
||||||
use flowy_user_pub::entities::AuthResponse;
|
|
||||||
use lib_infra::box_any::BoxAny;
|
|
||||||
|
|
||||||
use crate::supabase_test::util::{
|
|
||||||
collab_service, folder_service, get_supabase_ci_config, third_party_sign_up_param,
|
|
||||||
user_auth_service,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_create_workspace_test() {
|
|
||||||
if get_supabase_ci_config().is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let service = folder_service();
|
|
||||||
// will replace the uid with the real uid
|
|
||||||
let workspace = service.create_workspace(1, "test").await.unwrap();
|
|
||||||
dbg!(workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_get_folder_test() {
|
|
||||||
if get_supabase_ci_config().is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let folder_service = folder_service();
|
|
||||||
let user_service = user_auth_service();
|
|
||||||
let collab_service = collab_service();
|
|
||||||
let uuid = Uuid::new_v4().to_string();
|
|
||||||
let params = third_party_sign_up_param(uuid);
|
|
||||||
let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
|
|
||||||
|
|
||||||
let collab_object = CollabObject::new(
|
|
||||||
user.user_id,
|
|
||||||
user.latest_workspace.id.clone(),
|
|
||||||
CollabType::Folder,
|
|
||||||
user.latest_workspace.id.clone(),
|
|
||||||
"fake_device_id".to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let doc = Doc::with_client_id(1);
|
|
||||||
let map = { doc.get_or_insert_map("map") };
|
|
||||||
{
|
|
||||||
let mut txn = doc.transact_mut();
|
|
||||||
map.insert(&mut txn, "1", "a");
|
|
||||||
collab_service
|
|
||||||
.send_update(&collab_object, 0, txn.encode_update_v1())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut txn = doc.transact_mut();
|
|
||||||
map.insert(&mut txn, "2", "b");
|
|
||||||
collab_service
|
|
||||||
.send_update(&collab_object, 1, txn.encode_update_v1())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
};
|
|
||||||
|
|
||||||
// let updates = collab_service.get_all_updates(&collab_object).await.unwrap();
|
|
||||||
let updates = folder_service
|
|
||||||
.get_folder_doc_state(
|
|
||||||
&user.latest_workspace.id,
|
|
||||||
user.user_id,
|
|
||||||
CollabType::Folder,
|
|
||||||
&user.latest_workspace.id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(updates.len(), 2);
|
|
||||||
|
|
||||||
for _ in 0..5 {
|
|
||||||
collab_service
|
|
||||||
.send_init_sync(&collab_object, 3, vec![])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
let updates = folder_service
|
|
||||||
.get_folder_doc_state(
|
|
||||||
&user.latest_workspace.id,
|
|
||||||
user.user_id,
|
|
||||||
CollabType::Folder,
|
|
||||||
&user.latest_workspace.id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Other the init sync, try to get the updates from the server.
|
|
||||||
let expected_update = doc
|
|
||||||
.transact_mut()
|
|
||||||
.encode_state_as_update_v1(&StateVector::default());
|
|
||||||
|
|
||||||
// check the update is the same as local document update.
|
|
||||||
assert_eq!(updates, expected_update);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This async test function checks the behavior of updates duplication in Supabase.
|
|
||||||
/// It creates a new user and simulates two updates to the user's workspace with different values.
|
|
||||||
/// Then, it merges these updates and sends an initial synchronization request to test duplication handling.
|
|
||||||
/// Finally, it asserts that the duplicated updates don't affect the overall data consistency in Supabase.
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_duplicate_updates_test() {
|
|
||||||
if get_supabase_ci_config().is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let folder_service = folder_service();
|
|
||||||
let user_service = user_auth_service();
|
|
||||||
let collab_service = collab_service();
|
|
||||||
let uuid = Uuid::new_v4().to_string();
|
|
||||||
let params = third_party_sign_up_param(uuid);
|
|
||||||
let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
|
|
||||||
|
|
||||||
let collab_object = CollabObject::new(
|
|
||||||
user.user_id,
|
|
||||||
user.latest_workspace.id.clone(),
|
|
||||||
CollabType::Folder,
|
|
||||||
user.latest_workspace.id.clone(),
|
|
||||||
"fake_device_id".to_string(),
|
|
||||||
);
|
|
||||||
let doc = Doc::with_client_id(1);
|
|
||||||
let map = { doc.get_or_insert_map("map") };
|
|
||||||
let mut duplicated_updates = vec![];
|
|
||||||
{
|
|
||||||
let mut txn = doc.transact_mut();
|
|
||||||
map.insert(&mut txn, "1", "a");
|
|
||||||
let update = txn.encode_update_v1();
|
|
||||||
duplicated_updates.push(update.clone());
|
|
||||||
collab_service
|
|
||||||
.send_update(&collab_object, 0, update)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
};
|
|
||||||
{
|
|
||||||
let mut txn = doc.transact_mut();
|
|
||||||
map.insert(&mut txn, "2", "b");
|
|
||||||
let update = txn.encode_update_v1();
|
|
||||||
duplicated_updates.push(update.clone());
|
|
||||||
collab_service
|
|
||||||
.send_update(&collab_object, 1, update)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
};
|
|
||||||
// send init sync
|
|
||||||
collab_service
|
|
||||||
.send_init_sync(&collab_object, 3, vec![])
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let first_init_sync_update = folder_service
|
|
||||||
.get_folder_doc_state(
|
|
||||||
&user.latest_workspace.id,
|
|
||||||
user.user_id,
|
|
||||||
CollabType::Folder,
|
|
||||||
&user.latest_workspace.id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// simulate the duplicated updates.
|
|
||||||
let merged_update = merge_updates_v1(
|
|
||||||
&duplicated_updates
|
|
||||||
.iter()
|
|
||||||
.map(|update| update.as_ref())
|
|
||||||
.collect::<Vec<&[u8]>>(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
collab_service
|
|
||||||
.send_init_sync(&collab_object, 4, merged_update)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let second_init_sync_update = folder_service
|
|
||||||
.get_folder_doc_state(
|
|
||||||
&user.latest_workspace.id,
|
|
||||||
user.user_id,
|
|
||||||
CollabType::Folder,
|
|
||||||
&user.latest_workspace.id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let doc_2 = Doc::new();
|
|
||||||
assert_eq!(first_init_sync_update.len(), second_init_sync_update.len());
|
|
||||||
let map = { doc_2.get_or_insert_map("map") };
|
|
||||||
{
|
|
||||||
let mut txn = doc_2.transact_mut();
|
|
||||||
let update = Update::decode_v1(&second_init_sync_update).unwrap();
|
|
||||||
txn.apply_update(update).unwrap();
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let txn = doc_2.transact();
|
|
||||||
let json = map.to_json(&txn);
|
|
||||||
assert_json_eq!(
|
|
||||||
json,
|
|
||||||
json!({
|
|
||||||
"1": "a",
|
|
||||||
"2": "b"
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The state vector of doc;
|
|
||||||
/// ```json
|
|
||||||
/// "map": {},
|
|
||||||
/// "array": []
|
|
||||||
/// ```
|
|
||||||
/// The old version of doc:
|
|
||||||
/// ```json
|
|
||||||
/// "map": {}
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Try to apply the updates from doc to old version doc and check the result.
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_diff_state_vector_test() {
|
|
||||||
if get_supabase_ci_config().is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let folder_service = folder_service();
|
|
||||||
let user_service = user_auth_service();
|
|
||||||
let collab_service = collab_service();
|
|
||||||
let uuid = Uuid::new_v4().to_string();
|
|
||||||
let params = third_party_sign_up_param(uuid);
|
|
||||||
let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
|
|
||||||
|
|
||||||
let collab_object = CollabObject::new(
|
|
||||||
user.user_id,
|
|
||||||
user.latest_workspace.id.clone(),
|
|
||||||
CollabType::Folder,
|
|
||||||
user.latest_workspace.id.clone(),
|
|
||||||
"fake_device_id".to_string(),
|
|
||||||
);
|
|
||||||
let doc = Doc::with_client_id(1);
|
|
||||||
let map = { doc.get_or_insert_map("map") };
|
|
||||||
let array = { doc.get_or_insert_array("array") };
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut txn = doc.transact_mut();
|
|
||||||
map.insert(&mut txn, "1", "a");
|
|
||||||
map.insert(&mut txn, "inner_map", MapPrelim::<String>::new());
|
|
||||||
|
|
||||||
array.push_back(&mut txn, "element 1");
|
|
||||||
let update = txn.encode_update_v1();
|
|
||||||
collab_service
|
|
||||||
.send_update(&collab_object, 0, update)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
};
|
|
||||||
{
|
|
||||||
let mut txn = doc.transact_mut();
|
|
||||||
map.insert(&mut txn, "2", "b");
|
|
||||||
array.push_back(&mut txn, "element 2");
|
|
||||||
let update = txn.encode_update_v1();
|
|
||||||
collab_service
|
|
||||||
.send_update(&collab_object, 1, update)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
};
|
|
||||||
|
|
||||||
// restore the doc with given updates.
|
|
||||||
let old_version_doc = Doc::new();
|
|
||||||
let map = { old_version_doc.get_or_insert_map("map") };
|
|
||||||
let doc_state = folder_service
|
|
||||||
.get_folder_doc_state(
|
|
||||||
&user.latest_workspace.id,
|
|
||||||
user.user_id,
|
|
||||||
CollabType::Folder,
|
|
||||||
&user.latest_workspace.id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
{
|
|
||||||
let mut txn = old_version_doc.transact_mut();
|
|
||||||
let update = Update::decode_v1(&doc_state).unwrap();
|
|
||||||
txn.apply_update(update).unwrap();
|
|
||||||
}
|
|
||||||
let txn = old_version_doc.transact();
|
|
||||||
let json = map.to_json(&txn);
|
|
||||||
assert_json_eq!(
|
|
||||||
json,
|
|
||||||
json!({
|
|
||||||
"1": "a",
|
|
||||||
"2": "b",
|
|
||||||
"inner_map": {}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[tokio::test]
|
|
||||||
// async fn print_folder_object_test() {
|
|
||||||
// if get_supabase_dev_config().is_none() {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// let secret = Some("43bSxEPHeNkk5ZxxEYOfAjjd7sK2DJ$vVnxwuNc5ru0iKFvhs8wLg==".to_string());
|
|
||||||
// print_encryption_folder("f8b14b84-e8ec-4cf4-a318-c1e008ecfdfa", secret).await;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// #[tokio::test]
|
|
||||||
// async fn print_folder_snapshot_object_test() {
|
|
||||||
// if get_supabase_dev_config().is_none() {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// let secret = Some("NTXRXrDSybqFEm32jwMBDzbxvCtgjU$8np3TGywbBdJAzHtu1QIyQ==".to_string());
|
|
||||||
// // let secret = None;
|
|
||||||
// print_encryption_folder_snapshot("12533251-bdd4-41f4-995f-ff12fceeaa42", secret).await;
|
|
||||||
// }
|
|
|
@ -1,5 +0,0 @@
|
||||||
mod database_test;
|
|
||||||
mod file_test;
|
|
||||||
mod folder_test;
|
|
||||||
mod user_test;
|
|
||||||
mod util;
|
|
|
@ -1,141 +0,0 @@
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use flowy_encrypt::{encrypt_text, generate_encryption_secret};
|
|
||||||
use flowy_error::FlowyError;
|
|
||||||
use flowy_user_pub::entities::*;
|
|
||||||
use lib_infra::box_any::BoxAny;
|
|
||||||
|
|
||||||
use crate::supabase_test::util::{
|
|
||||||
get_supabase_ci_config, third_party_sign_up_param, user_auth_service,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ‼️‼️‼️ Warning: this test will create a table in the database
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_user_sign_up_test() {
|
|
||||||
if get_supabase_ci_config().is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let user_service = user_auth_service();
|
|
||||||
let uuid = Uuid::new_v4().to_string();
|
|
||||||
let params = third_party_sign_up_param(uuid);
|
|
||||||
let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
|
|
||||||
assert!(!user.latest_workspace.id.is_empty());
|
|
||||||
assert!(!user.user_workspaces.is_empty());
|
|
||||||
assert!(!user.latest_workspace.database_indexer_id.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_user_sign_up_with_existing_uuid_test() {
|
|
||||||
if get_supabase_ci_config().is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let user_service = user_auth_service();
|
|
||||||
let uuid = Uuid::new_v4().to_string();
|
|
||||||
let params = third_party_sign_up_param(uuid);
|
|
||||||
let _user: AuthResponse = user_service
|
|
||||||
.sign_up(BoxAny::new(params.clone()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
|
|
||||||
assert!(!user.latest_workspace.id.is_empty());
|
|
||||||
assert!(!user.latest_workspace.database_indexer_id.is_empty());
|
|
||||||
assert!(!user.user_workspaces.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_update_user_profile_test() {
|
|
||||||
if get_supabase_ci_config().is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let user_service = user_auth_service();
|
|
||||||
let uuid = Uuid::new_v4().to_string();
|
|
||||||
let params = third_party_sign_up_param(uuid);
|
|
||||||
let user: AuthResponse = user_service
|
|
||||||
.sign_up(BoxAny::new(params.clone()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let params = UpdateUserProfileParams::new(user.user_id)
|
|
||||||
.with_name("123")
|
|
||||||
.with_email(format!("{}@test.com", Uuid::new_v4()));
|
|
||||||
|
|
||||||
user_service
|
|
||||||
.update_user(UserCredentials::from_uid(user.user_id), params)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let user_profile = user_service
|
|
||||||
.get_user_profile(UserCredentials::from_uid(user.user_id))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(user_profile.name, "123");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_get_user_profile_test() {
|
|
||||||
if get_supabase_ci_config().is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let user_service = user_auth_service();
|
|
||||||
let uuid = Uuid::new_v4().to_string();
|
|
||||||
let params = third_party_sign_up_param(uuid);
|
|
||||||
let user: AuthResponse = user_service
|
|
||||||
.sign_up(BoxAny::new(params.clone()))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let credential = UserCredentials::from_uid(user.user_id);
|
|
||||||
user_service
|
|
||||||
.get_user_profile(credential.clone())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn supabase_get_not_exist_user_profile_test() {
|
|
||||||
if get_supabase_ci_config().is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_service = user_auth_service();
|
|
||||||
let result: FlowyError = user_service
|
|
||||||
.get_user_profile(UserCredentials::from_uid(i64::MAX))
|
|
||||||
.await
|
|
||||||
.unwrap_err();
|
|
||||||
// user not found
|
|
||||||
assert!(result.is_record_not_found());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn user_encryption_sign_test() {
|
|
||||||
if get_supabase_ci_config().is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let user_service = user_auth_service();
|
|
||||||
let uuid = Uuid::new_v4().to_string();
|
|
||||||
let params = third_party_sign_up_param(uuid);
|
|
||||||
let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap();
|
|
||||||
|
|
||||||
// generate encryption sign
|
|
||||||
let secret = generate_encryption_secret();
|
|
||||||
let sign = encrypt_text(user.user_id.to_string(), &secret).unwrap();
|
|
||||||
|
|
||||||
user_service
|
|
||||||
.update_user(
|
|
||||||
UserCredentials::from_uid(user.user_id),
|
|
||||||
UpdateUserProfileParams::new(user.user_id)
|
|
||||||
.with_encryption_type(EncryptionType::SelfEncryption(sign.clone())),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let user_profile: UserProfile = user_service
|
|
||||||
.get_user_profile(UserCredentials::from_uid(user.user_id))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
user_profile.encryption_type,
|
|
||||||
EncryptionType::SelfEncryption(sign)
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,162 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use collab::core::collab::{DataSource, MutexCollab};
|
|
||||||
use collab::core::origin::CollabOrigin;
|
|
||||||
use collab::preclude::Collab;
|
|
||||||
use collab_plugins::cloud_storage::RemoteCollabStorage;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use flowy_database_pub::cloud::DatabaseCloudService;
|
|
||||||
use flowy_error::FlowyError;
|
|
||||||
use flowy_folder_pub::cloud::{Folder, FolderCloudService};
|
|
||||||
use flowy_server::supabase::api::{
|
|
||||||
RESTfulPostgresServer, SupabaseCollabStorageImpl, SupabaseDatabaseServiceImpl,
|
|
||||||
SupabaseFolderServiceImpl, SupabaseServerServiceImpl, SupabaseUserServiceImpl,
|
|
||||||
};
|
|
||||||
use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_UUID};
|
|
||||||
use flowy_server::{AppFlowyEncryption, EncryptionImpl};
|
|
||||||
use flowy_server_pub::supabase_config::SupabaseConfiguration;
|
|
||||||
use flowy_user_pub::cloud::UserCloudService;
|
|
||||||
use lib_infra::future::FutureResult;
|
|
||||||
|
|
||||||
use crate::setup_log;
|
|
||||||
|
|
||||||
pub fn get_supabase_ci_config() -> Option<SupabaseConfiguration> {
|
|
||||||
dotenv::from_filename("./.env.ci").ok()?;
|
|
||||||
setup_log();
|
|
||||||
SupabaseConfiguration::from_env().ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn get_supabase_dev_config() -> Option<SupabaseConfiguration> {
|
|
||||||
dotenv::from_filename("./.env.dev").ok()?;
|
|
||||||
setup_log();
|
|
||||||
SupabaseConfiguration::from_env().ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn collab_service() -> Arc<dyn RemoteCollabStorage> {
|
|
||||||
let (server, encryption_impl) = supabase_server_service(None);
|
|
||||||
Arc::new(SupabaseCollabStorageImpl::new(
|
|
||||||
server,
|
|
||||||
None,
|
|
||||||
Arc::downgrade(&encryption_impl),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn database_service() -> Arc<dyn DatabaseCloudService> {
|
|
||||||
let (server, _encryption_impl) = supabase_server_service(None);
|
|
||||||
Arc::new(SupabaseDatabaseServiceImpl::new(server))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn user_auth_service() -> Arc<dyn UserCloudService> {
|
|
||||||
let (server, _encryption_impl) = supabase_server_service(None);
|
|
||||||
Arc::new(SupabaseUserServiceImpl::new(server, vec![], None))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn folder_service() -> Arc<dyn FolderCloudService> {
|
|
||||||
let (server, _encryption_impl) = supabase_server_service(None);
|
|
||||||
Arc::new(SupabaseFolderServiceImpl::new(server))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn file_storage_service() -> Arc<dyn ObjectStorageCloudService> {
|
|
||||||
let encryption_impl: Arc<dyn AppFlowyEncryption> = Arc::new(EncryptionImpl::new(None));
|
|
||||||
let config = SupabaseConfiguration::from_env().unwrap();
|
|
||||||
Arc::new(
|
|
||||||
SupabaseFileStorage::new(
|
|
||||||
&config,
|
|
||||||
Arc::downgrade(&encryption_impl),
|
|
||||||
Arc::new(TestFileStoragePlan),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn encryption_folder_service(
|
|
||||||
secret: Option<String>,
|
|
||||||
) -> (Arc<dyn FolderCloudService>, Arc<dyn AppFlowyEncryption>) {
|
|
||||||
let (server, encryption_impl) = supabase_server_service(secret);
|
|
||||||
let service = Arc::new(SupabaseFolderServiceImpl::new(server));
|
|
||||||
(service, encryption_impl)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn encryption_collab_service(
|
|
||||||
secret: Option<String>,
|
|
||||||
) -> (Arc<dyn RemoteCollabStorage>, Arc<dyn AppFlowyEncryption>) {
|
|
||||||
let (server, encryption_impl) = supabase_server_service(secret);
|
|
||||||
let service = Arc::new(SupabaseCollabStorageImpl::new(
|
|
||||||
server,
|
|
||||||
None,
|
|
||||||
Arc::downgrade(&encryption_impl),
|
|
||||||
));
|
|
||||||
(service, encryption_impl)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn print_encryption_folder(
|
|
||||||
uid: &i64,
|
|
||||||
folder_id: &str,
|
|
||||||
encryption_secret: Option<String>,
|
|
||||||
) {
|
|
||||||
let (cloud_service, _encryption) = encryption_folder_service(encryption_secret);
|
|
||||||
let folder_data = cloud_service.get_folder_data(folder_id, uid).await.unwrap();
|
|
||||||
let json = serde_json::to_value(folder_data).unwrap();
|
|
||||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn print_encryption_folder_snapshot(
|
|
||||||
uid: &i64,
|
|
||||||
folder_id: &str,
|
|
||||||
encryption_secret: Option<String>,
|
|
||||||
) {
|
|
||||||
let (cloud_service, _encryption) = encryption_collab_service(encryption_secret);
|
|
||||||
let snapshot = cloud_service
|
|
||||||
.get_snapshots(folder_id, 1)
|
|
||||||
.await
|
|
||||||
.pop()
|
|
||||||
.unwrap();
|
|
||||||
let collab = Arc::new(MutexCollab::new(
|
|
||||||
Collab::new_with_source(
|
|
||||||
CollabOrigin::Empty,
|
|
||||||
folder_id,
|
|
||||||
DataSource::DocStateV1(snapshot.blob),
|
|
||||||
vec![],
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
));
|
|
||||||
let folder_data = Folder::open(uid, collab, None)
|
|
||||||
.unwrap()
|
|
||||||
.get_folder_data(folder_id)
|
|
||||||
.unwrap();
|
|
||||||
let json = serde_json::to_value(folder_data).unwrap();
|
|
||||||
println!("{}", serde_json::to_string_pretty(&json).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn supabase_server_service(
|
|
||||||
encryption_secret: Option<String>,
|
|
||||||
) -> (SupabaseServerServiceImpl, Arc<dyn AppFlowyEncryption>) {
|
|
||||||
let config = SupabaseConfiguration::from_env().unwrap();
|
|
||||||
let encryption_impl: Arc<dyn AppFlowyEncryption> =
|
|
||||||
Arc::new(EncryptionImpl::new(encryption_secret));
|
|
||||||
let encryption = Arc::downgrade(&encryption_impl);
|
|
||||||
let server = Arc::new(RESTfulPostgresServer::new(config, encryption));
|
|
||||||
(SupabaseServerServiceImpl::new(server), encryption_impl)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn third_party_sign_up_param(uuid: String) -> HashMap<String, String> {
|
|
||||||
let mut params = HashMap::new();
|
|
||||||
params.insert(USER_UUID.to_string(), uuid);
|
|
||||||
params.insert(
|
|
||||||
USER_EMAIL.to_string(),
|
|
||||||
format!("{}@test.com", Uuid::new_v4()),
|
|
||||||
);
|
|
||||||
params.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string());
|
|
||||||
params
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TestFileStoragePlan;
|
|
|
@ -1 +0,0 @@
|
||||||
hello world
|
|
|
@ -84,7 +84,11 @@ pub trait UserCloudServiceProvider: Send + Sync {
|
||||||
/// * `enable_sync`: A boolean indicating whether synchronization should be enabled or disabled.
|
/// * `enable_sync`: A boolean indicating whether synchronization should be enabled or disabled.
|
||||||
fn set_enable_sync(&self, uid: i64, enable_sync: bool);
|
fn set_enable_sync(&self, uid: i64, enable_sync: bool);
|
||||||
|
|
||||||
fn set_server_auth_type(&self, auth_type: &AuthType);
|
fn set_server_auth_type(
|
||||||
|
&self,
|
||||||
|
auth_type: &AuthType,
|
||||||
|
token: Option<String>,
|
||||||
|
) -> Result<(), FlowyError>;
|
||||||
|
|
||||||
fn get_server_auth_type(&self) -> AuthType;
|
fn get_server_auth_type(&self) -> AuthType;
|
||||||
|
|
||||||
|
@ -231,9 +235,7 @@ pub trait UserCloudService: Send + Sync + 'static {
|
||||||
async fn get_workspace_members(
|
async fn get_workspace_members(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: Uuid,
|
workspace_id: Uuid,
|
||||||
) -> Result<Vec<WorkspaceMember>, FlowyError> {
|
) -> Result<Vec<WorkspaceMember>, FlowyError>;
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_user_awareness_doc_state(
|
async fn get_user_awareness_doc_state(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -114,6 +114,12 @@ pub struct UserWorkspace {
|
||||||
pub member_count: i64,
|
pub member_count: i64,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub role: Option<Role>,
|
pub role: Option<Role>,
|
||||||
|
#[serde(default = "default_workspace_type")]
|
||||||
|
pub workspace_type: AuthType,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_workspace_type() -> AuthType {
|
||||||
|
AuthType::AppFlowyCloud
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserWorkspace {
|
impl UserWorkspace {
|
||||||
|
@ -131,6 +137,7 @@ impl UserWorkspace {
|
||||||
icon: "".to_string(),
|
icon: "".to_string(),
|
||||||
member_count: 1,
|
member_count: 1,
|
||||||
role: Some(Role::Owner),
|
role: Some(Role::Owner),
|
||||||
|
workspace_type: AuthType::Local,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::entities::{UserAuthResponse, UserWorkspace};
|
use crate::entities::{AuthType, UserAuthResponse, UserWorkspace};
|
||||||
use base64::engine::general_purpose::STANDARD;
|
use base64::engine::general_purpose::STANDARD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
@ -77,6 +77,7 @@ impl<'de> Visitor<'de> for SessionVisitor {
|
||||||
icon: "".to_owned(),
|
icon: "".to_owned(),
|
||||||
member_count: 1,
|
member_count: 1,
|
||||||
role: None,
|
role: None,
|
||||||
|
workspace_type: AuthType::Local,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
use crate::cloud::UserUpdate;
|
use crate::cloud::UserUpdate;
|
||||||
use crate::entities::{AuthType, UpdateUserProfileParams, UserProfile};
|
use crate::entities::{AuthType, Role, UpdateUserProfileParams, UserProfile, UserWorkspace};
|
||||||
use crate::sql::select_user_workspace;
|
use crate::sql::{
|
||||||
|
select_user_workspace, upsert_user_workspace, upsert_workspace_member, WorkspaceMemberTable,
|
||||||
|
};
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
use flowy_sqlite::schema::user_table;
|
use flowy_sqlite::schema::user_table;
|
||||||
use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods, RunQueryDsl};
|
use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods, RunQueryDsl};
|
||||||
use tracing::{trace, warn};
|
use tracing::trace;
|
||||||
|
|
||||||
/// The order of the fields in the struct must be the same as the order of the fields in the table.
|
/// The order of the fields in the struct must be the same as the order of the fields in the table.
|
||||||
/// Check out the [schema.rs] for table schema.
|
/// Check out the [schema.rs] for table schema.
|
||||||
|
@ -92,6 +94,33 @@ pub fn update_user_profile(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_local_workspace(
|
||||||
|
uid: i64,
|
||||||
|
workspace_id: &str,
|
||||||
|
workspace_name: &str,
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
) -> FlowyResult<UserWorkspace> {
|
||||||
|
let user_workspace = UserWorkspace::new_local(workspace_id.to_string(), workspace_name);
|
||||||
|
conn.immediate_transaction(|conn| {
|
||||||
|
let row = select_user_table_row(uid, conn)?;
|
||||||
|
let row = WorkspaceMemberTable {
|
||||||
|
email: row.email,
|
||||||
|
role: Role::Owner as i32,
|
||||||
|
name: row.name,
|
||||||
|
avatar_url: Some(row.icon_url),
|
||||||
|
uid,
|
||||||
|
workspace_id: workspace_id.to_string(),
|
||||||
|
updated_at: chrono::Utc::now().naive_utc(),
|
||||||
|
};
|
||||||
|
|
||||||
|
upsert_user_workspace(uid, AuthType::Local, user_workspace.clone(), conn)?;
|
||||||
|
upsert_workspace_member(conn, row)?;
|
||||||
|
Ok::<_, FlowyError>(())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(user_workspace)
|
||||||
|
}
|
||||||
|
|
||||||
fn select_user_table_row(uid: i64, conn: &mut SqliteConnection) -> Result<UserTable, FlowyError> {
|
fn select_user_table_row(uid: i64, conn: &mut SqliteConnection) -> Result<UserTable, FlowyError> {
|
||||||
let row = user_table::dsl::user_table
|
let row = user_table::dsl::user_table
|
||||||
.filter(user_table::id.eq(&uid.to_string()))
|
.filter(user_table::id.eq(&uid.to_string()))
|
||||||
|
@ -128,26 +157,17 @@ pub fn select_user_profile(
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_workspace_auth_type(
|
pub fn select_user_auth_type(
|
||||||
uid: i64,
|
uid: i64,
|
||||||
workspace_id: &str,
|
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
) -> Result<AuthType, FlowyError> {
|
) -> Result<AuthType, FlowyError> {
|
||||||
match select_user_workspace(workspace_id, conn) {
|
let row = select_user_table_row(uid, conn)?;
|
||||||
Ok(workspace) => Ok(AuthType::from(workspace.workspace_type)),
|
Ok(AuthType::from(row.auth_type))
|
||||||
Err(err) => {
|
}
|
||||||
if err.is_record_not_found() {
|
|
||||||
let row = select_user_table_row(uid, conn)?;
|
pub fn select_user_token(uid: i64, conn: &mut SqliteConnection) -> Result<String, FlowyError> {
|
||||||
warn!(
|
let row = select_user_table_row(uid, conn)?;
|
||||||
"user user auth type:{} as workspace auth type",
|
Ok(row.token)
|
||||||
row.auth_type
|
|
||||||
);
|
|
||||||
Ok(AuthType::from(row.auth_type))
|
|
||||||
} else {
|
|
||||||
Err(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upsert_user(user: UserTable, mut conn: DBConnection) -> FlowyResult<()> {
|
pub fn upsert_user(user: UserTable, mut conn: DBConnection) -> FlowyResult<()> {
|
||||||
|
|
|
@ -149,6 +149,7 @@ impl From<UserWorkspaceTable> for UserWorkspace {
|
||||||
icon: value.icon,
|
icon: value.icon,
|
||||||
member_count: value.member_count,
|
member_count: value.member_count,
|
||||||
role: value.role.map(|v| v.into()),
|
role: value.role.map(|v| v.into()),
|
||||||
|
workspace_type: AuthType::from(value.workspace_type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,14 +156,10 @@ pub struct RepeatedUserWorkspacePB {
|
||||||
pub items: Vec<UserWorkspacePB>,
|
pub items: Vec<UserWorkspacePB>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(AuthType, Vec<UserWorkspace>)> for RepeatedUserWorkspacePB {
|
impl From<Vec<UserWorkspace>> for RepeatedUserWorkspacePB {
|
||||||
fn from(value: (AuthType, Vec<UserWorkspace>)) -> Self {
|
fn from(workspaces: Vec<UserWorkspace>) -> Self {
|
||||||
let (auth_type, workspaces) = value;
|
|
||||||
Self {
|
Self {
|
||||||
items: workspaces
|
items: workspaces.into_iter().map(UserWorkspacePB::from).collect(),
|
||||||
.into_iter()
|
|
||||||
.map(|w| UserWorkspacePB::from((auth_type, w)))
|
|
||||||
.collect(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -193,16 +189,16 @@ pub struct UserWorkspacePB {
|
||||||
pub workspace_auth_type: AuthTypePB,
|
pub workspace_auth_type: AuthTypePB,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(AuthType, UserWorkspace)> for UserWorkspacePB {
|
impl From<UserWorkspace> for UserWorkspacePB {
|
||||||
fn from(value: (AuthType, UserWorkspace)) -> Self {
|
fn from(workspace: UserWorkspace) -> Self {
|
||||||
Self {
|
Self {
|
||||||
workspace_id: value.1.id,
|
workspace_id: workspace.id,
|
||||||
name: value.1.name,
|
name: workspace.name,
|
||||||
created_at_timestamp: value.1.created_at.timestamp(),
|
created_at_timestamp: workspace.created_at.timestamp(),
|
||||||
icon: value.1.icon,
|
icon: workspace.icon,
|
||||||
member_count: value.1.member_count,
|
member_count: workspace.member_count,
|
||||||
role: value.1.role.map(AFRolePB::from),
|
role: workspace.role.map(AFRolePB::from),
|
||||||
workspace_auth_type: AuthTypePB::from(value.0),
|
workspace_auth_type: AuthTypePB::from(workspace.workspace_type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,7 +147,7 @@ pub struct UpdateWorkspaceMemberPB {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workspace Role
|
// Workspace Role
|
||||||
#[derive(Debug, ProtoBuf_Enum, Clone, Default)]
|
#[derive(Debug, ProtoBuf_Enum, Clone, Default, Eq, PartialEq)]
|
||||||
pub enum AFRolePB {
|
pub enum AFRolePB {
|
||||||
Owner = 0,
|
Owner = 0,
|
||||||
Member = 1,
|
Member = 1,
|
||||||
|
@ -200,7 +200,7 @@ pub struct OpenUserWorkspacePB {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
|
|
||||||
#[pb(index = 2)]
|
#[pb(index = 2)]
|
||||||
pub auth_type: AuthTypePB,
|
pub workspace_auth_type: AuthTypePB,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ProtoBuf, Default, Clone, Validate)]
|
#[derive(ProtoBuf, Default, Clone, Validate)]
|
||||||
|
@ -242,7 +242,7 @@ pub struct CreateWorkspacePB {
|
||||||
pub auth_type: AuthTypePB,
|
pub auth_type: AuthTypePB,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(ProtoBuf_Enum, Default, Debug, Clone, Eq, PartialEq)]
|
#[derive(ProtoBuf_Enum, Copy, Default, Debug, Clone, Eq, PartialEq)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum AuthTypePB {
|
pub enum AuthTypePB {
|
||||||
#[default]
|
#[default]
|
||||||
|
|
|
@ -439,10 +439,7 @@ pub async fn get_all_workspace_handler(
|
||||||
.get_all_user_workspaces(profile.uid, profile.auth_type)
|
.get_all_user_workspaces(profile.uid, profile.auth_type)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
data_result_ok(RepeatedUserWorkspacePB::from((
|
data_result_ok(RepeatedUserWorkspacePB::from(user_workspaces))
|
||||||
profile.auth_type,
|
|
||||||
user_workspaces,
|
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "info", skip(data, manager), err)]
|
#[tracing::instrument(level = "info", skip(data, manager), err)]
|
||||||
|
@ -454,7 +451,7 @@ pub async fn open_workspace_handler(
|
||||||
let params = data.try_into_inner()?;
|
let params = data.try_into_inner()?;
|
||||||
let workspace_id = Uuid::from_str(¶ms.workspace_id)?;
|
let workspace_id = Uuid::from_str(¶ms.workspace_id)?;
|
||||||
manager
|
manager
|
||||||
.open_workspace(&workspace_id, AuthType::from(params.auth_type))
|
.open_workspace(&workspace_id, AuthType::from(params.workspace_auth_type))
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -627,7 +624,7 @@ pub async fn create_workspace_handler(
|
||||||
let auth_type = AuthType::from(data.auth_type);
|
let auth_type = AuthType::from(data.auth_type);
|
||||||
let manager = upgrade_manager(manager)?;
|
let manager = upgrade_manager(manager)?;
|
||||||
let new_workspace = manager.create_workspace(&data.name, auth_type).await?;
|
let new_workspace = manager.create_workspace(&data.name, auth_type).await?;
|
||||||
data_result_ok(UserWorkspacePB::from((auth_type, new_workspace)))
|
data_result_ok(UserWorkspacePB::from(new_workspace))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip_all, err)]
|
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||||
|
|
|
@ -291,6 +291,11 @@ pub trait UserStatusCallback: Send + Sync + 'static {
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn did_launch(&self) -> FlowyResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Fires right after the user successfully signs in.
|
/// Fires right after the user successfully signs in.
|
||||||
async fn on_sign_in(
|
async fn on_sign_in(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use diesel::SqliteConnection;
|
use diesel::SqliteConnection;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{info, instrument};
|
use tracing::instrument;
|
||||||
|
|
||||||
use collab_integrate::CollabKVDB;
|
use collab_integrate::CollabKVDB;
|
||||||
use flowy_error::FlowyResult;
|
use flowy_error::FlowyResult;
|
||||||
|
@ -9,7 +9,7 @@ use flowy_user_pub::entities::AuthType;
|
||||||
|
|
||||||
use crate::migrations::migration::UserDataMigration;
|
use crate::migrations::migration::UserDataMigration;
|
||||||
use flowy_user_pub::session::Session;
|
use flowy_user_pub::session::Session;
|
||||||
use flowy_user_pub::sql::{select_user_workspace, upsert_user_workspace};
|
use flowy_user_pub::sql::upsert_user_workspace;
|
||||||
|
|
||||||
pub struct AnonUserWorkspaceTableMigration;
|
pub struct AnonUserWorkspaceTableMigration;
|
||||||
|
|
||||||
|
@ -34,23 +34,15 @@ impl UserDataMigration for AnonUserWorkspaceTableMigration {
|
||||||
&self,
|
&self,
|
||||||
session: &Session,
|
session: &Session,
|
||||||
_collab_db: &Arc<CollabKVDB>,
|
_collab_db: &Arc<CollabKVDB>,
|
||||||
auth_type: &AuthType,
|
user_auth_type: &AuthType,
|
||||||
db: &mut SqliteConnection,
|
db: &mut SqliteConnection,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
// For historical reason, anon user doesn't have a workspace in user_workspace_table.
|
// For historical reason, anon user doesn't have a workspace in user_workspace_table.
|
||||||
// So we need to create a new entry for the anon user in the user_workspace_table.
|
// So we need to create a new entry for the anon user in the user_workspace_table.
|
||||||
if matches!(auth_type, AuthType::Local) {
|
if matches!(user_auth_type, AuthType::Local) {
|
||||||
let user_workspace = &session.user_workspace;
|
let mut user_workspace = session.user_workspace.clone();
|
||||||
let result = select_user_workspace(&user_workspace.id, db);
|
user_workspace.workspace_type = AuthType::Local;
|
||||||
if let Err(e) = result {
|
upsert_user_workspace(session.user_id, *user_auth_type, user_workspace, db)?;
|
||||||
if e.is_record_not_found() {
|
|
||||||
info!(
|
|
||||||
"Anon user workspace not found in the database, creating a new entry for user_id: {}",
|
|
||||||
session.user_id
|
|
||||||
);
|
|
||||||
upsert_user_workspace(session.user_id, *auth_type, user_workspace.clone(), db)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -40,7 +40,7 @@ impl UserDataMigration for CollabDocKeyWithWorkspaceIdMigration {
|
||||||
&self,
|
&self,
|
||||||
session: &Session,
|
session: &Session,
|
||||||
collab_db: &Arc<CollabKVDB>,
|
collab_db: &Arc<CollabKVDB>,
|
||||||
_authenticator: &AuthType,
|
_user_auth_type: &AuthType,
|
||||||
_db: &mut SqliteConnection,
|
_db: &mut SqliteConnection,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
trace!(
|
trace!(
|
||||||
|
|
|
@ -42,13 +42,13 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration {
|
||||||
&self,
|
&self,
|
||||||
session: &Session,
|
session: &Session,
|
||||||
collab_db: &Arc<CollabKVDB>,
|
collab_db: &Arc<CollabKVDB>,
|
||||||
authenticator: &AuthType,
|
user_auth_type: &AuthType,
|
||||||
_db: &mut SqliteConnection,
|
_db: &mut SqliteConnection,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
// - The `empty document` struct has already undergone refactoring prior to the launch of the AppFlowy cloud version.
|
// - The `empty document` struct has already undergone refactoring prior to the launch of the AppFlowy cloud version.
|
||||||
// - Consequently, if a user is utilizing the AppFlowy cloud version, there is no need to perform any migration for the `empty document` struct.
|
// - Consequently, if a user is utilizing the AppFlowy cloud version, there is no need to perform any migration for the `empty document` struct.
|
||||||
// - This migration step is only necessary for users who are transitioning from a local version of AppFlowy to the cloud version.
|
// - This migration step is only necessary for users who are transitioning from a local version of AppFlowy to the cloud version.
|
||||||
if !matches!(authenticator, AuthType::Local) {
|
if !matches!(user_auth_type, AuthType::Local) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
collab_db.with_write_txn(|write_txn| {
|
collab_db.with_write_txn(|write_txn| {
|
||||||
|
|
|
@ -54,7 +54,7 @@ impl UserLocalDataMigration {
|
||||||
pub fn run(
|
pub fn run(
|
||||||
self,
|
self,
|
||||||
migrations: Vec<Box<dyn UserDataMigration>>,
|
migrations: Vec<Box<dyn UserDataMigration>>,
|
||||||
auth_type: &AuthType,
|
user_auth_type: &AuthType,
|
||||||
app_version: &Version,
|
app_version: &Version,
|
||||||
) -> FlowyResult<Vec<String>> {
|
) -> FlowyResult<Vec<String>> {
|
||||||
let mut applied_migrations = vec![];
|
let mut applied_migrations = vec![];
|
||||||
|
@ -75,7 +75,7 @@ impl UserLocalDataMigration {
|
||||||
|
|
||||||
let migration_name = migration.name().to_string();
|
let migration_name = migration.name().to_string();
|
||||||
if !duplicated_names.contains(&migration_name) {
|
if !duplicated_names.contains(&migration_name) {
|
||||||
migration.run(&self.session, &self.collab_db, auth_type, &mut conn)?;
|
migration.run(&self.session, &self.collab_db, user_auth_type, &mut conn)?;
|
||||||
applied_migrations.push(migration.name().to_string());
|
applied_migrations.push(migration.name().to_string());
|
||||||
save_migration_record(&mut conn, &migration_name);
|
save_migration_record(&mut conn, &migration_name);
|
||||||
duplicated_names.push(migration_name);
|
duplicated_names.push(migration_name);
|
||||||
|
@ -98,7 +98,7 @@ pub trait UserDataMigration {
|
||||||
&self,
|
&self,
|
||||||
user: &Session,
|
user: &Session,
|
||||||
collab_db: &Arc<CollabKVDB>,
|
collab_db: &Arc<CollabKVDB>,
|
||||||
authenticator: &AuthType,
|
user_auth_type: &AuthType,
|
||||||
db: &mut SqliteConnection,
|
db: &mut SqliteConnection,
|
||||||
) -> FlowyResult<()>;
|
) -> FlowyResult<()>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration {
|
||||||
&self,
|
&self,
|
||||||
session: &Session,
|
session: &Session,
|
||||||
collab_db: &Arc<CollabKVDB>,
|
collab_db: &Arc<CollabKVDB>,
|
||||||
_authenticator: &AuthType,
|
_user_auth_type: &AuthType,
|
||||||
_db: &mut SqliteConnection,
|
_db: &mut SqliteConnection,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
collab_db.with_write_txn(|write_txn| {
|
collab_db.with_write_txn(|write_txn| {
|
||||||
|
|
|
@ -38,7 +38,7 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration {
|
||||||
&self,
|
&self,
|
||||||
session: &Session,
|
session: &Session,
|
||||||
collab_db: &Arc<CollabKVDB>,
|
collab_db: &Arc<CollabKVDB>,
|
||||||
_authenticator: &AuthType,
|
_user_auth_type: &AuthType,
|
||||||
_db: &mut SqliteConnection,
|
_db: &mut SqliteConnection,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
collab_db.with_write_txn(|write_txn| {
|
collab_db.with_write_txn(|write_txn| {
|
||||||
|
|
|
@ -10,7 +10,7 @@ use collab_plugins::local_storage::kv::KVTransactionDB;
|
||||||
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
|
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
|
||||||
use flowy_sqlite::kv::KVStorePreferences;
|
use flowy_sqlite::kv::KVStorePreferences;
|
||||||
use flowy_sqlite::DBConnection;
|
use flowy_sqlite::DBConnection;
|
||||||
use flowy_user_pub::entities::UserWorkspace;
|
use flowy_user_pub::entities::{AuthType, UserWorkspace};
|
||||||
use flowy_user_pub::session::Session;
|
use flowy_user_pub::session::Session;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -48,14 +48,11 @@ impl AuthenticateUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_local_mode(&self) -> FlowyResult<bool> {
|
pub async fn is_local_mode(&self) -> FlowyResult<bool> {
|
||||||
let uid = self.user_id()?;
|
let session = self.get_session()?;
|
||||||
if let Ok(anon_user) = self.get_anon_user().await {
|
Ok(matches!(
|
||||||
if anon_user == uid {
|
session.user_workspace.workspace_type,
|
||||||
return Ok(true);
|
AuthType::Local
|
||||||
}
|
))
|
||||||
}
|
|
||||||
|
|
||||||
Ok(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn device_id(&self) -> FlowyResult<String> {
|
pub fn device_id(&self) -> FlowyResult<String> {
|
||||||
|
@ -150,28 +147,24 @@ impl AuthenticateUser {
|
||||||
|
|
||||||
match self
|
match self
|
||||||
.store_preferences
|
.store_preferences
|
||||||
.get_object::<Arc<Session>>(&self.user_config.session_cache_key)
|
.get_object::<Session>(&self.user_config.session_cache_key)
|
||||||
{
|
{
|
||||||
None => Err(FlowyError::new(
|
None => Err(FlowyError::new(
|
||||||
ErrorCode::RecordNotFound,
|
ErrorCode::RecordNotFound,
|
||||||
"User is not logged in",
|
"Can't find user session. Please login again",
|
||||||
)),
|
)),
|
||||||
Some(session) => {
|
Some(mut session) => {
|
||||||
|
// Set the workspace type to local if the user is anon.
|
||||||
|
if let Some(anon_session) = self.store_preferences.get_object::<Session>(ANON_USER) {
|
||||||
|
if session.user_id == anon_session.user_id {
|
||||||
|
session.user_workspace.workspace_type = AuthType::Local;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = Arc::new(session);
|
||||||
self.session.store(Some(session.clone()));
|
self.session.store(Some(session.clone()));
|
||||||
Ok(session)
|
Ok(session)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_anon_user(&self) -> FlowyResult<i64> {
|
|
||||||
let anon_session = self
|
|
||||||
.store_preferences
|
|
||||||
.get_object::<Session>(ANON_USER)
|
|
||||||
.ok_or(FlowyError::new(
|
|
||||||
ErrorCode::RecordNotFound,
|
|
||||||
"Anon user not found",
|
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok(anon_session.user_id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::migrations::session_migration::migrate_session_with_user_uuid;
|
||||||
use crate::services::data_import::importer::load_collab_by_object_ids;
|
use crate::services::data_import::importer::load_collab_by_object_ids;
|
||||||
use crate::services::db::UserDBPath;
|
use crate::services::db::UserDBPath;
|
||||||
use crate::services::entities::UserPaths;
|
use crate::services::entities::UserPaths;
|
||||||
use crate::user_manager::run_collab_data_migration;
|
use crate::user_manager::run_data_migration;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use collab::core::collab::DataSource;
|
use collab::core::collab::DataSource;
|
||||||
use collab::core::origin::CollabOrigin;
|
use collab::core::origin::CollabOrigin;
|
||||||
|
@ -36,7 +36,7 @@ use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use collab_document::blocks::TextDelta;
|
use collab_document::blocks::TextDelta;
|
||||||
use collab_document::document::Document;
|
use collab_document::document::Document;
|
||||||
use flowy_user_pub::sql::{select_user_profile, select_workspace_auth_type};
|
use flowy_user_pub::sql::{select_user_auth_type, select_user_profile};
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
@ -103,23 +103,17 @@ pub(crate) fn prepare_import(
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut conn = imported_sqlite_db.get_connection()?;
|
let mut conn = imported_sqlite_db.get_connection()?;
|
||||||
let imported_workspace_auth_type = select_user_profile(
|
let imported_user_auth_type = select_user_profile(
|
||||||
imported_session.user_id,
|
imported_session.user_id,
|
||||||
&imported_session.user_workspace.id,
|
&imported_session.user_workspace.id,
|
||||||
&mut conn,
|
&mut conn,
|
||||||
)
|
)
|
||||||
.map(|v| v.workspace_auth_type)
|
.map(|v| v.auth_type)
|
||||||
.or_else(|_| {
|
.or_else(|_| select_user_auth_type(imported_session.user_id, &mut conn))?;
|
||||||
select_workspace_auth_type(
|
|
||||||
imported_session.user_id,
|
|
||||||
&imported_session.user_workspace.id,
|
|
||||||
&mut conn,
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
run_collab_data_migration(
|
run_data_migration(
|
||||||
&imported_session,
|
&imported_session,
|
||||||
&imported_workspace_auth_type,
|
&imported_user_auth_type,
|
||||||
imported_collab_db.clone(),
|
imported_collab_db.clone(),
|
||||||
imported_sqlite_db.get_pool(),
|
imported_sqlite_db.get_pool(),
|
||||||
other_store_preferences.clone(),
|
other_store_preferences.clone(),
|
||||||
|
|
|
@ -7,7 +7,6 @@ use arc_swap::ArcSwapOption;
|
||||||
use collab::lock::RwLock;
|
use collab::lock::RwLock;
|
||||||
use collab_user::core::UserAwareness;
|
use collab_user::core::UserAwareness;
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use flowy_server_pub::AuthenticatorType;
|
|
||||||
use flowy_sqlite::kv::KVStorePreferences;
|
use flowy_sqlite::kv::KVStorePreferences;
|
||||||
use flowy_sqlite::schema::user_table;
|
use flowy_sqlite::schema::user_table;
|
||||||
use flowy_sqlite::ConnectionPool;
|
use flowy_sqlite::ConnectionPool;
|
||||||
|
@ -131,30 +130,16 @@ impl UserManager {
|
||||||
let user = self
|
let user = self
|
||||||
.get_user_profile_from_disk(session.user_id, &session.user_workspace.id)
|
.get_user_profile_from_disk(session.user_id, &session.user_workspace.id)
|
||||||
.await?;
|
.await?;
|
||||||
self.cloud_service.set_server_auth_type(&user.auth_type);
|
let auth_type = user.workspace_auth_type;
|
||||||
|
let token = self.token_from_auth_type(&auth_type)?;
|
||||||
// Get the current authenticator from the environment variable
|
self.cloud_service.set_server_auth_type(&auth_type, token)?;
|
||||||
let env_auth_type = current_authenticator();
|
|
||||||
|
|
||||||
// If the current authenticator is different from the authenticator in the session and it's
|
|
||||||
// not a local authenticator, we need to sign out the user.
|
|
||||||
if user.auth_type != AuthType::Local && user.auth_type != env_auth_type {
|
|
||||||
event!(
|
|
||||||
tracing::Level::INFO,
|
|
||||||
"Auth type changed from {:?} to {:?}",
|
|
||||||
user.auth_type,
|
|
||||||
env_auth_type
|
|
||||||
);
|
|
||||||
self.sign_out().await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
event!(
|
event!(
|
||||||
tracing::Level::INFO,
|
tracing::Level::INFO,
|
||||||
"init user session: {}:{}, auth type: {:?}",
|
"init user session: {}:{}, auth type: {:?}",
|
||||||
user.uid,
|
user.uid,
|
||||||
user.email,
|
user.email,
|
||||||
user.auth_type,
|
auth_type,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.prepare_user(&session).await;
|
self.prepare_user(&session).await;
|
||||||
|
@ -251,7 +236,7 @@ impl UserManager {
|
||||||
self.authenticate_user.database.get_pool(session.user_id),
|
self.authenticate_user.database.get_pool(session.user_id),
|
||||||
) {
|
) {
|
||||||
(Ok(collab_db), Ok(sqlite_pool)) => {
|
(Ok(collab_db), Ok(sqlite_pool)) => {
|
||||||
run_collab_data_migration(
|
run_data_migration(
|
||||||
&session,
|
&session,
|
||||||
&user.auth_type,
|
&user.auth_type,
|
||||||
collab_db,
|
collab_db,
|
||||||
|
@ -267,7 +252,7 @@ impl UserManager {
|
||||||
self.set_first_time_installed_version();
|
self.set_first_time_installed_version();
|
||||||
let cloud_config = get_cloud_config(session.user_id, &self.store_preferences);
|
let cloud_config = get_cloud_config(session.user_id, &self.store_preferences);
|
||||||
// Init the user awareness. here we ignore the error
|
// Init the user awareness. here we ignore the error
|
||||||
let _ = self.initial_user_awareness(&session, &user.auth_type).await;
|
let _ = self.initial_user_awareness(&session, &auth_type).await;
|
||||||
|
|
||||||
user_status_callback
|
user_status_callback
|
||||||
.on_launch_if_authenticated(
|
.on_launch_if_authenticated(
|
||||||
|
@ -275,7 +260,7 @@ impl UserManager {
|
||||||
&cloud_config,
|
&cloud_config,
|
||||||
&session.user_workspace,
|
&session.user_workspace,
|
||||||
&self.authenticate_user.user_config.device_id,
|
&self.authenticate_user.user_config.device_id,
|
||||||
&user.auth_type,
|
&auth_type,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -342,7 +327,7 @@ impl UserManager {
|
||||||
params: SignInParams,
|
params: SignInParams,
|
||||||
auth_type: AuthType,
|
auth_type: AuthType,
|
||||||
) -> Result<UserProfile, FlowyError> {
|
) -> Result<UserProfile, FlowyError> {
|
||||||
self.cloud_service.set_server_auth_type(&auth_type);
|
self.cloud_service.set_server_auth_type(&auth_type, None)?;
|
||||||
|
|
||||||
let response: AuthResponse = self
|
let response: AuthResponse = self
|
||||||
.cloud_service
|
.cloud_service
|
||||||
|
@ -357,7 +342,7 @@ impl UserManager {
|
||||||
self.save_auth_data(&response, auth_type, &session).await?;
|
self.save_auth_data(&response, auth_type, &session).await?;
|
||||||
|
|
||||||
let _ = self
|
let _ = self
|
||||||
.initial_user_awareness(&session, &user_profile.auth_type)
|
.initial_user_awareness(&session, &user_profile.workspace_auth_type)
|
||||||
.await;
|
.await;
|
||||||
self
|
self
|
||||||
.user_status_callback
|
.user_status_callback
|
||||||
|
@ -390,7 +375,7 @@ impl UserManager {
|
||||||
auth_type: AuthType,
|
auth_type: AuthType,
|
||||||
params: BoxAny,
|
params: BoxAny,
|
||||||
) -> Result<UserProfile, FlowyError> {
|
) -> Result<UserProfile, FlowyError> {
|
||||||
self.cloud_service.set_server_auth_type(&auth_type);
|
self.cloud_service.set_server_auth_type(&auth_type, None)?;
|
||||||
|
|
||||||
// sign out the current user if there is one
|
// sign out the current user if there is one
|
||||||
let migration_user = self.get_migration_user(&auth_type).await;
|
let migration_user = self.get_migration_user(&auth_type).await;
|
||||||
|
@ -556,7 +541,7 @@ impl UserManager {
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
) -> FlowyResult<()> {
|
) -> FlowyResult<()> {
|
||||||
// If the user is a local user, no need to refresh the user profile
|
// If the user is a local user, no need to refresh the user profile
|
||||||
if old_user_profile.auth_type.is_local() {
|
if old_user_profile.workspace_auth_type.is_local() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -616,6 +601,16 @@ impl UserManager {
|
||||||
self.authenticate_user.user_paths.user_data_dir(uid)
|
self.authenticate_user.user_paths.user_data_dir(uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn token_from_auth_type(&self, auth_type: &AuthType) -> FlowyResult<Option<String>> {
|
||||||
|
match auth_type {
|
||||||
|
AuthType::Local => Ok(None),
|
||||||
|
AuthType::AppFlowyCloud => {
|
||||||
|
let uid = self.user_id()?;
|
||||||
|
let mut conn = self.db_connection(uid)?;
|
||||||
|
Ok(select_user_token(uid, &mut conn).ok())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn user_setting(&self) -> Result<UserSettingPB, FlowyError> {
|
pub fn user_setting(&self) -> Result<UserSettingPB, FlowyError> {
|
||||||
let session = self.get_session()?;
|
let session = self.get_session()?;
|
||||||
let user_setting = UserSettingPB {
|
let user_setting = UserSettingPB {
|
||||||
|
@ -655,7 +650,9 @@ impl UserManager {
|
||||||
authenticator: &AuthType,
|
authenticator: &AuthType,
|
||||||
email: &str,
|
email: &str,
|
||||||
) -> Result<String, FlowyError> {
|
) -> Result<String, FlowyError> {
|
||||||
self.cloud_service.set_server_auth_type(authenticator);
|
self
|
||||||
|
.cloud_service
|
||||||
|
.set_server_auth_type(authenticator, None)?;
|
||||||
|
|
||||||
let auth_service = self.cloud_service.get_user_service()?;
|
let auth_service = self.cloud_service.get_user_service()?;
|
||||||
let url = auth_service.generate_sign_in_url_with_email(email).await?;
|
let url = auth_service.generate_sign_in_url_with_email(email).await?;
|
||||||
|
@ -670,7 +667,7 @@ impl UserManager {
|
||||||
) -> Result<GotrueTokenResponse, FlowyError> {
|
) -> Result<GotrueTokenResponse, FlowyError> {
|
||||||
self
|
self
|
||||||
.cloud_service
|
.cloud_service
|
||||||
.set_server_auth_type(&AuthType::AppFlowyCloud);
|
.set_server_auth_type(&AuthType::AppFlowyCloud, None)?;
|
||||||
let auth_service = self.cloud_service.get_user_service()?;
|
let auth_service = self.cloud_service.get_user_service()?;
|
||||||
let response = auth_service.sign_in_with_password(email, password).await?;
|
let response = auth_service.sign_in_with_password(email, password).await?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
|
@ -684,7 +681,7 @@ impl UserManager {
|
||||||
) -> Result<(), FlowyError> {
|
) -> Result<(), FlowyError> {
|
||||||
self
|
self
|
||||||
.cloud_service
|
.cloud_service
|
||||||
.set_server_auth_type(&AuthType::AppFlowyCloud);
|
.set_server_auth_type(&AuthType::AppFlowyCloud, None)?;
|
||||||
let auth_service = self.cloud_service.get_user_service()?;
|
let auth_service = self.cloud_service.get_user_service()?;
|
||||||
auth_service
|
auth_service
|
||||||
.sign_in_with_magic_link(email, redirect_to)
|
.sign_in_with_magic_link(email, redirect_to)
|
||||||
|
@ -700,7 +697,7 @@ impl UserManager {
|
||||||
) -> Result<GotrueTokenResponse, FlowyError> {
|
) -> Result<GotrueTokenResponse, FlowyError> {
|
||||||
self
|
self
|
||||||
.cloud_service
|
.cloud_service
|
||||||
.set_server_auth_type(&AuthType::AppFlowyCloud);
|
.set_server_auth_type(&AuthType::AppFlowyCloud, None)?;
|
||||||
let auth_service = self.cloud_service.get_user_service()?;
|
let auth_service = self.cloud_service.get_user_service()?;
|
||||||
let response = auth_service.sign_in_with_passcode(email, passcode).await?;
|
let response = auth_service.sign_in_with_passcode(email, passcode).await?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
|
@ -713,7 +710,7 @@ impl UserManager {
|
||||||
) -> Result<String, FlowyError> {
|
) -> Result<String, FlowyError> {
|
||||||
self
|
self
|
||||||
.cloud_service
|
.cloud_service
|
||||||
.set_server_auth_type(&AuthType::AppFlowyCloud);
|
.set_server_auth_type(&AuthType::AppFlowyCloud, None)?;
|
||||||
let auth_service = self.cloud_service.get_user_service()?;
|
let auth_service = self.cloud_service.get_user_service()?;
|
||||||
let url = auth_service
|
let url = auth_service
|
||||||
.generate_oauth_url_with_provider(oauth_provider)
|
.generate_oauth_url_with_provider(oauth_provider)
|
||||||
|
@ -800,13 +797,6 @@ impl UserManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_authenticator() -> AuthType {
|
|
||||||
match AuthenticatorType::from_env() {
|
|
||||||
AuthenticatorType::Local => AuthType::Local,
|
|
||||||
AuthenticatorType::AppFlowyCloud => AuthType::AppFlowyCloud,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn upsert_user_profile_change(
|
pub fn upsert_user_profile_change(
|
||||||
uid: i64,
|
uid: i64,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
|
@ -867,9 +857,9 @@ fn mark_all_migrations_as_applied(sqlite_pool: &Arc<ConnectionPool>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run_collab_data_migration(
|
pub(crate) fn run_data_migration(
|
||||||
session: &Session,
|
session: &Session,
|
||||||
auth_type: &AuthType,
|
user_auth_type: &AuthType,
|
||||||
collab_db: Arc<CollabKVDB>,
|
collab_db: Arc<CollabKVDB>,
|
||||||
sqlite_pool: Arc<ConnectionPool>,
|
sqlite_pool: Arc<ConnectionPool>,
|
||||||
kv: Arc<KVStorePreferences>,
|
kv: Arc<KVStorePreferences>,
|
||||||
|
@ -878,7 +868,7 @@ pub(crate) fn run_collab_data_migration(
|
||||||
let migrations = collab_migration_list();
|
let migrations = collab_migration_list();
|
||||||
match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool, kv).run(
|
match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool, kv).run(
|
||||||
migrations,
|
migrations,
|
||||||
auth_type,
|
user_auth_type,
|
||||||
app_version,
|
app_version,
|
||||||
) {
|
) {
|
||||||
Ok(applied_migrations) => {
|
Ok(applied_migrations) => {
|
||||||
|
|
|
@ -53,6 +53,18 @@ impl UserManager {
|
||||||
Ok(UserProfilePB::from(profile))
|
Ok(UserProfilePB::from(profile))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_anon_user_id(&self) -> FlowyResult<i64> {
|
||||||
|
let anon_session = self
|
||||||
|
.store_preferences
|
||||||
|
.get_object::<Session>(ANON_USER)
|
||||||
|
.ok_or(FlowyError::new(
|
||||||
|
ErrorCode::RecordNotFound,
|
||||||
|
"Anon user not found",
|
||||||
|
))?;
|
||||||
|
|
||||||
|
Ok(anon_session.user_id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Opens a historical user's session based on their user ID, device ID, and authentication type.
|
/// Opens a historical user's session based on their user ID, device ID, and authentication type.
|
||||||
///
|
///
|
||||||
/// This function facilitates the re-opening of a user's session from historical tracking.
|
/// This function facilitates the re-opening of a user's session from historical tracking.
|
||||||
|
|
|
@ -153,9 +153,10 @@ impl UserManager {
|
||||||
|
|
||||||
#[instrument(skip(self), err)]
|
#[instrument(skip(self), err)]
|
||||||
pub async fn open_workspace(&self, workspace_id: &Uuid, auth_type: AuthType) -> FlowyResult<()> {
|
pub async fn open_workspace(&self, workspace_id: &Uuid, auth_type: AuthType) -> FlowyResult<()> {
|
||||||
info!("open workspace: {}, auth_type:{}", workspace_id, auth_type);
|
info!("open workspace: {}, auth type:{}", workspace_id, auth_type);
|
||||||
let workspace_id_str = workspace_id.to_string();
|
let workspace_id_str = workspace_id.to_string();
|
||||||
self.cloud_service.set_server_auth_type(&auth_type);
|
let token = self.token_from_auth_type(&auth_type)?;
|
||||||
|
self.cloud_service.set_server_auth_type(&auth_type, token)?;
|
||||||
|
|
||||||
let uid = self.user_id()?;
|
let uid = self.user_id()?;
|
||||||
let profile = self
|
let profile = self
|
||||||
|
@ -227,7 +228,8 @@ impl UserManager {
|
||||||
workspace_name: &str,
|
workspace_name: &str,
|
||||||
auth_type: AuthType,
|
auth_type: AuthType,
|
||||||
) -> FlowyResult<UserWorkspace> {
|
) -> FlowyResult<UserWorkspace> {
|
||||||
self.cloud_service.set_server_auth_type(&auth_type);
|
let token = self.token_from_auth_type(&auth_type)?;
|
||||||
|
self.cloud_service.set_server_auth_type(&auth_type, token)?;
|
||||||
|
|
||||||
let new_workspace = self
|
let new_workspace = self
|
||||||
.cloud_service
|
.cloud_service
|
||||||
|
@ -451,7 +453,7 @@ impl UserManager {
|
||||||
);
|
);
|
||||||
// only send notification if there were real changes
|
// only send notification if there were real changes
|
||||||
if let Ok(updated_list) = select_all_user_workspace(uid, &mut conn) {
|
if let Ok(updated_list) = select_all_user_workspace(uid, &mut conn) {
|
||||||
let repeated_pb = RepeatedUserWorkspacePB::from((auth_copy, updated_list));
|
let repeated_pb = RepeatedUserWorkspacePB::from(updated_list);
|
||||||
send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces)
|
send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces)
|
||||||
.payload(repeated_pb)
|
.payload(repeated_pb)
|
||||||
.send();
|
.send();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue