use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::errors::WOWSQLError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnDefinition {
pub name: String,
#[serde(rename = "type")]
pub column_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_increment: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unique: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub not_null: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
impl ColumnDefinition {
pub fn new(name: impl Into<String>, column_type: impl Into<String>) -> Self {
Self {
name: name.into(),
column_type: column_type.into(),
auto_increment: None,
unique: None,
not_null: None,
default: None,
}
}
pub fn auto_increment(mut self, value: bool) -> Self {
self.auto_increment = Some(value);
self
}
pub fn unique(mut self, value: bool) -> Self {
self.unique = Some(value);
self
}
pub fn not_null(mut self, value: bool) -> Self {
self.not_null = Some(value);
self
}
pub fn default_value(mut self, value: impl Into<String>) -> Self {
self.default = Some(value.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexDefinition {
pub name: String,
pub columns: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CreateTableRequest {
pub table_name: String,
pub columns: Vec<ColumnDefinition>,
#[serde(skip_serializing_if = "Option::is_none")]
pub primary_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub indexes: Option<Vec<IndexDefinition>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct AlterTableRequest {
pub table_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub add_columns: Option<Vec<ColumnDefinition>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modify_columns: Option<Vec<ColumnDefinition>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub drop_columns: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rename_columns: Option<Vec<RenameColumn>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenameColumn {
pub old_name: String,
pub new_name: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SchemaResponse {
pub success: Option<bool>,
pub message: Option<String>,
pub table: Option<String>,
pub operation: Option<String>,
}
pub struct WOWSQLSchema {
base_url: String,
service_key: String,
client: Client,
}
impl WOWSQLSchema {
pub fn new(
project_url: impl Into<String>,
service_key: impl Into<String>,
) -> Result<Self, WOWSQLError> {
Ok(Self {
base_url: project_url.into().trim_end_matches('/').to_string(),
service_key: service_key.into(),
client: Client::new(),
})
}
pub async fn create_table(
&self,
request: CreateTableRequest,
) -> Result<SchemaResponse, WOWSQLError> {
let url = format!("{}/api/v2/schema/tables", self.base_url);
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", self.service_key))
.json(&request)
.send()
.await?;
if response.status() == StatusCode::FORBIDDEN {
return Err(WOWSQLError::PermissionDenied(
"Schema operations require a SERVICE ROLE key. \
You are using an anonymous key which cannot modify database schema. \
Please use your service role key instead."
.to_string(),
));
}
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(WOWSQLError::RequestFailed(format!(
"Failed to create table: {}",
error_text
)));
}
let result = response.json::<SchemaResponse>().await?;
Ok(result)
}
pub async fn alter_table(
&self,
request: AlterTableRequest,
) -> Result<SchemaResponse, WOWSQLError> {
let url = format!(
"{}/api/v2/schema/tables/{}",
self.base_url, request.table_name
);
let response = self
.client
.patch(&url)
.header("Authorization", format!("Bearer {}", self.service_key))
.json(&request)
.send()
.await?;
if response.status() == StatusCode::FORBIDDEN {
return Err(WOWSQLError::PermissionDenied(
"Schema operations require a SERVICE ROLE key.".to_string(),
));
}
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(WOWSQLError::RequestFailed(format!(
"Failed to alter table: {}",
error_text
)));
}
let result = response.json::<SchemaResponse>().await?;
Ok(result)
}
pub async fn drop_table(
&self,
table_name: &str,
cascade: bool,
) -> Result<SchemaResponse, WOWSQLError> {
let url = format!(
"{}/api/v2/schema/tables/{}?cascade={}",
self.base_url, table_name, cascade
);
let response = self
.client
.delete(&url)
.header("Authorization", format!("Bearer {}", self.service_key))
.send()
.await?;
if response.status() == StatusCode::FORBIDDEN {
return Err(WOWSQLError::PermissionDenied(
"Schema operations require a SERVICE ROLE key.".to_string(),
));
}
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(WOWSQLError::RequestFailed(format!(
"Failed to drop table: {}",
error_text
)));
}
let result = response.json::<SchemaResponse>().await?;
Ok(result)
}
pub async fn execute_sql(&self, sql: &str) -> Result<SchemaResponse, WOWSQLError> {
let url = format!("{}/api/v2/schema/execute", self.base_url);
let payload = json!({ "sql": sql });
let response = self
.client
.post(&url)
.header("Authorization", format!("Bearer {}", self.service_key))
.json(&payload)
.send()
.await?;
if response.status() == StatusCode::FORBIDDEN {
return Err(WOWSQLError::PermissionDenied(
"Schema operations require a SERVICE ROLE key.".to_string(),
));
}
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(WOWSQLError::RequestFailed(format!(
"Failed to execute SQL: {}",
error_text
)));
}
let result = response.json::<SchemaResponse>().await?;
Ok(result)
}
}