backend 409 on document conflict
This commit is contained in:
@@ -4,6 +4,7 @@ use axum::{
|
||||
Json,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub type AppResult<T> = Result<T, AppError>;
|
||||
@@ -13,6 +14,7 @@ pub struct AppError {
|
||||
status: StatusCode,
|
||||
message: String,
|
||||
code: Option<String>,
|
||||
details: Option<Value>,
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
@@ -21,6 +23,7 @@ impl AppError {
|
||||
status,
|
||||
message: message.into(),
|
||||
code: None,
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +51,11 @@ impl AppError {
|
||||
self.code = Some(code.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_details(mut self, details: Value) -> Self {
|
||||
self.details = Some(details);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
@@ -56,6 +64,7 @@ impl IntoResponse for AppError {
|
||||
let body = Json(ErrorResponse {
|
||||
error: self.message,
|
||||
code: self.code,
|
||||
details: self.details,
|
||||
});
|
||||
(status, body).into_response()
|
||||
}
|
||||
@@ -66,6 +75,8 @@ struct ErrorResponse {
|
||||
error: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
code: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
details: Option<Value>,
|
||||
}
|
||||
|
||||
impl From<diesel::result::Error> for AppError {
|
||||
|
||||
@@ -303,7 +303,6 @@ struct UploadRequest {
|
||||
enum UploadOutcome {
|
||||
Created(DocumentDetailResponse),
|
||||
Reused(DocumentDetailResponse),
|
||||
Skipped { document_id: Uuid },
|
||||
}
|
||||
|
||||
#[derive(ToSchema)]
|
||||
@@ -322,7 +321,7 @@ pub struct UploadDocumentForm {
|
||||
pub correspondents: Option<Vec<CorrespondentAssignmentInput>>,
|
||||
#[schema(nullable, example = "2024-01-01T00:00:00Z")]
|
||||
pub issued_at: Option<String>,
|
||||
#[schema(nullable)]
|
||||
#[schema(nullable, default = true)]
|
||||
pub skip_existing: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -713,7 +712,7 @@ pub async fn get_document(
|
||||
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")
|
||||
(status = 409, description = "Document with identical contents already exists")
|
||||
),
|
||||
tag = "Documents"
|
||||
)]
|
||||
@@ -732,7 +731,7 @@ pub async fn upload_document(
|
||||
let mut tag_ids: Vec<Uuid> = Vec::new();
|
||||
let mut correspondents: Vec<CorrespondentAssignmentInput> = Vec::new();
|
||||
let mut issued_at_override: Option<NaiveDateTime> = None;
|
||||
let mut skip_if_existing = false;
|
||||
let mut skip_if_existing = true;
|
||||
let mut title_override: Option<String> = None;
|
||||
|
||||
while let Some(field) = multipart.next_field().await.map_err(|err| {
|
||||
@@ -913,10 +912,6 @@ pub async fn upload_document(
|
||||
);
|
||||
(StatusCode::OK, Json(detail)).into_response()
|
||||
}
|
||||
UploadOutcome::Skipped { document_id } => {
|
||||
info!(document_id = %document_id, "document upload skipped by client request");
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
@@ -2138,11 +2133,15 @@ async fn process_upload(
|
||||
info!(
|
||||
document_id = %document.id,
|
||||
checksum = %checksum_hex,
|
||||
"upload skipped existing document due to skip flag",
|
||||
"upload rejected because document already exists",
|
||||
);
|
||||
return Err(
|
||||
AppError::conflict("a document with the same contents already exists")
|
||||
.with_code("duplicate_document")
|
||||
.with_details(json!({
|
||||
"conflict_document_id": document.id,
|
||||
})),
|
||||
);
|
||||
return Ok(UploadOutcome::Skipped {
|
||||
document_id: document.id,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(issued_at) = issued_at_override {
|
||||
|
||||
Reference in New Issue
Block a user