Compare commits

...

1 Commits

Author SHA1 Message Date
brian
27c94f4276 feat: init book handler 2025-06-08 20:42:24 +08:00
26 changed files with 4307 additions and 3 deletions

0
.env-template Normal file
View File

3
.gitignore vendored
View File

@@ -2,4 +2,5 @@
.idea
.vscode
.DS_Store
.env
conf.toml

3576
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,5 +4,22 @@ version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[workspace]
members = [".", "entity", "migration"]
[dependencies]
axum = { version = "0.8" }
axum-macros = "0.5"
sea-orm = { version = "1.1.12", features = [
"sqlx-postgres",
"runtime-tokio-rustls",
"macros",
"chrono",
] }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
dotenvy = "0.15.7"
toml = "0.8.22"
clap = { version = "4.0", features = ["derive"] }

22
migration/Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
[dependencies.sea-orm-migration]
version = "1.1.12"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
"sqlx-postgres", # `DATABASE_DRIVER` feature
]

41
migration/README.md Normal file
View File

@@ -0,0 +1,41 @@
# 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
```

22
migration/src/lib.rs Normal file
View File

@@ -0,0 +1,22 @@
pub use sea_orm_migration::prelude::*;
mod m20250525_000001_create_ledger_table_category;
mod m20250525_000002_create_ledger_table_book;
mod m20250525_000003_create_ledger_table_tag;
mod m20250525_000004_create_ledger_table_account;
mod m20250525_000005_create_ledger_table_transaction;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20250525_000001_create_ledger_table_category::Migration),
Box::new(m20250525_000002_create_ledger_table_book::Migration),
Box::new(m20250525_000003_create_ledger_table_tag::Migration),
Box::new(m20250525_000004_create_ledger_table_account::Migration),
Box::new(m20250525_000005_create_ledger_table_transaction::Migration),
]
}
}

View File

@@ -0,0 +1,64 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20250525_000001_create_ledger_table_category" // Make sure this matches with the file name
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Category::Table)
.col(
ColumnDef::new(Category::Id)
.big_integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Category::Name).string().not_null())
.col(ColumnDef::new(Category::Uid).big_integer().not_null())
.col(ColumnDef::new(Category::ParentId).big_integer().default(0i64).not_null())
.col(ColumnDef::new(Category::IsDeleted).boolean().default(false).not_null())
.col(
ColumnDef::new(Category::CreatedAt)
.date_time()
.default(Expr::current_timestamp())
.not_null(),
)
.col(
ColumnDef::new(Category::UpdatedAt)
.date_time()
.default(Expr::current_timestamp())
.not_null(),
)
.to_owned(),
)
.await
}
// Define how to rollback this migration: Drop the Bakery table.
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Category::Table).to_owned())
.await
}
}
#[derive(Iden)]
pub enum Category {
Table,
Id,
Name,
Uid,
ParentId,
IsDeleted,
CreatedAt,
UpdatedAt,
}

View File

@@ -0,0 +1,63 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20250525_000002_create_ledger_table_book" // Make sure this matches with the file name
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
// Define how to apply this migration
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Book::Table)
.col(
ColumnDef::new(Book::Id)
.big_integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Book::Name).string().not_null())
.col(ColumnDef::new(Book::Uid).big_integer().not_null())
.col(ColumnDef::new(Book::IsDeleted).boolean().default(false).not_null())
.col(
ColumnDef::new(Book::CreatedAt)
.date_time()
.default(Expr::current_timestamp())
.not_null(),
)
.col(
ColumnDef::new(Book::UpdatedAt)
.date_time()
.default(Expr::current_timestamp())
.not_null(),
)
.to_owned(),
)
.await
}
// Define how to rollback this migration: Drop the Bakery table.
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Book::Table).to_owned())
.await
}
}
#[derive(Iden)]
pub enum Book {
Table,
Id,
Name,
Uid,
IsDeleted,
CreatedAt,
UpdatedAt,
}

View File

@@ -0,0 +1,62 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20250525_000001_create_ledger_table_tag" // Make sure this matches with the file name
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Tag::Table)
.col(
ColumnDef::new(Tag::Id)
.big_integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Tag::Name).string().not_null())
.col(ColumnDef::new(Tag::Uid).big_integer().not_null())
.col(ColumnDef::new(Tag::IsDeleted).boolean().default(false).not_null())
.col(
ColumnDef::new(Tag::CreatedAt)
.date_time()
.default(Expr::current_timestamp())
.not_null(),
)
.col(
ColumnDef::new(Tag::UpdatedAt)
.date_time()
.default(Expr::current_timestamp())
.not_null(),
)
.to_owned(),
)
.await
}
// Define how to rollback this migration: Drop the Bakery table.
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Tag::Table).to_owned())
.await
}
}
#[derive(Iden)]
pub enum Tag {
Table,
Id,
Name,
Uid,
IsDeleted,
CreatedAt,
UpdatedAt,
}

View File

@@ -0,0 +1,64 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20250525_000004_create_ledger_table_account" // Make sure this matches with the file name
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Account::Table)
.col(
ColumnDef::new(Account::Id)
.big_integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Account::Name).string().not_null())
.col(ColumnDef::new(Account::Type).integer().not_null())
.col(ColumnDef::new(Account::Uid).big_integer().not_null())
.col(ColumnDef::new(Account::IsDeleted).boolean().default(false).not_null())
.col(
ColumnDef::new(Account::CreatedAt)
.date_time()
.default(Expr::current_timestamp())
.not_null(),
)
.col(
ColumnDef::new(Account::UpdatedAt)
.date_time()
.default(Expr::current_timestamp())
.not_null(),
)
.to_owned(),
)
.await
}
// Define how to rollback this migration: Drop the Bakery table.
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Account::Table).to_owned())
.await
}
}
#[derive(Iden)]
pub enum Account {
Table,
Id,
Name,
Uid,
Type,
IsDeleted,
CreatedAt,
UpdatedAt,
}

View File

@@ -0,0 +1,84 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20250525_000005_create_ledger_table_transaction" // Make sure this matches with the file name
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Transaction::Table)
.col(
ColumnDef::new(Transaction::Id)
.big_integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Transaction::Uid).big_integer().not_null())
.col(ColumnDef::new(Transaction::Type).integer().not_null())
.col(ColumnDef::new(Transaction::BookId).big_integer().not_null())
.col(
ColumnDef::new(Transaction::CategoryId)
.big_integer()
.not_null(),
)
.col(ColumnDef::new(Transaction::Description).string().not_null())
.col(
ColumnDef::new(Transaction::TransactionTime)
.timestamp_with_time_zone()
.default(Expr::current_timestamp())
.not_null(),
)
.col(
ColumnDef::new(Transaction::IsDeleted)
.boolean()
.default(false)
.not_null(),
)
.col(
ColumnDef::new(Transaction::CreatedAt)
.date_time()
.default(Expr::current_timestamp())
.not_null(),
)
.col(
ColumnDef::new(Transaction::UpdatedAt)
.date_time()
.default(Expr::current_timestamp())
.not_null(),
)
.to_owned(),
)
.await
}
// Define how to rollback this migration: Drop the Bakery table.
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Transaction::Table).to_owned())
.await
}
}
#[derive(Iden)]
pub enum Transaction {
Table,
Id,
Uid,
Type,
BookId,
CategoryId,
Description,
TransactionTime,
IsDeleted,
CreatedAt,
UpdatedAt,
}

6
migration/src/main.rs Normal file
View File

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

48
src/api/book.rs Normal file
View File

@@ -0,0 +1,48 @@
use axum::routing::{get, post};
use axum::{
extract::{Path, State},
http::StatusCode,
Json, Router,
};
use axum_macros::debug_handler;
use sea_orm::ColumnTrait;
use sea_orm::{entity::*, query::*,};
use crate::model::db::prelude::Book;
use crate::model::db::book::Model as BookModel;
use crate::model::db::book::Column as BookColumn;
use crate::model::http_body::book;
use crate::AppState;
use crate::model::http_body::book::BookItem;
pub fn get_nest_handlers() -> Router<crate::AppState> {
Router::new()
.route("/", get(get_all_books_handler))
// .route("/{id}", post(update_book).get(get_book))
}
// handlers
//
#[debug_handler]
async fn get_all_books_handler(
state: State<AppState>
) -> Result<Json<Vec<book::BookItem>>,(StatusCode,String)> {
// let conn = state.conn.get_postgres_connection_pool();
let uid :i64 = 1;
let all_books = Book::find()
.filter(BookColumn::Uid.eq(uid))
.all(&state.conn)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR,e.to_string()))?;
let mut books: Vec<BookItem> = Vec::new();
for b in all_books {
let book_resp = BookItem{
id: b.id,
name: b.name,
};
books.push(book_resp);
}
Ok(Json(books))
}

1
src/api/mod.rs Normal file
View File

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

View File

@@ -1,3 +1,99 @@
fn main() {
println!("Hello, world!");
use axum::Router;
use clap::Parser;
use sea_orm::{Database, DatabaseConnection};
use serde::Deserialize;
// Project modules
mod api;
mod model;
#[tokio::main]
async fn main() {
dotenvy::dotenv().unwrap();
// initialize tracing
tracing_subscriber::fmt::init();
let cli = Cli::parse();
match cli.command {
Command::Serve { config_path } => {
if let Ok(config) = load_config(&config_path).await {
println!("Loaded config.");
println!("{},{}", config.service.host.clone(), config.service.port);
// Proceed with server initialization using `config`
start_server(&config).await;
} else {
eprintln!("Failed to load config from {}", config_path);
}
}
}
}
#[derive(Clone)]
struct AppState {
conn: DatabaseConnection,
}
#[derive(Deserialize)]
struct Key {
jwt: String,
user: String,
}
#[derive(Deserialize)]
struct DatabaseConf {
connection: String,
}
#[derive(Deserialize)]
struct ServiceConf {
host: String,
port: u32,
}
#[derive(Deserialize)]
struct Config {
service: ServiceConf,
database: DatabaseConf,
keys: Key,
}
#[derive(clap::Parser)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(clap::Subcommand)]
enum Command {
Serve {
#[arg(long = "conf")]
config_path: String,
},
}
async fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
let content = tokio::fs::read_to_string(path).await?;
let config: Config = toml::from_str(&content)?;
Ok(config)
}
// ====== Commands ======
// start http server
async fn start_server(config: &Config){
// Define the router
// let app = Router.new()
// .nest();
let conn = Database::connect(&config.database.connection)
.await
.expect("Database connection failed.");
let state = AppState{conn };
let app = Router::new()
.nest("/api/v1/book", api::book::get_nest_handlers())
.with_state(state);
let host = config.service.host.clone();
let port = config.service.port;
let server_url = format!("{host}:{port}");
let listener = tokio::net::TcpListener::bind(&server_url).await.unwrap();
axum::serve(listener, app).await.expect("Service panic happened");
}

22
src/model/db/account.rs Normal file
View File

@@ -0,0 +1,22 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "account")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub name: String,
pub r#type: i32,
pub uid: i64,
pub is_deleted: bool,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

21
src/model/db/book.rs Normal file
View File

@@ -0,0 +1,21 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "book")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub name: String,
pub uid: i64,
pub is_deleted: bool,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

22
src/model/db/category.rs Normal file
View File

@@ -0,0 +1,22 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "category")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub name: String,
pub uid: i64,
pub parent_id: i64,
pub is_deleted: bool,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

9
src/model/db/mod.rs Normal file
View File

@@ -0,0 +1,9 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11
pub mod prelude;
pub mod account;
pub mod book;
pub mod category;
pub mod tag;
pub mod transaction;

7
src/model/db/prelude.rs Normal file
View File

@@ -0,0 +1,7 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11
pub use super::account::Entity as Account;
pub use super::book::Entity as Book;
pub use super::category::Entity as Category;
pub use super::tag::Entity as Tag;
pub use super::transaction::Entity as Transaction;

21
src/model/db/tag.rs Normal file
View File

@@ -0,0 +1,21 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "tag")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub name: String,
pub uid: i64,
pub is_deleted: bool,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,25 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "transaction")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub uid: i64,
pub r#type: i32,
pub book_id: i64,
pub category_id: i64,
pub description: String,
pub transaction_time: DateTimeWithTimeZone,
pub is_deleted: bool,
pub created_at: DateTime,
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,7 @@
use serde::Serialize;
#[derive(Serialize)]
pub struct BookItem {
pub id: i64,
pub name: String,
}

View File

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

2
src/model/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod db;
pub mod http_body;