Compare commits
17 Commits
a20d6456c6
...
d9105957a8
| Author | SHA1 | Date | |
|---|---|---|---|
| d9105957a8 | |||
|
|
f0cfe5ec43 | ||
|
|
afb10424d5 | ||
|
|
3de9ecc5c1 | ||
|
|
800c55238d | ||
|
|
6d1888e6c3 | ||
|
|
43c6b54ebd | ||
|
|
17f7e06e8a | ||
|
|
467e6bfcf5 | ||
|
|
d05d660198 | ||
|
|
829c4ef3e3 | ||
|
|
373065c95f | ||
|
|
6138e4b2b3 | ||
|
|
25c0756e70 | ||
|
|
7a1617e1ee | ||
|
|
de914e41a9 | ||
|
|
706a6c76f9 |
14
.github/actions/setup-rust/action.yml
vendored
14
.github/actions/setup-rust/action.yml
vendored
@@ -37,6 +37,19 @@ runs:
|
||||
restore-keys: |
|
||||
${{ 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)
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@@ -44,7 +57,6 @@ runs:
|
||||
key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Set up rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,3 +21,6 @@ target
|
||||
# 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.
|
||||
#.idea/
|
||||
|
||||
# generated environment variables file
|
||||
.env.generated
|
||||
|
||||
1889
Cargo.lock
generated
1889
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,10 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"apps/container",
|
||||
"apps/cli",
|
||||
"public/shared",
|
||||
"public/database",
|
||||
"public/migration"
|
||||
]
|
||||
|
||||
resolver = "3"
|
||||
|
||||
0
apps/api/.gitkeep
Normal file
0
apps/api/.gitkeep
Normal file
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,
|
||||
}
|
||||
}
|
||||
|
||||
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
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;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod sqlite;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use shared::db_type::DBType;
|
||||
use std::error::Error;
|
||||
use std::future::Future;
|
||||
use std::{pin::Pin, sync::Arc};
|
||||
use url::Host;
|
||||
@@ -55,5 +56,5 @@ pub trait DBInfo<T> {
|
||||
where
|
||||
Self: Sized;
|
||||
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>>;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[derive(Default)]
|
||||
pub struct OptionalContainerConfig {
|
||||
pub image: 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
use std::{error::Error, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
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(
|
||||
GenericImage::new(self.config.image.clone(), self.config.tag.clone())
|
||||
.with_exposed_port(5432.tcp())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::{error::Error, path::PathBuf, sync::Arc};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
@@ -15,6 +15,7 @@ pub struct ContainerConfig {
|
||||
pub absolute_dir_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct OptionalContainerConfig {
|
||||
// Add any optional configuration fields here
|
||||
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 {
|
||||
ContainerConfig {
|
||||
database_name: "sqlite".to_string(),
|
||||
@@ -69,7 +61,7 @@ impl SQLiteContainer {
|
||||
impl DBInfo<OptionalContainerConfig> for SQLiteContainer {
|
||||
async fn get_db_container_config_info(&self) -> DBConfigInfoType {
|
||||
// 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
|
||||
std::fs::create_dir_all(&self.config.absolute_dir_path)
|
||||
.expect("Failed to create directories for SQLite database");
|
||||
@@ -83,11 +75,11 @@ impl DBInfo<OptionalContainerConfig> for SQLiteContainer {
|
||||
let db_path = self.get_db_absolute_path();
|
||||
Arc::new(move || {
|
||||
// delete the sqlite database file
|
||||
if db_path.exists() {
|
||||
if let Err(e) = std::fs::remove_file(&db_path) {
|
||||
if db_path.exists()
|
||||
&& let Err(e) = std::fs::remove_file(&db_path)
|
||||
{
|
||||
eprintln!("Failed to delete SQLite database file: {}", e);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -107,7 +99,9 @@ impl DBInfo<OptionalContainerConfig> for SQLiteContainer {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_unstarted_container(&self) -> Result<UnStartedContainer, ()> {
|
||||
Err(())
|
||||
fn get_unstarted_container(&self) -> Result<UnStartedContainer, Box<dyn Error>> {
|
||||
Err(Box::new(std::io::Error::other(
|
||||
"SQLite does not use a container",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ async fn start(config: &Config) {
|
||||
//
|
||||
// write the config files for the api server and database client
|
||||
println!("Writing config files...");
|
||||
write_env_files(&db_config);
|
||||
write_env_files(db_config);
|
||||
println!("Config files written to:");
|
||||
println!(" - {}", to_absolute_path(API_CONFIG_PATH).display());
|
||||
println!(" - {}", to_absolute_path(DB_CONFIG_PATH).display());
|
||||
|
||||
@@ -31,8 +31,8 @@ pub fn write_env_files(db_config: &DBConfigInfoType) {
|
||||
|
||||
let api_env_file = EnvFile {
|
||||
file_type: env::EnvFileType::Yaml,
|
||||
db_type: db_type,
|
||||
db_url: db_url,
|
||||
db_type,
|
||||
db_url,
|
||||
};
|
||||
|
||||
let mut db_env_file = api_env_file.clone();
|
||||
|
||||
37
justfile
37
justfile
@@ -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:
|
||||
cd src/container && \
|
||||
cd apps/container && \
|
||||
if [ -n "{{args}}" ]; then \
|
||||
cargo run --bin container-simulate -- --db-type={{args}}; \
|
||||
else \
|
||||
cargo run --bin container-simulate; \
|
||||
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
|
||||
|
||||
19
public/database/Cargo.toml
Normal file
19
public/database/Cargo.toml
Normal 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
|
||||
1
public/database/src/generated.rs
Normal file
1
public/database/src/generated.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod entities;
|
||||
17
public/database/src/generated/entities/config.rs
Normal file
17
public/database/src/generated/entities/config.rs
Normal 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 {}
|
||||
6
public/database/src/generated/entities/mod.rs
Normal file
6
public/database/src/generated/entities/mod.rs
Normal 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;
|
||||
4
public/database/src/generated/entities/prelude.rs
Normal file
4
public/database/src/generated/entities/prelude.rs
Normal 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;
|
||||
21
public/database/src/generated/entities/user.rs
Normal file
21
public/database/src/generated/entities/user.rs
Normal 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 {}
|
||||
23
public/database/src/lib.rs
Normal file
23
public/database/src/lib.rs
Normal 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
|
||||
}
|
||||
21
public/migration/Cargo.toml
Normal file
21
public/migration/Cargo.toml
Normal 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
|
||||
]
|
||||
59
public/migration/README.md
Normal file
59
public/migration/README.md
Normal 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
|
||||
```
|
||||
63
public/migration/src/lib.rs
Normal file
63
public/migration/src/lib.rs
Normal 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
|
||||
}
|
||||
6
public/migration/src/main.rs
Normal file
6
public/migration/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
cli::run_cli(migration::Migrator).await;
|
||||
}
|
||||
2
public/migration/src/migrations.rs
Normal file
2
public/migration/src/migrations.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod m20251011_000001_create_user_table;
|
||||
pub mod m20251011_000002_create_config_table;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
10
public/shared/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "shared"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
28
public/shared/src/db_type.rs
Normal file
28
public/shared/src/db_type.rs
Normal 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
1
public/shared/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod db_type;
|
||||
Reference in New Issue
Block a user