mirror of
https://github.com/AppFlowy-IO/AppFlowy-Cloud.git
synced 2025-04-19 03:24: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
|
.env
|
||||||
.dockerignore
|
.dockerignore
|
||||||
spec.yaml
|
spec.yaml
|
||||||
|
**/target
|
||||||
target/
|
target/
|
||||||
deploy/
|
deploy/
|
||||||
tests/
|
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}")]
|
#[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 {
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
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