use crate::core::condition::SqlValue;
use crate::orm::postgres::model::PgModel;
pub struct AuditService<M>(std::marker::PhantomData<M>);
#[derive(Debug, Clone)]
pub struct AuditEntry {
pub table_name: String,
pub record_id: String,
pub operation: String,
pub old_data: Option<serde_json::Value>,
pub new_data: Option<serde_json::Value>,
pub changed_at: chrono::DateTime<chrono::Utc>,
}
impl<M: PgModel> AuditService<M> {
pub async fn touch(
id: impl Into<SqlValue> + Send,
pool: &sqlx::PgPool,
) -> Result<u64, sqlx::Error> {
M::touch_by_pk(pool, id.into()).await
}
pub async fn history(
id: impl Into<SqlValue> + Send,
pool: &sqlx::PgPool,
) -> Result<Vec<AuditEntry>, sqlx::Error> {
let id_str = id_to_string(id.into());
let table = M::table_name();
let rows = sqlx::query(
"SELECT table_name, record_id, operation, old_data, new_data, changed_at \
FROM audit_log \
WHERE table_name = $1 AND record_id = $2 \
ORDER BY changed_at DESC",
)
.bind(table)
.bind(&id_str)
.fetch_all(pool)
.await;
match rows {
Ok(rows) => {
use sqlx::Row;
rows.into_iter()
.map(|r| {
Ok(AuditEntry {
table_name: r.try_get("table_name")?,
record_id: r.try_get("record_id")?,
operation: r.try_get("operation")?,
old_data: r.try_get("old_data")?,
new_data: r.try_get("new_data")?,
changed_at: r.try_get("changed_at")?,
})
})
.collect()
}
Err(sqlx::Error::Database(ref e))
if e.message().contains("audit_log") || e.message().contains("does not exist") =>
{
Ok(Vec::new())
}
Err(e) => Err(e),
}
}
}
fn id_to_string(val: SqlValue) -> String {
match val {
SqlValue::Integer(n) => n.to_string(),
SqlValue::Text(s) => s,
SqlValue::Uuid(u) => u.to_string(),
other => format!("{other:?}"),
}
}