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::{Actor, AttrValue, Db, ModelListener, Op, Record, SaveCx, SavedCx};
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#[async_trait]
38pub trait Persister: Send + Sync + 'static {
39 async fn create(&self, cx: &mut SaveCx<'_>, rec: &Record) -> Result<i64, SaveError>;
41 async fn update(&self, cx: &mut SaveCx<'_>, id: &str, rec: &Record) -> Result<(), SaveError>;
43
44 async fn load(&self, cx: &mut SaveCx<'_>, id: &str) -> Result<Option<Record>, SaveError> {
48 let _ = (cx, id);
49 Ok(None)
50 }
51}
52
53fn bind(value: Option<&AttrValue>) -> SimpleExpr {
60 match value {
61 None | Some(AttrValue::Null) => Option::<String>::None.into(),
62 Some(AttrValue::Int(i)) => (*i).into(),
63 Some(AttrValue::Float(f)) => (*f).into(),
64 Some(AttrValue::Bool(b)) => i32::from(*b).into(),
65 Some(AttrValue::Text(s)) if s.is_empty() => Option::<String>::None.into(),
66 Some(other) => other.to_text().into(),
67 }
68}
69
70pub(crate) struct SaveRequest<'a> {
76 pub db: &'a Db,
77 pub listeners: &'a [Arc<dyn ModelListener>],
78 pub persister: &'a dyn Persister,
79 pub actor: &'a Actor,
81 pub op: Op,
82 pub id: Option<&'a str>,
84}
85
86pub(crate) async fn save(req: SaveRequest<'_>, rec: &mut Record) -> Result<(), SaveError> {
87 let SaveRequest {
88 db,
89 listeners,
90 persister,
91 actor,
92 op,
93 id,
94 } = req;
95 let mut tx = db
96 .pool
97 .begin()
98 .await
99 .map_err(|e| SaveError::Failed(e.to_string()))?;
100
101 let outcome = save_in(
102 &mut tx,
103 db.backend,
104 listeners,
105 persister,
106 actor.clone(),
107 rec,
108 op,
109 id,
110 )
111 .await;
112 match outcome {
113 Ok(()) => tx
114 .commit()
115 .await
116 .map_err(|e| SaveError::Failed(e.to_string()))?,
117 Err(e) => {
118 let _ = tx.rollback().await;
121 return Err(e);
122 }
123 }
124
125 let saved = SavedCx::new(db, actor);
127 for listener in listeners {
128 listener.after_save(&saved, rec, op).await;
129 }
130 Ok(())
131}
132
133#[allow(clippy::too_many_arguments)]
136async fn save_in(
137 tx: &mut sqlx::Transaction<'_, sqlx::Any>,
138 backend: laterite_core::migration::DbBackend,
139 listeners: &[Arc<dyn ModelListener>],
140 persister: &dyn Persister,
141 actor: Actor,
142 rec: &mut Record,
143 op: Op,
144 id: Option<&str>,
145) -> Result<(), SaveError> {
146 let mut cx = SaveCx::new(backend, tx, actor);
147
148 if op == Op::Update {
149 if let Some(id) = id {
150 if let Some(previous) = persister.load(&mut cx, id).await? {
151 rec.set_original(previous);
152 }
153 }
154 }
155
156 for listener in listeners {
157 listener
158 .before_save(&mut cx, rec, op)
159 .await
160 .map_err(SaveError::Invalid)?;
161 }
162
163 match op {
164 Op::Create => {
165 let new_id = persister.create(&mut cx, rec).await?;
166 rec.set_id(new_id);
167 }
168 Op::Update => {
169 let id = id.ok_or_else(|| SaveError::Failed("update without an id".to_string()))?;
170 persister.update(&mut cx, id, rec).await?;
171 }
172 _ => return Err(SaveError::Failed(format!("unsupported operation {op:?}"))),
174 }
175 Ok(())
176}
177
178pub struct PersisterReg {
180 pub name: String,
181 pub persister: Arc<dyn Persister>,
182}
183
184impl PersisterReg {
185 pub fn new(name: impl Into<String>, persister: Arc<dyn Persister>) -> Self {
186 Self {
187 name: name.into(),
188 persister,
189 }
190 }
191}
192
193pub type PersisterRegistry = HashMap<String, Arc<dyn Persister>>;
195
196pub(crate) struct DefaultPersister {
200 entity: String,
201 id_field: String,
202 columns: Vec<String>,
203}
204
205impl DefaultPersister {
206 fn write_columns(&self, rec: &Record) -> Vec<String> {
212 let mut cols = self.columns.clone();
213 for (key, _) in rec.iter() {
214 if key != self.id_field && !cols.iter().any(|c| c == key) {
215 cols.push(key.to_string());
216 }
217 }
218 cols.retain(|c| c != &self.id_field && crate::sql::valid_ident(c));
219 cols
220 }
221
222 pub(crate) fn from_config(config: &FormConfig) -> Self {
223 Self {
224 entity: config.entity.clone(),
225 id_field: config.id_field.clone(),
226 columns: config.fields.iter().map(|f| f.name.clone()).collect(),
227 }
228 }
229}
230
231#[async_trait]
232impl Persister for DefaultPersister {
233 async fn create(&self, cx: &mut SaveCx<'_>, rec: &Record) -> Result<i64, SaveError> {
234 let stmt = {
237 let cols = self.write_columns(rec);
238 let vals: Vec<SimpleExpr> = cols.iter().map(|c| bind(rec.get(c))).collect();
239 Query::insert()
240 .into_table(Alias::new(&self.entity))
241 .columns(cols.iter().map(Alias::new))
242 .values_panic(vals)
243 .to_owned()
244 };
245 let backend = cx.backend();
246 insert_returning_id_on(backend, cx.conn(), stmt, Alias::new(&self.id_field))
247 .await
248 .map_err(|e| SaveError::Failed(e.to_string()))
249 }
250
251 async fn update(&self, cx: &mut SaveCx<'_>, id: &str, rec: &Record) -> Result<(), SaveError> {
252 let backend = cx.backend();
253 let (sql, values) = {
254 let mut update = Query::update();
255 update.table(Alias::new(&self.entity));
256 for column in self.write_columns(rec) {
257 update.value(Alias::new(&column), bind(rec.get(&column)));
258 }
259 update.and_where(
260 Expr::col(Alias::new(&self.id_field))
261 .cast_as(Alias::new(text_cast(backend)))
262 .eq(id),
263 );
264 to_sql(backend, update)
265 };
266 bind_values(sqlx::query(&sql), values)
267 .execute(cx.conn())
268 .await
269 .map(|_| ())
270 .map_err(|e| SaveError::Failed(e.to_string()))
271 }
272
273 async fn load(&self, cx: &mut SaveCx<'_>, id: &str) -> Result<Option<Record>, SaveError> {
277 let backend = cx.backend();
278 let (sql, values) = {
279 let mut select = Query::select();
280 select.from(Alias::new(&self.entity));
281 for column in &self.columns {
282 select.expr_as(
283 Expr::col(Alias::new(column)).cast_as(Alias::new(text_cast(backend))),
284 Alias::new(column),
285 );
286 }
287 select.and_where(
288 Expr::col(Alias::new(&self.id_field))
289 .cast_as(Alias::new(text_cast(backend)))
290 .eq(id),
291 );
292 to_sql(backend, select)
293 };
294 let row = bind_values(sqlx::query(&sql), values)
295 .fetch_optional(cx.conn())
296 .await
297 .map_err(|e| SaveError::Failed(e.to_string()))?;
298 let Some(row) = row else {
299 return Ok(None);
300 };
301 let mut previous = Record::new(&self.entity);
302 for column in &self.columns {
303 match row.get_text_opt(column) {
304 Ok(Some(v)) => {
305 previous.set(column.clone(), v);
306 }
307 Ok(None) => {
308 previous.set(column.clone(), AttrValue::Null);
309 }
310 Err(e) => return Err(SaveError::Failed(e.to_string())),
311 }
312 }
313 Ok(Some(previous))
314 }
315}
316
317#[cfg(test)]
318mod pipeline_tests {
319 use super::*;
320 use laterite_core::testing::connect_test;
321 use laterite_core::{ListenerTarget, ModelListenerReg, Text};
322 use std::sync::Mutex;
323
324 #[derive(Default)]
326 struct SpyPersister {
327 seen: Mutex<Vec<(String, HashMap<String, String>)>>,
328 }
329
330 #[async_trait]
331 impl Persister for SpyPersister {
332 async fn create(&self, _cx: &mut SaveCx<'_>, rec: &Record) -> Result<i64, SaveError> {
333 self.seen
334 .lock()
335 .unwrap()
336 .push(("create".into(), rec.to_text_map()));
337 Ok(42)
338 }
339 async fn update(
340 &self,
341 _cx: &mut SaveCx<'_>,
342 id: &str,
343 rec: &Record,
344 ) -> Result<(), SaveError> {
345 self.seen
346 .lock()
347 .unwrap()
348 .push((format!("update:{id}"), rec.to_text_map()));
349 Ok(())
350 }
351 }
352
353 struct Stamp {
356 key: &'static str,
357 log: Arc<Mutex<Vec<&'static str>>>,
358 }
359
360 #[async_trait]
361 impl ModelListener for Stamp {
362 async fn before_save(
363 &self,
364 _cx: &mut SaveCx<'_>,
365 rec: &mut Record,
366 _op: Op,
367 ) -> Result<(), ErrorBag> {
368 rec.set(self.key, "set");
369 Ok(())
370 }
371 async fn after_save(&self, _cx: &SavedCx<'_>, _rec: &Record, _op: Op) {
372 self.log.lock().unwrap().push(self.key);
373 }
374 }
375
376 struct Veto;
378
379 #[async_trait]
380 impl ModelListener for Veto {
381 async fn before_save(
382 &self,
383 _cx: &mut SaveCx<'_>,
384 _rec: &mut Record,
385 _op: Op,
386 ) -> Result<(), ErrorBag> {
387 let mut bag = ErrorBag::default();
388 bag.add("name", Text::new("Not allowed"));
390 Err(bag)
391 }
392 }
393
394 fn actor() -> Actor {
395 Actor::system("test")
396 }
397
398 fn listeners(regs: &[ModelListenerReg], entity: &str) -> Vec<Arc<dyn ModelListener>> {
399 regs.iter()
400 .filter(|r| r.matches(entity))
401 .map(|r| r.listener.clone())
402 .collect()
403 }
404
405 #[tokio::test]
406 async fn a_listener_mutation_reaches_the_persister_and_the_id_comes_back() {
407 let (db, _guard) = connect_test(&[]).await;
408 let spy = SpyPersister::default();
409 let log = Arc::new(Mutex::new(Vec::new()));
410 let regs = vec![ModelListenerReg::all(Arc::new(Stamp {
411 key: "created_at",
412 log: log.clone(),
413 }))];
414 let mut rec = Record::new("samples");
415 rec.set("name", "Chair");
416
417 save(
418 SaveRequest {
419 db: &db,
420 listeners: &listeners(®s, "samples"),
421 persister: &spy,
422 actor: &actor(),
423 op: Op::Create,
424 id: None,
425 },
426 &mut rec,
427 )
428 .await
429 .unwrap();
430
431 let seen = spy.seen.lock().unwrap();
432 assert_eq!(seen[0].0, "create");
433 assert_eq!(seen[0].1.get("created_at").map(String::as_str), Some("set"));
434 assert_eq!(rec.id(), Some(42), "the create id lands on the record");
435 assert_eq!(*log.lock().unwrap(), ["created_at"]);
436 }
437
438 #[tokio::test]
439 async fn a_veto_stops_the_write_and_returns_field_messages() {
440 let (db, _guard) = connect_test(&[]).await;
441 let spy = SpyPersister::default();
442 let regs = vec![ModelListenerReg::all(Arc::new(Veto))];
443 let mut rec = Record::new("samples");
444
445 let err = save(
446 SaveRequest {
447 db: &db,
448 listeners: &listeners(®s, "samples"),
449 persister: &spy,
450 actor: &actor(),
451 op: Op::Create,
452 id: None,
453 },
454 &mut rec,
455 )
456 .await
457 .expect_err("the veto refuses the write");
458
459 match err {
460 SaveError::Invalid(bag) => assert_eq!(bag.messages("name").len(), 1),
461 SaveError::Failed(e) => panic!("expected a field error, got {e}"),
462 }
463 assert!(
464 spy.seen.lock().unwrap().is_empty(),
465 "nothing reached the persister"
466 );
467 }
468
469 #[tokio::test]
470 async fn listeners_run_in_registration_order() {
471 let (db, _guard) = connect_test(&[]).await;
472 let log = Arc::new(Mutex::new(Vec::new()));
473 let regs = vec![
474 ModelListenerReg::all(Arc::new(Stamp {
475 key: "first",
476 log: log.clone(),
477 })),
478 ModelListenerReg::all(Arc::new(Stamp {
479 key: "second",
480 log: log.clone(),
481 })),
482 ];
483 let mut rec = Record::new("samples");
484
485 save(
486 SaveRequest {
487 db: &db,
488 listeners: &listeners(®s, "samples"),
489 persister: &SpyPersister::default(),
490 actor: &actor(),
491 op: Op::Create,
492 id: None,
493 },
494 &mut rec,
495 )
496 .await
497 .unwrap();
498
499 assert_eq!(*log.lock().unwrap(), ["first", "second"]);
500 }
501
502 #[tokio::test]
503 async fn a_target_selects_which_listeners_run() {
504 let (db, _guard) = connect_test(&[]).await;
505 let log = Arc::new(Mutex::new(Vec::new()));
506 let regs = vec![
507 ModelListenerReg::all(Arc::new(Stamp {
508 key: "everywhere",
509 log: log.clone(),
510 })),
511 ModelListenerReg::for_entity(
512 "orders",
513 Arc::new(Stamp {
514 key: "orders_only",
515 log: log.clone(),
516 }),
517 ),
518 ];
519 let mut rec = Record::new("samples");
520
521 save(
522 SaveRequest {
523 db: &db,
524 listeners: &listeners(®s, "samples"),
525 persister: &SpyPersister::default(),
526 actor: &actor(),
527 op: Op::Create,
528 id: None,
529 },
530 &mut rec,
531 )
532 .await
533 .unwrap();
534
535 assert_eq!(
536 *log.lock().unwrap(),
537 ["everywhere"],
538 "another entity's listener stays out"
539 );
540 assert!(matches!(regs[1].target, ListenerTarget::Entity(ref e) if e == "orders"));
541 }
542
543 #[tokio::test]
544 async fn a_listener_injected_attribute_is_written_not_dropped() {
545 let (db, _guard) = connect_test(&[]).await;
546 let spy = SpyPersister::default();
547 let log = Arc::new(Mutex::new(Vec::new()));
548 let regs = vec![ModelListenerReg::all(Arc::new(Stamp {
549 key: "created_at",
550 log,
551 }))];
552 let mut rec = Record::new("samples");
553 rec.set("name", "Chair");
554
555 save(
556 SaveRequest {
557 db: &db,
558 listeners: &listeners(®s, "samples"),
559 persister: &spy,
560 actor: &actor(),
561 op: Op::Create,
562 id: None,
563 },
564 &mut rec,
565 )
566 .await
567 .unwrap();
568
569 let default = DefaultPersister {
570 entity: "samples".into(),
571 id_field: "id".into(),
572 columns: vec!["name".into()],
573 };
574 assert_eq!(default.write_columns(&rec), ["name", "created_at"]);
576 }
577
578 #[test]
579 fn the_id_column_is_never_written() {
580 let default = DefaultPersister {
581 entity: "samples".into(),
582 id_field: "id".into(),
583 columns: vec!["name".into()],
584 };
585 let mut rec = Record::with_id("samples", 3);
586 rec.set("name", "Chair").set("id", 3i64);
587 assert_eq!(default.write_columns(&rec), ["name"]);
588 }
589
590 #[test]
591 fn binding_maps_each_kind_to_its_storage_form() {
592 let null: SimpleExpr = Option::<String>::None.into();
595 assert_eq!(bind(None), null);
596 assert_eq!(bind(Some(&AttrValue::Null)), null);
597 assert_eq!(bind(Some(&AttrValue::Text(String::new()))), null);
598 assert_eq!(bind(Some(&AttrValue::Int(7))), 7i64.into());
599 assert_eq!(bind(Some(&AttrValue::Bool(true))), 1i32.into());
601 assert_eq!(
602 bind(Some(&AttrValue::Text("Chair".into()))),
603 Some("Chair".to_string()).into()
604 );
605 }
606
607 struct SnapshotPersister;
609
610 #[async_trait]
611 impl Persister for SnapshotPersister {
612 async fn create(&self, _cx: &mut SaveCx<'_>, _rec: &Record) -> Result<i64, SaveError> {
613 Ok(1)
614 }
615 async fn update(
616 &self,
617 _cx: &mut SaveCx<'_>,
618 _id: &str,
619 _rec: &Record,
620 ) -> Result<(), SaveError> {
621 Ok(())
622 }
623 async fn load(&self, _cx: &mut SaveCx<'_>, _id: &str) -> Result<Option<Record>, SaveError> {
624 let mut previous = Record::new("samples");
625 previous.set("name", "Chair");
626 Ok(Some(previous))
627 }
628 }
629
630 struct WatchChanges {
632 seen: Arc<Mutex<Vec<(bool, bool)>>>,
633 }
634
635 #[async_trait]
636 impl ModelListener for WatchChanges {
637 async fn before_save(
638 &self,
639 _cx: &mut SaveCx<'_>,
640 rec: &mut Record,
641 _op: Op,
642 ) -> Result<(), ErrorBag> {
643 self.seen
644 .lock()
645 .unwrap()
646 .push((rec.has_original(), rec.changed("name")));
647 Ok(())
648 }
649 }
650
651 #[tokio::test]
652 async fn an_update_hands_listeners_the_pre_write_row() {
653 let (db, _guard) = connect_test(&[]).await;
654 let seen = Arc::new(Mutex::new(Vec::new()));
655 let regs = vec![ModelListenerReg::all(Arc::new(WatchChanges {
656 seen: seen.clone(),
657 }))];
658 let mut rec = Record::with_id("samples", 1);
659 rec.set("name", "Stool");
660
661 save(
662 SaveRequest {
663 db: &db,
664 listeners: &listeners(®s, "samples"),
665 persister: &SnapshotPersister,
666 actor: &actor(),
667 op: Op::Update,
668 id: Some("1"),
669 },
670 &mut rec,
671 )
672 .await
673 .unwrap();
674
675 assert_eq!(*seen.lock().unwrap(), [(true, true)]);
676 }
677
678 #[tokio::test]
679 async fn a_create_has_no_snapshot() {
680 let (db, _guard) = connect_test(&[]).await;
681 let seen = Arc::new(Mutex::new(Vec::new()));
682 let regs = vec![ModelListenerReg::all(Arc::new(WatchChanges {
683 seen: seen.clone(),
684 }))];
685 let mut rec = Record::new("samples");
686 rec.set("name", "Chair");
687
688 save(
689 SaveRequest {
690 db: &db,
691 listeners: &listeners(®s, "samples"),
692 persister: &SnapshotPersister,
693 actor: &actor(),
694 op: Op::Create,
695 id: None,
696 },
697 &mut rec,
698 )
699 .await
700 .unwrap();
701
702 assert_eq!(*seen.lock().unwrap(), [(false, false)]);
703 }
704
705 #[tokio::test]
706 async fn an_update_passes_its_id_through() {
707 let (db, _guard) = connect_test(&[]).await;
708 let spy = SpyPersister::default();
709 let mut rec = Record::with_id("samples", 7);
710
711 save(
712 SaveRequest {
713 db: &db,
714 listeners: &[],
715 persister: &spy,
716 actor: &actor(),
717 op: Op::Update,
718 id: Some("7"),
719 },
720 &mut rec,
721 )
722 .await
723 .unwrap();
724
725 assert_eq!(spy.seen.lock().unwrap()[0].0, "update:7");
726 }
727
728 #[tokio::test]
729 async fn an_update_without_an_id_fails_rather_than_writing() {
730 let (db, _guard) = connect_test(&[]).await;
731 let spy = SpyPersister::default();
732 let mut rec = Record::new("samples");
733
734 let err = save(
735 SaveRequest {
736 db: &db,
737 listeners: &[],
738 persister: &spy,
739 actor: &actor(),
740 op: Op::Update,
741 id: None,
742 },
743 &mut rec,
744 )
745 .await
746 .expect_err("an update needs an id");
747
748 assert!(matches!(err, SaveError::Failed(_)));
749 assert!(spy.seen.lock().unwrap().is_empty());
750 }
751}