yeti_types/schema/audit.rs
1//! `@audit` directive types and the global audit sender.
2
3use std::collections::HashSet;
4
5// ============================================================================
6// @audit directive
7// ============================================================================
8
9/// Operations that can be audited.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
11pub enum AuditOp {
12 /// A read operation was performed.
13 Read,
14 /// A create operation was performed.
15 Create,
16 /// An update operation was performed.
17 Update,
18 /// A delete operation was performed.
19 Delete,
20}
21
22impl AuditOp {
23 /// Parse an audit operation from a string (case-insensitive).
24 #[must_use]
25 pub fn parse(s: &str) -> Option<Self> {
26 match s.to_lowercase().as_str() {
27 "read" => Some(Self::Read),
28 "create" => Some(Self::Create),
29 "update" => Some(Self::Update),
30 "delete" => Some(Self::Delete),
31 _ => None,
32 }
33 }
34}
35
36/// Audit configuration from `@audit` directive.
37///
38/// Bare `@audit` enables auditing with defaults: all mutations, 90-day retention,
39/// no state capture. Read auditing excluded from default (opt in with `read: true`).
40#[derive(Debug, Clone)]
41pub struct AuditConfig {
42 /// Which operations to audit (default: create, update, delete).
43 pub operations: HashSet<AuditOp>,
44 /// Retention period in days (default: 90, 0 = forever).
45 pub retention_days: u64,
46 /// Capture before/after record state on mutations (default: false).
47 pub capture_state: bool,
48 /// Log read operations to the `TransactionLog` (default: false).
49 ///
50 /// `@audit(read: true)` enables read auditing for this table.
51 /// Reads that scope `WriteContext` are gated by this flag so the
52 /// `TransactionLog` stays clean by default.
53 pub log_reads: bool,
54 /// Log write operations to the `TransactionLog` (default: true).
55 ///
56 /// `@audit(write: false)` disables write auditing for this table.
57 pub log_writes: bool,
58}
59
60impl Default for AuditConfig {
61 fn default() -> Self {
62 let mut operations = HashSet::new();
63 operations.insert(AuditOp::Create);
64 operations.insert(AuditOp::Update);
65 operations.insert(AuditOp::Delete);
66 Self {
67 operations,
68 retention_days: 90,
69 capture_state: false,
70 log_reads: false,
71 log_writes: true,
72 }
73 }
74}
75
76/// A table-level audit entry emitted after successful CRUD operations.
77///
78/// Sent via `try_send` (fire-and-forget) on the audit channel — never blocks
79/// the write path. Dropped silently if the channel is full.
80#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
81pub struct AuditEntry {
82 /// Epoch milliseconds
83 pub timestamp: f64,
84 /// Application that owns the table
85 pub app_id: String,
86 /// Table name
87 pub table: String,
88 /// Record ID
89 pub record_id: String,
90 /// What happened
91 pub operation: AuditOp,
92 /// Who did it (username or "anonymous")
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub user: Option<String>,
95 /// Record state after the operation (for create/update)
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub after: Option<serde_json::Value>,
98 /// Record state before the operation (only when `capture_state`: true)
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub before: Option<serde_json::Value>,
101 /// Interface that triggered the operation (e.g., "rest", "graphql", "mcp", "mqtt")
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub interface: Option<String>,
104}
105
106// ============================================================================
107// `AuditEntry`, `AuditOp`, and `AuditConfig` above are the HTTP response
108// shape for /{app}/audit — yeti-audit::AuditResource materializes them
109// from LogEntry on read. Audit durability is handled by the
110// LoggingBackend wrapper which appends a LogEntry per WriteOp to the
111// unified TransactionLog (yeti_types::backend::TransactionLog).
112// ============================================================================