use ulid::Ulid;
use crate::document::{Document, Value};
use crate::engine::StorageBackend;
use crate::error::TalaDbError;
use crate::time::now_ms;
pub(crate) const AUDIT_TABLE: &str = "docs::_audit";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuditOp {
Insert,
Update,
Delete,
}
impl AuditOp {
pub fn as_str(&self) -> &'static str {
match self {
Self::Insert => "insert",
Self::Update => "update",
Self::Delete => "delete",
}
}
}
#[derive(Debug, Clone)]
pub struct AuditEntry {
pub id: Ulid,
pub collection: String,
pub op: AuditOp,
pub doc_id: String,
pub ts: i64,
pub caller: String,
}
pub(crate) fn write_audit_entry(
wtxn: &mut dyn crate::engine::WriteTxn,
collection: &str,
op: AuditOp,
doc_id: &str,
caller: &str,
) -> Result<(), TalaDbError> {
let entry_id = crate::document::new_ulid_pub();
let doc = Document {
id: entry_id,
fields: vec![
("collection".into(), Value::Str(collection.into())),
("op".into(), Value::Str(op.as_str().into())),
("doc_id".into(), Value::Str(doc_id.into())),
("ts".into(), Value::Int(now_ms() as i64)),
("caller".into(), Value::Str(caller.into())),
],
};
let doc_bytes = postcard::to_allocvec(&doc)?;
wtxn.put(AUDIT_TABLE, &entry_id.to_bytes(), &doc_bytes)?;
Ok(())
}
pub fn read_audit_log(
backend: &dyn StorageBackend,
collection_filter: Option<&str>,
op_filter: Option<AuditOp>,
) -> Result<Vec<AuditEntry>, TalaDbError> {
read_audit_log_since(backend, None, collection_filter, op_filter)
}
pub fn read_audit_log_since(
backend: &dyn StorageBackend,
since: Option<Ulid>,
collection_filter: Option<&str>,
op_filter: Option<AuditOp>,
) -> Result<Vec<AuditEntry>, TalaDbError> {
let rtxn = backend.begin_read()?;
let pairs = match since {
Some(s) => {
let start_bytes = s.to_bytes();
rtxn.range(
AUDIT_TABLE,
std::ops::Bound::Excluded(&start_bytes[..]),
std::ops::Bound::Unbounded,
)
.unwrap_or_default()
}
None => rtxn.scan_all(AUDIT_TABLE).unwrap_or_default(),
};
drop(rtxn);
let mut entries = Vec::with_capacity(pairs.len());
for (_, v) in pairs {
let doc: Document = postcard::from_bytes(&v)?;
let collection = match doc.get("collection") {
Some(Value::Str(s)) => s.clone(),
_ => continue,
};
let op_str = match doc.get("op") {
Some(Value::Str(s)) => s.as_str(),
_ => continue,
};
let op = match op_str {
"insert" => AuditOp::Insert,
"update" => AuditOp::Update,
"delete" => AuditOp::Delete,
_ => continue,
};
let doc_id = match doc.get("doc_id") {
Some(Value::Str(s)) => s.clone(),
_ => continue,
};
let ts = match doc.get("ts") {
Some(Value::Int(i)) => *i,
_ => 0,
};
let caller = match doc.get("caller") {
Some(Value::Str(s)) => s.clone(),
_ => String::new(),
};
if let Some(cf) = collection_filter
&& collection != cf
{
continue;
}
if let Some(ref of_) = op_filter
&& &op != of_
{
continue;
}
entries.push(AuditEntry {
id: doc.id,
collection,
op,
doc_id,
ts,
caller,
});
}
Ok(entries)
}