diff --git a/.sqlx/query-dd0d0e3fd03f130aab947d13580796eee9a786e2ca01d339fd0e8356f8ad3824.json b/.sqlx/query-dd0d0e3fd03f130aab947d13580796eee9a786e2ca01d339fd0e8356f8ad3824.json new file mode 100644 index 0000000..4539a5e --- /dev/null +++ b/.sqlx/query-dd0d0e3fd03f130aab947d13580796eee9a786e2ca01d339fd0e8356f8ad3824.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM tags WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "dd0d0e3fd03f130aab947d13580796eee9a786e2ca01d339fd0e8356f8ad3824" +} diff --git a/src/cli/api/tags.rs b/src/cli/api/tags.rs index f522996..1a066eb 100644 --- a/src/cli/api/tags.rs +++ b/src/cli/api/tags.rs @@ -35,6 +35,7 @@ pub async fn run( icon, color, } => 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(()) } + +/// Delete a tag +async fn delete( + client: ApiClient, + reference: &str, + json: bool, +) -> Result<(), Box> { + 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(()) +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 0f7dda9..cd7cc12 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -229,6 +229,13 @@ pub enum TagsCommand { #[arg(long)] color: Option, }, + + /// Delete a tag + Delete { + /// Tag slug or UUID + #[arg(name = "ref")] + reference: String, + }, } #[derive(Subcommand, Debug)] diff --git a/src/db/tags.rs b/src/db/tags.rs index 5fe32e1..e0bc9c1 100644 --- a/src/db/tags.rs +++ b/src/db/tags.rs @@ -120,6 +120,13 @@ pub async fn get_tag_by_ref(pool: &PgPool, ref_str: &str) -> Result 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, sqlx::Error> { let rows = sqlx::query!( r#" diff --git a/src/handlers/tags.rs b/src/handlers/tags.rs index 329e5a2..03f6802 100644 --- a/src/handlers/tags.rs +++ b/src/handlers/tags.rs @@ -252,6 +252,66 @@ pub async fn update_tag_handler( } } +/// Delete a tag (requires authentication) +pub async fn delete_tag_handler( + State(state): State>, + axum::extract::Path(ref_str): axum::extract::Path, + 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 pub async fn get_related_tags_handler( State(state): State>, diff --git a/src/routes.rs b/src/routes.rs index 934ac7c..2b3204c 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -65,7 +65,9 @@ pub fn api_routes() -> Router> { ) .route( "/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( "/tags/{ref}/related",