feat: handle duplicated publish names for a workspace (#944)
* feat: handle duplicated publish names for a workspace * chore: cargo sqlx
This commit is contained in:
parent
1cbb5052f1
commit
56c55fb6a9
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM af_published_collab\n WHERE workspace_id = $1\n AND publish_name = $2\n )\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "exists",
|
||||||
|
"type_info": "Bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid",
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "12dcf313d0e4c0c0da2569f3326e49e1a78fa54537cb8826a20bef2769d04dd1"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT view_id\n FROM af_published_collab\n WHERE workspace_id = $1\n AND publish_name = $2\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "view_id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid",
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "2571550f7e2dd81103960741fecbf886f52f681c44344a814780b01fc06e4c8f"
|
||||||
|
}
|
||||||
|
|
@ -155,6 +155,12 @@ pub enum AppError {
|
||||||
|
|
||||||
#[error("There is existing access request for workspace {workspace_id} and view {view_id}")]
|
#[error("There is existing access request for workspace {workspace_id} and view {view_id}")]
|
||||||
AccessRequestAlreadyExists { workspace_id: Uuid, view_id: Uuid },
|
AccessRequestAlreadyExists { workspace_id: Uuid, view_id: Uuid },
|
||||||
|
|
||||||
|
#[error("There is existing published view for workspace {workspace_id} with publish_name {publish_name}")]
|
||||||
|
PublishNameAlreadyExists {
|
||||||
|
workspace_id: Uuid,
|
||||||
|
publish_name: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppError {
|
impl AppError {
|
||||||
|
|
@ -225,6 +231,7 @@ impl AppError {
|
||||||
AppError::MissingView(_) => ErrorCode::MissingView,
|
AppError::MissingView(_) => ErrorCode::MissingView,
|
||||||
AppError::AccessRequestAlreadyExists { .. } => ErrorCode::AccessRequestAlreadyExists,
|
AppError::AccessRequestAlreadyExists { .. } => ErrorCode::AccessRequestAlreadyExists,
|
||||||
AppError::TooManyImportTask(_) => ErrorCode::TooManyImportTask,
|
AppError::TooManyImportTask(_) => ErrorCode::TooManyImportTask,
|
||||||
|
AppError::PublishNameAlreadyExists { .. } => ErrorCode::PublishNameAlreadyExists,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -360,6 +367,7 @@ pub enum ErrorCode {
|
||||||
CustomNamespaceTooShort = 1047,
|
CustomNamespaceTooShort = 1047,
|
||||||
CustomNamespaceTooLong = 1048,
|
CustomNamespaceTooLong = 1048,
|
||||||
CustomNamespaceReserved = 1049,
|
CustomNamespaceReserved = 1049,
|
||||||
|
PublishNameAlreadyExists = 1050,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorCode {
|
impl ErrorCode {
|
||||||
|
|
|
||||||
|
|
@ -1599,3 +1599,49 @@ pub async fn update_import_task_metadata(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub async fn select_publish_name_exists(
|
||||||
|
pg_pool: &PgPool,
|
||||||
|
workspace_uuid: &Uuid,
|
||||||
|
publish_name: &str,
|
||||||
|
) -> Result<bool, AppError> {
|
||||||
|
let exists = sqlx::query_scalar!(
|
||||||
|
r#"
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM af_published_collab
|
||||||
|
WHERE workspace_id = $1
|
||||||
|
AND publish_name = $2
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
workspace_uuid,
|
||||||
|
publish_name
|
||||||
|
)
|
||||||
|
.fetch_one(pg_pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(exists.unwrap_or(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub async fn select_view_id_from_publish_name(
|
||||||
|
pg_pool: &PgPool,
|
||||||
|
workspace_uuid: &Uuid,
|
||||||
|
publish_name: &str,
|
||||||
|
) -> Result<Option<Uuid>, AppError> {
|
||||||
|
let res = sqlx::query_scalar!(
|
||||||
|
r#"
|
||||||
|
SELECT view_id
|
||||||
|
FROM af_published_collab
|
||||||
|
WHERE workspace_id = $1
|
||||||
|
AND publish_name = $2
|
||||||
|
"#,
|
||||||
|
workspace_uuid,
|
||||||
|
publish_name
|
||||||
|
)
|
||||||
|
.fetch_optional(pg_pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use database::{
|
||||||
select_default_published_view_id_for_namespace, update_published_collabs,
|
select_default_published_view_id_for_namespace, update_published_collabs,
|
||||||
update_workspace_default_publish_view, update_workspace_default_publish_view_set_null,
|
update_workspace_default_publish_view, update_workspace_default_publish_view_set_null,
|
||||||
},
|
},
|
||||||
|
workspace::{select_publish_name_exists, select_view_id_from_publish_name},
|
||||||
};
|
};
|
||||||
use database_entity::dto::PatchPublishedCollab;
|
use database_entity::dto::PatchPublishedCollab;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -291,6 +292,13 @@ impl PublishedCollabStore for PublishedCollabPostgresStore {
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
for publish_item in &publish_items {
|
for publish_item in &publish_items {
|
||||||
check_collab_publish_name(publish_item.meta.publish_name.as_str())?;
|
check_collab_publish_name(publish_item.meta.publish_name.as_str())?;
|
||||||
|
check_view_id_publish_name_conflict(
|
||||||
|
&self.pg_pool,
|
||||||
|
workspace_id,
|
||||||
|
&publish_item.meta.view_id,
|
||||||
|
publish_item.meta.publish_name.as_str(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
let publish_items_batch_size = publish_items.len() as i64;
|
let publish_items_batch_size = publish_items.len() as i64;
|
||||||
let result =
|
let result =
|
||||||
|
|
@ -415,6 +423,14 @@ impl PublishedCollabStore for PublishedCollabS3StoreWithPostgresFallback {
|
||||||
let mut handles: Vec<tokio::task::JoinHandle<()>> = vec![];
|
let mut handles: Vec<tokio::task::JoinHandle<()>> = vec![];
|
||||||
for publish_item in &publish_items {
|
for publish_item in &publish_items {
|
||||||
check_collab_publish_name(publish_item.meta.publish_name.as_str())?;
|
check_collab_publish_name(publish_item.meta.publish_name.as_str())?;
|
||||||
|
check_view_id_publish_name_conflict(
|
||||||
|
&self.pg_pool,
|
||||||
|
workspace_id,
|
||||||
|
&publish_item.meta.view_id,
|
||||||
|
publish_item.meta.publish_name.as_str(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let object_key = get_collab_s3_key(workspace_id, &publish_item.meta.view_id);
|
let object_key = get_collab_s3_key(workspace_id, &publish_item.meta.view_id);
|
||||||
let data = publish_item.data.clone();
|
let data = publish_item.data.clone();
|
||||||
let bucket_client = self.bucket_client.clone();
|
let bucket_client = self.bucket_client.clone();
|
||||||
|
|
@ -578,6 +594,7 @@ async fn patch_collabs(
|
||||||
for patch in patches {
|
for patch in patches {
|
||||||
if let Some(new_publish_name) = patch.publish_name.as_deref() {
|
if let Some(new_publish_name) = patch.publish_name.as_deref() {
|
||||||
check_collab_publish_name(new_publish_name)?;
|
check_collab_publish_name(new_publish_name)?;
|
||||||
|
check_publish_name_already_exists(pg_pool, workspace_id, new_publish_name).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
check_workspace_owner_or_publisher(pg_pool, user_uuid, workspace_id, &view_ids).await?;
|
check_workspace_owner_or_publisher(pg_pool, user_uuid, workspace_id, &view_ids).await?;
|
||||||
|
|
@ -587,3 +604,41 @@ async fn patch_collabs(
|
||||||
txn.commit().await?;
|
txn.commit().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if the `publish_name` already exists for the workspace
|
||||||
|
async fn check_publish_name_already_exists(
|
||||||
|
pg_pool: &PgPool,
|
||||||
|
workspace_id: &Uuid,
|
||||||
|
publish_name: &str,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let publish_name_exists = select_publish_name_exists(pg_pool, workspace_id, publish_name).await?;
|
||||||
|
if publish_name_exists {
|
||||||
|
return Err(AppError::PublishNameAlreadyExists {
|
||||||
|
workspace_id: *workspace_id,
|
||||||
|
publish_name: publish_name.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the `publish_name` already exists on another view
|
||||||
|
async fn check_view_id_publish_name_conflict(
|
||||||
|
pg_pool: &PgPool,
|
||||||
|
workspace_id: &Uuid,
|
||||||
|
view_id: &Uuid,
|
||||||
|
publish_name: &str,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
match select_view_id_from_publish_name(pg_pool, workspace_id, publish_name).await? {
|
||||||
|
Some(published_view_id) => {
|
||||||
|
if published_view_id != *view_id {
|
||||||
|
Err(AppError::PublishNameAlreadyExists {
|
||||||
|
workspace_id: *workspace_id,
|
||||||
|
publish_name: publish_name.to_string(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,7 @@ async fn test_publish_doc() {
|
||||||
let publish_name_2 = "publish-name-2";
|
let publish_name_2 = "publish-name-2";
|
||||||
let view_id_2 = uuid::Uuid::new_v4();
|
let view_id_2 = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
// User publishes two collabs
|
||||||
c.publish_collabs::<MyCustomMetadata, &[u8]>(
|
c.publish_collabs::<MyCustomMetadata, &[u8]>(
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
vec![
|
vec![
|
||||||
|
|
@ -123,6 +124,25 @@ async fn test_publish_doc() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// User cannot publish another view_id with the same publish name
|
||||||
|
let err = c
|
||||||
|
.publish_collabs::<MyCustomMetadata, &[u8]>(
|
||||||
|
&workspace_id,
|
||||||
|
vec![PublishCollabItem {
|
||||||
|
meta: PublishCollabMetadata {
|
||||||
|
view_id: uuid::Uuid::new_v4(),
|
||||||
|
publish_name: publish_name_1.to_string(),
|
||||||
|
metadata: MyCustomMetadata {
|
||||||
|
title: "some_other_title".to_string(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: "some_other_yrs_data".as_bytes(),
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.code, ErrorCode::PublishNameAlreadyExists, "{:?}", err);
|
||||||
|
|
||||||
{
|
{
|
||||||
// Check that the published collabs are listed
|
// Check that the published collabs are listed
|
||||||
let published_view_infos = c.list_published_views(&workspace_id).await.unwrap();
|
let published_view_infos = c.list_published_views(&workspace_id).await.unwrap();
|
||||||
|
|
@ -263,6 +283,21 @@ async fn test_publish_doc() {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
// User cannot change `publish_name` if the `publish_name` already exists
|
||||||
|
// for the same workspace
|
||||||
|
let err = c
|
||||||
|
.patch_published_collabs(
|
||||||
|
&workspace_id,
|
||||||
|
&[PatchPublishedCollab {
|
||||||
|
view_id: view_id_1,
|
||||||
|
// publish_name_2 already exists
|
||||||
|
publish_name: Some(publish_name_2.to_string()),
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(err.code, ErrorCode::PublishNameAlreadyExists, "{:?}", err);
|
||||||
|
|
||||||
let new_publish_name_1 = "new-publish-name-1".to_string();
|
let new_publish_name_1 = "new-publish-name-1".to_string();
|
||||||
|
|
||||||
// User change publish name
|
// User change publish name
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue