Skip to main content

tablo_core/resource/
commit.rs

1//! What a write changed, and the one place the framework hands it to the app's hooks.
2
3use topcoat::context::Cx;
4
5use super::{Action, HeaderAction, Resource};
6
7/// What one write of a resource changed: the mutation and the rows it wrote.
8///
9/// [`Resource::before_commit`] receives it inside the write's transaction and
10/// [`Resource::after_commit`] once the transaction commits.
11///
12/// The enum is exhaustive on purpose: an audit hook that matches every variant stops compiling
13/// when a new kind of write appears, instead of skipping it.
14///
15/// A create or an update that also links or unlinks records through a many-to-many form field
16/// reports the record only: the links are not in `M`. Read them through the hook's executor when
17/// an audit needs them; the relation table's own writes report as [`Change::Attach`] and
18/// [`Change::Detach`].
19///
20/// A save that also writes a [`Relation::has_one`](crate::Relation::has_one)'s child reports the
21/// owner here, and the child to the related resource's hooks, as its own pages would.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum Change<M> {
24    /// A create wrote this row.
25    Create(M),
26    /// An update wrote `after` over `before`, the snapshot the handler loaded and authorized
27    /// inside the transaction. A submission that names no field writes nothing and still
28    /// reports an update, with `before == after`.
29    Update { before: M, after: M },
30    /// A delete or a bulk delete removed these rows, or trashed them on a soft-deleting resource,
31    /// as they stood before the delete.
32    Delete(Vec<M>),
33    /// A restore brought back these trashed rows, as they stood before it: their deleted-at
34    /// column still holds the time they were trashed.
35    Restore(Vec<M>),
36    /// A force delete removed these trashed rows for good, as they stood before it.
37    ForceDelete(Vec<M>),
38    /// The custom [`Action`] `name` ran on these rows, as the handler loaded them before it ran.
39    Action { name: &'static str, records: Vec<M> },
40    /// The [`HeaderAction`](crate::HeaderAction) `name` ran, on no record: its audit can record
41    /// who ran it, through the context, and which action.
42    HeaderAction { name: &'static str },
43    /// A many-to-many relation's table linked `owner` to the related record `key` names.
44    ///
45    /// `relation` is the owner's `#[has_many(via = ..)]` field and `key` the related resource's
46    /// record key, as its URLs spell it whatever spelling the request posted. Attaching a linked
47    /// record writes nothing and still reports the attach.
48    Attach {
49        owner: M,
50        relation: String,
51        key: String,
52    },
53    /// A many-to-many relation's table unlinked `owner` from the related records `keys` name, as
54    /// their URLs spell them.
55    Detach {
56        owner: M,
57        relation: String,
58        keys: Vec<String>,
59    },
60}
61
62impl<M> Change<M> {
63    /// The rows of this resource the change names: the created row and the updated row as the
64    /// write left them, the deleted, restored or force-deleted rows and the rows an action ran on
65    /// as they stood before it, the owner of an attach or a detach, and none for a header action.
66    pub fn records(&self) -> &[M] {
67        match self {
68            Self::Create(record)
69            | Self::Update { after: record, .. }
70            | Self::Attach { owner: record, .. }
71            | Self::Detach { owner: record, .. } => std::slice::from_ref(record),
72            Self::Delete(records)
73            | Self::Restore(records)
74            | Self::ForceDelete(records)
75            | Self::Action { records, .. } => records,
76            Self::HeaderAction { .. } => &[],
77        }
78    }
79
80    /// The same change, each record read through `f`.
81    pub(crate) fn map<N>(&self, f: impl Fn(&M) -> N) -> Change<N> {
82        match self {
83            Self::Create(record) => Change::Create(f(record)),
84            Self::Update { before, after } => Change::Update {
85                before: f(before),
86                after: f(after),
87            },
88            Self::Delete(records) => Change::Delete(records.iter().map(f).collect()),
89            Self::Restore(records) => Change::Restore(records.iter().map(f).collect()),
90            Self::ForceDelete(records) => Change::ForceDelete(records.iter().map(f).collect()),
91            Self::Action { name, records } => Change::Action {
92                name,
93                records: records.iter().map(f).collect(),
94            },
95            Self::HeaderAction { name } => Change::HeaderAction { name },
96            Self::Attach {
97                owner,
98                relation,
99                key,
100            } => Change::Attach {
101                owner: f(owner),
102                relation: relation.clone(),
103                key: key.clone(),
104            },
105            Self::Detach {
106                owner,
107                relation,
108                keys,
109            } => Change::Detach {
110                owner: f(owner),
111                relation: relation.clone(),
112                keys: keys.clone(),
113            },
114        }
115    }
116
117    /// Whether the change is a run of the custom action `A`.
118    ///
119    /// It compares against `A::NAME`, as [`Ability::is_action`](crate::Ability::is_action) does,
120    /// and a type that is not an action of a resource over `M` does not compile. `R` is inferred
121    /// when `A` is an action of one resource.
122    ///
123    /// ```rust
124    /// # use tablo_core::{Action, Change, NoForm, Resource};
125    /// # use topcoat::{Result, context::Cx};
126    /// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
127    /// # struct Post { #[key] #[auto] id: uuid::Uuid, published: bool }
128    /// # struct PostResource;
129    /// # impl Resource for PostResource {
130    /// #     type Model = Post;
131    /// #     type Form = NoForm<Post>;
132    /// # }
133    /// # struct Publish;
134    /// # impl Action<PostResource> for Publish {
135    /// #     type Input = ();
136    /// #     const NAME: &'static str = "publish";
137    /// #     async fn run(_: &Cx, _: &[Post], _: (), _: &mut dyn toasty::Executor) -> Result<tablo_core::Outcome, tablo_core::WriteError<std::convert::Infallible>> { Ok(tablo_core::Outcome::done()) }
138    /// # }
139    /// fn published(change: &Change<Post>) -> usize {
140    ///     if change.is_action::<Publish, _>() {
141    ///         change.records().len()
142    ///     } else {
143    ///         0
144    ///     }
145    /// }
146    /// ```
147    pub fn is_action<A, R>(&self) -> bool
148    where
149        A: Action<R>,
150        R: Resource<Model = M>,
151    {
152        matches!(self, Self::Action { name, .. } if *name == A::NAME)
153    }
154
155    /// Whether the change is a run of the header action `A`.
156    ///
157    /// It compares against `A::NAME`, as
158    /// [`Ability::is_header_action`](crate::Ability::is_header_action) does.
159    ///
160    /// ```rust
161    /// # use tablo_core::{Change, HeaderAction};
162    /// # use topcoat::{Result, context::Cx};
163    /// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
164    /// # struct Post { #[key] #[auto] id: uuid::Uuid }
165    /// # struct RecountTags;
166    /// # impl HeaderAction for RecountTags {
167    /// #     type Input = ();
168    /// #     const NAME: &'static str = "recount-tags";
169    /// #     async fn run(_: &Cx, _: (), _: &mut dyn toasty::Executor) -> Result<tablo_core::Outcome, tablo_core::WriteError<std::convert::Infallible>> { Ok(tablo_core::Outcome::done()) }
170    /// # }
171    /// fn audited(change: &Change<Post>) -> bool {
172    ///     !change.is_header_action::<RecountTags>()
173    /// }
174    /// ```
175    pub fn is_header_action<A: HeaderAction>(&self) -> bool {
176        matches!(self, Self::HeaderAction { name } if *name == A::NAME)
177    }
178}
179
180/// The change a run of the custom action `A` made to `records`.
181pub(crate) fn acted<R: Resource, A: Action<R>>(records: Vec<R::Model>) -> Change<R::Model> {
182    Change::Action {
183        name: A::NAME,
184        records,
185    }
186}
187
188/// Hand a committed change to the app.
189///
190/// A hook returning `Err` is logged and ignored: the write stays committed.
191pub(crate) async fn run_after_commit<R: Resource>(cx: &Cx, change: Change<R::Model>) {
192    if let Err(error) = R::after_commit(cx, change).await {
193        tracing::error!(
194            error = %error,
195            resource = std::any::type_name::<R>(),
196            "after_commit failed; the write stays committed"
197        );
198    }
199}