17 Commits

Author SHA1 Message Date
d9105957a8 Merge pull request 'Setup database' (#1) from feature/database-setup into master
All checks were successful
Test / verify-generated-code (push) Successful in 1m9s
Test / test (push) Successful in 1m10s
Test / lint (push) Successful in 48s
Reviewed-on: #1
2025-11-19 21:49:30 +08:00
GW_MC
f0cfe5ec43 Cache rust toolchain in setup-rust action
All checks were successful
Test / verify-generated-code (pull_request) Successful in 2m15s
Test / test (pull_request) Successful in 1m10s
Test / lint (pull_request) Successful in 47s
2025-11-19 21:44:38 +08:00
GW_MC
afb10424d5 Merge branch 'master' into feature/database-setup
All checks were successful
Test / verify-generated-code (pull_request) Successful in 1m19s
Test / test (pull_request) Successful in 1m17s
Test / lint (pull_request) Successful in 58s
2025-11-19 21:17:35 +08:00
GW_MC
3de9ecc5c1 remove unused returns, and reexport ConnectOption
All checks were successful
Test / verify-generated-code (pull_request) Successful in 1m38s
Test / test (pull_request) Successful in 3m30s
Test / lint (pull_request) Successful in 3m57s
2025-11-19 21:02:28 +08:00
GW_MC
800c55238d fix clippy
All checks were successful
Test / test (pull_request) Successful in 3m14s
Test / verify-generated-code (pull_request) Successful in 3m43s
Test / lint (pull_request) Successful in 58s
2025-11-19 19:59:21 +08:00
GW_MC
6d1888e6c3 Fix clippy warnings
Some checks failed
Test / verify-generated-code (pull_request) Successful in 8m9s
Test / test (pull_request) Successful in 7m54s
Test / lint (pull_request) Failing after 3m21s
2025-11-19 19:27:43 +08:00
GW_MC
43c6b54ebd add gitkeep for api
Some checks failed
Test / verify-generated-code (pull_request) Successful in 7m53s
Test / test (pull_request) Successful in 8m3s
Test / lint (pull_request) Failing after 7m21s
2025-11-15 12:52:40 +08:00
GW_MC
17f7e06e8a Update verify generated code to watch only generated folders
Some checks failed
Test / verify-generated-code (pull_request) Failing after 7m55s
Test / test (pull_request) Successful in 7m59s
Test / lint (pull_request) Failing after 7m22s
2025-11-15 11:41:52 +08:00
GW_MC
467e6bfcf5 Add composite action for setting up Rust environment and create test workflow
Some checks failed
Test / verify-generated-code (pull_request) Has been cancelled
Test / test (pull_request) Has been cancelled
Test / lint (pull_request) Has been cancelled
2025-11-15 11:26:40 +08:00
GW_MC
d05d660198 Update generated entities with cli crate approach 2025-11-13 21:27:01 +08:00
GW_MC
829c4ef3e3 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.
2025-11-13 21:26:31 +08:00
GW_MC
373065c95f Fix incorrect shared file path 2025-11-13 20:35:14 +08:00
GW_MC
6138e4b2b3 Add missing shared library 2025-11-13 20:34:29 +08:00
GW_MC
25c0756e70 Add generated database entity, just recipe and fix config table missing PK 2025-11-13 20:13:05 +08:00
GW_MC
7a1617e1ee fix invalid simulate location and sqlite connection for seaorm 2025-11-13 20:11:04 +08:00
GW_MC
de914e41a9 Merge branch 'master' into feature/database-setup 2025-11-13 19:36:50 +08:00
GW_MC
706a6c76f9 init basic database folder structure 2025-11-11 20:28:49 +08:00
33 changed files with 2574 additions and 75 deletions

View File

@@ -37,6 +37,19 @@ runs:
restore-keys: | restore-keys: |
${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Sanitize components input
shell: bash
run: echo "SANITIZED_COMPONENTS=${{ inputs.components }}" | sed -E 's/, ?| /-/g' >> $GITHUB_ENV
- name: Cache Rust toolchain
uses: actions/cache@v4
with:
path: ~/.rustup
# Key includes the OS and the toolchain version (e.g., 'stable')
key: ${{ runner.os }}-rustup-${{ hashFiles('rust-toolchain.toml') }}-v1-${{ inputs.toolchain }}-${{ env.SANITIZED_COMPONENTS }}
restore-keys: |
${{ runner.os }}-rustup-
- name: Cache cargo build (target) - name: Cache cargo build (target)
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
@@ -44,7 +57,6 @@ runs:
key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
- name: Set up rust toolchain - name: Set up rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:

3
.gitignore vendored
View File

@@ -21,3 +21,6 @@ target
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# generated environment variables file
.env.generated

1889
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,10 @@
[workspace] [workspace]
members = [ members = [
"apps/container", "apps/container",
"apps/cli",
"public/shared",
"public/database",
"public/migration"
] ]
resolver = "3" resolver = "3"

0
apps/api/.gitkeep Normal file
View File

16
apps/cli/Cargo.toml Normal file
View 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
View 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);
}

View 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,
}
}
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
);
Ok(())
}
}
}

22
apps/cli/src/main.rs Normal file
View 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;
}

View File

@@ -4,6 +4,7 @@ pub mod sqlite;
use async_trait::async_trait; use async_trait::async_trait;
use shared::db_type::DBType; use shared::db_type::DBType;
use std::error::Error;
use std::future::Future; use std::future::Future;
use std::{pin::Pin, sync::Arc}; use std::{pin::Pin, sync::Arc};
use url::Host; use url::Host;
@@ -55,5 +56,5 @@ pub trait DBInfo<T> {
where where
Self: Sized; Self: Sized;
async fn get_db_container_config_info(&self) -> DBConfigInfoType; async fn get_db_container_config_info(&self) -> DBConfigInfoType;
fn get_unstarted_container(&self) -> Result<UnStartedContainer, ()>; fn get_unstarted_container(&self) -> Result<UnStartedContainer, Box<dyn Error>>;
} }

View File

@@ -1,3 +1,4 @@
#[derive(Default)]
pub struct OptionalContainerConfig { pub struct OptionalContainerConfig {
pub image: Option<String>, pub image: Option<String>,
pub tag: Option<String>, pub tag: Option<String>,
@@ -38,16 +39,3 @@ impl OptionalContainerConfig {
} }
} }
} }
impl Default for OptionalContainerConfig {
fn default() -> Self {
Self {
image: None,
tag: None,
container_name: None,
database_name: None,
user: None,
password: None,
}
}
}

View File

@@ -1,4 +1,4 @@
use std::sync::Arc; use std::{error::Error, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
use testcontainers::{ use testcontainers::{
@@ -79,7 +79,7 @@ impl DBInfo<OptionalContainerConfig> for PostgreSQLContainer {
} }
} }
fn get_unstarted_container(&self) -> Result<UnStartedContainer, ()> { fn get_unstarted_container(&self) -> Result<UnStartedContainer, Box<dyn Error>> {
Ok( Ok(
GenericImage::new(self.config.image.clone(), self.config.tag.clone()) GenericImage::new(self.config.image.clone(), self.config.tag.clone())
.with_exposed_port(5432.tcp()) .with_exposed_port(5432.tcp())

View File

@@ -1,4 +1,4 @@
use std::{path::PathBuf, sync::Arc}; use std::{error::Error, path::PathBuf, sync::Arc};
use async_trait::async_trait; use async_trait::async_trait;
@@ -15,6 +15,7 @@ pub struct ContainerConfig {
pub absolute_dir_path: PathBuf, pub absolute_dir_path: PathBuf,
} }
#[derive(Default)]
pub struct OptionalContainerConfig { pub struct OptionalContainerConfig {
// Add any optional configuration fields here // Add any optional configuration fields here
pub database_name: Option<String>, pub database_name: Option<String>,
@@ -36,15 +37,6 @@ impl OptionalContainerConfig {
} }
} }
impl Default for OptionalContainerConfig {
fn default() -> Self {
Self {
database_name: None,
absolute_path: None,
}
}
}
pub fn get_default_config() -> ContainerConfig { pub fn get_default_config() -> ContainerConfig {
ContainerConfig { ContainerConfig {
database_name: "sqlite".to_string(), database_name: "sqlite".to_string(),
@@ -69,7 +61,7 @@ impl SQLiteContainer {
impl DBInfo<OptionalContainerConfig> for SQLiteContainer { impl DBInfo<OptionalContainerConfig> for SQLiteContainer {
async fn get_db_container_config_info(&self) -> DBConfigInfoType { async fn get_db_container_config_info(&self) -> DBConfigInfoType {
// sqlite filepath url does not include the "sqlite://" prefix // sqlite filepath url does not include the "sqlite://" prefix
let sqlite_url = format!("{}", self.get_db_absolute_path().to_string_lossy()); let sqlite_url = format!("sqlite://{}", self.get_db_absolute_path().to_string_lossy());
// create the file // create the file
std::fs::create_dir_all(&self.config.absolute_dir_path) std::fs::create_dir_all(&self.config.absolute_dir_path)
.expect("Failed to create directories for SQLite database"); .expect("Failed to create directories for SQLite database");
@@ -83,10 +75,10 @@ impl DBInfo<OptionalContainerConfig> for SQLiteContainer {
let db_path = self.get_db_absolute_path(); let db_path = self.get_db_absolute_path();
Arc::new(move || { Arc::new(move || {
// delete the sqlite database file // delete the sqlite database file
if db_path.exists() { if db_path.exists()
if let Err(e) = std::fs::remove_file(&db_path) { && let Err(e) = std::fs::remove_file(&db_path)
eprintln!("Failed to delete SQLite database file: {}", e); {
} eprintln!("Failed to delete SQLite database file: {}", e);
} }
}) })
}, },
@@ -107,7 +99,9 @@ impl DBInfo<OptionalContainerConfig> for SQLiteContainer {
} }
} }
fn get_unstarted_container(&self) -> Result<UnStartedContainer, ()> { fn get_unstarted_container(&self) -> Result<UnStartedContainer, Box<dyn Error>> {
Err(()) Err(Box::new(std::io::Error::other(
"SQLite does not use a container",
)))
} }
} }

View File

@@ -60,7 +60,7 @@ async fn start(config: &Config) {
// //
// write the config files for the api server and database client // write the config files for the api server and database client
println!("Writing config files..."); println!("Writing config files...");
write_env_files(&db_config); write_env_files(db_config);
println!("Config files written to:"); println!("Config files written to:");
println!(" - {}", to_absolute_path(API_CONFIG_PATH).display()); println!(" - {}", to_absolute_path(API_CONFIG_PATH).display());
println!(" - {}", to_absolute_path(DB_CONFIG_PATH).display()); println!(" - {}", to_absolute_path(DB_CONFIG_PATH).display());

View File

@@ -31,8 +31,8 @@ pub fn write_env_files(db_config: &DBConfigInfoType) {
let api_env_file = EnvFile { let api_env_file = EnvFile {
file_type: env::EnvFileType::Yaml, file_type: env::EnvFileType::Yaml,
db_type: db_type, db_type,
db_url: db_url, db_url,
}; };
let mut db_env_file = api_env_file.clone(); let mut db_env_file = api_env_file.clone();

View File

@@ -1,7 +1,42 @@
set dotenv-load := true
# development environment file
set dotenv-filename := "./public/database/.env.generated"
cli *args:
cd apps/cli && \
if [ -n "{{args}}" ]; then \
cargo run -- {{args}}; \
else \
cargo run; \
fi
simulate *args: simulate *args:
cd src/container && \ cd apps/container && \
if [ -n "{{args}}" ]; then \ if [ -n "{{args}}" ]; then \
cargo run --bin container-simulate -- --db-type={{args}}; \ cargo run --bin container-simulate -- --db-type={{args}}; \
else \ else \
cargo run --bin container-simulate; \ cargo run --bin container-simulate; \
fi fi
# Usage: (following SeaORM migration commands)
# init: Initialize migration directory
# generate: Generate a new migration file
# up: Apply all pending migrations
# up -n 10: Apply 10 pending migrations
# down: Rollback last applied migration
# down -n 10: Rollback last 10 applied migrations
# status: Check the status of all migrations
# fresh: Drop all tables from the database, then reapply all migrations
# refresh: Rollback all applied migrations, then reapply all migrations
# reset: Rollback all applied migrations
migrate *args:
cd public/migration && \
if [ -n "{{args}}" ]; then \
cargo run -- {{args}}; \
else \
cargo run; \
fi
generate-entity:
# delegate to cli
just cli db:migrate_and_generate --output-path ../../public/database/src/generated/entities

View File

@@ -0,0 +1,19 @@
[package]
name = "database"
version = "0.1.0"
edition = "2024"
[lib]
path = "src/lib.rs"
[dependencies]
shared = { path = "../shared" }
migration = { path = "../migration" }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.47.0", features = ["full"] }
sea-orm = { version = "2.0.0-rc", features = [ "sqlx-postgres", "sqlx-mysql", "sqlx-sqlite", "runtime-tokio-rustls", "macros", "mock", "with-chrono", "with-json", "with-uuid", "sqlite-use-returning-for-3_35", "mariadb-use-returning" ] }
[lints]
workspace = true

View File

@@ -0,0 +1 @@
pub mod entities;

View File

@@ -0,0 +1,17 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "config")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub key: String,
pub value: String,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,6 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
pub mod prelude;
pub mod config;
pub mod user;

View File

@@ -0,0 +1,4 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
pub use super::config::Entity as Config;
pub use super::user::Entity as User;

View File

@@ -0,0 +1,21 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.18
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: Uuid,
#[sea_orm(unique)]
pub name: String,
pub is_admin: bool,
pub password_hash: String,
pub salt: String,
pub created_at: DateTimeUtc,
pub updated_at: DateTimeUtc,
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,23 @@
pub use sea_orm::ConnectOptions;
pub mod generated;
pub async fn get_connection<T: FnOnce(&mut ConnectOptions)>(
connection_string: &str,
option_fn: Option<T>,
) -> Result<sea_orm::DatabaseConnection, sea_orm::DbErr> {
use sea_orm::Database;
let mut opt = ConnectOptions::new(connection_string.to_string());
opt.max_connections(10)
.min_connections(0)
.connect_timeout(std::time::Duration::from_secs(8))
.idle_timeout(std::time::Duration::from_secs(8))
.test_before_acquire(true)
.sqlx_logging(true);
if let Some(option_fn) = option_fn {
option_fn(&mut opt);
}
Database::connect(opt).await
}

View File

@@ -0,0 +1,21 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2024"
rust-version = "1.85.0"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] }
sea-orm-cli = { version = "2.0.0-rc", features = ["sqlx-postgres", "sqlx-mysql", "sqlx-sqlite", "runtime-tokio"] }
[dependencies.sea-orm-migration]
version = "2.0.0-rc"
features = [
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
"sqlx-postgres", "sqlx-mysql", "sqlx-sqlite" # `DATABASE_DRIVER` features
]

View File

@@ -0,0 +1,59 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

View File

@@ -0,0 +1,63 @@
pub use sea_orm_migration::prelude::*;
mod migrations;
use migrations::*;
use sea_orm_migration::sea_orm::Database;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20251011_000001_create_user_table::Migration),
Box::new(m20251011_000002_create_config_table::Migration),
]
}
}
pub async fn migrate_database(db_url: &str) -> Result<(), DbErr> {
let db = Database::connect(db_url).await?;
Migrator::up(&db, None).await
}
pub async fn generate_entity(
db_url: &str,
output_dir: &str,
) -> Result<(), Box<dyn std::error::Error>> {
use sea_orm_cli::commands::generate::run_generate_command;
run_generate_command(
sea_orm_cli::GenerateSubcommands::Entity {
compact_format: true,
expanded_format: false,
frontend_format: false,
include_hidden_tables: false,
tables: vec![],
ignore_tables: vec!["seaql_migrations".to_string()],
max_connections: 1,
acquire_timeout: 30,
output_dir: output_dir.to_string(),
database_schema: Some("public".to_string()),
database_url: db_url.to_string(),
with_prelude: "all".to_string(),
with_serde: "both".to_string(),
serde_skip_deserializing_primary_key: false,
serde_skip_hidden_column: false,
with_copy_enums: true,
date_time_crate: sea_orm_cli::DateTimeCrate::Chrono,
lib: false,
model_extra_derives: vec![],
model_extra_attributes: vec![],
enum_extra_derives: vec![],
enum_extra_attributes: vec![],
seaography: false,
impl_active_model_behavior: true,
big_integer_type: sea_orm_cli::BigIntegerType::I64,
column_extra_derives: vec![],
entity_format: Some("dense".to_string()),
preserve_user_modifications: false,
},
false,
)
.await
}

View File

@@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[tokio::main]
async fn main() {
cli::run_cli(migration::Migrator).await;
}

View File

@@ -0,0 +1,2 @@
pub mod m20251011_000001_create_user_table;
pub mod m20251011_000002_create_config_table;

View File

@@ -0,0 +1,60 @@
use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[derive(DeriveIden)]
enum User {
Table,
Id,
//
Name,
IsAdmin,
PasswordHash,
Salt,
//
CreatedAt,
UpdatedAt,
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(User::Table)
.if_not_exists()
.col(pk_uuid(User::Id))
.col(ColumnDef::new(User::Name).string().not_null().unique_key())
.col(
ColumnDef::new(User::IsAdmin)
.boolean()
.default(false)
.not_null(),
)
.col(ColumnDef::new(User::PasswordHash).string().not_null())
.col(ColumnDef::new(User::Salt).string().not_null())
.col(
ColumnDef::new(User::CreatedAt)
.timestamp()
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
.not_null(),
)
.col(
ColumnDef::new(User::UpdatedAt)
.timestamp()
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
.not_null(),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(User::Table).to_owned())
.await
}
}

View File

@@ -0,0 +1,54 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[derive(DeriveIden)]
enum Config {
Table,
//
Key,
Value,
//
CreatedAt,
UpdatedAt,
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Config::Table)
.if_not_exists()
.col(
ColumnDef::new(Config::Key)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Config::Value).string().not_null())
.col(
ColumnDef::new(Config::CreatedAt)
.timestamp()
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
.not_null(),
)
.col(
ColumnDef::new(Config::UpdatedAt)
.timestamp()
.default(SimpleExpr::Keyword(Keyword::CurrentTimestamp))
.not_null(),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Config::Table).to_owned())
.await
}
}

10
public/shared/Cargo.toml Normal file
View File

@@ -0,0 +1,10 @@
[package]
name = "shared"
version = "0.1.0"
edition = "2024"
[lib]
path = "src/lib.rs"
[lints]
workspace = true

View File

@@ -0,0 +1,28 @@
use std::str::FromStr;
#[derive(Debug, Clone)]
pub enum DBType {
PostgreSQL,
SQLite,
}
impl std::fmt::Display for DBType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DBType::PostgreSQL => write!(f, "PostgreSQL"),
DBType::SQLite => write!(f, "SQLite"),
}
}
}
impl FromStr for DBType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"postgresql" | "postgres" => Ok(DBType::PostgreSQL),
"sqlite" => Ok(DBType::SQLite),
_ => Err(format!("Unknown DBType: {}", s)),
}
}
}

1
public/shared/src/lib.rs Normal file
View File

@@ -0,0 +1 @@
pub mod db_type;