pylon_plugin/builtin/
timestamps.rs1use crate::{Plugin, PluginError};
2use pylon_auth::AuthContext;
3use serde_json::Value;
4
5pub 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}