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