mod error; mod ingestion; use crate::error::AppError; use crate::ingestion::db; use crate::ingestion::ingestion_logic::from_csv_row; use crate::ingestion::routes::{monzo_batched_csv, monzo_batched_json, shortcuts_csv}; use axum::routing::{get, post}; use axum::{Extension, Router}; use clap::{Parser, Subcommand}; use migration::{Migrator, MigratorTrait}; use sea_orm::{ConnectionTrait, DatabaseConnection}; use std::fs::File; use std::net::SocketAddr; use std::path::PathBuf; use tower_http::trace::TraceLayer; use tracing::log::LevelFilter; #[derive(Debug, Subcommand)] enum Commands { /// Manually run database migrations. Migrate { /// Number of migration steps to perform. If not provided, all migrations will be run. #[arg(long)] steps: Option, /// If we should perform migration down. #[arg(long)] down: bool, }, /// Start web app for to the google-sheets app script Serve { /// If we should perform migration at startup. #[clap(short, long, env, default_value_t = true)] migrate: bool, /// The server address to bind to. #[clap(short, long, env, default_value = "0.0.0.0:3000")] addr: SocketAddr, }, /// Ingest a google-sheets CSV export into the database. Csv { /// The path of the CSV file to ingest. csv_file: PathBuf, /// The name of the account to ingest the CSV for. #[clap(long, short)] account: String, }, } /// Ingest and manage monzo transactions from the Monzo Plus auto-export feature #[derive(Debug, clap::Parser)] struct Cli { /// URL to PostgreSQL database. #[clap(short, long = "db", env)] database_url: String, #[command(subcommand)] command: Commands, } async fn health_check( Extension(db): Extension, ) -> Result<&'static str, AppError> { db.execute_unprepared("SELECT 1").await?; Ok("Ok") } #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt::init(); let cli: Cli = Cli::parse(); let connection = sea_orm::ConnectOptions::new(&cli.database_url) .sqlx_logging_level(LevelFilter::Debug) .to_owned(); let connection = sea_orm::Database::connect(connection).await?; match cli.command { Commands::Migrate { steps, down } => { if down { Migrator::down(&connection, steps).await?; } else { Migrator::up(&connection, steps).await? } } Commands::Serve { migrate, addr } => { if migrate { Migrator::up(&connection, None).await?; } serve_web(addr, connection).await?; } Commands::Csv { csv_file, account: account_name } => { let mut csv = csv::Reader::from_reader(File::open(csv_file)?); let data = csv.records(); let data = data .filter_map(|f| f.ok()) .map(from_csv_row) .collect::>()?; let account_id = db::get_account_id(&connection, Some(account_name)).await?; db::insert(&connection, data, account_id).await?; } } Ok(()) } async fn serve_web(address: SocketAddr, connection: DatabaseConnection) -> anyhow::Result<()> { let app = Router::new() .route("/health", get(health_check)) .route("/monzo-batch-export", post(monzo_batched_json)) .route("/monzo-csv-ingestion", post(monzo_batched_csv)) .route("/shortcuts-csv-import", post(shortcuts_csv)) .layer(Extension(connection.clone())) .layer(TraceLayer::new_for_http()); tracing::info!("listening on {}", &address); let listener = tokio::net::TcpListener::bind(&address).await?; axum::serve(listener, app).await?; Ok(()) }