Skip to main content

supabase_client_core/
client.rs

1use std::sync::Arc;
2
3#[cfg(feature = "direct-sql")]
4use sqlx::postgres::PgPoolOptions;
5#[cfg(feature = "direct-sql")]
6use sqlx::PgPool;
7
8use crate::config::SupabaseConfig;
9use crate::error::SupabaseResult;
10
11/// The main client for interacting with Supabase.
12///
13/// By default, uses the PostgREST REST API. Enable the `direct-sql` feature
14/// and call `with_database()` to also get a direct PostgreSQL connection pool.
15#[derive(Debug, Clone)]
16pub struct SupabaseClient {
17    config: Arc<SupabaseConfig>,
18    http: reqwest::Client,
19    #[cfg(feature = "direct-sql")]
20    pool: Option<Arc<PgPool>>,
21}
22
23impl SupabaseClient {
24    /// Create a new REST-only client (no database connection needed).
25    ///
26    /// This is the primary constructor. Queries go through PostgREST.
27    pub fn new(config: SupabaseConfig) -> SupabaseResult<Self> {
28        let http = reqwest::Client::new();
29        Ok(Self {
30            config: Arc::new(config),
31            http,
32            #[cfg(feature = "direct-sql")]
33            pool: None,
34        })
35    }
36
37    /// Create a client with a direct database connection pool.
38    ///
39    /// Requires the `direct-sql` feature and a `database_url` in config.
40    #[cfg(feature = "direct-sql")]
41    pub async fn with_database(config: SupabaseConfig) -> SupabaseResult<Self> {
42        let db_url = config
43            .database_url
44            .as_ref()
45            .ok_or_else(|| crate::error::SupabaseError::Config(
46                "database_url is required for direct-sql mode".into(),
47            ))?;
48
49        let pool = PgPoolOptions::new()
50            .max_connections(config.pool.max_connections)
51            .min_connections(config.pool.min_connections)
52            .acquire_timeout(config.pool.acquire_timeout)
53            .idle_timeout(config.pool.idle_timeout)
54            .max_lifetime(config.pool.max_lifetime)
55            .connect(db_url)
56            .await?;
57
58        let http = reqwest::Client::new();
59
60        Ok(Self {
61            config: Arc::new(config),
62            http,
63            pool: Some(Arc::new(pool)),
64        })
65    }
66
67    /// Create a client from an existing pool (direct-sql mode).
68    #[cfg(feature = "direct-sql")]
69    pub fn from_pool(pool: PgPool, config: SupabaseConfig) -> Self {
70        Self {
71            config: Arc::new(config),
72            http: reqwest::Client::new(),
73            pool: Some(Arc::new(pool)),
74        }
75    }
76
77    /// Get a reference to the HTTP client.
78    pub fn http(&self) -> &reqwest::Client {
79        &self.http
80    }
81
82    /// Get the Supabase project URL.
83    pub fn supabase_url(&self) -> &str {
84        &self.config.supabase_url
85    }
86
87    /// Get the Supabase API key.
88    pub fn api_key(&self) -> &str {
89        &self.config.supabase_key
90    }
91
92    /// Get the default schema.
93    pub fn schema(&self) -> &str {
94        &self.config.schema
95    }
96
97    /// Get the full config.
98    pub fn config(&self) -> &SupabaseConfig {
99        &self.config
100    }
101
102    /// Get a reference to the underlying connection pool (if available).
103    #[cfg(feature = "direct-sql")]
104    pub fn pool(&self) -> Option<&PgPool> {
105        self.pool.as_deref()
106    }
107
108    /// Get an Arc to the pool (for passing to builders).
109    #[cfg(feature = "direct-sql")]
110    pub fn pool_arc(&self) -> Option<Arc<PgPool>> {
111        self.pool.clone()
112    }
113
114    /// Check if direct-sql pool is available.
115    #[cfg(feature = "direct-sql")]
116    pub fn has_pool(&self) -> bool {
117        self.pool.is_some()
118    }
119
120    /// Close the connection pool gracefully (if available).
121    #[cfg(feature = "direct-sql")]
122    pub async fn close(&self) {
123        if let Some(pool) = &self.pool {
124            pool.close().await;
125        }
126    }
127
128    /// Check if the pool is closed (if available).
129    #[cfg(feature = "direct-sql")]
130    pub fn is_closed(&self) -> bool {
131        self.pool.as_ref().map_or(true, |p| p.is_closed())
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::config::SupabaseConfig;
139
140    fn test_config() -> SupabaseConfig {
141        SupabaseConfig::new("http://localhost:54321", "test-anon-key")
142    }
143
144    #[test]
145    fn test_new_succeeds_with_valid_config() {
146        let client = SupabaseClient::new(test_config());
147        assert!(client.is_ok());
148    }
149
150    #[test]
151    fn test_supabase_url_returns_correct_url() {
152        let client = SupabaseClient::new(test_config()).unwrap();
153        assert_eq!(client.supabase_url(), "http://localhost:54321");
154    }
155
156    #[test]
157    fn test_api_key_returns_correct_key() {
158        let client = SupabaseClient::new(test_config()).unwrap();
159        assert_eq!(client.api_key(), "test-anon-key");
160    }
161
162    #[test]
163    fn test_schema_returns_public_by_default() {
164        let client = SupabaseClient::new(test_config()).unwrap();
165        assert_eq!(client.schema(), "public");
166    }
167
168    #[test]
169    fn test_schema_returns_custom_schema() {
170        let config = SupabaseConfig::new("http://localhost:54321", "key").schema("custom");
171        let client = SupabaseClient::new(config).unwrap();
172        assert_eq!(client.schema(), "custom");
173    }
174
175    #[test]
176    fn test_http_returns_client_reference() {
177        let client = SupabaseClient::new(test_config()).unwrap();
178        // Verify we can get a reference to the HTTP client (non-null check)
179        let _http: &reqwest::Client = client.http();
180    }
181
182    #[test]
183    fn test_config_returns_config_reference() {
184        let client = SupabaseClient::new(test_config()).unwrap();
185        let config = client.config();
186        assert_eq!(config.supabase_url, "http://localhost:54321");
187        assert_eq!(config.supabase_key, "test-anon-key");
188        assert_eq!(config.schema, "public");
189    }
190
191    #[test]
192    fn test_client_is_clone() {
193        let client = SupabaseClient::new(test_config()).unwrap();
194        let cloned = client.clone();
195        assert_eq!(cloned.supabase_url(), client.supabase_url());
196        assert_eq!(cloned.api_key(), client.api_key());
197        assert_eq!(cloned.schema(), client.schema());
198    }
199}