use chrono::{DateTime, Utc};
use cortex_core::AuditRecordId;
use rusqlite::{params, Row};
use serde_json::Value;
use crate::{Pool, StoreResult};
#[derive(Debug, Clone, PartialEq)]
pub struct AuditEntry {
pub id: AuditRecordId,
pub operation: String,
pub target_ref: String,
pub before_hash: Option<String>,
pub after_hash: String,
pub reason: String,
pub actor_json: Value,
pub source_refs_json: Value,
pub created_at: DateTime<Utc>,
}
#[derive(Debug)]
pub struct AuditRepo<'a> {
pool: &'a Pool,
}
impl<'a> AuditRepo<'a> {
#[must_use]
pub const fn new(pool: &'a Pool) -> Self {
Self { pool }
}
pub fn append(&self, entry: &AuditEntry) -> StoreResult<()> {
self.pool.execute(
"INSERT INTO audit_records (
id, operation, target_ref, before_hash, after_hash, reason,
actor_json, source_refs_json, created_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);",
params![
entry.id.to_string(),
entry.operation,
entry.target_ref,
entry.before_hash,
entry.after_hash,
entry.reason,
serde_json::to_string(&entry.actor_json)?,
serde_json::to_string(&entry.source_refs_json)?,
entry.created_at.to_rfc3339(),
],
)?;
Ok(())
}
pub fn list_by_target_ref(&self, target_ref: &str) -> StoreResult<Vec<AuditEntry>> {
let mut stmt = self.pool.prepare(
"SELECT id, operation, target_ref, before_hash, after_hash, reason,
actor_json, source_refs_json, created_at
FROM audit_records
WHERE target_ref = ?1
ORDER BY created_at, id;",
)?;
let rows = stmt.query_map(params![target_ref], audit_row)?;
let mut entries = Vec::new();
for row in rows {
entries.push(row?.try_into()?);
}
Ok(entries)
}
}
#[derive(Debug)]
struct AuditEntryRow {
id: String,
operation: String,
target_ref: String,
before_hash: Option<String>,
after_hash: String,
reason: String,
actor_json: String,
source_refs_json: String,
created_at: String,
}
fn audit_row(row: &Row<'_>) -> rusqlite::Result<AuditEntryRow> {
Ok(AuditEntryRow {
id: row.get(0)?,
operation: row.get(1)?,
target_ref: row.get(2)?,
before_hash: row.get(3)?,
after_hash: row.get(4)?,
reason: row.get(5)?,
actor_json: row.get(6)?,
source_refs_json: row.get(7)?,
created_at: row.get(8)?,
})
}
impl TryFrom<AuditEntryRow> for AuditEntry {
type Error = crate::StoreError;
fn try_from(row: AuditEntryRow) -> StoreResult<Self> {
Ok(Self {
id: row.id.parse()?,
operation: row.operation,
target_ref: row.target_ref,
before_hash: row.before_hash,
after_hash: row.after_hash,
reason: row.reason,
actor_json: serde_json::from_str(&row.actor_json)?,
source_refs_json: serde_json::from_str(&row.source_refs_json)?,
created_at: chrono::DateTime::parse_from_rfc3339(&row.created_at)?.with_timezone(&Utc),
})
}
}