auditlog 0.1.0

Audit trail for your data models — an ORM-agnostic core with a pluggable, async sqlx backend (SQLite & Postgres).
Documentation
//! The persisted audit record and its in-flight (pre-insert) form.

use chrono::{DateTime, Utc};
use indexmap::IndexMap;

use crate::action::Action;
use crate::actor::Actor;
use crate::changes::{AuditedChanges, ChangeValue, ValueMap};
use crate::id::AuditId;

/// One row in the `audits` table — an immutable record of a single change.
#[derive(Clone, Debug, PartialEq)]
pub struct Audit {
    /// Primary key.
    pub id: i64,
    /// Polymorphic type of the audited record, e.g. `"User"`.
    pub auditable_type: String,
    /// Id of the audited record.
    pub auditable_id: AuditId,
    /// Polymorphic type of the associated/parent record, if any.
    pub associated_type: Option<String>,
    /// Id of the associated/parent record, if any.
    pub associated_id: Option<AuditId>,
    /// Polymorphic type of the acting user, when the user is a record.
    pub user_type: Option<String>,
    /// Id of the acting user, when the user is a record.
    pub user_id: Option<AuditId>,
    /// The acting user as a plain string (mutually exclusive with `user_id`/`user_type`).
    pub username: Option<String>,
    /// What happened.
    pub action: Action,
    /// The change payload (shape depends on `action`).
    pub audited_changes: AuditedChanges,
    /// Per-auditable monotonic version (1-based).
    pub version: i32,
    /// Optional comment.
    pub comment: Option<String>,
    /// Client remote address at the time of the change.
    pub remote_address: Option<String>,
    /// Correlation id grouping audits from the same request.
    pub request_uuid: Option<String>,
    /// When the change occurred.
    pub created_at: DateTime<Utc>,
}

impl Audit {
    /// The acting user, reconstructed from the stored columns (a [`Actor::Record`] if
    /// `user_id`/`user_type` are present, else the `username` string).
    pub fn user(&self) -> Option<Actor> {
        Actor::from_columns(
            self.user_id.clone(),
            self.user_type.clone(),
            self.username.clone(),
        )
    }

    /// The associated/parent record reference, if any.
    pub fn associated(&self) -> Option<(String, AuditId)> {
        match (&self.associated_type, &self.associated_id) {
            (Some(t), Some(i)) => Some((t.clone(), i.clone())),
            _ => None,
        }
    }

    /// The changed attributes with their *new* values (action-aware).
    pub fn new_attributes(&self) -> ValueMap {
        self.audited_changes.new_attributes(self.action)
    }

    /// The changed attributes with their *old* values (action-aware).
    pub fn old_attributes(&self) -> ValueMap {
        self.audited_changes.old_attributes(self.action)
    }

    /// The change set interpreted into typed [`ChangeValue`]s.
    pub fn changes(&self) -> IndexMap<String, ChangeValue> {
        self.audited_changes.typed(self.action)
    }

    /// Describe how to reverse this audit. Because this crate does not own your
    /// records' persistence, it returns an [`UndoPlan`] describing the operation to apply with
    /// your ORM rather than performing it directly.
    pub fn undo_plan(&self) -> crate::error::Result<UndoPlan> {
        match self.action {
            Action::Create => Ok(UndoPlan::Delete),
            Action::Destroy => Ok(UndoPlan::Recreate(self.new_attributes())),
            Action::Update => Ok(UndoPlan::Restore(self.old_attributes())),
        }
    }
}

/// The reverse operation for an audit, produced by [`Audit::undo_plan`].
#[derive(Clone, Debug, PartialEq)]
pub enum UndoPlan {
    /// Delete the auditable record (undo of a `create`).
    Delete,
    /// Re-create the record from this attribute snapshot (undo of a `destroy`).
    Recreate(ValueMap),
    /// Restore the record to these old attribute values (undo of an `update`).
    Restore(ValueMap),
}

/// An audit about to be written. The `version` is assigned by the backend at insert time (forced
/// to `1` for `create`, otherwise `max(version) + 1` for the auditable).
#[derive(Clone, Debug)]
pub struct NewAudit {
    /// Polymorphic type of the audited record.
    pub auditable_type: String,
    /// Id of the audited record.
    pub auditable_id: AuditId,
    /// Associated/parent record type.
    pub associated_type: Option<String>,
    /// Associated/parent record id.
    pub associated_id: Option<AuditId>,
    /// Acting user type.
    pub user_type: Option<String>,
    /// Acting user id.
    pub user_id: Option<AuditId>,
    /// Acting username.
    pub username: Option<String>,
    /// The action.
    pub action: Action,
    /// The change payload.
    pub audited_changes: AuditedChanges,
    /// Optional comment.
    pub comment: Option<String>,
    /// Remote address.
    pub remote_address: Option<String>,
    /// Request correlation id.
    pub request_uuid: Option<String>,
    /// Timestamp.
    pub created_at: DateTime<Utc>,
}