feat: published view outline endpoint

This commit is contained in:
khorshuheng 2024-09-03 10:43:22 +08:00
parent 8015e34841
commit 56b9f9daf4
11 changed files with 275 additions and 8 deletions

View file

@ -1,6 +1,7 @@
.env .env
.dockerignore .dockerignore
spec.yaml spec.yaml
**/target
target/ target/
deploy/ deploy/
tests/ tests/

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT workspace_id\n FROM af_workspace\n WHERE publish_namespace = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "785c4c1e9b393a1f04a88136211df0c5daa7ad0c52f3e7e071156595262ca44f"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT view_id\n FROM af_published_collab\n WHERE workspace_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "view_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "ee58ab18f882ed89f3506d3e28eef43c32e09e6c545a08f35fb5169e84553af6"
}

View file

@ -136,6 +136,9 @@ pub enum AppError {
#[error("{0}")] #[error("{0}")]
InvalidContentType(String), InvalidContentType(String),
#[error("{0}")]
InvalidPublishedOutline(String),
} }
impl AppError { impl AppError {
@ -200,6 +203,7 @@ impl AppError {
AppError::AIServiceUnavailable(_) => ErrorCode::AIServiceUnavailable, AppError::AIServiceUnavailable(_) => ErrorCode::AIServiceUnavailable,
AppError::StringLengthLimitReached(_) => ErrorCode::StringLengthLimitReached, AppError::StringLengthLimitReached(_) => ErrorCode::StringLengthLimitReached,
AppError::InvalidContentType(_) => ErrorCode::InvalidContentType, AppError::InvalidContentType(_) => ErrorCode::InvalidContentType,
AppError::InvalidPublishedOutline(_) => ErrorCode::InvalidPublishedOutline,
} }
} }
} }
@ -318,6 +322,7 @@ pub enum ErrorCode {
InvalidContentType = 1036, InvalidContentType = 1036,
SingleUploadLimitExceeded = 1037, SingleUploadLimitExceeded = 1037,
AppleRevokeTokenError = 1038, AppleRevokeTokenError = 1038,
InvalidPublishedOutline = 1039,
} }
impl ErrorCode { impl ErrorCode {

View file

@ -273,3 +273,39 @@ pub async fn select_published_collab_info<'a, E: Executor<'a, Database = Postgre
Ok(res) Ok(res)
} }
pub async fn select_workspace_id_for_publish_namespace<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
publish_namespace: &str,
) -> Result<Uuid, AppError> {
let res = sqlx::query!(
r#"
SELECT workspace_id
FROM af_workspace
WHERE publish_namespace = $1
"#,
publish_namespace,
)
.fetch_one(executor)
.await?;
Ok(res.workspace_id)
}
pub async fn select_published_view_ids_for_workspace<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
workspace_id: Uuid,
) -> Result<Vec<Uuid>, AppError> {
let res = sqlx::query!(
r#"
SELECT view_id
FROM af_published_collab
WHERE workspace_id = $1
"#,
workspace_id,
)
.fetch_all(executor)
.await?;
Ok(res.into_iter().map(|r| r.view_id).collect())
}

View file

@ -164,6 +164,12 @@ pub enum ViewLayout {
Chat = 4, Chat = 4,
} }
impl Default for ViewLayout {
fn default() -> Self {
Self::Document
}
}
#[derive(Default, Debug, Deserialize, Serialize)] #[derive(Default, Debug, Deserialize, Serialize)]
pub struct QueryWorkspaceParam { pub struct QueryWorkspaceParam {
pub include_member_count: Option<bool>, pub include_member_count: Option<bool>,
@ -173,3 +179,14 @@ pub struct QueryWorkspaceParam {
pub struct QueryWorkspaceFolder { pub struct QueryWorkspaceFolder {
pub depth: Option<u32>, pub depth: Option<u32>,
} }
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct PublishedView {
pub view_id: String,
pub name: String,
pub icon: Option<ViewIcon>,
pub layout: ViewLayout,
/// contains fields like `is_space`, and font information
pub extra: Option<serde_json::Value>,
pub children: Vec<PublishedView>,
}

View file

@ -179,6 +179,10 @@ pub fn workspace_scope() -> Scope {
web::resource("/{workspace_id}/folder") web::resource("/{workspace_id}/folder")
.route(web::get().to(get_workspace_folder_handler)) .route(web::get().to(get_workspace_folder_handler))
) )
.service(
web::resource("/published-outline/{publish_namespace}")
.route(web::get().to(get_workspace_publish_outline_handler))
)
.service( .service(
web::resource("/{workspace_id}/collab/{object_id}/member/list") web::resource("/{workspace_id}/collab/{object_id}/member/list")
.route(web::get().to(get_collab_member_list_handler)), .route(web::get().to(get_collab_member_list_handler)),
@ -1394,6 +1398,19 @@ async fn get_workspace_folder_handler(
Ok(Json(AppResponse::Ok().with_data(folder_view))) Ok(Json(AppResponse::Ok().with_data(folder_view)))
} }
async fn get_workspace_publish_outline_handler(
publish_namespace: web::Path<String>,
state: Data<AppState>,
) -> Result<Json<AppResponse<PublishedView>>> {
let published_view = biz::collab::ops::get_published_view(
state.collab_access_control_storage.clone(),
publish_namespace.into_inner(),
&state.pg_pool,
)
.await?;
Ok(Json(AppResponse::Ok().with_data(published_view)))
}
#[inline] #[inline]
async fn parser_realtime_msg( async fn parser_realtime_msg(
payload: Bytes, payload: Bytes,

View file

@ -1,7 +1,7 @@
use std::collections::HashSet; use std::collections::HashSet;
use collab_folder::Folder; use collab_folder::{Folder, ViewLayout as CollabFolderViewLayout};
use shared_entity::dto::workspace_dto::FolderView; use shared_entity::dto::workspace_dto::{FolderView, ViewLayout};
use uuid::Uuid; use uuid::Uuid;
pub fn collab_folder_to_folder_view(folder: &Folder, depth: u32) -> FolderView { pub fn collab_folder_to_folder_view(folder: &Folder, depth: u32) -> FolderView {
@ -126,7 +126,7 @@ impl<'a> From<FolderViewIntermediate<'a>> for FolderView {
} }
} }
fn view_is_space(view: &collab_folder::View) -> bool { pub fn view_is_space(view: &collab_folder::View) -> bool {
let extra = match view.extra.as_ref() { let extra = match view.extra.as_ref() {
Some(extra) => extra, Some(extra) => extra,
None => return false, None => return false,
@ -144,14 +144,16 @@ fn view_is_space(view: &collab_folder::View) -> bool {
} }
} }
fn to_dto_view_icon(icon: collab_folder::ViewIcon) -> shared_entity::dto::workspace_dto::ViewIcon { pub fn to_dto_view_icon(
icon: collab_folder::ViewIcon,
) -> shared_entity::dto::workspace_dto::ViewIcon {
shared_entity::dto::workspace_dto::ViewIcon { shared_entity::dto::workspace_dto::ViewIcon {
ty: to_dto_view_icon_type(icon.ty), ty: to_dto_view_icon_type(icon.ty),
value: icon.value, value: icon.value,
} }
} }
fn to_dto_view_icon_type( pub fn to_dto_view_icon_type(
icon: collab_folder::IconType, icon: collab_folder::IconType,
) -> shared_entity::dto::workspace_dto::IconType { ) -> shared_entity::dto::workspace_dto::IconType {
match icon { match icon {
@ -160,3 +162,13 @@ fn to_dto_view_icon_type(
collab_folder::IconType::Icon => shared_entity::dto::workspace_dto::IconType::Icon, collab_folder::IconType::Icon => shared_entity::dto::workspace_dto::IconType::Icon,
} }
} }
pub fn to_view_layout(collab_folder_view_layout: &CollabFolderViewLayout) -> ViewLayout {
match collab_folder_view_layout {
CollabFolderViewLayout::Document => ViewLayout::Document,
CollabFolderViewLayout::Grid => ViewLayout::Grid,
CollabFolderViewLayout::Board => ViewLayout::Board,
CollabFolderViewLayout::Calendar => ViewLayout::Calendar,
CollabFolderViewLayout::Chat => ViewLayout::Chat,
}
}

View file

@ -1,3 +1,4 @@
pub mod access_control; pub mod access_control;
pub mod folder_view; pub mod folder_view;
pub mod ops; pub mod ops;
pub mod publish_outline;

View file

@ -6,14 +6,17 @@ use collab_entity::CollabType;
use collab_entity::EncodedCollab; use collab_entity::EncodedCollab;
use collab_folder::{CollabOrigin, Folder}; use collab_folder::{CollabOrigin, Folder};
use database::collab::{CollabStorage, GetCollabOrigin}; use database::collab::{CollabStorage, GetCollabOrigin};
use database_entity::dto::QueryCollab; use database::publish::select_published_view_ids_for_workspace;
use database_entity::dto::QueryCollabParams; use database::publish::select_workspace_id_for_publish_namespace;
use database_entity::dto::{QueryCollab, QueryCollabParams};
use sqlx::PgPool; use sqlx::PgPool;
use std::ops::DerefMut; use std::ops::DerefMut;
use anyhow::Context; use anyhow::Context;
use shared_entity::dto::workspace_dto::FolderView; use shared_entity::dto::workspace_dto::{FolderView, PublishedView};
use sqlx::types::Uuid; use sqlx::types::Uuid;
use std::collections::HashSet;
use tracing::{event, trace}; use tracing::{event, trace};
use validator::Validate; use validator::Validate;
@ -24,6 +27,7 @@ use database_entity::dto::{
}; };
use super::folder_view::collab_folder_to_folder_view; use super::folder_view::collab_folder_to_folder_view;
use super::publish_outline::collab_folder_to_published_outline;
/// Create a new collab member /// Create a new collab member
/// If the collab member already exists, return [AppError::RecordAlreadyExists] /// If the collab member already exists, return [AppError::RecordAlreadyExists]
@ -215,3 +219,35 @@ pub async fn get_latest_collab_encoded(
) )
.await .await
} }
pub async fn get_published_view(
collab_storage: Arc<CollabAccessControlStorage>,
publish_namespace: String,
pg_pool: &PgPool,
) -> Result<PublishedView, AppError> {
let workspace_id = select_workspace_id_for_publish_namespace(pg_pool, &publish_namespace).await?;
let query_collab_params = QueryCollabParams::new(
workspace_id,
collab_entity::CollabType::Folder,
workspace_id,
);
let encoded_collab = collab_storage
.get_encode_collab(GetCollabOrigin::Server, query_collab_params, true)
.await?;
let folder = Folder::from_collab_doc_state(
0,
CollabOrigin::Server,
encoded_collab.into(),
&workspace_id.to_string(),
vec![],
)
.map_err(|e| AppError::Unhandled(e.to_string()))?;
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 published_view: PublishedView =
collab_folder_to_published_outline(&folder, &publish_view_ids)?;
Ok(published_view)
}

View file

@ -0,0 +1,98 @@
use std::collections::HashSet;
use app_error::AppError;
use collab_folder::Folder;
use shared_entity::dto::workspace_dto::PublishedView;
use super::folder_view::{to_dto_view_icon, to_view_layout, view_is_space};
pub fn collab_folder_to_published_outline(
folder: &Folder,
publish_view_ids: &HashSet<String>,
) -> Result<PublishedView, AppError> {
let mut unviewable = HashSet::new();
for private_section in folder.get_all_private_sections() {
unviewable.insert(private_section.id);
}
for trash_view in folder.get_all_trash_sections() {
unviewable.insert(trash_view.id);
}
let workspace_id = folder
.get_workspace_id()
.ok_or_else(|| AppError::InvalidPublishedOutline("failed to get workspace_id".to_string()))?;
let root = match folder.get_view(&workspace_id) {
Some(root) => root,
None => {
return Err(AppError::InvalidPublishedOutline(
"failed to get root view".to_string(),
));
},
};
let extra = root.extra.as_deref().map(|extra| {
serde_json::from_str::<serde_json::Value>(extra).unwrap_or_else(|e| {
tracing::warn!("failed to parse extra field({}): {}", extra, e);
serde_json::Value::Null
})
});
let published_view = PublishedView {
view_id: root.id.clone(),
name: root.name.clone(),
icon: root
.icon
.as_ref()
.map(|icon| to_dto_view_icon(icon.clone())),
layout: to_view_layout(&root.layout),
extra,
children: root
.children
.iter()
.filter(|v| !unviewable.contains(&v.id))
.filter_map(|v| to_publish_view(&v.id, folder, &unviewable, publish_view_ids))
.collect(),
};
Ok(published_view)
}
fn to_publish_view(
view_id: &str,
folder: &Folder,
unviewable: &HashSet<String>,
publish_view_ids: &HashSet<String>,
) -> Option<PublishedView> {
let view = match folder.get_view(view_id) {
Some(view) => view,
None => {
return None;
},
};
let extra = view.extra.as_deref().map(|extra| {
serde_json::from_str::<serde_json::Value>(extra).unwrap_or_else(|e| {
tracing::warn!("failed to parse extra field({}): {}", extra, e);
serde_json::Value::Null
})
});
let pruned_view: Vec<PublishedView> = view
.children
.iter()
.filter(|v| !unviewable.contains(&v.id))
.filter_map(|view_id| to_publish_view(&view_id.id, folder, unviewable, publish_view_ids))
.collect();
if view_is_space(&view) || publish_view_ids.contains(view_id) || !pruned_view.is_empty() {
Some(PublishedView {
view_id: view.id.clone(),
name: view.name.clone(),
icon: view
.icon
.as_ref()
.map(|icon| to_dto_view_icon(icon.clone())),
layout: to_view_layout(&view.layout),
extra,
children: pruned_view,
})
} else {
None
}
}