• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

doitsu2014 / my-cms / 14187034714

01 Apr 2025 04:12AM UTC coverage: 59.597% (+20.5%) from 39.058%
14187034714

push

github

web-flow
Feature/support multi language (#19)

* Enhance entity structure: add category and post translations, update migration dependencies, and improve README guidelines

* Add support for category translations: update create and modify handlers, requests, and tests

add TODO

* Add category translations support: update create, modify, and read handlers, and adjust request structures

* Refactor category translation requests: remove slug field and update handlers to set category ID for translations

* Add support for post translations: update create and modify requests, handlers, and models

* Update CI configuration and scripts for improved coverage reporting and toolchain management

* Update coverage configuration and scripts for improved reporting and toolchain management

* Update CI and coverage configurations for improved reporting and ignore patterns

* Update CI configuration and coverage scripts for improved reporting and cleanup

* Remove unused coverage step from CI configuration

* Update CI configuration to use fixed lcov report paths for coverage uploads

197 of 396 new or added lines in 19 files covered. (49.75%)

71 existing lines in 11 files now uncovered.

975 of 1636 relevant lines covered (59.6%)

26.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.96
/application_core/src/commands/category/modify/modify_handler.rs
1
use std::sync::Arc;
2

3
use crate::{
4
    commands::{
5
        category::read::category_read_handler::{
6
            self, CategoryReadHandlerTrait, CategoryReadResponse,
7
        },
8
        tag::create::create_handler::{TagCreateHandler, TagCreateHandlerTrait},
9
    },
10
    common::app_error::AppError,
11
    entities::{
12
        categories::{self, Column},
13
        category_tags,
14
        category_translations::{self},
15
    },
16
};
17
use sea_orm::{
18
    prelude::Uuid, sea_query::Expr, DatabaseConnection, EntityTrait, QueryFilter, Set,
19
    TransactionTrait,
20
};
21
use seaography::itertools::Itertools;
22
use tracing::instrument;
23

24
use super::modify_request::ModifyCategoryRequest;
25

26
pub trait CategoryModifyHandlerTrait {
27
    fn handle_modify_category(
28
        &self,
29
        body: ModifyCategoryRequest,
30
        actor_email: Option<String>,
31
    ) -> impl std::future::Future<Output = Result<Uuid, AppError>>;
32
}
33

34
#[derive(Debug)]
35
pub struct CategoryModifyHandler {
36
    pub db: Arc<DatabaseConnection>,
37
}
38

39
impl CategoryModifyHandlerTrait for CategoryModifyHandler {
40
    #[instrument]
41
    async fn handle_modify_category(
42
        &self,
43
        body: ModifyCategoryRequest,
44
        actor_email: Option<String>,
45
    ) -> Result<Uuid, AppError> {
46
        let category_read_handler = category_read_handler::CategoryReadHandler {
47
            db: self.db.clone(),
48
        };
49

50
        let tag_create_handler = TagCreateHandler {
51
            db: self.db.clone(),
52
        };
53

54
        // Exec Transaction to Update Category, Tags, and Translations
55
        // Update the category with current row version, if row version is not matched, return error
56
        let result: Result<Uuid, AppError> = self
57
            .db
58
            .as_ref()
59
            .transaction::<_, Uuid, AppError>(|tx| {
2✔
60
                Box::pin(async move {
2✔
61
                    // 1. Prepare Active Category
2✔
62
                    let modified_id = body.id;
2✔
63
                    let current_row_version = body.row_version;
2✔
64
                    let mut model = body.into_active_model();
2✔
65
                    model.last_modified_by = Set(actor_email.clone());
2✔
66

2✔
67
                    // 2. Insert new tags
2✔
68
                    let processing_tags: Vec<String> = body.tag_names.unwrap_or_default().clone();
2✔
69
                    let create_tags_response = tag_create_handler
2✔
70
                        .handle_create_tags_in_transaction(
2✔
71
                            processing_tags.clone(),
2✔
72
                            actor_email.clone(),
2✔
73
                            tx,
2✔
74
                        )
2✔
75
                        .await?;
2✔
76

77
                    // 2.1. Get existing category
78
                    let db_category: CategoryReadResponse = category_read_handler
2✔
79
                        .handle_get_category(modified_id)
2✔
80
                        .await?;
2✔
81

82
                    // 3. Update Category and Tags
83
                    // 3.1. Delete
84
                    let lower_case_tags: Vec<String> = processing_tags
2✔
85
                        .clone()
2✔
86
                        .iter()
2✔
87
                        .map(|t| t.to_lowercase())
2✔
88
                        .collect();
2✔
89

2✔
90
                    let tags_to_delete: Vec<Uuid> = db_category
2✔
91
                        .tags
2✔
92
                        .iter()
2✔
93
                        .filter(|t| !lower_case_tags.contains(&t.name.to_lowercase()))
7✔
94
                        .map(|t| t.id)
7✔
95
                        .collect();
2✔
96

2✔
97
                    if !tags_to_delete.is_empty() {
2✔
98
                        category_tags::Entity::delete_many()
2✔
99
                            .filter(Expr::col(category_tags::Column::CategoryId).eq(modified_id))
2✔
100
                            .filter(Expr::col(category_tags::Column::TagId).is_in(tags_to_delete))
2✔
101
                            .exec(tx)
2✔
102
                            .await
2✔
103
                            .map_err(|err| err.into())?;
2✔
UNCOV
104
                    }
×
105

106
                    // 3.2. Insert Category Tags
107
                    let binded_tag_ids = db_category
2✔
108
                        .tags
2✔
109
                        .iter()
2✔
110
                        .map(|tag| tag.id.to_owned())
7✔
111
                        .collect_vec();
2✔
112

2✔
113
                    let combined_ids = create_tags_response
2✔
114
                        .new_tag_ids
2✔
115
                        .iter()
2✔
116
                        .chain(create_tags_response.existing_tag_ids.iter())
2✔
117
                        .map(|id| id.to_owned())
2✔
118
                        .filter(|id| !binded_tag_ids.contains(id))
2✔
119
                        .collect_vec();
2✔
120

2✔
121
                    if !combined_ids.is_empty() {
2✔
122
                        let category_tags_to_insert = combined_ids
×
UNCOV
123
                            .iter()
×
124
                            .map(|tag_id| category_tags::ActiveModel {
×
125
                                category_id: Set(body.id),
×
126
                                tag_id: Set(tag_id.to_owned()),
×
127
                            })
×
UNCOV
128
                            .collect::<Vec<category_tags::ActiveModel>>();
×
UNCOV
129

×
130
                        category_tags::Entity::insert_many(category_tags_to_insert)
×
131
                            .exec(tx)
×
132
                            .await
×
133
                            .map_err(|err| err.into())?;
×
134
                    }
2✔
135

136
                    // 3.3. Modify Category information
137
                    let modified_result = categories::Entity::update_many()
2✔
138
                        .set(model)
2✔
139
                        .filter(Expr::col(Column::Id).eq(modified_id))
2✔
140
                        .filter(Expr::col(Column::RowVersion).eq(current_row_version))
2✔
141
                        .exec(tx)
2✔
142
                        .await
2✔
143
                        .map_err(|err| err.into())?;
2✔
144

145
                    match modified_result.rows_affected == 0 {
2✔
146
                        true => {
147
                            return Err(AppError::Logical("Row version is not matched".to_string()))
1✔
148
                        }
149
                        false => (),
1✔
150
                    }
151

152
                    // 4. Update Translations
153
                    if let Some(request_translations) = body.translations {
1✔
154
                        // Collect language codes from the incoming translations
NEW
155
                        let incoming_translation_ids: Vec<Uuid> = request_translations
×
NEW
156
                            .iter()
×
NEW
157
                            .filter(|t| t.id.is_some())
×
NEW
158
                            .map(|translation| translation.id.to_owned().unwrap_or_default())
×
NEW
159
                            .collect();
×
160

161
                        // Find existing translations for the category
NEW
162
                        let existing_translations = category_translations::Entity::find()
×
NEW
163
                            .filter(
×
NEW
164
                                Expr::col(category_translations::Column::CategoryId)
×
NEW
165
                                    .eq(modified_id),
×
NEW
166
                            )
×
NEW
167
                            .all(tx)
×
NEW
168
                            .await
×
NEW
169
                            .map_err(|err| err.into())?;
×
170

171
                        // Identify translations to delete
NEW
172
                        let translations_to_delete: Vec<Uuid> = existing_translations
×
NEW
173
                            .iter()
×
NEW
174
                            .filter(|existing_translation| {
×
NEW
175
                                !incoming_translation_ids.contains(&existing_translation.id)
×
NEW
176
                            })
×
NEW
177
                            .map(|existing_translation| existing_translation.id)
×
NEW
178
                            .collect();
×
NEW
179

×
NEW
180
                        // Delete translations that are no longer present
×
NEW
181
                        if !translations_to_delete.is_empty() {
×
NEW
182
                            category_translations::Entity::delete_many()
×
NEW
183
                                .filter(
×
NEW
184
                                    Expr::col(category_translations::Column::Id)
×
NEW
185
                                        .is_in(translations_to_delete),
×
NEW
186
                                )
×
NEW
187
                                .exec(tx)
×
NEW
188
                                .await
×
NEW
189
                                .map_err(|err| err.into())?;
×
NEW
190
                        }
×
191

192
                        // Process incoming translations
NEW
193
                        for request_translation in request_translations {
×
NEW
194
                            if request_translation.id.is_some() {
×
195
                                // If the translation ID is present, it means we are updating an existing translation
NEW
196
                                let mut existing_translation = request_translation.into_active_model();
×
NEW
197
                                existing_translation.category_id = Set(modified_id);
×
NEW
198
                                category_translations::Entity::update(existing_translation)
×
NEW
199
                                    .exec(tx)
×
NEW
200
                                    .await
×
NEW
201
                                    .map_err(|err| err.into())?;
×
202
                            } else {
203
                                // If the translation ID is not present, it means we are creating a new translation
NEW
204
                                let mut new_translation = request_translation.into_active_model();
×
NEW
205
                                new_translation.category_id = Set(modified_id);
×
NEW
206
                                category_translations::Entity::insert(new_translation)
×
NEW
207
                                    .exec(tx)
×
NEW
208
                                    .await
×
NEW
209
                                    .map_err(|err| err.into())?;
×
210
                            }
211
                        }
212
                    }
1✔
213

214
                    Ok(modified_id)
1✔
215
                })
2✔
216
            })
2✔
217
            .await
218
            .map_err(|e| e.into());
1✔
219

220
        result
221
    }
222
}
223

224
#[cfg(test)]
225
mod tests {
226
    use std::sync::Arc;
227
    use test_helpers::{setup_test_space, ContainerAsyncPostgresEx};
228

229
    use crate::{
230
        commands::category::{
231
            create::create_handler::{CategoryCreateHandler, CategoryCreateHandlerTrait},
232
            modify::{
233
                modify_handler::{CategoryModifyHandler, CategoryModifyHandlerTrait},
234
                modify_request::ModifyCategoryRequest,
235
            },
236
            read::category_read_handler::{CategoryReadHandler, CategoryReadHandlerTrait},
237
            test::fake_create_category_request,
238
        },
239
        entities::sea_orm_active_enums::CategoryType,
240
        StringExtension,
241
    };
242

243
    #[async_std::test]
244
    async fn handle_modify_category_testcase_successfully() {
245
        let beginning_test_timestamp = chrono::Utc::now();
246
        let test_space = setup_test_space().await;
247
        let database = test_space.postgres.get_database_connection().await;
248
        let number_of_tags = 2;
249
        let create_request = fake_create_category_request(number_of_tags);
250
        let origin_display_name = create_request.display_name.clone();
251

252
        let create_handler = CategoryCreateHandler {
253
            db: Arc::new(database.clone()),
254
        };
255
        let modify_handler = CategoryModifyHandler {
256
            db: Arc::new(database.clone()),
257
        };
258
        let read_handler = CategoryReadHandler {
259
            db: Arc::new(database),
260
        };
261
        let create_result = create_handler
262
            .handle_create_category_with_tags(create_request, Some("System".to_string()))
263
            .await
264
            .unwrap();
265

266
        assert!(!create_result.is_nil());
267
        let updated_name = format!("{} Updated", origin_display_name);
268
        let request = ModifyCategoryRequest {
269
            id: create_result,
270
            display_name: updated_name.clone(),
271
            category_type: CategoryType::Blog,
272
            parent_id: None,
273
            row_version: 1,
274
            tag_names: None,
275
            translations: None,
276
        };
277

278
        let result = modify_handler
279
            .handle_modify_category(request, Some("System".to_string()))
280
            .await
281
            .unwrap();
282
        assert!(!result.is_nil());
283
        let category_in_db = read_handler.handle_get_all_categories().await.unwrap();
284
        let first = &category_in_db.first().unwrap();
285

286
        assert_eq!(result, first.id);
287
        assert!(first.created_by == "System");
288
        assert!(first.created_at >= beginning_test_timestamp);
289
        assert!(first.row_version == 2);
290
        assert!(first.display_name == updated_name);
291
        assert!(first.slug == updated_name.to_slug());
292
        assert_eq!(first.tags.len(), 0);
293
    }
294

295
    #[async_std::test]
296
    async fn handle_modify_category_testcase_failed_due_to_rowversion() {
297
        let test_space = setup_test_space().await;
298
        let conn = test_space.postgres.get_database_connection().await;
299
        let create_request = fake_create_category_request(5);
300
        let origin_display_name = create_request.display_name.clone();
301

302
        let create_handler = CategoryCreateHandler {
303
            db: Arc::new(conn.clone()),
304
        };
305
        let modify_handler = CategoryModifyHandler {
306
            db: Arc::new(conn.clone()),
307
        };
308

309
        let create_result = create_handler
310
            .handle_create_category_with_tags(create_request, Some("System".to_string()))
311
            .await
312
            .unwrap();
313
        assert!(!create_result.is_nil());
314

315
        let updated_name = format!("{} Updated", origin_display_name);
316
        let wrong_row_version = 0;
317
        let request = ModifyCategoryRequest {
318
            id: create_result,
319
            display_name: updated_name.clone(),
320
            category_type: CategoryType::Blog,
321
            parent_id: None,
322
            row_version: wrong_row_version,
323
            tag_names: None,
324
            translations: None,
325
        };
326

327
        let result = modify_handler
328
            .handle_modify_category(request, Some("System".to_string()))
329
            .await;
330
        assert!(result.is_err());
331
    }
332
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc