bazzar/migration/src/main.rs
2023-06-19 17:09:53 +02:00

435 lines
12 KiB
Rust

#![feature(concat_idents)]
use std::error::Error;
use std::fmt::Display;
use std::process::exit;
use clap::*;
use dotenv::dotenv;
use migration::schema_list::PostgreSQLSchema;
use migration::sea_orm::{ConnectOptions, Database, DbConn};
use sea_orm_migration::prelude::*;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
// const MIGRATION_DIR: &str = "./";
macro_rules! migrate_schema {
($cli: expr, $schema_name: ident, $migrator_name: ident) => {{
run_cli(
&mut $cli,
PostgreSQLSchema::$schema_name,
migration::$migrator_name,
)
.await;
}};
}
#[async_std::main]
async fn main() {
dotenv().ok();
let mut cli: Cli = Cli::parse();
init_logger(cli.verbose);
migrate_schema!(cli, Public, PublicMigrator);
migrate_schema!(cli, Jobs, JobsMigrator);
migrate_schema!(cli, Carts, CartsMigrator);
migrate_schema!(cli, Marketing, MarketingMigrator);
migrate_schema!(cli, Checkouts, CheckoutsMigrator);
migrate_schema!(cli, Shipping, ShippingMigrator);
migrate_schema!(cli, Stocks, StocksMigrator);
migrate_schema!(cli, Identity, IdentityMigrator);
migrate_schema!(cli, Notifications, NotificationsMigrator);
migrate_schema!(cli, Oauth2, OAuth2Migrator);
}
pub async fn run_cli<M>(cli: &mut Cli, schema: PostgreSQLSchema, migrator: M)
where
M: MigratorTrait,
{
let url = cli
.database_url
.as_ref()
.expect("Environment variable 'DATABASE_URL' not set");
let schema = schema.as_str().to_string();
let connect_options = ConnectOptions::new(url.clone())
.set_schema_search_path(schema.clone())
.to_owned();
let db = Database::connect(connect_options)
.await
.expect("Fail to acquire database connection");
db.execute_unprepared(&format!("CREATE SCHEMA {}", schema))
.await
.ok();
db.execute_unprepared(&format!("SET search_path = '{}'", schema))
.await
.unwrap();
let res = run_migrate(migrator, &db, cli.command.clone()).await;
if cfg!(debug_assertions) {
res.unwrap();
} else {
res.unwrap_or_else(handle_error);
}
}
pub async fn run_migrate<M>(
_: M,
db: &DbConn,
command: Option<MigrateSubcommands>,
) -> Result<(), Box<dyn Error>>
where
M: MigratorTrait,
{
match command {
Some(MigrateSubcommands::Fresh) => M::fresh(db).await?,
Some(MigrateSubcommands::Refresh) => M::refresh(db).await?,
Some(MigrateSubcommands::Reset) => M::reset(db).await?,
Some(MigrateSubcommands::Status) => M::status(db).await?,
Some(MigrateSubcommands::Up { num }) => M::up(db, num).await?,
Some(MigrateSubcommands::Down { num }) => M::down(db, Some(num)).await?,
// Some(MigrateSubcommands::Init) => run_migrate_init(MIGRATION_DIR)?,
// Some(MigrateSubcommands::Generate {
// migration_name,
// universal_time: _,
// local_time,
// }) => run_migrate_generate(MIGRATION_DIR, &migration_name, !local_time)?,
_ => M::up(db, None).await?,
};
Ok(())
}
fn init_logger(verbose: bool) {
let filter = match verbose {
true => "debug",
false => "sea_orm_migration=trace",
};
let filter_layer = EnvFilter::try_new(filter).unwrap();
if verbose {
let fmt_layer = tracing_subscriber::fmt::layer();
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.init()
} else {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(true)
.with_level(true)
.without_time();
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.init()
};
}
fn handle_error<E>(error: E)
where
E: Display,
{
eprintln!("{error}");
exit(1);
}
#[derive(Parser)]
#[clap(version)]
pub struct Cli {
#[clap(action, short = 'v', long, global = true, help = "Show debug messages")]
verbose: bool,
#[clap(
value_parser,
global = true,
short = 'u',
long,
env = "DATABASE_URL",
help = "Database URL"
)]
database_url: Option<String>,
#[clap(subcommand)]
command: Option<MigrateSubcommands>,
}
#[derive(Subcommand, PartialEq, Eq, Debug)]
pub enum Commands {
#[clap(
about = "Codegen related commands",
arg_required_else_help = true,
display_order = 10
)]
Generate {
#[clap(subcommand)]
command: GenerateSubcommands,
},
#[clap(about = "Migration related commands", display_order = 20)]
Migrate {
#[clap(
value_parser,
global = true,
short = 'd',
long,
help = "Migration script directory.
If your migrations are in their own crate,
you can provide the root of that crate.
If your migrations are in a submodule of your app,
you should provide the directory of that submodule.",
default_value = "./migration"
)]
migration_dir: String,
#[clap(
value_parser,
global = true,
short = 'u',
long,
env = "DATABASE_URL",
help = "Database URL"
)]
database_url: Option<String>,
#[clap(subcommand)]
command: Option<MigrateSubcommands>,
},
}
#[derive(Subcommand, PartialEq, Eq, Debug, Clone)]
pub enum MigrateSubcommands {
#[clap(about = "Initialize migration directory", display_order = 10)]
Init,
#[clap(about = "Generate a new, empty migration", display_order = 20)]
Generate {
#[clap(
value_parser,
required = true,
takes_value = true,
help = "Name of the new migration"
)]
migration_name: String,
#[clap(
action,
long,
default_value = "true",
help = "Generate migration file based on Utc time",
conflicts_with = "local-time",
display_order = 1001
)]
universal_time: bool,
#[clap(
action,
long,
help = "Generate migration file based on Local time",
conflicts_with = "universal-time",
display_order = 1002
)]
local_time: bool,
},
#[clap(
about = "Drop all tables from the database, then reapply all migrations",
display_order = 30
)]
Fresh,
#[clap(
about = "Rollback all applied migrations, then reapply all migrations",
display_order = 40
)]
Refresh,
#[clap(about = "Rollback all applied migrations", display_order = 50)]
Reset,
#[clap(about = "Check the status of all migrations", display_order = 60)]
Status,
#[clap(about = "Apply pending migrations", display_order = 70)]
Up {
#[clap(
value_parser,
short,
long,
help = "Number of pending migrations to apply"
)]
num: Option<u32>,
},
#[clap(
value_parser,
about = "Rollback applied migrations",
display_order = 80
)]
Down {
#[clap(
value_parser,
short,
long,
default_value = "1",
help = "Number of applied migrations to be rolled back",
display_order = 90
)]
num: u32,
},
}
#[derive(Subcommand, PartialEq, Eq, Debug)]
pub enum GenerateSubcommands {
#[clap(about = "Generate entity")]
#[clap(arg_required_else_help = true)]
#[clap(group(ArgGroup::new("formats").args(&["compact-format", "expanded-format"])))]
#[clap(group(ArgGroup::new("group-tables").args(&["tables", "include-hidden-tables"])))]
Entity {
#[clap(action, long, help = "Generate entity file of compact format")]
compact_format: bool,
#[clap(action, long, help = "Generate entity file of expanded format")]
expanded_format: bool,
#[clap(
action,
long,
help = "Generate entity file for hidden tables (i.e. table name starts with an underscore)"
)]
include_hidden_tables: bool,
#[clap(
value_parser,
short = 't',
long,
use_value_delimiter = true,
takes_value = true,
help = "Generate entity file for specified tables only (comma separated)"
)]
tables: Vec<String>,
#[clap(
value_parser,
long,
use_value_delimiter = true,
takes_value = true,
default_value = "seaql_migrations",
help = "Skip generating entity file for specified tables (comma separated)"
)]
ignore_tables: Vec<String>,
#[clap(
value_parser,
long,
default_value = "1",
help = "The maximum amount of connections to use when connecting to the database."
)]
max_connections: u32,
#[clap(
value_parser,
short = 'o',
long,
default_value = "./",
help = "Entity file output directory"
)]
output_dir: String,
#[clap(
value_parser,
short = 's',
long,
env = "DATABASE_SCHEMA",
default_value = "public",
long_help = "Database schema\n \
- For MySQL, this argument is ignored.\n \
- For PostgreSQL, this argument is optional with default value 'public'."
)]
database_schema: String,
#[clap(
value_parser,
short = 'u',
long,
env = "DATABASE_URL",
help = "Database URL"
)]
database_url: String,
#[clap(
value_parser,
long,
default_value = "none",
help = "Automatically derive serde Serialize / Deserialize traits for the entity (none, \
serialize, deserialize, both)"
)]
with_serde: String,
#[clap(
action,
long,
help = "Generate a serde field attribute, '#[serde(skip_deserializing)]', for the primary key fields to skip them during deserialization, this flag will be affective only when '--with-serde' is 'both' or 'deserialize'"
)]
serde_skip_deserializing_primary_key: bool,
#[clap(
action,
long,
default_value = "false",
help = "Opt-in to add skip attributes to hidden columns (i.e. when 'with-serde' enabled and column name starts with an underscore)"
)]
serde_skip_hidden_column: bool,
#[clap(
action,
long,
default_value = "false",
long_help = "Automatically derive the Copy trait on generated enums.\n\
Enums generated from a database don't have associated data by default, and as such can \
derive Copy.
"
)]
with_copy_enums: bool,
#[clap(
arg_enum,
value_parser,
long,
default_value = "chrono",
help = "The datetime crate to use for generating entities."
)]
date_time_crate: DateTimeCrate,
#[clap(
action,
long,
short = 'l',
default_value = "false",
help = "Generate index file as `lib.rs` instead of `mod.rs`."
)]
lib: bool,
#[clap(
value_parser,
long,
use_value_delimiter = true,
takes_value = true,
help = "Add extra derive macros to generated model struct (comma separated), e.g. `--model-extra-derives 'ts_rs::Ts','CustomDerive'`"
)]
model_extra_derives: Vec<String>,
#[clap(
value_parser,
long,
use_value_delimiter = true,
takes_value = true,
help = r#"Add extra attributes to generated model struct, no need for `#[]` (comma separated), e.g. `--model-extra-attributes 'serde(rename_all = "camelCase")','ts(export)'`"#
)]
model_extra_attributes: Vec<String>,
},
}
#[derive(ArgEnum, Copy, Clone, Debug, PartialEq, Eq)]
pub enum DateTimeCrate {
Chrono,
Time,
}