kaccy_db/
generic_repository.rs

1//! Generic repository trait for standardized CRUD operations.
2//!
3//! This module provides:
4//! - Base trait for CRUD operations
5//! - Automatic caching integration
6//! - Generic pagination support
7
8use async_trait::async_trait;
9use serde::{de::DeserializeOwned, Serialize};
10use sqlx::PgPool;
11use std::sync::Arc;
12use uuid::Uuid;
13
14use crate::cache::RedisCache;
15use crate::error::Result;
16
17/// Pagination parameters
18#[derive(Debug, Clone)]
19pub struct Pagination {
20    /// Page number (0-indexed)
21    pub page: u64,
22    /// Number of items per page
23    pub page_size: u64,
24}
25
26impl Default for Pagination {
27    fn default() -> Self {
28        Self {
29            page: 0,
30            page_size: 20,
31        }
32    }
33}
34
35impl Pagination {
36    /// Create a new pagination
37    pub fn new(page: u64, page_size: u64) -> Self {
38        Self { page, page_size }
39    }
40
41    /// Get the LIMIT value
42    pub fn limit(&self) -> i64 {
43        self.page_size as i64
44    }
45
46    /// Get the OFFSET value
47    pub fn offset(&self) -> i64 {
48        (self.page * self.page_size) as i64
49    }
50}
51
52/// Paginated result
53#[derive(Debug, Clone, Serialize)]
54pub struct Page<T> {
55    /// Items in this page
56    pub items: Vec<T>,
57    /// Total number of items
58    pub total: u64,
59    /// Current page number
60    pub page: u64,
61    /// Page size
62    pub page_size: u64,
63    /// Total number of pages
64    pub total_pages: u64,
65}
66
67impl<T> Page<T> {
68    /// Create a new page
69    pub fn new(items: Vec<T>, total: u64, pagination: &Pagination) -> Self {
70        let total_pages = if pagination.page_size > 0 {
71            total.div_ceil(pagination.page_size)
72        } else {
73            0
74        };
75
76        Self {
77            items,
78            total,
79            page: pagination.page,
80            page_size: pagination.page_size,
81            total_pages,
82        }
83    }
84
85    /// Check if there is a next page
86    pub fn has_next(&self) -> bool {
87        self.page + 1 < self.total_pages
88    }
89
90    /// Check if there is a previous page
91    pub fn has_prev(&self) -> bool {
92        self.page > 0
93    }
94}
95
96/// Generic repository trait for CRUD operations
97#[async_trait]
98pub trait Repository<T>: Send + Sync
99where
100    T: Send + Sync + Clone + Serialize + DeserializeOwned,
101{
102    /// Get the entity type name for caching
103    fn entity_name(&self) -> &str;
104
105    /// Find an entity by ID
106    async fn find_by_id(&self, id: Uuid) -> Result<Option<T>>;
107
108    /// Find all entities with pagination
109    async fn find_all(&self, pagination: Pagination) -> Result<Page<T>>;
110
111    /// Create a new entity
112    async fn create(&self, entity: &T) -> Result<T>;
113
114    /// Update an existing entity
115    async fn update(&self, id: Uuid, entity: &T) -> Result<T>;
116
117    /// Delete an entity by ID
118    async fn delete(&self, id: Uuid) -> Result<bool>;
119
120    /// Count total entities
121    async fn count(&self) -> Result<u64>;
122
123    /// Check if an entity exists
124    async fn exists(&self, id: Uuid) -> Result<bool> {
125        Ok(self.find_by_id(id).await?.is_some())
126    }
127}
128
129/// Generic repository implementation with caching
130#[allow(dead_code)]
131pub struct CachedGenericRepository<T>
132where
133    T: Send + Sync + Clone + Serialize + DeserializeOwned,
134{
135    pool: Arc<PgPool>,
136    cache: Option<Arc<RedisCache>>,
137    entity_name: String,
138    _phantom: std::marker::PhantomData<T>,
139}
140
141impl<T> CachedGenericRepository<T>
142where
143    T: Send + Sync + Clone + Serialize + DeserializeOwned,
144{
145    /// Create a new cached generic repository
146    pub fn new(pool: Arc<PgPool>, cache: Option<Arc<RedisCache>>, entity_name: String) -> Self {
147        Self {
148            pool,
149            cache,
150            entity_name,
151            _phantom: std::marker::PhantomData,
152        }
153    }
154
155    /// Get cache key for an entity
156    #[allow(dead_code)]
157    fn cache_key(&self, id: Uuid) -> String {
158        format!("{}:{}", self.entity_name, id)
159    }
160
161    /// Get from cache
162    #[allow(dead_code)]
163    async fn get_from_cache(&self, id: Uuid) -> Result<Option<T>> {
164        if let Some(cache) = &self.cache {
165            cache.get(&self.cache_key(id)).await
166        } else {
167            Ok(None)
168        }
169    }
170
171    /// Set in cache
172    #[allow(dead_code)]
173    async fn set_in_cache(&self, id: Uuid, entity: &T, ttl_secs: u64) -> Result<()> {
174        if let Some(cache) = &self.cache {
175            cache.set(&self.cache_key(id), entity, ttl_secs).await?;
176        }
177        Ok(())
178    }
179
180    /// Invalidate cache
181    #[allow(dead_code)]
182    async fn invalidate_cache(&self, id: Uuid) -> Result<()> {
183        if let Some(cache) = &self.cache {
184            cache.delete(&self.cache_key(id)).await?;
185        }
186        Ok(())
187    }
188
189    /// Get pool reference
190    pub fn pool(&self) -> &PgPool {
191        &self.pool
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_pagination_default() {
201        let pagination = Pagination::default();
202        assert_eq!(pagination.page, 0);
203        assert_eq!(pagination.page_size, 20);
204        assert_eq!(pagination.limit(), 20);
205        assert_eq!(pagination.offset(), 0);
206    }
207
208    #[test]
209    fn test_pagination_offset() {
210        let pagination = Pagination::new(2, 10);
211        assert_eq!(pagination.limit(), 10);
212        assert_eq!(pagination.offset(), 20);
213    }
214
215    #[test]
216    fn test_page_creation() {
217        let items = vec![1, 2, 3];
218        let pagination = Pagination::new(0, 10);
219        let page = Page::new(items, 25, &pagination);
220
221        assert_eq!(page.items.len(), 3);
222        assert_eq!(page.total, 25);
223        assert_eq!(page.page, 0);
224        assert_eq!(page.page_size, 10);
225        assert_eq!(page.total_pages, 3);
226    }
227
228    #[test]
229    fn test_page_has_next() {
230        let items = vec![1, 2, 3];
231        let pagination = Pagination::new(0, 10);
232        let page = Page::new(items, 25, &pagination);
233
234        assert!(page.has_next());
235        assert!(!page.has_prev());
236    }
237
238    #[test]
239    fn test_page_has_prev() {
240        let items = vec![1, 2, 3];
241        let pagination = Pagination::new(1, 10);
242        let page = Page::new(items, 25, &pagination);
243
244        assert!(page.has_next());
245        assert!(page.has_prev());
246    }
247
248    #[test]
249    fn test_page_last_page() {
250        let items = vec![1, 2, 3];
251        let pagination = Pagination::new(2, 10);
252        let page = Page::new(items, 25, &pagination);
253
254        assert!(!page.has_next());
255        assert!(page.has_prev());
256    }
257}