feat: add row details
This commit is contained in:
parent
f3a2444615
commit
6d4200d1e7
|
|
@ -2,7 +2,9 @@ use crate::http::log_request_id;
|
||||||
use crate::{blocking_brotli_compress, brotli_compress, Client};
|
use crate::{blocking_brotli_compress, brotli_compress, Client};
|
||||||
use app_error::AppError;
|
use app_error::AppError;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use client_api_entity::workspace_dto::{AFDatabase, AFDatabaseRow, ListDatabaseParam};
|
use client_api_entity::workspace_dto::{
|
||||||
|
AFDatabase, AFDatabaseRow, AFDatabaseRowDetail, ListDatabaseParam, ListDatabaseRowDetailParam,
|
||||||
|
};
|
||||||
use client_api_entity::{
|
use client_api_entity::{
|
||||||
BatchQueryCollabParams, BatchQueryCollabResult, CollabParams, CreateCollabParams,
|
BatchQueryCollabParams, BatchQueryCollabResult, CollabParams, CreateCollabParams,
|
||||||
DeleteCollabParams, PublishCollabItem, QueryCollab, QueryCollabParams, UpdateCollabWebParams,
|
DeleteCollabParams, PublishCollabItem, QueryCollab, QueryCollabParams, UpdateCollabWebParams,
|
||||||
|
|
@ -190,6 +192,26 @@ impl Client {
|
||||||
AppResponse::from_response(resp).await?.into_data()
|
AppResponse::from_response(resp).await?.into_data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_database_row_details(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
database_id: &str,
|
||||||
|
row_ids: &[&str],
|
||||||
|
) -> Result<Vec<AFDatabaseRowDetail>, AppResponseError> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/workspace/{}/database/{}/row/detail",
|
||||||
|
self.base_url, workspace_id, database_id
|
||||||
|
);
|
||||||
|
let resp = self
|
||||||
|
.http_client_with_auth(Method::GET, &url)
|
||||||
|
.await?
|
||||||
|
.query(&ListDatabaseRowDetailParam::from(row_ids))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
log_request_id(&resp);
|
||||||
|
AppResponse::from_response(resp).await?.into_data()
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip_all, err)]
|
#[instrument(level = "debug", skip_all, err)]
|
||||||
pub async fn post_realtime_msg(
|
pub async fn post_realtime_msg(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,22 @@ pub struct ListDatabaseParam {
|
||||||
pub name_filter: Option<String>, // logic: if database name contains
|
pub name_filter: Option<String>, // logic: if database name contains
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct ListDatabaseRowDetailParam {
|
||||||
|
// Comma separated database row ids
|
||||||
|
// e.g. "<uuid_1>,<uuid_2>,<uuid_3>"
|
||||||
|
pub ids: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListDatabaseRowDetailParam {
|
||||||
|
pub fn from(ids: &[&str]) -> Self {
|
||||||
|
Self { ids: ids.join(",") }
|
||||||
|
}
|
||||||
|
pub fn into_ids(&self) -> Vec<&str> {
|
||||||
|
self.ids.split(',').collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||||
pub struct QueryWorkspaceFolder {
|
pub struct QueryWorkspaceFolder {
|
||||||
pub depth: Option<u32>,
|
pub depth: Option<u32>,
|
||||||
|
|
@ -339,3 +355,9 @@ pub struct AFDatabaseMeta {
|
||||||
pub struct AFDatabaseRow {
|
pub struct AFDatabaseRow {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct AFDatabaseRowDetail {
|
||||||
|
pub id: String,
|
||||||
|
pub cells: HashMap<String, HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,10 @@ pub fn workspace_scope() -> Scope {
|
||||||
web::resource("/{workspace_id}/database/{database_id}/row")
|
web::resource("/{workspace_id}/database/{database_id}/row")
|
||||||
.route(web::get().to(list_database_row_id_handler)),
|
.route(web::get().to(list_database_row_id_handler)),
|
||||||
)
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/{workspace_id}/database/{database_id}/row/detail")
|
||||||
|
.route(web::get().to(list_database_row_details_handler)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn collab_scope() -> Scope {
|
pub fn collab_scope() -> Scope {
|
||||||
|
|
@ -1897,6 +1901,33 @@ async fn list_database_row_id_handler(
|
||||||
Ok(Json(AppResponse::Ok().with_data(db_rows)))
|
Ok(Json(AppResponse::Ok().with_data(db_rows)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_database_row_details_handler(
|
||||||
|
user_uuid: UserUuid,
|
||||||
|
path_param: web::Path<(String, String)>,
|
||||||
|
state: Data<AppState>,
|
||||||
|
param: web::Query<ListDatabaseRowDetailParam>,
|
||||||
|
) -> Result<Json<AppResponse<Vec<AFDatabaseRowDetail>>>> {
|
||||||
|
let (workspace_id, db_id) = path_param.into_inner();
|
||||||
|
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
|
||||||
|
let list_db_row_query = param.into_inner();
|
||||||
|
let row_ids = list_db_row_query.into_ids();
|
||||||
|
|
||||||
|
state
|
||||||
|
.workspace_access_control
|
||||||
|
.enforce_action(&uid, &workspace_id, Action::Read)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let db_rows = biz::collab::ops::list_database_row_details(
|
||||||
|
&state.collab_access_control_storage,
|
||||||
|
uid,
|
||||||
|
workspace_id,
|
||||||
|
db_id,
|
||||||
|
&row_ids,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(AppResponse::Ok().with_data(db_rows)))
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
async fn parser_realtime_msg(
|
async fn parser_realtime_msg(
|
||||||
payload: Bytes,
|
payload: Bytes,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use app_error::AppError;
|
use app_error::AppError;
|
||||||
|
|
@ -5,6 +6,8 @@ use appflowy_collaborate::collab::storage::CollabAccessControlStorage;
|
||||||
use collab::preclude::Collab;
|
use collab::preclude::Collab;
|
||||||
use collab_database::database::DatabaseBody;
|
use collab_database::database::DatabaseBody;
|
||||||
use collab_database::entity::FieldType;
|
use collab_database::entity::FieldType;
|
||||||
|
use collab_database::fields::Field;
|
||||||
|
use collab_database::rows::RowDetail;
|
||||||
use collab_database::workspace_database::NoPersistenceDatabaseCollabService;
|
use collab_database::workspace_database::NoPersistenceDatabaseCollabService;
|
||||||
use collab_database::workspace_database::WorkspaceDatabaseBody;
|
use collab_database::workspace_database::WorkspaceDatabaseBody;
|
||||||
use collab_entity::CollabType;
|
use collab_entity::CollabType;
|
||||||
|
|
@ -20,6 +23,7 @@ use database_entity::dto::{QueryCollab, QueryCollabParams};
|
||||||
use shared_entity::dto::workspace_dto::AFDatabase;
|
use shared_entity::dto::workspace_dto::AFDatabase;
|
||||||
use shared_entity::dto::workspace_dto::AFDatabaseField;
|
use shared_entity::dto::workspace_dto::AFDatabaseField;
|
||||||
use shared_entity::dto::workspace_dto::AFDatabaseRow;
|
use shared_entity::dto::workspace_dto::AFDatabaseRow;
|
||||||
|
use shared_entity::dto::workspace_dto::AFDatabaseRowDetail;
|
||||||
use shared_entity::dto::workspace_dto::FavoriteFolderView;
|
use shared_entity::dto::workspace_dto::FavoriteFolderView;
|
||||||
use shared_entity::dto::workspace_dto::RecentFolderView;
|
use shared_entity::dto::workspace_dto::RecentFolderView;
|
||||||
use shared_entity::dto::workspace_dto::TrashFolderView;
|
use shared_entity::dto::workspace_dto::TrashFolderView;
|
||||||
|
|
@ -517,3 +521,130 @@ pub async fn list_database_row(
|
||||||
|
|
||||||
Ok(db_rows)
|
Ok(db_rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_database_row_details(
|
||||||
|
collab_storage: &CollabAccessControlStorage,
|
||||||
|
uid: i64,
|
||||||
|
workspace_uuid_str: String,
|
||||||
|
database_uuid_str: String,
|
||||||
|
row_ids: &[&str],
|
||||||
|
) -> Result<Vec<AFDatabaseRowDetail>, AppError> {
|
||||||
|
let query_collabs: Vec<QueryCollab> = row_ids
|
||||||
|
.iter()
|
||||||
|
.map(|id| QueryCollab {
|
||||||
|
object_id: id.to_string(),
|
||||||
|
collab_type: CollabType::DatabaseRow,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let database_collab = get_latest_collab(
|
||||||
|
collab_storage,
|
||||||
|
GetCollabOrigin::User { uid },
|
||||||
|
&workspace_uuid_str,
|
||||||
|
&database_uuid_str,
|
||||||
|
CollabType::Database,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let db_body = DatabaseBody::from_collab(
|
||||||
|
&database_collab,
|
||||||
|
Arc::new(NoPersistenceDatabaseCollabService),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
AppError::Internal(anyhow::anyhow!(
|
||||||
|
"Failed to create database body from collab, db_collab_id: {}",
|
||||||
|
database_uuid_str,
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// create a map of field id to field.
|
||||||
|
// ensure that the field name is unique.
|
||||||
|
// if the field name is repeated, it will be appended with the field id,
|
||||||
|
// under practical usage circumstances, no other collision should occur
|
||||||
|
let field_by_id: HashMap<String, Field> = {
|
||||||
|
let all_fields = db_body.fields.get_all_fields(&database_collab.transact());
|
||||||
|
|
||||||
|
let mut uniq_name_set: HashSet<String> = HashSet::with_capacity(all_fields.len());
|
||||||
|
let mut field_by_id: HashMap<String, Field> = HashMap::with_capacity(all_fields.len());
|
||||||
|
|
||||||
|
for mut field in all_fields {
|
||||||
|
// if the name already exists, append the field id to the name
|
||||||
|
if uniq_name_set.contains(&field.name) {
|
||||||
|
let new_name = format!("{}-{}", field.name, field.id);
|
||||||
|
field.name = new_name.clone();
|
||||||
|
}
|
||||||
|
uniq_name_set.insert(field.name.clone());
|
||||||
|
field_by_id.insert(field.id.clone(), field);
|
||||||
|
}
|
||||||
|
field_by_id
|
||||||
|
};
|
||||||
|
|
||||||
|
let database_row_details = collab_storage
|
||||||
|
.batch_get_collab(&uid, query_collabs, true)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|(id, result)| match result {
|
||||||
|
QueryCollabResult::Success { encode_collab_v1 } => {
|
||||||
|
let ec = EncodedCollab::decode_from_bytes(&encode_collab_v1).unwrap();
|
||||||
|
let collab =
|
||||||
|
Collab::new_with_source(CollabOrigin::Server, &id, ec.into(), vec![], false).unwrap();
|
||||||
|
let row_detail = RowDetail::from_collab(&collab).unwrap();
|
||||||
|
let cells = convert_database_cells_human_readable(row_detail.row.cells, &field_by_id);
|
||||||
|
Some(AFDatabaseRowDetail { id, cells })
|
||||||
|
},
|
||||||
|
QueryCollabResult::Failed { error } => {
|
||||||
|
tracing::warn!("Failed to get collab: {:?}", error);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect::<Vec<AFDatabaseRowDetail>>();
|
||||||
|
|
||||||
|
Ok(database_row_details)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_database_cells_human_readable(
|
||||||
|
db_cells: HashMap<String, HashMap<String, yrs::Any>>,
|
||||||
|
field_by_id: &HashMap<String, Field>,
|
||||||
|
) -> HashMap<String, HashMap<String, String>> {
|
||||||
|
let mut human_readable_records: HashMap<String, HashMap<String, String>> =
|
||||||
|
HashMap::with_capacity(db_cells.len());
|
||||||
|
|
||||||
|
for (field_id, cell) in db_cells {
|
||||||
|
let field = match field_by_id.get(&field_id) {
|
||||||
|
Some(field) => field,
|
||||||
|
None => {
|
||||||
|
tracing::error!("Failed to get field by id: {}", field_id);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut human_readable_cell: HashMap<String, String> = HashMap::with_capacity(cell.len());
|
||||||
|
for (key, value) in cell {
|
||||||
|
let serde_value: String = match key.as_str() {
|
||||||
|
"created_at" | "last_modified" => match value.cast::<i64>() {
|
||||||
|
Ok(timestamp) => chrono::DateTime::from_timestamp(timestamp, 0)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_rfc3339(),
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("Failed to cast timestamp: {:?}", err);
|
||||||
|
"".to_string()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"field_type" => match value.cast::<i64>() {
|
||||||
|
Ok(field_type_int) => {
|
||||||
|
let field_type = FieldType::from(field_type_int);
|
||||||
|
format!("{:?}", field_type)
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("Failed to cast field type: {:?}", err);
|
||||||
|
String::from("Unknown")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
_ => value.to_string(),
|
||||||
|
};
|
||||||
|
human_readable_cell.insert(key, serde_value);
|
||||||
|
}
|
||||||
|
human_readable_records.insert(field.name.clone(), human_readable_cell);
|
||||||
|
}
|
||||||
|
human_readable_records
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,51 @@ async fn workspace_list_database() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(db_row_ids.len(), 5, "{:?}", db_row_ids);
|
assert_eq!(db_row_ids.len(), 5, "{:?}", db_row_ids);
|
||||||
|
{
|
||||||
|
let db_row_ids: Vec<&str> = db_row_ids.iter().map(|s| s.id.as_str()).collect();
|
||||||
|
let db_row_ids: &[&str] = &db_row_ids;
|
||||||
|
let db_row_details = c
|
||||||
|
.list_database_row_details(&workspace_id, &untitled_db.id, db_row_ids)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(db_row_details.len(), 5, "{:#?}", db_row_details);
|
||||||
|
|
||||||
|
// cells: {
|
||||||
|
// "Multiselect": {
|
||||||
|
// "field_type": "MultiSelect",
|
||||||
|
// "last_modified": "2024-08-16T07:23:57+00:00",
|
||||||
|
// "created_at": "2024-08-16T07:23:35+00:00",
|
||||||
|
// "data": "BD-T,6UxM",
|
||||||
|
// },
|
||||||
|
// "Description": {
|
||||||
|
// "field_type": "RichText",
|
||||||
|
// "last_modified": "2024-08-16T07:17:03+00:00",
|
||||||
|
// "created_at": "2024-08-16T07:16:51+00:00",
|
||||||
|
// "data": "Install AppFlowy Mobile",
|
||||||
|
// },
|
||||||
|
// "Status": {
|
||||||
|
// "data": "CEZD",
|
||||||
|
// "field_type": "SingleSelect",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
let _ = db_row_details
|
||||||
|
.into_iter()
|
||||||
|
.find(|row| {
|
||||||
|
row.cells["Multiselect"]["field_type"] == "MultiSelect"
|
||||||
|
&& row.cells["Multiselect"]["last_modified"] == "2024-08-16T07:23:57+00:00"
|
||||||
|
&& row.cells["Multiselect"]["created_at"] == "2024-08-16T07:23:35+00:00"
|
||||||
|
&& row.cells["Multiselect"]["data"] == "BD-T,6UxM"
|
||||||
|
// Description
|
||||||
|
&& row.cells["Description"]["field_type"] == "RichText"
|
||||||
|
&& row.cells["Description"]["last_modified"] == "2024-08-16T07:17:03+00:00"
|
||||||
|
&& row.cells["Description"]["created_at"] == "2024-08-16T07:16:51+00:00"
|
||||||
|
&& row.cells["Description"]["data"] == "Install AppFlowy Mobile"
|
||||||
|
// Status
|
||||||
|
&& row.cells["Status"]["data"] == "CEZD"
|
||||||
|
&& row.cells["Status"]["field_type"] == "SingleSelect"
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue