Skip to main content

pleme_database/
repository.rs

1//! Repository pattern for data access
2
3use crate::Result;
4use async_trait::async_trait;
5use serde::{de::DeserializeOwned, Serialize};
6
7/// Repository trait for CRUD operations
8#[async_trait]
9pub trait Repository<T>: Send + Sync
10where
11    T: Serialize + DeserializeOwned + Send + Sync,
12{
13    /// Find entity by ID
14    async fn find_by_id(&self, id: &str) -> Result<Option<T>>;
15
16    /// Find all entities
17    async fn find_all(&self) -> Result<Vec<T>>;
18
19    /// Create new entity
20    async fn create(&self, entity: &T) -> Result<T>;
21
22    /// Update existing entity
23    async fn update(&self, id: &str, entity: &T) -> Result<T>;
24
25    /// Delete entity
26    async fn delete(&self, id: &str) -> Result<()>;
27}
28
29/// Soft delete trait for logical deletion
30///
31/// Implements the soft delete pattern where records are marked as deleted
32/// rather than physically removed from the database. This allows for:
33/// - Data recovery
34/// - Audit trails
35/// - Referential integrity maintenance
36#[async_trait]
37pub trait SoftDelete: Send + Sync {
38    /// Soft delete entity by marking it as deleted
39    async fn soft_delete(&self, id: &str) -> Result<()>;
40
41    /// Restore soft-deleted entity
42    async fn restore(&self, id: &str) -> Result<()>;
43
44    /// Check if entity is soft-deleted
45    async fn is_deleted(&self, id: &str) -> Result<bool>;
46
47    /// Find all entities including soft-deleted ones
48    async fn find_all_with_deleted(&self) -> Result<Vec<serde_json::Value>>;
49
50    /// Permanently delete soft-deleted entities (hard delete)
51    async fn purge_deleted(&self, older_than_days: u32) -> Result<u64>;
52}
53
54/// Product-scoped trait for multi-tenant data isolation
55///
56/// Implements product-level data isolation where all queries are automatically
57/// scoped to a specific product ID. This ensures data isolation between
58/// products (Lilitu, NovaSkyn, etc.) in the Pleme platform.
59#[async_trait]
60pub trait ProductScoped<T>: Send + Sync
61where
62    T: Serialize + DeserializeOwned + Send + Sync,
63{
64    /// Find entity by ID within product scope
65    async fn find_by_id_scoped(&self, product_id: &str, id: &str) -> Result<Option<T>>;
66
67    /// Find all entities for a specific product
68    async fn find_all_scoped(&self, product_id: &str) -> Result<Vec<T>>;
69
70    /// Create entity scoped to product
71    async fn create_scoped(&self, product_id: &str, entity: &T) -> Result<T>;
72
73    /// Update entity scoped to product
74    async fn update_scoped(&self, product_id: &str, id: &str, entity: &T) -> Result<T>;
75
76    /// Delete entity scoped to product
77    async fn delete_scoped(&self, product_id: &str, id: &str) -> Result<()>;
78
79    /// Count entities for a product
80    async fn count_scoped(&self, product_id: &str) -> Result<i64>;
81}
82
83/// Pagination parameters
84#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
85pub struct PaginationParams {
86    /// Page offset (zero-indexed)
87    pub offset: i64,
88    /// Number of items per page
89    pub limit: i64,
90    /// Optional sort field
91    pub sort_by: Option<String>,
92    /// Sort direction (asc/desc)
93    pub sort_desc: bool,
94}
95
96impl Default for PaginationParams {
97    fn default() -> Self {
98        Self {
99            offset: 0,
100            limit: 20,
101            sort_by: None,
102            sort_desc: false,
103        }
104    }
105}
106
107impl PaginationParams {
108    /// Create new pagination params
109    pub fn new(offset: i64, limit: i64) -> Self {
110        Self {
111            offset,
112            limit: limit.min(100), // Cap at 100 items
113            sort_by: None,
114            sort_desc: false,
115        }
116    }
117
118    /// Set sort field
119    pub fn with_sort(mut self, field: impl Into<String>, desc: bool) -> Self {
120        self.sort_by = Some(field.into());
121        self.sort_desc = desc;
122        self
123    }
124
125    /// Calculate SQL OFFSET value
126    pub fn sql_offset(&self) -> i64 {
127        self.offset
128    }
129
130    /// Calculate SQL LIMIT value
131    pub fn sql_limit(&self) -> i64 {
132        self.limit
133    }
134}
135
136/// Paginated response
137#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
138pub struct PaginatedResponse<T> {
139    pub items: Vec<T>,
140    pub total: i64,
141    pub offset: i64,
142    pub limit: i64,
143    pub has_more: bool,
144}
145
146impl<T> PaginatedResponse<T> {
147    pub fn new(items: Vec<T>, total: i64, params: &PaginationParams) -> Self {
148        let has_more = (params.offset + params.limit) < total;
149        Self {
150            items,
151            total,
152            offset: params.offset,
153            limit: params.limit,
154            has_more,
155        }
156    }
157}
158
159/// Paginated repository trait
160#[async_trait]
161pub trait PaginatedRepository<T>: Send + Sync
162where
163    T: Serialize + DeserializeOwned + Send + Sync,
164{
165    /// Find entities with pagination
166    async fn find_paginated(&self, params: &PaginationParams) -> Result<PaginatedResponse<T>>;
167
168    /// Find entities with pagination scoped to product
169    async fn find_paginated_scoped(
170        &self,
171        product_id: &str,
172        params: &PaginationParams,
173    ) -> Result<PaginatedResponse<T>>;
174}
175
176/// Base repository implementation
177pub struct BaseRepository<T> {
178    _phantom: std::marker::PhantomData<T>,
179}
180
181impl<T> BaseRepository<T> {
182    pub fn new() -> Self {
183        Self {
184            _phantom: std::marker::PhantomData,
185        }
186    }
187}
188
189impl<T> Default for BaseRepository<T> {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[derive(serde::Serialize, serde::Deserialize)]
200    struct TestEntity {
201        id: String,
202        name: String,
203    }
204
205    #[test]
206    fn test_base_repository() {
207        let _repo = BaseRepository::<TestEntity>::new();
208    }
209
210    #[test]
211    fn test_pagination_params() {
212        let params = PaginationParams::new(0, 50);
213        assert_eq!(params.offset, 0);
214        assert_eq!(params.limit, 50);
215        assert_eq!(params.sql_offset(), 0);
216        assert_eq!(params.sql_limit(), 50);
217
218        // Test limit cap
219        let params = PaginationParams::new(0, 200);
220        assert_eq!(params.limit, 100); // Capped at 100
221    }
222
223    #[test]
224    fn test_pagination_params_with_sort() {
225        let params = PaginationParams::new(20, 10)
226            .with_sort("created_at", true);
227
228        assert_eq!(params.offset, 20);
229        assert_eq!(params.limit, 10);
230        assert_eq!(params.sort_by, Some("created_at".to_string()));
231        assert!(params.sort_desc);
232    }
233
234    #[test]
235    fn test_pagination_params_defaults() {
236        let params = PaginationParams::default();
237        assert_eq!(params.offset, 0);
238        assert_eq!(params.limit, 20);
239        assert_eq!(params.sort_by, None);
240        assert!(!params.sort_desc);
241    }
242
243    #[test]
244    fn test_paginated_response() {
245        let items = vec![
246            TestEntity { id: "1".to_string(), name: "One".to_string() },
247            TestEntity { id: "2".to_string(), name: "Two".to_string() },
248        ];
249        let params = PaginationParams::new(0, 2);
250        let response = PaginatedResponse::new(items, 10, &params);
251
252        assert_eq!(response.items.len(), 2);
253        assert_eq!(response.total, 10);
254        assert_eq!(response.offset, 0);
255        assert_eq!(response.limit, 2);
256        assert!(response.has_more); // 0 + 2 < 10
257
258        // Test last page
259        let params = PaginationParams::new(8, 2);
260        let response: PaginatedResponse<TestEntity> = PaginatedResponse::new(vec![], 10, &params);
261        assert!(!response.has_more); // 8 + 2 >= 10
262    }
263}