#![cfg_attr(doc, doc = include_str!("../README.md"))]
use core::fmt;
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
pub use rp_supabase_http::RetryPolicy;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue;
pub use url;
use url::Url;
#[derive(Debug, thiserror::Error)]
pub enum GraphqlClientError {
#[error("project URL cannot be a base")]
UrlNotBase,
#[error(transparent)]
Url(#[from] url::ParseError),
#[error(transparent)]
HeaderName(#[from] reqwest::header::InvalidHeaderName),
#[error(transparent)]
HeaderValue(#[from] reqwest::header::InvalidHeaderValue),
#[error("GraphQL request serialization failed: {0}")]
Serialize(serde_json::Error),
#[error(transparent)]
Transport(#[from] reqwest::Error),
#[error("GraphQL HTTP error {status}")]
Http {
status: reqwest::StatusCode,
body: String,
},
#[error("GraphQL response decoding failed: {0}")]
Decode(serde_json::Error),
#[error("GraphQL response contains neither data nor errors")]
EmptyEnvelope,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GraphqlLocation {
pub line: u64,
pub column: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum GraphqlPathSegment {
Field(String),
Index(u64),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GraphqlError {
pub message: String,
#[serde(default)]
pub locations: Vec<GraphqlLocation>,
#[serde(default)]
pub path: Vec<GraphqlPathSegment>,
#[serde(default)]
pub extensions: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GraphqlResponse<T> {
pub data: Option<T>,
#[serde(default)]
pub errors: Vec<GraphqlError>,
pub extensions: Option<serde_json::Value>,
}
impl<T> GraphqlResponse<T> {
pub const fn into_result(self) -> Result<Self, Self> {
if self.errors.is_empty() {
Ok(self)
} else {
Err(self)
}
}
}
#[derive(Debug, Clone)]
pub struct GraphqlClient {
http: reqwest::Client,
endpoint: Url,
headers: HeaderMap,
retry_policy: RetryPolicy,
}
impl GraphqlClient {
pub fn new(project_url: &Url, api_key: &str) -> Result<Self, GraphqlClientError> {
Self::new_with_client(
project_url,
api_key,
rp_supabase_http::client_builder().build()?,
)
}
pub fn new_with_client(
project_url: &Url,
api_key: &str,
http: reqwest::Client,
) -> Result<Self, GraphqlClientError> {
if project_url.cannot_be_a_base() {
return Err(GraphqlClientError::UrlNotBase);
}
let mut root = project_url.clone();
if !root.path().ends_with('/') {
root.set_path(&format!("{}/", root.path()));
}
let endpoint = root.join("graphql/v1")?;
let mut headers = HeaderMap::new();
headers.insert("apikey", sensitive(api_key)?);
if !api_key.starts_with("sb_publishable_") && !api_key.starts_with("sb_secret_") {
headers.insert(AUTHORIZATION, sensitive(&format!("Bearer {api_key}"))?);
}
Ok(Self {
http,
endpoint,
headers,
retry_policy: RetryPolicy::default(),
})
}
pub fn with_access_token(&self, token: &str) -> Result<Self, GraphqlClientError> {
let mut next = self.clone();
next.headers
.insert(AUTHORIZATION, sensitive(&format!("Bearer {token}"))?);
Ok(next)
}
#[must_use]
pub const fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
self.retry_policy = policy;
self
}
pub fn query(&self, document: &str) -> QueryBuilder {
QueryBuilder {
http: self.http.clone(),
endpoint: self.endpoint.clone(),
retry_policy: self.retry_policy,
headers: self.headers.clone(),
state: Ok(RequestBody {
query: document.to_owned(),
variables: None,
operation_name: None,
}),
}
}
}
fn sensitive(value: &str) -> Result<HeaderValue, reqwest::header::InvalidHeaderValue> {
let mut header = HeaderValue::from_str(value)?;
header.set_sensitive(true);
Ok(header)
}
#[derive(Serialize)]
struct RequestBody {
query: String,
#[serde(skip_serializing_if = "Option::is_none")]
variables: Option<Box<RawValue>>,
#[serde(rename = "operationName", skip_serializing_if = "Option::is_none")]
operation_name: Option<String>,
}
#[must_use = "call send or fetch to run the request"]
pub struct QueryBuilder {
http: reqwest::Client,
endpoint: Url,
headers: HeaderMap,
state: Result<RequestBody, GraphqlClientError>,
retry_policy: RetryPolicy,
}
impl fmt::Debug for QueryBuilder {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("QueryBuilder")
.finish_non_exhaustive()
}
}
impl QueryBuilder {
fn update(
mut self,
apply: impl FnOnce(&mut RequestBody) -> Result<(), GraphqlClientError>,
) -> Self {
if let Ok(body) = self.state.as_mut()
&& let Err(error) = apply(body)
{
self.state = Err(error);
}
self
}
pub fn variables<T: Serialize + ?Sized>(self, variables: &T) -> Self {
self.update(|body| {
body.variables = Some(
serde_json::value::to_raw_value(variables)
.map_err(GraphqlClientError::Serialize)?,
);
Ok(())
})
}
pub fn operation_name(self, name: &str) -> Self {
self.update(|body| {
body.operation_name = Some(name.to_owned());
Ok(())
})
}
pub fn header(mut self, name: &str, value: &str) -> Self {
if self.state.is_ok() {
let result = HeaderName::from_bytes(name.as_bytes())
.map_err(GraphqlClientError::from)
.and_then(|name| Ok((name, sensitive(value)?)));
match result {
Ok((name, value)) => {
self.headers.insert(name, value);
}
Err(error) => self.state = Err(error),
}
}
self
}
pub fn headers(mut self, mut headers: HeaderMap) -> Self {
for value in headers.values_mut() {
value.set_sensitive(true);
}
self.headers.extend(headers);
self
}
pub async fn send(self) -> Result<reqwest::Response, GraphqlClientError> {
let body = serde_json::to_vec(&self.state?).map_err(GraphqlClientError::Serialize)?;
let builder = self
.http
.post(self.endpoint)
.headers(self.headers)
.header(CONTENT_TYPE, "application/json")
.body(body);
let (http, request) = builder.build_split();
let response = rp_supabase_http::execute(&http, request?, self.retry_policy).await?;
let status = response.status();
if !status.is_success() {
return Err(GraphqlClientError::Http {
status,
body: response.text().await?,
});
}
Ok(response)
}
pub async fn fetch<T: DeserializeOwned>(
self,
) -> Result<GraphqlResponse<T>, GraphqlClientError> {
let bytes = self.send().await?.bytes().await?;
let envelope: Envelope =
serde_json::from_slice(&bytes).map_err(GraphqlClientError::Decode)?;
if envelope.data.is_none() && envelope.errors.is_empty() {
return Err(GraphqlClientError::EmptyEnvelope);
}
let data = envelope
.data
.map(|raw| serde_json::from_str::<Option<T>>(raw.get()))
.transpose()
.map_err(GraphqlClientError::Decode)?
.flatten();
Ok(GraphqlResponse {
data,
errors: envelope.errors,
extensions: envelope.extensions,
})
}
}
#[derive(Deserialize)]
struct Envelope {
#[serde(default, deserialize_with = "present_data")]
data: Option<Box<RawValue>>,
#[serde(default)]
errors: Vec<GraphqlError>,
#[serde(default)]
extensions: Option<serde_json::Value>,
}
fn present_data<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Box<RawValue>>, D::Error> {
Box::<RawValue>::deserialize(deserializer).map(Some)
}