Add some data access methods to Connection

This commit is contained in:
Greg Burri 2022-11-19 00:16:07 +01:00
parent 4fbc599d07
commit cdb883c3c4
6 changed files with 180 additions and 114 deletions

View file

@ -1,64 +1,73 @@
-- Version 1 is the initial structure. -- Version 1 is the initial structure.
CREATE TABLE Version ( CREATE TABLE [Version] (
id INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
version INTEGER NOT NULL UNIQUE, [version] INTEGER NOT NULL UNIQUE,
datetime DATETIME [datetime] DATETIME
); );
CREATE TABLE User ( CREATE TABLE [User] (
id INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
email TEXT NOT NULL, [email] TEXT NOT NULL,
password TEXT NOT NULL, -- Hashed and salted. [password] TEXT NOT NULL, -- Hashed and salted.
name TEXT NOT NULL [name] TEXT NOT NULL
); );
CREATE TABLE Recipe ( CREATE TABLE [Recipe] (
id INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL, [user_id] INTEGER NOT NULL,
title TEXT NOT NULL, [title] TEXT NOT NULL,
estimate_time INTEGER, [estimate_time] INTEGER,
description DATETIME, [description] TEXT,
FOREIGN KEY(user_id) REFERENCES User(id) FOREIGN KEY([user_id]) REFERENCES [User]([id])
); );
CREATE TABLE Quantity ( CREATE TABLE [Quantity] (
id INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
value REAL, [value] REAL,
unit TEXT [unit] TEXT
); );
CREATE TABLE Ingredient ( CREATE TABLE [Ingredient] (
id INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
name TEXT NOT NULL, [name] TEXT NOT NULL,
quantity_id INTEGER, [quantity_id] INTEGER,
input_step_id INTEGER NOT NULL, [input_step_id] INTEGER NOT NULL,
FOREIGN KEY(quantity_id) REFERENCES Quantity(id), FOREIGN KEY([quantity_id]) REFERENCES Quantity([id]),
FOREIGN KEY(input_step_id) REFERENCES Step(id) FOREIGN KEY([input_step_id]) REFERENCES Step([id])
); );
CREATE TABLE [Group] ( CREATE TABLE [Group] (
id INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
name TEXT [order] INTEGER NOT NULL DEFAULT 0,
[recipe_id] INTEGER,
name TEXT,
FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id])
); );
CREATE TABLE Step ( CREATE INDEX [Group_order_index] ON [Group] ([order]);
id INTEGER PRIMARY KEY,
action TEXT NOT NULL, CREATE TABLE [Step] (
group_id INTEGER NOT NULL, [id] INTEGER PRIMARY KEY,
[order] INTEGER NOT NULL DEFAULT 0,
[action] TEXT NOT NULL,
[group_id] INTEGER NOT NULL,
FOREIGN KEY(group_id) REFERENCES [Group](id) FOREIGN KEY(group_id) REFERENCES [Group](id)
); );
CREATE TABLE IntermediateSubstance ( CREATE INDEX [Step_order_index] ON [Group] ([order]);
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
quantity_id INTEGER,
output_step_id INTEGER NOT NULL,
input_step_id INTEGER NOT NULL,
FOREIGN KEY(quantity_id) REFERENCES Quantity(id), CREATE TABLE [IntermediateSubstance] (
FOREIGN KEY(output_step_id) REFERENCES Step(id), [id] INTEGER PRIMARY KEY,
FOREIGN KEY(input_step_id) REFERENCES Step(id) [name] TEXT NOT NULL,
[quantity_id] INTEGER,
[output_step_id] INTEGER NOT NULL,
[input_step_id] INTEGER NOT NULL,
FOREIGN KEY([quantity_id]) REFERENCES [Quantity]([id]),
FOREIGN KEY([output_step_id]) REFERENCES [Step]([id]),
FOREIGN KEY([input_step_id]) REFERENCES [Step]([id])
); );

View file

@ -1,14 +1,14 @@
use crate::consts::SQL_FILENAME;
use super::consts;
use std::{fs::{self, File}, path::Path, io::Read}; use std::{fs::{self, File}, path::Path, io::Read};
use itertools::Itertools;
//use rusqlite::types::ToSql; //use rusqlite::types::ToSql;
//use rusqlite::{Connection, Result, NO_PARAMS}; //use rusqlite::{Connection, Result, NO_PARAMS};
use r2d2::Pool; use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager; use r2d2_sqlite::SqliteConnectionManager;
use crate::consts;
use crate::model;
const CURRENT_DB_VERSION: u32 = 1; const CURRENT_DB_VERSION: u32 = 1;
#[derive(Debug)] #[derive(Debug)]
@ -19,30 +19,28 @@ pub enum DBError {
Other(String), Other(String),
} }
pub struct Connection { impl From<rusqlite::Error> for DBError {
//con: rusqlite::Connection
pool: Pool<SqliteConnectionManager>
}
pub struct Recipe {
pub title: String,
pub id: i32,
}
impl std::convert::From<rusqlite::Error> for DBError {
fn from(error: rusqlite::Error) -> Self { fn from(error: rusqlite::Error) -> Self {
DBError::SqliteError(error) DBError::SqliteError(error)
} }
} }
impl std::convert::From<r2d2::Error> for DBError { impl From<r2d2::Error> for DBError {
fn from(error: r2d2::Error) -> Self { fn from(error: r2d2::Error) -> Self {
DBError::R2d2Error(error) DBError::R2d2Error(error)
} }
} }
type Result<T> = std::result::Result<T, DBError>;
#[derive(Clone)]
pub struct Connection {
//con: rusqlite::Connection
pool: Pool<SqliteConnectionManager>
}
impl Connection { impl Connection {
pub fn new() -> Result<Connection, DBError> { pub fn new() -> Result<Connection> {
let data_dir = Path::new(consts::DB_DIRECTORY); let data_dir = Path::new(consts::DB_DIRECTORY);
@ -62,11 +60,7 @@ impl Connection {
* Called after the connection has been established for creating or updating the database. * Called after the connection has been established for creating or updating the database.
* The 'Version' table tracks the current state of the database. * The 'Version' table tracks the current state of the database.
*/ */
fn create_or_update(self: &Self) -> Result<(), DBError> { fn create_or_update(&self) -> Result<()> {
// let connection = Connection::new();
// let mut stmt = connection.sqlite_con.prepare("SELECT * FROM versions ORDER BY date").unwrap();
// let mut stmt = connection.sqlite_con.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='versions'").unwrap();
// Check the Database version. // Check the Database version.
let mut con = self.pool.get()?; let mut con = self.pool.get()?;
let tx = con.transaction()?; let tx = con.transaction()?;
@ -78,7 +72,7 @@ impl Connection {
[], [],
|row| row.get::<usize, String>(0) |row| row.get::<usize, String>(0)
) { ) {
Ok(_) => tx.query_row("SELECT [version] FROM [Version]", [], |row| row.get(0)).unwrap_or_default(), Ok(_) => tx.query_row("SELECT [version] FROM [Version] ORDER BY [id] DESC", [], |row| row.get(0)).unwrap_or_default(),
Err(_) => 0 Err(_) => 0
} }
}; };
@ -92,14 +86,18 @@ impl Connection {
Ok(()) Ok(())
} }
fn update_to_next_version(current_version: u32, tx: &rusqlite::Transaction) -> Result<bool, DBError> { fn update_to_next_version(current_version: u32, tx: &rusqlite::Transaction) -> Result<bool> {
let next_version = current_version + 1; let next_version = current_version + 1;
if next_version <= CURRENT_DB_VERSION { if next_version <= CURRENT_DB_VERSION {
println!("Update to version {}...", next_version); println!("Update to version {}...", next_version);
} }
fn ok(updated: bool) -> Result<bool, DBError> { fn update_version(to_version: u32, tx: &rusqlite::Transaction) -> Result<()> {
tx.execute("INSERT INTO [Version] ([version], [datetime]) VALUES (?1, datetime('now'))", [to_version]).map(|_| ()).map_err(DBError::from)
}
fn ok(updated: bool) -> Result<bool> {
if updated { if updated {
println!("Version updated"); println!("Version updated");
} }
@ -109,6 +107,7 @@ impl Connection {
match next_version { match next_version {
1 => { 1 => {
tx.execute_batch(&load_sql_file(next_version)?)?; tx.execute_batch(&load_sql_file(next_version)?)?;
update_version(next_version, tx)?;
ok(true) ok(true)
} }
@ -122,13 +121,36 @@ impl Connection {
} }
} }
pub fn get_all_recipes() { pub fn get_all_recipe_titles(&self) -> Result<Vec<(i32, String)>> {
let con = self.pool.get()?;
let mut stmt = con.prepare("SELECT [id], [title] FROM [Recipe] ORDER BY [title]")?;
let titles =
stmt.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?))
})?.map(|r| r.unwrap()).collect_vec(); // TODO: remove unwrap.
Ok(titles)
}
pub fn get_all_recipes(&self) -> Result<Vec<model::Recipe>> {
let con = self.pool.get()?;
let mut stmt = con.prepare("SELECT [id], [title] FROM [Recipe] ORDER BY [title]")?;
let recipes =
stmt.query_map([], |row| {
Ok(model::Recipe::new(row.get(0)?, row.get(1)?))
})?.map(|r| r.unwrap()).collect_vec(); // TODO: remove unwrap.
Ok(recipes)
}
pub fn get_recipe(&self, id: i32) -> Result<model::Recipe> {
let con = self.pool.get()?;
con.query_row("SELECT [id], [title] FROM [Recipe] WHERE [id] = ?1", [id], |row| {
Ok(model::Recipe::new(row.get(0)?, row.get(1)?))
}).map_err(DBError::from)
} }
} }
fn load_sql_file(version: u32) -> Result<String, DBError> { fn load_sql_file(version: u32) -> Result<String> {
let sql_file = SQL_FILENAME.replace("{VERSION}", &version.to_string()); let sql_file = consts::SQL_FILENAME.replace("{VERSION}", &version.to_string());
let mut file = File::open(&sql_file).map_err(|err| DBError::Other(format!("Cannot open SQL file ({}): {}", &sql_file, err.to_string())))?; let mut file = File::open(&sql_file).map_err(|err| DBError::Other(format!("Cannot open SQL file ({}): {}", &sql_file, err.to_string())))?;
let mut sql = String::new(); let mut sql = String::new();
file.read_to_string(&mut sql).map_err(|err| DBError::Other(format!("Cannot read SQL file ({}) : {}", &sql_file, err.to_string())))?; file.read_to_string(&mut sql).map_err(|err| DBError::Other(format!("Cannot read SQL file ({}) : {}", &sql_file, err.to_string())))?;

View file

@ -1,15 +1,13 @@
use std::io::prelude::*; use std::fs::File;
use std::{fs::File, env::args}; use std::sync::Mutex;
use actix_files as fs; use actix_files as fs;
use actix_web::{get, web, Responder, middleware, App, HttpServer, HttpResponse, HttpRequest, web::Query}; use actix_web::{get, web, Responder, middleware, App, HttpServer, HttpRequest};
use askama_actix::Template; use askama_actix::Template;
use clap::Parser;
use ron::de::from_reader; use ron::de::from_reader;
use serde::Deserialize; use serde::Deserialize;
use itertools::Itertools;
mod consts; mod consts;
mod model; mod model;
mod db; mod db;
@ -17,14 +15,14 @@ mod db;
#[derive(Template)] #[derive(Template)]
#[template(path = "home.html")] #[template(path = "home.html")]
struct HomeTemplate { struct HomeTemplate {
recipes: Vec<db::Recipe> recipes: Vec<(i32, String)>,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "view_recipe.html")] #[template(path = "view_recipe.html")]
struct ViewRecipeTemplate { struct ViewRecipeTemplate {
recipes: Vec<db::Recipe>, recipes: Vec<(i32, String)>,
current_recipe: db::Recipe current_recipe: model::Recipe,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -33,13 +31,16 @@ pub struct Request {
} }
#[get("/")] #[get("/")]
async fn home_page(req: HttpRequest) -> impl Responder { async fn home_page(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
HomeTemplate { recipes: vec![ db::Recipe { title: String::from("Saumon en croûte feuilletée"), id: 1 }, db::Recipe { title: String::from("Croissant au jambon"), id: 2 } ] } HomeTemplate { recipes: connection.get_all_recipe_titles().unwrap() } // TODO: unwrap.
} }
#[get("/recipe/view/{id}")] #[get("/recipe/view/{id}")]
async fn view_page(req: HttpRequest, path: web::Path<(i32,)>) -> impl Responder { async fn view_recipe(req: HttpRequest, path: web::Path<(i32,)>, connection: web::Data<db::Connection>) -> impl Responder {
ViewRecipeTemplate { recipes: vec![ db::Recipe { title: String::from("Saumon en croûte feuilletée"), id: 1 }, db::Recipe { title: String::from("Croissant au jambon"), id: 2 } ], current_recipe: db::Recipe { title: String::from("Saumon en croûte feuilletée"), id: 1 } } ViewRecipeTemplate {
recipes: connection.get_all_recipe_titles().unwrap(),
current_recipe: connection.get_recipe(path.0).unwrap(),
}
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -72,18 +73,19 @@ async fn main() -> std::io::Result<()> {
println!("Configuration: {:?}", config); println!("Configuration: {:?}", config);
// let database_connection = db::create_or_update(); let db_connection = web::Data::new(db::Connection::new().unwrap()); // TODO: remove unwrap.
std::env::set_var("RUST_LOG", "actix_web=info"); std::env::set_var("RUST_LOG", "actix_web=info");
let mut server = let mut server =
HttpServer::new( HttpServer::new(
|| { move || {
App::new() App::new()
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.wrap(middleware::Compress::default()) .wrap(middleware::Compress::default())
.app_data(db_connection.clone())
.service(home_page) .service(home_page)
.service(view_page) .service(view_recipe)
.service(fs::Files::new("/static", "static").show_files_listing()) .service(fs::Files::new("/static", "static").show_files_listing())
} }
); );
@ -93,7 +95,27 @@ async fn main() -> std::io::Result<()> {
server.run().await server.run().await
} }
#[derive(Parser, Debug)]
struct Args {
#[arg(long)]
test: bool
}
fn process_args() -> bool { fn process_args() -> bool {
let args = Args::parse();
if args.test {
if let Err(error) = db::Connection::new() {
println!("Error: {:?}", error)
}
return true;
}
false
/*
fn print_usage() { fn print_usage() {
println!("Usage:"); println!("Usage:");
println!(" {} [--help] [--test]", get_exe_name()); println!(" {} [--help] [--test]", get_exe_name());
@ -111,6 +133,6 @@ fn process_args() -> bool {
} }
return true return true
} }
false false
*/
} }

View file

@ -1,44 +1,57 @@
struct Recipe { pub struct Recipe {
title: String, pub id: i32,
estimate_time: Option<i32>, // [min]. pub title: String,
difficulty: Option<Difficulty>, pub estimate_time: Option<i32>, // [min].
pub difficulty: Option<Difficulty>,
//ingredients: Vec<Ingredient>, // For four people. //ingredients: Vec<Ingredient>, // For four people.
process: Vec<Group>, pub process: Vec<Group>,
} }
struct Ingredient { impl Recipe {
quantity: Option<Quantity>, pub fn new(id: i32, title: String) -> Recipe {
name: String, Recipe {
id,
title,
estimate_time: None,
difficulty: None,
process: Vec::new(),
}
}
} }
struct Quantity { pub struct Ingredient {
value: f32, pub quantity: Option<Quantity>,
unit: String, pub name: String,
} }
struct Group { pub struct Quantity {
name: Option<String>, pub value: f32,
steps: Vec<Step>, pub unit: String,
} }
struct Step { pub struct Group {
action: String, pub name: Option<String>,
input: Vec<StepInput>, pub steps: Vec<Step>,
output: Vec<IntermediateSubstance>,
} }
struct IntermediateSubstance { pub struct Step {
name: String, pub action: String,
quantity: Option<Quantity>, pub input: Vec<StepInput>,
pub output: Vec<IntermediateSubstance>,
} }
enum StepInput { pub struct IntermediateSubstance {
pub name: String,
pub quantity: Option<Quantity>,
}
pub enum StepInput {
Ingredient(Ingredient), Ingredient(Ingredient),
IntermediateSubstance(IntermediateSubstance), IntermediateSubstance(IntermediateSubstance),
} }
enum Difficulty { pub enum Difficulty {
Unknown, Unknown,
Easy, Easy,
Medium, Medium,

View file

@ -3,8 +3,8 @@
{% block main_container %} {% block main_container %}
<div class="list"> <div class="list">
<ul> <ul>
{% for recipe in recipes %} {% for (id, title) in recipes %}
<li><a href="/recipe/view/{{ recipe.id }}">{{ recipe.title|escape }}</a></li> <li><a href="/recipe/view/{{ id }}">{{ title|escape }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View file

@ -2,6 +2,6 @@
{% block content %} {% block content %}
*** HOME - PUT SOMETHING HERE *** HOME: TODO
{% endblock %} {% endblock %}