mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 04:26:43 -06:00
feat: add tag deletion endpoint with CLI support
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "DELETE FROM tags WHERE id = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "dd0d0e3fd03f130aab947d13580796eee9a786e2ca01d339fd0e8356f8ad3824"
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ pub async fn run(
|
|||||||
icon,
|
icon,
|
||||||
color,
|
color,
|
||||||
} => update(client, &slug, name, new_slug, icon, color, json).await,
|
} => update(client, &slug, name, new_slug, icon, color, json).await,
|
||||||
|
TagsCommand::Delete { reference } => delete(client, &reference, json).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,3 +172,24 @@ async fn update(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a tag
|
||||||
|
async fn delete(
|
||||||
|
client: ApiClient,
|
||||||
|
reference: &str,
|
||||||
|
json: bool,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let response = client
|
||||||
|
.delete_auth(&format!("/api/tags/{}", reference))
|
||||||
|
.await?;
|
||||||
|
let response = check_response(response).await?;
|
||||||
|
let deleted: ApiTag = response.json().await?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&deleted)?);
|
||||||
|
} else {
|
||||||
|
output::success(&format!("Deleted tag: {}", deleted.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -229,6 +229,13 @@ pub enum TagsCommand {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
color: Option<String>,
|
color: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Delete a tag
|
||||||
|
Delete {
|
||||||
|
/// Tag slug or UUID
|
||||||
|
#[arg(name = "ref")]
|
||||||
|
reference: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
|
|||||||
@@ -120,6 +120,13 @@ pub async fn get_tag_by_ref(pool: &PgPool, ref_str: &str) -> Result<Option<DbTag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_tag(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query!("DELETE FROM tags WHERE id = $1", id)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_all_tags_with_counts(pool: &PgPool) -> Result<Vec<(DbTag, i32)>, sqlx::Error> {
|
pub async fn get_all_tags_with_counts(pool: &PgPool) -> Result<Vec<(DbTag, i32)>, sqlx::Error> {
|
||||||
let rows = sqlx::query!(
|
let rows = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
|
|||||||
@@ -252,6 +252,66 @@ pub async fn update_tag_handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a tag (requires authentication)
|
||||||
|
pub async fn delete_tag_handler(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
axum::extract::Path(ref_str): axum::extract::Path<String>,
|
||||||
|
jar: axum_extra::extract::CookieJar,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if auth::check_session(&state, &jar).is_none() {
|
||||||
|
return auth::require_auth_response().into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch tag before deletion to return it
|
||||||
|
let tag = match db::get_tag_by_ref(&state.pool, &ref_str).await {
|
||||||
|
Ok(Some(tag)) => tag,
|
||||||
|
Ok(None) => {
|
||||||
|
return (
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": "Not found",
|
||||||
|
"message": "Tag not found"
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(error = %err, "Failed to fetch tag before deletion");
|
||||||
|
return (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": "Internal server error",
|
||||||
|
"message": "Failed to delete tag"
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Delete tag (CASCADE handles project_tags and tag_cooccurrence)
|
||||||
|
match db::delete_tag(&state.pool, tag.id).await {
|
||||||
|
Ok(()) => {
|
||||||
|
tracing::info!(tag_id = %tag.id, tag_name = %tag.name, "Tag deleted");
|
||||||
|
|
||||||
|
// Invalidate cached pages - tags appear on project pages
|
||||||
|
state.isr_cache.invalidate("/").await;
|
||||||
|
|
||||||
|
Json(tag.to_api_tag()).into_response()
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!(error = %err, "Failed to delete tag");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": "Internal server error",
|
||||||
|
"message": "Failed to delete tag"
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get related tags by cooccurrence
|
/// Get related tags by cooccurrence
|
||||||
pub async fn get_related_tags_handler(
|
pub async fn get_related_tags_handler(
|
||||||
State(state): State<Arc<AppState>>,
|
State(state): State<Arc<AppState>>,
|
||||||
|
|||||||
+3
-1
@@ -65,7 +65,9 @@ pub fn api_routes() -> Router<Arc<AppState>> {
|
|||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/tags/{ref}",
|
"/tags/{ref}",
|
||||||
get(handlers::get_tag_handler).put(handlers::update_tag_handler),
|
get(handlers::get_tag_handler)
|
||||||
|
.put(handlers::update_tag_handler)
|
||||||
|
.delete(handlers::delete_tag_handler),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/tags/{ref}/related",
|
"/tags/{ref}/related",
|
||||||
|
|||||||
Reference in New Issue
Block a user