#![allow(dead_code)]
use anyhow::{anyhow, Context, Result};
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::time::Duration;
use crate::worker::db::Event;
#[derive(Debug, Clone, Deserialize)]
pub struct RuleRow {
pub scope: String,
pub content: String,
pub updated_at: String,
}
pub struct SupabaseClient {
http: Client,
base_url: String,
key: String,
client_id: String,
}
#[derive(Debug, Serialize)]
struct EventRow<'a> {
client_id: &'a str,
client_event_id: i64,
project: &'a str,
event_type: &'a str,
path: Option<&'a str>,
payload: Value,
severity: &'a str,
created_at: &'a str,
}
impl SupabaseClient {
pub fn new(url: &str, key: &str, client_id: &str) -> Result<Self> {
if url.is_empty() || key.is_empty() {
return Err(anyhow!("supabase_url and supabase_key required"));
}
if client_id.is_empty() {
return Err(anyhow!("client_id required"));
}
let http = Client::builder().timeout(Duration::from_secs(20)).build()?;
Ok(Self {
http,
base_url: url.trim_end_matches('/').to_string(),
key: key.to_string(),
client_id: client_id.to_string(),
})
}
pub fn push_events(&self, events: &[Event]) -> Result<usize> {
if events.is_empty() {
return Ok(0);
}
let rows: Vec<EventRow<'_>> = events
.iter()
.filter_map(|e| {
let id = e.id?;
let payload = serde_json::from_str::<Value>(&e.payload)
.unwrap_or_else(|_| Value::Object(Default::default()));
Some(EventRow {
client_id: &self.client_id,
client_event_id: id,
project: &e.project,
event_type: &e.event_type,
path: e.path.as_deref(),
payload,
severity: &e.severity,
created_at: &e.created_at,
})
})
.collect();
if rows.is_empty() {
return Ok(0);
}
let endpoint = format!("{}/rest/v1/worker_events", self.base_url);
let resp = self
.http
.post(&endpoint)
.header("apikey", &self.key)
.header("Authorization", format!("Bearer {}", self.key))
.header("Content-Type", "application/json")
.header("Prefer", "resolution=ignore-duplicates,return=minimal")
.json(&rows)
.send()
.context("supabase: request failed")?;
let status = resp.status();
if !status.is_success() {
let body = resp.text().unwrap_or_default();
return Err(anyhow!(
"supabase HTTP {}: {}",
status,
body.chars().take(400).collect::<String>()
));
}
Ok(rows.len())
}
pub fn list_rules(&self) -> Result<Vec<RuleRow>> {
let endpoint = format!(
"{}/rest/v1/worker_rules?select=scope,content,updated_at",
self.base_url
);
let resp = self
.http
.get(&endpoint)
.header("apikey", &self.key)
.header("Authorization", format!("Bearer {}", self.key))
.send()
.context("supabase list_rules: request failed")?;
let status = resp.status();
if !status.is_success() {
let body = resp.text().unwrap_or_default();
return Err(anyhow!(
"list_rules HTTP {}: {}",
status,
body.chars().take(400).collect::<String>()
));
}
let rows: Vec<RuleRow> = resp.json().context("list_rules: parse")?;
Ok(rows)
}
pub fn upsert_rule(&self, scope: &str, content: &str) -> Result<()> {
let endpoint = format!("{}/rest/v1/worker_rules", self.base_url);
let body = json!([{ "scope": scope, "content": content }]);
let resp = self
.http
.post(&endpoint)
.header("apikey", &self.key)
.header("Authorization", format!("Bearer {}", self.key))
.header("Content-Type", "application/json")
.header("Prefer", "resolution=merge-duplicates,return=minimal")
.json(&body)
.send()
.context("supabase upsert_rule: request failed")?;
let status = resp.status();
if !status.is_success() {
let body = resp.text().unwrap_or_default();
return Err(anyhow!(
"upsert_rule HTTP {}: {}",
status,
body.chars().take(400).collect::<String>()
));
}
Ok(())
}
}