pylon_plugin/builtin/
cascade.rs1use std::sync::Mutex;
2
3use crate::Plugin;
4use pylon_auth::AuthContext;
5
6#[derive(Debug, Clone)]
8pub struct CascadeRule {
9 pub parent: String,
11 pub child: String,
13 pub foreign_key: String,
15}
16
17pub struct CascadePlugin {
20 rules: Vec<CascadeRule>,
21 pending_deletes: Mutex<Vec<(String, String)>>, }
23
24impl CascadePlugin {
25 pub fn new() -> Self {
26 Self {
27 rules: Vec::new(),
28 pending_deletes: Mutex::new(Vec::new()),
29 }
30 }
31
32 pub fn add_rule(&mut self, parent: &str, child: &str, foreign_key: &str) {
34 self.rules.push(CascadeRule {
35 parent: parent.to_string(),
36 child: child.to_string(),
37 foreign_key: foreign_key.to_string(),
38 });
39 }
40
41 pub fn take_pending(&self) -> Vec<(String, String)> {
43 let mut pending = self.pending_deletes.lock().unwrap();
44 let items = pending.clone();
45 pending.clear();
46 items
47 }
48
49 pub fn rules_for(&self, parent: &str) -> Vec<&CascadeRule> {
51 self.rules.iter().filter(|r| r.parent == parent).collect()
52 }
53}
54
55impl Plugin for CascadePlugin {
56 fn name(&self) -> &str {
57 "cascade-delete"
58 }
59
60 fn after_delete(&self, entity: &str, id: &str, _auth: &AuthContext) {
61 let rules = self.rules_for(entity);
63 if !rules.is_empty() {
64 let mut pending = self.pending_deletes.lock().unwrap();
65 for rule in rules {
66 pending.push((
69 rule.child.clone(),
70 format!("__cascade__{}={}", rule.foreign_key, id),
71 ));
72 }
73 }
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn queues_cascade_on_delete() {
83 let mut plugin = CascadePlugin::new();
84 plugin.add_rule("User", "Todo", "authorId");
85
86 plugin.after_delete("User", "u1", &AuthContext::anonymous());
87
88 let pending = plugin.take_pending();
89 assert_eq!(pending.len(), 1);
90 assert_eq!(pending[0].0, "Todo");
91 assert!(pending[0].1.contains("authorId=u1"));
92 }
93
94 #[test]
95 fn no_rules_no_cascade() {
96 let plugin = CascadePlugin::new();
97 plugin.after_delete("User", "u1", &AuthContext::anonymous());
98 assert!(plugin.take_pending().is_empty());
99 }
100
101 #[test]
102 fn multiple_children() {
103 let mut plugin = CascadePlugin::new();
104 plugin.add_rule("User", "Todo", "authorId");
105 plugin.add_rule("User", "Comment", "userId");
106
107 plugin.after_delete("User", "u1", &AuthContext::anonymous());
108
109 let pending = plugin.take_pending();
110 assert_eq!(pending.len(), 2);
111 }
112
113 #[test]
114 fn take_clears_pending() {
115 let mut plugin = CascadePlugin::new();
116 plugin.add_rule("User", "Todo", "authorId");
117
118 plugin.after_delete("User", "u1", &AuthContext::anonymous());
119 let first = plugin.take_pending();
120 assert_eq!(first.len(), 1);
121
122 let second = plugin.take_pending();
123 assert!(second.is_empty());
124 }
125
126 #[test]
127 fn unrelated_entity_no_cascade() {
128 let mut plugin = CascadePlugin::new();
129 plugin.add_rule("User", "Todo", "authorId");
130
131 plugin.after_delete("Post", "p1", &AuthContext::anonymous());
132 assert!(plugin.take_pending().is_empty());
133 }
134}