feat(themes): Add ability to update email config for themes (#8033)

This commit is contained in:
Mani Chandra
2025-05-16 19:14:54 +05:30
committed by GitHub
parent 0e0686f92a
commit 564de627f4
7 changed files with 137 additions and 34 deletions

View File

@ -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<Vec<u8>>,
}
@ -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<ThemeData>,
pub email_config: Option<EmailThemeConfig>,
}
// All the below structs are for the theme.json file,

View File

@ -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<Self> {
let update_internal: ThemeUpdateInternal = update.into();
let predicate = dsl::theme_id.eq(theme_id);
generics::generic_update_with_unique_predicate_get_result::<
<Self as HasTable>::Table,
_,
_,
_,
>(conn, predicate, update_internal)
.await
}
pub async fn delete_by_theme_id_and_lineage(
conn: &PgPooledConn,
theme_id: String,

View File

@ -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<String>,
pub email_foreground_color: Option<String>,
pub email_background_color: Option<String>,
pub email_entity_name: Option<String>,
pub email_entity_logo_url: Option<String>,
}
#[derive(Clone)]
pub enum ThemeUpdate {
EmailConfig { email_config: EmailThemeConfig },
}
impl From<ThemeUpdate> 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),
},
}
}
}

View File

@ -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<theme_api::GetThemeResponse> {
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),

View File

@ -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<diesel_models::user::theme::Theme, errors::StorageError> {
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,

View File

@ -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<storage::Theme, errors::StorageError>;
async fn update_theme_by_theme_id(
&self,
theme_id: String,
theme_update: ThemeUpdate,
) -> CustomResult<storage::Theme, errors::StorageError>;
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<storage::Theme, errors::StorageError> {
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<storage::Theme, errors::StorageError> {
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,

View File

@ -56,12 +56,10 @@ pub async fn upload_file_to_theme_storage(
req: HttpRequest,
path: web::Path<String>,
MultipartForm(payload): MultipartForm<theme_api::UploadFileAssetData>,
query: web::Query<ThemeLineage>,
) -> 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()),
};