Add CLI application with database migration and entity generation commands
- Introduced a new CLI application in the `apps/cli` directory. - Implemented commands for database migration and entity generation. - Updated `Cargo.toml` files to include necessary dependencies. - Enhanced the `justfile` to facilitate CLI command execution. - Modified workspace configuration to include the new CLI application.
This commit is contained in:
16
apps/cli/Cargo.toml
Normal file
16
apps/cli/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "cli"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.89"
|
||||
container-simulate = { path = "../container" }
|
||||
migration = {path = "../../public/migration"}
|
||||
shared = {path = "../../public/shared"}
|
||||
testcontainers = "0.24.0"
|
||||
tokio = { version = "1.47.0", features = ["full"] }
|
||||
url = "2.5.7"
|
||||
clap = { version = "4.5.48", features = ["derive", "env"] }
|
||||
path-clean = "1.0.1"
|
||||
once_cell = "1.21.3"
|
||||
45
apps/cli/src/cmd.rs
Normal file
45
apps/cli/src/cmd.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::pin::Pin;
|
||||
use std::{future::Future, process::exit};
|
||||
|
||||
use clap::{ArgMatches, Command};
|
||||
|
||||
pub mod db_migrate_and_generate;
|
||||
|
||||
pub struct CliCommand {
|
||||
pub command: Command,
|
||||
pub action: fn(&clap::ArgMatches) -> Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
|
||||
}
|
||||
|
||||
static CLI_COMMANDS: once_cell::sync::Lazy<
|
||||
[CliCommand; 1 /* Update this count when adding new commands */],
|
||||
> =
|
||||
once_cell::sync::Lazy::new(|| {
|
||||
[
|
||||
// Add new commands here
|
||||
db_migrate_and_generate::get_cli_command(),
|
||||
]
|
||||
});
|
||||
|
||||
pub fn get_command() -> Command {
|
||||
let mut c = Command::new("cmd");
|
||||
|
||||
for cmd in CLI_COMMANDS.iter() {
|
||||
c = c.subcommand(cmd.command.clone());
|
||||
}
|
||||
|
||||
c
|
||||
}
|
||||
|
||||
pub fn execute(matches: &ArgMatches, help_msg: &str) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
if let Some((subcommand_name, subcommand_matches)) = matches.subcommand() {
|
||||
for cmd in CLI_COMMANDS.iter() {
|
||||
if cmd.command.get_name() == subcommand_name {
|
||||
return (cmd.action)(subcommand_matches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("Error: No valid subcommand provided.");
|
||||
eprintln!("{}", help_msg);
|
||||
exit(1);
|
||||
}
|
||||
169
apps/cli/src/cmd/db_migrate_and_generate.rs
Normal file
169
apps/cli/src/cmd/db_migrate_and_generate.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use clap::{Arg, Command};
|
||||
use container::{
|
||||
db::{DBInfo, sqlite::SQLiteContainer},
|
||||
types::ConfigInfoType,
|
||||
};
|
||||
use migration::{generate_entity, migrate_database};
|
||||
use shared::db_type::DBType;
|
||||
|
||||
use crate::cmd::CliCommand;
|
||||
|
||||
const MAX_DB_READY_ATTEMPTS: u8 = 10;
|
||||
const DB_READY_CHECK_INTERVAL_SECS: u64 = 2;
|
||||
const DB_READY_STRING: [&str; 1] = ["ready to accept connections"];
|
||||
|
||||
pub fn get_cli_command() -> CliCommand {
|
||||
CliCommand {
|
||||
command: command(),
|
||||
action: action,
|
||||
}
|
||||
}
|
||||
|
||||
fn command() -> Command {
|
||||
Command::new("db:migrate_and_generate")
|
||||
.arg(
|
||||
Arg::new("output_path")
|
||||
.short('o')
|
||||
.long("output-path")
|
||||
.value_name("PATH")
|
||||
.help("Path to output the generated entity schema")
|
||||
.required(true),
|
||||
)
|
||||
.about("Migrate database and generate entity schema")
|
||||
}
|
||||
|
||||
fn action(
|
||||
_matches: &clap::ArgMatches,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
|
||||
let output_path = _matches.get_one::<String>("output_path");
|
||||
let output_path = output_path.unwrap().to_string();
|
||||
Box::pin(async move {
|
||||
let mut error_occurred = false;
|
||||
let database_configs = vec![
|
||||
SQLiteContainer::new(None)
|
||||
.await
|
||||
.get_db_container_config_info()
|
||||
.await,
|
||||
// only sqlite is required to generate entities when using Seaorm
|
||||
// PostgreSQLContainer::new(None)
|
||||
// .await
|
||||
// .get_db_container_config_info()
|
||||
// .await,
|
||||
];
|
||||
|
||||
for db_config in database_configs {
|
||||
let config = container::Config {
|
||||
database: db_config,
|
||||
};
|
||||
let mut detached_handler = container::start_detached(&config).await;
|
||||
match migrate_and_generate_entity(&config, &output_path).await {
|
||||
Ok(_) => println!("Migration and entity generation succeeded."),
|
||||
Err(_) => {
|
||||
eprintln!("Migration and entity generation failed.");
|
||||
error_occurred = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
detached_handler.stop().await;
|
||||
}
|
||||
|
||||
if error_occurred {
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
std::process::exit(0);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn migrate_and_generate_entity(
|
||||
config: &container::Config,
|
||||
output_path: &str,
|
||||
) -> Result<(), ()> {
|
||||
let ready_result = await_database_ready(&config).await;
|
||||
if ready_result.is_err() {
|
||||
eprintln!("Database did not become ready in time.");
|
||||
return Err(());
|
||||
}
|
||||
|
||||
let db_url = match &config.database {
|
||||
ConfigInfoType::Containerized(container_info) => &container_info.url,
|
||||
ConfigInfoType::PreExisting(pre_existing_info) => &pre_existing_info.url,
|
||||
};
|
||||
|
||||
let db_type = get_database_type(&config);
|
||||
match migrate_database(db_url).await {
|
||||
Ok(_) => {
|
||||
println!("Database migrated successfully for {:?}", db_type);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to migrate database for {}: {:#?}", db_type, e);
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
|
||||
match generate_entity(db_url, output_path).await {
|
||||
Ok(_) => {
|
||||
println!(
|
||||
"Database entity schema generated successfully for {:?}",
|
||||
db_type
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Failed to generate database entity schema for {}: {:#?}",
|
||||
db_type, e
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_database_type(config: &container::Config) -> DBType {
|
||||
match config.database {
|
||||
ConfigInfoType::Containerized(ref container_info) => container_info.db_type.clone(),
|
||||
ConfigInfoType::PreExisting(ref pre_existing_info) => pre_existing_info.db_type.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn await_database_ready(config: &container::Config) -> Result<(), ()> {
|
||||
match config.database {
|
||||
ConfigInfoType::Containerized(ref container_info) => {
|
||||
let container_type = &container_info.db_type;
|
||||
for attempt in 1..=MAX_DB_READY_ATTEMPTS {
|
||||
println!(
|
||||
"Checking if database container {} is ready (attempt {}/{})...",
|
||||
container_info.db_type, attempt, MAX_DB_READY_ATTEMPTS
|
||||
);
|
||||
let logs = match container_info.container.stdout_to_vec().await {
|
||||
Ok(logs) => logs,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get container logs: {}", e);
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
let log_output = String::from_utf8_lossy(&logs);
|
||||
if DB_READY_STRING.iter().any(|&s| log_output.contains(s)) {
|
||||
println!("Database container {} is ready.", container_info.db_type);
|
||||
return Ok(());
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(DB_READY_CHECK_INTERVAL_SECS))
|
||||
.await;
|
||||
}
|
||||
eprintln!(
|
||||
"Database container {} did not become ready after {} attempts.",
|
||||
container_type, MAX_DB_READY_ATTEMPTS
|
||||
);
|
||||
Err(())
|
||||
}
|
||||
|
||||
ConfigInfoType::PreExisting(ref pre_existing_info) => {
|
||||
println!(
|
||||
"Pre-existing database of type {} assumed to be ready.",
|
||||
pre_existing_info.db_type
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
22
apps/cli/src/main.rs
Normal file
22
apps/cli/src/main.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
mod cmd;
|
||||
|
||||
use clap::error::ErrorKind;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut command = cmd::get_command();
|
||||
let help_output = format!("{}", command.render_help());
|
||||
let matches = command
|
||||
.try_get_matches()
|
||||
.unwrap_or_else(|err| match err.kind() {
|
||||
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
|
||||
err.print().expect("Error writing Error");
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {
|
||||
err.print().expect("Error writing Error");
|
||||
std::process::exit(1);
|
||||
}
|
||||
});
|
||||
cmd::execute(&matches, &help_output).await;
|
||||
}
|
||||
Reference in New Issue
Block a user