use reqwest::{Method, Response};
use serde::{de::DeserializeOwned, Serialize};
use crate::Supabase;
#[must_use = "a QueryBuilder does nothing until .execute() or .execute_and_parse() is called"]
pub struct QueryBuilder<'a> {
client: &'a Supabase,
table: String,
query_params: Vec<(String, String)>,
method: Method,
body: Option<String>,
}
impl<'a> QueryBuilder<'a> {
pub(crate) fn new(client: &'a Supabase, table: impl Into<String>) -> Self {
Self {
client,
table: table.into(),
query_params: Vec::new(),
method: Method::GET,
body: None,
}
}
pub fn select(mut self, columns: impl Into<String>) -> Self {
self.query_params.push(("select".into(), columns.into()));
self.method = Method::GET;
self
}
pub fn insert<T: Serialize>(mut self, data: &T) -> Result<Self, crate::Error> {
self.method = Method::POST;
self.body = Some(serde_json::to_string(data)?);
Ok(self)
}
pub fn update<T: Serialize>(mut self, data: &T) -> Result<Self, crate::Error> {
self.method = Method::PATCH;
self.body = Some(serde_json::to_string(data)?);
Ok(self)
}
pub fn delete(mut self) -> Self {
self.method = Method::DELETE;
self
}
pub fn eq(self, column: impl Into<String>, value: impl Into<String>) -> Self {
self.add_filter(column, "eq", value)
}
pub fn neq(self, column: impl Into<String>, value: impl Into<String>) -> Self {
self.add_filter(column, "neq", value)
}
pub fn gt(self, column: impl Into<String>, value: impl Into<String>) -> Self {
self.add_filter(column, "gt", value)
}
pub fn gte(self, column: impl Into<String>, value: impl Into<String>) -> Self {
self.add_filter(column, "gte", value)
}
pub fn lt(self, column: impl Into<String>, value: impl Into<String>) -> Self {
self.add_filter(column, "lt", value)
}
pub fn lte(self, column: impl Into<String>, value: impl Into<String>) -> Self {
self.add_filter(column, "lte", value)
}
pub fn like(self, column: impl Into<String>, pattern: impl Into<String>) -> Self {
self.add_filter(column, "like", pattern)
}
pub fn ilike(self, column: impl Into<String>, pattern: impl Into<String>) -> Self {
self.add_filter(column, "ilike", pattern)
}
pub fn in_<I, S>(mut self, column: impl Into<String>, values: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let values_str: Vec<_> = values.into_iter().map(|s| s.as_ref().to_string()).collect();
self.query_params
.push((column.into(), format!("in.({})", values_str.join(","))));
self
}
pub fn is_null(mut self, column: impl Into<String>) -> Self {
self.query_params.push((column.into(), "is.null".into()));
self
}
pub fn not_null(mut self, column: impl Into<String>) -> Self {
self.query_params.push((column.into(), "not.is.null".into()));
self
}
pub fn order(mut self, column: impl Into<String>) -> Self {
self.query_params.push(("order".into(), column.into()));
self
}
pub fn limit(mut self, count: usize) -> Self {
self.query_params.push(("limit".into(), count.to_string()));
self
}
pub fn offset(mut self, count: usize) -> Self {
self.query_params.push(("offset".into(), count.to_string()));
self
}
pub async fn execute(self) -> Result<Response, crate::Error> {
let url = format!("{}/rest/v1/{}", self.client.url, self.table);
let mut request = self
.client
.client
.request(self.method, &url)
.header("apikey", &self.client.api_key)
.header("Content-Type", "application/json");
if let Some(ref token) = self.client.bearer_token {
request = request.bearer_auth(token);
}
if !self.query_params.is_empty() {
request = request.query(&self.query_params);
}
if let Some(body) = self.body {
request = request.body(body);
}
let resp = request.send().await?;
let status = resp.status().as_u16();
if !(200..300).contains(&status) {
let message = resp.text().await.unwrap_or_default();
return Err(crate::Error::Api { status, message });
}
Ok(resp)
}
pub async fn execute_and_parse<T: DeserializeOwned>(self) -> Result<T, crate::Error> {
let resp = self.execute().await?;
let body = resp.text().await?;
let parsed: T = serde_json::from_str(&body)?;
Ok(parsed)
}
fn add_filter(
mut self,
column: impl Into<String>,
op: &str,
value: impl Into<String>,
) -> Self {
self.query_params
.push((column.into(), format!("{op}.{}", value.into())));
self
}
}
impl Supabase {
pub fn from(&self, table: impl Into<String>) -> QueryBuilder<'_> {
QueryBuilder::new(self, table)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
fn client() -> Supabase {
Supabase::new(None, None, None).unwrap_or_else(|_| {
Supabase::new(
Some("https://example.supabase.co"),
Some("test-key"),
None,
)
.unwrap()
})
}
fn is_acceptable_error(err: &crate::Error) -> bool {
matches!(
err,
crate::Error::Request(_) | crate::Error::Api { status: 401, .. }
)
}
#[derive(Debug, Serialize, Deserialize)]
struct TestItem {
name: String,
value: i32,
}
#[tokio::test]
async fn test_select() {
let client = client();
match client.from("test_items").select("*").execute().await {
Ok(_resp) => {}
Err(e) if is_acceptable_error(&e) => {
println!("Test skipped: {e}");
}
Err(e) => panic!("unexpected error: {e}"),
}
}
#[tokio::test]
async fn test_select_columns() {
let client = client();
match client.from("test_items").select("id,name").execute().await {
Ok(_resp) => {}
Err(e) if is_acceptable_error(&e) => {
println!("Test skipped: {e}");
}
Err(e) => panic!("unexpected error: {e}"),
}
}
#[tokio::test]
async fn test_select_with_filter() {
let client = client();
match client
.from("test_items")
.select("*")
.eq("name", "test")
.execute()
.await
{
Ok(_resp) => {}
Err(e) if is_acceptable_error(&e) => {
println!("Test skipped: {e}");
}
Err(e) => panic!("unexpected error: {e}"),
}
}
#[tokio::test]
async fn test_insert() {
let client = client();
let item = TestItem {
name: "test_item".into(),
value: 42,
};
match client
.from("test_items")
.insert(&item)
.expect("serialization should succeed")
.execute()
.await
{
Ok(_resp) => {}
Err(e) if is_acceptable_error(&e) => {
println!("Test skipped: {e}");
}
Err(e) => panic!("unexpected error: {e}"),
}
}
#[tokio::test]
async fn test_update() {
let client = client();
let updates = serde_json::json!({ "value": 100 });
match client
.from("test_items")
.update(&updates)
.expect("serialization should succeed")
.eq("name", "test_item")
.execute()
.await
{
Ok(_resp) => {}
Err(e) if is_acceptable_error(&e) => {
println!("Test skipped: {e}");
}
Err(e) => panic!("unexpected error: {e}"),
}
}
#[tokio::test]
async fn test_delete() {
let client = client();
match client
.from("test_items")
.delete()
.eq("name", "test_item")
.execute()
.await
{
Ok(_resp) => {}
Err(e) if is_acceptable_error(&e) => {
println!("Test skipped: {e}");
}
Err(e) => panic!("unexpected error: {e}"),
}
}
#[tokio::test]
async fn test_select_with_order_and_limit() {
let client = client();
match client
.from("test_items")
.select("*")
.order("id.desc")
.limit(10)
.execute()
.await
{
Ok(_resp) => {}
Err(e) if is_acceptable_error(&e) => {
println!("Test skipped: {e}");
}
Err(e) => panic!("unexpected error: {e}"),
}
}
#[tokio::test]
async fn test_select_with_multiple_filters() {
let client = client();
match client
.from("test_items")
.select("*")
.gte("value", "10")
.lte("value", "100")
.execute()
.await
{
Ok(_resp) => {}
Err(e) if is_acceptable_error(&e) => {
println!("Test skipped: {e}");
}
Err(e) => panic!("unexpected error: {e}"),
}
}
#[tokio::test]
async fn test_in_filter() {
let client = client();
match client
.from("test_items")
.select("*")
.in_("id", ["1", "2", "3"])
.execute()
.await
{
Ok(_resp) => {}
Err(e) if is_acceptable_error(&e) => {
println!("Test skipped: {e}");
}
Err(e) => panic!("unexpected error: {e}"),
}
}
#[test]
fn test_error_display() {
let err = crate::Error::Api {
status: 400,
message: "bad request".into(),
};
assert!(format!("{err}").contains("400"));
}
}