create remove metric from collections

This commit is contained in:
dal 2025-03-20 09:08:54 -06:00
parent a7d0f0d206
commit 8fe017a941
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
5 changed files with 392 additions and 18 deletions

View File

@ -77,7 +77,7 @@ pub async fn list_user_favorites(user: &AuthenticatedUser) -> Result<Vec<Favorit
.map(|f| f.0)
.collect::<Vec<Uuid>>(),
);
tokio::spawn(async move { get_favorite_dashboards(dashboard_ids) })
tokio::spawn(async move { get_favorite_dashboards(dashboard_ids).await })
};
let collection_favorites = {
@ -88,7 +88,7 @@ pub async fn list_user_favorites(user: &AuthenticatedUser) -> Result<Vec<Favorit
.map(|f| f.0)
.collect::<Vec<Uuid>>(),
);
tokio::spawn(async move { get_assets_from_collections(collection_ids) })
tokio::spawn(async move { get_assets_from_collections(collection_ids).await })
};
let threads_favorites = {
@ -99,7 +99,7 @@ pub async fn list_user_favorites(user: &AuthenticatedUser) -> Result<Vec<Favorit
.map(|f| f.0)
.collect::<Vec<Uuid>>(),
);
tokio::spawn(async move { get_favorite_threads(thread_ids) })
tokio::spawn(async move { get_favorite_threads(thread_ids).await })
};
let metrics_favorites = {
@ -110,7 +110,7 @@ pub async fn list_user_favorites(user: &AuthenticatedUser) -> Result<Vec<Favorit
.map(|f| f.0)
.collect::<Vec<Uuid>>(),
);
tokio::spawn(async move { get_favorite_metrics(metric_ids) })
tokio::spawn(async move { get_favorite_metrics(metric_ids).await })
};
let (dashboard_fav_res, collection_fav_res, threads_fav_res, metrics_fav_res) =
@ -124,7 +124,7 @@ pub async fn list_user_favorites(user: &AuthenticatedUser) -> Result<Vec<Favorit
}
};
let favorite_dashboards = match dashboard_fav_res.await {
let favorite_dashboards = match dashboard_fav_res {
Ok(dashboards) => dashboards,
Err(e) => {
tracing::error!("Error getting favorite dashboards: {}", e);
@ -132,7 +132,7 @@ pub async fn list_user_favorites(user: &AuthenticatedUser) -> Result<Vec<Favorit
}
};
let favorite_collections = match collection_fav_res.await {
let favorite_collections = match collection_fav_res {
Ok(collections) => collections,
Err(e) => {
tracing::error!("Error getting favorite collections: {}", e);
@ -140,7 +140,7 @@ pub async fn list_user_favorites(user: &AuthenticatedUser) -> Result<Vec<Favorit
}
};
let favorite_threads = match threads_fav_res.await {
let favorite_threads = match threads_fav_res {
Ok(threads) => threads,
Err(e) => {
tracing::error!("Error getting favorite threads: {}", e);
@ -148,7 +148,7 @@ pub async fn list_user_favorites(user: &AuthenticatedUser) -> Result<Vec<Favorit
}
};
let favorite_metrics = match metrics_fav_res.await {
let favorite_metrics = match metrics_fav_res {
Ok(metrics) => metrics,
Err(e) => {
tracing::error!("Error getting favorite metrics: {}", e);
@ -216,7 +216,7 @@ async fn get_favorite_threads(thread_ids: Arc<Vec<Uuid>>) -> Result<Vec<Favorite
let favorite_threads = thread_records
.iter()
.map(|(id, name)| FavoriteObject {
id: id.clone(),
id: *id,
name: name.clone().unwrap_or_else(|| String::from("Untitled")),
type_: AssetType::Thread,
})
@ -246,7 +246,7 @@ async fn get_favorite_dashboards(dashboard_ids: Arc<Vec<Uuid>>) -> Result<Vec<Fa
let favorite_dashboards = dashboard_records
.iter()
.map(|(id, name)| FavoriteObject {
id: id.clone(),
id: *id,
name: name.clone(),
type_: AssetType::Dashboard,
})
@ -339,7 +339,7 @@ async fn get_assets_from_collections(
Ok(collection_favorites)
}
async fn get_collection_names(collection_ids: &Vec<Uuid>) -> Result<Vec<(Uuid, String)>> {
async fn get_collection_names(collection_ids: &[Uuid]) -> Result<Vec<(Uuid, String)>> {
let mut conn = match get_pg_pool().get().await {
Ok(conn) => conn,
Err(e) => return Err(anyhow!("Error getting connection from pool: {:?}", e)),
@ -359,7 +359,7 @@ async fn get_collection_names(collection_ids: &Vec<Uuid>) -> Result<Vec<(Uuid, S
}
async fn get_dashboards_from_collections(
collection_ids: &Vec<Uuid>,
collection_ids: &[Uuid],
) -> Result<Vec<(Uuid, FavoriteObject)>> {
let mut conn = match get_pg_pool().get().await {
Ok(conn) => conn,
@ -403,7 +403,7 @@ async fn get_dashboards_from_collections(
}
async fn get_threads_from_collections(
collection_ids: &Vec<Uuid>,
collection_ids: &[Uuid],
) -> Result<Vec<(Uuid, FavoriteObject)>> {
let mut conn = match get_pg_pool().get().await {
Ok(conn) => conn,
@ -439,9 +439,9 @@ async fn get_threads_from_collections(
.iter()
.map(|(collection_id, id, name)| {
(
collection_id.clone(),
*collection_id,
FavoriteObject {
id: id.clone(),
id: *id,
name: name.clone().unwrap_or_else(|| String::from("Untitled")),
type_: AssetType::Thread,
},
@ -472,7 +472,7 @@ async fn get_favorite_metrics(metric_ids: Arc<Vec<Uuid>>) -> Result<Vec<Favorite
let favorite_metrics = metric_records
.iter()
.map(|(id, name)| FavoriteObject {
id: id.clone(),
id: *id,
name: name.clone(),
type_: AssetType::MetricFile,
})
@ -480,7 +480,7 @@ async fn get_favorite_metrics(metric_ids: Arc<Vec<Uuid>>) -> Result<Vec<Favorite
Ok(favorite_metrics)
}
pub async fn update_favorites(user: &AuthenticatedUser, favorites: &Vec<Uuid>) -> Result<()> {
pub async fn update_favorites(user: &AuthenticatedUser, favorites: &[Uuid]) -> Result<()> {
let mut conn = match get_pg_pool().get().await {
Ok(conn) => conn,
Err(e) => return Err(anyhow!("Error getting connection from pool: {:?}", e)),
@ -509,7 +509,7 @@ pub async fn update_favorites(user: &AuthenticatedUser, favorites: &Vec<Uuid>) -
let new_fav = UserFavorite {
user_id: user.id,
asset_id: *favorite_id,
asset_type: asset_type.clone(),
asset_type: *asset_type,
order_index: index as i32,
created_at: chrono::Utc::now(),
deleted_at: None,

View File

@ -4,6 +4,7 @@ pub mod get_metric_data_handler;
pub mod get_metric_handler;
pub mod list_metrics_handler;
pub mod post_metric_dashboard_handler;
pub mod remove_metrics_from_collection_handler;
pub mod update_metric_handler;
pub mod types;
pub mod sharing;
@ -14,6 +15,7 @@ pub use get_metric_data_handler::*;
pub use get_metric_handler::*;
pub use list_metrics_handler::*;
pub use post_metric_dashboard_handler::*;
pub use remove_metrics_from_collection_handler::*;
pub use update_metric_handler::*;
pub use types::*;
pub use sharing::*;

View File

@ -0,0 +1,293 @@
use anyhow::{anyhow, Result};
use database::{
enums::{AssetPermissionRole, AssetType, IdentityType},
helpers::metric_files::fetch_metric_file,
pool::get_pg_pool,
schema::{collections, collections_to_assets},
};
use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use sharing::check_asset_permission::has_permission;
use tracing::{error, info};
use uuid::Uuid;
/// Response for removing a metric from collections
#[derive(Debug)]
pub struct RemoveMetricsFromCollectionResponse {
pub removed_count: usize,
pub failed_count: usize,
pub failed_ids: Vec<Uuid>,
}
/// Removes a metric from multiple collections
///
/// # Arguments
///
/// * `metric_id` - The unique identifier of the metric
/// * `collection_ids` - Vector of collection IDs to remove the metric from
/// * `user_id` - The unique identifier of the user performing the action
///
/// # Returns
///
/// RemoveMetricsFromCollectionResponse on success, or an error if the operation fails
pub async fn remove_metrics_from_collection_handler(
metric_id: &Uuid,
collection_ids: Vec<Uuid>,
user_id: &Uuid,
) -> Result<RemoveMetricsFromCollectionResponse> {
info!(
metric_id = %metric_id,
user_id = %user_id,
collection_count = collection_ids.len(),
"Removing metric from collections"
);
if collection_ids.is_empty() {
return Ok(RemoveMetricsFromCollectionResponse {
removed_count: 0,
failed_count: 0,
failed_ids: vec![],
});
}
// 1. Validate the metric exists
let _metric = match fetch_metric_file(metric_id).await {
Ok(Some(metric)) => metric,
Ok(None) => {
error!(
metric_id = %metric_id,
"Metric not found"
);
return Err(anyhow!("Metric not found"));
}
Err(e) => {
error!("Error checking if metric exists: {}", e);
return Err(anyhow!("Database error: {}", e));
}
};
// 2. Check if user has permission to modify the metric
let has_metric_permission = has_permission(
*metric_id,
AssetType::MetricFile,
*user_id,
IdentityType::User,
AssetPermissionRole::CanEdit, // This will pass for Owner and FullAccess too
)
.await
.map_err(|e| {
error!(
metric_id = %metric_id,
user_id = %user_id,
"Error checking metric permission: {}", e
);
anyhow!("Error checking permissions: {}", e)
})?;
if !has_metric_permission {
error!(
metric_id = %metric_id,
user_id = %user_id,
"User does not have permission to modify this metric"
);
return Err(anyhow!("User does not have permission to modify this metric"));
}
// 3. Get database connection
let mut conn = get_pg_pool().get().await.map_err(|e| {
error!("Database connection error: {}", e);
anyhow!("Failed to get database connection: {}", e)
})?;
// 4. Process each collection
let mut failed_ids = Vec::new();
let mut removed_count = 0;
let now = chrono::Utc::now();
for collection_id in &collection_ids {
// Check if collection exists
let collection_exists = collections::table
.filter(collections::id.eq(collection_id))
.filter(collections::deleted_at.is_null())
.count()
.get_result::<i64>(&mut conn)
.await;
if let Err(e) = collection_exists {
error!(
collection_id = %collection_id,
"Error checking if collection exists: {}", e
);
failed_ids.push(*collection_id);
continue;
}
if collection_exists.unwrap() == 0 {
error!(
collection_id = %collection_id,
"Collection not found"
);
failed_ids.push(*collection_id);
continue;
}
// Check if user has permission to modify the collection
let has_collection_permission = has_permission(
*collection_id,
AssetType::Collection,
*user_id,
IdentityType::User,
AssetPermissionRole::CanEdit,
)
.await;
if let Err(e) = has_collection_permission {
error!(
collection_id = %collection_id,
user_id = %user_id,
"Error checking collection permission: {}", e
);
failed_ids.push(*collection_id);
continue;
}
if !has_collection_permission.unwrap() {
error!(
collection_id = %collection_id,
user_id = %user_id,
"User does not have permission to modify this collection"
);
failed_ids.push(*collection_id);
continue;
}
// Mark metric as deleted from this collection
match diesel::update(collections_to_assets::table)
.filter(collections_to_assets::collection_id.eq(collection_id))
.filter(collections_to_assets::asset_id.eq(metric_id))
.filter(collections_to_assets::asset_type.eq(AssetType::MetricFile))
.filter(collections_to_assets::deleted_at.is_null())
.set((
collections_to_assets::deleted_at.eq(now),
collections_to_assets::updated_at.eq(now),
collections_to_assets::updated_by.eq(user_id),
))
.execute(&mut conn)
.await
{
Ok(updated) => {
if updated > 0 {
removed_count += 1;
info!(
metric_id = %metric_id,
collection_id = %collection_id,
"Successfully removed metric from collection"
);
} else {
error!(
metric_id = %metric_id,
collection_id = %collection_id,
"Metric not found in collection"
);
failed_ids.push(*collection_id);
}
}
Err(e) => {
error!(
metric_id = %metric_id,
collection_id = %collection_id,
"Error removing metric from collection: {}", e
);
failed_ids.push(*collection_id);
}
}
}
let failed_count = failed_ids.len();
info!(
metric_id = %metric_id,
user_id = %user_id,
collection_count = collection_ids.len(),
removed_count = removed_count,
failed_count = failed_count,
"Finished removing metric from collections"
);
Ok(RemoveMetricsFromCollectionResponse {
removed_count,
failed_count,
failed_ids,
})
}
#[cfg(test)]
mod tests {
use super::*;
use database::enums::{AssetPermissionRole, AssetType, IdentityType};
use uuid::Uuid;
use std::sync::Arc;
use mockall::predicate::*;
use mockall::mock;
// Mock the database and sharing functions for testing
mock! {
MetricHelper {}
impl MetricHelper {
async fn fetch_metric_file(id: &Uuid) -> Result<Option<database::models::MetricFile>>;
}
}
mock! {
SharingHelper {}
impl SharingHelper {
async fn has_permission(
asset_id: Uuid,
asset_type: AssetType,
identity_id: Uuid,
identity_type: IdentityType,
role: AssetPermissionRole,
) -> Result<bool>;
}
}
mock! {
DatabaseConnection {}
impl DatabaseConnection {
async fn execute_query(&self, query: &str) -> Result<u64>;
async fn get_results<T>(&self, query: &str) -> Result<Vec<T>>;
}
}
#[tokio::test]
async fn test_remove_metrics_from_collection_handler() {
// This is a placeholder for the actual test
// In a real implementation, we would use test fixtures and a test database
// For now, let's just check that the basic input validation works
let metric_id = Uuid::new_v4();
let user_id = Uuid::new_v4();
let empty_collections: Vec<Uuid> = vec![];
// Test with empty collections - should return success with 0 removed
let result = remove_metrics_from_collection_handler(
&metric_id,
empty_collections,
&user_id
).await;
assert!(result.is_ok());
if let Ok(response) = result {
assert_eq!(response.removed_count, 0);
assert_eq!(response.failed_count, 0);
assert!(response.failed_ids.is_empty());
}
// In a real test, we would mock:
// 1. The metric_files database lookup
// 2. The permission checks
// 3. The collections database lookups
// 4. The update operations
// And then verify the behavior with various inputs
}
}

View File

@ -7,6 +7,7 @@ mod get_metric;
mod get_metric_data;
mod list_metrics;
mod post_metric_dashboard;
mod remove_metrics_from_collection;
mod update_metric;
mod sharing;
@ -28,5 +29,9 @@ pub fn router() -> Router {
"/:id/collections",
post(add_metric_to_collections::add_metric_to_collections_rest_handler),
)
.route(
"/:id/collections",
delete(remove_metrics_from_collection::remove_metrics_from_collection),
)
.nest("/:id/sharing", sharing::router())
}

View File

@ -0,0 +1,74 @@
use axum::{
extract::{Extension, Json, Path},
http::StatusCode,
};
use handlers::metrics::remove_metrics_from_collection_handler;
use middleware::AuthenticatedUser;
use serde::{Deserialize, Serialize};
use tracing::info;
use uuid::Uuid;
use crate::routes::rest::ApiResponse;
#[derive(Debug, Deserialize)]
pub struct RemoveFromCollectionsRequest {
pub collection_ids: Vec<Uuid>,
}
#[derive(Debug, Serialize)]
pub struct RemoveFromCollectionsResponse {
pub message: String,
pub removed_count: usize,
pub failed_count: usize,
pub failed_ids: Vec<Uuid>,
}
/// REST handler for removing a metric from multiple collections
///
/// # Arguments
///
/// * `user` - The authenticated user making the request
/// * `id` - The unique identifier of the metric
/// * `request` - The collection IDs to remove the metric from
///
/// # Returns
///
/// A success message on success, or an appropriate error response
pub async fn remove_metrics_from_collection(
Extension(user): Extension<AuthenticatedUser>,
Path(id): Path<Uuid>,
Json(request): Json<RemoveFromCollectionsRequest>,
) -> Result<ApiResponse<RemoveFromCollectionsResponse>, (StatusCode, String)> {
info!(
metric_id = %id,
user_id = %user.id,
collection_count = request.collection_ids.len(),
"Processing DELETE request to remove metric from collections"
);
match remove_metrics_from_collection_handler(&id, request.collection_ids, &user.id).await {
Ok(result) => {
let response = RemoveFromCollectionsResponse {
message: "Metric removed from collections successfully".to_string(),
removed_count: result.removed_count,
failed_count: result.failed_count,
failed_ids: result.failed_ids,
};
Ok(ApiResponse::JsonData(response))
}
Err(e) => {
tracing::error!("Error removing metric from collections: {}", e);
// Map specific errors to appropriate status codes
let error_message = e.to_string();
if error_message.contains("Metric not found") {
return Err((StatusCode::NOT_FOUND, format!("Metric not found: {}", e)));
} else if error_message.contains("permission") {
return Err((StatusCode::FORBIDDEN, format!("Insufficient permissions: {}", e)));
}
Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to remove metric from collections: {}", e)))
}
}
}