supabase_client_rs/
client.rs

1//! The main Supabase client.
2
3use crate::config::SupabaseConfig;
4use crate::error::{Error, Result};
5use postgrest::Postgrest;
6use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderName, HeaderValue};
7
8#[cfg(feature = "realtime")]
9use supabase_realtime_rs::{RealtimeClient, RealtimeClientOptions};
10
11/// The main Supabase client.
12///
13/// This client provides access to all Supabase services:
14/// - Database queries via PostgREST (`.from()`)
15/// - Realtime subscriptions (`.realtime()`) - requires `realtime` feature
16/// - Authentication (`.auth()`) - when community crate is available
17/// - Storage (`.storage()`) - when community crate is available
18/// - Edge Functions (`.functions()`) - when community crate is available
19///
20/// # Example
21///
22/// ```rust,no_run
23/// use supabase_client_rs::SupabaseClient;
24///
25/// #[tokio::main]
26/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
27///     let client = SupabaseClient::new(
28///         "https://your-project.supabase.co",
29///         "your-anon-key"
30///     )?;
31///
32///     // Query the database
33///     let response = client
34///         .from("users")
35///         .select("*")
36///         .execute()
37///         .await?;
38///
39///     println!("{}", response.text().await?);
40///     Ok(())
41/// }
42/// ```
43#[derive(Clone)]
44pub struct SupabaseClient {
45    config: SupabaseConfig,
46    http: reqwest::Client,
47    postgrest: Postgrest,
48    #[cfg(feature = "realtime")]
49    realtime: std::sync::Arc<RealtimeClient>,
50}
51
52impl SupabaseClient {
53    /// Create a new Supabase client with the given URL and API key.
54    ///
55    /// # Arguments
56    ///
57    /// * `url` - The Supabase project URL (e.g., `https://xyzcompany.supabase.co`)
58    /// * `api_key` - The Supabase API key (anon or service role)
59    ///
60    /// # Example
61    ///
62    /// ```rust,no_run
63    /// use supabase_client_rs::SupabaseClient;
64    ///
65    /// let client = SupabaseClient::new(
66    ///     "https://your-project.supabase.co",
67    ///     "your-anon-key"
68    /// ).unwrap();
69    /// ```
70    pub fn new(url: impl Into<String>, api_key: impl Into<String>) -> Result<Self> {
71        let config = SupabaseConfig::new(url, api_key);
72        Self::with_config(config)
73    }
74
75    /// Create a new Supabase client with custom configuration.
76    ///
77    /// # Example
78    ///
79    /// ```rust,no_run
80    /// use supabase_client_rs::{SupabaseClient, SupabaseConfig};
81    /// use std::time::Duration;
82    ///
83    /// let config = SupabaseConfig::new(
84    ///     "https://your-project.supabase.co",
85    ///     "your-anon-key"
86    /// )
87    /// .schema("custom_schema")
88    /// .timeout(Duration::from_secs(60));
89    ///
90    /// let client = SupabaseClient::with_config(config).unwrap();
91    /// ```
92    pub fn with_config(config: SupabaseConfig) -> Result<Self> {
93        if config.url.is_empty() {
94            return Err(Error::config("URL is required"));
95        }
96        if config.api_key.is_empty() {
97            return Err(Error::config("API key is required"));
98        }
99
100        // Build default headers
101        let mut headers = HeaderMap::new();
102        headers.insert(
103            "apikey",
104            HeaderValue::from_str(&config.api_key).map_err(|e| Error::config(e.to_string()))?,
105        );
106
107        // Add Authorization header
108        let auth_value = if let Some(ref jwt) = config.jwt {
109            format!("Bearer {}", jwt)
110        } else {
111            format!("Bearer {}", config.api_key)
112        };
113        headers.insert(
114            AUTHORIZATION,
115            HeaderValue::from_str(&auth_value).map_err(|e| Error::config(e.to_string()))?,
116        );
117
118        // Add custom headers
119        for (key, value) in &config.headers {
120            let name = HeaderName::try_from(key.as_str())
121                .map_err(|e| Error::config(format!("invalid header name: {}", e)))?;
122            let val = HeaderValue::from_str(value)
123                .map_err(|e| Error::config(format!("invalid header value: {}", e)))?;
124            headers.insert(name, val);
125        }
126
127        // Build HTTP client
128        let http = reqwest::Client::builder()
129            .default_headers(headers.clone())
130            .timeout(config.timeout)
131            .build()?;
132
133        // Build PostgREST client
134        let postgrest = Postgrest::new(config.rest_url())
135            .insert_header("apikey", &config.api_key)
136            .insert_header("Authorization", &auth_value);
137
138        // Build Realtime client if feature is enabled
139        #[cfg(feature = "realtime")]
140        let realtime = {
141            let realtime_client = RealtimeClient::new(
142                &config.realtime_url(),
143                RealtimeClientOptions {
144                    api_key: config.api_key.clone(),
145                    ..Default::default()
146                },
147            )?;
148            std::sync::Arc::new(realtime_client)
149        };
150
151        Ok(Self {
152            config: config.clone(),
153            http,
154            postgrest,
155            #[cfg(feature = "realtime")]
156            realtime,
157        })
158    }
159
160    /// Create a query builder for the given table.
161    ///
162    /// This is the main entry point for database operations using PostgREST.
163    ///
164    /// # Example
165    ///
166    /// ```rust,no_run
167    /// # use supabase_client_rs::SupabaseClient;
168    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
169    /// # let client = SupabaseClient::new("url", "key")?;
170    /// // Select all users
171    /// let users = client.from("users").select("*").execute().await?;
172    ///
173    /// // Insert a new user
174    /// let new_user = client
175    ///     .from("users")
176    ///     .insert(r#"{"name": "Alice", "email": "alice@example.com"}"#)
177    ///     .execute()
178    ///     .await?;
179    ///
180    /// // Update with filters
181    /// let updated = client
182    ///     .from("users")
183    ///     .update(r#"{"status": "active"}"#)
184    ///     .eq("id", "123")
185    ///     .execute()
186    ///     .await?;
187    ///
188    /// // Delete with filters
189    /// let deleted = client
190    ///     .from("users")
191    ///     .delete()
192    ///     .eq("status", "inactive")
193    ///     .execute()
194    ///     .await?;
195    /// # Ok(())
196    /// # }
197    /// ```
198    pub fn from(&self, table: &str) -> postgrest::Builder {
199        self.postgrest.from(table)
200    }
201
202    /// Execute a stored procedure (RPC).
203    ///
204    /// # Example
205    ///
206    /// ```rust,no_run
207    /// # use supabase_client_rs::SupabaseClient;
208    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
209    /// # let client = SupabaseClient::new("url", "key")?;
210    /// let result = client
211    ///     .rpc("my_function", r#"{"param1": "value1"}"#)
212    ///     .execute()
213    ///     .await?;
214    /// # Ok(())
215    /// # }
216    /// ```
217    pub fn rpc(&self, function: &str, params: &str) -> postgrest::Builder {
218        self.postgrest.rpc(function, params)
219    }
220
221    /// Get the configuration.
222    pub fn config(&self) -> &SupabaseConfig {
223        &self.config
224    }
225
226    /// Get the underlying HTTP client.
227    ///
228    /// Useful for making custom requests to Supabase APIs.
229    pub fn http(&self) -> &reqwest::Client {
230        &self.http
231    }
232
233    /// Get the PostgREST client.
234    ///
235    /// Use this if you need direct access to the PostgREST client.
236    pub fn postgrest(&self) -> &Postgrest {
237        &self.postgrest
238    }
239
240    /// Set a JWT for authenticated requests.
241    ///
242    /// This creates a new client with the updated JWT.
243    /// Use this after a user signs in to make authenticated requests.
244    ///
245    /// # Example
246    ///
247    /// ```rust,no_run
248    /// # use supabase_client_rs::SupabaseClient;
249    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
250    /// # let client = SupabaseClient::new("url", "key")?;
251    /// let jwt = "user-jwt-token";
252    /// let authenticated_client = client.with_jwt(jwt)?;
253    /// # Ok(())
254    /// # }
255    /// ```
256    pub fn with_jwt(&self, jwt: impl Into<String>) -> Result<Self> {
257        let mut new_config: SupabaseConfig = self.config.clone();
258        new_config.jwt = Some(jwt.into());
259        Self::with_config(new_config)
260    }
261
262    /*
263    // =========================================================================
264    // Future: Auth, Storage, Functions, Realtime
265    // These will be enabled when community crates are available
266    // =========================================================================
267    /// Access the Auth client.
268    ///
269    /// **Note:** This requires an auth provider to be set up.
270    /// See the `supabase-auth-rs` crate (when available).
271    #[cfg(feature = "auth")]
272    pub fn auth(&self) -> &dyn crate::traits::AuthProvider {
273        todo!("Auth provider not yet implemented - contribute at supabase-auth-rs!")
274    }
275
276    /// Access the Storage client.
277    ///
278    /// **Note:** This requires a storage provider to be set up.
279    /// See the `supabase-storage-rs` crate (when available).
280    #[cfg(feature = "storage")]
281    pub fn storage(&self) -> &dyn crate::traits::StorageProvider {
282        todo!("Storage provider not yet implemented - contribute at supabase-storage-rs!")
283    }
284
285    /// Access the Functions client.
286    ///
287    /// **Note:** This requires a functions provider to be set up.
288    /// See the `supabase-functions-rs` crate (when available).
289    #[cfg(feature = "functions")]
290    pub fn functions(&self) -> &dyn crate::traits::FunctionsProvider {
291        todo!("Functions provider not yet implemented - contribute at supabase-functions-rs!")
292    }
293    */
294
295    // =========================================================================
296    // Realtime - Integration with supabase-realtime-rs
297    // =========================================================================
298
299    /// Get the Realtime client.
300    ///
301    /// Requires the `realtime` feature to be enabled.
302    ///
303    /// # Example
304    ///
305    /// ```rust,no_run
306    /// # #[cfg(feature = "realtime")]
307    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
308    /// # use supabase_client_rs::SupabaseClient;
309    /// # use supabase_realtime_rs::{ChannelEvent, RealtimeChannelOptions};
310    /// # let client = SupabaseClient::new("url", "key")?;
311    /// // Get the realtime client
312    /// let realtime = client.realtime();
313    ///
314    /// // Connect to realtime
315    /// realtime.connect().await?;
316    ///
317    /// // Create a channel
318    /// let channel = realtime.channel("room:lobby", RealtimeChannelOptions::default()).await;
319    /// let mut rx = channel.on(ChannelEvent::broadcast("message")).await;
320    /// channel.subscribe().await?;
321    ///
322    /// // Listen for messages
323    /// tokio::spawn(async move {
324    ///     while let Some(msg) = rx.recv().await {
325    ///         println!("Received: {:?}", msg);
326    ///     }
327    /// });
328    /// # Ok(())
329    /// # }
330    /// ```
331    #[cfg(feature = "realtime")]
332    pub fn realtime(&self) -> &RealtimeClient {
333        &self.realtime
334    }
335
336    /// Get the Realtime WebSocket URL.
337    ///
338    /// Use this to initialize your own `supabase-realtime-rs` client if needed.
339    pub fn realtime_url(&self) -> String {
340        self.config.realtime_url()
341    }
342}
343
344impl std::fmt::Debug for SupabaseClient {
345    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346        f.debug_struct("SupabaseClient")
347            .field("url", &self.config.url)
348            .field("schema", &self.config.schema)
349            .finish_non_exhaustive()
350    }
351}