use anyhow::{Context, Result};
use deadpool_postgres::{Config, ManagerConfig, RecyclingMethod, Runtime};
use tokio_postgres::NoTls;
use tracing::info;
use crate::output::OutputFormatter;
const HELPERS_VERSION: &str = "2.2.0";
const MUTATION_RESPONSE_SQL: &str = include_str!("../../../sql/helpers/mutation_response.sql");
pub async fn run(
database_url: Option<&str>,
dry_run: bool,
formatter: &OutputFormatter,
) -> Result<()> {
if dry_run {
let db_url = database_url.unwrap_or("postgres://user:pass@localhost/db");
print_dry_run(db_url, formatter);
return Ok(());
}
let db_url = super::migrate::resolve_database_url(database_url)
.context("Failed to resolve database URL")?;
formatter.progress(&format!(
"🔧 Installing FraiseQL mutation helpers (v{}) to database...",
HELPERS_VERSION
));
let pool = connect_to_database(&db_url).await.context("Failed to connect to database")?;
apply_helpers(&pool, formatter).await.context("Failed to apply helpers")?;
formatter.progress(&format!(
"✅ FraiseQL mutation helpers v{} installed successfully",
HELPERS_VERSION
));
formatter.progress("Installed functions:");
formatter.progress(" - fraiseql.library_version()");
formatter.progress(" - fraiseql.mutation_ok(...)");
formatter.progress(" - fraiseql.mutation_err(...)");
Ok(())
}
fn print_dry_run(db_url: &str, formatter: &OutputFormatter) {
formatter.progress("📋 DRY RUN MODE (no changes will be made)");
formatter.progress("");
formatter.progress(&format!("Database URL: {}", mask_password(db_url)));
formatter.progress("");
formatter.progress("The following SQL will be executed:");
formatter.progress("");
formatter.progress(MUTATION_RESPONSE_SQL);
formatter.progress("");
formatter.progress("To apply these changes, run without --dry-run:");
formatter.progress(&format!(" fraiseql setup --database '{}'", mask_password(db_url)));
}
fn mask_password(url: &str) -> String {
if let Some(at_pos) = url.rfind('@') {
if let Some(colon_pos) = url[..at_pos].rfind(':') {
let before = &url[..=colon_pos];
let after = &url[at_pos..];
format!("{}***{}", before, after)
} else {
url.to_string()
}
} else {
url.to_string()
}
}
async fn connect_to_database(db_url: &str) -> Result<deadpool_postgres::Pool> {
let mut cfg = Config::new();
cfg.url = Some(db_url.to_string());
cfg.manager = Some(ManagerConfig {
recycling_method: RecyclingMethod::Fast,
});
cfg.pool = Some(deadpool_postgres::PoolConfig::new(2));
let pool = cfg
.create_pool(Some(Runtime::Tokio1), NoTls)
.context("Failed to create database pool")?;
let _client = pool.get().await.context("Failed to acquire database connection")?;
info!("Connected to database");
Ok(pool)
}
async fn apply_helpers(pool: &deadpool_postgres::Pool, formatter: &OutputFormatter) -> Result<()> {
formatter.progress("📝 Applying SQL helpers...");
let client = pool.get().await.context("Failed to acquire database connection")?;
for statement in MUTATION_RESPONSE_SQL.split(';') {
let trimmed = statement.trim();
if trimmed.is_empty() || trimmed.starts_with("--") {
continue;
}
client.execute(trimmed, &[]).await.with_context(|| {
format!("Failed to execute SQL: {}", trimmed.lines().next().unwrap_or(""))
})?;
}
formatter.progress("✓ SQL helpers applied");
let version: String = client
.query_one("SELECT fraiseql.library_version() AS version", &[])
.await
.context("Failed to verify helper installation")?
.get("version");
if version == HELPERS_VERSION {
info!("Helper version verified: {}", version);
} else {
formatter.progress(&format!(
"⚠️ Version mismatch: expected {}, got {}",
HELPERS_VERSION, version
));
}
Ok(())
}
#[cfg(test)]
mod tests;