First version

This commit is contained in:
Lol3rrr
2025-12-28 12:31:59 +01:00
commit a1d53deaea
32 changed files with 6599 additions and 0 deletions

1
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target/

1995
backend/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
backend/Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "backend"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = { version = "0.8.7" }
tokio = { version = "1.48.0", features = ["rt", "net"] }
tracing = { version = "0.1.43" }
tracing-subscriber = { version = "0.3.22", features = ["json"] }
sqlx = { version = "0.8.6", features = ["runtime-tokio", "postgres", "sqlite"] }
serde_json = "1.0.145"
serde = { version = "1.0.228", features = ["derive"] }
tower = "0.5.2"
tower-http = { version = "0.6.7", features = ["cors"] }

View File

@@ -0,0 +1,39 @@
CREATE TABLE IF NOT EXISTS units (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255),
short VARCHAR(255),
UNIQUE(name),
UNIQUE(short)
);
CREATE TABLE IF NOT EXISTS food (
id BIGSERIAL PRIMARY KEY,
version BIGINT,
name VARCHAR(255),
unit_id BIGINT REFERENCES units(id),
UNIQUE(name, version)
);
CREATE TABLE IF NOT EXISTS nutritional_kind (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255),
unit_id BIGINT REFERENCES units(id),
UNIQUE (name)
);
CREATE TABLE IF NOT EXISTS food_base (
food_id BIGINT REFERENCES food(id),
kind_id BIGINT REFERENCES nutritional_kind(id),
value FLOAT,
PRIMARY KEY (food_id, kind_id)
);
INSERT INTO units(name, short) VALUES('Gram', 'g');
INSERT INTO units(name, short) VALUES('Milliliter', 'ml');
CREATE TABLE IF NOT EXISTS tracking (
id BIGSERIAL PRIMARY KEY,
food_id BIGINT REFERENCES food(id),
quantity FLOAT,
ts BIGINT
);

312
backend/src/lib.rs Normal file
View File

@@ -0,0 +1,312 @@
use std::sync::Arc;
struct SharedState {
db: sqlx::postgres::PgPool,
}
pub fn app(db: sqlx::postgres::PgPool) -> axum::Router {
axum::Router::new()
.nest("/food", food::router())
.nest("/units", units::router())
.nest("/nutrition", nutrition::router())
.nest("/tracking", tracking::router())
.layer(tower::ServiceBuilder::new().layer(tower_http::cors::CorsLayer::very_permissive()))
.with_state(Arc::new(SharedState { db }))
}
mod units {
use super::SharedState;
use std::sync::Arc;
use axum::extract::State;
pub fn router() -> axum::Router<Arc<SharedState>> {
axum::Router::new()
.route("/list", axum::routing::get(list))
.route("/create", axum::routing::post(add))
}
#[derive(Debug, serde::Serialize)]
struct Unit {
id: i64,
name: String,
short: String,
}
async fn list(State(state): State<Arc<SharedState>>) -> axum::response::Json<Vec<Unit>> {
let result: Result<Vec<(i64, String, String)>, _> = sqlx::query_as("SELECT id, name, short FROM units").fetch_all(&state.db).await;
tracing::debug!(?result, "Query Result");
let result = result.unwrap();
axum::response::Json(result.into_iter().map(|row| Unit {
id: row.0,
name: row.1,
short: row.2,
}).collect())
}
#[derive(Debug, serde::Deserialize)]
struct AddUnitData {
name: String,
short: String,
}
async fn add(State(state): State<Arc<SharedState>>, axum::extract::Json(data): axum::extract::Json<AddUnitData>) -> String {
let result: Result<sqlx::postgres::PgQueryResult, sqlx::Error> = sqlx::query("INSERT INTO units(name, short) VALUES($1, $2)")
.bind(data.name)
.bind(data.short)
.execute(&state.db)
.await;
let result = result.unwrap();
tracing::debug!(?result, "Query Result");
String::new()
}
}
mod food {
use super::SharedState;
use std::sync::Arc;
use sqlx::Row;
use axum::extract::State;
pub fn router() -> axum::Router<Arc<SharedState>> {
axum::Router::new()
.route("/list", axum::routing::get(list_food))
.route("/create", axum::routing::post(create_food))
}
#[derive(Debug, serde::Serialize)]
struct Food {
id: i64,
version: i64,
name: String,
}
#[tracing::instrument(skip(state))]
async fn list_food(State(state): State<Arc<SharedState>>) -> axum::response::Json<Vec<Food>> {
tracing::debug!("List Food");
let result: Result<Vec<(i64, i64, String)>, _> =
sqlx::query_as("SELECT food.id, food.version, food.name FROM food")
.fetch_all(&state.db)
.await;
tracing::debug!(?result, "Query Result");
let result = result.unwrap();
axum::response::Json(
result
.into_iter()
.map(|row| Food {
id: row.0,
version: row.1,
name: row.2,
})
.collect(),
)
}
#[derive(Debug, serde::Deserialize)]
struct FoodCreateReqData {
name: String,
unit_id: i64,
base: Vec<FoodCreateReqDataBase>
}
#[derive(Debug, serde::Deserialize)]
struct FoodCreateReqDataBase {
nutrition_id: i64,
quantity: f32,
}
#[tracing::instrument(skip(state))]
async fn create_food(
State(state): State<Arc<SharedState>>,
axum::extract::Json(create_req): axum::extract::Json<FoodCreateReqData>,
) -> String {
tracing::debug!("Creating Food");
let version = 0;
let name = create_req.name;
let unit_id = create_req.unit_id;
let mut transaction = state.db.begin().await.unwrap();
let result: Result<sqlx::postgres::PgRow, sqlx::Error> = sqlx::query("INSERT INTO food(version, name, unit_id) VALUES($1, $2, $3) RETURNING id")
.bind(version)
.bind(name)
.bind(unit_id)
.fetch_one(&mut *transaction)
.await;
tracing::debug!(?result, "Query Result");
let result = result.unwrap();
let inserted_id: i64 = result.get("id");
tracing::debug!(?inserted_id, "Inserted item");
for base_n in create_req.base {
tracing::debug!(?base_n, "Creating base nutrition entry");
let result = sqlx::query("INSERT INTO food_base(food_id, kind_id, value) VALUES($1, $2, $3)")
.bind(inserted_id)
.bind(base_n.nutrition_id)
.bind(base_n.quantity)
.execute(&mut *transaction)
.await;
tracing::debug!(?result, "Query Result");
}
transaction.commit().await.unwrap();
String::new()
}
}
mod nutrition {
use super::SharedState;
use std::sync::Arc;
use axum::extract::State;
pub fn router() -> axum::Router<Arc<SharedState>> {
axum::Router::new()
.route("/list", axum::routing::get(list_nutritional_kind))
.route("/create", axum::routing::post(create_nutritional_kind))
}
#[derive(Debug, serde::Serialize)]
struct NutritionalKind {
id: i64,
name: String,
unit_id: i64,
unit_name: String,
}
#[tracing::instrument(skip(state))]
async fn list_nutritional_kind(State(state): State<Arc<SharedState>>) -> axum::response::Json<Vec<NutritionalKind>> {
tracing::debug!("Listing NutritionalKinds");
let result: Result<Vec<(i64, String, i64, String)>, _> =
sqlx::query_as("SELECT NK.id, NK.name, U.id, U.name FROM nutritional_kind NK INNER JOIN units U ON NK.unit_id = U.id")
.fetch_all(&state.db)
.await;
tracing::debug!(?result, "Query Result");
let result = result.unwrap();
axum::response::Json(result.into_iter().map(|row| {
NutritionalKind {
id: row.0,
name: row.1,
unit_id: row.2,
unit_name: row.3
}
}).collect())
}
#[derive(Debug, serde::Deserialize)]
struct NutritionalKindCreateReqData {
name: String,
unit_id: i64,
}
#[tracing::instrument(skip(state))]
async fn create_nutritional_kind(
State(state): State<Arc<SharedState>>,
axum::extract::Json(create_req): axum::extract::Json<NutritionalKindCreateReqData>,
) -> String {
tracing::debug!("Creating NutritionalKind");
let result = sqlx::query("INSERT INTO nutritional_kind(name, unit_id) VALUES($1, $2)")
.bind(create_req.name)
.bind(create_req.unit_id)
.execute(&state.db)
.await;
tracing::debug!(?result, "Query Result");
String::new()
}
}
mod tracking {
use super::SharedState;
use std::sync::Arc;
use axum::extract::State;
pub fn router() -> axum::Router<Arc<SharedState>> {
axum::Router::new()
.route("/list", axum::routing::get(list_entries))
.route("/add", axum::routing::post(add_tracking))
}
#[derive(Debug, serde::Serialize)]
struct TrackingEntry {
id: i64,
food_id: i64,
food_name: String,
unit_id: i64,
unit_short: String,
quantity: f64,
timestamp: i64,
}
#[tracing::instrument(skip(state))]
async fn list_entries(State(state): State<Arc<SharedState>>) -> axum::response::Json<Vec<TrackingEntry>> {
tracing::debug!("Listing Tracking Entries");
let result: Result<Vec<(i64, i64, String, i64, String, f64, i64)>, _> =
sqlx::query_as("
SELECT T.id, T.food_id, F.name, U.id, U.short, T.quantity, T.ts
FROM tracking T
INNER JOIN food F ON T.food_id = F.id
INNER JOIN units U ON F.unit_id = U.id
")
.fetch_all(&state.db)
.await;
tracing::debug!(?result, "Query Result");
let result = result.unwrap();
axum::response::Json(result.into_iter().map(|row| {
TrackingEntry {
id: row.0,
food_id: row.1,
food_name: row.2,
unit_id: row.3,
unit_short: row.4,
quantity: row.5,
timestamp: row.6
}
}).collect())
}
#[derive(Debug, serde::Deserialize)]
struct AddReqData {
food_id: i64,
quantity: f64,
timestamp: i64,
}
#[tracing::instrument(skip(state))]
async fn add_tracking(
State(state): State<Arc<SharedState>>,
axum::extract::Json(data): axum::extract::Json<AddReqData>
) -> String {
tracing::debug!("Creating NutritionalKind");
let result = sqlx::query("INSERT INTO tracking(food_id, quantity, ts) VALUES($1, $2, $3)")
.bind(data.food_id)
.bind(data.quantity)
.bind(data.timestamp)
.execute(&state.db)
.await;
tracing::debug!(?result, "Query Result");
String::new()
}
}

28
backend/src/main.rs Normal file
View File

@@ -0,0 +1,28 @@
use tracing_subscriber::layer::SubscriberExt;
fn main() {
let tracing_reg =
tracing_subscriber::Registry::default().with(tracing_subscriber::fmt::layer());
tracing::subscriber::set_global_default(tracing_reg).unwrap();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async move {
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(5)
.connect("postgres://postgres:testing@localhost/test")
.await
.unwrap();
sqlx::migrate!().run(&pool).await.unwrap();
let router = backend::app(pool);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, router).await.unwrap();
});
}