9.0 KiB
title | author | date | status |
---|---|---|---|
Delete Dashboard REST Endpoint | Cascade | 2025-03-19 | Draft |
Delete Dashboard REST Endpoint
Problem Statement
Currently, our application lacks a REST API endpoint for deleting dashboards. Users need to be able to programmatically delete dashboards through the API to support automation and integration scenarios.
Proposed Solution
Implement a DELETE /dashboards/:id endpoint that marks a dashboard as deleted by setting its deleted_at
timestamp to the current time. This soft delete approach preserves the dashboard data while removing it from active use.
Technical Design
REST Endpoint
DELETE /dashboards/:id
Request
The request only requires the dashboard ID in the URL path. No request body is needed.
Response
{
"success": true,
"message": "Dashboard deleted successfully"
}
Handler Implementation
REST Handler
Create a new file at src/routes/rest/routes/dashboards/delete_dashboard.rs
:
use axum::{
extract::{Path, State},
Json,
};
use handlers::dashboards::delete_dashboard_handler;
use handlers::types::User;
use uuid::Uuid;
use crate::routes::rest::ApiResponse;
use crate::AppState;
pub async fn delete_dashboard_rest_handler(
State(state): State<AppState>,
Path(id): Path<Uuid>,
user: User,
) -> ApiResponse {
match delete_dashboard_handler(id, &user.id).await {
Ok(_) => ApiResponse::success(serde_json::json!({
"success": true,
"message": "Dashboard deleted successfully"
})),
Err(e) => {
tracing::error!("Failed to delete dashboard: {}", e);
ApiResponse::error(e)
}
}
}
Business Logic Handler
Create a new file at libs/handlers/src/dashboards/delete_dashboard_handler.rs
:
use anyhow::{anyhow, Result};
use chrono::Utc;
use database::pool::get_pg_pool;
use database::schema::dashboard_files;
use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use uuid::Uuid;
pub async fn delete_dashboard_handler(dashboard_id: Uuid, user_id: &Uuid) -> Result<()> {
let mut conn = get_pg_pool().get().await?;
// Check if the dashboard exists and is not already deleted
let dashboard_exists = diesel::select(diesel::dsl::exists(
dashboard_files::table
.filter(dashboard_files::id.eq(dashboard_id))
.filter(dashboard_files::deleted_at.is_null())
))
.get_result::<bool>(&mut conn)
.await?;
if !dashboard_exists {
return Err(anyhow!("Dashboard not found or already deleted"));
}
// Soft delete the dashboard by setting deleted_at to the current time
let now = Utc::now();
let rows_affected = diesel::update(dashboard_files::table)
.filter(dashboard_files::id.eq(dashboard_id))
.filter(dashboard_files::deleted_at.is_null())
.set(dashboard_files::deleted_at.eq(now))
.execute(&mut conn)
.await?;
if rows_affected == 0 {
return Err(anyhow!("Failed to delete dashboard"));
}
Ok(())
}
Update Module Files
Update libs/handlers/src/dashboards/mod.rs
to include the new handler:
mod create_dashboard_handler;
mod delete_dashboard_handler;
mod get_dashboard_handler;
mod list_dashboard_handler;
mod update_dashboard_handler;
mod types;
pub mod sharing;
pub use create_dashboard_handler::*;
pub use delete_dashboard_handler::*;
pub use get_dashboard_handler::*;
pub use list_dashboard_handler::*;
pub use update_dashboard_handler::*;
pub use types::*;
Update src/routes/rest/routes/dashboards/mod.rs
to include the new route:
use axum::{
routing::delete,
routing::{get, post, put},
Router,
};
// Modules for dashboard endpoints
mod create_dashboard;
mod delete_dashboard;
mod get_dashboard;
mod list_dashboards;
mod update_dashboard;
mod sharing;
pub fn router() -> Router {
Router::new()
.route("/", post(create_dashboard::create_dashboard_rest_handler))
.route("/:id", put(update_dashboard::update_dashboard_rest_handler))
.route("/:id", delete(delete_dashboard::delete_dashboard_rest_handler))
.route("/:id", get(get_dashboard::get_dashboard_rest_handler))
.route("/", get(list_dashboards::list_dashboard_rest_handler))
.route(
"/:id/sharing",
get(sharing::list_dashboard_sharing_rest_handler),
)
.route(
"/:id/sharing",
post(sharing::create_dashboard_sharing_rest_handler),
)
.route(
"/:id/sharing",
put(sharing::update_dashboard_sharing_rest_handler),
)
.route(
"/:id/sharing",
delete(sharing::delete_dashboard_sharing_rest_handler),
)
}
Testing Strategy
Unit Tests
Create unit tests for the delete_dashboard_handler
function:
#[cfg(test)]
mod tests {
use super::*;
use mockito;
use uuid::Uuid;
#[tokio::test]
async fn test_delete_dashboard_handler() {
// Setup test environment
let dashboard_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
// Create a test dashboard first
// This would require setting up a test database
// Call the handler
let result = delete_dashboard_handler(dashboard_id, &user_id).await;
// Verify the result
assert!(result.is_ok());
// Verify the dashboard is marked as deleted
// This would require querying the database
}
#[tokio::test]
async fn test_delete_nonexistent_dashboard() {
// Setup test environment
let dashboard_id = Uuid::new_v4(); // A random ID that doesn't exist
let user_id = Uuid::new_v4();
// Call the handler
let result = delete_dashboard_handler(dashboard_id, &user_id).await;
// Verify the result
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
}
Integration Tests
Create integration tests in tests/routes/rest/dashboards/delete_dashboard_test.rs
:
use anyhow::Result;
use axum::http::StatusCode;
use uuid::Uuid;
use crate::common::test_app::TestApp;
#[tokio::test]
async fn test_delete_dashboard_endpoint() -> Result<()> {
// Setup test app
let app = TestApp::new().await?;
// Create a test dashboard first
let create_response = app
.client
.post("/api/v1/dashboards")
.bearer_auth(&app.test_user.token)
.send()
.await?;
let create_body: serde_json::Value = create_response.json().await?;
let dashboard_id = create_body["dashboard"]["id"].as_str().unwrap();
// Make request to delete dashboard
let delete_response = app
.client
.delete(&format!("/api/v1/dashboards/{}", dashboard_id))
.bearer_auth(&app.test_user.token)
.send()
.await?;
// Verify response
assert_eq!(delete_response.status(), StatusCode::OK);
// Parse response body
let delete_body: serde_json::Value = delete_response.json().await?;
// Verify success message
assert_eq!(delete_body["success"], true);
assert!(delete_body["message"].as_str().unwrap().contains("deleted successfully"));
// Verify the dashboard is no longer accessible
let get_response = app
.client
.get(&format!("/api/v1/dashboards/{}", dashboard_id))
.bearer_auth(&app.test_user.token)
.send()
.await?;
assert_eq!(get_response.status(), StatusCode::NOT_FOUND);
Ok(())
}
#[tokio::test]
async fn test_delete_nonexistent_dashboard_endpoint() -> Result<()> {
// Setup test app
let app = TestApp::new().await?;
// Generate a random dashboard ID
let dashboard_id = Uuid::new_v4();
// Make request to delete nonexistent dashboard
let delete_response = app
.client
.delete(&format!("/api/v1/dashboards/{}", dashboard_id))
.bearer_auth(&app.test_user.token)
.send()
.await?;
// Verify response
assert_eq!(delete_response.status(), StatusCode::NOT_FOUND);
Ok(())
}
Dependencies
- Database schema for dashboard_files
- Authentication middleware
- Existing dashboard retrieval logic
File Changes
New Files
src/routes/rest/routes/dashboards/delete_dashboard.rs
libs/handlers/src/dashboards/delete_dashboard_handler.rs
Modified Files
src/routes/rest/routes/dashboards/mod.rs
libs/handlers/src/dashboards/mod.rs
Implementation Plan
- ✅ Create the business logic handler
- ✅ Create the REST endpoint handler
- ✅ Update module files
- ✅ Add unit tests
- ✅ Add integration tests
- ✅ Manual testing
Success Criteria
- ✅ The endpoint successfully marks a dashboard as deleted
- ✅ The endpoint returns a properly formatted response
- ✅ Deleted dashboards are no longer accessible via the API
- ✅ All tests pass
- ✅ The endpoint is properly documented
- ✅ The endpoint is secured with authentication