use crate::{
error::{Error, Result},
types::{AuthConfig, DatabaseConfig, HttpConfig, StorageConfig, SupabaseConfig},
};
#[cfg(feature = "auth")]
use crate::auth::Auth;
#[cfg(feature = "database")]
use crate::database::Database;
#[cfg(feature = "storage")]
use crate::storage::Storage;
#[cfg(feature = "functions")]
use crate::functions::Functions;
#[cfg(feature = "realtime")]
use crate::realtime::Realtime;
use reqwest::{header::HeaderMap, Client as HttpClient};
use std::{collections::HashMap, sync::Arc};
#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;
use tracing::{debug, error, info};
use url::Url;
#[derive(Debug, Clone)]
pub struct Client {
http_client: Arc<HttpClient>,
config: Arc<SupabaseConfig>,
#[cfg(feature = "auth")]
auth: Auth,
#[cfg(feature = "database")]
database: Database,
#[cfg(feature = "storage")]
storage: Storage,
#[cfg(feature = "functions")]
functions: Functions,
#[cfg(feature = "realtime")]
realtime: Realtime,
}
impl Client {
pub fn new(url: &str, key: &str) -> Result<Self> {
let config = SupabaseConfig {
url: url.to_string(),
key: key.to_string(),
service_role_key: None,
http_config: HttpConfig::default(),
auth_config: AuthConfig::default(),
database_config: DatabaseConfig::default(),
storage_config: StorageConfig::default(),
};
Self::new_with_config(config)
}
pub fn new_with_service_role(
url: &str,
anon_key: &str,
service_role_key: &str,
) -> Result<Self> {
let config = SupabaseConfig {
url: url.to_string(),
key: anon_key.to_string(),
service_role_key: Some(service_role_key.to_string()),
http_config: HttpConfig::default(),
auth_config: AuthConfig::default(),
database_config: DatabaseConfig::default(),
storage_config: StorageConfig::default(),
};
Self::new_with_config(config)
}
pub fn new_with_config(config: SupabaseConfig) -> Result<Self> {
let _base_url =
Url::parse(&config.url).map_err(|e| Error::config(format!("Invalid URL: {}", e)))?;
debug!("Creating Supabase client for URL: {}", config.url);
let http_client = Arc::new(Self::build_http_client(&config)?);
let config = Arc::new(config);
#[cfg(feature = "auth")]
let auth = Auth::new(Arc::clone(&config), Arc::clone(&http_client))?;
#[cfg(feature = "database")]
let database = Database::new(Arc::clone(&config), Arc::clone(&http_client))?;
#[cfg(feature = "storage")]
let storage = Storage::new(Arc::clone(&config), Arc::clone(&http_client))?;
#[cfg(feature = "functions")]
let functions = Functions::new(Arc::clone(&config), Arc::clone(&http_client))?;
#[cfg(feature = "realtime")]
let realtime = Realtime::new(Arc::clone(&config))?;
info!("Supabase client initialized successfully");
Ok(Self {
http_client,
config,
#[cfg(feature = "auth")]
auth,
#[cfg(feature = "database")]
database,
#[cfg(feature = "storage")]
storage,
#[cfg(feature = "functions")]
functions,
#[cfg(feature = "realtime")]
realtime,
})
}
#[cfg(feature = "auth")]
pub fn auth(&self) -> &Auth {
&self.auth
}
#[cfg(feature = "database")]
pub fn database(&self) -> &Database {
&self.database
}
#[cfg(feature = "storage")]
pub fn storage(&self) -> &Storage {
&self.storage
}
#[cfg(feature = "functions")]
pub fn functions(&self) -> &Functions {
&self.functions
}
#[cfg(feature = "realtime")]
pub fn realtime(&self) -> &Realtime {
&self.realtime
}
pub fn http_client(&self) -> Arc<HttpClient> {
Arc::clone(&self.http_client)
}
pub fn config(&self) -> Arc<SupabaseConfig> {
Arc::clone(&self.config)
}
pub fn url(&self) -> &str {
&self.config.url
}
pub fn key(&self) -> &str {
&self.config.key
}
#[cfg(feature = "auth")]
pub async fn set_auth(&self, token: &str) -> Result<()> {
self.auth.set_session_token(token).await
}
#[cfg(feature = "auth")]
pub async fn clear_auth(&self) -> Result<()> {
self.auth.clear_session().await
}
#[cfg(feature = "auth")]
pub fn is_authenticated(&self) -> bool {
self.auth.is_authenticated()
}
#[cfg(feature = "auth")]
pub async fn current_user(&self) -> Result<Option<crate::auth::User>> {
self.auth.current_user().await
}
fn build_http_client(config: &SupabaseConfig) -> Result<HttpClient> {
let mut headers = HeaderMap::new();
headers.insert(
"apikey",
config
.key
.parse()
.map_err(|e| Error::config(format!("Invalid API key: {}", e)))?,
);
headers.insert(
"Authorization",
format!("Bearer {}", config.key)
.parse()
.map_err(|e| Error::config(format!("Invalid authorization header: {}", e)))?,
);
for (key, value) in &config.http_config.default_headers {
let header_name = key
.parse::<reqwest::header::HeaderName>()
.map_err(|e| Error::config(format!("Invalid header key '{}': {}", key, e)))?;
let header_value = value
.parse::<reqwest::header::HeaderValue>()
.map_err(|e| Error::config(format!("Invalid header value for '{}': {}", key, e)))?;
headers.insert(header_name, header_value);
}
#[cfg(not(target_arch = "wasm32"))]
let client = HttpClient::builder()
.timeout(Duration::from_secs(config.http_config.timeout))
.connect_timeout(Duration::from_secs(config.http_config.connect_timeout))
.redirect(reqwest::redirect::Policy::limited(
config.http_config.max_redirects,
))
.default_headers(headers)
.build()
.map_err(|e| Error::config(format!("Failed to build HTTP client: {}", e)))?;
#[cfg(target_arch = "wasm32")]
let client = HttpClient::builder()
.default_headers(headers)
.build()
.map_err(|e| Error::config(format!("Failed to build HTTP client: {}", e)))?;
Ok(client)
}
pub async fn health_check(&self) -> Result<bool> {
debug!("Performing health check");
let response = self
.http_client
.get(format!("{}/health", self.config.url))
.send()
.await?;
let is_healthy = response.status().is_success();
if is_healthy {
info!("Health check passed");
} else {
error!("Health check failed with status: {}", response.status());
}
Ok(is_healthy)
}
pub async fn version(&self) -> Result<HashMap<String, serde_json::Value>> {
debug!("Fetching version information");
let response = self
.http_client
.get(format!("{}/rest/v1/", self.config.url))
.send()
.await?;
if !response.status().is_success() {
return Err(Error::network(format!(
"Failed to fetch version info: {}",
response.status()
)));
}
let version_info = response.json().await?;
Ok(version_info)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_creation() {
let client = Client::new("https://test.supabase.co", "test-key");
assert!(client.is_ok());
}
#[test]
fn test_invalid_url() {
let client = Client::new("invalid-url", "test-key");
assert!(client.is_err());
}
#[test]
fn test_client_url() {
let client = Client::new("https://test.supabase.co", "test-key").unwrap();
assert_eq!(client.url(), "https://test.supabase.co");
}
#[test]
fn test_client_key() {
let client = Client::new("https://test.supabase.co", "test-key").unwrap();
assert_eq!(client.key(), "test-key");
}
}