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}