This commit is contained in:
2025-10-28 01:18:35 +01:00
parent 1179f4fcd4
commit 7df9a6415a
17 changed files with 935 additions and 935 deletions
+78 -146
View File
@@ -48,10 +48,7 @@ use asset_utils::{
build_download_path, derive_document_title, filename_with_retained_extension,
to_asset_detail_response, to_asset_object_response, to_asset_summary, to_version_response,
};
use correspondent_utils::{
is_valid_correspondent_role, normalize_correspondent_assignments, normalize_role,
CORRESPONDENT_ROLES,
};
use correspondent_utils::normalize_correspondent_assignments;
use search_utils::{build_quickwit_query, extract_document_id};
const PRESIGNED_URL_EXPIRY_SECONDS: u64 = 300;
@@ -186,7 +183,6 @@ pub struct DocumentVersionDetailResponse {
pub struct DocumentCorrespondentResponse {
pub id: Uuid,
pub name: String,
pub role: String,
pub metadata: Value,
pub assigned_at: String,
}
@@ -280,7 +276,6 @@ pub struct BulkCorrespondentResponse {
#[derive(Deserialize, ToSchema)]
pub struct CorrespondentAssignmentInput {
pub correspondent_id: Uuid,
pub role: String,
}
#[derive(Deserialize, ToSchema)]
@@ -319,12 +314,6 @@ pub struct BulkCorrespondentsRequest {
pub action: BulkCorrespondentAction,
}
#[derive(Deserialize, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub struct CorrespondentRoleQuery {
pub role: String,
}
#[derive(Deserialize, ToSchema)]
pub struct BulkReanalyzeSelectionRequest {
pub document_ids: Vec<Uuid>,
@@ -853,7 +842,7 @@ pub async fn upload_document(
})?;
correspondents = serde_json::from_str(&value).map_err(|err| {
let msg = format!(
"correspondents must be a JSON array of {{correspondent_id, role}} objects: {err}"
"correspondents must be a JSON array of {{correspondent_id}} objects: {err}"
);
error!(error = %err, "invalid correspondents json");
AppError::bad_request(msg)
@@ -1672,8 +1661,7 @@ pub async fn assign_correspondents(
return Err(AppError::bad_request("assignments must not be empty"));
}
let (normalized_pairs, _correspondent_ids, roles_vec) =
normalize_correspondent_assignments(&payload.assignments)?;
let correspondent_ids = normalize_correspondent_assignments(&payload.assignments)?;
let replace = payload.replace;
conn.transaction::<(), AppError, _>(|conn| {
@@ -1685,21 +1673,41 @@ pub async fn assign_correspondents(
return Err(AppError::not_found());
}
let mut deleted = 0;
let mut updated = false;
if replace {
deleted = diesel::delete(
document_correspondents::table
.filter(document_correspondents::document_id.eq(document_id))
.filter(document_correspondents::tenant_id.eq(tenant_id))
.filter(document_correspondents::role.eq_any(&roles_vec)),
)
.execute(conn)?;
use diesel::dsl::not;
let base = document_correspondents::table
.filter(document_correspondents::document_id.eq(document_id))
.filter(document_correspondents::tenant_id.eq(tenant_id));
let removed = if correspondent_ids.is_empty() {
diesel::delete(base).execute(conn)?
} else {
diesel::delete(base.filter(not(
document_correspondents::correspondent_id.eq_any(&correspondent_ids),
)))
.execute(conn)?
};
if removed > 0 {
updated = true;
}
}
let inserted =
insert_document_correspondents(conn, tenant_id, &document, user_id, &normalized_pairs)?;
let inserted = insert_document_correspondents(
conn,
tenant_id,
document.id,
user_id,
&correspondent_ids,
)?;
if replace && deleted > 0 && inserted == 0 {
if inserted > 0 {
updated = true;
}
if updated && inserted == 0 {
diesel::update(
documents::table
.find(document_id)
@@ -1731,8 +1739,7 @@ pub async fn bulk_assign_correspondents(
let mut document_ids = payload.document_ids;
validate_bulk_ids(&mut document_ids, "document_ids")?;
let (normalized_pairs, correspondents_vec, _roles_vec) =
normalize_correspondent_assignments(&payload.assignments)?;
let correspondent_ids = normalize_correspondent_assignments(&payload.assignments)?;
let action = payload.action;
let user_id_val = user_id;
let (assigned, removed) = conn.transaction::<(usize, usize), AppError, _>(|conn| {
@@ -1754,12 +1761,12 @@ pub async fn bulk_assign_correspondents(
));
}
if !correspondents_vec.is_empty() {
if !correspondent_ids.is_empty() {
let existing: Vec<Correspondent> = correspondents::table
.filter(correspondents::id.eq_any(&correspondents_vec))
.filter(correspondents::id.eq_any(&correspondent_ids))
.filter(correspondents::tenant_id.eq(tenant_id))
.load(conn)?;
if existing.len() != correspondents_vec.len() {
if existing.len() != correspondent_ids.len() {
return Err(AppError::bad_request(
"one or more correspondents do not exist",
));
@@ -1768,92 +1775,34 @@ pub async fn bulk_assign_correspondents(
match action {
BulkCorrespondentAction::Add => {
use diesel::dsl::not;
let mut grouped_by_role: HashMap<String, Vec<Uuid>> = HashMap::new();
for (correspondent_id, role) in &normalized_pairs {
grouped_by_role
.entry(role.clone())
.or_default()
.push(*correspondent_id);
let mut assigned_total = 0;
for (doc_id, _) in &docs {
assigned_total += insert_document_correspondents(
conn,
tenant_id,
*doc_id,
user_id_val,
&correspondent_ids,
)?;
}
let mut removed = 0;
for (role, ids) in grouped_by_role.iter() {
if ids.is_empty() {
continue;
}
let maintained_ids = ids.clone();
let deleted = diesel::delete(
document_correspondents::table
.filter(document_correspondents::document_id.eq_any(&document_ids))
.filter(document_correspondents::tenant_id.eq(tenant_id))
.filter(document_correspondents::role.eq(role.as_str()))
.filter(not(
document_correspondents::correspondent_id.eq_any(maintained_ids)
)),
)
.execute(conn)?;
removed += deleted;
}
let mut new_rows = Vec::with_capacity(document_ids.len() * normalized_pairs.len());
for doc_id in &document_ids {
for (correspondent_id, role) in &normalized_pairs {
new_rows.push(NewDocumentCorrespondent {
document_id: *doc_id,
correspondent_id: *correspondent_id,
role: role.clone(),
assigned_by: Some(user_id_val),
tenant_id,
});
}
}
let assigned = if new_rows.is_empty() {
0
} else {
diesel::insert_into(document_correspondents::table)
.values(&new_rows)
.on_conflict_do_nothing()
.execute(conn)?
};
if assigned > 0 || removed > 0 {
diesel::update(
documents::table
.filter(documents::id.eq_any(&document_ids))
.filter(documents::tenant_id.eq(tenant_id)),
)
.set(documents::updated_at.eq(Utc::now().naive_utc()))
.execute(conn)?;
}
Ok((assigned, removed))
Ok((assigned_total, 0))
}
BulkCorrespondentAction::Remove => {
let mut removed = 0;
if !normalized_pairs.is_empty() {
let mut grouped: HashMap<String, Vec<Uuid>> = HashMap::new();
for (correspondent_id, role) in &normalized_pairs {
grouped
.entry(role.clone())
.or_default()
.push(*correspondent_id);
}
for (role, ids) in grouped {
removed += diesel::delete(
document_correspondents::table
.filter(document_correspondents::document_id.eq_any(&document_ids))
.filter(document_correspondents::tenant_id.eq(tenant_id))
.filter(document_correspondents::role.eq(role.as_str()))
.filter(document_correspondents::correspondent_id.eq_any(&ids)),
)
.execute(conn)?;
}
if correspondent_ids.is_empty() {
return Ok((0, 0));
}
let removed = diesel::delete(
document_correspondents::table
.filter(document_correspondents::document_id.eq_any(&document_ids))
.filter(document_correspondents::tenant_id.eq(tenant_id))
.filter(
document_correspondents::correspondent_id.eq_any(&correspondent_ids),
),
)
.execute(conn)?;
if removed > 0 {
diesel::update(
documents::table
@@ -1875,24 +1824,12 @@ pub async fn bulk_assign_correspondents(
pub async fn remove_correspondent(
Path((document_id, correspondent_id)): Path<(Uuid, Uuid)>,
Query(query): Query<CorrespondentRoleQuery>,
TenantScopedConn {
mut conn,
tenant_id,
..
}: TenantScopedConn,
) -> AppResult<StatusCode> {
let role = normalize_role(&query.role);
if role.is_empty() {
return Err(AppError::bad_request("role must not be empty"));
}
if !is_valid_correspondent_role(&role) {
return Err(AppError::bad_request(format!(
"invalid correspondent role '{role}'. Allowed roles: {}",
CORRESPONDENT_ROLES.join(", ")
)));
}
let document: Document = documents::table
.find(document_id)
.filter(documents::tenant_id.eq(tenant_id))
@@ -1905,8 +1842,7 @@ pub async fn remove_correspondent(
document_correspondents::table
.filter(document_correspondents::document_id.eq(document_id))
.filter(document_correspondents::tenant_id.eq(tenant_id))
.filter(document_correspondents::correspondent_id.eq(correspondent_id))
.filter(document_correspondents::role.eq(&role)),
.filter(document_correspondents::correspondent_id.eq(correspondent_id)),
)
.execute(&mut conn)?;
@@ -2374,47 +2310,45 @@ fn assign_correspondents_internal(
return Ok(0);
}
let (normalized_pairs, _correspondent_ids, _roles) =
normalize_correspondent_assignments(assignments)?;
let ids = normalize_correspondent_assignments(assignments)?;
insert_document_correspondents(conn, tenant_id, document, user_id, &normalized_pairs)
insert_document_correspondents(conn, tenant_id, document.id, user_id, &ids)
}
fn insert_document_correspondents(
conn: &mut PgConnection,
tenant_id: Uuid,
document: &Document,
document_id: Uuid,
user_id: Uuid,
normalized_pairs: &[(Uuid, String)],
correspondent_ids: &[Uuid],
) -> AppResult<usize> {
if normalized_pairs.is_empty() {
if correspondent_ids.is_empty() {
return Ok(0);
}
let mut correspondent_ids: Vec<Uuid> = normalized_pairs.iter().map(|(id, _)| *id).collect();
correspondent_ids.sort_unstable();
correspondent_ids.dedup();
let mut unique_ids: Vec<Uuid> = correspondent_ids.to_vec();
unique_ids.sort_unstable();
unique_ids.dedup();
if !correspondent_ids.is_empty() {
if !unique_ids.is_empty() {
let existing: Vec<Uuid> = correspondents::table
.filter(correspondents::id.eq_any(&correspondent_ids))
.filter(correspondents::id.eq_any(&unique_ids))
.filter(correspondents::tenant_id.eq(tenant_id))
.select(correspondents::id)
.load(conn)?;
if existing.len() != correspondent_ids.len() {
if existing.len() != unique_ids.len() {
return Err(AppError::bad_request(
"one or more correspondents do not exist",
));
}
}
let new_rows: Vec<NewDocumentCorrespondent> = normalized_pairs
.iter()
.map(|(correspondent_id, role)| NewDocumentCorrespondent {
document_id: document.id,
correspondent_id: *correspondent_id,
role: role.clone(),
let new_rows: Vec<NewDocumentCorrespondent> = unique_ids
.into_iter()
.map(|correspondent_id| NewDocumentCorrespondent {
document_id,
correspondent_id,
assigned_by: Some(user_id),
tenant_id,
})
@@ -2432,7 +2366,7 @@ fn insert_document_correspondents(
if inserted > 0 {
diesel::update(
documents::table
.find(document.id)
.find(document_id)
.filter(documents::tenant_id.eq(tenant_id)),
)
.set(documents::updated_at.eq(Utc::now().naive_utc()))
@@ -2490,7 +2424,6 @@ pub(crate) fn load_correspondents_for_documents(
.filter(document_correspondents::document_id.eq_any(document_ids))
.order((
document_correspondents::document_id.asc(),
document_correspondents::role.asc(),
document_correspondents::assigned_at.asc(),
))
.load(conn)?;
@@ -2502,7 +2435,6 @@ pub(crate) fn load_correspondents_for_documents(
.push(DocumentCorrespondentResponse {
id: correspondent.id,
name: correspondent.name,
role: assignment.role,
metadata: correspondent.metadata,
assigned_at: to_iso(assignment.assigned_at),
});