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