mirror of
https://github.com/AppFlowy-IO/AppFlowy-Cloud.git
synced 2025-04-19 03:24:42 -04:00
feat: support last publish name
This commit is contained in:
parent
4c024ac479
commit
a07b453c0b
14 changed files with 225 additions and 39 deletions
8
.github/workflows/integration_test.yml
vendored
8
.github/workflows/integration_test.yml
vendored
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
15
.sqlx/query-9848af5453a70e7a3add208f8499698ca10d55da67436bb6c8e34c5818fd7a3a.json
generated
Normal file
15
.sqlx/query-9848af5453a70e7a3add208f8499698ca10d55da67436bb6c8e34c5818fd7a3a.json
generated
Normal 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"
|
||||
}
|
22
.sqlx/query-c45fa767d46311a0558110f5d27da6acd355ffe8e1df18d16c19aeb1b162fc8f.json
generated
Normal file
22
.sqlx/query-c45fa767d46311a0558110f5d27da6acd355ffe8e1df18d16c19aeb1b162fc8f.json
generated
Normal 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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
1
migrations/20241108155841_unpublished_collab.sql
Normal file
1
migrations/20241108155841_unpublished_collab.sql
Normal file
|
@ -0,0 +1 @@
|
|||
ALTER TABLE af_published_collab ADD COLUMN unpublished_at timestamp with time zone;
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue