Start working on profile combined

This commit is contained in:
Dessalines 2024-12-06 08:18:08 -05:00
parent 921d53227c
commit 724856d684
9 changed files with 432 additions and 0 deletions

View file

@ -185,6 +185,12 @@ pub struct DbUrl(pub(crate) Box<Url>);
/// The report combined id /// The report combined id
pub struct ReportCombinedId(i32); pub struct ReportCombinedId(i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The profile combined id
pub struct ProfileCombinedId(i32);
impl DbUrl { impl DbUrl {
pub fn inner(&self) -> &Url { pub fn inner(&self) -> &Url {
&self.0 &self.0

View file

@ -856,6 +856,15 @@ diesel::table! {
} }
} }
diesel::table! {
profile_combined (id) {
id -> Int4,
published -> Timestamptz,
post_id -> Nullable<Int4>,
comment_id -> Nullable<Int4>,
}
}
diesel::table! { diesel::table! {
received_activity (ap_id) { received_activity (ap_id) {
ap_id -> Text, ap_id -> Text,
@ -1043,6 +1052,8 @@ diesel::joinable!(post_aggregates -> person (creator_id));
diesel::joinable!(post_aggregates -> post (post_id)); diesel::joinable!(post_aggregates -> post (post_id));
diesel::joinable!(post_report -> post (post_id)); diesel::joinable!(post_report -> post (post_id));
diesel::joinable!(private_message_report -> private_message (private_message_id)); diesel::joinable!(private_message_report -> private_message (private_message_id));
diesel::joinable!(profile_combined -> comment (comment_id));
diesel::joinable!(profile_combined -> post (post_id));
diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> local_user (local_user_id));
diesel::joinable!(registration_application -> person (admin_id)); diesel::joinable!(registration_application -> person (admin_id));
diesel::joinable!(report_combined -> comment_report (comment_report_id)); diesel::joinable!(report_combined -> comment_report (comment_report_id));
@ -1113,6 +1124,7 @@ diesel::allow_tables_to_appear_in_same_query!(
post_report, post_report,
private_message, private_message,
private_message_report, private_message_report,
profile_combined,
received_activity, received_activity,
registration_application, registration_application,
remote_image, remote_image,

View file

@ -1 +1,2 @@
pub mod profile;
pub mod report; pub mod report;

View file

@ -0,0 +1,30 @@
use crate::newtypes::{CommentId, PostId, ProfileCombinedId};
#[cfg(feature = "full")]
use crate::schema::profile_combined;
use chrono::{DateTime, Utc};
#[cfg(feature = "full")]
use i_love_jesus::CursorKeysModule;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use ts_rs::TS;
#[skip_serializing_none]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(
feature = "full",
derive(Identifiable, Queryable, Selectable, TS, CursorKeysModule)
)]
#[cfg_attr(feature = "full", diesel(table_name = profile_combined))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
#[cfg_attr(feature = "full", cursor_keys_module(name = profile_combined_keys))]
/// A combined profile table.
pub struct ProfileCombined {
pub id: ProfileCombinedId,
pub published: DateTime<Utc>,
#[cfg_attr(feature = "full", ts(optional))]
pub post_id: Option<PostId>,
#[cfg_attr(feature = "full", ts(optional))]
pub comment_id: Option<CommentId>,
}

View file

@ -20,6 +20,8 @@ pub mod private_message_report_view;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod private_message_view; pub mod private_message_view;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod profile_combined_view;
#[cfg(feature = "full")]
pub mod registration_application_view; pub mod registration_application_view;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod report_combined_view; pub mod report_combined_view;

View file

@ -0,0 +1,304 @@
use crate::structs::{
CommentView,
LocalUserView,
PostView,
ProfileCombinedPaginationCursor,
ProfileCombinedView,
ProfileCombinedViewInternal,
};
use diesel::{
result::Error,
BoolExpressionMethods,
ExpressionMethods,
JoinOnDsl,
NullableExpressionMethods,
QueryDsl,
SelectableHelper,
};
use diesel_async::RunQueryDsl;
use i_love_jesus::PaginatedQueryBuilder;
use lemmy_db_schema::{
aliases::{self, creator_community_actions},
newtypes::CommunityId,
schema::{
comment,
comment_actions,
comment_aggregates,
community,
community_actions,
local_user,
person,
person_actions,
post,
post_actions,
post_aggregates,
profile_combined,
},
source::{
combined::profile::{profile_combined_keys as key, ProfileCombined},
community::CommunityFollower,
},
utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey},
};
use lemmy_utils::error::LemmyResult;
impl ProfileCombinedPaginationCursor {
// get cursor for page that starts immediately after the given post
pub fn after_post(view: &ProfileCombinedView) -> ProfileCombinedPaginationCursor {
let (prefix, id) = match view {
ProfileCombinedView::Comment(v) => ('C', v.comment.id.0),
ProfileCombinedView::Post(v) => ('P', v.post.id.0),
};
// hex encoding to prevent ossification
ProfileCombinedPaginationCursor(format!("{prefix}{id:x}"))
}
pub async fn read(&self, pool: &mut DbPool<'_>) -> Result<PaginationCursorData, Error> {
let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into());
let mut query = profile_combined::table
.select(ProfileCombined::as_select())
.into_boxed();
let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?;
let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?;
query = match prefix {
"C" => query.filter(profile_combined::comment_id.eq(id)),
"P" => query.filter(profile_combined::post_id.eq(id)),
_ => return Err(err_msg()),
};
let token = query.first(&mut get_conn(pool).await?).await?;
Ok(PaginationCursorData(token))
}
}
#[derive(Clone)]
pub struct PaginationCursorData(ProfileCombined);
// TODO check these
#[derive(Default)]
pub struct ProfileCombinedQuery {
pub community_id: Option<CommunityId>,
pub unresolved_only: Option<bool>,
pub page_after: Option<PaginationCursorData>,
pub page_back: Option<bool>,
}
impl ProfileCombinedQuery {
pub async fn list(
self,
pool: &mut DbPool<'_>,
user: &LocalUserView,
) -> LemmyResult<Vec<ProfileCombinedView>> {
let my_person_id = user.local_user.person_id;
// let item_creator = aliases::person1.field(person::id);
let item_creator = person::id;
let conn = &mut get_conn(pool).await?;
// Notes: since the post_id and comment_id are optional columns,
// many joins must use an OR condition.
// For example, the creator must be the person table joined to either:
// - post.creator_id
// - comment.creator_id
let mut query = profile_combined::table
// The comment
.left_join(comment::table.on(profile_combined::comment_id.eq(comment::id.nullable())))
// The post
.inner_join(
post::table.on(
profile_combined::post_id
.eq(post::id.nullable())
.or(comment::post_id.nullable().eq(profile_combined::post_id)),
),
)
// The item creator
.inner_join(
person::table.on(
post::creator_id
.eq(person::id)
.or(comment::creator_id.eq(person::id)),
),
)
// The item creator
// You can now use aliases::person1.field(person::id) / item_creator for all the item actions
// .inner_join(
// aliases::person1.on(
// post::creator_id
// .eq(item_creator)
// .or(comment::creator_id.eq(item_creator)),
// ),
// )
// The community
.inner_join(community::table.on(post::community_id.eq(community::id)))
.left_join(actions_alias(
creator_community_actions,
item_creator,
post::community_id,
))
.left_join(
local_user::table.on(
item_creator
.eq(local_user::person_id)
.and(local_user::admin.eq(true)),
),
)
.left_join(actions(
community_actions::table,
Some(my_person_id),
post::community_id,
))
.left_join(actions(post_actions::table, Some(my_person_id), post::id))
.left_join(actions(
person_actions::table,
Some(my_person_id),
item_creator,
))
.left_join(
post_aggregates::table
.on(profile_combined::post_id.eq(post_aggregates::post_id.nullable())),
)
.left_join(
comment_aggregates::table
.on(profile_combined::comment_id.eq(comment_aggregates::comment_id.nullable())),
)
.left_join(actions(
comment_actions::table,
Some(my_person_id),
comment::id,
))
.select((
// Post-specific
post::all_columns.nullable(),
// post_aggregates::all_columns.nullable(),
// coalesce(
// post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(),
// post_aggregates::comments,
// )
// .nullable(),
// post_actions::saved.nullable().is_not_null(),
// post_actions::read.nullable().is_not_null(),
// post_actions::hidden.nullable().is_not_null(),
// post_actions::like_score.nullable(),
// // Comment-specific
// comment::all_columns.nullable(),
// comment_aggregates::all_columns.nullable(),
// comment_actions::saved.nullable().is_not_null(),
// comment_actions::like_score.nullable(),
// // Private-message-specific
// private_message_profile::all_columns.nullable(),
// private_message::all_columns.nullable(),
// // Shared
// person::all_columns,
// aliases::person1.fields(person::all_columns),
// community::all_columns.nullable(),
// CommunityFollower::select_subscribed_type(),
// aliases::person2.fields(person::all_columns.nullable()),
// local_user::admin.nullable().is_not_null(),
// creator_community_actions
// .field(community_actions::received_ban)
// .nullable()
// .is_not_null(),
// creator_community_actions
// .field(community_actions::became_moderator)
// .nullable()
// .is_not_null(),
// person_actions::blocked.nullable().is_not_null(),
))
.into_boxed();
if let Some(community_id) = self.community_id {
query = query.filter(community::id.eq(community_id));
}
// If its not an admin, get only the ones you mod
if !user.local_user.admin {
query = query.filter(community_actions::became_moderator.is_not_null());
}
let mut query = PaginatedQueryBuilder::new(query);
let page_after = self.page_after.map(|c| c.0);
if self.page_back.unwrap_or_default() {
query = query.before(page_after).limit_and_offset_from_end();
} else {
query = query.after(page_after);
}
// If viewing all profiles, order by newest, but if viewing unresolved only, show the oldest
// first (FIFO)
if self.unresolved_only.unwrap_or_default() {
query = query
.filter(
post_profile::resolved
.eq(false)
.or(comment_profile::resolved.eq(false))
.or(private_message_profile::resolved.eq(false)),
)
// TODO: when a `then_asc` method is added, use it here, make the id sort direction match,
// and remove the separate index; unless additional columns are added to this sort
.then_desc(ReverseTimestampKey(key::published));
} else {
query = query.then_desc(key::published);
}
// Tie breaker
query = query.then_desc(key::id);
let res = query.load::<ProfileCombinedViewInternal>(conn).await?;
// Map the query results to the enum
let out = res.into_iter().filter_map(map_to_enum).collect();
Ok(out)
}
}
/// Maps the combined DB row to an enum
fn map_to_enum(view: ProfileCombinedViewInternal) -> Option<ProfileCombinedView> {
// Use for a short alias
let v = view;
if let (Some(post), Some(community), Some(unread_comments), Some(counts)) =
(v.post, v.community, v.post_unread_comments, v.post_counts)
{
Some(ProfileCombinedView::Post(PostView {
post,
community,
unread_comments,
counts,
creator: v.item_creator,
creator_banned_from_community: v.item_creator_banned_from_community,
creator_is_moderator: v.item_creator_is_moderator,
creator_is_admin: v.item_creator_is_admin,
creator_blocked: v.item_creator_blocked,
subscribed: v.subscribed,
saved: v.post_saved,
read: v.post_read,
hidden: v.post_hidden,
my_vote: v.my_post_vote,
}))
} else if let (Some(comment), Some(counts), Some(post), Some(community)) = (
v.comment,
v.comment_counts,
v.post.clone(),
v.community.clone(),
) {
Some(ProfileCombinedView::Comment(CommentView {
comment,
counts,
post,
community,
creator: v.item_creator,
creator_banned_from_community: v.item_creator_banned_from_community,
creator_is_moderator: v.item_creator_is_moderator,
creator_is_admin: v.item_creator_is_admin,
creator_blocked: v.item_creator_blocked,
subscribed: v.subscribed,
saved: v.comment_saved,
my_vote: v.my_comment_vote,
}))
} else {
None
}
}

View file

@ -132,6 +132,12 @@ pub struct PaginationCursor(pub String);
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
pub struct ReportCombinedPaginationCursor(pub String); pub struct ReportCombinedPaginationCursor(pub String);
/// like PaginationCursor but for the profile_combined table
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(ts_rs::TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct ProfileCombinedPaginationCursor(pub String);
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", derive(TS, Queryable))]
@ -289,3 +295,44 @@ pub enum ReportCombinedView {
Comment(CommentReportView), Comment(CommentReportView),
PrivateMessage(PrivateMessageReportView), PrivateMessage(PrivateMessageReportView),
} }
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
/// A combined profile view
pub struct ProfileCombinedViewInternal {
// Post-specific
pub post_counts: PostAggregates,
pub post_unread_comments: i64,
pub post_saved: bool,
pub post_read: bool,
pub post_hidden: bool,
pub my_post_vote: Option<i16>,
pub image_details: Option<ImageDetails>,
// Comment-specific
pub comment: Comment,
pub comment_counts: CommentAggregates,
pub comment_saved: bool,
pub my_comment_vote: Option<i16>,
// Shared
pub post: Post,
pub community: Community,
pub item_creator: Person,
pub subscribed: SubscribedType,
pub item_creator_is_admin: bool,
pub item_creator_is_moderator: bool,
pub item_creator_banned_from_community: bool,
pub item_creator_blocked: bool,
pub item_saved: bool,
pub banned_from_community: bool,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
// Use serde's internal tagging, to work easier with javascript libraries
#[serde(tag = "type_")]
pub enum ProfileCombinedView {
Post(PostView),
Comment(CommentView),
}

View file

@ -0,0 +1 @@
DROP TABLE profile_combined;

View file

@ -0,0 +1,29 @@
-- Creates combined tables for
-- Profile: (comment, post)
CREATE TABLE profile_combined (
id serial PRIMARY KEY,
published timestamptz NOT NULL,
post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE,
comment_id int UNIQUE REFERENCES comment ON UPDATE CASCADE ON DELETE CASCADE,
-- Make sure only one of the columns is not null
CHECK ((post_id IS NOT NULL)::integer + (comment_id IS NOT NULL)::integer = 1)
);
CREATE INDEX idx_profile_combined_published ON profile_combined (published DESC, id DESC);
CREATE INDEX idx_profile_combined_published_asc ON profile_combined (reverse_timestamp_sort (published) DESC, id DESC);
-- Updating the history
INSERT INTO profile_combined (published, post_id)
SELECT
published,
id
FROM
post;
INSERT INTO profile_combined (published, comment_id)
SELECT
published,
id
FROM
comment;