document types
This commit is contained in:
@@ -8,7 +8,7 @@ use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||
use diesel::dsl::exists;
|
||||
use diesel::{prelude::*, result::DatabaseErrorKind, select};
|
||||
use diesel::{prelude::*, result::DatabaseErrorKind, select, OptionalExtension};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -37,12 +37,13 @@ use crate::documents::{
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::jobs::{enqueue_job, JOB_ANALYZE_DOCUMENT, JOB_INDEX_DOCUMENT_TEXT};
|
||||
use crate::models::{
|
||||
Document, DocumentAsset, DocumentAssetObject, DocumentVersion, NewDocument, NewDocumentTag,
|
||||
NewDocumentVersion, Tag,
|
||||
Document, DocumentAsset, DocumentAssetObject, DocumentType, DocumentVersion, NewDocument,
|
||||
NewDocumentTag, NewDocumentVersion, Tag,
|
||||
};
|
||||
use crate::schema::{
|
||||
document_asset_objects, document_assets, document_correspondents, document_tags,
|
||||
document_versions, documents, folders, refresh_tokens::dsl as refresh_dsl, tags,
|
||||
document_types, document_versions, documents, folders, refresh_tokens::dsl as refresh_dsl,
|
||||
tags,
|
||||
};
|
||||
use crate::state::AppState;
|
||||
use crate::utils::{
|
||||
@@ -65,6 +66,7 @@ pub struct DocumentListQuery {
|
||||
pub query: Option<String>,
|
||||
pub tags: Option<String>,
|
||||
pub correspondents: Option<String>,
|
||||
pub document_types: Option<String>,
|
||||
#[serde(default = "default_document_status_filter")]
|
||||
pub status: DocumentStatusFilter,
|
||||
}
|
||||
@@ -128,6 +130,21 @@ impl From<Tag> for TagResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct DocumentTypeResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl From<DocumentType> for DocumentTypeResponse {
|
||||
fn from(value: DocumentType) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct DocumentResponse {
|
||||
pub id: Uuid,
|
||||
@@ -141,6 +158,10 @@ pub struct DocumentResponse {
|
||||
pub deleted_at: Option<String>,
|
||||
pub issued_at: Option<String>,
|
||||
pub metadata: Value,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub document_type_id: Option<Uuid>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub document_type: Option<DocumentTypeResponse>,
|
||||
pub tags: Vec<TagResponse>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub correspondents: Vec<DocumentCorrespondentResponse>,
|
||||
@@ -181,6 +202,9 @@ pub struct UpdateDocumentRequest {
|
||||
#[serde(default)]
|
||||
#[schema(nullable)]
|
||||
pub metadata: Option<DocumentMetadataUpdate>,
|
||||
#[serde(default)]
|
||||
#[schema(nullable, value_type = Option<Uuid>)]
|
||||
pub document_type_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
@@ -234,6 +258,7 @@ struct DocumentUpdateChangeset {
|
||||
issued_at: Option<Option<NaiveDateTime>>,
|
||||
metadata: Option<Value>,
|
||||
updated_at: Option<NaiveDateTime>,
|
||||
document_type_id: Option<Option<Uuid>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Copy, Clone, PartialEq, Eq, ToSchema)]
|
||||
@@ -346,6 +371,7 @@ pub async fn list_documents(
|
||||
query,
|
||||
tags,
|
||||
correspondents,
|
||||
document_types,
|
||||
status,
|
||||
} = params;
|
||||
|
||||
@@ -378,6 +404,11 @@ pub async fn list_documents(
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_owned());
|
||||
let document_types_param = document_types
|
||||
.as_ref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_owned());
|
||||
|
||||
let include_descendants = include_descendants.unwrap_or(true);
|
||||
|
||||
@@ -517,6 +548,20 @@ pub async fn list_documents(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(document_types_param) = document_types_param.as_ref() {
|
||||
let type_ids: Result<Vec<Uuid>, _> = document_types_param
|
||||
.split(',')
|
||||
.map(|s| Uuid::parse_str(s.trim()))
|
||||
.collect();
|
||||
|
||||
if let Ok(ids) = type_ids {
|
||||
if !ids.is_empty() {
|
||||
let filter_values: Vec<Option<Uuid>> = ids.into_iter().map(Some).collect();
|
||||
docs_query = docs_query.filter(documents::document_type_id.eq_any(filter_values));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref set) = filter_ids {
|
||||
if set.is_empty() {
|
||||
return Ok(Json(vec![]));
|
||||
@@ -565,6 +610,20 @@ 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)?;
|
||||
@@ -576,6 +635,9 @@ pub async fn list_documents(
|
||||
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,
|
||||
@@ -583,6 +645,7 @@ pub async fn list_documents(
|
||||
tags,
|
||||
correspondents,
|
||||
current_version,
|
||||
doc_type,
|
||||
)?);
|
||||
}
|
||||
|
||||
@@ -664,6 +727,8 @@ pub async fn get_document(
|
||||
.find(doc.current_version_id)
|
||||
.first(&mut conn)?;
|
||||
|
||||
let doc_type = load_document_type(&mut conn, tenant_id, doc.document_type_id)?;
|
||||
|
||||
let tags_map = load_tags_for_documents(&mut conn, &[document_id])?;
|
||||
let mut correspondents_map = load_correspondents_for_documents(&mut conn, &[document_id])?;
|
||||
let version_id = current_version.id;
|
||||
@@ -680,6 +745,7 @@ pub async fn get_document(
|
||||
tags_map.get(&document_id).cloned(),
|
||||
correspondents_map.remove(&document_id).unwrap_or_default(),
|
||||
Some((version_response, assets)),
|
||||
doc_type,
|
||||
)?,
|
||||
}))
|
||||
}
|
||||
@@ -1314,6 +1380,46 @@ pub async fn update_document(
|
||||
}
|
||||
}
|
||||
|
||||
let doc_type_update =
|
||||
classify_nullable(payload_obj.get("document_type_id")).map_err(AppError::bad_request)?;
|
||||
|
||||
match doc_type_update {
|
||||
NullableValue::Omitted => {}
|
||||
NullableValue::Null => {
|
||||
if document.document_type_id.is_some() {
|
||||
changes.document_type_id = Some(None);
|
||||
has_changes = true;
|
||||
}
|
||||
}
|
||||
NullableValue::String(raw) => {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(AppError::bad_request(
|
||||
"document_type_id must not be empty when provided",
|
||||
));
|
||||
}
|
||||
let parsed = Uuid::parse_str(trimmed)
|
||||
.map_err(|_| AppError::bad_request("document_type_id must be a valid UUID"))?;
|
||||
|
||||
if document.document_type_id != Some(parsed) {
|
||||
let exists = document_types::table
|
||||
.filter(document_types::tenant_id.eq(tenant_id))
|
||||
.find(parsed)
|
||||
.first::<DocumentType>(&mut conn)
|
||||
.optional()?;
|
||||
|
||||
if exists.is_none() {
|
||||
return Err(AppError::bad_request(
|
||||
"document_type_id does not exist for this tenant",
|
||||
));
|
||||
}
|
||||
|
||||
changes.document_type_id = Some(Some(parsed));
|
||||
has_changes = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !has_changes {
|
||||
return Err(AppError::bad_request("no changes provided"));
|
||||
}
|
||||
@@ -1347,6 +1453,8 @@ pub async fn update_document(
|
||||
.find(document.current_version_id)
|
||||
.first(&mut conn)?;
|
||||
|
||||
let doc_type = load_document_type(&mut conn, tenant_id, document.document_type_id)?;
|
||||
|
||||
if title_changed {
|
||||
if let Err(err) = enqueue_job(
|
||||
&mut conn,
|
||||
@@ -1383,6 +1491,7 @@ pub async fn update_document(
|
||||
tags_map.get(&document_id).cloned(),
|
||||
correspondents_map.remove(&document_id).unwrap_or_default(),
|
||||
Some((version_response, assets)),
|
||||
doc_type,
|
||||
)?,
|
||||
}))
|
||||
}
|
||||
@@ -2005,6 +2114,7 @@ async fn process_upload(
|
||||
document.updated_at = now;
|
||||
}
|
||||
|
||||
let doc_type = load_document_type(&mut conn, tenant_id, document.document_type_id)?;
|
||||
let tags_map = load_tags_for_documents(&mut conn, &[document.id])?;
|
||||
let mut correspondents_map =
|
||||
load_correspondents_for_documents(&mut conn, &[document.id])?;
|
||||
@@ -2028,6 +2138,7 @@ async fn process_upload(
|
||||
tags,
|
||||
correspondents,
|
||||
Some((version_response, assets)),
|
||||
doc_type,
|
||||
)?,
|
||||
}));
|
||||
}
|
||||
@@ -2067,6 +2178,7 @@ async fn process_upload(
|
||||
title: derived_title.clone(),
|
||||
metadata: metadata_value.clone(),
|
||||
tenant_id,
|
||||
document_type_id: None,
|
||||
};
|
||||
diesel::insert_into(documents::table)
|
||||
.values(&new_document)
|
||||
@@ -2125,6 +2237,7 @@ async fn process_upload(
|
||||
)?;
|
||||
}
|
||||
|
||||
let doc_type = load_document_type(&mut conn, tenant_id, document.document_type_id)?;
|
||||
let tags_map = load_tags_for_documents(&mut conn, &[doc_id])?;
|
||||
let mut correspondents_map = load_correspondents_for_documents(&mut conn, &[doc_id])?;
|
||||
let tags = tags_map.get(&doc_id).cloned();
|
||||
@@ -2139,6 +2252,7 @@ async fn process_upload(
|
||||
tags,
|
||||
correspondents,
|
||||
Some((to_version_response(version.clone()), Vec::new())),
|
||||
doc_type,
|
||||
)?,
|
||||
}
|
||||
};
|
||||
@@ -2171,6 +2285,7 @@ pub(crate) fn to_document_response(
|
||||
tags: Option<Vec<Tag>>,
|
||||
correspondents: Vec<DocumentCorrespondentResponse>,
|
||||
current_version: Option<(DocumentVersionResponse, Vec<DocumentAssetResponse>)>,
|
||||
doc_type: Option<DocumentType>,
|
||||
) -> AppResult<DocumentResponse> {
|
||||
let current_version = if let Some((version, assets)) = current_version {
|
||||
let download_path = build_download_path(state, &doc, user_id)?;
|
||||
@@ -2195,6 +2310,11 @@ pub(crate) fn to_document_response(
|
||||
deleted_at: doc.deleted_at.map(to_iso),
|
||||
issued_at: doc.issued_at.map(to_iso),
|
||||
metadata: doc.metadata,
|
||||
document_type_id: doc.document_type_id,
|
||||
document_type: doc_type.map(|typ| DocumentTypeResponse {
|
||||
id: typ.id,
|
||||
name: typ.name,
|
||||
}),
|
||||
tags: tags
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
@@ -2204,3 +2324,19 @@ pub(crate) fn to_document_response(
|
||||
current_version,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_document_type(
|
||||
conn: &mut PgConnection,
|
||||
tenant_id: Uuid,
|
||||
type_id: Option<Uuid>,
|
||||
) -> diesel::QueryResult<Option<DocumentType>> {
|
||||
if let Some(id) = type_id {
|
||||
document_types::table
|
||||
.filter(document_types::tenant_id.eq(tenant_id))
|
||||
.find(id)
|
||||
.first::<DocumentType>(conn)
|
||||
.optional()
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user