use cratefield_core::{Database, DbError, Row, Statement};
use time::OffsetDateTime;
use crate::clock::iso;
pub const TABLE: &str = "org_audit";
pub const MAX_PAGE: usize = 200;
const MAX_FIELD: usize = 2000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuditEntry {
pub id: String,
pub org_id: String,
pub actor_kind: String,
pub actor_id: String,
pub on_behalf_of: Option<String>,
pub credential: String,
pub action: String,
pub resource: String,
pub detail: String,
pub at: String,
}
fn cut(value: &str) -> String {
value
.chars()
.filter(|ch| !ch.is_control() || *ch == '\n')
.take(MAX_FIELD)
.collect()
}
pub async fn record(
db: &dyn Database,
entry: &AuditEntry,
now: OffsetDateTime,
) -> Result<(), DbError> {
let statement = Statement::with_values(
"INSERT INTO org_audit \
(id, org_id, actor_kind, actor_id, on_behalf_of, credential, action, resource, \
detail, at) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
vec![
entry.id.as_str().into(),
entry.org_id.as_str().into(),
cut(&entry.actor_kind).into(),
cut(&entry.actor_id).into(),
entry.on_behalf_of.as_deref().map(cut).into(),
cut(&entry.credential).into(),
cut(&entry.action).into(),
cut(&entry.resource).into(),
cut(&entry.detail).into(),
iso(now).into(),
],
);
db.execute(&statement).await.map(|_| ())
}
fn entry_from(row: &Row) -> AuditEntry {
let text = |name: &str| row.get::<String>(name).unwrap_or_default();
AuditEntry {
id: text("id"),
org_id: text("org_id"),
actor_kind: text("actor_kind"),
actor_id: text("actor_id"),
on_behalf_of: row
.get::<String>("on_behalf_of")
.filter(|value| !value.is_empty()),
credential: text("credential"),
action: text("action"),
resource: text("resource"),
detail: text("detail"),
at: text("at"),
}
}
pub async fn list(
db: &dyn Database,
org_id: &str,
before: Option<&str>,
limit: usize,
) -> Result<Vec<AuditEntry>, DbError> {
let limit = limit.clamp(1, MAX_PAGE);
let columns = "id, org_id, actor_kind, actor_id, on_behalf_of, credential, action, \
resource, detail, at";
let statement = match before {
None => Statement::with_values(
format!(
"SELECT {columns} FROM org_audit WHERE org_id = ? ORDER BY id DESC LIMIT {limit}"
),
vec![org_id.into()],
),
Some(before) => Statement::with_values(
format!(
"SELECT {columns} FROM org_audit WHERE org_id = ? AND id < ? \
ORDER BY id DESC LIMIT {limit}"
),
vec![org_id.into(), before.into()],
),
};
Ok(db
.query(&statement)
.await?
.rows
.iter()
.map(entry_from)
.collect())
}