Skip to main content

max / goingson

4.4 KB · 142 lines History Blame Raw
1 //! SQLite implementation of the DailyNoteRepository.
2
3 use async_trait::async_trait;
4 use chrono::{NaiveDate, Utc};
5 use sqlx::SqlitePool;
6 use goingson_core::{CoreError, DailyNote, DailyNoteId, DailyNoteRepository, Result, UserId};
7
8 use crate::utils::{format_datetime, parse_datetime, parse_uuid};
9
10 pub struct SqliteDailyNoteRepository {
11 pool: SqlitePool,
12 }
13
14 impl SqliteDailyNoteRepository {
15 #[tracing::instrument(skip_all)]
16 pub fn new(pool: SqlitePool) -> Self {
17 Self { pool }
18 }
19 }
20
21 #[derive(sqlx::FromRow)]
22 struct DailyNoteRow {
23 id: String,
24 user_id: String,
25 note_date: String,
26 went_well: String,
27 could_improve: String,
28 is_reviewed: i32,
29 reviewed_at: Option<String>,
30 created_at: String,
31 updated_at: String,
32 }
33
34 impl TryFrom<DailyNoteRow> for DailyNote {
35 type Error = CoreError;
36
37 fn try_from(row: DailyNoteRow) -> Result<Self> {
38 Ok(DailyNote {
39 id: parse_uuid(&row.id)?.into(),
40 user_id: parse_uuid(&row.user_id)?.into(),
41 note_date: NaiveDate::parse_from_str(&row.note_date, "%Y-%m-%d")
42 .map_err(|_| CoreError::parse("Invalid date"))?,
43 went_well: row.went_well,
44 could_improve: row.could_improve,
45 is_reviewed: row.is_reviewed != 0,
46 reviewed_at: row.reviewed_at.as_deref().map(parse_datetime).transpose()?,
47 created_at: parse_datetime(&row.created_at)?,
48 updated_at: parse_datetime(&row.updated_at)?,
49 })
50 }
51 }
52
53 #[async_trait]
54 impl DailyNoteRepository for SqliteDailyNoteRepository {
55 #[tracing::instrument(skip_all)]
56 async fn get_by_date(&self, user_id: UserId, date: NaiveDate) -> Result<Option<DailyNote>> {
57 let user_id_str = user_id.to_string();
58 let date_str = date.format("%Y-%m-%d").to_string();
59
60 let row: Option<DailyNoteRow> = sqlx::query_as(
61 "SELECT id, user_id, note_date, went_well, could_improve, is_reviewed, reviewed_at, created_at, updated_at
62 FROM daily_notes
63 WHERE user_id = ? AND note_date = ?"
64 )
65 .bind(&user_id_str)
66 .bind(&date_str)
67 .fetch_optional(&self.pool)
68 .await
69 .map_err(CoreError::database)?;
70
71 row.map(DailyNote::try_from).transpose()
72 }
73
74 #[tracing::instrument(skip_all)]
75 async fn upsert(
76 &self,
77 user_id: UserId,
78 date: NaiveDate,
79 went_well: &str,
80 could_improve: &str,
81 is_reviewed: bool,
82 ) -> Result<DailyNote> {
83 let user_id_str = user_id.to_string();
84 let date_str = date.format("%Y-%m-%d").to_string();
85 let now = Utc::now();
86 let now_str = format_datetime(&now);
87 let reviewed_at_str = if is_reviewed { Some(now_str.clone()) } else { None };
88
89 let existing = self.get_by_date(user_id, date).await?;
90
91 let (id, created_at) = if let Some(ref existing) = existing {
92 sqlx::query(
93 "UPDATE daily_notes SET went_well = ?, could_improve = ?, is_reviewed = ?, reviewed_at = ?, updated_at = ?
94 WHERE id = ?"
95 )
96 .bind(went_well)
97 .bind(could_improve)
98 .bind(is_reviewed as i32)
99 .bind(&reviewed_at_str)
100 .bind(&now_str)
101 .bind(existing.id.to_string())
102 .execute(&self.pool)
103 .await
104 .map_err(CoreError::database)?;
105
106 (existing.id, existing.created_at)
107 } else {
108 let id = DailyNoteId::new();
109 sqlx::query(
110 "INSERT INTO daily_notes (id, user_id, note_date, went_well, could_improve, is_reviewed, reviewed_at, created_at, updated_at)
111 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
112 )
113 .bind(id.to_string())
114 .bind(&user_id_str)
115 .bind(&date_str)
116 .bind(went_well)
117 .bind(could_improve)
118 .bind(is_reviewed as i32)
119 .bind(&reviewed_at_str)
120 .bind(&now_str)
121 .bind(&now_str)
122 .execute(&self.pool)
123 .await
124 .map_err(CoreError::database)?;
125
126 (id, now)
127 };
128
129 Ok(DailyNote {
130 id,
131 user_id,
132 note_date: date,
133 went_well: went_well.to_string(),
134 could_improve: could_improve.to_string(),
135 is_reviewed,
136 reviewed_at: reviewed_at_str.as_deref().map(parse_datetime).transpose()?,
137 created_at,
138 updated_at: now,
139 })
140 }
141 }
142