wowsql 1.1.0

Official Rust SDK for WOWSQL - MySQL Backend-as-a-Service with S3 Storage
Documentation
use crate::client::WOWSQLClient;
use crate::errors::WOWSQLError;
use crate::models::{
    CreateResponse, DeleteResponse, FilterExpression, FilterOperator, QueryResponse, SortDirection,
    UpdateResponse,
};
use serde::de::DeserializeOwned;
use serde_json::Value;

/// Fluent query builder for constructing and executing queries
pub struct QueryBuilder<'a> {
    client: &'a WOWSQLClient,
    table_name: String,
    selected_columns: Option<Vec<String>>,
    filters: Vec<FilterExpression>,
    order_column: Option<String>,
    order_direction: Option<SortDirection>,
    limit_value: Option<usize>,
    offset_value: Option<usize>,
}

impl<'a> QueryBuilder<'a> {
    pub(crate) fn new(client: &'a WOWSQLClient, table_name: &str) -> Self {
        Self {
            client,
            table_name: table_name.to_string(),
            selected_columns: None,
            filters: Vec::new(),
            order_column: None,
            order_direction: None,
            limit_value: None,
            offset_value: None,
        }
    }

    /// Select specific columns
    pub fn select(mut self, columns: &[&str]) -> Self {
        self.selected_columns = Some(columns.iter().map(|s| s.to_string()).collect());
        self
    }

    /// Add equality filter
    pub fn eq(mut self, column: &str, value: Value) -> Self {
        self.filters.push(FilterExpression {
            column: column.to_string(),
            operator: FilterOperator::Eq,
            value: Some(value),
        });
        self
    }

    /// Add not-equal filter
    pub fn neq(mut self, column: &str, value: Value) -> Self {
        self.filters.push(FilterExpression {
            column: column.to_string(),
            operator: FilterOperator::Neq,
            value: Some(value),
        });
        self
    }

    /// Add greater-than filter
    pub fn gt(mut self, column: &str, value: Value) -> Self {
        self.filters.push(FilterExpression {
            column: column.to_string(),
            operator: FilterOperator::Gt,
            value: Some(value),
        });
        self
    }

    /// Add greater-than-or-equal filter
    pub fn gte(mut self, column: &str, value: Value) -> Self {
        self.filters.push(FilterExpression {
            column: column.to_string(),
            operator: FilterOperator::Gte,
            value: Some(value),
        });
        self
    }

    /// Add less-than filter
    pub fn lt(mut self, column: &str, value: Value) -> Self {
        self.filters.push(FilterExpression {
            column: column.to_string(),
            operator: FilterOperator::Lt,
            value: Some(value),
        });
        self
    }

    /// Add less-than-or-equal filter
    pub fn lte(mut self, column: &str, value: Value) -> Self {
        self.filters.push(FilterExpression {
            column: column.to_string(),
            operator: FilterOperator::Lte,
            value: Some(value),
        });
        self
    }

    /// Add LIKE filter
    pub fn like(mut self, column: &str, pattern: &str) -> Self {
        self.filters.push(FilterExpression {
            column: column.to_string(),
            operator: FilterOperator::Like,
            value: Some(Value::String(pattern.to_string())),
        });
        self
    }

    /// Add IS NULL filter
    pub fn is_null(mut self, column: &str) -> Self {
        self.filters.push(FilterExpression {
            column: column.to_string(),
            operator: FilterOperator::IsNull,
            value: None,
        });
        self
    }

    /// Set order by
    pub fn order_by(mut self, column: &str, direction: SortDirection) -> Self {
        self.order_column = Some(column.to_string());
        self.order_direction = Some(direction);
        self
    }

    /// Set limit
    pub fn limit(mut self, value: usize) -> Self {
        self.limit_value = Some(value);
        self
    }

    /// Set offset
    pub fn offset(mut self, value: usize) -> Self {
        self.offset_value = Some(value);
        self
    }

    /// Execute query
    pub async fn execute<T: DeserializeOwned>(self) -> Result<QueryResponse<T>, WOWSQLError> {
        let mut url = format!("{}/api/v2/{}", self.client.base_url, self.table_name);
        let params = self.build_query_params();
        if !params.is_empty() {
            let query_string: Vec<String> = params
                .iter()
                .map(|(k, v)| format!("{}={}", k, urlencoding::encode(&v.to_string())))
                .collect();
            url = format!("{}?{}", url, query_string.join("&"));
        }
        let response: Value = self.client.execute_request(&url, "GET", None).await?;

        let data: Vec<T> = if let Some(data_array) = response.get("data").and_then(|v| v.as_array())
        {
            let mut results = Vec::new();
            for item in data_array {
                if let Ok(parsed) = serde_json::from_value(item.clone()) {
                    results.push(parsed);
                }
            }
            results
        } else {
            Vec::new()
        };

        let count = response
            .get("count")
            .and_then(|v| v.as_u64())
            .unwrap_or(data.len() as u64) as usize;

        let total = response
            .get("total")
            .and_then(|v| v.as_u64())
            .map(|v| v as usize);

        let error = response
            .get("error")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        Ok(QueryResponse {
            data,
            count,
            total,
            error,
        })
    }

    /// Get first result
    pub async fn first<T: DeserializeOwned>(self) -> Result<Option<T>, WOWSQLError> {
        Ok(self.limit(1).execute().await?.data.into_iter().next())
    }

    /// Create record (alias for insert)
    pub async fn create(self, data: Value) -> Result<CreateResponse, WOWSQLError> {
        self.insert(data).await
    }

    /// Insert record
    pub async fn insert(self, data: Value) -> Result<CreateResponse, WOWSQLError> {
        let url = format!("{}/api/v2/{}", self.client.base_url, self.table_name);
        self.client.execute_request(&url, "POST", Some(data)).await
    }

    /// Update records
    pub async fn update(self, data: Value) -> Result<UpdateResponse, WOWSQLError> {
        let url = format!("{}/api/v2/{}", self.client.base_url, self.table_name);
        let mut body = serde_json::json!({ "data": data });

        if !self.filters.is_empty() {
            let filters_json: Vec<Value> = self
                .filters
                .iter()
                .map(|f| {
                    serde_json::json!({
                        "column": f.column,
                        "operator": f.operator,
                        "value": f.value
                    })
                })
                .collect();
            body["filters"] = Value::Array(filters_json);
        }

        self.client.execute_request(&url, "PATCH", Some(body)).await
    }

    /// Delete records
    pub async fn delete(self) -> Result<DeleteResponse, WOWSQLError> {
        let url = format!("{}/api/v2/{}", self.client.base_url, self.table_name);
        let body = if !self.filters.is_empty() {
            let filters_json: Vec<Value> = self
                .filters
                .iter()
                .map(|f| {
                    serde_json::json!({
                        "column": f.column,
                        "operator": f.operator,
                        "value": f.value
                    })
                })
                .collect();
            Some(serde_json::json!({ "filters": filters_json }))
        } else {
            None
        };

        self.client.execute_request(&url, "DELETE", body).await
    }

    fn build_query_params(&self) -> Vec<(&str, String)> {
        let mut params = Vec::new();

        if let Some(ref columns) = self.selected_columns {
            params.push(("select", columns.join(",")));
        }

        if !self.filters.is_empty() {
            let filter_strings: Vec<String> = self
                .filters
                .iter()
                .map(|f| {
                    let value_str = f
                        .value
                        .as_ref()
                        .map(|v| v.to_string())
                        .unwrap_or_else(|| "null".to_string());
                    format!("{}.{}.{}", f.column, f.operator, value_str)
                })
                .collect();
            params.push(("filter", filter_strings.join(",")));
        }

        if let Some(ref order_col) = self.order_column {
            params.push(("order", order_col.clone()));
        }

        if let Some(ref order_dir) = self.order_direction {
            params.push(("order_direction", format!("{:?}", order_dir).to_lowercase()));
        }

        if let Some(limit) = self.limit_value {
            params.push(("limit", limit.to_string()));
        }

        if let Some(offset) = self.offset_value {
            params.push(("offset", offset.to_string()));
        }

        params
    }

    #[allow(dead_code)]
    fn build_query_body(&self) -> Value {
        let mut body = serde_json::json!({});

        if let Some(ref columns) = self.selected_columns {
            body["columns"] =
                Value::Array(columns.iter().map(|c| Value::String(c.clone())).collect());
        }

        if !self.filters.is_empty() {
            let filters_json: Vec<Value> = self
                .filters
                .iter()
                .map(|f| {
                    serde_json::json!({
                        "column": f.column,
                        "operator": f.operator,
                        "value": f.value
                    })
                })
                .collect();
            body["filters"] = Value::Array(filters_json);
        }

        if let Some(ref order_col) = self.order_column {
            body["order_by"] = Value::String(order_col.clone());
        }

        if let Some(ref order_dir) = self.order_direction {
            body["order_direction"] = serde_json::to_value(order_dir).unwrap();
        }

        if let Some(limit) = self.limit_value {
            body["limit"] = Value::Number(limit.into());
        }

        if let Some(offset) = self.offset_value {
            body["offset"] = Value::Number(offset.into());
        }

        body
    }
}