fraiseql_cli/commands/setup/
mod.rs1use anyhow::{Context, Result};
8use deadpool_postgres::{Config, ManagerConfig, RecyclingMethod, Runtime};
9use tokio_postgres::NoTls;
10use tracing::info;
11
12use crate::output::OutputFormatter;
13
14const HELPERS_VERSION: &str = "2.2.0";
16
17const MUTATION_RESPONSE_SQL: &str = include_str!("../../../sql/helpers/mutation_response.sql");
19
20pub async fn run(
27 database_url: Option<&str>,
28 dry_run: bool,
29 formatter: &OutputFormatter,
30) -> Result<()> {
31 if dry_run {
32 let db_url = database_url.unwrap_or("postgres://user:pass@localhost/db");
34 print_dry_run(db_url, formatter);
35 return Ok(());
36 }
37
38 let db_url = super::migrate::resolve_database_url(database_url)
40 .context("Failed to resolve database URL")?;
41
42 formatter.progress(&format!(
43 "🔧 Installing FraiseQL mutation helpers (v{}) to database...",
44 HELPERS_VERSION
45 ));
46
47 let pool = connect_to_database(&db_url).await.context("Failed to connect to database")?;
49
50 apply_helpers(&pool, formatter).await.context("Failed to apply helpers")?;
52
53 formatter.progress(&format!(
55 "✅ FraiseQL mutation helpers v{} installed successfully",
56 HELPERS_VERSION
57 ));
58
59 formatter.progress("Installed functions:");
60 formatter.progress(" - fraiseql.library_version()");
61 formatter.progress(" - fraiseql.mutation_ok(...)");
62 formatter.progress(" - fraiseql.mutation_err(...)");
63
64 Ok(())
65}
66
67fn print_dry_run(db_url: &str, formatter: &OutputFormatter) {
69 formatter.progress("📋 DRY RUN MODE (no changes will be made)");
70 formatter.progress("");
71 formatter.progress(&format!("Database URL: {}", mask_password(db_url)));
72 formatter.progress("");
73 formatter.progress("The following SQL will be executed:");
74 formatter.progress("");
75 formatter.progress(MUTATION_RESPONSE_SQL);
76 formatter.progress("");
77 formatter.progress("To apply these changes, run without --dry-run:");
78 formatter.progress(&format!(" fraiseql setup --database '{}'", mask_password(db_url)));
79}
80
81fn mask_password(url: &str) -> String {
83 if let Some(at_pos) = url.rfind('@') {
84 if let Some(colon_pos) = url[..at_pos].rfind(':') {
85 let before = &url[..=colon_pos];
86 let after = &url[at_pos..];
87 format!("{}***{}", before, after)
88 } else {
89 url.to_string()
90 }
91 } else {
92 url.to_string()
93 }
94}
95
96async fn connect_to_database(db_url: &str) -> Result<deadpool_postgres::Pool> {
98 let mut cfg = Config::new();
100 cfg.url = Some(db_url.to_string());
101 cfg.manager = Some(ManagerConfig {
102 recycling_method: RecyclingMethod::Fast,
103 });
104 cfg.pool = Some(deadpool_postgres::PoolConfig::new(2));
105
106 let pool = cfg
108 .create_pool(Some(Runtime::Tokio1), NoTls)
109 .context("Failed to create database pool")?;
110
111 let _client = pool.get().await.context("Failed to acquire database connection")?;
113
114 info!("Connected to database");
115
116 Ok(pool)
117}
118
119async fn apply_helpers(pool: &deadpool_postgres::Pool, formatter: &OutputFormatter) -> Result<()> {
121 formatter.progress("📝 Applying SQL helpers...");
122
123 let client = pool.get().await.context("Failed to acquire database connection")?;
125
126 for statement in MUTATION_RESPONSE_SQL.split(';') {
131 let trimmed = statement.trim();
132 if trimmed.is_empty() || trimmed.starts_with("--") {
133 continue;
134 }
135
136 client.execute(trimmed, &[]).await.with_context(|| {
137 format!("Failed to execute SQL: {}", trimmed.lines().next().unwrap_or(""))
138 })?;
139 }
140
141 formatter.progress("✓ SQL helpers applied");
142
143 let version: String = client
145 .query_one("SELECT fraiseql.library_version() AS version", &[])
146 .await
147 .context("Failed to verify helper installation")?
148 .get("version");
149
150 if version == HELPERS_VERSION {
151 info!("Helper version verified: {}", version);
152 } else {
153 formatter.progress(&format!(
155 "⚠️ Version mismatch: expected {}, got {}",
156 HELPERS_VERSION, version
157 ));
158 }
159
160 Ok(())
161}
162
163#[cfg(test)]
164mod tests;