mirror of
https://github.com/AppFlowy-IO/AppFlowy-Cloud.git
synced 2025-04-19 03:24:42 -04:00
feat: add endpoint for getting page view collab (#831)
This commit is contained in:
parent
44d76bb004
commit
0b193e1606
10 changed files with 410 additions and 2 deletions
34
.sqlx/query-84c224af99f654e2e0ba11a411376794855483eedb0c30b1873ed660ca8d10cd.json
generated
Normal file
34
.sqlx/query-84c224af99f654e2e0ba11a411376794855483eedb0c30b1873ed660ca8d10cd.json
generated
Normal 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"
|
||||
}
|
27
libs/client-api/src/http_view.rs
Normal file
27
libs/client-api/src/http_view.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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")]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod access_control;
|
||||
pub mod ops;
|
||||
pub mod page_view;
|
||||
pub mod publish;
|
||||
pub mod publish_dup;
|
||||
|
|
242
src/biz/workspace/page_view.rs
Normal file
242
src/biz/workspace/page_view.rs
Normal 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(),
|
||||
})
|
||||
}
|
|
@ -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;
|
||||
|
|
41
tests/workspace/page_view.rs
Normal file
41
tests/workspace/page_view.rs
Normal 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);
|
||||
}
|
Loading…
Add table
Reference in a new issue