use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::fmt;
use thiserror::Error;
use url::Url;
use serde_json::json;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct PostgrestApiErrorDetails {
pub code: Option<String>,
pub message: Option<String>,
pub details: Option<String>,
pub hint: Option<String>,
}
impl fmt::Display for PostgrestApiErrorDetails {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts = Vec::new();
if let Some(code) = &self.code {
parts.push(format!("Code: {}", code));
}
if let Some(message) = &self.message {
parts.push(format!("Message: {}", message));
}
if let Some(details) = &self.details {
parts.push(format!("Details: {}", details));
}
if let Some(hint) = &self.hint {
parts.push(format!("Hint: {}", hint));
}
write!(f, "{}", parts.join(", "))
}
}
#[derive(Error, Debug)]
pub enum PostgrestError {
#[error("API error: {details} (Status: {status})")]
ApiError {
details: PostgrestApiErrorDetails,
status: reqwest::StatusCode,
},
#[error("API error (unparsed): {message} (Status: {status})")]
UnparsedApiError {
message: String,
status: reqwest::StatusCode,
},
#[error("Network error: {0}")]
NetworkError(#[from] reqwest::Error),
#[error("URL parse error: {0}")]
UrlParseError(#[from] url::ParseError),
#[error("JSON serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("Invalid parameters: {0}")]
InvalidParameters(String),
#[error("Transaction error: {0}")]
TransactionError(String),
#[error("Deserialization error: {0}")]
DeserializationError(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortOrder {
Ascending,
Descending,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsolationLevel {
ReadCommitted,
RepeatableRead,
Serializable,
}
impl IsolationLevel {
fn display(&self) -> &'static str {
match self {
IsolationLevel::ReadCommitted => "read committed",
IsolationLevel::RepeatableRead => "repeatable read",
IsolationLevel::Serializable => "serializable",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransactionMode {
ReadWrite,
ReadOnly,
}
impl TransactionMode {
fn display(&self) -> &'static str {
match self {
TransactionMode::ReadWrite => "read write",
TransactionMode::ReadOnly => "read only",
}
}
}
#[allow(dead_code)]
enum TransactionState {
Inactive,
Active,
Committed,
RolledBack,
}
pub struct PostgrestClient {
base_url: String,
api_key: String,
table: String,
http_client: Client,
headers: HeaderMap,
query_params: HashMap<String, String>,
#[allow(dead_code)]
path: Option<String>,
#[allow(dead_code)]
is_rpc: bool,
#[allow(dead_code)]
rpc_params: Option<Value>,
}
impl PostgrestClient {
pub fn new(base_url: &str, api_key: &str, table: &str, http_client: Client) -> Self {
let mut headers = HeaderMap::new();
headers.insert("apikey", HeaderValue::from_str(api_key).unwrap());
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
Self {
base_url: base_url.to_string(),
api_key: api_key.to_string(),
table: table.to_string(),
http_client,
headers,
query_params: HashMap::new(),
path: None,
is_rpc: false,
rpc_params: None,
}
}
pub fn rpc(
base_url: &str,
api_key: &str,
function_name: &str,
params: Value,
http_client: Client,
) -> Self {
let mut headers = HeaderMap::new();
headers.insert("apikey", HeaderValue::from_str(api_key).unwrap());
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
Self {
base_url: base_url.to_string(),
api_key: api_key.to_string(),
table: function_name.to_string(),
http_client,
headers,
query_params: HashMap::new(),
path: None,
is_rpc: true,
rpc_params: Some(params),
}
}
pub fn with_header(mut self, key: &str, value: &str) -> Result<Self, PostgrestError> {
let header_value = HeaderValue::from_str(value).map_err(|_| {
PostgrestError::InvalidParameters(format!("Invalid header value: {}", value))
})?;
let header_name = HeaderName::from_bytes(key.as_bytes()).map_err(|_| {
PostgrestError::InvalidParameters(format!("Invalid header name: {}", key))
})?;
self.headers.insert(header_name, header_value);
Ok(self)
}
pub fn with_auth(self, token: &str) -> Result<Self, PostgrestError> {
self.with_header("Authorization", &format!("Bearer {}", token))
}
pub fn select(mut self, columns: &str) -> Self {
self.query_params
.insert("select".to_string(), columns.to_string());
self
}
pub fn inner_join(mut self, foreign_table: &str, column: &str, foreign_column: &str) -> Self {
let current_select = self
.query_params
.get("select")
.cloned()
.unwrap_or_else(|| "*".to_string());
let new_select = if current_select == "*" {
format!("*,{}!inner({})", foreign_table, foreign_column)
} else {
format!(
"{},{},{}!inner({})",
current_select, column, foreign_table, foreign_column
)
};
self.query_params.insert("select".to_string(), new_select);
self
}
pub fn left_join(mut self, foreign_table: &str, column: &str, foreign_column: &str) -> Self {
let current_select = self
.query_params
.get("select")
.cloned()
.unwrap_or_else(|| "*".to_string());
let new_select = if current_select == "*" {
format!("*,{}!left({})", foreign_table, foreign_column)
} else {
format!(
"{},{},{}!left({})",
current_select, column, foreign_table, foreign_column
)
};
self.query_params.insert("select".to_string(), new_select);
self
}
pub fn include(
mut self,
foreign_table: &str,
_foreign_column: &str,
columns: Option<&str>,
) -> Self {
let current_select = self
.query_params
.get("select")
.cloned()
.unwrap_or_else(|| "*".to_string());
let columns_str = columns.unwrap_or("*");
let new_select = if current_select == "*" {
format!("*,{}({})", foreign_table, columns_str)
} else {
format!("{},{}({})", current_select, foreign_table, columns_str)
};
self.query_params.insert("select".to_string(), new_select);
self
}
pub fn referenced_by(mut self, foreign_table: &str, foreign_column: &str) -> Self {
let current_select = self
.query_params
.get("select")
.cloned()
.unwrap_or_else(|| "*".to_string());
let new_select = if current_select == "*" {
format!("*,{}!fk({})", foreign_table, foreign_column)
} else {
format!(
"{},{}!fk({})",
current_select, foreign_table, foreign_column
)
};
self.query_params.insert("select".to_string(), new_select);
self
}
pub fn eq(mut self, column: &str, value: &str) -> Self {
self.query_params
.insert(column.to_string(), format!("eq.{}", value));
self
}
pub fn gt(mut self, column: &str, value: &str) -> Self {
self.query_params
.insert(column.to_string(), format!("gt.{}", value));
self
}
pub fn gte(mut self, column: &str, value: &str) -> Self {
self.query_params
.insert(column.to_string(), format!("gte.{}", value));
self
}
pub fn lt(mut self, column: &str, value: &str) -> Self {
self.query_params
.insert(column.to_string(), format!("lt.{}", value));
self
}
pub fn lte(mut self, column: &str, value: &str) -> Self {
self.query_params
.insert(column.to_string(), format!("lte.{}", value));
self
}
pub fn like(mut self, column: &str, pattern: &str) -> Self {
self.query_params
.insert(column.to_string(), format!("like.{}", pattern));
self
}
pub fn ilike(mut self, column: &str, pattern: &str) -> Self {
self.query_params
.insert(column.to_string(), format!("ilike.{}", pattern));
self
}
pub fn in_list(mut self, column: &str, values: &[&str]) -> Self {
let value_list = values.join(",");
self.query_params
.insert(column.to_string(), format!("in.({})", value_list));
self
}
pub fn not(mut self, column: &str, operator_with_value: &str) -> Self {
self.query_params
.insert(column.to_string(), format!("not.{}", operator_with_value));
self
}
pub fn contains(mut self, column: &str, value: &Value) -> Result<Self, PostgrestError> {
let value_str = serde_json::to_string(value)?;
self.query_params
.insert(column.to_string(), format!("cs.{}", value_str));
Ok(self)
}
pub fn contained_by(mut self, column: &str, value: &Value) -> Result<Self, PostgrestError> {
let value_str = serde_json::to_string(value)?;
self.query_params
.insert(column.to_string(), format!("cd.{}", value_str));
Ok(self)
}
pub fn order(mut self, column: &str, order: SortOrder) -> Self {
let order_str = match order {
SortOrder::Ascending => "asc",
SortOrder::Descending => "desc",
};
self.query_params
.insert("order".to_string(), format!("{}.{}", column, order_str));
self
}
pub fn limit(mut self, count: i32) -> Self {
self.query_params
.insert("limit".to_string(), count.to_string());
self
}
pub fn offset(mut self, count: i32) -> Self {
self.query_params
.insert("offset".to_string(), count.to_string());
self
}
pub fn text_search(mut self, column: &str, query: &str, config: Option<&str>) -> Self {
let search_param = match config {
Some(cfg) => format!("fts({}).{}", cfg, query),
None => format!("fts.{}", query),
};
self.query_params.insert(column.to_string(), search_param);
self
}
pub fn geo_distance(
mut self,
column: &str,
lat: f64,
lng: f64,
distance: f64,
unit: &str,
) -> Self {
self.query_params.insert(
column.to_string(),
format!("st_dwithin.POINT({} {}).{}.{}", lng, lat, distance, unit),
);
self
}
pub fn group_by(mut self, columns: &str) -> Self {
self.query_params
.insert("group".to_string(), columns.to_string());
self
}
pub fn count(mut self, exact: bool) -> Self {
let count_method = if exact { "exact" } else { "planned" };
self.query_params
.insert("count".to_string(), count_method.to_string());
self
}
pub fn ignore_rls(mut self) -> Self {
self.headers.insert(
reqwest::header::HeaderName::from_static("x-supabase-admin-role"),
reqwest::header::HeaderValue::from_static("service_role"),
);
self
}
pub fn schema(mut self, schema_name: &str) -> Self {
self.query_params
.insert("schema".to_string(), schema_name.to_string());
self
}
pub async fn export_csv(&self) -> Result<String, PostgrestError> {
let mut url = self.build_url()?;
if url.contains('?') {
url.push('&');
} else {
url.push('?');
}
url.push_str("accept=text/csv");
let mut headers = self.headers.clone();
headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("text/csv"),
);
let response = self.http_client.get(url).headers(headers).send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await?;
let details = serde_json::from_str::<PostgrestApiErrorDetails>(&error_text)
.unwrap_or_else(|_| PostgrestApiErrorDetails {
code: None,
message: Some(error_text.clone()),
details: None,
hint: None,
});
return Err(PostgrestError::ApiError { details, status });
}
let csv_data = response.text().await?;
Ok(csv_data)
}
pub async fn execute<T: for<'de> Deserialize<'de>>(&self) -> Result<Vec<T>, PostgrestError> {
let url = self.build_url()?;
let response = self
.http_client
.get(&url)
.headers(self.headers.clone())
.send()
.await
.map_err(PostgrestError::NetworkError)?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
if let Ok(details) = serde_json::from_str::<PostgrestApiErrorDetails>(&error_text) {
return Err(PostgrestError::ApiError { details, status });
} else {
return Err(PostgrestError::UnparsedApiError {
message: error_text,
status,
});
}
}
response
.json::<Vec<T>>()
.await
.map_err(|e| PostgrestError::DeserializationError(e.to_string()))
}
pub async fn insert<T: Serialize>(&self, values: T) -> Result<Value, PostgrestError> {
let url = self.build_url()?;
let mut headers = self.headers.clone();
headers.insert(
HeaderName::from_static("prefer"),
HeaderValue::from_static("return=representation"),
);
let response = self
.http_client
.post(&url)
.headers(headers) .json(&values)
.send()
.await
.map_err(PostgrestError::NetworkError)?;
let status = response.status();
if status.is_success() {
let body_text = response.text().await.map_err(|e| {
PostgrestError::DeserializationError(format!("Failed to read response body: {}", e))
})?;
if body_text.trim().is_empty() {
Ok(Value::Null)
} else {
serde_json::from_str::<Value>(&body_text)
.map_err(|e| PostgrestError::DeserializationError(e.to_string()))
}
} else {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
let details_result: Result<PostgrestApiErrorDetails, _> =
serde_json::from_str(&error_text);
match details_result {
Ok(details) => Err(PostgrestError::ApiError { details, status }),
Err(_) => Err(PostgrestError::UnparsedApiError {
message: error_text,
status,
}),
}
}
}
pub async fn update<T: Serialize>(&self, values: T) -> Result<Value, PostgrestError> {
let url = self.build_url()?;
let mut headers = self.headers.clone();
headers.insert(
HeaderName::from_static("prefer"),
HeaderValue::from_static("return=representation"),
);
let response = self
.http_client
.patch(&url)
.headers(headers) .json(&values)
.send()
.await
.map_err(PostgrestError::NetworkError)?;
let status = response.status();
if status.is_success() {
let body_text = response.text().await.map_err(|e| {
PostgrestError::DeserializationError(format!("Failed to read response body: {}", e))
})?;
if body_text.trim().is_empty() {
Ok(Value::Null)
} else {
serde_json::from_str::<Value>(&body_text)
.map_err(|e| PostgrestError::DeserializationError(e.to_string()))
}
} else {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
let details_result: Result<PostgrestApiErrorDetails, _> =
serde_json::from_str(&error_text);
match details_result {
Ok(details) => Err(PostgrestError::ApiError { details, status }),
Err(_) => Err(PostgrestError::UnparsedApiError {
message: error_text,
status,
}),
}
}
}
pub async fn delete(&self) -> Result<Value, PostgrestError> {
let url = self.build_url()?;
let mut headers = self.headers.clone();
headers.insert(
HeaderName::from_static("prefer"),
HeaderValue::from_static("return=representation"),
);
let response = self
.http_client
.delete(&url)
.headers(headers) .send()
.await
.map_err(PostgrestError::NetworkError)?;
let status = response.status();
if status.is_success() {
let body_text = response.text().await.map_err(|e| {
PostgrestError::DeserializationError(format!("Failed to read response body: {}", e))
})?;
if body_text.trim().is_empty() {
Ok(Value::Null)
} else {
serde_json::from_str::<Value>(&body_text)
.map_err(|e| PostgrestError::DeserializationError(e.to_string()))
}
} else {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
let details_result: Result<PostgrestApiErrorDetails, _> =
serde_json::from_str(&error_text);
match details_result {
Ok(details) => Err(PostgrestError::ApiError { details, status }),
Err(_) => Err(PostgrestError::UnparsedApiError {
message: error_text,
status,
}),
}
}
}
pub async fn call_rpc<T: for<'de> Deserialize<'de>>(&self) -> Result<T, PostgrestError> {
if !self.is_rpc {
return Err(PostgrestError::InvalidParameters(
"Client was not created for RPC. Use PostgrestClient::rpc().".to_string(),
));
}
let url = format!("{}/rest/v1/rpc/{}", self.base_url, self.table);
let params = self.rpc_params.as_ref().ok_or_else(|| {
PostgrestError::InvalidParameters("RPC parameters are missing.".to_string())
})?;
let response = self
.http_client
.post(&url)
.headers(self.headers.clone())
.json(params)
.send()
.await
.map_err(PostgrestError::NetworkError)?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
let details_result: Result<PostgrestApiErrorDetails, _> =
serde_json::from_str(&error_text);
return match details_result {
Ok(details) => Err(PostgrestError::ApiError { details, status }),
Err(_) => Err(PostgrestError::UnparsedApiError {
message: error_text,
status,
}),
};
}
response.json::<T>().await.map_err(|e| {
PostgrestError::DeserializationError(format!(
"Failed to deserialize RPC response: {}",
e
))
})
}
fn build_url(&self) -> Result<String, PostgrestError> {
let mut url = Url::parse(&format!("{}/rest/v1/{}", self.base_url, self.table))?;
for (key, value) in &self.query_params {
url.query_pairs_mut().append_pair(key, value);
}
Ok(url.to_string())
}
pub async fn begin_transaction(
&self,
isolation_level: Option<IsolationLevel>,
transaction_mode: Option<TransactionMode>,
timeout_seconds: Option<u64>,
) -> Result<PostgrestTransaction, PostgrestError> {
let isolation = isolation_level.unwrap_or(IsolationLevel::ReadCommitted);
let mode = transaction_mode.unwrap_or(TransactionMode::ReadWrite);
let mut request_body = json!({
"isolation_level": isolation.display(),
"mode": mode.display(),
});
if let Some(timeout) = timeout_seconds {
request_body["timeout_seconds"] = json!(timeout);
}
let transaction_url = format!("{}/rpc/begin_transaction", self.base_url);
let response = self
.http_client
.post(&transaction_url)
.headers(self.headers.clone())
.json(&request_body)
.send()
.await
.map_err(PostgrestError::NetworkError)?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
return Err(PostgrestError::TransactionError(format!(
"Failed to begin transaction: {} (Status: {})",
error_text, status
)));
}
#[derive(Debug, Deserialize)]
struct TransactionResponse {
transaction_id: String,
}
let response_data = response
.json::<TransactionResponse>()
.await
.map_err(|e| PostgrestError::DeserializationError(e.to_string()))?;
Ok(PostgrestTransaction::new(
&self.base_url,
&self.api_key,
self.http_client.clone(),
self.headers.clone(),
response_data.transaction_id,
))
}
}
pub struct PostgrestTransaction {
base_url: String,
api_key: String,
http_client: Client,
headers: HeaderMap,
transaction_id: String,
state: Arc<AtomicBool>, }
impl PostgrestTransaction {
fn new(
base_url: &str,
api_key: &str,
http_client: Client,
headers: HeaderMap,
transaction_id: String,
) -> Self {
Self {
base_url: base_url.to_string(),
api_key: api_key.to_string(),
http_client,
headers,
transaction_id,
state: Arc::new(AtomicBool::new(true)), }
}
pub fn from(&self, table: &str) -> PostgrestClient {
let mut client = PostgrestClient::new(
&self.base_url,
&self.api_key,
table,
self.http_client.clone(),
);
for (key, value) in self.headers.iter() {
if let Ok(value_str) = value.to_str() {
if let Ok(client_with_header) = PostgrestClient::new(
&self.base_url,
&self.api_key,
table,
self.http_client.clone(),
)
.with_header(key.as_str(), value_str)
{
client = client_with_header;
}
}
}
client
.query_params
.insert("transaction".to_string(), self.transaction_id.clone());
client
}
pub async fn commit(&self) -> Result<(), PostgrestError> {
if !self.state.load(Ordering::SeqCst) {
return Err(PostgrestError::TransactionError(
"Cannot commit: transaction is no longer active".to_string(),
));
}
let commit_url = format!("{}/rpc/commit_transaction", self.base_url);
let commit_body = json!({
"transaction_id": self.transaction_id
});
let response = self
.http_client
.post(&commit_url)
.headers(self.headers.clone())
.json(&commit_body)
.send()
.await
.map_err(PostgrestError::NetworkError)?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
return Err(PostgrestError::TransactionError(format!(
"Failed to commit transaction: {} (Status: {})",
error_text, status
)));
}
self.state.store(false, Ordering::SeqCst);
Ok(())
}
pub async fn rollback(&self) -> Result<(), PostgrestError> {
if !self.state.load(Ordering::SeqCst) {
return Err(PostgrestError::TransactionError(
"Cannot rollback: transaction is no longer active".to_string(),
));
}
let rollback_url = format!("{}/rpc/rollback_transaction", self.base_url);
let rollback_body = json!({
"transaction_id": self.transaction_id
});
let response = self
.http_client
.post(&rollback_url)
.headers(self.headers.clone())
.json(&rollback_body)
.send()
.await
.map_err(PostgrestError::NetworkError)?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
return Err(PostgrestError::TransactionError(format!(
"Failed to rollback transaction: {} (Status: {})",
error_text, status
)));
}
self.state.store(false, Ordering::SeqCst);
Ok(())
}
pub async fn savepoint(&self, name: &str) -> Result<(), PostgrestError> {
if !self.state.load(Ordering::SeqCst) {
return Err(PostgrestError::TransactionError(
"Cannot create savepoint: transaction is no longer active".to_string(),
));
}
let savepoint_url = format!("{}/rpc/create_savepoint", self.base_url);
let savepoint_body = json!({
"transaction_id": self.transaction_id,
"name": name
});
let response = self
.http_client
.post(&savepoint_url)
.headers(self.headers.clone())
.json(&savepoint_body)
.send()
.await
.map_err(PostgrestError::NetworkError)?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
return Err(PostgrestError::TransactionError(format!(
"Failed to create savepoint '{}': {} (Status: {})",
name, error_text, status
)));
}
Ok(())
}
pub async fn rollback_to_savepoint(&self, name: &str) -> Result<(), PostgrestError> {
if !self.state.load(Ordering::SeqCst) {
return Err(PostgrestError::TransactionError(
"Cannot rollback to savepoint: transaction is no longer active".to_string(),
));
}
let rollback_url = format!("{}/rpc/rollback_to_savepoint", self.base_url);
let rollback_body = json!({
"transaction_id": self.transaction_id,
"name": name
});
let response = self
.http_client
.post(&rollback_url)
.headers(self.headers.clone())
.json(&rollback_body)
.send()
.await
.map_err(PostgrestError::NetworkError)?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
return Err(PostgrestError::TransactionError(format!(
"Failed to rollback to savepoint '{}': {} (Status: {})",
name, error_text, status
)));
}
Ok(())
}
}
impl Drop for PostgrestTransaction {
fn drop(&mut self) {
if self.state.load(Ordering::SeqCst) {
eprintln!("Warning: Active transaction is being dropped without commit or rollback. Performing automatic rollback.");
let url = format!("{}/rest/v1/rpc/rollback_transaction", self.base_url);
let client = Client::new();
let future = client
.post(url)
.headers(self.headers.clone())
.json(&json!({ "transaction_id": self.transaction_id }))
.send();
std::mem::drop(future);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use wiremock::matchers::{body_json, header, method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn test_select() {
let mock_server = MockServer::start().await;
println!("Mock server started at: {}", mock_server.uri());
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("select", "*")) .respond_with(ResponseTemplate::new(200).set_body_json(json!([
{ "id": 1, "name": "Test Item 1" },
{ "id": 2, "name": "Test Item 2" }
])))
.mount(&mock_server)
.await;
println!("Select mock set up");
let client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"items", reqwest::Client::new(),
);
println!("Client created for select test");
let result = client.select("*").execute::<serde_json::Value>().await;
if let Err(e) = &result {
println!("Select query failed: {:?}", e);
}
assert!(result.is_ok());
let data = result.unwrap();
assert_eq!(data.len(), 2);
assert_eq!(
data.first()
.and_then(|v: &Value| v.get("name"))
.and_then(Value::as_str),
Some("Test Item 1")
);
assert_eq!(
data.first()
.and_then(|v: &Value| v.get("id"))
.and_then(Value::as_i64),
Some(1)
);
}
#[tokio::test]
async fn test_rpc() {
let mock_server = MockServer::start().await;
println!("Mock server started at: {}", mock_server.uri());
let rpc_params = json!({ "arg1": "value1", "arg2": 123 });
Mock::given(method("POST"))
.and(path("/rest/v1/rpc/my_rpc_function"))
.and(body_json(&rpc_params)) .respond_with(ResponseTemplate::new(200).set_body_json(json!({
"result": "success",
"data": 456
})))
.mount(&mock_server)
.await;
println!("RPC mock set up");
let client = PostgrestClient::rpc(
&mock_server.uri(),
"fake-key",
"my_rpc_function", rpc_params.clone(),
reqwest::Client::new(),
);
println!("Client created for RPC test");
#[derive(Deserialize, Debug, PartialEq)]
struct RpcResponse {
result: String,
data: i32,
}
let result = client.call_rpc::<RpcResponse>().await;
if let Err(e) = &result {
println!("RPC call failed: {:?}", e);
}
assert!(result.is_ok());
let response_data = result.unwrap();
assert_eq!(
response_data,
RpcResponse {
result: "success".to_string(),
data: 456
}
);
}
#[tokio::test]
async fn test_join_queries() {
let mock_server = MockServer::start().await;
println!("Mock server started at: {}", mock_server.uri());
Mock::given(method("GET"))
.and(path("/rest/v1/posts"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([
{
"id": 1,
"title": "First Post",
"content": "Content",
"comments": [
{ "id": 1, "text": "Comment 1", "user_id": 2 },
{ "id": 2, "text": "Comment 2", "user_id": 3 }
],
"users": { "id": 1 }
}
])))
.mount(&mock_server)
.await;
println!("Join query mock set up");
let client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"posts",
reqwest::Client::new(),
);
println!("Client created");
let result = client
.select("id,title,content")
.include("comments", "post_id", Some("id,text,user_id"))
.inner_join("users", "user_id", "id")
.execute::<serde_json::Value>()
.await;
if let Err(e) = &result {
println!("Join query failed: {:?}", e);
}
assert!(result.is_ok());
let data = result.unwrap();
assert_eq!(data.len(), 1);
assert_eq!(
data.first()
.and_then(|v: &Value| v.get("title"))
.and_then(Value::as_str),
Some("First Post")
);
assert_eq!(
data.first()
.and_then(|v: &Value| v.get("comments"))
.and_then(Value::as_array)
.map(|a| a.len()),
Some(2)
);
}
#[tokio::test]
async fn test_text_search() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/rest/v1/articles"))
.and(query_param("content", "fts(english).search terms"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([
{ "id": 1, "title": "Search Result", "content": "This is a search result" }
])))
.mount(&mock_server)
.await;
let client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"articles",
reqwest::Client::new(),
);
let result = client
.text_search("content", "search terms", Some("english"))
.execute::<serde_json::Value>()
.await;
assert!(result.is_ok());
let data = result.unwrap();
assert_eq!(data.len(), 1);
assert_eq!(
data.first()
.and_then(|v: &Value| v.get("title"))
.and_then(Value::as_str),
Some("Search Result")
);
}
#[tokio::test]
async fn test_csv_export() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/rest/v1/users"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(
"id,name,email\n1,User 1,user1@example.com\n2,User 2,user2@example.com",
)
.append_header("Content-Type", "text/csv"),
)
.mount(&mock_server)
.await;
let client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"users",
reqwest::Client::new(),
);
let result = client.export_csv().await;
assert!(result.is_ok());
let csv_data = result.unwrap();
assert!(csv_data.contains("id,name,email"));
assert!(csv_data.contains("User 1"));
assert!(csv_data.contains("User 2"));
}
#[tokio::test]
async fn test_transaction() {
let mock_server = MockServer::start().await;
println!("Mock server started at: {}", mock_server.uri());
Mock::given(method("POST"))
.and(path("/rpc/begin_transaction"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"transaction_id": "tx-12345"
})))
.mount(&mock_server)
.await;
println!("Begin transaction mock set up");
Mock::given(method("POST"))
.and(path("/rest/v1/users"))
.and(query_param("transaction", "tx-12345"))
.respond_with(ResponseTemplate::new(201).set_body_json(json!([{
"id": 1,
"name": "テストユーザー"
}])))
.mount(&mock_server)
.await;
println!("Insert mock set up");
Mock::given(method("GET"))
.and(path("/rest/v1/users"))
.and(query_param("transaction", "tx-12345"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([{
"id": 1,
"name": "テストユーザー"
}])))
.mount(&mock_server)
.await;
println!("Select mock set up");
Mock::given(method("POST"))
.and(path("/rpc/commit_transaction"))
.and(body_json(json!({
"transaction_id": "tx-12345"
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"success": true
})))
.mount(&mock_server)
.await;
println!("Commit mock set up");
let client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"users",
reqwest::Client::new(),
);
println!("Client created");
let transaction = client
.begin_transaction(
Some(IsolationLevel::ReadCommitted),
Some(TransactionMode::ReadWrite),
Some(30),
)
.await;
if let Err(e) = &transaction {
println!("Transaction failed: {:?}", e);
}
assert!(transaction.is_ok());
let transaction = transaction.unwrap();
let insert_result = transaction
.from("users")
.insert(json!({
"name": "テストユーザー"
}))
.await;
assert!(insert_result.is_ok());
let query_result = transaction
.from("users")
.select("id, name")
.execute::<serde_json::Value>()
.await;
assert!(query_result.is_ok());
let users = query_result.unwrap();
assert_eq!(
users
.first()
.and_then(|v: &Value| v.get("name"))
.and_then(Value::as_str),
Some("テストユーザー")
);
let commit_result = transaction.commit().await;
assert!(commit_result.is_ok());
}
#[tokio::test]
async fn test_transaction_rollback() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/rpc/begin_transaction"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"transaction_id": "tx-67890"
})))
.mount(&mock_server)
.await;
Mock::given(method("POST"))
.and(path("/rpc/rollback_transaction"))
.and(body_json(json!({
"transaction_id": "tx-67890"
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"success": true
})))
.mount(&mock_server)
.await;
let client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"users",
reqwest::Client::new(),
);
let transaction = client.begin_transaction(None, None, None).await;
assert!(transaction.is_ok());
let transaction = transaction.unwrap();
let rollback_result = transaction.rollback().await;
assert!(rollback_result.is_ok());
}
#[tokio::test]
async fn test_transaction_savepoint() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/rpc/begin_transaction"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"transaction_id": "tx-savepoint"
})))
.mount(&mock_server)
.await;
Mock::given(method("POST"))
.and(path("/rpc/create_savepoint"))
.and(body_json(json!({
"transaction_id": "tx-savepoint",
"name": "sp1"
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"success": true
})))
.mount(&mock_server)
.await;
Mock::given(method("POST"))
.and(path("/rpc/rollback_to_savepoint"))
.and(body_json(json!({
"transaction_id": "tx-savepoint",
"name": "sp1"
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"success": true
})))
.mount(&mock_server)
.await;
Mock::given(method("POST"))
.and(path("/rpc/commit_transaction"))
.and(body_json(json!({
"transaction_id": "tx-savepoint"
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"success": true
})))
.mount(&mock_server)
.await;
let client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"users",
reqwest::Client::new(),
);
let transaction = client.begin_transaction(None, None, None).await;
assert!(transaction.is_ok());
let transaction = transaction.unwrap();
let savepoint_result = transaction.savepoint("sp1").await;
assert!(savepoint_result.is_ok());
let rollback_to_savepoint_result = transaction.rollback_to_savepoint("sp1").await;
assert!(rollback_to_savepoint_result.is_ok());
let commit_result = transaction.commit().await;
assert!(commit_result.is_ok());
}
#[tokio::test]
async fn test_jsonb_filters() {
let mock_server = MockServer::start().await;
let contains_value = json!({ "key": "value" });
let contained_by_value = json!(["a", "b"]);
Mock::given(method("GET"))
.and(path("/rest/v1/data"))
.and(query_param("metadata", format!("cs.{}", contains_value)))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([{"id": 1}])))
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/data"))
.and(query_param("tags", format!("cd.{}", contained_by_value)))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([{"id": 2}])))
.mount(&mock_server)
.await;
let _base_client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"data",
reqwest::Client::new(),
);
let result_contains = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"data",
reqwest::Client::new(), )
.contains("metadata", &contains_value)
.unwrap() .execute::<serde_json::Value>()
.await;
assert!(result_contains.is_ok());
assert_eq!(result_contains.unwrap().len(), 1);
let result_contained_by = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"data",
reqwest::Client::new(), )
.contained_by("tags", &contained_by_value)
.unwrap()
.execute::<serde_json::Value>()
.await;
assert!(result_contained_by.is_ok());
assert_eq!(result_contained_by.unwrap().len(), 1);
}
#[tokio::test]
async fn test_filter_on_related_table() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/rest/v1/posts"))
.and(query_param("author.name", "eq.Specific Author")) .and(query_param("select", "title,author!inner(name)")) .respond_with(ResponseTemplate::new(200).set_body_json(json!([
{ "title": "Post by Specific Author", "author": { "name": "Specific Author" } }
])))
.mount(&mock_server)
.await;
let client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"posts",
reqwest::Client::new(),
);
let result = client
.select("title,author!inner(name)") .eq("author.name", "Specific Author") .execute::<serde_json::Value>()
.await;
if let Err(e) = &result {
println!("Join query failed: {:?}", e);
}
assert!(result.is_ok(), "Request failed: {:?}", result.err());
let data = result.unwrap();
assert_eq!(data.len(), 1);
let post = data
.first()
.expect("Post should exist in related table test");
assert_eq!(
post.get("title").and_then(Value::as_str),
Some("Post by Specific Author")
);
let author_obj: Option<&Value> = post.get("author");
let name_val = author_obj
.and_then(|a: &Value| a.get("name"))
.and_then(Value::as_str);
assert_eq!(name_val, Some("Specific Author"));
}
#[tokio::test]
async fn test_insert() {
let mock_server = MockServer::start().await;
println!(
"Mock server started for insert test at: {}",
mock_server.uri()
);
let insert_data = json!({ "name": "New Item", "value": 10 });
let expected_response = json!([{ "id": 3, "name": "New Item", "value": 10 }]);
Mock::given(method("POST"))
.and(path("/rest/v1/items"))
.and(header("apikey", "fake-key"))
.and(header("content-type", "application/json"))
.and(header("Prefer", "return=representation"))
.and(body_json(&insert_data))
.respond_with(ResponseTemplate::new(201).set_body_json(&expected_response))
.mount(&mock_server)
.await;
println!("Insert mock set up");
let client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"items",
reqwest::Client::new(),
);
println!("Client created for insert test");
let result = client.insert(&insert_data).await;
if let Err(e) = &result {
println!("Insert query failed: {:?}", e);
}
assert!(result.is_ok());
let data = result.unwrap();
assert_eq!(data, expected_response);
}
#[tokio::test]
async fn test_update() {
let mock_server = MockServer::start().await;
println!(
"Mock server started for update test at: {}",
mock_server.uri()
);
let update_data = json!({ "value": 20 });
let expected_response = json!([{ "id": 1, "name": "Updated Item", "value": 20 }]);
Mock::given(method("PATCH"))
.and(path("/rest/v1/items"))
.and(query_param("id", "eq.1"))
.and(header("apikey", "fake-key"))
.and(header("content-type", "application/json"))
.and(header("Prefer", "return=representation"))
.and(body_json(&update_data))
.respond_with(ResponseTemplate::new(200).set_body_json(&expected_response))
.mount(&mock_server)
.await;
println!("Update mock set up");
let client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"items",
reqwest::Client::new(),
);
println!("Client created for update test");
let result = client.eq("id", "1").update(&update_data).await;
if let Err(e) = &result {
println!("Update query failed: {:?}", e);
}
assert!(result.is_ok());
let data = result.unwrap();
assert_eq!(data, expected_response);
}
#[tokio::test]
async fn test_delete() {
let mock_server = MockServer::start().await;
println!(
"Mock server started for delete test at: {}",
mock_server.uri()
);
let expected_response = json!([{ "id": 1, "name": "Deleted Item", "value": 10 }]);
Mock::given(method("DELETE"))
.and(path("/rest/v1/items"))
.and(query_param("id", "eq.1"))
.and(header("apikey", "fake-key"))
.and(header("content-type", "application/json"))
.and(header("Prefer", "return=representation"))
.respond_with(ResponseTemplate::new(200).set_body_json(&expected_response))
.mount(&mock_server)
.await;
println!("Delete mock set up");
let client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"items",
reqwest::Client::new(),
);
println!("Client created for delete test");
let result = client.eq("id", "1").delete().await;
if let Err(e) = &result {
println!("Delete query failed: {:?}", e);
}
assert!(result.is_ok());
let data = result.unwrap();
assert_eq!(data, expected_response);
}
#[tokio::test]
async fn test_filters() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("id", "gt.10"))
.and(header("apikey", "fake-key"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!([{ "id": 11, "name": "Item 11" }])),
)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("name", "like.*test*"))
.and(header("apikey", "fake-key"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!([{ "id": 1, "name": "test item" }])),
)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("status", "in.(active,pending)"))
.and(header("apikey", "fake-key"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!([{ "id": 5, "status": "active" }])),
)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("value", "gte.50"))
.and(header("apikey", "fake-key"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!([{ "id": 3, "value": 50 }])),
)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("id", "lt.5"))
.and(header("apikey", "fake-key"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!([{ "id": 4, "name": "Item 4" }])),
)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("value", "lte.100"))
.and(header("apikey", "fake-key"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!([{ "id": 7, "value": 100 }])),
)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("name", "ilike.*CASE*"))
.and(header("apikey", "fake-key"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!([{ "id": 8, "name": "Case Test" }])),
)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("status", "not.eq.archived"))
.and(header("apikey", "fake-key"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!([{ "id": 9, "status": "active" }])),
)
.mount(&mock_server)
.await;
let base_uri = mock_server.uri();
let api_key = "fake-key";
let table_name = "items";
let client_gt =
PostgrestClient::new(&base_uri, api_key, table_name, reqwest::Client::new());
let result_gt = client_gt.gt("id", "10").execute::<Value>().await;
assert!(result_gt.is_ok(), "GT filter failed: {:?}", result_gt.err());
assert_eq!(result_gt.unwrap().len(), 1);
let client_like =
PostgrestClient::new(&base_uri, api_key, table_name, reqwest::Client::new());
let result_like = client_like.like("name", "*test*").execute::<Value>().await;
assert!(
result_like.is_ok(),
"LIKE filter failed: {:?}",
result_like.err()
);
assert_eq!(result_like.unwrap().len(), 1);
let client_in =
PostgrestClient::new(&base_uri, api_key, table_name, reqwest::Client::new());
let result_in = client_in
.in_list("status", &["active", "pending"])
.execute::<Value>()
.await;
assert!(result_in.is_ok(), "IN filter failed: {:?}", result_in.err());
assert_eq!(result_in.unwrap().len(), 1);
let client_gte =
PostgrestClient::new(&base_uri, api_key, table_name, reqwest::Client::new());
let result_gte = client_gte.gte("value", "50").execute::<Value>().await;
assert!(
result_gte.is_ok(),
"GTE filter failed: {:?}",
result_gte.err()
);
assert_eq!(result_gte.unwrap().len(), 1);
let client_lt =
PostgrestClient::new(&base_uri, api_key, table_name, reqwest::Client::new());
let result_lt = client_lt.lt("id", "5").execute::<Value>().await;
assert!(result_lt.is_ok(), "LT filter failed: {:?}", result_lt.err());
assert_eq!(result_lt.unwrap().len(), 1);
let client_lte =
PostgrestClient::new(&base_uri, api_key, table_name, reqwest::Client::new());
let result_lte = client_lte.lte("value", "100").execute::<Value>().await;
assert!(
result_lte.is_ok(),
"LTE filter failed: {:?}",
result_lte.err()
);
assert_eq!(result_lte.unwrap().len(), 1);
let client_ilike =
PostgrestClient::new(&base_uri, api_key, table_name, reqwest::Client::new());
let result_ilike = client_ilike
.ilike("name", "*CASE*")
.execute::<Value>()
.await;
assert!(
result_ilike.is_ok(),
"ILIKE filter failed: {:?}",
result_ilike.err()
);
assert_eq!(result_ilike.unwrap().len(), 1);
let client_not =
PostgrestClient::new(&base_uri, api_key, table_name, reqwest::Client::new());
let result_not = client_not
.not("status", "eq.archived")
.execute::<Value>()
.await;
assert!(
result_not.is_ok(),
"NOT filter failed: {:?}",
result_not.err()
);
assert_eq!(result_not.unwrap().len(), 1);
}
#[tokio::test]
async fn test_modifiers() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/rest/v1/protected_items"))
.and(header("apikey", "fake-key"))
.and(header("x-supabase-admin-role", "service_role")) .respond_with(
ResponseTemplate::new(200).set_body_json(json!([{ "id": 1, "data": "secret" }])),
) .mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("order", "name.desc"))
.and(header("apikey", "fake-key"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!([{ "id": 1, "name": "Zebra" }])),
)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("limit", "5"))
.and(header("apikey", "fake-key"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([{}, {}, {}, {}, {}])))
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("offset", "10"))
.and(header("apikey", "fake-key"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([{ "id": 11 }])))
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(query_param("limit", "2"))
.and(query_param("offset", "3")) .and(header("apikey", "fake-key"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!([{ "id": 4 }, { "id": 5 }])),
)
.mount(&mock_server)
.await;
let client = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"protected_items",
reqwest::Client::new(),
);
let result_rls = client.ignore_rls().execute::<Value>().await;
assert!(result_rls.is_ok());
assert_eq!(result_rls.unwrap().len(), 1);
let client_order = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"items",
reqwest::Client::new(),
);
let result_order = client_order
.order("name", SortOrder::Descending)
.execute::<Value>()
.await;
assert!(
result_order.is_ok(),
"Order modifier failed: {:?}",
result_order.err()
);
assert_eq!(result_order.unwrap().len(), 1);
let client_limit = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"items",
reqwest::Client::new(),
);
let result_limit = client_limit.limit(5).execute::<Value>().await;
assert!(
result_limit.is_ok(),
"Limit modifier failed: {:?}",
result_limit.err()
);
assert_eq!(result_limit.unwrap().len(), 5);
let client_offset = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"items",
reqwest::Client::new(),
);
let result_offset = client_offset.offset(10).execute::<Value>().await;
assert!(
result_offset.is_ok(),
"Offset modifier failed: {:?}",
result_offset.err()
);
assert_eq!(result_offset.unwrap().len(), 1);
let client_limit_offset = PostgrestClient::new(
&mock_server.uri(),
"fake-key",
"items",
reqwest::Client::new(),
);
let result_limit_offset = client_limit_offset
.limit(2)
.offset(3)
.execute::<Value>()
.await;
assert!(
result_limit_offset.is_ok(),
"Limit/Offset modifier failed: {:?}",
result_limit_offset.err()
);
assert_eq!(result_limit_offset.unwrap().len(), 2);
}
#[tokio::test]
async fn test_error_handling() {
let mock_server = MockServer::start().await;
let base_uri = mock_server.uri();
let api_key = "fake-key";
let table_name = "items";
Mock::given(method("GET"))
.and(path("/rest/v1/items"))
.and(header("apikey", "invalid-key")) .respond_with(ResponseTemplate::new(401).set_body_json(json!({
"message": "Invalid API key"
})))
.mount(&mock_server)
.await;
let insert_bad_data = json!({ "value": 10 }); Mock::given(method("POST"))
.and(path("/rest/v1/items"))
.and(header("apikey", api_key))
.and(header("content-type", "application/json"))
.and(header("prefer", "return=representation"))
.and(body_json(&insert_bad_data))
.respond_with(ResponseTemplate::new(400).set_body_json(json!({
"code": "23502",
"message": "null value in column \"name\" violates not-null constraint",
"details": null,
"hint": null
})))
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/rest/v1/server_error"))
.and(header("apikey", api_key))
.respond_with(ResponseTemplate::new(500).set_body_string("Internal Server Error"))
.mount(&mock_server)
.await;
let client_401 =
PostgrestClient::new(&base_uri, "invalid-key", table_name, reqwest::Client::new());
let result_401 = client_401.select("*").execute::<Value>().await;
assert!(result_401.is_err());
match result_401.err().unwrap() {
PostgrestError::ApiError { details, status } => {
assert_eq!(status, reqwest::StatusCode::UNAUTHORIZED);
assert_eq!(details.message, Some("Invalid API key".to_string()));
}
PostgrestError::UnparsedApiError { message, status } => {
assert_eq!(status, reqwest::StatusCode::UNAUTHORIZED);
assert!(message.contains("Invalid API key"));
}
e => panic!("Expected ApiError or UnparsedApiError for 401, got {:?}", e),
}
let client_400 =
PostgrestClient::new(&base_uri, api_key, table_name, reqwest::Client::new());
let result_400 = client_400.insert(&insert_bad_data).await;
assert!(result_400.is_err());
match result_400.err().unwrap() {
PostgrestError::ApiError { details, status } => {
assert_eq!(status, reqwest::StatusCode::BAD_REQUEST);
assert_eq!(details.code, Some("23502".to_string()));
assert!(details
.message
.unwrap()
.contains("violates not-null constraint"));
}
e => panic!("Expected ApiError for 400, got {:?}", e),
}
let client_500 =
PostgrestClient::new(&base_uri, api_key, "server_error", reqwest::Client::new());
let result_500 = client_500.select("*").execute::<Value>().await;
assert!(result_500.is_err());
match result_500.err().unwrap() {
PostgrestError::UnparsedApiError { message, status } => {
assert_eq!(status, reqwest::StatusCode::INTERNAL_SERVER_ERROR);
assert_eq!(message, "Internal Server Error");
}
e => panic!("Expected UnparsedApiError for 500, got {:?}", e),
}
}
}