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