Skip to main content

rustauth_core/options/
init_database_hooks.rs

1//! Init-time database hooks (parity with Better Auth `databaseHooks`).
2
3use std::fmt;
4use std::sync::Arc;
5
6use crate::db::DbRecord;
7use crate::error::RustAuthError;
8use crate::plugin::PluginDatabaseHookContext;
9
10/// Action returned by an init-time database before hook.
11#[derive(Debug, PartialEq)]
12pub enum InitDatabaseBeforeAction {
13    Continue,
14    Cancel(RustAuthError),
15    Replace(DbRecord),
16}
17
18/// Runs before a core model create/update mutation.
19pub trait InitDatabaseBeforeHook: Send + Sync + 'static {
20    fn before(
21        &self,
22        context: &PluginDatabaseHookContext<'_>,
23        record: &mut DbRecord,
24    ) -> Result<InitDatabaseBeforeAction, RustAuthError>;
25}
26
27impl<F> InitDatabaseBeforeHook for F
28where
29    F: Fn(
30            &PluginDatabaseHookContext<'_>,
31            &mut DbRecord,
32        ) -> Result<InitDatabaseBeforeAction, RustAuthError>
33        + Send
34        + Sync
35        + 'static,
36{
37    fn before(
38        &self,
39        context: &PluginDatabaseHookContext<'_>,
40        record: &mut DbRecord,
41    ) -> Result<InitDatabaseBeforeAction, RustAuthError> {
42        self(context, record)
43    }
44}
45
46/// Runs after a core model create/update mutation.
47pub trait InitDatabaseAfterHook: Send + Sync + 'static {
48    fn after(
49        &self,
50        context: &PluginDatabaseHookContext<'_>,
51        record: &DbRecord,
52    ) -> Result<(), RustAuthError>;
53}
54
55impl<F> InitDatabaseAfterHook for F
56where
57    F: Fn(&PluginDatabaseHookContext<'_>, &DbRecord) -> Result<(), RustAuthError>
58        + Send
59        + Sync
60        + 'static,
61{
62    fn after(
63        &self,
64        context: &PluginDatabaseHookContext<'_>,
65        record: &DbRecord,
66    ) -> Result<(), RustAuthError> {
67        self(context, record)
68    }
69}
70
71/// Before/after hook pair for a single mutation kind.
72#[derive(Clone, Default)]
73pub struct DatabaseOperationHooks {
74    pub before: Option<Arc<dyn InitDatabaseBeforeHook>>,
75    pub after: Option<Arc<dyn InitDatabaseAfterHook>>,
76}
77
78impl fmt::Debug for DatabaseOperationHooks {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        formatter
81            .debug_struct("DatabaseOperationHooks")
82            .field(
83                "before",
84                &self.before.as_ref().map(|_| "<init-database-before>"),
85            )
86            .field(
87                "after",
88                &self.after.as_ref().map(|_| "<init-database-after>"),
89            )
90            .finish()
91    }
92}
93
94impl DatabaseOperationHooks {
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    #[must_use]
100    pub fn before<H>(mut self, hook: H) -> Self
101    where
102        H: InitDatabaseBeforeHook,
103    {
104        self.before = Some(Arc::new(hook));
105        self
106    }
107
108    #[must_use]
109    pub fn after<H>(mut self, hook: H) -> Self
110    where
111        H: InitDatabaseAfterHook,
112    {
113        self.after = Some(Arc::new(hook));
114        self
115    }
116}
117
118/// Create/update hooks for one core model.
119#[derive(Clone, Debug, Default)]
120pub struct DatabaseModelHooks {
121    pub create: DatabaseOperationHooks,
122    pub update: DatabaseOperationHooks,
123}
124
125impl DatabaseModelHooks {
126    pub fn new() -> Self {
127        Self::default()
128    }
129}
130
131/// Structured init-time database hooks for core models.
132#[derive(Clone, Debug, Default)]
133pub struct InitDatabaseHooksOptions {
134    pub user: DatabaseModelHooks,
135    pub session: DatabaseModelHooks,
136    pub account: DatabaseModelHooks,
137    pub verification: DatabaseModelHooks,
138}
139
140impl InitDatabaseHooksOptions {
141    pub fn new() -> Self {
142        Self::default()
143    }
144}
145
146pub fn plugin_database_hooks_from_init(
147    options: &InitDatabaseHooksOptions,
148) -> Vec<crate::plugin::PluginDatabaseHook> {
149    let mut hooks = Vec::new();
150    append_model_hooks(&mut hooks, "user", &options.user);
151    append_model_hooks(&mut hooks, "session", &options.session);
152    append_model_hooks(&mut hooks, "account", &options.account);
153    append_model_hooks(&mut hooks, "verification", &options.verification);
154    hooks
155}
156
157fn append_model_hooks(
158    hooks: &mut Vec<crate::plugin::PluginDatabaseHook>,
159    model: &str,
160    model_hooks: &DatabaseModelHooks,
161) {
162    if let Some(before) = model_hooks.create.before.clone() {
163        append_create_before(hooks, model, before);
164    }
165    if let Some(after) = model_hooks.create.after.clone() {
166        append_create_after(hooks, model, after);
167    }
168    if let Some(before) = model_hooks.update.before.clone() {
169        append_update_before(hooks, model, before);
170    }
171    if let Some(after) = model_hooks.update.after.clone() {
172        append_update_after(hooks, model, after);
173    }
174}
175
176fn append_create_before(
177    hooks: &mut Vec<crate::plugin::PluginDatabaseHook>,
178    model: &str,
179    hook: Arc<dyn InitDatabaseBeforeHook>,
180) {
181    use crate::plugin::{
182        PluginDatabaseBeforeAction, PluginDatabaseBeforeInput, PluginDatabaseHook,
183    };
184
185    let model = model.to_owned();
186    hooks.push(PluginDatabaseHook::before_create(
187        format!("{model}-create-before"),
188        move |context, query| {
189            if query.model != model {
190                return Ok(PluginDatabaseBeforeAction::Continue(
191                    PluginDatabaseBeforeInput::Create(query),
192                ));
193            }
194            let mut query = query;
195            match hook.before(context, &mut query.data)? {
196                InitDatabaseBeforeAction::Continue => Ok(PluginDatabaseBeforeAction::Continue(
197                    PluginDatabaseBeforeInput::Create(query),
198                )),
199                InitDatabaseBeforeAction::Cancel(error) => {
200                    Ok(PluginDatabaseBeforeAction::Cancel(error))
201                }
202                InitDatabaseBeforeAction::Replace(record) => {
203                    query.data = record;
204                    Ok(PluginDatabaseBeforeAction::Continue(
205                        PluginDatabaseBeforeInput::Create(query),
206                    ))
207                }
208            }
209        },
210    ));
211}
212
213fn append_create_after(
214    hooks: &mut Vec<crate::plugin::PluginDatabaseHook>,
215    model: &str,
216    hook: Arc<dyn InitDatabaseAfterHook>,
217) {
218    use crate::plugin::PluginDatabaseHook;
219
220    let model = model.to_owned();
221    hooks.push(PluginDatabaseHook::after_create(
222        format!("{model}-create-after"),
223        move |context, query, result| {
224            if query.model != model {
225                return Ok(());
226            }
227            hook.after(context, result)
228        },
229    ));
230}
231
232fn append_update_before(
233    hooks: &mut Vec<crate::plugin::PluginDatabaseHook>,
234    model: &str,
235    hook: Arc<dyn InitDatabaseBeforeHook>,
236) {
237    use crate::plugin::{
238        PluginDatabaseBeforeAction, PluginDatabaseBeforeInput, PluginDatabaseHook,
239    };
240
241    let model = model.to_owned();
242    hooks.push(PluginDatabaseHook::before_update(
243        format!("{model}-update-before"),
244        move |context, query| {
245            if query.model != model {
246                return Ok(PluginDatabaseBeforeAction::Continue(
247                    PluginDatabaseBeforeInput::Update(query),
248                ));
249            }
250            let mut query = query;
251            match hook.before(context, &mut query.data)? {
252                InitDatabaseBeforeAction::Continue => Ok(PluginDatabaseBeforeAction::Continue(
253                    PluginDatabaseBeforeInput::Update(query),
254                )),
255                InitDatabaseBeforeAction::Cancel(error) => {
256                    Ok(PluginDatabaseBeforeAction::Cancel(error))
257                }
258                InitDatabaseBeforeAction::Replace(record) => {
259                    query.data = record;
260                    Ok(PluginDatabaseBeforeAction::Continue(
261                        PluginDatabaseBeforeInput::Update(query),
262                    ))
263                }
264            }
265        },
266    ));
267}
268
269fn append_update_after(
270    hooks: &mut Vec<crate::plugin::PluginDatabaseHook>,
271    model: &str,
272    hook: Arc<dyn InitDatabaseAfterHook>,
273) {
274    use crate::plugin::PluginDatabaseHook;
275
276    let model = model.to_owned();
277    hooks.push(PluginDatabaseHook::after_update(
278        format!("{model}-update-after"),
279        move |context, query, result| {
280            if query.model != model {
281                return Ok(());
282            }
283            if let Some(record) = result {
284                hook.after(context, record)?;
285            }
286            Ok(())
287        },
288    ));
289}