Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add database listings #842

Merged
merged 5 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions libs/client-api/src/http_collab.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::http::log_request_id;
use crate::{blocking_brotli_compress, Client};
use app_error::AppError;
use client_api_entity::workspace_dto::AFDatabase;
use client_api_entity::{
BatchQueryCollabParams, BatchQueryCollabResult, CreateCollabParams, DeleteCollabParams,
QueryCollab, UpdateCollabWebParams,
Expand Down Expand Up @@ -123,6 +124,7 @@ impl Client {
.await?
.into_data()
}

#[instrument(level = "info", skip_all, err)]
pub async fn delete_collab(&self, params: DeleteCollabParams) -> Result<(), AppResponseError> {
let url = format!(
Expand All @@ -138,4 +140,19 @@ impl Client {
log_request_id(&resp);
AppResponse::<()>::from_response(resp).await?.into_error()
}

#[instrument(level = "info", skip_all, err)]
pub async fn list_databases(
&self,
workspace_id: &str,
) -> Result<Vec<AFDatabase>, AppResponseError> {
let url = format!("{}/api/workspace/{}/database", self.base_url, workspace_id);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
log_request_id(&resp);
AppResponse::from_response(resp).await?.into_data()
}
}
13 changes: 13 additions & 0 deletions libs/shared-entity/src/dto/workspace_dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,16 @@ pub struct PublishedView {
pub extra: Option<serde_json::Value>,
pub children: Vec<PublishedView>,
}

#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct AFDatabase {
pub id: String,
pub name: String,
pub fields: Vec<AFDatabaseField>,
}

#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AFDatabaseField {
pub name: String,
pub field_type: String,
}
18 changes: 18 additions & 0 deletions src/api/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ pub fn workspace_scope() -> Scope {
// Web browser can't carry payload when using GET method, so for browser compatibility, we use POST method
.route(web::post().to(batch_get_collab_handler)),
)
.service(web::resource("/{workspace_id}/database").route(web::get().to(list_database_handler)))
}

pub fn collab_scope() -> Scope {
Expand Down Expand Up @@ -1730,6 +1731,23 @@ async fn get_workspace_publish_outline_handler(
Ok(Json(AppResponse::Ok().with_data(published_view)))
}

async fn list_database_handler(
user_uuid: UserUuid,
workspace_id: web::Path<String>,
state: Data<AppState>,
) -> Result<Json<AppResponse<Vec<AFDatabase>>>> {
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let workspace_id = workspace_id.into_inner();
let dbs = biz::collab::ops::list_database(
&state.pg_pool,
&state.collab_access_control_storage,
uid,
workspace_id,
)
.await?;
Ok(Json(AppResponse::Ok().with_data(dbs)))
}

#[inline]
async fn parser_realtime_msg(
payload: Bytes,
Expand Down
107 changes: 107 additions & 0 deletions src/biz/collab/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@ use std::sync::Arc;

use app_error::AppError;
use appflowy_collaborate::collab::storage::CollabAccessControlStorage;
use collab::preclude::Collab;
use collab_database::database::DatabaseBody;
use collab_database::entity::FieldType;
use collab_database::workspace_database::NoPersistenceDatabaseCollabService;
use collab_database::workspace_database::WorkspaceDatabaseBody;
use collab_entity::CollabType;
use collab_entity::EncodedCollab;
use collab_folder::SectionItem;
use collab_folder::{CollabOrigin, Folder};
use database::collab::select_workspace_database_oid;
use database::collab::{CollabStorage, GetCollabOrigin};
use database::publish::select_published_view_ids_for_workspace;
use database::publish::select_workspace_id_for_publish_namespace;
use database_entity::dto::QueryCollabResult;
use database_entity::dto::{QueryCollab, QueryCollabParams};
use shared_entity::dto::workspace_dto::AFDatabase;
use shared_entity::dto::workspace_dto::AFDatabaseField;
use shared_entity::dto::workspace_dto::FavoriteFolderView;
use shared_entity::dto::workspace_dto::RecentFolderView;
use shared_entity::dto::workspace_dto::TrashFolderView;
Expand Down Expand Up @@ -348,3 +357,101 @@ pub async fn get_published_view(
collab_folder_to_published_outline(&workspace_id.to_string(), &folder, &publish_view_ids)?;
Ok(published_view)
}

pub async fn list_database(
pg_pool: &PgPool,
collab_storage: &CollabAccessControlStorage,
uid: i64,
workspace_uuid_str: String,
) -> Result<Vec<AFDatabase>, AppError> {
let workspace_uuid: Uuid = workspace_uuid_str.as_str().parse()?;
let ws_db_oid = select_workspace_database_oid(pg_pool, &workspace_uuid).await?;

let ec = get_latest_collab_encoded(
collab_storage,
GetCollabOrigin::Server,
&workspace_uuid_str,
&ws_db_oid,
CollabType::WorkspaceDatabase,
)
.await?;
let mut collab: Collab =
Collab::new_with_source(CollabOrigin::Server, &ws_db_oid, ec.into(), vec![], false).map_err(
|e| {
AppError::Internal(anyhow::anyhow!(
"Failed to create collab from encoded collab: {:?}",
e
))
},
)?;

let ws_body = WorkspaceDatabaseBody::open(&mut collab).map_err(|e| {
AppError::Internal(anyhow::anyhow!(
"Failed to open workspace database body: {:?}",
e
))
})?;
let db_metas = ws_body.get_all_meta(&collab.transact());
let query_collabs: Vec<QueryCollab> = db_metas
.into_iter()
.map(|meta| QueryCollab {
object_id: meta.database_id.clone(),
collab_type: CollabType::Database,
})
.collect();
let results = collab_storage
.batch_get_collab(&uid, query_collabs, true)
.await;

let txn = collab.transact();
let mut af_databases: Vec<AFDatabase> = Vec::with_capacity(results.len());
for (oid, result) in results {
match result {
QueryCollabResult::Success { encode_collab_v1 } => {
match EncodedCollab::decode_from_bytes(&encode_collab_v1) {
Ok(ec) => {
match Collab::new_with_source(CollabOrigin::Server, &oid, ec.into(), vec![], false) {
Ok(db_collab) => match DatabaseBody::from_collab(
&db_collab,
Arc::new(NoPersistenceDatabaseCollabService),
None,
) {
Some(db_body) => match db_body.metas.get_inline_view_id(&txn) {
Some(iid) => match db_body.views.get_view(&txn, &iid) {
Some(iview) => {
let name = iview.name;

let db_fields = db_body.fields.get_all_fields(&txn);
let mut af_fields: Vec<AFDatabaseField> = Vec::with_capacity(db_fields.len());
for db_field in db_fields {
af_fields.push(AFDatabaseField {
name: db_field.name,
field_type: format!("{:?}", FieldType::from(db_field.field_type)),
});
}
af_databases.push(AFDatabase {
id: db_body.get_database_id(&txn),
name,
fields: af_fields,
});
},
None => tracing::warn!("Failed to get inline view: {}", iid),
},
None => tracing::error!("Failed to get inline view id for database: {}", oid),
},
None => tracing::error!("Failed to create db_body from db_collab, oid: {}", oid),
},
Err(err) => tracing::error!("Failed to create db_collab: {:?}", err),
}
},
Err(err) => tracing::error!("Failed to decode collab: {:?}", err),
}
},
QueryCollabResult::Failed { error } => {
tracing::warn!("Failed to get collab: {:?}", error)
},
}
}

Ok(af_databases)
}
33 changes: 33 additions & 0 deletions tests/workspace/workspace_crud.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,42 @@
use client_api_test::generate_unique_registered_user_client;
use collab_entity::CollabType;
use database_entity::dto::QueryCollabParams;
use shared_entity::dto::workspace_dto::AFDatabaseField;
use shared_entity::dto::workspace_dto::CreateWorkspaceParam;
use shared_entity::dto::workspace_dto::PatchWorkspaceParam;

#[tokio::test]
async fn workspace_list_database() {
let (c, _user) = generate_unique_registered_user_client().await;
let workspace_id = c.get_workspaces().await.unwrap()[0].workspace_id;
let dbs = c.list_databases(&workspace_id.to_string()).await.unwrap();
assert_eq!(dbs.len(), 1);

let db = &dbs[0];

assert_eq!(db.name, "");
assert!(db.fields.contains(&AFDatabaseField {
name: "Last modified".to_string(),
field_type: "LastEditedTime".to_string(),
}));
assert!(db.fields.contains(&AFDatabaseField {
name: "Multiselect".to_string(),
field_type: "MultiSelect".to_string(),
}));
assert!(db.fields.contains(&AFDatabaseField {
name: "Tasks".to_string(),
field_type: "Checklist".to_string(),
}));
assert!(db.fields.contains(&AFDatabaseField {
name: "Status".to_string(),
field_type: "SingleSelect".to_string(),
}));
assert!(db.fields.contains(&AFDatabaseField {
name: "Description".to_string(),
field_type: "RichText".to_string(),
}));
}

#[tokio::test]
async fn add_and_delete_workspace_for_user() {
let (c, _user) = generate_unique_registered_user_client().await;
Expand Down
Loading