devist 0.7.0

Project bootstrap CLI for AI-assisted development. Spin up new projects from templates, manage backends, and keep your codebase comprehensible.
#![allow(dead_code)]
// Supabase L2 push: batches local SQLite events into worker_events table
// via the PostgREST endpoint. Idempotent on (client_id, client_event_id).

use anyhow::{anyhow, Context, Result};
use reqwest::blocking::Client;
use serde::Serialize;
use serde_json::Value;
use std::time::Duration;

use crate::worker::db::Event;

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(),
        })
    }

    /// Push events to `worker_events`. Returns the count actually accepted
    /// (best-effort — Supabase returns minimal on success).
    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())
    }
}