Initial
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/config.toml
|
||||
1445
Cargo.lock
generated
Normal file
1445
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
Cargo.toml
Normal file
34
Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/erplt",
|
||||
"crates/erplt-config",
|
||||
"crates/erplt-api",
|
||||
"crates/erplt-db",
|
||||
"crates/erplt-ledger",
|
||||
]
|
||||
|
||||
resolver = "3"
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1.0.104"
|
||||
axum = { version = "0.8.9", features = ["macros"] }
|
||||
chrono = { version = "0.4.45", features = ["serde"] }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
diesel = { version = "2.3.13", features = ["postgres", "uuid", "chrono", "r2d2"] }
|
||||
diesel_migrations = "2.3.2"
|
||||
leptos = "0.8.20"
|
||||
leptos_axum = "0.8.10"
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
serde_json = "1.0.151"
|
||||
thiserror = "2.0.20"
|
||||
tokio = { version = "1.53.1", features = ["full"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
uuid = { version = "1.26.0", features = ["v4", "serde"] }
|
||||
wasm-bindgen = "0.2.128"
|
||||
clap = { version = "4.6.6", features = ["derive"] }
|
||||
toml = "1.1.5"
|
||||
camino = { version = "1.2.5", features = ["serde1"]}
|
||||
humantime = "2.4.0"
|
||||
humantime-serde = "1.1.1"
|
||||
12
crates/erplt-api/Cargo.toml
Normal file
12
crates/erplt-api/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "erplt-api"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
erplt-db.path="../erplt-db"
|
||||
erplt-config.path = "../erplt-config"
|
||||
anyhow.workspace = true
|
||||
axum.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
5
crates/erplt-api/src/lib.rs
Normal file
5
crates/erplt-api/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod routes;
|
||||
mod state;
|
||||
mod server;
|
||||
|
||||
pub use server::start;
|
||||
3
crates/erplt-api/src/routes/health.rs
Normal file
3
crates/erplt-api/src/routes/health.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub async fn get() -> &'static str {
|
||||
"OK"
|
||||
}
|
||||
8
crates/erplt-api/src/routes/mod.rs
Normal file
8
crates/erplt-api/src/routes/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use axum::{routing::get, Router};
|
||||
|
||||
mod health;
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route("/health", get(health::get))
|
||||
}
|
||||
33
crates/erplt-api/src/server.rs
Normal file
33
crates/erplt-api/src/server.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use anyhow::Context;
|
||||
use axum::http::StatusCode;
|
||||
use erplt_db::DbPool;
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn fallback() -> (StatusCode, &'static str) {
|
||||
(StatusCode::NOT_FOUND, "Not Found")
|
||||
}
|
||||
|
||||
|
||||
pub async fn start(db: DbPool) -> anyhow::Result<()> {
|
||||
let config = erplt_config::get();
|
||||
|
||||
|
||||
let app = axum::Router::new()
|
||||
.merge(crate::routes::router())
|
||||
.fallback(fallback);
|
||||
|
||||
|
||||
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(format!("{}:{}", config.web.host, config.web.port))
|
||||
.await
|
||||
.context("Failed to bind HTTP listener")?;
|
||||
|
||||
tracing::info!("Web server listening on http://{}", listener.local_addr()?);
|
||||
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.context("Web server stopped unexpectedly")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
0
crates/erplt-api/src/state.rs
Normal file
0
crates/erplt-api/src/state.rs
Normal file
15
crates/erplt-config/Cargo.toml
Normal file
15
crates/erplt-config/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "erplt-config"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
camino.workspace = true
|
||||
clap.workspace = true
|
||||
serde.workspace = true
|
||||
toml.workspace = true
|
||||
anyhow.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
humantime.workspace = true
|
||||
humantime-serde.workspace = true
|
||||
18
crates/erplt-config/src/config/cli.rs
Normal file
18
crates/erplt-config/src/config/cli.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use camino::Utf8PathBuf;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, clap::Parser)]
|
||||
#[command(
|
||||
name = "erplt",
|
||||
about = "ERP system for European businesses",
|
||||
long_about = "ERPLT is a lightweight ERP system built for the European market.",
|
||||
)]
|
||||
pub struct Cli {
|
||||
/// Config path
|
||||
#[arg(long, short='c', default_value="config.toml")]
|
||||
pub config: Utf8PathBuf,
|
||||
|
||||
/// Debug output
|
||||
#[arg(long, short='d', default_value_t=false)]
|
||||
pub debug: bool,
|
||||
}
|
||||
107
crates/erplt-config/src/config/mod.rs
Normal file
107
crates/erplt-config/src/config/mod.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use camino::Utf8PathBuf;
|
||||
use serde::de;
|
||||
|
||||
pub mod cli;
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Config {
|
||||
#[serde(skip)]
|
||||
pub config_path: Utf8PathBuf,
|
||||
pub core: ConfigCore,
|
||||
pub database: ConfigDatabase,
|
||||
pub web: ConfigWeb,
|
||||
}
|
||||
|
||||
|
||||
impl Config {
|
||||
pub(crate) fn parse() -> anyhow::Result<Self> {
|
||||
if !crate::CLI.config.exists() {
|
||||
tracing::warn!("Configuration does not exist, creating at {}", crate::CLI.config.canonicalize_utf8()?);
|
||||
let tml = toml::to_string_pretty(&Config::default()).context("Invalid default config????")?;
|
||||
std::fs::write(&crate::CLI.config, tml).context("Failed to write default config, check permissions?")?;
|
||||
}
|
||||
let text = std::fs::read_to_string(&crate::CLI.config).context("Failed to read config, check permissions?")?;
|
||||
let mut tml: Self = toml::from_str(&text)?;
|
||||
|
||||
|
||||
tml.config_path = crate::CLI.config.clone();
|
||||
tml.core.debug = tml.core.debug || crate::CLI.debug;
|
||||
|
||||
|
||||
tracing::info!("Read config at {}", crate::CLI.config.canonicalize_utf8()?);
|
||||
Ok(tml)
|
||||
}
|
||||
pub fn get() -> &'static Self {
|
||||
crate::CONFIG.get().expect("Config hasnt been read and initialised yet")
|
||||
}
|
||||
pub(crate) fn load_to_global(self) {
|
||||
crate::CONFIG.set(self).expect("Config was already set globaly")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ConfigCore {
|
||||
pub debug: bool,
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DatabaseType {
|
||||
#[default]
|
||||
Postgresql,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ConfigDatabase {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub database: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
#[serde(rename="type")]
|
||||
pub db_type: DatabaseType,
|
||||
|
||||
pub max_pool_size: u32,
|
||||
pub min_idle: u32,
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub connection_timeout: Duration,
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub idle_timeout: Duration,
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub max_lifetime: Duration,
|
||||
pub test_on_check_out: bool
|
||||
}
|
||||
|
||||
impl ConfigDatabase {
|
||||
pub fn connect_url(&self) -> String {
|
||||
format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
self.username,
|
||||
self.password,
|
||||
self.host,
|
||||
self.port,
|
||||
self.database,
|
||||
)
|
||||
}
|
||||
pub fn connect_url_redacted(&self) -> String {
|
||||
format!(
|
||||
"postgres://{}:***@{}:{}/{}",
|
||||
self.username,
|
||||
self.host,
|
||||
self.port,
|
||||
self.database,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct ConfigWeb {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
}
|
||||
20
crates/erplt-config/src/lib.rs
Normal file
20
crates/erplt-config/src/lib.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use std::sync::{LazyLock, OnceLock};
|
||||
use clap::Parser;
|
||||
mod config;
|
||||
|
||||
pub use config::Config;
|
||||
|
||||
|
||||
static CLI: LazyLock<config::cli::Cli> = LazyLock::new(config::cli::Cli::parse);
|
||||
static CONFIG: OnceLock<config::Config> = OnceLock::new();
|
||||
|
||||
pub fn init() -> anyhow::Result<()> {
|
||||
let _ = &*CLI;
|
||||
config::Config::parse()?.load_to_global();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// helper function for faster access
|
||||
pub fn get() -> &'static Config {
|
||||
Config::get()
|
||||
}
|
||||
13
crates/erplt-db/Cargo.toml
Normal file
13
crates/erplt-db/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "erplt-db"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
erplt-config.path = "../erplt-config"
|
||||
diesel.workspace = true
|
||||
diesel_migrations.workspace = true
|
||||
serde.workspace = true
|
||||
toml.workspace = true
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
6
crates/erplt-db/src/lib.rs
Normal file
6
crates/erplt-db/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod models;
|
||||
mod pool;
|
||||
// pub mod schema;
|
||||
mod migrations;
|
||||
|
||||
pub use pool::{DbPool, connect, DbConnection};
|
||||
17
crates/erplt-db/src/migrations.rs
Normal file
17
crates/erplt-db/src/migrations.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use anyhow::Context;
|
||||
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
|
||||
|
||||
|
||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("../../migrations");
|
||||
|
||||
pub fn run_migrations(pool: &crate::DbPool) -> anyhow::Result<usize> {
|
||||
let mut connection = pool
|
||||
.get()
|
||||
.context("Failed to get database connection for migrations")?;
|
||||
|
||||
connection
|
||||
.run_pending_migrations(MIGRATIONS)
|
||||
.map(|migrations| migrations.len())
|
||||
.map_err(|e| anyhow::anyhow!(e) )
|
||||
.context("Failed to run database migrations")
|
||||
}
|
||||
0
crates/erplt-db/src/models/mod.rs
Normal file
0
crates/erplt-db/src/models/mod.rs
Normal file
30
crates/erplt-db/src/pool.rs
Normal file
30
crates/erplt-db/src/pool.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use anyhow::Context;
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
|
||||
pub type DbBackend = diesel::pg::Pg;
|
||||
pub type DbConnection = diesel::PgConnection;
|
||||
pub type DbPool = diesel::r2d2::Pool<
|
||||
diesel::r2d2::ConnectionManager<DbConnection>
|
||||
>;
|
||||
|
||||
pub fn connect(url: &str) -> anyhow::Result<DbPool> {
|
||||
let config= erplt_config::Config::get();
|
||||
let manager = ConnectionManager::<DbConnection>::new(url);
|
||||
|
||||
tracing::info!("Creating database pool at {}", config.database.connect_url_redacted());
|
||||
|
||||
let pool = DbPool::builder()
|
||||
.max_size(config.database.max_pool_size)
|
||||
.min_idle(Some(config.database.min_idle))
|
||||
.connection_timeout(config.database.connection_timeout)
|
||||
.idle_timeout(Some(config.database.idle_timeout))
|
||||
.max_lifetime(Some(config.database.max_lifetime))
|
||||
.test_on_check_out(config.database.test_on_check_out)
|
||||
.build(manager)
|
||||
.context("Failed to create database connection pool")?;
|
||||
|
||||
pool.get()
|
||||
.context("Failed to connect to PostgreSQL")?;
|
||||
tracing::info!("Connected to database");
|
||||
Ok(pool)
|
||||
}
|
||||
6
crates/erplt-ledger/Cargo.toml
Normal file
6
crates/erplt-ledger/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "erplt-ledger"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
14
crates/erplt-ledger/src/lib.rs
Normal file
14
crates/erplt-ledger/src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
19
crates/erplt/Cargo.toml
Normal file
19
crates/erplt/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "erplt"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
erplt-db.path="../erplt-db"
|
||||
erplt-api.path="../erplt-api"
|
||||
erplt-config.path="../erplt-config"
|
||||
camino.workspace = true
|
||||
clap.workspace = true
|
||||
serde.workspace = true
|
||||
toml.workspace = true
|
||||
anyhow.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
humantime.workspace = true
|
||||
humantime-serde.workspace = true
|
||||
tokio.workspace = true
|
||||
31
crates/erplt/src/main.rs
Normal file
31
crates/erplt/src/main.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
|
||||
let exit_code = match real_main().await {
|
||||
Ok(()) => 0,
|
||||
Err(err) => {
|
||||
eprintln!("Exited with error: {err:#}");
|
||||
eprintln!("Closing!");
|
||||
1
|
||||
}
|
||||
};
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
pub async fn real_main() -> anyhow::Result<()> {
|
||||
erplt_config::init()?;
|
||||
tracing::info!("Starting application");
|
||||
|
||||
let db = erplt_db::connect(&erplt_config::Config::get().database.connect_url())?;
|
||||
|
||||
erplt_api::start(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
9
diesel.toml
Normal file
9
diesel.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "crates/erplt-db/src/schema.rs"
|
||||
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
|
||||
|
||||
[migrations_directory]
|
||||
dir = "migrations"
|
||||
0
migrations/.diesel_lock
Normal file
0
migrations/.diesel_lock
Normal file
0
migrations/.keep
Normal file
0
migrations/.keep
Normal file
1
migrations/2026-09-07-190250-0000_initial/down.sql
Normal file
1
migrations/2026-09-07-190250-0000_initial/down.sql
Normal file
@@ -0,0 +1 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
1
migrations/2026-09-07-190250-0000_initial/up.sql
Normal file
1
migrations/2026-09-07-190250-0000_initial/up.sql
Normal file
@@ -0,0 +1 @@
|
||||
-- Your SQL goes here
|
||||
Reference in New Issue
Block a user