From 564de627f46b7656e43b9aaca277daa387bf9d72 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Fri, 16 May 2025 19:14:54 +0530 Subject: [PATCH] feat(themes): Add ability to update email config for themes (#8033) --- crates/api_models/src/user/theme.rs | 6 +-- crates/diesel_models/src/query/user/theme.rs | 19 ++++++- crates/diesel_models/src/user/theme.rs | 34 ++++++++++++- crates/router/src/core/user/theme.rs | 52 +++++++++++--------- crates/router/src/db/kafka_store.rs | 10 ++++ crates/router/src/db/user/theme.rs | 48 +++++++++++++++++- crates/router/src/routes/user/theme.rs | 2 - 7 files changed, 137 insertions(+), 34 deletions(-) diff --git a/crates/api_models/src/user/theme.rs b/crates/api_models/src/user/theme.rs index 0616a5aa22..27a8493bc4 100644 --- a/crates/api_models/src/user/theme.rs +++ b/crates/api_models/src/user/theme.rs @@ -29,7 +29,6 @@ pub struct UploadFileAssetData { #[derive(Serialize, Deserialize, Debug)] pub struct UploadFileRequest { - pub lineage: ThemeLineage, pub asset_name: String, pub asset_data: Secret>, } @@ -44,9 +43,8 @@ pub struct CreateThemeRequest { #[derive(Serialize, Deserialize, Debug)] pub struct UpdateThemeRequest { - pub lineage: ThemeLineage, - pub theme_data: ThemeData, - // TODO: Add support to update email config + pub theme_data: Option, + pub email_config: Option, } // All the below structs are for the theme.json file, diff --git a/crates/diesel_models/src/query/user/theme.rs b/crates/diesel_models/src/query/user/theme.rs index e8fcf13577..8a9f415dd4 100644 --- a/crates/diesel_models/src/query/user/theme.rs +++ b/crates/diesel_models/src/query/user/theme.rs @@ -18,7 +18,7 @@ use crate::{ db_metrics::{track_database_call, DatabaseOperation}, }, schema::themes::dsl, - user::theme::{Theme, ThemeNew}, + user::theme::{Theme, ThemeNew, ThemeUpdate, ThemeUpdateInternal}, PgPooledConn, StorageResult, }; @@ -131,6 +131,23 @@ impl Theme { .await } + pub async fn update_by_theme_id( + conn: &PgPooledConn, + theme_id: String, + update: ThemeUpdate, + ) -> StorageResult { + let update_internal: ThemeUpdateInternal = update.into(); + + let predicate = dsl::theme_id.eq(theme_id); + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >(conn, predicate, update_internal) + .await + } + pub async fn delete_by_theme_id_and_lineage( conn: &PgPooledConn, theme_id: String, diff --git a/crates/diesel_models/src/user/theme.rs b/crates/diesel_models/src/user/theme.rs index fad4599564..b3f748fb4b 100644 --- a/crates/diesel_models/src/user/theme.rs +++ b/crates/diesel_models/src/user/theme.rs @@ -3,7 +3,8 @@ use common_utils::{ date_time, id_type, types::user::{EmailThemeConfig, ThemeLineage}, }; -use diesel::{Identifiable, Insertable, Queryable, Selectable}; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; +use router_derive::DebugAsDisplay; use time::PrimitiveDateTime; use crate::schema::themes; @@ -27,7 +28,7 @@ pub struct Theme { pub email_entity_logo_url: String, } -#[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] +#[derive(Clone, Debug, Insertable, DebugAsDisplay)] #[diesel(table_name = themes)] pub struct ThemeNew { pub theme_id: String, @@ -85,3 +86,32 @@ impl Theme { } } } + +#[derive(Clone, Debug, Default, AsChangeset, DebugAsDisplay)] +#[diesel(table_name = themes)] +pub struct ThemeUpdateInternal { + pub email_primary_color: Option, + pub email_foreground_color: Option, + pub email_background_color: Option, + pub email_entity_name: Option, + pub email_entity_logo_url: Option, +} + +#[derive(Clone)] +pub enum ThemeUpdate { + EmailConfig { email_config: EmailThemeConfig }, +} + +impl From for ThemeUpdateInternal { + fn from(value: ThemeUpdate) -> Self { + match value { + ThemeUpdate::EmailConfig { email_config } => Self { + email_primary_color: Some(email_config.primary_color), + email_foreground_color: Some(email_config.foreground_color), + email_background_color: Some(email_config.background_color), + email_entity_name: Some(email_config.entity_name), + email_entity_logo_url: Some(email_config.entity_logo_url), + }, + } + } +} diff --git a/crates/router/src/core/user/theme.rs b/crates/router/src/core/user/theme.rs index d5c9e65306..bcba04176f 100644 --- a/crates/router/src/core/user/theme.rs +++ b/crates/router/src/core/user/theme.rs @@ -3,7 +3,7 @@ use common_utils::{ ext_traits::{ByteSliceExt, Encode}, types::user::ThemeLineage, }; -use diesel_models::user::theme::ThemeNew; +use diesel_models::user::theme::{ThemeNew, ThemeUpdate}; use error_stack::ResultExt; use hyperswitch_domain_models::api::ApplicationResponse; use masking::ExposeInterface; @@ -91,17 +91,13 @@ pub async fn upload_file_to_theme_storage( ) -> UserResponse<()> { let db_theme = state .store - .find_theme_by_lineage(request.lineage) + .find_theme_by_theme_id(theme_id) .await .to_not_found_response(UserErrors::ThemeNotFound)?; - if theme_id != db_theme.theme_id { - return Err(UserErrors::ThemeNotFound.into()); - } - theme_utils::upload_file_to_theme_bucket( &state, - &theme_utils::get_specific_file_key(&theme_id, &request.asset_name), + &theme_utils::get_specific_file_key(&db_theme.theme_id, &request.asset_name), request.asset_data.expose(), ) .await?; @@ -175,26 +171,34 @@ pub async fn update_theme( theme_id: String, request: theme_api::UpdateThemeRequest, ) -> UserResponse { - let db_theme = state - .store - .find_theme_by_lineage(request.lineage) - .await - .to_not_found_response(UserErrors::ThemeNotFound)?; + let db_theme = match request.email_config { + Some(email_config) => { + let theme_update = ThemeUpdate::EmailConfig { email_config }; + state + .store + .update_theme_by_theme_id(theme_id.clone(), theme_update) + .await + .to_not_found_response(UserErrors::ThemeNotFound)? + } + None => state + .store + .find_theme_by_theme_id(theme_id) + .await + .to_not_found_response(UserErrors::ThemeNotFound)?, + }; - if theme_id != db_theme.theme_id { - return Err(UserErrors::ThemeNotFound.into()); + if let Some(theme_data) = request.theme_data { + theme_utils::upload_file_to_theme_bucket( + &state, + &theme_utils::get_theme_file_key(&db_theme.theme_id), + theme_data + .encode_to_vec() + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to parse ThemeData")?, + ) + .await?; } - theme_utils::upload_file_to_theme_bucket( - &state, - &theme_utils::get_theme_file_key(&db_theme.theme_id), - request - .theme_data - .encode_to_vec() - .change_context(UserErrors::InternalServerError)?, - ) - .await?; - let file = theme_utils::retrieve_file_from_theme_bucket( &state, &theme_utils::get_theme_file_key(&db_theme.theme_id), diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 8dbef1d30f..8c4ac07a32 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -4137,6 +4137,16 @@ impl ThemeInterface for KafkaStore { self.diesel_store.find_theme_by_lineage(lineage).await } + async fn update_theme_by_theme_id( + &self, + theme_id: String, + theme_update: diesel_models::user::theme::ThemeUpdate, + ) -> CustomResult { + self.diesel_store + .update_theme_by_theme_id(theme_id, theme_update) + .await + } + async fn delete_theme_by_lineage_and_theme_id( &self, theme_id: String, diff --git a/crates/router/src/db/user/theme.rs b/crates/router/src/db/user/theme.rs index 4202b69d99..0a52144e5c 100644 --- a/crates/router/src/db/user/theme.rs +++ b/crates/router/src/db/user/theme.rs @@ -1,5 +1,5 @@ use common_utils::types::user::ThemeLineage; -use diesel_models::user::theme as storage; +use diesel_models::user::theme::{self as storage, ThemeUpdate}; use error_stack::report; use super::MockDb; @@ -31,6 +31,12 @@ pub trait ThemeInterface { lineage: ThemeLineage, ) -> CustomResult; + async fn update_theme_by_theme_id( + &self, + theme_id: String, + theme_update: ThemeUpdate, + ) -> CustomResult; + async fn delete_theme_by_lineage_and_theme_id( &self, theme_id: String, @@ -81,6 +87,17 @@ impl ThemeInterface for Store { .map_err(|error| report!(errors::StorageError::from(error))) } + async fn update_theme_by_theme_id( + &self, + theme_id: String, + theme_update: ThemeUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Theme::update_by_theme_id(&conn, theme_id, theme_update) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } + async fn delete_theme_by_lineage_and_theme_id( &self, theme_id: String, @@ -256,6 +273,35 @@ impl ThemeInterface for MockDb { ) } + async fn update_theme_by_theme_id( + &self, + theme_id: String, + theme_update: ThemeUpdate, + ) -> CustomResult { + let mut themes = self.themes.lock().await; + themes + .iter_mut() + .find(|theme| theme.theme_id == theme_id) + .map(|theme| { + match theme_update { + ThemeUpdate::EmailConfig { email_config } => { + theme.email_primary_color = email_config.primary_color; + theme.email_foreground_color = email_config.foreground_color; + theme.email_background_color = email_config.background_color; + theme.email_entity_name = email_config.entity_name; + theme.email_entity_logo_url = email_config.entity_logo_url; + } + } + theme.clone() + }) + .ok_or_else(|| { + report!(errors::StorageError::ValueNotFound(format!( + "Theme with id {} not found", + theme_id, + ))) + }) + } + async fn delete_theme_by_lineage_and_theme_id( &self, theme_id: String, diff --git a/crates/router/src/routes/user/theme.rs b/crates/router/src/routes/user/theme.rs index 6f6926dadc..3437d393bd 100644 --- a/crates/router/src/routes/user/theme.rs +++ b/crates/router/src/routes/user/theme.rs @@ -56,12 +56,10 @@ pub async fn upload_file_to_theme_storage( req: HttpRequest, path: web::Path, MultipartForm(payload): MultipartForm, - query: web::Query, ) -> HttpResponse { let flow = Flow::UploadFileToThemeStorage; let theme_id = path.into_inner(); let payload = theme_api::UploadFileRequest { - lineage: query.into_inner(), asset_name: payload.asset_name.into_inner(), asset_data: Secret::new(payload.asset_data.data.to_vec()), };