1use 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#[derive(Debug)]
25pub enum SaveError {
26 Invalid(ErrorBag),
29 Failed(String),
32}
33
34#[derive(Debug)]
36pub enum DeleteError {
37 Refused(Text),
41 Failed(String),
44}
45
46#[async_trait]
51pub trait Persister: Send + Sync + 'static {
52 async fn create(&self, cx: &mut SaveCx<'_>, rec: &Record) -> Result<i64, SaveError>;
54 async fn update(&self, cx: &mut SaveCx<'_>, id: &str, rec: &Record) -> Result<(), SaveError>;
56
57 async fn load(&self, cx: &mut SaveCx<'_>, id: &str) -> Result<Option<Record>, SaveError> {
61 let _ = (cx, id);
62 Ok(None)
63 }
64
65 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 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#[derive(Debug)]
114#[non_exhaustive]
115pub struct MoveTo<'a> {
116 pub entity: &'a str,
117 pub id_field: &'a str,
118 pub id: &'a str,
120 pub parent_key: &'a str,
122 pub parent: Option<&'a str>,
124}
125
126fn 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
144pub(crate) struct SaveRequest<'a> {
150 pub db: &'a Db,
151 pub listeners: &'a [Arc<dyn ModelListener>],
152 pub persister: &'a dyn Persister,
153 pub actor: &'a Actor,
155 pub op: Op,
156 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 let _ = tx.rollback().await;
195 return Err(e);
196 }
197 }
198
199 let saved = SavedCx::new(db, actor);
201 for listener in listeners {
202 listener.after_save(&saved, rec, op).await;
203 }
204 Ok(())
205}
206
207pub(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
217pub(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 let _ = tx.rollback().await;
259 return Err(e);
260 }
261 };
262
263 let saved = SavedCx::new(db, actor);
265 for listener in listeners {
266 listener.after_delete(&saved, &rec).await;
267 }
268 Ok(rec)
269}
270
271async 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 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#[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 _ => return Err(SaveError::Failed(format!("unsupported operation {op:?}"))),
350 }
351 Ok(())
352}
353
354pub 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
369pub type PersisterRegistry = HashMap<String, Arc<dyn Persister>>;
371
372pub(crate) struct DefaultPersister {
376 entity: String,
377 id_field: String,
378 columns: Vec<String>,
379}
380
381impl DefaultPersister {
382 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 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 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 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 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 #[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 #[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 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 #[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 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 #[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 #[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 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 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 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(®s, "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(®s, "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(®s, "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(®s, "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(®s, "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 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 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 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 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 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(®s, "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(®s, "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}