1use 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#[derive(Debug, Clone)]
32pub struct Client {
33 http_client: Arc<HttpClient>,
35 config: Arc<SupabaseConfig>,
37 #[cfg(feature = "auth")]
39 auth: Auth,
40 #[cfg(feature = "database")]
42 database: Database,
43 #[cfg(feature = "storage")]
45 storage: Storage,
46
47 #[cfg(feature = "functions")]
48 functions: Functions,
49
50 #[cfg(feature = "realtime")]
52 realtime: Realtime,
53}
54
55impl Client {
56 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 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 pub fn new_with_config(config: SupabaseConfig) -> Result<Self> {
148 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 let http_client = Arc::new(Self::build_http_client(&config)?);
156 let config = Arc::new(config);
157
158 #[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 #[cfg(feature = "auth")]
194 pub fn auth(&self) -> &Auth {
195 &self.auth
196 }
197
198 #[cfg(feature = "database")]
200 pub fn database(&self) -> &Database {
201 &self.database
202 }
203
204 #[cfg(feature = "storage")]
206 pub fn storage(&self) -> &Storage {
207 &self.storage
208 }
209
210 #[cfg(feature = "functions")]
212 pub fn functions(&self) -> &Functions {
213 &self.functions
214 }
215
216 #[cfg(feature = "realtime")]
218 pub fn realtime(&self) -> &Realtime {
219 &self.realtime
220 }
221
222 pub fn http_client(&self) -> Arc<HttpClient> {
224 Arc::clone(&self.http_client)
225 }
226
227 pub fn config(&self) -> Arc<SupabaseConfig> {
229 Arc::clone(&self.config)
230 }
231
232 pub fn url(&self) -> &str {
234 &self.config.url
235 }
236
237 pub fn key(&self) -> &str {
239 &self.config.key
240 }
241
242 #[cfg(feature = "auth")]
244 pub async fn set_auth(&self, token: &str) -> Result<()> {
245 self.auth.set_session_token(token).await
246 }
247
248 #[cfg(feature = "auth")]
250 pub async fn clear_auth(&self) -> Result<()> {
251 self.auth.clear_session().await
252 }
253
254 #[cfg(feature = "auth")]
256 pub fn is_authenticated(&self) -> bool {
257 self.auth.is_authenticated()
258 }
259
260 #[cfg(feature = "auth")]
262 pub async fn current_user(&self) -> Result<Option<crate::auth::User>> {
263 self.auth.current_user().await
264 }
265
266 fn build_http_client(config: &SupabaseConfig) -> Result<HttpClient> {
268 let mut headers = HeaderMap::new();
269
270 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 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 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 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}