backend/model/entity/
guided_tour_impl.rs

1//! Contains the implementation of the Guided Tour (progress + state).
2
3use diesel::dsl::now;
4use diesel::ExpressionMethods;
5use diesel::OptionalExtension;
6use diesel::{debug_query, pg::Pg, QueryDsl, QueryResult};
7use diesel_async::{AsyncPgConnection, RunQueryDsl};
8use log::debug;
9use uuid::Uuid;
10
11use crate::{
12    model::{
13        dto::GuidedTourDto,
14        entity::{GuidedTourProgress, GuidedTourState, NewGuidedTourProgress},
15    },
16    schema::{guided_tour_progress, guided_tour_states},
17};
18
19pub struct GuidedTour;
20
21impl GuidedTour {
22    /// Get guided tour info for a user:
23    /// - completed steps
24    /// - paused bool (computed from `paused_at` + `pause_count`)
25    ///
26    /// # Errors
27    /// * Unknown, diesel doesn't say why it might error.
28    pub async fn find_by_user(
29        user_id: Uuid,
30        conn: &mut AsyncPgConnection,
31    ) -> QueryResult<GuidedTourDto> {
32        // progress rows
33        let progress_query =
34            guided_tour_progress::table.filter(guided_tour_progress::user_id.eq(user_id));
35        debug!("{}", debug_query::<Pg, _>(&progress_query));
36        let progress_rows = progress_query.load::<GuidedTourProgress>(conn).await?;
37
38        // state row
39        let state_query = guided_tour_states::table.find(user_id);
40        debug!("{}", debug_query::<Pg, _>(&state_query));
41        let state_opt = state_query
42            .first::<GuidedTourState>(conn)
43            .await
44            .optional()?;
45
46        Ok(GuidedTourDto::from((progress_rows, state_opt)))
47    }
48
49    /// Mark a step completed (idempotent).
50    ///
51    /// # Errors
52    /// * Unknown, diesel doesn't say why it might error.
53    pub async fn complete_step(
54        user_id: Uuid,
55        step_key: String,
56        conn: &mut AsyncPgConnection,
57    ) -> QueryResult<()> {
58        let row = NewGuidedTourProgress { user_id, step_key };
59
60        let query = diesel::insert_into(guided_tour_progress::table)
61            .values(&row)
62            .on_conflict((
63                guided_tour_progress::user_id,
64                guided_tour_progress::step_key,
65            ))
66            .do_nothing();
67
68        debug!("{}", debug_query::<Pg, _>(&query));
69        query.execute(conn).await.map(|_| ())
70    }
71
72    /// Mark multiple steps as completed (idempotent)
73    ///
74    /// # Errors
75    /// * Unknown, diesel doesn't say why it might error.
76    pub async fn complete_steps(
77        user_id: Uuid,
78        step_keys: Vec<String>,
79        conn: &mut AsyncPgConnection,
80    ) -> QueryResult<()> {
81        if step_keys.is_empty() {
82            return Ok(());
83        }
84
85        let rows: Vec<NewGuidedTourProgress> = step_keys
86            .into_iter()
87            .map(|step_key| NewGuidedTourProgress { user_id, step_key })
88            .collect();
89
90        let query = diesel::insert_into(guided_tour_progress::table)
91            .values(&rows)
92            .on_conflict((
93                guided_tour_progress::user_id,
94                guided_tour_progress::step_key,
95            ))
96            .do_nothing();
97
98        debug!("{}", debug_query::<Pg, _>(&query));
99        query.execute(conn).await.map(|_| ())
100    }
101
102    /// Pause the tour:
103    /// - sets `paused_at` = `now()`
104    /// - increments `pause_count` (or inserts with `pause_count` = 1 if missing)
105    ///
106    /// # Errors
107    /// * Unknown, diesel doesn't say why it might error.
108    pub async fn pause(user_id: Uuid, conn: &mut AsyncPgConnection) -> QueryResult<()> {
109        let query = diesel::insert_into(guided_tour_states::table)
110            .values((
111                guided_tour_states::user_id.eq(user_id),
112                guided_tour_states::paused_at.eq(now),
113                guided_tour_states::pause_count.eq(1),
114            ))
115            .on_conflict(guided_tour_states::user_id)
116            .do_update()
117            .set((
118                guided_tour_states::paused_at.eq(now),
119                guided_tour_states::pause_count.eq(guided_tour_states::pause_count + 1),
120            ));
121
122        debug!("{}", debug_query::<Pg, _>(&query));
123        query.execute(conn).await.map(|_| ())
124    }
125}