feat: CRUD interface for custom namespace (#882)

* feat: listing all published_info

* fix: add sqlx files

* feat: add additional fields for publish info

* feat: get and set default publish info

* chore: cargo sqlx prepare

* fix: cargo clippy

* fix: test case exe order

* chore: cargo sqlx

* feat: get info and meta from workspace namespace

* chore: cargo sqlx

* feat: add original doc info for published view

* chore: log all publish endpoints

* fix: default values for publish info extra fields

* feat: move namespace restriction to gateway
This commit is contained in:
Zack 2024-10-19 10:09:54 +08:00 committed by GitHub
parent 71fdace975
commit 60c589bd9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 708 additions and 185 deletions

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE af_workspace\n SET default_published_view_id = $1\n WHERE workspace_id = $2\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "0781735c56d22370302beec06863dccbbb9e664b212de93e5073508a82b91609"
}

View file

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n aw.publish_namespace AS namespace,\n apc.publish_name,\n apc.view_id,\n au.email AS publisher_email,\n apc.created_at AS publish_timestamp\n FROM af_published_collab apc\n JOIN af_user au ON apc.published_by = au.uid\n JOIN af_workspace aw ON apc.workspace_id = aw.workspace_id\n WHERE apc.workspace_id = $1;\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "namespace",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "publish_name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "view_id",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "publisher_email",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "publish_timestamp",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "3ab817803745c1dc20eef819d944b8cdf56df3cd31b3ed8bc15e51eacace8e04"
}

View file

@ -0,0 +1,46 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n aw.publish_namespace AS namespace,\n apc.publish_name,\n apc.view_id,\n au.email AS publisher_email,\n apc.created_at AS publish_timestamp\n FROM af_published_collab apc\n JOIN af_user au ON apc.published_by = au.uid\n JOIN af_workspace aw ON apc.workspace_id = aw.workspace_id\n WHERE apc.view_id = $1;\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "namespace",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "publish_name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "view_id",
"type_info": "Uuid"
},
{
"ordinal": 3,
"name": "publisher_email",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "publish_timestamp",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false,
false,
false
]
},
"hash": "b2724ac4427c488c23e645c1aaf3c1e9b23e8042abb3ad4e255533dda39f6309"
}

View file

@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n aw.publish_namespace AS namespace,\n apc.publish_name,\n apc.view_id\n FROM af_published_collab apc\n LEFT JOIN af_workspace aw\n ON apc.workspace_id = aw.workspace_id\n WHERE apc.view_id = $1;\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "namespace",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "publish_name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "view_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "bc5df5a1fe64ed4f32654f09d0d62459d02f494912fb38b97f87c46b62a69b1f"
}

View file

@ -1,34 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n (SELECT publish_namespace FROM af_workspace aw WHERE aw.workspace_id = apc.workspace_id) AS namespace,\n publish_name,\n view_id\n FROM af_published_collab apc\n WHERE view_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "namespace",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "publish_name",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "view_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
null,
false,
false
]
},
"hash": "f8e631002ccbe616ab558a2025d892f7812dfa039276534664f60b37e3898c75"
}

View file

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

View file

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

View file

@ -350,6 +350,8 @@ pub enum ErrorCode {
NotInviteeOfWorkspaceInvitation = 1041,
MissingView = 1042,
AccessRequestAlreadyExists = 1043,
CustomNamespaceDisabled = 1044,
CustomNamespaceDisallowed = 1045,
}
impl ErrorCode {

View file

@ -1,17 +1,39 @@
use bytes::Bytes;
use client_api_entity::workspace_dto::PublishInfoView;
use client_api_entity::{workspace_dto::PublishedDuplicate, PublishInfo, UpdatePublishNamespace};
use client_api_entity::{
CreateGlobalCommentParams, CreateReactionParams, DeleteGlobalCommentParams, DeleteReactionParams,
GetReactionQueryParams, GlobalComments, Reactions,
GetReactionQueryParams, GlobalComments, PublishInfoMeta, Reactions, UpdateDefaultPublishView,
};
use reqwest::Method;
use shared_entity::response::{AppResponse, AppResponseError};
use tracing::instrument;
use crate::Client;
use crate::{log_request_id, Client};
// Publisher API
impl Client {
#[instrument(level = "debug", skip_all)]
pub async fn list_published_views(
&self,
workspace_id: &str,
) -> Result<Vec<PublishInfoView>, AppResponseError> {
let url = format!(
"{}/api/workspace/{}/published-info",
self.base_url, workspace_id,
);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
log_request_id(&resp);
AppResponse::<Vec<PublishInfoView>>::from_response(resp)
.await?
.into_data()
}
pub async fn set_workspace_publish_namespace(
&self,
workspace_id: &str,
@ -30,7 +52,7 @@ impl Client {
})
.send()
.await?;
log_request_id(&resp);
AppResponse::<()>::from_response(resp).await?.into_error()
}
@ -47,6 +69,7 @@ impl Client {
.await?
.send()
.await?;
log_request_id(&resp);
AppResponse::<String>::from_response(resp)
.await?
.into_data()
@ -64,6 +87,7 @@ impl Client {
.json(view_ids)
.send()
.await?;
log_request_id(&resp);
AppResponse::<()>::from_response(resp).await?.into_error()
}
@ -86,6 +110,7 @@ impl Client {
})
.send()
.await?;
log_request_id(&resp);
AppResponse::<()>::from_response(resp).await?.into_error()
}
@ -106,6 +131,7 @@ impl Client {
})
.send()
.await?;
log_request_id(&resp);
AppResponse::<()>::from_response(resp).await?.into_error()
}
@ -128,6 +154,7 @@ impl Client {
})
.send()
.await?;
log_request_id(&resp);
AppResponse::<()>::from_response(resp).await?.into_error()
}
@ -150,8 +177,47 @@ impl Client {
})
.send()
.await?;
log_request_id(&resp);
AppResponse::<()>::from_response(resp).await?.into_error()
}
pub async fn set_default_publish_view(
&self,
workspace_id: &str,
view_id: uuid::Uuid,
) -> Result<(), AppResponseError> {
let url = format!(
"{}/api/workspace/{}/publish-default",
self.base_url, workspace_id
);
let resp = self
.http_client_with_auth(Method::PUT, &url)
.await?
.json(&UpdateDefaultPublishView { view_id })
.send()
.await?;
log_request_id(&resp);
AppResponse::<()>::from_response(resp).await?.into_error()
}
pub async fn get_default_publish_view_info(
&self,
workspace_id: &str,
) -> Result<PublishInfo, AppResponseError> {
let url = format!(
"{}/api/workspace/{}/publish-default",
self.base_url, workspace_id
);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
log_request_id(&resp);
AppResponse::<PublishInfo>::from_response(resp)
.await?
.into_data()
}
}
// Optional login
@ -171,6 +237,7 @@ impl Client {
};
let resp = client.send().await?;
log_request_id(&resp);
AppResponse::<GlobalComments>::from_response(resp)
.await?
.into_data()
@ -192,6 +259,32 @@ impl Client {
.into_data()
}
#[instrument(level = "debug", skip_all)]
pub async fn get_default_published_collab<T>(
&self,
publish_namespace: &str,
) -> Result<PublishInfoMeta<T>, AppResponseError>
where
T: serde::de::DeserializeOwned + 'static,
{
let url = format!(
"{}/api/workspace/published/{}",
self.base_url, publish_namespace,
);
let resp = self
.cloud_client
.get(&url)
.send()
.await?
.error_for_status()?;
log_request_id(&resp);
AppResponse::<PublishInfoMeta<T>>::from_response(resp)
.await?
.into_data()
}
#[instrument(level = "debug", skip_all)]
pub async fn get_published_collab<T>(
&self,
@ -217,6 +310,7 @@ impl Client {
.send()
.await?
.error_for_status()?;
log_request_id(&resp);
let txt = resp.text().await?;
@ -244,14 +338,9 @@ impl Client {
"{}/api/workspace/published/{}/{}/blob",
self.base_url, publish_namespace, publish_name
);
let bytes = self
.cloud_client
.get(&url)
.send()
.await?
.error_for_status()?
.bytes()
.await?;
let resp = self.cloud_client.get(&url).send().await?;
log_request_id(&resp);
let bytes = resp.error_for_status()?.bytes().await?;
if let Ok(app_err) = serde_json::from_slice::<AppResponseError>(&bytes) {
return Err(app_err);
@ -275,6 +364,7 @@ impl Client {
.json(publish_duplicate)
.send()
.await?;
log_request_id(&resp);
AppResponse::<()>::from_response(resp).await?.into_error()
}
@ -295,6 +385,7 @@ impl Client {
})
.send()
.await?;
log_request_id(&resp);
AppResponse::<Reactions>::from_response(resp)
.await?
.into_data()

View file

@ -374,6 +374,17 @@ pub struct UpdatePublishNamespace {
pub new_namespace: String,
}
#[derive(Serialize, Deserialize)]
pub struct UpdateDefaultPublishView {
pub view_id: Uuid,
}
#[derive(Serialize, Deserialize)]
pub struct DefaultPublishViewInfoMeta {
pub info: PublishInfo,
pub meta: serde_json::Value,
}
#[derive(Debug, Clone, Validate, Serialize, Deserialize)]
pub struct QueryCollabMembers {
#[validate(custom = "validate_not_empty_str")]
@ -397,11 +408,21 @@ pub struct AFCollabMember {
pub permission: AFPermission,
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct PublishInfo {
pub namespace: Option<String>,
pub publish_name: String,
pub view_id: Uuid,
#[serde(default)]
pub publisher_email: String,
#[serde(default)]
pub publish_timestamp: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PublishInfoMeta<Meta> {
pub info: PublishInfo,
pub meta: Meta,
}
#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, Hash)]

View file

@ -82,6 +82,34 @@ pub async fn update_workspace_publish_namespace<'a, E: Executor<'a, Database = P
Ok(())
}
#[inline]
pub async fn update_workspace_default_publish_view<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
workspace_id: &Uuid,
new_view_id: &Uuid,
) -> Result<(), AppError> {
let res = sqlx::query!(
r#"
UPDATE af_workspace
SET default_published_view_id = $1
WHERE workspace_id = $2
"#,
new_view_id,
workspace_id,
)
.execute(executor)
.await?;
if res.rows_affected() != 1 {
tracing::error!(
"Failed to update workspace default publish view, workspace_id: {}, new_view_id: {}, rows_affected: {}",
workspace_id, new_view_id, res.rows_affected()
);
}
Ok(())
}
#[inline]
pub async fn select_workspace_publish_namespace<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
@ -290,6 +318,45 @@ pub async fn select_published_collab_blob<'a, E: Executor<'a, Database = Postgre
Ok(res)
}
pub async fn select_default_published_view_id_for_namespace<
'a,
E: Executor<'a, Database = Postgres>,
>(
executor: E,
namespace: &str,
) -> Result<Option<Uuid>, AppError> {
let res = sqlx::query_scalar!(
r#"
SELECT default_published_view_id
FROM af_workspace
WHERE publish_namespace = $1
"#,
namespace,
)
.fetch_one(executor)
.await?;
Ok(res)
}
pub async fn select_default_published_view_id<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
workspace_id: &Uuid,
) -> Result<Option<Uuid>, AppError> {
let res = sqlx::query_scalar!(
r#"
SELECT default_published_view_id
FROM af_workspace
WHERE workspace_id = $1
"#,
workspace_id,
)
.fetch_one(executor)
.await?;
Ok(res)
}
pub async fn select_published_collab_info<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
view_id: &Uuid,
@ -300,10 +367,12 @@ pub async fn select_published_collab_info<'a, E: Executor<'a, Database = Postgre
SELECT
aw.publish_namespace AS namespace,
apc.publish_name,
apc.view_id
apc.view_id,
au.email AS publisher_email,
apc.created_at AS publish_timestamp
FROM af_published_collab apc
LEFT JOIN af_workspace aw
ON apc.workspace_id = aw.workspace_id
JOIN af_user au ON apc.published_by = au.uid
JOIN af_workspace aw ON apc.workspace_id = aw.workspace_id
WHERE apc.view_id = $1;
"#,
view_id,
@ -314,6 +383,32 @@ pub async fn select_published_collab_info<'a, E: Executor<'a, Database = Postgre
Ok(res)
}
pub async fn select_all_published_collab_info<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
workspace_id: &Uuid,
) -> Result<Vec<PublishInfo>, AppError> {
let res = sqlx::query_as!(
PublishInfo,
r#"
SELECT
aw.publish_namespace AS namespace,
apc.publish_name,
apc.view_id,
au.email AS publisher_email,
apc.created_at AS publish_timestamp
FROM af_published_collab apc
JOIN af_user au ON apc.published_by = au.uid
JOIN af_workspace aw ON apc.workspace_id = aw.workspace_id
WHERE apc.workspace_id = $1;
"#,
workspace_id,
)
.fetch_all(executor)
.await?;
Ok(res)
}
pub async fn select_workspace_id_for_publish_namespace<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
publish_namespace: &str,

View file

@ -1,7 +1,7 @@
use chrono::{DateTime, Utc};
use database_entity::dto::{
AFRole, AFWorkspaceInvitation, AFWorkspaceInvitationStatus, AFWorkspaceSettings, GlobalComment,
PublishInfo, Reaction,
Reaction,
};
use futures_util::stream::BoxStream;
use sqlx::{types::uuid, Executor, PgPool, Postgres, Transaction};
@ -1190,28 +1190,6 @@ pub async fn select_published_collab_blob<'a, E: Executor<'a, Database = Postgre
Ok(res)
}
pub async fn select_published_collab_info<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
view_id: &Uuid,
) -> Result<PublishInfo, AppError> {
let res = sqlx::query_as!(
PublishInfo,
r#"
SELECT
(SELECT publish_namespace FROM af_workspace aw WHERE aw.workspace_id = apc.workspace_id) AS namespace,
publish_name,
view_id
FROM af_published_collab apc
WHERE view_id = $1
"#,
view_id,
)
.fetch_one(executor)
.await?;
Ok(res)
}
pub async fn select_owner_of_published_collab<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
view_id: &Uuid,

View file

@ -1,6 +1,6 @@
use chrono::{DateTime, Utc};
use collab_entity::{CollabType, EncodedCollab};
use database_entity::dto::{AFRole, AFWebUser, AFWorkspaceInvitationStatus};
use database_entity::dto::{AFRole, AFWebUser, AFWorkspaceInvitationStatus, PublishInfo};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::{collections::HashMap, ops::Deref};
@ -166,6 +166,13 @@ pub struct FolderViewMinimal {
pub layout: ViewLayout,
}
/// Publish info with actual view info
#[derive(Debug, Serialize, Deserialize)]
pub struct PublishInfoView {
pub view: FolderViewMinimal,
pub info: PublishInfo,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct SectionItems {
pub views: Vec<FolderView>,

View file

@ -0,0 +1,4 @@
ALTER TABLE af_workspace ADD COLUMN default_published_view_id UUID;
ALTER TABLE af_published_collab ALTER COLUMN created_at SET NOT NULL;
ALTER TABLE af_published_collab ALTER COLUMN updated_at SET NOT NULL;

View file

@ -40,7 +40,7 @@ async fn get_access_request_handler(
let uid = state.user_cache.get_user_uid(&uuid).await?;
let access_request = get_access_request(
&state.pg_pool,
state.collab_access_control_storage.clone(),
&state.collab_access_control_storage,
access_request_id,
uid,
)

View file

@ -49,6 +49,7 @@ use crate::biz::workspace::ops::{
get_reactions_on_published_view, remove_comment_on_published_view, remove_reaction_on_comment,
};
use crate::biz::workspace::page_view::{get_page_view_collab, update_page_collab_data};
use crate::biz::workspace::publish::get_workspace_default_publish_view_info_meta;
use crate::domain::compression::{
blocking_decompress, decompress, CompressionType, X_COMPRESSION_TYPE,
};
@ -150,6 +151,10 @@ pub fn workspace_scope() -> Scope {
.route(web::put().to(update_collab_member_handler))
.route(web::delete().to(remove_collab_member_handler)),
)
.service(
web::resource("/published/{publish_namespace}")
.route(web::get().to(get_default_published_collab_info_meta_handler)),
)
.service(
web::resource("/published/{publish_namespace}/{publish_name}")
.route(web::get().to(get_published_collab_handler)),
@ -162,6 +167,10 @@ pub fn workspace_scope() -> Scope {
web::resource("{workspace_id}/published-duplicate")
.route(web::post().to(post_published_duplicate_handler)),
)
.service(
web::resource("/{workspace_id}/published-info")
.route(web::get().to(list_published_collab_info_handler)),
)
.service(
web::resource("/published-info/{view_id}")
.route(web::get().to(get_published_collab_info_handler)),
@ -183,6 +192,11 @@ pub fn workspace_scope() -> Scope {
.route(web::put().to(put_publish_namespace_handler))
.route(web::get().to(get_publish_namespace_handler)),
)
.service(
web::resource("/{workspace_id}/publish-default")
.route(web::put().to(put_workspace_default_published_view_handler))
.route(web::get().to(get_workspace_published_default_info_handler)),
)
.service(
web::resource("/{workspace_id}/publish")
.route(web::post().to(post_publish_collabs_handler))
@ -1136,6 +1150,34 @@ async fn remove_collab_member_handler(
Ok(Json(AppResponse::Ok()))
}
async fn put_workspace_default_published_view_handler(
user_uuid: UserUuid,
workspace_id: web::Path<Uuid>,
payload: Json<UpdateDefaultPublishView>,
state: Data<AppState>,
) -> Result<Json<AppResponse<()>>> {
let new_default_pub_view_id = payload.into_inner().view_id;
biz::workspace::publish::set_workspace_default_publish_view(
&state.pg_pool,
&user_uuid,
&workspace_id,
&new_default_pub_view_id,
)
.await?;
Ok(Json(AppResponse::Ok()))
}
async fn get_workspace_published_default_info_handler(
workspace_id: web::Path<Uuid>,
state: Data<AppState>,
) -> Result<Json<AppResponse<PublishInfo>>> {
let workspace_id = workspace_id.into_inner();
let info =
biz::workspace::publish::get_workspace_default_publish_view_info(&state.pg_pool, &workspace_id)
.await?;
Ok(Json(AppResponse::Ok().with_data(info)))
}
async fn put_publish_namespace_handler(
user_uuid: UserUuid,
workspace_id: web::Path<Uuid>,
@ -1164,6 +1206,18 @@ async fn get_publish_namespace_handler(
Ok(Json(AppResponse::Ok().with_data(namespace)))
}
async fn get_default_published_collab_info_meta_handler(
publish_namespace: web::Path<String>,
state: Data<AppState>,
) -> Result<Json<AppResponse<PublishInfoMeta<serde_json::Value>>>> {
let publish_namespace = publish_namespace.into_inner();
let (info, meta) =
get_workspace_default_publish_view_info_meta(&state.pg_pool, &publish_namespace).await?;
Ok(Json(
AppResponse::Ok().with_data(PublishInfoMeta { info, meta }),
))
}
async fn get_published_collab_handler(
path_param: web::Path<(String, String)>,
state: Data<AppState>,
@ -1209,6 +1263,20 @@ async fn post_published_duplicate_handler(
Ok(Json(AppResponse::Ok()))
}
async fn list_published_collab_info_handler(
workspace_id: web::Path<Uuid>,
state: Data<AppState>,
) -> Result<Json<AppResponse<Vec<PublishInfoView>>>> {
let publish_infos = biz::workspace::publish::list_collab_publish_info(
state.published_collab_store.as_ref(),
&state.collab_access_control_storage,
&workspace_id.into_inner(),
)
.await?;
Ok(Json(AppResponse::Ok().with_data(publish_infos)))
}
async fn get_published_collab_info_handler(
view_id: web::Path<Uuid>,
state: Data<AppState>,
@ -1478,7 +1546,7 @@ async fn get_workspace_folder_handler(
workspace_id.to_string()
};
let folder_view = biz::collab::ops::get_user_workspace_structure(
state.collab_access_control_storage.clone(),
&state.collab_access_control_storage,
&state.pg_pool,
uid,
workspace_id,
@ -1497,7 +1565,7 @@ async fn get_recent_views_handler(
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let workspace_id = workspace_id.into_inner();
let folder_views = get_user_recent_folder_views(
state.collab_access_control_storage.clone(),
&state.collab_access_control_storage,
&state.pg_pool,
uid,
workspace_id,
@ -1517,7 +1585,7 @@ async fn get_favorite_views_handler(
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let workspace_id = workspace_id.into_inner();
let folder_views = get_user_favorite_folder_views(
state.collab_access_control_storage.clone(),
&state.collab_access_control_storage,
&state.pg_pool,
uid,
workspace_id,
@ -1536,12 +1604,8 @@ async fn get_trash_views_handler(
) -> Result<Json<AppResponse<SectionItems>>> {
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
let workspace_id = workspace_id.into_inner();
let folder_views = get_user_trash_folder_views(
state.collab_access_control_storage.clone(),
uid,
workspace_id,
)
.await?;
let folder_views =
get_user_trash_folder_views(&state.collab_access_control_storage, uid, workspace_id).await?;
let section_items = SectionItems {
views: folder_views,
};
@ -1553,7 +1617,7 @@ async fn get_workspace_publish_outline_handler(
state: Data<AppState>,
) -> Result<Json<AppResponse<PublishedView>>> {
let published_view = biz::collab::ops::get_published_view(
state.collab_access_control_storage.clone(),
&state.collab_access_control_storage,
publish_namespace.into_inner(),
&state.pg_pool,
)

View file

@ -1,9 +1,10 @@
use std::{ops::DerefMut, sync::Arc};
use std::ops::DerefMut;
use std::sync::Arc;
use crate::mailer::AFCloudMailer;
use crate::{
biz::collab::{
folder_view::{to_dto_view_icon, to_view_layout},
folder_view::{to_dto_view_icon, to_dto_view_layout},
ops::get_latest_collab_folder,
},
mailer::{WorkspaceAccessRequestApprovedMailerParam, WorkspaceAccessRequestMailerParam},
@ -72,7 +73,7 @@ pub async fn create_access_request(
pub async fn get_access_request(
pg_pool: &PgPool,
collab_storage: Arc<CollabAccessControlStorage>,
collab_storage: &CollabAccessControlStorage,
access_request_id: Uuid,
user_uid: i64,
) -> Result<AccessRequest, AppError> {
@ -96,7 +97,7 @@ pub async fn get_access_request(
view_id: v.id.clone(),
name: v.name.clone(),
icon: v.icon.as_ref().map(|icon| to_dto_view_icon(icon.clone())),
layout: to_view_layout(&v.layout),
layout: to_dto_view_layout(&v.layout),
})
.ok_or(AppError::MissingView(format!(
"the view {} is missing",

View file

@ -3,7 +3,7 @@ use std::collections::HashSet;
use app_error::AppError;
use chrono::DateTime;
use collab_folder::{Folder, SectionItem, ViewLayout as CollabFolderViewLayout};
use shared_entity::dto::workspace_dto::{FolderView, ViewLayout};
use shared_entity::dto::workspace_dto::{FolderView, FolderViewMinimal, ViewLayout};
/// Return all folders belonging to a workspace, excluding private sections which the user does not have access to.
pub fn collab_folder_to_folder_view(
@ -108,7 +108,7 @@ fn to_folder_view(
is_space: view_is_space(&view),
is_private,
is_published: published_view_ids.contains(view_id),
layout: to_view_layout(&view.layout),
layout: to_dto_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,
@ -134,7 +134,7 @@ pub fn section_items_to_folder_view(
is_published: published_view_ids.contains(&v.id),
created_at: DateTime::from_timestamp(v.created_at, 0).unwrap_or_default(),
last_edited_time: DateTime::from_timestamp(v.last_edited_time, 0).unwrap_or_default(),
layout: to_view_layout(&v.layout),
layout: to_dto_view_layout(&v.layout),
extra: v.extra.as_ref().map(|e| parse_extra_field_as_json(e)),
children: vec![],
})
@ -186,7 +186,7 @@ pub fn to_dto_view_icon_type(
}
}
pub fn to_view_layout(collab_folder_view_layout: &CollabFolderViewLayout) -> ViewLayout {
pub fn to_dto_view_layout(collab_folder_view_layout: &CollabFolderViewLayout) -> ViewLayout {
match collab_folder_view_layout {
CollabFolderViewLayout::Document => ViewLayout::Document,
CollabFolderViewLayout::Grid => ViewLayout::Grid,
@ -195,3 +195,12 @@ pub fn to_view_layout(collab_folder_view_layout: &CollabFolderViewLayout) -> Vie
CollabFolderViewLayout::Chat => ViewLayout::Chat,
}
}
pub fn to_dto_folder_view_miminal(collab_folder_view: &collab_folder::View) -> FolderViewMinimal {
FolderViewMinimal {
view_id: collab_folder_view.id.clone(),
name: collab_folder_view.name.clone(),
icon: collab_folder_view.icon.clone().map(to_dto_view_icon),
layout: to_dto_view_layout(&collab_folder_view.layout),
}
}

View file

@ -159,7 +159,7 @@ pub async fn get_collab_member_list(
}
pub async fn get_user_favorite_folder_views(
collab_storage: Arc<CollabAccessControlStorage>,
collab_storage: &CollabAccessControlStorage,
pg_pool: &PgPool,
uid: i64,
workspace_id: Uuid,
@ -193,7 +193,7 @@ pub async fn get_user_favorite_folder_views(
}
pub async fn get_user_recent_folder_views(
collab_storage: Arc<CollabAccessControlStorage>,
collab_storage: &CollabAccessControlStorage,
pg_pool: &PgPool,
uid: i64,
workspace_id: Uuid,
@ -227,7 +227,7 @@ pub async fn get_user_recent_folder_views(
}
pub async fn get_user_trash_folder_views(
collab_storage: Arc<CollabAccessControlStorage>,
collab_storage: &CollabAccessControlStorage,
uid: i64,
workspace_id: Uuid,
) -> Result<Vec<FolderView>, AppError> {
@ -246,7 +246,7 @@ pub async fn get_user_trash_folder_views(
}
pub async fn get_user_workspace_structure(
collab_storage: Arc<CollabAccessControlStorage>,
collab_storage: &CollabAccessControlStorage,
pg_pool: &PgPool,
uid: i64,
workspace_id: Uuid,
@ -275,7 +275,7 @@ pub async fn get_user_workspace_structure(
}
pub async fn get_latest_collab_folder(
collab_storage: Arc<CollabAccessControlStorage>,
collab_storage: &CollabAccessControlStorage,
collab_origin: GetCollabOrigin,
workspace_id: &str,
) -> Result<Folder, AppError> {
@ -305,7 +305,7 @@ pub async fn get_latest_collab_folder(
}
pub async fn get_latest_collab_encoded(
collab_storage: Arc<CollabAccessControlStorage>,
collab_storage: &CollabAccessControlStorage,
collab_origin: GetCollabOrigin,
workspace_id: &str,
oid: &str,
@ -327,7 +327,7 @@ pub async fn get_latest_collab_encoded(
}
pub async fn get_published_view(
collab_storage: Arc<CollabAccessControlStorage>,
collab_storage: &CollabAccessControlStorage,
publish_namespace: String,
pg_pool: &PgPool,
) -> Result<PublishedView, AppError> {

View file

@ -4,7 +4,7 @@ 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};
use super::folder_view::{to_dto_view_icon, to_dto_view_layout};
/// Returns only folders that are published, or one of the nested subfolders is published.
/// Exclude folders that are in the trash.
@ -90,7 +90,7 @@ fn to_publish_view(
.as_ref()
.map(|icon| to_dto_view_icon(icon.clone())),
is_published,
layout: to_view_layout(&view.layout),
layout: to_dto_view_layout(&view.layout),
extra,
children: pruned_view,
})

View file

@ -23,7 +23,7 @@ use yrs::Update;
use crate::api::metrics::AppFlowyWebMetrics;
use crate::biz::collab::folder_view::{
parse_extra_field_as_json, to_dto_view_icon, to_view_layout,
parse_extra_field_as_json, to_dto_view_icon, to_dto_view_layout,
};
use crate::biz::collab::{
folder_view::view_is_space,
@ -40,7 +40,7 @@ pub async fn get_page_view_collab(
view_id: &str,
) -> Result<PageCollab, AppResponseError> {
let folder = get_latest_collab_folder(
collab_access_control_storage.clone(),
&collab_access_control_storage,
GetCollabOrigin::User { uid },
&workspace_id.to_string(),
)
@ -73,7 +73,7 @@ pub async fn get_page_view_collab(
is_space: view_is_space(&view),
is_private: false,
is_published: publish_view_ids.contains(view_id),
layout: to_view_layout(&view.layout),
layout: to_dto_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)),
@ -81,13 +81,8 @@ pub async fn get_page_view_collab(
};
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
get_page_collab_data_for_document(&collab_access_control_storage, uid, workspace_id, view_id)
.await
},
collab_folder::ViewLayout::Grid
| collab_folder::ViewLayout::Board
@ -126,7 +121,7 @@ async fn get_page_collab_data_for_database(
) -> 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(),
&collab_access_control_storage,
GetCollabOrigin::User { uid },
&workspace_id.to_string(),
&ws_db_oid,
@ -147,7 +142,7 @@ async fn get_page_collab_data_for_database(
.database_id
};
let db = get_latest_collab_encoded(
collab_access_control_storage.clone(),
&collab_access_control_storage,
GetCollabOrigin::User { uid },
&workspace_id.to_string(),
&db_oid,
@ -225,13 +220,13 @@ async fn get_page_collab_data_for_database(
}
async fn get_page_collab_data_for_document(
collab_access_control_storage: Arc<CollabAccessControlStorage>,
collab_access_control_storage: &CollabAccessControlStorage,
uid: i64,
workspace_id: Uuid,
view_id: &str,
) -> Result<PageCollabData, AppResponseError> {
let collab = get_latest_collab_encoded(
collab_access_control_storage.clone(),
collab_access_control_storage,
GetCollabOrigin::User { uid },
&workspace_id.to_string(),
view_id,

View file

@ -1,9 +1,20 @@
use appflowy_collaborate::collab::storage::CollabAccessControlStorage;
use database::{
collab::GetCollabOrigin,
publish::{
select_all_published_collab_info, select_default_published_view_id,
select_default_published_view_id_for_namespace, update_workspace_default_publish_view,
},
};
use std::sync::Arc;
use app_error::AppError;
use async_trait::async_trait;
use database_entity::dto::{PublishCollabItem, PublishInfo};
use shared_entity::dto::publish_dto::PublishViewMetaData;
use shared_entity::dto::{
publish_dto::PublishViewMetaData,
workspace_dto::{FolderViewMinimal, PublishInfoView},
};
use sqlx::PgPool;
use tracing::debug;
use uuid::Uuid;
@ -21,7 +32,10 @@ use database::{
workspace::select_user_is_workspace_owner,
};
use crate::api::metrics::PublishedCollabMetrics;
use crate::{
api::metrics::PublishedCollabMetrics,
biz::collab::{folder_view::to_dto_folder_view_miminal, ops::get_latest_collab_folder},
};
use super::ops::check_workspace_owner;
@ -86,6 +100,61 @@ pub async fn set_workspace_namespace(
Ok(())
}
pub async fn set_workspace_default_publish_view(
pg_pool: &PgPool,
user_uuid: &Uuid,
workspace_id: &Uuid,
new_view_id: &Uuid,
) -> Result<(), AppError> {
check_workspace_owner(pg_pool, user_uuid, workspace_id).await?;
update_workspace_default_publish_view(pg_pool, workspace_id, new_view_id).await?;
Ok(())
}
pub async fn get_workspace_default_publish_view_info(
pg_pool: &PgPool,
workspace_id: &Uuid,
) -> Result<PublishInfo, AppError> {
let view_id = select_default_published_view_id(pg_pool, workspace_id)
.await?
.ok_or_else(|| {
AppError::RecordNotFound(format!(
"Default published view not found for workspace_id: {}",
workspace_id
))
})?;
let pub_info = select_published_collab_info(pg_pool, &view_id).await?;
Ok(pub_info)
}
pub async fn get_workspace_default_publish_view_info_meta(
pg_pool: &PgPool,
namespace: &str,
) -> Result<(PublishInfo, serde_json::Value), AppError> {
let view_id = select_default_published_view_id_for_namespace(pg_pool, namespace)
.await?
.ok_or_else(|| {
AppError::RecordNotFound(format!(
"Default published view not found for namespace: {}",
namespace
))
})?;
let (pub_info, meta) = tokio::try_join!(
select_published_collab_info(pg_pool, &view_id),
select_published_metadata_for_view_id(pg_pool, &view_id)
)?;
let meta = meta.ok_or_else(|| {
AppError::RecordNotFound(format!(
"Published metadata not found for view_id: {}",
view_id
))
})?;
Ok((pub_info, meta.1))
}
pub async fn get_workspace_publish_namespace(
pg_pool: &PgPool,
workspace_id: &Uuid,
@ -93,20 +162,51 @@ pub async fn get_workspace_publish_namespace(
select_workspace_publish_namespace(pg_pool, workspace_id).await
}
pub async fn list_collab_publish_info(
publish_collab_store: &dyn PublishedCollabStore,
collab_storage: &CollabAccessControlStorage,
workspace_id: &Uuid,
) -> Result<Vec<PublishInfoView>, AppError> {
let folder = get_latest_collab_folder(
collab_storage,
GetCollabOrigin::Server,
&workspace_id.to_string(),
)
.await?;
let publish_infos = publish_collab_store
.list_collab_publish_info(workspace_id)
.await?;
let mut publish_info_views: Vec<PublishInfoView> = Vec::with_capacity(publish_infos.len());
for publish_info in publish_infos {
let view_id = publish_info.view_id.to_string();
match folder.get_view(&view_id) {
Some(view) => {
publish_info_views.push(PublishInfoView {
view: to_dto_folder_view_miminal(&view),
info: publish_info,
});
},
None => {
tracing::error!("View {} not found in folder but is published", view_id);
publish_info_views.push(PublishInfoView {
view: FolderViewMinimal {
view_id,
name: publish_info.publish_name.clone(),
..Default::default()
},
info: publish_info,
});
},
};
}
Ok(publish_info_views)
}
async fn check_workspace_namespace(new_namespace: &str) -> Result<(), AppError> {
// Check len
if new_namespace.len() < 8 {
return Err(AppError::InvalidRequest(
"Namespace must be at least 8 characters long".to_string(),
));
}
if new_namespace.len() > 64 {
return Err(AppError::InvalidRequest(
"Namespace must be at most 32 characters long".to_string(),
));
}
// Must be url safe
// Only contain alphanumeric characters and hyphens
for c in new_namespace.chars() {
if !c.is_alphanumeric() && c != '-' {
@ -115,9 +215,6 @@ async fn check_workspace_namespace(new_namespace: &str) -> Result<(), AppError>
));
}
}
// TODO: add more checks for reserved words
Ok(())
}
@ -141,6 +238,11 @@ pub trait PublishedCollabStore: Sync + Send + 'static {
publish_name: &str,
) -> Result<serde_json::Value, AppError>;
async fn list_collab_publish_info(
&self,
workspace_id: &Uuid,
) -> Result<Vec<PublishInfo>, AppError>;
async fn get_collab_publish_info(&self, view_id: &Uuid) -> Result<PublishInfo, AppError>;
async fn get_collab_blob_by_publish_namespace(
@ -224,6 +326,13 @@ impl PublishedCollabStore for PublishedCollabPostgresStore {
result
}
async fn list_collab_publish_info(
&self,
workspace_id: &Uuid,
) -> Result<Vec<PublishInfo>, AppError> {
select_all_published_collab_info(&self.pg_pool, workspace_id).await
}
async fn get_collab_publish_info(&self, view_id: &Uuid) -> Result<PublishInfo, AppError> {
select_published_collab_info(&self.pg_pool, view_id).await
}
@ -371,6 +480,13 @@ impl PublishedCollabStore for PublishedCollabS3StoreWithPostgresFallback {
select_published_collab_info(&self.pg_pool, view_id).await
}
async fn list_collab_publish_info(
&self,
workspace_id: &Uuid,
) -> Result<Vec<PublishInfo>, AppError> {
select_all_published_collab_info(&self.pg_pool, workspace_id).await
}
async fn get_collab_blob_by_publish_namespace(
&self,
publish_namespace: &str,

View file

@ -198,7 +198,7 @@ impl PublishCollabDuplicator {
let ws_db_oid = select_workspace_database_oid(&pg_pool, &dest_workspace_id.parse()?).await?;
let ws_db_collab = {
let ws_database_ec = get_latest_collab_encoded(
collab_storage.clone(),
&collab_storage,
GetCollabOrigin::User {
uid: duplicator_uid,
},
@ -256,7 +256,7 @@ impl PublishCollabDuplicator {
}
let collab_folder_encoded = get_latest_collab_encoded(
collab_storage.clone(),
&collab_storage,
GetCollabOrigin::User {
uid: duplicator_uid,
},

View file

@ -1,7 +1,9 @@
use app_error::ErrorCode;
use appflowy_cloud::biz::collab::folder_view::collab_folder_to_folder_view;
use appflowy_cloud::biz::workspace::ops::collab_from_doc_state;
use client_api::entity::{AFRole, GlobalComment, PublishCollabItem, PublishCollabMetadata};
use client_api::entity::{
AFRole, GlobalComment, PublishCollabItem, PublishCollabMetadata, PublishInfoMeta,
};
use client_api_test::TestClient;
use client_api_test::{generate_unique_registered_user_client, localhost_client};
use collab::util::MapExt;
@ -47,7 +49,12 @@ async fn test_set_publish_namespace_set() {
.await
.err()
.unwrap();
assert_eq!(format!("{:?}", err.code), "PublishNamespaceAlreadyTaken");
assert_eq!(
err.code,
ErrorCode::PublishNamespaceAlreadyTaken,
"{:?}",
err
);
}
{
// can replace the namespace
@ -62,16 +69,6 @@ async fn test_set_publish_namespace_set() {
.unwrap();
assert_eq!(got_namespace, namespace);
}
{
// cannot set namespace too short
let err = c
.set_workspace_publish_namespace(&workspace_id.to_string(), "a") // too short
.await
.err()
.unwrap();
assert_eq!(format!("{:?}", err.code), "InvalidRequest");
}
{
// cannot set namespace with invalid chars
let err = c
@ -79,7 +76,7 @@ async fn test_set_publish_namespace_set() {
.await
.err()
.unwrap();
assert_eq!(format!("{:?}", err.code), "InvalidRequest");
assert_eq!(err.code, ErrorCode::InvalidRequest, "{:?}", err);
}
}
@ -125,6 +122,23 @@ async fn test_publish_doc() {
.await
.unwrap();
{
// Check that the published collabs are listed
let published_view_infos = c.list_published_views(&workspace_id).await.unwrap();
assert_eq!(published_view_infos.len(), 2);
let view_info_1 = published_view_infos
.iter()
.find(|view_info| view_info.info.publish_name == publish_name_1)
.unwrap();
assert_eq!(view_info_1.info.view_id, view_id_1);
let view_info_2 = published_view_infos
.iter()
.find(|view_info| view_info.info.publish_name == publish_name_2)
.unwrap();
assert_eq!(view_info_2.info.view_id, view_id_2);
}
{
// Non login user should be able to view the published collab
let guest_client = localhost_client();
@ -208,7 +222,36 @@ async fn test_publish_doc() {
assert_eq!(blob, "yrs_encoded_data_4");
}
c.unpublish_collabs(&workspace_id, &[view_id_1])
{
// Try to get default publish view info but not set
let err = c
.get_default_publish_view_info(&workspace_id)
.await
.unwrap_err();
assert_eq!(err.code, ErrorCode::RecordNotFound, "{:?}", err);
// Set publish view as default workspace view
c.set_default_publish_view(&workspace_id, view_id_1.to_owned())
.await
.unwrap();
// Get default publish view
let publish_info = c
.get_default_publish_view_info(&workspace_id)
.await
.unwrap();
assert_eq!(publish_info.view_id, view_id_1);
// Public can use namespace to get default publish view info and view metadata
let default_info_meta: PublishInfoMeta<MyCustomMetadata> = localhost_client()
.get_default_published_collab(&my_namespace)
.await
.unwrap();
assert_eq!(default_info_meta.info.view_id, view_id_1);
assert_eq!(default_info_meta.meta.title, "my_title_1");
}
c.unpublish_collabs(&workspace_id, &[view_id_1, view_id_2])
.await
.unwrap();
@ -220,7 +263,7 @@ async fn test_publish_doc() {
.await
.err()
.unwrap();
assert_eq!(format!("{:?}", err.code), "RecordNotFound");
assert_eq!(err.code, ErrorCode::RecordNotFound, "{:?}", err);
let guest_client = localhost_client();
let err = guest_client
@ -228,7 +271,21 @@ async fn test_publish_doc() {
.await
.err()
.unwrap();
assert_eq!(format!("{:?}", err.code), "RecordNotFound");
assert_eq!(err.code, ErrorCode::RecordNotFound, "{:?}", err);
// default publish view should not be accessible
let err = c
.get_default_publish_view_info(&workspace_id)
.await
.unwrap_err();
assert_eq!(err.code, ErrorCode::RecordNotFound, "{:?}", err);
}
{
// check that the published collabs are removed
// listing published collab should return empty
let published_infos = c.list_published_views(&workspace_id).await.unwrap();
assert_eq!(published_infos.len(), 0);
}
}