mirror of https://github.com/buster-so/buster.git
347 lines
8.9 KiB
Markdown
347 lines
8.9 KiB
Markdown
|
---
|
||
|
title: Delete Dashboard REST Endpoint
|
||
|
author: Cascade
|
||
|
date: 2025-03-19
|
||
|
status: 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
|
||
|
|
||
|
```json
|
||
|
{
|
||
|
"success": true,
|
||
|
"message": "Dashboard deleted successfully"
|
||
|
}
|
||
|
```
|
||
|
|
||
|
### Handler Implementation
|
||
|
|
||
|
#### REST Handler
|
||
|
|
||
|
Create a new file at `src/routes/rest/routes/dashboards/delete_dashboard.rs`:
|
||
|
|
||
|
```rust
|
||
|
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`:
|
||
|
|
||
|
```rust
|
||
|
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:
|
||
|
|
||
|
```rust
|
||
|
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:
|
||
|
|
||
|
```rust
|
||
|
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:
|
||
|
|
||
|
```rust
|
||
|
#[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`:
|
||
|
|
||
|
```rust
|
||
|
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
|
||
|
|
||
|
1. Create the business logic handler
|
||
|
2. Create the REST endpoint handler
|
||
|
3. Update module files
|
||
|
4. Add unit tests
|
||
|
5. Add integration tests
|
||
|
6. Manual testing
|
||
|
|
||
|
## Success Criteria
|
||
|
|
||
|
1. The endpoint successfully marks a dashboard as deleted
|
||
|
2. The endpoint returns a properly formatted response
|
||
|
3. Deleted dashboards are no longer accessible via the API
|
||
|
4. All tests pass
|
||
|
5. The endpoint is properly documented
|
||
|
6. The endpoint is secured with authentication
|