Merge pull request #1019 from AppFlowy-IO/feat/get-database-fields
feat: api to get database fields
This commit is contained in:
commit
e31928e77a
|
|
@ -4,7 +4,7 @@ use app_error::AppError;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use client_api_entity::workspace_dto::{
|
use client_api_entity::workspace_dto::{
|
||||||
AFDatabase, AFDatabaseRow, AFDatabaseRowDetail, DatabaseRowUpdatedItem,
|
AFDatabase, AFDatabaseField, AFDatabaseRow, AFDatabaseRowDetail, DatabaseRowUpdatedItem,
|
||||||
ListDatabaseRowDetailParam, ListDatabaseRowUpdatedParam,
|
ListDatabaseRowDetailParam, ListDatabaseRowUpdatedParam,
|
||||||
};
|
};
|
||||||
use client_api_entity::{
|
use client_api_entity::{
|
||||||
|
|
@ -192,6 +192,24 @@ impl Client {
|
||||||
AppResponse::from_response(resp).await?.into_data()
|
AppResponse::from_response(resp).await?.into_data()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_database_fields(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
database_id: &str,
|
||||||
|
) -> Result<Vec<AFDatabaseField>, AppResponseError> {
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/workspace/{}/database/{}/fields",
|
||||||
|
self.base_url, workspace_id, database_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()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn list_database_row_ids_updated(
|
pub async fn list_database_row_ids_updated(
|
||||||
&self,
|
&self,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
|
|
|
||||||
|
|
@ -357,12 +357,6 @@ pub struct AFDatabase {
|
||||||
pub views: Vec<FolderViewMinimal>,
|
pub views: Vec<FolderViewMinimal>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct AFDatabaseField {
|
|
||||||
pub name: String,
|
|
||||||
pub field_type: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AFDatabaseRow {
|
pub struct AFDatabaseRow {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
@ -373,3 +367,12 @@ pub struct AFDatabaseRowDetail {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub cells: HashMap<String, HashMap<String, serde_json::Value>>,
|
pub cells: HashMap<String, HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct AFDatabaseField {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub field_type: String,
|
||||||
|
pub type_option: HashMap<String, serde_json::Value>,
|
||||||
|
pub is_primary: bool,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,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}/fields")
|
||||||
|
.route(web::get().to(get_database_fields_handler)),
|
||||||
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/{workspace_id}/database/{database_id}/row/updated")
|
web::resource("/{workspace_id}/database/{database_id}/row/updated")
|
||||||
.route(web::get().to(list_database_row_id_updated_handler)),
|
.route(web::get().to(list_database_row_id_updated_handler)),
|
||||||
|
|
@ -1906,6 +1910,28 @@ 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 get_database_fields_handler(
|
||||||
|
user_uuid: UserUuid,
|
||||||
|
path_param: web::Path<(String, String)>,
|
||||||
|
state: Data<AppState>,
|
||||||
|
) -> Result<Json<AppResponse<Vec<AFDatabaseField>>>> {
|
||||||
|
let (workspace_id, db_id) = path_param.into_inner();
|
||||||
|
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
|
||||||
|
state
|
||||||
|
.workspace_access_control
|
||||||
|
.enforce_action(&uid, &workspace_id, Action::Read)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let db_fields = biz::collab::ops::get_database_fields(
|
||||||
|
&state.collab_access_control_storage,
|
||||||
|
&workspace_id,
|
||||||
|
&db_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(AppResponse::Ok().with_data(db_fields)))
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_database_row_id_updated_handler(
|
async fn list_database_row_id_updated_handler(
|
||||||
user_uuid: UserUuid,
|
user_uuid: UserUuid,
|
||||||
path_param: web::Path<(String, String)>,
|
path_param: web::Path<(String, String)>,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ use database::publish::select_workspace_id_for_publish_namespace;
|
||||||
use database_entity::dto::QueryCollabResult;
|
use database_entity::dto::QueryCollabResult;
|
||||||
use database_entity::dto::{QueryCollab, QueryCollabParams};
|
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::AFDatabaseRow;
|
use shared_entity::dto::workspace_dto::AFDatabaseRow;
|
||||||
use shared_entity::dto::workspace_dto::AFDatabaseRowDetail;
|
use shared_entity::dto::workspace_dto::AFDatabaseRowDetail;
|
||||||
use shared_entity::dto::workspace_dto::DatabaseRowUpdatedItem;
|
use shared_entity::dto::workspace_dto::DatabaseRowUpdatedItem;
|
||||||
|
|
@ -446,20 +447,26 @@ pub async fn list_database(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let trash = folder
|
||||||
|
.get_all_trash_sections()
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.id)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
let mut af_databases = Vec::with_capacity(db_metas.len());
|
let mut af_databases = Vec::with_capacity(db_metas.len());
|
||||||
for db_meta in db_metas {
|
for db_meta in db_metas {
|
||||||
let id = db_meta.database_id;
|
let id = db_meta.database_id;
|
||||||
let views: Vec<FolderViewMinimal> = db_meta
|
let mut views: Vec<FolderViewMinimal> = Vec::new();
|
||||||
.linked_views
|
for linked_view_id in db_meta.linked_views {
|
||||||
.into_iter()
|
if !trash.contains(&linked_view_id) {
|
||||||
.map(|view_id| {
|
if let Some(folder_view) = folder.get_view(&linked_view_id) {
|
||||||
folder
|
views.push(to_dto_folder_view_miminal(&folder_view));
|
||||||
.get_view(&view_id)
|
};
|
||||||
.map(|view| to_dto_folder_view_miminal(&view))
|
}
|
||||||
.unwrap_or_default()
|
}
|
||||||
})
|
if !views.is_empty() {
|
||||||
.collect();
|
af_databases.push(AFDatabase { id, views });
|
||||||
af_databases.push(AFDatabase { id, views });
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(af_databases)
|
Ok(af_databases)
|
||||||
|
|
@ -470,26 +477,8 @@ pub async fn list_database_row_ids(
|
||||||
workspace_uuid_str: &str,
|
workspace_uuid_str: &str,
|
||||||
database_uuid_str: &str,
|
database_uuid_str: &str,
|
||||||
) -> Result<Vec<AFDatabaseRow>, AppError> {
|
) -> Result<Vec<AFDatabaseRow>, AppError> {
|
||||||
let db_collab = get_latest_collab(
|
let (db_collab, db_body) =
|
||||||
collab_storage,
|
get_database_body(collab_storage, workspace_uuid_str, database_uuid_str).await?;
|
||||||
GetCollabOrigin::Server,
|
|
||||||
workspace_uuid_str,
|
|
||||||
database_uuid_str,
|
|
||||||
CollabType::Database,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let db_body = DatabaseBody::from_collab(
|
|
||||||
&db_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,
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// get any view_id
|
// get any view_id
|
||||||
let txn = db_collab.transact();
|
let txn = db_collab.transact();
|
||||||
let iid = db_body.get_inline_view_id(&txn);
|
let iid = db_body.get_inline_view_id(&txn);
|
||||||
|
|
@ -509,6 +498,29 @@ pub async fn list_database_row_ids(
|
||||||
Ok(db_rows)
|
Ok(db_rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_database_fields(
|
||||||
|
collab_storage: &CollabAccessControlStorage,
|
||||||
|
workspace_uuid_str: &str,
|
||||||
|
database_uuid_str: &str,
|
||||||
|
) -> Result<Vec<AFDatabaseField>, AppError> {
|
||||||
|
let (db_collab, db_body) =
|
||||||
|
get_database_body(collab_storage, workspace_uuid_str, database_uuid_str).await?;
|
||||||
|
|
||||||
|
let all_fields = db_body.fields.get_all_fields(&db_collab.transact());
|
||||||
|
let mut acc = Vec::with_capacity(all_fields.len());
|
||||||
|
for field in all_fields {
|
||||||
|
let field_type = FieldType::from(field.field_type);
|
||||||
|
acc.push(AFDatabaseField {
|
||||||
|
id: field.id,
|
||||||
|
name: field.name,
|
||||||
|
field_type: format!("{:?}", field_type),
|
||||||
|
type_option: type_options_serde(&field.type_options, &field_type),
|
||||||
|
is_primary: field.is_primary,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(acc)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn list_database_row_ids_updated(
|
pub async fn list_database_row_ids_updated(
|
||||||
collab_storage: &CollabAccessControlStorage,
|
collab_storage: &CollabAccessControlStorage,
|
||||||
pg_pool: &PgPool,
|
pg_pool: &PgPool,
|
||||||
|
|
@ -763,6 +775,33 @@ fn add_to_selection_from_type_options(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_database_body(
|
||||||
|
collab_storage: &CollabAccessControlStorage,
|
||||||
|
workspace_uuid_str: &str,
|
||||||
|
database_uuid_str: &str,
|
||||||
|
) -> Result<(Collab, DatabaseBody), AppError> {
|
||||||
|
let db_collab = get_latest_collab(
|
||||||
|
collab_storage,
|
||||||
|
GetCollabOrigin::Server,
|
||||||
|
workspace_uuid_str,
|
||||||
|
database_uuid_str,
|
||||||
|
CollabType::Database,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let db_body = DatabaseBody::from_collab(
|
||||||
|
&db_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,
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok((db_collab, db_body))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn collab_from_doc_state(doc_state: Vec<u8>, object_id: &str) -> Result<Collab, AppError> {
|
pub fn collab_from_doc_state(doc_state: Vec<u8>, object_id: &str) -> Result<Collab, AppError> {
|
||||||
let collab = Collab::new_with_source(
|
let collab = Collab::new_with_source(
|
||||||
CollabOrigin::Server,
|
CollabOrigin::Server,
|
||||||
|
|
@ -774,3 +813,31 @@ pub fn collab_from_doc_state(doc_state: Vec<u8>, object_id: &str) -> Result<Coll
|
||||||
.map_err(|e| AppError::Unhandled(e.to_string()))?;
|
.map_err(|e| AppError::Unhandled(e.to_string()))?;
|
||||||
Ok(collab)
|
Ok(collab)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn type_options_serde(
|
||||||
|
type_options: &TypeOptions,
|
||||||
|
field_type: &FieldType,
|
||||||
|
) -> HashMap<String, serde_json::Value> {
|
||||||
|
let type_option = match type_options.get(&field_type.type_id()) {
|
||||||
|
Some(type_option) => type_option,
|
||||||
|
None => return HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result = HashMap::with_capacity(type_option.len());
|
||||||
|
for (key, value) in type_option {
|
||||||
|
match field_type {
|
||||||
|
FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Media => {
|
||||||
|
if let yrs::Any::String(arc_str) = value {
|
||||||
|
if let Ok(serde_value) = serde_json::from_str::<serde_json::Value>(arc_str) {
|
||||||
|
result.insert(key.clone(), serde_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
result.insert(key.clone(), serde_json::to_value(value).unwrap_or_default());
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use client_api_test::generate_unique_registered_user_client;
|
use client_api_test::generate_unique_registered_user_client;
|
||||||
use collab_entity::CollabType;
|
use collab_entity::CollabType;
|
||||||
use database_entity::dto::QueryCollabParams;
|
use database_entity::dto::QueryCollabParams;
|
||||||
|
use serde_json::json;
|
||||||
|
use shared_entity::dto::workspace_dto::AFDatabaseField;
|
||||||
use shared_entity::dto::workspace_dto::CreateWorkspaceParam;
|
use shared_entity::dto::workspace_dto::CreateWorkspaceParam;
|
||||||
use shared_entity::dto::workspace_dto::PatchWorkspaceParam;
|
use shared_entity::dto::workspace_dto::PatchWorkspaceParam;
|
||||||
|
|
||||||
|
|
@ -24,6 +28,100 @@ async fn workspace_list_database() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(db_row_ids.len(), 5, "{:?}", db_row_ids);
|
assert_eq!(db_row_ids.len(), 5, "{:?}", db_row_ids);
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
let mut db_fields = c
|
||||||
|
.get_database_fields(&workspace_id, &todos_db.id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// convert to hashset to check for equeality
|
||||||
|
db_fields.sort_by(|a, b| a.id.cmp(&b.id));
|
||||||
|
let mut expected = vec![
|
||||||
|
AFDatabaseField {
|
||||||
|
id: "wdX8DG".to_string(),
|
||||||
|
name: "Multiselect".to_string(),
|
||||||
|
field_type: "MultiSelect".to_string(),
|
||||||
|
type_option: {
|
||||||
|
let mut options = HashMap::new();
|
||||||
|
options.insert(
|
||||||
|
"content".to_string(),
|
||||||
|
json!({
|
||||||
|
"disable_color": false,
|
||||||
|
"options": [
|
||||||
|
{"color": "Purple", "id": "4PDn", "name": "get things done"},
|
||||||
|
{"color": "Blue", "id": "Bpyg", "name": "self-host"},
|
||||||
|
{"color": "Aqua", "id": "GOQj", "name": "open source"},
|
||||||
|
{"color": "Green", "id": "BD-T", "name": "looks great"},
|
||||||
|
{"color": "Lime", "id": "6UxM", "name": "fast"},
|
||||||
|
{"color": "Yellow", "id": "g2Uq", "name": "Claude 3"},
|
||||||
|
{"color": "Orange", "id": "Tt-J", "name": "GPT-4o"},
|
||||||
|
{"color": "LightPink", "id": "5QDY", "name": "Q&A"},
|
||||||
|
{"color": "Pink", "id": "XYUx", "name": "news"},
|
||||||
|
{"color": "Purple", "id": "hoZx", "name": "social"},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
options
|
||||||
|
},
|
||||||
|
is_primary: false,
|
||||||
|
},
|
||||||
|
AFDatabaseField {
|
||||||
|
id: "SqwRg1".to_string(),
|
||||||
|
name: "Status".to_string(),
|
||||||
|
field_type: "SingleSelect".to_string(),
|
||||||
|
type_option: {
|
||||||
|
let mut options = HashMap::new();
|
||||||
|
options.insert(
|
||||||
|
"content".to_string(),
|
||||||
|
json!({
|
||||||
|
"disable_color": false,
|
||||||
|
"options": [
|
||||||
|
{"color": "Purple", "id": "CEZD", "name": "To Do"},
|
||||||
|
{"color": "Orange", "id": "TznH", "name": "Doing"},
|
||||||
|
{"color": "Yellow", "id": "__n6", "name": "✅ Done"},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
options
|
||||||
|
},
|
||||||
|
is_primary: false,
|
||||||
|
},
|
||||||
|
AFDatabaseField {
|
||||||
|
id: "phVRgL".to_string(),
|
||||||
|
name: "Description".to_string(),
|
||||||
|
field_type: "RichText".to_string(),
|
||||||
|
type_option: {
|
||||||
|
let mut options = HashMap::new();
|
||||||
|
options.insert("data".to_string(), json!(""));
|
||||||
|
options
|
||||||
|
},
|
||||||
|
is_primary: true,
|
||||||
|
},
|
||||||
|
AFDatabaseField {
|
||||||
|
id: "KinVda".to_string(),
|
||||||
|
name: "Tasks".to_string(),
|
||||||
|
field_type: "Checklist".to_string(),
|
||||||
|
type_option: HashMap::new(),
|
||||||
|
is_primary: false,
|
||||||
|
},
|
||||||
|
AFDatabaseField {
|
||||||
|
id: "3AE6iK".to_string(),
|
||||||
|
name: "Last modified".to_string(),
|
||||||
|
field_type: "LastEditedTime".to_string(),
|
||||||
|
type_option: {
|
||||||
|
let mut options = HashMap::new();
|
||||||
|
options.insert("date_format".to_string(), json!(3));
|
||||||
|
options.insert("field_type".to_string(), json!(8));
|
||||||
|
options.insert("include_time".to_string(), json!(true));
|
||||||
|
options.insert("time_format".to_string(), json!(1));
|
||||||
|
options
|
||||||
|
},
|
||||||
|
is_primary: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
expected.sort_by(|a, b| a.id.cmp(&b.id));
|
||||||
|
assert_eq!(db_fields, expected, "{:#?}", db_fields);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
let db_row_ids = c
|
let db_row_ids = c
|
||||||
.list_database_row_ids_updated(&workspace_id, &todos_db.id, None)
|
.list_database_row_ids_updated(&workspace_id, &todos_db.id, None)
|
||||||
|
|
@ -31,7 +129,6 @@ async fn workspace_list_database() {
|
||||||
.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 = c
|
let db_row_ids = c
|
||||||
.list_database_row_ids(&workspace_id, &todos_db.id)
|
.list_database_row_ids(&workspace_id, &todos_db.id)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue