supabase_rust_client/
client.rs

1// src/client.rs
2
3// Reverting to original structure with v0.2.0 path dependencies
4// and stubbing out problematic implementations.
5
6use crate::error::{Result, SupabaseError};
7use crate::models::{AuthCredentials, Item, User};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12// Correct imports based on crate structure
13use 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/// Configuration for the Supabase client.
24/// It's recommended to load these values from environment variables or a secure config source.
25#[derive(Debug, Clone)]
26pub struct SupabaseConfig {
27    pub url: Url,
28    pub anon_key: String,
29}
30
31impl SupabaseConfig {
32    /// Creates a new configuration, validating the URL.
33    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    /// Attempts to create configuration from environment variables.
44    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/// Represents the different types of changes received from a realtime subscription.
56#[derive(Debug, Clone, PartialEq)]
57pub enum ItemChange {
58    Insert(Item),
59    Update(Item),
60    Delete(HashMap<String, Value>),
61    Error(String),
62}
63
64/// Wraps Supabase sub-clients and manages configuration/state.
65#[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    /// Creates a new Supabase client wrapper from configuration.
76    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    /// Convenience function to create a client directly from environment variables.
114    pub fn from_env() -> Result<Self> {
115        let config = SupabaseConfig::from_env()?;
116        Self::new(config)
117    }
118
119    /// Authenticates a user using email and password.
120    /// Corresponds to `authenticateUser` in the SSOT.
121    /// Returns the Supabase User details on success.
122    pub async fn authenticate(&self, credentials: AuthCredentials) -> Result<User> {
123        println!(
124            "[IMPL] Attempting to authenticate user: {}",
125            credentials.email
126        ); // Changed STUB to IMPL for clarity
127        match self
128            .auth
129            .sign_in_with_password(&credentials.email, &credentials.password)
130            .await
131        {
132            Ok(session) => {
133                // Authentication successful, store the session
134                let mut session_guard = self.current_session.lock().await;
135                *session_guard = Some(session.clone()); // Clone session to store and return user
136                println!(
137                    "[IMPL] Authentication successful for user: {}",
138                    session.user.id
139                );
140                Ok(session.user.into()) // Convert auth::User to models::User
141            }
142            Err(e) => {
143                // Authentication failed
144                eprintln!("[IMPL] Authentication failed: {:?}", e); // Use eprintln for errors
145                Err(SupabaseError::Auth(e)) // Map the AuthError to SupabaseError
146            }
147        }
148    }
149
150    /// Logs out the currently authenticated user by invalidating the session/token.
151    /// Corresponds to `logoutUser` in the SSOT.
152    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    /// Fetches 'items' from the database.
158    /// Requires authentication.
159    /// Corresponds to `fetchItemsFromSupabase` in the SSOT.
160    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        // execute<T>() deserializes into Vec<T>
173        client
174            .select("*")
175            .execute::<Item>() // T is Item, returns Result<Vec<Item>, PostgrestError>
176            .await
177            .map_err(SupabaseError::Postgrest)
178    }
179
180    /// Subscribes to item changes.
181    /// Corresponds to `subscribeToItemChanges` in the SSOT.
182    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    // --- CRUD Operations for Items ---
188
189    /// Creates a new item in the database.
190    /// Requires authentication.
191    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        // insert() returns a Future<Output = Result<Value, PostgrestError>>
204        let response_value = client
205            .insert(vec![new_item])
206            .await // Await the future directly
207            .map_err(SupabaseError::Postgrest)?;
208
209        // Parse the serde_json::Value into Vec<Item>
210        // Postgrest insert with return=representation returns an array
211        let mut created_items: Vec<Item> =
212            serde_json::from_value(response_value).map_err(SupabaseError::Json)?; // Map serde_json::Error using #[from]
213
214        // Extract the first item
215        created_items.pop().ok_or_else(|| {
216            // Use PostgrestError::DeserializationError when parsing is ok but result is empty/unexpected
217            SupabaseError::Postgrest(PostgrestError::DeserializationError(
218                "No item data returned after insert".to_string(),
219            ))
220        })
221    }
222
223    /// Fetches a single 'item' by its ID.
224    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    /// Updates an existing 'item' by its ID.
230    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    /// Deletes an 'item' by its ID.
236    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)] // Allowed because methods using it are stubbed
242    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()) // Use map() instead of and_then(Some())
247            .ok_or_else(|| {
248                SupabaseError::Auth(AuthError::ApiError("Missing session token".to_string()))
249            })
250    }
251
252    // --- Test-only Helper ---
253    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    // --- Public Getters ---
259    /// Returns the Supabase Anon Key used by the client.
260    pub fn anon_key(&self) -> &str {
261        &self.config.anon_key
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*; // Import items from parent module
268    use dotenv::dotenv;
269
270    #[test]
271    fn config_new_valid() {
272        dotenv().ok(); // Load .env file for testing if available
273
274        // Temporarily set dummy env vars for this test
275        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        // Test creating config with the (now set) env vars
281        // let url_from_env = std::env::var("SUPABASE_URL").expect("SUPABASE_URL must be set for tests");
282        // let key_from_env =
283        //     std::env::var("SUPABASE_ANON_KEY").expect("SUPABASE_ANON_KEY must be set for tests");
284        // Use the values directly now that we set them
285        let config = SupabaseConfig::new(url, key.to_string()).unwrap();
286
287        // Fix: Url::parse adds a trailing slash if missing path, format! needs literal and arg
288        // Use the original URL value for comparison
289        assert_eq!(config.url.to_string(), format!("{}/", url));
290        assert_eq!(config.anon_key, key);
291
292        // Optional: Unset the vars? Generally not needed as it affects only this process.
293        // std::env::remove_var("SUPABASE_URL");
294        // std::env::remove_var("SUPABASE_ANON_KEY");
295    }
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(_) => {} // Expected error
305            _ => 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    // Add tests for SupabaseConfig::from_env() - requires setting env vars for test
322    // This might be better suited for integration tests or require helper libraries.
323}