BLOG-133 Unique label name and semantic ID (#135)
All checks were successful
Frontend CI / build (push) Successful in 1m23s
All checks were successful
Frontend CI / build (push) Successful in 1m23s
### Description - It returns status `409 CONFLICT` if there is already a label with same name or a post with same semantic ID. ### Package Changes _No response_ ### Screenshots _No response_ ### Reference Resolves #133. ### Checklist - [x] A milestone is set - [x] The related issuse has been linked to this branch Reviewed-on: #135 Co-authored-by: SquidSpirit <squid@squidspirit.com> Co-committed-by: SquidSpirit <squid@squidspirit.com>
This commit is contained in:
parent
565df7aace
commit
1ae104cd56
22
backend/.sqlx/query-4e022312b61c557d23f4541961f07b7afa8bf1f62f4a8e248d6520864b126a6a.json
generated
Normal file
22
backend/.sqlx/query-4e022312b61c557d23f4541961f07b7afa8bf1f62f4a8e248d6520864b126a6a.json
generated
Normal file
@ -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"
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"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": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@ -11,6 +11,7 @@
|
|||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Left": [
|
"Left": [
|
||||||
|
"Varchar",
|
||||||
"Text",
|
"Text",
|
||||||
"Text",
|
"Text",
|
||||||
"Text",
|
"Text",
|
||||||
@ -22,5 +23,5 @@
|
|||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "f0c2c0fe0a30790e88449da79c859d4e3829b9b2a6a496c9a429a05fbdb2e30a"
|
"hash": "82c4c8d03298009e776d42ae6069182cc318e0b1d8497a675cf4449adcaf1927"
|
||||||
}
|
}
|
20
backend/.sqlx/query-9e486528becb873ace62656c78805034ddca950db7306ee04257d6b589b5a0d9.json
generated
Normal file
20
backend/.sqlx/query-9e486528becb873ace62656c78805034ddca950db7306ee04257d6b589b5a0d9.json
generated
Normal file
@ -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"
|
||||||
|
}
|
@ -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"
|
|
||||||
}
|
|
@ -5,6 +5,8 @@ pub enum PostError {
|
|||||||
NotFound,
|
NotFound,
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
InvalidSemanticId,
|
InvalidSemanticId,
|
||||||
|
DuplicatedSemanticId,
|
||||||
|
DuplicatedLabelName,
|
||||||
Unexpected(anyhow::Error),
|
Unexpected(anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,6 +19,8 @@ impl Display for PostError {
|
|||||||
f,
|
f,
|
||||||
"Semantic ID shouldn't be numeric and must conform to `^[0-9a-zA-Z_\\-]+$`"
|
"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),
|
PostError::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,14 @@ impl LabelDbService for LabelDbServiceImpl {
|
|||||||
)
|
)
|
||||||
.fetch_one(&self.db_pool)
|
.fetch_one(&self.db_pool)
|
||||||
.await
|
.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)
|
Ok(id)
|
||||||
}
|
}
|
||||||
@ -50,7 +57,14 @@ impl LabelDbService for LabelDbServiceImpl {
|
|||||||
)
|
)
|
||||||
.execute(&self.db_pool)
|
.execute(&self.db_pool)
|
||||||
.await
|
.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();
|
.rows_affected();
|
||||||
|
|
||||||
if affected_rows == 0 {
|
if affected_rows == 0 {
|
||||||
|
@ -211,7 +211,14 @@ impl PostDbService for PostDbServiceImpl {
|
|||||||
)
|
)
|
||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await
|
.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() {
|
for (order, &label_id) in label_ids.iter().enumerate() {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
@ -266,7 +273,14 @@ impl PostDbService for PostDbServiceImpl {
|
|||||||
)
|
)
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await
|
.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();
|
.rows_affected();
|
||||||
|
|
||||||
if affected_rows == 0 {
|
if affected_rows == 0 {
|
||||||
|
@ -34,7 +34,10 @@ pub async fn create_label_handler(
|
|||||||
Ok(label) => HttpResponse::Created().json(label),
|
Ok(label) => HttpResponse::Created().json(label),
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||||
PostError::NotFound | PostError::InvalidSemanticId => {
|
PostError::DuplicatedLabelName => HttpResponse::Conflict().finish(),
|
||||||
|
PostError::NotFound
|
||||||
|
| PostError::InvalidSemanticId
|
||||||
|
| PostError::DuplicatedSemanticId => {
|
||||||
capture_anyhow(&anyhow!(e));
|
capture_anyhow(&anyhow!(e));
|
||||||
HttpResponse::InternalServerError().finish()
|
HttpResponse::InternalServerError().finish()
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,8 @@ pub async fn create_post_handler(
|
|||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||||
PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(),
|
PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(),
|
||||||
PostError::NotFound => {
|
PostError::DuplicatedSemanticId => HttpResponse::Conflict().finish(),
|
||||||
|
PostError::NotFound | PostError::DuplicatedLabelName => {
|
||||||
capture_anyhow(&anyhow!(e));
|
capture_anyhow(&anyhow!(e));
|
||||||
HttpResponse::InternalServerError().finish()
|
HttpResponse::InternalServerError().finish()
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,11 @@ pub async fn get_all_labels_handler(
|
|||||||
match result {
|
match result {
|
||||||
Ok(labels) => HttpResponse::Ok().json(labels),
|
Ok(labels) => HttpResponse::Ok().json(labels),
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
PostError::NotFound | PostError::Unauthorized | PostError::InvalidSemanticId => {
|
PostError::NotFound
|
||||||
|
| PostError::Unauthorized
|
||||||
|
| PostError::InvalidSemanticId
|
||||||
|
| PostError::DuplicatedSemanticId
|
||||||
|
| PostError::DuplicatedLabelName => {
|
||||||
capture_anyhow(&anyhow!(e));
|
capture_anyhow(&anyhow!(e));
|
||||||
HttpResponse::InternalServerError().finish()
|
HttpResponse::InternalServerError().finish()
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,11 @@ pub async fn get_all_post_info_handler(
|
|||||||
match result {
|
match result {
|
||||||
Ok(post_info_list) => HttpResponse::Ok().json(post_info_list),
|
Ok(post_info_list) => HttpResponse::Ok().json(post_info_list),
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
PostError::NotFound | PostError::Unauthorized | PostError::InvalidSemanticId => {
|
PostError::NotFound
|
||||||
|
| PostError::Unauthorized
|
||||||
|
| PostError::InvalidSemanticId
|
||||||
|
| PostError::DuplicatedSemanticId
|
||||||
|
| PostError::DuplicatedLabelName => {
|
||||||
capture_anyhow(&anyhow!(e));
|
capture_anyhow(&anyhow!(e));
|
||||||
HttpResponse::InternalServerError().finish()
|
HttpResponse::InternalServerError().finish()
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,9 @@ pub async fn get_post_by_id_handler(
|
|||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
PostError::NotFound => HttpResponse::NotFound().finish(),
|
PostError::NotFound => HttpResponse::NotFound().finish(),
|
||||||
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||||
PostError::InvalidSemanticId => {
|
PostError::InvalidSemanticId
|
||||||
|
| PostError::DuplicatedSemanticId
|
||||||
|
| PostError::DuplicatedLabelName => {
|
||||||
capture_anyhow(&anyhow!(e));
|
capture_anyhow(&anyhow!(e));
|
||||||
HttpResponse::InternalServerError().finish()
|
HttpResponse::InternalServerError().finish()
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,8 @@ pub async fn update_label_handler(
|
|||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
PostError::NotFound => HttpResponse::NotFound().finish(),
|
PostError::NotFound => HttpResponse::NotFound().finish(),
|
||||||
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||||
PostError::InvalidSemanticId => {
|
PostError::DuplicatedLabelName => HttpResponse::Conflict().finish(),
|
||||||
|
PostError::InvalidSemanticId | PostError::DuplicatedSemanticId => {
|
||||||
capture_anyhow(&anyhow!(e));
|
capture_anyhow(&anyhow!(e));
|
||||||
HttpResponse::InternalServerError().finish()
|
HttpResponse::InternalServerError().finish()
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use actix_web::{HttpResponse, Responder, web};
|
use actix_web::{HttpResponse, Responder, web};
|
||||||
|
use anyhow::anyhow;
|
||||||
use auth::framework::web::auth_middleware::UserId;
|
use auth::framework::web::auth_middleware::UserId;
|
||||||
use sentry::integrations::anyhow::capture_anyhow;
|
use sentry::integrations::anyhow::capture_anyhow;
|
||||||
|
|
||||||
@ -39,7 +40,12 @@ pub async fn update_post_handler(
|
|||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
PostError::NotFound => HttpResponse::NotFound().finish(),
|
PostError::NotFound => HttpResponse::NotFound().finish(),
|
||||||
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
PostError::Unauthorized => HttpResponse::Unauthorized().finish(),
|
||||||
|
PostError::DuplicatedSemanticId => HttpResponse::Conflict().finish(),
|
||||||
PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(),
|
PostError::InvalidSemanticId => HttpResponse::BadRequest().finish(),
|
||||||
|
PostError::DuplicatedLabelName => {
|
||||||
|
capture_anyhow(&anyhow!(e));
|
||||||
|
HttpResponse::InternalServerError().finish()
|
||||||
|
}
|
||||||
PostError::Unexpected(e) => {
|
PostError::Unexpected(e) => {
|
||||||
capture_anyhow(&e);
|
capture_anyhow(&e);
|
||||||
HttpResponse::InternalServerError().finish()
|
HttpResponse::InternalServerError().finish()
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- Remove unique index from label name column
|
||||||
|
DROP INDEX IF EXISTS "idx_label_name";
|
@ -0,0 +1,2 @@
|
|||||||
|
-- Add unique index to label name column
|
||||||
|
CREATE UNIQUE INDEX "idx_label_name" ON "label" ("name");
|
Loading…
x
Reference in New Issue
Block a user