use serde_json::{Map, Value, json};
use crate::migrate::{Column, ModelMeta};
use crate::orm::SqlType;
pub const AUDIT_TABLE: &str = "umbral_audit";
pub const CREATE: &str = "create";
pub const UPDATE: &str = "update";
pub const DELETE: &str = "delete";
pub fn audit_meta() -> ModelMeta {
let col = |name: &str, ty: SqlType, nullable: bool| Column {
name: name.to_string(),
ty,
nullable,
..Column::default()
};
ModelMeta {
view: None,
materialized: false,
name: "UmbralAudit".to_string(),
table: AUDIT_TABLE.to_string(),
fields: vec![
Column {
name: "id".to_string(),
ty: SqlType::BigInt,
primary_key: true,
..Column::default()
},
col("table_name", SqlType::Text, false),
col("row_pk", SqlType::Text, false),
col("action", SqlType::Text, false),
col("actor", SqlType::Text, true),
col("at", SqlType::Timestamptz, false),
col("changes", SqlType::Text, false),
],
ordering: vec![("id".to_string(), true)],
..ModelMeta::default()
}
}
fn diff(before: Option<&Map<String, Value>>, after: Option<&Map<String, Value>>) -> Value {
let mut out = Map::new();
match (before, after) {
(None, Some(a)) => {
for (k, v) in a {
out.insert(k.clone(), json!({ "from": Value::Null, "to": v }));
}
}
(Some(b), None) => {
for (k, v) in b {
out.insert(k.clone(), json!({ "from": v, "to": Value::Null }));
}
}
(Some(b), Some(a)) => {
for (k, new) in a {
let old = b.get(k).unwrap_or(&Value::Null);
if old != new {
out.insert(k.clone(), json!({ "from": old, "to": new }));
}
}
}
(None, None) => {}
}
Value::Object(out)
}
pub async fn record(
meta: &ModelMeta,
row_pk: &str,
action: &str,
before: Option<&Map<String, Value>>,
after: Option<&Map<String, Value>>,
) {
if !meta.audited {
return;
}
let changes = diff(before, after);
if action == UPDATE && changes.as_object().is_some_and(Map::is_empty) {
return;
}
let mut body = Map::new();
body.insert("table_name".into(), json!(meta.table));
body.insert("row_pk".into(), json!(row_pk));
body.insert("action".into(), json!(action));
body.insert(
"actor".into(),
match crate::db::route_context::current_user_id() {
Some(u) => json!(u),
None => Value::Null,
},
);
body.insert("at".into(), json!(chrono::Utc::now()));
body.insert("changes".into(), json!(changes.to_string()));
let audit = audit_meta();
if let Err(e) = crate::orm::dynamic::DynQuerySet::for_meta(&audit)
.insert_json(&body)
.await
{
tracing::error!(
table = %meta.table,
row_pk = %row_pk,
action = %action,
"umbral: failed to write audit row: {e:?}",
);
}
}
pub async fn record_many(
meta: &ModelMeta,
action: &str,
rows: Vec<(
String,
Option<Map<String, Value>>,
Option<Map<String, Value>>,
)>,
) {
if !meta.audited {
return;
}
for (pk, before, after) in rows {
record(meta, &pk, action, before.as_ref(), after.as_ref()).await;
}
}
pub fn pk_in_condition(meta: &ModelMeta, pks: &[Value]) -> Option<sea_query::Condition> {
use sea_query::{Alias, Expr};
let pk = meta.pk_column()?;
if pks.is_empty() {
return None;
}
let vals: Vec<sea_query::Value> = pks
.iter()
.filter_map(|v| crate::orm::write::json_to_sea_value(pk.ty, v, false, &pk.name, None).ok())
.collect();
if vals.is_empty() {
return None;
}
Some(sea_query::Condition::all().add(Expr::col(Alias::new(&pk.name)).is_in(vals)))
}
pub fn pk_of(meta: &ModelMeta, row: &Map<String, Value>) -> String {
meta.pk_column()
.and_then(|pk| row.get(&pk.name))
.map(|v| match v {
Value::String(s) => s.clone(),
other => other.to_string(),
})
.unwrap_or_default()
}