diff --git a/backend/.sqlx/query-4e022312b61c557d23f4541961f07b7afa8bf1f62f4a8e248d6520864b126a6a.json b/backend/.sqlx/query-4e022312b61c557d23f4541961f07b7afa8bf1f62f4a8e248d6520864b126a6a.json new file mode 100644 index 0000000..30d2f58 --- /dev/null +++ b/backend/.sqlx/query-4e022312b61c557d23f4541961f07b7afa8bf1f62f4a8e248d6520864b126a6a.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id\n FROM post\n WHERE semantic_id = $1 AND deleted_time IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "4e022312b61c557d23f4541961f07b7afa8bf1f62f4a8e248d6520864b126a6a" +} diff --git a/backend/.sqlx/query-f0c2c0fe0a30790e88449da79c859d4e3829b9b2a6a496c9a429a05fbdb2e30a.json b/backend/.sqlx/query-82c4c8d03298009e776d42ae6069182cc318e0b1d8497a675cf4449adcaf1927.json similarity index 50% rename from backend/.sqlx/query-f0c2c0fe0a30790e88449da79c859d4e3829b9b2a6a496c9a429a05fbdb2e30a.json rename to backend/.sqlx/query-82c4c8d03298009e776d42ae6069182cc318e0b1d8497a675cf4449adcaf1927.json index 7d34ac9..4680dea 100644 --- a/backend/.sqlx/query-f0c2c0fe0a30790e88449da79c859d4e3829b9b2a6a496c9a429a05fbdb2e30a.json +++ b/backend/.sqlx/query-82c4c8d03298009e776d42ae6069182cc318e0b1d8497a675cf4449adcaf1927.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO post (\n title, description, preview_image_url, content, published_time\n ) VALUES ($1, $2, $3, $4, $5)\n RETURNING id\n ", + "query": "\n INSERT INTO post (\n semantic_id, title, description, preview_image_url, content, published_time\n ) VALUES ($1, $2, $3, $4, $5, $6)\n RETURNING id\n ", "describe": { "columns": [ { @@ -11,6 +11,7 @@ ], "parameters": { "Left": [ + "Varchar", "Text", "Text", "Text", @@ -22,5 +23,5 @@ false ] }, - "hash": "f0c2c0fe0a30790e88449da79c859d4e3829b9b2a6a496c9a429a05fbdb2e30a" + "hash": "82c4c8d03298009e776d42ae6069182cc318e0b1d8497a675cf4449adcaf1927" } diff --git a/backend/.sqlx/query-9e486528becb873ace62656c78805034ddca950db7306ee04257d6b589b5a0d9.json b/backend/.sqlx/query-9e486528becb873ace62656c78805034ddca950db7306ee04257d6b589b5a0d9.json new file mode 100644 index 0000000..bcb449f --- /dev/null +++ b/backend/.sqlx/query-9e486528becb873ace62656c78805034ddca950db7306ee04257d6b589b5a0d9.json @@ -0,0 +1,20 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE post\n SET \n semantic_id = $1,\n title = $2, \n description = $3, \n preview_image_url = $4, \n content = $5, \n published_time = $6\n WHERE id = $7\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Text", + "Text", + "Text", + "Timestamp", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "9e486528becb873ace62656c78805034ddca950db7306ee04257d6b589b5a0d9" +} diff --git a/backend/.sqlx/query-d0867ba2857fedcdc9a754d0394c4f040d559118d0b9f8b6f4dcd6e6fde5d381.json b/backend/.sqlx/query-d0867ba2857fedcdc9a754d0394c4f040d559118d0b9f8b6f4dcd6e6fde5d381.json deleted file mode 100644 index f194863..0000000 --- a/backend/.sqlx/query-d0867ba2857fedcdc9a754d0394c4f040d559118d0b9f8b6f4dcd6e6fde5d381.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE post\n SET \n title = $1, \n description = $2, \n preview_image_url = $3, \n content = $4, \n published_time = $5\n WHERE id = $6\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Text", - "Text", - "Timestamp", - "Int4" - ] - }, - "nullable": [] - }, - "hash": "d0867ba2857fedcdc9a754d0394c4f040d559118d0b9f8b6f4dcd6e6fde5d381" -} diff --git a/backend/feature/post/src/application/error/post_error.rs b/backend/feature/post/src/application/error/post_error.rs index 4d13e4e..275efcc 100644 --- a/backend/feature/post/src/application/error/post_error.rs +++ b/backend/feature/post/src/application/error/post_error.rs @@ -5,6 +5,8 @@ pub enum PostError { NotFound, Unauthorized, InvalidSemanticId, + DuplicatedSemanticId, + DuplicatedLabelName, Unexpected(anyhow::Error), } @@ -17,6 +19,8 @@ impl Display for PostError { f, "Semantic ID shouldn't be numeric and must conform to `^[0-9a-zA-Z_\\-]+$`" ), + PostError::DuplicatedSemanticId => write!(f, "Semantic ID already exists"), + PostError::DuplicatedLabelName => write!(f, "Label name already exists"), PostError::Unexpected(e) => write!(f, "Unexpected error: {}", e), } } diff --git a/backend/feature/post/src/framework/db/label_db_service_impl.rs b/backend/feature/post/src/framework/db/label_db_service_impl.rs index f189fd1..9a0d775 100644 --- a/backend/feature/post/src/framework/db/label_db_service_impl.rs +++ b/backend/feature/post/src/framework/db/label_db_service_impl.rs @@ -32,7 +32,14 @@ impl LabelDbService for LabelDbServiceImpl { ) .fetch_one(&self.db_pool) .await - .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?; + .map_err(|e| { + if let sqlx::Error::Database(db_err) = &e { + if db_err.constraint() == Some("idx_label_name") { + return PostError::DuplicatedLabelName; + } + } + PostError::Unexpected(DatabaseError(e).into()) + })?; Ok(id) } @@ -50,7 +57,14 @@ impl LabelDbService for LabelDbServiceImpl { ) .execute(&self.db_pool) .await - .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))? + .map_err(|e| { + if let sqlx::Error::Database(db_err) = &e { + if db_err.constraint() == Some("idx_label_name") { + return PostError::DuplicatedLabelName; + } + } + PostError::Unexpected(DatabaseError(e).into()) + })? .rows_affected(); if affected_rows == 0 { diff --git a/backend/feature/post/src/framework/db/post_db_service_impl.rs b/backend/feature/post/src/framework/db/post_db_service_impl.rs index fa46e0e..ac68214 100644 --- a/backend/feature/post/src/framework/db/post_db_service_impl.rs +++ b/backend/feature/post/src/framework/db/post_db_service_impl.rs @@ -211,7 +211,14 @@ impl PostDbService for PostDbServiceImpl { ) .fetch_one(&mut *tx) .await - .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))?; + .map_err(|e| { + if let sqlx::Error::Database(db_err) = &e { + if db_err.constraint() == Some("idx_post_semantic_id") { + return PostError::DuplicatedSemanticId; + } + } + PostError::Unexpected(DatabaseError(e).into()) + })?; for (order, &label_id) in label_ids.iter().enumerate() { sqlx::query!( @@ -266,7 +273,14 @@ impl PostDbService for PostDbServiceImpl { ) .execute(&mut *tx) .await - .map_err(|e| PostError::Unexpected(DatabaseError(e).into()))? + .map_err(|e| { + if let sqlx::Error::Database(db_err) = &e { + if db_err.constraint() == Some("idx_post_semantic_id") { + return PostError::DuplicatedSemanticId; + } + } + PostError::Unexpected(DatabaseError(e).into()) + })? .rows_affected(); if affected_rows == 0 { diff --git a/backend/feature/post/src/framework/web/create_label_handler.rs b/backend/feature/post/src/framework/web/create_label_handler.rs index a06f2c2..ca1fda6 100644 --- a/backend/feature/post/src/framework/web/create_label_handler.rs +++ b/backend/feature/post/src/framework/web/create_label_handler.rs @@ -34,7 +34,10 @@ pub async fn create_label_handler( Ok(label) => HttpResponse::Created().json(label), Err(e) => match e { PostError::Unauthorized => HttpResponse::Unauthorized().finish(), - PostError::NotFound | PostError::InvalidSemanticId => { + PostError::DuplicatedLabelName => HttpResponse::Conflict().finish(), + PostError::NotFound + | PostError::InvalidSemanticId + | PostError::DuplicatedSemanticId => { capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } diff --git a/backend/feature/post/src/framework/web/create_post_handler.rs b/backend/feature/post/src/framework/web/create_post_handler.rs index f8ee1b3..e884022 100644 --- a/backend/feature/post/src/framework/web/create_post_handler.rs +++ b/backend/feature/post/src/framework/web/create_post_handler.rs @@ -37,7 +37,8 @@ pub async fn create_post_handler( Err(e) => match e { PostError::Unauthorized => HttpResponse::Unauthorized().finish(), PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(), - PostError::NotFound => { + PostError::DuplicatedSemanticId => HttpResponse::Conflict().finish(), + PostError::NotFound | PostError::DuplicatedLabelName => { capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } diff --git a/backend/feature/post/src/framework/web/get_all_labels_handler.rs b/backend/feature/post/src/framework/web/get_all_labels_handler.rs index 2aac104..7ad0cda 100644 --- a/backend/feature/post/src/framework/web/get_all_labels_handler.rs +++ b/backend/feature/post/src/framework/web/get_all_labels_handler.rs @@ -24,7 +24,11 @@ pub async fn get_all_labels_handler( match result { Ok(labels) => HttpResponse::Ok().json(labels), Err(e) => match e { - PostError::NotFound | PostError::Unauthorized | PostError::InvalidSemanticId => { + PostError::NotFound + | PostError::Unauthorized + | PostError::InvalidSemanticId + | PostError::DuplicatedSemanticId + | PostError::DuplicatedLabelName => { capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } diff --git a/backend/feature/post/src/framework/web/get_all_post_info_handler.rs b/backend/feature/post/src/framework/web/get_all_post_info_handler.rs index a18e412..f3f084d 100644 --- a/backend/feature/post/src/framework/web/get_all_post_info_handler.rs +++ b/backend/feature/post/src/framework/web/get_all_post_info_handler.rs @@ -36,7 +36,11 @@ pub async fn get_all_post_info_handler( match result { Ok(post_info_list) => HttpResponse::Ok().json(post_info_list), Err(e) => match e { - PostError::NotFound | PostError::Unauthorized | PostError::InvalidSemanticId => { + PostError::NotFound + | PostError::Unauthorized + | PostError::InvalidSemanticId + | PostError::DuplicatedSemanticId + | PostError::DuplicatedLabelName => { capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } diff --git a/backend/feature/post/src/framework/web/get_post_by_id_handler.rs b/backend/feature/post/src/framework/web/get_post_by_id_handler.rs index 891265d..9705eb2 100644 --- a/backend/feature/post/src/framework/web/get_post_by_id_handler.rs +++ b/backend/feature/post/src/framework/web/get_post_by_id_handler.rs @@ -34,7 +34,9 @@ pub async fn get_post_by_id_handler( Err(e) => match e { PostError::NotFound => HttpResponse::NotFound().finish(), PostError::Unauthorized => HttpResponse::Unauthorized().finish(), - PostError::InvalidSemanticId => { + PostError::InvalidSemanticId + | PostError::DuplicatedSemanticId + | PostError::DuplicatedLabelName => { capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } diff --git a/backend/feature/post/src/framework/web/update_label_handler.rs b/backend/feature/post/src/framework/web/update_label_handler.rs index cb06770..f72352f 100644 --- a/backend/feature/post/src/framework/web/update_label_handler.rs +++ b/backend/feature/post/src/framework/web/update_label_handler.rs @@ -40,7 +40,8 @@ pub async fn update_label_handler( Err(e) => match e { PostError::NotFound => HttpResponse::NotFound().finish(), PostError::Unauthorized => HttpResponse::Unauthorized().finish(), - PostError::InvalidSemanticId => { + PostError::DuplicatedLabelName => HttpResponse::Conflict().finish(), + PostError::InvalidSemanticId | PostError::DuplicatedSemanticId => { capture_anyhow(&anyhow!(e)); HttpResponse::InternalServerError().finish() } diff --git a/backend/feature/post/src/framework/web/update_post_handler.rs b/backend/feature/post/src/framework/web/update_post_handler.rs index 40cce3e..ef256b3 100644 --- a/backend/feature/post/src/framework/web/update_post_handler.rs +++ b/backend/feature/post/src/framework/web/update_post_handler.rs @@ -1,4 +1,5 @@ use actix_web::{HttpResponse, Responder, web}; +use anyhow::anyhow; use auth::framework::web::auth_middleware::UserId; use sentry::integrations::anyhow::capture_anyhow; @@ -39,7 +40,12 @@ pub async fn update_post_handler( Err(e) => match e { PostError::NotFound => HttpResponse::NotFound().finish(), PostError::Unauthorized => HttpResponse::Unauthorized().finish(), + PostError::DuplicatedSemanticId => HttpResponse::Conflict().finish(), PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(), + PostError::DuplicatedLabelName => { + capture_anyhow(&anyhow!(e)); + HttpResponse::InternalServerError().finish() + } PostError::Unexpected(e) => { capture_anyhow(&e); HttpResponse::InternalServerError().finish() diff --git a/backend/migrations/20251012123809_add_unique_index_to_label_name.down.sql b/backend/migrations/20251012123809_add_unique_index_to_label_name.down.sql new file mode 100644 index 0000000..70eca82 --- /dev/null +++ b/backend/migrations/20251012123809_add_unique_index_to_label_name.down.sql @@ -0,0 +1,2 @@ +-- Remove unique index from label name column +DROP INDEX IF EXISTS "idx_label_name"; diff --git a/backend/migrations/20251012123809_add_unique_index_to_label_name.sql b/backend/migrations/20251012123809_add_unique_index_to_label_name.sql new file mode 100644 index 0000000..4d561ea --- /dev/null +++ b/backend/migrations/20251012123809_add_unique_index_to_label_name.sql @@ -0,0 +1,2 @@ +-- Add unique index to label name column +CREATE UNIQUE INDEX "idx_label_name" ON "label" ("name");