feat: improve workspace api

This commit is contained in:
Zack Fu Zi Xiang 2024-07-25 04:12:22 +08:00
parent 47f87cee1c
commit 11c1521b71
No known key found for this signature in database
19 changed files with 151 additions and 54 deletions

View file

@ -0,0 +1,28 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT workspace_id, COUNT(*) AS member_count\n FROM af_workspace_member\n WHERE workspace_id = ANY($1)\n GROUP BY workspace_id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "member_count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"UuidArray"
]
},
"nullable": [
false,
null
]
},
"hash": "f1c55a5255b1c7e5c96e21cb8557b6d46f713630d819f94cc7a966dbdaf00a2a"
}

View file

@ -72,7 +72,6 @@ pub async fn workspace_id_from_client(c: &Client) -> String {
c.get_workspaces()
.await
.unwrap()
.0
.first()
.unwrap()
.workspace_id

View file

@ -391,7 +391,6 @@ impl TestClient {
.get_workspaces()
.await
.unwrap()
.0
.first()
.unwrap()
.workspace_id

View file

@ -1,5 +1,6 @@
use crate::notify::{ClientToken, TokenStateReceiver};
use app_error::AppError;
use client_api_entity::workspace_dto::QueryWorkspaceParam;
use client_api_entity::AuthProvider;
use client_api_entity::CollabType;
use gotrue::grant::PasswordGrant;
@ -16,7 +17,7 @@ use reqwest::Method;
use reqwest::RequestBuilder;
use client_api_entity::{
AFSnapshotMeta, AFSnapshotMetas, AFUserProfile, AFUserWorkspaceInfo, AFWorkspace, AFWorkspaces,
AFSnapshotMeta, AFSnapshotMetas, AFUserProfile, AFUserWorkspaceInfo, AFWorkspace,
QuerySnapshotParams, SnapshotData,
};
use semver::Version;
@ -627,16 +628,26 @@ impl Client {
AppResponse::<()>::from_response(resp).await?.into_error()
}
pub async fn get_workspaces(&self) -> Result<Vec<AFWorkspace>, AppResponseError> {
self
.get_workspaces_opt(QueryWorkspaceParam::default())
.await
}
#[instrument(level = "info", skip_all, err)]
pub async fn get_workspaces(&self) -> Result<AFWorkspaces, AppResponseError> {
pub async fn get_workspaces_opt(
&self,
param: QueryWorkspaceParam,
) -> Result<Vec<AFWorkspace>, AppResponseError> {
let url = format!("{}/api/workspace", self.base_url);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.query(&param)
.send()
.await?;
log_request_id(&resp);
AppResponse::<AFWorkspaces>::from_response(resp)
AppResponse::<Vec<AFWorkspace>>::from_response(resp)
.await?
.into_data()
}

View file

@ -530,11 +530,9 @@ pub struct AFWorkspace {
pub workspace_name: String,
pub created_at: DateTime<Utc>,
pub icon: String,
pub member_count: Option<i64>,
}
#[derive(Serialize, Deserialize)]
pub struct AFWorkspaces(pub Vec<AFWorkspace>);
#[derive(Serialize, Deserialize)]
pub struct AFWorkspaceSettings {
#[serde(default)]

View file

@ -47,6 +47,7 @@ impl TryFrom<AFWorkspaceRow> for AFWorkspace {
workspace_name,
created_at,
icon,
member_count: None,
})
}
}

View file

@ -643,6 +643,39 @@ pub async fn select_all_user_workspaces<'a, E: Executor<'a, Database = Postgres>
Ok(workspaces)
}
pub async fn select_member_count_for_workspaces<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
workspace_ids: &[Uuid],
) -> Result<HashMap<Uuid, i64>, AppError> {
let query_res = sqlx::query!(
r#"
SELECT workspace_id, COUNT(*) AS member_count
FROM af_workspace_member
WHERE workspace_id = ANY($1)
GROUP BY workspace_id
"#,
workspace_ids
)
.fetch_all(executor)
.await?;
let mut ret = HashMap::with_capacity(workspace_ids.len());
for row in query_res {
let count = match row.member_count {
Some(c) => c,
None => continue,
};
ret.insert(row.workspace_id, count);
}
for workspace_id in workspace_ids.iter() {
if !ret.contains_key(workspace_id) {
ret.insert(*workspace_id, 0);
}
}
Ok(ret)
}
pub async fn select_permission(
pool: &PgPool,
permission_id: &i64,

View file

@ -120,3 +120,8 @@ pub struct CollabResponse {
#[serde(default)]
pub object_id: String,
}
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct QueryWorkspaceParam {
pub include_member_count: Option<bool>,
}

View file

@ -231,28 +231,20 @@ async fn delete_workspace_handler(
Ok(AppResponse::Ok().into())
}
// TODO: also get shared workspaces
/// Get all user owned and shared workspaces
#[instrument(skip_all, err)]
async fn list_workspace_handler(
uuid: UserUuid,
state: Data<AppState>,
) -> Result<JsonAppResponse<AFWorkspaces>> {
let rows = workspace::ops::get_all_user_workspaces(&state.pg_pool, &uuid).await?;
let workspaces = rows
.into_iter()
.flat_map(|row| {
let result = AFWorkspace::try_from(row);
if let Err(err) = &result {
event!(
tracing::Level::ERROR,
"Failed to convert workspace row to AFWorkspace: {:?}",
err
);
}
result
})
.collect::<Vec<_>>();
Ok(AppResponse::Ok().with_data(AFWorkspaces(workspaces)).into())
query: web::Query<QueryWorkspaceParam>,
) -> Result<JsonAppResponse<Vec<AFWorkspace>>> {
let workspaces = workspace::ops::get_all_user_workspaces(
&state.pg_pool,
&uuid,
query.into_inner().include_member_count.unwrap_or(false),
)
.await?;
Ok(AppResponse::Ok().with_data(workspaces).into())
}
#[instrument(skip(payload, state), err)]

View file

@ -15,18 +15,19 @@ use app_error::{AppError, ErrorCode};
use appflowy_collaborate::collab::storage::CollabAccessControlStorage;
use database::collab::upsert_collab_member_with_txn;
use database::file::s3_client_impl::S3BucketStorage;
use database::pg_row::{AFWorkspaceMemberRow, AFWorkspaceRow};
use database::pg_row::AFWorkspaceMemberRow;
use database::user::select_uid_from_email;
use database::workspace::{
change_workspace_icon, delete_from_workspace, delete_published_collabs, delete_workspace_members,
get_invitation_by_id, insert_or_replace_publish_collab_metas, insert_user_workspace,
insert_workspace_invitation, rename_workspace, select_all_user_workspaces,
select_publish_collab_meta, select_published_collab_blob, select_published_collab_info,
select_user_is_collab_publisher_for_all_views, select_user_is_workspace_owner, select_workspace,
select_workspace_invitations_for_user, select_workspace_member, select_workspace_member_list,
select_workspace_publish_namespace, select_workspace_publish_namespace_exists,
select_workspace_settings, select_workspace_total_collab_bytes, update_updated_at_of_workspace,
select_member_count_for_workspaces, select_publish_collab_meta, select_published_collab_blob,
select_published_collab_info, select_user_is_collab_publisher_for_all_views,
select_user_is_workspace_owner, select_workspace, select_workspace_invitations_for_user,
select_workspace_member, select_workspace_member_list, select_workspace_publish_namespace,
select_workspace_publish_namespace_exists, select_workspace_settings,
select_workspace_total_collab_bytes, update_updated_at_of_workspace,
update_workspace_invitation_set_status_accepted, update_workspace_publish_namespace,
upsert_workspace_member, upsert_workspace_member_with_txn, upsert_workspace_settings,
};
@ -187,8 +188,32 @@ pub async fn delete_published_workspace_collab(
pub async fn get_all_user_workspaces(
pg_pool: &PgPool,
user_uuid: &Uuid,
) -> Result<Vec<AFWorkspaceRow>, AppResponseError> {
include_member_count: bool,
) -> Result<Vec<AFWorkspace>, AppResponseError> {
let workspaces = select_all_user_workspaces(pg_pool, user_uuid).await?;
let mut workspaces = workspaces
.into_iter()
.flat_map(|row| {
let result = AFWorkspace::try_from(row);
if let Err(err) = &result {
tracing::error!("Failed to convert workspace row to AFWorkspace: {:?}", err);
}
result
})
.collect::<Vec<_>>();
if include_member_count {
let ids = workspaces
.iter()
.map(|row| row.workspace_id)
.collect::<Vec<_>>();
let member_count_by_workspace_id = select_member_count_for_workspaces(pg_pool, &ids).await?;
for workspace in workspaces.iter_mut() {
if let Some(member_count) = member_count_by_workspace_id.get(&workspace.workspace_id) {
workspace.member_count = Some(*member_count);
}
}
}
Ok(workspaces)
}

View file

@ -16,7 +16,6 @@ async fn get_but_not_exists() {
.get_workspaces()
.await
.unwrap()
.0
.first()
.unwrap()
.workspace_id

View file

@ -152,7 +152,7 @@ async fn admin_generate_link_and_user_sign_in_and_invite() {
assert!(is_new);
let workspaces = client.get_workspaces().await.unwrap();
assert_eq!(workspaces.0.len(), 1);
assert_eq!(workspaces.len(), 1);
let friend_email = generate_unique_email();
client.invite(&friend_email).await.unwrap();

View file

@ -4,7 +4,7 @@ use gotrue::params::{AdminDeleteUserParams, AdminUserParams};
#[tokio::test]
async fn admin_delete_create_same_user_hard() {
let (client, user) = generate_unique_registered_user_client().await;
let workspaces = client.get_workspaces().await.unwrap().0;
let workspaces = client.get_workspaces().await.unwrap();
assert_eq!(workspaces.len(), 1);
let workspace_id = workspaces[0].workspace_id;
let user_uuid = client.get_profile().await.unwrap().uuid;
@ -48,7 +48,7 @@ async fn admin_delete_create_same_user_hard() {
.await
.unwrap();
let recreated_user_uuid = client.get_profile().await.unwrap().uuid;
let recreated_workspace_uuid = client.get_workspaces().await.unwrap().0[0].workspace_id;
let recreated_workspace_uuid = client.get_workspaces().await.unwrap()[0].workspace_id;
assert_ne!(user_uuid, recreated_user_uuid);
assert_ne!(workspace_id, recreated_workspace_uuid);
}

View file

@ -63,7 +63,7 @@ async fn sign_in_success() {
.is_some());
let workspaces = c.get_workspaces().await.unwrap();
assert_eq!(workspaces.0.len(), 1);
assert_eq!(workspaces.len(), 1);
let _ = c.get_profile().await.unwrap();
}
@ -78,7 +78,7 @@ async fn sign_in_success() {
// workspaces should be the same
let workspaces = c.get_workspaces().await.unwrap();
assert_eq!(workspaces.0.len(), 1);
assert_eq!(workspaces.len(), 1);
}
}

View file

@ -1,6 +1,6 @@
use client_api_test::generate_unique_registered_user_client;
use database_entity::dto::{AFRole, AFWorkspaceInvitationStatus};
use shared_entity::dto::workspace_dto::WorkspaceMemberInvitation;
use shared_entity::dto::workspace_dto::{QueryWorkspaceParam, WorkspaceMemberInvitation};
#[tokio::test]
async fn invite_workspace_crud() {
@ -9,7 +9,6 @@ async fn invite_workspace_crud() {
.get_workspaces()
.await
.unwrap()
.0
.first()
.unwrap()
.workspace_id;
@ -67,4 +66,17 @@ async fn invite_workspace_crud() {
.await
.unwrap();
assert_eq!(accepted_invs.len(), 1);
// workspace now have 2 members
let member_count = bob_client
.get_workspaces_opt(QueryWorkspaceParam {
include_member_count: Some(true),
})
.await
.unwrap()
.first()
.unwrap()
.member_count
.unwrap();
assert_eq!(member_count, 2);
}

View file

@ -148,7 +148,7 @@ async fn add_not_exist_workspace_members() {
.unwrap();
let workspaces = invited_client.get_workspaces().await.unwrap();
assert_eq!(workspaces.0.len(), 2);
assert_eq!(workspaces.len(), 2);
}
#[tokio::test]

View file

@ -251,7 +251,6 @@ async fn get_first_workspace_string(c: &client_api::Client) -> String {
c.get_workspaces()
.await
.unwrap()
.0
.first()
.unwrap()
.workspace_id

View file

@ -8,7 +8,7 @@ use shared_entity::dto::workspace_dto::PatchWorkspaceParam;
async fn add_and_delete_workspace_for_user() {
let (c, _user) = generate_unique_registered_user_client().await;
let workspaces = c.get_workspaces().await.unwrap();
assert_eq!(workspaces.0.len(), 1);
assert_eq!(workspaces.len(), 1);
let newly_added_workspace = c
.create_workspace(CreateWorkspaceParam {
workspace_name: Some("my_workspace".to_string()),
@ -16,10 +16,9 @@ async fn add_and_delete_workspace_for_user() {
.await
.unwrap();
let workspaces = c.get_workspaces().await.unwrap();
assert_eq!(workspaces.0.len(), 2);
assert_eq!(workspaces.len(), 2);
let _ = workspaces
.0
.iter()
.find(|w| {
w.workspace_name == "my_workspace" && w.workspace_id == newly_added_workspace.workspace_id
@ -39,7 +38,7 @@ async fn add_and_delete_workspace_for_user() {
c.delete_workspace(&workspace_id).await.unwrap();
let workspaces = c.get_workspaces().await.unwrap();
assert_eq!(workspaces.0.len(), 1);
assert_eq!(workspaces.len(), 1);
}
#[tokio::test]
@ -49,7 +48,6 @@ async fn test_workspace_rename_and_icon_change() {
.get_workspaces()
.await
.unwrap()
.0
.first()
.unwrap()
.workspace_id;
@ -66,7 +64,6 @@ async fn test_workspace_rename_and_icon_change() {
let workspaces = c.get_workspaces().await.expect("Failed to get workspaces");
let actual_new_name = &workspaces
.0
.first()
.expect("No workspace found")
.workspace_name;
@ -83,7 +80,6 @@ async fn test_workspace_rename_and_icon_change() {
.expect("Failed to rename workspace");
let workspaces = c.get_workspaces().await.expect("Failed to get workspaces");
let actual_new_name = &workspaces
.0
.first()
.expect("No workspace found")
.workspace_name;
@ -98,7 +94,7 @@ async fn test_workspace_rename_and_icon_change() {
.await
.expect("Failed to change icon");
let workspaces = c.get_workspaces().await.expect("Failed to get workspaces");
let icon = &workspaces.0.first().expect("No workspace found").icon;
let icon = &workspaces.first().expect("No workspace found").icon;
assert_eq!(icon, "icon123");
}
{
@ -110,7 +106,7 @@ async fn test_workspace_rename_and_icon_change() {
.await
.expect("Failed to change icon");
let workspaces = c.get_workspaces().await.expect("Failed to get workspaces");
let workspace = workspaces.0.first().expect("No workspace found");
let workspace = workspaces.first().expect("No workspace found");
let icon = workspace.icon.as_str();
let name = workspace.workspace_name.as_str();

View file

@ -8,7 +8,7 @@ use uuid::Uuid;
#[tokio::test]
async fn get_and_set_workspace_by_owner() {
let (c, _user) = generate_unique_registered_user_client().await;
let workspaces = c.get_workspaces().await.unwrap().0;
let workspaces = c.get_workspaces().await.unwrap();
let workspace_id = workspaces.first().unwrap().workspace_id.to_string();
let mut settings = c.get_workspace_settings(&workspace_id).await.unwrap();
@ -32,7 +32,7 @@ async fn get_and_set_workspace_by_owner() {
#[tokio::test]
async fn get_and_set_workspace_by_non_owner() {
let (alice_client, _alice) = generate_unique_registered_user_client().await;
let workspaces = alice_client.get_workspaces().await.unwrap().0;
let workspaces = alice_client.get_workspaces().await.unwrap();
let alice_workspace_id = workspaces.first().unwrap().workspace_id;
let (bob_client, bob) = generate_unique_registered_user_client().await;