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 cache: Option<&'a laterite_core::Cache>,
154 pub listeners: &'a [Arc<dyn ModelListener>],
155 pub persister: &'a dyn Persister,
156 pub actor: &'a Actor,
158 pub op: Op,
159 pub id: Option<&'a str>,
161}
162
163pub(crate) async fn save(req: SaveRequest<'_>, rec: &mut Record) -> Result<(), SaveError> {
164 let SaveRequest {
165 db,
166 cache,
167 listeners,
168 persister,
169 actor,
170 op,
171 id,
172 } = req;
173 let mut tx = db
174 .pool
175 .begin()
176 .await
177 .map_err(|e| SaveError::Failed(e.to_string()))?;
178
179 let outcome = save_in(
180 &mut tx,
181 db.backend,
182 listeners,
183 persister,
184 actor.clone(),
185 rec,
186 op,
187 id,
188 )
189 .await;
190 match outcome {
191 Ok(()) => tx
192 .commit()
193 .await
194 .map_err(|e| SaveError::Failed(e.to_string()))?,
195 Err(e) => {
196 let _ = tx.rollback().await;
199 return Err(e);
200 }
201 }
202
203 if let Some(cache) = cache {
206 let _ = cache
207 .bump(&laterite_core::cache::records(rec.entity()))
208 .await;
209 }
210 let saved = SavedCx::new(db, actor);
211 for listener in listeners {
212 listener.after_save(&saved, rec, op).await;
213 }
214 Ok(())
215}
216
217pub(crate) struct DeleteRequest<'a> {
219 pub db: &'a Db,
220 pub cache: Option<&'a laterite_core::Cache>,
221 pub listeners: &'a [Arc<dyn ModelListener>],
222 pub persister: &'a dyn Persister,
223 pub actor: &'a Actor,
224 pub entity: &'a str,
225 pub id: &'a str,
226}
227
228pub(crate) async fn delete(req: DeleteRequest<'_>) -> Result<Record, DeleteError> {
234 let DeleteRequest {
235 db,
236 cache,
237 listeners,
238 persister,
239 actor,
240 entity,
241 id,
242 } = req;
243
244 let mut tx = db
245 .pool
246 .begin()
247 .await
248 .map_err(|e| DeleteError::Failed(e.to_string()))?;
249
250 let outcome = delete_in(
251 &mut tx,
252 db.backend,
253 listeners,
254 persister,
255 actor.clone(),
256 entity,
257 id,
258 )
259 .await;
260 let rec = match outcome {
261 Ok(rec) => {
262 tx.commit()
263 .await
264 .map_err(|e| DeleteError::Failed(e.to_string()))?;
265 rec
266 }
267 Err(e) => {
268 let _ = tx.rollback().await;
271 return Err(e);
272 }
273 };
274
275 if let Some(cache) = cache {
277 let _ = cache.bump(&laterite_core::cache::records(entity)).await;
278 }
279 let saved = SavedCx::new(db, actor);
280 for listener in listeners {
281 listener.after_delete(&saved, &rec).await;
282 }
283 Ok(rec)
284}
285
286async fn delete_in(
289 tx: &mut sqlx::Transaction<'_, sqlx::Any>,
290 backend: laterite_core::migration::DbBackend,
291 listeners: &[Arc<dyn ModelListener>],
292 persister: &dyn Persister,
293 actor: Actor,
294 entity: &str,
295 id: &str,
296) -> Result<Record, DeleteError> {
297 let mut cx = SaveCx::new(backend, tx, actor);
298
299 let loaded = persister.load(&mut cx, id).await.map_err(|e| match e {
303 SaveError::Invalid(_) => DeleteError::Failed("snapshot read refused".to_string()),
304 SaveError::Failed(m) => DeleteError::Failed(m),
305 })?;
306 let mut rec = loaded.unwrap_or_else(|| Record::new(entity));
307 if rec.id().is_none() {
308 if let Ok(numeric) = id.parse::<i64>() {
309 rec.set_id(numeric);
310 }
311 }
312
313 for listener in listeners {
314 listener
315 .before_delete(&mut cx, &rec)
316 .await
317 .map_err(DeleteError::Refused)?;
318 }
319
320 persister.delete(&mut cx, id, &rec).await?;
321 Ok(rec)
322}
323
324#[allow(clippy::too_many_arguments)]
327async fn save_in(
328 tx: &mut sqlx::Transaction<'_, sqlx::Any>,
329 backend: laterite_core::migration::DbBackend,
330 listeners: &[Arc<dyn ModelListener>],
331 persister: &dyn Persister,
332 actor: Actor,
333 rec: &mut Record,
334 op: Op,
335 id: Option<&str>,
336) -> Result<(), SaveError> {
337 let mut cx = SaveCx::new(backend, tx, actor);
338
339 if op == Op::Update {
340 if let Some(id) = id {
341 if let Some(previous) = persister.load(&mut cx, id).await? {
342 rec.set_original(previous);
343 }
344 }
345 }
346
347 for listener in listeners {
348 listener
349 .before_save(&mut cx, rec, op)
350 .await
351 .map_err(SaveError::Invalid)?;
352 }
353
354 match op {
355 Op::Create => {
356 let new_id = persister.create(&mut cx, rec).await?;
357 rec.set_id(new_id);
358 }
359 Op::Update => {
360 let id = id.ok_or_else(|| SaveError::Failed("update without an id".to_string()))?;
361 persister.update(&mut cx, id, rec).await?;
362 }
363 _ => return Err(SaveError::Failed(format!("unsupported operation {op:?}"))),
365 }
366 Ok(())
367}
368
369pub struct PersisterReg {
371 pub name: String,
372 pub persister: Arc<dyn Persister>,
373}
374
375impl PersisterReg {
376 pub fn new(name: impl Into<String>, persister: Arc<dyn Persister>) -> Self {
377 Self {
378 name: name.into(),
379 persister,
380 }
381 }
382}
383
384pub type PersisterRegistry = HashMap<String, Arc<dyn Persister>>;
386
387pub(crate) struct DefaultPersister {
391 entity: String,
392 id_field: String,
393 columns: Vec<String>,
394}
395
396impl DefaultPersister {
397 fn write_columns(&self, rec: &Record) -> Vec<String> {
403 let mut cols = self.columns.clone();
404 for (key, _) in rec.iter() {
405 if key != self.id_field && !cols.iter().any(|c| c == key) {
406 cols.push(key.to_string());
407 }
408 }
409 cols.retain(|c| c != &self.id_field && crate::sql::valid_ident(c));
410 cols
411 }
412
413 pub(crate) fn from_list(config: &crate::list::ListConfig) -> Self {
417 Self {
418 entity: config.entity.clone(),
419 id_field: config.id_field.clone(),
420 columns: config.columns.iter().map(|c| c.field.clone()).collect(),
421 }
422 }
423
424 pub(crate) fn from_config(config: &FormConfig) -> Self {
425 Self {
426 entity: config.entity.clone(),
427 id_field: config.id_field.clone(),
428 columns: config.fields.iter().map(|f| f.name.clone()).collect(),
429 }
430 }
431}
432
433#[async_trait]
434impl Persister for DefaultPersister {
435 async fn create(&self, cx: &mut SaveCx<'_>, rec: &Record) -> Result<i64, SaveError> {
436 let stmt = {
439 let cols = self.write_columns(rec);
440 let vals: Vec<SimpleExpr> = cols.iter().map(|c| bind(rec.get(c))).collect();
441 Query::insert()
442 .into_table(Alias::new(&self.entity))
443 .columns(cols.iter().map(Alias::new))
444 .values_panic(vals)
445 .to_owned()
446 };
447 let backend = cx.backend();
448 insert_returning_id_on(backend, cx.conn(), stmt, Alias::new(&self.id_field))
449 .await
450 .map_err(|e| SaveError::Failed(e.to_string()))
451 }
452
453 async fn update(&self, cx: &mut SaveCx<'_>, id: &str, rec: &Record) -> Result<(), SaveError> {
454 let backend = cx.backend();
455 let columns: Vec<String> = rec
458 .iter()
459 .map(|(key, _)| key.to_string())
460 .filter(|c| c != &self.id_field && crate::sql::valid_ident(c))
461 .collect();
462 if columns.is_empty() {
463 return Ok(());
464 }
465 let (sql, values) = {
466 let mut update = Query::update();
467 update.table(Alias::new(&self.entity));
468 for column in &columns {
469 update.value(Alias::new(column), bind(rec.get(column)));
470 }
471 update.and_where(
472 Expr::col(Alias::new(&self.id_field))
473 .cast_as(Alias::new(text_cast(backend)))
474 .eq(id),
475 );
476 to_sql(backend, update)
477 };
478 bind_values(sqlx::query(&sql), values)
479 .execute(cx.conn())
480 .await
481 .map(|_| ())
482 .map_err(|e| SaveError::Failed(e.to_string()))
483 }
484
485 async fn delete(
488 &self,
489 cx: &mut SaveCx<'_>,
490 id: &str,
491 _rec: &Record,
492 ) -> Result<(), DeleteError> {
493 let backend = cx.backend();
494 let (sql, values) = {
495 let mut del = Query::delete();
496 del.from_table(Alias::new(&self.entity));
497 del.and_where(
498 Expr::col(Alias::new(&self.id_field))
499 .cast_as(Alias::new(text_cast(backend)))
500 .eq(id),
501 );
502 to_sql(backend, del)
503 };
504 bind_values(sqlx::query(&sql), values)
505 .execute(cx.conn())
506 .await
507 .map(|_| ())
508 .map_err(|e| DeleteError::Failed(e.to_string()))
509 }
510
511 async fn load(&self, cx: &mut SaveCx<'_>, id: &str) -> Result<Option<Record>, SaveError> {
515 let backend = cx.backend();
516 let (sql, values) = {
517 let mut select = Query::select();
518 select.from(Alias::new(&self.entity));
519 for column in &self.columns {
520 select.expr_as(
521 Expr::col(Alias::new(column)).cast_as(Alias::new(text_cast(backend))),
522 Alias::new(column),
523 );
524 }
525 select.and_where(
526 Expr::col(Alias::new(&self.id_field))
527 .cast_as(Alias::new(text_cast(backend)))
528 .eq(id),
529 );
530 to_sql(backend, select)
531 };
532 let row = bind_values(sqlx::query(&sql), values)
533 .fetch_optional(cx.conn())
534 .await
535 .map_err(|e| SaveError::Failed(e.to_string()))?;
536 let Some(row) = row else {
537 return Ok(None);
538 };
539 let mut previous = Record::new(&self.entity);
540 for column in &self.columns {
541 match row.get_text_opt(column) {
542 Ok(Some(v)) => {
543 previous.set(column.clone(), v);
544 }
545 Ok(None) => {
546 previous.set(column.clone(), AttrValue::Null);
547 }
548 Err(e) => return Err(SaveError::Failed(e.to_string())),
549 }
550 }
551 Ok(Some(previous))
552 }
553}
554
555#[cfg(test)]
556mod pipeline_tests {
557 use super::*;
558 use laterite_core::testing::connect_test;
559 use laterite_core::{ListenerTarget, ModelListenerReg, Text};
560 use std::sync::Mutex;
561
562 #[derive(Default)]
564 struct SpyPersister {
565 seen: Mutex<Vec<(String, HashMap<String, String>)>>,
566 }
567
568 #[async_trait]
569 impl Persister for SpyPersister {
570 async fn create(&self, _cx: &mut SaveCx<'_>, rec: &Record) -> Result<i64, SaveError> {
571 self.seen
572 .lock()
573 .unwrap()
574 .push(("create".into(), rec.to_text_map()));
575 Ok(42)
576 }
577 async fn update(
578 &self,
579 _cx: &mut SaveCx<'_>,
580 id: &str,
581 rec: &Record,
582 ) -> Result<(), SaveError> {
583 self.seen
584 .lock()
585 .unwrap()
586 .push((format!("update:{id}"), rec.to_text_map()));
587 Ok(())
588 }
589 }
590
591 #[derive(Default)]
593 struct DeleteSpy {
594 deleted: Mutex<Vec<String>>,
595 refuse: Option<&'static str>,
596 }
597
598 #[async_trait]
599 impl Persister for DeleteSpy {
600 async fn create(&self, _cx: &mut SaveCx<'_>, _rec: &Record) -> Result<i64, SaveError> {
601 Ok(1)
602 }
603 async fn update(
604 &self,
605 _cx: &mut SaveCx<'_>,
606 _id: &str,
607 _rec: &Record,
608 ) -> Result<(), SaveError> {
609 Ok(())
610 }
611 async fn load(&self, _cx: &mut SaveCx<'_>, id: &str) -> Result<Option<Record>, SaveError> {
612 let mut rec = Record::new("samples");
613 rec.set("code", format!("code-{id}"));
614 if let Ok(n) = id.parse::<i64>() {
615 rec.set_id(n);
616 }
617 Ok(Some(rec))
618 }
619 async fn delete(
620 &self,
621 _cx: &mut SaveCx<'_>,
622 id: &str,
623 _rec: &Record,
624 ) -> Result<(), DeleteError> {
625 if let Some(why) = self.refuse {
626 return Err(DeleteError::Refused(Text::new(why)));
627 }
628 self.deleted.lock().unwrap().push(id.to_string());
629 Ok(())
630 }
631 }
632
633 struct Guard;
635
636 #[async_trait]
637 impl ModelListener for Guard {
638 async fn before_delete(&self, _cx: &mut SaveCx<'_>, _rec: &Record) -> Result<(), Text> {
639 Err(Text::new("still referenced"))
640 }
641 }
642
643 #[derive(Default)]
645 struct Mourner {
646 seen: Arc<Mutex<Vec<String>>>,
647 }
648
649 #[async_trait]
650 impl ModelListener for Mourner {
651 async fn after_delete(&self, _cx: &SavedCx<'_>, rec: &Record) {
652 self.seen
653 .lock()
654 .unwrap()
655 .push(rec.text("code").unwrap_or_default().to_string());
656 }
657 }
658
659 #[tokio::test]
660 async fn a_delete_runs_the_listeners_around_the_persister() {
661 let (db, _guard) = connect_test(&[]).await;
662 let persister = DeleteSpy::default();
663 let seen = Arc::new(Mutex::new(Vec::new()));
664 let listeners: Vec<Arc<dyn ModelListener>> = vec![Arc::new(Mourner { seen: seen.clone() })];
665
666 let rec = delete(DeleteRequest {
667 db: &db,
668 cache: None,
669 listeners: &listeners,
670 persister: &persister,
671 actor: &actor(),
672 entity: "samples",
673 id: "7",
674 })
675 .await
676 .expect("deleted");
677
678 assert_eq!(persister.deleted.lock().unwrap().as_slice(), ["7"]);
679 assert_eq!(seen.lock().unwrap().as_slice(), ["code-7"]);
682 assert_eq!(rec.text("code"), Some("code-7"));
683 assert_eq!(rec.id(), Some(7));
684 }
685
686 #[tokio::test]
689 async fn a_listener_can_refuse_a_delete() {
690 let (db, _guard) = connect_test(&[]).await;
691 let persister = DeleteSpy::default();
692 let seen = Arc::new(Mutex::new(Vec::new()));
693 let listeners: Vec<Arc<dyn ModelListener>> =
694 vec![Arc::new(Guard), Arc::new(Mourner { seen: seen.clone() })];
695
696 let err = delete(DeleteRequest {
697 db: &db,
698 cache: None,
699 listeners: &listeners,
700 persister: &persister,
701 actor: &actor(),
702 entity: "samples",
703 id: "7",
704 })
705 .await
706 .expect_err("refused");
707
708 match err {
709 DeleteError::Refused(message) => assert_eq!(message.source(), "still referenced"),
710 other => panic!("expected a refusal, got {other:?}"),
711 }
712 assert!(
713 persister.deleted.lock().unwrap().is_empty(),
714 "nothing removed"
715 );
716 assert!(
717 seen.lock().unwrap().is_empty(),
718 "no after-stage on a refusal"
719 );
720 }
721
722 #[tokio::test]
725 async fn a_persister_without_delete_refuses() {
726 let (db, _guard) = connect_test(&[]).await;
727 let persister = SpyPersister::default();
728 let err = delete(DeleteRequest {
729 db: &db,
730 cache: None,
731 listeners: &[],
732 persister: &persister,
733 actor: &actor(),
734 entity: "samples",
735 id: "1",
736 })
737 .await
738 .expect_err("unsupported");
739 assert!(matches!(err, DeleteError::Refused(_)));
740 }
741
742 struct Stamp {
745 key: &'static str,
746 log: Arc<Mutex<Vec<&'static str>>>,
747 }
748
749 #[async_trait]
750 impl ModelListener for Stamp {
751 async fn before_save(
752 &self,
753 _cx: &mut SaveCx<'_>,
754 rec: &mut Record,
755 _op: Op,
756 ) -> Result<(), ErrorBag> {
757 rec.set(self.key, "set");
758 Ok(())
759 }
760 async fn after_save(&self, _cx: &SavedCx<'_>, _rec: &Record, _op: Op) {
761 self.log.lock().unwrap().push(self.key);
762 }
763 }
764
765 struct Veto;
767
768 #[async_trait]
769 impl ModelListener for Veto {
770 async fn before_save(
771 &self,
772 _cx: &mut SaveCx<'_>,
773 _rec: &mut Record,
774 _op: Op,
775 ) -> Result<(), ErrorBag> {
776 let mut bag = ErrorBag::default();
777 bag.add("name", Text::new("Not allowed"));
779 Err(bag)
780 }
781 }
782
783 fn actor() -> Actor {
784 Actor::system("test")
785 }
786
787 fn listeners(regs: &[ModelListenerReg], entity: &str) -> Vec<Arc<dyn ModelListener>> {
788 regs.iter()
789 .filter(|r| r.matches(entity))
790 .map(|r| r.listener.clone())
791 .collect()
792 }
793
794 #[tokio::test]
795 async fn a_listener_mutation_reaches_the_persister_and_the_id_comes_back() {
796 let (db, _guard) = connect_test(&[]).await;
797 let spy = SpyPersister::default();
798 let log = Arc::new(Mutex::new(Vec::new()));
799 let regs = vec![ModelListenerReg::all(Arc::new(Stamp {
800 key: "created_at",
801 log: log.clone(),
802 }))];
803 let mut rec = Record::new("samples");
804 rec.set("name", "Chair");
805
806 save(
807 SaveRequest {
808 db: &db,
809 cache: None,
810 listeners: &listeners(®s, "samples"),
811 persister: &spy,
812 actor: &actor(),
813 op: Op::Create,
814 id: None,
815 },
816 &mut rec,
817 )
818 .await
819 .unwrap();
820
821 let seen = spy.seen.lock().unwrap();
822 assert_eq!(seen[0].0, "create");
823 assert_eq!(seen[0].1.get("created_at").map(String::as_str), Some("set"));
824 assert_eq!(rec.id(), Some(42), "the create id lands on the record");
825 assert_eq!(*log.lock().unwrap(), ["created_at"]);
826 }
827
828 #[tokio::test]
829 async fn a_veto_stops_the_write_and_returns_field_messages() {
830 let (db, _guard) = connect_test(&[]).await;
831 let spy = SpyPersister::default();
832 let regs = vec![ModelListenerReg::all(Arc::new(Veto))];
833 let mut rec = Record::new("samples");
834
835 let err = save(
836 SaveRequest {
837 db: &db,
838 cache: None,
839 listeners: &listeners(®s, "samples"),
840 persister: &spy,
841 actor: &actor(),
842 op: Op::Create,
843 id: None,
844 },
845 &mut rec,
846 )
847 .await
848 .expect_err("the veto refuses the write");
849
850 match err {
851 SaveError::Invalid(bag) => assert_eq!(bag.messages("name").len(), 1),
852 SaveError::Failed(e) => panic!("expected a field error, got {e}"),
853 }
854 assert!(
855 spy.seen.lock().unwrap().is_empty(),
856 "nothing reached the persister"
857 );
858 }
859
860 #[tokio::test]
861 async fn listeners_run_in_registration_order() {
862 let (db, _guard) = connect_test(&[]).await;
863 let log = Arc::new(Mutex::new(Vec::new()));
864 let regs = vec![
865 ModelListenerReg::all(Arc::new(Stamp {
866 key: "first",
867 log: log.clone(),
868 })),
869 ModelListenerReg::all(Arc::new(Stamp {
870 key: "second",
871 log: log.clone(),
872 })),
873 ];
874 let mut rec = Record::new("samples");
875
876 save(
877 SaveRequest {
878 db: &db,
879 cache: None,
880 listeners: &listeners(®s, "samples"),
881 persister: &SpyPersister::default(),
882 actor: &actor(),
883 op: Op::Create,
884 id: None,
885 },
886 &mut rec,
887 )
888 .await
889 .unwrap();
890
891 assert_eq!(*log.lock().unwrap(), ["first", "second"]);
892 }
893
894 #[tokio::test]
895 async fn a_target_selects_which_listeners_run() {
896 let (db, _guard) = connect_test(&[]).await;
897 let log = Arc::new(Mutex::new(Vec::new()));
898 let regs = vec![
899 ModelListenerReg::all(Arc::new(Stamp {
900 key: "everywhere",
901 log: log.clone(),
902 })),
903 ModelListenerReg::for_entity(
904 "orders",
905 Arc::new(Stamp {
906 key: "orders_only",
907 log: log.clone(),
908 }),
909 ),
910 ];
911 let mut rec = Record::new("samples");
912
913 save(
914 SaveRequest {
915 db: &db,
916 cache: None,
917 listeners: &listeners(®s, "samples"),
918 persister: &SpyPersister::default(),
919 actor: &actor(),
920 op: Op::Create,
921 id: None,
922 },
923 &mut rec,
924 )
925 .await
926 .unwrap();
927
928 assert_eq!(
929 *log.lock().unwrap(),
930 ["everywhere"],
931 "another entity's listener stays out"
932 );
933 assert!(matches!(regs[1].target, ListenerTarget::Entity(ref e) if e == "orders"));
934 }
935
936 #[tokio::test]
937 async fn a_listener_injected_attribute_is_written_not_dropped() {
938 let (db, _guard) = connect_test(&[]).await;
939 let spy = SpyPersister::default();
940 let log = Arc::new(Mutex::new(Vec::new()));
941 let regs = vec![ModelListenerReg::all(Arc::new(Stamp {
942 key: "created_at",
943 log,
944 }))];
945 let mut rec = Record::new("samples");
946 rec.set("name", "Chair");
947
948 save(
949 SaveRequest {
950 db: &db,
951 cache: None,
952 listeners: &listeners(®s, "samples"),
953 persister: &spy,
954 actor: &actor(),
955 op: Op::Create,
956 id: None,
957 },
958 &mut rec,
959 )
960 .await
961 .unwrap();
962
963 let default = DefaultPersister {
964 entity: "samples".into(),
965 id_field: "id".into(),
966 columns: vec!["name".into()],
967 };
968 assert_eq!(default.write_columns(&rec), ["name", "created_at"]);
970 }
971
972 #[test]
973 fn the_id_column_is_never_written() {
974 let default = DefaultPersister {
975 entity: "samples".into(),
976 id_field: "id".into(),
977 columns: vec!["name".into()],
978 };
979 let mut rec = Record::with_id("samples", 3);
980 rec.set("name", "Chair").set("id", 3i64);
981 assert_eq!(default.write_columns(&rec), ["name"]);
982 }
983
984 #[test]
985 fn binding_maps_each_kind_to_its_storage_form() {
986 let null = SimpleExpr::Keyword(sea_query::Keyword::Null);
989 assert_eq!(bind(None), null);
990 assert_eq!(bind(Some(&AttrValue::Null)), null);
991 assert_eq!(bind(Some(&AttrValue::Text(String::new()))), null);
992 assert_eq!(bind(Some(&AttrValue::Int(7))), 7i64.into());
993 assert_eq!(bind(Some(&AttrValue::Bool(true))), 1i32.into());
995 assert_eq!(
996 bind(Some(&AttrValue::Text("Chair".into()))),
997 Some("Chair".to_string()).into()
998 );
999 }
1000
1001 struct SnapshotPersister;
1003
1004 #[async_trait]
1005 impl Persister for SnapshotPersister {
1006 async fn create(&self, _cx: &mut SaveCx<'_>, _rec: &Record) -> Result<i64, SaveError> {
1007 Ok(1)
1008 }
1009 async fn update(
1010 &self,
1011 _cx: &mut SaveCx<'_>,
1012 _id: &str,
1013 _rec: &Record,
1014 ) -> Result<(), SaveError> {
1015 Ok(())
1016 }
1017 async fn load(&self, _cx: &mut SaveCx<'_>, _id: &str) -> Result<Option<Record>, SaveError> {
1018 let mut previous = Record::new("samples");
1019 previous.set("name", "Chair");
1020 Ok(Some(previous))
1021 }
1022 }
1023
1024 struct WatchChanges {
1026 seen: Arc<Mutex<Vec<(bool, bool)>>>,
1027 }
1028
1029 #[async_trait]
1030 impl ModelListener for WatchChanges {
1031 async fn before_save(
1032 &self,
1033 _cx: &mut SaveCx<'_>,
1034 rec: &mut Record,
1035 _op: Op,
1036 ) -> Result<(), ErrorBag> {
1037 self.seen
1038 .lock()
1039 .unwrap()
1040 .push((rec.has_original(), rec.changed("name")));
1041 Ok(())
1042 }
1043 }
1044
1045 #[tokio::test]
1046 async fn an_update_hands_listeners_the_pre_write_row() {
1047 let (db, _guard) = connect_test(&[]).await;
1048 let seen = Arc::new(Mutex::new(Vec::new()));
1049 let regs = vec![ModelListenerReg::all(Arc::new(WatchChanges {
1050 seen: seen.clone(),
1051 }))];
1052 let mut rec = Record::with_id("samples", 1);
1053 rec.set("name", "Stool");
1054
1055 save(
1056 SaveRequest {
1057 db: &db,
1058 cache: None,
1059 listeners: &listeners(®s, "samples"),
1060 persister: &SnapshotPersister,
1061 actor: &actor(),
1062 op: Op::Update,
1063 id: Some("1"),
1064 },
1065 &mut rec,
1066 )
1067 .await
1068 .unwrap();
1069
1070 assert_eq!(*seen.lock().unwrap(), [(true, true)]);
1071 }
1072
1073 #[tokio::test]
1074 async fn a_create_has_no_snapshot() {
1075 let (db, _guard) = connect_test(&[]).await;
1076 let seen = Arc::new(Mutex::new(Vec::new()));
1077 let regs = vec![ModelListenerReg::all(Arc::new(WatchChanges {
1078 seen: seen.clone(),
1079 }))];
1080 let mut rec = Record::new("samples");
1081 rec.set("name", "Chair");
1082
1083 save(
1084 SaveRequest {
1085 db: &db,
1086 cache: None,
1087 listeners: &listeners(®s, "samples"),
1088 persister: &SnapshotPersister,
1089 actor: &actor(),
1090 op: Op::Create,
1091 id: None,
1092 },
1093 &mut rec,
1094 )
1095 .await
1096 .unwrap();
1097
1098 assert_eq!(*seen.lock().unwrap(), [(false, false)]);
1099 }
1100
1101 #[tokio::test]
1102 async fn an_update_passes_its_id_through() {
1103 let (db, _guard) = connect_test(&[]).await;
1104 let spy = SpyPersister::default();
1105 let mut rec = Record::with_id("samples", 7);
1106
1107 save(
1108 SaveRequest {
1109 db: &db,
1110 cache: None,
1111 listeners: &[],
1112 persister: &spy,
1113 actor: &actor(),
1114 op: Op::Update,
1115 id: Some("7"),
1116 },
1117 &mut rec,
1118 )
1119 .await
1120 .unwrap();
1121
1122 assert_eq!(spy.seen.lock().unwrap()[0].0, "update:7");
1123 }
1124
1125 #[tokio::test]
1126 async fn an_update_without_an_id_fails_rather_than_writing() {
1127 let (db, _guard) = connect_test(&[]).await;
1128 let spy = SpyPersister::default();
1129 let mut rec = Record::new("samples");
1130
1131 let err = save(
1132 SaveRequest {
1133 db: &db,
1134 cache: None,
1135 listeners: &[],
1136 persister: &spy,
1137 actor: &actor(),
1138 op: Op::Update,
1139 id: None,
1140 },
1141 &mut rec,
1142 )
1143 .await
1144 .expect_err("an update needs an id");
1145
1146 assert!(matches!(err, SaveError::Failed(_)));
1147 assert!(spy.seen.lock().unwrap().is_empty());
1148 }
1149}