Skip to main content

keepsake_sqlx/repository/
audit.rs

1use keepsake::{AuditEvent, CommandContext};
2use sqlx::{Postgres, Transaction};
3
4use super::{KeepsakeRepository, RelationCache, RepositoryResult};
5
6impl<C> KeepsakeRepository<C>
7where
8    C: RelationCache,
9{
10    /// Appends an explicit audit event without mutating lifecycle state.
11    ///
12    /// Prefer `apply` and `revoke` for lifecycle mutations so state and audit
13    /// rows commit together. This helper is for application-owned audit events
14    /// that do not have a built-in repository command.
15    pub async fn append_audit_event(&self, event: &AuditEvent) -> RepositoryResult<i64> {
16        event.subject.validate()?;
17        event.actor.validate()?;
18
19        let mut tx = self.pool.begin().await?;
20        let audit_event_id = record_audit_event_tx(&mut tx, event).await?;
21        tx.commit().await?;
22        Ok(audit_event_id)
23    }
24}
25
26pub(super) async fn record_audit_event_tx(
27    tx: &mut Transaction<'_, Postgres>,
28    event: &AuditEvent,
29) -> RepositoryResult<i64> {
30    let decision = serde_json::to_value(&event.decision)?;
31    let audit_event_id = sqlx::query_scalar::<_, i64>(
32        r"
33        insert into keepsake_audit_events
34            (keepsake_id, relation_id, subject_kind, subject_id, actor_kind, actor_id,
35             event_type, decision, occurred_at)
36        values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
37        returning id
38        ",
39    )
40    .bind(event.keepsake_id)
41    .bind(event.relation_id)
42    .bind(&event.subject.kind)
43    .bind(&event.subject.id)
44    .bind(&event.actor.kind)
45    .bind(&event.actor.id)
46    .bind(event.event_type.as_str())
47    .bind(decision)
48    .bind(event.at)
49    .fetch_one(&mut **tx)
50    .await?;
51
52    for (key, value) in &event.context.attributes {
53        sqlx::query(
54            r"
55            insert into keepsake_audit_context_attributes
56                (audit_event_id, key, value)
57            values ($1, $2, $3)
58            ",
59        )
60        .bind(audit_event_id)
61        .bind(key)
62        .bind(value)
63        .execute(&mut **tx)
64        .await?;
65    }
66
67    Ok(audit_event_id)
68}
69
70pub(super) fn audit_context_from_command(context: &CommandContext) -> keepsake::AuditContext {
71    let mut attributes = context.metadata.clone();
72    if let Some(idempotency_key) = &context.idempotency_key {
73        attributes
74            .entry("idempotency_key".to_owned())
75            .or_insert_with(|| idempotency_key.clone());
76    }
77    keepsake::AuditContext { attributes }
78}