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
+288 -38
View File
@@ -378,6 +378,13 @@ pub struct AssetObjectsQuery {
pub limit: Option<i32>,
}
#[utoipa::path(
get,
path = "/api/documents",
params(DocumentListQuery),
responses((status = 200, description = "List documents", body = [DocumentResponse])),
tag = "Documents"
)]
pub async fn list_documents(
State(state): State<AppState>,
Query(params): Query<DocumentListQuery>,
@@ -633,48 +640,18 @@ pub async fn list_documents(
.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 response = 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());
response.push(to_document_response(
&state,
user_id,
doc,
tags,
correspondents,
current_version,
doc_type,
)?);
}
let response = hydrate_documents(&state, &mut conn, tenant_id, user_id, docs)?;
Ok(Json(response))
}
#[utoipa::path(
get,
path = "/api/documents/check",
params(DocumentCheckQuery),
responses((status = 200, description = "Checksum lookup", body = DocumentCheckResponse)),
tag = "Documents"
)]
pub async fn check_document(
Query(query): Query<DocumentCheckQuery>,
TenantScopedConn {
@@ -728,6 +705,13 @@ pub async fn check_document(
}
}
#[utoipa::path(
get,
path = "/api/documents/{id}",
params(("id" = Uuid, Path, description = "Document ID")),
responses((status = 200, description = "Document detail", body = DocumentDetailResponse)),
tag = "Documents"
)]
pub async fn get_document(
State(state): State<AppState>,
Path(document_id): Path<Uuid>,
@@ -773,6 +757,17 @@ pub async fn get_document(
}))
}
#[utoipa::path(
post,
path = "/api/documents",
request_body = UploadDocumentForm,
responses(
(status = 201, description = "Document created", body = DocumentDetailResponse),
(status = 200, description = "Existing document reused", body = DocumentDetailResponse),
(status = 204, description = "Upload skipped because the document already exists")
),
tag = "Documents"
)]
pub async fn upload_document(
State(state): State<AppState>,
TenantScopedConn {
@@ -978,6 +973,13 @@ pub async fn upload_document(
Ok(response)
}
#[utoipa::path(
post,
path = "/api/documents/{id}/assets",
params(("id" = Uuid, Path, description = "Document ID"), AssetRequestQuery),
responses((status = 202, description = "Asset generation requested")),
tag = "Assets"
)]
pub async fn request_document_assets(
Path(document_id): Path<Uuid>,
Query(query): Query<AssetRequestQuery>,
@@ -1014,6 +1016,13 @@ pub async fn request_document_assets(
Ok(StatusCode::ACCEPTED)
}
#[utoipa::path(
post,
path = "/api/documents/bulk/reanalyze",
request_body = BulkReanalyzeSelectionRequest,
responses((status = 200, description = "Reanalyze queued", body = BulkReanalyzeResponse)),
tag = "Documents"
)]
pub async fn reanalyze_selected_documents(
TenantScopedConn {
mut conn,
@@ -1065,6 +1074,13 @@ pub async fn reanalyze_selected_documents(
Ok((StatusCode::ACCEPTED, Json(BulkReanalyzeResponse { queued })))
}
#[utoipa::path(
get,
path = "/api/documents/{id}/assets",
params(("id" = Uuid, Path, description = "Document ID")),
responses((status = 200, description = "Document assets", body = [DocumentAssetResponse])),
tag = "Assets"
)]
pub async fn list_document_assets(
State(state): State<AppState>,
Path(document_id): Path<Uuid>,
@@ -1089,6 +1105,13 @@ pub async fn list_document_assets(
Ok(Json(assets))
}
#[utoipa::path(
get,
path = "/api/assets/{asset_id}",
params(("asset_id" = Uuid, Path, description = "Asset ID"), AssetObjectsQuery),
responses((status = 200, description = "Asset detail", body = DocumentAssetDetailResponse)),
tag = "Assets"
)]
pub async fn get_document_asset(
State(state): State<AppState>,
Path(asset_id): Path<Uuid>,
@@ -1163,6 +1186,13 @@ pub async fn get_document_asset(
Ok(Json(to_asset_detail_response(asset, object_responses)))
}
#[utoipa::path(
get,
path = "/api/documents/{id}/versions",
params(("id" = Uuid, Path, description = "Document ID")),
responses((status = 200, description = "Document versions", body = [DocumentVersionResponse])),
tag = "Documents"
)]
pub async fn list_document_versions(
Path(document_id): Path<Uuid>,
TenantScopedConn {
@@ -1192,6 +1222,16 @@ pub async fn list_document_versions(
Ok(Json(versions))
}
#[utoipa::path(
get,
path = "/api/documents/{id}/versions/{version_id}",
params(
("id" = Uuid, Path, description = "Document ID"),
("version_id" = Uuid, Path, description = "Version ID"),
),
responses((status = 200, description = "Document version detail", body = DocumentVersionDetailResponse)),
tag = "Documents"
)]
pub async fn get_document_version(
State(state): State<AppState>,
Path((document_id, version_id)): Path<(Uuid, Uuid)>,
@@ -1230,6 +1270,13 @@ pub async fn get_document_version(
}))
}
#[utoipa::path(
get,
path = "/download/{token}",
params(("token" = String, Path, description = "Download token")),
responses((status = 302, description = "Redirect to pre-signed URL")),
tag = "Documents"
)]
pub async fn download_with_token(
State(state): State<AppState>,
Path(token): Path<String>,
@@ -1282,6 +1329,13 @@ pub async fn download_with_token(
Ok(axum::response::Redirect::temporary(&presigned_url))
}
#[utoipa::path(
delete,
path = "/api/documents/{id}",
params(("id" = Uuid, Path, description = "Document ID")),
responses((status = 204, description = "Document deleted")),
tag = "Documents"
)]
pub async fn delete_document(
Path(document_id): Path<Uuid>,
TenantScopedConn {
@@ -1304,6 +1358,14 @@ pub async fn delete_document(
Ok(StatusCode::NO_CONTENT)
}
#[utoipa::path(
patch,
path = "/api/documents/{id}",
params(("id" = Uuid, Path, description = "Document ID")),
request_body = UpdateDocumentRequest,
responses((status = 200, description = "Updated document", body = DocumentDetailResponse)),
tag = "Documents"
)]
pub async fn update_document(
State(state): State<AppState>,
Path(document_id): Path<Uuid>,
@@ -1519,6 +1581,14 @@ pub async fn update_document(
}))
}
#[utoipa::path(
post,
path = "/api/documents/{id}/restore",
params(("id" = Uuid, Path, description = "Document ID")),
request_body = RestoreDocumentRequest,
responses((status = 204, description = "Document restored")),
tag = "Documents"
)]
pub async fn restore_document(
Path(document_id): Path<Uuid>,
TenantScopedConn {
@@ -1569,6 +1639,14 @@ pub async fn restore_document(
Ok(StatusCode::NO_CONTENT)
}
#[utoipa::path(
patch,
path = "/api/documents/{id}/folder",
params(("id" = Uuid, Path, description = "Document ID")),
request_body = MoveDocumentRequest,
responses((status = 204, description = "Document moved")),
tag = "Documents"
)]
pub async fn move_document(
Path(document_id): Path<Uuid>,
TenantScopedConn {
@@ -1597,6 +1675,13 @@ pub async fn move_document(
Ok(StatusCode::NO_CONTENT)
}
#[utoipa::path(
post,
path = "/api/documents/bulk/move",
request_body = BulkMoveRequest,
responses((status = 200, description = "Bulk move outcome", body = BulkMoveResponse)),
tag = "Documents"
)]
pub async fn bulk_move_documents(
TenantScopedConn {
mut conn,
@@ -1680,6 +1765,14 @@ pub async fn bulk_move_documents(
Ok((StatusCode::OK, body.into_json()?))
}
#[utoipa::path(
post,
path = "/api/documents/{id}/correspondents",
params(("id" = Uuid, Path, description = "Document ID")),
request_body = AssignCorrespondentsRequest,
responses((status = 204, description = "Correspondents assigned")),
tag = "Documents"
)]
pub async fn assign_correspondents(
Path(document_id): Path<Uuid>,
TenantScopedConn {
@@ -1761,6 +1854,13 @@ pub async fn assign_correspondents(
no_content()
}
#[utoipa::path(
post,
path = "/api/documents/bulk/correspondents",
request_body = BulkCorrespondentsRequest,
responses((status = 200, description = "Bulk correspondents outcome", body = BulkCorrespondentResponse)),
tag = "Documents"
)]
pub async fn bulk_assign_correspondents(
TenantScopedConn {
mut conn,
@@ -1853,6 +1953,16 @@ pub async fn bulk_assign_correspondents(
Ok((StatusCode::OK, body.into_json()?))
}
#[utoipa::path(
delete,
path = "/api/documents/{id}/correspondents/{correspondent_id}",
params(
("id" = Uuid, Path, description = "Document ID"),
("correspondent_id" = Uuid, Path, description = "Correspondent ID")
),
responses((status = 204, description = "Correspondent removed")),
tag = "Documents"
)]
pub async fn remove_correspondent(
Path((document_id, correspondent_id)): Path<(Uuid, Uuid)>,
TenantScopedConn {
@@ -1892,6 +2002,14 @@ pub async fn remove_correspondent(
no_content()
}
#[utoipa::path(
post,
path = "/api/documents/{id}/tags",
params(("id" = Uuid, Path, description = "Document ID")),
request_body = AssignTagsRequest,
responses((status = 204, description = "Tags assigned")),
tag = "Documents"
)]
pub async fn assign_tags(
Path(document_id): Path<Uuid>,
TenantScopedConn {
@@ -1922,6 +2040,13 @@ pub async fn assign_tags(
Ok(StatusCode::NO_CONTENT)
}
#[utoipa::path(
post,
path = "/api/documents/bulk/tags",
request_body = BulkTagRequest,
responses((status = 200, description = "Bulk tag outcome", body = BulkTagResponse)),
tag = "Documents"
)]
pub async fn bulk_update_tags(
TenantScopedConn {
mut conn,
@@ -2007,6 +2132,16 @@ pub async fn bulk_update_tags(
Ok((StatusCode::OK, response.into_json()?))
}
#[utoipa::path(
delete,
path = "/api/documents/{id}/tags/{tag_id}",
params(
("id" = Uuid, Path, description = "Document ID"),
("tag_id" = Uuid, Path, description = "Tag ID")
),
responses((status = 204, description = "Tag removed")),
tag = "Documents"
)]
pub async fn remove_tag(
Path((document_id, tag_id)): Path<(Uuid, Uuid)>,
TenantScopedConn {
@@ -2363,3 +2498,118 @@ fn load_document_type(
Ok(None)
}
}
pub(crate) fn hydrate_documents(
state: &AppState,
conn: &mut PgConnection,
tenant_id: Uuid,
user_id: Uuid,
docs: Vec<Document>,
) -> AppResult<Vec<DocumentResponse>> {
if docs.is_empty() {
return Ok(Vec::new());
}
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>(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(conn, &doc_ids)?;
let mut correspondents_map = load_correspondents_for_documents(conn, &doc_ids)?;
let primary_versions = load_primary_assets(state, tenant_id, &docs)?;
let mut responses = 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());
responses.push(to_document_response(
state,
user_id,
doc,
tags,
correspondents,
current_version,
doc_type,
)?);
}
Ok(responses)
}
#[derive(utoipa::OpenApi)]
#[openapi(
paths(
crate::routes::documents::list_documents,
crate::routes::documents::check_document,
crate::routes::documents::upload_document,
crate::routes::documents::get_document,
crate::routes::documents::update_document,
crate::routes::documents::delete_document,
crate::routes::documents::restore_document,
crate::routes::documents::download_with_token,
crate::routes::documents::move_document,
crate::routes::documents::assign_tags,
crate::routes::documents::remove_tag,
crate::routes::documents::bulk_move_documents,
crate::routes::documents::bulk_update_tags,
crate::routes::documents::bulk_assign_correspondents,
crate::routes::documents::assign_correspondents,
crate::routes::documents::remove_correspondent,
crate::routes::documents::reanalyze_selected_documents,
crate::routes::documents::list_document_assets,
crate::routes::documents::request_document_assets,
crate::routes::documents::get_document_asset,
crate::routes::documents::list_document_versions,
crate::routes::documents::get_document_version,
),
components(schemas(
crate::routes::documents::DocumentListQuery,
crate::routes::documents::DocumentStatusFilter,
crate::routes::documents::AssetRequestQuery,
crate::routes::documents::DocumentCheckQuery,
crate::routes::documents::DocumentCheckResponse,
crate::routes::documents::DocumentResponse,
crate::routes::documents::DocumentDetailResponse,
crate::routes::documents::DocumentMetadataUpdate,
crate::routes::documents::DocumentTypeResponse,
crate::routes::documents::TagResponse,
crate::routes::documents::CorrespondentAssignmentInput,
crate::routes::documents::AssignCorrespondentsRequest,
crate::routes::documents::BulkCorrespondentAction,
crate::routes::documents::BulkCorrespondentsRequest,
crate::routes::documents::BulkCorrespondentResponse,
crate::routes::documents::BulkMoveRequest,
crate::routes::documents::BulkMoveResponse,
crate::routes::documents::BulkTagAction,
crate::routes::documents::BulkTagRequest,
crate::routes::documents::BulkTagResponse,
crate::routes::documents::AssignTagsRequest,
crate::routes::documents::MoveDocumentRequest,
crate::routes::documents::BulkReanalyzeSelectionRequest,
crate::routes::documents::BulkReanalyzeResponse,
crate::routes::documents::AssetObjectsQuery,
crate::routes::documents::UploadDocumentForm,
crate::documents::asset::DocumentVersionResponse,
crate::documents::asset::DocumentVersionDetailResponse,
crate::documents::asset::DocumentAssetResponse,
crate::documents::asset::DocumentAssetDetailResponse,
crate::documents::asset::DocumentAssetObjectResponse,
crate::documents::correspondents::DocumentCorrespondentResponse,
))
)]
pub struct DocumentsApiDoc;