]> Untitled Git - lemmy.git/blob - crates/db_views/src/comment_view.rs
d469b0c5fc7b204afc3991b7d231d1529783bc4e
[lemmy.git] / crates / db_views / src / comment_view.rs
1 use crate::structs::CommentView;
2 use diesel::{dsl::*, result::Error, *};
3 use diesel_ltree::{nlevel, subpath, Ltree, LtreeExtensions};
4 use lemmy_db_schema::{
5   aggregates::structs::CommentAggregates,
6   newtypes::{CommentId, CommunityId, DbUrl, LocalUserId, PersonId, PostId},
7   schema::{
8     comment, comment_aggregates, comment_like, comment_saved, community, community_block,
9     community_follower, community_person_ban, language, local_user_language, person, person_block,
10     post,
11   },
12   source::{
13     comment::{Comment, CommentSaved},
14     community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe},
15     local_user::LocalUser,
16     person::{Person, PersonSafe},
17     person_block::PersonBlock,
18     post::Post,
19   },
20   traits::{ToSafe, ViewToVec},
21   utils::{functions::hot_rank, fuzzy_search, limit_and_offset_unlimited},
22   CommentSortType, ListingType,
23 };
24 use typed_builder::TypedBuilder;
25
26 type CommentViewTuple = (
27   Comment,
28   PersonSafe,
29   Post,
30   CommunitySafe,
31   CommentAggregates,
32   Option<CommunityPersonBan>,
33   Option<CommunityFollower>,
34   Option<CommentSaved>,
35   Option<PersonBlock>,
36   Option<i16>,
37 );
38
39 impl CommentView {
40   pub fn read(
41     conn: &PgConnection,
42     comment_id: CommentId,
43     my_person_id: Option<PersonId>,
44   ) -> Result<Self, Error> {
45     // The left join below will return None in this case
46     let person_id_join = my_person_id.unwrap_or(PersonId(-1));
47
48     let (
49       comment,
50       creator,
51       post,
52       community,
53       counts,
54       creator_banned_from_community,
55       follower,
56       saved,
57       creator_blocked,
58       comment_like,
59     ) = comment::table
60       .find(comment_id)
61       .inner_join(person::table)
62       .inner_join(post::table)
63       .inner_join(community::table.on(post::community_id.eq(community::id)))
64       .inner_join(comment_aggregates::table)
65       .left_join(
66         community_person_ban::table.on(
67           community::id
68             .eq(community_person_ban::community_id)
69             .and(community_person_ban::person_id.eq(comment::creator_id))
70             .and(
71               community_person_ban::expires
72                 .is_null()
73                 .or(community_person_ban::expires.gt(now)),
74             ),
75         ),
76       )
77       .left_join(
78         community_follower::table.on(
79           post::community_id
80             .eq(community_follower::community_id)
81             .and(community_follower::person_id.eq(person_id_join)),
82         ),
83       )
84       .left_join(
85         comment_saved::table.on(
86           comment::id
87             .eq(comment_saved::comment_id)
88             .and(comment_saved::person_id.eq(person_id_join)),
89         ),
90       )
91       .left_join(
92         person_block::table.on(
93           comment::creator_id
94             .eq(person_block::target_id)
95             .and(person_block::person_id.eq(person_id_join)),
96         ),
97       )
98       .left_join(
99         comment_like::table.on(
100           comment::id
101             .eq(comment_like::comment_id)
102             .and(comment_like::person_id.eq(person_id_join)),
103         ),
104       )
105       .select((
106         comment::all_columns,
107         Person::safe_columns_tuple(),
108         post::all_columns,
109         Community::safe_columns_tuple(),
110         comment_aggregates::all_columns,
111         community_person_ban::all_columns.nullable(),
112         community_follower::all_columns.nullable(),
113         comment_saved::all_columns.nullable(),
114         person_block::all_columns.nullable(),
115         comment_like::score.nullable(),
116       ))
117       .first::<CommentViewTuple>(conn)?;
118
119     // If a person is given, then my_vote, if None, should be 0, not null
120     // Necessary to differentiate between other person's votes
121     let my_vote = if my_person_id.is_some() && comment_like.is_none() {
122       Some(0)
123     } else {
124       comment_like
125     };
126
127     Ok(CommentView {
128       comment,
129       post,
130       creator,
131       community,
132       counts,
133       creator_banned_from_community: creator_banned_from_community.is_some(),
134       subscribed: CommunityFollower::to_subscribed_type(&follower),
135       saved: saved.is_some(),
136       creator_blocked: creator_blocked.is_some(),
137       my_vote,
138     })
139   }
140 }
141
142 #[derive(TypedBuilder)]
143 #[builder(field_defaults(default))]
144 pub struct CommentQuery<'a> {
145   #[builder(!default)]
146   conn: &'a PgConnection,
147   listing_type: Option<ListingType>,
148   sort: Option<CommentSortType>,
149   community_id: Option<CommunityId>,
150   community_actor_id: Option<DbUrl>,
151   post_id: Option<PostId>,
152   parent_path: Option<Ltree>,
153   creator_id: Option<PersonId>,
154   local_user: Option<&'a LocalUser>,
155   search_term: Option<String>,
156   saved_only: Option<bool>,
157   page: Option<i64>,
158   limit: Option<i64>,
159   max_depth: Option<i32>,
160 }
161
162 impl<'a> CommentQuery<'a> {
163   pub fn list(self) -> Result<Vec<CommentView>, Error> {
164     use diesel::dsl::*;
165
166     // The left join below will return None in this case
167     let person_id_join = self.local_user.map(|l| l.person_id).unwrap_or(PersonId(-1));
168     let local_user_id_join = self.local_user.map(|l| l.id).unwrap_or(LocalUserId(-1));
169
170     let mut query = comment::table
171       .inner_join(person::table)
172       .inner_join(post::table)
173       .inner_join(community::table.on(post::community_id.eq(community::id)))
174       .inner_join(comment_aggregates::table)
175       .left_join(
176         community_person_ban::table.on(
177           community::id
178             .eq(community_person_ban::community_id)
179             .and(community_person_ban::person_id.eq(comment::creator_id))
180             .and(
181               community_person_ban::expires
182                 .is_null()
183                 .or(community_person_ban::expires.gt(now)),
184             ),
185         ),
186       )
187       .left_join(
188         community_follower::table.on(
189           post::community_id
190             .eq(community_follower::community_id)
191             .and(community_follower::person_id.eq(person_id_join)),
192         ),
193       )
194       .left_join(
195         comment_saved::table.on(
196           comment::id
197             .eq(comment_saved::comment_id)
198             .and(comment_saved::person_id.eq(person_id_join)),
199         ),
200       )
201       .left_join(
202         person_block::table.on(
203           comment::creator_id
204             .eq(person_block::target_id)
205             .and(person_block::person_id.eq(person_id_join)),
206         ),
207       )
208       .left_join(
209         community_block::table.on(
210           community::id
211             .eq(community_block::community_id)
212             .and(community_block::person_id.eq(person_id_join)),
213         ),
214       )
215       .left_join(
216         comment_like::table.on(
217           comment::id
218             .eq(comment_like::comment_id)
219             .and(comment_like::person_id.eq(person_id_join)),
220         ),
221       )
222       .inner_join(language::table)
223       .left_join(
224         local_user_language::table.on(
225           post::language_id
226             .eq(local_user_language::language_id)
227             .and(local_user_language::local_user_id.eq(local_user_id_join)),
228         ),
229       )
230       .select((
231         comment::all_columns,
232         Person::safe_columns_tuple(),
233         post::all_columns,
234         Community::safe_columns_tuple(),
235         comment_aggregates::all_columns,
236         community_person_ban::all_columns.nullable(),
237         community_follower::all_columns.nullable(),
238         comment_saved::all_columns.nullable(),
239         person_block::all_columns.nullable(),
240         comment_like::score.nullable(),
241       ))
242       .into_boxed();
243
244     if let Some(creator_id) = self.creator_id {
245       query = query.filter(comment::creator_id.eq(creator_id));
246     };
247
248     if let Some(post_id) = self.post_id {
249       query = query.filter(comment::post_id.eq(post_id));
250     };
251
252     if let Some(parent_path) = self.parent_path.as_ref() {
253       query = query.filter(comment::path.contained_by(parent_path));
254     };
255
256     if let Some(search_term) = self.search_term {
257       query = query.filter(comment::content.ilike(fuzzy_search(&search_term)));
258     };
259
260     if let Some(listing_type) = self.listing_type {
261       match listing_type {
262         ListingType::Subscribed => {
263           query = query.filter(community_follower::person_id.is_not_null())
264         } // TODO could be this: and(community_follower::person_id.eq(person_id_join)),
265         ListingType::Local => {
266           query = query.filter(community::local.eq(true)).filter(
267             community::hidden
268               .eq(false)
269               .or(community_follower::person_id.eq(person_id_join)),
270           )
271         }
272         ListingType::All => {
273           query = query.filter(
274             community::hidden
275               .eq(false)
276               .or(community_follower::person_id.eq(person_id_join)),
277           )
278         }
279       }
280     };
281
282     if let Some(community_id) = self.community_id {
283       query = query.filter(post::community_id.eq(community_id));
284     }
285
286     if let Some(community_actor_id) = self.community_actor_id {
287       query = query.filter(community::actor_id.eq(community_actor_id))
288     }
289
290     if self.saved_only.unwrap_or(false) {
291       query = query.filter(comment_saved::id.is_not_null());
292     }
293
294     if !self.local_user.map(|l| l.show_bot_accounts).unwrap_or(true) {
295       query = query.filter(person::bot_account.eq(false));
296     };
297
298     if self.local_user.is_some() {
299       // Filter out the rows with missing languages
300       query = query.filter(local_user_language::id.is_not_null());
301
302       // Don't show blocked communities or persons
303       query = query.filter(community_block::person_id.is_null());
304       query = query.filter(person_block::person_id.is_null());
305     }
306
307     // A Max depth given means its a tree fetch
308     let (limit, offset) = if let Some(max_depth) = self.max_depth {
309       let depth_limit = if let Some(parent_path) = self.parent_path.as_ref() {
310         parent_path.0.split('.').count() as i32 + max_depth
311         // Add one because of root "0"
312       } else {
313         max_depth + 1
314       };
315
316       query = query.filter(nlevel(comment::path).le(depth_limit));
317
318       // Always order by the parent path first
319       query = query.order_by(subpath(comment::path, 0, -1));
320
321       // TODO limit question. Limiting does not work for comment threads ATM, only max_depth
322       // For now, don't do any limiting for tree fetches
323       // https://stackoverflow.com/questions/72983614/postgres-ltree-how-to-limit-the-max-number-of-children-at-any-given-level
324
325       // Don't use the regular error-checking one, many more comments must ofter be fetched.
326       // This does not work for comment trees, and the limit should be manually set to a high number
327       //
328       // If a max depth is given, then you know its a tree fetch, and limits should be ignored
329       (i64::MAX, 0)
330     } else {
331       limit_and_offset_unlimited(self.page, self.limit)
332     };
333
334     query = match self.sort.unwrap_or(CommentSortType::Hot) {
335       CommentSortType::Hot => query
336         .then_order_by(hot_rank(comment_aggregates::score, comment_aggregates::published).desc())
337         .then_order_by(comment_aggregates::published.desc()),
338       CommentSortType::New => query.then_order_by(comment::published.desc()),
339       CommentSortType::Old => query.then_order_by(comment::published.asc()),
340       CommentSortType::Top => query.order_by(comment_aggregates::score.desc()),
341     };
342
343     // Note: deleted and removed comments are done on the front side
344     let res = query
345       .limit(limit)
346       .offset(offset)
347       .load::<CommentViewTuple>(self.conn)?;
348
349     Ok(CommentView::from_tuple_to_vec(res))
350   }
351 }
352
353 impl ViewToVec for CommentView {
354   type DbTuple = CommentViewTuple;
355   fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
356     items
357       .into_iter()
358       .map(|a| Self {
359         comment: a.0,
360         creator: a.1,
361         post: a.2,
362         community: a.3,
363         counts: a.4,
364         creator_banned_from_community: a.5.is_some(),
365         subscribed: CommunityFollower::to_subscribed_type(&a.6),
366         saved: a.7.is_some(),
367         creator_blocked: a.8.is_some(),
368         my_vote: a.9,
369       })
370       .collect::<Vec<Self>>()
371   }
372 }
373
374 #[cfg(test)]
375 mod tests {
376   use crate::comment_view::*;
377   use lemmy_db_schema::{
378     aggregates::structs::CommentAggregates,
379     newtypes::LanguageId,
380     source::{
381       comment::*, community::*, local_user::LocalUserForm, person::*,
382       person_block::PersonBlockForm, post::*,
383     },
384     traits::{Blockable, Crud, Likeable},
385     utils::establish_unpooled_connection,
386     SubscribedType,
387   };
388   use serial_test::serial;
389
390   struct Data {
391     inserted_comment_0: Comment,
392     inserted_comment_1: Comment,
393     inserted_comment_2: Comment,
394     inserted_post: Post,
395     inserted_person: Person,
396     inserted_local_user: LocalUser,
397     inserted_person_2: Person,
398     inserted_community: Community,
399   }
400
401   fn init_data(conn: &PgConnection) -> Data {
402     let new_person = PersonForm {
403       name: "timmy".into(),
404       public_key: Some("pubkey".to_string()),
405       ..PersonForm::default()
406     };
407     let inserted_person = Person::create(&conn, &new_person).unwrap();
408     let local_user_form = LocalUserForm {
409       person_id: Some(inserted_person.id),
410       password_encrypted: Some("".to_string()),
411       ..Default::default()
412     };
413     let inserted_local_user = LocalUser::create(&conn, &local_user_form).unwrap();
414
415     let new_person_2 = PersonForm {
416       name: "sara".into(),
417       public_key: Some("pubkey".to_string()),
418       ..PersonForm::default()
419     };
420     let inserted_person_2 = Person::create(&conn, &new_person_2).unwrap();
421
422     let new_community = CommunityForm {
423       name: "test community 5".to_string(),
424       title: "nada".to_owned(),
425       public_key: Some("pubkey".to_string()),
426       ..CommunityForm::default()
427     };
428
429     let inserted_community = Community::create(&conn, &new_community).unwrap();
430
431     let new_post = PostForm {
432       name: "A test post 2".into(),
433       creator_id: inserted_person.id,
434       community_id: inserted_community.id,
435       ..PostForm::default()
436     };
437
438     let inserted_post = Post::create(&conn, &new_post).unwrap();
439
440     // Create a comment tree with this hierarchy
441     //       0
442     //     \     \
443     //    1      2
444     //    \
445     //  3  4
446     //     \
447     //     5
448     let comment_form_0 = CommentForm {
449       content: "Comment 0".into(),
450       creator_id: inserted_person.id,
451       post_id: inserted_post.id,
452       ..CommentForm::default()
453     };
454
455     let inserted_comment_0 = Comment::create(&conn, &comment_form_0, None).unwrap();
456
457     let comment_form_1 = CommentForm {
458       content: "Comment 1, A test blocked comment".into(),
459       creator_id: inserted_person_2.id,
460       post_id: inserted_post.id,
461       ..CommentForm::default()
462     };
463
464     let inserted_comment_1 =
465       Comment::create(&conn, &comment_form_1, Some(&inserted_comment_0.path)).unwrap();
466
467     let comment_form_2 = CommentForm {
468       content: "Comment 2".into(),
469       creator_id: inserted_person.id,
470       post_id: inserted_post.id,
471       ..CommentForm::default()
472     };
473
474     let inserted_comment_2 =
475       Comment::create(&conn, &comment_form_2, Some(&inserted_comment_0.path)).unwrap();
476
477     let comment_form_3 = CommentForm {
478       content: "Comment 3".into(),
479       creator_id: inserted_person.id,
480       post_id: inserted_post.id,
481       ..CommentForm::default()
482     };
483
484     let _inserted_comment_3 =
485       Comment::create(&conn, &comment_form_3, Some(&inserted_comment_1.path)).unwrap();
486
487     let comment_form_4 = CommentForm {
488       content: "Comment 4".into(),
489       creator_id: inserted_person.id,
490       post_id: inserted_post.id,
491       ..CommentForm::default()
492     };
493
494     let inserted_comment_4 =
495       Comment::create(&conn, &comment_form_4, Some(&inserted_comment_1.path)).unwrap();
496
497     let comment_form_5 = CommentForm {
498       content: "Comment 5".into(),
499       creator_id: inserted_person.id,
500       post_id: inserted_post.id,
501       ..CommentForm::default()
502     };
503
504     let _inserted_comment_5 =
505       Comment::create(&conn, &comment_form_5, Some(&inserted_comment_4.path)).unwrap();
506
507     let timmy_blocks_sara_form = PersonBlockForm {
508       person_id: inserted_person.id,
509       target_id: inserted_person_2.id,
510     };
511
512     let inserted_block = PersonBlock::block(&conn, &timmy_blocks_sara_form).unwrap();
513
514     let expected_block = PersonBlock {
515       id: inserted_block.id,
516       person_id: inserted_person.id,
517       target_id: inserted_person_2.id,
518       published: inserted_block.published,
519     };
520     assert_eq!(expected_block, inserted_block);
521
522     Data {
523       inserted_comment_0,
524       inserted_comment_1,
525       inserted_comment_2,
526       inserted_post,
527       inserted_person,
528       inserted_local_user,
529       inserted_person_2,
530       inserted_community,
531     }
532   }
533
534   #[test]
535   #[serial]
536   fn test_crud() {
537     let conn = establish_unpooled_connection();
538     let data = init_data(&conn);
539
540     let comment_like_form = CommentLikeForm {
541       comment_id: data.inserted_comment_0.id,
542       post_id: data.inserted_post.id,
543       person_id: data.inserted_person.id,
544       score: 1,
545     };
546
547     let _inserted_comment_like = CommentLike::like(&conn, &comment_like_form).unwrap();
548
549     let expected_comment_view_no_person = expected_comment_view(&data, &conn);
550
551     let mut expected_comment_view_with_person = expected_comment_view_no_person.to_owned();
552     expected_comment_view_with_person.my_vote = Some(1);
553
554     let read_comment_views_no_person = CommentQuery::builder()
555       .conn(&conn)
556       .post_id(Some(data.inserted_post.id))
557       .build()
558       .list()
559       .unwrap();
560
561     assert_eq!(
562       expected_comment_view_no_person,
563       read_comment_views_no_person[0]
564     );
565
566     let read_comment_views_with_person = CommentQuery::builder()
567       .conn(&conn)
568       .post_id(Some(data.inserted_post.id))
569       .local_user(Some(&data.inserted_local_user))
570       .build()
571       .list()
572       .unwrap();
573
574     assert_eq!(
575       expected_comment_view_with_person,
576       read_comment_views_with_person[0]
577     );
578
579     // Make sure its 1, not showing the blocked comment
580     assert_eq!(5, read_comment_views_with_person.len());
581
582     let read_comment_from_blocked_person = CommentView::read(
583       &conn,
584       data.inserted_comment_1.id,
585       Some(data.inserted_person.id),
586     )
587     .unwrap();
588
589     // Make sure block set the creator blocked
590     assert!(read_comment_from_blocked_person.creator_blocked);
591
592     cleanup(data, &conn);
593   }
594
595   #[test]
596   #[serial]
597   fn test_comment_tree() {
598     let conn = establish_unpooled_connection();
599     let data = init_data(&conn);
600
601     let top_path = data.inserted_comment_0.path.clone();
602     let read_comment_views_top_path = CommentQuery::builder()
603       .conn(&conn)
604       .post_id(Some(data.inserted_post.id))
605       .parent_path(Some(top_path))
606       .build()
607       .list()
608       .unwrap();
609
610     let child_path = data.inserted_comment_1.path.clone();
611     let read_comment_views_child_path = CommentQuery::builder()
612       .conn(&conn)
613       .post_id(Some(data.inserted_post.id))
614       .parent_path(Some(child_path))
615       .build()
616       .list()
617       .unwrap();
618
619     // Make sure the comment parent-limited fetch is correct
620     assert_eq!(6, read_comment_views_top_path.len());
621     assert_eq!(4, read_comment_views_child_path.len());
622
623     // Make sure it contains the parent, but not the comment from the other tree
624     let child_comments = read_comment_views_child_path
625       .into_iter()
626       .map(|c| c.comment)
627       .collect::<Vec<Comment>>();
628     assert!(child_comments.contains(&data.inserted_comment_1));
629     assert!(!child_comments.contains(&data.inserted_comment_2));
630
631     let read_comment_views_top_max_depth = CommentQuery::builder()
632       .conn(&conn)
633       .post_id(Some(data.inserted_post.id))
634       .max_depth(Some(1))
635       .build()
636       .list()
637       .unwrap();
638
639     // Make sure a depth limited one only has the top comment
640     assert_eq!(
641       expected_comment_view(&data, &conn),
642       read_comment_views_top_max_depth[0]
643     );
644     assert_eq!(1, read_comment_views_top_max_depth.len());
645
646     let child_path = data.inserted_comment_1.path.clone();
647     let read_comment_views_parent_max_depth = CommentQuery::builder()
648       .conn(&conn)
649       .post_id(Some(data.inserted_post.id))
650       .parent_path(Some(child_path))
651       .max_depth(Some(1))
652       .sort(Some(CommentSortType::New))
653       .build()
654       .list()
655       .unwrap();
656
657     // Make sure a depth limited one, and given child comment 1, has 3
658     assert!(read_comment_views_parent_max_depth[2]
659       .comment
660       .content
661       .eq("Comment 3"));
662     assert_eq!(3, read_comment_views_parent_max_depth.len());
663
664     cleanup(data, &conn);
665   }
666
667   fn cleanup(data: Data, conn: &PgConnection) {
668     CommentLike::remove(&conn, data.inserted_person.id, data.inserted_comment_0.id).unwrap();
669     Comment::delete(&conn, data.inserted_comment_0.id).unwrap();
670     Comment::delete(&conn, data.inserted_comment_1.id).unwrap();
671     Post::delete(&conn, data.inserted_post.id).unwrap();
672     Community::delete(&conn, data.inserted_community.id).unwrap();
673     Person::delete(&conn, data.inserted_person.id).unwrap();
674     Person::delete(&conn, data.inserted_person_2.id).unwrap();
675   }
676
677   fn expected_comment_view(data: &Data, conn: &PgConnection) -> CommentView {
678     let agg = CommentAggregates::read(&conn, data.inserted_comment_0.id).unwrap();
679     CommentView {
680       creator_banned_from_community: false,
681       my_vote: None,
682       subscribed: SubscribedType::NotSubscribed,
683       saved: false,
684       creator_blocked: false,
685       comment: Comment {
686         id: data.inserted_comment_0.id,
687         content: "Comment 0".into(),
688         creator_id: data.inserted_person.id,
689         post_id: data.inserted_post.id,
690         removed: false,
691         deleted: false,
692         published: data.inserted_comment_0.published,
693         ap_id: data.inserted_comment_0.ap_id.clone(),
694         updated: None,
695         local: true,
696         distinguished: false,
697         path: data.inserted_comment_0.to_owned().path,
698         language_id: LanguageId(0),
699       },
700       creator: PersonSafe {
701         id: data.inserted_person.id,
702         name: "timmy".into(),
703         display_name: None,
704         published: data.inserted_person.published,
705         avatar: None,
706         actor_id: data.inserted_person.actor_id.to_owned(),
707         local: true,
708         banned: false,
709         deleted: false,
710         admin: false,
711         bot_account: false,
712         bio: None,
713         banner: None,
714         updated: None,
715         inbox_url: data.inserted_person.inbox_url.to_owned(),
716         shared_inbox_url: None,
717         matrix_user_id: None,
718         ban_expires: None,
719       },
720       post: Post {
721         id: data.inserted_post.id,
722         name: data.inserted_post.name.to_owned(),
723         creator_id: data.inserted_person.id,
724         url: None,
725         body: None,
726         published: data.inserted_post.published,
727         updated: None,
728         community_id: data.inserted_community.id,
729         removed: false,
730         deleted: false,
731         locked: false,
732         stickied: false,
733         nsfw: false,
734         embed_title: None,
735         embed_description: None,
736         embed_video_url: None,
737         thumbnail_url: None,
738         ap_id: data.inserted_post.ap_id.to_owned(),
739         local: true,
740         language_id: Default::default(),
741       },
742       community: CommunitySafe {
743         id: data.inserted_community.id,
744         name: "test community 5".to_string(),
745         icon: None,
746         removed: false,
747         deleted: false,
748         nsfw: false,
749         actor_id: data.inserted_community.actor_id.to_owned(),
750         local: true,
751         title: "nada".to_owned(),
752         description: None,
753         updated: None,
754         banner: None,
755         hidden: false,
756         posting_restricted_to_mods: false,
757         published: data.inserted_community.published,
758       },
759       counts: CommentAggregates {
760         id: agg.id,
761         comment_id: data.inserted_comment_0.id,
762         score: 1,
763         upvotes: 1,
764         downvotes: 0,
765         published: agg.published,
766         child_count: 5,
767       },
768     }
769   }
770 }