Create budget creation request
This commit is contained in:
parent
efd9e80633
commit
cd013d0009
13 changed files with 275 additions and 11 deletions
|
@ -6,9 +6,12 @@ DROP INDEX IF EXISTS permission_category_idx;
|
|||
DROP INDEX IF EXISTS permission_category_name_idx;
|
||||
DROP INDEX IF EXISTS user_account_permission_user_id_idx;
|
||||
DROP INDEX IF EXISTS user_account_permission_account_id_idx;
|
||||
DROP INDEX IF EXISTS transaction_budget_id_idx
|
||||
DROP INDEX IF EXISTS transaction_budget_id_idx;
|
||||
DROP INDEX IF EXISTS user_budget_permission_user_id_idx;
|
||||
DROP INDEX IF EXISTS user_budget_permission_budget_id_idx;
|
||||
|
||||
DROP TABLE IF EXISTS public.user_account_permission;
|
||||
DROP TABLE IF EXISTS public.user_budget_permission;
|
||||
DROP TABLE IF EXISTS public.account;
|
||||
DROP TABLE IF EXISTS public.permission;
|
||||
DROP TABLE IF EXISTS public.user;
|
||||
|
@ -16,7 +19,7 @@ DROP TABLE IF EXISTS public.transaction_line_item;
|
|||
DROP TABLE IF EXISTS public.transaction CASCADE;
|
||||
DROP TABLE IF EXISTS public.budget CASCADE;
|
||||
|
||||
DROP TYPE public.entry_type;
|
||||
DROP TYPE public.account_type;
|
||||
DROP TYPE public.permission_category;
|
||||
DROP TYPE public.status;
|
||||
DROP TYPE IF EXISTS public.entry_type;
|
||||
DROP TYPE IF EXISTS public.account_type;
|
||||
DROP TYPE IF EXISTS public.permission_category;
|
||||
DROP TYPE IF EXISTS public.status;
|
||||
|
|
|
@ -32,7 +32,7 @@ AS ENUM
|
|||
CREATE TYPE
|
||||
public.permission_category
|
||||
AS ENUM
|
||||
('Account');
|
||||
('Account', 'Budget');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.user (
|
||||
|
@ -71,7 +71,11 @@ INSERT INTO
|
|||
('Account', 'View', 1),
|
||||
('Account', 'Edit', 2),
|
||||
('Account', 'Delete', 4),
|
||||
('Account', 'Grant', 8);
|
||||
('Account', 'Grant', 8),
|
||||
('Budget', 'View', 1),
|
||||
('Budget', 'Edit', 2),
|
||||
('Budget', 'Delete', 4),
|
||||
('Budget', 'Grant', 8);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.account (
|
||||
|
@ -110,6 +114,20 @@ CREATE TABLE IF NOT EXISTS
|
|||
updated_at TIMESTAMP WITH TIME ZONE NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.user_budget_permission (
|
||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
user_id INT NOT NULL REFERENCES public.user(id),
|
||||
budget_id UUID NOT NULL REFERENCES public.budget(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 INDEX IF NOT EXISTS user_budget_permission_user_id_idx ON public.user_budget_permission(user_id);
|
||||
CREATE INDEX IF NOT EXISTS user_budget_permission_budget_id_idx ON public.user_budget_permission(budget_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
public.transaction (
|
||||
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||
|
|
50
api/src/db/budget.rs
Normal file
50
api/src/db/budget.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use sqlx::prelude::*;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::AppError;
|
||||
|
||||
use super::{DbPool, PermissionEntity, StatusType, associate_budget_with_user_as_owner};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NewBudgetEntity {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct BudgetEntity {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub status: StatusType,
|
||||
}
|
||||
|
||||
pub async fn insert_new_budget_for_user(
|
||||
pool: &DbPool,
|
||||
new_budget: NewBudgetEntity,
|
||||
user_id: i32,
|
||||
) -> Result<(BudgetEntity, Vec<PermissionEntity>), AppError> {
|
||||
let NewBudgetEntity {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
} = &new_budget;
|
||||
|
||||
let new_budget = sqlx::query_as::<_, BudgetEntity>("INSERT INTO public.budget (name, description, icon) VALUES ($1, $2, $3) RETURNING id, name, description, icon, status;")
|
||||
.bind(name)
|
||||
.bind(description)
|
||||
.bind(icon)
|
||||
.fetch_one(pool).await
|
||||
.inspect_err(|err| error!(?err, record = ?new_budget, "Cannot insert new budget record"))
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
let budget_permissions =
|
||||
associate_budget_with_user_as_owner(pool, new_budget.id, user_id).await?;
|
||||
|
||||
Ok((new_budget, budget_permissions))
|
||||
}
|
|
@ -1,18 +1,22 @@
|
|||
mod account;
|
||||
mod account_type;
|
||||
mod budget;
|
||||
mod permission;
|
||||
mod status;
|
||||
mod user;
|
||||
mod user_account_permission;
|
||||
mod user_budget_permission;
|
||||
|
||||
pub use account::*;
|
||||
pub use account_type::*;
|
||||
pub use budget::*;
|
||||
pub use permission::*;
|
||||
pub use status::*;
|
||||
pub use user::*;
|
||||
pub use user_account_permission::*;
|
||||
pub use user_budget_permission::*;
|
||||
|
||||
use sqlx::{self, postgres::PgPoolOptions, Pool, Postgres};
|
||||
use sqlx::{self, Pool, Postgres, postgres::PgPoolOptions};
|
||||
|
||||
use crate::models::AppError;
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ use super::DbPool;
|
|||
#[sqlx(type_name = "permission_category", rename_all = "PascalCase")]
|
||||
pub enum PermissionCategoryType {
|
||||
Account,
|
||||
Budget,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
|
61
api/src/db/user_budget_permission.rs
Normal file
61
api/src/db/user_budget_permission.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use sqlx::prelude::*;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::AppError;
|
||||
|
||||
use super::{
|
||||
DbPool, PermissionCategoryType, PermissionEntity, StatusType, get_all_permissions_by_category,
|
||||
get_many_permissions_by_id,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone)]
|
||||
pub struct NewUserBudgetPermissionEntity {
|
||||
pub user_id: i32,
|
||||
pub budget_id: Uuid,
|
||||
pub permission_id: i32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct UserBudgetPermissionEntity {
|
||||
pub id: i32,
|
||||
pub user_id: i32,
|
||||
pub budget_id: Uuid,
|
||||
pub permission_id: i32,
|
||||
pub status: StatusType,
|
||||
}
|
||||
|
||||
pub async fn associate_budget_with_user_as_owner(
|
||||
pool: &DbPool,
|
||||
budget_id: Uuid,
|
||||
user_id: i32,
|
||||
) -> Result<Vec<PermissionEntity>, AppError> {
|
||||
let values = get_all_permissions_by_category(pool, PermissionCategoryType::Budget)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|permission| format!("({user_id}, {budget_id}, {})", permission.id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
let query = format!(
|
||||
"INSERT INTO public.user_budget_permission (user_id, budget_id, permission_id) VALUES {values} RETURNING id, user_id, budget_id, permission_id, status;"
|
||||
);
|
||||
|
||||
let permission_ids = sqlx::query_as::<_, UserBudgetPermissionEntity>(query.as_str())
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.inspect_err(
|
||||
|err| error!(?err, %budget_id, user_id, "Cannot associate budget with user as owner"),
|
||||
)
|
||||
.map_err(AppError::from)
|
||||
.map(|relations| {
|
||||
relations
|
||||
.into_iter()
|
||||
.map(|relation| relation.permission_id)
|
||||
.collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
get_many_permissions_by_id(pool, permission_ids.as_slice()).await
|
||||
}
|
52
api/src/requests/budget/create/handler.rs
Normal file
52
api/src/requests/budget/create/handler.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use axum::{
|
||||
Json, debug_handler,
|
||||
extract::State,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use http::StatusCode;
|
||||
|
||||
use crate::{
|
||||
db::{DbPool, NewBudgetEntity, insert_new_budget_for_user},
|
||||
models::{AppError, Session},
|
||||
requests::AppState,
|
||||
};
|
||||
|
||||
use super::models::{BudgetCreationRequest, BudgetCreationResponse};
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn budget_create_post_request(
|
||||
State(state): State<AppState>,
|
||||
session: Session,
|
||||
Json(request): Json<BudgetCreationRequest>,
|
||||
) -> Result<Response, AppError> {
|
||||
let pool = state.db_pool();
|
||||
let user_id = session.user_id;
|
||||
|
||||
budget_creation_request(pool, user_id, request)
|
||||
.await
|
||||
.map(|(status_code, response)| (status_code, Json(response)).into_response())
|
||||
}
|
||||
|
||||
async fn budget_creation_request(
|
||||
pool: &DbPool,
|
||||
user_id: i32,
|
||||
request: BudgetCreationRequest,
|
||||
) -> Result<(StatusCode, BudgetCreationResponse), AppError> {
|
||||
let BudgetCreationRequest {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
} = request;
|
||||
|
||||
let new_budget = NewBudgetEntity {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
};
|
||||
|
||||
let response = insert_new_budget_for_user(pool, new_budget, user_id)
|
||||
.await
|
||||
.map(BudgetCreationResponse::from)?;
|
||||
|
||||
Ok((StatusCode::CREATED, response))
|
||||
}
|
4
api/src/requests/budget/create/mod.rs
Normal file
4
api/src/requests/budget/create/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
mod handler;
|
||||
mod models;
|
||||
|
||||
pub use handler::*;
|
5
api/src/requests/budget/create/models/mod.rs
Normal file
5
api/src/requests/budget/create/models/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod request;
|
||||
mod response;
|
||||
|
||||
pub use request::*;
|
||||
pub use response::*;
|
9
api/src/requests/budget/create/models/request.rs
Normal file
9
api/src/requests/budget/create/models/request.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BudgetCreationRequest {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
}
|
40
api/src/requests/budget/create/models/response.rs
Normal file
40
api/src/requests/budget/create/models/response.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::db::{BudgetEntity, PermissionEntity};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BudgetCreationResponse {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub permissions: i32,
|
||||
}
|
||||
|
||||
impl From<(BudgetEntity, Vec<PermissionEntity>)> for BudgetCreationResponse {
|
||||
fn from(other: (BudgetEntity, Vec<PermissionEntity>)) -> Self {
|
||||
let (budget_entity, permissions) = other;
|
||||
|
||||
let BudgetEntity {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
..
|
||||
} = budget_entity;
|
||||
|
||||
let permissions = permissions
|
||||
.into_iter()
|
||||
.map(|permission| permission.value)
|
||||
.sum();
|
||||
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
permissions,
|
||||
}
|
||||
}
|
||||
}
|
15
api/src/requests/budget/mod.rs
Normal file
15
api/src/requests/budget/mod.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use axum::{Router, routing::post};
|
||||
use create::budget_create_post_request;
|
||||
|
||||
use super::AppState;
|
||||
|
||||
mod create;
|
||||
|
||||
pub fn requests(state: AppState) -> Router {
|
||||
Router::new().nest(
|
||||
"/budget",
|
||||
Router::new()
|
||||
.route("/", post(budget_create_post_request))
|
||||
.with_state(state),
|
||||
)
|
||||
}
|
|
@ -1,24 +1,25 @@
|
|||
mod account;
|
||||
mod auth;
|
||||
mod budget;
|
||||
mod user;
|
||||
|
||||
use std::{sync::mpsc::Sender, time::Duration};
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{MatchedPath, Request},
|
||||
response::Response,
|
||||
Router,
|
||||
};
|
||||
use humantime::format_duration;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::{error, info, info_span, warn, Span};
|
||||
use tracing::{Span, error, info, info_span, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
db::DbPool,
|
||||
models::AppError,
|
||||
services::{config::Config, CachePool, UserConfirmationMessage},
|
||||
services::{CachePool, UserConfirmationMessage, config::Config},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -96,6 +97,7 @@ pub async fn start_app(
|
|||
.merge(user::requests(state.clone()))
|
||||
.merge(auth::requests(state.clone()))
|
||||
.merge(account::requests(state.clone()))
|
||||
.merge(budget::requests(state.clone()))
|
||||
.layer(logging_layer);
|
||||
|
||||
info!("API started successfully.");
|
||||
|
|
Loading…
Add table
Reference in a new issue