pg-api 0.1.0

A high-performance PostgreSQL REST API driver with rate limiting, connection pooling, and observability
use crate::rate_limit::RateLimitInfo;
use crate::license::License;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use deadpool_postgres::Pool;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{collections::HashMap, sync::Arc};
use tokio::sync::Mutex;
use uuid::Uuid;

#[derive(Clone)]
pub struct AppState {
    pub instances: Arc<tokio::sync::RwLock<HashMap<String, PostgresInstance>>>,
    pub connections: Arc<DashMap<String, Pool>>,
    pub accounts: Arc<tokio::sync::RwLock<HashMap<String, Account>>>,
    pub rate_limiter: Arc<Mutex<HashMap<String, RateLimitInfo>>>,
    pub active_connections: Arc<DashMap<String, u32>>, // account_id -> active connection count
    pub license: Arc<Option<License>>, // License information
}

impl AppState {
    pub async fn new() -> anyhow::Result<Self> {
        // Try to load license
        let license = match crate::license::LicenseValidator::validate_from_env() {
            Ok(lic) => {
                tracing::info!("License validated successfully: {:?}", lic.license_type);
                Some(lic)
            }
            Err(e) => {
                tracing::warn!("License validation failed: {}", e);
                None
            }
        };
        
        Ok(Self {
            instances: Arc::new(tokio::sync::RwLock::new(load_instances().await?)),
            connections: Arc::new(DashMap::new()),
            accounts: Arc::new(tokio::sync::RwLock::new(load_accounts().await?)),
            rate_limiter: Arc::new(Mutex::new(HashMap::new())),
            active_connections: Arc::new(DashMap::new()),
            license: Arc::new(license),
        })
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PostgresInstance {
    pub id: String,
    pub name: String,
    pub host: String,
    pub port: u16,
    pub superuser: String,
    pub superuser_password: String,
    pub instance_type: InstanceType,
    pub created_at: DateTime<Utc>,
    pub status: InstanceStatus,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InstanceType {
    Single,
    Primary,
    Replica,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InstanceStatus {
    Active,
    Maintenance,
    Degraded,
    Offline,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Account {
    pub id: String,
    pub name: String,
    pub api_key: String,
    pub instance_id: String,
    pub databases: Vec<DatabaseAccess>,
    pub role: AccountRole,
    pub created_at: DateTime<Utc>,
    pub last_used: DateTime<Utc>,
    pub rate_limit: u32,
    pub max_connections: u32,
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub notes: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DatabaseAccess {
    pub database: String,
    pub username: String,
    pub password: String,
    pub permissions: Vec<Permission>,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum Permission {
    Select,
    Insert,
    Update,
    Delete,
    Create,
    Drop,
    Truncate,
    References,
    Trigger,
    Execute,
    Usage,
    All,
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum AccountRole {
    Owner,
    Admin,
    Developer,
    ReadWrite,
    ReadOnly,
}

#[derive(Debug, Deserialize)]
pub struct QueryRequest {
    pub query: String,
    pub database: String,
    #[serde(default)]
    pub params: Vec<Value>,
    #[serde(default)]
    #[allow(dead_code)]
    pub options: QueryOptions,
}

#[derive(Debug, Deserialize, Default)]
pub struct QueryOptions {
    #[serde(default)]
    #[allow(dead_code)]
    pub timeout_ms: Option<u64>,
    #[serde(default)]
    #[allow(dead_code)]
    pub read_only: bool,
    #[serde(default)]
    #[allow(dead_code)]
    pub as_transaction: bool,
}

#[derive(Debug, Serialize)]
pub struct ApiResponse<T> {
    pub success: bool,
    pub data: Option<T>,
    pub error: Option<ErrorInfo>,
    pub metadata: ResponseMetadata,
}

#[derive(Debug, Serialize)]
pub struct ErrorInfo {
    pub code: String,
    pub message: String,
    pub details: Option<Value>,
}

#[derive(Debug, Serialize)]
pub struct ResponseMetadata {
    pub request_id: String,
    pub execution_time_ms: u128,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rows_affected: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub instance_id: Option<String>,
    pub timestamp: DateTime<Utc>,
}

#[derive(Debug, Serialize)]
pub struct QueryResult {
    pub rows: Vec<Value>,
    pub fields: Vec<FieldMetadata>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub query_plan: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct FieldMetadata {
    pub name: String,
    pub data_type: String,
    pub nullable: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_length: Option<i32>,
}

impl<T: Serialize> ApiResponse<T> {
    pub fn success(data: T, metadata: ResponseMetadata) -> Self {
        Self {
            success: true,
            data: Some(data),
            error: None,
            metadata,
        }
    }

    pub fn error(code: &str, message: String, metadata: ResponseMetadata) -> Self {
        Self {
            success: false,
            data: None,
            error: Some(ErrorInfo {
                code: code.to_string(),
                message,
                details: None,
            }),
            metadata,
        }
    }
}

async fn load_instances() -> anyhow::Result<HashMap<String, PostgresInstance>> {
    let mut instances = HashMap::new();
    
    instances.insert(
        "default".to_string(),
        PostgresInstance {
            id: "default".to_string(),
            name: "Primary Instance".to_string(),
            host: "127.0.0.1".to_string(),
            port: 5432,
            superuser: "postgres".to_string(),
            superuser_password: "postgres".to_string(),
            instance_type: InstanceType::Single,
            created_at: Utc::now(),
            status: InstanceStatus::Active,
        },
    );
    
    Ok(instances)
}

async fn load_accounts() -> anyhow::Result<HashMap<String, Account>> {
    let mut accounts = HashMap::new();
    
    // Get config directory from environment or use default /etc/pg-api for production
    let config_dir = std::env::var("CONFIG_DIR").unwrap_or_else(|_| "/etc/pg-api".to_string());
    let config_path = std::path::PathBuf::from(&config_dir).join("accounts.json");
    
    eprintln!("[ACCOUNT LOADING] Loading accounts from: {}", config_path.display());
    tracing::info!("Loading accounts from: {}", config_path.display());
    
    // Load from config file if exists
    match tokio::fs::read_to_string(&config_path).await {
        Ok(content) => {
            match serde_json::from_str::<Vec<Account>>(&content) {
                Ok(loaded_accounts) => {
                    eprintln!("[ACCOUNT LOADING] Loaded {} accounts from config file", loaded_accounts.len());
                    tracing::info!("Loaded {} accounts from config file", loaded_accounts.len());
                    for account in loaded_accounts {
                        eprintln!("[ACCOUNT LOADING] Loading account: {} with key: {}...", account.name, &account.api_key[..20.min(account.api_key.len())]);
                        tracing::debug!("Loading account: {} with key: {}...", account.name, &account.api_key[..20.min(account.api_key.len())]);
                        accounts.insert(account.api_key.clone(), account);
                    }
                }
                Err(e) => {
                    eprintln!("[ACCOUNT LOADING ERROR] Failed to parse accounts.json: {}", e);
                    tracing::error!("Failed to parse accounts.json: {}", e);
                    return Err(anyhow::anyhow!("Failed to parse accounts configuration: {}", e));
                }
            }
        }
        Err(e) => {
            eprintln!("[ACCOUNT LOADING] Could not read accounts file from {}: {}", config_path.display(), e);
            tracing::warn!("Could not read accounts file from {}: {}", config_path.display(), e);
            // Only use default if file doesn't exist, not on parse errors
            if e.kind() == std::io::ErrorKind::NotFound {
                tracing::info!("Using default account configuration");
                // Default account
                accounts.insert(
                    "TWDo79SIVGM21sdZqbIW68q5FuW+SQTL9jl88t2iF1j5vfP2poxU0wp43NHSVXdI".to_string(),
                    Account {
                        id: Uuid::new_v4().to_string(),
                        name: "sentric-production".to_string(),
                        api_key: "TWDo79SIVGM21sdZqbIW68q5FuW+SQTL9jl88t2iF1j5vfP2poxU0wp43NHSVXdI".to_string(),
                        instance_id: "default".to_string(),
                        databases: vec![
                            DatabaseAccess {
                                database: "camera".to_string(),
                                username: "sentric".to_string(),
                                password: "X5pzEXGqAyLVy9CQQKEbCsrF".to_string(),
                                permissions: vec![Permission::All],
                            },
                            DatabaseAccess {
                                database: "sentric".to_string(),
                                username: "sentric".to_string(),
                                password: "X5pzEXGqAyLVy9CQQKEbCsrF".to_string(),
                                permissions: vec![Permission::All],
                            },
                        ],
                        role: AccountRole::Developer,
                        created_at: Utc::now(),
                        last_used: Utc::now(),
                        rate_limit: 1000,
                        max_connections: 50,
                        notes: Some("Default account - replace with proper configuration".to_string()),
                    },
                );
            } else {
                return Err(anyhow::anyhow!("Failed to read accounts configuration: {}", e));
            }
        }
    }
    
    Ok(accounts)
}