Skip to main content

rp_supabase_graphql/
lib.rs

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/// Request construction, transport, HTTP, or response decoding failure.
14/// GraphQL execution failures instead appear in [`GraphqlResponse::errors`].
15#[derive(Debug, thiserror::Error)]
16pub enum GraphqlClientError {
17    /// The project URL cannot hold a path.
18    #[error("project URL cannot be a base")]
19    UrlNotBase,
20    /// Invalid project URL.
21    #[error(transparent)]
22    Url(#[from] url::ParseError),
23    /// Invalid header name.
24    #[error(transparent)]
25    HeaderName(#[from] reqwest::header::InvalidHeaderName),
26    /// Invalid header value.
27    #[error(transparent)]
28    HeaderValue(#[from] reqwest::header::InvalidHeaderValue),
29    /// Variables or the request could not be serialized.
30    #[error("GraphQL request serialization failed: {0}")]
31    Serialize(serde_json::Error),
32    /// The HTTP request failed.
33    #[error(transparent)]
34    Transport(#[from] reqwest::Error),
35    /// Non-success HTTP status; the body is retained for caller inspection.
36    #[error("GraphQL HTTP error {status}")]
37    Http {
38        /// HTTP response status.
39        status: reqwest::StatusCode,
40        /// Response body.
41        body: String,
42    },
43    /// JSON did not match the response envelope or requested data type.
44    #[error("GraphQL response decoding failed: {0}")]
45    Decode(serde_json::Error),
46    /// Neither data nor a nonempty errors array was returned.
47    #[error("GraphQL response contains neither data nor errors")]
48    EmptyEnvelope,
49}
50
51/// A source position in the GraphQL document (one-based).
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct GraphqlLocation {
54    /// Source line.
55    pub line: u64,
56    /// Source column.
57    pub column: u64,
58}
59
60/// A response field or zero-based list index in an execution error path.
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(untagged)]
63pub enum GraphqlPathSegment {
64    /// Response field name, including aliases.
65    Field(String),
66    /// Zero-based list index.
67    Index(u64),
68}
69
70/// A GraphQL execution or validation error, not an HTTP failure.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct GraphqlError {
73    /// Human-readable error message.
74    pub message: String,
75    /// Source positions, when supplied by the server.
76    #[serde(default)]
77    pub locations: Vec<GraphqlLocation>,
78    /// Response path, when supplied by the server.
79    #[serde(default)]
80    pub path: Vec<GraphqlPathSegment>,
81    /// Implementation-specific error metadata.
82    #[serde(default)]
83    pub extensions: Option<serde_json::Value>,
84}
85
86/// A response that preserves partial data alongside execution errors.
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct GraphqlResponse<T> {
89    /// Typed data, absent for null or omitted data.
90    pub data: Option<T>,
91    /// Execution or validation errors; an empty list means success.
92    #[serde(default)]
93    pub errors: Vec<GraphqlError>,
94    /// Implementation-specific response metadata.
95    pub extensions: Option<serde_json::Value>,
96}
97
98impl<T> GraphqlResponse<T> {
99    /// Reject execution errors while retaining the entire partial response on failure.
100    ///
101    /// # Errors
102    /// Returns this response unchanged if it contains execution errors.
103    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/// A cloneable client for the project's `graphql/v1` endpoint.
113#[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    /// Create a same-origin-redirect-only HTTP client.
123    /// Legacy keys also supply a bearer token; new publishable/secret keys do not.
124    ///
125    /// # Errors
126    /// Returns invalid URL, credential-header, or HTTP client construction errors.
127    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    /// Use an existing HTTP client without changing its TLS or redirect configuration.
136    ///
137    /// # Errors
138    /// Returns invalid URL or credential-header errors.
139    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    /// Clone the client with a user's bearer token, preserving its retry policy.
166    ///
167    /// # Errors
168    /// Returns an error if the token is not a valid header value.
169    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    /// Configure 429 retries. POST requests are never replayed unless explicitly opted in.
177    #[must_use]
178    pub const fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
179        self.retry_policy = policy;
180        self
181    }
182
183    /// Start a POST request. Documents and variables are omitted from builder debug output.
184    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/// Buffered GraphQL request. Serialization/header failures surface when sent.
215#[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    /// Serialize variables once and retain their raw JSON until request serialization.
246    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    /// Select an operation in a multi-operation document.
257    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    /// Replace a request header. All custom values are treated as sensitive.
265    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    /// Merge request headers, marking every supplied value sensitive.
281    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    /// Send and return the raw successful HTTP response, without interpreting execution errors.
290    ///
291    /// # Errors
292    /// Returns construction, serialization, transport, or non-2xx HTTP errors.
293    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    /// Decode typed data and preserve execution errors, including null/partial data.
314    ///
315    /// # Errors
316    /// Returns send errors, malformed envelopes, or data decoding errors.
317    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}