Skip to main content

sofie_db/
outcome.rs

1use std::borrow::Cow;
2use std::sync::Arc;
3
4use serde_json::Value;
5use sofie_core::{Error, FieldError, Instant, Result, Uuid};
6
7use crate::heard::Known;
8use crate::messages::queue_steps;
9use crate::{Dialect, GuardCheck, MessageKind, RaisedEvent, Reach, Reply, Step, UniqueViolation};
10
11pub struct Plan<'d> {
12    dialect: &'d dyn Dialect,
13    reach: Reach,
14    stamp: Stamp,
15    steps: Vec<Step>,
16    leading: usize,
17    guards: Vec<Step>,
18    guard_checks: Vec<Arc<GuardCheck>>,
19    pub(crate) events: Vec<RaisedEvent>,
20    unique_fields: Vec<UniqueField>,
21    queues_messages: bool,
22    pub(crate) heard: Option<Arc<Known>>,
23    #[cfg(feature = "cache")]
24    changed: Vec<crate::ChangedRecord>,
25    #[cfg(feature = "live")]
26    live: Vec<crate::TableChange>,
27    #[cfg(feature = "module-roles")]
28    role: Option<&'static str>,
29    #[cfg(feature = "search-engine")]
30    indexes_searches: bool,
31}
32
33pub(crate) struct Planned {
34    pub(crate) steps: Vec<Step>,
35    pub(crate) guard_checks: Vec<Arc<GuardCheck>>,
36    pub(crate) unique_fields: Vec<UniqueField>,
37    pub(crate) queues_messages: bool,
38    #[cfg(feature = "cache")]
39    pub(crate) changed: Vec<crate::ChangedRecord>,
40    #[cfg(feature = "live")]
41    pub(crate) live: Vec<crate::TableChange>,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct Stamp {
46    pub now: Instant,
47    pub actor: Option<Uuid>,
48    pub action: Option<&'static str>,
49}
50
51const UNIQUE_KEY: &str = "validation.unique";
52#[cfg(feature = "module-roles")]
53pub(crate) const FRAMEWORK_MODULE: &str = "sofie";
54const TAKEN: &str = "is already taken";
55
56pub(crate) struct UniqueField {
57    index: Cow<'static, str>,
58    /// Where known, the only prefix a database without schemas may add to the index's name.
59    module: Option<&'static str>,
60    field: &'static str,
61    message: &'static str,
62}
63
64impl<'d> Plan<'d> {
65    pub(crate) fn new(
66        dialect: &'d dyn Dialect,
67        reach: Reach,
68        now: Instant,
69        steps: Vec<Step>,
70        heard: Option<Arc<Known>>,
71    ) -> Self {
72        Self {
73            dialect,
74            reach,
75            stamp: Stamp {
76                now,
77                actor: reach.actor(),
78                action: crate::action::current(),
79            },
80            leading: steps.len(),
81            steps,
82            guards: Vec::new(),
83            guard_checks: Vec::new(),
84            events: Vec::new(),
85            unique_fields: Vec::new(),
86            queues_messages: false,
87            heard,
88            #[cfg(feature = "cache")]
89            changed: Vec::new(),
90            #[cfg(feature = "live")]
91            live: Vec::new(),
92            #[cfg(feature = "module-roles")]
93            role: None,
94            #[cfg(feature = "search-engine")]
95            indexes_searches: false,
96        }
97    }
98
99    #[cfg(feature = "search-engine")]
100    pub(crate) fn index_searches(&mut self) {
101        self.indexes_searches = true;
102    }
103
104    #[cfg(feature = "search-engine")]
105    pub(crate) fn indexes_searches(&self) -> bool {
106        self.indexes_searches
107    }
108
109    /// The framework's own records are written as the app's login, which owns them.
110    #[cfg(feature = "module-roles")]
111    pub(crate) fn act_as(&mut self, module: &'static str) {
112        let role = (module != FRAMEWORK_MODULE).then_some(module);
113        if role == self.role {
114            return;
115        }
116        if let Some(statement) = self.dialect.enter_role(role) {
117            self.steps.push(Step::Script(statement));
118        }
119        self.role = role;
120    }
121
122    #[must_use]
123    pub fn dialect(&self) -> &'d dyn Dialect {
124        self.dialect
125    }
126
127    #[must_use]
128    pub fn reach(&self) -> Reach {
129        self.reach
130    }
131
132    #[must_use]
133    pub fn stamp(&self) -> Stamp {
134        self.stamp
135    }
136
137    pub fn push(&mut self, step: Step) {
138        self.steps.push(step);
139    }
140
141    pub(crate) fn guard(&mut self, steps: Vec<Step>, check: Arc<GuardCheck>) {
142        self.guards.extend(steps);
143        self.guard_checks.push(check);
144    }
145
146    pub fn raise(&mut self, events: &[RaisedEvent]) {
147        self.events.extend_from_slice(events);
148    }
149
150    #[cfg(any(feature = "contracts", feature = "publishing", feature = "retention"))]
151    pub(crate) fn raise_one(&mut self, name: &'static str, payload: Value) {
152        self.events.push(RaisedEvent { name, payload });
153    }
154
155    pub(crate) fn queue<'a>(
156        &mut self,
157        kind: MessageKind,
158        messages: impl IntoIterator<Item = (&'a str, Value)>,
159    ) -> Result<()> {
160        self.queue_at(kind, messages, self.stamp.now.timestamp())
161    }
162
163    pub(crate) fn queue_at<'a>(
164        &mut self,
165        kind: MessageKind,
166        messages: impl IntoIterator<Item = (&'a str, Value)>,
167        at: sofie_core::Timestamp,
168    ) -> Result<()> {
169        let heard = self.heard.as_deref();
170        let queuing = self.stamp.queuing(kind, at);
171        self.queues_messages |=
172            queue_steps(self.dialect, &queuing, messages, heard, &mut self.steps)?;
173        Ok(())
174    }
175
176    pub(crate) fn push_message(&mut self, step: Step) {
177        self.queues_messages = true;
178        self.steps.push(step);
179    }
180
181    #[cfg(feature = "cache")]
182    pub(crate) fn changed(&mut self, record: crate::ChangedRecord) {
183        self.changed.push(record);
184    }
185
186    #[cfg(feature = "live")]
187    pub(crate) fn live_change(&mut self, change: crate::TableChange) {
188        self.live.push(change);
189    }
190
191    /// Catches a duplicate another request committed after the handler's own check.
192    pub fn unique(&mut self, index: &'static str, field: &'static str, message: &'static str) {
193        self.unique_fields.push(UniqueField {
194            index: Cow::Borrowed(index),
195            module: None,
196            field,
197            message,
198        });
199    }
200
201    /// `<table>_<column>`, or `<table>_<column>_key` as Postgres names an inline `UNIQUE`.
202    pub(crate) fn unique_column(&mut self, module: &'static str, index: &str, field: &'static str) {
203        self.unique_fields.push(UniqueField {
204            index: Cow::Owned(index.to_owned()),
205            module: Some(module),
206            field,
207            message: TAKEN,
208        });
209    }
210
211    pub(crate) fn into_parts(mut self) -> Result<Planned> {
212        let events = std::mem::take(&mut self.events);
213        // A listener's job is queued with `ON CONFLICT`, which needs SELECT a module role lacks.
214        #[cfg(feature = "module-roles")]
215        if !events.is_empty() {
216            self.act_as(FRAMEWORK_MODULE);
217        }
218        let named = events.into_iter().map(|event| (event.name, event.payload));
219        self.queue(MessageKind::Event, named)?;
220        let mut steps = self.steps;
221        let writes = steps.split_off(self.leading);
222        steps.extend(self.guards);
223        steps.extend(writes);
224        Ok(Planned {
225            steps,
226            guard_checks: self.guard_checks,
227            unique_fields: self.unique_fields,
228            queues_messages: self.queues_messages,
229            #[cfg(feature = "cache")]
230            changed: self.changed,
231            #[cfg(feature = "live")]
232            live: self.live,
233        })
234    }
235}
236
237pub(crate) fn explain_duplicate(error: Error, unique_fields: &[UniqueField]) -> Error {
238    let Error::Internal {
239        source: Some(source),
240        ..
241    } = &error
242    else {
243        return error;
244    };
245    let Some(violation) = source.downcast_ref::<UniqueViolation>() else {
246        return error;
247    };
248    let index = violation.index.as_str();
249    let index = index.strip_suffix("_key").unwrap_or(index);
250    // A database without schemas names the index after the module's table too.
251    let prefixed = |unique: &&UniqueField| match unique.module {
252        Some(module) => {
253            index
254                .strip_prefix(module)
255                .and_then(|rest| rest.strip_prefix('_'))
256                == Some(&*unique.index)
257        }
258        None => index.ends_with(&format!("_{}", unique.index)),
259    };
260    let exact = unique_fields.iter().find(|unique| index == unique.index);
261    match exact.or_else(|| unique_fields.iter().find(prefixed)) {
262        Some(unique) => Error::Invalid(vec![FieldError::new(
263            unique.field,
264            UNIQUE_KEY,
265            unique.message,
266        )]),
267        None => error,
268    }
269}
270
271pub trait Outcome: Send {
272    fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
273        let _ = plan;
274        Ok(())
275    }
276
277    fn committed(&mut self, stamp: Stamp) {
278        let _ = stamp;
279    }
280
281    /// What was planned is already committed when serializing fails.
282    fn reply(self) -> Result<Reply>
283    where
284        Self: Sized,
285    {
286        Ok(Reply::Empty)
287    }
288}
289
290/// What a GET may answer with: its outcome is replied, never committed.
291#[diagnostic::on_unimplemented(
292    message = "a GET, screen or island handler cannot return `{Self}`: a GET never writes",
293    label = "a writable outcome",
294    note = "return a read model, `Paginated`, `Screen` or another reply; a write belongs in a POST, or in a job a domain event triggers"
295)]
296pub trait ReadOnly: Outcome {}
297
298impl Outcome for () {}
299
300impl ReadOnly for () {}
301
302impl<T: ReadOnly> ReadOnly for Vec<T> {}
303
304impl<T: Outcome> Outcome for Vec<T> {
305    fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
306        self.iter().try_for_each(|outcome| outcome.plan(plan))
307    }
308
309    fn committed(&mut self, stamp: Stamp) {
310        for outcome in self {
311            outcome.committed(stamp);
312        }
313    }
314
315    fn reply(self) -> Result<Reply> {
316        self.into_iter()
317            .map(Outcome::reply)
318            .collect::<Result<_>>()
319            .map(Reply::List)
320    }
321}
322
323impl<T: Outcome> Outcome for Option<T> {
324    fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
325        self.as_ref().map_or(Ok(()), |outcome| outcome.plan(plan))
326    }
327
328    fn committed(&mut self, stamp: Stamp) {
329        if let Some(outcome) = self {
330            outcome.committed(stamp);
331        }
332    }
333
334    fn reply(self) -> Result<Reply> {
335        self.map_or(Ok(Reply::Empty), Outcome::reply)
336    }
337}
338
339macro_rules! tuple_outcome {
340    ($($name:ident),+) => {
341        #[allow(non_snake_case)]
342        impl<$($name: Outcome),+> Outcome for ($($name,)+) {
343            fn plan(&self, plan: &mut Plan<'_>) -> Result<()> {
344                let ($($name,)+) = self;
345                $($name.plan(plan)?;)+
346                Ok(())
347            }
348
349            fn committed(&mut self, stamp: Stamp) {
350                let ($($name,)+) = self;
351                $($name.committed(stamp);)+
352            }
353
354            /// The first part answering anything: `(Booking, Queued)` answers the booking.
355            fn reply(self) -> Result<Reply> {
356                let ($($name,)+) = self;
357                $(
358                    let reply = $name.reply()?;
359                    if !matches!(reply, Reply::Empty) {
360                        return Ok(reply);
361                    }
362                )+
363                Ok(Reply::Empty)
364            }
365        }
366
367        impl<$($name: ReadOnly),+> ReadOnly for ($($name,)+) {}
368    };
369}
370
371tuple_outcome!(A, B);
372tuple_outcome!(A, B, C);
373tuple_outcome!(A, B, C, D);
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    fn email_field() -> Vec<UniqueField> {
380        vec![UniqueField {
381            index: Cow::Borrowed("account_email"),
382            module: Some("shop"),
383            field: "email",
384            message: "is already registered",
385        }]
386    }
387
388    fn violation(index: &str) -> Error {
389        Error::internal(
390            "running an insert",
391            UniqueViolation {
392                index: index.to_owned(),
393            },
394        )
395    }
396
397    #[test]
398    fn an_absent_outcome_replies_empty_and_a_present_one_replies_its_own() {
399        let absent: Option<Vec<()>> = None;
400
401        assert!(matches!(absent.reply().unwrap(), Reply::Empty));
402        assert!(matches!(
403            Some(vec![()]).reply().unwrap(),
404            Reply::List(items) if matches!(items[..], [Reply::Empty])
405        ));
406    }
407
408    struct Saved;
409
410    impl Outcome for Saved {
411        fn reply(self) -> Result<Reply> {
412            Ok(Reply::Json(serde_json::json!({ "id": "1" })))
413        }
414    }
415
416    #[test]
417    fn a_tuple_replies_its_first_part_that_answers_anything() {
418        let reply = ((), Saved, Saved).reply().unwrap();
419
420        assert!(
421            matches!(&reply, Reply::Json(value) if value["id"] == "1"),
422            "{:?}",
423            reply.to_json()
424        );
425        assert!(matches!(((), ()).reply().unwrap(), Reply::Empty));
426    }
427
428    #[test]
429    fn a_violation_named_after_the_module_s_table_is_invalid_on_its_field() {
430        let error = explain_duplicate(violation("shop_account_email"), &email_field());
431
432        assert!(matches!(&error, Error::Invalid(fields) if fields[0].field == "email"));
433    }
434
435    #[test]
436    fn a_violation_on_a_table_whose_name_ends_like_another_s_is_not_put_on_its_field() {
437        let error = explain_duplicate(violation("billing_account_email_key"), &email_field());
438
439        assert!(matches!(error, Error::Internal { .. }), "{error}");
440    }
441
442    #[test]
443    fn a_violation_of_a_named_index_is_invalid_on_its_field() {
444        let error = explain_duplicate(violation("account_email"), &email_field());
445
446        assert!(
447            matches!(&error, Error::Invalid(fields) if fields[0].field == "email"
448                && fields[0].key == "validation.unique"
449                && fields[0].message == "is already registered"),
450            "{error}"
451        );
452    }
453
454    #[test]
455    fn any_other_failure_is_left_as_it_was() {
456        let unnamed = explain_duplicate(violation("other_index"), &email_field());
457        let plain = explain_duplicate(Error::internal_message("lost"), &email_field());
458
459        assert!(matches!(unnamed, Error::Internal { .. }), "{unnamed}");
460        assert!(matches!(plain, Error::Internal { .. }), "{plain}");
461    }
462}