1#![cfg_attr(doc, doc = include_str!("../README.md"))]
2
3use core::fmt;
4
5use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
6pub use rp_supabase_http::RetryPolicy;
7use serde::de::DeserializeOwned;
8use serde::{Deserialize, Serialize};
9use serde_json::value::RawValue;
10pub use url;
11use url::Url;
12
13#[derive(Debug, thiserror::Error)]
16pub enum GraphqlClientError {
17 #[error("project URL cannot be a base")]
19 UrlNotBase,
20 #[error(transparent)]
22 Url(#[from] url::ParseError),
23 #[error(transparent)]
25 HeaderName(#[from] reqwest::header::InvalidHeaderName),
26 #[error(transparent)]
28 HeaderValue(#[from] reqwest::header::InvalidHeaderValue),
29 #[error("GraphQL request serialization failed: {0}")]
31 Serialize(serde_json::Error),
32 #[error(transparent)]
34 Transport(#[from] reqwest::Error),
35 #[error("GraphQL HTTP error {status}")]
37 Http {
38 status: reqwest::StatusCode,
40 body: String,
42 },
43 #[error("GraphQL response decoding failed: {0}")]
45 Decode(serde_json::Error),
46 #[error("GraphQL response contains neither data nor errors")]
48 EmptyEnvelope,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct GraphqlLocation {
54 pub line: u64,
56 pub column: u64,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(untagged)]
63pub enum GraphqlPathSegment {
64 Field(String),
66 Index(u64),
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct GraphqlError {
73 pub message: String,
75 #[serde(default)]
77 pub locations: Vec<GraphqlLocation>,
78 #[serde(default)]
80 pub path: Vec<GraphqlPathSegment>,
81 #[serde(default)]
83 pub extensions: Option<serde_json::Value>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct GraphqlResponse<T> {
89 pub data: Option<T>,
91 #[serde(default)]
93 pub errors: Vec<GraphqlError>,
94 pub extensions: Option<serde_json::Value>,
96}
97
98impl<T> GraphqlResponse<T> {
99 pub const fn into_result(self) -> Result<Self, Self> {
104 if self.errors.is_empty() {
105 Ok(self)
106 } else {
107 Err(self)
108 }
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct GraphqlClient {
115 http: reqwest::Client,
116 endpoint: Url,
117 headers: HeaderMap,
118 retry_policy: RetryPolicy,
119}
120
121impl GraphqlClient {
122 pub fn new(project_url: &Url, api_key: &str) -> Result<Self, GraphqlClientError> {
128 Self::new_with_client(
129 project_url,
130 api_key,
131 rp_supabase_http::client_builder().build()?,
132 )
133 }
134
135 pub fn new_with_client(
140 project_url: &Url,
141 api_key: &str,
142 http: reqwest::Client,
143 ) -> Result<Self, GraphqlClientError> {
144 if project_url.cannot_be_a_base() {
145 return Err(GraphqlClientError::UrlNotBase);
146 }
147 let mut root = project_url.clone();
148 if !root.path().ends_with('/') {
149 root.set_path(&format!("{}/", root.path()));
150 }
151 let endpoint = root.join("graphql/v1")?;
152 let mut headers = HeaderMap::new();
153 headers.insert("apikey", sensitive(api_key)?);
154 if !api_key.starts_with("sb_publishable_") && !api_key.starts_with("sb_secret_") {
155 headers.insert(AUTHORIZATION, sensitive(&format!("Bearer {api_key}"))?);
156 }
157 Ok(Self {
158 http,
159 endpoint,
160 headers,
161 retry_policy: RetryPolicy::default(),
162 })
163 }
164
165 pub fn with_access_token(&self, token: &str) -> Result<Self, GraphqlClientError> {
170 let mut next = self.clone();
171 next.headers
172 .insert(AUTHORIZATION, sensitive(&format!("Bearer {token}"))?);
173 Ok(next)
174 }
175
176 #[must_use]
178 pub const fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
179 self.retry_policy = policy;
180 self
181 }
182
183 pub fn query(&self, document: &str) -> QueryBuilder {
185 QueryBuilder {
186 http: self.http.clone(),
187 endpoint: self.endpoint.clone(),
188 retry_policy: self.retry_policy,
189 headers: self.headers.clone(),
190 state: Ok(RequestBody {
191 query: document.to_owned(),
192 variables: None,
193 operation_name: None,
194 }),
195 }
196 }
197}
198
199fn sensitive(value: &str) -> Result<HeaderValue, reqwest::header::InvalidHeaderValue> {
200 let mut header = HeaderValue::from_str(value)?;
201 header.set_sensitive(true);
202 Ok(header)
203}
204
205#[derive(Serialize)]
206struct RequestBody {
207 query: String,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 variables: Option<Box<RawValue>>,
210 #[serde(rename = "operationName", skip_serializing_if = "Option::is_none")]
211 operation_name: Option<String>,
212}
213
214#[must_use = "call send or fetch to run the request"]
216pub struct QueryBuilder {
217 http: reqwest::Client,
218 endpoint: Url,
219 headers: HeaderMap,
220 state: Result<RequestBody, GraphqlClientError>,
221 retry_policy: RetryPolicy,
222}
223
224impl fmt::Debug for QueryBuilder {
225 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
226 formatter
227 .debug_struct("QueryBuilder")
228 .finish_non_exhaustive()
229 }
230}
231
232impl QueryBuilder {
233 fn update(
234 mut self,
235 apply: impl FnOnce(&mut RequestBody) -> Result<(), GraphqlClientError>,
236 ) -> Self {
237 if let Ok(body) = self.state.as_mut()
238 && let Err(error) = apply(body)
239 {
240 self.state = Err(error);
241 }
242 self
243 }
244
245 pub fn variables<T: Serialize + ?Sized>(self, variables: &T) -> Self {
247 self.update(|body| {
248 body.variables = Some(
249 serde_json::value::to_raw_value(variables)
250 .map_err(GraphqlClientError::Serialize)?,
251 );
252 Ok(())
253 })
254 }
255
256 pub fn operation_name(self, name: &str) -> Self {
258 self.update(|body| {
259 body.operation_name = Some(name.to_owned());
260 Ok(())
261 })
262 }
263
264 pub fn header(mut self, name: &str, value: &str) -> Self {
266 if self.state.is_ok() {
267 let result = HeaderName::from_bytes(name.as_bytes())
268 .map_err(GraphqlClientError::from)
269 .and_then(|name| Ok((name, sensitive(value)?)));
270 match result {
271 Ok((name, value)) => {
272 self.headers.insert(name, value);
273 }
274 Err(error) => self.state = Err(error),
275 }
276 }
277 self
278 }
279
280 pub fn headers(mut self, mut headers: HeaderMap) -> Self {
282 for value in headers.values_mut() {
283 value.set_sensitive(true);
284 }
285 self.headers.extend(headers);
286 self
287 }
288
289 pub async fn send(self) -> Result<reqwest::Response, GraphqlClientError> {
294 let body = serde_json::to_vec(&self.state?).map_err(GraphqlClientError::Serialize)?;
295 let builder = self
296 .http
297 .post(self.endpoint)
298 .headers(self.headers)
299 .header(CONTENT_TYPE, "application/json")
300 .body(body);
301 let (http, request) = builder.build_split();
302 let response = rp_supabase_http::execute(&http, request?, self.retry_policy).await?;
303 let status = response.status();
304 if !status.is_success() {
305 return Err(GraphqlClientError::Http {
306 status,
307 body: response.text().await?,
308 });
309 }
310 Ok(response)
311 }
312
313 pub async fn fetch<T: DeserializeOwned>(
318 self,
319 ) -> Result<GraphqlResponse<T>, GraphqlClientError> {
320 let bytes = self.send().await?.bytes().await?;
321 let envelope: Envelope =
322 serde_json::from_slice(&bytes).map_err(GraphqlClientError::Decode)?;
323 if envelope.data.is_none() && envelope.errors.is_empty() {
324 return Err(GraphqlClientError::EmptyEnvelope);
325 }
326 let data = envelope
327 .data
328 .map(|raw| serde_json::from_str::<Option<T>>(raw.get()))
329 .transpose()
330 .map_err(GraphqlClientError::Decode)?
331 .flatten();
332 Ok(GraphqlResponse {
333 data,
334 errors: envelope.errors,
335 extensions: envelope.extensions,
336 })
337 }
338}
339
340#[derive(Deserialize)]
341struct Envelope {
342 #[serde(default, deserialize_with = "present_data")]
343 data: Option<Box<RawValue>>,
344 #[serde(default)]
345 errors: Vec<GraphqlError>,
346 #[serde(default)]
347 extensions: Option<serde_json::Value>,
348}
349
350fn present_data<'de, D: serde::Deserializer<'de>>(
351 deserializer: D,
352) -> Result<Option<Box<RawValue>>, D::Error> {
353 Box::<RawValue>::deserialize(deserializer).map(Some)
354}