doido_generators/commands/db.rs
1//! `doido db` — database management.
2//!
3//! Exposes every SeaORM CLI subcommand and option verbatim (`doido db migrate
4//! …`, `doido db generate entity …`) plus Doido's own `doido db create`, which
5//! SeaORM does not provide. Doido changes two SeaORM defaults to match its app
6//! layout:
7//! * migrations live in [`DEFAULT_MIGRATION_DIR`] (`db/migration`), and
8//! * generated entities are written to [`DEFAULT_ENTITY_OUTPUT_DIR`]
9//! (`app/models/_entities`).
10//!
11//! A user-supplied `-d/--migration-dir` or `-o/--output-dir` always wins.
12
13use clap::Subcommand;
14use sea_orm_cli::{
15 handle_error, run_generate_command, run_migrate_command, Commands, GenerateSubcommands,
16};
17
18/// Subcommands of `doido db`: Doido's `create` plus the flattened SeaORM CLI.
19#[derive(Subcommand)]
20// The flattened SeaORM `Commands` is large, but this is parsed once at startup
21// and can't be boxed through clap's `#[command(flatten)]`.
22#[allow(clippy::large_enum_variant)]
23pub enum DbCommand {
24 /// Create the database for the current environment
25 Create,
26 /// SeaORM CLI commands (migrate, generate entity)
27 #[command(flatten)]
28 SeaOrm(Commands),
29}
30
31/// Where Doido keeps its SeaORM migration crate.
32const DEFAULT_MIGRATION_DIR: &str = "db/migration";
33/// Where Doido writes generated SeaORM entities.
34const DEFAULT_ENTITY_OUTPUT_DIR: &str = "app/models/_entities";
35
36/// Upstream SeaORM CLI defaults — used to detect "the user didn't override this".
37const SEA_ORM_CLI_DEFAULT_MIGRATION_DIR: &str = "./migration";
38const SEA_ORM_CLI_DEFAULT_OUTPUT_DIR: &str = "./";
39
40/// Populates `DATABASE_URL` from the app's `config/<env>.yml` (`database.url`)
41/// when it isn't already set in the environment.
42///
43/// SeaORM CLI reads the database URL from the `DATABASE_URL` env var (both
44/// `migrate` and `generate entity` bind to it). Seeding it from config means
45/// `doido db …` works without the user exporting `DATABASE_URL` by hand, while
46/// an explicit `-u/--database-url` or a pre-set env var still wins. Call this
47/// before clap parses so the required `generate entity` URL is satisfied.
48pub fn ensure_database_url_from_config() {
49 if std::env::var_os("DATABASE_URL").is_some() {
50 return;
51 }
52 // Only seed from a real config file; absent config leaves DATABASE_URL unset
53 // so the user gets the usual "missing database URL" error rather than a
54 // surprising default.
55 if let Ok(config) = doido_model::config::YamlConfig::load() {
56 std::env::set_var("DATABASE_URL", config.database.url);
57 }
58}
59
60/// Runs a `doido db <command>`.
61pub async fn run(command: DbCommand, verbose: bool) {
62 match command {
63 DbCommand::Create => create().await,
64 DbCommand::SeaOrm(command) => run_sea_orm(command, verbose).await,
65 }
66}
67
68/// Creates the database named by the resolved [`database_url`].
69async fn create() {
70 let url = database_url();
71 match doido_model::create_database(&url).await {
72 Ok(()) => doido_core::tracing::info!("created database: {url}"),
73 Err(e) if e.to_string().contains("already exists") => {
74 doido_core::tracing::info!("database already exists: {url}");
75 }
76 Err(e) => handle_error(e),
77 }
78}
79
80/// Resolves the database URL from `DATABASE_URL` or `config/<env>.yml`, exiting
81/// with an error if neither is available.
82fn database_url() -> String {
83 if let Ok(url) = std::env::var("DATABASE_URL") {
84 return url;
85 }
86 if let Ok(config) = doido_model::config::YamlConfig::load() {
87 return config.database.url;
88 }
89 doido_core::tracing::error!("DATABASE_URL is not set and config/<env>.yml could not be read");
90 std::process::exit(1);
91}
92
93/// Dispatches a flattened SeaORM CLI command, applying Doido's directory defaults.
94async fn run_sea_orm(command: Commands, verbose: bool) {
95 match command {
96 Commands::Generate { mut command } => {
97 apply_entity_output_default(&mut command);
98 run_generate_command(command, verbose)
99 .await
100 .unwrap_or_else(handle_error);
101 }
102 Commands::Migrate {
103 migration_dir,
104 database_schema,
105 database_url,
106 command,
107 } => {
108 let migration_dir = override_migration_dir(migration_dir);
109 run_migrate_command(
110 command,
111 &migration_dir,
112 database_schema,
113 database_url,
114 verbose,
115 )
116 .unwrap_or_else(handle_error);
117 }
118 }
119}
120
121/// Substitutes Doido's migration directory when the user left the SeaORM default.
122fn override_migration_dir(migration_dir: String) -> String {
123 if migration_dir == SEA_ORM_CLI_DEFAULT_MIGRATION_DIR {
124 DEFAULT_MIGRATION_DIR.to_string()
125 } else {
126 migration_dir
127 }
128}
129
130/// Substitutes Doido's entity output directory when the user left the SeaORM default.
131fn apply_entity_output_default(command: &mut GenerateSubcommands) {
132 let GenerateSubcommands::Entity { output_dir, .. } = command;
133 if output_dir == SEA_ORM_CLI_DEFAULT_OUTPUT_DIR {
134 *output_dir = DEFAULT_ENTITY_OUTPUT_DIR.to_string();
135 }
136}