supabase_client_graphql/
client.rs1use std::sync::{Arc, RwLock};
2
3use reqwest::header::{HeaderMap, HeaderValue};
4use serde::de::DeserializeOwned;
5use serde_json::Value;
6use tracing::debug;
7use url::Url;
8
9use crate::error::GraphqlError;
10use crate::mutation::{MutationBuilder, MutationKind as BuilderMutationKind};
11use crate::query::QueryBuilder;
12use crate::types::GraphqlResponse;
13
14#[derive(Debug, Clone)]
35pub struct GraphqlClient {
36 http: reqwest::Client,
37 base_url: Url,
38 api_key: String,
39 auth_override: Arc<RwLock<Option<String>>>,
41}
42
43impl GraphqlClient {
44 pub fn new(supabase_url: &str, api_key: &str) -> Result<Self, GraphqlError> {
49 let base = supabase_url.trim_end_matches('/');
50 let base_url = Url::parse(&format!("{}/graphql/v1", base))?;
51
52 let mut default_headers = HeaderMap::new();
53 default_headers.insert(
54 "apikey",
55 HeaderValue::from_str(api_key).map_err(|e| {
56 GraphqlError::InvalidConfig(format!("Invalid API key header: {}", e))
57 })?,
58 );
59 default_headers.insert(
60 reqwest::header::AUTHORIZATION,
61 HeaderValue::from_str(&format!("Bearer {}", api_key)).map_err(|e| {
62 GraphqlError::InvalidConfig(format!("Invalid auth header: {}", e))
63 })?,
64 );
65 default_headers.insert(
66 reqwest::header::CONTENT_TYPE,
67 HeaderValue::from_static("application/json"),
68 );
69
70 let http = reqwest::Client::builder()
71 .default_headers(default_headers)
72 .build()
73 .map_err(GraphqlError::Http)?;
74
75 Ok(Self {
76 http,
77 base_url,
78 api_key: api_key.to_string(),
79 auth_override: Arc::new(RwLock::new(None)),
80 })
81 }
82
83 pub fn base_url(&self) -> &Url {
85 &self.base_url
86 }
87
88 pub fn api_key(&self) -> &str {
90 &self.api_key
91 }
92
93 pub fn set_auth(&self, token: &str) {
97 let mut auth = self.auth_override.write().unwrap();
98 *auth = Some(token.to_string());
99 }
100
101 pub async fn execute<T: DeserializeOwned>(
111 &self,
112 query: &str,
113 variables: Option<Value>,
114 operation_name: Option<&str>,
115 ) -> Result<GraphqlResponse<T>, GraphqlError> {
116 let mut body = serde_json::json!({ "query": query });
117
118 if let Some(vars) = variables {
119 body["variables"] = vars;
120 }
121 if let Some(op) = operation_name {
122 body["operationName"] = Value::String(op.to_string());
123 }
124
125 debug!(query = query, "Executing GraphQL query");
126
127 let mut request = self.http.post(self.base_url.as_str()).json(&body);
128
129 if let Some(ref token) = *self.auth_override.read().unwrap() {
131 request = request.header(
132 reqwest::header::AUTHORIZATION,
133 HeaderValue::from_str(&format!("Bearer {}", token)).map_err(|e| {
134 GraphqlError::InvalidConfig(format!("Invalid auth override header: {}", e))
135 })?,
136 );
137 }
138
139 let response = request.send().await?;
140 let status = response.status().as_u16();
141
142 if status >= 400 {
143 let body_text = response.text().await.unwrap_or_default();
144 debug!(status, body = %body_text, "GraphQL HTTP error");
145 return Err(GraphqlError::HttpError {
146 status,
147 message: body_text,
148 });
149 }
150
151 let gql_response: GraphqlResponse<T> = response.json().await?;
152
153 if !gql_response.errors.is_empty() && gql_response.data.is_none() {
155 return Err(GraphqlError::GraphqlErrors(gql_response.errors));
156 }
157
158 Ok(gql_response)
159 }
160
161 pub async fn execute_raw(
166 &self,
167 query: &str,
168 variables: Option<Value>,
169 operation_name: Option<&str>,
170 ) -> Result<GraphqlResponse<Value>, GraphqlError> {
171 self.execute(query, variables, operation_name).await
172 }
173
174 pub fn collection(&self, name: &str) -> QueryBuilder {
184 QueryBuilder::new(self.clone(), name.to_string())
185 }
186
187 pub fn insert_into(&self, collection: &str) -> MutationBuilder {
197 MutationBuilder::new(
198 self.clone(),
199 collection.to_string(),
200 BuilderMutationKind::Insert,
201 )
202 }
203
204 pub fn update(&self, collection: &str) -> MutationBuilder {
216 MutationBuilder::new(
217 self.clone(),
218 collection.to_string(),
219 BuilderMutationKind::Update,
220 )
221 }
222
223 pub fn delete_from(&self, collection: &str) -> MutationBuilder {
234 MutationBuilder::new(
235 self.clone(),
236 collection.to_string(),
237 BuilderMutationKind::Delete,
238 )
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn client_new_ok() {
248 let client = GraphqlClient::new("https://example.supabase.co", "test-key");
249 assert!(client.is_ok());
250 }
251
252 #[test]
253 fn client_base_url() {
254 let client = GraphqlClient::new("https://example.supabase.co", "test-key").unwrap();
255 assert_eq!(client.base_url().path(), "/graphql/v1");
256 }
257
258 #[test]
259 fn client_base_url_trailing_slash() {
260 let client = GraphqlClient::new("https://example.supabase.co/", "test-key").unwrap();
261 assert_eq!(client.base_url().path(), "/graphql/v1");
262 }
263
264 #[test]
265 fn client_api_key() {
266 let client = GraphqlClient::new("https://example.supabase.co", "my-key").unwrap();
267 assert_eq!(client.api_key(), "my-key");
268 }
269
270 #[test]
271 fn set_auth_updates_override() {
272 let client = GraphqlClient::new("https://example.supabase.co", "test-key").unwrap();
273 assert!(client.auth_override.read().unwrap().is_none());
274 client.set_auth("new-token");
275 assert_eq!(
276 client.auth_override.read().unwrap().as_deref(),
277 Some("new-token")
278 );
279 }
280
281 #[test]
282 fn set_auth_clone_shares_state() {
283 let client = GraphqlClient::new("https://example.supabase.co", "test-key").unwrap();
284 let clone = client.clone();
285 client.set_auth("shared-token");
286 assert_eq!(
287 clone.auth_override.read().unwrap().as_deref(),
288 Some("shared-token")
289 );
290 }
291}