scim_server/storage/
sqlite.rs

1//! SQLite-based storage implementation for SCIM resources.
2//!
3//! This module provides persistent storage using SQLite database with the same interface
4//! as in-memory storage. Resources are stored as key-value pairs where the key represents
5//! the hierarchical tenant/resource_type/resource_id structure and the value contains
6//! the JSON resource data.
7//!
8//! # Database Schema
9//!
10//! The storage uses a simple table structure:
11//! - `tenant_id`: Text field for tenant isolation
12//! - `resource_type`: Text field for resource type (User, Group, etc.)
13//! - `resource_id`: Text field for the resource identifier
14//! - `data`: Text field containing JSON resource data
15//! - Primary key: (tenant_id, resource_type, resource_id)
16//!
17//! # Usage
18//!
19//! ```rust
20//! use scim_server::storage::{SqliteStorage, StorageProvider, StorageKey};
21//! use serde_json::json;
22//!
23//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
24//! // Default database at scim_data/scim_server.db (creates directory if needed)
25//! let storage = SqliteStorage::new().await?;
26//!
27//! // Or custom path
28//! let storage = SqliteStorage::new_with_path("custom/path/data.db").await?;
29//!
30//! // Or in-memory for testing
31//! let storage = SqliteStorage::new_in_memory().await?;
32//!
33//! let key = StorageKey::new("tenant1", "User", "123");
34//! let user_data = json!({
35//!     "id": "123",
36//!     "userName": "john.doe"
37//! });
38//!
39//! let stored = storage.put(key.clone(), user_data).await?;
40//! let retrieved = storage.get(key).await?;
41//!
42//! // Check statistics
43//! let stats = storage.stats().await?;
44//! println!("Total resources: {}", stats.total_resources);
45//! # Ok(())
46//! # }
47//! ```
48//!
49//! # Database Creation Behavior
50//!
51//! SQLiteStorage provides explicit control over database file creation:
52//!
53//! - **`new()`**: Creates database at `scim_data/scim_server.db`
54//! - **`new_with_path(path)`**: Creates database at custom path
55//! - **`new_in_memory()`**: Creates temporary in-memory database
56//!
57//! If the database file doesn't exist, it will be created along with any
58//! necessary parent directories. If it exists, it will be opened for read-write access.
59//!
60//! # Examples
61//!
62//! ```rust
63//! use scim_server::storage::{SqliteStorage, StorageProvider, StorageKey};
64//! use serde_json::json;
65//!
66//! # async fn examples() -> Result<(), Box<dyn std::error::Error>> {
67//! // Production usage - creates scim_data/scim_server.db
68//! let storage = SqliteStorage::new().await?;
69//!
70//! // Store a user
71//! let key = StorageKey::new("company1", "User", "user123");
72//! let user = json!({
73//!     "id": "user123",
74//!     "userName": "john.doe",
75//!     "displayName": "John Doe",
76//!     "emails": [{"value": "john@company1.com", "primary": true}]
77//! });
78//!
79//! storage.put(key.clone(), user.clone()).await?;
80//!
81//! // Retrieve the user
82//! let retrieved = storage.get(key).await?;
83//! assert_eq!(retrieved, Some(user));
84//!
85//! // Get statistics
86//! let stats = storage.stats().await?;
87//! println!("Storage contains {} resources across {} tenants",
88//!          stats.total_resources, stats.tenant_count);
89//!
90//! // Search for users
91//! let found = storage.find_by_attribute(
92//!     StorageKey::prefix("company1", "User"),
93//!     "userName",
94//!     "john.doe"
95//! ).await?;
96//! println!("Found {} users matching criteria", found.len());
97//! # Ok(())
98//! # }
99//! ```
100
101use crate::storage::{StorageError, StorageKey, StoragePrefix, StorageProvider, StorageStats};
102use serde_json::Value;
103use sqlx::{Row, SqlitePool};
104
105/// SQLite-based storage provider for SCIM resources.
106///
107/// Provides persistent storage with the same interface as InMemoryStorage.
108/// Uses a simple key-value table structure for efficient storage and retrieval.
109pub struct SqliteStorage {
110    pool: SqlitePool,
111}
112
113impl SqliteStorage {
114    /// Create a new SQLite storage instance with the default database file path.
115    ///
116    /// Creates or opens the database at `scim_data/scim_server.db`. The database file
117    /// will be created if it doesn't exist, along with the `scim_data` directory.
118    ///
119    /// # Returns
120    /// A new SqliteStorage instance with initialized database schema.
121    pub async fn new() -> Result<Self, StorageError> {
122        Self::new_with_path("scim_data/scim_server.db").await
123    }
124
125    /// Create a new SQLite storage instance with a custom database file path.
126    ///
127    /// # Arguments
128    /// * `database_path` - Path to the SQLite database file
129    ///
130    /// # Returns
131    /// A new SqliteStorage instance with initialized database schema.
132    ///
133    /// # Behavior
134    /// - Creates the database file if it doesn't exist
135    /// - Creates parent directories if they don't exist
136    /// - Opens existing database for read-write access
137    pub async fn new_with_path(database_path: &str) -> Result<Self, StorageError> {
138        use sqlx::sqlite::SqliteConnectOptions;
139        use std::path::Path;
140
141        // Create parent directory if it doesn't exist
142        if let Some(parent) = Path::new(database_path).parent() {
143            if !parent.exists() {
144                std::fs::create_dir_all(parent).map_err(|e| {
145                    StorageError::configuration(format!(
146                        "Failed to create directory {}: {}",
147                        parent.display(),
148                        e
149                    ))
150                })?;
151            }
152        }
153
154        // Configure SQLite connection with explicit creation behavior
155        let options = SqliteConnectOptions::new()
156            .filename(database_path)
157            .create_if_missing(true)
158            .foreign_keys(true);
159
160        let pool = SqlitePool::connect_with(options).await.map_err(|e| {
161            StorageError::configuration(format!(
162                "Failed to connect to SQLite database at {}: {}",
163                database_path, e
164            ))
165        })?;
166
167        let storage = Self { pool };
168        storage.initialize_schema().await?;
169        Ok(storage)
170    }
171
172    /// Create a new in-memory SQLite storage instance for testing.
173    ///
174    /// # Returns
175    /// A new SqliteStorage instance with initialized database schema.
176    pub async fn new_in_memory() -> Result<Self, StorageError> {
177        let pool = SqlitePool::connect(":memory:").await.map_err(|e| {
178            StorageError::configuration(format!("Failed to create in-memory SQLite: {}", e))
179        })?;
180
181        let storage = Self { pool };
182        storage.initialize_schema().await?;
183        Ok(storage)
184    }
185
186    /// Initialize the database schema if it doesn't exist.
187    async fn initialize_schema(&self) -> Result<(), StorageError> {
188        sqlx::query(
189            r#"
190            CREATE TABLE IF NOT EXISTS scim_resources (
191                tenant_id TEXT NOT NULL,
192                resource_type TEXT NOT NULL,
193                resource_id TEXT NOT NULL,
194                data TEXT NOT NULL,
195                PRIMARY KEY (tenant_id, resource_type, resource_id)
196            )
197            "#,
198        )
199        .execute(&self.pool)
200        .await
201        .map_err(|e| StorageError::internal(format!("Failed to create schema: {}", e)))?;
202
203        Ok(())
204    }
205
206    /// Extract a nested attribute value from JSON data using dot notation.
207    fn extract_attribute_value(data: &Value, attribute_path: &str) -> Option<String> {
208        let parts: Vec<&str> = attribute_path.split('.').collect();
209        let mut current = data;
210
211        for part in parts {
212            if let Ok(index) = part.parse::<usize>() {
213                // Array index
214                current = current.get(index)?;
215            } else {
216                // Object key
217                current = current.get(part)?;
218            }
219        }
220
221        // Convert the final value to string for comparison
222        match current {
223            Value::String(s) => Some(s.clone()),
224            Value::Number(n) => Some(n.to_string()),
225            Value::Bool(b) => Some(b.to_string()),
226            _ => current.as_str().map(|s| s.to_string()),
227        }
228    }
229}
230
231impl StorageProvider for SqliteStorage {
232    type Error = StorageError;
233
234    async fn put(&self, key: StorageKey, data: Value) -> Result<Value, Self::Error> {
235        let data_str = serde_json::to_string(&data)
236            .map_err(|e| StorageError::serialization(format!("Failed to serialize data: {}", e)))?;
237
238        sqlx::query(
239            "INSERT OR REPLACE INTO scim_resources (tenant_id, resource_type, resource_id, data) VALUES (?, ?, ?, ?)"
240        )
241        .bind(key.tenant_id())
242        .bind(key.resource_type())
243        .bind(key.resource_id())
244        .bind(&data_str)
245        .execute(&self.pool)
246        .await
247        .map_err(|e| StorageError::internal(format!("Failed to store resource: {}", e)))?;
248
249        Ok(data)
250    }
251
252    async fn get(&self, key: StorageKey) -> Result<Option<Value>, Self::Error> {
253        let row = sqlx::query(
254            "SELECT data FROM scim_resources WHERE tenant_id = ? AND resource_type = ? AND resource_id = ?"
255        )
256        .bind(key.tenant_id())
257        .bind(key.resource_type())
258        .bind(key.resource_id())
259        .fetch_optional(&self.pool)
260        .await
261        .map_err(|e| StorageError::internal(format!("Failed to fetch resource: {}", e)))?;
262
263        match row {
264            Some(row) => {
265                let data_str: String = row.get("data");
266                let value = serde_json::from_str(&data_str).map_err(|e| {
267                    StorageError::serialization(format!("Failed to deserialize data: {}", e))
268                })?;
269                Ok(Some(value))
270            }
271            None => Ok(None),
272        }
273    }
274
275    async fn delete(&self, key: StorageKey) -> Result<bool, Self::Error> {
276        let result = sqlx::query(
277            "DELETE FROM scim_resources WHERE tenant_id = ? AND resource_type = ? AND resource_id = ?"
278        )
279        .bind(key.tenant_id())
280        .bind(key.resource_type())
281        .bind(key.resource_id())
282        .execute(&self.pool)
283        .await
284        .map_err(|e| StorageError::internal(format!("Failed to delete resource: {}", e)))?;
285
286        Ok(result.rows_affected() > 0)
287    }
288
289    async fn list(
290        &self,
291        prefix: StoragePrefix,
292        offset: usize,
293        limit: usize,
294    ) -> Result<Vec<(StorageKey, Value)>, Self::Error> {
295        let rows = sqlx::query(
296            "SELECT resource_id, data FROM scim_resources
297             WHERE tenant_id = ? AND resource_type = ?
298             ORDER BY resource_id
299             LIMIT ? OFFSET ?",
300        )
301        .bind(prefix.tenant_id())
302        .bind(prefix.resource_type())
303        .bind(limit as i64)
304        .bind(offset as i64)
305        .fetch_all(&self.pool)
306        .await
307        .map_err(|e| StorageError::internal(format!("Failed to list resources: {}", e)))?;
308
309        let mut results = Vec::new();
310        for row in rows {
311            let resource_id: String = row.get("resource_id");
312            let data_str: String = row.get("data");
313            let data: Value = serde_json::from_str(&data_str).map_err(|e| {
314                StorageError::serialization(format!("Failed to deserialize data: {}", e))
315            })?;
316
317            let key = StorageKey::new(prefix.tenant_id(), prefix.resource_type(), resource_id);
318            results.push((key, data));
319        }
320
321        Ok(results)
322    }
323
324    async fn find_by_attribute(
325        &self,
326        prefix: StoragePrefix,
327        attribute: &str,
328        value: &str,
329    ) -> Result<Vec<(StorageKey, Value)>, Self::Error> {
330        let rows = sqlx::query(
331            "SELECT resource_id, data FROM scim_resources WHERE tenant_id = ? AND resource_type = ?"
332        )
333        .bind(prefix.tenant_id())
334        .bind(prefix.resource_type())
335        .fetch_all(&self.pool)
336        .await
337        .map_err(|e| StorageError::internal(format!("Failed to search resources: {}", e)))?;
338
339        let mut results = Vec::new();
340        for row in rows {
341            let resource_id: String = row.get("resource_id");
342            let data_str: String = row.get("data");
343            let data: Value = serde_json::from_str(&data_str).map_err(|e| {
344                StorageError::serialization(format!("Failed to deserialize data: {}", e))
345            })?;
346
347            if let Some(attr_value) = Self::extract_attribute_value(&data, attribute) {
348                if attr_value == value {
349                    let key =
350                        StorageKey::new(prefix.tenant_id(), prefix.resource_type(), resource_id);
351                    results.push((key, data));
352                }
353            }
354        }
355
356        Ok(results)
357    }
358
359    async fn exists(&self, key: StorageKey) -> Result<bool, Self::Error> {
360        let row = sqlx::query(
361            "SELECT 1 FROM scim_resources WHERE tenant_id = ? AND resource_type = ? AND resource_id = ?"
362        )
363        .bind(key.tenant_id())
364        .bind(key.resource_type())
365        .bind(key.resource_id())
366        .fetch_optional(&self.pool)
367        .await
368        .map_err(|e| StorageError::internal(format!("Failed to check resource existence: {}", e)))?;
369
370        Ok(row.is_some())
371    }
372
373    async fn count(&self, prefix: StoragePrefix) -> Result<usize, Self::Error> {
374        let row = sqlx::query(
375            "SELECT COUNT(*) as count FROM scim_resources WHERE tenant_id = ? AND resource_type = ?"
376        )
377        .bind(prefix.tenant_id())
378        .bind(prefix.resource_type())
379        .fetch_one(&self.pool)
380        .await
381        .map_err(|e| StorageError::internal(format!("Failed to count resources: {}", e)))?;
382
383        let count: i64 = row.get("count");
384        Ok(count as usize)
385    }
386
387    async fn list_tenants(&self) -> Result<Vec<String>, Self::Error> {
388        let rows = sqlx::query("SELECT DISTINCT tenant_id FROM scim_resources ORDER BY tenant_id")
389            .fetch_all(&self.pool)
390            .await
391            .map_err(|e| StorageError::internal(format!("Failed to list tenants: {}", e)))?;
392
393        let tenants = rows.into_iter().map(|row| row.get("tenant_id")).collect();
394        Ok(tenants)
395    }
396
397    async fn list_resource_types(&self, tenant_id: &str) -> Result<Vec<String>, Self::Error> {
398        let rows = sqlx::query(
399            "SELECT DISTINCT resource_type FROM scim_resources WHERE tenant_id = ? ORDER BY resource_type"
400        )
401        .bind(tenant_id)
402        .fetch_all(&self.pool)
403        .await
404        .map_err(|e| StorageError::internal(format!("Failed to list resource types: {}", e)))?;
405
406        let types = rows
407            .into_iter()
408            .map(|row| row.get("resource_type"))
409            .collect();
410        Ok(types)
411    }
412
413    async fn list_all_resource_types(&self) -> Result<Vec<String>, Self::Error> {
414        let rows =
415            sqlx::query("SELECT DISTINCT resource_type FROM scim_resources ORDER BY resource_type")
416                .fetch_all(&self.pool)
417                .await
418                .map_err(|e| {
419                    StorageError::internal(format!("Failed to list all resource types: {}", e))
420                })?;
421
422        let types = rows
423            .into_iter()
424            .map(|row| row.get("resource_type"))
425            .collect();
426        Ok(types)
427    }
428
429    async fn clear(&self) -> Result<(), Self::Error> {
430        sqlx::query("DELETE FROM scim_resources")
431            .execute(&self.pool)
432            .await
433            .map_err(|e| StorageError::internal(format!("Failed to clear storage: {}", e)))?;
434
435        Ok(())
436    }
437
438    async fn stats(&self) -> Result<StorageStats, Self::Error> {
439        let tenant_count_row =
440            sqlx::query("SELECT COUNT(DISTINCT tenant_id) as count FROM scim_resources")
441                .fetch_one(&self.pool)
442                .await
443                .map_err(|e| StorageError::internal(format!("Failed to count tenants: {}", e)))?;
444        let tenant_count: i64 = tenant_count_row.get("count");
445
446        let resource_type_count_row = sqlx::query(
447            "SELECT COUNT(DISTINCT tenant_id || '/' || resource_type) as count FROM scim_resources",
448        )
449        .fetch_one(&self.pool)
450        .await
451        .map_err(|e| StorageError::internal(format!("Failed to count resource types: {}", e)))?;
452        let resource_type_count: i64 = resource_type_count_row.get("count");
453
454        let total_resources_row = sqlx::query("SELECT COUNT(*) as count FROM scim_resources")
455            .fetch_one(&self.pool)
456            .await
457            .map_err(|e| {
458                StorageError::internal(format!("Failed to count total resources: {}", e))
459            })?;
460        let total_resources: i64 = total_resources_row.get("count");
461
462        Ok(StorageStats {
463            tenant_count: tenant_count as usize,
464            resource_type_count: resource_type_count as usize,
465            total_resources: total_resources as usize,
466        })
467    }
468}