Skip to main content

rullst_orm/
lib.rs

1#[cfg(not(any(
2    feature = "strict-postgres",
3    feature = "strict-mysql",
4    feature = "strict-sqlite"
5)))]
6pub use sqlx::AnyPool as RullstPool;
7
8#[cfg(not(any(
9    feature = "strict-postgres",
10    feature = "strict-mysql",
11    feature = "strict-sqlite"
12)))]
13pub use sqlx::any::AnyPoolOptions as RullstPoolOptions;
14
15#[cfg(feature = "strict-postgres")]
16pub use sqlx::PgPool as RullstPool;
17
18#[cfg(feature = "strict-postgres")]
19pub use sqlx::postgres::PgPoolOptions as RullstPoolOptions;
20
21#[cfg(all(feature = "strict-mysql", not(feature = "strict-postgres")))]
22pub use sqlx::MySqlPool as RullstPool;
23
24#[cfg(all(feature = "strict-mysql", not(feature = "strict-postgres")))]
25pub use sqlx::mysql::MySqlPoolOptions as RullstPoolOptions;
26
27#[cfg(all(
28    feature = "strict-sqlite",
29    not(feature = "strict-postgres"),
30    not(feature = "strict-mysql")
31))]
32pub use sqlx::SqlitePool as RullstPool;
33
34#[cfg(all(
35    feature = "strict-sqlite",
36    not(feature = "strict-postgres"),
37    not(feature = "strict-mysql")
38))]
39pub use sqlx::sqlite::SqlitePoolOptions as RullstPoolOptions;
40
41#[cfg(not(any(
42    feature = "strict-postgres",
43    feature = "strict-mysql",
44    feature = "strict-sqlite"
45)))]
46use sqlx::any::install_default_drivers;
47
48use std::sync::OnceLock;
49use std::sync::atomic::{AtomicUsize, Ordering};
50
51// Hide underlying libraries for macro usage while keeping the public API clean
52#[doc(hidden)]
53pub use futures as _futures;
54#[doc(hidden)]
55pub use serde as _serde;
56#[doc(hidden)]
57pub use serde_json as _serde_json;
58#[doc(hidden)]
59pub use sqlx as _sqlx;
60
61#[cfg(feature = "redis")]
62#[doc(hidden)]
63pub use redis as _redis;
64pub mod admin;
65pub mod audit;
66pub mod collection;
67pub mod database;
68pub mod db;
69pub mod error;
70pub mod resource;
71pub mod schema;
72pub mod scout;
73pub mod tenant;
74pub mod types;
75
76// Export the custom Error enum to the root
77pub use error::RullstError as Error;
78
79// Re-exports
80pub use _sqlx::FromRow;
81pub use admin::dashboard_html;
82pub use collection::RullstCollection;
83pub use database::RullstDatabase;
84pub use resource::{ApiResource, JsonResource, ResourceCollection};
85pub use rullst_orm_macros::Orm;
86pub use scout::{SearchEngine, get_search_engine, set_search_engine};
87pub use tenant::{get_tenant_id, with_tenant};
88pub use types::Json;
89
90// Re-export async_trait so the macro can use it implicitly
91pub use async_trait::async_trait;
92
93// Re-export sqlx and FromRow for database mapping
94pub use schema::{JoinClause, SubqueryBuilder};
95
96/// The global connection pool
97static DB_POOL: OnceLock<RullstPool> = OnceLock::new();
98
99/// The driver identifier (postgres, mysql, sqlite) to help macro syntax formatting
100static DB_DRIVER: OnceLock<String> = OnceLock::new();
101
102/// The replica connection pools for read operations
103static REPLICA_POOLS: OnceLock<Vec<RullstPool>> = OnceLock::new();
104
105/// Atomic index for replica round-robin selection
106static REPLICA_INDEX: AtomicUsize = AtomicUsize::new(0);
107
108#[cfg(feature = "redis")]
109static REDIS_CLIENT: OnceLock<_redis::Client> = OnceLock::new();
110
111#[cfg(feature = "redis")]
112static REDIS_MANAGER: OnceLock<_redis::aio::ConnectionManager> = OnceLock::new();
113
114/// Enum dinâmico para encapsular qualquer tipo que possa ser associado ao banco de dados pelo Macro
115#[derive(Clone, Debug)]
116pub enum RullstValue {
117    String(String),
118    Int(i32),
119    Float(f64),
120    Bool(bool),
121}
122
123impl From<&str> for RullstValue {
124    fn from(s: &str) -> Self {
125        RullstValue::String(s.to_string())
126    }
127}
128impl From<String> for RullstValue {
129    fn from(s: String) -> Self {
130        RullstValue::String(s)
131    }
132}
133impl From<i32> for RullstValue {
134    fn from(i: i32) -> Self {
135        RullstValue::Int(i)
136    }
137}
138impl From<f64> for RullstValue {
139    fn from(f: f64) -> Self {
140        RullstValue::Float(f)
141    }
142}
143impl From<bool> for RullstValue {
144    fn from(b: bool) -> Self {
145        RullstValue::Bool(b)
146    }
147}
148
149impl TryFrom<RullstValue> for String {
150    type Error = &'static str;
151    fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
152        match val {
153            RullstValue::String(s) => Ok(s),
154            _ => Err("Not a string"),
155        }
156    }
157}
158impl TryFrom<RullstValue> for i32 {
159    type Error = &'static str;
160    fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
161        match val {
162            RullstValue::Int(i) => Ok(i),
163            _ => Err("Not an i32"),
164        }
165    }
166}
167impl TryFrom<RullstValue> for f64 {
168    type Error = &'static str;
169    fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
170        match val {
171            RullstValue::Float(f) => Ok(f),
172            _ => Err("Not an f64"),
173        }
174    }
175}
176impl TryFrom<RullstValue> for bool {
177    type Error = &'static str;
178    fn try_from(val: RullstValue) -> Result<Self, Self::Error> {
179        match val {
180            RullstValue::Bool(b) => Ok(b),
181            _ => Err("Not a bool"),
182        }
183    }
184}
185
186/// Orm configuration structure
187pub struct Orm;
188
189impl Orm {
190    /// Initialize the global database connection pool using an agnostic URI
191    pub async fn init(database_url: &str) -> Result<(), crate::Error> {
192        Self::validate_dsn(database_url);
193
194        #[cfg(not(any(
195            feature = "strict-postgres",
196            feature = "strict-mysql",
197            feature = "strict-sqlite"
198        )))]
199        install_default_drivers();
200
201        let pool = RullstPool::connect(database_url).await?;
202
203        if DB_POOL.set(pool).is_err() {
204            return Err(crate::Error::Internal(
205                "Orm has already been initialized".to_string(),
206            ));
207        }
208
209        let driver = if database_url.starts_with("postgres") {
210            "postgres"
211        } else if database_url.starts_with("mysql") {
212            "mysql"
213        } else {
214            "sqlite"
215        };
216
217        let _ = DB_DRIVER.set(driver.to_string());
218        let _ = REPLICA_POOLS.set(vec![]);
219
220        Ok(())
221    }
222
223    /// Initialize the global database connection pool with specific pool options
224    pub async fn init_with_options(
225        database_url: &str,
226        max_connections: u32,
227        acquire_timeout_secs: u64,
228    ) -> Result<(), crate::Error> {
229        Self::validate_dsn(database_url);
230
231        #[cfg(not(any(
232            feature = "strict-postgres",
233            feature = "strict-mysql",
234            feature = "strict-sqlite"
235        )))]
236        install_default_drivers();
237
238        let pool = RullstPoolOptions::new()
239            .max_connections(max_connections)
240            .acquire_timeout(std::time::Duration::from_secs(acquire_timeout_secs))
241            .connect(database_url)
242            .await?;
243
244        if DB_POOL.set(pool).is_err() {
245            return Err(crate::Error::Internal(
246                "Orm has already been initialized".to_string(),
247            ));
248        }
249
250        let driver = if database_url.starts_with("postgres") {
251            "postgres"
252        } else if database_url.starts_with("mysql") {
253            "mysql"
254        } else {
255            "sqlite"
256        };
257
258        let _ = DB_DRIVER.set(driver.to_string());
259        let _ = REPLICA_POOLS.set(vec![]);
260
261        Ok(())
262    }
263
264    fn validate_dsn(database_url: &str) {
265        if database_url.contains("sslmode=disable")
266            && !database_url.contains("localhost")
267            && !database_url.contains("127.0.0.1")
268        {
269            eprintln!(
270                "⚠️ [SECURITY WARNING] Rullst ORM: TLS/SSL disabled on external database connection! This is highly discouraged in production environments."
271            );
272        }
273    }
274
275    /// Initialize the global database connection pool and its read replicas
276    pub async fn init_with_replicas(
277        primary_url: &str,
278        replica_urls: Vec<&str>,
279    ) -> Result<(), crate::Error> {
280        #[cfg(not(any(
281            feature = "strict-postgres",
282            feature = "strict-mysql",
283            feature = "strict-sqlite"
284        )))]
285        install_default_drivers();
286
287        let pool = RullstPool::connect(primary_url).await?;
288
289        if DB_POOL.set(pool).is_err() {
290            return Err(crate::Error::Internal(
291                "Orm has already been initialized".to_string(),
292            ));
293        }
294
295        let driver = if primary_url.starts_with("postgres") {
296            "postgres"
297        } else if primary_url.starts_with("mysql") {
298            "mysql"
299        } else {
300            "sqlite"
301        };
302
303        let _ = DB_DRIVER.set(driver.to_string());
304
305        // Initialize all replica pools concurrently — each connect() is independent I/O.
306        let replica_futures: Vec<_> = replica_urls.into_iter().map(RullstPool::connect).collect();
307        let replicas = futures::future::try_join_all(replica_futures).await?;
308        let _ = REPLICA_POOLS.set(replicas);
309
310        Ok(())
311    }
312
313    /// Retrieve the global database connection pool (strictly for writes)
314    pub fn pool() -> &'static RullstPool {
315        DB_POOL
316            .get()
317            .expect("Orm must be initialized before querying")
318    }
319
320    /// Retrieve the connection pool for read operations.
321    /// Performs a round-robin load balancing over replicas if configured.
322    pub fn read_pool() -> &'static RullstPool {
323        if let Some(replicas) = REPLICA_POOLS.get()
324            && !replicas.is_empty()
325        {
326            let idx = REPLICA_INDEX.fetch_add(1, Ordering::Relaxed) % replicas.len();
327            return &replicas[idx];
328        }
329        Self::pool()
330    }
331
332    /// Retrieve the active driver string
333    pub fn driver() -> &'static str {
334        DB_DRIVER
335            .get()
336            .expect("Orm must be initialized before querying")
337            .as_str()
338    }
339
340    pub async fn begin_transaction() -> Result<crate::db::Transaction<'static>, crate::Error> {
341        let pool = Self::pool();
342        pool.begin().await.map_err(Into::into)
343    }
344
345    /// Run an array of seeders sequentially
346    pub async fn seed(seeders: Vec<Box<dyn Seeder>>) -> Result<(), crate::Error> {
347        for seeder in seeders {
348            seeder.run().await?;
349        }
350        Ok(())
351    }
352
353    /// Enable query logging to print all queries to the terminal
354    pub fn enable_query_log() {
355        crate::schema::enable_query_log();
356    }
357
358    /// Disable query logging
359    pub fn disable_query_log() {
360        crate::schema::disable_query_log();
361    }
362
363    /// Set a global maximum limit for all queries without an explicit limit override
364    pub fn set_max_query_limit(limit: usize) {
365        crate::schema::set_max_query_limit(limit);
366    }
367
368    /// Set a global maximum execution timeout for all queries
369    pub fn set_query_timeout(secs: u64) {
370        crate::schema::set_query_timeout(secs);
371    }
372
373    /// Initialize Redis connection and connection manager for caching and events
374    #[cfg(feature = "redis")]
375    pub async fn init_redis(redis_url: &str) -> Result<(), crate::Error> {
376        let client = _redis::Client::open(redis_url)?;
377        let manager = _redis::aio::ConnectionManager::new(client.clone()).await?;
378        let _ = REDIS_CLIENT.set(client);
379        let _ = REDIS_MANAGER.set(manager);
380        Ok(())
381    }
382
383    /// Get reference to the global Redis client
384    #[cfg(feature = "redis")]
385    pub fn redis_client() -> Result<&'static _redis::Client, crate::Error> {
386        REDIS_CLIENT.get().ok_or_else(|| {
387            crate::Error::Internal(
388                "Orm::init_redis() must be called before using cache features".to_string(),
389            )
390        })
391    }
392
393    /// Get clone of the thread-safe connection manager for async Redis queries
394    #[cfg(feature = "redis")]
395    pub fn redis_manager() -> Result<_redis::aio::ConnectionManager, crate::Error> {
396        REDIS_MANAGER.get().cloned().ok_or_else(|| {
397            crate::Error::Internal(
398                "Orm::init_redis() must be called before using cache features".to_string(),
399            )
400        })
401    }
402}
403
404/// A database seeder trait for populating tables
405#[async_trait]
406pub trait Seeder: Send + Sync {
407    async fn run(&self) -> Result<(), crate::Error>;
408}
409
410/// The core trait that all Orm models will implement via #[derive(Orm)]
411#[async_trait]
412pub trait RullstModel {
413    fn table_name() -> &'static str;
414}
415
416/// Represents a paginated result set
417#[derive(Debug, Clone)]
418pub struct PaginationResult<T> {
419    pub data: Vec<T>,
420    pub total: i64,
421    pub per_page: usize,
422    pub current_page: usize,
423    pub last_page: usize,
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[test]
431    fn test_pagination_result() {
432        let mut pr = PaginationResult {
433            data: vec![1, 2, 3],
434            total: 3,
435            per_page: 10,
436            current_page: 1,
437            last_page: 1,
438        };
439        assert_eq!(pr.data.len(), 3);
440        assert_eq!(pr.total, 3);
441        pr.data.push(4);
442        assert_eq!(pr.data.len(), 4);
443    }
444
445    #[test]
446    fn test_rullst_value_conversions() {
447        let v: RullstValue = "test".into();
448        assert!(matches!(v, RullstValue::String(_)));
449        let v_int: RullstValue = 100.into();
450        assert!(matches!(v_int, RullstValue::Int(100)));
451        let v_bool: RullstValue = false.into();
452        assert!(matches!(v_bool, RullstValue::Bool(false)));
453    }
454
455    #[test]
456    fn test_enable_query_log_wrapper() {
457        // Orm::enable/disable_query_log delegate to schema — verify the delegation works.
458        Orm::disable_query_log();
459        assert!(!crate::schema::is_query_log_enabled());
460        Orm::enable_query_log();
461        assert!(crate::schema::is_query_log_enabled());
462        Orm::disable_query_log();
463        assert!(!crate::schema::is_query_log_enabled());
464    }
465
466    #[test]
467    fn test_disable_query_log_wrapper() {
468        Orm::enable_query_log();
469        Orm::disable_query_log();
470        assert!(!crate::schema::is_query_log_enabled());
471    }
472
473    #[cfg(feature = "redis")]
474    #[test]
475    fn test_redis_client_uninitialized() {
476        let err = Orm::redis_client().unwrap_err();
477        assert!(matches!(err, crate::Error::Internal(_)));
478    }
479
480    #[cfg(feature = "redis")]
481    #[test]
482    fn test_redis_manager_uninitialized() {
483        let err = Orm::redis_manager().unwrap_err();
484        assert!(matches!(err, crate::Error::Internal(_)));
485    }
486}