aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJoris2025-01-31 22:28:53 +0100
committerJoris2025-01-31 22:28:53 +0100
commit0adf5a093494bdb7f5d5c0f12913133e333ddfad (patch)
treeada6df0f3480647bec99429819f1bfffd36194ce /src
parent24eeb54a6b7159964e8887ade7fa5173b50feb3a (diff)
Migrate to tokio_rusqlite
Diffstat (limited to 'src')
-rw-r--r--src/controller/balance.rs8
-rw-r--r--src/controller/categories.rs13
-rw-r--r--src/controller/incomes.rs25
-rw-r--r--src/controller/login.rs11
-rw-r--r--src/controller/payments.rs30
-rw-r--r--src/controller/statistics.rs6
-rw-r--r--src/controller/wallet.rs4
-rw-r--r--src/db/categories.rs174
-rw-r--r--src/db/incomes.rs656
-rw-r--r--src/db/jobs.rs42
-rw-r--r--src/db/payments.rs827
-rw-r--r--src/db/users.rs180
-rw-r--r--src/db/utils.rs24
-rw-r--r--src/jobs/mod.rs16
-rw-r--r--src/jobs/weekly_report.rs22
-rw-r--r--src/main.rs18
-rw-r--r--src/model/action.rs1
-rw-r--r--src/model/category.rs4
-rw-r--r--src/model/frequency.rs20
-rw-r--r--src/model/income.rs7
-rw-r--r--src/model/job.rs10
-rw-r--r--src/model/payment.rs9
-rw-r--r--src/model/report.rs30
-rw-r--r--src/model/user.rs4
-rw-r--r--src/queries.rs9
-rw-r--r--src/routes.rs14
-rw-r--r--src/templates.rs3
27 files changed, 1128 insertions, 1039 deletions
diff --git a/src/controller/balance.rs b/src/controller/balance.rs
index efe5af6..ae6d810 100644
--- a/src/controller/balance.rs
+++ b/src/controller/balance.rs
@@ -11,18 +11,18 @@ use crate::payer;
use crate::templates;
pub async fn get(wallet: &Wallet) -> Response<Full<Bytes>> {
- let users = db::users::list(&wallet.pool).await;
+ let users = db::users::list(&wallet.db_conn).await;
- let incomes_from = db::incomes::defined_for_all(&wallet.pool).await;
+ let incomes_from = db::incomes::defined_for_all(&wallet.db_conn).await;
let user_incomes = match incomes_from {
- Some(from) => db::incomes::cumulative(&wallet.pool, from).await,
+ Some(from) => db::incomes::cumulative(&wallet.db_conn, from).await,
None => HashMap::new(),
};
let template_user_incomes =
get_template_user_incomes(&users, &user_incomes);
let total_income: i64 = user_incomes.values().sum();
- let user_payments = db::payments::repartition(&wallet.pool).await;
+ let user_payments = db::payments::repartition(&wallet.db_conn).await;
let template_user_payments =
get_template_user_payments(&users, &user_payments);
let total_payments: i64 = user_payments.iter().map(|p| p.1).sum();
diff --git a/src/controller/categories.rs b/src/controller/categories.rs
index fbbd309..8772e38 100644
--- a/src/controller/categories.rs
+++ b/src/controller/categories.rs
@@ -14,7 +14,7 @@ pub async fn table(
wallet: &Wallet,
query: queries::Categories,
) -> Response<Full<Bytes>> {
- let categories = db::categories::list(&wallet.pool).await;
+ let categories = db::categories::list(&wallet.db_conn).await;
let context = minijinja::context!(
header => templates::Header::Categories,
@@ -65,7 +65,7 @@ pub async fn create(
match validation::category::create(&form) {
Some(category) => {
- match db::categories::create(&wallet.pool, &category).await {
+ match db::categories::create(&wallet.db_conn, category).await {
Some(id) => {
utils::redirect(&format!("/categories?highlight={}", id))
}
@@ -86,9 +86,9 @@ async fn update_form_feedback(
form: HashMap<String, String>,
error: Option<String>,
) -> Response<Full<Bytes>> {
- let category = db::categories::get(&wallet.pool, id).await;
+ let category = db::categories::get(&wallet.db_conn, id).await;
let is_category_used =
- db::payments::is_category_used(&wallet.pool, id).await;
+ db::payments::is_category_used(&wallet.db_conn, id).await;
let context = minijinja::context!(
header => templates::Header::Categories,
@@ -119,7 +119,8 @@ pub async fn update(
match validation::category::update(&form) {
Some(update_category) => {
- if db::categories::update(&wallet.pool, id, &update_category).await
+ if db::categories::update(&wallet.db_conn, id, update_category)
+ .await
{
utils::redirect(&format!("/categories?highlight={}", id))
} else {
@@ -131,7 +132,7 @@ pub async fn update(
}
pub async fn delete(id: i64, wallet: &Wallet) -> Response<Full<Bytes>> {
- if db::categories::delete(&wallet.pool, id).await {
+ if db::categories::delete(&wallet.db_conn, id).await {
utils::redirect("/categories")
} else {
update_form_feedback(
diff --git a/src/controller/incomes.rs b/src/controller/incomes.rs
index ac3a332..09d483e 100644
--- a/src/controller/incomes.rs
+++ b/src/controller/incomes.rs
@@ -19,8 +19,8 @@ pub async fn table(
query: queries::Incomes,
) -> Response<Full<Bytes>> {
let page = query.page.unwrap_or(1);
- let count = db::incomes::count(&wallet.pool).await;
- let incomes = db::incomes::list(&wallet.pool, page, PER_PAGE).await;
+ let count = db::incomes::count(&wallet.db_conn).await;
+ let incomes = db::incomes::list(&wallet.db_conn, page, PER_PAGE).await;
let max_page = (count as f32 / PER_PAGE as f32).ceil() as i64;
let context = minijinja::context!(
@@ -68,7 +68,7 @@ async fn create_form_feedback(
form: HashMap<String, String>,
error: Option<String>,
) -> Response<Full<Bytes>> {
- let users = db::users::list(&wallet.pool).await;
+ let users = db::users::list(&wallet.db_conn).await;
let context = minijinja::context!(
header => templates::Header::Incomes,
@@ -101,7 +101,7 @@ pub async fn create(
match validation::income::create(&form) {
Some(income) => {
if !db::incomes::defined_at(
- &wallet.pool,
+ &wallet.db_conn,
income.user_id,
income.date,
)
@@ -110,9 +110,10 @@ pub async fn create(
{
error("Un revenu est déjà défini à cette date.").await
} else {
- match db::incomes::create(&wallet.pool, &income).await {
+ match db::incomes::create(&wallet.db_conn, income).await {
Some(id) => {
- let row = db::incomes::get_row(&wallet.pool, id).await;
+ let row =
+ db::incomes::get_row(&wallet.db_conn, id).await;
let page = (row - 1) / PER_PAGE + 1;
utils::redirect(&format!(
"/incomes?page={}&highlight={}",
@@ -142,8 +143,8 @@ async fn update_form_feedback(
form: HashMap<String, String>,
error: Option<String>,
) -> Response<Full<Bytes>> {
- let users = db::users::list(&wallet.pool).await;
- let income = db::incomes::get(&wallet.pool, id).await;
+ let users = db::users::list(&wallet.db_conn).await;
+ let income = db::incomes::get(&wallet.db_conn, id).await;
let context = minijinja::context!(
header => &templates::Header::Incomes,
@@ -184,15 +185,15 @@ pub async fn update(
match validation::income::update(&form) {
Some(income) => {
let existing_incomes = db::incomes::defined_at(
- &wallet.pool,
+ &wallet.db_conn,
income.user_id,
income.date,
)
.await;
if existing_incomes.into_iter().any(|eid| eid != id) {
error("Un revenu est déjà défini à cette date.").await
- } else if db::incomes::update(&wallet.pool, id, &income).await {
- let row = db::incomes::get_row(&wallet.pool, id).await;
+ } else if db::incomes::update(&wallet.db_conn, id, income).await {
+ let row = db::incomes::get_row(&wallet.db_conn, id).await;
let page = (row - 1) / PER_PAGE + 1;
utils::redirect(&format!(
"/incomes?page={}&highlight={}",
@@ -211,7 +212,7 @@ pub async fn delete(
wallet: &Wallet,
query: queries::Incomes,
) -> Response<Full<Bytes>> {
- if db::incomes::delete(&wallet.pool, id).await {
+ if db::incomes::delete(&wallet.db_conn, id).await {
utils::redirect(&format!("/incomes?page={}", query.page.unwrap_or(1)))
} else {
update_form_feedback(
diff --git a/src/controller/login.rs b/src/controller/login.rs
index 31370cc..d01f799 100644
--- a/src/controller/login.rs
+++ b/src/controller/login.rs
@@ -3,8 +3,8 @@ use http_body_util::Full;
use hyper::body::Bytes;
use hyper::header::SET_COOKIE;
use hyper::Response;
-use sqlx::sqlite::SqlitePool;
use std::collections::HashMap;
+use tokio_rusqlite::Connection;
use crate::controller::utils::with_headers;
use crate::controller::wallet::Wallet;
@@ -35,18 +35,19 @@ pub async fn login(
assets: &HashMap<String, String>,
templates: &minijinja::Environment<'_>,
form: HashMap<String, String>,
- pool: SqlitePool,
+ db_conn: Connection,
) -> Response<Full<Bytes>> {
match validation::login::login(&form) {
Some(login) => {
- match db::users::get_password_hash(&pool, login.email.clone()).await
+ match db::users::get_password_hash(&db_conn, login.email.clone())
+ .await
{
Some(hash) => match bcrypt::verify(login.password, &hash) {
Ok(true) => {
let login_token = cookie::generate_token();
if db::users::set_login_token(
- &pool,
+ &db_conn,
login.email,
login_token.clone().to_string(),
)
@@ -110,7 +111,7 @@ async fn not_authorized(
}
pub async fn logout(config: &Config, wallet: &Wallet) -> Response<Full<Bytes>> {
- if db::users::remove_login_token(&wallet.pool, wallet.user.id).await {
+ if db::users::remove_login_token(&wallet.db_conn, wallet.user.id).await {
with_headers(
utils::redirect("/"),
vec![(SET_COOKIE, &cookie::logout(config))],
diff --git a/src/controller/payments.rs b/src/controller/payments.rs
index b5c0256..1ffa09f 100644
--- a/src/controller/payments.rs
+++ b/src/controller/payments.rs
@@ -19,12 +19,12 @@ pub async fn table(
query: queries::Payments,
) -> Response<Full<Bytes>> {
let page = query.page.unwrap_or(1);
- let count = db::payments::count(&wallet.pool, &query).await;
+ let count = db::payments::count(&wallet.db_conn, &query).await;
let payments =
- db::payments::list_for_table(&wallet.pool, &query, PER_PAGE).await;
+ db::payments::list_for_table(&wallet.db_conn, &query, PER_PAGE).await;
let max_page = (count.count as f32 / PER_PAGE as f32).ceil() as i64;
- let users = db::users::list(&wallet.pool).await;
- let categories = db::categories::list(&wallet.pool).await;
+ let users = db::users::list(&wallet.db_conn).await;
+ let categories = db::categories::list(&wallet.db_conn).await;
let context = minijinja::context!(
header => templates::Header::Payments,
@@ -60,8 +60,8 @@ async fn create_form_feedback(
form: HashMap<String, String>,
error: Option<String>,
) -> Response<Full<Bytes>> {
- let users = db::users::list(&wallet.pool).await;
- let categories = db::categories::list(&wallet.pool).await;
+ let users = db::users::list(&wallet.db_conn).await;
+ let categories = db::categories::list(&wallet.db_conn).await;
let context = minijinja::context!(
header => templates::Header::Payments,
@@ -92,10 +92,10 @@ pub async fn create(
match validation::payment::create(&form) {
Some(create_payment) => {
- match db::payments::create(&wallet.pool, &create_payment).await {
+ match db::payments::create(&wallet.db_conn, create_payment.clone()).await {
Some(id) => {
let row = db::payments::get_row(
- &wallet.pool,
+ &wallet.db_conn,
id,
create_payment.frequency,
)
@@ -139,9 +139,9 @@ async fn update_form_feedback(
form: HashMap<String, String>,
error: Option<String>,
) -> Response<Full<Bytes>> {
- let payment = db::payments::get_for_form(&wallet.pool, id).await;
- let users = db::users::list(&wallet.pool).await;
- let categories = db::categories::list(&wallet.pool).await;
+ let payment = db::payments::get_for_form(&wallet.db_conn, id).await;
+ let users = db::users::list(&wallet.db_conn).await;
+ let categories = db::categories::list(&wallet.db_conn).await;
let context = minijinja::context!(
header => templates::Header::Payments,
@@ -181,10 +181,10 @@ pub async fn update(
match validation::payment::update(&form) {
Some(update_payment) => {
- if db::payments::update(&wallet.pool, id, &update_payment).await {
+ if db::payments::update(&wallet.db_conn, id, update_payment).await {
let frequency = query.frequency.unwrap_or(Frequency::Punctual);
let row =
- db::payments::get_row(&wallet.pool, id, frequency).await;
+ db::payments::get_row(&wallet.db_conn, id, frequency).await;
let page = (row - 1) / PER_PAGE + 1;
// TODO: keep name, cost, user and category when updating a line
let query = queries::Payments {
@@ -212,7 +212,7 @@ pub async fn delete(
wallet: &Wallet,
query: queries::Payments,
) -> Response<Full<Bytes>> {
- if db::payments::delete(&wallet.pool, id).await {
+ if db::payments::delete(&wallet.db_conn, id).await {
let query = queries::Payments {
highlight: None,
..query
@@ -234,7 +234,7 @@ pub async fn search_category(
wallet: &Wallet,
query: queries::PaymentCategory,
) -> Response<Full<Bytes>> {
- match db::payments::search_category(&wallet.pool, query.payment_name).await
+ match db::payments::search_category(&wallet.db_conn, query.payment_name).await
{
Some(category_id) => utils::with_headers(
Response::new(format!("{}", category_id).into()),
diff --git a/src/controller/statistics.rs b/src/controller/statistics.rs
index e57e2be..a546c67 100644
--- a/src/controller/statistics.rs
+++ b/src/controller/statistics.rs
@@ -8,9 +8,9 @@ use crate::db;
use crate::templates;
pub async fn get(wallet: &Wallet) -> Response<Full<Bytes>> {
- let categories = db::categories::list(&wallet.pool).await;
- let payments = db::payments::list_for_stats(&wallet.pool).await;
- let incomes = db::incomes::total_each_month(&wallet.pool).await;
+ let categories = db::categories::list(&wallet.db_conn).await;
+ let payments = db::payments::list_for_stats(&wallet.db_conn).await;
+ let incomes = db::incomes::total_each_month(&wallet.db_conn).await;
let context = minijinja::context!(
header => templates::Header::Statistics,
diff --git a/src/controller/wallet.rs b/src/controller/wallet.rs
index 7537406..edc773d 100644
--- a/src/controller/wallet.rs
+++ b/src/controller/wallet.rs
@@ -1,11 +1,11 @@
-use sqlx::sqlite::SqlitePool;
use std::collections::HashMap;
+use tokio_rusqlite::Connection;
use crate::model::user::User;
#[derive(Clone)]
pub struct Wallet {
- pub pool: SqlitePool,
+ pub db_conn: Connection,
pub assets: HashMap<String, String>,
pub templates: minijinja::Environment<'static>,
pub user: User,
diff --git a/src/db/categories.rs b/src/db/categories.rs
index fafe459..31cb3d0 100644
--- a/src/db/categories.rs
+++ b/src/db/categories.rs
@@ -1,24 +1,38 @@
-use sqlx::sqlite::SqlitePool;
+use tokio_rusqlite::{named_params, Connection, Row};
+use crate::db::utils;
use crate::model::category::{Category, Create, Update};
-pub async fn list(pool: &SqlitePool) -> Vec<Category> {
- let res = sqlx::query_as::<_, Category>(
- r#"
-SELECT
- id,
- name,
- color
-FROM
- categories
-WHERE
- deleted_at IS NULL
-ORDER BY
- name
- "#,
- )
- .fetch_all(pool)
- .await;
+fn row_to_category(row: &Row) -> Result<Category, rusqlite::Error> {
+ Ok(Category {
+ id: row.get(0)?,
+ name: row.get(1)?,
+ color: row.get(2)?,
+ })
+}
+
+pub async fn list(conn: &Connection) -> Vec<Category> {
+ let query = r#"
+ SELECT
+ id,
+ name,
+ color
+ FROM categories
+ WHERE deleted_at IS NULL
+ ORDER BY name
+ "#;
+
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+
+ let users = stmt
+ .query_map([], row_to_category)?
+ .collect::<Result<Vec<Category>, _>>()?;
+
+ Ok(users)
+ })
+ .await;
match res {
Ok(categories) => categories,
@@ -29,26 +43,29 @@ ORDER BY
}
}
-pub async fn get(pool: &SqlitePool, id: i64) -> Option<Category> {
+pub async fn get(conn: &Connection, id: i64) -> Option<Category> {
let query = r#"
-SELECT
- id,
- name,
- color
-FROM
- categories
-WHERE
- id = ?
- AND deleted_at IS NULL
+ SELECT
+ id,
+ name,
+ color
+ FROM categories
+ WHERE
+ id = :id
+ AND deleted_at IS NULL
"#;
- let res = sqlx::query_as::<_, Category>(query)
- .bind(id)
- .fetch_one(pool)
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let mut iter =
+ stmt.query_map(named_params![":id": id], row_to_category)?;
+ utils::one(&mut iter)
+ })
.await;
match res {
- Ok(p) => Some(p),
+ Ok(category) => Some(category),
Err(err) => {
log::error!("Error looking for category {}: {:?}", id, err);
None
@@ -56,22 +73,21 @@ WHERE
}
}
-pub async fn create(pool: &SqlitePool, c: &Create) -> Option<i64> {
- let res = sqlx::query(
- r#"
-INSERT INTO
- categories(name, color)
-VALUES
- (?, ?)
- "#,
- )
- .bind(c.name.clone())
- .bind(c.color.clone())
- .execute(pool)
- .await;
+pub async fn create(conn: &Connection, c: Create) -> Option<i64> {
+ let query = r#"INSERT INTO categories(name, color) VALUES (:name, :color)"#;
+
+ let res = conn
+ .call(move |conn| {
+ conn.execute(
+ query,
+ named_params![":name": c.name, ":color": c.color],
+ )?;
+ Ok(conn.last_insert_rowid())
+ })
+ .await;
match res {
- Ok(x) => Some(x.last_insert_rowid()),
+ Ok(category_id) => Some(category_id),
Err(err) => {
log::error!("Error creating category: {:?}", err);
None
@@ -79,24 +95,24 @@ VALUES
}
}
-pub async fn update(pool: &SqlitePool, id: i64, c: &Update) -> bool {
- let res = sqlx::query(
- r#"
-UPDATE
- categories
-SET
- name = ?,
- color = ?,
- updated_at = datetime()
-WHERE
- id = ?
- "#,
- )
- .bind(c.name.clone())
- .bind(c.color.clone())
- .bind(id)
- .execute(pool)
- .await;
+pub async fn update(conn: &Connection, id: i64, c: Update) -> bool {
+ let query = r#"
+ UPDATE categories
+ SET
+ name = :name,
+ color = :color,
+ updated_at = datetime()
+ WHERE id = :id
+ "#;
+
+ let res = conn
+ .call(move |conn| {
+ Ok(conn.execute(
+ query,
+ named_params![":name": c.name, ":color": c.color, ":id": id],
+ )?)
+ })
+ .await;
match res {
Ok(_) => true,
@@ -107,20 +123,22 @@ WHERE
}
}
-pub async fn delete(pool: &SqlitePool, id: i64) -> bool {
- let res = sqlx::query(
- r#"
-UPDATE
- categories
-SET
- deleted_at = datetime()
-WHERE
- id = ?
- "#,
- )
- .bind(id)
- .execute(pool)
- .await;
+pub async fn delete(conn: &Connection, id: i64) -> bool {
+ let res = conn
+ .call(move |conn| {
+ Ok(conn.execute(
+ r#"
+ UPDATE
+ categories
+ SET
+ deleted_at = datetime()
+ WHERE
+ id = :id
+ "#,
+ named_params![":id": id],
+ )?)
+ })
+ .await;
match res {
Ok(_) => true,
diff --git a/src/db/incomes.rs b/src/db/incomes.rs
index 97cb2b7..90282c0 100644
--- a/src/db/incomes.rs
+++ b/src/db/incomes.rs
@@ -1,26 +1,51 @@
use chrono::NaiveDate;
-use sqlx::error::Error;
-use sqlx::sqlite::{SqlitePool, SqliteRow};
-use sqlx_core::row::Row;
use std::collections::HashMap;
use std::iter::FromIterator;
+use tokio_rusqlite::{named_params, Connection, Row};
+use crate::db::utils;
use crate::model::income::{Create, Form, Stat, Table, Update};
use crate::model::report::Report;
-pub async fn count(pool: &SqlitePool) -> i64 {
+fn row_to_table(row: &Row) -> Result<Table, rusqlite::Error> {
+ Ok(Table {
+ id: row.get(0)?,
+ date: row.get(1)?,
+ user: row.get(2)?,
+ amount: row.get(3)?,
+ })
+}
+
+fn row_to_form(row: &Row) -> Result<Form, rusqlite::Error> {
+ Ok(Form {
+ id: row.get(0)?,
+ amount: row.get(1)?,
+ user_id: row.get(2)?,
+ month: row.get(3)?,
+ year: row.get(4)?,
+ })
+}
+
+fn row_to_stat(row: &Row) -> Result<Stat, rusqlite::Error> {
+ Ok(Stat {
+ date: row.get(0)?,
+ amount: row.get(1)?,
+ })
+}
+
+pub async fn count(conn: &Connection) -> i64 {
let query = r#"
-SELECT
- COUNT(*) AS count
-FROM
- incomes
-WHERE
- incomes.deleted_at IS NULL
- "#;
-
- let res = sqlx::query(query)
- .map(|row: SqliteRow| row.get("count"))
- .fetch_one(pool)
+ SELECT COUNT(*)
+ FROM incomes
+ WHERE incomes.deleted_at IS NULL
+ "#;
+
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let mut iter = stmt.query_map([], |row| row.get(0))?;
+ utils::one::<i64, _>(&mut iter)
+ })
.await;
match res {
@@ -32,32 +57,35 @@ WHERE
}
}
-pub async fn list(pool: &SqlitePool, page: i64, per_page: i64) -> Vec<Table> {
+pub async fn list(conn: &Connection, page: i64, per_page: i64) -> Vec<Table> {
let query = r#"
-SELECT
- incomes.id,
- users.name AS user,
- strftime('%m/%Y', incomes.date) AS date,
- incomes.amount
-FROM
- incomes
-INNER JOIN
- users
-ON
- incomes.user_id = users.id
-WHERE
- incomes.deleted_at IS NULL
-ORDER BY
- incomes.date DESC
-LIMIT ?
-OFFSET ?
+ SELECT
+ incomes.id,
+ users.name AS user,
+ strftime('%m/%Y', incomes.date) AS date,
+ incomes.amount
+ FROM incomes
+ INNER JOIN users
+ ON incomes.user_id = users.id
+ WHERE incomes.deleted_at IS NULL
+ ORDER BY incomes.date DESC
+ LIMIT :limit
+ OFFSET :offset
"#;
- let res = sqlx::query_as::<_, Table>(query)
- .bind(per_page)
- .bind((page - 1) * per_page)
- .fetch_all(pool)
- .await;
+ let res = conn.call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+
+ let incomes = stmt
+ .query_map(
+ named_params![":limit": per_page, ":offset": (page - 1) * per_page],
+ row_to_table
+ )?
+ .collect::<Result<Vec<Table>, _>>()?;
+
+ Ok(incomes)
+ })
+ .await;
match res {
Ok(incomes) => incomes,
@@ -68,31 +96,30 @@ OFFSET ?
}
}
-pub async fn get_row(pool: &SqlitePool, id: i64) -> i64 {
+pub async fn get_row(conn: &Connection, id: i64) -> i64 {
let query = r#"
-SELECT
- row
-FROM (
- SELECT
- ROW_NUMBER () OVER (ORDER BY date DESC) AS row,
- id
- FROM
- incomes
- WHERE
- deleted_at IS NULL
-)
-WHERE
- id = ?
+ SELECT row
+ FROM (
+ SELECT
+ ROW_NUMBER () OVER (ORDER BY date DESC) AS row,
+ id
+ FROM incomes
+ WHERE deleted_at IS NULL
+ )
+ WHERE id = :id
"#;
- let res = sqlx::query(query)
- .bind(id)
- .map(|row: SqliteRow| row.get("row"))
- .fetch_one(pool)
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let mut iter =
+ stmt.query_map(named_params![":id": id], |row| row.get(0))?;
+ utils::one::<i64, _>(&mut iter)
+ })
.await;
match res {
- Ok(count) => count,
+ Ok(row) => row,
Err(err) => {
log::error!("Error getting income row: {:?}", err);
1
@@ -100,28 +127,31 @@ WHERE
}
}
-pub async fn get(pool: &SqlitePool, id: i64) -> Option<Form> {
+pub async fn get(conn: &Connection, id: i64) -> Option<Form> {
let query = r#"
-SELECT
- id,
- amount,
- user_id,
- CAST(strftime('%m', date) AS INTEGER) as month,
- CAST(strftime('%Y', date) AS INTEGER) as year
-FROM
- incomes
-WHERE
- id = ?
- AND deleted_at IS NULL
+ SELECT
+ id,
+ amount,
+ user_id,
+ CAST(strftime('%m', date) AS INTEGER) as month,
+ CAST(strftime('%Y', date) AS INTEGER) as year
+ FROM incomes
+ WHERE
+ id = :id
+ AND deleted_at IS NULL
"#;
- let res = sqlx::query_as::<_, Form>(query)
- .bind(id)
- .fetch_one(pool)
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let mut iter =
+ stmt.query_map(named_params![":id": id], row_to_form)?;
+ utils::one(&mut iter)
+ })
.await;
match res {
- Ok(p) => Some(p),
+ Ok(income) => Some(income),
Err(err) => {
log::error!("Error looking for income {}: {:?}", id, err);
None
@@ -129,23 +159,28 @@ WHERE
}
}
-pub async fn create(pool: &SqlitePool, i: &Create) -> Option<i64> {
- let res = sqlx::query(
- r#"
-INSERT INTO
- incomes(user_id, date, amount)
-VALUES
- (?, ?, ?)
- "#,
- )
- .bind(i.user_id)
- .bind(i.date)
- .bind(i.amount)
- .execute(pool)
- .await;
+pub async fn create(conn: &Connection, i: Create) -> Option<i64> {
+ let query = r#"
+ INSERT INTO incomes(user_id, date, amount)
+ VALUES (:user_id, :date, :amount)
+ "#;
+
+ let res = conn
+ .call(move |conn| {
+ conn.execute(
+ query,
+ named_params![
+ ":user_id": i.user_id,
+ ":date": i.date,
+ ":amount": i.amount
+ ],
+ )?;
+ Ok(conn.last_insert_rowid())
+ })
+ .await;
match res {
- Ok(x) => Some(x.last_insert_rowid()),
+ Ok(income_id) => Some(income_id),
Err(err) => {
log::error!("Error creating income: {:?}", err);
None
@@ -154,31 +189,36 @@ VALUES
}
pub async fn defined_at(
- pool: &SqlitePool,
+ conn: &Connection,
user_id: i64,
date: NaiveDate,
) -> Vec<i64> {
let query = r#"
-SELECT
- id
-FROM
- incomes
-WHERE
- user_id = ?
- AND date = ?
- AND deleted_at IS NULL
+ SELECT id
+ FROM incomes
+ WHERE
+ user_id = :user_id
+ AND date = :date
+ AND deleted_at IS NULL
"#;
- let res = sqlx::query(query)
- .bind(user_id)
- .bind(date)
- .map(|row: SqliteRow| row.get("id"))
- .fetch_all(pool)
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+
+ let incomes = stmt
+ .query_map(
+ named_params![":user_id": user_id, ":date": date],
+ |row| row.get(0),
+ )?
+ .collect::<Result<Vec<i64>, _>>()?;
+
+ Ok(incomes)
+ })
.await;
match res {
Ok(ids) => ids,
- Err(Error::RowNotFound) => vec![],
Err(err) => {
log::error!("Error looking if income is defined: {:?}", err);
vec![]
@@ -186,26 +226,30 @@ WHERE
}
}
-pub async fn update(pool: &SqlitePool, id: i64, i: &Update) -> bool {
- let res = sqlx::query(
- r#"
-UPDATE
- incomes
-SET
- user_id = ?,
- date = ?,
- amount = ?,
- updated_at = datetime()
-WHERE
- id = ?
- "#,
- )
- .bind(i.user_id)
- .bind(i.date)
- .bind(i.amount)
- .bind(id)
- .execute(pool)
- .await;
+pub async fn update(conn: &Connection, id: i64, i: Update) -> bool {
+ let query = r#"
+ UPDATE incomes
+ SET
+ user_id = :user_id,
+ date = :date,
+ amount = :amount,
+ updated_at = datetime()
+ WHERE id = :id
+ "#;
+
+ let res = conn
+ .call(move |conn| {
+ Ok(conn.execute(
+ query,
+ named_params![
+ ":user_id": i.user_id,
+ ":date": i.date,
+ ":amount": i.amount,
+ ":id": id
+ ],
+ )?)
+ })
+ .await;
match res {
Ok(_) => true,
@@ -216,20 +260,12 @@ WHERE
}
}
-pub async fn delete(pool: &SqlitePool, id: i64) -> bool {
- let res = sqlx::query(
- r#"
-UPDATE
- incomes
-SET
- deleted_at = datetime()
-WHERE
- id = ?
- "#,
- )
- .bind(id)
- .execute(pool)
- .await;
+pub async fn delete(conn: &Connection, id: i64) -> bool {
+ let query = r#"UPDATE incomes SET deleted_at = datetime() WHERE id = :id"#;
+
+ let res = conn
+ .call(move |conn| Ok(conn.execute(query, named_params![":id": id])?))
+ .await;
match res {
Ok(_) => true,
@@ -240,37 +276,34 @@ WHERE
}
}
-pub async fn defined_for_all(pool: &SqlitePool) -> Option<NaiveDate> {
- let res = sqlx::query(
- r#"
-SELECT
- (CASE COUNT(users.id) == COUNT(min_income.date)
- WHEN 1 THEN MIN(min_income.date)
- ELSE NULL
- END) AS date
-FROM
- users
-LEFT OUTER JOIN (
- SELECT
- user_id,
- MIN(date) AS date
- FROM
- incomes
- WHERE
- deleted_at IS NULL
- GROUP BY
- user_id
-) min_income
-ON
- users.id = min_income.user_id;
- "#,
- )
- .map(|row: SqliteRow| row.get("date"))
- .fetch_one(pool)
- .await;
+pub async fn defined_for_all(conn: &Connection) -> Option<NaiveDate> {
+ let query = r#"
+ SELECT
+ (CASE COUNT(users.id) == COUNT(min_income.date)
+ WHEN 1 THEN MIN(min_income.date)
+ ELSE NULL
+ END) AS date
+ FROM users
+ LEFT OUTER JOIN
+ (SELECT
+ user_id,
+ MIN(date) AS date
+ FROM incomes
+ WHERE deleted_at IS NULL
+ GROUP BY user_id) min_income
+ ON users.id = min_income.user_id;
+ "#;
+
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let mut iter = stmt.query_map([], |row| row.get(0))?;
+ utils::one::<NaiveDate, _>(&mut iter)
+ })
+ .await;
match res {
- Ok(d) => d,
+ Ok(d) => Some(d),
Err(err) => {
log::error!("Error looking for incomes defined for all: {:?}", err);
None
@@ -279,12 +312,17 @@ ON
}
pub async fn cumulative(
- pool: &SqlitePool,
+ conn: &Connection,
from: NaiveDate,
) -> HashMap<i64, i64> {
- let res = sqlx::query(&cumulative_query(from))
- .map(|row: SqliteRow| (row.get("user_id"), row.get("income")))
- .fetch_all(pool)
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(&cumulative_query(from))?;
+ let incomes = stmt
+ .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
+ .collect::<Result<Vec<(i64, i64)>, _>>()?;
+ Ok(incomes)
+ })
.await;
match res {
@@ -306,34 +344,34 @@ pub async fn cumulative(
fn cumulative_query(from: NaiveDate) -> String {
format!(
r#"
-SELECT
- users.id AS user_id,
- COALESCE(incomes.income, 0) AS income
-FROM
- users
-LEFT OUTER JOIN (
- SELECT
- user_id,
- CAST(ROUND(SUM(count)) AS INTEGER) AS income
- FROM (
SELECT
- I1.user_id,
- ((JULIANDAY(MIN(I2.date)) - JULIANDAY(I1.date)) * I1.amount * 12 / 365) AS count
+ users.id AS user_id,
+ COALESCE(incomes.income, 0) AS income
FROM
- ({}) AS I1
- INNER JOIN
- ({}) AS I2
+ users
+ LEFT OUTER JOIN (
+ SELECT
+ user_id,
+ CAST(ROUND(SUM(count)) AS INTEGER) AS income
+ FROM (
+ SELECT
+ I1.user_id,
+ ((JULIANDAY(MIN(I2.date)) - JULIANDAY(I1.date)) * I1.amount * 12 / 365) AS count
+ FROM
+ ({}) AS I1
+ INNER JOIN
+ ({}) AS I2
+ ON
+ I2.date > I1.date
+ AND I2.user_id == I1.user_id
+ GROUP BY
+ I1.date, I1.user_id
+ )
+ GROUP BY
+ user_id
+ ) incomes
ON
- I2.date > I1.date
- AND I2.user_id == I1.user_id
- GROUP BY
- I1.date, I1.user_id
- )
- GROUP BY
- user_id
-) incomes
-ON
- users.id = incomes.user_id
+ users.id = incomes.user_id
"#,
bounded_query(">".to_string(), from.format("%Y-%m-%d").to_string()),
bounded_query("<".to_string(), "date()".to_string())
@@ -347,33 +385,33 @@ ON
fn bounded_query(op: String, date: String) -> String {
format!(
r#"
-SELECT
- user_id,
- date,
- amount
-FROM (
- SELECT
- user_id,
- {} AS date,
- amount,
- MAX(date) AS max_date
- FROM
- incomes
- WHERE
- date <= {}
- AND deleted_at IS NULL
- GROUP BY
- user_id
-) UNION
-SELECT
- user_id,
- date,
- amount
-FROM
- incomes
-WHERE
- date {} {}
- AND deleted_at IS NULL
+ SELECT
+ user_id,
+ date,
+ amount
+ FROM (
+ SELECT
+ user_id,
+ {} AS date,
+ amount,
+ MAX(date) AS max_date
+ FROM
+ incomes
+ WHERE
+ date <= {}
+ AND deleted_at IS NULL
+ GROUP BY
+ user_id
+ ) UNION
+ SELECT
+ user_id,
+ date,
+ amount
+ FROM
+ incomes
+ WHERE
+ date {} {}
+ AND deleted_at IS NULL
"#,
date, date, op, date
)
@@ -383,54 +421,63 @@ WHERE
///
/// For each month, from the first defined income and until now,
/// compute the total income of the users.
-pub async fn total_each_month(pool: &SqlitePool) -> Vec<Stat> {
+pub async fn total_each_month(conn: &Connection) -> Vec<Stat> {
let query = r#"
-WITH RECURSIVE dates(date) AS (
- VALUES((
- SELECT
- strftime('%Y-%m-01', MIN(date))
- FROM
- incomes
- WHERE
- deleted_at IS NULL
- ))
- UNION ALL
- SELECT
- date(date, '+1 month')
- FROM
- dates
- WHERE
- date < date(date(), '-1 month')
-)
-SELECT
- strftime('%Y-%m-01', dates.date) AS date,
- (
- SELECT
- SUM(amount) AS amount
- FROM (
- SELECT (
+ WITH RECURSIVE dates(date) AS (
+ VALUES((
SELECT
- amount
+ strftime('%Y-%m-01', MIN(date))
FROM
incomes
WHERE
- user_id = users.id
- AND date < date(dates.date, '+1 month')
- AND deleted_at IS NULL
- ORDER BY
- date DESC
- LIMIT
- 1
- ) AS amount
+ deleted_at IS NULL
+ ))
+ UNION ALL
+ SELECT
+ date(date, '+1 month')
FROM
- users
+ dates
+ WHERE
+ date < date(date(), '-1 month')
)
- ) AS amount
-FROM
- dates;
+ SELECT
+ strftime('%Y-%m-01', dates.date) AS date,
+ (
+ SELECT
+ SUM(amount) AS amount
+ FROM (
+ SELECT (
+ SELECT
+ amount
+ FROM
+ incomes
+ WHERE
+ user_id = users.id
+ AND date < date(dates.date, '+1 month')
+ AND deleted_at IS NULL
+ ORDER BY
+ date DESC
+ LIMIT
+ 1
+ ) AS amount
+ FROM
+ users
+ )
+ ) AS amount
+ FROM
+ dates;
"#;
- let res = sqlx::query_as::<_, Stat>(query).fetch_all(pool).await;
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let stats = stmt
+ .query_map([], row_to_stat)?
+ .collect::<Result<Vec<Stat>, _>>()?;
+
+ Ok(stats)
+ })
+ .await;
match res {
Ok(xs) => xs,
@@ -441,50 +488,59 @@ FROM
}
}
-pub async fn last_week(pool: &SqlitePool) -> Vec<Report> {
+pub async fn last_week(conn: &Connection) -> Vec<Report> {
let query = r#"
-SELECT
- strftime('%m/%Y', incomes.date) AS date,
- users.name AS name,
- incomes.amount AS amount,
- (CASE
- WHEN
- incomes.deleted_at IS NOT NULL
- THEN
- 'Deleted'
- WHEN
- incomes.updated_at IS NOT NULL
- AND incomes.created_at < date('now', 'weekday 0', '-13 days')
- THEN
- 'Updated'
- ELSE
- 'Created'
- END) AS action
-FROM
- incomes
-INNER JOIN
- users
-ON
- incomes.user_id = users.id
-WHERE
- (
- incomes.created_at >= date('now', 'weekday 0', '-13 days')
- AND incomes.created_at < date('now', 'weekday 0', '-6 days')
- ) OR (
- incomes.updated_at >= date('now', 'weekday 0', '-13 days')
- AND incomes.updated_at < date('now', 'weekday 0', '-6 days')
- ) OR (
- incomes.deleted_at >= date('now', 'weekday 0', '-13 days')
- AND incomes.deleted_at < date('now', 'weekday 0', '-6 days')
- )
-ORDER BY
- incomes.date
+ SELECT
+ strftime('%m/%Y', incomes.date) AS date,
+ users.name AS name,
+ incomes.amount AS amount,
+ (CASE
+ WHEN
+ incomes.deleted_at IS NOT NULL
+ THEN
+ 'Deleted'
+ WHEN
+ incomes.updated_at IS NOT NULL
+ AND incomes.created_at < date('now', 'weekday 0', '-13 days')
+ THEN
+ 'Updated'
+ ELSE
+ 'Created'
+ END) AS action
+ FROM
+ incomes
+ INNER JOIN
+ users
+ ON
+ incomes.user_id = users.id
+ WHERE
+ (
+ incomes.created_at >= date('now', 'weekday 0', '-13 days')
+ AND incomes.created_at < date('now', 'weekday 0', '-6 days')
+ ) OR (
+ incomes.updated_at >= date('now', 'weekday 0', '-13 days')
+ AND incomes.updated_at < date('now', 'weekday 0', '-6 days')
+ ) OR (
+ incomes.deleted_at >= date('now', 'weekday 0', '-13 days')
+ AND incomes.deleted_at < date('now', 'weekday 0', '-6 days')
+ )
+ ORDER BY
+ incomes.date
"#;
- let res = sqlx::query_as::<_, Report>(query).fetch_all(pool).await;
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let xs = stmt
+ .query_map([], utils::row_to_report)?
+ .collect::<Result<Vec<Report>, _>>()?;
+
+ Ok(xs)
+ })
+ .await;
match res {
- Ok(payments) => payments,
+ Ok(xs) => xs,
Err(err) => {
log::error!("Error listing payments for report: {:?}", err);
vec![]
diff --git a/src/db/jobs.rs b/src/db/jobs.rs
index 7d9386a..1d00408 100644
--- a/src/db/jobs.rs
+++ b/src/db/jobs.rs
@@ -1,32 +1,27 @@
-use sqlx::error::Error;
-use sqlx::sqlite::SqlitePool;
+use tokio_rusqlite::{named_params, Connection};
use crate::model::job::Job;
-pub async fn should_run(pool: &SqlitePool, job: Job) -> bool {
+pub async fn should_run(conn: &Connection, job: Job) -> bool {
let run_from = match job {
Job::WeeklyReport => "date('now', 'weekday 0', '-6 days')",
Job::MonthlyPayment => "date('now', 'start of month')",
};
let query = format!(
- r#"
-SELECT
- 1
-FROM
- jobs
-WHERE
- name = ?
- AND last_execution < {}
- "#,
+ r#"SELECT 1 FROM jobs WHERE name = ? AND last_execution < {}"#,
run_from
);
- let res = sqlx::query(&query).bind(job).fetch_one(pool).await;
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(&query)?;
+ Ok(stmt.exists([job.to_string()])?)
+ })
+ .await;
match res {
- Ok(_) => true,
- Err(Error::RowNotFound) => false,
+ Ok(r) => r,
Err(err) => {
log::error!("Error looking if job should run: {:?}", err);
false
@@ -34,17 +29,18 @@ WHERE
}
}
-pub async fn actualize_last_execution(pool: &SqlitePool, job: Job) {
+pub async fn actualize_last_execution(conn: &Connection, job: Job) {
let query = r#"
-UPDATE
- jobs
-SET
- last_execution = datetime()
-WHERE
- name = ?
+ UPDATE jobs
+ SET last_execution = datetime()
+ WHERE name = :name
"#;
- let res = sqlx::query(query).bind(job).execute(pool).await;
+ let res = conn
+ .call(move |conn| {
+ Ok(conn.execute(query, named_params![":name": job.to_string()])?)
+ })
+ .await;
match res {
Ok(_) => (),
diff --git a/src/db/payments.rs b/src/db/payments.rs
index 25b10f4..540a006 100644
--- a/src/db/payments.rs
+++ b/src/db/payments.rs
@@ -1,10 +1,6 @@
-use sqlx::error::Error;
-use sqlx::sqlite::{Sqlite, SqliteArguments};
-use sqlx::sqlite::{SqlitePool, SqliteRow};
-use sqlx::FromRow;
-use sqlx_core::row::Row;
use std::collections::HashMap;
use std::iter::FromIterator;
+use tokio_rusqlite::{Connection, Row, params_from_iter, types::ToSql, named_params};
use crate::db::utils;
use crate::model::frequency::Frequency;
@@ -13,71 +9,75 @@ use crate::model::report::Report;
use crate::queries;
use crate::utils::text;
-#[derive(FromRow)]
pub struct Count {
pub count: i64,
pub total_cost: i64,
}
+fn row_to_count(row: &Row) -> Result<Count, rusqlite::Error> {
+ Ok(Count {
+ count: row.get(0)?,
+ total_cost: row.get(1)?,
+ })
+}
+
+fn row_to_table(row: &Row) -> Result<payment::Table, rusqlite::Error> {
+ Ok(payment::Table {
+ id: row.get(0)?,
+ name: row.get(1)?,
+ cost: row.get(2)?,
+ user: row.get(3)?,
+ category_name: row.get(4)?,
+ category_color: row.get(5)?,
+ date: row.get(6)?,
+ frequency: row.get(7)?
+ })
+}
+
+fn row_to_stat(row: &Row) -> Result<payment::Stat, rusqlite::Error> {
+ Ok(payment::Stat {
+ start_date: row.get(0)?,
+ cost: row.get(1)?,
+ category_id: row.get(2)?,
+ })
+}
+
+fn row_to_form(row: &Row) -> Result<payment::Form, rusqlite::Error> {
+ Ok(payment::Form {
+ id: row.get(0)?,
+ name: row.get(1)?,
+ cost: row.get(2)?,
+ user_id: row.get(3)?,
+ category_id: row.get(4)?,
+ date: row.get(5)?,
+ frequency: row.get(6)?
+ })
+}
+
pub async fn count(
- pool: &SqlitePool,
+ conn: &Connection,
payment_query: &queries::Payments,
) -> Count {
- let query = format!(
- r#"
-SELECT
- COUNT(*) AS count,
- SUM(payments.cost) AS total_cost
-FROM
- payments
-INNER JOIN
- users ON users.id = payments.user_id
-INNER JOIN
- categories ON categories.id = payments.category_id
-WHERE
- payments.deleted_at IS NULL
- AND payments.frequency = ?
- {} {} {} {} {} {}
- "#,
- name_query(payment_query.name.clone()),
- cost_query(payment_query.cost.clone()),
- user_query(payment_query.user),
- category_query(payment_query.category),
- date_query(
- "payments.date >=".to_string(),
- payment_query.start_date.clone()
- ),
- date_query(
- "payments.date <=".to_string(),
- payment_query.end_date.clone()
- )
- );
-
- let res = bind_date(
- bind_date(
- bind_category(
- bind_user(
- bind_cost(
- bind_name(
- sqlx::query_as::<_, Count>(&query).bind(
- payment_query
- .frequency
- .unwrap_or(Frequency::Punctual),
- ),
- payment_query.name.clone(),
- ),
- payment_query.cost.clone(),
- ),
- payment_query.user,
- ),
- payment_query.category,
- ),
- payment_query.start_date.clone(),
- ),
- payment_query.end_date.clone(),
- )
- .fetch_one(pool)
- .await;
+ let mut query = format!(r#"
+ SELECT
+ COUNT(*) AS count,
+ SUM(payments.cost) AS total_cost
+ FROM payments
+ INNER JOIN users ON users.id = payments.user_id
+ INNER JOIN categories ON categories.id = payments.category_id
+ WHERE payments.deleted_at IS NULL
+ "#);
+
+ let mut params = Vec::<Box<dyn ToSql + Send>>::new();
+ complete_search_query(payment_query, &mut query, &mut params);
+
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(&query)?;
+ let mut iter = stmt.query_map(params_from_iter(params), row_to_count)?;
+ utils::one(&mut iter)
+ })
+ .await;
match res {
Ok(count) => count,
@@ -92,79 +92,42 @@ WHERE
}
pub async fn list_for_table(
- pool: &SqlitePool,
+ conn: &Connection,
payment_query: &queries::Payments,
per_page: i64,
) -> Vec<payment::Table> {
- let offset = (payment_query.page.unwrap_or(1) - 1) * per_page;
-
- let query = format!(
- r#"
-SELECT
- payments.id,
- payments.name,
- payments.cost,
- users.name AS user,
- categories.name AS category_name,
- categories.color AS category_color,
- strftime('%d/%m/%Y', date) AS date,
- payments.frequency AS frequency
-FROM
- payments
-INNER JOIN
- users ON users.id = payments.user_id
-INNER JOIN
- categories ON categories.id = payments.category_id
-WHERE
- payments.deleted_at IS NULL
- AND payments.frequency = ?
- {} {} {} {} {} {}
-ORDER BY
- payments.date DESC
-LIMIT ?
-OFFSET ?
- "#,
- name_query(payment_query.name.clone()),
- cost_query(payment_query.cost.clone()),
- user_query(payment_query.user),
- category_query(payment_query.category),
- date_query(
- "payments.date >=".to_string(),
- payment_query.start_date.clone()
- ),
- date_query(
- "payments.date <=".to_string(),
- payment_query.end_date.clone()
- )
- );
-
- let res = bind_date(
- bind_date(
- bind_category(
- bind_user(
- bind_cost(
- bind_name(
- sqlx::query_as::<_, payment::Table>(&query).bind(
- payment_query
- .frequency
- .unwrap_or(Frequency::Punctual),
- ),
- payment_query.name.clone(),
- ),
- payment_query.cost.clone(),
- ),
- payment_query.user,
- ),
- payment_query.category,
- ),
- payment_query.start_date.clone(),
- ),
- payment_query.end_date.clone(),
- )
- .bind(per_page)
- .bind(offset)
- .fetch_all(pool)
- .await;
+ let mut query = format!(r#"
+ SELECT
+ payments.id,
+ payments.name,
+ payments.cost,
+ users.name AS user,
+ categories.name AS category_name,
+ categories.color AS category_color,
+ strftime('%d/%m/%Y', date) AS date,
+ payments.frequency AS frequency
+ FROM payments
+ INNER JOIN users ON users.id = payments.user_id
+ INNER JOIN categories ON categories.id = payments.category_id
+ WHERE payments.deleted_at IS NULL
+ "#);
+
+ let mut params = Vec::<Box<dyn ToSql + Send>>::new();
+ complete_search_query(payment_query, &mut query, &mut params);
+
+ query.push_str("ORDER BY payments.date DESC LIMIT ? OFFSET ?");
+ params.push(Box::new(per_page));
+ params.push(Box::new((payment_query.page.unwrap_or(1) - 1) * per_page));
+
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(&query)?;
+ let payments = stmt
+ .query_map(params_from_iter(params), row_to_table)?
+ .collect::<Result<Vec<payment::Table>, _>>()?;
+ Ok(payments)
+ })
+ .await;
match res {
Ok(payments) => payments,
@@ -175,137 +138,101 @@ OFFSET ?
}
}
-fn name_query(name: Option<String>) -> String {
- if name.map_or_else(|| false, |str| !str.is_empty()) {
- format!(
- "AND {} LIKE ?",
- utils::format_key_for_search("payments.name")
- )
- } else {
- "".to_string()
- }
-}
-
-fn bind_name<'a, Row: FromRow<'a, SqliteRow>>(
- query: sqlx::query::QueryAs<'a, Sqlite, Row, SqliteArguments<'a>>,
- name: Option<String>,
-) -> sqlx::query::QueryAs<'a, Sqlite, Row, SqliteArguments<'a>> {
- match name {
- Some(str) => {
- if str.is_empty() {
- query
- } else {
- query.bind(text::format_search(&str))
- }
- }
- _ => query,
- }
+fn complete_search_query(
+ q: &queries::Payments,
+ query: &mut String,
+ params: &mut Vec<Box<dyn ToSql + Send>>
+) {
+ complete_frequency(q.frequency, query, params);
+ complete_name(q.name.clone(), query, params);
+ complete_cost(q.cost.clone(), query, params);
+ complete_user(q.user, query, params);
+ complete_category(q.category, query, params);
+ complete_date("payments.date >=".to_string(), q.start_date.clone(), query, params);
+ complete_date("payments.date <=".to_string(), q.end_date.clone(), query, params);
}
-fn cost_query(cost: Option<String>) -> String {
- if cost.map_or_else(|| false, |str| !str.is_empty()) {
- "AND payments.cost = ?".to_string()
- } else {
- "".to_string()
- }
+fn complete_frequency(frequency: Option<Frequency>, query: &mut String, params: &mut Vec<Box<dyn ToSql + Send>>) {
+ query.push_str("AND payments.frequency = ?");
+ params.push(Box::new(frequency.unwrap_or(Frequency::Punctual).to_string()));
}
-fn bind_cost<'a, Row: FromRow<'a, SqliteRow>>(
- query: sqlx::query::QueryAs<'a, Sqlite, Row, SqliteArguments<'a>>,
- cost: Option<String>,
-) -> sqlx::query::QueryAs<'a, Sqlite, Row, SqliteArguments<'a>> {
- match cost {
- Some(str) => {
- if str.is_empty() {
- query
- } else {
- query.bind(str)
- }
+fn complete_name(name: Option<String>, query: &mut String, params: &mut Vec<Box<dyn ToSql + Send>>) {
+ if let Some(name) = name {
+ if !name.is_empty() {
+ query.push_str(format!(
+ "AND {} LIKE ?",
+ utils::format_key_for_search("payments.name")
+ ).as_str());
+ params.push(Box::new(name));
}
- _ => query,
- }
-}
-
-fn user_query(user: Option<i64>) -> String {
- if user.is_some() {
- "AND payments.user_id = ?".to_string()
- } else {
- "".to_string()
}
}
-fn bind_user<'a, Row: FromRow<'a, SqliteRow>>(
- query: sqlx::query::QueryAs<'a, Sqlite, Row, SqliteArguments<'a>>,
- user: Option<i64>,
-) -> sqlx::query::QueryAs<'a, Sqlite, Row, SqliteArguments<'a>> {
- match user {
- Some(id) => query.bind(id),
- _ => query,
- }
-}
-
-fn category_query(category: Option<i64>) -> String {
- if category.is_some() {
- "AND payments.category_id = ?".to_string()
- } else {
- "".to_string()
+fn complete_cost(cost: Option<String>, query: &mut String, params: &mut Vec<Box<dyn ToSql + Send>>) {
+ if let Some(cost) = cost {
+ if !cost.is_empty() {
+ query.push_str("AND payments.cost = ?");
+ params.push(Box::new(cost))
+ }
}
}
-fn bind_category<'a, Row: FromRow<'a, SqliteRow>>(
- query: sqlx::query::QueryAs<'a, Sqlite, Row, SqliteArguments<'a>>,
- category: Option<i64>,
-) -> sqlx::query::QueryAs<'a, Sqlite, Row, SqliteArguments<'a>> {
- match category {
- Some(id) => query.bind(id),
- _ => query,
+fn complete_user(user: Option<i64>, query: &mut String, params: &mut Vec<Box<dyn ToSql + Send>>) {
+ if let Some(user) = user {
+ query.push_str("AND payments.user_id = ?");
+ params.push(Box::new(user))
}
}
-fn date_query(name_and_op: String, date: Option<String>) -> String {
- if date.map_or_else(|| false, |str| !str.is_empty()) {
- format!("AND {} ?", name_and_op)
- } else {
- "".to_string()
+fn complete_category(category: Option<i64>, query: &mut String, params: &mut Vec<Box<dyn ToSql + Send>>) {
+ if let Some(category) = category {
+ query.push_str("AND payments.category_id = ?");
+ params.push(Box::new(category));
}
}
-fn bind_date<'a, Row: FromRow<'a, SqliteRow>>(
- query: sqlx::query::QueryAs<'a, Sqlite, Row, SqliteArguments<'a>>,
+fn complete_date(
+ name_and_op: String,
date: Option<String>,
-) -> sqlx::query::QueryAs<'a, Sqlite, Row, SqliteArguments<'a>> {
- match date {
- Some(d) => {
- if d.is_empty() {
- query
- } else {
- query.bind(d)
- }
+ query: &mut String,
+ params: &mut Vec<Box<dyn ToSql + Send>>
+) {
+ if let Some(date) = date {
+ if !date.is_empty() {
+ query.push_str(format!("AND {name_and_op} ?").as_str());
+ params.push(Box::new(date));
}
- _ => query,
}
}
-pub async fn list_for_stats(pool: &SqlitePool) -> Vec<payment::Stat> {
+
+pub async fn list_for_stats(conn: &Connection) -> Vec<payment::Stat> {
let query = r#"
-SELECT
- strftime('%Y-%m-01', payments.date) AS start_date,
- SUM(payments.cost) AS cost,
- payments.category_id AS category_id
-FROM
- payments
-WHERE
- payments.deleted_at IS NULL
- AND payments.frequency = 'Punctual'
-GROUP BY
- start_date,
- payments.category_id;
+ SELECT
+ strftime('%Y-%m-01', payments.date) AS start_date,
+ SUM(payments.cost) AS cost,
+ payments.category_id AS category_id
+ FROM
+ payments
+ WHERE
+ payments.deleted_at IS NULL
+ AND payments.frequency = 'Punctual'
+ GROUP BY
+ start_date,
+ payments.category_id;
"#;
- let result = sqlx::query_as::<_, payment::Stat>(query)
- .fetch_all(pool)
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(&query)?;
+ let payments = stmt
+ .query_map([], row_to_stat)?
+ .collect::<Result<Vec<payment::Stat>, _>>()?;
+ Ok(payments)
+ })
.await;
- match result {
+ match res {
Ok(payments) => payments,
Err(err) => {
log::error!("Error listing payments for statistics: {:?}", err);
@@ -314,29 +241,30 @@ GROUP BY
}
}
-pub async fn get_row(pool: &SqlitePool, id: i64, frequency: Frequency) -> i64 {
+pub async fn get_row(conn: &Connection, id: i64, frequency: Frequency) -> i64 {
let query = r#"
-SELECT
- row
-FROM (
- SELECT
- ROW_NUMBER () OVER (ORDER BY date DESC) AS row,
- id
- FROM
- payments
- WHERE
- deleted_at IS NULL
- AND frequency = ?
-)
-WHERE
- id = ?
+ SELECT row
+ FROM (
+ SELECT
+ ROW_NUMBER () OVER (ORDER BY date DESC) AS row,
+ id
+ FROM payments
+ WHERE
+ deleted_at IS NULL
+ AND frequency = :frequency
+ )
+ WHERE id = :id
"#;
- let res = sqlx::query(query)
- .bind(frequency)
- .bind(id)
- .map(|row: SqliteRow| row.get("row"))
- .fetch_one(pool)
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let mut iter = stmt.query_map(
+ named_params![":id": id, ":frequency": frequency.to_string()],
+ |row| row.get(0)
+ )?;
+ utils::one::<i64, _>(&mut iter)
+ })
.await;
match res {
@@ -348,26 +276,28 @@ WHERE
}
}
-pub async fn get_for_form(pool: &SqlitePool, id: i64) -> Option<payment::Form> {
+pub async fn get_for_form(conn: &Connection, id: i64) -> Option<payment::Form> {
let query = r#"
-SELECT
- id,
- name,
- cost,
- user_id,
- category_id,
- strftime('%Y-%m-%d', date) AS date,
- frequency AS frequency
-FROM
- payments
-WHERE
- id = ?
- AND deleted_at IS NULL
+ SELECT
+ id,
+ name,
+ cost,
+ user_id,
+ category_id,
+ strftime('%Y-%m-%d', date) AS date,
+ frequency AS frequency
+ FROM payments
+ WHERE
+ id = :id
+ AND deleted_at IS NULL
"#;
- let res = sqlx::query_as::<_, payment::Form>(query)
- .bind(id)
- .fetch_one(pool)
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let mut iter = stmt.query_map(named_params![":id": id], row_to_form)?;
+ utils::one(&mut iter)
+ })
.await;
match res {
@@ -379,26 +309,31 @@ WHERE
}
}
-pub async fn create(pool: &SqlitePool, p: &payment::Create) -> Option<i64> {
- let res = sqlx::query(
- r#"
-INSERT INTO
- payments(name, cost, user_id, category_id, date, frequency)
-VALUES
- (?, ?, ?, ?, ?, ?)
- "#,
- )
- .bind(p.name.clone())
- .bind(p.cost)
- .bind(p.user_id)
- .bind(p.category_id)
- .bind(p.date)
- .bind(p.frequency)
- .execute(pool)
- .await;
+pub async fn create(conn: &Connection, p: payment::Create) -> Option<i64> {
+ let query = r#"
+ INSERT INTO payments(name, cost, user_id, category_id, date, frequency)
+ VALUES (:name, :cost, :user_id, :category_id, :date, :frequency)
+ "#;
+
+ let res = conn
+ .call(move |conn| {
+ conn.execute(
+ query,
+ named_params![
+ ":name": p.name,
+ ":cost": p.cost,
+ ":user_id": p.user_id,
+ ":category_id": p.category_id,
+ ":date": p.date,
+ ":frequency": p.frequency.to_string()
+ ],
+ )?;
+ Ok(conn.last_insert_rowid())
+ })
+ .await;
match res {
- Ok(x) => Some(x.last_insert_rowid()),
+ Ok(payment_id) => Some(payment_id),
Err(err) => {
log::error!("Error creating payment: {:?}", err);
None
@@ -406,30 +341,35 @@ VALUES
}
}
-pub async fn update(pool: &SqlitePool, id: i64, p: &payment::Update) -> bool {
- let res = sqlx::query(
- r#"
-UPDATE
- payments
-SET
- name = ?,
- cost = ?,
- user_id = ?,
- category_id = ?,
- date = ?,
- updated_at = datetime()
-WHERE
- id = ?
- "#,
- )
- .bind(p.name.clone())
- .bind(p.cost)
- .bind(p.user_id)
- .bind(p.category_id)
- .bind(p.date)
- .bind(id)
- .execute(pool)
- .await;
+pub async fn update(conn: &Connection, id: i64, p: payment::Update) -> bool {
+ let query = r#"
+ UPDATE payments
+ SET
+ name = :name,
+ cost = :cost,
+ user_id = :user_id,
+ category_id = :category_id,
+ date = :date,
+ updated_at = datetime()
+ WHERE id = :id
+ "#;
+
+ let res = conn
+ .call(move |conn| {
+ Ok(conn.execute(
+ query,
+ named_params![
+ ":name": p.name,
+ ":cost": p.cost,
+ ":user_id": p.user_id,
+ ":category_id": p.category_id,
+ ":date": p.date,
+ ":id": id
+ ],
+ )?)
+ })
+ .await;
+
match res {
Ok(_) => true,
@@ -440,20 +380,12 @@ WHERE
}
}
-pub async fn delete(pool: &SqlitePool, id: i64) -> bool {
- let res = sqlx::query(
- r#"
-UPDATE
- payments
-SET
- deleted_at = datetime()
-WHERE
- id = ?
- "#,
- )
- .bind(id)
- .execute(pool)
- .await;
+pub async fn delete(conn: &Connection, id: i64) -> bool {
+ let query = r#"UPDATE payments SET deleted_at = datetime() WHERE id = :id"#;
+
+ let res = conn
+ .call(move |conn| Ok(conn.execute(query, named_params![":id": id])?))
+ .await;
match res {
Ok(_) => true,
@@ -465,33 +397,34 @@ WHERE
}
pub async fn search_category(
- pool: &SqlitePool,
+ conn: &Connection,
payment_name: String,
) -> Option<i64> {
- let query = format!(
- r#"
-SELECT
- category_id
-FROM
- payments
-WHERE
- deleted_at IS NULL
- AND {} LIKE ?
-ORDER BY
- updated_at, created_at
- "#,
- utils::format_key_for_search("name")
- );
-
- let res = sqlx::query(&query)
- .bind(text::format_search(&payment_name))
- .map(|row: SqliteRow| row.get("category_id"))
- .fetch_one(pool)
+ let query = format!(r#"
+ SELECT category_id
+ FROM payments
+ WHERE
+ deleted_at IS NULL
+ AND {} LIKE :name
+ ORDER BY updated_at, created_at
+ "#, utils::format_key_for_search("name"));
+
+ let payment_name_closure = payment_name.clone();
+
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(&query)?;
+ let mut iter = stmt.query_map(
+ named_params![":name": text::format_search(&payment_name_closure)],
+ |row| row.get(0)
+ )?;
+ utils::one::<i64, _>(&mut iter)
+ })
.await;
match res {
Ok(category) => Some(category),
- Err(Error::RowNotFound) => None,
+ Err(tokio_rusqlite::Error::Rusqlite(rusqlite::Error::QueryReturnedNoRows)) => None,
Err(err) => {
log::error!(
"Error looking for the category of {}: {:?}",
@@ -503,24 +436,25 @@ ORDER BY
}
}
-pub async fn is_category_used(pool: &SqlitePool, category_id: i64) -> bool {
+pub async fn is_category_used(conn: &Connection, category_id: i64) -> bool {
let query = r#"
-SELECT
- 1
-FROM
- payments
-WHERE
- category_id = ?
- AND deleted_at IS NULL
-LIMIT
- 1
+ SELECT 1
+ FROM payments
+ WHERE
+ category_id = :category_id
+ AND deleted_at IS NULL
+ LIMIT 1
"#;
- let res = sqlx::query(query).bind(category_id).fetch_one(pool).await;
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(&query)?;
+ Ok(stmt.exists(named_params![":category_id": category_id])?)
+ })
+ .await;
match res {
- Ok(_) => true,
- Err(Error::RowNotFound) => false,
+ Ok(is_used) => is_used,
Err(err) => {
log::error!(
"Error looking if category {} is used: {:?}",
@@ -532,31 +466,33 @@ LIMIT
}
}
-pub async fn repartition(pool: &SqlitePool) -> HashMap<i64, i64> {
+pub async fn repartition(conn: &Connection) -> HashMap<i64, i64> {
let query = r#"
-SELECT
- users.id AS user_id,
- COALESCE(payments.sum, 0) AS sum
-FROM
- users
-LEFT OUTER JOIN (
- SELECT
- user_id,
- SUM(cost) AS sum
- FROM
- payments
- WHERE
- deleted_at IS NULL
- AND frequency = 'Punctual'
- GROUP BY
- user_id
-) payments
-ON
- users.id = payments.user_id"#;
-
- let res = sqlx::query(query)
- .map(|row: SqliteRow| (row.get("user_id"), row.get("sum")))
- .fetch_all(pool)
+ SELECT
+ users.id,
+ COALESCE(payments.sum, 0)
+ FROM users
+ LEFT OUTER JOIN (
+ SELECT
+ user_id,
+ SUM(cost) AS sum
+ FROM payments
+ WHERE
+ deleted_at IS NULL
+ AND frequency = 'Punctual'
+ GROUP BY user_id
+ ) payments
+ ON users.id = payments.user_id
+ "#;
+
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let payments = stmt
+ .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
+ .collect::<Result<Vec<(i64, i64)>, _>>()?;
+ Ok(payments)
+ })
.await;
match res {
@@ -568,25 +504,25 @@ ON
}
}
-pub async fn create_monthly_payments(pool: &SqlitePool) {
+pub async fn create_monthly_payments(conn: &Connection) {
let query = r#"
-INSERT INTO
- payments(name, cost, user_id, category_id, date, frequency)
-SELECT
- name,
- cost,
- user_id,
- category_id,
- date() AS date,
- 'Punctual' AS frequency
-FROM
- payments
-WHERE
- frequency = 'Monthly'
- AND deleted_at IS NULL
+ INSERT INTO payments(name, cost, user_id, category_id, date, frequency)
+ SELECT
+ name,
+ cost,
+ user_id,
+ category_id,
+ date() AS date,
+ 'Punctual' AS frequency
+ FROM payments
+ WHERE
+ frequency = 'Monthly'
+ AND deleted_at IS NULL
"#;
- let res = sqlx::query(query).execute(pool).await;
+ let res = conn
+ .call(move |conn| Ok(conn.execute(query, [])?))
+ .await;
match res {
Ok(_) => (),
@@ -594,50 +530,51 @@ WHERE
}
}
-pub async fn last_week(pool: &SqlitePool) -> Vec<Report> {
+pub async fn last_week(conn: &Connection) -> Vec<Report> {
let query = r#"
-SELECT
- strftime('%d/%m/%Y', payments.date) AS date,
- (payments.name || ' (' || users.name || ')') AS name,
- payments.cost AS amount,
- (CASE
- WHEN
- payments.deleted_at IS NOT NULL
- THEN
- 'Deleted'
- WHEN
- payments.updated_at IS NOT NULL
- AND payments.created_at < date('now', 'weekday 0', '-13 days')
- THEN
- 'Updated'
- ELSE
- 'Created'
- END) AS action
-FROM
- payments
-INNER JOIN
- users
-ON
- payments.user_id = users.id
-WHERE
- payments.frequency = 'Punctual'
- AND (
- (
- payments.created_at >= date('now', 'weekday 0', '-13 days')
- AND payments.created_at < date('now', 'weekday 0', '-6 days')
- ) OR (
- payments.updated_at >= date('now', 'weekday 0', '-13 days')
- AND payments.updated_at < date('now', 'weekday 0', '-6 days')
- ) OR (
- payments.deleted_at >= date('now', 'weekday 0', '-13 days')
- AND payments.deleted_at < date('now', 'weekday 0', '-6 days')
- )
- )
-ORDER BY
- payments.date
+ SELECT
+ strftime('%d/%m/%Y', payments.date) AS date,
+ (payments.name || ' (' || users.name || ')') AS name,
+ payments.cost AS amount,
+ (CASE
+ WHEN payments.deleted_at IS NOT NULL
+ THEN 'Deleted'
+ WHEN
+ payments.updated_at IS NOT NULL
+ AND payments.created_at < date('now', 'weekday 0', '-13 days')
+ THEN 'Updated'
+ ELSE 'Created'
+ END) AS action
+ FROM payments
+ INNER JOIN users
+ ON payments.user_id = users.id
+ WHERE
+ payments.frequency = 'Punctual'
+ AND (
+ (
+ payments.created_at >= date('now', 'weekday 0', '-13 days')
+ AND payments.created_at < date('now', 'weekday 0', '-6 days')
+ ) OR (
+ payments.updated_at >= date('now', 'weekday 0', '-13 days')
+ AND payments.updated_at < date('now', 'weekday 0', '-6 days')
+ ) OR (
+ payments.deleted_at >= date('now', 'weekday 0', '-13 days')
+ AND payments.deleted_at < date('now', 'weekday 0', '-6 days')
+ )
+ )
+ ORDER BY payments.date
"#;
- let res = sqlx::query_as::<_, Report>(query).fetch_all(pool).await;
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let xs = stmt
+ .query_map([], utils::row_to_report)?
+ .collect::<Result<Vec<Report>, _>>()?;
+
+ Ok(xs)
+ })
+ .await;
match res {
Ok(payments) => payments,
diff --git a/src/db/users.rs b/src/db/users.rs
index f463421..8b21ff4 100644
--- a/src/db/users.rs
+++ b/src/db/users.rs
@@ -1,24 +1,30 @@
-use sqlx::error::Error;
-use sqlx::sqlite::{SqlitePool, SqliteRow};
-use sqlx_core::row::Row;
+use tokio_rusqlite::{named_params, Connection, Row};
+use crate::db::utils;
use crate::model::user::User;
-pub async fn list(pool: &SqlitePool) -> Vec<User> {
- let res = sqlx::query_as::<_, User>(
- r#"
-SELECT
- id,
- name,
- email
-FROM
- users
-ORDER BY
- name
- "#,
- )
- .fetch_all(pool)
- .await;
+fn row_to_user(row: &Row) -> Result<User, rusqlite::Error> {
+ Ok(User {
+ id: row.get(0)?,
+ name: row.get(1)?,
+ email: row.get(2)?,
+ })
+}
+
+pub async fn list(conn: &Connection) -> Vec<User> {
+ let query = r#"SELECT id, name, email FROM users ORDER BY name"#;
+
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+
+ let users = stmt
+ .query_map([], row_to_user)?
+ .collect::<Result<Vec<User>, _>>()?;
+
+ Ok(users)
+ })
+ .await;
match res {
Ok(users) => users,
@@ -30,25 +36,30 @@ ORDER BY
}
pub async fn set_login_token(
- pool: &SqlitePool,
+ conn: &Connection,
email: String,
login_token: String,
) -> bool {
- let res = sqlx::query(
- r#"
-UPDATE
- users
-SET
- login_token = ?,
- updated_at = datetime()
-WHERE
- email = ?
- "#,
- )
- .bind(login_token)
- .bind(email)
- .execute(pool)
- .await;
+ let query = r#"
+ UPDATE users
+ SET
+ login_token = :login_token,
+ updated_at = datetime()
+ WHERE
+ email = :email
+ "#;
+
+ let res = conn
+ .call(move |conn| {
+ Ok(conn.execute(
+ query,
+ named_params![
+ ":login_token": login_token,
+ ":email": email
+ ],
+ )?)
+ })
+ .await;
match res {
Ok(_) => true,
@@ -59,21 +70,18 @@ WHERE
}
}
-pub async fn remove_login_token(pool: &SqlitePool, id: i64) -> bool {
- let res = sqlx::query(
- r#"
-UPDATE
- users
-SET
- login_token = NULL,
- updated_at = datetime()
-WHERE
- id = ?
- "#,
- )
- .bind(id)
- .execute(pool)
- .await;
+pub async fn remove_login_token(conn: &Connection, id: i64) -> bool {
+ let query = r#"
+ UPDATE users
+ SET
+ login_token = NULL,
+ updated_at = datetime()
+ WHERE id = :id
+ "#;
+
+ let res = conn
+ .call(move |conn| Ok(conn.execute(query, named_params![":id": id])?))
+ .await;
match res {
Ok(_) => true,
@@ -85,59 +93,61 @@ WHERE
}
pub async fn get_by_login_token(
- pool: &SqlitePool,
+ conn: &Connection,
login_token: String,
) -> Option<User> {
- let res = sqlx::query_as::<_, User>(
- r#"
-SELECT
- id,
- name,
- email
-FROM
- users
-WHERE
- login_token = ?
- "#,
- )
- .bind(login_token)
- .fetch_one(pool)
- .await;
+ let query = r#"
+ SELECT
+ id,
+ name,
+ email
+ FROM users
+ WHERE login_token = :login_token
+ "#;
+
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let mut iter = stmt.query_map(
+ named_params![":login_token": login_token],
+ row_to_user,
+ )?;
+ utils::one(&mut iter)
+ })
+ .await;
match res {
Ok(user) => Some(user),
- Err(Error::RowNotFound) => None,
Err(err) => {
- log::error!("Error getting user from login token: {:?}", err);
+ log::error!("Error getting user from login_token: {err:?}");
None
}
}
}
pub async fn get_password_hash(
- pool: &SqlitePool,
+ conn: &Connection,
email: String,
) -> Option<String> {
- let res = sqlx::query(
- r#"
-SELECT
- password
-FROM
- users
-WHERE
- email = ?
- "#,
- )
- .bind(email)
- .map(|row: SqliteRow| row.get("password"))
- .fetch_one(pool)
- .await;
+ let query = r#"
+ SELECT password
+ FROM users
+ WHERE email = :email
+ "#;
+
+ let res = conn
+ .call(move |conn| {
+ let mut stmt = conn.prepare(query)?;
+ let mut iter = stmt
+ .query_map(named_params![":email": email], |row| row.get(0))?;
+ utils::one::<String, _>(&mut iter)
+ })
+ .await;
match res {
- Ok(hash) => Some(hash),
- Err(Error::RowNotFound) => None,
+ Ok(hash) => Some(hash.clone()),
Err(err) => {
- log::error!("Error getting password hash: {:?}", err);
+ log::error!("Error listing users: {:?}", err);
None
}
}
diff --git a/src/db/utils.rs b/src/db/utils.rs
index 621a69c..f61d20a 100644
--- a/src/db/utils.rs
+++ b/src/db/utils.rs
@@ -1,3 +1,27 @@
+use crate::model::report::Report;
+use tokio_rusqlite::Row;
+
pub fn format_key_for_search(value: &str) -> String {
format!("replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(lower({}), 'à', 'a'), 'â', 'a'), 'ç', 'c'), 'è', 'e'), 'é', 'e'), 'ê', 'e'), 'ë', 'e'), 'î', 'i'), 'ï', 'i'), 'ô', 'o'), 'ù', 'u'), 'û', 'u'), 'ü', 'u')", value)
}
+
+pub fn one<A, I: Iterator<Item = Result<A, rusqlite::Error>>>(
+ mut iter: I,
+) -> Result<A, tokio_rusqlite::Error> {
+ match iter.next() {
+ Some(Ok(user)) => Ok(user),
+ Some(Err(err)) => Err(tokio_rusqlite::Error::Rusqlite(err)),
+ None => Err(tokio_rusqlite::Error::Rusqlite(
+ rusqlite::Error::QueryReturnedNoRows,
+ )),
+ }
+}
+
+pub fn row_to_report(row: &Row) -> Result<Report, rusqlite::Error> {
+ Ok(Report {
+ date: row.get(0)?,
+ name: row.get(1)?,
+ amount: row.get(2)?,
+ action: row.get(3)?,
+ })
+}
diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs
index a718d93..3bfca71 100644
--- a/src/jobs/mod.rs
+++ b/src/jobs/mod.rs
@@ -1,7 +1,7 @@
mod weekly_report;
-use sqlx::sqlite::SqlitePool;
use tokio::time::{sleep, Duration};
+use tokio_rusqlite::Connection;
use crate::db;
use crate::model::config::Config;
@@ -9,21 +9,21 @@ use crate::model::job::Job;
pub async fn start(
config: Config,
- pool: SqlitePool,
+ db_conn: Connection,
templates: minijinja::Environment<'_>,
) {
loop {
- if db::jobs::should_run(&pool, Job::WeeklyReport).await {
+ if db::jobs::should_run(&db_conn, Job::WeeklyReport).await {
log::info!("Starting weekly report job");
- if weekly_report::send(&config, &pool, &templates).await {
- db::jobs::actualize_last_execution(&pool, Job::WeeklyReport)
+ if weekly_report::send(&config, &db_conn, &templates).await {
+ db::jobs::actualize_last_execution(&db_conn, Job::WeeklyReport)
.await;
}
}
- if db::jobs::should_run(&pool, Job::MonthlyPayment).await {
+ if db::jobs::should_run(&db_conn, Job::MonthlyPayment).await {
log::info!("Starting monthly payment job");
- db::payments::create_monthly_payments(&pool).await;
- db::jobs::actualize_last_execution(&pool, Job::MonthlyPayment)
+ db::payments::create_monthly_payments(&db_conn).await;
+ db::jobs::actualize_last_execution(&db_conn, Job::MonthlyPayment)
.await;
}
// Sleeping 8 hours
diff --git a/src/jobs/weekly_report.rs b/src/jobs/weekly_report.rs
index 5058c52..a91a3fb 100644
--- a/src/jobs/weekly_report.rs
+++ b/src/jobs/weekly_report.rs
@@ -1,5 +1,5 @@
-use sqlx::sqlite::SqlitePool;
use std::collections::HashMap;
+use tokio_rusqlite::Connection;
use crate::db;
use crate::mail;
@@ -8,12 +8,12 @@ use crate::payer;
pub async fn send(
config: &Config,
- pool: &SqlitePool,
+ db_conn: &Connection,
env: &minijinja::Environment<'_>,
) -> bool {
- match get_weekly_report(pool, env).await {
+ match get_weekly_report(db_conn, env).await {
Ok(report) => {
- let users = db::users::list(pool).await;
+ let users = db::users::list(db_conn).await;
mail::send(
config,
users
@@ -39,21 +39,21 @@ pub async fn send(
}
async fn get_weekly_report(
- pool: &SqlitePool,
+ db_conn: &Connection,
env: &minijinja::Environment<'_>,
) -> Result<String, minijinja::Error> {
- let users = db::users::list(pool).await;
- let incomes_from = db::incomes::defined_for_all(pool).await;
+ let users = db::users::list(db_conn).await;
+ let incomes_from = db::incomes::defined_for_all(db_conn).await;
let user_incomes = match incomes_from {
- Some(from) => db::incomes::cumulative(pool, from).await,
+ Some(from) => db::incomes::cumulative(db_conn, from).await,
None => HashMap::new(),
};
- let user_payments = db::payments::repartition(pool).await;
+ let user_payments = db::payments::repartition(db_conn).await;
let exceeding_payers =
payer::exceeding(&users, &user_incomes, &user_payments);
- let last_week_payments = db::payments::last_week(pool).await;
- let last_week_incomes = db::incomes::last_week(pool).await;
+ let last_week_payments = db::payments::last_week(db_conn).await;
+ let last_week_incomes = db::incomes::last_week(db_conn).await;
let template = env.get_template("report/report.j2")?;
template.render(minijinja::context!(
diff --git a/src/main.rs b/src/main.rs
index 5fe8a94..0786f46 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,8 +1,8 @@
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper_util::rt::TokioIo;
-use sqlx::sqlite::SqlitePool;
use tokio::net::TcpListener;
+use tokio_rusqlite::Connection;
mod assets;
mod controller;
@@ -27,22 +27,28 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = config::from_env()
.unwrap_or_else(|err| panic!("Error reading config: {err}"));
- let pool = SqlitePool::connect(&format!("sqlite:{}", config.db_path))
+ let db_conn = Connection::open(config.db_path.clone())
.await
- .unwrap();
+ .unwrap_or_else(|_| {
+ panic!("Error while openning DB: {}", config.db_path)
+ });
let assets = assets::get();
let templates = templates::get()?;
- tokio::spawn(jobs::start(config.clone(), pool.clone(), templates.clone()));
+ tokio::spawn(jobs::start(
+ config.clone(),
+ db_conn.clone(),
+ templates.clone(),
+ ));
let listener = TcpListener::bind(config.socket_address).await?;
log::info!("Starting server at {}", config.socket_address);
loop {
let config = config.clone();
- let pool = pool.clone();
+ let db_conn = db_conn.clone();
let assets = assets.clone();
let templates = templates.clone();
let (stream, _) = listener.accept().await?;
@@ -56,7 +62,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
service_fn(move |req| {
routes::routes(
config.clone(),
- pool.clone(),
+ db_conn.clone(),
assets.clone(),
templates.clone(),
req,
diff --git a/src/model/action.rs b/src/model/action.rs
deleted file mode 100644
index a77543a..0000000
--- a/src/model/action.rs
+++ /dev/null
@@ -1 +0,0 @@
-use serde::Serialize;
diff --git a/src/model/category.rs b/src/model/category.rs
index de08dea..ecece96 100644
--- a/src/model/category.rs
+++ b/src/model/category.rs
@@ -1,6 +1,4 @@
-use serde::Serialize;
-
-#[derive(sqlx::FromRow, Serialize)]
+#[derive(serde::Serialize, Clone)]
pub struct Category {
pub id: i64,
pub name: String,
diff --git a/src/model/frequency.rs b/src/model/frequency.rs
index bb83e27..91aab89 100644
--- a/src/model/frequency.rs
+++ b/src/model/frequency.rs
@@ -1,7 +1,7 @@
-use serde::{Deserialize, Serialize};
+use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef};
use std::{fmt, str};
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, sqlx::Type)]
+#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq)]
pub enum Frequency {
Punctual,
Monthly,
@@ -29,3 +29,19 @@ impl str::FromStr for Frequency {
}
}
}
+
+impl FromSql for Frequency {
+ fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+ match value {
+ ValueRef::Text(text) => match std::str::from_utf8(text) {
+ Ok("Punctual") => Ok(Frequency::Punctual),
+ Ok("Monthly") => Ok(Frequency::Monthly),
+ Ok(str) => Err(FromSqlError::Other(
+ format!("Unknown frequency: {str}").into(),
+ )),
+ Err(err) => Err(FromSqlError::Other(err.into())),
+ },
+ _ => Err(FromSqlError::InvalidType),
+ }
+ }
+}
diff --git a/src/model/income.rs b/src/model/income.rs
index ef97b56..c953251 100644
--- a/src/model/income.rs
+++ b/src/model/income.rs
@@ -1,13 +1,12 @@
use chrono::NaiveDate;
-use serde::Serialize;
-#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
+#[derive(Debug, Clone, serde::Serialize)]
pub struct Stat {
pub date: String,
pub amount: i64,
}
-#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
+#[derive(Debug, Clone, serde::Serialize)]
pub struct Table {
pub id: i64,
pub date: String,
@@ -15,7 +14,7 @@ pub struct Table {
pub amount: i64,
}
-#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
+#[derive(Debug, Clone, serde::Serialize)]
pub struct Form {
pub id: i64,
pub amount: i64,
diff --git a/src/model/job.rs b/src/model/job.rs
index 74151ae..f31cfa0 100644
--- a/src/model/job.rs
+++ b/src/model/job.rs
@@ -1,5 +1,13 @@
-#[derive(Debug, sqlx::Type)]
+use std::fmt;
+
+#[derive(Debug)]
pub enum Job {
MonthlyPayment,
WeeklyReport,
}
+
+impl fmt::Display for Job {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
diff --git a/src/model/payment.rs b/src/model/payment.rs
index 5ce6bb9..d732528 100644
--- a/src/model/payment.rs
+++ b/src/model/payment.rs
@@ -1,9 +1,8 @@
use chrono::NaiveDate;
-use serde::Serialize;
use crate::model::frequency::Frequency;
-#[derive(Debug, sqlx::FromRow, Serialize)]
+#[derive(Debug, serde::Serialize)]
pub struct Table {
pub id: i64,
pub name: String,
@@ -15,7 +14,7 @@ pub struct Table {
pub frequency: Frequency,
}
-#[derive(Debug, sqlx::FromRow, Serialize)]
+#[derive(Debug, serde::Serialize)]
pub struct Form {
pub id: i64,
pub name: String,
@@ -26,14 +25,14 @@ pub struct Form {
pub frequency: Frequency,
}
-#[derive(Debug, sqlx::FromRow, Serialize)]
+#[derive(Debug, serde::Serialize)]
pub struct Stat {
pub start_date: String,
pub cost: i64,
pub category_id: i64,
}
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct Create {
pub name: String,
pub cost: i64,
diff --git a/src/model/report.rs b/src/model/report.rs
index 4858402..e944745 100644
--- a/src/model/report.rs
+++ b/src/model/report.rs
@@ -1,6 +1,7 @@
-use serde::Serialize;
+use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ValueRef};
+use std::fmt;
-#[derive(Debug, sqlx::FromRow, Serialize)]
+#[derive(Debug, serde::Serialize)]
pub struct Report {
pub date: String,
pub name: String,
@@ -8,9 +9,32 @@ pub struct Report {
pub action: Action,
}
-#[derive(Debug, PartialEq, Serialize, sqlx::Type)]
+#[derive(Debug, PartialEq, serde::Serialize)]
pub enum Action {
Created,
Updated,
Deleted,
}
+
+impl fmt::Display for Action {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+impl FromSql for Action {
+ fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
+ match value {
+ ValueRef::Text(text) => match std::str::from_utf8(text) {
+ Ok("Created") => Ok(Action::Created),
+ Ok("Updated") => Ok(Action::Updated),
+ Ok("Deleted") => Ok(Action::Deleted),
+ Ok(str) => Err(FromSqlError::Other(
+ format!("Unknown action: {str}").into(),
+ )),
+ Err(err) => Err(FromSqlError::Other(err.into())),
+ },
+ _ => Err(FromSqlError::InvalidType),
+ }
+ }
+}
diff --git a/src/model/user.rs b/src/model/user.rs
index e8a61bf..06165dd 100644
--- a/src/model/user.rs
+++ b/src/model/user.rs
@@ -1,6 +1,4 @@
-use serde::Serialize;
-
-#[derive(Debug, sqlx::FromRow, Clone, Serialize)]
+#[derive(Debug, Clone, serde::Serialize)]
pub struct User {
pub id: i64,
pub name: String,
diff --git a/src/queries.rs b/src/queries.rs
index 8ecc4fe..9699d57 100644
--- a/src/queries.rs
+++ b/src/queries.rs
@@ -1,7 +1,6 @@
use crate::model::frequency::Frequency;
-use serde::{Deserialize, Serialize};
-#[derive(Deserialize, Serialize, Clone)]
+#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct Payments {
pub page: Option<i64>,
pub name: Option<String>,
@@ -69,18 +68,18 @@ pub fn payments_url(q: Payments) -> String {
}
}
-#[derive(Deserialize, Serialize, Clone)]
+#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct Incomes {
pub page: Option<i64>,
pub highlight: Option<i64>,
}
-#[derive(Deserialize, Serialize, Clone)]
+#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct Categories {
pub highlight: Option<i64>,
}
-#[derive(Deserialize, Serialize)]
+#[derive(serde::Serialize, serde::Deserialize)]
pub struct PaymentCategory {
pub payment_name: String,
}
diff --git a/src/routes.rs b/src/routes.rs
index 5f17ca5..aca4284 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -2,9 +2,9 @@ use http_body_util::{BodyExt, Full};
use hyper::body::{Bytes, Incoming};
use hyper::{Method, Request, Response};
use serde::Deserialize;
-use sqlx::sqlite::SqlitePool;
use std::collections::HashMap;
use std::convert::Infallible;
+use tokio_rusqlite::Connection;
use url::form_urlencoded;
use crate::controller;
@@ -17,7 +17,7 @@ use crate::utils::cookie;
pub async fn routes(
config: Config,
- pool: SqlitePool,
+ db_conn: Connection,
assets: HashMap<String, String>,
templates: minijinja::Environment<'static>,
request: Request<Incoming>,
@@ -38,7 +38,7 @@ pub async fn routes(
&assets,
&templates,
body_form(request).await,
- pool,
+ db_conn,
)
.await
}
@@ -49,10 +49,10 @@ pub async fn routes(
"icon.png" => file("assets/icon.png", "image/png").await,
_ => controller::utils::not_found(),
},
- _ => match connected_user(&config, &pool, &request).await {
+ _ => match connected_user(&config, &db_conn, &request).await {
Some(user) => {
let wallet = Wallet {
- pool,
+ db_conn,
assets,
templates,
user,
@@ -68,12 +68,12 @@ pub async fn routes(
async fn connected_user(
config: &Config,
- pool: &SqlitePool,
+ db_conn: &Connection,
request: &Request<Incoming>,
) -> Option<User> {
let cookie = request.headers().get("COOKIE")?.to_str().ok()?;
let login_token = cookie::extract_token(config, cookie).ok()?;
- db::users::get_by_login_token(pool, login_token.to_string()).await
+ db::users::get_by_login_token(db_conn, login_token.to_string()).await
}
async fn authenticated_routes(
diff --git a/src/templates.rs b/src/templates.rs
index c9a750b..8f160dc 100644
--- a/src/templates.rs
+++ b/src/templates.rs
@@ -1,9 +1,8 @@
-use serde::Serialize;
use std::fs;
use crate::queries;
-#[derive(Debug, Serialize)]
+#[derive(Debug, serde::Serialize)]
pub enum Header {
Payments,
Categories,