First version
This commit is contained in:
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target/
|
||||
1995
backend/Cargo.lock
generated
Normal file
1995
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
backend/Cargo.toml
Normal file
16
backend/Cargo.toml
Normal 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"] }
|
||||
39
backend/migrations/1_initial.sql
Normal file
39
backend/migrations/1_initial.sql
Normal 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
312
backend/src/lib.rs
Normal 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
28
backend/src/main.rs
Normal 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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user