mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-21 19:29:49 -04:00
API backend for more granular storage of ingredient mappings
This commit is contained in:
parent
aa4e2e8f8a
commit
44e8c0f727
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -55,7 +55,7 @@ checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
|
||||
|
||||
[[package]]
|
||||
name = "api"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "api"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
@ -179,3 +179,11 @@ impl From<InventoryData> for InventoryResponse {
|
||||
Response::Success(inventory_data)
|
||||
}
|
||||
}
|
||||
|
||||
pub type CategoryMappingResponse = Response<Vec<(String, String)>>;
|
||||
|
||||
impl From<Vec<(String, String)>> for CategoryMappingResponse {
|
||||
fn from(mappings: Vec<(String, String)>) -> Self {
|
||||
Response::Success(mappings)
|
||||
}
|
||||
}
|
||||
|
4
kitchen/migrations/20230105215101_category_maps.down.sql
Normal file
4
kitchen/migrations/20230105215101_category_maps.down.sql
Normal file
@ -0,0 +1,4 @@
|
||||
-- Add down migration script here
|
||||
|
||||
drop index user_category_lookup;
|
||||
drop table category_mappings;
|
10
kitchen/migrations/20230105215101_category_maps.up.sql
Normal file
10
kitchen/migrations/20230105215101_category_maps.up.sql
Normal file
@ -0,0 +1,10 @@
|
||||
-- Add up migration script here
|
||||
|
||||
create table category_mappings(
|
||||
user_id TEXT NOT NULL,
|
||||
ingredient_name TEXT NOT NULL,
|
||||
category_name TEXT NOT NULL DEFAULT "Misc",
|
||||
primary key(user_id, ingredient_name)
|
||||
);
|
||||
|
||||
create index user_category_lookup on category_mappings (user_id, category_name);
|
@ -112,6 +112,30 @@
|
||||
},
|
||||
"query": "select plan_date as \"plan_date: NaiveDate\", recipe_id, count\nfrom plan_recipes\nwhere\n user_id = ?\n and date(plan_date) > ?\norder by user_id, plan_date"
|
||||
},
|
||||
"37f382be1b53efd2f79a0d59ae6a8717f88a86908a7a4128d5ed7339147ca59d": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "ingredient_name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "category_name",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "select ingredient_name, category_name from category_mappings where user_id = ?"
|
||||
},
|
||||
"3caefb86073c47b5dd5d05f639ddef2f7ed2d1fd80f224457d1ec34243cc56c7": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -318,6 +342,16 @@
|
||||
},
|
||||
"query": "select category_text from categories where user_id = ?"
|
||||
},
|
||||
"d73e4bfb1fbee6d2dd35fc787141a1c2909a77cf4b19950671f87e694289c242": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
}
|
||||
},
|
||||
"query": "insert into category_mappings\n (user_id, ingredient_name, category_name)\n values (?, ?, ?)"
|
||||
},
|
||||
"d84685a82585c5e4ae72c86ba1fe6e4a7241c4c3c9e948213e5849d956132bad": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
|
@ -120,6 +120,47 @@ async fn api_recipes(
|
||||
result.into()
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn api_category_mappings(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
) -> api::CategoryMappingResponse {
|
||||
use storage::UserIdFromSession::*;
|
||||
match session {
|
||||
NoUserId => api::Response::Unauthorized,
|
||||
FoundUserId(user_id) => match app_store.get_category_mappings_for_user(&user_id.0).await {
|
||||
Ok(Some(mappings)) => api::CategoryMappingResponse::from(mappings),
|
||||
Ok(None) => api::CategoryMappingResponse::from(Vec::new()),
|
||||
Err(e) => api::CategoryMappingResponse::error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
format!("{:?}", e),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn api_save_category_mappings(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
Json(mappings): Json<Vec<(String, String)>>,
|
||||
) -> api::EmptyResponse {
|
||||
use storage::UserIdFromSession::*;
|
||||
match session {
|
||||
NoUserId => api::Response::Unauthorized,
|
||||
FoundUserId(user_id) => match app_store
|
||||
.save_category_mappings_for_user(&user_id.0, &mappings)
|
||||
.await
|
||||
{
|
||||
Ok(_) => api::EmptyResponse::success(()),
|
||||
Err(e) => api::EmptyResponse::error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
format!("{:?}", e),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn api_categories(
|
||||
Extension(store): Extension<Arc<storage::file_store::AsyncFileStore>>,
|
||||
@ -369,7 +410,12 @@ fn mk_v2_routes() -> Router {
|
||||
"/inventory",
|
||||
get(api_inventory_v2).post(api_save_inventory_v2),
|
||||
)
|
||||
// TODO(jwall): This is now deprecated but will still work
|
||||
.route("/categories", get(api_categories).post(api_save_categories))
|
||||
.route(
|
||||
"/category_map",
|
||||
get(api_category_mappings).post(api_save_category_mappings),
|
||||
)
|
||||
// All the routes above require a UserId.
|
||||
.route("/auth", get(auth::handler).post(auth::handler))
|
||||
.route("/account", get(api_user_account))
|
||||
|
@ -0,0 +1 @@
|
||||
select ingredient_name, category_name from category_mappings where user_id = ?
|
@ -90,6 +90,17 @@ fn check_pass(payload: &String, pass: &Secret<String>) -> bool {
|
||||
pub trait APIStore {
|
||||
async fn get_categories_for_user(&self, user_id: &str) -> Result<Option<String>>;
|
||||
|
||||
async fn get_category_mappings_for_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Option<Vec<(String, String)>>>;
|
||||
|
||||
async fn save_category_mappings_for_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
mappings: &Vec<(String, String)>,
|
||||
) -> Result<()>;
|
||||
|
||||
async fn get_recipes_for_user(&self, user_id: &str) -> Result<Option<Vec<RecipeEntry>>>;
|
||||
|
||||
async fn store_recipes_for_user(&self, user_id: &str, recipes: &Vec<RecipeEntry>)
|
||||
@ -326,6 +337,50 @@ impl APIStore for SqliteStore {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_category_mappings_for_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> Result<Option<Vec<(String, String)>>> {
|
||||
struct Row {
|
||||
ingredient_name: String,
|
||||
category_name: String,
|
||||
}
|
||||
let rows: Vec<Row> = sqlx::query_file_as!(
|
||||
Row,
|
||||
"src/web/storage/fetch_category_mappings_for_user.sql",
|
||||
user_id
|
||||
)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
if rows.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let mut mappings = Vec::new();
|
||||
for r in rows {
|
||||
mappings.push((r.ingredient_name, r.category_name));
|
||||
}
|
||||
Ok(Some(mappings))
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_category_mappings_for_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
mappings: &Vec<(String, String)>,
|
||||
) -> Result<()> {
|
||||
for (name, category) in mappings.iter() {
|
||||
sqlx::query_file!(
|
||||
"src/web/storage/save_category_mappings_for_user.sql",
|
||||
user_id,
|
||||
name,
|
||||
category,
|
||||
)
|
||||
.execute(self.pool.as_ref())
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_recipe_entry_for_user<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
|
@ -0,0 +1,3 @@
|
||||
insert into category_mappings
|
||||
(user_id, ingredient_name, category_name)
|
||||
values (?, ?, ?)
|
Loading…
x
Reference in New Issue
Block a user