feat: add endpoint for getting page view collab (#831)

This commit is contained in:
Khor Shu Heng 2024-09-18 12:56:34 +08:00 committed by GitHub
parent 44d76bb004
commit 0b193e1606
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 410 additions and 2 deletions

View file

@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n uuid,\n name,\n metadata ->> 'icon_url' AS avatar_url\n FROM af_user\n WHERE uid = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "uuid",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "avatar_url",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": [
false,
false,
null
]
},
"hash": "84c224af99f654e2e0ba11a411376794855483eedb0c30b1873ed660ca8d10cd"
}

View file

@ -0,0 +1,27 @@
use client_api_entity::workspace_dto::PageCollab;
use reqwest::Method;
use shared_entity::response::{AppResponse, AppResponseError};
use uuid::Uuid;
use crate::Client;
impl Client {
pub async fn get_workspace_page_view(
&self,
workspace_id: Uuid,
view_id: Uuid,
) -> Result<PageCollab, AppResponseError> {
let url = format!(
"{}/api/workspace/{}/page_view/{}",
self.base_url, workspace_id, view_id
);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
AppResponse::<PageCollab>::from_response(resp)
.await?
.into_data()
}
}

View file

@ -8,6 +8,7 @@ mod http_history;
mod http_member;
mod http_publish;
mod http_template;
mod http_view;
pub use http::*;
#[cfg(feature = "collab-sync")]

View file

@ -1,3 +1,4 @@
use database_entity::dto::AFWebUser;
use futures_util::stream::BoxStream;
use sqlx::postgres::PgArguments;
use sqlx::types::JsonValue;
@ -240,3 +241,22 @@ pub async fn select_name_from_uuid(pool: &PgPool, user_uuid: &Uuid) -> Result<St
.await?;
Ok(email)
}
pub async fn select_web_user_from_uid(pool: &PgPool, uid: i64) -> Result<AFWebUser, AppError> {
let row = sqlx::query_as!(
AFWebUser,
r#"
SELECT
uuid,
name,
metadata ->> 'icon_url' AS avatar_url
FROM af_user
WHERE uid = $1
"#,
uid
)
.fetch_one(pool)
.await?;
Ok(row)
}

View file

@ -1,9 +1,9 @@
use chrono::{DateTime, Utc};
use collab_entity::{CollabType, EncodedCollab};
use database_entity::dto::{AFRole, AFWorkspaceInvitationStatus};
use database_entity::dto::{AFRole, AFWebUser, AFWorkspaceInvitationStatus};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::ops::Deref;
use std::{collections::HashMap, ops::Deref};
use uuid::Uuid;
#[derive(Deserialize, Serialize)]
@ -122,6 +122,20 @@ pub struct CollabResponse {
pub object_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageCollabData {
pub encoded_collab: Vec<u8>,
pub row_data: HashMap<String, Vec<u8>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageCollab {
pub view: FolderView,
pub data: PageCollabData,
pub owner: Option<AFWebUser>,
pub last_editor: Option<AFWebUser>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublishedDuplicate {
pub published_view_id: String,

View file

@ -47,6 +47,7 @@ use crate::biz::workspace::ops::{
create_comment_on_published_view, create_reaction_on_comment, get_comments_on_published_view,
get_reactions_on_published_view, remove_comment_on_published_view, remove_reaction_on_comment,
};
use crate::biz::workspace::page_view::get_page_view_collab;
use crate::domain::compression::{
blocking_decompress, decompress, CompressionType, X_COMPRESSION_TYPE,
};
@ -117,6 +118,10 @@ pub fn workspace_scope() -> Scope {
web::resource("/v1/{workspace_id}/collab/{object_id}")
.route(web::get().to(v1_get_collab_handler)),
)
.service(
web::resource("/{workspace_id}/page_view/{view_id}")
.route(web::get().to(get_page_view_handler)),
)
.service(
web::resource("/{workspace_id}/batch/collab")
.route(web::post().to(batch_create_collab_handler)),
@ -776,6 +781,28 @@ async fn v1_get_collab_handler(
Ok(Json(AppResponse::Ok().with_data(resp)))
}
async fn get_page_view_handler(
user_uuid: UserUuid,
path: web::Path<(Uuid, String)>,
state: Data<AppState>,
) -> Result<Json<AppResponse<PageCollab>>> {
let (workspace_uuid, view_id) = path.into_inner();
let uid = state
.user_cache
.get_user_uid(&user_uuid)
.await
.map_err(AppResponseError::from)?;
let page_collab = get_page_view_collab(
&state.pg_pool,
state.collab_access_control_storage.clone(),
uid,
workspace_uuid,
&view_id,
)
.await?;
Ok(Json(AppResponse::Ok().with_data(page_collab)))
}
#[instrument(level = "trace", skip_all, err)]
async fn get_collab_snapshot_handler(
payload: Json<QuerySnapshotParams>,

View file

@ -1,4 +1,5 @@
pub mod access_control;
pub mod ops;
pub mod page_view;
pub mod publish;
pub mod publish_dup;

View file

@ -0,0 +1,242 @@
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use app_error::ErrorCode;
use appflowy_collaborate::collab::storage::CollabAccessControlStorage;
use chrono::DateTime;
use collab::core::collab::Collab;
use collab_database::{
database::DatabaseBody, rows::RowId, workspace_database::WorkspaceDatabaseBody,
};
use collab_entity::{CollabType, EncodedCollab};
use collab_folder::CollabOrigin;
use database::collab::{select_workspace_database_oid, CollabStorage, GetCollabOrigin};
use database::publish::select_published_view_ids_for_workspace;
use database::user::select_web_user_from_uid;
use database_entity::dto::{QueryCollab, QueryCollabResult};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use shared_entity::dto::workspace_dto::{FolderView, PageCollab, PageCollabData};
use shared_entity::response::AppResponseError;
use sqlx::PgPool;
use uuid::Uuid;
use crate::biz::collab::folder_view::{
parse_extra_field_as_json, to_dto_view_icon, to_view_layout,
};
use crate::biz::collab::{
folder_view::view_is_space,
ops::{get_latest_collab_encoded, get_latest_collab_folder},
};
use super::publish_dup::collab_from_doc_state;
pub async fn get_page_view_collab(
pg_pool: &PgPool,
collab_access_control_storage: Arc<CollabAccessControlStorage>,
uid: i64,
workspace_id: Uuid,
view_id: &str,
) -> Result<PageCollab, AppResponseError> {
let folder = get_latest_collab_folder(
collab_access_control_storage.clone(),
GetCollabOrigin::User { uid },
&workspace_id.to_string(),
)
.await?;
let view = folder.get_view(view_id).ok_or(AppResponseError::new(
ErrorCode::InvalidFolderView,
format!("View {} not found", view_id),
))?;
let owner = match view.created_by {
Some(uid) => Some(select_web_user_from_uid(pg_pool, uid).await?),
None => None,
};
let last_editor = match view.last_edited_by {
Some(uid) => Some(select_web_user_from_uid(pg_pool, uid).await?),
None => None,
};
let publish_view_ids = select_published_view_ids_for_workspace(pg_pool, workspace_id).await?;
let publish_view_ids: HashSet<String> = publish_view_ids
.into_iter()
.map(|id| id.to_string())
.collect();
let folder_view = FolderView {
view_id: view_id.to_string(),
name: view.name.clone(),
icon: view
.icon
.as_ref()
.map(|icon| to_dto_view_icon(icon.clone())),
is_space: view_is_space(&view),
is_private: false,
is_published: publish_view_ids.contains(view_id),
layout: to_view_layout(&view.layout),
created_at: DateTime::from_timestamp(view.created_at, 0).unwrap_or_default(),
last_edited_time: DateTime::from_timestamp(view.last_edited_time, 0).unwrap_or_default(),
extra: view.extra.as_ref().map(|e| parse_extra_field_as_json(e)),
children: vec![],
};
let page_collab_data = match view.layout {
collab_folder::ViewLayout::Document => {
get_page_collab_data_for_document(
collab_access_control_storage.clone(),
uid,
workspace_id,
view_id,
)
.await
},
collab_folder::ViewLayout::Grid
| collab_folder::ViewLayout::Board
| collab_folder::ViewLayout::Calendar => {
get_page_collab_data_for_database(
pg_pool,
collab_access_control_storage.clone(),
uid,
workspace_id,
view_id,
)
.await
},
collab_folder::ViewLayout::Chat => Err(AppResponseError::new(
ErrorCode::InvalidRequest,
"Page view for AI chat is not supported at the moment",
)),
}?;
let page_collab = PageCollab {
view: folder_view,
data: page_collab_data,
owner,
last_editor,
};
Ok(page_collab)
}
async fn get_page_collab_data_for_database(
pg_pool: &PgPool,
collab_access_control_storage: Arc<CollabAccessControlStorage>,
uid: i64,
workspace_id: Uuid,
view_id: &str,
) -> Result<PageCollabData, AppResponseError> {
let ws_db_oid = select_workspace_database_oid(pg_pool, &workspace_id).await?;
let ws_db = get_latest_collab_encoded(
collab_access_control_storage.clone(),
GetCollabOrigin::User { uid },
&workspace_id.to_string(),
&ws_db_oid,
CollabType::WorkspaceDatabase,
)
.await?;
let mut ws_db_collab = collab_from_doc_state(ws_db.doc_state.to_vec(), &ws_db_oid)?;
let ws_db_body = WorkspaceDatabaseBody::open(&mut ws_db_collab);
let db_oid = {
let txn = ws_db_collab.transact();
ws_db_body
.get_database_meta_with_view_id(&txn, view_id)
.ok_or(AppResponseError::new(
ErrorCode::NoRequiredData,
format!("Database view {} not found", view_id),
))?
.database_id
};
let db = get_latest_collab_encoded(
collab_access_control_storage.clone(),
GetCollabOrigin::User { uid },
&workspace_id.to_string(),
&db_oid,
CollabType::Database,
)
.await?;
let db_collab = Collab::new_with_source(
CollabOrigin::Server,
&db_oid,
db.clone().into(),
vec![],
false,
)
.map_err(|err| {
AppResponseError::new(
ErrorCode::Internal,
format!(
"Unable to create collab from object id {}: {}",
&db_oid, err
),
)
})?;
let db_body = DatabaseBody::from_collab(&db_collab).unwrap();
let inline_view_id = {
let txn = db_collab.transact();
db_body.get_inline_view_id(&txn)
};
let row_ids: Vec<RowId> = {
let txn = db_collab.transact();
db_body
.views
.get_row_orders(&txn, &inline_view_id)
.iter()
.map(|ro| ro.id.clone())
.collect()
};
let queries: Vec<QueryCollab> = row_ids
.iter()
.map(|row_id| QueryCollab {
object_id: row_id.to_string(),
collab_type: CollabType::DatabaseRow,
})
.collect();
let row_query_collab_results = collab_access_control_storage
.batch_get_collab(&uid, queries)
.await;
let row_data = tokio::task::spawn_blocking(move || {
let row_collabs: HashMap<String, Vec<u8>> = row_query_collab_results
.into_par_iter()
.filter_map(|(row_id, query_collab_result)| match query_collab_result {
QueryCollabResult::Success { encode_collab_v1 } => {
let decoded_result = EncodedCollab::decode_from_bytes(&encode_collab_v1);
match decoded_result {
Ok(decoded) => Some((row_id, decoded.doc_state.to_vec())),
Err(err) => {
tracing::error!("Failed to decode collab for row {}: {}", row_id, err);
None
},
}
},
QueryCollabResult::Failed { error } => {
tracing::error!("Failed to get collab: {:?}", error);
None
},
})
.collect();
row_collabs
})
.await?;
Ok(PageCollabData {
encoded_collab: db.doc_state.to_vec(),
row_data,
})
}
async fn get_page_collab_data_for_document(
collab_access_control_storage: Arc<CollabAccessControlStorage>,
uid: i64,
workspace_id: Uuid,
view_id: &str,
) -> Result<PageCollabData, AppResponseError> {
let collab = get_latest_collab_encoded(
collab_access_control_storage.clone(),
GetCollabOrigin::User { uid },
&workspace_id.to_string(),
view_id,
CollabType::Document,
)
.await?;
Ok(PageCollabData {
encoded_collab: collab.doc_state.clone().to_vec(),
row_data: HashMap::default(),
})
}

View file

@ -2,6 +2,7 @@ mod default_user_workspace;
mod edit_workspace;
mod invitation_crud;
mod member_crud;
mod page_view;
mod publish;
mod published_data;
mod template;

View file

@ -0,0 +1,41 @@
use client_api_test::generate_unique_registered_user_client;
use uuid::Uuid;
#[tokio::test]
async fn get_page_view() {
let (c, _user) = generate_unique_registered_user_client().await;
let workspaces = c.get_workspaces().await.unwrap();
assert_eq!(workspaces.len(), 1);
let workspace_id = workspaces[0].workspace_id;
let folder_view = c
.get_workspace_folder(&workspace_id.to_string(), Some(2), None)
.await
.unwrap();
let general_workspace = &folder_view
.children
.into_iter()
.find(|v| v.name == "General")
.unwrap();
let todo = general_workspace
.children
.iter()
.find(|v| v.name == "To-dos")
.unwrap();
let todo_list_view_id = Uuid::parse_str(&todo.view_id).unwrap();
let resp = c
.get_workspace_page_view(workspace_id, todo_list_view_id)
.await
.unwrap();
assert_eq!(resp.data.row_data.len(), 5);
let getting_started = general_workspace
.children
.iter()
.find(|v| v.name == "Getting started")
.unwrap();
let getting_started_view_id = Uuid::parse_str(&getting_started.view_id).unwrap();
let resp = c
.get_workspace_page_view(workspace_id, getting_started_view_id)
.await
.unwrap();
assert_eq!(resp.data.row_data.len(), 0);
}