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;
21
22#[derive(Debug)]
24pub enum SaveError {
25 Invalid(ErrorBag),
28 Failed(String),
31}
32
33#[derive(Debug)]
35pub enum DeleteError {
36 Refused(Text),
40 Failed(String),
43}
44
45#[async_trait]
50pub trait Persister: Send + Sync + 'static {
51 async fn create(&self, cx: &mut SaveCx<'_>, rec: &Record) -> Result<i64, SaveError>;
53 async fn update(&self, cx: &mut SaveCx<'_>, id: &str, rec: &Record) -> Result<(), SaveError>;
55
56 async fn load(&self, cx: &mut SaveCx<'_>, id: &str) -> Result<Option<Record>, SaveError> {
60 let _ = (cx, id);
61 Ok(None)
62 }
63
64 async fn delete(&self, cx: &mut SaveCx<'_>, id: &str, rec: &Record) -> Result<(), DeleteError> {
70 let _ = (cx, id, rec);
71 Err(DeleteError::Refused(t!(
72 "Deleting this kind of record is not supported."
73 )))
74 }
75}
76
77fn bind(value: Option<&AttrValue>) -> SimpleExpr {
84 match value {
85 None | Some(AttrValue::Null) => Option::<String>::None.into(),
86 Some(AttrValue::Int(i)) => (*i).into(),
87 Some(AttrValue::Float(f)) => (*f).into(),
88 Some(AttrValue::Bool(b)) => i32::from(*b).into(),
89 Some(AttrValue::Text(s)) if s.is_empty() => Option::<String>::None.into(),
90 Some(other) => other.to_text().into(),
91 }
92}
93
94pub(crate) struct SaveRequest<'a> {
100 pub db: &'a Db,
101 pub listeners: &'a [Arc<dyn ModelListener>],
102 pub persister: &'a dyn Persister,
103 pub actor: &'a Actor,
105 pub op: Op,
106 pub id: Option<&'a str>,
108}
109
110pub(crate) async fn save(req: SaveRequest<'_>, rec: &mut Record) -> Result<(), SaveError> {
111 let SaveRequest {
112 db,
113 listeners,
114 persister,
115 actor,
116 op,
117 id,
118 } = req;
119 let mut tx = db
120 .pool
121 .begin()
122 .await
123 .map_err(|e| SaveError::Failed(e.to_string()))?;
124
125 let outcome = save_in(
126 &mut tx,
127 db.backend,
128 listeners,
129 persister,
130 actor.clone(),
131 rec,
132 op,
133 id,
134 )
135 .await;
136 match outcome {
137 Ok(()) => tx
138 .commit()
139 .await
140 .map_err(|e| SaveError::Failed(e.to_string()))?,
141 Err(e) => {
142 let _ = tx.rollback().await;
145 return Err(e);
146 }
147 }
148
149 let saved = SavedCx::new(db, actor);
151 for listener in listeners {
152 listener.after_save(&saved, rec, op).await;
153 }
154 Ok(())
155}
156
157pub(crate) struct DeleteRequest<'a> {
159 pub db: &'a Db,
160 pub listeners: &'a [Arc<dyn ModelListener>],
161 pub persister: &'a dyn Persister,
162 pub actor: &'a Actor,
163 pub entity: &'a str,
164 pub id: &'a str,
165}
166
167pub(crate) async fn delete(req: DeleteRequest<'_>) -> Result<Record, DeleteError> {
173 let DeleteRequest {
174 db,
175 listeners,
176 persister,
177 actor,
178 entity,
179 id,
180 } = req;
181
182 let mut tx = db
183 .pool
184 .begin()
185 .await
186 .map_err(|e| DeleteError::Failed(e.to_string()))?;
187
188 let outcome = delete_in(
189 &mut tx,
190 db.backend,
191 listeners,
192 persister,
193 actor.clone(),
194 entity,
195 id,
196 )
197 .await;
198 let rec = match outcome {
199 Ok(rec) => {
200 tx.commit()
201 .await
202 .map_err(|e| DeleteError::Failed(e.to_string()))?;
203 rec
204 }
205 Err(e) => {
206 let _ = tx.rollback().await;
209 return Err(e);
210 }
211 };
212
213 let saved = SavedCx::new(db, actor);
215 for listener in listeners {
216 listener.after_delete(&saved, &rec).await;
217 }
218 Ok(rec)
219}
220
221async fn delete_in(
224 tx: &mut sqlx::Transaction<'_, sqlx::Any>,
225 backend: laterite_core::migration::DbBackend,
226 listeners: &[Arc<dyn ModelListener>],
227 persister: &dyn Persister,
228 actor: Actor,
229 entity: &str,
230 id: &str,
231) -> Result<Record, DeleteError> {
232 let mut cx = SaveCx::new(backend, tx, actor);
233
234 let loaded = persister.load(&mut cx, id).await.map_err(|e| match e {
238 SaveError::Invalid(_) => DeleteError::Failed("snapshot read refused".to_string()),
239 SaveError::Failed(m) => DeleteError::Failed(m),
240 })?;
241 let mut rec = loaded.unwrap_or_else(|| Record::new(entity));
242 if rec.id().is_none() {
243 if let Ok(numeric) = id.parse::<i64>() {
244 rec.set_id(numeric);
245 }
246 }
247
248 for listener in listeners {
249 listener
250 .before_delete(&mut cx, &rec)
251 .await
252 .map_err(DeleteError::Refused)?;
253 }
254
255 persister.delete(&mut cx, id, &rec).await?;
256 Ok(rec)
257}
258
259#[allow(clippy::too_many_arguments)]
262async fn save_in(
263 tx: &mut sqlx::Transaction<'_, sqlx::Any>,
264 backend: laterite_core::migration::DbBackend,
265 listeners: &[Arc<dyn ModelListener>],
266 persister: &dyn Persister,
267 actor: Actor,
268 rec: &mut Record,
269 op: Op,
270 id: Option<&str>,
271) -> Result<(), SaveError> {
272 let mut cx = SaveCx::new(backend, tx, actor);
273
274 if op == Op::Update {
275 if let Some(id) = id {
276 if let Some(previous) = persister.load(&mut cx, id).await? {
277 rec.set_original(previous);
278 }
279 }
280 }
281
282 for listener in listeners {
283 listener
284 .before_save(&mut cx, rec, op)
285 .await
286 .map_err(SaveError::Invalid)?;
287 }
288
289 match op {
290 Op::Create => {
291 let new_id = persister.create(&mut cx, rec).await?;
292 rec.set_id(new_id);
293 }
294 Op::Update => {
295 let id = id.ok_or_else(|| SaveError::Failed("update without an id".to_string()))?;
296 persister.update(&mut cx, id, rec).await?;
297 }
298 _ => return Err(SaveError::Failed(format!("unsupported operation {op:?}"))),
300 }
301 Ok(())
302}
303
304pub struct PersisterReg {
306 pub name: String,
307 pub persister: Arc<dyn Persister>,
308}
309
310impl PersisterReg {
311 pub fn new(name: impl Into<String>, persister: Arc<dyn Persister>) -> Self {
312 Self {
313 name: name.into(),
314 persister,
315 }
316 }
317}
318
319pub type PersisterRegistry = HashMap<String, Arc<dyn Persister>>;
321
322pub(crate) struct DefaultPersister {
326 entity: String,
327 id_field: String,
328 columns: Vec<String>,
329}
330
331impl DefaultPersister {
332 fn write_columns(&self, rec: &Record) -> Vec<String> {
338 let mut cols = self.columns.clone();
339 for (key, _) in rec.iter() {
340 if key != self.id_field && !cols.iter().any(|c| c == key) {
341 cols.push(key.to_string());
342 }
343 }
344 cols.retain(|c| c != &self.id_field && crate::sql::valid_ident(c));
345 cols
346 }
347
348 pub(crate) fn from_list(config: &crate::list::ListConfig) -> Self {
352 Self {
353 entity: config.entity.clone(),
354 id_field: config.id_field.clone(),
355 columns: config.columns.iter().map(|c| c.field.clone()).collect(),
356 }
357 }
358
359 pub(crate) fn from_config(config: &FormConfig) -> Self {
360 Self {
361 entity: config.entity.clone(),
362 id_field: config.id_field.clone(),
363 columns: config.fields.iter().map(|f| f.name.clone()).collect(),
364 }
365 }
366}
367
368#[async_trait]
369impl Persister for DefaultPersister {
370 async fn create(&self, cx: &mut SaveCx<'_>, rec: &Record) -> Result<i64, SaveError> {
371 let stmt = {
374 let cols = self.write_columns(rec);
375 let vals: Vec<SimpleExpr> = cols.iter().map(|c| bind(rec.get(c))).collect();
376 Query::insert()
377 .into_table(Alias::new(&self.entity))
378 .columns(cols.iter().map(Alias::new))
379 .values_panic(vals)
380 .to_owned()
381 };
382 let backend = cx.backend();
383 insert_returning_id_on(backend, cx.conn(), stmt, Alias::new(&self.id_field))
384 .await
385 .map_err(|e| SaveError::Failed(e.to_string()))
386 }
387
388 async fn update(&self, cx: &mut SaveCx<'_>, id: &str, rec: &Record) -> Result<(), SaveError> {
389 let backend = cx.backend();
390 let (sql, values) = {
391 let mut update = Query::update();
392 update.table(Alias::new(&self.entity));
393 for column in self.write_columns(rec) {
394 update.value(Alias::new(&column), bind(rec.get(&column)));
395 }
396 update.and_where(
397 Expr::col(Alias::new(&self.id_field))
398 .cast_as(Alias::new(text_cast(backend)))
399 .eq(id),
400 );
401 to_sql(backend, update)
402 };
403 bind_values(sqlx::query(&sql), values)
404 .execute(cx.conn())
405 .await
406 .map(|_| ())
407 .map_err(|e| SaveError::Failed(e.to_string()))
408 }
409
410 async fn delete(
413 &self,
414 cx: &mut SaveCx<'_>,
415 id: &str,
416 _rec: &Record,
417 ) -> Result<(), DeleteError> {
418 let backend = cx.backend();
419 let (sql, values) = {
420 let mut del = Query::delete();
421 del.from_table(Alias::new(&self.entity));
422 del.and_where(
423 Expr::col(Alias::new(&self.id_field))
424 .cast_as(Alias::new(text_cast(backend)))
425 .eq(id),
426 );
427 to_sql(backend, del)
428 };
429 bind_values(sqlx::query(&sql), values)
430 .execute(cx.conn())
431 .await
432 .map(|_| ())
433 .map_err(|e| DeleteError::Failed(e.to_string()))
434 }
435
436 async fn load(&self, cx: &mut SaveCx<'_>, id: &str) -> Result<Option<Record>, SaveError> {
440 let backend = cx.backend();
441 let (sql, values) = {
442 let mut select = Query::select();
443 select.from(Alias::new(&self.entity));
444 for column in &self.columns {
445 select.expr_as(
446 Expr::col(Alias::new(column)).cast_as(Alias::new(text_cast(backend))),
447 Alias::new(column),
448 );
449 }
450 select.and_where(
451 Expr::col(Alias::new(&self.id_field))
452 .cast_as(Alias::new(text_cast(backend)))
453 .eq(id),
454 );
455 to_sql(backend, select)
456 };
457 let row = bind_values(sqlx::query(&sql), values)
458 .fetch_optional(cx.conn())
459 .await
460 .map_err(|e| SaveError::Failed(e.to_string()))?;
461 let Some(row) = row else {
462 return Ok(None);
463 };
464 let mut previous = Record::new(&self.entity);
465 for column in &self.columns {
466 match row.get_text_opt(column) {
467 Ok(Some(v)) => {
468 previous.set(column.clone(), v);
469 }
470 Ok(None) => {
471 previous.set(column.clone(), AttrValue::Null);
472 }
473 Err(e) => return Err(SaveError::Failed(e.to_string())),
474 }
475 }
476 Ok(Some(previous))
477 }
478}
479
480#[cfg(test)]
481mod pipeline_tests {
482 use super::*;
483 use laterite_core::testing::connect_test;
484 use laterite_core::{ListenerTarget, ModelListenerReg, Text};
485 use std::sync::Mutex;
486
487 #[derive(Default)]
489 struct SpyPersister {
490 seen: Mutex<Vec<(String, HashMap<String, String>)>>,
491 }
492
493 #[async_trait]
494 impl Persister for SpyPersister {
495 async fn create(&self, _cx: &mut SaveCx<'_>, rec: &Record) -> Result<i64, SaveError> {
496 self.seen
497 .lock()
498 .unwrap()
499 .push(("create".into(), rec.to_text_map()));
500 Ok(42)
501 }
502 async fn update(
503 &self,
504 _cx: &mut SaveCx<'_>,
505 id: &str,
506 rec: &Record,
507 ) -> Result<(), SaveError> {
508 self.seen
509 .lock()
510 .unwrap()
511 .push((format!("update:{id}"), rec.to_text_map()));
512 Ok(())
513 }
514 }
515
516 #[derive(Default)]
518 struct DeleteSpy {
519 deleted: Mutex<Vec<String>>,
520 refuse: Option<&'static str>,
521 }
522
523 #[async_trait]
524 impl Persister for DeleteSpy {
525 async fn create(&self, _cx: &mut SaveCx<'_>, _rec: &Record) -> Result<i64, SaveError> {
526 Ok(1)
527 }
528 async fn update(
529 &self,
530 _cx: &mut SaveCx<'_>,
531 _id: &str,
532 _rec: &Record,
533 ) -> Result<(), SaveError> {
534 Ok(())
535 }
536 async fn load(&self, _cx: &mut SaveCx<'_>, id: &str) -> Result<Option<Record>, SaveError> {
537 let mut rec = Record::new("samples");
538 rec.set("code", format!("code-{id}"));
539 if let Ok(n) = id.parse::<i64>() {
540 rec.set_id(n);
541 }
542 Ok(Some(rec))
543 }
544 async fn delete(
545 &self,
546 _cx: &mut SaveCx<'_>,
547 id: &str,
548 _rec: &Record,
549 ) -> Result<(), DeleteError> {
550 if let Some(why) = self.refuse {
551 return Err(DeleteError::Refused(Text::new(why)));
552 }
553 self.deleted.lock().unwrap().push(id.to_string());
554 Ok(())
555 }
556 }
557
558 struct Guard;
560
561 #[async_trait]
562 impl ModelListener for Guard {
563 async fn before_delete(&self, _cx: &mut SaveCx<'_>, _rec: &Record) -> Result<(), Text> {
564 Err(Text::new("still referenced"))
565 }
566 }
567
568 #[derive(Default)]
570 struct Mourner {
571 seen: Arc<Mutex<Vec<String>>>,
572 }
573
574 #[async_trait]
575 impl ModelListener for Mourner {
576 async fn after_delete(&self, _cx: &SavedCx<'_>, rec: &Record) {
577 self.seen
578 .lock()
579 .unwrap()
580 .push(rec.text("code").unwrap_or_default().to_string());
581 }
582 }
583
584 #[tokio::test]
585 async fn a_delete_runs_the_listeners_around_the_persister() {
586 let (db, _guard) = connect_test(&[]).await;
587 let persister = DeleteSpy::default();
588 let seen = Arc::new(Mutex::new(Vec::new()));
589 let listeners: Vec<Arc<dyn ModelListener>> = vec![Arc::new(Mourner { seen: seen.clone() })];
590
591 let rec = delete(DeleteRequest {
592 db: &db,
593 listeners: &listeners,
594 persister: &persister,
595 actor: &actor(),
596 entity: "samples",
597 id: "7",
598 })
599 .await
600 .expect("deleted");
601
602 assert_eq!(persister.deleted.lock().unwrap().as_slice(), ["7"]);
603 assert_eq!(seen.lock().unwrap().as_slice(), ["code-7"]);
606 assert_eq!(rec.text("code"), Some("code-7"));
607 assert_eq!(rec.id(), Some(7));
608 }
609
610 #[tokio::test]
613 async fn a_listener_can_refuse_a_delete() {
614 let (db, _guard) = connect_test(&[]).await;
615 let persister = DeleteSpy::default();
616 let seen = Arc::new(Mutex::new(Vec::new()));
617 let listeners: Vec<Arc<dyn ModelListener>> =
618 vec![Arc::new(Guard), Arc::new(Mourner { seen: seen.clone() })];
619
620 let err = delete(DeleteRequest {
621 db: &db,
622 listeners: &listeners,
623 persister: &persister,
624 actor: &actor(),
625 entity: "samples",
626 id: "7",
627 })
628 .await
629 .expect_err("refused");
630
631 match err {
632 DeleteError::Refused(message) => assert_eq!(message.source(), "still referenced"),
633 other => panic!("expected a refusal, got {other:?}"),
634 }
635 assert!(
636 persister.deleted.lock().unwrap().is_empty(),
637 "nothing removed"
638 );
639 assert!(
640 seen.lock().unwrap().is_empty(),
641 "no after-stage on a refusal"
642 );
643 }
644
645 #[tokio::test]
648 async fn a_persister_without_delete_refuses() {
649 let (db, _guard) = connect_test(&[]).await;
650 let persister = SpyPersister::default();
651 let err = delete(DeleteRequest {
652 db: &db,
653 listeners: &[],
654 persister: &persister,
655 actor: &actor(),
656 entity: "samples",
657 id: "1",
658 })
659 .await
660 .expect_err("unsupported");
661 assert!(matches!(err, DeleteError::Refused(_)));
662 }
663
664 struct Stamp {
667 key: &'static str,
668 log: Arc<Mutex<Vec<&'static str>>>,
669 }
670
671 #[async_trait]
672 impl ModelListener for Stamp {
673 async fn before_save(
674 &self,
675 _cx: &mut SaveCx<'_>,
676 rec: &mut Record,
677 _op: Op,
678 ) -> Result<(), ErrorBag> {
679 rec.set(self.key, "set");
680 Ok(())
681 }
682 async fn after_save(&self, _cx: &SavedCx<'_>, _rec: &Record, _op: Op) {
683 self.log.lock().unwrap().push(self.key);
684 }
685 }
686
687 struct Veto;
689
690 #[async_trait]
691 impl ModelListener for Veto {
692 async fn before_save(
693 &self,
694 _cx: &mut SaveCx<'_>,
695 _rec: &mut Record,
696 _op: Op,
697 ) -> Result<(), ErrorBag> {
698 let mut bag = ErrorBag::default();
699 bag.add("name", Text::new("Not allowed"));
701 Err(bag)
702 }
703 }
704
705 fn actor() -> Actor {
706 Actor::system("test")
707 }
708
709 fn listeners(regs: &[ModelListenerReg], entity: &str) -> Vec<Arc<dyn ModelListener>> {
710 regs.iter()
711 .filter(|r| r.matches(entity))
712 .map(|r| r.listener.clone())
713 .collect()
714 }
715
716 #[tokio::test]
717 async fn a_listener_mutation_reaches_the_persister_and_the_id_comes_back() {
718 let (db, _guard) = connect_test(&[]).await;
719 let spy = SpyPersister::default();
720 let log = Arc::new(Mutex::new(Vec::new()));
721 let regs = vec![ModelListenerReg::all(Arc::new(Stamp {
722 key: "created_at",
723 log: log.clone(),
724 }))];
725 let mut rec = Record::new("samples");
726 rec.set("name", "Chair");
727
728 save(
729 SaveRequest {
730 db: &db,
731 listeners: &listeners(®s, "samples"),
732 persister: &spy,
733 actor: &actor(),
734 op: Op::Create,
735 id: None,
736 },
737 &mut rec,
738 )
739 .await
740 .unwrap();
741
742 let seen = spy.seen.lock().unwrap();
743 assert_eq!(seen[0].0, "create");
744 assert_eq!(seen[0].1.get("created_at").map(String::as_str), Some("set"));
745 assert_eq!(rec.id(), Some(42), "the create id lands on the record");
746 assert_eq!(*log.lock().unwrap(), ["created_at"]);
747 }
748
749 #[tokio::test]
750 async fn a_veto_stops_the_write_and_returns_field_messages() {
751 let (db, _guard) = connect_test(&[]).await;
752 let spy = SpyPersister::default();
753 let regs = vec![ModelListenerReg::all(Arc::new(Veto))];
754 let mut rec = Record::new("samples");
755
756 let err = save(
757 SaveRequest {
758 db: &db,
759 listeners: &listeners(®s, "samples"),
760 persister: &spy,
761 actor: &actor(),
762 op: Op::Create,
763 id: None,
764 },
765 &mut rec,
766 )
767 .await
768 .expect_err("the veto refuses the write");
769
770 match err {
771 SaveError::Invalid(bag) => assert_eq!(bag.messages("name").len(), 1),
772 SaveError::Failed(e) => panic!("expected a field error, got {e}"),
773 }
774 assert!(
775 spy.seen.lock().unwrap().is_empty(),
776 "nothing reached the persister"
777 );
778 }
779
780 #[tokio::test]
781 async fn listeners_run_in_registration_order() {
782 let (db, _guard) = connect_test(&[]).await;
783 let log = Arc::new(Mutex::new(Vec::new()));
784 let regs = vec![
785 ModelListenerReg::all(Arc::new(Stamp {
786 key: "first",
787 log: log.clone(),
788 })),
789 ModelListenerReg::all(Arc::new(Stamp {
790 key: "second",
791 log: log.clone(),
792 })),
793 ];
794 let mut rec = Record::new("samples");
795
796 save(
797 SaveRequest {
798 db: &db,
799 listeners: &listeners(®s, "samples"),
800 persister: &SpyPersister::default(),
801 actor: &actor(),
802 op: Op::Create,
803 id: None,
804 },
805 &mut rec,
806 )
807 .await
808 .unwrap();
809
810 assert_eq!(*log.lock().unwrap(), ["first", "second"]);
811 }
812
813 #[tokio::test]
814 async fn a_target_selects_which_listeners_run() {
815 let (db, _guard) = connect_test(&[]).await;
816 let log = Arc::new(Mutex::new(Vec::new()));
817 let regs = vec![
818 ModelListenerReg::all(Arc::new(Stamp {
819 key: "everywhere",
820 log: log.clone(),
821 })),
822 ModelListenerReg::for_entity(
823 "orders",
824 Arc::new(Stamp {
825 key: "orders_only",
826 log: log.clone(),
827 }),
828 ),
829 ];
830 let mut rec = Record::new("samples");
831
832 save(
833 SaveRequest {
834 db: &db,
835 listeners: &listeners(®s, "samples"),
836 persister: &SpyPersister::default(),
837 actor: &actor(),
838 op: Op::Create,
839 id: None,
840 },
841 &mut rec,
842 )
843 .await
844 .unwrap();
845
846 assert_eq!(
847 *log.lock().unwrap(),
848 ["everywhere"],
849 "another entity's listener stays out"
850 );
851 assert!(matches!(regs[1].target, ListenerTarget::Entity(ref e) if e == "orders"));
852 }
853
854 #[tokio::test]
855 async fn a_listener_injected_attribute_is_written_not_dropped() {
856 let (db, _guard) = connect_test(&[]).await;
857 let spy = SpyPersister::default();
858 let log = Arc::new(Mutex::new(Vec::new()));
859 let regs = vec![ModelListenerReg::all(Arc::new(Stamp {
860 key: "created_at",
861 log,
862 }))];
863 let mut rec = Record::new("samples");
864 rec.set("name", "Chair");
865
866 save(
867 SaveRequest {
868 db: &db,
869 listeners: &listeners(®s, "samples"),
870 persister: &spy,
871 actor: &actor(),
872 op: Op::Create,
873 id: None,
874 },
875 &mut rec,
876 )
877 .await
878 .unwrap();
879
880 let default = DefaultPersister {
881 entity: "samples".into(),
882 id_field: "id".into(),
883 columns: vec!["name".into()],
884 };
885 assert_eq!(default.write_columns(&rec), ["name", "created_at"]);
887 }
888
889 #[test]
890 fn the_id_column_is_never_written() {
891 let default = DefaultPersister {
892 entity: "samples".into(),
893 id_field: "id".into(),
894 columns: vec!["name".into()],
895 };
896 let mut rec = Record::with_id("samples", 3);
897 rec.set("name", "Chair").set("id", 3i64);
898 assert_eq!(default.write_columns(&rec), ["name"]);
899 }
900
901 #[test]
902 fn binding_maps_each_kind_to_its_storage_form() {
903 let null: SimpleExpr = Option::<String>::None.into();
906 assert_eq!(bind(None), null);
907 assert_eq!(bind(Some(&AttrValue::Null)), null);
908 assert_eq!(bind(Some(&AttrValue::Text(String::new()))), null);
909 assert_eq!(bind(Some(&AttrValue::Int(7))), 7i64.into());
910 assert_eq!(bind(Some(&AttrValue::Bool(true))), 1i32.into());
912 assert_eq!(
913 bind(Some(&AttrValue::Text("Chair".into()))),
914 Some("Chair".to_string()).into()
915 );
916 }
917
918 struct SnapshotPersister;
920
921 #[async_trait]
922 impl Persister for SnapshotPersister {
923 async fn create(&self, _cx: &mut SaveCx<'_>, _rec: &Record) -> Result<i64, SaveError> {
924 Ok(1)
925 }
926 async fn update(
927 &self,
928 _cx: &mut SaveCx<'_>,
929 _id: &str,
930 _rec: &Record,
931 ) -> Result<(), SaveError> {
932 Ok(())
933 }
934 async fn load(&self, _cx: &mut SaveCx<'_>, _id: &str) -> Result<Option<Record>, SaveError> {
935 let mut previous = Record::new("samples");
936 previous.set("name", "Chair");
937 Ok(Some(previous))
938 }
939 }
940
941 struct WatchChanges {
943 seen: Arc<Mutex<Vec<(bool, bool)>>>,
944 }
945
946 #[async_trait]
947 impl ModelListener for WatchChanges {
948 async fn before_save(
949 &self,
950 _cx: &mut SaveCx<'_>,
951 rec: &mut Record,
952 _op: Op,
953 ) -> Result<(), ErrorBag> {
954 self.seen
955 .lock()
956 .unwrap()
957 .push((rec.has_original(), rec.changed("name")));
958 Ok(())
959 }
960 }
961
962 #[tokio::test]
963 async fn an_update_hands_listeners_the_pre_write_row() {
964 let (db, _guard) = connect_test(&[]).await;
965 let seen = Arc::new(Mutex::new(Vec::new()));
966 let regs = vec![ModelListenerReg::all(Arc::new(WatchChanges {
967 seen: seen.clone(),
968 }))];
969 let mut rec = Record::with_id("samples", 1);
970 rec.set("name", "Stool");
971
972 save(
973 SaveRequest {
974 db: &db,
975 listeners: &listeners(®s, "samples"),
976 persister: &SnapshotPersister,
977 actor: &actor(),
978 op: Op::Update,
979 id: Some("1"),
980 },
981 &mut rec,
982 )
983 .await
984 .unwrap();
985
986 assert_eq!(*seen.lock().unwrap(), [(true, true)]);
987 }
988
989 #[tokio::test]
990 async fn a_create_has_no_snapshot() {
991 let (db, _guard) = connect_test(&[]).await;
992 let seen = Arc::new(Mutex::new(Vec::new()));
993 let regs = vec![ModelListenerReg::all(Arc::new(WatchChanges {
994 seen: seen.clone(),
995 }))];
996 let mut rec = Record::new("samples");
997 rec.set("name", "Chair");
998
999 save(
1000 SaveRequest {
1001 db: &db,
1002 listeners: &listeners(®s, "samples"),
1003 persister: &SnapshotPersister,
1004 actor: &actor(),
1005 op: Op::Create,
1006 id: None,
1007 },
1008 &mut rec,
1009 )
1010 .await
1011 .unwrap();
1012
1013 assert_eq!(*seen.lock().unwrap(), [(false, false)]);
1014 }
1015
1016 #[tokio::test]
1017 async fn an_update_passes_its_id_through() {
1018 let (db, _guard) = connect_test(&[]).await;
1019 let spy = SpyPersister::default();
1020 let mut rec = Record::with_id("samples", 7);
1021
1022 save(
1023 SaveRequest {
1024 db: &db,
1025 listeners: &[],
1026 persister: &spy,
1027 actor: &actor(),
1028 op: Op::Update,
1029 id: Some("7"),
1030 },
1031 &mut rec,
1032 )
1033 .await
1034 .unwrap();
1035
1036 assert_eq!(spy.seen.lock().unwrap()[0].0, "update:7");
1037 }
1038
1039 #[tokio::test]
1040 async fn an_update_without_an_id_fails_rather_than_writing() {
1041 let (db, _guard) = connect_test(&[]).await;
1042 let spy = SpyPersister::default();
1043 let mut rec = Record::new("samples");
1044
1045 let err = save(
1046 SaveRequest {
1047 db: &db,
1048 listeners: &[],
1049 persister: &spy,
1050 actor: &actor(),
1051 op: Op::Update,
1052 id: None,
1053 },
1054 &mut rec,
1055 )
1056 .await
1057 .expect_err("an update needs an id");
1058
1059 assert!(matches!(err, SaveError::Failed(_)));
1060 assert!(spy.seen.lock().unwrap().is_empty());
1061 }
1062}