use std::path::PathBuf;
use clap::Subcommand;
use rustio_admin::migrations;
#[derive(Subcommand)]
pub enum Action {
Apply {
#[arg(long, default_value = "migrations")]
dir: PathBuf,
},
Status {
#[arg(long, default_value = "migrations")]
dir: PathBuf,
},
}
pub async fn run(action: Action) -> Result<(), String> {
let db = crate::db().await?;
match action {
Action::Apply { dir } => apply(db, dir).await,
Action::Status { dir } => status(db, dir).await,
}
}
async fn apply(db: rustio_admin::Db, dir: PathBuf) -> Result<(), String> {
let opts = migrations::ApplyOptions { verbose: true };
let step = crate::progress::Step::start("Applying migrations");
let applied = match migrations::apply_with(&db, &dir, opts).await {
Ok(a) => {
step.clear();
a
}
Err(e) => {
step.clear();
return Err(crate::ui::classify_migration_error(&format!("apply: {e}")).format());
}
};
if applied.is_empty() {
println!("Nothing to apply -- every migration is up to date.");
} else {
println!("Applied {} migration(s):", applied.len());
for name in applied {
println!(" ✓ {name}");
}
}
suggest_after_migrate(&db).await;
Ok(())
}
async fn suggest_after_migrate(db: &rustio_admin::Db) {
if !crate::style::is_interactive() {
return;
}
if active_admin_exists(db).await {
crate::style::next_step(
"launch your app",
&[(
"cargo run".to_string(),
format!(
"{} {}",
crate::style::hint("→"),
crate::style::url("http://127.0.0.1:8000/admin")
),
)],
);
} else {
let proj = current_project_name();
crate::style::next_step(
"create your admin login",
&[(
format!("rustio-admin user create --email admin@{proj}.local --role administrator"),
String::new(),
)],
);
}
}
async fn active_admin_exists(db: &rustio_admin::Db) -> bool {
let exists: bool = sqlx::query_scalar(
"SELECT EXISTS (
SELECT 1 FROM information_schema.tables WHERE table_name = 'rustio_users'
)",
)
.fetch_one(db.pool())
.await
.unwrap_or(false);
if !exists {
return false;
}
let n: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM rustio_users \
WHERE role IN ('administrator', 'developer') AND is_active = TRUE",
)
.fetch_one(db.pool())
.await
.unwrap_or(0);
n > 0
}
fn current_project_name() -> String {
std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "app".to_string())
}
async fn status(db: rustio_admin::Db, dir: PathBuf) -> Result<(), String> {
let entries = migrations::status(&db, &dir)
.await
.map_err(|e| format!("status: {e}"))?;
if entries.is_empty() {
println!("No migration files found in {}.", dir.display());
return Ok(());
}
let applied = entries.iter().filter(|(_, a)| *a).count();
let pending = entries.len() - applied;
for (name, ok) in &entries {
let mark = if *ok { "✓" } else { "·" };
println!(" {mark} {name}");
}
println!();
println!("{applied} applied, {pending} pending.");
Ok(())
}