Turn static data within the database into an enum
This commit is contained in:
parent
066b912e42
commit
3a93b7e12f
22 changed files with 546 additions and 104 deletions
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "debt-pirate"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
argon2 = "0.5"
|
||||
|
@ -11,8 +11,8 @@ axum = { version = "0.8", features = [
|
|||
"ws",
|
||||
] }
|
||||
base64 = "0.22"
|
||||
bb8-redis = "0.20"
|
||||
blake3 = { version = "1.5", features = ["serde"] }
|
||||
deadpool-redis = { version = "0.20", features = ["serde"] }
|
||||
futures = "0.3"
|
||||
http = "1.0"
|
||||
humantime = "2.1"
|
||||
|
@ -28,7 +28,7 @@ lettre = { version = "0.11", default-features = false, features = [
|
|||
] }
|
||||
num_cpus = "1.16"
|
||||
pasetors = "0.7"
|
||||
redis = { version = "0.28", features = ["aio"] }
|
||||
rust_decimal = "1.36"
|
||||
serde = { version = "1.0", features = ["derive", "rc", "std"] }
|
||||
serde_json = "1.0"
|
||||
serde_with = "3.9"
|
||||
|
@ -37,6 +37,7 @@ sqlx = { version = "0.8", features = [
|
|||
"time",
|
||||
"postgres",
|
||||
"runtime-tokio",
|
||||
"rust_decimal",
|
||||
] }
|
||||
syslog-tracing = "0.3.1"
|
||||
time = { version = "0.3", features = ["formatting", "macros"] }
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
DROP INDEX IF EXISTS status_name_uniq_idx;
|
||||
DROP INDEX IF EXISTS user_username_uniq_idx;
|
||||
DROP INDEX IF EXISTS user_email_uniq_idx;
|
||||
DROP INDEX IF EXISTS permission_name_uniq_idx;
|
||||
DROP INDEX IF EXISTS account_type_name_uniq_idx;
|
||||
DROP INDEX IF EXISTS permission_category_idx;
|
||||
DROP INDEX IF EXISTS permission_category_name_idx;
|
||||
|
||||
DROP TABLE IF EXISTS public.user_account_permission;
|
||||
DROP TABLE IF EXISTS public.account;
|
||||
DROP TABLE IF EXISTS public.permission;
|
||||
DROP TABLE IF EXISTS public.user;
|
||||
DROP TABLE IF EXISTS public.transaction_line_item;
|
||||
DROP TABLE IF EXISTS public.transaction CASCADE;
|
||||
DROP TABLE IF EXISTS public.budget CASCADE;
|
||||
DROP TABLE IF EXISTS public.status CASCADE;
|
||||
DROP TABLE IF EXISTS public.account_type CASCADE;
|
||||
|
||||
DROP TYPE public.entry_type;
|
||||
DROP TYPE public.account_type;
|
||||
DROP TYPE public.permission_category;
|
||||
DROP TYPE public.status;
|
||||
|
|
|
@ -1,51 +1,29 @@
|
|||
CREATE TABLE IF NOT EXISTS
|
||||
public.status (
|
||||
CREATE TYPE
|
||||
public.status
|
||||
AS ENUM
|
||||
('Active', 'Unverified', 'Removed', 'Quarantined');
|
||||
|
||||
CREATE TYPE
|
||||
public.account_type
|
||||
AS ENUM
|
||||
('Asset', 'Equity', 'Expense', 'Liability', 'Revenue');
|
||||
|
||||
CREATE TYPE
|
||||
public.entry_type
|
||||
AS ENUM
|
||||
('Debit', 'Credit');
|
||||
|
||||
CREATE TYPE
|
||||
public.permission_category
|
||||
AS ENUM
|
||||
('Account');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.user (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS status_name_uniq_idx ON public.status(name);
|
||||
|
||||
INSERT INTO
|
||||
public.status (
|
||||
name
|
||||
)
|
||||
VALUES
|
||||
('Active'),
|
||||
('Unverified'),
|
||||
('Removed'),
|
||||
('Quarantined');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.account_type (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS account_type_name_uniq_idx ON public.account_type(name);
|
||||
|
||||
INSERT INTO
|
||||
public.account_type (
|
||||
name
|
||||
)
|
||||
VALUES
|
||||
('Asset'),
|
||||
('Equity'),
|
||||
('Expense'),
|
||||
('Liability'),
|
||||
('Revenue');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.user (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
email TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
status_id INT NOT NULL REFERENCES status(id) DEFAULT 2,
|
||||
status status NOT NULL DEFAULT 'Unverified',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NULL
|
||||
);
|
||||
|
@ -55,33 +33,81 @@ CREATE UNIQUE INDEX IF NOT EXISTS user_email_uniq_idx ON public.user(email);
|
|||
CREATE TABLE IF NOT EXISTS
|
||||
public.permission (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
category permission_category NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
status_id INT NOT NULL REFERENCES status(id) DEFAULT 1,
|
||||
value INT NOT NULL,
|
||||
status status NOT NULL DEFAULT 'Active',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS permission_name_uniq_idx ON public.permission(name);
|
||||
CREATE INDEX IF NOT EXISTS permission_category_idx ON public.permission(category);
|
||||
CREATE INDEX IF NOT EXISTS permission_category_name_idx ON public.permission(category, name);
|
||||
|
||||
INSERT INTO
|
||||
public.permission (
|
||||
category,
|
||||
name,
|
||||
value
|
||||
)
|
||||
VALUES
|
||||
('Account', 'View', 1),
|
||||
('Account', 'Edit', 2),
|
||||
('Account', 'Delete', 4),
|
||||
('Account', 'Grant', 8);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.account (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
account_type account_type NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
currency_code TEXT NOT NULL,
|
||||
status status NOT NULL DEFAULT 'Active',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.user_account_permission (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id INT NOT NULL REFERENCES public.user(id),
|
||||
account_id INT NOT NULL REFERENCES public.account(id),
|
||||
permission_id INT NOT NULL REFERENCES public.permission(id),
|
||||
status status NOT NULL DEFAULT 'Active',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.budget (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
status_id INT NOT NULL REFERENCES status(id) DEFAULT 1,
|
||||
icon TEXT NULL,
|
||||
status status NOT NULL DEFAULT 'Active',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.account (
|
||||
public.transaction (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
account_type_id INT NOT NULL REFERENCES account_type(id),
|
||||
budget_id INT NOT NULL REFERENCES budget(id),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
currency_code TEXT NOT NULL,
|
||||
status_id INT NOT NULL REFERENCES status(id) DEFAULT 1,
|
||||
description TEXT NOT NULL,
|
||||
budget_id INT NOT NULL REFERENCES public.budget(id),
|
||||
status status NOT NULL DEFAULT 'Active',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.transaction_line_item (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
transaction_id INT NOT NULL REFERENCES public.transaction(id),
|
||||
account_id INT NOT NULL REFERENCES public.account(id),
|
||||
entry_type entry_type NOT NULL,
|
||||
value NUMERIC(12, 2) NOT NULL,
|
||||
status status NOT NULL DEFAULT 'Active',
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE NULL
|
||||
);
|
||||
|
|
55
api/src/db/account.rs
Normal file
55
api/src/db/account.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
use sqlx::prelude::*;
|
||||
use tracing::error;
|
||||
|
||||
use crate::models::AppError;
|
||||
|
||||
use super::{
|
||||
associate_account_with_user_as_owner, AccountType, DbPool, PermissionEntity, StatusType,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NewAccountEntity {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub account_type: AccountType,
|
||||
pub currency_code: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct AccountEntity {
|
||||
pub id: i32,
|
||||
pub account_type: AccountType,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub currency_code: String,
|
||||
pub status: StatusType,
|
||||
}
|
||||
|
||||
pub async fn insert_new_account_for_user(
|
||||
pool: &DbPool,
|
||||
new_account: NewAccountEntity,
|
||||
user_id: i32,
|
||||
) -> Result<(AccountEntity, Vec<PermissionEntity>), AppError> {
|
||||
let NewAccountEntity {
|
||||
name,
|
||||
description,
|
||||
account_type,
|
||||
currency_code,
|
||||
} = &new_account;
|
||||
|
||||
let new_account = sqlx::query_as::<_, AccountEntity>("INSERT INTO public.account (name, description, account_type, currency_code) VALUES ($1, $2, $3, $4) RETURNING id, account_type, name, description, currency_code, status;")
|
||||
.bind(name)
|
||||
.bind(description)
|
||||
.bind(account_type)
|
||||
.bind(currency_code)
|
||||
.fetch_one(pool).await
|
||||
.inspect_err(|err| error!(?err, record = ?new_account, "Cannot insert new account record"))
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
let account_permissions =
|
||||
associate_account_with_user_as_owner(pool, new_account.id, user_id).await?;
|
||||
|
||||
Ok((new_account, account_permissions))
|
||||
}
|
13
api/src/db/account_type.rs
Normal file
13
api/src/db/account_type.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, sqlx::Type,
|
||||
)]
|
||||
#[sqlx(type_name = "account_type", rename_all = "PascalCase")]
|
||||
pub enum AccountType {
|
||||
Asset,
|
||||
Equity,
|
||||
Expense,
|
||||
Liability,
|
||||
Revenue,
|
||||
}
|
|
@ -1,7 +1,18 @@
|
|||
mod account;
|
||||
mod account_type;
|
||||
mod permission;
|
||||
mod status;
|
||||
mod user;
|
||||
mod user_account_permission;
|
||||
|
||||
pub use account::*;
|
||||
pub use account_type::*;
|
||||
pub use permission::*;
|
||||
pub use status::*;
|
||||
pub use user::*;
|
||||
pub use user_account_permission::*;
|
||||
|
||||
use sqlx::{self, postgres::PgPoolOptions, Pool, Postgres};
|
||||
pub use user::*;
|
||||
|
||||
use crate::models::AppError;
|
||||
|
||||
|
|
91
api/src/db/permission.rs
Normal file
91
api/src/db/permission.rs
Normal file
|
@ -0,0 +1,91 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::prelude::*;
|
||||
use tracing::error;
|
||||
|
||||
use crate::models::AppError;
|
||||
|
||||
use super::DbPool;
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, sqlx::Type,
|
||||
)]
|
||||
#[sqlx(type_name = "permission_category", rename_all = "PascalCase")]
|
||||
pub enum PermissionCategoryType {
|
||||
Account,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct PermissionEntity {
|
||||
pub id: i32,
|
||||
pub category: PermissionCategoryType,
|
||||
pub name: String,
|
||||
pub value: i32,
|
||||
}
|
||||
|
||||
pub async fn get_all_permissions_by_category(
|
||||
pool: &DbPool,
|
||||
category: PermissionCategoryType,
|
||||
) -> Result<Vec<PermissionEntity>, AppError> {
|
||||
sqlx::query_as::<_, PermissionEntity>(
|
||||
"SELECT id, category, name value FROM public.permission WHERE category = $1;",
|
||||
)
|
||||
.bind(category)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
?category,
|
||||
"Unable to fetch permissions associated with category"
|
||||
)
|
||||
})
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub async fn _get_permission_by_id(pool: &DbPool, id: i32) -> Result<PermissionEntity, AppError> {
|
||||
sqlx::query_as::<_, PermissionEntity>(
|
||||
"SELECT id, category, name, value FROM public.permission WHERE id = $1;",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.inspect_err(|err| error!(?err, ?id, "Unable to fetch permission"))
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub async fn get_many_permissions_by_id(
|
||||
pool: &DbPool,
|
||||
ids: &[i32],
|
||||
) -> Result<Vec<PermissionEntity>, AppError> {
|
||||
let ids = ids
|
||||
.iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
sqlx::query_as::<_, PermissionEntity>(
|
||||
"SELECT id, category, name, value FROM public.permission WHERE id IN ($1);",
|
||||
)
|
||||
.bind(ids.as_str())
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.inspect_err(|err| error!(?err, ?ids, "Unable to fetch permissions"))
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub async fn _get_permission_by_category_and_name(
|
||||
pool: &DbPool,
|
||||
category: &str,
|
||||
name: &str,
|
||||
) -> Result<PermissionEntity, AppError> {
|
||||
sqlx::query_as::<_, PermissionEntity>(
|
||||
"SELECT id, category, name, value FROM public.permission WHERE category = $1 AND name = $2;"
|
||||
)
|
||||
.bind(category)
|
||||
.bind(name)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.inspect_err(|err| error!(?err, ?category, ?name, "Unable to fetch permission"))
|
||||
.map_err(From::from)
|
||||
}
|
12
api/src/db/status.rs
Normal file
12
api/src/db/status.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, sqlx::Type,
|
||||
)]
|
||||
#[sqlx(type_name = "status", rename_all = "PascalCase")]
|
||||
pub enum StatusType {
|
||||
Active,
|
||||
Unverified,
|
||||
Removed,
|
||||
Quarantined,
|
||||
}
|
|
@ -30,7 +30,6 @@ pub struct UserEntity {
|
|||
pub id: i32,
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub status_id: i32,
|
||||
}
|
||||
|
||||
pub async fn insert_new_user(
|
||||
|
@ -43,16 +42,13 @@ pub async fn insert_new_user(
|
|||
name,
|
||||
} = new_user.clone();
|
||||
|
||||
sqlx::query_as::<_, UserEntity>("INSERT INTO public.user (email, password, name) VALUES ($1, $2, $3) RETURNING id, email, name, status_id;")
|
||||
sqlx::query_as::<_, UserEntity>("INSERT INTO public.user (email, password, name) VALUES ($1, $2, $3) RETURNING id, email, name;")
|
||||
.bind(email)
|
||||
.bind(password)
|
||||
.bind(name)
|
||||
.fetch_one(pool).await
|
||||
.map_err(|err| {
|
||||
error!(?err, record = ?new_user, "Cannot insert new user record");
|
||||
|
||||
AppError::from(err)
|
||||
})
|
||||
.inspect_err(|err| error!(?err, record = ?new_user, "Cannot insert new user record"))
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
|
@ -71,20 +67,16 @@ pub async fn get_username_and_password_by_email(
|
|||
.bind(email)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(?err, "Unable to find user");
|
||||
AppError::from(err)
|
||||
})
|
||||
.inspect_err(|err| error!(?err, "Unable to find user"))
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
||||
pub async fn verify_user(pool: &DbPool, user_id: i32) -> Result<(), AppError> {
|
||||
sqlx::query("UPDATE public.user SET status_id = 1, updated_at = now() WHERE id = $1;")
|
||||
sqlx::query("UPDATE public.user SET status = 'Active', updated_at = now() WHERE id = $1;")
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(?err, user_id, "Error verifying user");
|
||||
AppError::from(err)
|
||||
})
|
||||
.inspect_err(|err| error!(?err, user_id, "Error verifying user"))
|
||||
.map_err(From::from)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
|
61
api/src/db/user_account_permission.rs
Normal file
61
api/src/db/user_account_permission.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use sqlx::prelude::*;
|
||||
use tracing::error;
|
||||
|
||||
use crate::models::AppError;
|
||||
|
||||
use super::{
|
||||
get_all_permissions_by_category, get_many_permissions_by_id, DbPool, PermissionCategoryType,
|
||||
PermissionEntity, StatusType,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone)]
|
||||
pub struct NewUserAccountPermissionEntity {
|
||||
pub user_id: i32,
|
||||
pub account_id: i32,
|
||||
pub permission_id: i32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct UserAccountPermissionEntity {
|
||||
pub id: i32,
|
||||
pub user_id: i32,
|
||||
pub account_id: i32,
|
||||
pub permission_id: i32,
|
||||
pub status: StatusType,
|
||||
}
|
||||
|
||||
pub async fn associate_account_with_user_as_owner(
|
||||
pool: &DbPool,
|
||||
account_id: i32,
|
||||
user_id: i32,
|
||||
) -> Result<Vec<PermissionEntity>, AppError> {
|
||||
let values = get_all_permissions_by_category(pool, PermissionCategoryType::Account)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|permission| format!("({user_id}, {account_id}, {})", permission.id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
let query = format!("INSERT INTO public.user_account_relation (user_id, account_id, permission_id) VALUES {values} RETURNING (id, user_id, account_id, permission_id, status);");
|
||||
|
||||
let permission_ids = sqlx::query_as::<_, UserAccountPermissionEntity>(query.as_str())
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
error!(
|
||||
?err,
|
||||
account_id, user_id, "Cannot associate account with user as owner"
|
||||
)
|
||||
})
|
||||
.map_err(|err| AppError::from(err))
|
||||
.map(|relations| {
|
||||
relations
|
||||
.into_iter()
|
||||
.map(|relation| relation.permission_id)
|
||||
.collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
get_many_permissions_by_id(pool, permission_ids.as_slice()).await
|
||||
}
|
|
@ -8,7 +8,7 @@ mod services;
|
|||
use db::{create_db_connection_pool, run_migrations};
|
||||
use requests::start_app;
|
||||
use services::{
|
||||
create_cache_connection_pool, initialize_logger, start_emailer_service, UserConfirmationMessage,
|
||||
UserConfirmationMessage, create_cache_connection_pool, initialize_logger, start_emailer_service,
|
||||
};
|
||||
use tokio::runtime::Handle;
|
||||
use tracing::{error, info};
|
||||
|
@ -38,7 +38,6 @@ async fn main() {
|
|||
|
||||
info!("Initializing cache service connection pool...");
|
||||
let cache_pool = create_cache_connection_pool(config.cache_connection_uri().to_string())
|
||||
.await
|
||||
.inspect_err(|err| error!(?err))
|
||||
.unwrap();
|
||||
info!("Cache service connection pool created successfully.");
|
||||
|
|
|
@ -6,10 +6,9 @@ use std::{
|
|||
};
|
||||
|
||||
use axum::response::IntoResponse;
|
||||
use bb8_redis::bb8::RunError;
|
||||
use deadpool_redis::{PoolError, redis::RedisError};
|
||||
use http::StatusCode;
|
||||
use redis::RedisError;
|
||||
use sqlx::{error::DatabaseError, migrate::MigrateError, Error as SqlxError};
|
||||
use sqlx::{Error as SqlxError, error::DatabaseError, migrate::MigrateError};
|
||||
use toml::{self, de::Error as TomlError};
|
||||
use tracing::trace;
|
||||
|
||||
|
@ -105,8 +104,8 @@ impl From<RedisError> for AppError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<RunError<RedisError>> for AppError {
|
||||
fn from(other: RunError<RedisError>) -> Self {
|
||||
impl From<PoolError> for AppError {
|
||||
fn from(other: PoolError) -> Self {
|
||||
trace!(err = ?other, "Cache pool error");
|
||||
Self::new(ErrorKind::Cache(other.to_string()))
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use std::time::SystemTime;
|
||||
|
||||
use axum::extract::FromRequestParts;
|
||||
use deadpool_redis::redis::ToRedisArgs;
|
||||
use http::request::Parts;
|
||||
use humantime::format_rfc3339;
|
||||
use redis::ToRedisArgs;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
|
|
53
api/src/requests/account/create/handler.rs
Normal file
53
api/src/requests/account/create/handler.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use axum::{
|
||||
debug_handler,
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use http::StatusCode;
|
||||
|
||||
use crate::{
|
||||
db::{insert_new_account_for_user, DbPool, NewAccountEntity},
|
||||
models::{AppError, Session},
|
||||
requests::AppState,
|
||||
};
|
||||
|
||||
use super::models::{AccountCreationRequest, AccountCreationResponse};
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn account_creation_post_handler(
|
||||
State(state): State<AppState>,
|
||||
session: Session,
|
||||
Json(request): Json<AccountCreationRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
let pool = state.db_pool();
|
||||
let user_id = session.user_id;
|
||||
|
||||
account_creation_request(request, user_id, pool).await
|
||||
}
|
||||
|
||||
async fn account_creation_request(
|
||||
request: AccountCreationRequest,
|
||||
user_id: i32,
|
||||
pool: &DbPool,
|
||||
) -> Result<Response, AppError> {
|
||||
let AccountCreationRequest {
|
||||
r#type: account_type,
|
||||
name,
|
||||
description,
|
||||
currency_code,
|
||||
} = request;
|
||||
|
||||
let new_account = NewAccountEntity {
|
||||
name,
|
||||
description,
|
||||
account_type: account_type.into(),
|
||||
currency_code,
|
||||
};
|
||||
|
||||
let response = insert_new_account_for_user(pool, new_account, user_id)
|
||||
.await
|
||||
.map(|response| AccountCreationResponse::from(response))?;
|
||||
|
||||
Ok((StatusCode::CREATED, Json(response)).into_response())
|
||||
}
|
4
api/src/requests/account/create/mod.rs
Normal file
4
api/src/requests/account/create/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
mod handler;
|
||||
mod models;
|
||||
|
||||
pub use handler::*;
|
42
api/src/requests/account/create/models/mod.rs
Normal file
42
api/src/requests/account/create/models/mod.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
mod request;
|
||||
mod response;
|
||||
|
||||
pub use request::*;
|
||||
pub use response::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::db::AccountType as DbAccountType;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AccountType {
|
||||
Asset,
|
||||
Equity,
|
||||
Expense,
|
||||
Liability,
|
||||
Revenue,
|
||||
}
|
||||
|
||||
impl From<DbAccountType> for AccountType {
|
||||
fn from(other: DbAccountType) -> Self {
|
||||
match other {
|
||||
DbAccountType::Asset => Self::Asset,
|
||||
DbAccountType::Equity => Self::Equity,
|
||||
DbAccountType::Expense => Self::Expense,
|
||||
DbAccountType::Liability => Self::Liability,
|
||||
DbAccountType::Revenue => Self::Revenue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<DbAccountType> for AccountType {
|
||||
fn into(self) -> DbAccountType {
|
||||
match self {
|
||||
Self::Asset => DbAccountType::Asset,
|
||||
Self::Equity => DbAccountType::Equity,
|
||||
Self::Expense => DbAccountType::Expense,
|
||||
Self::Liability => DbAccountType::Liability,
|
||||
Self::Revenue => DbAccountType::Revenue,
|
||||
}
|
||||
}
|
||||
}
|
11
api/src/requests/account/create/models/request.rs
Normal file
11
api/src/requests/account/create/models/request.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
use crate::db::AccountType;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AccountCreationRequest {
|
||||
pub r#type: AccountType,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub currency_code: String,
|
||||
}
|
44
api/src/requests/account/create/models/response.rs
Normal file
44
api/src/requests/account/create/models/response.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use crate::db::{AccountEntity, PermissionEntity};
|
||||
|
||||
use super::AccountType;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AccountCreationResponse {
|
||||
pub id: i32,
|
||||
pub r#type: AccountType,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub currency_code: String,
|
||||
pub permissions: i32,
|
||||
}
|
||||
|
||||
impl From<(AccountEntity, Vec<PermissionEntity>)> for AccountCreationResponse {
|
||||
fn from(other: (AccountEntity, Vec<PermissionEntity>)) -> Self {
|
||||
let (account_entity, permissions) = other;
|
||||
|
||||
let AccountEntity {
|
||||
id,
|
||||
account_type,
|
||||
name,
|
||||
description,
|
||||
currency_code,
|
||||
..
|
||||
} = account_entity;
|
||||
|
||||
let permissions = permissions
|
||||
.into_iter()
|
||||
.map(|permission| permission.value)
|
||||
.sum();
|
||||
|
||||
Self {
|
||||
id,
|
||||
r#type: AccountType::from(account_type),
|
||||
name,
|
||||
description,
|
||||
currency_code,
|
||||
permissions,
|
||||
}
|
||||
}
|
||||
}
|
15
api/src/requests/account/mod.rs
Normal file
15
api/src/requests/account/mod.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
mod create;
|
||||
|
||||
use axum::{routing::post, Router};
|
||||
use create::account_creation_post_handler;
|
||||
|
||||
use super::AppState;
|
||||
|
||||
pub fn requests(state: AppState) -> Router {
|
||||
Router::new().nest(
|
||||
"/account",
|
||||
Router::new()
|
||||
.route("/", post(account_creation_post_handler))
|
||||
.with_state(state),
|
||||
)
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
mod account;
|
||||
mod auth;
|
||||
mod user;
|
||||
|
||||
|
@ -94,6 +95,7 @@ pub async fn start_app(
|
|||
let app = Router::new()
|
||||
.merge(user::requests(state.clone()))
|
||||
.merge(auth::requests(state.clone()))
|
||||
.merge(account::requests(state.clone()))
|
||||
.layer(logging_layer);
|
||||
|
||||
info!("API started successfully.");
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
use std::{collections::HashMap, fmt::Debug, hash::Hash, time::Duration};
|
||||
use std::{collections::HashMap, hash::Hash, time::Duration};
|
||||
|
||||
use bb8_redis::{
|
||||
bb8::{Pool, PooledConnection},
|
||||
RedisConnectionManager,
|
||||
use deadpool_redis::{
|
||||
Config, Connection, Pool,
|
||||
redis::{AsyncCommands, FromRedisValue, IntoConnectionInfo, ToRedisArgs, Value},
|
||||
};
|
||||
use redis::{AsyncCommands, FromRedisValue, IntoConnectionInfo, ToRedisArgs, Value};
|
||||
use tracing::error;
|
||||
|
||||
use crate::models::AppError;
|
||||
|
||||
pub type CachePool = Pool<RedisConnectionManager>;
|
||||
pub type CachePool = Pool;
|
||||
|
||||
pub async fn create_cache_connection_pool(
|
||||
connection_info: impl IntoConnectionInfo + Clone + Debug,
|
||||
pub fn create_cache_connection_pool(
|
||||
connection_info: impl IntoConnectionInfo,
|
||||
) -> Result<CachePool, AppError> {
|
||||
let manager = RedisConnectionManager::new(connection_info)
|
||||
.map_err(|_| AppError::connection_info("cache"))?;
|
||||
let pool = Pool::builder().build(manager).await.unwrap();
|
||||
let config = Config::from_connection_info(
|
||||
connection_info
|
||||
.into_connection_info()
|
||||
.map_err(|_| AppError::connection_info("cache"))?,
|
||||
);
|
||||
|
||||
Ok(pool)
|
||||
config
|
||||
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
|
||||
.map_err(|_| AppError::connection_info("cache"))
|
||||
}
|
||||
|
||||
pub async fn store_object<
|
||||
|
@ -90,8 +94,10 @@ pub async fn exists(cache_pool: &CachePool, key: &str) -> Result<bool, AppError>
|
|||
conn.exists(key).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn get_connection_from_pool(
|
||||
cache_pool: &CachePool,
|
||||
) -> Result<PooledConnection<'_, RedisConnectionManager>, AppError> {
|
||||
cache_pool.get().await.map_err(From::from)
|
||||
async fn get_connection_from_pool(cache_pool: &CachePool) -> Result<Connection, AppError> {
|
||||
cache_pool
|
||||
.get()
|
||||
.await
|
||||
.inspect_err(|err| error!(?err))
|
||||
.map_err(From::from)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use deadpool_redis::redis::FromRedisValue;
|
||||
use humantime::parse_rfc3339;
|
||||
use redis::FromRedisValue;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
|
|
Loading…
Add table
Reference in a new issue