1use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11use tauri::State;
12
13use super::storage::DatabaseWrapper;
14use crate::storage::db_service::{DatabaseService, Query, QueryParam};
15
16#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct SyncQueueItem {
20 pub id: i64,
21 pub user_id: String,
22 pub operation: String,
23 pub item_id: Option<String>,
24 pub payload: Option<String>,
25 pub status: String,
26 pub retry_count: i32,
27 pub created_at: Option<String>,
28 pub error_message: Option<String>,
29 pub item_name: Option<String>,
35}
36
37#[tauri::command]
39#[specta::specta]
40pub async fn sync_queue_mutation(
41 db: State<'_, DatabaseWrapper>,
42 user_id: String,
43 operation: String,
44 item_id: Option<String>,
45 payload: Option<String>,
46) -> Result<i64, String> {
47 let db_service = {
48 let database = db.0.lock().map_err(|e| e.to_string())?;
49 Arc::new(database.service())
50 };
51
52 let query = Query::with_params(
53 "INSERT INTO sync_queue (user_id, operation, item_id, payload, status, created_at)
54 VALUES (?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP)",
55 vec![
56 QueryParam::String(user_id),
57 QueryParam::String(operation),
58 item_id.map(QueryParam::String).unwrap_or(QueryParam::Null),
59 payload.map(QueryParam::String).unwrap_or(QueryParam::Null),
60 ],
61 );
62
63 db_service.execute(query).await.map_err(|e| e.to_string())?;
64 let id = db_service
65 .last_insert_rowid()
66 .await
67 .map_err(|e| e.to_string())?;
68
69 Ok(id)
70}
71
72#[tauri::command]
74#[specta::specta]
75pub async fn sync_get_pending(
76 db: State<'_, DatabaseWrapper>,
77 user_id: String,
78 limit: Option<i32>,
79) -> Result<Vec<SyncQueueItem>, String> {
80 let db_service = {
81 let database = db.0.lock().map_err(|e| e.to_string())?;
82 Arc::new(database.service())
83 };
84
85 const SELECT: &str = "SELECT q.id, q.user_id, q.operation, q.item_id, q.payload, q.status,
90 COALESCE(q.retry_count, 0), q.created_at, q.error_message, i.name
91 FROM sync_queue q
92 LEFT JOIN items i ON i.id = q.item_id
93 WHERE q.user_id = ? AND q.status IN ('pending', 'failed')
94 ORDER BY q.created_at ASC, q.id ASC";
95
96 let sql = match limit {
97 Some(l) => format!("{} LIMIT {}", SELECT, l),
98 None => SELECT.to_string(),
99 };
100
101 let query = Query::with_params(sql, vec![QueryParam::String(user_id)]);
102
103 db_service
104 .query_many(query, |row| {
105 Ok(SyncQueueItem {
106 id: row.get(0)?,
107 user_id: row.get(1)?,
108 operation: row.get(2)?,
109 item_id: row.get(3)?,
110 payload: row.get(4)?,
111 status: row.get(5)?,
112 retry_count: row.get(6)?,
113 created_at: row.get(7)?,
114 error_message: row.get(8)?,
115 item_name: row.get(9)?,
116 })
117 })
118 .await
119 .map_err(|e| e.to_string())
120}
121
122#[tauri::command]
124#[specta::specta]
125pub async fn sync_mark_processing(db: State<'_, DatabaseWrapper>, id: i64) -> Result<(), String> {
126 let db_service = {
127 let database = db.0.lock().map_err(|e| e.to_string())?;
128 Arc::new(database.service())
129 };
130
131 let query = Query::with_params(
132 "UPDATE sync_queue SET status = 'processing' WHERE id = ?",
133 vec![QueryParam::Int64(id)],
134 );
135
136 db_service.execute(query).await.map_err(|e| e.to_string())?;
137 Ok(())
138}
139
140#[tauri::command]
142#[specta::specta]
143pub async fn sync_mark_completed(db: State<'_, DatabaseWrapper>, id: i64) -> Result<(), String> {
144 let db_service = {
145 let database = db.0.lock().map_err(|e| e.to_string())?;
146 Arc::new(database.service())
147 };
148
149 let query = Query::with_params(
150 "UPDATE sync_queue SET status = 'completed', processed_at = CURRENT_TIMESTAMP WHERE id = ?",
151 vec![QueryParam::Int64(id)],
152 );
153
154 db_service.execute(query).await.map_err(|e| e.to_string())?;
155 Ok(())
156}
157
158#[tauri::command]
160#[specta::specta]
161pub async fn sync_mark_failed(
162 db: State<'_, DatabaseWrapper>,
163 id: i64,
164 error: String,
165) -> Result<(), String> {
166 let db_service = {
167 let database = db.0.lock().map_err(|e| e.to_string())?;
168 Arc::new(database.service())
169 };
170
171 let query = Query::with_params(
172 "UPDATE sync_queue
173 SET status = 'failed',
174 retry_count = retry_count + 1,
175 error_message = ?,
176 processed_at = CURRENT_TIMESTAMP
177 WHERE id = ?",
178 vec![QueryParam::String(error), QueryParam::Int64(id)],
179 );
180
181 db_service.execute(query).await.map_err(|e| e.to_string())?;
182 Ok(())
183}
184
185#[tauri::command]
187#[specta::specta]
188pub async fn sync_get_pending_count(
189 db: State<'_, DatabaseWrapper>,
190 user_id: String,
191) -> Result<i32, String> {
192 let db_service = {
193 let database = db.0.lock().map_err(|e| e.to_string())?;
194 Arc::new(database.service())
195 };
196
197 let query = Query::with_params(
198 "SELECT COUNT(*) FROM sync_queue WHERE user_id = ? AND status IN ('pending', 'failed')",
199 vec![QueryParam::String(user_id)],
200 );
201
202 db_service
203 .query_one(query, |row| row.get(0))
204 .await
205 .map_err(|e| e.to_string())
206}
207
208#[tauri::command]
210#[specta::specta]
211pub async fn sync_cleanup_completed(
212 db: State<'_, DatabaseWrapper>,
213 days_old: i32,
214) -> Result<i32, String> {
215 let db_service = {
216 let database = db.0.lock().map_err(|e| e.to_string())?;
217 Arc::new(database.service())
218 };
219
220 let query = Query::with_params(
221 "DELETE FROM sync_queue
222 WHERE status = 'completed'
223 AND processed_at < datetime('now', ?)",
224 vec![QueryParam::String(format!("-{} days", days_old))],
225 );
226
227 let deleted = db_service.execute(query).await.map_err(|e| e.to_string())?;
228 Ok(deleted as i32)
229}
230
231#[tauri::command]
233#[specta::specta]
234pub async fn sync_clear_user(
235 db: State<'_, DatabaseWrapper>,
236 user_id: String,
237) -> Result<(), String> {
238 let db_service = {
239 let database = db.0.lock().map_err(|e| e.to_string())?;
240 Arc::new(database.service())
241 };
242
243 let query = Query::with_params(
244 "DELETE FROM sync_queue WHERE user_id = ?",
245 vec![QueryParam::String(user_id)],
246 );
247
248 db_service.execute(query).await.map_err(|e| e.to_string())?;
249 Ok(())
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_sync_queue_item_serialization() {
258 let item = SyncQueueItem {
259 id: 1,
260 user_id: "user-123".to_string(),
261 operation: "favorite".to_string(),
262 item_id: Some("item-456".to_string()),
263 payload: Some(r#"{"isFavorite": true}"#.to_string()),
264 status: "pending".to_string(),
265 retry_count: 0,
266 created_at: Some("2024-02-14T08:00:00Z".to_string()),
267 error_message: None,
268 item_name: None,
269 };
270
271 let json = serde_json::to_string(&item);
273 assert!(json.is_ok());
274
275 let serialized = json.unwrap();
276 assert!(serialized.contains("user-123"));
277 assert!(serialized.contains("favorite"));
278 assert!(serialized.contains("pending"));
279 }
280
281 #[test]
282 fn test_sync_queue_item_with_error() {
283 let item = SyncQueueItem {
284 id: 2,
285 user_id: "user-789".to_string(),
286 operation: "update_progress".to_string(),
287 item_id: Some("item-999".to_string()),
288 payload: None,
289 status: "failed".to_string(),
290 retry_count: 3,
291 created_at: Some("2024-02-14T07:00:00Z".to_string()),
292 error_message: Some("Connection timeout".to_string()),
293 item_name: None,
294 };
295
296 let json = serde_json::to_string(&item).unwrap();
297 assert!(json.contains("failed"));
298 assert!(json.contains("Connection timeout"));
299 assert!(json.contains("3")); }
301
302 #[test]
303 fn test_sync_queue_item_without_optional_fields() {
304 let item = SyncQueueItem {
305 id: 3,
306 user_id: "user-000".to_string(),
307 operation: "clear_progress".to_string(),
308 item_id: None,
309 payload: None,
310 status: "completed".to_string(),
311 retry_count: 0,
312 created_at: None,
313 error_message: None,
314 item_name: None,
315 };
316
317 let json = serde_json::to_string(&item).unwrap();
318 assert!(json.contains("completed"));
319 assert!(json.contains("null") || json.contains("\"itemId\":null"));
320 }
321
322 #[test]
323 fn test_sync_status_values() {
324 let valid_statuses = vec!["pending", "processing", "completed", "failed"];
326
327 for status in valid_statuses {
328 let item = SyncQueueItem {
329 id: 1,
330 user_id: "test".to_string(),
331 operation: "test".to_string(),
332 item_id: None,
333 payload: None,
334 status: status.to_string(),
335 retry_count: 0,
336 created_at: None,
337 error_message: None,
338 item_name: None,
339 };
340
341 let json = serde_json::to_string(&item).unwrap();
342 assert!(json.contains(status));
343 }
344 }
345
346 #[test]
347 fn test_query_param_generation() {
348 let user_id = "user-123".to_string();
350 let operation = "favorite".to_string();
351
352 let params: Vec<QueryParam> = vec![
353 QueryParam::String(user_id.clone()),
354 QueryParam::String(operation.clone()),
355 QueryParam::Null,
356 QueryParam::Null,
357 ];
358
359 assert_eq!(params.len(), 4);
360 assert!(matches!(params[0], QueryParam::String(_)));
361 assert!(matches!(params[1], QueryParam::String(_)));
362 assert!(matches!(params[2], QueryParam::Null));
363 assert!(matches!(params[3], QueryParam::Null));
364 }
365
366 #[test]
367 fn test_retry_count_increment() {
368 let mut item = SyncQueueItem {
370 id: 1,
371 user_id: "user-123".to_string(),
372 operation: "favorite".to_string(),
373 item_id: None,
374 payload: None,
375 status: "pending".to_string(),
376 retry_count: 0,
377 created_at: None,
378 error_message: None,
379 item_name: None,
380 };
381
382 for i in 1..=5 {
384 item.retry_count = i;
385 item.status = if i < 3 { "pending" } else { "failed" }.to_string();
386
387 assert!(item.retry_count == i);
388 }
389 }
390}