use crate::cli::{AppCommands, Cli};
use crate::client::ApiClient;
use crate::config::Config;
use crate::error::{CliError, Result};
use crate::output::{create_table_with_headers, format_output, print_success, print_warning, OutputFormat};
use chrono::{DateTime, Utc};
use dialoguer::Confirm;
use serde::{Deserialize, Serialize};
pub async fn execute(command: &AppCommands, cli: &Cli) -> Result<()> {
let output_format = cli.output.as_ref().map(|s| OutputFormat::from_str(s)).unwrap_or(OutputFormat::Table);
match command {
AppCommands::Create { name, default_db, allowed_dbs, max_users, storage_quota, max_connections, qps_limit } => {
create_app(name.clone(), default_db.clone(), allowed_dbs.clone(), *max_users, *storage_quota, *max_connections, *qps_limit, &output_format).await
}
AppCommands::List { status, page, page_size } => {
list_apps(status.clone(), *page, *page_size, &output_format).await
}
AppCommands::Show { app_id } => {
show_app(app_id.clone(), &output_format).await
}
AppCommands::Update { app_id, name, default_db, allowed_dbs, max_users, storage_quota, max_connections, qps_limit } => {
update_app(app_id.clone(), name.clone(), default_db.clone(), allowed_dbs.clone(), *max_users, *storage_quota, *max_connections, *qps_limit, &output_format).await
}
AppCommands::Enable { app_id } => {
enable_app(app_id.clone(), &output_format).await
}
AppCommands::Disable { app_id } => {
disable_app(app_id.clone(), &output_format).await
}
AppCommands::Delete { app_id, force } => {
delete_app(app_id.clone(), *force, &output_format).await
}
AppCommands::ResetToken { app_id } => {
reset_token(app_id.clone(), &output_format).await
}
AppCommands::Stats { app_id } => {
show_stats(app_id.clone(), &output_format).await
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateAppRequest {
pub app_name: String,
pub default_database: String,
pub allowed_databases: Vec<String>,
pub max_users: u32,
pub default_storage_quota_mb: u32,
pub default_max_connections: u32,
pub default_qps: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateAppResponse {
pub app_id: String,
pub app_name: String,
pub app_token: String,
pub default_database: String,
pub allowed_databases: Vec<String>,
pub max_users: u32,
pub status: String,
pub created_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AppListResponse {
pub items: Vec<AppListItem>,
pub total: u32,
pub page: u32,
pub page_size: u32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AppListItem {
pub app_id: String,
pub app_name: String,
pub status: String,
pub user_count: u32,
pub created_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AppDetail {
pub app_id: String,
pub app_name: String,
pub default_database: String,
pub allowed_databases: Vec<String>,
pub status: String,
pub max_users: u32,
pub default_storage_quota_mb: u32,
pub default_max_connections: u32,
pub default_qps: u32,
pub user_count: u32,
pub total_storage_mb: f64,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateAppRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub app_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_database: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_databases: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_users: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_storage_quota_mb: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_max_connections: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_qps: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateAppResponse {
pub app_id: String,
pub updated_fields: Vec<String>,
pub updated_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeleteAppResponse {
pub app_id: String,
pub deleted: bool,
pub affected_users: u32,
pub workspace_cleanup: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResetTokenResponse {
pub app_id: String,
pub app_token: String,
pub rotated_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AppStats {
pub app_id: String,
pub user_count: u32,
pub active_user_count: u32,
pub total_storage_mb: f64,
pub total_connections: u32,
pub recent_users: Vec<RecentUser>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RecentUser {
pub user_uid: String,
pub label: Option<String>,
pub last_accessed_at: String,
}
pub async fn create_app(
name: String,
default_db: String,
allowed_dbs: String,
max_users: u32,
storage_quota: u32,
max_connections: u32,
qps_limit: u32,
output_format: &OutputFormat,
) -> Result<()> {
let config = Config::load()?;
let account = config.get_default_account().ok_or(CliError::NotLoggedIn)?;
let client = ApiClient::new(account.server.clone(), Some(account.token.clone()))?;
let allowed_databases: Vec<String> = allowed_dbs
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
let request = CreateAppRequest {
app_name: name,
default_database: default_db,
allowed_databases,
max_users,
default_storage_quota_mb: storage_quota,
default_max_connections: max_connections,
default_qps: qps_limit,
};
let request_json = serde_json::to_value(&request)
.map_err(|e| CliError::SerializationError(format!("Failed to serialize request: {}", e)))?;
let response: CreateAppResponse = client.post("/api/v1/apps", &request_json).await?;
match output_format {
OutputFormat::Json | OutputFormat::Yaml => {
println!("{}", format_output(&response, *output_format)?);
}
OutputFormat::Table => {
print_success("App created successfully");
println!();
println!(" App ID: {}", response.app_id);
println!(" App Name: {}", response.app_name);
println!(" App Token: {}", response.app_token);
println!();
print_warning("⚠️ Save the app token - it won't be shown again!");
}
}
Ok(())
}
pub async fn list_apps(
status: Option<String>,
page: u32,
page_size: u32,
output_format: &OutputFormat,
) -> Result<()> {
let config = Config::load()?;
let account = config.get_default_account().ok_or(CliError::NotLoggedIn)?;
let client = ApiClient::new(account.server.clone(), Some(account.token.clone()))?;
let mut query = vec![
("page", page.to_string()),
("page_size", page_size.to_string()),
];
if let Some(s) = status {
query.push(("status", s));
}
let query_refs: Vec<(&str, String)> = query.iter().map(|(k, v)| (*k, v.clone())).collect();
let response: AppListResponse = client.get("/api/v1/apps", &query_refs).await?;
match output_format {
OutputFormat::Json | OutputFormat::Yaml => {
println!("{}", format_output(&response, *output_format)?);
}
OutputFormat::Table => {
if response.items.is_empty() {
println!("No applications found.");
return Ok(());
}
let mut table = create_table_with_headers(vec!["APP ID", "NAME", "STATUS", "USERS", "CREATED AT"]);
for item in &response.items {
let created_at = parse_and_format_date(&item.created_at);
table.add_row(vec![
&item.app_id,
&item.app_name,
&item.status,
&item.user_count.to_string(),
&created_at,
]);
}
println!("{}", table);
println!(
"\nShowing {} of {} apps (page {}/{})",
response.items.len(),
response.total,
response.page,
(response.total + response.page_size - 1) / response.page_size
);
}
}
Ok(())
}
pub async fn show_app(app_id: String, output_format: &OutputFormat) -> Result<()> {
let config = Config::load()?;
let account = config.get_default_account().ok_or(CliError::NotLoggedIn)?;
let client = ApiClient::new(account.server.clone(), Some(account.token.clone()))?;
let url = format!("/api/v1/apps/{}", app_id);
let response: AppDetail = client.get(&url, &[]).await?;
match output_format {
OutputFormat::Json | OutputFormat::Yaml => {
println!("{}", format_output(&response, *output_format)?);
}
OutputFormat::Table => {
println!("Application Details");
println!("==================");
println!();
println!(" App ID: {}", response.app_id);
println!(" Name: {}", response.app_name);
println!(" Status: {}", response.status);
println!();
println!("Database Configuration:");
println!(" Default Database: {}", response.default_database);
println!(" Allowed Databases: {}", response.allowed_databases.join(", "));
println!();
println!("Quotas:");
println!(" Max Users: {}", response.max_users);
println!(" Storage Quota (per user): {} MB", response.default_storage_quota_mb);
println!(" Max Connections (per user): {}", response.default_max_connections);
println!(" QPS Limit (per user): {}", response.default_qps);
println!();
println!("Usage:");
println!(" Current Users: {}", response.user_count);
println!(" Total Storage: {:.2} MB", response.total_storage_mb);
println!();
println!(" Created: {}", parse_and_format_date(&response.created_at));
println!(" Updated: {}", parse_and_format_date(&response.updated_at));
}
}
Ok(())
}
pub async fn update_app(
app_id: String,
name: Option<String>,
default_db: Option<String>,
allowed_dbs: Option<String>,
max_users: Option<u32>,
storage_quota: Option<u32>,
max_connections: Option<u32>,
qps_limit: Option<u32>,
output_format: &OutputFormat,
) -> Result<()> {
let config = Config::load()?;
let account = config.get_default_account().ok_or(CliError::NotLoggedIn)?;
let client = ApiClient::new(account.server.clone(), Some(account.token.clone()))?;
let allowed_databases = allowed_dbs.map(|dbs| {
dbs.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
});
let request = UpdateAppRequest {
app_name: name,
default_database: default_db,
allowed_databases,
max_users,
default_storage_quota_mb: storage_quota,
default_max_connections: max_connections,
default_qps: qps_limit,
status: None,
};
let url = format!("/api/v1/apps/{}", app_id);
let request_json = serde_json::to_value(&request)
.map_err(|e| CliError::SerializationError(format!("Failed to serialize request: {}", e)))?;
let response: UpdateAppResponse = client.patch(&url, &request_json).await?;
match output_format {
OutputFormat::Json | OutputFormat::Yaml => {
println!("{}", format_output(&response, *output_format)?);
}
OutputFormat::Table => {
print_success("App updated successfully");
println!();
println!(" App ID: {}", response.app_id);
println!(" Updated Fields: {}", response.updated_fields.join(", "));
println!(" Updated At: {}", parse_and_format_date(&response.updated_at));
}
}
Ok(())
}
pub async fn enable_app(app_id: String, output_format: &OutputFormat) -> Result<()> {
change_app_status(app_id, "active", output_format).await
}
pub async fn disable_app(app_id: String, output_format: &OutputFormat) -> Result<()> {
change_app_status(app_id, "disabled", output_format).await
}
async fn change_app_status(
app_id: String,
status: &str,
output_format: &OutputFormat,
) -> Result<()> {
let config = Config::load()?;
let account = config.get_default_account().ok_or(CliError::NotLoggedIn)?;
let client = ApiClient::new(account.server.clone(), Some(account.token.clone()))?;
let request = UpdateAppRequest {
app_name: None,
default_database: None,
allowed_databases: None,
max_users: None,
default_storage_quota_mb: None,
default_max_connections: None,
default_qps: None,
status: Some(status.to_string()),
};
let url = format!("/api/v1/apps/{}", app_id);
let request_json = serde_json::to_value(&request)
.map_err(|e| CliError::SerializationError(format!("Failed to serialize request: {}", e)))?;
let response: UpdateAppResponse = client.patch(&url, &request_json).await?;
match output_format {
OutputFormat::Json | OutputFormat::Yaml => {
println!("{}", format_output(&response, *output_format)?);
}
OutputFormat::Table => {
let action = if status == "active" { "enabled" } else { "disabled" };
print_success(&format!("App {} successfully", action));
}
}
Ok(())
}
pub async fn delete_app(app_id: String, force: bool, output_format: &OutputFormat) -> Result<()> {
let config = Config::load()?;
let account = config.get_default_account().ok_or(CliError::NotLoggedIn)?;
let client = ApiClient::new(account.server.clone(), Some(account.token.clone()))?;
let url = format!("/api/v1/apps/{}", app_id);
let app: AppDetail = client.get(&url, &[]).await?;
if !force {
println!();
print_warning("⚠️ WARNING: This will permanently delete the app and all its users.");
println!(" App ID: {}", app.app_id);
println!(" App Name: {}", app.app_name);
println!(" Users: {}", app.user_count);
println!();
let confirmed = Confirm::new()
.with_prompt("Are you sure?")
.default(false)
.interact()
.map_err(|e| CliError::InvalidInput(format!("Failed to read input: {}", e)))?;
if !confirmed {
println!("Cancelled.");
return Ok(());
}
}
let response: DeleteAppResponse = client.delete(&url).await?;
match output_format {
OutputFormat::Json | OutputFormat::Yaml => {
println!("{}", format_output(&response, *output_format)?);
}
OutputFormat::Table => {
print_success("App deleted successfully");
println!();
println!(" Affected Users: {}", response.affected_users);
println!(" Workspace Cleanup: {}", response.workspace_cleanup);
}
}
Ok(())
}
pub async fn reset_token(app_id: String, output_format: &OutputFormat) -> Result<()> {
let config = Config::load()?;
let account = config.get_default_account().ok_or(CliError::NotLoggedIn)?;
let client = ApiClient::new(account.server.clone(), Some(account.token.clone()))?;
let confirmed = Confirm::new()
.with_prompt("This will invalidate the old token. Continue?")
.default(false)
.interact()
.map_err(|e| CliError::InvalidInput(format!("Failed to read input: {}", e)))?;
if !confirmed {
println!("Cancelled.");
return Ok(());
}
let url = format!("/api/v1/apps/{}/rotate-token", app_id);
let response: ResetTokenResponse = client.post(&url, &serde_json::json!({})).await?;
match output_format {
OutputFormat::Json | OutputFormat::Yaml => {
println!("{}", format_output(&response, *output_format)?);
}
OutputFormat::Table => {
print_success("App token reset successfully");
println!();
println!(" App ID: {}", response.app_id);
println!(" New Token: {}", response.app_token);
println!();
print_warning("⚠️ Save the new token - it won't be shown again!");
}
}
Ok(())
}
pub async fn show_stats(app_id: String, output_format: &OutputFormat) -> Result<()> {
let config = Config::load()?;
let account = config.get_default_account().ok_or(CliError::NotLoggedIn)?;
let client = ApiClient::new(account.server.clone(), Some(account.token.clone()))?;
let url = format!("/api/v1/apps/{}/stats", app_id);
let response: AppStats = client.get(&url, &[]).await?;
match output_format {
OutputFormat::Json | OutputFormat::Yaml => {
println!("{}", format_output(&response, *output_format)?);
}
OutputFormat::Table => {
println!("Application Statistics");
println!("=====================");
println!();
println!(" App ID: {}", response.app_id);
println!(" Total Users: {}", response.user_count);
println!(" Active Users: {}", response.active_user_count);
println!(" Total Storage: {:.2} MB", response.total_storage_mb);
println!(" Active Connections: {}", response.total_connections);
println!();
if !response.recent_users.is_empty() {
println!("Recent Users:");
let mut table = create_table_with_headers(vec!["USER UID", "LABEL", "LAST ACCESSED"]);
for user in &response.recent_users {
let label = user.label.as_deref().unwrap_or("-");
let last_accessed = parse_and_format_date(&user.last_accessed_at);
table.add_row(vec![&user.user_uid, label, &last_accessed]);
}
println!("{}", table);
}
}
}
Ok(())
}
fn parse_and_format_date(date_str: &str) -> String {
DateTime::parse_from_rfc3339(date_str)
.map(|dt| dt.with_timezone(&Utc).format("%Y-%m-%d %H:%M:%S UTC").to_string())
.unwrap_or_else(|_| date_str.to_string())
}