1use super::{map_db_error, parse_datetime_row, parse_uuid_row, with_immediate_transaction};
4use chrono::Utc;
5use r2d2::Pool;
6use r2d2_sqlite::SqliteConnectionManager;
7use stateset_core::{
8 AddWishlistItem, CommerceError, CreateWishlist, ProductId, Result, UpdateWishlist, Wishlist,
9 WishlistFilter, WishlistId, WishlistItem, WishlistRepository,
10};
11
12#[derive(Debug)]
13pub struct SqliteWishlistRepository {
14 pool: Pool<SqliteConnectionManager>,
15}
16
17impl SqliteWishlistRepository {
18 #[must_use]
19 pub const fn new(pool: Pool<SqliteConnectionManager>) -> Self {
20 Self { pool }
21 }
22
23 fn conn(&self) -> Result<r2d2::PooledConnection<SqliteConnectionManager>> {
24 self.pool.get().map_err(|e| CommerceError::DatabaseError(e.to_string()))
25 }
26
27 fn row_to_wishlist(row: &rusqlite::Row<'_>) -> rusqlite::Result<Wishlist> {
28 Ok(Wishlist {
29 id: parse_uuid_row(&row.get::<_, String>("id")?, "wishlist", "id")?.into(),
30 customer_id: parse_uuid_row(
31 &row.get::<_, String>("customer_id")?,
32 "wishlist",
33 "customer_id",
34 )?
35 .into(),
36 name: row.get("name")?,
37 is_public: row.get::<_, i32>("is_public")? != 0,
38 items: Vec::new(),
39 created_at: parse_datetime_row(
40 &row.get::<_, String>("created_at")?,
41 "wishlist",
42 "created_at",
43 )?,
44 updated_at: parse_datetime_row(
45 &row.get::<_, String>("updated_at")?,
46 "wishlist",
47 "updated_at",
48 )?,
49 })
50 }
51
52 fn row_to_item(row: &rusqlite::Row<'_>) -> rusqlite::Result<WishlistItem> {
53 Ok(WishlistItem {
54 product_id: parse_uuid_row(
55 &row.get::<_, String>("product_id")?,
56 "wishlist_item",
57 "product_id",
58 )?
59 .into(),
60 variant_id: row.get("variant_id")?,
61 added_at: parse_datetime_row(
62 &row.get::<_, String>("added_at")?,
63 "wishlist_item",
64 "added_at",
65 )?,
66 note: row.get("notes")?,
67 quantity: row.get::<_, i64>("quantity")? as u32,
68 priority: row.get("priority")?,
69 })
70 }
71
72 fn load_items(
73 conn: &rusqlite::Connection,
74 wishlist_id: &str,
75 ) -> rusqlite::Result<Vec<WishlistItem>> {
76 let mut stmt =
77 conn.prepare("SELECT * FROM wishlist_items WHERE wishlist_id = ? ORDER BY added_at")?;
78 let items = stmt
79 .query_map([wishlist_id], Self::row_to_item)?
80 .collect::<std::result::Result<Vec<_>, _>>()?;
81 Ok(items)
82 }
83
84 fn load_items_batch(
85 conn: &rusqlite::Connection,
86 ids: &[String],
87 ) -> rusqlite::Result<std::collections::HashMap<String, Vec<WishlistItem>>> {
88 let mut map: std::collections::HashMap<String, Vec<WishlistItem>> =
89 std::collections::HashMap::with_capacity(ids.len());
90 for chunk in ids.chunks(500) {
91 let placeholders = super::build_in_clause(chunk.len());
92 let sql = format!(
93 "SELECT * FROM wishlist_items WHERE wishlist_id IN ({placeholders}) ORDER BY added_at"
94 );
95 let mut stmt = conn.prepare(&sql)?;
96 let param_refs: Vec<&dyn rusqlite::types::ToSql> =
97 chunk.iter().map(|s| s as &dyn rusqlite::types::ToSql).collect();
98 let rows = stmt.query_map(param_refs.as_slice(), |row| {
99 let parent: String = row.get("wishlist_id")?;
100 Ok((parent, Self::row_to_item(row)?))
101 })?;
102 for row in rows {
103 let (parent, item) = row?;
104 map.entry(parent).or_default().push(item);
105 }
106 }
107 Ok(map)
108 }
109}
110
111impl WishlistRepository for SqliteWishlistRepository {
112 fn create(&self, input: CreateWishlist) -> Result<Wishlist> {
113 let id = WishlistId::new();
114 let now = Utc::now();
115 let id_str = id.to_string();
116 let now_str = now.to_rfc3339();
117
118 with_immediate_transaction(&self.pool, |tx| {
119 tx.execute(
120 "INSERT INTO wishlists (id, customer_id, name, is_public, created_at, updated_at)
121 VALUES (?, ?, ?, ?, ?, ?)",
122 rusqlite::params![
123 &id_str,
124 input.customer_id.to_string(),
125 &input.name,
126 input.is_public as i32,
127 &now_str,
128 &now_str,
129 ],
130 )?;
131
132 tx.query_row("SELECT * FROM wishlists WHERE id = ?", [&id_str], Self::row_to_wishlist)
133 })
134 }
135
136 fn get(&self, id: WishlistId) -> Result<Option<Wishlist>> {
137 let conn = self.conn()?;
138 let id_str = id.to_string();
139 match conn.query_row(
140 "SELECT * FROM wishlists WHERE id = ?",
141 [&id_str],
142 Self::row_to_wishlist,
143 ) {
144 Ok(mut wishlist) => {
145 wishlist.items = Self::load_items(&conn, &id_str).map_err(map_db_error)?;
146 Ok(Some(wishlist))
147 }
148 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
149 Err(e) => Err(map_db_error(e)),
150 }
151 }
152
153 fn update(&self, id: WishlistId, input: UpdateWishlist) -> Result<Wishlist> {
154 let id_str = id.to_string();
155 let now_str = Utc::now().to_rfc3339();
156
157 with_immediate_transaction(&self.pool, |tx| {
158 let mut sets = vec!["updated_at = ?".to_string()];
159 let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = vec![Box::new(now_str.clone())];
160
161 if let Some(ref name) = input.name {
162 sets.push("name = ?".into());
163 params.push(Box::new(name.clone()));
164 }
165 if let Some(is_public) = input.is_public {
166 sets.push("is_public = ?".into());
167 params.push(Box::new(is_public as i32));
168 }
169
170 let sql = format!("UPDATE wishlists SET {} WHERE id = ?", sets.join(", "));
171 params.push(Box::new(id_str.clone()));
172
173 let param_refs: Vec<&dyn rusqlite::types::ToSql> =
174 params.iter().map(|p| p.as_ref()).collect();
175 tx.execute(&sql, param_refs.as_slice())?;
176
177 let mut wishlist = tx.query_row(
178 "SELECT * FROM wishlists WHERE id = ?",
179 [&id_str],
180 Self::row_to_wishlist,
181 )?;
182 wishlist.items = Self::load_items(tx, &id_str)?;
183 Ok(wishlist)
184 })
185 }
186
187 fn list(&self, filter: WishlistFilter) -> Result<Vec<Wishlist>> {
188 let conn = self.conn()?;
189 let mut sql = "SELECT * FROM wishlists WHERE 1=1".to_string();
190 let mut params: Vec<Box<dyn rusqlite::types::ToSql>> = vec![];
191
192 if let Some(customer_id) = filter.customer_id {
193 sql.push_str(" AND customer_id = ?");
194 params.push(Box::new(customer_id.to_string()));
195 }
196 if let Some(is_public) = filter.is_public {
197 sql.push_str(" AND is_public = ?");
198 params.push(Box::new(is_public as i32));
199 }
200
201 sql.push_str(" ORDER BY created_at DESC");
202
203 crate::sqlite::append_limit_offset(&mut sql, filter.limit, filter.offset);
204
205 let param_refs: Vec<&dyn rusqlite::types::ToSql> =
206 params.iter().map(|p| p.as_ref()).collect();
207 let mut stmt = conn.prepare(&sql).map_err(map_db_error)?;
208 let wishlists = stmt
209 .query_map(param_refs.as_slice(), Self::row_to_wishlist)
210 .map_err(map_db_error)?
211 .collect::<std::result::Result<Vec<_>, _>>()
212 .map_err(map_db_error)?;
213
214 let ids: Vec<String> = wishlists.iter().map(|w| w.id.to_string()).collect();
216 let mut items_by_id = Self::load_items_batch(&conn, &ids).map_err(map_db_error)?;
217 let mut result = Vec::with_capacity(wishlists.len());
218 for mut wl in wishlists {
219 wl.items = items_by_id.remove(&wl.id.to_string()).unwrap_or_default();
220 result.push(wl);
221 }
222 Ok(result)
223 }
224
225 fn delete(&self, id: WishlistId) -> Result<()> {
226 let conn = self.conn()?;
227 let id_str = id.to_string();
228 conn.execute("DELETE FROM wishlist_items WHERE wishlist_id = ?", [&id_str])
230 .map_err(map_db_error)?;
231 conn.execute("DELETE FROM wishlists WHERE id = ?", [&id_str]).map_err(map_db_error)?;
232 Ok(())
233 }
234
235 fn add_item(&self, wishlist_id: WishlistId, item: AddWishlistItem) -> Result<WishlistItem> {
236 let wl_id_str = wishlist_id.to_string();
237 let item_id = uuid::Uuid::new_v4().to_string();
238 let now_str = Utc::now().to_rfc3339();
239
240 with_immediate_transaction(&self.pool, |tx| {
241 tx.query_row("SELECT id FROM wishlists WHERE id = ?", [&wl_id_str], |row| {
243 row.get::<_, String>(0)
244 })
245 .map_err(|e| match e {
246 rusqlite::Error::QueryReturnedNoRows => rusqlite::Error::QueryReturnedNoRows,
247 other => other,
248 })?;
249
250 let product_id_str = item.product_id.to_string();
251 let quantity = i64::from(item.quantity.unwrap_or(1));
252
253 tx.execute(
254 "INSERT INTO wishlist_items (id, wishlist_id, product_id, variant_id, priority, quantity, added_at, notes)
255 VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
256 rusqlite::params![
257 &item_id,
258 &wl_id_str,
259 &product_id_str,
260 &item.variant_id,
261 &item.priority,
262 quantity,
263 &now_str,
264 &item.note,
265 ],
266 )?;
267
268 tx.execute(
270 "UPDATE wishlists SET updated_at = ? WHERE id = ?",
271 rusqlite::params![&now_str, &wl_id_str],
272 )?;
273
274 tx.query_row("SELECT * FROM wishlist_items WHERE id = ?", [&item_id], Self::row_to_item)
275 })
276 }
277
278 fn remove_item(&self, wishlist_id: WishlistId, product_id: ProductId) -> Result<()> {
279 let conn = self.conn()?;
280 let wl_id_str = wishlist_id.to_string();
281 let product_id_str = product_id.to_string();
282 let now_str = Utc::now().to_rfc3339();
283
284 conn.execute(
285 "DELETE FROM wishlist_items WHERE wishlist_id = ? AND product_id = ?",
286 rusqlite::params![&wl_id_str, &product_id_str],
287 )
288 .map_err(map_db_error)?;
289
290 conn.execute(
292 "UPDATE wishlists SET updated_at = ? WHERE id = ?",
293 rusqlite::params![&now_str, &wl_id_str],
294 )
295 .map_err(map_db_error)?;
296
297 Ok(())
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::DatabaseConfig;
305 use crate::sqlite::SqliteDatabase;
306 use stateset_core::CustomerId;
307
308 fn test_repo() -> SqliteWishlistRepository {
309 let db = SqliteDatabase::new(&DatabaseConfig::in_memory()).expect("in-memory db");
310 let conn = db.conn().expect("conn");
311 conn.execute_batch(
312 "CREATE TABLE IF NOT EXISTS wishlists (
313 id TEXT PRIMARY KEY,
314 customer_id TEXT NOT NULL,
315 name TEXT NOT NULL DEFAULT 'My Wishlist',
316 is_public INTEGER NOT NULL DEFAULT 0,
317 created_at TEXT,
318 updated_at TEXT
319 );
320 CREATE TABLE IF NOT EXISTS wishlist_items (
321 id TEXT PRIMARY KEY,
322 wishlist_id TEXT NOT NULL,
323 product_id TEXT NOT NULL,
324 added_at TEXT NOT NULL DEFAULT (datetime('now')),
325 notes TEXT,
326 UNIQUE(wishlist_id, product_id)
327 );",
328 )
329 .expect("create tables");
330 SqliteWishlistRepository::new(db.pool().clone())
331 }
332
333 #[test]
334 fn create_and_get() {
335 let repo = test_repo();
336 let customer_id = CustomerId::new();
337 let wishlist = repo
338 .create(CreateWishlist { customer_id, name: "Birthday Ideas".into(), is_public: true })
339 .expect("create");
340
341 assert_eq!(wishlist.name, "Birthday Ideas");
342 assert!(wishlist.is_public);
343 assert!(wishlist.items.is_empty());
344
345 let fetched = repo.get(wishlist.id).expect("get").expect("Some");
346 assert_eq!(fetched.id, wishlist.id);
347 assert_eq!(fetched.customer_id, customer_id);
348 assert_eq!(fetched.name, "Birthday Ideas");
349 assert!(fetched.is_public);
350 }
351
352 #[test]
353 fn add_item() {
354 let repo = test_repo();
355 let wishlist = repo
356 .create(CreateWishlist {
357 customer_id: CustomerId::new(),
358 name: "Gadgets".into(),
359 is_public: false,
360 })
361 .expect("create");
362
363 let product_id = ProductId::new();
364 let item = repo
365 .add_item(
366 wishlist.id,
367 AddWishlistItem {
368 product_id,
369 variant_id: Some("VAR-1".into()),
370 note: Some("Love this one".into()),
371 quantity: Some(3),
372 priority: Some(2),
373 },
374 )
375 .expect("add_item");
376
377 assert_eq!(item.product_id, product_id);
378 assert_eq!(item.note.as_deref(), Some("Love this one"));
379 assert_eq!(item.variant_id.as_deref(), Some("VAR-1"));
380 assert_eq!(item.quantity, 3);
381 assert_eq!(item.priority, Some(2));
382
383 let fetched = repo.get(wishlist.id).expect("get").expect("Some");
387 assert_eq!(fetched.items.len(), 1);
388 let stored = &fetched.items[0];
389 assert_eq!(stored.product_id, product_id);
390 assert_eq!(stored.variant_id.as_deref(), Some("VAR-1"));
391 assert_eq!(stored.quantity, 3, "quantity must survive persistence");
392 assert_eq!(stored.priority, Some(2));
393 }
394
395 #[test]
396 fn delete() {
397 let repo = test_repo();
398 let wishlist = repo
399 .create(CreateWishlist {
400 customer_id: CustomerId::new(),
401 name: "Temp".into(),
402 is_public: false,
403 })
404 .expect("create");
405
406 repo.add_item(
408 wishlist.id,
409 AddWishlistItem {
410 product_id: ProductId::new(),
411 variant_id: None,
412 note: None,
413 quantity: None,
414 priority: None,
415 },
416 )
417 .expect("add_item");
418
419 repo.delete(wishlist.id).expect("delete");
420 assert!(repo.get(wishlist.id).expect("get after delete").is_none());
421 }
422}