auditlog 0.1.0

Audit trail for your data models — an ORM-agnostic core with a pluggable, async sqlx backend (SQLite & Postgres).
Documentation
//! The pluggable persistence layer.
//!
//! [`Backend`] is the one trait you implement (or use the built-in [`crate::SqlxBackend`]) to give
//! `auditlog` somewhere to store audits. It is intentionally small: insert, query, count, and the
//! two mutations needed for `max_audits` pruning.

use async_trait::async_trait;
use chrono::{DateTime, Utc};

use crate::audit::{Audit, NewAudit};
use crate::changes::AuditedChanges;
use crate::error::Result;
use crate::id::AuditId;

mod memory;
pub use memory::MemoryBackend;

#[cfg(any(feature = "sqlite", feature = "postgres"))]
mod sqlx_backend;
#[cfg(any(feature = "sqlite", feature = "postgres"))]
pub use sqlx_backend::SqlxBackend;

/// Result ordering for an audit query.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Order {
    /// By `version` ascending. This is the default ordering for a record's audits.
    #[default]
    VersionAsc,
    /// By `version` descending.
    VersionDesc,
    /// By `created_at` ascending.
    CreatedAtAsc,
    /// By `created_at` descending. This is the ordering used when combining a record's own
    /// and associated audits.
    CreatedAtDesc,
}

/// A composable audit query supporting filters by action (`creates`/`updates`/`destroys`),
/// version range (`from_version`/`to_version`), `up_until`, and ascending/descending ordering.
#[derive(Clone, Debug, Default)]
pub struct AuditQuery {
    /// Restrict to one action (`creates`/`updates`/`destroys`).
    pub action: Option<crate::action::Action>,
    /// `version >= from_version`.
    pub from_version: Option<i32>,
    /// `version <= to_version`.
    pub to_version: Option<i32>,
    /// `created_at <= up_until`.
    pub up_until: Option<DateTime<Utc>>,
    /// Result ordering.
    pub order: Order,
    /// Maximum rows.
    pub limit: Option<i64>,
    /// Row offset.
    pub offset: Option<i64>,
}

impl AuditQuery {
    /// Whether a given audit matches this query's filters (used by in-memory backends).
    pub fn matches(&self, audit: &Audit) -> bool {
        if let Some(a) = self.action
            && audit.action != a
        {
            return false;
        }
        if let Some(v) = self.from_version
            && audit.version < v
        {
            return false;
        }
        if let Some(v) = self.to_version
            && audit.version > v
        {
            return false;
        }
        if let Some(t) = self.up_until
            && audit.created_at > t
        {
            return false;
        }
        true
    }
}

/// Where to store and how to retrieve audits.
///
/// Implementations must:
/// * assign `version` at insert time — `1` for [`crate::Action::Create`], otherwise
///   `max(version) + 1` for the `(auditable_type, auditable_id)` — ideally inside a transaction
///   guarded by a unique constraint on `(auditable_type, auditable_id, version)`.
/// * apply [`AuditQuery`] filters and ordering exactly.
#[async_trait]
pub trait Backend: Send + Sync {
    /// Insert a new audit, assigning its `version`, and return the persisted row.
    async fn insert(&self, audit: NewAudit) -> Result<Audit>;

    /// All audits for one auditable record, filtered/ordered by `query`.
    async fn audits_for_auditable(
        &self,
        auditable_type: &str,
        auditable_id: &AuditId,
        query: &AuditQuery,
    ) -> Result<Vec<Audit>>;

    /// All audits whose `associated` points at the given record (the record's associated audits).
    async fn audits_for_associated(
        &self,
        associated_type: &str,
        associated_id: &AuditId,
        query: &AuditQuery,
    ) -> Result<Vec<Audit>>;

    /// Union of a record's own audits and its associated audits, ordered by `created_at`
    /// descending, newest first.
    async fn own_and_associated_audits(
        &self,
        auditable_type: &str,
        auditable_id: &AuditId,
    ) -> Result<Vec<Audit>>;

    /// Count of audits for one auditable record.
    async fn count_for_auditable(
        &self,
        auditable_type: &str,
        auditable_id: &AuditId,
    ) -> Result<i64>;

    /// Fetch a single audit by primary key.
    async fn find(&self, id: i64) -> Result<Option<Audit>>;

    /// Combine pruned audits: overwrite `target`'s changes/comment and delete the `older_ids`,
    /// atomically. Used by `max_audits` pruning. Deadlock-class errors should be treated as
    /// success (a concurrent identical combine already won).
    async fn combine(
        &self,
        target_id: i64,
        merged_changes: &AuditedChanges,
        comment: Option<&str>,
        older_ids: &[i64],
    ) -> Result<()>;
}