1use rustlavel_core::Json;
4
5#[derive(Debug, Clone, PartialEq)]
7pub struct Entry {
8 pub id: i64,
9 pub user_id: Option<i64>,
12 pub user_name: Option<String>,
14 pub event: String,
15 pub model_type: Option<String>,
16 pub model_id: Option<String>,
17 pub description: Option<String>,
18 pub properties: Json,
19 pub ip_address: Option<String>,
20 pub user_agent: Option<String>,
21 pub created_at: String,
24}
25
26impl Default for Entry {
30 fn default() -> Entry {
31 Entry {
32 id: 0,
33 user_id: None,
34 user_name: None,
35 event: String::new(),
36 model_type: None,
37 model_id: None,
38 description: None,
39 properties: Json::Null,
40 ip_address: None,
41 user_agent: None,
42 created_at: String::new(),
43 }
44 }
45}
46
47impl Entry {
48 pub fn new(event: impl Into<String>) -> Entry {
49 Entry { event: event.into(), ..Default::default() }
50 }
51
52 pub fn summary(&self) -> String {
55 if let Some(description) = self.description.as_deref().filter(|d| !d.is_empty()) {
56 return description.to_string();
57 }
58 let who = self.user_name.as_deref().unwrap_or("Somebody");
59 match (&self.model_type, &self.model_id) {
60 (Some(kind), Some(id)) => format!("{who} performed {} on {kind} #{id}", self.event),
61 _ => format!("{who} performed {}", self.event),
62 }
63 }
64}
65
66pub struct Builder {
71 pub(crate) trail: crate::store::Trail,
72 pub(crate) entry: Entry,
73}
74
75impl Builder {
76 pub fn by(mut self, id: i64, name: impl Into<String>) -> Builder {
79 self.entry.user_id = Some(id);
80 self.entry.user_name = Some(name.into());
81 self
82 }
83
84 pub fn named(mut self, name: impl Into<String>) -> Builder {
91 self.entry.user_name = Some(name.into());
92 self
93 }
94
95 pub fn on(mut self, kind: impl Into<String>, id: impl std::fmt::Display) -> Builder {
97 self.entry.model_type = Some(kind.into());
98 self.entry.model_id = Some(id.to_string());
99 self
100 }
101
102 pub fn describe(mut self, description: impl Into<String>) -> Builder {
104 self.entry.description = Some(description.into());
105 self
106 }
107
108 pub fn from_ip(mut self, ip: impl Into<String>) -> Builder {
109 self.entry.ip_address = Some(ip.into());
110 self
111 }
112
113 pub fn user_agent(mut self, agent: impl Into<String>) -> Builder {
114 self.entry.user_agent = Some(agent.into());
115 self
116 }
117
118 pub fn with(mut self, key: &str, value: impl Into<Json>) -> Builder {
120 let mut properties = match std::mem::replace(&mut self.entry.properties, Json::Null) {
121 Json::Object(map) => map,
122 _ => Default::default(),
123 };
124 properties.insert(key.to_string(), value.into());
125 self.entry.properties = Json::Object(properties);
126 self
127 }
128
129 pub fn changed(self, from: impl Into<Json>, to: impl Into<Json>) -> Builder {
131 self.with("from", from).with("to", to)
132 }
133
134 pub fn entry(&self) -> &Entry {
136 &self.entry
137 }
138
139 pub async fn save(self) -> rustlavel_core::Result<i64> {
141 self.trail.write(self.entry).await
142 }
143
144 pub async fn record(self) {
151 let event = self.entry.event.clone();
152 if let Err(error) = self.save().await {
153 rustlavel_core::warn!("could not write the audit entry for {event}: {error}");
154 }
155 }
156}