use crate::cli::SeedArgs;
use crate::util::{Result, repo_root, usage_error};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
const DEFAULT_AUTH_DATABASE_URL: &str =
"postgres://executesoft:executesoft@localhost:5432/core_auth?sslmode=disable";
pub(crate) fn run_seed(args: SeedArgs) -> Result<()> {
let seed_dir = seed_dir(&args)?;
let database_url = args
.database_url
.or_else(|| env::var("DATABASE_URL").ok())
.unwrap_or_else(|| DEFAULT_AUTH_DATABASE_URL.to_string());
let files = seed_files(&seed_dir)?;
if files.is_empty() {
return usage_error(format!("no seed SQL files found in {}", seed_dir.display()));
}
println!("Applying local seeds:");
println!(" database: {}", redact_database_url(&database_url));
println!(" seed dir: {}", seed_dir.display());
for file in files {
apply_seed_file(&database_url, &file)?;
}
println!("Local seeds applied.");
Ok(())
}
fn seed_dir(args: &SeedArgs) -> Result<PathBuf> {
if let Some(seed_dir) = &args.seed_dir {
return Ok(path_from_repo(seed_dir));
}
match args.service.as_str() {
"auth" => Ok(repo_root().join("services/core/auth/migrations/seed")),
service => usage_error(format!(
"unsupported seed service `{service}`; pass --seed-dir for a custom seed directory"
)),
}
}
fn path_from_repo(value: &str) -> PathBuf {
let path = PathBuf::from(value);
if path.is_absolute() {
path
} else {
repo_root().join(path)
}
}
fn seed_files(dir: &Path) -> Result<Vec<PathBuf>> {
if !dir.exists() {
return usage_error(format!("seed directory does not exist: {}", dir.display()));
}
let mut files = Vec::new();
for entry in fs::read_dir(dir)? {
let path = entry?.path();
if path.is_file() && path.extension().is_some_and(|extension| extension == "sql") {
files.push(path);
}
}
files.sort();
Ok(files)
}
fn apply_seed_file(database_url: &str, file: &Path) -> Result<()> {
println!("Applying seed: {}", file.display());
let status = Command::new("psql")
.args(["-v", "ON_ERROR_STOP=1", database_url, "-f"])
.arg(file)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map_err(|error| {
format!(
"failed to start psql: {error}. Install psql or pass a working local DATABASE_URL."
)
})?;
if status.success() {
Ok(())
} else {
Err(format!("psql failed for {} with {status}", file.display()).into())
}
}
fn redact_database_url(database_url: &str) -> String {
let Some((scheme, rest)) = database_url.split_once("://") else {
return database_url.to_string();
};
let Some((credentials, host_and_path)) = rest.split_once('@') else {
return database_url.to_string();
};
let Some((user, _password)) = credentials.split_once(':') else {
return database_url.to_string();
};
format!("{scheme}://{user}:<redacted>@{host_and_path}")
}
#[cfg(test)]
mod tests {
use super::redact_database_url;
#[test]
fn redacts_database_password() {
assert_eq!(
redact_database_url("postgres://executesoft:secret@localhost:5432/core_auth"),
"postgres://executesoft:<redacted>@localhost:5432/core_auth"
);
}
}