Skip to main content

pylon_plugin/builtin/
timestamps.rs

1use crate::{Plugin, PluginError};
2use pylon_auth::AuthContext;
3use serde_json::Value;
4
5/// Timestamps plugin. Auto-sets `createdAt` on insert and `updatedAt` on update.
6pub struct TimestampsPlugin {
7    pub created_field: String,
8    pub updated_field: String,
9}
10
11impl TimestampsPlugin {
12    pub fn new() -> Self {
13        Self {
14            created_field: "createdAt".into(),
15            updated_field: "updatedAt".into(),
16        }
17    }
18
19    pub fn with_fields(created: &str, updated: &str) -> Self {
20        Self {
21            created_field: created.into(),
22            updated_field: updated.into(),
23        }
24    }
25}
26
27fn now_iso() -> String {
28    use std::time::{SystemTime, UNIX_EPOCH};
29    let ts = SystemTime::now()
30        .duration_since(UNIX_EPOCH)
31        .unwrap_or_default()
32        .as_secs();
33    let secs_per_day: u64 = 86400;
34    let days = ts / secs_per_day;
35    let rem = ts % secs_per_day;
36    let h = rem / 3600;
37    let m = (rem % 3600) / 60;
38    let s = rem % 60;
39    let z = days + 719468;
40    let era = z / 146097;
41    let doe = z - era * 146097;
42    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
43    let y = yoe + era * 400;
44    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
45    let mp = (5 * doy + 2) / 153;
46    let d = doy - (153 * mp + 2) / 5 + 1;
47    let mo = if mp < 10 { mp + 3 } else { mp - 9 };
48    let yr = if mo <= 2 { y + 1 } else { y };
49    format!("{yr:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
50}
51
52impl Plugin for TimestampsPlugin {
53    fn name(&self) -> &str {
54        "timestamps"
55    }
56
57    fn before_insert(
58        &self,
59        _entity: &str,
60        data: &mut Value,
61        _auth: &AuthContext,
62    ) -> Result<(), PluginError> {
63        if let Some(obj) = data.as_object_mut() {
64            let now = now_iso();
65            obj.entry(&self.created_field)
66                .or_insert(Value::String(now.clone()));
67            obj.entry(&self.updated_field).or_insert(Value::String(now));
68        }
69        Ok(())
70    }
71
72    fn before_update(
73        &self,
74        _entity: &str,
75        _id: &str,
76        data: &mut Value,
77        _auth: &AuthContext,
78    ) -> Result<(), PluginError> {
79        if let Some(obj) = data.as_object_mut() {
80            obj.insert(self.updated_field.clone(), Value::String(now_iso()));
81        }
82        Ok(())
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn sets_created_at_on_insert() {
92        let plugin = TimestampsPlugin::new();
93        let mut data = serde_json::json!({"title": "Test"});
94        plugin
95            .before_insert("Todo", &mut data, &AuthContext::anonymous())
96            .unwrap();
97        assert!(data.get("createdAt").is_some());
98        assert!(data.get("updatedAt").is_some());
99    }
100
101    #[test]
102    fn does_not_overwrite_existing_created_at() {
103        let plugin = TimestampsPlugin::new();
104        let mut data = serde_json::json!({"title": "Test", "createdAt": "2020-01-01"});
105        plugin
106            .before_insert("Todo", &mut data, &AuthContext::anonymous())
107            .unwrap();
108        assert_eq!(data["createdAt"], "2020-01-01");
109    }
110
111    #[test]
112    fn sets_updated_at_on_update() {
113        let plugin = TimestampsPlugin::new();
114        let mut data = serde_json::json!({"title": "Updated"});
115        plugin
116            .before_update("Todo", "t1", &mut data, &AuthContext::anonymous())
117            .unwrap();
118        assert!(data.get("updatedAt").is_some());
119    }
120
121    #[test]
122    fn custom_field_names() {
123        let plugin = TimestampsPlugin::with_fields("created", "modified");
124        let mut data = serde_json::json!({"title": "Test"});
125        plugin
126            .before_insert("Todo", &mut data, &AuthContext::anonymous())
127            .unwrap();
128        assert!(data.get("created").is_some());
129        assert!(data.get("modified").is_some());
130    }
131}