Skip to main content

pylon_plugin/builtin/
soft_delete.rs

1use crate::{Plugin, PluginError};
2use pylon_auth::AuthContext;
3
4/// Soft delete plugin. Converts deletes to updates that set `deletedAt`.
5/// Adds a before_delete hook that rejects the actual delete and instead
6/// sets a timestamp. The application should filter out soft-deleted rows.
7pub struct SoftDeletePlugin {
8    pub field: String,
9    /// Entity names to apply soft delete to. Empty = all entities.
10    pub entities: Vec<String>,
11}
12
13impl SoftDeletePlugin {
14    pub fn new() -> Self {
15        Self {
16            field: "deletedAt".into(),
17            entities: vec![],
18        }
19    }
20
21    pub fn for_entities(entities: Vec<String>) -> Self {
22        Self {
23            field: "deletedAt".into(),
24            entities,
25        }
26    }
27
28    fn applies_to(&self, entity: &str) -> bool {
29        self.entities.is_empty() || self.entities.iter().any(|e| e == entity)
30    }
31}
32
33#[allow(dead_code)]
34fn now_iso() -> String {
35    use std::time::{SystemTime, UNIX_EPOCH};
36    let ts = SystemTime::now()
37        .duration_since(UNIX_EPOCH)
38        .unwrap_or_default()
39        .as_secs();
40    format!("{ts}Z")
41}
42
43impl Plugin for SoftDeletePlugin {
44    fn name(&self) -> &str {
45        "soft-delete"
46    }
47
48    fn before_delete(
49        &self,
50        entity: &str,
51        id: &str,
52        _auth: &AuthContext,
53    ) -> Result<(), PluginError> {
54        if self.applies_to(entity) {
55            // Block the real delete — the server should instead update with deletedAt.
56            Err(PluginError {
57                code: "SOFT_DELETE".into(),
58                message: format!(
59                    "Entity {} uses soft delete. Set {}.{} instead of deleting row {}.",
60                    entity, entity, self.field, id
61                ),
62                status: 400,
63            })
64        } else {
65            Ok(())
66        }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn blocks_delete_for_all_entities() {
76        let plugin = SoftDeletePlugin::new();
77        let result = plugin.before_delete("Todo", "t1", &AuthContext::anonymous());
78        assert!(result.is_err());
79        assert_eq!(result.unwrap_err().code, "SOFT_DELETE");
80    }
81
82    #[test]
83    fn blocks_delete_for_specific_entities() {
84        let plugin = SoftDeletePlugin::for_entities(vec!["Todo".into()]);
85        assert!(plugin
86            .before_delete("Todo", "t1", &AuthContext::anonymous())
87            .is_err());
88        assert!(plugin
89            .before_delete("User", "u1", &AuthContext::anonymous())
90            .is_ok());
91    }
92
93    #[test]
94    fn allows_delete_for_non_matching() {
95        let plugin = SoftDeletePlugin::for_entities(vec!["Todo".into()]);
96        let result = plugin.before_delete("Comment", "c1", &AuthContext::anonymous());
97        assert!(result.is_ok());
98    }
99}