use axum::{ extract::{Json, Path, Query, State}, http::StatusCode, }; use utoipa::OpenApi; use uuid::Uuid; use crate::{ auth::TenantScopedConn, error::{AppError, AppResult}, http::responders::{created_json, no_content, ok_json, JsonResponse}, services::folders::{ CreateFolderRequest, EnsureFolderPathRequest, FolderContentsData, FolderContentsQuery, FolderInfo, FolderService, FolderTreeNode, UpdateFolderRequest, }, state::AppState, }; use crate::services::documents::DocumentResponse; #[derive(utoipa::ToSchema, serde::Serialize)] pub struct FolderResponse { pub folder: FolderInfo, } #[derive(utoipa::ToSchema, serde::Serialize)] pub struct FolderContentsResponse { #[schema(nullable)] pub folder: Option, pub subfolders: Vec, pub documents: Vec, } #[utoipa::path( get, path = "/api/folders/{id}", params(("id" = Uuid, Path, description = "Folder ID")), responses((status = 200, description = "Folder detail", body = FolderResponse)), tag = "Folders" )] pub async fn get_folder( State(state): State, Path(folder_id): Path, TenantScopedConn { mut conn, tenant_id, .. }: TenantScopedConn, ) -> AppResult> { let service = FolderService::new(&state); let folder = service.get_folder(&mut conn, tenant_id, folder_id)?; ok_json(FolderResponse { folder }) } #[utoipa::path( post, path = "/api/folders/path", request_body = EnsureFolderPathRequest, responses((status = 200, description = "Folder path ensured", body = FolderResponse)), tag = "Folders" )] pub async fn ensure_folder_path( State(state): State, TenantScopedConn { mut conn, tenant_id, .. }: TenantScopedConn, Json(payload): Json, ) -> AppResult> { let service = FolderService::new(&state); let folder = service.ensure_folder_path(&mut conn, tenant_id, payload)?; ok_json(FolderResponse { folder }) } #[utoipa::path( post, path = "/api/folders", request_body = CreateFolderRequest, responses( (status = 201, description = "Folder created", body = FolderResponse), (status = 200, description = "Folder already existed", body = FolderResponse) ), tag = "Folders" )] pub async fn create_folder( State(state): State, TenantScopedConn { mut conn, tenant_id, .. }: TenantScopedConn, Json(payload): Json, ) -> AppResult> { let service = FolderService::new(&state); let (folder, created) = service.create_folder(&mut conn, tenant_id, payload)?; let response = FolderResponse { folder }; if created { created_json(response) } else { ok_json(response) } } #[utoipa::path( get, path = "/api/folders/{id}/contents", params(("id" = String, Path, description = "Folder ID or 'root'"), FolderContentsQuery), responses((status = 200, description = "Folder contents", body = FolderContentsResponse)), tag = "Folders" )] pub async fn list_folder_contents( State(state): State, Path(folder_identifier): Path, Query(query): Query, TenantScopedConn { mut conn, tenant_id, user_id, .. }: TenantScopedConn, ) -> AppResult> { let FolderContentsQuery { include_documents, sort, dir, } = query; let folder_id = if folder_identifier.eq_ignore_ascii_case("root") { None } else { Some( Uuid::parse_str(&folder_identifier) .map_err(|_| AppError::bad_request("folder identifier must be 'root' or a UUID"))?, ) }; let service = FolderService::new(&state); let FolderContentsData { folder, subfolders, documents, } = service.list_folder_contents( &mut conn, tenant_id, folder_id, sort, dir, include_documents, )?; let documents = if include_documents { service.hydrate_documents(&mut conn, tenant_id, user_id, documents)? } else { Vec::new() }; ok_json(FolderContentsResponse { folder, subfolders, documents, }) } #[utoipa::path( get, path = "/api/folders/tree", responses((status = 200, description = "Folder hierarchy", body = [FolderTreeNode])), tag = "Folders" )] pub async fn list_folder_tree( State(state): State, TenantScopedConn { mut conn, tenant_id, .. }: TenantScopedConn, ) -> AppResult>> { let service = FolderService::new(&state); let tree = service.list_folder_tree(&mut conn, tenant_id)?; ok_json(tree) } #[utoipa::path( delete, path = "/api/folders/{id}", params(("id" = Uuid, Path, description = "Folder ID")), responses((status = 204, description = "Folder deleted")), tag = "Folders" )] pub async fn delete_folder( State(state): State, Path(folder_id): Path, TenantScopedConn { mut conn, tenant_id, .. }: TenantScopedConn, ) -> AppResult { FolderService::new(&state).delete_folder(&mut conn, tenant_id, folder_id)?; no_content() } #[utoipa::path( patch, path = "/api/folders/{id}", params(("id" = Uuid, Path, description = "Folder ID")), request_body = UpdateFolderRequest, responses((status = 204, description = "Folder updated")), tag = "Folders" )] pub async fn update_folder( State(state): State, Path(folder_id): Path, TenantScopedConn { mut conn, tenant_id, .. }: TenantScopedConn, Json(payload): Json, ) -> AppResult { FolderService::new(&state).update_folder(&mut conn, tenant_id, folder_id, payload)?; no_content() } #[derive(OpenApi)] #[openapi( paths( crate::routes::folders::create_folder, crate::routes::folders::ensure_folder_path, crate::routes::folders::get_folder, crate::routes::folders::list_folder_contents, crate::routes::folders::list_folder_tree, crate::routes::folders::delete_folder, crate::routes::folders::update_folder ), components(schemas( crate::services::folders::CreateFolderRequest, crate::services::folders::EnsureFolderPathRequest, crate::routes::folders::FolderResponse, crate::services::folders::FolderInfo, crate::services::folders::FolderContentsQuery, crate::routes::folders::FolderContentsResponse, crate::services::folders::FolderTreeNode, crate::services::folders::UpdateFolderRequest )) )] pub struct FoldersApiDoc;