mirror of
https://github.com/AppFlowy-IO/AppFlowy-Cloud.git
synced 2025-04-17 18:44:42 -04:00
feat: published view outline endpoint
This commit is contained in:
parent
8015e34841
commit
56b9f9daf4
11 changed files with 275 additions and 8 deletions
|
@ -1,6 +1,7 @@
|
|||
.env
|
||||
.dockerignore
|
||||
spec.yaml
|
||||
**/target
|
||||
target/
|
||||
deploy/
|
||||
tests/
|
||||
|
|
22
.sqlx/query-785c4c1e9b393a1f04a88136211df0c5daa7ad0c52f3e7e071156595262ca44f.json
generated
Normal file
22
.sqlx/query-785c4c1e9b393a1f04a88136211df0c5daa7ad0c52f3e7e071156595262ca44f.json
generated
Normal 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"
|
||||
}
|
22
.sqlx/query-ee58ab18f882ed89f3506d3e28eef43c32e09e6c545a08f35fb5169e84553af6.json
generated
Normal file
22
.sqlx/query-ee58ab18f882ed89f3506d3e28eef43c32e09e6c545a08f35fb5169e84553af6.json
generated
Normal 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"
|
||||
}
|
|
@ -136,6 +136,9 @@ pub enum AppError {
|
|||
|
||||
#[error("{0}")]
|
||||
InvalidContentType(String),
|
||||
|
||||
#[error("{0}")]
|
||||
InvalidPublishedOutline(String),
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
|
@ -200,6 +203,7 @@ impl AppError {
|
|||
AppError::AIServiceUnavailable(_) => ErrorCode::AIServiceUnavailable,
|
||||
AppError::StringLengthLimitReached(_) => ErrorCode::StringLengthLimitReached,
|
||||
AppError::InvalidContentType(_) => ErrorCode::InvalidContentType,
|
||||
AppError::InvalidPublishedOutline(_) => ErrorCode::InvalidPublishedOutline,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -318,6 +322,7 @@ pub enum ErrorCode {
|
|||
InvalidContentType = 1036,
|
||||
SingleUploadLimitExceeded = 1037,
|
||||
AppleRevokeTokenError = 1038,
|
||||
InvalidPublishedOutline = 1039,
|
||||
}
|
||||
|
||||
impl ErrorCode {
|
||||
|
|
|
@ -273,3 +273,39 @@ pub async fn select_published_collab_info<'a, E: Executor<'a, Database = Postgre
|
|||
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -164,6 +164,12 @@ pub enum ViewLayout {
|
|||
Chat = 4,
|
||||
}
|
||||
|
||||
impl Default for ViewLayout {
|
||||
fn default() -> Self {
|
||||
Self::Document
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||
pub struct QueryWorkspaceParam {
|
||||
pub include_member_count: Option<bool>,
|
||||
|
@ -173,3 +179,14 @@ pub struct QueryWorkspaceParam {
|
|||
pub struct QueryWorkspaceFolder {
|
||||
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>,
|
||||
}
|
||||
|
|
|
@ -179,6 +179,10 @@ pub fn workspace_scope() -> Scope {
|
|||
web::resource("/{workspace_id}/folder")
|
||||
.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(
|
||||
web::resource("/{workspace_id}/collab/{object_id}/member/list")
|
||||
.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)))
|
||||
}
|
||||
|
||||
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]
|
||||
async fn parser_realtime_msg(
|
||||
payload: Bytes,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use collab_folder::Folder;
|
||||
use shared_entity::dto::workspace_dto::FolderView;
|
||||
use collab_folder::{Folder, ViewLayout as CollabFolderViewLayout};
|
||||
use shared_entity::dto::workspace_dto::{FolderView, ViewLayout};
|
||||
use uuid::Uuid;
|
||||
|
||||
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() {
|
||||
Some(extra) => extra,
|
||||
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 {
|
||||
ty: to_dto_view_icon_type(icon.ty),
|
||||
value: icon.value,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_dto_view_icon_type(
|
||||
pub fn to_dto_view_icon_type(
|
||||
icon: collab_folder::IconType,
|
||||
) -> shared_entity::dto::workspace_dto::IconType {
|
||||
match icon {
|
||||
|
@ -160,3 +162,13 @@ fn to_dto_view_icon_type(
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod access_control;
|
||||
pub mod folder_view;
|
||||
pub mod ops;
|
||||
pub mod publish_outline;
|
||||
|
|
|
@ -6,14 +6,17 @@ use collab_entity::CollabType;
|
|||
use collab_entity::EncodedCollab;
|
||||
use collab_folder::{CollabOrigin, Folder};
|
||||
use database::collab::{CollabStorage, GetCollabOrigin};
|
||||
use database_entity::dto::QueryCollab;
|
||||
use database_entity::dto::QueryCollabParams;
|
||||
use database::publish::select_published_view_ids_for_workspace;
|
||||
use database::publish::select_workspace_id_for_publish_namespace;
|
||||
use database_entity::dto::{QueryCollab, QueryCollabParams};
|
||||
use sqlx::PgPool;
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use anyhow::Context;
|
||||
use shared_entity::dto::workspace_dto::FolderView;
|
||||
use shared_entity::dto::workspace_dto::{FolderView, PublishedView};
|
||||
use sqlx::types::Uuid;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use tracing::{event, trace};
|
||||
use validator::Validate;
|
||||
|
||||
|
@ -24,6 +27,7 @@ use database_entity::dto::{
|
|||
};
|
||||
|
||||
use super::folder_view::collab_folder_to_folder_view;
|
||||
use super::publish_outline::collab_folder_to_published_outline;
|
||||
|
||||
/// Create a new collab member
|
||||
/// If the collab member already exists, return [AppError::RecordAlreadyExists]
|
||||
|
@ -215,3 +219,35 @@ pub async fn get_latest_collab_encoded(
|
|||
)
|
||||
.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)
|
||||
}
|
||||
|
|
98
src/biz/collab/publish_outline.rs
Normal file
98
src/biz/collab/publish_outline.rs
Normal 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
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue