supabase/
functions.rs

1//! Edge Functions module for Supabase
2//!
3//! This module provides functionality to invoke Supabase Edge Functions.
4//! Edge Functions are server-side TypeScript functions that run on the edge,
5//! close to your users for reduced latency.
6
7use crate::{
8    error::{Error, Result},
9    types::SupabaseConfig,
10};
11use reqwest::Client as HttpClient;
12use serde_json::Value as JsonValue;
13use std::sync::Arc;
14use tracing::{debug, info};
15
16/// Edge Functions client for invoking serverless functions
17///
18/// # Examples
19///
20/// Basic function invocation:
21///
22/// ```rust,no_run
23/// use supabase::Client;
24/// use serde_json::json;
25///
26/// # async fn example() -> supabase::Result<()> {
27/// let client = Client::new("your-project-url", "your-anon-key")?;
28///
29/// // Invoke a function with parameters
30/// let result = client.functions()
31///     .invoke("hello-world", Some(json!({"name": "World"})))
32///     .await?;
33///
34/// println!("Function result: {}", result);
35/// # Ok(())
36/// # }
37/// ```
38///
39/// Function with custom headers:
40///
41/// ```rust,no_run
42/// use supabase::Client;
43/// use serde_json::json;
44/// use std::collections::HashMap;
45///
46/// # async fn example() -> supabase::Result<()> {
47/// let client = Client::new("your-project-url", "your-anon-key")?;
48///
49/// let mut headers = HashMap::new();
50/// headers.insert("X-Custom-Header".to_string(), "custom-value".to_string());
51///
52/// let result = client.functions()
53///     .invoke_with_options("my-function", Some(json!({"data": "value"})), Some(headers))
54///     .await?;
55/// # Ok(())
56/// # }
57/// ```
58#[derive(Debug, Clone)]
59pub struct Functions {
60    http_client: Arc<HttpClient>,
61    config: Arc<SupabaseConfig>,
62}
63
64impl Functions {
65    /// Create a new Functions instance
66    pub fn new(config: Arc<SupabaseConfig>, http_client: Arc<HttpClient>) -> Result<Self> {
67        debug!("Initializing Functions module");
68
69        Ok(Self {
70            http_client,
71            config,
72        })
73    }
74
75    /// Invoke an Edge Function
76    ///
77    /// # Parameters
78    ///
79    /// * `function_name` - Name of the function to invoke
80    /// * `body` - Optional JSON body to send to the function
81    ///
82    /// # Examples
83    ///
84    /// ```rust,no_run
85    /// use serde_json::json;
86    ///
87    /// # async fn example(functions: &supabase::Functions) -> supabase::Result<()> {
88    /// // Simple function call
89    /// let result = functions.invoke("hello", None).await?;
90    ///
91    /// // Function with parameters
92    /// let result = functions.invoke("process-data", Some(json!({
93    ///     "user_id": 123,
94    ///     "action": "update_profile"
95    /// }))).await?;
96    /// # Ok(())
97    /// # }
98    /// ```
99    pub async fn invoke(&self, function_name: &str, body: Option<JsonValue>) -> Result<JsonValue> {
100        self.invoke_with_options(function_name, body, None).await
101    }
102
103    /// Invoke an Edge Function with custom options
104    ///
105    /// # Parameters
106    ///
107    /// * `function_name` - Name of the function to invoke
108    /// * `body` - Optional JSON body to send to the function
109    /// * `headers` - Optional additional headers to send
110    ///
111    /// # Examples
112    ///
113    /// ```rust,no_run
114    /// use serde_json::json;
115    /// use std::collections::HashMap;
116    ///
117    /// # async fn example(functions: &supabase::Functions) -> supabase::Result<()> {
118    /// let mut headers = HashMap::new();
119    /// headers.insert("X-API-Version".to_string(), "v1".to_string());
120    /// headers.insert("X-Custom-Auth".to_string(), "bearer token".to_string());
121    ///
122    /// let result = functions.invoke_with_options(
123    ///     "secure-function",
124    ///     Some(json!({"sensitive": "data"})),
125    ///     Some(headers)
126    /// ).await?;
127    /// # Ok(())
128    /// # }
129    /// ```
130    pub async fn invoke_with_options(
131        &self,
132        function_name: &str,
133        body: Option<JsonValue>,
134        headers: Option<std::collections::HashMap<String, String>>,
135    ) -> Result<JsonValue> {
136        debug!("Invoking Edge Function: {}", function_name);
137
138        let url = format!("{}/functions/v1/{}", self.config.url, function_name);
139
140        let mut request = self
141            .http_client
142            .post(&url)
143            .header("Authorization", format!("Bearer {}", self.config.key))
144            .header("Content-Type", "application/json");
145
146        // Add custom headers if provided
147        if let Some(custom_headers) = headers {
148            for (key, value) in custom_headers {
149                request = request.header(key, value);
150            }
151        }
152
153        // Add body if provided
154        if let Some(body) = body {
155            request = request.json(&body);
156        }
157
158        let response = request.send().await?;
159
160        if !response.status().is_success() {
161            let status = response.status();
162            let error_msg = match response.text().await {
163                Ok(text) => {
164                    // Try to parse error message from Supabase
165                    if let Ok(error_json) = serde_json::from_str::<JsonValue>(&text) {
166                        if let Some(message) = error_json.get("message") {
167                            message.as_str().unwrap_or(&text).to_string()
168                        } else {
169                            text
170                        }
171                    } else {
172                        text
173                    }
174                }
175                Err(_) => format!("Function invocation failed with status: {}", status),
176            };
177            return Err(Error::functions(error_msg));
178        }
179
180        let result: JsonValue = response.json().await?;
181        info!("Edge Function {} invoked successfully", function_name);
182
183        Ok(result)
184    }
185
186    /// Get the base Functions URL
187    pub fn functions_url(&self) -> String {
188        format!("{}/functions/v1", self.config.url)
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use crate::types::{AuthConfig, DatabaseConfig, HttpConfig, StorageConfig, SupabaseConfig};
196
197    fn create_test_functions() -> Functions {
198        let config = Arc::new(SupabaseConfig {
199            url: "http://localhost:54321".to_string(),
200            key: "test-key".to_string(),
201            service_role_key: None,
202            http_config: HttpConfig::default(),
203            auth_config: AuthConfig::default(),
204            database_config: DatabaseConfig::default(),
205            storage_config: StorageConfig::default(),
206        });
207
208        let http_client = Arc::new(HttpClient::new());
209        Functions::new(config, http_client).unwrap()
210    }
211
212    #[test]
213    fn test_functions_creation() {
214        let functions = create_test_functions();
215        assert_eq!(
216            functions.functions_url(),
217            "http://localhost:54321/functions/v1"
218        );
219    }
220
221    #[test]
222    fn test_functions_url_generation() {
223        let functions = create_test_functions();
224        assert_eq!(
225            functions.functions_url(),
226            "http://localhost:54321/functions/v1"
227        );
228    }
229}