cufflink-cli 0.7.12

CLI for the Cufflink CRUD microservice platform — deploy, init, and manage services
use crate::config::CliConfig;
use std::process::Command;

/// Get the service ID by extracting manifest name and looking up via API
async fn get_service_id(config: &CliConfig) -> eyre::Result<String> {
    // Extract manifest to get service name
    let output = Command::new("cargo")
        .args(["run", "--", "--emit-manifest"])
        .output()?;

    if !output.status.success() {
        eyre::bail!("Failed to build service. Run from a cufflink service directory.");
    }

    let stdout = String::from_utf8(output.stdout)?;
    let manifest: serde_json::Value = serde_json::from_str(stdout.trim())?;
    let service_name = manifest["name"]
        .as_str()
        .ok_or_else(|| eyre::eyre!("No service name in manifest"))?;

    // Look up service ID via the services list API
    let client = config.http_client();
    let resp = config
        .auth_request(
            &client,
            reqwest::Method::GET,
            &format!("{}/api/services", config.api_url),
        )
        .send()
        .await?;

    let services: serde_json::Value = resp.json().await?;
    let service = services
        .as_array()
        .and_then(|arr| {
            arr.iter()
                .find(|s| s["name"].as_str() == Some(service_name))
        })
        .ok_or_else(|| eyre::eyre!("Service '{}' not found on platform", service_name))?;

    let id = service["id"]
        .as_str()
        .ok_or_else(|| eyre::eyre!("Service has no ID"))?;

    Ok(id.to_string())
}

/// Export a backup of the current service's data
pub async fn export(output_path: &str, env: Option<&str>) -> eyre::Result<()> {
    let config = CliConfig::load_with_env(env)?;

    if let Some(ref name) = config.env_name {
        println!("Environment: {}", name);
    }

    println!("Resolving service...");
    let service_id = get_service_id(&config).await?;

    println!("Exporting backup...");
    let client = config.http_client();
    let resp = config
        .auth_request(
            &client,
            reqwest::Method::GET,
            &format!("{}/api/services/{}/backup", config.api_url, service_id),
        )
        .send()
        .await?;

    if resp.status().is_success() {
        let body = resp.text().await?;
        std::fs::write(output_path, &body)?;
        println!("Backup saved to {}", output_path);
    } else {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Backup failed ({}): {}", status, body);
    }

    Ok(())
}

/// Restore a backup from a file
pub async fn restore(
    input_path: &str,
    clear_existing: bool,
    env: Option<&str>,
) -> eyre::Result<()> {
    let config = CliConfig::load_with_env(env)?;

    if let Some(ref name) = config.env_name {
        println!("Environment: {}", name);
    }

    println!("Resolving service...");
    let service_id = get_service_id(&config).await?;

    let data = std::fs::read_to_string(input_path)?;
    let mut payload: serde_json::Value = serde_json::from_str(&data)?;

    if clear_existing {
        if let Some(obj) = payload.as_object_mut() {
            obj.insert("clear_existing".to_string(), serde_json::Value::Bool(true));
        }
    }

    println!("Restoring backup...");
    let client = config.http_client();
    let resp = config
        .auth_request(
            &client,
            reqwest::Method::POST,
            &format!("{}/api/services/{}/restore", config.api_url, service_id),
        )
        .json(&payload)
        .send()
        .await?;

    if resp.status().is_success() {
        let body: serde_json::Value = resp.json().await?;
        println!(
            "Restore complete: {} tables, {} rows",
            body["tables_restored"], body["rows_inserted"]
        );
    } else {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        eyre::bail!("Restore failed ({}): {}", status, body);
    }

    Ok(())
}