pg-api 0.2.0

A high-performance PostgreSQL REST API driver with rate limiting, connection pooling, and observability
/// PG-API Rust Client Example
///
/// Exemplo de cliente Rust assíncrono para PG-API.
///
/// Adicione ao seu Cargo.toml:
/// ```toml
/// [dependencies]
/// reqwest = { version = "0.12", features = ["json"] }
/// tokio = { version = "1", features = ["full"] }
/// serde = { version = "1", features = ["derive"] }
/// serde_json = "1"
/// anyhow = "1"
/// ```
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::env;

/// Cliente PG-API
pub struct PGAPIClient {
    base_url: String,
    api_key: String,
    client: reqwest::Client,
}

/// Resposta padrão da API
#[derive(Debug, Deserialize)]
pub struct ApiResponse<T> {
    pub success: bool,
    pub data: Option<T>,
    pub error: Option<ApiError>,
    pub metadata: ResponseMetadata,
}

#[derive(Debug, Deserialize)]
pub struct ApiError {
    pub code: String,
    pub message: String,
}

#[derive(Debug, Deserialize)]
pub struct ResponseMetadata {
    pub request_id: String,
    pub execution_time_ms: u64,
    pub rows_affected: Option<u64>,
    pub instance_id: String,
    pub timestamp: String,
}

/// Informações da conta
#[derive(Debug, Deserialize)]
pub struct AccountInfo {
    pub id: String,
    pub name: String,
    pub role: String,
    pub rate_limit: u32,
    pub max_connections: u32,
    pub databases: Vec<String>,
}

/// Resultado de query
#[derive(Debug, Deserialize)]
pub struct QueryResult {
    pub rows: Vec<serde_json::Value>,
    pub columns: Vec<String>,
    pub row_count: u64,
}

/// Estatísticas de uso
#[derive(Debug, Deserialize)]
pub struct UsageStats {
    pub requests_today: u64,
    pub active_connections: u32,
    pub rate_limit_remaining: u32,
}

impl PGAPIClient {
    /// Cria um novo cliente
    pub fn new(base_url: impl Into<String>, api_key: impl Into<String>) -> Result<Self> {
        Ok(Self {
            base_url: base_url.into(),
            api_key: api_key.into(),
            client: reqwest::Client::new(),
        })
    }

    /// Faz uma requisição GET
    async fn get<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<ApiResponse<T>> {
        let url = format!("{}{}", self.base_url, endpoint);
        let response = self
            .client
            .get(&url)
            .header("X-API-Key", &self.api_key)
            .send()
            .await
            .context("Falha na requisição GET")?;

        let status = response.status();
        let body = response
            .json::<ApiResponse<T>>()
            .await
            .context("Falha ao parsear resposta")?;

        if !status.is_success() || !body.success {
            anyhow::bail!(
                "Erro na API: {:?}",
                body.error.unwrap_or_else(|| ApiError {
                    code: "UNKNOWN".to_string(),
                    message: "Erro desconhecido".to_string(),
                })
            );
        }

        Ok(body)
    }

    /// Faz uma requisição POST
    async fn post<T: for<'de> Deserialize<'de>>(
        &self,
        endpoint: &str,
        body: impl Serialize,
    ) -> Result<ApiResponse<T>> {
        let url = format!("{}{}", self.base_url, endpoint);
        let response = self
            .client
            .post(&url)
            .header("X-API-Key", &self.api_key)
            .header("Content-Type", "application/json")
            .json(&body)
            .send()
            .await
            .context("Falha na requisição POST")?;

        let status = response.status();
        let body = response
            .json::<ApiResponse<T>>()
            .await
            .context("Falha ao parsear resposta")?;

        if !status.is_success() || !body.success {
            anyhow::bail!(
                "Erro na API: {:?}",
                body.error.unwrap_or_else(|| ApiError {
                    code: "UNKNOWN".to_string(),
                    message: "Erro desconhecido".to_string(),
                })
            );
        }

        Ok(body)
    }

    /// Health check
    pub async fn health(&self) -> Result<serde_json::Value> {
        let url = format!("{}/health", self.base_url);
        let response = self
            .client
            .get(&url)
            .send()
            .await
            .context("Falha no health check")?;

        Ok(response.json().await?)
    }

    /// Retorna informações da conta
    pub async fn get_account(&self) -> Result<AccountInfo> {
        let response = self.get::<AccountInfo>("/v1/account").await?;
        Ok(response.data.unwrap())
    }

    /// Retorna estatísticas de uso
    pub async fn get_usage(&self) -> Result<UsageStats> {
        let response = self.get::<UsageStats>("/v1/account/usage").await?;
        Ok(response.data.unwrap())
    }

    /// Lista databases disponíveis
    pub async fn list_databases(&self) -> Result<Vec<String>> {
        let response = self
            .get::<serde_json::Value>("/v1/databases")
            .await?;
        let data = response.data.unwrap();
        Ok(serde_json::from_value(data["databases"].clone())?)
    }

    /// Executa uma query SQL
    pub async fn query(
        &self,
        database: &str,
        sql: &str,
        params: Option<Vec<serde_json::Value>>,
    ) -> Result<QueryResult> {
        let mut body = json!({
            "database": database,
            "query": sql
        });

        if let Some(p) = params {
            body["params"] = json!(p);
        }

        let response = self.post::<QueryResult>("/v1/query", body).await?;
        Ok(response.data.unwrap())
    }

    /// Executa múltiplas queries em batch
    pub async fn batch(
        &self,
        queries: Vec<serde_json::Value>,
    ) -> Result<Vec<QueryResult>> {
        let response = self.post::<Vec<QueryResult>>("/v1/batch", queries).await?;
        Ok(response.data.unwrap())
    }

    /// Executa queries em uma transação
    pub async fn transaction(
        &self,
        database: &str,
        queries: Vec<serde_json::Value>,
    ) -> Result<serde_json::Value> {
        let body = json!({
            "database": database,
            "queries": queries
        });

        let response = self.post::<serde_json::Value>("/v1/transaction", body).await?;
        Ok(response.data.unwrap())
    }
}

fn print_section(title: &str) {
    println!("\n{}", "=".repeat(50));
    println!("  {}", title);
    println!("{}", "=".repeat(50));
}

#[tokio::main]
async fn main() -> Result<()> {
    // Configurações
    let api_key = env::var("API_KEY").unwrap_or_else(|_| "sk_dev_123456789".to_string());
    let base_url = env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:8580".to_string());

    println!("{}", "=".repeat(50));
    println!("  PG-API Rust Client Example");
    println!("{}", "=".repeat(50));
    println!("\nConfiguração:");
    println!("  Base URL: {}", base_url);
    println!("  API Key: {}...", &api_key[..10.min(api_key.len())]);

    // Criar cliente
    let client = PGAPIClient::new(&base_url, &api_key)?;

    // 1. Health Check
    print_section("1. Health Check");
    let health = client.health().await?;
    println!("Status: {}", health["status"]);
    println!("Versão: {}", health["version"]);

    // 2. Informações da Conta
    print_section("2. Informações da Conta");
    let account = client.get_account().await?;
    println!("ID: {}", account.id);
    println!("Nome: {}", account.name);
    println!("Role: {}", account.role);
    println!("Databases: {}", account.databases.join(", "));

    // 3. Versão do PostgreSQL
    print_section("3. Versão do PostgreSQL");
    let result = client.query("postgres", "SELECT version()", None).await?;
    println!("Resultado: {}", result.rows[0]["version"]);

    // 4. Criar tabela de exemplo
    print_section("4. Criar Tabela de Exemplo");
    let result = client
        .query(
            "postgres",
            "CREATE TABLE IF NOT EXISTS rust_demo (
                id SERIAL PRIMARY KEY,
                name VARCHAR(100),
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )",
            None,
        )
        .await?;
    println!("Tabela criada: {} linha(s)", result.row_count);

    // 5. Inserir dados
    print_section("5. Inserir Dados");
    let result = client
        .query(
            "postgres",
            "INSERT INTO rust_demo (name) VALUES ($1), ($2) RETURNING *",
            Some(vec![json!("Alice"), json!("Bob")]),
        )
        .await?;
    println!("Linhas inseridas: {}", result.row_count);
    for row in &result.rows {
        println!("  - ID: {}, Name: {}", row["id"], row["name"]);
    }

    // 6. Consultar dados
    print_section("6. Consultar Dados");
    let result = client
        .query("postgres", "SELECT * FROM rust_demo ORDER BY id", None)
        .await?;
    println!("Total de registros: {}", result.row_count);
    for row in &result.rows {
        println!("  ID: {}, Name: {}", row["id"], row["name"]);
    }

    // 7. Query com parâmetros
    print_section("7. Query com Parâmetros");
    let result = client
        .query(
            "postgres",
            "SELECT * FROM rust_demo WHERE name = $1",
            Some(vec![json!("Alice")]),
        )
        .await?;
    println!("Encontrado: {} registro(s)", result.row_count);

    // 8. Batch de queries
    print_section("8. Batch de Queries");
    let batch_queries = vec![
        json!({
            "database": "postgres",
            "query": "SELECT $1::text as msg",
            "params": ["Batch Query 1"]
        }),
        json!({
            "database": "postgres",
            "query": "SELECT $1::text as msg",
            "params": ["Batch Query 2"]
        }),
    ];
    let results = client.batch(batch_queries).await?;
    println!("Queries executadas: {}", results.len());

    // 9. Transação
    print_section("9. Transação");
    let tx_queries = vec![
        json!({
            "query": "INSERT INTO rust_demo (name) VALUES ($1) RETURNING id",
            "params": ["Transaction User"]
        }),
    ];
    let result = client.transaction("postgres", tx_queries).await?;
    println!("Transação commitada: {}", result["committed"]);

    // 10. Listar databases
    print_section("10. Listar Databases");
    let databases = client.list_databases().await?;
    println!("Databases: {}", databases.join(", "));

    // 11. Limpar dados
    print_section("11. Limpar Dados de Exemplo");
    let result = client
        .query(
            "postgres",
            "DELETE FROM rust_demo WHERE name LIKE $1",
            Some(vec![json!("%User%")]),
        )
        .await?;
    println!("Registros removidos");

    // 12. Dropar tabela
    print_section("12. Dropar Tabela");
    let _ = client
        .query("postgres", "DROP TABLE IF EXISTS rust_demo", None)
        .await?;
    println!("Tabela removida");

    // 13. Estatísticas de uso
    print_section("13. Estatísticas de Uso");
    let usage = client.get_usage().await?;
    println!("Requisições hoje: {}", usage.requests_today);
    println!("Conexões ativas: {}", usage.active_connections);

    print_section("Exemplos Concluídos! ✅");
    println!("Todos os exemplos foram executados com sucesso!");

    Ok(())
}