feat(db): add catalogue object update/delete with audited field diffs
update_object records only changed fields as audit diffs and skips the audit entry for no-op updates; delete_object records a Deleted entry. Both operations are atomic on the caller's connection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -157,8 +157,6 @@ fn creation_changes(input: &ObjectInput) -> Vec<FieldChange> {
|
||||
}
|
||||
|
||||
/// Audit changes between two field sets: only the fields whose value changed.
|
||||
/// (Used by `update_object` in the next task.)
|
||||
#[allow(dead_code)]
|
||||
fn update_changes(old: &ObjectInput, new: &ObjectInput) -> Vec<FieldChange> {
|
||||
field_values(old)
|
||||
.into_iter()
|
||||
@@ -176,3 +174,86 @@ fn update_changes(old: &ObjectInput, new: &ObjectInput) -> Vec<FieldChange> {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Update an object and record an `updated` audit entry with field-level diffs,
|
||||
/// both on `conn`. Returns `false` if the object does not exist. A no-op update
|
||||
/// (no fields changed) records no audit entry.
|
||||
pub async fn update_object(
|
||||
conn: &mut sqlx::PgConnection,
|
||||
actor: AuditActor,
|
||||
id: ObjectId,
|
||||
input: &ObjectInput,
|
||||
) -> Result<bool, sqlx::Error> {
|
||||
let Some(old) = object_by_id(&mut *conn, id).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE object SET \
|
||||
object_number = $2, object_name = $3, number_of_objects = $4, \
|
||||
brief_description = $5, current_location = $6, current_owner = $7, \
|
||||
recorder = $8, recording_date = $9, visibility = $10, updated_at = now() \
|
||||
WHERE id = $1",
|
||||
)
|
||||
.bind(id.to_uuid())
|
||||
.bind(&input.object_number)
|
||||
.bind(&input.object_name)
|
||||
.bind(input.number_of_objects)
|
||||
.bind(input.brief_description.as_deref())
|
||||
.bind(input.current_location.as_deref())
|
||||
.bind(input.current_owner.as_deref())
|
||||
.bind(input.recorder.as_deref())
|
||||
.bind(input.recording_date)
|
||||
.bind(input.visibility.as_str())
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
let changes = update_changes(&old.to_input(), input);
|
||||
|
||||
if !changes.is_empty() {
|
||||
audit::record(
|
||||
&mut *conn,
|
||||
&NewAuditEvent {
|
||||
actor,
|
||||
action: AuditAction::Updated,
|
||||
entity_type: ENTITY_TYPE.to_owned(),
|
||||
entity_id: id.to_uuid(),
|
||||
changes,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Delete an object and record a `deleted` audit entry, both on `conn`.
|
||||
/// Returns `false` if the object did not exist.
|
||||
pub async fn delete_object(
|
||||
conn: &mut sqlx::PgConnection,
|
||||
actor: AuditActor,
|
||||
id: ObjectId,
|
||||
) -> Result<bool, sqlx::Error> {
|
||||
if object_by_id(&mut *conn, id).await?.is_none() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
sqlx::query("DELETE FROM object WHERE id = $1")
|
||||
.bind(id.to_uuid())
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
audit::record(
|
||||
&mut *conn,
|
||||
&NewAuditEvent {
|
||||
actor,
|
||||
action: AuditAction::Deleted,
|
||||
entity_type: ENTITY_TYPE.to_owned(),
|
||||
entity_id: id.to_uuid(),
|
||||
changes: Vec::new(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user