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}