supabase/
client.rs

1//! Main Supabase client
2
3use crate::{
4    error::{Error, Result},
5    types::{AuthConfig, DatabaseConfig, HttpConfig, StorageConfig, SupabaseConfig},
6};
7
8#[cfg(feature = "auth")]
9use crate::auth::Auth;
10
11#[cfg(feature = "database")]
12use crate::database::Database;
13
14#[cfg(feature = "storage")]
15use crate::storage::Storage;
16
17#[cfg(feature = "functions")]
18use crate::functions::Functions;
19
20#[cfg(feature = "realtime")]
21use crate::realtime::Realtime;
22use reqwest::{header::HeaderMap, Client as HttpClient};
23use std::{collections::HashMap, sync::Arc};
24
25#[cfg(not(target_arch = "wasm32"))]
26use std::time::Duration;
27use tracing::{debug, error, info};
28use url::Url;
29
30/// Main Supabase client for interacting with all services
31#[derive(Debug, Clone)]
32pub struct Client {
33    /// HTTP client for making requests
34    http_client: Arc<HttpClient>,
35    /// Client configuration
36    config: Arc<SupabaseConfig>,
37    /// Authentication module
38    #[cfg(feature = "auth")]
39    auth: Auth,
40    /// Database module
41    #[cfg(feature = "database")]
42    database: Database,
43    /// Storage module
44    #[cfg(feature = "storage")]
45    storage: Storage,
46
47    #[cfg(feature = "functions")]
48    functions: Functions,
49
50    /// Realtime module
51    #[cfg(feature = "realtime")]
52    realtime: Realtime,
53}
54
55impl Client {
56    /// Create a new Supabase client with URL and API key
57    ///
58    /// # Arguments
59    ///
60    /// * `url` - Your Supabase project URL (e.g., "https://your-project.supabase.co")
61    /// * `key` - Your Supabase API key (anon key for client-side operations)
62    ///
63    /// # Example
64    ///
65    /// ```rust
66    /// use supabase::Client;
67    ///
68    /// let client = Client::new("https://your-project.supabase.co", "your-anon-key")?;
69    /// # Ok::<(), Box<dyn std::error::Error>>(())
70    /// ```
71    pub fn new(url: &str, key: &str) -> Result<Self> {
72        let config = SupabaseConfig {
73            url: url.to_string(),
74            key: key.to_string(),
75            service_role_key: None,
76            http_config: HttpConfig::default(),
77            auth_config: AuthConfig::default(),
78            database_config: DatabaseConfig::default(),
79            storage_config: StorageConfig::default(),
80        };
81
82        Self::new_with_config(config)
83    }
84
85    /// Create a new Supabase client with service role key for admin operations
86    ///
87    /// # Arguments
88    ///
89    /// * `url` - Your Supabase project URL (e.g., "https://your-project.supabase.co")
90    /// * `anon_key` - Your Supabase anon API key for client-side operations
91    /// * `service_role_key` - Your Supabase service role key for admin operations
92    ///
93    /// # Example
94    ///
95    /// ```rust
96    /// use supabase::Client;
97    ///
98    /// let client = Client::new_with_service_role(
99    ///     "https://your-project.supabase.co",
100    ///     "your-anon-key",
101    ///     "your-service-role-key"
102    /// )?;
103    /// # Ok::<(), Box<dyn std::error::Error>>(())
104    /// ```
105    pub fn new_with_service_role(
106        url: &str,
107        anon_key: &str,
108        service_role_key: &str,
109    ) -> Result<Self> {
110        let config = SupabaseConfig {
111            url: url.to_string(),
112            key: anon_key.to_string(),
113            service_role_key: Some(service_role_key.to_string()),
114            http_config: HttpConfig::default(),
115            auth_config: AuthConfig::default(),
116            database_config: DatabaseConfig::default(),
117            storage_config: StorageConfig::default(),
118        };
119
120        Self::new_with_config(config)
121    }
122
123    /// Create a new Supabase client with custom configuration
124    ///
125    /// # Arguments
126    ///
127    /// * `config` - Custom Supabase configuration
128    ///
129    /// # Example
130    ///
131    /// ```rust
132    /// use supabase::{Client, types::*};
133    ///
134    /// let config = SupabaseConfig {
135    ///     url: "https://your-project.supabase.co".to_string(),
136    ///     key: "your-anon-key".to_string(),
137    ///     service_role_key: None,
138    ///     http_config: HttpConfig::default(),
139    ///     auth_config: AuthConfig::default(),
140    ///     database_config: DatabaseConfig::default(),
141    ///     storage_config: StorageConfig::default(),
142    /// };
143    ///
144    /// let client = Client::new_with_config(config)?;
145    /// # Ok::<(), Box<dyn std::error::Error>>(())
146    /// ```
147    pub fn new_with_config(config: SupabaseConfig) -> Result<Self> {
148        // Validate URL
149        let _base_url =
150            Url::parse(&config.url).map_err(|e| Error::config(format!("Invalid URL: {}", e)))?;
151
152        debug!("Creating Supabase client for URL: {}", config.url);
153
154        // Build HTTP client
155        let http_client = Arc::new(Self::build_http_client(&config)?);
156        let config = Arc::new(config);
157
158        // Initialize modules conditionally based on features
159        #[cfg(feature = "auth")]
160        let auth = Auth::new(Arc::clone(&config), Arc::clone(&http_client))?;
161
162        #[cfg(feature = "database")]
163        let database = Database::new(Arc::clone(&config), Arc::clone(&http_client))?;
164
165        #[cfg(feature = "storage")]
166        let storage = Storage::new(Arc::clone(&config), Arc::clone(&http_client))?;
167
168        #[cfg(feature = "functions")]
169        let functions = Functions::new(Arc::clone(&config), Arc::clone(&http_client))?;
170
171        #[cfg(feature = "realtime")]
172        let realtime = Realtime::new(Arc::clone(&config))?;
173
174        info!("Supabase client initialized successfully");
175
176        Ok(Self {
177            http_client,
178            config,
179            #[cfg(feature = "auth")]
180            auth,
181            #[cfg(feature = "database")]
182            database,
183            #[cfg(feature = "storage")]
184            storage,
185            #[cfg(feature = "functions")]
186            functions,
187            #[cfg(feature = "realtime")]
188            realtime,
189        })
190    }
191
192    /// Get the authentication module
193    #[cfg(feature = "auth")]
194    pub fn auth(&self) -> &Auth {
195        &self.auth
196    }
197
198    /// Get the database module
199    #[cfg(feature = "database")]
200    pub fn database(&self) -> &Database {
201        &self.database
202    }
203
204    /// Get the storage module
205    #[cfg(feature = "storage")]
206    pub fn storage(&self) -> &Storage {
207        &self.storage
208    }
209
210    /// Get the functions module
211    #[cfg(feature = "functions")]
212    pub fn functions(&self) -> &Functions {
213        &self.functions
214    }
215
216    /// Get the realtime module
217    #[cfg(feature = "realtime")]
218    pub fn realtime(&self) -> &Realtime {
219        &self.realtime
220    }
221
222    /// Get the HTTP client
223    pub fn http_client(&self) -> Arc<HttpClient> {
224        Arc::clone(&self.http_client)
225    }
226
227    /// Get the client configuration
228    pub fn config(&self) -> Arc<SupabaseConfig> {
229        Arc::clone(&self.config)
230    }
231
232    /// Get the base URL for the Supabase project
233    pub fn url(&self) -> &str {
234        &self.config.url
235    }
236
237    /// Get the API key
238    pub fn key(&self) -> &str {
239        &self.config.key
240    }
241
242    /// Set a custom authorization header (JWT token)
243    #[cfg(feature = "auth")]
244    pub async fn set_auth(&self, token: &str) -> Result<()> {
245        self.auth.set_session_token(token).await
246    }
247
248    /// Clear the current authorization
249    #[cfg(feature = "auth")]
250    pub async fn clear_auth(&self) -> Result<()> {
251        self.auth.clear_session().await
252    }
253
254    /// Check if client is authenticated
255    #[cfg(feature = "auth")]
256    pub fn is_authenticated(&self) -> bool {
257        self.auth.is_authenticated()
258    }
259
260    /// Get current user if authenticated
261    #[cfg(feature = "auth")]
262    pub async fn current_user(&self) -> Result<Option<crate::auth::User>> {
263        self.auth.current_user().await
264    }
265
266    /// Build HTTP client with configuration
267    fn build_http_client(config: &SupabaseConfig) -> Result<HttpClient> {
268        let mut headers = HeaderMap::new();
269
270        // Add default headers
271        headers.insert(
272            "apikey",
273            config
274                .key
275                .parse()
276                .map_err(|e| Error::config(format!("Invalid API key: {}", e)))?,
277        );
278        headers.insert(
279            "Authorization",
280            format!("Bearer {}", config.key)
281                .parse()
282                .map_err(|e| Error::config(format!("Invalid authorization header: {}", e)))?,
283        );
284
285        // Add custom headers
286        for (key, value) in &config.http_config.default_headers {
287            let header_name = key
288                .parse::<reqwest::header::HeaderName>()
289                .map_err(|e| Error::config(format!("Invalid header key '{}': {}", key, e)))?;
290            let header_value = value
291                .parse::<reqwest::header::HeaderValue>()
292                .map_err(|e| Error::config(format!("Invalid header value for '{}': {}", key, e)))?;
293            headers.insert(header_name, header_value);
294        }
295
296        #[cfg(not(target_arch = "wasm32"))]
297        let client = HttpClient::builder()
298            .timeout(Duration::from_secs(config.http_config.timeout))
299            .connect_timeout(Duration::from_secs(config.http_config.connect_timeout))
300            .redirect(reqwest::redirect::Policy::limited(
301                config.http_config.max_redirects,
302            ))
303            .default_headers(headers)
304            .build()
305            .map_err(|e| Error::config(format!("Failed to build HTTP client: {}", e)))?;
306
307        #[cfg(target_arch = "wasm32")]
308        let client = HttpClient::builder()
309            .default_headers(headers)
310            .build()
311            .map_err(|e| Error::config(format!("Failed to build HTTP client: {}", e)))?;
312
313        Ok(client)
314    }
315
316    /// Perform a health check on the Supabase instance
317    pub async fn health_check(&self) -> Result<bool> {
318        debug!("Performing health check");
319
320        let response = self
321            .http_client
322            .get(format!("{}/health", self.config.url))
323            .send()
324            .await?;
325
326        let is_healthy = response.status().is_success();
327
328        if is_healthy {
329            info!("Health check passed");
330        } else {
331            error!("Health check failed with status: {}", response.status());
332        }
333
334        Ok(is_healthy)
335    }
336
337    /// Get the current API version information
338    pub async fn version(&self) -> Result<HashMap<String, serde_json::Value>> {
339        debug!("Fetching version information");
340
341        let response = self
342            .http_client
343            .get(format!("{}/rest/v1/", self.config.url))
344            .send()
345            .await?;
346
347        if !response.status().is_success() {
348            return Err(Error::network(format!(
349                "Failed to fetch version info: {}",
350                response.status()
351            )));
352        }
353
354        let version_info = response.json().await?;
355        Ok(version_info)
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn test_client_creation() {
365        let client = Client::new("https://test.supabase.co", "test-key");
366        assert!(client.is_ok());
367    }
368
369    #[test]
370    fn test_invalid_url() {
371        let client = Client::new("invalid-url", "test-key");
372        assert!(client.is_err());
373    }
374
375    #[test]
376    fn test_client_url() {
377        let client = Client::new("https://test.supabase.co", "test-key").unwrap();
378        assert_eq!(client.url(), "https://test.supabase.co");
379    }
380
381    #[test]
382    fn test_client_key() {
383        let client = Client::new("https://test.supabase.co", "test-key").unwrap();
384        assert_eq!(client.key(), "test-key");
385    }
386}