Skip to main content

vantage_table/table/sets/
hooks.rs

1//! Fire a table's lifecycle hooks around a write.
2//!
3//! Before-write hooks run on the record about to be written, in phase order,
4//! just ahead of set-invariant enforcement; an error cancels the operation.
5//! `before_delete` hooks may instead return [`HookReturn::Handled`] to take over
6//! (soft-delete). After-commit hooks run for their side-effects. Hooks receive
7//! the entity-erased table for relation/datasource access.
8
9use vantage_core::Result;
10use vantage_types::{EmptyEntity, Entity, InvariantValue, Record};
11
12use super::invariants::enforce_invariants;
13use crate::table::{
14    AfterCx, AfterFn, BeforeCx, BeforeDeleteFn, BeforeFn, DeleteAllCx, DeleteAllFn, DeleteCx,
15    HookReturn, Phase, Table,
16};
17use crate::traits::table_source::TableSource;
18use crate::write_context::WriteContext;
19
20/// The two halves of a single-record insert that surround the driver's write.
21/// A driver's bulk path calls them around its batch so that it stays
22/// equivalent to inserting the records one by one.
23impl<T: TableSource, E: Entity<T::Value>> Table<T, E>
24where
25    T::Value: InvariantValue,
26{
27    /// Everything a single insert does to `record` before the driver write:
28    /// the confined-writes check, before-insert hooks, dropping read-only
29    /// imported columns, and conforming to the set's invariants. Returns the
30    /// record to store.
31    pub async fn prepare_insert(&self, record: &Record<T::Value>) -> Result<Record<T::Value>> {
32        self.require_confined_writes("insert")?;
33        let mut record = record.clone();
34        run_before(
35            self.before_insert_hooks(),
36            &mut record,
37            None,
38            self.as_entity_erased(),
39        )
40        .await?;
41        self.strip_imported_columns(&mut record);
42        enforce_invariants(&mut record, self.invariants())?;
43        Ok(record)
44    }
45
46    /// Run the after-insert hooks for a record stored under `id`, as a single
47    /// insert does once the driver write has succeeded.
48    pub async fn finish_insert(&self, id: &T::Id, record: &Record<T::Value>) -> Result<()> {
49        run_after(
50            self.after_insert_hooks(),
51            id,
52            record,
53            None,
54            self.as_entity_erased(),
55        )
56        .await
57    }
58}
59
60/// The row an update writes, the row as the set holds it, and whether the
61/// update is a patch (its record holds only the patched fields).
62pub(crate) struct UpdateOf<'a, T: TableSource> {
63    pub id: &'a T::Id,
64    pub stored: Option<&'a Record<T::Value>>,
65    pub patch: bool,
66}
67
68/// `update` is `None` on insert.
69pub(crate) async fn run_before<T: TableSource>(
70    hooks: &[(Phase, BeforeFn<T>)],
71    record: &mut Record<T::Value>,
72    update: Option<UpdateOf<'_, T>>,
73    table: &Table<T, EmptyEntity>,
74) -> Result<()> {
75    if hooks.is_empty() {
76        return Ok(());
77    }
78    let context = WriteContext::current();
79    for (_phase, hook) in hooks {
80        hook(BeforeCx {
81            record: &mut *record,
82            id: update.as_ref().map(|u| u.id),
83            stored: update.as_ref().and_then(|u| u.stored),
84            patch: update.as_ref().is_some_and(|u| u.patch),
85            context: context.as_ref(),
86            table,
87        })
88        .await?;
89    }
90    Ok(())
91}
92
93pub(crate) async fn run_after<T: TableSource>(
94    hooks: &[AfterFn<T>],
95    id: &T::Id,
96    record: &Record<T::Value>,
97    before: Option<&Record<T::Value>>,
98    table: &Table<T, EmptyEntity>,
99) -> Result<()> {
100    if hooks.is_empty() {
101        return Ok(());
102    }
103    let context = WriteContext::current();
104    for hook in hooks {
105        hook(AfterCx {
106            id,
107            record,
108            before,
109            context: context.as_ref(),
110            table,
111        })
112        .await?;
113    }
114    Ok(())
115}
116
117pub(crate) async fn run_delete_all<T: TableSource>(
118    hooks: &[DeleteAllFn<T>],
119    table: &Table<T, EmptyEntity>,
120) -> Result<()> {
121    if hooks.is_empty() {
122        return Ok(());
123    }
124    let context = WriteContext::current();
125    for hook in hooks {
126        hook(DeleteAllCx {
127            context: context.as_ref(),
128            table,
129        })
130        .await?;
131    }
132    Ok(())
133}
134
135/// Run before-delete hooks; the first `Handled` short-circuits and signals the
136/// caller to skip the underlying delete.
137pub(crate) async fn run_before_delete<T: TableSource>(
138    hooks: &[BeforeDeleteFn<T>],
139    id: &T::Id,
140    record: &Record<T::Value>,
141    table: &Table<T, EmptyEntity>,
142) -> Result<HookReturn> {
143    let context = WriteContext::current();
144    for hook in hooks {
145        let cx = DeleteCx {
146            id,
147            record,
148            context: context.as_ref(),
149            table,
150        };
151        if let HookReturn::Handled = hook(cx).await? {
152            return Ok(HookReturn::Handled);
153        }
154    }
155    Ok(HookReturn::Proceed)
156}