use sea_orm::{
ColumnTrait, ConnectionTrait, EntityTrait, Order, QueryFilter, QueryOrder, QuerySelect,
};
use crate::actor::AuditActor;
use crate::entity::{self, Column};
use crate::entry::AuditEntry;
use crate::error::AuditError;
use crate::target::AuditTarget;
pub async fn history_for_target<C: ConnectionTrait>(
target: &AuditTarget,
conn: &C,
) -> Result<Vec<AuditEntry>, AuditError> {
let entries = entity::Entity::find()
.filter(Column::TargetKind.eq(target.kind.clone()))
.filter(Column::TargetId.eq(target.id.clone()))
.order_by(Column::CreatedAt, Order::Asc)
.all(conn)
.await?;
Ok(entries)
}
pub async fn recent_by_actor<C: ConnectionTrait>(
actor: &AuditActor,
conn: &C,
limit: u64,
) -> Result<Vec<AuditEntry>, AuditError> {
let mut query = entity::Entity::find().filter(Column::ActorKind.eq(actor.kind()));
match actor.id() {
Some(id) => {
query = query.filter(Column::ActorId.eq(id.to_string()));
}
None => {
query = query.filter(Column::ActorId.is_null());
}
}
let entries = query
.order_by(Column::CreatedAt, Order::Desc)
.limit(limit)
.all(conn)
.await?;
Ok(entries)
}
pub async fn recent<C: ConnectionTrait>(
conn: &C,
limit: u64,
) -> Result<Vec<AuditEntry>, AuditError> {
let entries = entity::Entity::find()
.order_by(Column::CreatedAt, Order::Desc)
.limit(limit)
.all(conn)
.await?;
Ok(entries)
}
#[cfg(test)]
mod tests {
use super::*;
use sea_orm::{Database, DatabaseConnection};
use sea_orm_migration::prelude::*;
use std::time::Duration;
struct TestMigrator;
#[async_trait::async_trait]
impl MigratorTrait for TestMigrator {
fn migrations() -> Vec<Box<dyn sea_orm_migration::MigrationTrait>> {
vec![Box::new(crate::migration::Migration)]
}
}
async fn fresh_db() -> DatabaseConnection {
let conn = Database::connect("sqlite::memory:")
.await
.expect("connect sqlite::memory:");
TestMigrator::up(&conn, None).await.expect("run migration");
conn
}
#[tokio::test]
async fn history_ordering() {
let conn = fresh_db().await;
let target = AuditTarget::new("inventory.unit", "abc");
let e1 = AuditEntry::record("inventory.unit.created")
.target(target.clone())
.write(&conn)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(1100)).await;
let e2 = AuditEntry::record("inventory.unit.updated")
.target(target.clone())
.write(&conn)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(1100)).await;
let e3 = AuditEntry::record("inventory.unit.deleted")
.target(target.clone())
.write(&conn)
.await
.unwrap();
let entries = history_for_target(&target, &conn).await.unwrap();
assert_eq!(entries.len(), 3);
assert!(entries[0].created_at <= entries[1].created_at);
assert!(entries[1].created_at <= entries[2].created_at);
let ids: std::collections::HashSet<_> = entries.iter().map(|e| e.id).collect();
assert!(ids.contains(&e1.id));
assert!(ids.contains(&e2.id));
assert!(ids.contains(&e3.id));
let other_target = AuditTarget::new("inventory.unit", "xyz");
let other = history_for_target(&other_target, &conn).await.unwrap();
assert_eq!(other.len(), 0);
}
#[tokio::test]
async fn recent_by_actor_test() {
let conn = fresh_db().await;
let alice = AuditActor::User("alice".into());
let bob = AuditActor::User("bob".into());
for i in 0..3 {
AuditEntry::record("test.event")
.actor(alice.clone())
.target(AuditTarget::new("test", format!("a{i}")))
.write(&conn)
.await
.unwrap();
}
for i in 0..2 {
AuditEntry::record("test.event")
.actor(bob.clone())
.target(AuditTarget::new("test", format!("b{i}")))
.write(&conn)
.await
.unwrap();
}
let alice_entries = recent_by_actor(&alice, &conn, 10).await.unwrap();
assert_eq!(alice_entries.len(), 3);
assert!(alice_entries
.iter()
.all(|e| e.actor_id == Some("alice".into())));
let alice_limited = recent_by_actor(&alice, &conn, 2).await.unwrap();
assert_eq!(alice_limited.len(), 2);
let bob_entries = recent_by_actor(&bob, &conn, 10).await.unwrap();
assert_eq!(bob_entries.len(), 2);
AuditEntry::record("system.cleanup")
.actor(AuditActor::System)
.target(AuditTarget::new("system", "task"))
.write(&conn)
.await
.unwrap();
let system_entries = recent_by_actor(&AuditActor::System, &conn, 10)
.await
.unwrap();
assert_eq!(system_entries.len(), 1);
assert_eq!(system_entries[0].actor_kind, "system");
assert_eq!(system_entries[0].actor_id, None);
}
#[tokio::test]
async fn recent_global() {
let conn = fresh_db().await;
for i in 0..5 {
AuditEntry::record("test.event")
.target(AuditTarget::new("test", format!("t{i}")))
.write(&conn)
.await
.unwrap();
}
let entries = recent(&conn, 3).await.unwrap();
assert_eq!(entries.len(), 3);
assert!(entries[0].created_at >= entries[1].created_at);
assert!(entries[1].created_at >= entries[2].created_at);
let none = recent(&conn, 0).await.unwrap();
assert_eq!(none.len(), 0);
let all = recent(&conn, 100).await.unwrap();
assert_eq!(all.len(), 5);
}
}