Skip to main content

vantage_table/table/
hooks.rs

1//! Lifecycle hooks attached to a [`Table`].
2//!
3//! Register them with [`Table::with_hook`] / [`Table::add_hook`]. Before-write
4//! hooks run ahead of set-invariant enforcement, ordered by [`Phase`]; the
5//! firing itself lives in the `table::sets` write paths.
6
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10
11use vantage_types::{EmptyEntity, Entity, Record};
12
13use crate::table::Table;
14use crate::traits::table_source::TableSource;
15use crate::write_context::WriteContext;
16
17/// Ordering band for before-write hooks. Hooks run in this order (and in
18/// registration order within a band), ahead of set-invariant enforcement.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
20pub enum Phase {
21    /// Clean inputs (trim, default empties).
22    Normalize,
23    /// Set or compute fields (audit stamps, derived values). The default.
24    Populate,
25    /// Read-only checks, last — so they see the final record.
26    Validate,
27}
28
29/// What a `before_delete` hook decided.
30pub enum HookReturn {
31    /// Carry out the underlying operation.
32    Proceed,
33    /// The hook already performed the operation (e.g. a soft-delete patch); skip
34    /// the real one and report success.
35    Handled,
36}
37
38/// The future a hook returns.
39pub type BoxFuture<'r, X> = Pin<Box<dyn Future<Output = X> + Send + 'r>>;
40
41/// What a before-write hook receives: the record about to be written (mutable),
42/// the write's [`WriteContext`] if one is in scope, and the entity-erased table.
43///
44/// On an update (replace or patch), `id` is the row being written and
45/// `stored` the row as the set holds it now, read once before the hooks
46/// run; `stored` is `None` when the set doesn't hold the row. Both are
47/// `None` on insert.
48///
49/// `patch` is true on a patch, whose `record` holds only the patched
50/// fields: a hook judging the whole row reads the rest from `stored`. On a
51/// replace, `record` is the whole new row and fields it lacks are dropped.
52#[non_exhaustive]
53pub struct BeforeCx<'r, T: TableSource> {
54    pub record: &'r mut Record<T::Value>,
55    pub id: Option<&'r T::Id>,
56    pub stored: Option<&'r Record<T::Value>>,
57    pub patch: bool,
58    pub context: Option<&'r WriteContext>,
59    pub table: &'r Table<T, EmptyEntity>,
60}
61
62/// What an after-commit hook receives. `record` is the committed row (the former
63/// contents for a delete); `before` is the row as it was before an update, and
64/// `None` for inserts and deletes.
65#[non_exhaustive]
66pub struct AfterCx<'r, T: TableSource> {
67    pub id: &'r T::Id,
68    pub record: &'r Record<T::Value>,
69    pub before: Option<&'r Record<T::Value>>,
70    pub context: Option<&'r WriteContext>,
71    pub table: &'r Table<T, EmptyEntity>,
72}
73
74/// What a before-delete hook receives: the row id and its current contents.
75#[non_exhaustive]
76pub struct DeleteCx<'r, T: TableSource> {
77    pub id: &'r T::Id,
78    pub record: &'r Record<T::Value>,
79    pub context: Option<&'r WriteContext>,
80    pub table: &'r Table<T, EmptyEntity>,
81}
82
83/// What a bulk-delete hook receives: the table whose whole set is being
84/// deleted. Its `conditions()` describe the set.
85#[non_exhaustive]
86pub struct DeleteAllCx<'r, T: TableSource> {
87    pub context: Option<&'r WriteContext>,
88    pub table: &'r Table<T, EmptyEntity>,
89}
90
91/// A bulk-delete hook, before (an `Err` cancels) or after `delete_all`.
92pub type DeleteAllFn<T> = Arc<
93    dyn for<'r> Fn(DeleteAllCx<'r, T>) -> BoxFuture<'r, vantage_core::Result<()>> + Send + Sync,
94>;
95
96/// Build a [`DeleteAllFn`] from a closure.
97pub fn delete_all_hook<T, F>(f: F) -> DeleteAllFn<T>
98where
99    T: TableSource,
100    F: for<'r> Fn(DeleteAllCx<'r, T>) -> BoxFuture<'r, vantage_core::Result<()>>
101        + Send
102        + Sync
103        + 'static,
104{
105    Arc::new(f)
106}
107
108/// A before-write hook: mutate the record in place ahead of invariant
109/// enforcement, returning `Err` to cancel the write.
110pub type BeforeFn<T> =
111    Arc<dyn for<'r> Fn(BeforeCx<'r, T>) -> BoxFuture<'r, vantage_core::Result<()>> + Send + Sync>;
112
113/// A before-delete hook: may `Err` to veto, or return [`HookReturn::Handled`]
114/// to take over (soft-delete).
115pub type BeforeDeleteFn<T> = Arc<
116    dyn for<'r> Fn(DeleteCx<'r, T>) -> BoxFuture<'r, vantage_core::Result<HookReturn>>
117        + Send
118        + Sync,
119>;
120
121/// An after-commit hook: side-effects on the committed row. Used for inserts,
122/// updates, and deletes.
123pub type AfterFn<T> =
124    Arc<dyn for<'r> Fn(AfterCx<'r, T>) -> BoxFuture<'r, vantage_core::Result<()>> + Send + Sync>;
125
126/// Build a [`BeforeFn`] from a closure; the bound guides closure inference.
127pub fn before_hook<T, F>(f: F) -> BeforeFn<T>
128where
129    T: TableSource,
130    F: for<'r> Fn(BeforeCx<'r, T>) -> BoxFuture<'r, vantage_core::Result<()>>
131        + Send
132        + Sync
133        + 'static,
134{
135    Arc::new(f)
136}
137
138/// Build a [`BeforeDeleteFn`] from a closure.
139pub fn before_delete_hook<T, F>(f: F) -> BeforeDeleteFn<T>
140where
141    T: TableSource,
142    F: for<'r> Fn(DeleteCx<'r, T>) -> BoxFuture<'r, vantage_core::Result<HookReturn>>
143        + Send
144        + Sync
145        + 'static,
146{
147    Arc::new(f)
148}
149
150/// Build a [`BeforeFn`] from a synchronous closure over the record and the
151/// write's context; an `Err` cancels the write. For callers with nothing to
152/// await, such as a script evaluated in place.
153pub fn before_hook_sync<T: TableSource>(
154    f: impl Fn(&mut Record<T::Value>, Option<&WriteContext>) -> vantage_core::Result<()>
155    + Send
156    + Sync
157    + 'static,
158) -> BeforeFn<T> {
159    Arc::new(move |cx: BeforeCx<'_, T>| {
160        let outcome = f(cx.record, cx.context);
161        Box::pin(std::future::ready(outcome))
162    })
163}
164
165/// Build a [`BeforeFn`] from a synchronous closure over the whole
166/// [`BeforeCx`]: the record, and on an update the id and the stored row.
167pub fn before_hook_sync_cx<T: TableSource>(
168    f: impl for<'r> Fn(BeforeCx<'r, T>) -> vantage_core::Result<()> + Send + Sync + 'static,
169) -> BeforeFn<T> {
170    Arc::new(move |cx: BeforeCx<'_, T>| Box::pin(std::future::ready(f(cx))))
171}
172
173/// Build a [`BeforeDeleteFn`] from a synchronous closure over the row id and
174/// the write's context: `Ok` proceeds with the delete, an `Err` vetoes it.
175pub fn before_delete_hook_sync<T: TableSource>(
176    f: impl Fn(&T::Id, Option<&WriteContext>) -> vantage_core::Result<()> + Send + Sync + 'static,
177) -> BeforeDeleteFn<T> {
178    Arc::new(move |cx: DeleteCx<'_, T>| {
179        let outcome = f(cx.id, cx.context).map(|()| HookReturn::Proceed);
180        Box::pin(std::future::ready(outcome))
181    })
182}
183
184/// Build an [`AfterFn`] from a closure.
185pub fn after_hook<T, F>(f: F) -> AfterFn<T>
186where
187    T: TableSource,
188    F: for<'r> Fn(AfterCx<'r, T>) -> BoxFuture<'r, vantage_core::Result<()>>
189        + Send
190        + Sync
191        + 'static,
192{
193    Arc::new(f)
194}
195
196/// What a read hook receives: the table about to be read (its conditions
197/// describe the set) and the [`WriteContext`] in scope, if any — the same
198/// task-local a write sees, so one scope covers a request's reads and writes.
199#[non_exhaustive]
200pub struct SelectCx<'r, T: TableSource> {
201    pub context: Option<&'r WriteContext>,
202    pub table: &'r Table<T, EmptyEntity>,
203}
204
205/// A read hook: an `Err` refuses the read.
206pub type SelectFn<T> =
207    Arc<dyn for<'r> Fn(SelectCx<'r, T>) -> BoxFuture<'r, vantage_core::Result<()>> + Send + Sync>;
208
209/// Build a [`SelectFn`] from a closure.
210pub fn select_hook<T, F>(f: F) -> SelectFn<T>
211where
212    T: TableSource,
213    F: for<'r> Fn(SelectCx<'r, T>) -> BoxFuture<'r, vantage_core::Result<()>>
214        + Send
215        + Sync
216        + 'static,
217{
218    Arc::new(f)
219}
220
221/// A lifecycle hook attached to a table via [`Table::with_hook`]. Each variant
222/// carries a closure receiving exactly the references available at that stage.
223/// `BeforeSave`/`AfterSave` are sugar that register for both insert and update.
224#[non_exhaustive]
225pub enum Hook<T: TableSource> {
226    /// Before every read that returns rows or facts about rows: list, get,
227    /// get-some, streams (before the first item), count and the aggregates
228    /// (sum/max/min). An `Err` refuses the read. Vista shells that read the
229    /// source directly call [`Table::check_read`] first.
230    ///
231    /// It does **not** run when the table is embedded in another query —
232    /// `related_in_condition`, `column_table_values_expr`, `get_*_query` and
233    /// `get_expr_*` build expressions or subqueries, not reads of this table.
234    /// Rules that must hold inside a subquery belong in the table's
235    /// conditions, added when it is built.
236    ///
237    /// Reads a write makes run it too, so a refusing `BeforeSelect` also
238    /// refuses: an update when before- or after-update hooks exist (the
239    /// stored row is read first), a delete when delete hooks exist (the row is read for
240    /// them), and a write whose backend reads the row back. A Dio serves
241    /// reads from its cache, filled by its own loader, so reads through a
242    /// Dio don't run the master table's `BeforeSelect`.
243    BeforeSelect(SelectFn<T>),
244    BeforeInsert(Phase, BeforeFn<T>),
245    BeforeUpdate(Phase, BeforeFn<T>),
246    BeforeSave(Phase, BeforeFn<T>),
247    BeforeDelete(BeforeDeleteFn<T>),
248    AfterInsert(AfterFn<T>),
249    AfterUpdate(AfterFn<T>),
250    AfterSave(AfterFn<T>),
251    AfterDelete(AfterFn<T>),
252    /// Before `delete_all`, once for the whole set; per-row delete hooks don't run.
253    BeforeDeleteAll(DeleteAllFn<T>),
254    AfterDeleteAll(DeleteAllFn<T>),
255}
256
257/// A table's registered lifecycle hooks, split by placement. The before-write
258/// bands are kept ordered by [`Phase`]. Populated via [`Table::with_hook`].
259#[derive(Clone)]
260pub struct Hooks<T: TableSource> {
261    pub(crate) before_select: Vec<SelectFn<T>>,
262    pub(crate) before_insert: Vec<(Phase, BeforeFn<T>)>,
263    pub(crate) before_update: Vec<(Phase, BeforeFn<T>)>,
264    pub(crate) before_delete: Vec<BeforeDeleteFn<T>>,
265    pub(crate) after_insert: Vec<AfterFn<T>>,
266    pub(crate) after_update: Vec<AfterFn<T>>,
267    pub(crate) after_delete: Vec<AfterFn<T>>,
268    pub(crate) before_delete_all: Vec<DeleteAllFn<T>>,
269    pub(crate) after_delete_all: Vec<DeleteAllFn<T>>,
270}
271
272impl<T: TableSource> Default for Hooks<T> {
273    fn default() -> Self {
274        Self {
275            before_select: Vec::new(),
276            before_insert: Vec::new(),
277            before_update: Vec::new(),
278            before_delete: Vec::new(),
279            after_insert: Vec::new(),
280            after_update: Vec::new(),
281            after_delete: Vec::new(),
282            before_delete_all: Vec::new(),
283            after_delete_all: Vec::new(),
284        }
285    }
286}
287
288impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
289    pub(crate) fn before_insert_hooks(&self) -> &[(Phase, BeforeFn<T>)] {
290        &self.hooks.before_insert
291    }
292    pub(crate) fn before_update_hooks(&self) -> &[(Phase, BeforeFn<T>)] {
293        &self.hooks.before_update
294    }
295    pub(crate) fn before_delete_hooks(&self) -> &[BeforeDeleteFn<T>] {
296        &self.hooks.before_delete
297    }
298    pub(crate) fn after_insert_hooks(&self) -> &[AfterFn<T>] {
299        &self.hooks.after_insert
300    }
301    pub(crate) fn after_update_hooks(&self) -> &[AfterFn<T>] {
302        &self.hooks.after_update
303    }
304    pub(crate) fn after_delete_hooks(&self) -> &[AfterFn<T>] {
305        &self.hooks.after_delete
306    }
307    pub(crate) fn before_delete_all_hooks(&self) -> &[DeleteAllFn<T>] {
308        &self.hooks.before_delete_all
309    }
310    pub(crate) fn after_delete_all_hooks(&self) -> &[DeleteAllFn<T>] {
311        &self.hooks.after_delete_all
312    }
313
314    /// Register a lifecycle [`Hook`]. Before-write hooks are kept ordered by
315    /// [`Phase`] (then registration order); `BeforeSave`/`AfterSave` register
316    /// for both insert and update.
317    pub fn add_hook(&mut self, hook: Hook<T>) {
318        let hooks = &mut self.hooks;
319        match hook {
320            Hook::BeforeSelect(f) => hooks.before_select.push(f),
321            Hook::BeforeInsert(phase, f) => push_phased(&mut hooks.before_insert, phase, f),
322            Hook::BeforeUpdate(phase, f) => push_phased(&mut hooks.before_update, phase, f),
323            Hook::BeforeSave(phase, f) => {
324                push_phased(&mut hooks.before_insert, phase, f.clone());
325                push_phased(&mut hooks.before_update, phase, f);
326            }
327            Hook::BeforeDelete(f) => hooks.before_delete.push(f),
328            Hook::AfterInsert(f) => hooks.after_insert.push(f),
329            Hook::AfterUpdate(f) => hooks.after_update.push(f),
330            Hook::AfterSave(f) => {
331                hooks.after_insert.push(f.clone());
332                hooks.after_update.push(f);
333            }
334            Hook::AfterDelete(f) => hooks.after_delete.push(f),
335            Hook::BeforeDeleteAll(f) => hooks.before_delete_all.push(f),
336            Hook::AfterDeleteAll(f) => hooks.after_delete_all.push(f),
337        }
338    }
339
340    /// Builder form of [`Self::add_hook`].
341    pub fn with_hook(mut self, hook: Hook<T>) -> Self {
342        self.add_hook(hook);
343        self
344    }
345
346    /// Run the [`Hook::BeforeSelect`] hooks; an `Err` means the read is
347    /// refused. Every `Table` read path calls this. Code that reads through
348    /// the table's data source directly (a vista shell's count or window)
349    /// must call it first.
350    pub async fn check_read(&self) -> vantage_core::Result<()> {
351        if self.hooks.before_select.is_empty() {
352            return Ok(());
353        }
354        let context = WriteContext::current();
355        let table = self.as_entity_erased();
356        for hook in &self.hooks.before_select {
357            hook(SelectCx {
358                context: context.as_ref(),
359                table,
360            })
361            .await?;
362        }
363        Ok(())
364    }
365}
366
367/// Append a before-write hook and keep the vector ordered by phase (stable, so
368/// registration order is preserved within a phase).
369fn push_phased<T: TableSource>(
370    hooks: &mut Vec<(Phase, BeforeFn<T>)>,
371    phase: Phase,
372    f: BeforeFn<T>,
373) {
374    hooks.push((phase, f));
375    hooks.sort_by_key(|(p, _)| *p);
376}