use anyhow::{anyhow, Result};
use postgrest::Postgrest;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
pub rest_url: String,
pub scout_api_key: String,
pub supabase_api_key: String,
}
impl DatabaseConfig {
pub fn from_env() -> Result<Self> {
dotenv::dotenv().ok();
let mut rest_url = std::env::var("SCOUT_DATABASE_REST_URL")
.map_err(|_| anyhow!("SCOUT_DATABASE_REST_URL environment variable is required"))?;
if !rest_url.ends_with("/rest/v1") {
if rest_url.ends_with("/") {
rest_url.push_str("rest/v1");
} else {
rest_url.push_str("/rest/v1");
}
}
let scout_api_key = std::env::var("SCOUT_DEVICE_API_KEY")
.map_err(|_| anyhow!("SCOUT_DEVICE_API_KEY environment variable not found"))?;
let supabase_api_key = std::env::var("SUPABASE_PUBLIC_API_KEY").map_err(|_| {
anyhow!("SUPABASE_PUBLIC_API_KEY environment variable is required for Supabase access")
})?;
Ok(DatabaseConfig {
rest_url,
scout_api_key,
supabase_api_key,
})
}
pub fn get_rest_url(&self) -> &str {
&self.rest_url
}
pub fn get_scout_api_key(&self) -> &str {
&self.scout_api_key
}
pub fn get_supabase_api_key(&self) -> &str {
&self.supabase_api_key
}
}
pub struct ScoutDbClient {
config: DatabaseConfig,
client: Option<Postgrest>,
}
impl std::fmt::Debug for ScoutDbClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ScoutDbClient")
.field("config", &self.config)
.field(
"client",
if self.client.is_some() {
&"Connected"
} else {
&"Disconnected"
},
)
.finish()
}
}
impl ScoutDbClient {
pub fn new(config: DatabaseConfig) -> Self {
Self {
config,
client: None,
}
}
pub fn connect(&mut self) -> Result<()> {
let rest_url = self.config.get_rest_url();
let client = Postgrest::new(rest_url)
.insert_header("apikey", self.config.get_supabase_api_key())
.insert_header("api_key", &format!("{}", self.config.get_scout_api_key()));
self.client = Some(client);
Ok(())
}
pub fn get_client(&mut self) -> Result<&Postgrest> {
if self.client.is_none() {
self.connect()?;
}
self.client
.as_ref()
.ok_or_else(|| anyhow!("No PostgREST client available"))
}
pub fn disconnect(&mut self) {
if self.client.is_some() {
self.client = None;
}
}
pub async fn query<T>(
&mut self,
query_builder: impl FnOnce(&Postgrest) -> postgrest::Builder,
) -> Result<Vec<T>>
where
T: for<'de> serde::Deserialize<'de>,
{
let client = self.get_client()?;
let builder = query_builder(client);
let response = builder.execute().await?;
let body = response.text().await?;
if let Ok(results) = serde_json::from_str::<Vec<T>>(&body) {
Ok(results)
} else {
if let Ok(error_response) = serde_json::from_str::<serde_json::Value>(&body) {
if let Some(error_msg) = error_response.get("error") {
return Err(anyhow!("Database error: {}", error_msg));
} else if let Some(message) = error_response.get("message") {
return Err(anyhow!("Database message: {}", message));
} else {
return Err(anyhow!("Database returned unexpected format: {}", body));
}
} else {
return Err(anyhow!(
"Failed to parse database response as JSON: {}",
body
));
}
}
}
pub async fn query_one<T>(
&mut self,
query_builder: impl FnOnce(&Postgrest) -> postgrest::Builder,
) -> Result<T>
where
T: for<'de> serde::Deserialize<'de>,
{
let client = self.get_client()?;
let builder = query_builder(client);
let response = builder.execute().await?;
let body = response.text().await?;
let results: Vec<T> = serde_json::from_str(&body)?;
if results.is_empty() {
return Err(anyhow!("No results found"));
}
Ok(results.into_iter().next().unwrap())
}
pub async fn execute(
&mut self,
query_builder: impl FnOnce(&Postgrest) -> postgrest::Builder,
) -> Result<()> {
let client = self.get_client()?;
let builder = query_builder(client);
let response = builder.execute().await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await?;
return Err(anyhow!(
"Operation failed: HTTP {} - {}",
status,
error_text
));
}
Ok(())
}
pub async fn insert<T>(&mut self, table: &str, data: &T) -> Result<Vec<T>>
where
T: for<'de> serde::Deserialize<'de> + serde::Serialize,
{
let client = self.get_client()?;
let json_data = serde_json::to_string(data)?;
let response = client.from(table).insert(&json_data).execute().await?;
let body = response.text().await?;
if let Ok(results) = serde_json::from_str::<Vec<T>>(&body) {
Ok(results)
} else {
if let Ok(error_response) = serde_json::from_str::<serde_json::Value>(&body) {
if let Some(error_msg) = error_response.get("error") {
return Err(anyhow!("Database insert error: {}", error_msg));
} else if let Some(message) = error_response.get("message") {
return Err(anyhow!("Database insert message: {}", message));
} else {
return Err(anyhow!(
"Database insert returned unexpected format: {}",
body
));
}
} else {
return Err(anyhow!(
"Failed to parse database insert response as JSON: {}",
body
));
}
}
}
pub async fn insert_bulk<T>(&mut self, table: &str, data: &[T]) -> Result<Vec<T>>
where
T: for<'de> serde::Deserialize<'de> + serde::Serialize,
{
let client = self.get_client()?;
let json_data = serde_json::to_string(data)?;
let response = client.from(table).insert(&json_data).execute().await?;
let body = response.text().await?;
if let Ok(results) = serde_json::from_str::<Vec<T>>(&body) {
Ok(results)
} else {
if let Ok(error_response) = serde_json::from_str::<serde_json::Value>(&body) {
if let Some(error_msg) = error_response.get("error") {
return Err(anyhow!("Database bulk insert error: {}", error_msg));
} else if let Some(message) = error_response.get("message") {
return Err(anyhow!("Database bulk insert message: {}", message));
} else {
return Err(anyhow!(
"Database bulk insert returned unexpected format: {}",
body
));
}
} else {
return Err(anyhow!(
"Failed to parse database bulk insert response as JSON: {}",
body
));
}
}
}
pub async fn upsert_bulk<T>(&mut self, table: &str, data: &[T]) -> Result<Vec<T>>
where
T: for<'de> serde::Deserialize<'de> + serde::Serialize,
{
let client = self.get_client()?;
let json_data = serde_json::to_string(data)?;
let response = client
.from(table)
.upsert(&json_data)
.on_conflict("id")
.execute()
.await?;
let body = response.text().await?;
if let Ok(results) = serde_json::from_str::<Vec<T>>(&body) {
Ok(results)
} else {
if let Ok(error_response) = serde_json::from_str::<serde_json::Value>(&body) {
if let Some(error_msg) = error_response.get("error") {
return Err(anyhow!("Database bulk upsert error: {}", error_msg));
} else if let Some(message) = error_response.get("message") {
return Err(anyhow!("Database bulk upsert message: {}", message));
} else {
return Err(anyhow!(
"Database bulk upsert returned unexpected format: {}",
body
));
}
} else {
return Err(anyhow!(
"Failed to parse database bulk upsert response as JSON: {}",
body
));
}
}
}
pub async fn update<T>(
&mut self,
data: &T,
filter_builder: impl FnOnce(&Postgrest) -> postgrest::Builder,
) -> Result<Vec<T>>
where
T: for<'de> serde::Deserialize<'de> + serde::Serialize,
{
let client = self.get_client()?;
let json_data = serde_json::to_string(data)?;
let builder = filter_builder(client);
let response = builder.update(&json_data).execute().await?;
let body = response.text().await?;
let results: Vec<T> = serde_json::from_str(&body)?;
Ok(results)
}
pub async fn delete(
&mut self,
filter_builder: impl FnOnce(&Postgrest) -> postgrest::Builder,
) -> Result<()> {
let client = self.get_client()?;
let builder = filter_builder(client);
let response = builder.delete().execute().await?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await?;
return Err(anyhow!(
"Delete operation failed: HTTP {} - {}",
status,
error_text
));
}
Ok(())
}
}
impl Drop for ScoutDbClient {
fn drop(&mut self) {
self.disconnect();
}
}