feat: quick note CRUD APIs

This commit is contained in:
khorshuheng 2024-12-19 11:13:49 +08:00
parent ea131f0baa
commit 8128c2f8e5
15 changed files with 590 additions and 2 deletions

View File

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM af_quick_note WHERE quick_note_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "5cce5f82c0fb9237f724478e2167243bc772c092910f07b8226431a6dd70a7da"
}

View File

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE af_quick_note SET data = $1, updated_at = NOW() WHERE quick_note_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Uuid"
]
},
"nullable": []
},
"hash": "770a4979e137ca08c5ea625259221f9d397a56defb8e498eb92da7b3a8af612b"
}

View File

@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO af_quick_note (workspace_id, uid, data) VALUES ($1, $2, $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Int8",
"Jsonb"
]
},
"nullable": []
},
"hash": "fba0b66b63ae9561c4868891770821abe34953a6ed21fd25639beab9a76780dd"
}

View File

@ -0,0 +1,84 @@
use client_api_entity::{
CreateQuickNoteParams, ListQuickNotesQueryParams, QuickNotes, UpdateQuickNoteParams,
};
use reqwest::Method;
use shared_entity::response::{AppResponse, AppResponseError};
use uuid::Uuid;
use crate::Client;
fn quick_note_resources_url(base_url: &str, workspace_id: Uuid) -> String {
format!("{base_url}/api/workspace/{workspace_id}/quick-note")
}
fn quick_note_resource_url(base_url: &str, workspace_id: Uuid, quick_note_id: Uuid) -> String {
let quick_note_resources_prefix = quick_note_resources_url(base_url, workspace_id);
format!("{quick_note_resources_prefix}/{quick_note_id}")
}
// Quick Note API
impl Client {
pub async fn create_quick_note(&self, workspace_id: Uuid) -> Result<(), AppResponseError> {
let url = quick_note_resources_url(&self.base_url, workspace_id);
let resp = self
.http_client_with_auth(Method::POST, &url)
.await?
.json(&CreateQuickNoteParams {})
.send()
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()
}
pub async fn list_quick_notes(
&self,
workspace_id: Uuid,
search_term: Option<String>,
offset: Option<i32>,
limit: Option<i32>,
) -> Result<QuickNotes, AppResponseError> {
let url = quick_note_resources_url(&self.base_url, workspace_id);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.query(&ListQuickNotesQueryParams {
search_term,
offset,
limit,
})
.send()
.await?;
AppResponse::<QuickNotes>::from_response(resp)
.await?
.into_data()
}
pub async fn update_quick_note(
&self,
workspace_id: Uuid,
quick_note_id: Uuid,
data: serde_json::Value,
) -> Result<(), AppResponseError> {
let url = quick_note_resource_url(&self.base_url, workspace_id, quick_note_id);
let resp = self
.http_client_with_auth(Method::PUT, &url)
.await?
.json(&UpdateQuickNoteParams { data })
.send()
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()
}
pub async fn delete_quick_note(
&self,
workspace_id: Uuid,
quick_note_id: Uuid,
) -> Result<(), AppResponseError> {
let url = quick_note_resource_url(&self.base_url, workspace_id, quick_note_id);
let resp = self
.http_client_with_auth(Method::DELETE, &url)
.await?
.send()
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()
}
}

View File

@ -8,6 +8,7 @@ mod http_collab;
mod http_history;
mod http_member;
mod http_publish;
mod http_quick_note;
mod http_search;
mod http_template;
mod http_view;

View File

@ -1167,6 +1167,35 @@ pub struct WorkspaceNamespace {
pub is_original: bool,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct QuickNote {
pub id: Uuid,
pub data: serde_json::Value,
pub created_at: DateTime<Utc>,
pub last_updated_at: DateTime<Utc>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct QuickNotes {
pub quick_notes: Vec<QuickNote>,
pub has_more: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CreateQuickNoteParams {}
#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateQuickNoteParams {
pub data: serde_json::Value,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ListQuickNotesQueryParams {
pub search_term: Option<String>,
pub offset: Option<i32>,
pub limit: Option<i32>,
}
#[cfg(test)]
mod test {
use crate::dto::{CollabParams, CollabParamsV0};

View File

@ -7,6 +7,7 @@ pub mod index;
pub mod listener;
pub mod pg_row;
pub mod publish;
pub mod quick_note;
pub mod resource_usage;
pub mod template;
pub mod user;

View File

@ -5,8 +5,9 @@ use chrono::{DateTime, Utc};
use database_entity::dto::{
AFAccessLevel, AFRole, AFUserProfile, AFWebUser, AFWorkspace, AFWorkspaceInvitationStatus,
AccessRequestMinimal, AccessRequestStatus, AccessRequestWithViewId, AccessRequesterInfo,
AccountLink, GlobalComment, Reaction, Template, TemplateCategory, TemplateCategoryMinimal,
TemplateCategoryType, TemplateCreator, TemplateCreatorMinimal, TemplateGroup, TemplateMinimal,
AccountLink, GlobalComment, QuickNote, Reaction, Template, TemplateCategory,
TemplateCategoryMinimal, TemplateCategoryType, TemplateCreator, TemplateCreatorMinimal,
TemplateGroup, TemplateMinimal,
};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
@ -647,3 +648,22 @@ impl TryFrom<AFAccessRequestWithViewIdColumn> for AccessRequestWithViewId {
})
}
}
#[derive(FromRow, Serialize, Debug)]
pub struct AFQuickNoteRow {
pub quick_note_id: Uuid,
pub data: serde_json::Value,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl From<AFQuickNoteRow> for QuickNote {
fn from(value: AFQuickNoteRow) -> Self {
Self {
id: value.quick_note_id,
data: value.data,
created_at: value.created_at,
last_updated_at: value.updated_at,
}
}
}

View File

@ -0,0 +1,100 @@
use app_error::AppError;
use database_entity::dto::QuickNote;
use sqlx::{Executor, Postgres, QueryBuilder};
use uuid::Uuid;
use crate::pg_row::AFQuickNoteRow;
pub async fn insert_new_quick_note<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
workspace_id: Uuid,
uid: i64,
data: &serde_json::Value,
) -> Result<(), AppError> {
sqlx::query!(
"INSERT INTO af_quick_note (workspace_id, uid, data) VALUES ($1, $2, $3)",
workspace_id,
uid,
data
)
.execute(executor)
.await?;
Ok(())
}
pub async fn select_quick_notes_with_one_more_than_limit<
'a,
E: Executor<'a, Database = Postgres>,
>(
executor: E,
workspace_id: Uuid,
uid: i64,
search_term: Option<String>,
offset: Option<i32>,
limit: Option<i32>,
) -> Result<Vec<QuickNote>, AppError> {
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
r#"
SELECT
quick_note_id,
data,
created_at,
updated_at
FROM af_quick_note WHERE workspace_id =
"#,
);
query_builder.push_bind(workspace_id);
query_builder.push(" AND uid = ");
query_builder.push_bind(uid);
if let Some(search_term) = search_term {
query_builder.push(" AND data @? ");
let json_path_query = format!("'$.**.insert ? (@ like_regex \".*{}.*\")'", search_term);
query_builder.push(json_path_query);
}
query_builder.push(" ORDER BY created_at DESC");
if let Some(limit) = limit {
query_builder.push(" LIMIT ");
query_builder.push_bind(limit);
query_builder.push(" + 1 ");
}
if let Some(offset) = offset {
query_builder.push(" OFFSET ");
query_builder.push_bind(offset);
}
let query = query_builder.build_query_as::<AFQuickNoteRow>();
let quick_notes_with_one_more_than_limit = query
.fetch_all(executor)
.await?
.into_iter()
.map(Into::into)
.collect();
Ok(quick_notes_with_one_more_than_limit)
}
pub async fn update_quick_note_by_id<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
quick_note_id: Uuid,
data: &serde_json::Value,
) -> Result<(), AppError> {
sqlx::query!(
"UPDATE af_quick_note SET data = $1, updated_at = NOW() WHERE quick_note_id = $2",
data,
quick_note_id
)
.execute(executor)
.await?;
Ok(())
}
pub async fn delete_quick_note_by_id<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
quick_note_id: Uuid,
) -> Result<(), AppError> {
sqlx::query!(
"DELETE FROM af_quick_note WHERE quick_note_id = $1",
quick_note_id
)
.execute(executor)
.await?;
Ok(())
}

View File

@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS af_quick_note (
quick_note_id UUID NOT NULL DEFAULT gen_random_uuid (),
workspace_id UUID NOT NULL,
uid BIGINT NOT NULL REFERENCES af_user (uid) ON DELETE CASCADE,
updated_at TIMESTAMP
WITH
TIME ZONE DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP
WITH
TIME ZONE DEFAULT CURRENT_TIMESTAMP,
data JSONB NOT NULL,
PRIMARY KEY (quick_note_id)
);
CREATE INDEX IF NOT EXISTS idx_workspace_id_on_af_quick_note ON af_quick_note (workspace_id);
CREATE INDEX IF NOT EXISTS idx_uid_on_af_quick_note ON af_quick_note (uid);

View File

@ -17,6 +17,9 @@ use crate::biz::workspace::page_view::{
update_page, update_page_collab_data, update_space,
};
use crate::biz::workspace::publish::get_workspace_default_publish_view_info_meta;
use crate::biz::workspace::quick_note::{
create_quick_note, delete_quick_note, list_quick_notes, update_quick_note,
};
use crate::domain::compression::{
blocking_decompress, decompress, CompressionType, X_COMPRESSION_TYPE,
};
@ -300,6 +303,16 @@ pub fn workspace_scope() -> Scope {
web::resource("/{workspace_id}/database/{database_id}/row/detail")
.route(web::get().to(list_database_row_details_handler)),
)
.service(
web::resource("/{workspace_id}/quick-note")
.route(web::get().to(list_quick_notes_handler))
.route(web::post().to(post_quick_note_handler)),
)
.service(
web::resource("/{workspace_id}/quick-note/{quick_note_id}")
.route(web::put().to(update_quick_note_handler))
.route(web::delete().to(delete_quick_note_handler)),
)
}
pub fn collab_scope() -> Scope {
@ -2430,3 +2443,78 @@ async fn collab_full_sync_handler(
Err(err) => Ok(err.error_response()),
}
}
async fn post_quick_note_handler(
user_uuid: UserUuid,
workspace_id: web::Path<Uuid>,
state: Data<AppState>,
) -> Result<JsonAppResponse<()>> {
let workspace_id = workspace_id.into_inner();
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
state
.workspace_access_control
.enforce_role(&uid, &workspace_id.to_string(), AFRole::Member)
.await?;
create_quick_note(&state.pg_pool, uid, workspace_id).await?;
Ok(Json(AppResponse::Ok()))
}
async fn list_quick_notes_handler(
user_uuid: UserUuid,
workspace_id: web::Path<Uuid>,
state: Data<AppState>,
query: web::Query<ListQuickNotesQueryParams>,
) -> Result<JsonAppResponse<QuickNotes>> {
let workspace_id = workspace_id.into_inner();
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
state
.workspace_access_control
.enforce_role(&uid, &workspace_id.to_string(), AFRole::Member)
.await?;
let ListQuickNotesQueryParams {
search_term,
offset,
limit,
} = query.into_inner();
let quick_notes = list_quick_notes(
&state.pg_pool,
uid,
workspace_id,
search_term,
offset,
limit,
)
.await?;
Ok(Json(AppResponse::Ok().with_data(quick_notes)))
}
async fn update_quick_note_handler(
user_uuid: UserUuid,
path_param: web::Path<(Uuid, Uuid)>,
state: Data<AppState>,
data: Json<UpdateQuickNoteParams>,
) -> Result<JsonAppResponse<()>> {
let (workspace_id, quick_note_id) = path_param.into_inner();
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
state
.workspace_access_control
.enforce_role(&uid, &workspace_id.to_string(), AFRole::Member)
.await?;
update_quick_note(&state.pg_pool, quick_note_id, &data.data).await?;
Ok(Json(AppResponse::Ok()))
}
async fn delete_quick_note_handler(
user_uuid: UserUuid,
path_param: web::Path<(Uuid, Uuid)>,
state: Data<AppState>,
) -> Result<JsonAppResponse<()>> {
let (workspace_id, quick_note_id) = path_param.into_inner();
let uid = state.user_cache.get_user_uid(&user_uuid).await?;
state
.workspace_access_control
.enforce_role(&uid, &workspace_id.to_string(), AFRole::Member)
.await?;
delete_quick_note(&state.pg_pool, quick_note_id).await?;
Ok(Json(AppResponse::Ok()))
}

View File

@ -2,3 +2,4 @@ pub mod ops;
pub mod page_view;
pub mod publish;
pub mod publish_dup;
pub mod quick_note;

View File

@ -0,0 +1,64 @@
use app_error::AppError;
use database::quick_note::{
delete_quick_note_by_id, insert_new_quick_note, select_quick_notes_with_one_more_than_limit,
update_quick_note_by_id,
};
use serde_json::json;
use sqlx::PgPool;
use uuid::Uuid;
use database_entity::dto::QuickNotes;
pub async fn create_quick_note(
pg_pool: &PgPool,
uid: i64,
workspace_id: Uuid,
) -> Result<(), AppError> {
let default_data = json!([]);
insert_new_quick_note(pg_pool, workspace_id, uid, &default_data).await
}
pub async fn update_quick_note(
pg_pool: &PgPool,
quick_note_id: Uuid,
data: &serde_json::Value,
) -> Result<(), AppError> {
update_quick_note_by_id(pg_pool, quick_note_id, data).await
}
pub async fn delete_quick_note(pg_pool: &PgPool, quick_note_id: Uuid) -> Result<(), AppError> {
delete_quick_note_by_id(pg_pool, quick_note_id).await
}
pub async fn list_quick_notes(
pg_pool: &PgPool,
uid: i64,
workspace_id: Uuid,
search_term: Option<String>,
offset: Option<i32>,
limit: Option<i32>,
) -> Result<QuickNotes, AppError> {
let mut quick_notes_with_one_more_than_limit = select_quick_notes_with_one_more_than_limit(
pg_pool,
workspace_id,
uid,
search_term,
offset,
limit,
)
.await?;
let has_more = if let Some(limit) = limit {
quick_notes_with_one_more_than_limit.len() as i32 > limit
} else {
false
};
if let Some(limit) = limit {
quick_notes_with_one_more_than_limit.truncate(limit as usize);
}
let quick_notes = quick_notes_with_one_more_than_limit;
Ok(QuickNotes {
quick_notes,
has_more,
})
}

View File

@ -7,6 +7,7 @@ mod member_crud;
mod page_view;
mod publish;
mod published_data;
mod quick_note;
mod template;
mod workspace_crud;
mod workspace_folder;

View File

@ -0,0 +1,137 @@
use std::time::Duration;
use client_api_test::TestClient;
use serde_json::json;
use tokio::time;
use uuid::Uuid;
#[tokio::test]
async fn quick_note_crud_test() {
let client = TestClient::new_user_without_ws_conn().await;
let workspace_id = client.workspace_id().await;
let workspace_uuid = Uuid::parse_str(&workspace_id).unwrap();
for _ in 0..2 {
client
.api_client
.create_quick_note(workspace_uuid)
.await
.expect("create quick note");
// To ensure that the creation time is different
time::sleep(Duration::from_millis(1)).await;
}
let quick_notes = client
.api_client
.list_quick_notes(workspace_uuid, None, None, None)
.await
.expect("list quick notes");
assert_eq!(quick_notes.quick_notes.len(), 2);
assert!(!quick_notes.has_more);
let mut notes_sorted_by_created_at_asc = quick_notes.quick_notes.clone();
notes_sorted_by_created_at_asc.sort_by(|a, b| a.created_at.cmp(&b.created_at));
let quick_note_id_1 = notes_sorted_by_created_at_asc[0].id;
let quick_note_id_2 = notes_sorted_by_created_at_asc[1].id;
let data_1 = json!([
{
"type": "paragraph",
"delta": {
"insert": "orange",
"attributes": {
"bold": true
},
},
},
{
"type": "heading",
"data": {
"level": 1
},
"delta": {
"insert": "apple",
"attributes": {
"bold": true
},
},
},
]);
let data_2 = json!([
{
"type": "paragraph",
"delta": {
"insert": "banana",
"attributes": {
"bold": true
},
},
},
{
"type": "heading",
"data": {
"level": 1
},
"delta": {
"insert": "melon",
"attributes": {
"bold": true
},
},
},
]);
client
.api_client
.update_quick_note(workspace_uuid, quick_note_id_1, data_1)
.await
.expect("update quick note");
client
.api_client
.update_quick_note(workspace_uuid, quick_note_id_2, data_2)
.await
.expect("update quick note");
let quick_notes = client
.api_client
.list_quick_notes(workspace_uuid, None, None, None)
.await
.expect("list quick notes");
assert_eq!(quick_notes.quick_notes.len(), 2);
let quick_notes_with_offset_and_limit = client
.api_client
.list_quick_notes(workspace_uuid, None, Some(1), Some(1))
.await
.expect("list quick notes with offset and limit");
assert_eq!(quick_notes_with_offset_and_limit.quick_notes.len(), 1);
assert!(!quick_notes_with_offset_and_limit.has_more);
assert_eq!(
quick_notes_with_offset_and_limit.quick_notes[0].id,
quick_note_id_1
);
let quick_notes_with_offset_and_limit = client
.api_client
.list_quick_notes(workspace_uuid, None, Some(0), Some(1))
.await
.expect("list quick notes with offset and limit");
assert_eq!(quick_notes_with_offset_and_limit.quick_notes.len(), 1);
assert!(quick_notes_with_offset_and_limit.has_more);
assert_eq!(
quick_notes_with_offset_and_limit.quick_notes[0].id,
quick_note_id_2
);
let filtered_quick_notes = client
.api_client
.list_quick_notes(workspace_uuid, Some("pple".to_string()), None, None)
.await
.expect("list quick notes with filter");
assert_eq!(filtered_quick_notes.quick_notes.len(), 1);
assert_eq!(filtered_quick_notes.quick_notes[0].id, quick_note_id_1);
client
.api_client
.delete_quick_note(workspace_uuid, quick_note_id_1)
.await
.expect("delete quick note");
let quick_notes = client
.api_client
.list_quick_notes(workspace_uuid, None, None, None)
.await
.expect("list quick notes");
assert_eq!(quick_notes.quick_notes.len(), 1);
assert_eq!(quick_notes.quick_notes[0].id, quick_note_id_2);
}