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

doitsu2014 / my-cms / 14186917396

01 Apr 2025 04:03AM UTC coverage: 59.597%. First build
14186917396

Pull #19

github

web-flow
Merge 4fa5451ff into adbf44952
Pull Request #19: Feature/support multi language

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

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

53.72
/application_core/src/commands/post/modify/modify_handler.rs
1
use sea_orm::{
2
    sea_query::Expr, DatabaseConnection, EntityTrait, QueryFilter, Set, TransactionTrait,
3
};
4
use seaography::itertools::Itertools;
5
use std::sync::Arc;
6
use tracing::instrument;
7
use uuid::Uuid;
8

9
use crate::{
10
    commands::{
11
        post::read::read_handler::{PostReadHandler, PostReadHandlerTrait, PostReadResponse},
12
        tag::create::create_handler::{TagCreateHandler, TagCreateHandlerTrait},
13
    },
14
    common::{app_error::AppError, datetime_generator::generate_vietnam_now},
15
    entities::{
16
        post_tags,
17
        posts::{self, Column},
18
        post_translations,
19
    },
20
};
21

22
use super::modify_request::ModifyPostRequest;
23

24
pub trait PostModifyHandlerTrait {
25
    fn handle_modify_post(
26
        &self,
27
        body: ModifyPostRequest,
28
        actor_email: Option<String>,
29
    ) -> impl std::future::Future<Output = Result<Uuid, AppError>>;
30
}
31

32
#[derive(Debug)]
33
pub struct PostModifyHandler {
34
    pub db: Arc<DatabaseConnection>,
35
}
36

37
impl PostModifyHandlerTrait for PostModifyHandler {
38
    #[instrument]
39
    async fn handle_modify_post(
40
        &self,
41
        body: ModifyPostRequest,
42
        actor_email: Option<String>,
43
    ) -> Result<Uuid, AppError> {
44
        let post_read_handler = PostReadHandler {
45
            db: self.db.clone(),
46
        };
47

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

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

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

72
                    // 2.1. Get existing category
73
                    let db_post: PostReadResponse =
2✔
74
                        post_read_handler.handle_get_post(modified_id).await?;
2✔
75

76
                    // 3. Update Category and Tags
77
                    // 3.1. Delete Tags
78
                    let lower_case_tags: Vec<String> = processing_tags
2✔
79
                        .clone()
2✔
80
                        .into_iter()
2✔
81
                        .map(|tag| tag.to_lowercase())
2✔
82
                        .collect();
2✔
83
                    let tags_to_delete: Vec<Uuid> = db_post
2✔
84
                        .tags
2✔
85
                        .iter()
2✔
86
                        .filter(|t| !lower_case_tags.contains(&t.name.to_lowercase()))
10✔
87
                        .map(|t| t.id)
10✔
88
                        .collect();
2✔
89
                    if !tags_to_delete.is_empty() {
2✔
90
                        post_tags::Entity::delete_many()
2✔
91
                            .filter(Expr::col(post_tags::Column::PostId).eq(modified_id))
2✔
92
                            .filter(Expr::col(post_tags::Column::TagId).is_in(tags_to_delete))
2✔
93
                            .exec(tx)
2✔
94
                            .await
2✔
95
                            .map_err(|err| err.into())?;
2✔
96
                    }
×
97

98
                    // 3.2. Insert post Tags
99
                    let binded_tag_ids = db_post
2✔
100
                        .tags
2✔
101
                        .iter()
2✔
102
                        .map(|tag| tag.id.to_owned())
10✔
103
                        .collect_vec();
2✔
104
                    let insert_tag_ids = create_tags_response
2✔
105
                        .existing_tag_ids
2✔
106
                        .into_iter()
2✔
107
                        .chain(create_tags_response.new_tag_ids)
2✔
108
                        .filter(|tag_id| !binded_tag_ids.contains(tag_id))
2✔
109
                        .collect::<Vec<Uuid>>();
2✔
110

2✔
111
                    if !insert_tag_ids.is_empty() {
2✔
112
                        let post_tags_to_insert = insert_tag_ids
×
113
                            .iter()
×
114
                            .map(|tag_id| post_tags::ActiveModel {
×
115
                                post_id: Set(body.id),
×
116
                                tag_id: Set(tag_id.to_owned()),
×
117
                            })
×
118
                            .collect::<Vec<post_tags::ActiveModel>>();
×
119

×
120
                        post_tags::Entity::insert_many(post_tags_to_insert)
×
121
                            .exec(tx)
×
122
                            .await
×
123
                            .map_err(|err| err.into())?;
×
124
                    }
2✔
125

126
                    // 3.3. Modify Category information
127
                    let modified_result = posts::Entity::update_many()
2✔
128
                        .set(model)
2✔
129
                        .filter(Expr::col(Column::Id).eq(modified_id))
2✔
130
                        .filter(Expr::col(Column::RowVersion).eq(current_row_version))
2✔
131
                        .exec(tx)
2✔
132
                        .await
2✔
133
                        .map_err(|err| err.into())?;
2✔
134
                    match modified_result.rows_affected == 0 {
2✔
135
                        true => {
136
                            return Err(AppError::Logical("Row version is not matched".to_string()))
1✔
137
                        }
138
                        false => (),
1✔
139
                    }
140

141
                    // 4. Update Translations
142
                    if let Some(request_translations) = body.translations {
1✔
NEW
143
                        let incoming_translation_ids: Vec<Uuid> = request_translations
×
NEW
144
                            .iter()
×
NEW
145
                            .filter(|t| t.id.is_some())
×
NEW
146
                            .map(|t| t.id.unwrap())
×
NEW
147
                            .collect();
×
148

NEW
149
                        let existing_translations = post_translations::Entity::find()
×
NEW
150
                            .filter(Expr::col(post_translations::Column::PostId).eq(modified_id))
×
NEW
151
                            .all(tx)
×
NEW
152
                            .await
×
NEW
153
                            .map_err(|err| err.into())?;
×
154

NEW
155
                        let translations_to_delete: Vec<Uuid> = existing_translations
×
NEW
156
                            .iter()
×
NEW
157
                            .filter(|existing_translation| {
×
NEW
158
                                !incoming_translation_ids.contains(&existing_translation.id)
×
NEW
159
                            })
×
NEW
160
                            .map(|existing_translation| existing_translation.id)
×
NEW
161
                            .collect();
×
NEW
162

×
NEW
163
                        if !translations_to_delete.is_empty() {
×
NEW
164
                            post_translations::Entity::delete_many()
×
NEW
165
                                .filter(
×
NEW
166
                                    Expr::col(post_translations::Column::Id)
×
NEW
167
                                        .is_in(translations_to_delete),
×
NEW
168
                                )
×
NEW
169
                                .exec(tx)
×
NEW
170
                                .await
×
NEW
171
                                .map_err(|err| err.into())?;
×
NEW
172
                        }
×
173

NEW
174
                        for request_translation in request_translations {
×
NEW
175
                            if request_translation.id.is_some() {
×
NEW
176
                                let mut existing_translation =
×
NEW
177
                                    request_translation.into_active_model();
×
NEW
178
                                existing_translation.post_id = Set(modified_id);
×
NEW
179
                                post_translations::Entity::update(existing_translation)
×
NEW
180
                                    .exec(tx)
×
NEW
181
                                    .await
×
NEW
182
                                    .map_err(|err| err.into())?;
×
183
                            } else {
NEW
184
                                let mut new_translation = request_translation.into_active_model();
×
NEW
185
                                new_translation.post_id = Set(modified_id);
×
NEW
186
                                post_translations::Entity::insert(new_translation)
×
NEW
187
                                    .exec(tx)
×
NEW
188
                                    .await
×
NEW
189
                                    .map_err(|err| err.into())?;
×
190
                            }
191
                        }
192
                    }
1✔
193

194
                    Ok(modified_id)
1✔
195
                })
2✔
196
            })
2✔
197
            .await
198
            .map_err(|e| e.into());
1✔
199

200
        result
201
    }
202
}
203

204
#[cfg(test)]
205
mod tests {
206
    use std::sync::Arc;
207
    use test_helpers::{setup_test_space, ContainerAsyncPostgresEx};
208

209
    use crate::{
210
        commands::{
211
            category::{
212
                create::create_handler::{CategoryCreateHandler, CategoryCreateHandlerTrait},
213
                test::fake_create_category_request,
214
            },
215
            post::{
216
                create::create_handler::{PostCreateHandler, PostCreateHandlerTrait},
217
                modify::{
218
                    modify_handler::{PostModifyHandler, PostModifyHandlerTrait},
219
                    modify_request::ModifyPostRequest,
220
                },
221
                read::read_handler::{PostReadHandler, PostReadHandlerTrait},
222
                test::fake_create_post_request,
223
            },
224
        },
225
        StringExtension,
226
    };
227

228
    #[async_std::test]
229
    async fn handle_modify_post_testcase_successfully() {
230
        let beginning_test_timestamp = chrono::Utc::now();
231
        let test_space = setup_test_space().await;
232
        let database = test_space.postgres.get_database_connection().await;
233

234
        let arc_conn = Arc::new(database.clone());
235

236
        let category_create_handler = CategoryCreateHandler {
237
            db: arc_conn.clone(),
238
        };
239
        let post_create_handler = PostCreateHandler {
240
            db: arc_conn.clone(),
241
        };
242
        let post_modify_handler = PostModifyHandler {
243
            db: arc_conn.clone(),
244
        };
245
        let post_read_handler = PostReadHandler {
246
            db: arc_conn.clone(),
247
        };
248

249
        let create_category_request = fake_create_category_request(3);
250
        let created_category_id = category_create_handler
251
            .handle_create_category_with_tags(create_category_request, None)
252
            .await
253
            .unwrap();
254
        let create_post_request = fake_create_post_request(created_category_id, 5);
255
        let result = post_create_handler
256
            .handle_create_post(create_post_request.clone(), None)
257
            .await
258
            .unwrap();
259

260
        let updated_title = format!("{} Updated", create_post_request.title);
261
        let updated_content = format!("{} Updated", create_post_request.content);
262
        let request = ModifyPostRequest {
263
            id: result,
264
            title: updated_title.to_owned(),
265
            content: updated_content.to_owned(),
266
            preview_content: None,
267
            published: true,
268
            category_id: created_category_id,
269
            row_version: 1,
270
            tag_names: None,
271
            thumbnail_paths: vec![],
272
            translations: None,
273
        };
274
        let result = post_modify_handler
275
            .handle_modify_post(request.clone(), Some("Last Modifier".to_string()))
276
            .await
277
            .unwrap();
278
        let posts_in_db = post_read_handler.handle_get_all_posts().await.unwrap();
279
        let first = posts_in_db.first().unwrap();
280

281
        assert_eq!(result, first.id);
282
        assert!(first.created_at >= beginning_test_timestamp);
283
        assert!(first.row_version == 2);
284
        assert!(first.title == request.title);
285
        assert!(first.slug == request.title.to_slug());
286
        assert!(first.content == request.content);
287
        assert!(first.created_by == "System");
288
        assert!(first.last_modified_by == Some("Last Modifier".to_string()));
289
    }
290

291
    #[async_std::test]
292
    async fn handle_modify_post_testcase_failed() {
293
        let test_space = setup_test_space().await;
294
        let conn = test_space.postgres.get_database_connection().await;
295

296
        let arc_conn = Arc::new(conn.clone());
297
        let category_create_handler = CategoryCreateHandler {
298
            db: arc_conn.clone(),
299
        };
300
        let post_create_handler = PostCreateHandler {
301
            db: arc_conn.clone(),
302
        };
303
        let post_modify_handler = PostModifyHandler {
304
            db: arc_conn.clone(),
305
        };
306

307
        let create_category_request = fake_create_category_request(3);
308
        let created_category_id = category_create_handler
309
            .handle_create_category_with_tags(create_category_request, None)
310
            .await
311
            .unwrap();
312
        let create_post_request = fake_create_post_request(created_category_id, 5);
313
        let result = post_create_handler
314
            .handle_create_post(create_post_request.clone(), None)
315
            .await
316
            .unwrap();
317
        let updated_title = format!("{} Updated", create_post_request.title);
318
        let request = ModifyPostRequest {
319
            id: result,
320
            title: format!("{} Updated", updated_title),
321
            content: format!("{} Updated", create_post_request.content),
322
            preview_content: None,
323
            published: true,
324
            category_id: created_category_id,
325
            row_version: 0,
326
            tag_names: None,
327
            thumbnail_paths: vec![],
328
            translations: None,
329
        };
330

331
        let result = post_modify_handler
332
            .handle_modify_post(request, Some("System".to_string()))
333
            .await;
334
        assert!(result.is_err());
335
    }
336
}
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

© 2026 Coveralls, Inc