Skip to main content

laterite_admin/
persist.rs

1//! The persister seam: a form's write is a handler, resolved at boot from a
2//! string key on the descriptor.
3//!
4//! Every form gets a [`DefaultPersister`] (the descriptor-driven single-statement
5//! insert/update) unless it names a registered [`Persister`], letting an entity
6//! that needs a custom, atomic, multi-statement write (a tree's ancestor closure,
7//! say) plug in without a hand-written CRUD handler. A persister is a registered
8//! domain write service, the same category as a picker source.
9
10use std::collections::HashMap;
11use std::sync::Arc;
12
13use laterite_core::query::{bind_values, build as to_sql, insert_returning_id_on, text_cast};
14use laterite_core::strata::async_trait;
15use laterite_core::validation::ErrorBag;
16use laterite_core::AnyRowExt;
17use laterite_core::{t, Actor, AttrValue, Db, ModelListener, Op, Record, SaveCx, SavedCx, Text};
18use sea_query::{Alias, Expr, Query, SimpleExpr};
19
20use crate::form::FormConfig;
21use crate::sql::valid_ident;
22
23/// Why a persist failed.
24#[derive(Debug)]
25pub enum SaveError {
26    /// A persist-time domain check (for example "re-parenting is not supported"):
27    /// the form re-renders 422 with these per-field messages.
28    Invalid(ErrorBag),
29    /// The write itself failed: logged, and the form re-renders the generic
30    /// banner.
31    Failed(String),
32}
33
34/// Why a delete failed.
35#[derive(Debug)]
36pub enum DeleteError {
37    /// A listener or the persister refused it. Unlike a save, a delete has no
38    /// fields to hang messages on, so this is one sentence for the operator,
39    /// such as "three records still reference this one".
40    Refused(Text),
41    /// The delete itself failed: logged, and the operator sees a generic
42    /// message.
43    Failed(String),
44}
45
46/// Replaces the default insert/update for one form. An implementor owns its own
47/// transaction when the write is multi-statement. `rec` holds the validated
48/// submission plus anything the listeners added; `rec.to_text_map()` gives the
49/// all-text shape if that suits an implementation better.
50#[async_trait]
51pub trait Persister: Send + Sync + 'static {
52    /// Persists a new record, returning its id.
53    async fn create(&self, cx: &mut SaveCx<'_>, rec: &Record) -> Result<i64, SaveError>;
54    /// Persists an edit to the record with primary key `id`.
55    async fn update(&self, cx: &mut SaveCx<'_>, id: &str, rec: &Record) -> Result<(), SaveError>;
56
57    /// The row as it stands, read in the same transaction, so listeners can see
58    /// what an update changes. Returning `None` (the default) means this
59    /// persister supplies no snapshot and change-based listeners sit out.
60    async fn load(&self, cx: &mut SaveCx<'_>, id: &str) -> Result<Option<Record>, SaveError> {
61        let _ = (cx, id);
62        Ok(None)
63    }
64
65    /// Puts the record under another parent, on the pipeline's transaction:
66    /// what a tree list's "Move to" calls. The default writes the parent
67    /// column; a persister keeping more than that column in step (a path, a
68    /// closure table) overrides it.
69    async fn move_to(&self, cx: &mut SaveCx<'_>, mv: &MoveTo<'_>) -> Result<(), SaveError> {
70        if !(valid_ident(mv.entity) && valid_ident(mv.id_field) && valid_ident(mv.parent_key)) {
71            return Err(SaveError::Failed("invalid identifier".to_string()));
72        }
73        let backend = cx.backend();
74        let (sql, values) = to_sql(
75            backend,
76            Query::update()
77                .table(Alias::new(mv.entity))
78                .value(
79                    Alias::new(mv.parent_key),
80                    match mv.parent.and_then(|p| p.parse::<i64>().ok()) {
81                        Some(parent) => SimpleExpr::from(parent),
82                        None => SimpleExpr::Keyword(sea_query::Keyword::Null),
83                    },
84                )
85                .and_where(
86                    Expr::col(Alias::new(mv.id_field))
87                        .cast_as(Alias::new(text_cast(backend)))
88                        .eq(mv.id.to_string()),
89                )
90                .to_owned(),
91        );
92        bind_values(sqlx::query(&sql), values)
93            .execute(cx.conn())
94            .await
95            .map_err(|e| SaveError::Failed(e.to_string()))?;
96        Ok(())
97    }
98
99    /// Removes the record with primary key `id`, on the pipeline's transaction.
100    ///
101    /// The default refuses. A persister that writes through more than one table
102    /// has to say how those rows come apart, and guessing at that is worse than
103    /// declining: an implementor opts in by overriding this.
104    async fn delete(&self, cx: &mut SaveCx<'_>, id: &str, rec: &Record) -> Result<(), DeleteError> {
105        let _ = (cx, id, rec);
106        Err(DeleteError::Refused(t!(
107            "Deleting this kind of record is not supported."
108        )))
109    }
110}
111
112/// A move under another parent, as [`Persister::move_to`] receives it.
113#[derive(Debug)]
114#[non_exhaustive]
115pub struct MoveTo<'a> {
116    pub entity: &'a str,
117    pub id_field: &'a str,
118    /// The record moving.
119    pub id: &'a str,
120    /// The column holding the parent id.
121    pub parent_key: &'a str,
122    /// The new parent's id, or `None` for the top level.
123    pub parent: Option<&'a str>,
124}
125
126/// Binds one attribute for the storage layer.
127///
128/// Absent, null, and empty text all render the SQL `NULL` keyword, which every
129/// backend accepts for a column of any type; a bound null parameter carries a
130/// text type that Postgres refuses for a bigint column. Booleans bind as
131/// integers (the one representation every backend indexes), and timestamps
132/// and JSON bind as text.
133fn bind(value: Option<&AttrValue>) -> SimpleExpr {
134    match value {
135        None | Some(AttrValue::Null) => SimpleExpr::Keyword(sea_query::Keyword::Null),
136        Some(AttrValue::Int(i)) => (*i).into(),
137        Some(AttrValue::Float(f)) => (*f).into(),
138        Some(AttrValue::Bool(b)) => i32::from(*b).into(),
139        Some(AttrValue::Text(s)) if s.is_empty() => SimpleExpr::Keyword(sea_query::Keyword::Null),
140        Some(other) => other.to_text().into(),
141    }
142}
143
144/// Runs the listeners around a persister write: each `before_save` may change
145/// the record or refuse the write, then the persister writes, then each
146/// `after_save` runs. `after_save` is not atomic with the write.
147///
148/// `id` is the row to update, and is ignored on a create.
149pub(crate) struct SaveRequest<'a> {
150    pub db: &'a Db,
151    pub listeners: &'a [Arc<dyn ModelListener>],
152    pub persister: &'a dyn Persister,
153    /// Who is performing the write; reaches listeners and the persister.
154    pub actor: &'a Actor,
155    pub op: Op,
156    /// The row to update; `None` on a create.
157    pub id: Option<&'a str>,
158}
159
160pub(crate) async fn save(req: SaveRequest<'_>, rec: &mut Record) -> Result<(), SaveError> {
161    let SaveRequest {
162        db,
163        listeners,
164        persister,
165        actor,
166        op,
167        id,
168    } = req;
169    let mut tx = db
170        .pool
171        .begin()
172        .await
173        .map_err(|e| SaveError::Failed(e.to_string()))?;
174
175    let outcome = save_in(
176        &mut tx,
177        db.backend,
178        listeners,
179        persister,
180        actor.clone(),
181        rec,
182        op,
183        id,
184    )
185    .await;
186    match outcome {
187        Ok(()) => tx
188            .commit()
189            .await
190            .map_err(|e| SaveError::Failed(e.to_string()))?,
191        Err(e) => {
192            // A veto or a failed write rolls the whole save back, snapshot read
193            // included.
194            let _ = tx.rollback().await;
195            return Err(e);
196        }
197    }
198
199    // After the commit, so a listener here cannot roll the write back.
200    let saved = SavedCx::new(db, actor);
201    for listener in listeners {
202        listener.after_save(&saved, rec, op).await;
203    }
204    Ok(())
205}
206
207/// What one delete needs: where to write, who is doing it, and which record.
208pub(crate) struct DeleteRequest<'a> {
209    pub db: &'a Db,
210    pub listeners: &'a [Arc<dyn ModelListener>],
211    pub persister: &'a dyn Persister,
212    pub actor: &'a Actor,
213    pub entity: &'a str,
214    pub id: &'a str,
215}
216
217/// Removes one record through the same pipeline a save runs through: the
218/// transaction is owned here, the listeners run inside it and may refuse, and
219/// the after-stage runs once it has committed.
220///
221/// Returns the record as it was, so the caller can report what went.
222pub(crate) async fn delete(req: DeleteRequest<'_>) -> Result<Record, DeleteError> {
223    let DeleteRequest {
224        db,
225        listeners,
226        persister,
227        actor,
228        entity,
229        id,
230    } = req;
231
232    let mut tx = db
233        .pool
234        .begin()
235        .await
236        .map_err(|e| DeleteError::Failed(e.to_string()))?;
237
238    let outcome = delete_in(
239        &mut tx,
240        db.backend,
241        listeners,
242        persister,
243        actor.clone(),
244        entity,
245        id,
246    )
247    .await;
248    let rec = match outcome {
249        Ok(rec) => {
250            tx.commit()
251                .await
252                .map_err(|e| DeleteError::Failed(e.to_string()))?;
253            rec
254        }
255        Err(e) => {
256            // A veto or a failed delete rolls the whole thing back, the read
257            // included.
258            let _ = tx.rollback().await;
259            return Err(e);
260        }
261    };
262
263    // After the commit: the row is gone, so nothing here can undo it.
264    let saved = SavedCx::new(db, actor);
265    for listener in listeners {
266        listener.after_delete(&saved, &rec).await;
267    }
268    Ok(rec)
269}
270
271/// Everything a delete does inside the transaction: read the row, offer it to
272/// the listeners, then remove it.
273async fn delete_in(
274    tx: &mut sqlx::Transaction<'_, sqlx::Any>,
275    backend: laterite_core::migration::DbBackend,
276    listeners: &[Arc<dyn ModelListener>],
277    persister: &dyn Persister,
278    actor: Actor,
279    entity: &str,
280    id: &str,
281) -> Result<Record, DeleteError> {
282    let mut cx = SaveCx::new(backend, tx, actor);
283
284    // The row as it stands, so a listener can decide on its contents and the
285    // after-stage can report what was removed. A persister that supplies no
286    // snapshot leaves an identity-only record, the same as on an update.
287    let loaded = persister.load(&mut cx, id).await.map_err(|e| match e {
288        SaveError::Invalid(_) => DeleteError::Failed("snapshot read refused".to_string()),
289        SaveError::Failed(m) => DeleteError::Failed(m),
290    })?;
291    let mut rec = loaded.unwrap_or_else(|| Record::new(entity));
292    if rec.id().is_none() {
293        if let Ok(numeric) = id.parse::<i64>() {
294            rec.set_id(numeric);
295        }
296    }
297
298    for listener in listeners {
299        listener
300            .before_delete(&mut cx, &rec)
301            .await
302            .map_err(DeleteError::Refused)?;
303    }
304
305    persister.delete(&mut cx, id, &rec).await?;
306    Ok(rec)
307}
308
309/// Everything a save does inside the transaction: read the pre-write row, run
310/// the listeners, then write.
311#[allow(clippy::too_many_arguments)]
312async fn save_in(
313    tx: &mut sqlx::Transaction<'_, sqlx::Any>,
314    backend: laterite_core::migration::DbBackend,
315    listeners: &[Arc<dyn ModelListener>],
316    persister: &dyn Persister,
317    actor: Actor,
318    rec: &mut Record,
319    op: Op,
320    id: Option<&str>,
321) -> Result<(), SaveError> {
322    let mut cx = SaveCx::new(backend, tx, actor);
323
324    if op == Op::Update {
325        if let Some(id) = id {
326            if let Some(previous) = persister.load(&mut cx, id).await? {
327                rec.set_original(previous);
328            }
329        }
330    }
331
332    for listener in listeners {
333        listener
334            .before_save(&mut cx, rec, op)
335            .await
336            .map_err(SaveError::Invalid)?;
337    }
338
339    match op {
340        Op::Create => {
341            let new_id = persister.create(&mut cx, rec).await?;
342            rec.set_id(new_id);
343        }
344        Op::Update => {
345            let id = id.ok_or_else(|| SaveError::Failed("update without an id".to_string()))?;
346            persister.update(&mut cx, id, rec).await?;
347        }
348        // `Op` is non-exhaustive; a stage added later needs its arm here.
349        _ => return Err(SaveError::Failed(format!("unsupported operation {op:?}"))),
350    }
351    Ok(())
352}
353
354/// A registered persister: its dotted `vendor.name` key and the handler.
355pub struct PersisterReg {
356    pub name: String,
357    pub persister: Arc<dyn Persister>,
358}
359
360impl PersisterReg {
361    pub fn new(name: impl Into<String>, persister: Arc<dyn Persister>) -> Self {
362        Self {
363            name: name.into(),
364            persister,
365        }
366    }
367}
368
369/// The persister registry, keyed by name.
370pub type PersisterRegistry = HashMap<String, Arc<dyn Persister>>;
371
372/// The built-in write: a descriptor-driven single-statement insert/update over
373/// the form's own columns, what every form gets unless it names a custom one.
374/// Trusts validated identifiers (the form guards them before calling).
375pub(crate) struct DefaultPersister {
376    entity: String,
377    id_field: String,
378    columns: Vec<String>,
379}
380
381impl DefaultPersister {
382    /// The columns to write: the descriptor's own fields, plus any attribute a
383    /// listener added, minus the database-assigned id. Widening this way means a
384    /// listener-injected `created_at` is written rather than silently dropped;
385    /// the form admits only descriptor fields, so nothing a request submitted can
386    /// reach a column it did not declare.
387    fn write_columns(&self, rec: &Record) -> Vec<String> {
388        let mut cols = self.columns.clone();
389        for (key, _) in rec.iter() {
390            if key != self.id_field && !cols.iter().any(|c| c == key) {
391                cols.push(key.to_string());
392            }
393        }
394        cols.retain(|c| c != &self.id_field && crate::sql::valid_ident(c));
395        cols
396    }
397
398    /// A persister over a list's own table, for a resource whose writes are not
399    /// a generic form (a bespoke editor, or a read screen that still deletes).
400    /// The list's columns become the snapshot a listener sees.
401    pub(crate) fn from_list(config: &crate::list::ListConfig) -> Self {
402        Self {
403            entity: config.entity.clone(),
404            id_field: config.id_field.clone(),
405            columns: config.columns.iter().map(|c| c.field.clone()).collect(),
406        }
407    }
408
409    pub(crate) fn from_config(config: &FormConfig) -> Self {
410        Self {
411            entity: config.entity.clone(),
412            id_field: config.id_field.clone(),
413            columns: config.fields.iter().map(|f| f.name.clone()).collect(),
414        }
415    }
416}
417
418#[async_trait]
419impl Persister for DefaultPersister {
420    async fn create(&self, cx: &mut SaveCx<'_>, rec: &Record) -> Result<i64, SaveError> {
421        // The PK is database-assigned, so the insert never lists it. Scope the
422        // builder so it drops before the await, keeping the future `Send`.
423        let stmt = {
424            let cols = self.write_columns(rec);
425            let vals: Vec<SimpleExpr> = cols.iter().map(|c| bind(rec.get(c))).collect();
426            Query::insert()
427                .into_table(Alias::new(&self.entity))
428                .columns(cols.iter().map(Alias::new))
429                .values_panic(vals)
430                .to_owned()
431        };
432        let backend = cx.backend();
433        insert_returning_id_on(backend, cx.conn(), stmt, Alias::new(&self.id_field))
434            .await
435            .map_err(|e| SaveError::Failed(e.to_string()))
436    }
437
438    async fn update(&self, cx: &mut SaveCx<'_>, id: &str, rec: &Record) -> Result<(), SaveError> {
439        let backend = cx.backend();
440        let (sql, values) = {
441            let mut update = Query::update();
442            update.table(Alias::new(&self.entity));
443            for column in self.write_columns(rec) {
444                update.value(Alias::new(&column), bind(rec.get(&column)));
445            }
446            update.and_where(
447                Expr::col(Alias::new(&self.id_field))
448                    .cast_as(Alias::new(text_cast(backend)))
449                    .eq(id),
450            );
451            to_sql(backend, update)
452        };
453        bind_values(sqlx::query(&sql), values)
454            .execute(cx.conn())
455            .await
456            .map(|_| ())
457            .map_err(|e| SaveError::Failed(e.to_string()))
458    }
459
460    /// Removes the row. One table, one statement, on the pipeline's transaction,
461    /// so a listener's veto rolls it back with everything else.
462    async fn delete(
463        &self,
464        cx: &mut SaveCx<'_>,
465        id: &str,
466        _rec: &Record,
467    ) -> Result<(), DeleteError> {
468        let backend = cx.backend();
469        let (sql, values) = {
470            let mut del = Query::delete();
471            del.from_table(Alias::new(&self.entity));
472            del.and_where(
473                Expr::col(Alias::new(&self.id_field))
474                    .cast_as(Alias::new(text_cast(backend)))
475                    .eq(id),
476            );
477            to_sql(backend, del)
478        };
479        bind_values(sqlx::query(&sql), values)
480            .execute(cx.conn())
481            .await
482            .map(|_| ())
483            .map_err(|e| DeleteError::Failed(e.to_string()))
484    }
485
486    /// Reads the row's declared columns in the transaction, so a listener sees
487    /// what an update changes. Values come back as text, the shape `sqlx::Any`
488    /// gives for a mixed row.
489    async fn load(&self, cx: &mut SaveCx<'_>, id: &str) -> Result<Option<Record>, SaveError> {
490        let backend = cx.backend();
491        let (sql, values) = {
492            let mut select = Query::select();
493            select.from(Alias::new(&self.entity));
494            for column in &self.columns {
495                select.expr_as(
496                    Expr::col(Alias::new(column)).cast_as(Alias::new(text_cast(backend))),
497                    Alias::new(column),
498                );
499            }
500            select.and_where(
501                Expr::col(Alias::new(&self.id_field))
502                    .cast_as(Alias::new(text_cast(backend)))
503                    .eq(id),
504            );
505            to_sql(backend, select)
506        };
507        let row = bind_values(sqlx::query(&sql), values)
508            .fetch_optional(cx.conn())
509            .await
510            .map_err(|e| SaveError::Failed(e.to_string()))?;
511        let Some(row) = row else {
512            return Ok(None);
513        };
514        let mut previous = Record::new(&self.entity);
515        for column in &self.columns {
516            match row.get_text_opt(column) {
517                Ok(Some(v)) => {
518                    previous.set(column.clone(), v);
519                }
520                Ok(None) => {
521                    previous.set(column.clone(), AttrValue::Null);
522                }
523                Err(e) => return Err(SaveError::Failed(e.to_string())),
524            }
525        }
526        Ok(Some(previous))
527    }
528}
529
530#[cfg(test)]
531mod pipeline_tests {
532    use super::*;
533    use laterite_core::testing::connect_test;
534    use laterite_core::{ListenerTarget, ModelListenerReg, Text};
535    use std::sync::Mutex;
536
537    /// Records what reached the persister, so a test can assert on the write.
538    #[derive(Default)]
539    struct SpyPersister {
540        seen: Mutex<Vec<(String, HashMap<String, String>)>>,
541    }
542
543    #[async_trait]
544    impl Persister for SpyPersister {
545        async fn create(&self, _cx: &mut SaveCx<'_>, rec: &Record) -> Result<i64, SaveError> {
546            self.seen
547                .lock()
548                .unwrap()
549                .push(("create".into(), rec.to_text_map()));
550            Ok(42)
551        }
552        async fn update(
553            &self,
554            _cx: &mut SaveCx<'_>,
555            id: &str,
556            rec: &Record,
557        ) -> Result<(), SaveError> {
558            self.seen
559                .lock()
560                .unwrap()
561                .push((format!("update:{id}"), rec.to_text_map()));
562            Ok(())
563        }
564    }
565
566    /// Records deletes, and can refuse them.
567    #[derive(Default)]
568    struct DeleteSpy {
569        deleted: Mutex<Vec<String>>,
570        refuse: Option<&'static str>,
571    }
572
573    #[async_trait]
574    impl Persister for DeleteSpy {
575        async fn create(&self, _cx: &mut SaveCx<'_>, _rec: &Record) -> Result<i64, SaveError> {
576            Ok(1)
577        }
578        async fn update(
579            &self,
580            _cx: &mut SaveCx<'_>,
581            _id: &str,
582            _rec: &Record,
583        ) -> Result<(), SaveError> {
584            Ok(())
585        }
586        async fn load(&self, _cx: &mut SaveCx<'_>, id: &str) -> Result<Option<Record>, SaveError> {
587            let mut rec = Record::new("samples");
588            rec.set("code", format!("code-{id}"));
589            if let Ok(n) = id.parse::<i64>() {
590                rec.set_id(n);
591            }
592            Ok(Some(rec))
593        }
594        async fn delete(
595            &self,
596            _cx: &mut SaveCx<'_>,
597            id: &str,
598            _rec: &Record,
599        ) -> Result<(), DeleteError> {
600            if let Some(why) = self.refuse {
601                return Err(DeleteError::Refused(Text::new(why)));
602            }
603            self.deleted.lock().unwrap().push(id.to_string());
604            Ok(())
605        }
606    }
607
608    /// Refuses every delete, the way a listener guarding its references would.
609    struct Guard;
610
611    #[async_trait]
612    impl ModelListener for Guard {
613        async fn before_delete(&self, _cx: &mut SaveCx<'_>, _rec: &Record) -> Result<(), Text> {
614            Err(Text::new("still referenced"))
615        }
616    }
617
618    /// Notes what the after-stage saw, which is the record as it was.
619    #[derive(Default)]
620    struct Mourner {
621        seen: Arc<Mutex<Vec<String>>>,
622    }
623
624    #[async_trait]
625    impl ModelListener for Mourner {
626        async fn after_delete(&self, _cx: &SavedCx<'_>, rec: &Record) {
627            self.seen
628                .lock()
629                .unwrap()
630                .push(rec.text("code").unwrap_or_default().to_string());
631        }
632    }
633
634    #[tokio::test]
635    async fn a_delete_runs_the_listeners_around_the_persister() {
636        let (db, _guard) = connect_test(&[]).await;
637        let persister = DeleteSpy::default();
638        let seen = Arc::new(Mutex::new(Vec::new()));
639        let listeners: Vec<Arc<dyn ModelListener>> = vec![Arc::new(Mourner { seen: seen.clone() })];
640
641        let rec = delete(DeleteRequest {
642            db: &db,
643            listeners: &listeners,
644            persister: &persister,
645            actor: &actor(),
646            entity: "samples",
647            id: "7",
648        })
649        .await
650        .expect("deleted");
651
652        assert_eq!(persister.deleted.lock().unwrap().as_slice(), ["7"]);
653        // The after-stage sees the row as it was, which is the point of reading
654        // it before the delete.
655        assert_eq!(seen.lock().unwrap().as_slice(), ["code-7"]);
656        assert_eq!(rec.text("code"), Some("code-7"));
657        assert_eq!(rec.id(), Some(7));
658    }
659
660    /// A listener's refusal stops the delete: the persister is never reached and
661    /// the after-stage never runs.
662    #[tokio::test]
663    async fn a_listener_can_refuse_a_delete() {
664        let (db, _guard) = connect_test(&[]).await;
665        let persister = DeleteSpy::default();
666        let seen = Arc::new(Mutex::new(Vec::new()));
667        let listeners: Vec<Arc<dyn ModelListener>> =
668            vec![Arc::new(Guard), Arc::new(Mourner { seen: seen.clone() })];
669
670        let err = delete(DeleteRequest {
671            db: &db,
672            listeners: &listeners,
673            persister: &persister,
674            actor: &actor(),
675            entity: "samples",
676            id: "7",
677        })
678        .await
679        .expect_err("refused");
680
681        match err {
682            DeleteError::Refused(message) => assert_eq!(message.source(), "still referenced"),
683            other => panic!("expected a refusal, got {other:?}"),
684        }
685        assert!(
686            persister.deleted.lock().unwrap().is_empty(),
687            "nothing removed"
688        );
689        assert!(
690            seen.lock().unwrap().is_empty(),
691            "no after-stage on a refusal"
692        );
693    }
694
695    /// A persister with no delete of its own says so rather than reporting
696    /// success, since a silent no-op would look like the record went.
697    #[tokio::test]
698    async fn a_persister_without_delete_refuses() {
699        let (db, _guard) = connect_test(&[]).await;
700        let persister = SpyPersister::default();
701        let err = delete(DeleteRequest {
702            db: &db,
703            listeners: &[],
704            persister: &persister,
705            actor: &actor(),
706            entity: "samples",
707            id: "1",
708        })
709        .await
710        .expect_err("unsupported");
711        assert!(matches!(err, DeleteError::Refused(_)));
712    }
713
714    /// Sets an attribute before the write, and appends its name to a shared log
715    /// after it.
716    struct Stamp {
717        key: &'static str,
718        log: Arc<Mutex<Vec<&'static str>>>,
719    }
720
721    #[async_trait]
722    impl ModelListener for Stamp {
723        async fn before_save(
724            &self,
725            _cx: &mut SaveCx<'_>,
726            rec: &mut Record,
727            _op: Op,
728        ) -> Result<(), ErrorBag> {
729            rec.set(self.key, "set");
730            Ok(())
731        }
732        async fn after_save(&self, _cx: &SavedCx<'_>, _rec: &Record, _op: Op) {
733            self.log.lock().unwrap().push(self.key);
734        }
735    }
736
737    /// Refuses every write.
738    struct Veto;
739
740    #[async_trait]
741    impl ModelListener for Veto {
742        async fn before_save(
743            &self,
744            _cx: &mut SaveCx<'_>,
745            _rec: &mut Record,
746            _op: Op,
747        ) -> Result<(), ErrorBag> {
748            let mut bag = ErrorBag::default();
749            // A plain Text, not `t!`: a test fixture must not enter the catalog.
750            bag.add("name", Text::new("Not allowed"));
751            Err(bag)
752        }
753    }
754
755    fn actor() -> Actor {
756        Actor::system("test")
757    }
758
759    fn listeners(regs: &[ModelListenerReg], entity: &str) -> Vec<Arc<dyn ModelListener>> {
760        regs.iter()
761            .filter(|r| r.matches(entity))
762            .map(|r| r.listener.clone())
763            .collect()
764    }
765
766    #[tokio::test]
767    async fn a_listener_mutation_reaches_the_persister_and_the_id_comes_back() {
768        let (db, _guard) = connect_test(&[]).await;
769        let spy = SpyPersister::default();
770        let log = Arc::new(Mutex::new(Vec::new()));
771        let regs = vec![ModelListenerReg::all(Arc::new(Stamp {
772            key: "created_at",
773            log: log.clone(),
774        }))];
775        let mut rec = Record::new("samples");
776        rec.set("name", "Chair");
777
778        save(
779            SaveRequest {
780                db: &db,
781                listeners: &listeners(&regs, "samples"),
782                persister: &spy,
783                actor: &actor(),
784                op: Op::Create,
785                id: None,
786            },
787            &mut rec,
788        )
789        .await
790        .unwrap();
791
792        let seen = spy.seen.lock().unwrap();
793        assert_eq!(seen[0].0, "create");
794        assert_eq!(seen[0].1.get("created_at").map(String::as_str), Some("set"));
795        assert_eq!(rec.id(), Some(42), "the create id lands on the record");
796        assert_eq!(*log.lock().unwrap(), ["created_at"]);
797    }
798
799    #[tokio::test]
800    async fn a_veto_stops_the_write_and_returns_field_messages() {
801        let (db, _guard) = connect_test(&[]).await;
802        let spy = SpyPersister::default();
803        let regs = vec![ModelListenerReg::all(Arc::new(Veto))];
804        let mut rec = Record::new("samples");
805
806        let err = save(
807            SaveRequest {
808                db: &db,
809                listeners: &listeners(&regs, "samples"),
810                persister: &spy,
811                actor: &actor(),
812                op: Op::Create,
813                id: None,
814            },
815            &mut rec,
816        )
817        .await
818        .expect_err("the veto refuses the write");
819
820        match err {
821            SaveError::Invalid(bag) => assert_eq!(bag.messages("name").len(), 1),
822            SaveError::Failed(e) => panic!("expected a field error, got {e}"),
823        }
824        assert!(
825            spy.seen.lock().unwrap().is_empty(),
826            "nothing reached the persister"
827        );
828    }
829
830    #[tokio::test]
831    async fn listeners_run_in_registration_order() {
832        let (db, _guard) = connect_test(&[]).await;
833        let log = Arc::new(Mutex::new(Vec::new()));
834        let regs = vec![
835            ModelListenerReg::all(Arc::new(Stamp {
836                key: "first",
837                log: log.clone(),
838            })),
839            ModelListenerReg::all(Arc::new(Stamp {
840                key: "second",
841                log: log.clone(),
842            })),
843        ];
844        let mut rec = Record::new("samples");
845
846        save(
847            SaveRequest {
848                db: &db,
849                listeners: &listeners(&regs, "samples"),
850                persister: &SpyPersister::default(),
851                actor: &actor(),
852                op: Op::Create,
853                id: None,
854            },
855            &mut rec,
856        )
857        .await
858        .unwrap();
859
860        assert_eq!(*log.lock().unwrap(), ["first", "second"]);
861    }
862
863    #[tokio::test]
864    async fn a_target_selects_which_listeners_run() {
865        let (db, _guard) = connect_test(&[]).await;
866        let log = Arc::new(Mutex::new(Vec::new()));
867        let regs = vec![
868            ModelListenerReg::all(Arc::new(Stamp {
869                key: "everywhere",
870                log: log.clone(),
871            })),
872            ModelListenerReg::for_entity(
873                "orders",
874                Arc::new(Stamp {
875                    key: "orders_only",
876                    log: log.clone(),
877                }),
878            ),
879        ];
880        let mut rec = Record::new("samples");
881
882        save(
883            SaveRequest {
884                db: &db,
885                listeners: &listeners(&regs, "samples"),
886                persister: &SpyPersister::default(),
887                actor: &actor(),
888                op: Op::Create,
889                id: None,
890            },
891            &mut rec,
892        )
893        .await
894        .unwrap();
895
896        assert_eq!(
897            *log.lock().unwrap(),
898            ["everywhere"],
899            "another entity's listener stays out"
900        );
901        assert!(matches!(regs[1].target, ListenerTarget::Entity(ref e) if e == "orders"));
902    }
903
904    #[tokio::test]
905    async fn a_listener_injected_attribute_is_written_not_dropped() {
906        let (db, _guard) = connect_test(&[]).await;
907        let spy = SpyPersister::default();
908        let log = Arc::new(Mutex::new(Vec::new()));
909        let regs = vec![ModelListenerReg::all(Arc::new(Stamp {
910            key: "created_at",
911            log,
912        }))];
913        let mut rec = Record::new("samples");
914        rec.set("name", "Chair");
915
916        save(
917            SaveRequest {
918                db: &db,
919                listeners: &listeners(&regs, "samples"),
920                persister: &spy,
921                actor: &actor(),
922                op: Op::Create,
923                id: None,
924            },
925            &mut rec,
926        )
927        .await
928        .unwrap();
929
930        let default = DefaultPersister {
931            entity: "samples".into(),
932            id_field: "id".into(),
933            columns: vec!["name".into()],
934        };
935        // The descriptor knows only `name`; the listener's key widens the write.
936        assert_eq!(default.write_columns(&rec), ["name", "created_at"]);
937    }
938
939    #[test]
940    fn the_id_column_is_never_written() {
941        let default = DefaultPersister {
942            entity: "samples".into(),
943            id_field: "id".into(),
944            columns: vec!["name".into()],
945        };
946        let mut rec = Record::with_id("samples", 3);
947        rec.set("name", "Chair").set("id", 3i64);
948        assert_eq!(default.write_columns(&rec), ["name"]);
949    }
950
951    #[test]
952    fn binding_maps_each_kind_to_its_storage_form() {
953        // Absent, null and empty text all render the NULL keyword, so an
954        // optional column of any type stores rather than failing on "".
955        let null = SimpleExpr::Keyword(sea_query::Keyword::Null);
956        assert_eq!(bind(None), null);
957        assert_eq!(bind(Some(&AttrValue::Null)), null);
958        assert_eq!(bind(Some(&AttrValue::Text(String::new()))), null);
959        assert_eq!(bind(Some(&AttrValue::Int(7))), 7i64.into());
960        // Booleans store as integers, the representation every backend indexes.
961        assert_eq!(bind(Some(&AttrValue::Bool(true))), 1i32.into());
962        assert_eq!(
963            bind(Some(&AttrValue::Text("Chair".into()))),
964            Some("Chair".to_string()).into()
965        );
966    }
967
968    /// Supplies a pre-write snapshot, as the built-in persister does.
969    struct SnapshotPersister;
970
971    #[async_trait]
972    impl Persister for SnapshotPersister {
973        async fn create(&self, _cx: &mut SaveCx<'_>, _rec: &Record) -> Result<i64, SaveError> {
974            Ok(1)
975        }
976        async fn update(
977            &self,
978            _cx: &mut SaveCx<'_>,
979            _id: &str,
980            _rec: &Record,
981        ) -> Result<(), SaveError> {
982            Ok(())
983        }
984        async fn load(&self, _cx: &mut SaveCx<'_>, _id: &str) -> Result<Option<Record>, SaveError> {
985            let mut previous = Record::new("samples");
986            previous.set("name", "Chair");
987            Ok(Some(previous))
988        }
989    }
990
991    /// Reads the snapshot in `before_save` and records what it saw.
992    struct WatchChanges {
993        seen: Arc<Mutex<Vec<(bool, bool)>>>,
994    }
995
996    #[async_trait]
997    impl ModelListener for WatchChanges {
998        async fn before_save(
999            &self,
1000            _cx: &mut SaveCx<'_>,
1001            rec: &mut Record,
1002            _op: Op,
1003        ) -> Result<(), ErrorBag> {
1004            self.seen
1005                .lock()
1006                .unwrap()
1007                .push((rec.has_original(), rec.changed("name")));
1008            Ok(())
1009        }
1010    }
1011
1012    #[tokio::test]
1013    async fn an_update_hands_listeners_the_pre_write_row() {
1014        let (db, _guard) = connect_test(&[]).await;
1015        let seen = Arc::new(Mutex::new(Vec::new()));
1016        let regs = vec![ModelListenerReg::all(Arc::new(WatchChanges {
1017            seen: seen.clone(),
1018        }))];
1019        let mut rec = Record::with_id("samples", 1);
1020        rec.set("name", "Stool");
1021
1022        save(
1023            SaveRequest {
1024                db: &db,
1025                listeners: &listeners(&regs, "samples"),
1026                persister: &SnapshotPersister,
1027                actor: &actor(),
1028                op: Op::Update,
1029                id: Some("1"),
1030            },
1031            &mut rec,
1032        )
1033        .await
1034        .unwrap();
1035
1036        assert_eq!(*seen.lock().unwrap(), [(true, true)]);
1037    }
1038
1039    #[tokio::test]
1040    async fn a_create_has_no_snapshot() {
1041        let (db, _guard) = connect_test(&[]).await;
1042        let seen = Arc::new(Mutex::new(Vec::new()));
1043        let regs = vec![ModelListenerReg::all(Arc::new(WatchChanges {
1044            seen: seen.clone(),
1045        }))];
1046        let mut rec = Record::new("samples");
1047        rec.set("name", "Chair");
1048
1049        save(
1050            SaveRequest {
1051                db: &db,
1052                listeners: &listeners(&regs, "samples"),
1053                persister: &SnapshotPersister,
1054                actor: &actor(),
1055                op: Op::Create,
1056                id: None,
1057            },
1058            &mut rec,
1059        )
1060        .await
1061        .unwrap();
1062
1063        assert_eq!(*seen.lock().unwrap(), [(false, false)]);
1064    }
1065
1066    #[tokio::test]
1067    async fn an_update_passes_its_id_through() {
1068        let (db, _guard) = connect_test(&[]).await;
1069        let spy = SpyPersister::default();
1070        let mut rec = Record::with_id("samples", 7);
1071
1072        save(
1073            SaveRequest {
1074                db: &db,
1075                listeners: &[],
1076                persister: &spy,
1077                actor: &actor(),
1078                op: Op::Update,
1079                id: Some("7"),
1080            },
1081            &mut rec,
1082        )
1083        .await
1084        .unwrap();
1085
1086        assert_eq!(spy.seen.lock().unwrap()[0].0, "update:7");
1087    }
1088
1089    #[tokio::test]
1090    async fn an_update_without_an_id_fails_rather_than_writing() {
1091        let (db, _guard) = connect_test(&[]).await;
1092        let spy = SpyPersister::default();
1093        let mut rec = Record::new("samples");
1094
1095        let err = save(
1096            SaveRequest {
1097                db: &db,
1098                listeners: &[],
1099                persister: &spy,
1100                actor: &actor(),
1101                op: Op::Update,
1102                id: None,
1103            },
1104            &mut rec,
1105        )
1106        .await
1107        .expect_err("an update needs an id");
1108
1109        assert!(matches!(err, SaveError::Failed(_)));
1110        assert!(spy.seen.lock().unwrap().is_empty());
1111    }
1112}