feat: support last publish name

This commit is contained in:
Zack Fu Zi Xiang 2024-11-08 17:29:02 +08:00
parent 4c024ac479
commit a07b453c0b
No known key found for this signature in database
14 changed files with 225 additions and 39 deletions

View file

@ -113,7 +113,7 @@ jobs:
export APPFLOWY_ADMIN_FRONTEND_VERSION=${GITHUB_SHA}
docker compose -f docker-compose-ci.yml up -d
docker ps -a
container_id=$(docker ps --filter name=appflowy-cloud-ai-1 -q)
if [ -n "$container_id" ]; then
echo "Displaying logs for the AppFlowy-AI container..."
@ -133,6 +133,12 @@ jobs:
RUST_LOG="info" DISABLE_CI_TEST_LOG="true" cargo test ${{ matrix.test_cmd }}
RUST_LOG="info" DISABLE_CI_TEST_LOG="true" cargo test -p admin_frontend ${{ matrix.test_cmd }}
- name: Run Tests from main branch
run: |
git checkout main
RUST_LOG="info" DISABLE_CI_TEST_LOG="true" cargo test ${{ matrix.test_cmd }}
RUST_LOG="info" DISABLE_CI_TEST_LOG="true" cargo test -p admin_frontend ${{ matrix.test_cmd }}
cleanup:
name: Cleanup Docker Images
if: always()

View file

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM af_published_collab\n WHERE workspace_id = $1\n AND view_id = ANY($2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"UuidArray"
]
},
"nullable": []
},
"hash": "02eab22805a4cba99dc7fc554f63f6a16f89a5160d1095e1906985b9ec2123ec"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n awn.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 JOIN af_workspace_namespace awn ON aw.workspace_id = awn.workspace_id AND awn.is_original = TRUE\n WHERE apc.workspace_id = $1;\n ",
"query": "\n SELECT\n awn.namespace,\n apc.publish_name,\n apc.view_id,\n au.email AS publisher_email,\n apc.created_at AS publish_timestamp,\n apc.unpublished_at AS unpublished_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 JOIN af_workspace_namespace awn ON aw.workspace_id = awn.workspace_id AND awn.is_original = TRUE\n WHERE apc.workspace_id = $1 AND apc.unpublished_at IS NULL;\n ",
"describe": {
"columns": [
{
@ -27,6 +27,11 @@
"ordinal": 4,
"name": "publish_timestamp",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "unpublished_timestamp",
"type_info": "Timestamptz"
}
],
"parameters": {
@ -39,8 +44,9 @@
false,
false,
false,
false
false,
true
]
},
"hash": "b450decddb39bd057d850efce1a8680e00a16c0d8887e94f008a2764a7a4a6f6"
"hash": "8ca5095db6ae36bd60dd32861baa2d8cd87b7478f6aa226d441b5e409abf08ab"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE af_published_collab\n SET\n blob = NULL,\n unpublished_at = NOW()\n WHERE workspace_id = $1\n AND view_id = ANY($2)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"UuidArray"
]
},
"nullable": []
},
"hash": "9848af5453a70e7a3add208f8499698ca10d55da67436bb6c8e34c5818fd7a3a"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM af_published_collab\n WHERE publish_name = ANY($1::text[])\n RETURNING publish_name\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "publish_name",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"TextArray"
]
},
"nullable": [
false
]
},
"hash": "c45fa767d46311a0558110f5d27da6acd355ffe8e1df18d16c19aeb1b162fc8f"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n awn.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 JOIN af_workspace_namespace awn ON aw.workspace_id = awn.workspace_id AND awn.is_original = TRUE\n WHERE apc.view_id = ANY($1);\n ",
"query": "\n SELECT\n awn.namespace,\n apc.publish_name,\n apc.view_id,\n au.email AS publisher_email,\n apc.created_at AS publish_timestamp,\n apc.unpublished_at AS unpublished_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 JOIN af_workspace_namespace awn ON aw.workspace_id = awn.workspace_id AND awn.is_original = TRUE\n WHERE apc.view_id = ANY($1);\n ",
"describe": {
"columns": [
{
@ -27,6 +27,11 @@
"ordinal": 4,
"name": "publish_timestamp",
"type_info": "Timestamptz"
},
{
"ordinal": 5,
"name": "unpublished_timestamp",
"type_info": "Timestamptz"
}
],
"parameters": {
@ -39,8 +44,9 @@
false,
false,
false,
false
false,
true
]
},
"hash": "55dec46e9fb30d6236013a207d2016c3aaa966cac58e1a24148ea1a954e4a544"
"hash": "e1c7d372486de8d609a2389a7af8af627af939f63302fb2a8549685eab437312"
}

View file

@ -290,7 +290,10 @@ impl Client {
&self,
view_id: &uuid::Uuid,
) -> Result<PublishInfo, AppResponseError> {
let url = format!("{}/api/workspace/published-info/{}", self.base_url, view_id);
let url = format!(
"{}/api/workspace/v1/published-info/{}",
self.base_url, view_id
);
let resp = self.cloud_client.get(&url).send().await?;
AppResponse::<PublishInfo>::from_response(resp)

View file

@ -419,6 +419,8 @@ pub struct PublishInfo {
pub publisher_email: String,
#[serde(default)]
pub publish_timestamp: DateTime<Utc>,
#[serde(default)]
pub unpublished_timestamp: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize)]

View file

@ -214,8 +214,8 @@ pub async fn select_workspace_publish_namespace(
}
#[inline]
pub async fn insert_or_replace_publish_collabs<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
pub async fn insert_or_replace_publish_collabs(
pg_pool: &PgPool,
workspace_id: &Uuid,
publisher_uuid: &Uuid,
publish_items: Vec<PublishCollabItem<serde_json::Value, Vec<u8>>>,
@ -232,6 +232,25 @@ pub async fn insert_or_replace_publish_collabs<'a, E: Executor<'a, Database = Po
blobs.push(item.data);
});
let mut txn = pg_pool.begin().await?;
let delete_publish_names = sqlx::query_scalar!(
r#"
DELETE FROM af_published_collab
WHERE publish_name = ANY($1::text[])
RETURNING publish_name
"#,
&publish_names,
)
.fetch_all(txn.as_mut())
.await?;
if delete_publish_names.len() > 0 {
tracing::info!(
"Deleted existing published collab record with publish names: {:?}",
delete_publish_names
);
}
let res = sqlx::query!(
r#"
INSERT INTO af_published_collab (workspace_id, view_id, publish_name, published_by, metadata, blob)
@ -257,7 +276,7 @@ pub async fn insert_or_replace_publish_collabs<'a, E: Executor<'a, Database = Po
&blobs,
item_count as i32,
)
.execute(executor)
.execute(txn.as_mut())
.await?;
if res.rows_affected() != item_count as u64 {
@ -267,6 +286,7 @@ pub async fn insert_or_replace_publish_collabs<'a, E: Executor<'a, Database = Po
);
}
txn.commit().await?;
Ok(())
}
@ -300,7 +320,10 @@ pub async fn delete_published_collabs<'a, E: Executor<'a, Database = Postgres>>(
) -> Result<(), AppError> {
let res = sqlx::query!(
r#"
DELETE FROM af_published_collab
UPDATE af_published_collab
SET
blob = NULL,
unpublished_at = NOW()
WHERE workspace_id = $1
AND view_id = ANY($2)
"#,
@ -502,7 +525,7 @@ async fn select_first_non_original_namespace(
Ok(res)
}
pub async fn select_published_collab_info_for_view_ids(
pub async fn select_publish_info_for_view_ids(
pg_pool: &PgPool,
view_ids: &[Uuid],
) -> Result<Vec<PublishInfo>, AppError> {
@ -514,7 +537,8 @@ pub async fn select_published_collab_info_for_view_ids(
apc.publish_name,
apc.view_id,
au.email AS publisher_email,
apc.created_at AS publish_timestamp
apc.created_at AS publish_timestamp,
apc.unpublished_at AS unpublished_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
@ -543,7 +567,7 @@ pub async fn select_published_collab_info(
pg_pool: &PgPool,
view_id: &Uuid,
) -> Result<PublishInfo, AppError> {
select_published_collab_info_for_view_ids(pg_pool, &[*view_id])
select_publish_info_for_view_ids(pg_pool, &[*view_id])
.await?
.into_iter()
.next()
@ -562,12 +586,13 @@ pub async fn select_all_published_collab_info(
apc.publish_name,
apc.view_id,
au.email AS publisher_email,
apc.created_at AS publish_timestamp
apc.created_at AS publish_timestamp,
apc.unpublished_at AS unpublished_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
JOIN af_workspace_namespace awn ON aw.workspace_id = awn.workspace_id AND awn.is_original = TRUE
WHERE apc.workspace_id = $1;
WHERE apc.workspace_id = $1 AND apc.unpublished_at IS NULL;
"#,
workspace_id,
)

View file

@ -0,0 +1 @@
ALTER TABLE af_published_collab ADD COLUMN unpublished_at timestamp with time zone;

View file

@ -187,9 +187,14 @@ pub fn workspace_scope() -> Scope {
.route(web::get().to(list_published_collab_info_handler)),
)
.service(
// deprecated since 0.7.4
web::resource("/published-info/{view_id}")
.route(web::get().to(get_published_collab_info_handler)),
)
.service(
web::resource("/v1/published-info/{view_id}")
.route(web::get().to(get_v1_published_collab_info_handler)),
)
.service(
web::resource("/published-info/{view_id}/comment")
.route(web::get().to(get_published_collab_comment_handler))
@ -1387,9 +1392,25 @@ async fn list_published_collab_info_handler(
Ok(Json(AppResponse::Ok().with_data(publish_infos)))
}
// Deprecated since 0.7.4
async fn get_published_collab_info_handler(
view_id: web::Path<Uuid>,
state: Data<AppState>,
) -> Result<Json<AppResponse<PublishInfo>>> {
let view_id = view_id.into_inner();
let collab_data = state
.published_collab_store
.get_collab_publish_info(&view_id)
.await?;
if collab_data.unpublished_timestamp.is_some() {
return Err(AppError::RecordNotFound("Collab is unpublished".to_string()).into());
}
Ok(Json(AppResponse::Ok().with_data(collab_data)))
}
async fn get_v1_published_collab_info_handler(
view_id: web::Path<Uuid>,
state: Data<AppState>,
) -> Result<Json<AppResponse<PublishInfo>>> {
let view_id = view_id.into_inner();
let collab_data = state
@ -1568,7 +1589,7 @@ async fn delete_published_collabs_handler(
}
state
.published_collab_store
.delete_collabs(&workspace_id, &view_ids, &user_uuid)
.unpublish_collabs(&workspace_id, &view_ids, &user_uuid)
.await?;
Ok(Json(AppResponse::Ok()))
}

View file

@ -10,7 +10,7 @@ use app_error::ErrorCode;
use aws_sdk_s3::primitives::ByteStream;
use database::{
file::{s3_client_impl::AwsS3BucketClientImpl, BucketClient, ResponseBlob},
publish::{select_published_collab_info, select_published_collab_info_for_view_ids},
publish::{select_published_collab_info, select_publish_info_for_view_ids},
template::*,
};
use database_entity::dto::{
@ -252,7 +252,7 @@ pub async fn get_templates_with_publish_info(
.await?;
let view_ids = templates.iter().map(|t| t.view_id).collect::<Vec<Uuid>>();
let publish_info_for_views =
select_published_collab_info_for_view_ids(pg_pool, &view_ids).await?;
select_publish_info_for_view_ids(pg_pool, &view_ids).await?;
let mut publish_info_map = publish_info_for_views
.into_iter()
.map(|info| (info.view_id, info))
@ -326,7 +326,7 @@ pub async fn get_template_homepage(
let all_view_ids: Vec<Uuid> = all_view_ids.into_iter().collect();
let publish_info_for_views: Vec<PublishInfo> =
select_published_collab_info_for_view_ids(pg_pool, &all_view_ids).await?;
select_publish_info_for_view_ids(pg_pool, &all_view_ids).await?;
let publish_info_map = publish_info_for_views
.into_iter()
.map(|info| (info.view_id, info))

View file

@ -287,7 +287,7 @@ pub trait PublishedCollabStore: Sync + Send + 'static {
publish_name: &str,
) -> Result<Vec<u8>, AppError>;
async fn delete_collabs(
async fn unpublish_collabs(
&self,
workspace_id: &Uuid,
view_ids: &[Uuid],
@ -401,7 +401,7 @@ impl PublishedCollabStore for PublishedCollabPostgresStore {
result
}
async fn delete_collabs(
async fn unpublish_collabs(
&self,
workspace_id: &Uuid,
view_ids: &[Uuid],
@ -586,7 +586,7 @@ impl PublishedCollabStore for PublishedCollabS3StoreWithPostgresFallback {
}
}
async fn delete_collabs(
async fn unpublish_collabs(
&self,
workspace_id: &Uuid,
view_ids: &[Uuid],

View file

@ -1591,3 +1591,97 @@ fn get_database_id_and_row_ids(published_db_blob: &[u8]) -> (String, HashSet<Str
let row_ids: HashSet<String> = pub_db_data.database_row_collabs.into_keys().collect();
(pub_db_id, row_ids)
}
#[tokio::test]
async fn test_republish_doc() {
let (c, _user) = generate_unique_registered_user_client().await;
let workspace_id = get_first_workspace_string(&c).await;
let my_namespace = uuid::Uuid::new_v4().to_string();
c.set_workspace_publish_namespace(&workspace_id.to_string(), my_namespace.clone())
.await
.unwrap();
let publish_name = "my-publish-name";
let view_id = uuid::Uuid::new_v4();
// User publishes 1 doc
c.publish_collabs::<MyCustomMetadata, &[u8]>(
&workspace_id,
vec![PublishCollabItem {
meta: PublishCollabMetadata {
view_id,
publish_name: publish_name.to_string(),
metadata: MyCustomMetadata {
title: "my_title_1".to_string(),
},
},
data: "yrs_encoded_data_1".as_bytes(),
}],
)
.await
.unwrap();
{
// Check that the doc is published with correct publish name
let publish_info = c.get_published_collab_info(&view_id).await.unwrap();
assert_eq!(
publish_info.publish_name, publish_name,
"{:?}",
publish_info
);
}
// user unpublishes the doc
c.unpublish_collabs(&workspace_id, &[view_id])
.await
.unwrap();
{
// Check that the doc is unpublished
let publish_info = c.get_published_collab_info(&view_id).await.unwrap();
assert!(
publish_info.unpublished_timestamp.is_some(),
"{:?}",
publish_info
);
assert_eq!(
publish_info.publish_name, publish_name,
"{:?}",
publish_info
);
}
{
// User publish another doc with different id but same publish name
let view_id_2 = uuid::Uuid::new_v4();
c.publish_collabs::<MyCustomMetadata, &[u8]>(
&workspace_id,
vec![PublishCollabItem {
meta: PublishCollabMetadata {
view_id: view_id_2,
publish_name: publish_name.to_string(),
metadata: MyCustomMetadata {
title: "my_title_2".to_string(),
},
},
data: "yrs_encoded_data_2".as_bytes(),
}],
)
.await
.unwrap();
let publish_info = c.get_published_collab_info(&view_id_2).await.unwrap();
assert_eq!(
publish_info.publish_name, publish_name,
"{:?}",
publish_info
);
}
{
// When fetching original document, it should return not found
// since the binded publish name is already used by another document
let err = c.get_published_collab_info(&view_id).await.unwrap_err();
assert_eq!(err.code, ErrorCode::RecordNotFound, "{:?}", err);
}
}