From 2bdb3ff67aa899b10f37f6e3ae0313909c2e794a Mon Sep 17 00:00:00 2001 From: "Z. Charles Dziura" Date: Thu, 6 Mar 2025 16:05:12 -0500 Subject: [PATCH] Create endpoint to fetch all accounts for a user --- api/src/db/account.rs | 74 ++++++++++++++++++- api/src/db/account_type.rs | 3 +- api/src/requests/account/create/handler.rs | 11 ++- api/src/requests/account/mod.rs | 8 +- api/src/requests/account/read/handler.rs | 45 +++++++++++ api/src/requests/account/read/mod.rs | 4 + api/src/requests/account/read/models/mod.rs | 3 + .../requests/account/read/models/response.rs | 38 ++++++++++ 8 files changed, 177 insertions(+), 9 deletions(-) create mode 100644 api/src/requests/account/read/handler.rs create mode 100644 api/src/requests/account/read/mod.rs create mode 100644 api/src/requests/account/read/models/mod.rs create mode 100644 api/src/requests/account/read/models/response.rs diff --git a/api/src/db/account.rs b/api/src/db/account.rs index 21802f0..3150ff9 100644 --- a/api/src/db/account.rs +++ b/api/src/db/account.rs @@ -4,7 +4,7 @@ use tracing::error; use crate::models::AppError; use super::{ - associate_account_with_user_as_owner, AccountType, DbPool, PermissionEntity, StatusType, + AccountType, DbPool, PermissionEntity, StatusType, associate_account_with_user_as_owner, }; #[allow(dead_code)] @@ -27,6 +27,18 @@ pub struct AccountEntity { pub status: StatusType, } +#[allow(dead_code)] +#[derive(Debug, Default, FromRow)] +pub struct AccountWithPermissionValueEntity { + pub id: i32, + pub account_type: AccountType, + pub name: String, + pub description: Option, + pub currency_code: String, + pub permission_name: String, + pub permission_value: i32, +} + pub async fn insert_new_account_for_user( pool: &DbPool, new_account: NewAccountEntity, @@ -53,3 +65,63 @@ pub async fn insert_new_account_for_user( Ok((new_account, account_permissions)) } + +pub async fn get_all_accounts_for_user( + pool: &DbPool, + user_id: i32, +) -> Result, AppError> { + sqlx::query_as::<_, AccountWithPermissionValueEntity>( + "SELECT + a.id AS id, + a.account_type AS account_type, + a.name AS name, + a.description AS description, + a.currency_code AS currency_code, + p.name AS permission_name, + p.value AS permission_value + FROM + public.account a + INNER JOIN public.user_account_permission uap ON a.id = uap.account_id + INNER JOIN public.permission p ON uap.permission_id = p.id + WHERE + uap.user_id = $1 + ORDER BY + a.id, + p.value;", + ) + .bind(user_id) + .fetch_all(pool) + .await + .inspect_err(|err| { + error!( + ?err, + user_id, "Cannot fetch permission values for the user's accounts" + ) + }) + .map_err(AppError::from) + .map(|all_accounts| { + all_accounts + .chunk_by(|a, b| a.id == b.id) + .map(|accounts| { + accounts.into_iter().fold( + AccountWithPermissionValueEntity::default(), + |acc, entity| AccountWithPermissionValueEntity { + id: entity.id, + account_type: entity.account_type, + name: entity.name.clone(), + description: entity.description.clone(), + currency_code: entity.currency_code.clone(), + permission_name: acc + .permission_name + .split('|') + .filter(|name| !name.is_empty()) + .chain([entity.permission_name.as_str()]) + .collect::>() + .join("|"), + permission_value: acc.permission_value + entity.permission_value, + }, + ) + }) + .collect::>() + }) +} diff --git a/api/src/db/account_type.rs b/api/src/db/account_type.rs index a52668e..328733b 100644 --- a/api/src/db/account_type.rs +++ b/api/src/db/account_type.rs @@ -1,11 +1,12 @@ use serde::{Deserialize, Serialize}; #[derive( - Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, sqlx::Type, + Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize, sqlx::Type, )] #[sqlx(type_name = "account_type", rename_all = "PascalCase")] #[serde(rename_all = "lowercase")] pub enum AccountType { + #[default] Asset, Equity, Expense, diff --git a/api/src/requests/account/create/handler.rs b/api/src/requests/account/create/handler.rs index 44d3c6d..c59be0c 100644 --- a/api/src/requests/account/create/handler.rs +++ b/api/src/requests/account/create/handler.rs @@ -1,13 +1,12 @@ use axum::{ - debug_handler, + Json, debug_handler, extract::State, response::{IntoResponse, Response}, - Json, }; use http::StatusCode; use crate::{ - db::{insert_new_account_for_user, DbPool, NewAccountEntity}, + db::{DbPool, NewAccountEntity, insert_new_account_for_user}, models::{AppError, Session}, requests::AppState, }; @@ -23,13 +22,13 @@ pub async fn account_creation_post_handler( let pool = state.db_pool(); let user_id = session.user_id; - account_creation_request(request, user_id, pool).await + account_creation_request(pool, user_id, request).await } async fn account_creation_request( - request: AccountCreationRequest, - user_id: i32, pool: &DbPool, + user_id: i32, + request: AccountCreationRequest, ) -> Result { let AccountCreationRequest { r#type: account_type, diff --git a/api/src/requests/account/mod.rs b/api/src/requests/account/mod.rs index 46ebd45..fd02eb6 100644 --- a/api/src/requests/account/mod.rs +++ b/api/src/requests/account/mod.rs @@ -1,7 +1,12 @@ mod create; +mod read; -use axum::{routing::post, Router}; +use axum::{ + Router, + routing::{get, post}, +}; use create::account_creation_post_handler; +use read::account_read_all_get_request; use super::AppState; @@ -10,6 +15,7 @@ pub fn requests(state: AppState) -> Router { "/account", Router::new() .route("/", post(account_creation_post_handler)) + .route("/", get(account_read_all_get_request)) .with_state(state), ) } diff --git a/api/src/requests/account/read/handler.rs b/api/src/requests/account/read/handler.rs new file mode 100644 index 0000000..dc47957 --- /dev/null +++ b/api/src/requests/account/read/handler.rs @@ -0,0 +1,45 @@ +use axum::{ + Json, debug_handler, + extract::State, + response::{IntoResponse, Response}, +}; +use http::StatusCode; + +use crate::{ + db::{DbPool, get_all_accounts_for_user}, + models::{ApiResponse, AppError, Session}, + requests::AppState, +}; + +use super::models::AccountsReadAllResponse; + +#[debug_handler] +pub async fn account_read_all_get_request( + State(state): State, + session: Session, +) -> Result { + let pool = state.db_pool(); + let user_id = session.user_id; + + account_read_all_request(pool, user_id) + .await + .map(|(status_code, response)| { + (status_code, Json(ApiResponse::new(response))).into_response() + }) +} + +async fn account_read_all_request( + pool: &DbPool, + user_id: i32, +) -> Result<(StatusCode, Vec), AppError> { + let accounts = get_all_accounts_for_user(pool, user_id) + .await + .map(|accounts| { + accounts + .into_iter() + .map(AccountsReadAllResponse::from) + .collect::>() + })?; + + Ok((StatusCode::OK, accounts)) +} diff --git a/api/src/requests/account/read/mod.rs b/api/src/requests/account/read/mod.rs new file mode 100644 index 0000000..f00e7ca --- /dev/null +++ b/api/src/requests/account/read/mod.rs @@ -0,0 +1,4 @@ +mod handler; +mod models; + +pub use handler::*; diff --git a/api/src/requests/account/read/models/mod.rs b/api/src/requests/account/read/models/mod.rs new file mode 100644 index 0000000..1d28ef9 --- /dev/null +++ b/api/src/requests/account/read/models/mod.rs @@ -0,0 +1,3 @@ +mod response; + +pub use response::*; diff --git a/api/src/requests/account/read/models/response.rs b/api/src/requests/account/read/models/response.rs new file mode 100644 index 0000000..b9a7580 --- /dev/null +++ b/api/src/requests/account/read/models/response.rs @@ -0,0 +1,38 @@ +use serde::Serialize; + +use crate::db::{AccountType, AccountWithPermissionValueEntity}; + +#[derive(Debug, Serialize)] +pub struct AccountsReadAllResponse { + pub id: i32, + pub account_type: AccountType, + pub name: String, + pub description: Option, + pub currency_code: String, + pub permission_name: String, + pub permission_value: i32, +} + +impl From for AccountsReadAllResponse { + fn from(other: AccountWithPermissionValueEntity) -> Self { + let AccountWithPermissionValueEntity { + id, + account_type, + name, + description, + currency_code, + permission_name, + permission_value, + } = other; + + Self { + id, + account_type, + name, + description, + currency_code, + permission_name, + permission_value, + } + } +}