feat: batch favorites endpoint + frontend dedup fixes

Backend:
- Add POST /api/favorites/batch endpoint (single multi-row INSERT)
- Add BatchFavoritesResult/BatchFavoritesStats DTOs
- Add batch methods to ports, service, PG repository
- Transaction-based insert with ON CONFLICT DO NOTHING, chunking at 5000

Frontend:
- Rewrite batchFavorites() to single API call (40 requests → 1)
- Add _replaceCacheFromResponse() to avoid extra GET round-trip
- Centralize formatFileSize, isTextViewable, formatDateTime, formatDateShort
- Remove duplicate icon mapping from app.js
- Fix inconsistent quota defaults (10GB everywhere)
This commit is contained in:
Dionisio
2026-02-16 09:17:54 +01:00
parent fb652c07e3
commit d6c4eb884d
17 changed files with 433 additions and 141 deletions
@@ -1,15 +1,24 @@
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use axum::{Json, extract::{Path, State}, http::StatusCode, response::IntoResponse};
use serde::Deserialize;
use std::sync::Arc;
use tracing::{error, info};
use crate::application::ports::favorites_ports::FavoritesUseCase;
use crate::interfaces::middleware::auth::AuthUser;
/// Single item in a batch-add-favorites request.
#[derive(Debug, Deserialize)]
pub struct BatchFavoriteItem {
pub item_id: String,
pub item_type: String,
}
/// Request body for POST /api/favorites/batch
#[derive(Debug, Deserialize)]
pub struct BatchFavoritesRequest {
pub items: Vec<BatchFavoriteItem>,
}
/// Handler for favorite-related API endpoints
pub async fn get_favorites(
State(favorites_service): State<Arc<dyn FavoritesUseCase>>,
@@ -120,3 +129,63 @@ pub async fn remove_favorite(
}
}
}
/// Add multiple items to favourites in a single transaction.
/// POST /api/favorites/batch
pub async fn batch_add_favorites(
State(favorites_service): State<Arc<dyn FavoritesUseCase>>,
auth_user: AuthUser,
Json(body): Json<BatchFavoritesRequest>,
) -> impl IntoResponse {
let user_id = &auth_user.id;
if body.items.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "items array must not be empty" })),
)
.into_response();
}
// Validate item types
for item in &body.items {
if item.item_type != "file" && item.item_type != "folder" {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": format!("Item type must be 'file' or 'folder', got '{}'", item.item_type)
})),
)
.into_response();
}
}
let items: Vec<(String, String)> = body
.items
.into_iter()
.map(|i| (i.item_id, i.item_type))
.collect();
match favorites_service
.batch_add_to_favorites(user_id, &items)
.await
{
Ok(result) => {
info!(
"Batch favourites: {} requested, {} inserted, {} already existed",
result.stats.requested, result.stats.inserted, result.stats.already_existed
);
(StatusCode::OK, Json(serde_json::json!(result))).into_response()
}
Err(err) => {
error!("Error in batch add favorites: {}", err);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": format!("Failed to batch add favorites: {}", err)
})),
)
.into_response()
}
}
}
+1
View File
@@ -231,6 +231,7 @@ pub fn create_api_routes(app_state: &AppState) -> Router<AppState> {
Router::new()
.route("/", get(favorites_handler::get_favorites))
.route("/batch", post(favorites_handler::batch_add_favorites))
.route(
"/{item_type}/{item_id}",
post(favorites_handler::add_favorite),