supabase_rust_client/
client.rs1use crate::error::{Result, SupabaseError};
7use crate::models::{AuthCredentials, Item, User};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use reqwest::Client as ReqwestClient;
14use supabase_rust_auth::AuthOptions;
15use supabase_rust_auth::{Auth, AuthError, Session as AuthSession};
16use supabase_rust_postgrest::PostgrestError;
17use supabase_rust_realtime::RealtimeClient;
18
19use tokio::sync::{mpsc, Mutex};
20use url::Url;
21use uuid::Uuid;
22
23#[derive(Debug, Clone)]
26pub struct SupabaseConfig {
27 pub url: Url,
28 pub anon_key: String,
29}
30
31impl SupabaseConfig {
32 pub fn new(url_str: &str, anon_key: String) -> Result<Self> {
34 let url = Url::parse(url_str).map_err(SupabaseError::UrlParse)?;
35 if anon_key.is_empty() {
36 return Err(SupabaseError::Config(
37 "anon_key cannot be empty".to_string(),
38 ));
39 }
40 Ok(Self { url, anon_key })
41 }
42
43 pub fn from_env() -> Result<Self> {
45 let url_str = std::env::var("SUPABASE_URL").map_err(|_| {
46 SupabaseError::Config("SUPABASE_URL environment variable not found".to_string())
47 })?;
48 let anon_key = std::env::var("SUPABASE_ANON_KEY").map_err(|_| {
49 SupabaseError::Config("SUPABASE_ANON_KEY environment variable not found".to_string())
50 })?;
51 Self::new(&url_str, anon_key)
52 }
53}
54
55#[derive(Debug, Clone, PartialEq)]
57pub enum ItemChange {
58 Insert(Item),
59 Update(Item),
60 Delete(HashMap<String, Value>),
61 Error(String),
62}
63
64#[derive(Clone)]
66pub struct SupabaseClientWrapper {
67 config: Arc<SupabaseConfig>,
68 http_client: ReqwestClient,
69 pub auth: Arc<Auth>,
70 pub realtime: Arc<RealtimeClient>,
71 current_session: Arc<Mutex<Option<AuthSession>>>,
72}
73
74impl SupabaseClientWrapper {
75 pub fn new(config: SupabaseConfig) -> Result<Self> {
77 let http_client = ReqwestClient::builder()
78 .build()
79 .map_err(SupabaseError::Network)?;
80
81 let auth_client = Auth::new(
82 config.url.as_str(),
83 &config.anon_key,
84 http_client.clone(),
85 AuthOptions::default(),
86 );
87
88 let mut rt_url_builder = config.url.clone();
89 let scheme = if config.url.scheme() == "https" {
90 "wss"
91 } else {
92 "ws"
93 };
94 rt_url_builder.set_scheme(scheme).map_err(|_| {
95 SupabaseError::Initialization("Failed to set scheme for Realtime URL".to_string())
96 })?;
97 let rt_url = rt_url_builder.join("realtime/v1").map_err(|e| {
98 SupabaseError::Initialization(format!("Failed to construct Realtime URL: {}", e))
99 })?;
100 let realtime_client = RealtimeClient::new(rt_url.as_ref(), &config.anon_key);
101
102 println!("Supabase client initialized (Auth & Realtime - Postgrest on demand).");
103
104 Ok(Self {
105 config: Arc::new(config),
106 http_client,
107 auth: Arc::new(auth_client),
108 realtime: Arc::new(realtime_client),
109 current_session: Arc::new(Mutex::new(None)),
110 })
111 }
112
113 pub fn from_env() -> Result<Self> {
115 let config = SupabaseConfig::from_env()?;
116 Self::new(config)
117 }
118
119 pub async fn authenticate(&self, credentials: AuthCredentials) -> Result<User> {
123 println!(
124 "[IMPL] Attempting to authenticate user: {}",
125 credentials.email
126 ); match self
128 .auth
129 .sign_in_with_password(&credentials.email, &credentials.password)
130 .await
131 {
132 Ok(session) => {
133 let mut session_guard = self.current_session.lock().await;
135 *session_guard = Some(session.clone()); println!(
137 "[IMPL] Authentication successful for user: {}",
138 session.user.id
139 );
140 Ok(session.user.into()) }
142 Err(e) => {
143 eprintln!("[IMPL] Authentication failed: {:?}", e); Err(SupabaseError::Auth(e)) }
147 }
148 }
149
150 pub async fn logout(&self) -> Result<()> {
153 println!("[STUB] Attempting to log out user");
154 unimplemented!("Logout logic needs fixing for v0.2.0 API");
155 }
156
157 pub async fn fetch_items(&self) -> Result<Vec<Item>> {
161 println!("[IMPL] Attempting to fetch items");
162 let token = self.get_auth_token().await?;
163
164 let client = supabase_rust_postgrest::PostgrestClient::new(
165 self.config.url.as_str(),
166 &self.config.anon_key,
167 "items",
168 self.http_client.clone(),
169 )
170 .with_auth(&token)?;
171
172 client
174 .select("*")
175 .execute::<Item>() .await
177 .map_err(SupabaseError::Postgrest)
178 }
179
180 pub async fn subscribe_to_item_changes(&self) -> Result<mpsc::UnboundedReceiver<ItemChange>> {
183 println!("[STUB] Attempting to subscribe to item changes");
184 unimplemented!("Realtime subscription logic needs fixing for v0.2.0 API");
185 }
186
187 pub async fn create_item(&self, new_item: Item) -> Result<Item> {
192 println!("[IMPL] Attempting to create item");
193 let token = self.get_auth_token().await?;
194
195 let client = supabase_rust_postgrest::PostgrestClient::new(
196 self.config.url.as_str(),
197 &self.config.anon_key,
198 "items",
199 self.http_client.clone(),
200 )
201 .with_auth(&token)?;
202
203 let response_value = client
205 .insert(vec![new_item])
206 .await .map_err(SupabaseError::Postgrest)?;
208
209 let mut created_items: Vec<Item> =
212 serde_json::from_value(response_value).map_err(SupabaseError::Json)?; created_items.pop().ok_or_else(|| {
216 SupabaseError::Postgrest(PostgrestError::DeserializationError(
218 "No item data returned after insert".to_string(),
219 ))
220 })
221 }
222
223 pub async fn fetch_item_by_id(&self, _item_id: Uuid) -> Result<Option<Item>> {
225 println!("[STUB] Attempting to fetch item by ID");
226 unimplemented!("Postgrest fetch single logic needs fixing for v0.2.0 API");
227 }
228
229 pub async fn update_item(&self, _item_id: Uuid, _item_update: Item) -> Result<Item> {
231 println!("[STUB] Attempting to update item");
232 unimplemented!("Postgrest update logic needs fixing for v0.2.0 API");
233 }
234
235 pub async fn delete_item(&self, _item_id: Uuid) -> Result<()> {
237 println!("[STUB] Attempting to delete item");
238 unimplemented!("Postgrest delete logic needs fixing for v0.2.0 API");
239 }
240
241 #[allow(dead_code)] async fn get_auth_token(&self) -> Result<String> {
243 let session_guard = self.current_session.lock().await;
244 session_guard
245 .as_ref()
246 .map(|s| s.access_token.clone()) .ok_or_else(|| {
248 SupabaseError::Auth(AuthError::ApiError("Missing session token".to_string()))
249 })
250 }
251
252 pub async fn set_session_for_test(&self, session: Option<AuthSession>) {
254 let mut session_guard = self.current_session.lock().await;
255 *session_guard = session;
256 }
257
258 pub fn anon_key(&self) -> &str {
261 &self.config.anon_key
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*; use dotenv::dotenv;
269
270 #[test]
271 fn config_new_valid() {
272 dotenv().ok(); let url = "http://localhost:12345";
276 let key = "dummy-anon-key";
277 std::env::set_var("SUPABASE_URL", url);
278 std::env::set_var("SUPABASE_ANON_KEY", key);
279
280 let config = SupabaseConfig::new(url, key.to_string()).unwrap();
286
287 assert_eq!(config.url.to_string(), format!("{}/", url));
290 assert_eq!(config.anon_key, key);
291
292 }
296
297 #[test]
298 fn config_new_invalid_url() {
299 let url = "not a valid url";
300 let key = "some_anon_key";
301 let config = SupabaseConfig::new(url, key.to_string());
302 assert!(config.is_err());
303 match config.err().unwrap() {
304 SupabaseError::UrlParse(_) => {} _ => panic!("Expected UrlParse error"),
306 }
307 }
308
309 #[test]
310 fn config_new_empty_key() {
311 let url = "http://localhost:54321";
312 let key = "";
313 let config = SupabaseConfig::new(url, key.to_string());
314 assert!(config.is_err());
315 match config.err().unwrap() {
316 SupabaseError::Config(msg) => assert!(msg.contains("anon_key cannot be empty")),
317 _ => panic!("Expected Config error for empty key"),
318 }
319 }
320
321 }