mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -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]]
|
[[package]]
|
||||||
name = "api"
|
name = "api"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "api"
|
name = "api"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# 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)
|
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"
|
"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": {
|
"3caefb86073c47b5dd5d05f639ddef2f7ed2d1fd80f224457d1ec34243cc56c7": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
@ -318,6 +342,16 @@
|
|||||||
},
|
},
|
||||||
"query": "select category_text from categories where user_id = ?"
|
"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": {
|
"d84685a82585c5e4ae72c86ba1fe6e4a7241c4c3c9e948213e5849d956132bad": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
@ -120,6 +120,47 @@ async fn api_recipes(
|
|||||||
result.into()
|
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]
|
#[instrument]
|
||||||
async fn api_categories(
|
async fn api_categories(
|
||||||
Extension(store): Extension<Arc<storage::file_store::AsyncFileStore>>,
|
Extension(store): Extension<Arc<storage::file_store::AsyncFileStore>>,
|
||||||
@ -369,7 +410,12 @@ fn mk_v2_routes() -> Router {
|
|||||||
"/inventory",
|
"/inventory",
|
||||||
get(api_inventory_v2).post(api_save_inventory_v2),
|
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("/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.
|
// All the routes above require a UserId.
|
||||||
.route("/auth", get(auth::handler).post(auth::handler))
|
.route("/auth", get(auth::handler).post(auth::handler))
|
||||||
.route("/account", get(api_user_account))
|
.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 {
|
pub trait APIStore {
|
||||||
async fn get_categories_for_user(&self, user_id: &str) -> Result<Option<String>>;
|
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 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>)
|
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>(
|
async fn get_recipe_entry_for_user<S: AsRef<str> + Send>(
|
||||||
&self,
|
&self,
|
||||||
user_id: S,
|
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