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");
const CASCADE_SQL: &str = include_str!("../../../sql/helpers/cascade.sql");
const CHANGELOG_CONTRACT_SQL: &str =
include_str!("../../../sql/helpers/entity_change_log_contract.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(...)");
formatter.progress("Installed tables:");
formatter.progress(" - core.tb_entity_change_log (mutation change-log outbox — #569)");
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("Would install:");
formatter.progress(" - fraiseql.mutation_ok/err/library_version helpers");
formatter.progress(" - fraiseql.build_cascade cascade builders");
formatter.progress(" - core.tb_entity_change_log change-log contract table (#569)");
formatter.progress("");
formatter.progress("Mutation-helper SQL that 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")?;
client
.batch_execute(MUTATION_RESPONSE_SQL)
.await
.context("Failed to install FraiseQL helper library")?;
client
.batch_execute(CASCADE_SQL)
.await
.context("Failed to install FraiseQL cascade builders")?;
client
.batch_execute(CHANGELOG_CONTRACT_SQL)
.await
.context("Failed to install FraiseQL change-log contract table")?;
formatter.progress("✓ SQL helpers + change-log contract 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;