1use clap::{ArgAction, ArgGroup, Parser, Subcommand, ValueEnum};
2#[cfg(feature = "codegen")]
3use dotenvy::dotenv;
4
5#[cfg(feature = "codegen")]
6use crate::{handle_error, run_generate_command, run_migrate_command};
7
8#[derive(Parser, Debug)]
9#[command(
10 version,
11 author,
12 help_template = r#"{before-help}{name} {version}
13{about-with-newline}
14
15{usage-heading} {usage}
16
17{all-args}{after-help}
18
19AUTHORS:
20 {author}
21"#,
22 about = r#"
23 ____ ___ ____ __ __ /\
24 / ___| ___ __ _ / _ \ | _ \ | \/ | {.-}
25 \___ \ / _ \ / _` || | | || |_) || |\/| | ;_.-'\
26 ___) || __/| (_| || |_| || _ < | | | | { _.}_
27 |____/ \___| \__,_| \___/ |_| \_\|_| |_| \.-' / `,
28 \ | /
29 An async & dynamic ORM for Rust \ | ,/
30 =============================== \|_/
31
32 Getting Started
33 - Documentation: https://www.sea-ql.org/SeaORM
34 - Tutorial: https://www.sea-ql.org/sea-orm-tutorial
35 - Examples: https://github.com/SeaQL/sea-orm/tree/master/examples
36 - Cookbook: https://www.sea-ql.org/sea-orm-cookbook
37
38 Join our Discord server to chat with others in the SeaQL community!
39 - Invitation: https://discord.com/invite/uCPdDXzbdv
40
41 SeaQL Community Survey 2024
42 - Link: https://sea-ql.org/community-survey
43
44 If you like what we do, consider starring, sharing and contributing!
45"#
46)]
47pub struct Cli {
48 #[arg(global = true, short, long, help = "Show debug messages")]
49 pub verbose: bool,
50
51 #[command(subcommand)]
52 pub command: Commands,
53}
54
55#[derive(Subcommand, PartialEq, Eq, Debug)]
56pub enum Commands {
57 #[command(
58 about = "Codegen related commands",
59 arg_required_else_help = true,
60 display_order = 10
61 )]
62 Generate {
63 #[command(subcommand)]
64 command: GenerateSubcommands,
65 },
66 #[command(about = "Migration related commands", display_order = 20)]
67 Migrate {
68 #[arg(
69 global = true,
70 short = 'd',
71 long,
72 env = "MIGRATION_DIR",
73 help = "Migration script directory.
74If your migrations are in their own crate,
75you can provide the root of that crate.
76If your migrations are in a submodule of your app,
77you should provide the directory of that submodule.",
78 default_value = "./migration"
79 )]
80 migration_dir: String,
81
82 #[arg(
83 global = true,
84 short = 's',
85 long,
86 env = "DATABASE_SCHEMA",
87 long_help = "Database schema\n \
88 - For MySQL and SQLite, this argument is ignored.\n \
89 - For PostgreSQL, this argument is optional with default value 'public'.\n"
90 )]
91 database_schema: Option<String>,
92
93 #[arg(
94 global = true,
95 short = 'u',
96 long,
97 env = "DATABASE_URL",
98 help = "Database URL"
99 )]
100 database_url: Option<String>,
101
102 #[command(subcommand)]
103 command: Option<MigrateSubcommands>,
104 },
105}
106
107#[derive(Subcommand, PartialEq, Eq, Debug)]
108pub enum MigrateSubcommands {
109 #[command(about = "Initialize migration directory", display_order = 10)]
110 Init,
111 #[command(about = "Generate a new, empty migration", display_order = 20)]
112 Generate {
113 #[arg(required = true, help = "Name of the new migration")]
114 migration_name: String,
115
116 #[arg(
117 long,
118 default_value = "true",
119 help = "Generate migration file based on Utc time",
120 conflicts_with = "local_time",
121 display_order = 1001
122 )]
123 universal_time: bool,
124
125 #[arg(
126 long,
127 help = "Generate migration file based on Local time",
128 conflicts_with = "universal_time",
129 display_order = 1002
130 )]
131 local_time: bool,
132 },
133 #[command(
134 about = "Drop all tables from the database, then reapply all migrations",
135 display_order = 30
136 )]
137 Fresh,
138 #[command(
139 about = "Rollback all applied migrations, then reapply all migrations",
140 display_order = 40
141 )]
142 Refresh,
143 #[command(about = "Rollback all applied migrations", display_order = 50)]
144 Reset,
145 #[command(about = "Check the status of all migrations", display_order = 60)]
146 Status,
147 #[command(about = "Apply pending migrations", display_order = 70)]
148 Up {
149 #[arg(short, long, help = "Number of pending migrations to apply")]
150 num: Option<u32>,
151 },
152 #[command(about = "Rollback applied migrations", display_order = 80)]
153 Down {
154 #[arg(
155 short,
156 long,
157 default_value = "1",
158 help = "Number of applied migrations to be rolled back",
159 display_order = 90
160 )]
161 num: u32,
162 },
163}
164
165#[derive(Subcommand, PartialEq, Eq, Debug)]
166pub enum GenerateSubcommands {
167 #[command(about = "Generate entity")]
168 #[command(group(ArgGroup::new("formats").args(&["compact_format", "expanded_format"])))]
169 #[command(group(ArgGroup::new("group-tables").args(&["tables", "include_hidden_tables"])))]
170 Entity {
171 #[arg(long, help = "Generate entity file of compact format")]
172 compact_format: bool,
173
174 #[arg(long, help = "Generate entity file of expanded format")]
175 expanded_format: bool,
176
177 #[arg(
178 long,
179 help = "Generate entity file for hidden tables (i.e. table name starts with an underscore)"
180 )]
181 include_hidden_tables: bool,
182
183 #[arg(
184 short = 't',
185 long,
186 value_delimiter = ',',
187 help = "Generate entity file for specified tables only (comma separated)"
188 )]
189 tables: Vec<String>,
190
191 #[arg(
192 long,
193 value_delimiter = ',',
194 default_value = "seaql_migrations",
195 help = "Skip generating entity file for specified tables (comma separated)"
196 )]
197 ignore_tables: Vec<String>,
198
199 #[arg(
200 long,
201 default_value = "1",
202 help = "The maximum amount of connections to use when connecting to the database."
203 )]
204 max_connections: u32,
205
206 #[arg(
207 long,
208 default_value = "30",
209 long_help = "Acquire timeout in seconds of the connection used for schema discovery"
210 )]
211 acquire_timeout: u64,
212
213 #[arg(
214 short = 'o',
215 long,
216 default_value = "./",
217 help = "Entity file output directory"
218 )]
219 output_dir: String,
220
221 #[arg(
222 short = 's',
223 long,
224 env = "DATABASE_SCHEMA",
225 long_help = "Database schema\n \
226 - For MySQL, this argument is ignored.\n \
227 - For PostgreSQL, this argument is optional with default value 'public'."
228 )]
229 database_schema: Option<String>,
230
231 #[arg(short = 'u', long, env = "DATABASE_URL", help = "Database URL")]
232 database_url: String,
233
234 #[arg(
235 long,
236 default_value = "all",
237 help = "Generate prelude.rs file (all, none, all-allow-unused-imports)"
238 )]
239 with_prelude: String,
240
241 #[arg(
242 long,
243 default_value = "none",
244 help = "Automatically derive serde Serialize / Deserialize traits for the entity (none, \
245 serialize, deserialize, both)"
246 )]
247 with_serde: String,
248
249 #[arg(
250 long,
251 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'"
252 )]
253 serde_skip_deserializing_primary_key: bool,
254
255 #[arg(
256 long,
257 default_value = "false",
258 help = "Opt-in to add skip attributes to hidden columns (i.e. when 'with-serde' enabled and column name starts with an underscore)"
259 )]
260 serde_skip_hidden_column: bool,
261
262 #[arg(
263 long,
264 default_value = "false",
265 long_help = "Automatically derive the Copy trait on generated enums.\n\
266 Enums generated from a database don't have associated data by default, and as such can \
267 derive Copy.
268 "
269 )]
270 with_copy_enums: bool,
271
272 #[arg(
273 long,
274 default_value_t,
275 value_enum,
276 help = "The datetime crate to use for generating entities."
277 )]
278 date_time_crate: DateTimeCrate,
279
280 #[arg(
281 long,
282 short = 'l',
283 default_value = "false",
284 help = "Generate index file as `lib.rs` instead of `mod.rs`."
285 )]
286 lib: bool,
287
288 #[arg(
289 long,
290 value_delimiter = ',',
291 help = "Add extra derive macros to generated model struct (comma separated), e.g. `--model-extra-derives 'ts_rs::Ts','CustomDerive'`"
292 )]
293 model_extra_derives: Vec<String>,
294
295 #[arg(
296 long,
297 value_delimiter = ',',
298 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)'`"#
299 )]
300 model_extra_attributes: Vec<String>,
301
302 #[arg(
303 long,
304 value_delimiter = ',',
305 help = "Add extra derive macros to generated enums (comma separated), e.g. `--enum-extra-derives 'ts_rs::Ts','CustomDerive'`"
306 )]
307 enum_extra_derives: Vec<String>,
308
309 #[arg(
310 long,
311 value_delimiter = ',',
312 help = r#"Add extra attributes to generated enums, no need for `#[]` (comma separated), e.g. `--enum-extra-attributes 'serde(rename_all = "camelCase")','ts(export)'`"#
313 )]
314 enum_extra_attributes: Vec<String>,
315
316 #[arg(
317 long,
318 default_value = "false",
319 long_help = "Generate helper Enumerations that are used by Seaography."
320 )]
321 seaography: bool,
322
323 #[arg(
324 long,
325 default_value = "true",
326 default_missing_value = "true",
327 num_args = 0..=1,
328 require_equals = true,
329 action = ArgAction::Set,
330 long_help = "Generate empty ActiveModelBehavior impls."
331 )]
332 impl_active_model_behavior: bool,
333 },
334}
335
336#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum, Default)]
337pub enum DateTimeCrate {
338 #[default]
339 Chrono,
340 Time,
341}
342
343#[cfg(feature = "codegen")]
346pub async fn main() {
347 dotenv().ok();
348
349 let cli = Cli::parse();
350 let verbose = cli.verbose;
351
352 match cli.command {
353 Commands::Generate { command } => {
354 run_generate_command(command, verbose)
355 .await
356 .unwrap_or_else(handle_error);
357 }
358 Commands::Migrate {
359 migration_dir,
360 database_schema,
361 database_url,
362 command,
363 } => run_migrate_command(
364 command,
365 &migration_dir,
366 database_schema,
367 database_url,
368 verbose,
369 )
370 .unwrap_or_else(handle_error),
371 }
372}