mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
Compare commits
No commits in common. "main" and "v0.2.16" have entirely different histories.
9
.envrc
9
.envrc
@ -1,9 +0,0 @@
|
||||
if has lorri; then
|
||||
eval "$(lorri direnv)"
|
||||
elif has nix; then
|
||||
echo "Using flake fallback since lorri isn't installed"
|
||||
use flake
|
||||
else
|
||||
# noop
|
||||
echo "Unsupported direnv configuration. We need nix flake support and lorri installed"
|
||||
fi
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,10 +1,10 @@
|
||||
target/
|
||||
.lsp/
|
||||
.clj-kondo/
|
||||
web/dist/
|
||||
webdist/
|
||||
nix/*/result
|
||||
result
|
||||
.vscode/
|
||||
.session_store/
|
||||
.gitignore/
|
||||
.DS_Store/
|
||||
.env
|
||||
.gitignore/
|
2515
Cargo.lock
generated
2515
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,13 @@
|
||||
[workspace]
|
||||
members = [ "recipes", "kitchen", "web", "api" ]
|
||||
resolver = "2"
|
||||
|
||||
[patch.crates-io]
|
||||
# TODO(jwall): When the fix for RcSignal Binding is released we can drop this patch.
|
||||
sycamore = { git = "https://github.com/sycamore-rs/sycamore/", rev = "5d49777b4a66fb5730c40898fd2ee8cde15bcdc3" }
|
||||
sycamore-router = { git = "https://github.com/sycamore-rs/sycamore/", rev = "5d49777b4a66fb5730c40898fd2ee8cde15bcdc3" }
|
||||
# NOTE(jwall): We are maintaining a patch to remove the unstable async_std_feature. It breaks in our project on
|
||||
# Rust v1.64
|
||||
sqlx = { git = "https://github.com/zaphar/sqlx", branch = "remove_unstable_async_std_feature" }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
17
Makefile
17
Makefile
@ -14,8 +14,6 @@
|
||||
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
|
||||
mkfile_dir := $(dir $(mkfile_path))
|
||||
sqlite_url := sqlite://$(mkfile_dir)/.session_store/store.db
|
||||
export out := dist
|
||||
export project := kitchen
|
||||
|
||||
kitchen: wasm kitchen/src/*.rs
|
||||
cd kitchen; cargo build
|
||||
@ -29,19 +27,15 @@ static-prep: web/index.html web/favicon.ico web/static/*.css
|
||||
cp -r web/favicon.ico web/dist/
|
||||
cp -r web/static web/dist/
|
||||
|
||||
wasmrelease: wasm-opt static-prep
|
||||
|
||||
wasm-opt: wasmrelease-dist
|
||||
cd web; sh ../scripts/wasm-opt.sh release
|
||||
wasmrelease: wasmrelease-dist static-prep
|
||||
|
||||
wasmrelease-dist: web/src/*.rs web/src/components/*.rs
|
||||
cd web; sh ../scripts/wasm-build.sh release
|
||||
cd web; wasm-pack build --mode no-install --release --target web --out-dir dist/
|
||||
|
||||
wasm: wasm-dist static-prep
|
||||
|
||||
wasm-dist: web/src/*.rs web/src/components/*.rs
|
||||
cd web; sh ../scripts/wasm-build.sh debug
|
||||
cd web; sh ../scripts/wasm-sourcemap.sh
|
||||
cd web; wasm-pack build --mode no-install --target web --out-dir dist/
|
||||
|
||||
clean:
|
||||
rm -rf web/dist/*
|
||||
@ -50,11 +44,8 @@ clean:
|
||||
sqlx-migrate:
|
||||
cd kitchen; cargo sqlx migrate run --database-url $(sqlite_url)
|
||||
|
||||
sqlx-add-%:
|
||||
cd kitchen; cargo sqlx migrate add -r $*
|
||||
|
||||
sqlx-revert:
|
||||
cd kitchen; cargo sqlx migrate revert --database-url $(sqlite_url)
|
||||
|
||||
sqlx-prepare: wasm
|
||||
sqlx-prepare:
|
||||
cd kitchen; cargo sqlx prepare --database-url $(sqlite_url)
|
||||
|
@ -1,17 +1,15 @@
|
||||
[package]
|
||||
name = "api"
|
||||
version = "0.1.3"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0.144"
|
||||
recipes = { path = "../recipes" }
|
||||
chrono = "0.4.22"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.204"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.axum]
|
||||
version = "0.5.16"
|
||||
@ -20,4 +18,4 @@ optional = true
|
||||
[features]
|
||||
default = []
|
||||
server = ["axum"]
|
||||
browser = []
|
||||
browser = []
|
@ -77,28 +77,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> From<Result<Option<T>, E>> for Response<T>
|
||||
where
|
||||
T: Default,
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
fn from(val: Result<Option<T>, E>) -> Self {
|
||||
impl<T> From<Result<T, String>> for Response<T> {
|
||||
fn from(val: Result<T, String>) -> Self {
|
||||
match val {
|
||||
Ok(Some(val)) => Response::Success(val),
|
||||
Ok(None) => Response::Success(T::default()),
|
||||
Err(e) => Response::error(500, format!("{:?}", e)),
|
||||
Ok(val) => Response::Success(val),
|
||||
Err(e) => Response::error(500, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> From<Result<T, E>> for Response<T>
|
||||
impl<T> From<Result<Option<T>, String>> for Response<T>
|
||||
where
|
||||
E: std::fmt::Debug,
|
||||
T: Default,
|
||||
{
|
||||
fn from(val: Result<T, E>) -> Self {
|
||||
fn from(val: Result<Option<T>, String>) -> Self {
|
||||
match val {
|
||||
Ok(v) => Response::success(v),
|
||||
Err(e) => Response::error(500, format!("{:?}", e)),
|
||||
Ok(Some(val)) => Response::Success(val),
|
||||
Ok(None) => Response::Success(T::default()),
|
||||
Err(e) => Response::error(500, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
103
flake.lock
generated
103
flake.lock
generated
@ -1,43 +1,5 @@
|
||||
{
|
||||
"nodes": {
|
||||
"cargo-wasm2map-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1693927731,
|
||||
"narHash": "sha256-oqJ9ZZLvUK57A9Kf6L4pPrW6nHqb+18+JGKj9HfIaaM=",
|
||||
"owner": "mtolmacs",
|
||||
"repo": "wasm2map",
|
||||
"rev": "c7d80748b7f3af37df24770b9330b17aa9599e3e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "mtolmacs",
|
||||
"repo": "wasm2map",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"naersk",
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1752475459,
|
||||
"narHash": "sha256-z6QEu4ZFuHiqdOPbYss4/Q8B0BFhacR8ts6jO/F/aOU=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "bf0d6f70f4c9a9cf8845f992105652173f4b617f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@ -69,6 +31,21 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@ -87,15 +64,14 @@
|
||||
},
|
||||
"naersk": {
|
||||
"inputs": {
|
||||
"fenix": "fenix",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1752689277,
|
||||
"narHash": "sha256-uldUBFkZe/E7qbvxa3mH1ItrWZyT6w1dBKJQF/3ZSsc=",
|
||||
"lastModified": 1659610603,
|
||||
"narHash": "sha256-LYgASYSPYo7O71WfeUOaEUzYfzuXm8c8eavJcel+pfI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "0e72363d0938b0208d6c646d10649164c43f4d64",
|
||||
"rev": "c6a45e4277fa58abd524681466d3450f896dc094",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -106,27 +82,25 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1752077645,
|
||||
"narHash": "sha256-HM791ZQtXV93xtCY+ZxG1REzhQenSQO020cu6rHtAPk=",
|
||||
"lastModified": 1659868656,
|
||||
"narHash": "sha256-LINDS957FYzOb412t/Zha44LQqGniMpUIUz4Pi+fvSs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "be9e214982e20b8310878ac2baa063a961c1bdf6",
|
||||
"rev": "80fc83ad314fe701766ee66ac8286307d65b39e3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1753135609,
|
||||
"narHash": "sha256-//xMo8MwSw1HoTnIk455J7NIJpsDqwVyD69MOXb7gZM=",
|
||||
"lastModified": 1668907448,
|
||||
"narHash": "sha256-l71WVOLoOLTuVgdn69SX2IosITFu4eQJXVtJqAmg0Wk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5d9316e7fb2d6395818d506ef997530eba1545b7",
|
||||
"rev": "e7cbe75849e582b20884f4b9651a80dffafffb16",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -137,7 +111,6 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"cargo-wasm2map-src": "cargo-wasm2map-src",
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"gitignore": "gitignore",
|
||||
@ -146,35 +119,19 @@
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1752428706,
|
||||
"narHash": "sha256-EJcdxw3aXfP8Ex1Nm3s0awyH9egQvB2Gu+QEnJn2Sfg=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "591e3b7624be97e4443ea7b5542c191311aa141d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1750964660,
|
||||
"narHash": "sha256-YQ6EyFetjH1uy5JhdhRdPe6cuNXlYpMAQePFfZj4W7M=",
|
||||
"lastModified": 1665398664,
|
||||
"narHash": "sha256-y/UcVB5k0Wdc0j+7whJE2+vko8m296wZYX37b2lFSpI=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "04f0fcfb1a50c63529805a798b4b5c21610ff390",
|
||||
"rev": "af29a900f10dd6e467622202fb4f6d944d72a3a6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
68
flake.nix
68
flake.nix
@ -2,31 +2,28 @@
|
||||
description = "kitchen";
|
||||
# Pin nixpkgs
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs";
|
||||
gitignore = { url = "github:hercules-ci/gitignore.nix"; flake = false; };
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay?ref=stable";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
|
||||
cargo-wasm2map-src = { url = "github:mtolmacs/wasm2map"; flake = false; };
|
||||
nixpkgs.url = "github:NixOS/nixpkgs";
|
||||
gitignore = { url = "github:hercules-ci/gitignore.nix"; flake = false; };
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay?ref=stable";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
naersk.url = "github:nix-community/naersk";
|
||||
flake-compat = { url = github:edolstra/flake-compat; flake = false; };
|
||||
};
|
||||
outputs = {nixpkgs, flake-utils, rust-overlay, naersk, cargo-wasm2map-src, ...}:
|
||||
outputs = {self, nixpkgs, flake-utils, rust-overlay, naersk, gitignore, flake-compat}:
|
||||
let
|
||||
kitchenGen = (import ./nix/kitchen/default.nix);
|
||||
kitchenWasmGen = (import ./nix/kitchenWasm/default.nix);
|
||||
moduleGen = (import ./nix/kitchen/module.nix);
|
||||
wasm-packGen = (import ./nix/wasm-pack/default.nix);
|
||||
wasm-bindgenGen = (import ./nix/wasm-bindgen/default.nix);
|
||||
version = "0.2.25";
|
||||
version = "0.2.16";
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
overlays = [ rust-overlay.overlays.default ];
|
||||
pkgs = import nixpkgs { inherit system overlays; };
|
||||
rust-wasm = pkgs.rust-bin.stable."1.87.0".default.override {
|
||||
rust-wasm = pkgs.rust-bin.stable."1.64.0".default.override {
|
||||
extensions = [ "rust-src" ];
|
||||
# Add wasm32 as an extra target besides the native target.
|
||||
targets = [ "wasm32-unknown-unknown" ];
|
||||
@ -37,29 +34,8 @@
|
||||
rustc = rust-wasm;
|
||||
cargo = rust-wasm;
|
||||
};
|
||||
# TODO(jwall): Do the same thing for wasm-bindgen as well?
|
||||
# We've run into a few problems with the bundled wasm-pack in nixpkgs.
|
||||
# Better to just control this part of our toolchain directly.
|
||||
wasm-pack = wasm-packGen {
|
||||
inherit rust-wasm naersk-lib pkgs;
|
||||
};
|
||||
cargo-wasm2map = naersk-lib.buildPackage {
|
||||
pname = "cargo-wasm2map";
|
||||
version = "v0.1.0";
|
||||
build-inputs = [ rust-wasm ];
|
||||
src = cargo-wasm2map-src;
|
||||
cargoBuildOptions = opts: opts ++ ["-p" "cargo-wasm2map" ];
|
||||
};
|
||||
wasm-bindgen = pkgs.callPackage wasm-bindgenGen { inherit pkgs; };
|
||||
kitchenWasm = kitchenWasmGen {
|
||||
inherit pkgs rust-wasm wasm-bindgen version cargo-wasm2map;
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
# I'm maintaining some patches for these so the lockfile hashes are a little
|
||||
# incorrect. We override those here.
|
||||
"wasm-web-component-0.2.0" = "sha256-quuPgzGb2F96blHmD3BAUjsWQYbSyJGZl27PVrwL92k=";
|
||||
"sycamore-0.8.2" = "sha256-D968+8C5EelGGmot9/LkAlULZOf/Cr+1WYXRCMwb1nQ=";
|
||||
};
|
||||
inherit pkgs rust-wasm version;
|
||||
};
|
||||
kitchen = (kitchenGen {
|
||||
inherit pkgs version naersk-lib kitchenWasm rust-wasm;
|
||||
@ -69,15 +45,8 @@
|
||||
root = ./.;
|
||||
});
|
||||
kitchenWasmDebug = kitchenWasmGen {
|
||||
inherit pkgs rust-wasm wasm-bindgen version cargo-wasm2map;
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
# I'm maintaining some patches for these so the lockfile hashes are a little
|
||||
# incorrect. We override those here.
|
||||
"wasm-web-component-0.2.0" = "sha256-quuPgzGb2F96blHmD3BAUjsWQYbSyJGZl27PVrwL92k=";
|
||||
"sycamore-0.8.2" = "sha256-D968+8C5EelGGmot9/LkAlULZOf/Cr+1WYXRCMwb1nQ=";
|
||||
};
|
||||
#features = "--features debug_logs";
|
||||
inherit pkgs rust-wasm version;
|
||||
features = "--features debug_logs";
|
||||
};
|
||||
kitchenDebug = (kitchenGen {
|
||||
inherit pkgs version naersk-lib rust-wasm;
|
||||
@ -103,10 +72,7 @@
|
||||
type = "app";
|
||||
program = "${kitchen}/bin/kitchen";
|
||||
};
|
||||
devShell = pkgs.callPackage ./nix/devShell/default.nix {
|
||||
inherit rust-wasm wasm-bindgen cargo-wasm2map;
|
||||
wasm-pack-hermetic = wasm-pack;
|
||||
};
|
||||
devShell = pkgs.callPackage ./nix/devShell/default.nix { inherit rust-wasm; };
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "select recipe_id, recipe_text, category, serving_count from recipes where user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "recipe_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "recipe_text",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "category",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "serving_count",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "01018c919131848f8fa907a1356a1356b2aa6ca0912de8a296f5fef3486b5ff9"
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "select password_hashed from users where id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "password_hashed",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "104f07472670436d3eee1733578bbf0c92dc4f965d3d13f9bf4bfbc92958c5b6"
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "with latest_dates as (\n select user_id, max(date(plan_date)) as plan_date from plan_recipes\n where user_id = ?\n group by user_id\n)\n\nselect\n extra_items.name,\n extra_items.amt\nfrom latest_dates\ninner join extra_items on\n latest_dates.user_id = extra_items.user_id\n and latest_dates.plan_date = extra_items.plan_date",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "amt",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "10de1e9950d7d3ae7f017b9175a1cee4ff7fcbc7403a39ea02930c75b4b9160a"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "delete from modified_amts where user_id = ? and plan_date = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "10e1c111a16d647a106a3147f4e61e34b0176860ca99cb62cb43dc72550ad990"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into filtered_ingredients(user_id, name, form, measure_type, plan_date)\n values (?, ?, ?, ?, date()) on conflict(user_id, name, form, measure_type, plan_date) DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "160a9dfccf2e91a37d81f75eba21ec73105a7453c4f1fe76a430d04e525bc6cd"
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"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",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "plan_date: NaiveDate",
|
||||
"ordinal": 0,
|
||||
"type_info": "Date"
|
||||
},
|
||||
{
|
||||
"name": "recipe_id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "19832e3582c05ed49c676fde33cde64274379a83a8dd130f6eec96c1d7250909"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into staples (user_id, content) values (?, ?)\n on conflict(user_id) do update set content = excluded.content",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1b4a7250e451991ee7e642c6389656814e0dd00c94e59383c02af6313bc76213"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into modified_amts(user_id, name, form, measure_type, amt, plan_date)\n values (?, ?, ?, ?, ?, ?) on conflict (user_id, name, form, measure_type, plan_date) do update set amt=excluded.amt",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1b6fd91460bef61cf02f210404a4ca57b520c969d1f9613e7101ee6aa7a9962a"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "delete from filtered_ingredients where user_id = ? and plan_date = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "23beb05e40cf011170182d4e98cdf1faa3d8df6e5956e471245e666f32e56962"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into category_mappings\n (user_id, ingredient_name, category_name)\n values (?, ?, ?)\n on conflict (user_id, ingredient_name)\n do update set category_name=excluded.category_name\n",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2582522f8ca9f12eccc70a3b339d9030aee0f52e62d6674cfd3862de2a68a177"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "delete from plan_table where user_id = ? and plan_date = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "27aa0a21f534cdf580841fa111136fc26cf1a0ca4ddb308c12f3f8f5a62d6178"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into plan_table (user_id, plan_date) values (?, ?)\n on conflict (user_id, plan_date) do nothing;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "288535e7b9e1f02ad1b677e3dddc85f38c0766ce16d26fc1bdd2bf90ab9a7f7c"
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "select plan_date as \"plan_date: NaiveDate\", recipe_id, count\n from plan_recipes\nwhere\n user_id = ?\n and plan_date = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "plan_date: NaiveDate",
|
||||
"ordinal": 0,
|
||||
"type_info": "Date"
|
||||
},
|
||||
{
|
||||
"name": "recipe_id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2e076acd2405d234daaa866e5a2ac1e10989fc8d2820f90aa722464a7b17db6b"
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "select ingredient_name, category_name from category_mappings where user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "ingredient_name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "category_name",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "37f382be1b53efd2f79a0d59ae6a8717f88a86908a7a4128d5ed7339147ca59d"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into extra_items (user_id, name, plan_date, amt)\nvalues (?, ?, date(), ?)\non conflict (user_id, name, plan_date) do update set amt=excluded.amt",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3caefb86073c47b5dd5d05f639ddef2f7ed2d1fd80f224457d1ec34243cc56c7"
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "with latest_dates as (\n select user_id, max(date(plan_date)) as plan_date from plan_recipes\n where user_id = ?\n group by user_id\n)\n\nselect\n modified_amts.name,\n modified_amts.form,\n modified_amts.measure_type,\n modified_amts.amt\nfrom latest_dates\ninner join modified_amts on\n latest_dates.user_id = modified_amts.user_id\n and latest_dates.plan_date = modified_amts.plan_date",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "form",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "measure_type",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "amt",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3e43f06f5c2e959f66587c8d74696d6db27d89fd2f7d7e1ed6fa5016b4bd1a91"
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "select\n name,\n amt\nfrom extra_items\nwhere\n user_id = ?\n and plan_date = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "amt",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "4237ff804f254c122a36a14135b90434c6576f48d3a83245503d702552ea9f30"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into filtered_ingredients(user_id, name, form, measure_type, plan_date)\n values (?, ?, ?, ?, ?) on conflict(user_id, name, form, measure_type, plan_date) DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5883c4a57def93cca45f8f9d81c8bba849547758217cd250e7ab28cc166ab42b"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into users (id, password_hashed) values (?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5d743897fb0d8fd54c3708f1b1c6e416346201faa9e28823c1ba5a421472b1fa"
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "select content from staples where user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "content",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "64af3f713eb4c61ac02cab2dfea83d0ed197e602e99079d4d32cb38d677edf2e"
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "select\n modified_amts.name,\n modified_amts.form,\n modified_amts.measure_type,\n modified_amts.amt\nfrom modified_amts\nwhere\n user_id = ?\n and plan_date = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "form",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "measure_type",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "amt",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "699ff0f0d4d4c6e26a21c1922a5b5249d89ed1677680a2276899a7f8b26344ee"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "delete from recipes where user_id = ? and recipe_id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6c43908d90f229b32ed8b1b076be9b452a995e1b42ba2554e947c515b031831a"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into modified_amts(user_id, name, form, measure_type, amt, plan_date)\n values (?, ?, ?, ?, ?, date()) on conflict (user_id, name, form, measure_type, plan_date) do update set amt=excluded.amt",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6e28698330e42fd6c87ba1e6f1deb664c0d3995caa2b937ceac8c908e98aded6"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "delete from extra_items where user_id = ? and plan_date = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6f11d90875a6230766a5f9bd1d67665dc4d00c13d7e81b0d18d60baa67987da9"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "delete from sessions where id = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "7578157607967a6a4c60f12408c5d9900d15b429a49681a4cae4e02d31c524ec"
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "select\n filtered_ingredients.name,\n filtered_ingredients.form,\n filtered_ingredients.measure_type\nfrom filtered_ingredients\nwhere\n user_id = ?\n and plan_date = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "form",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "measure_type",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7695a0602395006f9b76ecd4d0cb5ecd5dee419b71b3b0b9ea4f47a83f3df41a"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into plan_recipes (user_id, plan_date, recipe_id, count) values (?, ?, ?, ?)\n on conflict (user_id, plan_date, recipe_id) do update set count=excluded.count;",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "83824ea638cb64c524f5c8984ef6ef28dfe781f0abf168abc4ae9a51e6e0ae88"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into categories (user_id, category_text) values (?, ?)\n on conflict(user_id) do update set category_text=excluded.category_text",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8490e1bb40879caed62ac1c38cb9af48246f3451b6f7f1e1f33850f1dbe25f58"
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "select session_value from sessions where id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "session_value",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "928a479ca0f765ec7715bf8784c5490e214486edbf5b78fd501823feb328375b"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "delete from plan_recipes where user_id = ? and plan_date = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "93af0c367a0913d49c92aa69022fa30fc0564bd4dbab7f3ae78673a01439cd6e"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into sessions (id, session_value) values (?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9ad4acd9b9d32c9f9f441276aa71a17674fe4d65698848044778bd4aef77d42d"
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "with max_date as (\n select user_id, max(date(plan_date)) as plan_date from plan_recipes group by user_id\n)\n\nselect plan_recipes.plan_date as \"plan_date: NaiveDate\", plan_recipes.recipe_id, plan_recipes.count\n from plan_recipes\n inner join max_date on plan_recipes.user_id = max_date.user_id\nwhere\n plan_recipes.user_id = ?\n and plan_recipes.plan_date = max_date.plan_date",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "plan_date: NaiveDate",
|
||||
"ordinal": 0,
|
||||
"type_info": "Date"
|
||||
},
|
||||
{
|
||||
"name": "recipe_id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "ad3408cd773dd8f9308255ec2800171638a1aeda9817c57fb8360f97115f8e97"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into extra_items (user_id, name, amt, plan_date)\nvalues (?, ?, ?, ?)\non conflict (user_id, name, plan_date) do update set amt=excluded.amt",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "ba07658eb11f9d6cfdb5dbee4496b2573f1e51f4b4d9ae760eca3b977649b5c7"
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "select category_text from categories where user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "category_text",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c988364f9f83f4fa8bd0e594bab432ee7c9ec47ca40f4d16e5e2a8763653f377"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "delete from sessions",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d84685a82585c5e4ae72c86ba1fe6e4a7241c4c3c9e948213e5849d956132bad"
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "with latest_dates as (\n select user_id, max(date(plan_date)) as plan_date from plan_recipes\n where user_id = ?\n group by user_id\n)\n\nselect\n filtered_ingredients.name,\n filtered_ingredients.form,\n filtered_ingredients.measure_type\nfrom latest_dates\ninner join filtered_ingredients on\n latest_dates.user_id = filtered_ingredients.user_id\n and latest_dates.plan_date = filtered_ingredients.plan_date",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "form",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "measure_type",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "e38183e2e16afa308672044e5d314296d7cd84c1ffedcbfe790743547dc62de8"
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "insert into recipes (user_id, recipe_id, recipe_text, category, serving_count) values (?, ?, ?, ?, ?)\n on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text, category=excluded.category",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "eb99a37e18009e0dd46caccacea57ba0b25510d80a4e4a282a5ac2be50bba81c"
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "select recipe_id, recipe_text, category, serving_count from recipes where user_id = ? and recipe_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "recipe_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "recipe_text",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "category",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "serving_count",
|
||||
"ordinal": 3,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "ee0491c7d1a31ef80d7abe6ea4c9a8b0618dba58a0a8bceef7bdafec98ccd543"
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "select distinct plan_date as \"plan_date: NaiveDate\" from plan_table\nwhere user_id = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "plan_date: NaiveDate",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fd818a6b1c800c2014b5cfe8a923ac9228832b11d7575585cf7930fbf91306d1"
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kitchen"
|
||||
version = "0.2.25"
|
||||
version = "0.2.16"
|
||||
authors = ["Jeremy Wall <jeremy@marzhillstudios.com>"]
|
||||
edition = "2021"
|
||||
|
||||
@ -18,21 +18,12 @@ async-trait = "0.1.57"
|
||||
async-session = "3.0.0"
|
||||
ciborium = "0.2.0"
|
||||
tower = "0.4.13"
|
||||
cookie = "0.17.0"
|
||||
metrics = "0.20.1"
|
||||
metrics-exporter-prometheus = "0.11.0"
|
||||
futures = "0.3"
|
||||
metrics-process = "1.0.8"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4.22"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.204"
|
||||
serde = "1.0.144"
|
||||
cookie = "0.16.0"
|
||||
chrono = "0.4.22"
|
||||
|
||||
[dependencies.argon2]
|
||||
version = "0.5.0"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies.secrecy]
|
||||
version = "0.8.0"
|
||||
@ -67,5 +58,5 @@ version = "1.12.0"
|
||||
features = ["tokio1"]
|
||||
|
||||
[dependencies.sqlx]
|
||||
version = "0.7"
|
||||
features = ["sqlite", "runtime-async-std", "tls-rustls", "chrono"]
|
||||
version = "0.6.2"
|
||||
features = ["sqlite", "runtime-async-std-rustls", "offline", "chrono"]
|
@ -2,4 +2,4 @@
|
||||
fn main() {
|
||||
// trigger recompilation when a new migration is added
|
||||
println!("cargo:rerun-if-changed=migrations");
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
-- Add down migration script here
|
||||
alter table recipes drop column category;
|
@ -1,2 +0,0 @@
|
||||
-- Add up migration script here
|
||||
alter table recipes add column category TEXT;
|
@ -1,2 +0,0 @@
|
||||
-- Add down migration script here
|
||||
drop table plan_table;
|
@ -1,10 +0,0 @@
|
||||
-- Add up migration script here
|
||||
create temp table TEMP_plan_dates_deduped AS
|
||||
select distinct user_id, plan_date from plan_recipes;
|
||||
|
||||
create table plan_table (user_id TEXT NOT NULL, plan_date TEXT NOT NULL, primary key (user_id, plan_date) );
|
||||
|
||||
insert into plan_table
|
||||
select user_id, plan_date from TEMP_plan_dates_deduped;
|
||||
|
||||
drop table TEMP_plan_dates_deduped;
|
@ -1,2 +0,0 @@
|
||||
-- Add down migration script here
|
||||
ALTER TABLE recipes DROP COLUMN serving_count;
|
@ -1,2 +0,0 @@
|
||||
-- Add up migration script here
|
||||
ALTER TABLE recipes ADD COLUMN serving_count INT;
|
@ -1,14 +1,34 @@
|
||||
{
|
||||
"db": "SQLite",
|
||||
"05a9f963e3f18b8ceb787c33b6dbdac993f999ff32bb5155f2dff8dc18d840bf": {
|
||||
"04987493e4b13793a2dff75cc2710972bb28abf303275f5e6346470cdf5c2c17": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "form",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "measure_type",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "insert into recipes (user_id, recipe_id, recipe_text, category) values (?, ?, ?, ?)\n on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text, category=excluded.category"
|
||||
"query": "with latest_dates as (\n select\n user_id,\n name,\n form,\n measure_type,\n max(plan_date) as plan_date\n from filtered_ingredients\n where user_id = ?\n)\n\nselect\n filtered_ingredients.name,\n filtered_ingredients.form,\n filtered_ingredients.measure_type\nfrom latest_dates\ninner join filtered_ingredients on\n latest_dates.user_id = filtered_ingredients.user_id\n and latest_dates.name = filtered_ingredients.name\n and latest_dates.form = filtered_ingredients.form\n and latest_dates.measure_type = filtered_ingredients.measure_type\n and latest_dates.plan_date = filtered_ingredients.plan_date"
|
||||
},
|
||||
"104f07472670436d3eee1733578bbf0c92dc4f965d3d13f9bf4bfbc92958c5b6": {
|
||||
"describe": {
|
||||
@ -28,40 +48,6 @@
|
||||
},
|
||||
"query": "select password_hashed from users where id = ?"
|
||||
},
|
||||
"10de1e9950d7d3ae7f017b9175a1cee4ff7fcbc7403a39ea02930c75b4b9160a": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "amt",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "with latest_dates as (\n select user_id, max(date(plan_date)) as plan_date from plan_recipes\n where user_id = ?\n group by user_id\n)\n\nselect\n extra_items.name,\n extra_items.amt\nfrom latest_dates\ninner join extra_items on\n latest_dates.user_id = extra_items.user_id\n and latest_dates.plan_date = extra_items.plan_date"
|
||||
},
|
||||
"10e1c111a16d647a106a3147f4e61e34b0176860ca99cb62cb43dc72550ad990": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "delete from modified_amts where user_id = ? and plan_date = ?"
|
||||
},
|
||||
"160a9dfccf2e91a37d81f75eba21ec73105a7453c4f1fe76a430d04e525bc6cd": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -72,6 +58,30 @@
|
||||
},
|
||||
"query": "insert into filtered_ingredients(user_id, name, form, measure_type, plan_date)\n values (?, ?, ?, ?, date()) on conflict(user_id, name, form, measure_type, plan_date) DO NOTHING"
|
||||
},
|
||||
"196e289cbd65224293c4213552160a0cdf82f924ac597810fe05102e247b809d": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "recipe_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "recipe_text",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "select recipe_id, recipe_text from recipes where user_id = ? and recipe_id = ?"
|
||||
},
|
||||
"19832e3582c05ed49c676fde33cde64274379a83a8dd130f6eec96c1d7250909": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -112,56 +122,6 @@
|
||||
},
|
||||
"query": "insert into staples (user_id, content) values (?, ?)\n on conflict(user_id) do update set content = excluded.content"
|
||||
},
|
||||
"1b6fd91460bef61cf02f210404a4ca57b520c969d1f9613e7101ee6aa7a9962a": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
}
|
||||
},
|
||||
"query": "insert into modified_amts(user_id, name, form, measure_type, amt, plan_date)\n values (?, ?, ?, ?, ?, ?) on conflict (user_id, name, form, measure_type, plan_date) do update set amt=excluded.amt"
|
||||
},
|
||||
"1cc4412dfc3d4acdf257e839b50d6c9abbb6e74e7af606fd12da20f0aedde3de": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "recipe_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "recipe_text",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "category",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "select recipe_id, recipe_text, category from recipes where user_id = ? and recipe_id = ?"
|
||||
},
|
||||
"23beb05e40cf011170182d4e98cdf1faa3d8df6e5956e471245e666f32e56962": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "delete from filtered_ingredients where user_id = ? and plan_date = ?"
|
||||
},
|
||||
"2582522f8ca9f12eccc70a3b339d9030aee0f52e62d6674cfd3862de2a68a177": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -172,56 +132,6 @@
|
||||
},
|
||||
"query": "insert into category_mappings\n (user_id, ingredient_name, category_name)\n values (?, ?, ?)\n on conflict (user_id, ingredient_name)\n do update set category_name=excluded.category_name\n"
|
||||
},
|
||||
"27aa0a21f534cdf580841fa111136fc26cf1a0ca4ddb308c12f3f8f5a62d6178": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "delete from plan_table where user_id = ? and plan_date = ?"
|
||||
},
|
||||
"288535e7b9e1f02ad1b677e3dddc85f38c0766ce16d26fc1bdd2bf90ab9a7f7c": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "insert into plan_table (user_id, plan_date) values (?, ?)\n on conflict (user_id, plan_date) do nothing;"
|
||||
},
|
||||
"2e076acd2405d234daaa866e5a2ac1e10989fc8d2820f90aa722464a7b17db6b": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "plan_date: NaiveDate",
|
||||
"ordinal": 0,
|
||||
"type_info": "Date"
|
||||
},
|
||||
{
|
||||
"name": "recipe_id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int64"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "select plan_date as \"plan_date: NaiveDate\", recipe_id, count\n from plan_recipes\nwhere\n user_id = ?\n and plan_date = ?"
|
||||
},
|
||||
"37f382be1b53efd2f79a0d59ae6a8717f88a86908a7a4128d5ed7339147ca59d": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -256,7 +166,17 @@
|
||||
},
|
||||
"query": "insert into extra_items (user_id, name, plan_date, amt)\nvalues (?, ?, date(), ?)\non conflict (user_id, name, plan_date) do update set amt=excluded.amt"
|
||||
},
|
||||
"3e43f06f5c2e959f66587c8d74696d6db27d89fd2f7d7e1ed6fa5016b4bd1a91": {
|
||||
"3fd4017569dca4fe73e97e0e2bd612027a8c1b17b0b10faabd6459f56ca1c0bb": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
}
|
||||
},
|
||||
"query": "insert into recipes (user_id, recipe_id, recipe_text) values (?, ?, ?)\n on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text"
|
||||
},
|
||||
"406aac6ac2b0084c31c29adec6fa2fb9bb925d92121305c8afbac009caf1ecc0": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -290,71 +210,7 @@
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "with latest_dates as (\n select user_id, max(date(plan_date)) as plan_date from plan_recipes\n where user_id = ?\n group by user_id\n)\n\nselect\n modified_amts.name,\n modified_amts.form,\n modified_amts.measure_type,\n modified_amts.amt\nfrom latest_dates\ninner join modified_amts on\n latest_dates.user_id = modified_amts.user_id\n and latest_dates.plan_date = modified_amts.plan_date"
|
||||
},
|
||||
"40c589d8cb88d7ed723c8651833fe8541756ef0c57bf6296a4dfbda7d504dca8": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "recipe_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "recipe_text",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "category",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "select recipe_id, recipe_text, category from recipes where user_id = ?"
|
||||
},
|
||||
"4237ff804f254c122a36a14135b90434c6576f48d3a83245503d702552ea9f30": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "amt",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "select\n name,\n amt\nfrom extra_items\nwhere\n user_id = ?\n and plan_date = ?"
|
||||
},
|
||||
"5883c4a57def93cca45f8f9d81c8bba849547758217cd250e7ab28cc166ab42b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
}
|
||||
},
|
||||
"query": "insert into filtered_ingredients(user_id, name, form, measure_type, plan_date)\n values (?, ?, ?, ?, ?) on conflict(user_id, name, form, measure_type, plan_date) DO NOTHING"
|
||||
"query": "with latest_dates as (\n select\n user_id,\n name,\n form,\n measure_type,\n amt,\n max(plan_date) as plan_date\n from modified_amts\n where user_id = ?\n)\n\nselect\n modified_amts.name,\n modified_amts.form,\n modified_amts.measure_type,\n modified_amts.amt\nfrom latest_dates\ninner join modified_amts on\n latest_dates.user_id = modified_amts.user_id\n and latest_dates.name = modified_amts.name\n and latest_dates.form = modified_amts.form\n and latest_dates.amt = modified_amts.amt\n and latest_dates.plan_date = modified_amts.plan_date"
|
||||
},
|
||||
"5d743897fb0d8fd54c3708f1b1c6e416346201faa9e28823c1ba5a421472b1fa": {
|
||||
"describe": {
|
||||
@ -384,42 +240,6 @@
|
||||
},
|
||||
"query": "select content from staples where user_id = ?"
|
||||
},
|
||||
"699ff0f0d4d4c6e26a21c1922a5b5249d89ed1677680a2276899a7f8b26344ee": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "form",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "measure_type",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "amt",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "select\n modified_amts.name,\n modified_amts.form,\n modified_amts.measure_type,\n modified_amts.amt\nfrom modified_amts\nwhere\n user_id = ?\n and plan_date = ?"
|
||||
},
|
||||
"6c43908d90f229b32ed8b1b076be9b452a995e1b42ba2554e947c515b031831a": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -440,16 +260,6 @@
|
||||
},
|
||||
"query": "insert into modified_amts(user_id, name, form, measure_type, amt, plan_date)\n values (?, ?, ?, ?, ?, date()) on conflict (user_id, name, form, measure_type, plan_date) do update set amt=excluded.amt"
|
||||
},
|
||||
"6f11d90875a6230766a5f9bd1d67665dc4d00c13d7e81b0d18d60baa67987da9": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "delete from extra_items where user_id = ? and plan_date = ?"
|
||||
},
|
||||
"7578157607967a6a4c60f12408c5d9900d15b429a49681a4cae4e02d31c524ec": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -460,36 +270,6 @@
|
||||
},
|
||||
"query": "delete from sessions where id = ?"
|
||||
},
|
||||
"7695a0602395006f9b76ecd4d0cb5ecd5dee419b71b3b0b9ea4f47a83f3df41a": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "form",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "measure_type",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "select\n filtered_ingredients.name,\n filtered_ingredients.form,\n filtered_ingredients.measure_type\nfrom filtered_ingredients\nwhere\n user_id = ?\n and plan_date = ?"
|
||||
},
|
||||
"83824ea638cb64c524f5c8984ef6ef28dfe781f0abf168abc4ae9a51e6e0ae88": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@ -528,15 +308,29 @@
|
||||
},
|
||||
"query": "select session_value from sessions where id = ?"
|
||||
},
|
||||
"93af0c367a0913d49c92aa69022fa30fc0564bd4dbab7f3ae78673a01439cd6e": {
|
||||
"95fbc362a2e17add05218a2dac431275b5cc55bd7ac8f4173ee10afefceafa3b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"columns": [
|
||||
{
|
||||
"name": "recipe_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "recipe_text",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
true
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "delete from plan_recipes where user_id = ? and plan_date = ?"
|
||||
"query": "select recipe_id, recipe_text from recipes where user_id = ?"
|
||||
},
|
||||
"9ad4acd9b9d32c9f9f441276aa71a17674fe4d65698848044778bd4aef77d42d": {
|
||||
"describe": {
|
||||
@ -578,16 +372,6 @@
|
||||
},
|
||||
"query": "with max_date as (\n select user_id, max(date(plan_date)) as plan_date from plan_recipes group by user_id\n)\n\nselect plan_recipes.plan_date as \"plan_date: NaiveDate\", plan_recipes.recipe_id, plan_recipes.count\n from plan_recipes\n inner join max_date on plan_recipes.user_id = max_date.user_id\nwhere\n plan_recipes.user_id = ?\n and plan_recipes.plan_date = max_date.plan_date"
|
||||
},
|
||||
"ba07658eb11f9d6cfdb5dbee4496b2573f1e51f4b4d9ae760eca3b977649b5c7": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
}
|
||||
},
|
||||
"query": "insert into extra_items (user_id, name, amt, plan_date)\nvalues (?, ?, ?, ?)\non conflict (user_id, name, plan_date) do update set amt=excluded.amt"
|
||||
},
|
||||
"c988364f9f83f4fa8bd0e594bab432ee7c9ec47ca40f4d16e5e2a8763653f377": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@ -616,7 +400,7 @@
|
||||
},
|
||||
"query": "delete from sessions"
|
||||
},
|
||||
"e38183e2e16afa308672044e5d314296d7cd84c1ffedcbfe790743547dc62de8": {
|
||||
"f34ec23c5cc8f61f92464ecf68620150a8d4521b68b5099a0a7dac3328651880": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@ -625,18 +409,12 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "form",
|
||||
"name": "amt",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "measure_type",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
@ -644,24 +422,6 @@
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "with latest_dates as (\n select user_id, max(date(plan_date)) as plan_date from plan_recipes\n where user_id = ?\n group by user_id\n)\n\nselect\n filtered_ingredients.name,\n filtered_ingredients.form,\n filtered_ingredients.measure_type\nfrom latest_dates\ninner join filtered_ingredients on\n latest_dates.user_id = filtered_ingredients.user_id\n and latest_dates.plan_date = filtered_ingredients.plan_date"
|
||||
},
|
||||
"fd818a6b1c800c2014b5cfe8a923ac9228832b11d7575585cf7930fbf91306d1": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "plan_date: NaiveDate",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "select distinct plan_date as \"plan_date: NaiveDate\" from plan_table\nwhere user_id = ?"
|
||||
"query": "with latest_dates as (\n select\n user_id,\n name,\n max(plan_date) as plan_date\n from extra_items\n where user_id = ?\n group by user_id, name\n)\n\nselect\n extra_items.name,\n extra_items.amt\nfrom latest_dates\ninner join extra_items on\n latest_dates.user_id = extra_items.user_id\n and latest_dates.name = extra_items.name\n and latest_dates.plan_date= extra_items.plan_date"
|
||||
}
|
||||
}
|
@ -38,13 +38,13 @@ fn create_app<'a>() -> clap::App<'a> {
|
||||
)
|
||||
(@subcommand groceries =>
|
||||
(about: "print out a grocery list for a set of recipes")
|
||||
(@arg csv: --csv "output ingredients as csv")
|
||||
(@arg csv: --csv "output ingredeints as csv")
|
||||
(@arg INPUT: +required "Input menu file to parse. One recipe file per line.")
|
||||
)
|
||||
(@subcommand serve =>
|
||||
(about: "Serve the interface via the web")
|
||||
(@arg recipe_dir: -d --dir +takes_value "Directory containing recipe files to use")
|
||||
(@arg session_dir: --session_dir +takes_value +required "Session store directory to use")
|
||||
(@arg session_dir: --session_dir +takes_value "Session store directory to use")
|
||||
(@arg tls: --tls "Use TLS to serve.")
|
||||
(@arg cert_path: --cert +takes_value "Certificate path. Required if you specified --tls.")
|
||||
(@arg key_path: --cert_key +takes_value "Certificate key path. Required if you specified --tls")
|
||||
@ -55,7 +55,7 @@ fn create_app<'a>() -> clap::App<'a> {
|
||||
(@arg recipe_dir: -d --dir +takes_value "Directory containing recipe files to load for user")
|
||||
(@arg user: -u --user +takes_value +required "username to add")
|
||||
(@arg pass: -p --pass +takes_value +required "password to add for this user")
|
||||
(@arg session_dir: --session_dir +takes_value +required "Session store directory to use")
|
||||
(@arg session_dir: --session_dir +takes_value "Session store directory to use")
|
||||
)
|
||||
)
|
||||
.setting(clap::AppSettings::SubcommandRequiredElseHelp)
|
||||
@ -65,10 +65,9 @@ fn get_session_store_path(matches: &ArgMatches) -> PathBuf {
|
||||
if let Some(dir) = matches.value_of("session_dir") {
|
||||
PathBuf::from(dir)
|
||||
} else {
|
||||
let mut dir = std::env::var("HOME")
|
||||
.map(PathBuf::from)
|
||||
.expect("Unable to get user home directory. Bailing out.");
|
||||
dir.push(".kitchen");
|
||||
let mut dir =
|
||||
std::env::current_dir().expect("Unable to get current directory. Bailing out.");
|
||||
dir.push(".session_store");
|
||||
dir
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,6 @@ pub async fn handler(
|
||||
.domain(domain)
|
||||
.secure(true)
|
||||
.path("/")
|
||||
.permanent()
|
||||
.finish();
|
||||
let parsed_cookie = match cookie.to_string().parse() {
|
||||
Err(err) => {
|
||||
|
@ -1,178 +0,0 @@
|
||||
// Copyright 2023 Jeremy Wall (Jeremy@marzhilsltudios.com)
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//! A [metrics] powered [TraceLayer] that works with any [Tower](https://crates.io/crates/tower) middleware.
|
||||
use axum::http::{Request, Response};
|
||||
use metrics::{histogram, increment_counter, Label};
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
use tower_http::{
|
||||
classify::{ServerErrorsAsFailures, SharedClassifier},
|
||||
trace::{
|
||||
DefaultMakeSpan, DefaultOnEos, OnBodyChunk, OnFailure, OnRequest, OnResponse, TraceLayer,
|
||||
},
|
||||
};
|
||||
use tracing;
|
||||
|
||||
/// A Metrics Trace Layer using a [MetricsRecorder].
|
||||
///
|
||||
/// The layer will record 4 different metrics:
|
||||
///
|
||||
/// * http_request_counter
|
||||
/// * http_request_failure_counter
|
||||
/// * http_request_size_bytes_hist
|
||||
/// * http_request_request_time_micros_hist
|
||||
///
|
||||
/// Each of the metrics are labled by host, method, and path
|
||||
pub type MetricsTraceLayer<B, F> = TraceLayer<
|
||||
SharedClassifier<ServerErrorsAsFailures>,
|
||||
DefaultMakeSpan,
|
||||
MetricsRecorder<B, F>,
|
||||
MetricsRecorder<B, F>,
|
||||
MetricsRecorder<B, F>,
|
||||
DefaultOnEos,
|
||||
MetricsRecorder<B, F>,
|
||||
>;
|
||||
|
||||
/// Holds the state required for recording metrics on a given request.
|
||||
pub struct MetricsRecorder<B, F>
|
||||
where
|
||||
F: Fn(&B) -> u64,
|
||||
{
|
||||
labels: Arc<Mutex<Vec<Label>>>,
|
||||
size: Arc<AtomicU64>,
|
||||
chunk_len: Arc<F>,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<B, F> Clone for MetricsRecorder<B, F>
|
||||
where
|
||||
F: Fn(&B) -> u64,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
labels: self.labels.clone(),
|
||||
size: self.size.clone(),
|
||||
chunk_len: self.chunk_len.clone(),
|
||||
_phantom: self._phantom.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, F> MetricsRecorder<B, F>
|
||||
where
|
||||
F: Fn(&B) -> u64,
|
||||
{
|
||||
/// Construct a new [MetricsRecorder] using the installed [Recorder].
|
||||
pub fn new(f: F) -> Self {
|
||||
Self {
|
||||
labels: Arc::new(Mutex::new(Vec::new())),
|
||||
size: Arc::new(AtomicU64::new(0)),
|
||||
chunk_len: Arc::new(f),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, F> OnBodyChunk<B> for MetricsRecorder<B, F>
|
||||
where
|
||||
F: Fn(&B) -> u64,
|
||||
{
|
||||
fn on_body_chunk(&mut self, chunk: &B, _latency: std::time::Duration, _span: &tracing::Span) {
|
||||
let _ = self
|
||||
.size
|
||||
.fetch_add(self.chunk_len.as_ref()(chunk), Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, FailureClass, F> OnFailure<FailureClass> for MetricsRecorder<B, F>
|
||||
where
|
||||
F: Fn(&B) -> u64,
|
||||
{
|
||||
fn on_failure(
|
||||
&mut self,
|
||||
_failure_classification: FailureClass,
|
||||
_latency: std::time::Duration,
|
||||
_span: &tracing::Span,
|
||||
) {
|
||||
let labels = self.labels.lock().expect("Failed to unlock labels").clone();
|
||||
increment_counter!("http_request_failure_counter", labels);
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, RB, F> OnResponse<RB> for MetricsRecorder<B, F>
|
||||
where
|
||||
F: Fn(&B) -> u64,
|
||||
{
|
||||
fn on_response(
|
||||
self,
|
||||
_response: &Response<RB>,
|
||||
latency: std::time::Duration,
|
||||
_span: &tracing::Span,
|
||||
) {
|
||||
let labels = self.labels.lock().expect("Failed to unlock labels").clone();
|
||||
histogram!(
|
||||
"http_request_time_micros_hist",
|
||||
latency.as_micros() as f64,
|
||||
labels.clone()
|
||||
);
|
||||
histogram!(
|
||||
"http_request_size_bytes_hist",
|
||||
self.size.as_ref().load(Ordering::SeqCst) as f64,
|
||||
labels
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn make_request_lables(path: String, host: String, method: String) -> Vec<Label> {
|
||||
vec![
|
||||
Label::new("path", path),
|
||||
Label::new("host", host),
|
||||
Label::new("method", method),
|
||||
]
|
||||
}
|
||||
|
||||
impl<B, RB, F> OnRequest<RB> for MetricsRecorder<B, F>
|
||||
where
|
||||
F: Fn(&B) -> u64,
|
||||
{
|
||||
fn on_request(&mut self, request: &Request<RB>, _span: &tracing::Span) {
|
||||
let path = request.uri().path().to_lowercase();
|
||||
let host = request.uri().host().unwrap_or("").to_lowercase();
|
||||
let method = request.method().to_string();
|
||||
|
||||
let labels = make_request_lables(path, host, method);
|
||||
let mut labels_lock = self.labels.lock().expect("Failed to unlock labels");
|
||||
(*labels_lock.as_mut()) = labels.clone();
|
||||
increment_counter!("http_request_counter", labels);
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a [TraceLayer] that will use an installed [metrics::Recorder] to record metrics per request.
|
||||
pub fn make_layer<B, F>(f: F) -> MetricsTraceLayer<B, F>
|
||||
where
|
||||
F: Fn(&B) -> u64,
|
||||
{
|
||||
let metrics_recorder = MetricsRecorder::new(f);
|
||||
let layer = TraceLayer::new_for_http()
|
||||
.on_body_chunk(metrics_recorder.clone())
|
||||
.on_request(metrics_recorder.clone())
|
||||
.on_response(metrics_recorder.clone())
|
||||
.on_failure(metrics_recorder.clone());
|
||||
layer
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
// Copyright 2022 Jeremy Wall
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -11,7 +12,6 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::{collections::BTreeSet, net::SocketAddr};
|
||||
@ -23,19 +23,17 @@ use axum::{
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::{get, Router},
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use client_api as api;
|
||||
use metrics_process::Collector;
|
||||
use mime_guess;
|
||||
use recipes::{IngredientKey, RecipeEntry};
|
||||
use rust_embed::RustEmbed;
|
||||
use storage::{APIStore, AuthStore};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{debug, info, instrument};
|
||||
use tracing::{debug, error, info, instrument};
|
||||
|
||||
use client_api as api;
|
||||
use storage::{APIStore, AuthStore};
|
||||
|
||||
mod auth;
|
||||
mod metrics;
|
||||
mod storage;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
@ -88,13 +86,17 @@ async fn api_recipe_entry(
|
||||
Path(recipe_id): Path<String>,
|
||||
) -> api::Response<Option<RecipeEntry>> {
|
||||
use storage::{UserId, UserIdFromSession::*};
|
||||
match session {
|
||||
NoUserId => store.get_recipe_entry(recipe_id).await.into(),
|
||||
let result = match session {
|
||||
NoUserId => store
|
||||
.get_recipe_entry(recipe_id)
|
||||
.await
|
||||
.map_err(|e| format!("Error: {:?}", e)),
|
||||
FoundUserId(UserId(id)) => app_store
|
||||
.get_recipe_entry_for_user(id, recipe_id)
|
||||
.await
|
||||
.into(),
|
||||
}
|
||||
.map_err(|e| format!("Error: {:?}", e)),
|
||||
};
|
||||
result.into()
|
||||
}
|
||||
|
||||
async fn api_recipe_delete(
|
||||
@ -103,13 +105,21 @@ async fn api_recipe_delete(
|
||||
Path(recipe_id): Path<String>,
|
||||
) -> api::EmptyResponse {
|
||||
use storage::{UserId, UserIdFromSession::*};
|
||||
match session {
|
||||
let result = match session {
|
||||
NoUserId => api::EmptyResponse::Unauthorized,
|
||||
FoundUserId(UserId(id)) => app_store
|
||||
.delete_recipes_for_user(&id, &vec![recipe_id])
|
||||
.await
|
||||
.into(),
|
||||
}
|
||||
FoundUserId(UserId(id)) => {
|
||||
if let Err(e) = app_store
|
||||
.delete_recipes_for_user(&id, &vec![recipe_id])
|
||||
.await
|
||||
.map_err(|e| format!("Error: {:?}", e))
|
||||
{
|
||||
api::EmptyResponse::error(StatusCode::INTERNAL_SERVER_ERROR.as_u16(), e)
|
||||
} else {
|
||||
api::EmptyResponse::success(())
|
||||
}
|
||||
}
|
||||
};
|
||||
result.into()
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
@ -120,10 +130,17 @@ async fn api_recipes(
|
||||
) -> api::RecipeEntryResponse {
|
||||
// Select recipes based on the user-id if it exists or serve the default if it does not.
|
||||
use storage::{UserId, UserIdFromSession::*};
|
||||
match session {
|
||||
NoUserId => api::RecipeEntryResponse::from(store.get_recipes().await),
|
||||
FoundUserId(UserId(id)) => app_store.get_recipes_for_user(id.as_str()).await.into(),
|
||||
}
|
||||
let result = match session {
|
||||
NoUserId => store
|
||||
.get_recipes()
|
||||
.await
|
||||
.map_err(|e| format!("Error: {:?}", e)),
|
||||
FoundUserId(UserId(id)) => app_store
|
||||
.get_recipes_for_user(id.as_str())
|
||||
.await
|
||||
.map_err(|e| format!("Error: {:?}", e)),
|
||||
};
|
||||
result.into()
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
@ -134,10 +151,14 @@ async fn api_category_mappings(
|
||||
use storage::UserIdFromSession::*;
|
||||
match session {
|
||||
NoUserId => api::Response::Unauthorized,
|
||||
FoundUserId(user_id) => app_store
|
||||
.get_category_mappings_for_user(&user_id.0)
|
||||
.await
|
||||
.into(),
|
||||
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),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,25 +192,38 @@ async fn api_categories(
|
||||
) -> api::Response<String> {
|
||||
// Select Categories based on the user-id if it exists or serve the default if it does not.
|
||||
use storage::{UserId, UserIdFromSession::*};
|
||||
match session {
|
||||
NoUserId => store.get_categories().await.into(),
|
||||
FoundUserId(UserId(id)) => app_store.get_categories_for_user(id.as_str()).await.into(),
|
||||
}
|
||||
let categories_result = match session {
|
||||
NoUserId => store
|
||||
.get_categories()
|
||||
.await
|
||||
.map_err(|e| format!("Error: {:?}", e)),
|
||||
FoundUserId(UserId(id)) => app_store
|
||||
.get_categories_for_user(id.as_str())
|
||||
.await
|
||||
.map_err(|e| format!("Error: {:?}", e)),
|
||||
};
|
||||
categories_result.into()
|
||||
}
|
||||
|
||||
async fn api_save_categories(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
Json(categories): Json<String>,
|
||||
) -> api::EmptyResponse {
|
||||
) -> api::CategoryResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
app_store
|
||||
if let Err(e) = app_store
|
||||
.store_categories_for_user(id.as_str(), categories.as_str())
|
||||
.await
|
||||
.into()
|
||||
{
|
||||
return api::Response::error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
format!("{:?}", e),
|
||||
);
|
||||
}
|
||||
api::CategoryResponse::success("Successfully saved categories".into())
|
||||
} else {
|
||||
api::EmptyResponse::Unauthorized
|
||||
api::CategoryResponse::Unauthorized
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,35 +234,26 @@ async fn api_save_recipes(
|
||||
) -> api::EmptyResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
app_store
|
||||
let result = app_store
|
||||
.store_recipes_for_user(id.as_str(), &recipes)
|
||||
.await
|
||||
.into()
|
||||
.await;
|
||||
result.map_err(|e| format!("Error: {:?}", e)).into()
|
||||
} else {
|
||||
api::EmptyResponse::Unauthorized
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_plan_for_date(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
Path(date): Path<chrono::NaiveDate>,
|
||||
) -> api::PlanDataResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
app_store.fetch_meal_plan_for_date(&id, date).await.into()
|
||||
} else {
|
||||
api::Response::Unauthorized
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_plan(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
) -> api::PlanDataResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
app_store.fetch_latest_meal_plan(&id).await.into()
|
||||
app_store
|
||||
.fetch_latest_meal_plan(&id)
|
||||
.await
|
||||
.map_err(|e| format!("Error: {:?}", e))
|
||||
.into()
|
||||
} else {
|
||||
api::Response::Unauthorized
|
||||
}
|
||||
@ -241,57 +266,16 @@ async fn api_plan_since(
|
||||
) -> api::PlanHistoryResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
app_store.fetch_meal_plans_since(&id, date).await.into()
|
||||
app_store
|
||||
.fetch_meal_plans_since(&id, date)
|
||||
.await
|
||||
.map_err(|e| format!("Error: {:?}", e))
|
||||
.into()
|
||||
} else {
|
||||
api::PlanHistoryResponse::Unauthorized
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_all_plans(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
) -> api::Response<Vec<NaiveDate>> {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
app_store.fetch_all_meal_plans(&id).await.into()
|
||||
} else {
|
||||
api::Response::Unauthorized
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_delete_plan_for_date(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
Path(date): Path<chrono::NaiveDate>,
|
||||
) -> api::EmptyResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
app_store
|
||||
.delete_meal_plan_for_date(id.as_str(), date)
|
||||
.await
|
||||
.into()
|
||||
} else {
|
||||
api::EmptyResponse::Unauthorized
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_save_plan_for_date(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
Path(date): Path<chrono::NaiveDate>,
|
||||
Json(meal_plan): Json<Vec<(String, i32)>>,
|
||||
) -> api::EmptyResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
app_store
|
||||
.save_meal_plan(id.as_str(), &meal_plan, date)
|
||||
.await
|
||||
.into()
|
||||
} else {
|
||||
api::EmptyResponse::Unauthorized
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_save_plan(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
@ -302,6 +286,7 @@ async fn api_save_plan(
|
||||
app_store
|
||||
.save_meal_plan(id.as_str(), &meal_plan, chrono::Local::now().date_naive())
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
.into()
|
||||
} else {
|
||||
api::EmptyResponse::Unauthorized
|
||||
@ -317,26 +302,7 @@ async fn api_inventory_v2(
|
||||
app_store
|
||||
.fetch_latest_inventory_data(id)
|
||||
.await
|
||||
.map(|d| {
|
||||
let data: api::InventoryData = d.into();
|
||||
data
|
||||
})
|
||||
.into()
|
||||
} else {
|
||||
api::Response::Unauthorized
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_inventory_for_date(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
Path(date): Path<chrono::NaiveDate>,
|
||||
) -> api::InventoryResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
app_store
|
||||
.fetch_inventory_for_date(id, date)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
.map(|d| {
|
||||
let data: api::InventoryData = d.into();
|
||||
data
|
||||
@ -356,6 +322,7 @@ async fn api_inventory(
|
||||
app_store
|
||||
.fetch_latest_inventory_data(id)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
.map(|(filtered, modified, _)| (filtered, modified))
|
||||
.into()
|
||||
} else {
|
||||
@ -363,35 +330,6 @@ async fn api_inventory(
|
||||
}
|
||||
}
|
||||
|
||||
async fn api_save_inventory_for_date(
|
||||
Extension(app_store): Extension<Arc<storage::SqliteStore>>,
|
||||
session: storage::UserIdFromSession,
|
||||
Path(date): Path<NaiveDate>,
|
||||
Json((filtered_ingredients, modified_amts, extra_items)): Json<(
|
||||
Vec<IngredientKey>,
|
||||
Vec<(IngredientKey, String)>,
|
||||
Vec<(String, String)>,
|
||||
)>,
|
||||
) -> api::EmptyResponse {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(id)) = session {
|
||||
let filtered_ingredients = filtered_ingredients.into_iter().collect();
|
||||
let modified_amts = modified_amts.into_iter().collect();
|
||||
app_store
|
||||
.save_inventory_data_for_date(
|
||||
id,
|
||||
&date,
|
||||
filtered_ingredients,
|
||||
modified_amts,
|
||||
extra_items,
|
||||
)
|
||||
.await
|
||||
.into()
|
||||
} else {
|
||||
api::EmptyResponse::Unauthorized
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_inventory_data(
|
||||
app_store: Arc<storage::SqliteStore>,
|
||||
id: String,
|
||||
@ -402,6 +340,7 @@ async fn save_inventory_data(
|
||||
app_store
|
||||
.save_inventory_data(id, filtered_ingredients, modified_amts, extra_items)
|
||||
.await
|
||||
.map_err(|e| format!("{:?}", e))
|
||||
.into()
|
||||
}
|
||||
|
||||
@ -473,7 +412,16 @@ async fn api_staples(
|
||||
) -> api::Response<Option<String>> {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(user_id)) = session {
|
||||
app_store.fetch_staples(user_id).await.into()
|
||||
match app_store.fetch_staples(user_id).await {
|
||||
Ok(staples) => api::Response::success(staples),
|
||||
Err(err) => {
|
||||
error!(?err);
|
||||
api::Response::error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
format!("{:?}", err),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
api::Response::Unauthorized
|
||||
}
|
||||
@ -486,7 +434,16 @@ async fn api_save_staples(
|
||||
) -> api::Response<()> {
|
||||
use storage::{UserId, UserIdFromSession::FoundUserId};
|
||||
if let FoundUserId(UserId(user_id)) = session {
|
||||
app_store.save_staples(user_id, content).await.into()
|
||||
match app_store.save_staples(user_id, content).await {
|
||||
Ok(_) => api::EmptyResponse::success(()),
|
||||
Err(err) => {
|
||||
error!(?err);
|
||||
api::EmptyResponse::error(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
format!("{:?}", err),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
api::EmptyResponse::Unauthorized
|
||||
}
|
||||
@ -517,22 +474,11 @@ fn mk_v2_routes() -> Router {
|
||||
)
|
||||
// mealplan api path routes
|
||||
.route("/plan", get(api_plan).post(api_save_plan))
|
||||
.route("/plan/since/:date", get(api_plan_since))
|
||||
.route(
|
||||
"/plan/at/:date",
|
||||
get(api_plan_for_date)
|
||||
.post(api_save_plan_for_date)
|
||||
.delete(api_delete_plan_for_date),
|
||||
)
|
||||
.route("/plan/all", get(api_all_plans))
|
||||
.route("/plan/:date", get(api_plan_since))
|
||||
.route(
|
||||
"/inventory",
|
||||
get(api_inventory_v2).post(api_save_inventory_v2),
|
||||
)
|
||||
.route(
|
||||
"/inventory/at/:date",
|
||||
get(api_inventory_for_date).post(api_save_inventory_for_date),
|
||||
)
|
||||
// TODO(jwall): This is now deprecated but will still work
|
||||
.route("/categories", get(api_categories).post(api_save_categories))
|
||||
.route(
|
||||
@ -547,13 +493,6 @@ fn mk_v2_routes() -> Router {
|
||||
|
||||
#[instrument(fields(recipe_dir=?recipe_dir_path), skip_all)]
|
||||
pub async fn make_router(recipe_dir_path: PathBuf, store_path: PathBuf) -> Router {
|
||||
let handle = metrics_exporter_prometheus::PrometheusBuilder::new()
|
||||
.install_recorder()
|
||||
.expect("Failed to install Prometheus Recorder");
|
||||
// Setup the prometheus process metrics.
|
||||
let collector = Collector::default();
|
||||
collector.describe();
|
||||
let metrics_trace_layer = metrics::make_layer(|b: &axum::body::Bytes| b.len() as u64);
|
||||
let store = Arc::new(storage::file_store::AsyncFileStore::new(
|
||||
recipe_dir_path.clone(),
|
||||
));
|
||||
@ -578,21 +517,13 @@ pub async fn make_router(recipe_dir_path: PathBuf, store_path: PathBuf) -> Route
|
||||
.nest("/v1", mk_v1_routes())
|
||||
.nest("/v2", mk_v2_routes()),
|
||||
)
|
||||
.route(
|
||||
"/metrics/prometheus",
|
||||
get(|| async move {
|
||||
collector.collect();
|
||||
handle.render()
|
||||
}),
|
||||
)
|
||||
// NOTE(jwall): Note that this layer is applied to the preceding routes not
|
||||
// NOTE(jwall): Note that the layers are applied to the preceding routes not
|
||||
// the following routes.
|
||||
.layer(
|
||||
// NOTE(jwall): However service builder will apply these layers from top
|
||||
// NOTE(jwall): However service builder will apply the layers from top
|
||||
// to bottom.
|
||||
ServiceBuilder::new()
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(metrics_trace_layer)
|
||||
.layer(Extension(store))
|
||||
.layer(Extension(app_store)),
|
||||
)
|
||||
|
@ -1,2 +0,0 @@
|
||||
select distinct plan_date as "plan_date: NaiveDate" from plan_table
|
||||
where user_id = ?
|
@ -1,7 +1,11 @@
|
||||
with latest_dates as (
|
||||
select user_id, max(date(plan_date)) as plan_date from plan_recipes
|
||||
select
|
||||
user_id,
|
||||
name,
|
||||
max(plan_date) as plan_date
|
||||
from extra_items
|
||||
where user_id = ?
|
||||
group by user_id
|
||||
group by user_id, name
|
||||
)
|
||||
|
||||
select
|
||||
@ -10,4 +14,5 @@ select
|
||||
from latest_dates
|
||||
inner join extra_items on
|
||||
latest_dates.user_id = extra_items.user_id
|
||||
and latest_dates.plan_date = extra_items.plan_date
|
||||
and latest_dates.name = extra_items.name
|
||||
and latest_dates.plan_date= extra_items.plan_date
|
@ -1,8 +0,0 @@
|
||||
select
|
||||
filtered_ingredients.name,
|
||||
filtered_ingredients.form,
|
||||
filtered_ingredients.measure_type
|
||||
from filtered_ingredients
|
||||
where
|
||||
user_id = ?
|
||||
and plan_date = ?
|
@ -1,7 +1,12 @@
|
||||
with latest_dates as (
|
||||
select user_id, max(date(plan_date)) as plan_date from plan_recipes
|
||||
select
|
||||
user_id,
|
||||
name,
|
||||
form,
|
||||
measure_type,
|
||||
max(plan_date) as plan_date
|
||||
from filtered_ingredients
|
||||
where user_id = ?
|
||||
group by user_id
|
||||
)
|
||||
|
||||
select
|
||||
@ -11,4 +16,7 @@ select
|
||||
from latest_dates
|
||||
inner join filtered_ingredients on
|
||||
latest_dates.user_id = filtered_ingredients.user_id
|
||||
and latest_dates.name = filtered_ingredients.name
|
||||
and latest_dates.form = filtered_ingredients.form
|
||||
and latest_dates.measure_type = filtered_ingredients.measure_type
|
||||
and latest_dates.plan_date = filtered_ingredients.plan_date
|
@ -1,7 +1,13 @@
|
||||
with latest_dates as (
|
||||
select user_id, max(date(plan_date)) as plan_date from plan_recipes
|
||||
select
|
||||
user_id,
|
||||
name,
|
||||
form,
|
||||
measure_type,
|
||||
amt,
|
||||
max(plan_date) as plan_date
|
||||
from modified_amts
|
||||
where user_id = ?
|
||||
group by user_id
|
||||
)
|
||||
|
||||
select
|
||||
@ -12,4 +18,7 @@ select
|
||||
from latest_dates
|
||||
inner join modified_amts on
|
||||
latest_dates.user_id = modified_amts.user_id
|
||||
and latest_dates.name = modified_amts.name
|
||||
and latest_dates.form = modified_amts.form
|
||||
and latest_dates.amt = modified_amts.amt
|
||||
and latest_dates.plan_date = modified_amts.plan_date
|
@ -1,9 +0,0 @@
|
||||
select
|
||||
modified_amts.name,
|
||||
modified_amts.form,
|
||||
modified_amts.measure_type,
|
||||
modified_amts.amt
|
||||
from modified_amts
|
||||
where
|
||||
user_id = ?
|
||||
and plan_date = ?
|
@ -1,5 +0,0 @@
|
||||
select plan_date as "plan_date: NaiveDate", recipe_id, count
|
||||
from plan_recipes
|
||||
where
|
||||
user_id = ?
|
||||
and plan_date = ?
|
@ -22,7 +22,6 @@ use tracing::{debug, instrument};
|
||||
|
||||
use super::RecipeEntry;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct Error(String);
|
||||
|
||||
@ -99,7 +98,7 @@ impl AsyncFileStore {
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
debug!("adding recipe file {}", file_name);
|
||||
let recipe_contents = read_to_string(entry.path()).await?;
|
||||
entry_vec.push(RecipeEntry::new(file_name, recipe_contents));
|
||||
entry_vec.push(RecipeEntry(file_name, recipe_contents));
|
||||
} else {
|
||||
warn!(
|
||||
file = %entry.path().to_string_lossy(),
|
||||
@ -119,12 +118,7 @@ impl AsyncFileStore {
|
||||
if recipe_path.exists().await && recipe_path.is_file().await {
|
||||
debug!("Found recipe file {}", recipe_path.to_string_lossy());
|
||||
let recipe_contents = read_to_string(recipe_path).await?;
|
||||
return Ok(Some(RecipeEntry {
|
||||
id: id.as_ref().to_owned(),
|
||||
text: recipe_contents,
|
||||
category: None,
|
||||
serving_count: None,
|
||||
}));
|
||||
return Ok(Some(RecipeEntry(id.as_ref().to_owned(), recipe_contents)));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
|
@ -1,2 +0,0 @@
|
||||
insert into plan_table (user_id, plan_date) values (?, ?)
|
||||
on conflict (user_id, plan_date) do nothing;
|
@ -14,7 +14,6 @@
|
||||
use async_std::sync::Arc;
|
||||
use std::collections::BTreeSet;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use std::{collections::BTreeMap, path::Path};
|
||||
|
||||
use argon2::{
|
||||
@ -122,29 +121,12 @@ pub trait APIStore {
|
||||
user_id: S,
|
||||
) -> Result<Option<Vec<(String, i32)>>>;
|
||||
|
||||
async fn fetch_meal_plan_for_date<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
date: NaiveDate,
|
||||
) -> Result<Option<Vec<(String, i32)>>>;
|
||||
|
||||
async fn fetch_meal_plans_since<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
date: NaiveDate,
|
||||
) -> Result<Option<BTreeMap<NaiveDate, Vec<(String, i32)>>>>;
|
||||
|
||||
async fn fetch_all_meal_plans<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
) -> Result<Option<Vec<NaiveDate>>>;
|
||||
|
||||
async fn delete_meal_plan_for_date<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
date: NaiveDate,
|
||||
) -> Result<()>;
|
||||
|
||||
async fn save_meal_plan<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
@ -152,16 +134,6 @@ pub trait APIStore {
|
||||
date: NaiveDate,
|
||||
) -> Result<()>;
|
||||
|
||||
async fn fetch_inventory_for_date<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
date: NaiveDate,
|
||||
) -> Result<(
|
||||
Vec<IngredientKey>,
|
||||
Vec<(IngredientKey, String)>,
|
||||
Vec<(String, String)>,
|
||||
)>;
|
||||
|
||||
async fn fetch_latest_inventory_data<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
@ -171,15 +143,6 @@ pub trait APIStore {
|
||||
Vec<(String, String)>,
|
||||
)>;
|
||||
|
||||
async fn save_inventory_data_for_date<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
date: &NaiveDate,
|
||||
filtered_ingredients: BTreeSet<IngredientKey>,
|
||||
modified_amts: BTreeMap<IngredientKey, String>,
|
||||
extra_items: Vec<(String, String)>,
|
||||
) -> Result<()>;
|
||||
|
||||
async fn save_inventory_data<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
@ -257,10 +220,8 @@ pub struct SqliteStore {
|
||||
|
||||
impl SqliteStore {
|
||||
pub async fn new<P: AsRef<Path>>(path: P) -> sqlx::Result<Self> {
|
||||
std::fs::create_dir_all(&path)?;
|
||||
let url = format!("sqlite://{}/store.db", path.as_ref().to_string_lossy());
|
||||
let options = SqliteConnectOptions::from_str(&url)?
|
||||
.busy_timeout(Duration::from_secs(5))
|
||||
.journal_mode(SqliteJournalMode::Wal)
|
||||
.create_if_missing(true);
|
||||
info!(?options, "Connecting to sqlite db");
|
||||
@ -270,7 +231,7 @@ impl SqliteStore {
|
||||
|
||||
#[instrument(fields(conn_string=self.url), skip_all)]
|
||||
pub async fn run_migrations(&self) -> sqlx::Result<()> {
|
||||
info!("Running database migrations");
|
||||
info!("Running databse migrations");
|
||||
sqlx::migrate!("./migrations")
|
||||
.run(self.pool.as_ref())
|
||||
.await?;
|
||||
@ -431,10 +392,19 @@ impl APIStore for SqliteStore {
|
||||
user_id: S,
|
||||
id: S,
|
||||
) -> Result<Option<RecipeEntry>> {
|
||||
// NOTE(jwall): We allow dead code becaue Rust can't figure out that
|
||||
// this code is actually constructed but it's done via the query_as
|
||||
// macro.
|
||||
#[allow(dead_code)]
|
||||
struct RecipeRow {
|
||||
pub recipe_id: String,
|
||||
pub recipe_text: Option<String>,
|
||||
}
|
||||
let id = id.as_ref();
|
||||
let user_id = user_id.as_ref();
|
||||
let entry = sqlx::query!(
|
||||
"select recipe_id, recipe_text, category, serving_count from recipes where user_id = ? and recipe_id = ?",
|
||||
let entry = sqlx::query_as!(
|
||||
RecipeRow,
|
||||
"select recipe_id, recipe_text from recipes where user_id = ? and recipe_id = ?",
|
||||
user_id,
|
||||
id,
|
||||
)
|
||||
@ -442,32 +412,37 @@ impl APIStore for SqliteStore {
|
||||
.await?
|
||||
.iter()
|
||||
.map(|row| {
|
||||
RecipeEntry {
|
||||
id: row.recipe_id.clone(),
|
||||
text: row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
||||
category: row.category.clone(),
|
||||
serving_count: row.serving_count.clone(),
|
||||
}
|
||||
RecipeEntry(
|
||||
row.recipe_id.clone(),
|
||||
row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
||||
)
|
||||
})
|
||||
.nth(0);
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
async fn get_recipes_for_user(&self, user_id: &str) -> Result<Option<Vec<RecipeEntry>>> {
|
||||
let rows = sqlx::query!(
|
||||
"select recipe_id, recipe_text, category, serving_count from recipes where user_id = ?",
|
||||
// NOTE(jwall): We allow dead code becaue Rust can't figure out that
|
||||
// this code is actually constructed but it's done via the query_as
|
||||
// macro.
|
||||
#[allow(dead_code)]
|
||||
struct RecipeRow {
|
||||
pub recipe_id: String,
|
||||
pub recipe_text: Option<String>,
|
||||
}
|
||||
let rows = sqlx::query_as!(
|
||||
RecipeRow,
|
||||
"select recipe_id, recipe_text from recipes where user_id = ?",
|
||||
user_id,
|
||||
)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?
|
||||
.iter()
|
||||
.map(|row| {
|
||||
RecipeEntry {
|
||||
id: row.recipe_id.clone(),
|
||||
text: row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
||||
category: row.category.clone(),
|
||||
serving_count: row.serving_count.clone(),
|
||||
}
|
||||
RecipeEntry(
|
||||
row.recipe_id.clone(),
|
||||
row.recipe_text.clone().unwrap_or_else(|| String::new()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Ok(Some(rows))
|
||||
@ -481,16 +456,12 @@ impl APIStore for SqliteStore {
|
||||
for entry in recipes {
|
||||
let recipe_id = entry.recipe_id().to_owned();
|
||||
let recipe_text = entry.recipe_text().to_owned();
|
||||
let category = entry.category();
|
||||
let serving_count = entry.serving_count();
|
||||
sqlx::query!(
|
||||
"insert into recipes (user_id, recipe_id, recipe_text, category, serving_count) values (?, ?, ?, ?, ?)
|
||||
on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text, category=excluded.category",
|
||||
"insert into recipes (user_id, recipe_id, recipe_text) values (?, ?, ?)
|
||||
on conflict(user_id, recipe_id) do update set recipe_text=excluded.recipe_text",
|
||||
user_id,
|
||||
recipe_id,
|
||||
recipe_text,
|
||||
category,
|
||||
serving_count,
|
||||
)
|
||||
.execute(self.pool.as_ref())
|
||||
.await?;
|
||||
@ -506,7 +477,7 @@ impl APIStore for SqliteStore {
|
||||
user_id,
|
||||
recipe_id,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.execute(&mut transaction)
|
||||
.await?;
|
||||
}
|
||||
transaction.commit().await?;
|
||||
@ -533,16 +504,6 @@ impl APIStore for SqliteStore {
|
||||
) -> Result<()> {
|
||||
let user_id = user_id.as_ref();
|
||||
let mut transaction = self.pool.as_ref().begin().await?;
|
||||
sqlx::query!(
|
||||
"delete from plan_recipes where user_id = ? and plan_date = ?",
|
||||
user_id,
|
||||
date,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
sqlx::query_file!("src/web/storage/init_meal_plan.sql", user_id, date)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
for (id, count) in recipe_counts {
|
||||
sqlx::query_file!(
|
||||
"src/web/storage/save_meal_plan.sql",
|
||||
@ -551,35 +512,13 @@ impl APIStore for SqliteStore {
|
||||
id,
|
||||
count
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.execute(&mut transaction)
|
||||
.await?;
|
||||
}
|
||||
transaction.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_all_meal_plans<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
) -> Result<Option<Vec<NaiveDate>>> {
|
||||
let user_id = user_id.as_ref();
|
||||
struct Row {
|
||||
pub plan_date: NaiveDate,
|
||||
}
|
||||
let rows = sqlx::query_file_as!(Row, r#"src/web/storage/fetch_all_plans.sql"#, user_id,)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
if rows.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut result = Vec::new();
|
||||
for row in rows {
|
||||
let date: NaiveDate = row.plan_date;
|
||||
result.push(date);
|
||||
}
|
||||
Ok(Some(result))
|
||||
}
|
||||
|
||||
async fn fetch_meal_plans_since<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
@ -617,88 +556,6 @@ impl APIStore for SqliteStore {
|
||||
Ok(Some(result))
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(user_id=user_id.as_ref(), date))]
|
||||
async fn delete_meal_plan_for_date<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
date: NaiveDate,
|
||||
) -> Result<()> {
|
||||
debug!("Processing delete request");
|
||||
let user_id = user_id.as_ref();
|
||||
let mut transaction = self.pool.as_ref().begin().await?;
|
||||
sqlx::query!(
|
||||
"delete from plan_table where user_id = ? and plan_date = ?",
|
||||
user_id,
|
||||
date
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"delete from plan_recipes where user_id = ? and plan_date = ?",
|
||||
user_id,
|
||||
date
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"delete from filtered_ingredients where user_id = ? and plan_date = ?",
|
||||
user_id,
|
||||
date
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"delete from modified_amts where user_id = ? and plan_date = ?",
|
||||
user_id,
|
||||
date
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"delete from extra_items where user_id = ? and plan_date = ?",
|
||||
user_id,
|
||||
date
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
transaction.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_meal_plan_for_date<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
date: NaiveDate,
|
||||
) -> Result<Option<Vec<(String, i32)>>> {
|
||||
let user_id = user_id.as_ref();
|
||||
struct Row {
|
||||
pub plan_date: NaiveDate,
|
||||
pub recipe_id: String,
|
||||
pub count: i64,
|
||||
}
|
||||
// NOTE(jwall): It feels like I shouldn't have to use an override here
|
||||
// but I do because of the way sqlite does types and how that interacts
|
||||
// with sqlx's type inference machinery.
|
||||
let rows = sqlx::query_file_as!(
|
||||
Row,
|
||||
"src/web/storage/fetch_plan_for_date.sql",
|
||||
user_id,
|
||||
date
|
||||
)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
if rows.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut result = Vec::new();
|
||||
for row in rows {
|
||||
let (_, recipe_id, count): (NaiveDate, String, i64) =
|
||||
(row.plan_date, row.recipe_id, row.count);
|
||||
result.push((recipe_id, count as i32));
|
||||
}
|
||||
Ok(Some(result))
|
||||
}
|
||||
|
||||
async fn fetch_latest_meal_plan<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
@ -728,90 +585,7 @@ impl APIStore for SqliteStore {
|
||||
Ok(Some(result))
|
||||
}
|
||||
|
||||
async fn fetch_inventory_for_date<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
date: NaiveDate,
|
||||
) -> Result<(
|
||||
Vec<IngredientKey>,
|
||||
Vec<(IngredientKey, String)>,
|
||||
Vec<(String, String)>,
|
||||
)> {
|
||||
let user_id = user_id.as_ref();
|
||||
struct FilteredIngredientRow {
|
||||
name: String,
|
||||
form: String,
|
||||
measure_type: String,
|
||||
}
|
||||
let filtered_ingredient_rows: Vec<FilteredIngredientRow> = sqlx::query_file_as!(
|
||||
FilteredIngredientRow,
|
||||
"src/web/storage/fetch_filtered_ingredients_for_date.sql",
|
||||
user_id,
|
||||
date,
|
||||
)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
let mut filtered_ingredients = Vec::new();
|
||||
for row in filtered_ingredient_rows {
|
||||
filtered_ingredients.push(IngredientKey::new(
|
||||
row.name,
|
||||
if row.form.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(row.form)
|
||||
},
|
||||
row.measure_type,
|
||||
));
|
||||
}
|
||||
struct ModifiedAmtRow {
|
||||
name: String,
|
||||
form: String,
|
||||
measure_type: String,
|
||||
amt: String,
|
||||
}
|
||||
let modified_amt_rows = sqlx::query_file_as!(
|
||||
ModifiedAmtRow,
|
||||
"src/web/storage/fetch_modified_amts_for_date.sql",
|
||||
user_id,
|
||||
date,
|
||||
)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
let mut modified_amts = Vec::new();
|
||||
for row in modified_amt_rows {
|
||||
modified_amts.push((
|
||||
IngredientKey::new(
|
||||
row.name,
|
||||
if row.form.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(row.form)
|
||||
},
|
||||
row.measure_type,
|
||||
),
|
||||
row.amt,
|
||||
));
|
||||
}
|
||||
pub struct ExtraItemRow {
|
||||
name: String,
|
||||
amt: String,
|
||||
}
|
||||
let extra_items_rows = sqlx::query_file_as!(
|
||||
ExtraItemRow,
|
||||
"src/web/storage/fetch_extra_items_for_date.sql",
|
||||
user_id,
|
||||
date,
|
||||
)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await?;
|
||||
let mut extra_items = Vec::new();
|
||||
for row in extra_items_rows {
|
||||
extra_items.push((row.name, row.amt));
|
||||
}
|
||||
Ok((filtered_ingredients, modified_amts, extra_items))
|
||||
}
|
||||
|
||||
// TODO(jwall): Deprecated
|
||||
// TODO(jwall): Do we need fetch for date variants of this.
|
||||
async fn fetch_latest_inventory_data<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
@ -891,87 +665,6 @@ impl APIStore for SqliteStore {
|
||||
Ok((filtered_ingredients, modified_amts, extra_items))
|
||||
}
|
||||
|
||||
async fn save_inventory_data_for_date<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
date: &NaiveDate,
|
||||
filtered_ingredients: BTreeSet<IngredientKey>,
|
||||
modified_amts: BTreeMap<IngredientKey, String>,
|
||||
extra_items: Vec<(String, String)>,
|
||||
) -> Result<()> {
|
||||
let user_id = user_id.as_ref();
|
||||
let mut transaction = self.pool.as_ref().begin().await?;
|
||||
// store the filtered_ingredients
|
||||
sqlx::query!(
|
||||
"delete from filtered_ingredients where user_id = ? and plan_date = ?",
|
||||
user_id,
|
||||
date
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
for key in filtered_ingredients {
|
||||
let name = key.name();
|
||||
let form = key.form();
|
||||
let measure_type = key.measure_type();
|
||||
sqlx::query_file!(
|
||||
"src/web/storage/save_filtered_ingredients_for_date.sql",
|
||||
user_id,
|
||||
name,
|
||||
form,
|
||||
measure_type,
|
||||
date,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
sqlx::query!(
|
||||
"delete from modified_amts where user_id = ? and plan_date = ?",
|
||||
user_id,
|
||||
date
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
// store the modified amts
|
||||
for (key, amt) in modified_amts {
|
||||
let name = key.name();
|
||||
let form = key.form();
|
||||
let measure_type = key.measure_type();
|
||||
let amt = &amt;
|
||||
sqlx::query_file!(
|
||||
"src/web/storage/save_modified_amts_for_date.sql",
|
||||
user_id,
|
||||
name,
|
||||
form,
|
||||
measure_type,
|
||||
amt,
|
||||
date,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
sqlx::query!(
|
||||
"delete from extra_items where user_id = ? and plan_date = ?",
|
||||
user_id,
|
||||
date
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
// Store the extra items
|
||||
for (name, amt) in extra_items {
|
||||
sqlx::query_file!(
|
||||
"src/web/storage/store_extra_items_for_date.sql",
|
||||
user_id,
|
||||
name,
|
||||
amt,
|
||||
date
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
transaction.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_inventory_data<S: AsRef<str> + Send>(
|
||||
&self,
|
||||
user_id: S,
|
||||
@ -993,7 +686,7 @@ impl APIStore for SqliteStore {
|
||||
form,
|
||||
measure_type,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.execute(&mut transaction)
|
||||
.await?;
|
||||
}
|
||||
// store the modified amts
|
||||
@ -1010,13 +703,13 @@ impl APIStore for SqliteStore {
|
||||
measure_type,
|
||||
amt,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.execute(&mut transaction)
|
||||
.await?;
|
||||
}
|
||||
// Store the extra items
|
||||
for (name, amt) in extra_items {
|
||||
sqlx::query_file!("src/web/storage/store_extra_items.sql", user_id, name, amt)
|
||||
.execute(&mut *transaction)
|
||||
.execute(&mut transaction)
|
||||
.await?;
|
||||
}
|
||||
transaction.commit().await?;
|
||||
|
@ -1,2 +0,0 @@
|
||||
insert into filtered_ingredients(user_id, name, form, measure_type, plan_date)
|
||||
values (?, ?, ?, ?, ?) on conflict(user_id, name, form, measure_type, plan_date) DO NOTHING
|
@ -1,2 +0,0 @@
|
||||
insert into modified_amts(user_id, name, form, measure_type, amt, plan_date)
|
||||
values (?, ?, ?, ?, ?, ?) on conflict (user_id, name, form, measure_type, plan_date) do update set amt=excluded.amt
|
@ -1,3 +0,0 @@
|
||||
insert into extra_items (user_id, name, amt, plan_date)
|
||||
values (?, ?, ?, ?)
|
||||
on conflict (user_id, name, plan_date) do update set amt=excluded.amt
|
@ -1,51 +0,0 @@
|
||||
sig Id {}
|
||||
sig Text {}
|
||||
|
||||
sig Recipe {
|
||||
, id: one Id
|
||||
, text: one Text
|
||||
}
|
||||
|
||||
fact {
|
||||
no r1, r2: Recipe | (r1.id = r2.id) and (r1.text != r2.text)
|
||||
no r1, r2: Recipe | (r1 != r2) and (r1.id = r2.id)
|
||||
}
|
||||
|
||||
sig Ingredient {}
|
||||
sig Modifier {}
|
||||
sig Amt {}
|
||||
|
||||
sig ModifiedInventory {
|
||||
, ingredient: one Ingredient
|
||||
, modifier: lone Modifier
|
||||
, amt: one Amt
|
||||
}
|
||||
|
||||
fact {
|
||||
no mi1, mi2: ModifiedInventory | mi1 != mi2 && (mi1.ingredient = mi2.ingredient) and (mi1.modifier = mi2.modifier)
|
||||
}
|
||||
|
||||
sig DeletedInventory {
|
||||
, ingredient: one Ingredient
|
||||
, modifier: lone Modifier
|
||||
}
|
||||
|
||||
fact {
|
||||
no mi1, mi2: DeletedInventory | mi1 != mi2 && (mi1.ingredient = mi2.ingredient) and (mi1.modifier = mi2.modifier)
|
||||
}
|
||||
|
||||
sig ExtraItems {
|
||||
, ingredient: one Ingredient
|
||||
, amt: one Amt
|
||||
}
|
||||
|
||||
sig State {
|
||||
, recipes: some Recipe
|
||||
, modified: set ModifiedInventory
|
||||
, deleted: set DeletedInventory
|
||||
, extras: set ExtraItems
|
||||
} {
|
||||
no rs: Recipe | rs not in recipes
|
||||
}
|
||||
|
||||
run { } for 3 but exactly 2 State, 2 Modifier, exactly 3 ModifiedInventory, exactly 9 Ingredient
|
@ -1,17 +0,0 @@
|
||||
Meal Planning: {
|
||||
shape: sequence_diagram
|
||||
user: Cook; client: Kitchen frontend; kitchen: Kitchen backend
|
||||
|
||||
user -> client: Start new meal Plan
|
||||
client -> kitchen: new plan created
|
||||
user -> client: Add recipe to meal plan
|
||||
client -> kitchen: Update meal plan with recipe
|
||||
client -> client: cache updated meal plan
|
||||
user -> client: Do inventory
|
||||
client -> kitchen: Store inventory mutations
|
||||
client -> client: cache inventory mutations
|
||||
user -> client: Undo mutation
|
||||
client -> kitchen: Store inventory mutations
|
||||
client -> client: cache inventory mutations
|
||||
user -> user: Cook recipes
|
||||
}
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 94 KiB |
@ -1,5 +1,5 @@
|
||||
{ pkgs, rust-wasm, wasm-pack-hermetic, wasm-bindgen, cargo-wasm2map }:
|
||||
{ pkgs, rust-wasm }:
|
||||
with pkgs;
|
||||
mkShell {
|
||||
buildInputs = (with pkgs; [wasm-bindgen wasm-pack-hermetic llvm clang rust-wasm binaryen cargo-wasm2map]);
|
||||
}
|
||||
buildInputs = (if stdenv.isDarwin then [ pkgs.darwin.apple_sdk.frameworks.Security ] else [ ]) ++ (with pkgs; [wasm-bindgen-cli wasm-pack llvm clang rust-wasm]);
|
||||
}
|
@ -10,16 +10,14 @@ with pkgs;
|
||||
(naersk-lib.buildPackage rec {
|
||||
pname = "kitchen";
|
||||
inherit version;
|
||||
buildInputs = [ rust-wasm libclang ];
|
||||
buildInputs = [ rust-wasm ];
|
||||
# However the crate we are building has it's root in specific crate.
|
||||
nativeBuildInputs = [llvm clang rust-bindgen];
|
||||
src = root;
|
||||
nativeBuildInputs = (if stdenv.isDarwin then [ xcbuild pkgs.darwin.apple_sdk.frameworks.Security ] else [ ]) ++ [llvm clang];
|
||||
cargoBuildOptions = opts: opts ++ ["-p" "${pname}" ];
|
||||
postPatch = ''
|
||||
mkdir -p web/dist
|
||||
cp -r ${kitchenWasm}/* web/dist/
|
||||
ls web/dist/
|
||||
'';
|
||||
# We have to tell libproc where the libclang.dylib lives
|
||||
LIBCLANG_PATH="${libclang.lib}/lib/";
|
||||
})
|
||||
})
|
@ -1,43 +1,43 @@
|
||||
{pkgs? (import <nixpkgs>) {},
|
||||
version,
|
||||
features ? "",
|
||||
rust-wasm,
|
||||
wasm-bindgen,
|
||||
lockFile,
|
||||
outputHashes,
|
||||
cargo-wasm2map,
|
||||
}:
|
||||
with pkgs;
|
||||
let
|
||||
pname = "kitchen-wasm";
|
||||
src = ./../..;
|
||||
lockFile = ./../../Cargo.lock;
|
||||
# NOTE(jwall): Because we use wasm-pack directly below we need
|
||||
# the cargo dependencies to already be installed.
|
||||
cargoDeps = (pkgs.rustPlatform.importCargoLock { inherit lockFile outputHashes; });
|
||||
cargoDeps = (pkgs.rustPlatform.importCargoLock { inherit lockFile; outputHashes = {
|
||||
# I'm maintaining some patches for these so the lockfile hashes are a little
|
||||
# incorrect. We override those here.
|
||||
"sycamore-0.8.2" = "sha256-D968+8C5EelGGmot9/LkAlULZOf/Cr+1WYXRCMwb1nQ=";
|
||||
"sqlx-0.6.2" = "sha256-X/LFvtzRfiOIEZJiVzmFvvULPpjhqvI99pSwH7a//GM=";
|
||||
};
|
||||
});
|
||||
in
|
||||
# TODO(zaphar): I should actually be leveraging naersklib.buildPackage with a postInstall for the optimization and bindgen
|
||||
stdenv.mkDerivation {
|
||||
inherit src pname;
|
||||
version = version;
|
||||
# we need wasmb-bindgen v0.2.83 exactly
|
||||
buildInputs = [ rust-wasm wasm-bindgen wasm-pack binaryen cargo-wasm2map];
|
||||
propagatedBuildInputs = [ rust-wasm wasm-bindgen wasm-pack binaryen];
|
||||
buildInputs = [ rust-wasm wasm-bindgen-cli wasm-pack binaryen];
|
||||
propagatedBuildInputs = [ rust-wasm wasm-bindgen-cli wasm-pack binaryen];
|
||||
phases = [ "postUnpackPhase" "buildPhase"];
|
||||
postUnpackPhase = ''
|
||||
ln -s ${cargoDeps} ./cargo-vendor-dir
|
||||
cp -r ./cargo-vendor-dir/.cargo ./
|
||||
cp -r $src/* ./
|
||||
'';
|
||||
# TODO(jwall): Use the makefile for as much of this as possible.
|
||||
# TODO(jwall): Build this from the root rather than the src.
|
||||
buildPhase = ''
|
||||
echo building with wasm-pack
|
||||
mkdir -p $out
|
||||
cd web
|
||||
cp -r static $out
|
||||
export project=kitchen
|
||||
sh ../scripts/wasm-build.sh release
|
||||
sh ../scripts/wasm-sourcemap.sh
|
||||
RUST_LOG=info wasm-pack build --mode no-install --release --target web --out-dir $out ${features};
|
||||
cp -r index.html $out
|
||||
cp -r favicon.ico $out
|
||||
rm -rf $out/release
|
||||
rm -rf $out/wasm32-unknown-unknown
|
||||
'';
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
{ pkgs
|
||||
, lib
|
||||
, rustPlatform
|
||||
, fetchCrate
|
||||
, nodejs
|
||||
, pkg-config
|
||||
, openssl
|
||||
, curl
|
||||
}:
|
||||
|
||||
# This package is special so we don't use the naersk infrastructure to build it.
|
||||
# Instead we crib from the nixpkgs version with some tweaks to work with our
|
||||
# flake setup.
|
||||
rustPlatform.buildRustPackage rec {
|
||||
pname = "wasm-bindgen-cli";
|
||||
# NOTE(jwall): This must exactly match the version of the wasm-bindgen crate
|
||||
# we are using.
|
||||
version = "0.2.89";
|
||||
|
||||
src = fetchCrate {
|
||||
inherit pname version;
|
||||
sha256 = "sha256-IPxP68xtNSpwJjV2yNMeepAS0anzGl02hYlSTvPocz8=";
|
||||
};
|
||||
|
||||
cargoHash = "sha256-EsGFW1f9+E5NnMadP/0rRzFCxVJQb0mlTLz/3zYQ5Ac=";
|
||||
|
||||
nativeBuildInputs = [ pkg-config ];
|
||||
|
||||
buildInputs = [ openssl curl ];
|
||||
|
||||
nativeCheckInputs = [ nodejs ];
|
||||
|
||||
# other tests require it to be ran in the wasm-bindgen monorepo
|
||||
#cargoTestFlags = [ "--test=reference" ];
|
||||
doCheck = false;
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
{pkgs,
|
||||
naersk-lib,
|
||||
rust-wasm,
|
||||
}:
|
||||
with pkgs;
|
||||
(naersk-lib.buildPackage rec {
|
||||
pname = "wasm-pack";
|
||||
version = "v0.12.1";
|
||||
buildInputs = [ rust-wasm pkgs.openssl curl];
|
||||
nativeBuildInputs =[llvm clang pkg-config];
|
||||
OPENSSL_NO_VENDOR=1;
|
||||
# The checks use network so disable them here
|
||||
doCheck = false;
|
||||
src = fetchFromGitHub {
|
||||
owner = "rustwasm";
|
||||
repo = "wasm-pack";
|
||||
rev = version;
|
||||
hash = "sha256-L4mCgUPG4cgTUpCoaIUOTONBOggXn5vMyPKj48B3MMk=";
|
||||
};
|
||||
cargoBuildOptions = opts: opts ++ ["-p" "${pname}" ];
|
||||
})
|
@ -6,6 +6,10 @@ A web assembly experiment in Meal Planning and Shopping List management.
|
||||
|
||||
Ensure you have rust installed with support for the web assembly target. You can see instructions here: [Rust wasm book](https://rustwasm.github.io/docs/book/game-of-life/setup.html).
|
||||
|
||||
You will also want to have trunk installed. You can see instructions for that here: [trunk](https://trunkrs.dev/)
|
||||
|
||||
Then obtain the source. We do not at this time publish kitchen on [crates.io](https://crates.io/).
|
||||
|
||||
```sh
|
||||
git clone https://github.com/zaphar/kitchen
|
||||
cd kitchen
|
||||
@ -19,7 +23,7 @@ make release
|
||||
|
||||
# Hacking on kitchen
|
||||
|
||||
The run script will run build the app and run it for you.
|
||||
If you want to hack on kitchen, then you may find it useful to use trunk in dev mode. The run script will run build the app and run trunk with it watching for changes and reloading on demand in your browser.
|
||||
|
||||
```sh
|
||||
./run.sh
|
||||
@ -33,4 +37,4 @@ If all of the above looks like too much work, and you already use the nix packag
|
||||
|
||||
```sh
|
||||
nix run github:zaphar/kitchen
|
||||
```
|
||||
```
|
@ -8,14 +8,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
abortable_parser = "~0.2.6"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4.22"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.204"
|
||||
features = ["derive"]
|
||||
chrono = "~0.4"
|
||||
serde = "1.0.144"
|
||||
|
||||
[dependencies.num-rational]
|
||||
version = "~0.4.0"
|
||||
|
@ -50,49 +50,27 @@ impl Mealplan {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct RecipeEntry {
|
||||
pub id: String,
|
||||
pub text: String,
|
||||
pub category: Option<String>,
|
||||
pub serving_count: Option<i64>,
|
||||
}
|
||||
pub struct RecipeEntry(pub String, pub String);
|
||||
|
||||
impl RecipeEntry {
|
||||
pub fn new<IS: Into<String>, TS: Into<String>>(recipe_id: IS, text: TS) -> Self {
|
||||
Self {
|
||||
id: recipe_id.into(),
|
||||
text: text.into(),
|
||||
category: None,
|
||||
serving_count: None,
|
||||
}
|
||||
Self(recipe_id.into(), text.into())
|
||||
}
|
||||
|
||||
pub fn set_recipe_id<S: Into<String>>(&mut self, id: S) {
|
||||
self.id = id.into();
|
||||
self.0 = id.into();
|
||||
}
|
||||
|
||||
pub fn recipe_id(&self) -> &str {
|
||||
self.id.as_str()
|
||||
self.0.as_str()
|
||||
}
|
||||
|
||||
pub fn set_recipe_text<S: Into<String>>(&mut self, text: S) {
|
||||
self.text = text.into();
|
||||
self.1 = text.into();
|
||||
}
|
||||
|
||||
pub fn recipe_text(&self) -> &str {
|
||||
self.text.as_str()
|
||||
}
|
||||
|
||||
pub fn set_category<S: Into<String>>(&mut self, cat: S) {
|
||||
self.category = Some(cat.into());
|
||||
}
|
||||
|
||||
pub fn category(&self) -> Option<&String> {
|
||||
self.category.as_ref()
|
||||
}
|
||||
|
||||
pub fn serving_count(&self) -> Option<i64> {
|
||||
self.serving_count.clone()
|
||||
self.1.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,7 +79,6 @@ impl RecipeEntry {
|
||||
pub struct Recipe {
|
||||
pub title: String,
|
||||
pub desc: Option<String>,
|
||||
pub serving_count: Option<i64>,
|
||||
pub steps: Vec<Step>,
|
||||
}
|
||||
|
||||
@ -111,7 +88,6 @@ impl Recipe {
|
||||
title: title.into(),
|
||||
desc: desc.map(|s| s.into()),
|
||||
steps: Vec::new(),
|
||||
serving_count: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,16 +124,6 @@ impl Recipe {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&RecipeEntry> for Recipe {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &RecipeEntry) -> Result<Self, Self::Error> {
|
||||
let mut parsed = parse::as_recipe(&value.text)?;
|
||||
parsed.serving_count = value.serving_count.clone();
|
||||
Ok(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IngredientAccumulator {
|
||||
inner: BTreeMap<IngredientKey, (Ingredient, BTreeSet<String>)>,
|
||||
}
|
||||
@ -182,28 +148,16 @@ impl IngredientAccumulator {
|
||||
set.insert(recipe_title.clone());
|
||||
self.inner.insert(key, (i.clone(), set));
|
||||
} else {
|
||||
let amts = match (&self.inner[&key].0.amt, &i.amt) {
|
||||
(Volume(rvm), Volume(lvm)) => vec![Volume(lvm + rvm)],
|
||||
(Count(lqty), Count(rqty)) => vec![Count(lqty + rqty)],
|
||||
(Weight(lqty), Weight(rqty)) => vec![Weight(lqty + rqty)],
|
||||
(Package(lnm, lqty), Package(rnm, rqty)) => {
|
||||
if lnm == rnm {
|
||||
vec![Package(lnm.clone(), lqty + rqty)]
|
||||
} else {
|
||||
vec![
|
||||
Package(lnm.clone(), lqty.clone()),
|
||||
Package(rnm.clone(), rqty.clone()),
|
||||
]
|
||||
}
|
||||
}
|
||||
let amt = match (self.inner[&key].0.amt, i.amt) {
|
||||
(Volume(rvm), Volume(lvm)) => Volume(lvm + rvm),
|
||||
(Count(lqty), Count(rqty)) => Count(lqty + rqty),
|
||||
(Weight(lqty), Weight(rqty)) => Weight(lqty + rqty),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
for amt in amts {
|
||||
self.inner.get_mut(&key).map(|(i, set)| {
|
||||
i.amt = amt;
|
||||
set.insert(recipe_title.clone());
|
||||
});
|
||||
}
|
||||
self.inner.get_mut(&key).map(|(i, set)| {
|
||||
i.amt = amt;
|
||||
set.insert(recipe_title.clone());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -232,7 +186,7 @@ pub struct Step {
|
||||
impl Step {
|
||||
pub fn new<S: Into<String>>(prep_time: Option<std::time::Duration>, instructions: S) -> Self {
|
||||
Self {
|
||||
prep_time,
|
||||
prep_time: prep_time,
|
||||
instructions: instructions.into(),
|
||||
ingredients: Vec::new(),
|
||||
}
|
||||
|
@ -334,14 +334,7 @@ make_fn!(unit<StrIter, String>,
|
||||
text_token!("kg"),
|
||||
text_token!("grams"),
|
||||
text_token!("gram"),
|
||||
text_token!("g"),
|
||||
text_token!("pkg"),
|
||||
text_token!("package"),
|
||||
text_token!("bottle"),
|
||||
text_token!("bot"),
|
||||
text_token!("bag"),
|
||||
text_token!("can")
|
||||
),
|
||||
text_token!("g")),
|
||||
_ => ws,
|
||||
(u.to_lowercase().to_singular())
|
||||
)
|
||||
@ -400,7 +393,6 @@ pub fn measure(i: StrIter) -> abortable_parser::Result<StrIter, Measure> {
|
||||
"oz" => Weight(Oz(qty)),
|
||||
"kg" | "kilogram" => Weight(Kilogram(qty)),
|
||||
"g" | "gram" => Weight(Gram(qty)),
|
||||
"pkg" | "package" | "can" | "bag" | "bottle" | "bot" => Measure::pkg(s, qty),
|
||||
_u => {
|
||||
eprintln!("Invalid unit: {}", _u);
|
||||
unreachable!()
|
||||
@ -426,8 +418,9 @@ pub fn normalize_name(name: &str) -> String {
|
||||
// NOTE(jwall): The below unwrap is safe because of the length
|
||||
// check above.
|
||||
let last = parts.last().unwrap();
|
||||
let normalized = last.to_singular();
|
||||
prefix.push(' ');
|
||||
prefix.push_str(&last.to_string());
|
||||
prefix.push_str(&normalized);
|
||||
return prefix;
|
||||
}
|
||||
return name.trim().to_lowercase().to_owned();
|
||||
|
@ -235,30 +235,32 @@ fn test_ingredient_name_parse() {
|
||||
#[test]
|
||||
fn test_ingredient_parse() {
|
||||
for (i, expected) in vec![
|
||||
(
|
||||
"1 cup flour ",
|
||||
Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))),
|
||||
),
|
||||
(
|
||||
"\t1 cup flour ",
|
||||
Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))),
|
||||
),
|
||||
(
|
||||
"1 cup apple (chopped)",
|
||||
Ingredient::new(
|
||||
"apple",
|
||||
Some("chopped".to_owned()),
|
||||
Volume(Cup(Quantity::Whole(1))),
|
||||
),
|
||||
),
|
||||
(
|
||||
"1 cup apple (chopped) ",
|
||||
Ingredient::new(
|
||||
"apple",
|
||||
Some("chopped".to_owned()),
|
||||
Volume(Cup(Quantity::Whole(1))),
|
||||
),
|
||||
),
|
||||
//(
|
||||
// "1 cup flour ",
|
||||
// Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""),
|
||||
//),
|
||||
//(
|
||||
// "\t1 cup flour ",
|
||||
// Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""),
|
||||
//),
|
||||
//(
|
||||
// "1 cup apple (chopped)",
|
||||
// Ingredient::new(
|
||||
// "apple",
|
||||
// Some("chopped".to_owned()),
|
||||
// Volume(Cup(Quantity::Whole(1))),
|
||||
// "",
|
||||
// ),
|
||||
//),
|
||||
//(
|
||||
// "1 cup apple (chopped) ",
|
||||
// Ingredient::new(
|
||||
// "apple",
|
||||
// Some("chopped".to_owned()),
|
||||
// Volume(Cup(Quantity::Whole(1))),
|
||||
// "",
|
||||
// ),
|
||||
//),
|
||||
(
|
||||
"1 green bell pepper (chopped) ",
|
||||
Ingredient::new(
|
||||
@ -267,46 +269,6 @@ fn test_ingredient_parse() {
|
||||
Count(Quantity::Whole(1)),
|
||||
),
|
||||
),
|
||||
(
|
||||
"1 pkg green onion",
|
||||
Ingredient::new(
|
||||
"green onion",
|
||||
None,
|
||||
Package("pkg".into(), Quantity::Whole(1)),
|
||||
),
|
||||
),
|
||||
(
|
||||
"1 bottle green onion",
|
||||
Ingredient::new(
|
||||
"green onion",
|
||||
None,
|
||||
Package("bottle".into(), Quantity::Whole(1)),
|
||||
),
|
||||
),
|
||||
(
|
||||
"1 bot green onion",
|
||||
Ingredient::new(
|
||||
"green onion",
|
||||
None,
|
||||
Package("bot".into(), Quantity::Whole(1)),
|
||||
),
|
||||
),
|
||||
(
|
||||
"1 bag green onion",
|
||||
Ingredient::new(
|
||||
"green onion",
|
||||
None,
|
||||
Package("bag".into(), Quantity::Whole(1)),
|
||||
),
|
||||
),
|
||||
(
|
||||
"1 can baked beans",
|
||||
Ingredient::new(
|
||||
"baked beans",
|
||||
None,
|
||||
Package("can".into(), Quantity::Whole(1)),
|
||||
),
|
||||
),
|
||||
] {
|
||||
match parse::ingredient(StrIter::new(i)) {
|
||||
ParseResult::Complete(_, ing) => assert_eq!(ing, expected),
|
||||
|
@ -22,7 +22,6 @@ use std::{
|
||||
convert::TryFrom,
|
||||
fmt::Display,
|
||||
ops::{Add, Div, Mul, Sub},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use num_rational::Ratio;
|
||||
@ -180,20 +179,6 @@ impl VolumeMeasure {
|
||||
|
||||
macro_rules! volume_op {
|
||||
($trait:ident, $method:ident) => {
|
||||
impl $trait for &VolumeMeasure {
|
||||
type Output = VolumeMeasure;
|
||||
|
||||
fn $method(self, lhs: Self) -> Self::Output {
|
||||
let (l, r) = (self.get_ml(), lhs.get_ml());
|
||||
let result = ML($trait::$method(l, r));
|
||||
if self.metric() {
|
||||
result.normalize()
|
||||
} else {
|
||||
result.into_tsp().normalize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl $trait for VolumeMeasure {
|
||||
type Output = Self;
|
||||
|
||||
@ -308,20 +293,6 @@ impl WeightMeasure {
|
||||
|
||||
macro_rules! weight_op {
|
||||
($trait:ident, $method:ident) => {
|
||||
impl $trait for &WeightMeasure {
|
||||
type Output = WeightMeasure;
|
||||
|
||||
fn $method(self, lhs: Self) -> Self::Output {
|
||||
let (l, r) = (self.get_grams(), lhs.get_grams());
|
||||
let result = WeightMeasure::Gram($trait::$method(l, r));
|
||||
if self.metric() {
|
||||
result.normalize()
|
||||
} else {
|
||||
result.into_oz().normalize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl $trait for WeightMeasure {
|
||||
type Output = Self;
|
||||
|
||||
@ -364,19 +335,18 @@ impl Display for WeightMeasure {
|
||||
|
||||
use WeightMeasure::{Gram, Kilogram, Oz, Pound};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
|
||||
/// Measurements in a Recipe with associated units for them.
|
||||
pub enum Measure {
|
||||
/// Volume measurements as meter cubed base unit
|
||||
Volume(VolumeMeasure),
|
||||
/// Simple count of items
|
||||
Count(Quantity),
|
||||
Package(Rc<str>, Quantity),
|
||||
/// Weight measure as Grams base unit
|
||||
Weight(WeightMeasure),
|
||||
}
|
||||
|
||||
use Measure::{Count, Package, Volume, Weight};
|
||||
use Measure::{Count, Volume, Weight};
|
||||
|
||||
impl Measure {
|
||||
pub fn tsp(qty: Quantity) -> Self {
|
||||
@ -437,16 +407,11 @@ impl Measure {
|
||||
Weight(Oz(qty))
|
||||
}
|
||||
|
||||
pub fn pkg<S: Into<Rc<str>>>(name: S, qty: Quantity) -> Self {
|
||||
Package(name.into(), qty)
|
||||
}
|
||||
|
||||
pub fn measure_type(&self) -> String {
|
||||
match self {
|
||||
Volume(_) => "Volume",
|
||||
Count(_) => "Count",
|
||||
Weight(_) => "Weight",
|
||||
Package(_, _) => "Package",
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
@ -456,7 +421,6 @@ impl Measure {
|
||||
Volume(vm) => vm.plural(),
|
||||
Count(qty) => qty.plural(),
|
||||
Weight(wm) => wm.plural(),
|
||||
Package(_, qty) => qty.plural(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -465,7 +429,6 @@ impl Measure {
|
||||
Volume(vm) => Volume(vm.normalize()),
|
||||
Count(qty) => Count(qty.clone()),
|
||||
Weight(wm) => Weight(wm.normalize()),
|
||||
Package(nm, qty) => Package(nm.clone(), qty.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -476,7 +439,6 @@ impl Display for Measure {
|
||||
Volume(vm) => write!(w, "{}", vm),
|
||||
Count(qty) => write!(w, "{}", qty),
|
||||
Weight(wm) => write!(w, "{}", wm),
|
||||
Package(nm, qty) => write!(w, "{} {}", qty, nm),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -571,26 +533,6 @@ impl TryFrom<f32> for Quantity {
|
||||
|
||||
macro_rules! quantity_op {
|
||||
($trait:ident, $method:ident) => {
|
||||
impl $trait for &Quantity {
|
||||
type Output = Quantity;
|
||||
|
||||
fn $method(self, lhs: Self) -> Self::Output {
|
||||
match (self, lhs) {
|
||||
(Whole(rhs), Whole(lhs)) => Frac($trait::$method(
|
||||
Ratio::from_integer(*rhs),
|
||||
Ratio::from_integer(*lhs),
|
||||
)),
|
||||
(Frac(rhs), Frac(lhs)) => Frac($trait::$method(rhs, lhs)),
|
||||
(Whole(rhs), Frac(lhs)) => {
|
||||
Frac($trait::$method(Ratio::from_integer(*rhs), lhs))
|
||||
}
|
||||
(Frac(rhs), Whole(lhs)) => {
|
||||
Frac($trait::$method(rhs, Ratio::from_integer(*lhs)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl $trait for Quantity {
|
||||
type Output = Self;
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
# Copyright 2022 Jeremy Wall
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
EXAMPLES=${EXAMPLES:-../examples}
|
||||
echo Starting server serving ${EXAMPLES}
|
||||
mkdir -p .session_store
|
||||
make kitchen
|
||||
./target/debug/kitchen --verbose debug serve --listen 127.0.0.1:3030 --session_dir .session_store --dir ${EXAMPLES} --tls --cert ~/tls-certs/localhost+1.pem --cert_key ~/tls-certs/localhost+1-key.pem $@
|
||||
# This is ghetto but I'm doing it anyway
|
5
run.sh
5
run.sh
@ -12,7 +12,6 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
EXAMPLES=${EXAMPLES:-../examples}
|
||||
echo Starting server serving ${EXAMPLES}
|
||||
mkdir .session_store
|
||||
nix run .\#kitchenDebug -- --verbose debug serve --session_dir .session_store --dir ${EXAMPLES} --tls --cert ~/tls-certs/localhost+1.pem --cert_key ~/tls-certs/localhost+1-key.pem $@
|
||||
echo Starting api server serving ${EXAMPLES}
|
||||
nix run .\#kitchenDebug -- --verbose debug serve --dir ${EXAMPLES} --tls --cert ~/tls-certs/localhost+2.pem --cert_key ~/tls-certs/localhost+2-key.pem
|
||||
# This is ghetto but I'm doing it anyway
|
||||
|
@ -1,11 +0,0 @@
|
||||
set -x
|
||||
buildtype=$1;
|
||||
|
||||
mkdir -p $out
|
||||
|
||||
if [ ${buildtype} = "release" ]; then
|
||||
buildtype_flag="--release"
|
||||
fi
|
||||
|
||||
cargo build --lib ${buildtype_flag} --target wasm32-unknown-unknown --target-dir $out --features debug_logs
|
||||
wasm-bindgen $out/wasm32-unknown-unknown/${buildtype}/${project}_wasm.wasm --out-dir $out --typescript --target web
|
@ -1,6 +0,0 @@
|
||||
set -x
|
||||
buildtype=$1;
|
||||
|
||||
wasm-opt $out/wasm32-unknown-unknown/${buildtype}/${project}_wasm.wasm --output $out/${project}_wasm_bg-opt.wasm -O
|
||||
rm -f $out/${project}_wasm_bg.wasm
|
||||
mv $out/${project}_wasm_bg-opt.wasm $out/${project}_wasm_bg.wasm
|
@ -1,3 +0,0 @@
|
||||
set -x
|
||||
|
||||
cargo-wasm2map wasm2map --patch $out/${project}_wasm_bg.wasm --base-url=http://localhost:3030
|
11
shell.nix
Normal file
11
shell.nix
Normal file
@ -0,0 +1,11 @@
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
in
|
||||
(import (
|
||||
fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
||||
}
|
||||
) {
|
||||
src = ./.;
|
||||
}).devShell
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kitchen-wasm"
|
||||
version = "0.2.25"
|
||||
version = "0.2.16"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
@ -22,18 +22,9 @@ console_error_panic_hook = "0.1.7"
|
||||
serde_json = "1.0.79"
|
||||
tracing = "0.1.35"
|
||||
async-trait = "0.1.57"
|
||||
base64 = "0.21.0"
|
||||
base64 = "0.20.0"
|
||||
sycamore-router = "0.8"
|
||||
js-sys = "0.3.60"
|
||||
wasm-web-component = { git = "https://github.com/zaphar/wasm-web-components.git", rev = "v0.3.0" }
|
||||
maud = "*"
|
||||
indexed-db = "0.4.1"
|
||||
anyhow = "1.0.86"
|
||||
serde-wasm-bindgen = "0.6.5"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.204"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.tracing-subscriber]
|
||||
version = "0.3.16"
|
||||
@ -46,26 +37,20 @@ features = ["fmt", "time"]
|
||||
version = "0.4.22"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.gloo-net]
|
||||
version = "0.4.0"
|
||||
[dependencies.reqwasm]
|
||||
version = "0.5.0"
|
||||
|
||||
[dependencies.wasm-bindgen]
|
||||
version = "= 0.2.89"
|
||||
# we need wasm-bindgen v0.2.83 exactly
|
||||
version = "= 0.2.83"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3"
|
||||
features = [
|
||||
"Event",
|
||||
"InputEvent",
|
||||
"CustomEvent",
|
||||
"CustomEventInit",
|
||||
"EventTarget",
|
||||
"History",
|
||||
"HtmlAnchorElement",
|
||||
"HtmlDivElement",
|
||||
"HtmlSpanElement",
|
||||
"HtmlInputElement",
|
||||
"HtmlTextAreaElement",
|
||||
"HtmlBaseElement",
|
||||
"HtmlDialogElement",
|
||||
"KeyboardEvent",
|
||||
@ -73,12 +58,7 @@ features = [
|
||||
"PopStateEvent",
|
||||
"Url",
|
||||
"Window",
|
||||
"IdbFactory",
|
||||
"IdbOpenDbRequest",
|
||||
"IdbRequest",
|
||||
"IdbDatabase",
|
||||
"IdbRequestReadyState",
|
||||
"Storage",
|
||||
"Storage"
|
||||
]
|
||||
|
||||
[dependencies.sycamore]
|
||||
|
BIN
web/favicon.ico
BIN
web/favicon.ico
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@ -19,7 +19,7 @@
|
||||
<head>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" name="viewport"
|
||||
content="width=device-width, initial-scale=1.0" charset="UTF-8">
|
||||
<link rel="stylesheet" href="/ui/static/normalize.css">
|
||||
<link rel="stylesheet" href="/ui/static/pico.min.css">
|
||||
<link rel="stylesheet" href="/ui/static/app.css">
|
||||
</head>
|
||||
|
||||
@ -35,4 +35,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
729
web/src/api.rs
729
web/src/api.rs
@ -13,33 +13,19 @@
|
||||
// limitations under the License.
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use base64::{self, Engine};
|
||||
use chrono::NaiveDate;
|
||||
use gloo_net;
|
||||
// TODO(jwall): Remove this when we have gone a few migrations past.
|
||||
use serde_json::from_str;
|
||||
use base64;
|
||||
use reqwasm;
|
||||
use serde_json::{from_str, to_string};
|
||||
use sycamore::prelude::*;
|
||||
use tracing::{debug, error, instrument};
|
||||
|
||||
use anyhow::Result;
|
||||
use client_api::*;
|
||||
use recipes::{IngredientKey, RecipeEntry};
|
||||
use serde_wasm_bindgen::{from_value, Serializer};
|
||||
use wasm_bindgen::JsValue;
|
||||
// TODO(jwall): Remove this when we have gone a few migrations past.
|
||||
use web_sys::Storage;
|
||||
|
||||
fn to_js<T: serde::ser::Serialize>(value: T) -> Result<JsValue, serde_wasm_bindgen::Error> {
|
||||
let s = Serializer::new().serialize_maps_as_objects(true);
|
||||
value.serialize(&s)
|
||||
}
|
||||
use crate::{app_state::AppState, js_lib};
|
||||
|
||||
use crate::{
|
||||
app_state::{parse_recipes, AppState},
|
||||
js_lib::{self, DBFactory},
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct Error(String);
|
||||
|
||||
@ -79,290 +65,256 @@ impl From<std::string::FromUtf8Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<gloo_net::Error> for Error {
|
||||
fn from(item: gloo_net::Error) -> Self {
|
||||
impl From<reqwasm::Error> for Error {
|
||||
fn from(item: reqwasm::Error) -> Self {
|
||||
Error(format!("{:?}", item))
|
||||
}
|
||||
}
|
||||
|
||||
fn token68(user: String, pass: String) -> String {
|
||||
base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass))
|
||||
fn recipe_key<S: std::fmt::Display>(id: S) -> String {
|
||||
format!("recipe:{}", id)
|
||||
}
|
||||
|
||||
fn convert_to_io_error<V, E>(res: Result<V, E>) -> Result<V, std::io::Error>
|
||||
where
|
||||
E: Into<Box<dyn std::error::Error>> + std::fmt::Debug,
|
||||
{
|
||||
match res {
|
||||
Ok(v) => Ok(v),
|
||||
Err(e) => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("{:?}", e),
|
||||
)),
|
||||
}
|
||||
fn category_key<S: std::fmt::Display>(id: S) -> String {
|
||||
format!("category:{}", id)
|
||||
}
|
||||
|
||||
fn token68(user: String, pass: String) -> String {
|
||||
base64::encode(format!("{}:{}", user, pass))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LocalStore {
|
||||
// TODO(zaphar): Remove this when it's safe to delete the migration
|
||||
old_store: Storage,
|
||||
store: DBFactory<'static>,
|
||||
store: Storage,
|
||||
}
|
||||
|
||||
const APP_STATE_KEY: &'static str = "app-state";
|
||||
const USER_DATA_KEY: &'static str = "user_data";
|
||||
|
||||
impl LocalStore {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
store: DBFactory::default(),
|
||||
old_store: js_lib::get_storage(),
|
||||
store: js_lib::get_storage(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn migrate(&self) {
|
||||
// 1. migrate app-state from localstore to indexeddb
|
||||
debug!("Peforming localstorage migration");
|
||||
if let Ok(Some(v)) = self.old_store.get("app_state") {
|
||||
if let Ok(Some(local_state)) = from_str::<Option<AppState>>(&v) {
|
||||
self.store_app_state(&local_state).await;
|
||||
}
|
||||
}
|
||||
let _ = self.old_store.remove_item("app_state");
|
||||
// 2. migrate user-state from localstore to indexeddb
|
||||
if let Ok(Some(v)) = self.old_store.get(USER_DATA_KEY) {
|
||||
if let Ok(local_user_data) = from_str::<Option<UserData>>(&v) {
|
||||
self.set_user_data(local_user_data.as_ref()).await;
|
||||
}
|
||||
}
|
||||
let _ = self.old_store.remove_item(USER_DATA_KEY);
|
||||
// 3. Recipes
|
||||
let store_len = self.old_store.length().unwrap();
|
||||
let mut key_list = Vec::new();
|
||||
for i in 0..store_len {
|
||||
let key = self.old_store.key(i).unwrap().unwrap();
|
||||
if key.starts_with("recipe:") {
|
||||
key_list.push(key);
|
||||
}
|
||||
}
|
||||
for k in key_list {
|
||||
if let Ok(Some(recipe)) = self.old_store.get(&k) {
|
||||
if let Ok(recipe) = from_str::<RecipeEntry>(&recipe) {
|
||||
self.set_recipe_entry(&recipe).await;
|
||||
}
|
||||
}
|
||||
let _ = self.old_store.delete(&k);
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn store_app_state(&self, state: &AppState) {
|
||||
let state = match to_js(state) {
|
||||
Ok(state) => state,
|
||||
Err(err) => {
|
||||
error!(?err, ?state, "Error deserializing app_state");
|
||||
return;
|
||||
}
|
||||
};
|
||||
web_sys::console::log_1(&state);
|
||||
let key = to_js(APP_STATE_KEY).expect("Failed to serialize key");
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||
object_store.put_kv(&key, &state).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.expect("Failed to store app-state");
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn fetch_app_state(&self) -> Option<AppState> {
|
||||
debug!("Loading state from local store");
|
||||
let recipes = parse_recipes(&self.get_recipes().await).expect("Failed to parse recipes");
|
||||
self.store
|
||||
.ro_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let key = convert_to_io_error(to_js(APP_STATE_KEY))?;
|
||||
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||
let mut app_state: AppState = match object_store.get(&key).await? {
|
||||
Some(s) => convert_to_io_error(from_value(s))?,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
if let Some(recipes) = recipes {
|
||||
debug!("Populating recipes");
|
||||
for (id, recipe) in recipes {
|
||||
debug!(id, "Adding recipe from local storage");
|
||||
app_state.recipes.insert(id, recipe);
|
||||
}
|
||||
}
|
||||
Ok(Some(app_state))
|
||||
})
|
||||
.await
|
||||
.expect("Failed to fetch app-state")
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
/// Gets user data from local storage.
|
||||
pub async fn get_user_data(&self) -> Option<UserData> {
|
||||
pub fn get_user_data(&self) -> Option<UserData> {
|
||||
self.store
|
||||
.ro_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let key = to_js(USER_DATA_KEY).expect("Failed to serialize key");
|
||||
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||
let user_data: UserData = match object_store.get(&key).await? {
|
||||
Some(s) => convert_to_io_error(from_value(s))?,
|
||||
None => return Ok(None),
|
||||
};
|
||||
Ok(Some(user_data))
|
||||
})
|
||||
.await
|
||||
.expect("Failed to fetch user_data")
|
||||
.get("user_data")
|
||||
.map_or(None, |val| val.map(|val| from_str(&val).unwrap_or(None)))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
// Set's user data to local storage.
|
||||
pub async fn set_user_data(&self, data: Option<&UserData>) {
|
||||
let key = to_js(USER_DATA_KEY).expect("Failed to serialize key");
|
||||
pub fn set_user_data(&self, data: Option<&UserData>) {
|
||||
if let Some(data) = data {
|
||||
let data = data.clone();
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||
object_store
|
||||
.put_kv(&key, &convert_to_io_error(to_js(&data))?)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.set(
|
||||
"user_data",
|
||||
&to_string(data).expect("Failed to desrialize user_data"),
|
||||
)
|
||||
.expect("Failed to set user_data");
|
||||
} else {
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||
object_store.delete(&key).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.delete("user_data")
|
||||
.expect("Failed to delete user_data");
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
|
||||
self.store
|
||||
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let mut keys = Vec::new();
|
||||
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||
let key_vec = object_store.get_all_keys(None).await?;
|
||||
for k in key_vec {
|
||||
if let Ok(v) = from_value(k) {
|
||||
keys.push(v);
|
||||
/// Gets categories from local storage.
|
||||
pub fn get_categories(&self) -> Option<Vec<(String, String)>> {
|
||||
let mut mappings = Vec::new();
|
||||
for k in self.get_category_keys() {
|
||||
if let Some(mut cat_map) = self
|
||||
.store
|
||||
.get(&k)
|
||||
.expect(&format!("Failed to get category key {}", k))
|
||||
.map(|v| {
|
||||
from_str::<Vec<(String, String)>>(&v)
|
||||
.expect(&format!("Failed to parse category key {}", k))
|
||||
})
|
||||
{
|
||||
mappings.extend(cat_map.drain(0..));
|
||||
}
|
||||
}
|
||||
if mappings.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(mappings)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the categories to the given string.
|
||||
pub fn set_categories(&self, mappings: Option<&Vec<(String, String)>>) {
|
||||
if let Some(mappings) = mappings {
|
||||
for (i, cat) in mappings.iter() {
|
||||
self.store
|
||||
.set(
|
||||
&category_key(i),
|
||||
&to_string(&(i, cat)).expect("Failed to serialize category mapping"),
|
||||
)
|
||||
.expect("Failed to store category mapping");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_storage_keys(&self) -> Vec<String> {
|
||||
let mut keys = Vec::new();
|
||||
for idx in 0..self.store.length().unwrap() {
|
||||
if let Some(k) = self.store.key(idx).expect("Failed to get storage key") {
|
||||
keys.push(k)
|
||||
}
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
fn get_category_keys(&self) -> impl Iterator<Item = String> {
|
||||
self.get_storage_keys()
|
||||
.into_iter()
|
||||
.filter(|k| k.starts_with("category:"))
|
||||
}
|
||||
|
||||
fn get_recipe_keys(&self) -> impl Iterator<Item = String> {
|
||||
self.get_storage_keys()
|
||||
.into_iter()
|
||||
.filter(|k| k.starts_with("recipe:"))
|
||||
}
|
||||
|
||||
/// Gets all the recipes from local storage.
|
||||
pub fn get_recipes(&self) -> Option<Vec<RecipeEntry>> {
|
||||
let mut recipe_list = Vec::new();
|
||||
for recipe_key in self.get_recipe_keys() {
|
||||
if let Some(entry) = self
|
||||
.store
|
||||
.get(&recipe_key)
|
||||
.expect(&format!("Failed to get recipe: {}", recipe_key))
|
||||
{
|
||||
match from_str(&entry) {
|
||||
Ok(entry) => {
|
||||
recipe_list.push(entry);
|
||||
}
|
||||
Err(e) => {
|
||||
error!(recipe_key, err = ?e, "Failed to parse recipe entry");
|
||||
}
|
||||
}
|
||||
Ok(keys)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to get storage keys")
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
if recipe_list.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(recipe_list)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
/// Gets all the recipes from local storage.
|
||||
pub async fn get_recipes(&self) -> Option<Vec<RecipeEntry>> {
|
||||
pub fn get_recipe_entry(&self, id: &str) -> Option<RecipeEntry> {
|
||||
let key = recipe_key(id);
|
||||
self.store
|
||||
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let mut recipe_list = Vec::new();
|
||||
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||
let mut c = object_store.cursor().open().await?;
|
||||
while let Some(value) = c.value() {
|
||||
recipe_list.push(convert_to_io_error(from_value(value))?);
|
||||
c.advance(1).await?;
|
||||
}
|
||||
if recipe_list.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(recipe_list))
|
||||
})
|
||||
.await
|
||||
.expect("Failed to get recipes")
|
||||
.get(&key)
|
||||
.expect(&format!("Failed to get recipe {}", key))
|
||||
.map(|entry| from_str(&entry).expect(&format!("Failed to get recipe {}", key)))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn get_recipe_entry(&self, id: &str) -> Option<RecipeEntry> {
|
||||
let key = to_js(id).expect("Failed to serialize key");
|
||||
self.store
|
||||
.ro_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||
let entry: Option<RecipeEntry> = match object_store.get(&key).await? {
|
||||
Some(v) => convert_to_io_error(from_value(v))?,
|
||||
None => None,
|
||||
};
|
||||
Ok(entry)
|
||||
})
|
||||
.await
|
||||
.expect("Failed to get recipes")
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
/// Sets the set of recipes to the entries passed in. Deletes any recipes not
|
||||
/// in the list.
|
||||
pub async fn set_all_recipes(&self, entries: &Vec<RecipeEntry>) {
|
||||
for recipe_key in self.get_recipe_keys().await {
|
||||
let key = to_js(&recipe_key).expect("Failed to serialize key");
|
||||
pub fn set_all_recipes(&self, entries: &Vec<RecipeEntry>) {
|
||||
for recipe_key in self.get_recipe_keys() {
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::STATE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::STATE_STORE_NAME)?;
|
||||
object_store.delete(&key).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.expect("Failed to delete user_data");
|
||||
.delete(&recipe_key)
|
||||
.expect(&format!("Failed to get recipe {}", recipe_key));
|
||||
}
|
||||
for entry in entries {
|
||||
let entry = entry.clone();
|
||||
let key = to_js(entry.recipe_id()).expect("Failed to serialize recipe key");
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||
object_store
|
||||
.put_kv(&key, &convert_to_io_error(to_js(&entry))?)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.expect("Failed to store recipe entry");
|
||||
self.set_recipe_entry(entry);
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
/// Set recipe entry in local storage.
|
||||
pub async fn set_recipe_entry(&self, entry: &RecipeEntry) {
|
||||
let entry = entry.clone();
|
||||
let key = to_js(entry.recipe_id()).expect("Failed to serialize recipe key");
|
||||
pub fn set_recipe_entry(&self, entry: &RecipeEntry) {
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||
object_store
|
||||
.put_kv(&key, &convert_to_io_error(to_js(&entry))?)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.expect("Failed to store recipe entry");
|
||||
.set(
|
||||
&recipe_key(entry.recipe_id()),
|
||||
&to_string(&entry).expect(&format!("Failed to get recipe {}", entry.recipe_id())),
|
||||
)
|
||||
.expect(&format!("Failed to store recipe {}", entry.recipe_id()))
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
/// Delete recipe entry from local storage.
|
||||
pub async fn delete_recipe_entry(&self, recipe_id: &str) {
|
||||
let key = to_js(recipe_id).expect("Failed to serialize key");
|
||||
pub fn delete_recipe_entry(&self, recipe_id: &str) {
|
||||
self.store
|
||||
.rw_transaction(&[js_lib::RECIPE_STORE_NAME], |trx| async move {
|
||||
let object_store = trx.object_store(js_lib::RECIPE_STORE_NAME)?;
|
||||
object_store.delete(&key).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.expect("Failed to delete user_data");
|
||||
.delete(&recipe_key(recipe_id))
|
||||
.expect(&format!("Failed to delete recipe {}", recipe_id))
|
||||
}
|
||||
|
||||
/// Save working plan to local storage.
|
||||
pub fn store_plan(&self, plan: &Vec<(String, i32)>) {
|
||||
self.store
|
||||
.set("plan", &to_string(&plan).expect("Failed to serialize plan"))
|
||||
.expect("Failed to store plan'");
|
||||
}
|
||||
|
||||
pub fn get_plan(&self) -> Option<Vec<(String, i32)>> {
|
||||
if let Some(plan) = self.store.get("plan").expect("Failed to store plan") {
|
||||
Some(from_str(&plan).expect("Failed to deserialize plan"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_inventory_data(
|
||||
&self,
|
||||
) -> Option<(
|
||||
BTreeSet<IngredientKey>,
|
||||
BTreeMap<IngredientKey, String>,
|
||||
Vec<(String, String)>,
|
||||
)> {
|
||||
if let Some(inventory) = self
|
||||
.store
|
||||
.get("inventory")
|
||||
.expect("Failed to retrieve inventory data")
|
||||
{
|
||||
let (filtered, modified, extras): (
|
||||
BTreeSet<IngredientKey>,
|
||||
Vec<(IngredientKey, String)>,
|
||||
Vec<(String, String)>,
|
||||
) = from_str(&inventory).expect("Failed to deserialize inventory");
|
||||
return Some((filtered, BTreeMap::from_iter(modified), extras));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
pub fn set_inventory_data(
|
||||
&self,
|
||||
inventory: (
|
||||
&BTreeSet<IngredientKey>,
|
||||
&BTreeMap<IngredientKey, String>,
|
||||
&Vec<(String, String)>,
|
||||
),
|
||||
) {
|
||||
let filtered = inventory.0;
|
||||
let modified_amts = inventory
|
||||
.1
|
||||
.iter()
|
||||
.map(|(k, amt)| (k.clone(), amt.clone()))
|
||||
.collect::<Vec<(IngredientKey, String)>>();
|
||||
let extras = inventory.2;
|
||||
let inventory_data = (filtered, &modified_amts, extras);
|
||||
self.store
|
||||
.set(
|
||||
"inventory",
|
||||
&to_string(&inventory_data).expect(&format!(
|
||||
"Failed to serialize inventory {:?}",
|
||||
inventory_data
|
||||
)),
|
||||
)
|
||||
.expect("Failed to set inventory");
|
||||
}
|
||||
|
||||
pub fn set_staples(&self, content: &String) {
|
||||
self.store
|
||||
.set("staples", content)
|
||||
.expect("Failed to set staples in local store");
|
||||
}
|
||||
|
||||
pub fn get_staples(&self) -> Option<String> {
|
||||
self.store
|
||||
.get("staples")
|
||||
.expect("Failed to retreive staples from local store")
|
||||
}
|
||||
}
|
||||
|
||||
@ -400,17 +352,13 @@ impl HttpStore {
|
||||
debug!("attempting login request against api.");
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/auth");
|
||||
let request = gloo_net::http::Request::get(&path)
|
||||
let result = reqwasm::http::Request::get(&path)
|
||||
.header(
|
||||
"authorization",
|
||||
"Authorization",
|
||||
format!("Basic {}", token68(user, pass)).as_str(),
|
||||
)
|
||||
.mode(web_sys::RequestMode::SameOrigin)
|
||||
.credentials(web_sys::RequestCredentials::SameOrigin)
|
||||
.build()
|
||||
.expect("Failed to build request");
|
||||
debug!(?request, "Sending auth request");
|
||||
let result = request.send().await;
|
||||
.send()
|
||||
.await;
|
||||
if let Ok(resp) = &result {
|
||||
if resp.status() == 200 {
|
||||
let user_data = resp
|
||||
@ -432,7 +380,7 @@ impl HttpStore {
|
||||
debug!("Retrieving User Account data");
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/account");
|
||||
let result = gloo_net::http::Request::get(&path).send().await;
|
||||
let result = reqwasm::http::Request::get(&path).send().await;
|
||||
if let Ok(resp) = &result {
|
||||
if resp.status() == 200 {
|
||||
let user_data = resp
|
||||
@ -453,11 +401,11 @@ impl HttpStore {
|
||||
pub async fn fetch_categories(&self) -> Result<Option<Vec<(String, String)>>, Error> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/category_map");
|
||||
let resp = match gloo_net::http::Request::get(&path).send().await {
|
||||
let resp = match reqwasm::http::Request::get(&path).send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(gloo_net::Error::JsError(err)) => {
|
||||
Err(reqwasm::Error::JsError(err)) => {
|
||||
error!(path, ?err, "Error hitting api");
|
||||
return Ok(None);
|
||||
return Ok(self.local_store.get_categories());
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err)?;
|
||||
@ -483,11 +431,11 @@ impl HttpStore {
|
||||
pub async fn fetch_recipes(&self) -> Result<Option<Vec<RecipeEntry>>, Error> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/recipes");
|
||||
let resp = match gloo_net::http::Request::get(&path).send().await {
|
||||
let resp = match reqwasm::http::Request::get(&path).send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(gloo_net::Error::JsError(err)) => {
|
||||
Err(reqwasm::Error::JsError(err)) => {
|
||||
error!(path, ?err, "Error hitting api");
|
||||
return Ok(self.local_store.get_recipes().await);
|
||||
return Ok(self.local_store.get_recipes());
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err)?;
|
||||
@ -513,11 +461,11 @@ impl HttpStore {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/recipe/");
|
||||
path.push_str(id.as_ref());
|
||||
let resp = match gloo_net::http::Request::get(&path).send().await {
|
||||
let resp = match reqwasm::http::Request::get(&path).send().await {
|
||||
Ok(resp) => resp,
|
||||
Err(gloo_net::Error::JsError(err)) => {
|
||||
Err(reqwasm::Error::JsError(err)) => {
|
||||
error!(path, ?err, "Error hitting api");
|
||||
return Ok(self.local_store.get_recipe_entry(id.as_ref()).await);
|
||||
return Ok(self.local_store.get_recipe_entry(id.as_ref()));
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err)?;
|
||||
@ -537,7 +485,7 @@ impl HttpStore {
|
||||
.as_success()
|
||||
.unwrap();
|
||||
if let Some(ref entry) = entry {
|
||||
self.local_store.set_recipe_entry(entry).await;
|
||||
self.local_store.set_recipe_entry(entry);
|
||||
}
|
||||
Ok(entry)
|
||||
}
|
||||
@ -551,7 +499,7 @@ impl HttpStore {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/recipe");
|
||||
path.push_str(&format!("/{}", recipe.as_ref()));
|
||||
let resp = gloo_net::http::Request::delete(&path).send().await?;
|
||||
let resp = reqwasm::http::Request::delete(&path).send().await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
@ -569,9 +517,10 @@ impl HttpStore {
|
||||
return Err("Recipe Ids can not be empty".into());
|
||||
}
|
||||
}
|
||||
let resp = gloo_net::http::Request::post(&path)
|
||||
.json(&recipes)
|
||||
.expect("Failed to set body")
|
||||
let serialized = to_string(&recipes).expect("Unable to serialize recipe entries");
|
||||
let resp = reqwasm::http::Request::post(&path)
|
||||
.body(&serialized)
|
||||
.header("content-type", "application/json")
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
@ -586,9 +535,9 @@ impl HttpStore {
|
||||
pub async fn store_categories(&self, categories: &Vec<(String, String)>) -> Result<(), Error> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/category_map");
|
||||
let resp = gloo_net::http::Request::post(&path)
|
||||
.json(&categories)
|
||||
.expect("Failed to set body")
|
||||
let resp = reqwasm::http::Request::post(&path)
|
||||
.body(to_string(&categories).expect("Unable to encode categories as json"))
|
||||
.header("content-type", "application/json")
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
@ -600,49 +549,32 @@ impl HttpStore {
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn store_app_state(&self, state: &AppState) -> Result<(), Error> {
|
||||
pub async fn store_app_state(&self, state: AppState) -> Result<(), Error> {
|
||||
let mut plan = Vec::new();
|
||||
for (key, count) in state.recipe_counts.iter() {
|
||||
plan.push((key.clone(), *count as i32));
|
||||
}
|
||||
if let Some(cached_plan_date) = &state.selected_plan_date {
|
||||
debug!(?plan, "Saving plan data");
|
||||
self.store_plan_for_date(plan, cached_plan_date).await?;
|
||||
debug!("Saving inventory data");
|
||||
self.store_inventory_data_for_date(
|
||||
state.filtered_ingredients.clone(),
|
||||
state.modified_amts.clone(),
|
||||
state
|
||||
.extras
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<(String, String)>>(),
|
||||
cached_plan_date,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
debug!("Saving plan data");
|
||||
self.store_plan(plan).await?;
|
||||
debug!("Saving inventory data");
|
||||
self.store_inventory_data(
|
||||
state.filtered_ingredients.clone(),
|
||||
state.modified_amts.clone(),
|
||||
state
|
||||
.extras
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<(String, String)>>(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
debug!("Saving plan data");
|
||||
self.store_plan(plan).await?;
|
||||
debug!("Saving inventory data");
|
||||
self.store_inventory_data(
|
||||
state.filtered_ingredients,
|
||||
state.modified_amts,
|
||||
state
|
||||
.extras
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<(String, String)>>(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn store_plan(&self, plan: Vec<(String, i32)>) -> Result<(), Error> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/plan");
|
||||
let resp = gloo_net::http::Request::post(&path)
|
||||
.json(&plan)
|
||||
.expect("Failed to set body")
|
||||
let resp = reqwasm::http::Request::post(&path)
|
||||
.body(to_string(&plan).expect("Unable to encode plan as json"))
|
||||
.header("content-type", "application/json")
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
@ -653,68 +585,10 @@ impl HttpStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn store_plan_for_date(
|
||||
&self,
|
||||
plan: Vec<(String, i32)>,
|
||||
date: &NaiveDate,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn fetch_plan(&self) -> Result<Option<Vec<(String, i32)>>, Error> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/plan");
|
||||
path.push_str("/at");
|
||||
path.push_str(&format!("/{}", date));
|
||||
let resp = gloo_net::http::Request::post(&path)
|
||||
.json(&plan)
|
||||
.expect("Failed to set body")
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_plan_dates(&self) -> Result<Option<Vec<NaiveDate>>, Error> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/plan");
|
||||
path.push_str("/all");
|
||||
let resp = gloo_net::http::Request::get(&path).send().await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
debug!("We got a valid response back");
|
||||
let plan = resp
|
||||
.json::<Response<Vec<NaiveDate>>>()
|
||||
.await
|
||||
.map_err(|e| format!("{}", e))?
|
||||
.as_success();
|
||||
Ok(plan)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_plan_for_date(&self, date: &NaiveDate) -> Result<(), Error> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/plan");
|
||||
path.push_str("/at");
|
||||
path.push_str(&format!("/{}", date));
|
||||
let resp = gloo_net::http::Request::delete(&path).send().await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_plan_for_date(
|
||||
&self,
|
||||
date: &NaiveDate,
|
||||
) -> Result<Option<Vec<(String, i32)>>, Error> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/plan");
|
||||
path.push_str("/at");
|
||||
path.push_str(&format!("/{}", date));
|
||||
let resp = gloo_net::http::Request::get(&path).send().await?;
|
||||
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
@ -728,61 +602,6 @@ impl HttpStore {
|
||||
}
|
||||
}
|
||||
|
||||
//pub async fn fetch_plan(&self) -> Result<Option<Vec<(String, i32)>>, Error> {
|
||||
// let mut path = self.v2_path();
|
||||
// path.push_str("/plan");
|
||||
// let resp = gloo_net::http::Request::get(&path).send().await?;
|
||||
// if resp.status() != 200 {
|
||||
// Err(format!("Status: {}", resp.status()).into())
|
||||
// } else {
|
||||
// debug!("We got a valid response back");
|
||||
// let plan = resp
|
||||
// .json::<PlanDataResponse>()
|
||||
// .await
|
||||
// .map_err(|e| format!("{}", e))?
|
||||
// .as_success();
|
||||
// Ok(plan)
|
||||
// }
|
||||
//}
|
||||
|
||||
pub async fn fetch_inventory_for_date(
|
||||
&self,
|
||||
date: &NaiveDate,
|
||||
) -> Result<
|
||||
(
|
||||
BTreeSet<IngredientKey>,
|
||||
BTreeMap<IngredientKey, String>,
|
||||
Vec<(String, String)>,
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/inventory");
|
||||
path.push_str("/at");
|
||||
path.push_str(&format!("/{}", date));
|
||||
let resp = gloo_net::http::Request::get(&path).send().await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
debug!("We got a valid response back");
|
||||
let InventoryData {
|
||||
filtered_ingredients,
|
||||
modified_amts,
|
||||
extra_items,
|
||||
} = resp
|
||||
.json::<InventoryResponse>()
|
||||
.await
|
||||
.map_err(|e| format!("{}", e))?
|
||||
.as_success()
|
||||
.unwrap();
|
||||
Ok((
|
||||
filtered_ingredients.into_iter().collect(),
|
||||
modified_amts.into_iter().collect(),
|
||||
extra_items,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fetch_inventory_data(
|
||||
&self,
|
||||
) -> Result<
|
||||
@ -795,9 +614,13 @@ impl HttpStore {
|
||||
> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/inventory");
|
||||
let resp = gloo_net::http::Request::get(&path).send().await?;
|
||||
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||
if resp.status() != 200 {
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
let err = Err(format!("Status: {}", resp.status()).into());
|
||||
Ok(match self.local_store.get_inventory_data() {
|
||||
Some(val) => val,
|
||||
None => return err,
|
||||
})
|
||||
} else {
|
||||
debug!("We got a valid response back");
|
||||
let InventoryData {
|
||||
@ -818,35 +641,6 @@ impl HttpStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn store_inventory_data_for_date(
|
||||
&self,
|
||||
filtered_ingredients: BTreeSet<IngredientKey>,
|
||||
modified_amts: BTreeMap<IngredientKey, String>,
|
||||
extra_items: Vec<(String, String)>,
|
||||
date: &NaiveDate,
|
||||
) -> Result<(), Error> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/inventory");
|
||||
path.push_str("/at");
|
||||
path.push_str(&format!("/{}", date));
|
||||
let filtered_ingredients: Vec<IngredientKey> = filtered_ingredients.into_iter().collect();
|
||||
let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect();
|
||||
debug!("Storing inventory data via API");
|
||||
let resp = gloo_net::http::Request::post(&path)
|
||||
.json(&(filtered_ingredients, modified_amts, extra_items))
|
||||
.expect("Failed to set body")
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
debug!("Invalid response back");
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
} else {
|
||||
debug!("We got a valid response back!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub async fn store_inventory_data(
|
||||
&self,
|
||||
@ -858,10 +652,13 @@ impl HttpStore {
|
||||
path.push_str("/inventory");
|
||||
let filtered_ingredients: Vec<IngredientKey> = filtered_ingredients.into_iter().collect();
|
||||
let modified_amts: Vec<(IngredientKey, String)> = modified_amts.into_iter().collect();
|
||||
debug!("Storing inventory data in cache");
|
||||
let serialized_inventory = to_string(&(filtered_ingredients, modified_amts, extra_items))
|
||||
.expect("Unable to encode plan as json");
|
||||
debug!("Storing inventory data via API");
|
||||
let resp = gloo_net::http::Request::post(&path)
|
||||
.json(&(filtered_ingredients, modified_amts, extra_items))
|
||||
.expect("Failed to set body")
|
||||
let resp = reqwasm::http::Request::post(&path)
|
||||
.body(&serialized_inventory)
|
||||
.header("content-type", "application/json")
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
@ -876,7 +673,7 @@ impl HttpStore {
|
||||
pub async fn fetch_staples(&self) -> Result<Option<String>, Error> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/staples");
|
||||
let resp = gloo_net::http::Request::get(&path).send().await?;
|
||||
let resp = reqwasm::http::Request::get(&path).send().await?;
|
||||
if resp.status() != 200 {
|
||||
debug!("Invalid response back");
|
||||
Err(format!("Status: {}", resp.status()).into())
|
||||
@ -890,15 +687,15 @@ impl HttpStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn store_staples<S: AsRef<str> + serde::Serialize>(
|
||||
&self,
|
||||
content: S,
|
||||
) -> Result<(), Error> {
|
||||
pub async fn store_staples<S: AsRef<str>>(&self, content: S) -> Result<(), Error> {
|
||||
let mut path = self.v2_path();
|
||||
path.push_str("/staples");
|
||||
let resp = gloo_net::http::Request::post(&path)
|
||||
.json(&content)
|
||||
.expect("Failed to set body")
|
||||
let serialized_staples: String =
|
||||
to_string(content.as_ref()).expect("Failed to serialize staples to json");
|
||||
|
||||
let resp = reqwasm::http::Request::post(&path)
|
||||
.body(&serialized_staples)
|
||||
.header("content-type", "application/json")
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status() != 200 {
|
||||
|
@ -16,51 +16,32 @@ use std::{
|
||||
fmt::Debug,
|
||||
};
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use client_api::UserData;
|
||||
use recipes::{parse, Ingredient, IngredientKey, Recipe, RecipeEntry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sycamore::futures::spawn_local_scoped;
|
||||
use sycamore::prelude::*;
|
||||
use sycamore_state::{Handler, MessageMapper};
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use wasm_bindgen::throw_str;
|
||||
|
||||
use crate::{
|
||||
api::{HttpStore, LocalStore},
|
||||
linear::LinearSignal,
|
||||
};
|
||||
use crate::api::{HttpStore, LocalStore};
|
||||
|
||||
fn bool_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AppState {
|
||||
pub recipe_counts: BTreeMap<String, u32>,
|
||||
pub recipe_categories: BTreeMap<String, String>,
|
||||
pub recipe_counts: BTreeMap<String, usize>,
|
||||
pub extras: Vec<(String, String)>,
|
||||
// FIXME(jwall): This should really be storable I think?
|
||||
#[serde(skip_deserializing, skip_serializing)]
|
||||
pub staples: Option<BTreeSet<Ingredient>>,
|
||||
// FIXME(jwall): This should really be storable I think?
|
||||
#[serde(skip_deserializing, skip_serializing)]
|
||||
pub recipes: BTreeMap<String, Recipe>,
|
||||
pub category_map: BTreeMap<String, String>,
|
||||
pub filtered_ingredients: BTreeSet<IngredientKey>,
|
||||
pub modified_amts: BTreeMap<IngredientKey, String>,
|
||||
pub auth: Option<UserData>,
|
||||
pub plan_dates: BTreeSet<NaiveDate>,
|
||||
pub selected_plan_date: Option<NaiveDate>,
|
||||
#[serde(default = "bool_true")]
|
||||
pub use_staples: bool,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
recipe_counts: BTreeMap::new(),
|
||||
recipe_categories: BTreeMap::new(),
|
||||
extras: Vec::new(),
|
||||
staples: None,
|
||||
recipes: BTreeMap::new(),
|
||||
@ -68,33 +49,27 @@ impl AppState {
|
||||
filtered_ingredients: BTreeSet::new(),
|
||||
modified_amts: BTreeMap::new(),
|
||||
auth: None,
|
||||
plan_dates: BTreeSet::new(),
|
||||
selected_plan_date: None,
|
||||
use_staples: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Message {
|
||||
ResetRecipeCounts,
|
||||
UpdateRecipeCount(String, u32),
|
||||
UpdateRecipeCount(String, usize),
|
||||
AddExtra(String, String),
|
||||
RemoveExtra(usize),
|
||||
UpdateExtra(usize, String, String),
|
||||
SaveRecipe(RecipeEntry, Option<Box<dyn FnOnce()>>),
|
||||
SetRecipe(String, Recipe),
|
||||
RemoveRecipe(String, Option<Box<dyn FnOnce()>>),
|
||||
UpdateCategory(String, String, Option<Box<dyn FnOnce()>>),
|
||||
ResetInventory,
|
||||
AddFilteredIngredient(IngredientKey),
|
||||
RemoveFilteredIngredient(IngredientKey),
|
||||
UpdateAmt(IngredientKey, String),
|
||||
SetUserData(UserData),
|
||||
SaveState(Option<Box<dyn FnOnce()>>),
|
||||
LoadState(Option<Box<dyn FnOnce()>>),
|
||||
UpdateStaples(String, Option<Box<dyn FnOnce()>>),
|
||||
DeletePlan(NaiveDate, Option<Box<dyn FnOnce()>>),
|
||||
SelectPlanDate(NaiveDate, Option<Box<dyn FnOnce()>>),
|
||||
UpdateUseStaples(bool), // TODO(jwall): Should this just be various settings?
|
||||
}
|
||||
|
||||
impl Debug for Message {
|
||||
@ -117,6 +92,9 @@ impl Debug for Message {
|
||||
.field(arg2)
|
||||
.finish(),
|
||||
Self::SaveRecipe(arg0, _) => f.debug_tuple("SaveRecipe").field(arg0).finish(),
|
||||
Self::SetRecipe(arg0, arg1) => {
|
||||
f.debug_tuple("SetRecipe").field(arg0).field(arg1).finish()
|
||||
}
|
||||
Self::RemoveRecipe(arg0, _) => f.debug_tuple("SetCategoryMap").field(arg0).finish(),
|
||||
Self::UpdateCategory(i, c, _) => {
|
||||
f.debug_tuple("UpdateCategory").field(i).field(c).finish()
|
||||
@ -125,9 +103,6 @@ impl Debug for Message {
|
||||
Self::AddFilteredIngredient(arg0) => {
|
||||
f.debug_tuple("AddFilteredIngredient").field(arg0).finish()
|
||||
}
|
||||
Self::RemoveFilteredIngredient(arg0) => {
|
||||
f.debug_tuple("RemoveFilteredIngredient").field(arg0).finish()
|
||||
}
|
||||
Self::UpdateAmt(arg0, arg1) => {
|
||||
f.debug_tuple("UpdateAmt").field(arg0).field(arg1).finish()
|
||||
}
|
||||
@ -135,9 +110,6 @@ impl Debug for Message {
|
||||
Self::SaveState(_) => write!(f, "SaveState"),
|
||||
Self::LoadState(_) => write!(f, "LoadState"),
|
||||
Self::UpdateStaples(arg, _) => f.debug_tuple("UpdateStaples").field(arg).finish(),
|
||||
Self::UpdateUseStaples(arg) => f.debug_tuple("UpdateUseStaples").field(arg).finish(),
|
||||
Self::SelectPlanDate(arg, _) => f.debug_tuple("SelectPlanDate").field(arg).finish(),
|
||||
Self::DeletePlan(arg, _) => f.debug_tuple("DeletePlan").field(arg).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -148,14 +120,14 @@ pub struct StateMachine {
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
pub fn parse_recipes(
|
||||
fn parse_recipes(
|
||||
recipe_entries: &Option<Vec<RecipeEntry>>,
|
||||
) -> Result<Option<BTreeMap<String, Recipe>>, String> {
|
||||
match recipe_entries {
|
||||
Some(parsed) => {
|
||||
let mut parsed_map = BTreeMap::new();
|
||||
for r in parsed {
|
||||
let recipe = match r.try_into() {
|
||||
let recipe = match parse::as_recipe(&r.recipe_text()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Error parsing recipe {}", e);
|
||||
@ -175,122 +147,98 @@ impl StateMachine {
|
||||
Self { store, local_store }
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn load_state(
|
||||
store: &HttpStore,
|
||||
local_store: &LocalStore,
|
||||
original: &Signal<AppState>,
|
||||
) -> Result<(), crate::api::Error> {
|
||||
// NOTE(jwall): We use a linear Signal in here to ensure that we only
|
||||
// call set on the signal once. When the LinearSignal get's dropped it
|
||||
// will call set on the contained Signal.
|
||||
let mut original: LinearSignal<AppState> = original.into();
|
||||
if let Some(state) = local_store.fetch_app_state().await {
|
||||
original = original.update(state);
|
||||
}
|
||||
let mut state = original.get().as_ref().clone();
|
||||
info!("Synchronizing Recipes");
|
||||
let recipe_entries = &store.fetch_recipes().await?;
|
||||
let recipes = parse_recipes(&recipe_entries)?;
|
||||
debug!(?recipes, "Parsed Recipes");
|
||||
|
||||
if let Some(recipes) = recipes {
|
||||
state.recipes = recipes;
|
||||
};
|
||||
|
||||
info!("Synchronizing staples");
|
||||
state.staples = if let Some(content) = store.fetch_staples().await? {
|
||||
local_store.set_staples(&content);
|
||||
// now we need to parse staples as ingredients
|
||||
let mut staples = parse::as_ingredient_list(&content)?;
|
||||
Some(staples.drain(0..).collect())
|
||||
} else {
|
||||
Some(BTreeSet::new())
|
||||
if let Some(content) = local_store.get_staples() {
|
||||
let mut staples = parse::as_ingredient_list(&content)?;
|
||||
Some(staples.drain(0..).collect())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
info!("Synchronizing recipe");
|
||||
if let Some(recipe_entries) = recipe_entries {
|
||||
local_store.set_all_recipes(recipe_entries).await;
|
||||
state.recipe_categories = recipe_entries
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
debug!(recipe_entry=?entry, "Getting recipe category");
|
||||
(
|
||||
entry.recipe_id().to_owned(),
|
||||
entry
|
||||
.category()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "Entree".to_owned()),
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<String, String>>();
|
||||
local_store.set_all_recipes(recipe_entries);
|
||||
}
|
||||
|
||||
info!("Fetching meal plan list");
|
||||
if let Some(mut plan_dates) = store.fetch_plan_dates().await? {
|
||||
debug!(?plan_dates, "meal plan list");
|
||||
state.plan_dates = BTreeSet::from_iter(plan_dates.drain(0..));
|
||||
}
|
||||
|
||||
info!("Synchronizing meal plan");
|
||||
let plan = if let Some(ref cached_plan_date) = state.selected_plan_date {
|
||||
store
|
||||
.fetch_plan_for_date(cached_plan_date)
|
||||
.await?
|
||||
.or_else(|| Some(Vec::new()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let plan = store.fetch_plan().await?;
|
||||
if let Some(plan) = plan {
|
||||
// set the counts.
|
||||
let mut plan_map = BTreeMap::new();
|
||||
for (id, count) in plan {
|
||||
plan_map.insert(id, count as u32);
|
||||
plan_map.insert(id, count as usize);
|
||||
}
|
||||
state.recipe_counts = plan_map;
|
||||
for (id, _) in state.recipes.iter() {
|
||||
if !state.recipe_counts.contains_key(id) {
|
||||
state.recipe_counts.insert(id.clone(), 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Initialize things to zero.
|
||||
if let Some(rs) = recipe_entries {
|
||||
for r in rs {
|
||||
state.recipe_counts.insert(r.recipe_id().to_owned(), 0);
|
||||
if let Some(plan) = local_store.get_plan() {
|
||||
state.recipe_counts = plan.iter().map(|(k, v)| (k.clone(), *v as usize)).collect();
|
||||
} else {
|
||||
// Initialize things to zero.
|
||||
if let Some(rs) = recipe_entries {
|
||||
for r in rs {
|
||||
state.recipe_counts.insert(r.recipe_id().to_owned(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let plan = state
|
||||
.recipe_counts
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), *v as i32))
|
||||
.collect::<Vec<(String, i32)>>();
|
||||
local_store.store_plan(&plan);
|
||||
info!("Checking for user account data");
|
||||
if let Some(user_data) = store.fetch_user_data().await {
|
||||
debug!("Successfully got account data from server");
|
||||
local_store.set_user_data(Some(&user_data)).await;
|
||||
local_store.set_user_data(Some(&user_data));
|
||||
state.auth = Some(user_data);
|
||||
} else {
|
||||
debug!("Using account data from local store");
|
||||
let user_data = local_store.get_user_data().await;
|
||||
let user_data = local_store.get_user_data();
|
||||
state.auth = user_data;
|
||||
}
|
||||
info!("Synchronizing categories");
|
||||
match store.fetch_categories().await {
|
||||
Ok(Some(mut categories_content)) => {
|
||||
debug!(categories=?categories_content);
|
||||
local_store.set_categories(Some(&categories_content));
|
||||
let category_map = BTreeMap::from_iter(categories_content.drain(0..));
|
||||
state.category_map = category_map;
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!("There is no category file");
|
||||
local_store.set_categories(None);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
let inventory_data = if let Some(cached_plan_date) = &state.selected_plan_date {
|
||||
store.fetch_inventory_for_date(cached_plan_date).await
|
||||
} else {
|
||||
store.fetch_inventory_data().await
|
||||
};
|
||||
info!("Synchronizing inventory data");
|
||||
match inventory_data {
|
||||
match store.fetch_inventory_data().await {
|
||||
Ok((filtered_ingredients, modified_amts, extra_items)) => {
|
||||
local_store.set_inventory_data((
|
||||
&filtered_ingredients,
|
||||
&modified_amts,
|
||||
&extra_items,
|
||||
));
|
||||
state.modified_amts = modified_amts;
|
||||
state.filtered_ingredients = filtered_ingredients;
|
||||
state.extras = extra_items;
|
||||
@ -299,9 +247,7 @@ impl StateMachine {
|
||||
error!("{:?}", e);
|
||||
}
|
||||
}
|
||||
// Finally we store all of this app state back to our localstore
|
||||
local_store.store_app_state(&state).await;
|
||||
original.update(state);
|
||||
original.set(state);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -317,49 +263,69 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
for (id, _) in original_copy.recipes.iter() {
|
||||
map.insert(id.clone(), 0);
|
||||
}
|
||||
let plan: Vec<(String, i32)> =
|
||||
map.iter().map(|(s, i)| (s.clone(), *i as i32)).collect();
|
||||
self.local_store.store_plan(&plan);
|
||||
original_copy.recipe_counts = map;
|
||||
}
|
||||
Message::UpdateRecipeCount(id, count) => {
|
||||
original_copy.recipe_counts.insert(id, count);
|
||||
let plan: Vec<(String, i32)> = original_copy
|
||||
.recipe_counts
|
||||
.iter()
|
||||
.map(|(s, i)| (s.clone(), *i as i32))
|
||||
.collect();
|
||||
self.local_store.store_plan(&plan);
|
||||
}
|
||||
Message::AddExtra(amt, name) => {
|
||||
original_copy.extras.push((amt, name));
|
||||
self.local_store.set_inventory_data((
|
||||
&original_copy.filtered_ingredients,
|
||||
&original_copy.modified_amts,
|
||||
&original_copy.extras,
|
||||
))
|
||||
}
|
||||
Message::RemoveExtra(idx) => {
|
||||
original_copy.extras.remove(idx);
|
||||
self.local_store.set_inventory_data((
|
||||
&original_copy.filtered_ingredients,
|
||||
&original_copy.modified_amts,
|
||||
&original_copy.extras,
|
||||
))
|
||||
}
|
||||
Message::UpdateExtra(idx, amt, name) => match original_copy.extras.get_mut(idx) {
|
||||
Some(extra) => {
|
||||
extra.0 = amt;
|
||||
extra.1 = name;
|
||||
Message::UpdateExtra(idx, amt, name) => {
|
||||
match original_copy.extras.get_mut(idx) {
|
||||
Some(extra) => {
|
||||
extra.0 = amt;
|
||||
extra.1 = name;
|
||||
}
|
||||
None => {
|
||||
throw_str("Attempted to remove extra that didn't exist");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
throw_str("Attempted to remove extra that didn't exist");
|
||||
}
|
||||
},
|
||||
self.local_store.set_inventory_data((
|
||||
&original_copy.filtered_ingredients,
|
||||
&original_copy.modified_amts,
|
||||
&original_copy.extras,
|
||||
))
|
||||
}
|
||||
Message::SetRecipe(id, recipe) => {
|
||||
original_copy.recipes.insert(id, recipe);
|
||||
}
|
||||
Message::SaveRecipe(entry, callback) => {
|
||||
let recipe_id = entry.recipe_id().to_owned();
|
||||
let recipe: Recipe = (&entry).try_into().expect("Failed to parse RecipeEntry");
|
||||
original_copy.recipes.insert(recipe_id.clone(), recipe);
|
||||
if !original_copy.recipe_counts.contains_key(entry.recipe_id()) {
|
||||
original_copy.recipe_counts.insert(recipe_id.clone(), 0);
|
||||
}
|
||||
if let Some(cat) = entry.category().cloned() {
|
||||
original_copy
|
||||
.recipe_categories
|
||||
.entry(recipe_id.clone())
|
||||
.and_modify(|c| *c = cat.clone())
|
||||
.or_insert(cat);
|
||||
}
|
||||
let recipe =
|
||||
parse::as_recipe(entry.recipe_text()).expect("Failed to parse RecipeEntry");
|
||||
original_copy
|
||||
.recipes
|
||||
.insert(entry.recipe_id().to_owned(), recipe);
|
||||
original_copy
|
||||
.recipe_counts
|
||||
.insert(entry.recipe_id().to_owned(), 0);
|
||||
let store = self.store.clone();
|
||||
let local_store = self.local_store.clone();
|
||||
self.local_store.set_recipe_entry(&entry);
|
||||
spawn_local_scoped(cx, async move {
|
||||
local_store.set_recipe_entry(&entry).await;
|
||||
if let Err(e) = store.store_recipes(vec![entry]).await {
|
||||
// FIXME(jwall): We should have a global way to trigger error messages
|
||||
error!(err=?e, "Unable to save Recipe");
|
||||
// FIXME(jwall): This should be an error message
|
||||
} else {
|
||||
}
|
||||
callback.map(|f| f());
|
||||
});
|
||||
@ -367,10 +333,9 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
Message::RemoveRecipe(recipe, callback) => {
|
||||
original_copy.recipe_counts.remove(&recipe);
|
||||
original_copy.recipes.remove(&recipe);
|
||||
self.local_store.delete_recipe_entry(&recipe);
|
||||
let store = self.store.clone();
|
||||
let local_store = self.local_store.clone();
|
||||
spawn_local_scoped(cx, async move {
|
||||
local_store.delete_recipe_entry(&recipe).await;
|
||||
if let Err(err) = store.delete_recipe(&recipe).await {
|
||||
error!(?err, "Failed to delete recipe");
|
||||
}
|
||||
@ -378,6 +343,8 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
});
|
||||
}
|
||||
Message::UpdateCategory(ingredient, category, callback) => {
|
||||
self.local_store
|
||||
.set_categories(Some(&vec![(ingredient.clone(), category.clone())]));
|
||||
original_copy
|
||||
.category_map
|
||||
.insert(ingredient.clone(), category.clone());
|
||||
@ -393,145 +360,73 @@ impl MessageMapper<Message, AppState> for StateMachine {
|
||||
original_copy.filtered_ingredients = BTreeSet::new();
|
||||
original_copy.modified_amts = BTreeMap::new();
|
||||
original_copy.extras = Vec::new();
|
||||
self.local_store.set_inventory_data((
|
||||
&original_copy.filtered_ingredients,
|
||||
&original_copy.modified_amts,
|
||||
&original_copy.extras,
|
||||
));
|
||||
}
|
||||
Message::AddFilteredIngredient(key) => {
|
||||
original_copy.filtered_ingredients.insert(key);
|
||||
}
|
||||
Message::RemoveFilteredIngredient(key) => {
|
||||
original_copy.filtered_ingredients.remove(&key);
|
||||
self.local_store.set_inventory_data((
|
||||
&original_copy.filtered_ingredients,
|
||||
&original_copy.modified_amts,
|
||||
&original_copy.extras,
|
||||
));
|
||||
}
|
||||
Message::UpdateAmt(key, amt) => {
|
||||
original_copy.modified_amts.insert(key, amt);
|
||||
self.local_store.set_inventory_data((
|
||||
&original_copy.filtered_ingredients,
|
||||
&original_copy.modified_amts,
|
||||
&original_copy.extras,
|
||||
));
|
||||
}
|
||||
Message::SetUserData(user_data) => {
|
||||
let local_store = self.local_store.clone();
|
||||
original_copy.auth = Some(user_data.clone());
|
||||
spawn_local_scoped(cx, async move {
|
||||
local_store.set_user_data(Some(&user_data)).await;
|
||||
});
|
||||
self.local_store.set_user_data(Some(&user_data));
|
||||
original_copy.auth = Some(user_data);
|
||||
}
|
||||
Message::SaveState(f) => {
|
||||
let mut original_copy = original_copy.clone();
|
||||
let original_copy = original_copy.clone();
|
||||
let store = self.store.clone();
|
||||
let local_store = self.local_store.clone();
|
||||
spawn_local_scoped(cx, async move {
|
||||
if original_copy.selected_plan_date.is_none() {
|
||||
original_copy.selected_plan_date = Some(chrono::Local::now().date_naive());
|
||||
}
|
||||
original_copy.plan_dates.insert(
|
||||
original_copy
|
||||
.selected_plan_date
|
||||
.as_ref()
|
||||
.map(|d| d.clone())
|
||||
.unwrap(),
|
||||
);
|
||||
if let Err(e) = store.store_app_state(&original_copy).await {
|
||||
error!(err=?e, "Error saving app state");
|
||||
if let Err(e) = store.store_app_state(original_copy).await {
|
||||
error!(err=?e, "Error saving app state")
|
||||
};
|
||||
local_store.store_app_state(&original_copy).await;
|
||||
original.set(original_copy);
|
||||
f.map(|f| f());
|
||||
});
|
||||
// NOTE(jwall): We set the original signal in the async above
|
||||
// so we return immediately here.
|
||||
return;
|
||||
}
|
||||
Message::LoadState(f) => {
|
||||
let store = self.store.clone();
|
||||
let local_store = self.local_store.clone();
|
||||
debug!("Loading user state.");
|
||||
spawn_local_scoped(cx, async move {
|
||||
if let Err(err) = Self::load_state(&store, &local_store, original).await {
|
||||
error!(?err, "Failed to load user state");
|
||||
}
|
||||
Self::load_state(&store, &local_store, original)
|
||||
.await
|
||||
.expect("Failed to load_state.");
|
||||
local_store.set_inventory_data((
|
||||
&original.get().filtered_ingredients,
|
||||
&original.get().modified_amts,
|
||||
&original.get().extras,
|
||||
));
|
||||
f.map(|f| f());
|
||||
});
|
||||
return;
|
||||
}
|
||||
Message::UpdateStaples(content, callback) => {
|
||||
let store = self.store.clone();
|
||||
spawn_local_scoped(cx, async move {
|
||||
if let Err(err) = store.store_staples(content).await {
|
||||
error!(?err, "Failed to store staples");
|
||||
} else {
|
||||
callback.map(|f| f());
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
Message::UpdateUseStaples(value) => {
|
||||
original_copy.use_staples = value;
|
||||
}
|
||||
Message::SelectPlanDate(date, callback) => {
|
||||
let store = self.store.clone();
|
||||
let local_store = self.local_store.clone();
|
||||
spawn_local_scoped(cx, async move {
|
||||
if let Ok(Some(mut plan)) = store
|
||||
.fetch_plan_for_date(&date)
|
||||
.await
|
||||
{
|
||||
// Note(jwall): This is a little unusual but because this
|
||||
// is async code we can't rely on the set below.
|
||||
original_copy.recipe_counts =
|
||||
BTreeMap::from_iter(plan.drain(0..).map(|(k, v)| (k, v as u32)));
|
||||
let (filtered, modified, extras) = store
|
||||
.fetch_inventory_for_date(&date)
|
||||
.await
|
||||
.expect("Failed to fetch inventory_data for date");
|
||||
original_copy.modified_amts = modified;
|
||||
original_copy.filtered_ingredients = filtered;
|
||||
original_copy.extras = extras;
|
||||
} else {
|
||||
store.store_plan_for_date(Vec::new(), &date).await.expect("failed to set plan on server");
|
||||
}
|
||||
original_copy.plan_dates.insert(date.clone());
|
||||
original_copy.selected_plan_date = Some(date.clone());
|
||||
local_store.set_staples(&content);
|
||||
store
|
||||
.store_plan_for_date(vec![], &date)
|
||||
.store_staples(content)
|
||||
.await
|
||||
.expect("Failed to init meal plan for date");
|
||||
local_store.store_app_state(&original_copy).await;
|
||||
original.set(original_copy);
|
||||
|
||||
.expect("Failed to store staples");
|
||||
callback.map(|f| f());
|
||||
});
|
||||
// NOTE(jwall): Because we do our signal set above in the async block
|
||||
// we have to return here to avoid lifetime issues and double setting
|
||||
// the original signal.
|
||||
return;
|
||||
}
|
||||
Message::DeletePlan(date, callback) => {
|
||||
let store = self.store.clone();
|
||||
let local_store = self.local_store.clone();
|
||||
spawn_local_scoped(cx, async move {
|
||||
if let Err(err) = store.delete_plan_for_date(&date).await {
|
||||
error!(?err, "Error deleting plan");
|
||||
} else {
|
||||
original_copy.plan_dates.remove(&date);
|
||||
// Reset all meal planning state;
|
||||
let _ = original_copy.recipe_counts.iter_mut().map(|(_, v)| *v = 0);
|
||||
original_copy.filtered_ingredients = BTreeSet::new();
|
||||
original_copy.modified_amts = BTreeMap::new();
|
||||
original_copy.extras = Vec::new();
|
||||
local_store.store_app_state(&original_copy).await;
|
||||
original.set(original_copy);
|
||||
|
||||
callback.map(|f| f());
|
||||
}
|
||||
});
|
||||
// NOTE(jwall): Because we do our signal set above in the async block
|
||||
// we have to return here to avoid lifetime issues and double setting
|
||||
// the original signal.
|
||||
return;
|
||||
}
|
||||
}
|
||||
spawn_local_scoped(cx, {
|
||||
let local_store = self.local_store.clone();
|
||||
async move {
|
||||
local_store.store_app_state(&original_copy).await;
|
||||
original.set(original_copy);
|
||||
}
|
||||
});
|
||||
original.set(original_copy);
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user