feat: show loading indicator when loading data from remote (#3120)

* chore: show circle indicator if fetch the data from remote

* chore: fix the lb warnings

* chore: create sdk-build for macOS
This commit is contained in:
Nathan.fooo 2023-08-05 15:02:05 +08:00 committed by GitHub
parent ab7acbd5de
commit 9a72f31d60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 198 additions and 531 deletions

View File

@ -20,14 +20,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
# - name: Create .env file
# working-directory: frontend/appflowy_flutter
# run: |
# touch .env
# echo SUPABASE_URL=${{ secrets.HOST_URL }} >> .env
# echo SUPABASE_ANON_KEY=${{ secrets.HOST_ANON_KEY }} >> .env
# echo SUPABASE_JWT_SECRET=${{ secrets.HOST_JWT_SECRET }} >> .env
- name: Build release notes
run: |
touch ${{ env.RELEASE_NOTES_PATH }}

View File

@ -13,6 +13,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:collection/collection.dart';
import 'dart:async';
import 'package:dartz/dartz.dart';
import 'package:flutter/material.dart';
import 'database_view_service.dart';
import 'defines.dart';
import 'layout/layout_service.dart';
@ -92,6 +93,8 @@ class DatabaseController {
final DatabaseGroupListener _groupListener;
final DatabaseLayoutSettingListener _layoutListener;
final ValueNotifier<bool> _isLoading = ValueNotifier(true);
DatabaseController({required ViewPB view})
: viewId = view.id,
_databaseViewBackendSvc = DatabaseViewBackendService(viewId: view.id),
@ -109,6 +112,12 @@ class DatabaseController {
_listenOnLayoutChanged();
}
void setIsLoading(bool isLoading) {
_isLoading.value = isLoading;
}
ValueNotifier<bool> get isLoading => _isLoading;
void addListener({
DatabaseCallbacks? onDatabaseChanged,
DatabaseLayoutSettingCallbacks? onLayoutChanged,

View File

@ -3,12 +3,16 @@ import 'dart:collection';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../grid/presentation/widgets/filter/filter_info.dart';
import 'field/field_controller.dart';
import 'row/row_cache.dart';
import 'row/row_service.dart';
part 'defines.freezed.dart';
typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
typedef OnFiltersChanged = void Function(List<FilterInfo>);
typedef OnSortsChanged = void Function(List<SortInfo>);
@ -27,3 +31,11 @@ typedef OnNumOfRowsChanged = void Function(
);
typedef OnError = void Function(FlowyError);
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.loading() = _Loading;
const factory LoadingState.finish(
Either<Unit, FlowyError> successOrFail,
) = _Finish;
}

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:collection';
import 'package:appflowy/plugins/database_view/application/defines.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:dartz/dartz.dart';
@ -254,11 +255,14 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
Future<void> _openGrid(Emitter<BoardState> emit) async {
final result = await databaseController.open();
result.fold(
(grid) => emit(
state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
),
(grid) {
databaseController.setIsLoading(false);
emit(
state.copyWith(loadingState: LoadingState.finish(left(unit))),
);
},
(err) => emit(
state.copyWith(loadingState: GridLoadingState.finish(right(err))),
state.copyWith(loadingState: LoadingState.finish(right(err))),
),
);
}
@ -323,7 +327,7 @@ class BoardState with _$BoardState {
required Option<DatabasePB> grid,
required List<String> groupIds,
required Option<BoardEditingRow> editingRow,
required GridLoadingState loadingState,
required LoadingState loadingState,
required Option<FlowyError> noneOrError,
}) = _BoardState;
@ -333,18 +337,10 @@ class BoardState with _$BoardState {
groupIds: [],
editingRow: none(),
noneOrError: none(),
loadingState: const _Loading(),
loadingState: const LoadingState.loading(),
);
}
@freezed
class GridLoadingState with _$GridLoadingState {
const factory GridLoadingState.loading() = _Loading;
const factory GridLoadingState.finish(
Either<Unit, FlowyError> successOrFail,
) = _Finish;
}
class GridFieldEquatable extends Equatable {
final UnmodifiableListView<FieldPB> _fields;
const GridFieldEquatable(

View File

@ -1,4 +1,5 @@
import 'package:appflowy/plugins/database_view/application/cell/cell_service.dart';
import 'package:appflowy/plugins/database_view/application/defines.dart';
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
@ -133,11 +134,14 @@ class CalendarBloc extends Bloc<CalendarEvent, CalendarState> {
Future<void> _openDatabase(Emitter<CalendarState> emit) async {
final result = await databaseController.open();
result.fold(
(database) => emit(
state.copyWith(loadingState: DatabaseLoadingState.finish(left(unit))),
),
(database) {
databaseController.setIsLoading(false);
emit(
state.copyWith(loadingState: LoadingState.finish(left(unit))),
);
},
(err) => emit(
state.copyWith(loadingState: DatabaseLoadingState.finish(right(err))),
state.copyWith(loadingState: LoadingState.finish(right(err))),
),
);
}
@ -425,7 +429,7 @@ class CalendarState with _$CalendarState {
CalendarEventData<CalendarDayEvent>? updateEvent,
required List<String> deleteEventIds,
required Option<CalendarLayoutSettingPB> settings,
required DatabaseLoadingState loadingState,
required LoadingState loadingState,
required Option<FlowyError> noneOrError,
}) = _CalendarState;
@ -436,18 +440,10 @@ class CalendarState with _$CalendarState {
deleteEventIds: [],
settings: none(),
noneOrError: none(),
loadingState: const _Loading(),
loadingState: const LoadingState.loading(),
);
}
@freezed
class DatabaseLoadingState with _$DatabaseLoadingState {
const factory DatabaseLoadingState.loading() = _Loading;
const factory DatabaseLoadingState.finish(
Either<Unit, FlowyError> successOrFail,
) = _Finish;
}
class CalendarEditingRow {
RowPB row;
int? index;

View File

@ -159,10 +159,22 @@ class _CalendarPageState extends State<CalendarPage> {
],
child: BlocBuilder<CalendarBloc, CalendarState>(
builder: (context, state) {
return _buildCalendar(
context,
_eventController,
state.settings.foldLeft(0, (previous, a) => a.firstDayOfWeek),
return ValueListenableBuilder<bool>(
valueListenable: widget.databaseController.isLoading,
builder: (_, value, ___) {
if (value) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
} else {
return _buildCalendar(
context,
_eventController,
state.settings
.foldLeft(0, (previous, a) => a.firstDayOfWeek),
);
}
},
);
},
),

View File

@ -1,11 +1,11 @@
import 'dart:async';
import 'package:appflowy/plugins/database_view/application/defines.dart';
import 'package:appflowy/plugins/database_view/application/row/row_cache.dart';
import 'package:appflowy/plugins/database_view/application/row/row_service.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/sort_info.dart';
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -134,12 +134,13 @@ class GridBloc extends Bloc<GridEvent, GridState> {
final result = await databaseController.open();
result.fold(
(grid) {
databaseController.setIsLoading(false);
emit(
state.copyWith(loadingState: GridLoadingState.finish(left(unit))),
state.copyWith(loadingState: LoadingState.finish(left(unit))),
);
},
(err) => emit(
state.copyWith(loadingState: GridLoadingState.finish(right(err))),
state.copyWith(loadingState: LoadingState.finish(right(err))),
),
);
}
@ -177,7 +178,7 @@ class GridState with _$GridState {
required GridFieldEquatable fields,
required List<RowInfo> rowInfos,
required int rowCount,
required GridLoadingState loadingState,
required LoadingState loadingState,
required bool reorderable,
required ChangedReason reason,
required List<SortInfo> sorts,
@ -191,21 +192,13 @@ class GridState with _$GridState {
grid: none(),
viewId: viewId,
reorderable: true,
loadingState: const _Loading(),
loadingState: const LoadingState.loading(),
reason: const InitialListState(),
filters: [],
sorts: [],
);
}
@freezed
class GridLoadingState with _$GridLoadingState {
const factory GridLoadingState.loading() = _Loading;
const factory GridLoadingState.finish(
Either<Unit, FlowyError> successOrFail,
) = _Finish;
}
class GridFieldEquatable extends Equatable {
final List<FieldInfo> _fields;
const GridFieldEquatable(

View File

@ -47,20 +47,29 @@ class GridSettingBar extends StatelessWidget {
listener: (context, state) => toggleExtension.toggle(),
),
],
child: SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(width: GridSize.leadingHeaderPadding),
const Spacer(),
const FilterButton(),
const SortButton(),
SettingButton(
databaseController: controller,
),
],
),
child: ValueListenableBuilder<bool>(
valueListenable: controller.isLoading,
builder: (context, value, child) {
if (value) {
return const SizedBox.shrink();
} else {
return SizedBox(
height: 40,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(width: GridSize.leadingHeaderPadding),
const Spacer(),
const FilterButton(),
const SortButton(),
SettingButton(
databaseController: controller,
),
],
),
);
}
},
),
),
);

View File

@ -93,30 +93,46 @@ class _DatabaseTabBarViewState extends State<DatabaseTabBarView> {
],
child: Column(
children: [
Row(
children: [
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return const Flexible(
child: Padding(
padding: EdgeInsets.only(left: 50),
child: DatabaseTabBar(),
),
);
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return ValueListenableBuilder<bool>(
valueListenable: state
.tabBarControllerByViewId[state.parentView.id]!
.controller
.isLoading,
builder: (_, value, ___) {
if (value) {
return const SizedBox.shrink();
} else {
return Row(
children: [
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return const Flexible(
child: Padding(
padding: EdgeInsets.only(left: 50),
child: DatabaseTabBar(),
),
);
},
),
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return SizedBox(
width: 300,
child: Padding(
padding: const EdgeInsets.only(right: 50),
child: pageSettingBarFromState(state),
),
);
},
),
],
);
}
},
),
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {
return SizedBox(
width: 300,
child: Padding(
padding: const EdgeInsets.only(right: 50),
child: pageSettingBarFromState(state),
),
);
},
),
],
);
},
),
BlocBuilder<GridTabBarBloc, GridTabBarState>(
builder: (context, state) {

View File

@ -65,7 +65,8 @@ class _DocumentPageState extends State<DocumentPage> {
child: BlocBuilder<DocumentBloc, DocumentState>(
builder: (context, state) {
return state.loadingState.when(
loading: () => const SizedBox.shrink(),
loading: () =>
const Center(child: CircularProgressIndicator.adaptive()),
finish: (result) => result.fold(
(error) {
Log.error(error);

View File

@ -18,7 +18,7 @@ class UserBackendService {
static Future<Either<FlowyError, UserProfilePB>>
getCurrentUserProfile() async {
final result = await UserEventGetUserProfile().send().then((value) {
value.fold((l) => null, (r) => Log.error(r));
value.fold((l) => null, (r) => Log.info(r));
return value;
});
return result.swap();

View File

@ -10,10 +10,10 @@ use collab_document::document_data::default_document_data;
use collab_document::YrsDocAction;
use parking_lot::RwLock;
use crate::document::MutexDocument;
use flowy_document_deps::cloud::DocumentCloudService;
use flowy_error::{internal_error, FlowyError, FlowyResult};
use crate::document::MutexDocument;
use crate::entities::DocumentSnapshotPB;
pub trait DocumentUser: Send + Sync {
@ -77,13 +77,13 @@ impl DocumentManager {
let mut updates = vec![];
if !self.is_doc_exist(doc_id)? {
// Try to get the document from the cloud service
if let Ok(document_updates) = self.cloud_service.get_document_updates(doc_id).await {
updates = document_updates;
} else {
return Err(
FlowyError::record_not_found().context(format!("document: {} is not exist", doc_id)),
);
};
match self.cloud_service.get_document_updates(doc_id).await {
Ok(document_updates) => updates = document_updates,
Err(e) => {
tracing::error!("Get document data failed: {:?}", e);
return Err(FlowyError::internal().context("Can't not read the document data"));
},
}
}
tracing::debug!("open_document: {:?}", doc_id);

View File

@ -1,108 +0,0 @@
# AppFlowy Cloud Architecture
AppFlowy supports multiple cloud solutions. Users can choose their preferred cloud provider, such as Supabase, Firebase,
AWS, or our own AppFlowyCloud (Self-hosted server).
![](architecture-Application.png)
## Design
AppFlowy use the traits [AppFlowyServer] to abstract the cloud provider. Each cloud provider implements the [AppFlowyServer]
trait. As the image below shows. Users can choose their preferred cloud provider or simply use the default option, which is the LocalServer. When using the
LocalServer, data is stored on the local file system. Users can migrate to a cloud provider if needed. For instance, one
could migrate from LocalServer to AppFlowyCloud. This migration would create a new user in the cloud and transfer all the
data from the local database to the cloud.
![](architecture.png)
## AppFlowy Cloud Implementation (WIP)
### Restful API
### Table schema
## Supabase Implementation
### Table schema
![](./schema.png)
1. `af_roles` table: This table contains a list of roles that are used in your application, such as 'Owner', 'Member', and 'Guest'.
2. `af_permissions` table: This table stores permissions that are used in your application. Each permission has a name, a description, and an access level.
3. `af_role_permissions` table: This is a many-to-many relation table between roles and permissions. It represents which permissions a role has.
4. `af_user` table: This stores the details of users like uuid, email, uid, name, created_at. Here, uid is an auto-incrementing integer that uniquely identifies a user.
5. `af_workspace` table: This table contains all the workspaces. Each workspace has an owner which is associated with the uid of a user in the `af_user` table.
6. `af_workspace_member` table: This table maintains a list of all the members associated with a workspace and their roles.
7. `af_collab` and `af_collab_member` tables: These tables store the collaborations and their members respectively. Each collaboration has an owner and a workspace associated with it.
8. `af_collab_update`, `af_collab_update_document`, `af_collab_update_database`, `af_collab_update_w_database`, `af_collab_update_folder`, `af_database_row_update` tables: These tables are used for handling updates to collaborations.
9. `af_collab_statistics`, `af_collab_snapshot`, `af_collab_state`: These tables and view are used for maintaining statistics and snapshots of collaborations.
10. `af_user_profile_view` view: This view is used to get the latest workspace_id for each user.
![](./schema-Triggers_in_Database.png)
Here's a detailed description for each of these triggers:
1. `create_af_workspace_trigger`:
This trigger is designed to automate the process of workspace creation in the `af_workspace` table after a new user is inserted into the `af_user` table. When a new user is added, this trigger fires and inserts a new record into the `af_workspace` table, setting the `owner_uid` to the UID of the new user.
2. `manage_af_workspace_member_role_trigger`:
This trigger helps to manage the roles of workspace members. After an insert operation on the `af_workspace` table, this trigger automatically fires and creates a new record in the `af_workspace_member` table. The new record identifies the user as a member of the workspace with the role 'Owner'. This ensures that every new workspace has an owner.
3. `insert_into_af_collab_trigger`:
The purpose of this trigger is to ensure consistency between the `af_collab_update` and `af_collab` tables. When an insert operation is about to be performed on the `af_collab_update` table, this trigger fires before the insert operation. It checks if a corresponding collaboration exists in the `af_collab` table using the oid and uid. If a corresponding collaboration does not exist, the trigger creates one, using the oid, uid, and current timestamp. This way, every collab update operation corresponds to a valid collaboration.
4. `insert_into_af_collab_member_trigger`:
This trigger helps to manage the membership of users in collaborations. After a new collaboration is inserted into the `af_collab` table, this trigger fires. It checks if a corresponding collaboration member exists in the `af_collab_member` table. If a corresponding member does not exist, the trigger creates one, using the collaboration id and user id. This ensures that every collaboration has at least one member.
5. `af_collab_snapshot_update_edit_count_trigger`:
This trigger is designed to keep track of the number of edits on each collaboration snapshot in the `af_collab_snapshot` table. When an update operation is performed on the `af_collab_snapshot` table, this trigger fires. It increments the `edit_count` of the corresponding record in the `af_collab_snapshot` table by one. This ensures that the application can keep track of how many times each collaboration snapshot has been edited.
### Supabase configuration
#### Test
In order to run the test, you need to set up the .env.test file.
```dotenv
# Supabase configuration
SUPABASE_URL="your-supabase-url"
SUPABASE_ANON_KEY="your-supabase-anonymous-key"
SUPABASE_KEY="your-supabase-key"
SUPABASE_JWT_SECRET="your-supabase-jwt-secret"
# Supabase Database configuration
SUPABASE_DB="your-supabase-db-url"
SUPABASE_DB_USER="your-db-username"
SUPABASE_DB_PORT="your-db-port"
SUPABASE_DB_PASSWORD="your-db-password"
```
1. `SUPABASE_URL`: This is the URL of your Supabase server instance. Your application will use this URL to interact with the Supabase service.
2. `SUPABASE_ANON_KEY`: This is the anonymous API key from Supabase, used for operations that don't require user authentication. Operations performed with this key are done as the anonymous role in the database.
3. `SUPABASE_KEY`: This is the API key with higher privileges from Supabase. It is generally used for server-side operations that require more permissions than an anonymous user.
4. `SUPABASE_JWT_SECRET`: This is the secret used to verify JWT tokens generated by Supabase. JWT or JSON Web Token is a standard method for securely transferring data between parties as a JSON object.
5. `SUPABASE_DB`: This is the URL for the database your Supabase server instance is using.
6. `SUPABASE_DB_USER`: This is the username used to authenticate with the Supabase database, in this case, it's 'postgres', which is a common default for PostgreSQL.
7. `SUPABASE_DB_PORT`: This is the port number where your Supabase database service is accessible. The default PostgreSQL port is 5432, and you are using this default port.
8. `SUPABASE_DB_PASSWORD`: This is the password used to authenticate the `SUPABASE_DB_USER` with the Supabase database.
For example, if you want to run the supabase tests located in flowy-test crate. You need to put the `.env.test` file under
the flowy-test folder.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

@ -1,78 +0,0 @@
@startuml
title "Application"
left to right direction
package "AppFlowy Application" {
[User]
}
cloud "Supabase Server" {
[RESTful Component]
[Realtime Component]
[Postgres DB]
}
database "LocalServer" {
[Local Server Component]
}
cloud "AppFlowy Cloud Server" {
[RESTful Component] as [AppFlowy RESTful Component]
[Realtime Component] as [AppFlowy Realtime Component]
[Postgres DB] as [AppFlowy Postgres DB]
}
User --> [AppFlowy Application]
[AppFlowy Application] --> [Local Server Component] : Connect
[AppFlowy Application] --> [RESTful Component] : RESTful API Communication
[AppFlowy Application] <..> [Realtime Component] : WebSocket Communication
[AppFlowy Application] --> [AppFlowy RESTful Component] : RESTful API Communication
[AppFlowy Application] <..> [AppFlowy Realtime Component] : WebSocket Communication
@enduml
@startuml
left to right direction
interface AppFlowyServer {
+ enable_sync(_enable: bool)
+ user_service(): Arc<dyn UserService>
+ folder_service(): Arc<dyn FolderCloudService>
+ database_service(): Arc<dyn DatabaseCloudService>
+ document_service(): Arc<dyn DocumentCloudService>
+ collab_storage(): Option<Arc<dyn RemoteCollabStorage>>
}
class SupabaseServer {
+ enable_sync(_enable: bool)
+ user_service(): Arc<dyn UserService>
+ folder_service(): Arc<dyn FolderCloudService>
+ database_service(): Arc<dyn DatabaseCloudService>
+ document_service(): Arc<dyn DocumentCloudService>
+ collab_storage(): Option<Arc<dyn RemoteCollabStorage>>
}
class SelfHostServer {
+ user_service(): Arc<dyn UserService>
+ folder_service(): Arc<dyn FolderCloudService>
+ database_service(): Arc<dyn DatabaseCloudService>
+ document_service(): Arc<dyn DocumentCloudService>
+ collab_storage(): Option<Arc<dyn RemoteCollabStorage>>
}
class LocalServer {
+ user_service(): Arc<dyn UserService>
+ folder_service(): Arc<dyn FolderCloudService>
+ database_service(): Arc<dyn DatabaseCloudService>
+ document_service(): Arc<dyn DocumentCloudService>
+ collab_storage(): Option<Arc<dyn RemoteCollabStorage>>
}
SupabaseServer -u-|> AppFlowyServer
SelfHostServer -u-|> AppFlowyServer
LocalServer -u-|> AppFlowyServer
@enduml

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

View File

@ -1,203 +0,0 @@
@startuml
left to right direction
entity "af_roles" as roles {
id : SERIAL (PK)
name : TEXT
}
entity "af_permissions" as permissions {
id : SERIAL (PK)
name : VARCHAR(255)
access_level : INTEGER
description : TEXT
}
entity "af_role_permissions" as role_permissions {
role_id : INT (FK af_roles.id)
permission_id : INT (FK af_permissions.id)
--
(role_id, permission_id) : PK
}
entity "af_user" as user {
uuid : UUID (PK)
email : TEXT
uid : BIGSERIAL
name : TEXT
created_at : TIMESTAMP WITH TIME ZONE
}
entity "af_workspace" as workspace {
workspace_id : UUID (PK)
database_storage_id : UUID
owner_uid : BIGINT (FK af_user.uid)
created_at : TIMESTAMP WITH TIME ZONE
workspace_type : INTEGER
workspace_name : TEXT
}
entity "af_workspace_member" as workspace_member {
uid : BIGINT
role_id : INT (FK af_roles.id)
workspace_id : UUID (FK af_workspace.workspace_id)
created_at : TIMESTAMP WITH TIME ZONE
updated_at : TIMESTAMP WITH TIME ZONE
--
(uid, workspace_id) : PK
}
entity "af_collab" as collab {
oid : TEXT (PK)
owner_uid : BIGINT
workspace_id : UUID (FK af_workspace.workspace_id)
access_level : INTEGER
created_at : TIMESTAMP WITH TIME ZONE
}
entity "af_collab_update" as collab_update {
oid : TEXT (FK af_collab.oid)
key : BIGSERIAL
value : BYTEA
value_size : INTEGER
partition_key : INTEGER
uid : BIGINT
md5 : TEXT
created_at : TIMESTAMP WITH TIME ZONE
workspace_id : UUID (FK af_workspace.workspace_id)
--
(oid, key, partition_key) : PK
}
entity "af_collab_update_document" as af_collab_update_document {
Inherits af_collab_update (partition_key = 0)
}
entity "af_collab_update_database" as af_collab_update_database {
Inherits af_collab_update (partition_key = 1)
}
entity "af_collab_update_w_database" as af_collab_update_w_database {
Inherits af_collab_update (partition_key = 2)
}
entity "af_collab_update_folder" as af_collab_update_folder {
Inherits af_collab_update (partition_key = 3)
}
af_collab_update_document -u-|> collab_update
af_collab_update_database -u-|> collab_update
af_collab_update_w_database -u-|> collab_update
af_collab_update_folder -u-|> collab_update
entity "af_database_row_update" as database_row_update {
oid : TEXT
key : BIGSERIAL
value : BYTEA
value_size : INTEGER
partition_key : INTEGER
uid : BIGINT
md5 : TEXT
workspace_id : UUID (FK af_workspace.workspace_id)
--
(oid, key) : PK
}
entity "af_collab_member" as collab_member {
uid : BIGINT (FK af_user.uid)
oid : TEXT (FK af_collab.oid)
role_id : INTEGER (FK af_roles.id)
--
(uid, oid) : PK
}
entity "af_collab_statistics" as collab_statistics {
oid : TEXT (PK)
edit_count : BIGINT
}
entity "af_collab_snapshot" as collab_snapshot {
sid : BIGSERIAL (PK)
oid : TEXT (FK af_collab.oid)
name : TEXT
blob : BYTEA
blob_size : INTEGER
edit_count : BIGINT
created_at : TIMESTAMP WITH TIME ZONE
}
roles <-- role_permissions : FK
permissions <-u- role_permissions : FK
user <-- collab : FK
user <-- workspace : FK
user <-- collab_member : FK
roles <-- workspace_member : FK
workspace <-- workspace_member : FK
workspace <-- collab : FK
workspace <-- database_row_update : FK
collab <-- collab_update : FK
collab <-- collab_snapshot: FK
collab <-u- collab_member : FK
collab <-- collab_statistics : PK
roles <-- collab_member : FK
@enduml
@startuml
title Triggers in Database Schema
participant "af_user" as A
participant "af_workspace" as B
participant "af_workspace_member" as C
participant "af_collab" as D
participant "af_collab_update" as E
participant "af_collab_member" as F
participant "af_collab_statistics" as G
participant "af_collab_snapshot" as H
A -> B: create_af_workspace_trigger
note right
This trigger fires after an insert on af_user. It automatically creates a workspace
with the uid of the new user as the owner_uid.
end note
B -> C: manage_af_workspace_member_role_trigger
note right
This trigger fires after an insert on af_workspace. It automatically
creates a workspace member in the af_workspace_member table with the
role 'Owner'.
end note
E -> D: insert_into_af_collab_trigger
note right
This trigger fires before an insert on af_collab_update.
It checks if a corresponding collab exists in the af_collab table.
If not, it creates one with the oid, uid, and current timestamp.
end note
D -> F: insert_into_af_collab_member_trigger
note right
This trigger fires after an insert on af_collab.
It automatically adds the collab's owner to the af_collab_member
table with the role 'Owner'.
end note
E -> G: af_collab_update_edit_count_trigger
note right
This trigger fires after an insert on af_collab_update.
It increments the edit_count of the corresponding collab in
the af_collab_statistics table.
end note
H -> G: af_collab_snapshot_update_edit_count_trigger
note right
This trigger fires after an insert on af_collab_snapshot.
It sets the edit_count of the new snapshot to the current
edit_count of the collab in the af_collab_statistics table.
end note
@enduml

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

View File

@ -21,16 +21,16 @@ use crate::supabase::api::util::{ExtendedResponse, InsertParamsBuilder};
use crate::supabase::api::{PostgresWrapper, SupabaseServerService};
use crate::supabase::define::*;
pub struct RESTfulSupabaseCollabStorageImpl<T>(T);
pub struct SupabaseCollabStorageImpl<T>(T);
impl<T> RESTfulSupabaseCollabStorageImpl<T> {
impl<T> SupabaseCollabStorageImpl<T> {
pub fn new(server: T) -> Self {
Self(server)
}
}
#[async_trait]
impl<T> RemoteCollabStorage for RESTfulSupabaseCollabStorageImpl<T>
impl<T> RemoteCollabStorage for SupabaseCollabStorageImpl<T>
where
T: SupabaseServerService,
{

View File

@ -12,17 +12,17 @@ use crate::supabase::api::request::{
};
use crate::supabase::api::SupabaseServerService;
pub struct RESTfulSupabaseDatabaseServiceImpl<T> {
pub struct SupabaseDatabaseServiceImpl<T> {
server: T,
}
impl<T> RESTfulSupabaseDatabaseServiceImpl<T> {
impl<T> SupabaseDatabaseServiceImpl<T> {
pub fn new(server: T) -> Self {
Self { server }
}
}
impl<T> DatabaseCloudService for RESTfulSupabaseDatabaseServiceImpl<T>
impl<T> DatabaseCloudService for SupabaseDatabaseServiceImpl<T>
where
T: SupabaseServerService,
{

View File

@ -11,14 +11,14 @@ use lib_infra::future::FutureResult;
use crate::supabase::api::request::{get_latest_snapshot_from_server, FetchObjectUpdateAction};
use crate::supabase::api::SupabaseServerService;
pub struct RESTfulSupabaseDocumentServiceImpl<T>(T);
impl<T> RESTfulSupabaseDocumentServiceImpl<T> {
pub struct SupabaseDocumentServiceImpl<T>(T);
impl<T> SupabaseDocumentServiceImpl<T> {
pub fn new(server: T) -> Self {
Self(server)
}
}
impl<T> DocumentCloudService for RESTfulSupabaseDocumentServiceImpl<T>
impl<T> DocumentCloudService for SupabaseDocumentServiceImpl<T>
where
T: SupabaseServerService,
{
@ -31,7 +31,7 @@ where
async move {
let postgrest = try_get_postgrest?;
let action = FetchObjectUpdateAction::new(document_id, CollabType::Document, postgrest);
action.run_with_fix_interval(5, 5).await
action.run_with_fix_interval(5, 10).await
}
.await,
)

View File

@ -19,15 +19,15 @@ use crate::supabase::api::util::{ExtendedResponse, InsertParamsBuilder};
use crate::supabase::api::SupabaseServerService;
use crate::supabase::define::*;
pub struct RESTfulSupabaseFolderServiceImpl<T>(T);
pub struct SupabaseFolderServiceImpl<T>(T);
impl<T> RESTfulSupabaseFolderServiceImpl<T> {
impl<T> SupabaseFolderServiceImpl<T> {
pub fn new(server: T) -> Self {
Self(server)
}
}
impl<T> FolderCloudService for RESTfulSupabaseFolderServiceImpl<T>
impl<T> FolderCloudService for SupabaseFolderServiceImpl<T>
where
T: SupabaseServerService,
{

View File

@ -10,7 +10,7 @@ use chrono::{DateTime, Utc};
use collab_plugins::cloud_storage::{CollabObject, CollabType, RemoteCollabSnapshot};
use serde_json::Value;
use tokio_retry::strategy::FixedInterval;
use tokio_retry::{Action, Retry};
use tokio_retry::{Action, Condition, RetryIf};
use flowy_database_deps::cloud::{CollabObjectUpdate, CollabObjectUpdateByOid};
use lib_infra::util::md5;
@ -34,18 +34,20 @@ impl FetchObjectUpdateAction {
}
}
pub fn run(self) -> Retry<Take<FixedInterval>, FetchObjectUpdateAction> {
pub fn run(self) -> RetryIf<Take<FixedInterval>, FetchObjectUpdateAction, RetryCondition> {
let postgrest = self.postgrest.clone();
let retry_strategy = FixedInterval::new(Duration::from_secs(5)).take(3);
Retry::spawn(retry_strategy, self)
RetryIf::spawn(retry_strategy, self, RetryCondition(postgrest))
}
pub fn run_with_fix_interval(
self,
secs: u64,
times: usize,
) -> Retry<Take<FixedInterval>, FetchObjectUpdateAction> {
) -> RetryIf<Take<FixedInterval>, FetchObjectUpdateAction, RetryCondition> {
let postgrest = self.postgrest.clone();
let retry_strategy = FixedInterval::new(Duration::from_secs(secs)).take(times);
Retry::spawn(retry_strategy, self)
RetryIf::spawn(retry_strategy, self, RetryCondition(postgrest))
}
}
@ -89,9 +91,10 @@ impl BatchFetchObjectUpdateAction {
}
}
pub fn run(self) -> Retry<Take<FixedInterval>, BatchFetchObjectUpdateAction> {
pub fn run(self) -> RetryIf<Take<FixedInterval>, BatchFetchObjectUpdateAction, RetryCondition> {
let postgrest = self.postgrest.clone();
let retry_strategy = FixedInterval::new(Duration::from_secs(5)).take(3);
Retry::spawn(retry_strategy, self)
RetryIf::spawn(retry_strategy, self, RetryCondition(postgrest))
}
}
@ -302,3 +305,10 @@ fn decode_hex_string(s: &str) -> Option<Vec<u8>> {
let s = s.strip_prefix("\\x")?;
hex::decode(s).ok()
}
pub struct RetryCondition(Weak<PostgresWrapper>);
impl Condition<anyhow::Error> for RetryCondition {
fn should_retry(&mut self, _error: &anyhow::Error) -> bool {
self.0.upgrade().is_some()
}
}

View File

@ -10,9 +10,9 @@ use flowy_server_config::supabase_config::SupabaseConfiguration;
use flowy_user_deps::cloud::UserService;
use crate::supabase::api::{
RESTfulPostgresServer, RESTfulSupabaseCollabStorageImpl, RESTfulSupabaseDatabaseServiceImpl,
RESTfulSupabaseDocumentServiceImpl, RESTfulSupabaseFolderServiceImpl,
RESTfulSupabaseUserAuthServiceImpl, SupabaseServerServiceImpl,
RESTfulPostgresServer, RESTfulSupabaseUserAuthServiceImpl, SupabaseCollabStorageImpl,
SupabaseDatabaseServiceImpl, SupabaseDocumentServiceImpl, SupabaseFolderServiceImpl,
SupabaseServerServiceImpl,
};
use crate::AppFlowyServer;
@ -96,25 +96,25 @@ impl AppFlowyServer for SupabaseServer {
}
fn folder_service(&self) -> Arc<dyn FolderCloudService> {
Arc::new(RESTfulSupabaseFolderServiceImpl::new(
SupabaseServerServiceImpl(self.restful_postgres.clone()),
))
Arc::new(SupabaseFolderServiceImpl::new(SupabaseServerServiceImpl(
self.restful_postgres.clone(),
)))
}
fn database_service(&self) -> Arc<dyn DatabaseCloudService> {
Arc::new(RESTfulSupabaseDatabaseServiceImpl::new(
SupabaseServerServiceImpl(self.restful_postgres.clone()),
))
Arc::new(SupabaseDatabaseServiceImpl::new(SupabaseServerServiceImpl(
self.restful_postgres.clone(),
)))
}
fn document_service(&self) -> Arc<dyn DocumentCloudService> {
Arc::new(RESTfulSupabaseDocumentServiceImpl::new(
SupabaseServerServiceImpl(self.restful_postgres.clone()),
))
Arc::new(SupabaseDocumentServiceImpl::new(SupabaseServerServiceImpl(
self.restful_postgres.clone(),
)))
}
fn collab_storage(&self) -> Option<Arc<dyn RemoteCollabStorage>> {
Some(Arc::new(RESTfulSupabaseCollabStorageImpl::new(
Some(Arc::new(SupabaseCollabStorageImpl::new(
SupabaseServerServiceImpl(self.restful_postgres.clone()),
)))
}

View File

@ -7,8 +7,8 @@ use uuid::Uuid;
use flowy_database_deps::cloud::DatabaseCloudService;
use flowy_folder_deps::cloud::FolderCloudService;
use flowy_server::supabase::api::{
RESTfulPostgresServer, RESTfulSupabaseCollabStorageImpl, RESTfulSupabaseDatabaseServiceImpl,
RESTfulSupabaseFolderServiceImpl, RESTfulSupabaseUserAuthServiceImpl, SupabaseServerServiceImpl,
RESTfulPostgresServer, RESTfulSupabaseUserAuthServiceImpl, SupabaseCollabStorageImpl,
SupabaseDatabaseServiceImpl, SupabaseFolderServiceImpl, SupabaseServerServiceImpl,
};
use flowy_server::supabase::define::{USER_EMAIL, USER_UUID};
use flowy_server_config::supabase_config::SupabaseConfiguration;
@ -25,7 +25,7 @@ pub fn get_supabase_config() -> Option<SupabaseConfiguration> {
pub fn collab_service() -> Arc<dyn RemoteCollabStorage> {
let config = SupabaseConfiguration::from_env().unwrap();
let server = Arc::new(RESTfulPostgresServer::new(config));
Arc::new(RESTfulSupabaseCollabStorageImpl::new(
Arc::new(SupabaseCollabStorageImpl::new(
SupabaseServerServiceImpl::new(server),
))
}
@ -33,7 +33,7 @@ pub fn collab_service() -> Arc<dyn RemoteCollabStorage> {
pub fn database_service() -> Arc<dyn DatabaseCloudService> {
let config = SupabaseConfiguration::from_env().unwrap();
let server = Arc::new(RESTfulPostgresServer::new(config));
Arc::new(RESTfulSupabaseDatabaseServiceImpl::new(
Arc::new(SupabaseDatabaseServiceImpl::new(
SupabaseServerServiceImpl::new(server),
))
}
@ -49,7 +49,7 @@ pub fn user_auth_service() -> Arc<dyn UserService> {
pub fn folder_service() -> Arc<dyn FolderCloudService> {
let config = SupabaseConfiguration::from_env().unwrap();
let server = Arc::new(RESTfulPostgresServer::new(config));
Arc::new(RESTfulSupabaseFolderServiceImpl::new(
Arc::new(SupabaseFolderServiceImpl::new(
SupabaseServerServiceImpl::new(server),
))
}

View File

@ -584,10 +584,7 @@ impl UserSession {
match KV::get_object::<Session>(&self.session_config.session_cache_key) {
None => Err(FlowyError::new(
ErrorCode::RecordNotFound,
format!(
"Can't find the value of {}, User is not logged in",
self.session_config.session_cache_key
),
"User is not logged in",
)),
Some(session) => Ok(session),
}

View File

@ -59,6 +59,19 @@ script = [
]
script_runner = "@shell"
[tasks.sdk-build.mac]
private = true
script = [
"""
cd rust-lib/
rustup show
echo cargo build --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}"
RUSTFLAGS="-C target-cpu=native -C link-arg=-mmacosx-version-min=11.0" cargo build --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}"
cd ../
""",
]
script_runner = "@shell"
[tasks.sdk-build-android]
private = true
script = [