This commit is contained in:
2025-10-31 01:19:01 +01:00
parent 5b5916ff92
commit fa43bc749e
16 changed files with 878 additions and 872 deletions
+100 -54
View File
@@ -1,5 +1,3 @@
use std::collections::{HashMap, HashSet};
use axum::{
extract::{Json, Path, Query, State},
http::StatusCode,
@@ -9,20 +7,16 @@ use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use crate::models::{Document, DocumentType, Folder, NewFolder};
use crate::schema::{document_types, documents, folders};
use crate::models::{Document, Folder, NewFolder};
use crate::schema::{documents, folders};
use crate::state::AppState;
use crate::{
auth::TenantScopedConn,
error::{AppError, AppResult},
};
use super::documents::{to_document_response, DocumentResponse};
use crate::documents::{
asset::load_primary_assets, correspondents::load_correspondents_for_documents,
tags::load_tags_for_documents,
};
use crate::utils::time::to_iso;
use super::documents::{hydrate_documents, DocumentResponse};
use crate::utils::{json::deserialize_patch_field, time::to_iso};
#[derive(Deserialize, ToSchema)]
pub struct CreateFolderRequest {
@@ -74,16 +68,47 @@ pub struct FolderInfo {
}
#[derive(Default, Deserialize, ToSchema)]
#[serde(default)]
pub struct UpdateFolderRequest {
#[serde(default)]
#[serde(default, deserialize_with = "deserialize_patch_field")]
#[schema(nullable, value_type = Option<Uuid>)]
pub parent_id: Option<Option<Uuid>>,
#[serde(default)]
#[serde(default, deserialize_with = "deserialize_patch_field")]
#[schema(nullable)]
pub name: Option<Option<String>>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn update_folder_request_deserializes_null_parent() {
let req: UpdateFolderRequest =
serde_json::from_value(json!({ "parent_id": null })).unwrap();
assert!(matches!(req.parent_id, Some(None)));
}
#[test]
fn update_folder_request_deserializes_absent_parent() {
let req: UpdateFolderRequest = serde_json::from_value(json!({})).unwrap();
assert!(req.parent_id.is_none());
}
#[test]
fn update_folder_request_deserializes_null_name() {
let req: UpdateFolderRequest = serde_json::from_value(json!({ "name": null })).unwrap();
assert!(matches!(req.name, Some(None)));
}
}
#[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(
Path(folder_id): Path<Uuid>,
TenantScopedConn {
@@ -102,6 +127,13 @@ pub async fn get_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(
TenantScopedConn {
mut conn,
@@ -188,6 +220,16 @@ pub async fn ensure_folder_path(
}))
}
#[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(
TenantScopedConn {
mut conn,
@@ -275,6 +317,13 @@ pub async fn create_folder(
}
}
#[utoipa::path(
get,
path = "/api/folders/{id}/contents",
params(("id" = Uuid, Path, description = "Folder ID"), FolderContentsQuery),
responses((status = 200, description = "Folder contents", body = FolderContentsResponse)),
tag = "Folders"
)]
pub async fn list_folder_contents(
State(state): State<AppState>,
Path(folder_identifier): Path<String>,
@@ -336,47 +385,7 @@ pub async fn list_folder_contents(
.load(&mut conn)?
};
let type_ids: HashSet<Uuid> = docs.iter().filter_map(|doc| doc.document_type_id).collect();
let doc_type_map: HashMap<Uuid, DocumentType> = if type_ids.is_empty() {
HashMap::new()
} else {
let ids: Vec<Uuid> = type_ids.iter().copied().collect();
document_types::table
.filter(document_types::tenant_id.eq(tenant_id))
.filter(document_types::id.eq_any(&ids))
.load::<DocumentType>(&mut conn)?
.into_iter()
.map(|typ| (typ.id, typ))
.collect()
};
let doc_ids: Vec<Uuid> = docs.iter().map(|doc| doc.id).collect();
let tags_map = load_tags_for_documents(&mut conn, &doc_ids)?;
let mut correspondents_map = load_correspondents_for_documents(&mut conn, &doc_ids)?;
drop(conn);
let primary_versions = load_primary_assets(&state, tenant_id, &docs)?;
let mut documents = Vec::with_capacity(doc_ids.len());
for doc in docs {
let tags = tags_map.get(&doc.id).cloned();
let correspondents = correspondents_map.remove(&doc.id).unwrap_or_default();
let current_version = primary_versions.get(&doc.id).cloned();
let doc_type = doc
.document_type_id
.and_then(|id| doc_type_map.get(&id).cloned());
documents.push(to_document_response(
&state,
user_id,
doc,
tags,
correspondents,
current_version,
doc_type,
)?);
}
documents
hydrate_documents(&state, &mut conn, tenant_id, user_id, docs)?
} else {
Vec::new()
};
@@ -388,6 +397,13 @@ pub async fn list_folder_contents(
}))
}
#[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(
Path(folder_id): Path<Uuid>,
TenantScopedConn {
@@ -442,6 +458,14 @@ pub async fn delete_folder(
Ok(StatusCode::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(
Path(folder_id): Path<Uuid>,
TenantScopedConn {
@@ -586,3 +610,25 @@ pub(super) fn gather_descendant_folder_ids(
Ok(ids)
}
#[derive(utoipa::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::delete_folder,
crate::routes::folders::update_folder
),
components(schemas(
crate::routes::folders::CreateFolderRequest,
crate::routes::folders::EnsureFolderPathRequest,
crate::routes::folders::FolderResponse,
crate::routes::folders::FolderInfo,
crate::routes::folders::FolderContentsQuery,
crate::routes::folders::FolderContentsResponse,
crate::routes::folders::UpdateFolderRequest
))
)]
pub struct FoldersApiDoc;