API backend for more granular storage of ingredient mappings

This commit is contained in:
Jeremy Wall 2023-01-05 13:39:16 -05:00
parent aa4e2e8f8a
commit 44e8c0f727
10 changed files with 163 additions and 2 deletions

2
Cargo.lock generated
View File

@ -55,7 +55,7 @@ checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
[[package]]
name = "api"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"axum",
"chrono",

View File

@ -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

View File

@ -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)
}
}

View File

@ -0,0 +1,4 @@
-- Add down migration script here
drop index user_category_lookup;
drop table category_mappings;

View 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);

View File

@ -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": [],

View File

@ -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))

View File

@ -0,0 +1 @@
select ingredient_name, category_name from category_mappings where user_id = ?

View File

@ -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,

View File

@ -0,0 +1,3 @@
insert into category_mappings
(user_id, ingredient_name, category_name)
values (?, ?, ?)