supabase/
client.rs

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