From 9b0fce895309298a649c3e32e6c5eb4138e05c07 Mon Sep 17 00:00:00 2001 From: Jack DeVries Date: Fri, 15 Dec 2023 09:42:54 -0500 Subject: [PATCH] feat: meal deletion --- sqlx-data.json | 33 ++++++++++++---- src/components.rs | 2 +- src/controllers.rs | 20 +++++++++- src/count_chat/counter.rs | 58 ++++++++++++++++++++++++---- src/count_chat/llm_parse_response.rs | 24 +++++++----- src/count_chat/mod.rs | 9 ++--- src/count_chat/models.rs | 12 ------ src/routes.rs | 11 +++++- 8 files changed, 123 insertions(+), 46 deletions(-) delete mode 100644 src/count_chat/models.rs diff --git a/sqlx-data.json b/sqlx-data.json index e42ef4d..d1ed018 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -17,32 +17,50 @@ }, "query": "insert into meal (user_id, name, calories, fat, protein, carbohydrates)\n values ($1, $2, $3, $4, $5, $6)" }, - "4eae706064c5ddfeb72d5f5c9babc0f7ea7a4519454bc1ccc0b4e6af1f60ca4b": { + "21d97a21e8486f639820b3b00f89ba477a5443acfc3493413cef15ecf4d51f07": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int4", + "Int4" + ] + } + }, + "query": "delete from meal where user_id = $1 and id = $2" + }, + "62c068b9691dd48e0d6634b951ddd8d4b0d7957b7fd9f54d5d580ae9d812c052": { "describe": { "columns": [ { - "name": "meal_name", + "name": "id", "ordinal": 0, + "type_info": "Int4" + }, + { + "name": "meal_name", + "ordinal": 1, "type_info": "Text" }, { "name": "calories", - "ordinal": 1, + "ordinal": 2, "type_info": "Int4" }, { "name": "fat_grams", - "ordinal": 2, + "ordinal": 3, "type_info": "Int4" }, { "name": "protein_grams", - "ordinal": 3, + "ordinal": 4, "type_info": "Int4" }, { "name": "carbohydrates_grams", - "ordinal": 4, + "ordinal": 5, "type_info": "Int4" } ], @@ -51,6 +69,7 @@ false, false, false, + false, false ], "parameters": { @@ -59,7 +78,7 @@ ] } }, - "query": "select name meal_name, calories, fat fat_grams, protein protein_grams, carbohydrates carbohydrates_grams\n from meal\n where user_id = $1\n order by id desc\n " + "query": "select id, name meal_name, calories, fat fat_grams, protein protein_grams, carbohydrates carbohydrates_grams\n from meal\n where user_id = $1\n order by id desc\n " }, "68a469121ce660fafeb842413a14b4ec275bb6fcacdb10a939f5458329cf95a1": { "describe": { diff --git a/src/components.rs b/src/components.rs index 387bf8c..c1a4f1e 100644 --- a/src/components.rs +++ b/src/components.rs @@ -189,7 +189,7 @@ impl Component for ExternalLink<'_> { pub struct UserHome<'a> { pub user: &'a models::User, - pub meals: &'a Vec, + pub meals: &'a Vec, } impl Component for UserHome<'_> { fn render(&self) -> String { diff --git a/src/controllers.rs b/src/controllers.rs index 638085a..c135928 100644 --- a/src/controllers.rs +++ b/src/controllers.rs @@ -5,12 +5,13 @@ use super::{ }; use anyhow::Result; use axum::{ - extract::State, + extract::{Path, State}, http::{HeaderMap, HeaderValue}, response::IntoResponse, Form, }; use serde::Deserialize; +use sqlx::query; pub async fn root() -> impl IntoResponse { components::Page { @@ -191,3 +192,20 @@ pub async fn handle_login( )) } } + +pub async fn delete_meal( + State(AppState { db }): State, + headers: HeaderMap, + Path(id): Path, +) -> Result { + let session = Session::from_headers(&headers) + .ok_or_else(|| ServerError::forbidden("delete meal"))?; + query!( + "delete from meal where user_id = $1 and id = $2", + session.user.id, + id + ) + .execute(&db) + .await?; + Ok("") +} diff --git a/src/count_chat/counter.rs b/src/count_chat/counter.rs index 557e09b..9247494 100644 --- a/src/count_chat/counter.rs +++ b/src/count_chat/counter.rs @@ -2,7 +2,7 @@ //! are colocated here). use super::{ - llm_parse_response::{MealCard, MealInfo, ParserResult}, + llm_parse_response::{MealCard, ParserResult}, openai::OpenAI, }; use crate::{ @@ -19,14 +19,34 @@ use axum::{ use serde::Deserialize; use sqlx::{query, query_as, PgPool}; +pub struct Meal { + id: i32, + info: MealInfo, +} + +#[derive(Debug, Deserialize)] +pub struct MealInfo { + pub calories: i32, + pub protein_grams: i32, + pub carbohydrates_grams: i32, + pub fat_grams: i32, + pub meal_name: String, +} + pub struct Chat<'a> { - pub meals: &'a Vec, + pub meals: &'a Vec, } impl Component for Chat<'_> { fn render(&self) -> String { let handler = Route::HandleChat; let meals = self.meals.iter().fold(String::new(), |mut acc, meal| { - acc.push_str(&MealCard { info: meal }.render()); + acc.push_str( + &MealCard { + info: &meal.info, + meal_id: Some(meal.id), + } + .render(), + ); acc }); format!( @@ -108,16 +128,38 @@ pub async fn chat_form( Ok(content) } -pub async fn get_meals(db: &PgPool, user_id: i32) -> AResult> { - Ok(query_as!( - MealInfo, - "select name meal_name, calories, fat fat_grams, protein protein_grams, carbohydrates carbohydrates_grams +pub async fn get_meals(db: &PgPool, user_id: i32) -> AResult> { + struct Qres { + id: i32, + meal_name: String, + calories: i32, + fat_grams: i32, + protein_grams: i32, + carbohydrates_grams: i32, + } + let mut res = query_as!( + Qres, + "select id, name meal_name, calories, fat fat_grams, protein protein_grams, carbohydrates carbohydrates_grams from meal where user_id = $1 order by id desc ", user_id - ).fetch_all(db).await?) + ).fetch_all(db).await?; + + Ok(res + .drain(..) + .map(|r| Meal { + id: r.id, + info: MealInfo { + meal_name: r.meal_name, + calories: r.calories, + carbohydrates_grams: r.carbohydrates_grams, + fat_grams: r.fat_grams, + protein_grams: r.protein_grams, + }, + }) + .collect::>()) } pub async fn handle_save_meal( diff --git a/src/count_chat/llm_parse_response.rs b/src/count_chat/llm_parse_response.rs index 81ee050..e16c531 100644 --- a/src/count_chat/llm_parse_response.rs +++ b/src/count_chat/llm_parse_response.rs @@ -2,10 +2,10 @@ //! next time an a resonably well-structured declarative way. Inspired by //! https://www.youtube.com/watch?v=yj-wSRJwrrc. +use super::counter::MealInfo; use crate::{components::Component, routes::Route}; use ammonia::clean; use regex::{Captures, Regex}; -use serde::Deserialize; #[derive(Debug)] pub struct FollowUp { @@ -22,14 +22,6 @@ pub enum ParserResult { FollowUp(FollowUp), } -#[derive(Debug, Deserialize)] -pub struct MealInfo { - pub calories: i32, - pub protein_grams: i32, - pub carbohydrates_grams: i32, - pub fat_grams: i32, - pub meal_name: String, -} impl Component for MealInfo { fn render(&self) -> String { let save_route = Route::SaveMeal; @@ -70,9 +62,21 @@ impl Component for MealInfo { pub struct MealCard<'a> { pub info: &'a MealInfo, + pub meal_id: Option, } impl Component for MealCard<'_> { fn render(&self) -> String { + let delete_button = match self.meal_id { + Some(id) => { + let href = Route::DeleteMeal(Some(id)); + format!( + r#""# + ) + } + None => "".into(), + }; let meal_name = clean(&self.info.meal_name); let calories = self.info.calories; let protein = self.info.protein_grams; @@ -86,7 +90,7 @@ impl Component for MealCard<'_> {

Protein: {protein} grams

Carbs: {carbs} grams

Fat: {fat} grams

-

(todo: add delete button!)

+ {delete_button} "## ) diff --git a/src/count_chat/mod.rs b/src/count_chat/mod.rs index b48257b..994ded9 100644 --- a/src/count_chat/mod.rs +++ b/src/count_chat/mod.rs @@ -2,10 +2,7 @@ mod counter; mod llm_parse_response; mod openai; -pub use self::{ - counter::{ - chat_form, get_meals, handle_chat, handle_save_meal, - Chat as ChatContainer, - }, - llm_parse_response::MealInfo, +pub use self::counter::{ + chat_form, get_meals, handle_chat, handle_save_meal, Chat as ChatContainer, + Meal, }; diff --git a/src/count_chat/models.rs b/src/count_chat/models.rs deleted file mode 100644 index 4e1a820..0000000 --- a/src/count_chat/models.rs +++ /dev/null @@ -1,12 +0,0 @@ -/// I am randomly switching over to `i32` for my numbers instead of `u16` -/// because it's occurred to me that getting a u16 in and out of the DB with -/// SQLx is going to be a PITA. I should change all my modeling around these -/// numbers to just use `i32` instead of `u16` at some point. -#[derive(Deserialize)] -pub struct Meal { - name: String - calories: i32, - fat: i32, - protein: i32, - carbohydrates: i32 -} diff --git a/src/routes.rs b/src/routes.rs index 8ca92a0..32efd14 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,7 +1,7 @@ //! All possible routes with their params are defined in a big enum. use super::{controllers, count_chat, models}; -use axum::routing::{get, post, Router}; +use axum::routing::{delete, get, post, Router}; /// This enum contains all of the route strings in the application. This /// solves several problems. @@ -23,6 +23,7 @@ pub enum Route<'a> { HandleChat, ChatForm, SaveMeal, + DeleteMeal(Option), /// The `String` slug is unnecessary here, but this is the general pattern /// for handling routes that have slug parameters. UserHome(Option<&'a str>), @@ -41,6 +42,10 @@ impl Route<'_> { Self::HandleChat => "/chat".into(), Self::ChatForm => "/chat-form".into(), Self::SaveMeal => "/save-meal".into(), + Self::DeleteMeal(slug) => match slug { + Some(value) => format!("/delete-meal/{value}"), + None => "/delete-meal/:id".into(), + }, Self::UserHome(slug) => match slug { Some(value) => format!("/home/{value}"), None => "/home/:slug".into(), @@ -79,6 +84,10 @@ pub fn get_protected_routes() -> Router { &Route::SaveMeal.as_string(), post(count_chat::handle_save_meal), ) + .route( + &Route::DeleteMeal(None).as_string(), + delete(controllers::delete_meal), + ) } /// In [crate::main], these routes are not protected by any authentication, so