feat(recent): add cursor + groupby support
This commit is contained in:
@@ -8,8 +8,18 @@ use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::application::dtos::display_helpers::{
|
||||
category_for, format_file_size, icon_class_for, icon_special_class_for,
|
||||
};
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::application::dtos::folder_dto::FolderDto;
|
||||
use crate::application::dtos::grant_dto::{ResourceContentDto, ResourceTypeDto};
|
||||
use crate::application::dtos::recent_dto::{
|
||||
RecentResourceItemDto, RecentResourcesDto, RecentResourcesQuery,
|
||||
};
|
||||
use crate::application::ports::recent_ports::RecentItemsUseCase;
|
||||
use crate::application::services::recent_service::RecentService;
|
||||
use crate::interfaces::errors::AppError;
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
|
||||
/// Query parameters for getting recent items
|
||||
@@ -19,7 +29,8 @@ pub struct GetRecentParams {
|
||||
limit: Option<i32>,
|
||||
}
|
||||
|
||||
/// Get user's recent items
|
||||
/// Get user's recent items (deprecated — use `GET /api/recent/resources` instead)
|
||||
#[deprecated = "Use GET /api/recent/resources instead"]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/recent",
|
||||
@@ -213,3 +224,121 @@ pub async fn clear_recent_items(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List recently accessed resources with cursor pagination.
|
||||
///
|
||||
/// Sorted by `accessed_at` DESC by default (most recently accessed first).
|
||||
/// `path` is cleared when the resource is not owned by the requesting user.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/recent/resources",
|
||||
params(RecentResourcesQuery),
|
||||
responses(
|
||||
(status = 200, description = "Paginated list of recently accessed resources",
|
||||
body = RecentResourcesDto),
|
||||
(status = 400, description = "Invalid cursor or query parameters"),
|
||||
),
|
||||
security(("bearerAuth" = [])),
|
||||
tag = "recent"
|
||||
)]
|
||||
pub async fn list_recent_resources(
|
||||
State(recent_service): State<Arc<RecentService>>,
|
||||
auth_user: AuthUser,
|
||||
Query(q): Query<RecentResourcesQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let user_id = auth_user.id;
|
||||
|
||||
let order_by = q.order_by.as_deref().unwrap_or("accessed_at").to_owned();
|
||||
|
||||
// If a cursor exists, validate that it matches the requested sort/direction.
|
||||
let cursor = q
|
||||
.decode_cursor()
|
||||
.filter(|c| c.order_by == order_by && c.reverse == q.reverse);
|
||||
|
||||
let kinds = q.resource_kinds();
|
||||
|
||||
match recent_service
|
||||
.list_resources_paged(
|
||||
user_id,
|
||||
q.limit_clamped(),
|
||||
cursor,
|
||||
&order_by,
|
||||
kinds.as_deref(),
|
||||
q.reverse,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((rows, next_cursor)) => {
|
||||
let items: Vec<RecentResourceItemDto> = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
// Path is only shown to the owner; non-owners see ""
|
||||
// to avoid leaking another user's folder hierarchy.
|
||||
let path = if row.is_owner {
|
||||
row.path.clone().unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
if row.resource_type == "folder" {
|
||||
let dto = FolderDto {
|
||||
id: row.resource_id.to_string(),
|
||||
name: row.name.clone(),
|
||||
path,
|
||||
parent_id: row.parent_id.map(|u| u.to_string()),
|
||||
owner_id: Some(row.owner_id.to_string()),
|
||||
created_at: row.resource_created_at.timestamp() as u64,
|
||||
modified_at: row.modified_at.timestamp() as u64,
|
||||
is_root: false,
|
||||
icon_class: std::sync::Arc::from("fas fa-folder"),
|
||||
icon_special_class: std::sync::Arc::from("folder-icon"),
|
||||
category: std::sync::Arc::from("Folder"),
|
||||
};
|
||||
RecentResourceItemDto {
|
||||
resource_type: ResourceTypeDto::Folder,
|
||||
accessed_at: row.accessed_at,
|
||||
resource: ResourceContentDto::Folder(dto),
|
||||
}
|
||||
} else {
|
||||
let mime = row
|
||||
.mime_type
|
||||
.as_deref()
|
||||
.unwrap_or("application/octet-stream");
|
||||
let size_bytes = row.size.max(0) as u64;
|
||||
let dto = FileDto {
|
||||
id: row.resource_id.to_string(),
|
||||
name: row.name.clone(),
|
||||
path,
|
||||
size: size_bytes,
|
||||
mime_type: std::sync::Arc::from(mime),
|
||||
folder_id: row.parent_id.map(|u| u.to_string()),
|
||||
created_at: row.resource_created_at.timestamp() as u64,
|
||||
modified_at: row.modified_at.timestamp() as u64,
|
||||
icon_class: std::sync::Arc::from(icon_class_for(&row.name, mime)),
|
||||
icon_special_class: std::sync::Arc::from(icon_special_class_for(
|
||||
&row.name, mime,
|
||||
)),
|
||||
category: std::sync::Arc::from(category_for(&row.name, mime)),
|
||||
size_formatted: format_file_size(size_bytes),
|
||||
owner_id: Some(row.owner_id.to_string()),
|
||||
sort_date: None,
|
||||
etag: String::new(),
|
||||
};
|
||||
RecentResourceItemDto {
|
||||
resource_type: ResourceTypeDto::File,
|
||||
accessed_at: row.accessed_at,
|
||||
resource: ResourceContentDto::File(dto),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(RecentResourcesDto::with_cursor(items, next_cursor)),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Err(e) => AppError::from(e).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,10 +355,12 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
|
||||
// Create routes for recent items if the service is available
|
||||
let recent_router = if let Some(recent_service) = recent_service.clone() {
|
||||
#[allow(deprecated)]
|
||||
use crate::interfaces::api::handlers::recent_handler;
|
||||
|
||||
Router::new()
|
||||
.route("/", get(recent_handler::get_recent_items))
|
||||
.route("/resources", get(recent_handler::list_recent_resources))
|
||||
.route(
|
||||
"/{item_type}/{item_id}",
|
||||
post(recent_handler::record_item_access),
|
||||
|
||||
Reference in New Issue
Block a user