mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-12-23 03:11:32 +00:00
Merge branch 'combined_tables_2' into combined_profile
This commit is contained in:
commit
0b514c5b92
23 changed files with 726 additions and 244 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2686,8 +2686,10 @@ dependencies = [
|
||||||
"lemmy_utils",
|
"lemmy_utils",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
|
"test-context",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId},
|
newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, TagId},
|
||||||
ListingType,
|
ListingType,
|
||||||
PostFeatureType,
|
PostFeatureType,
|
||||||
PostSortType,
|
PostSortType,
|
||||||
|
@ -37,6 +37,8 @@ pub struct CreatePost {
|
||||||
/// Instead of fetching a thumbnail, use a custom one.
|
/// Instead of fetching a thumbnail, use a custom one.
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub custom_thumbnail: Option<String>,
|
pub custom_thumbnail: Option<String>,
|
||||||
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
|
pub tags: Option<Vec<TagId>>,
|
||||||
/// Time when this post should be scheduled. Null means publish immediately.
|
/// Time when this post should be scheduled. Null means publish immediately.
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub scheduled_publish_time: Option<i64>,
|
pub scheduled_publish_time: Option<i64>,
|
||||||
|
@ -164,6 +166,8 @@ pub struct EditPost {
|
||||||
/// Instead of fetching a thumbnail, use a custom one.
|
/// Instead of fetching a thumbnail, use a custom one.
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub custom_thumbnail: Option<String>,
|
pub custom_thumbnail: Option<String>,
|
||||||
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
|
pub tags: Option<Vec<TagId>>,
|
||||||
/// Time when this post should be scheduled. Null means publish immediately.
|
/// Time when this post should be scheduled. Null means publish immediately.
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub scheduled_publish_time: Option<i64>,
|
pub scheduled_publish_time: Option<i64>,
|
||||||
|
|
|
@ -51,9 +51,11 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder {
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResult<LinkMetadata> {
|
pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResult<LinkMetadata> {
|
||||||
info!("Fetching site metadata for url: {}", url);
|
info!("Fetching site metadata for url: {}", url);
|
||||||
// We only fetch the first 64kB of data in order to not waste bandwidth especially for large
|
// We only fetch the first MB of data in order to not waste bandwidth especially for large
|
||||||
// binary files
|
// binary files. This high limit is particularly needed for youtube, which includes a lot of
|
||||||
let bytes_to_fetch = 64 * 1024;
|
// javascript code before the opengraph tags. Mastodon also uses a 1 MB limit:
|
||||||
|
// https://github.com/mastodon/mastodon/blob/295ad6f19a016b3f16e1201ffcbb1b3ad6b455a2/app/lib/request.rb#L213
|
||||||
|
let bytes_to_fetch = 1024 * 1024;
|
||||||
let response = context
|
let response = context
|
||||||
.client()
|
.client()
|
||||||
.get(url.as_str())
|
.get(url.as_str())
|
||||||
|
|
|
@ -42,7 +42,8 @@ pub async fn markdown_rewrite_remote_links(
|
||||||
let mut local_url = local_url.to_string();
|
let mut local_url = local_url.to_string();
|
||||||
// restore title
|
// restore title
|
||||||
if let Some(extra) = extra {
|
if let Some(extra) = extra {
|
||||||
local_url = format!("{local_url} {extra}");
|
local_url.push(' ');
|
||||||
|
local_url.push_str(extra);
|
||||||
}
|
}
|
||||||
src.replace_range(start..end, local_url.as_str());
|
src.replace_range(start..end, local_url.as_str());
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,4 +35,5 @@ pub mod private_message_report;
|
||||||
pub mod registration_application;
|
pub mod registration_application;
|
||||||
pub mod secret;
|
pub mod secret;
|
||||||
pub mod site;
|
pub mod site;
|
||||||
|
pub mod tag;
|
||||||
pub mod tagline;
|
pub mod tagline;
|
||||||
|
|
53
crates/db_schema/src/impls/tag.rs
Normal file
53
crates/db_schema/src/impls/tag.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
use crate::{
|
||||||
|
newtypes::TagId,
|
||||||
|
schema::{post_tag, tag},
|
||||||
|
source::tag::{PostTagInsertForm, Tag, TagInsertForm},
|
||||||
|
traits::Crud,
|
||||||
|
utils::{get_conn, DbPool},
|
||||||
|
};
|
||||||
|
use diesel::{insert_into, result::Error, QueryDsl};
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
|
use lemmy_utils::error::LemmyResult;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Crud for Tag {
|
||||||
|
type InsertForm = TagInsertForm;
|
||||||
|
|
||||||
|
type UpdateForm = TagInsertForm;
|
||||||
|
|
||||||
|
type IdType = TagId;
|
||||||
|
|
||||||
|
async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result<Self, Error> {
|
||||||
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
insert_into(tag::table)
|
||||||
|
.values(form)
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(
|
||||||
|
pool: &mut DbPool<'_>,
|
||||||
|
pid: TagId,
|
||||||
|
form: &Self::UpdateForm,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
diesel::update(tag::table.find(pid))
|
||||||
|
.set(form)
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostTagInsertForm {
|
||||||
|
pub async fn insert_tag_associations(
|
||||||
|
pool: &mut DbPool<'_>,
|
||||||
|
tags: &[PostTagInsertForm],
|
||||||
|
) -> LemmyResult<()> {
|
||||||
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
insert_into(post_tag::table)
|
||||||
|
.values(tags)
|
||||||
|
.execute(conn)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -298,3 +298,9 @@ impl InstanceId {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
|
||||||
|
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
/// The internal tag id.
|
||||||
|
pub struct TagId(pub i32);
|
||||||
|
|
|
@ -845,6 +845,14 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
post_tag (post_id, tag_id) {
|
||||||
|
post_id -> Int4,
|
||||||
|
tag_id -> Int4,
|
||||||
|
published -> Timestamptz,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
private_message (id) {
|
private_message (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
|
@ -980,6 +988,18 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
tag (id) {
|
||||||
|
id -> Int4,
|
||||||
|
ap_id -> Text,
|
||||||
|
name -> Text,
|
||||||
|
community_id -> Int4,
|
||||||
|
published -> Timestamptz,
|
||||||
|
updated -> Nullable<Timestamptz>,
|
||||||
|
deleted -> Bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
tagline (id) {
|
tagline (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
|
@ -1066,6 +1086,8 @@ diesel::joinable!(post_aggregates -> instance (instance_id));
|
||||||
diesel::joinable!(post_aggregates -> person (creator_id));
|
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!(post_tag -> post (post_id));
|
||||||
|
diesel::joinable!(post_tag -> tag (tag_id));
|
||||||
diesel::joinable!(private_message_report -> private_message (private_message_id));
|
diesel::joinable!(private_message_report -> private_message (private_message_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));
|
||||||
|
@ -1076,6 +1098,7 @@ diesel::joinable!(site -> instance (instance_id));
|
||||||
diesel::joinable!(site_aggregates -> site (site_id));
|
diesel::joinable!(site_aggregates -> site (site_id));
|
||||||
diesel::joinable!(site_language -> language (language_id));
|
diesel::joinable!(site_language -> language (language_id));
|
||||||
diesel::joinable!(site_language -> site (site_id));
|
diesel::joinable!(site_language -> site (site_id));
|
||||||
|
diesel::joinable!(tag -> community (community_id));
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
admin_allow_instance,
|
admin_allow_instance,
|
||||||
|
@ -1137,6 +1160,7 @@ diesel::allow_tables_to_appear_in_same_query!(
|
||||||
post_actions,
|
post_actions,
|
||||||
post_aggregates,
|
post_aggregates,
|
||||||
post_report,
|
post_report,
|
||||||
|
post_tag,
|
||||||
private_message,
|
private_message,
|
||||||
private_message_report,
|
private_message_report,
|
||||||
received_activity,
|
received_activity,
|
||||||
|
@ -1148,5 +1172,6 @@ diesel::allow_tables_to_appear_in_same_query!(
|
||||||
site,
|
site,
|
||||||
site_aggregates,
|
site_aggregates,
|
||||||
site_language,
|
site_language,
|
||||||
|
tag,
|
||||||
tagline,
|
tagline,
|
||||||
);
|
);
|
||||||
|
|
|
@ -41,6 +41,7 @@ pub mod private_message_report;
|
||||||
pub mod registration_application;
|
pub mod registration_application;
|
||||||
pub mod secret;
|
pub mod secret;
|
||||||
pub mod site;
|
pub mod site;
|
||||||
|
pub mod tag;
|
||||||
pub mod tagline;
|
pub mod tagline;
|
||||||
|
|
||||||
/// Default value for columns like [community::Community.inbox_url] which are marked as serde(skip).
|
/// Default value for columns like [community::Community.inbox_url] which are marked as serde(skip).
|
||||||
|
|
57
crates/db_schema/src/source/tag.rs
Normal file
57
crates/db_schema/src/source/tag.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use crate::newtypes::{CommunityId, DbUrl, PostId, TagId};
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
use crate::schema::{post_tag, tag};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::skip_serializing_none;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
use ts_rs::TS;
|
||||||
|
|
||||||
|
/// A tag that can be assigned to a post within a community.
|
||||||
|
/// The tag object is created by the community moderators.
|
||||||
|
/// The assignment happens by the post creator and can be updated by the community moderators.
|
||||||
|
///
|
||||||
|
/// A tag is a federatable object that gives additional context to another object, which can be
|
||||||
|
/// displayed and filtered on currently, we only have community post tags, which is a tag that is
|
||||||
|
/// created by post authors as well as mods of a community, to categorize a post. in the future we
|
||||||
|
/// may add more tag types, depending on the requirements, this will lead to either expansion of
|
||||||
|
/// this table (community_id optional, addition of tag_type enum) or split of this table / creation
|
||||||
|
/// of new tables.
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable, Identifiable))]
|
||||||
|
#[cfg_attr(feature = "full", diesel(table_name = tag))]
|
||||||
|
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
pub struct Tag {
|
||||||
|
pub id: TagId,
|
||||||
|
pub ap_id: DbUrl,
|
||||||
|
pub name: String,
|
||||||
|
/// the community that owns this tag
|
||||||
|
pub community_id: CommunityId,
|
||||||
|
pub published: DateTime<Utc>,
|
||||||
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
|
pub updated: Option<DateTime<Utc>>,
|
||||||
|
pub deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
|
||||||
|
#[cfg_attr(feature = "full", diesel(table_name = tag))]
|
||||||
|
pub struct TagInsertForm {
|
||||||
|
pub ap_id: DbUrl,
|
||||||
|
pub name: String,
|
||||||
|
pub community_id: CommunityId,
|
||||||
|
// default now
|
||||||
|
pub published: Option<DateTime<Utc>>,
|
||||||
|
pub updated: Option<DateTime<Utc>>,
|
||||||
|
pub deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
|
||||||
|
#[cfg_attr(feature = "full", diesel(table_name = post_tag))]
|
||||||
|
pub struct PostTagInsertForm {
|
||||||
|
pub post_id: PostId,
|
||||||
|
pub tag_id: TagId,
|
||||||
|
}
|
|
@ -547,6 +547,11 @@ pub mod functions {
|
||||||
|
|
||||||
// really this function is variadic, this just adds the two-argument version
|
// really this function is variadic, this just adds the two-argument version
|
||||||
define_sql_function!(fn coalesce<T: diesel::sql_types::SqlType + diesel::sql_types::SingleValue>(x: diesel::sql_types::Nullable<T>, y: T) -> T);
|
define_sql_function!(fn coalesce<T: diesel::sql_types::SqlType + diesel::sql_types::SingleValue>(x: diesel::sql_types::Nullable<T>, y: T) -> T);
|
||||||
|
|
||||||
|
define_sql_function! {
|
||||||
|
#[aggregate]
|
||||||
|
fn json_agg<T: diesel::sql_types::SqlType + diesel::sql_types::SingleValue>(obj: T) -> Json
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DELETED_REPLACEMENT_TEXT: &str = "*Permanently Deleted*";
|
pub const DELETED_REPLACEMENT_TEXT: &str = "*Permanently Deleted*";
|
||||||
|
|
|
@ -35,6 +35,7 @@ diesel-async = { workspace = true, optional = true }
|
||||||
diesel_ltree = { workspace = true, optional = true }
|
diesel_ltree = { workspace = true, optional = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_with = { workspace = true }
|
serde_with = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
tracing = { workspace = true, optional = true }
|
tracing = { workspace = true, optional = true }
|
||||||
ts-rs = { workspace = true, optional = true }
|
ts-rs = { workspace = true, optional = true }
|
||||||
actix-web = { workspace = true, optional = true }
|
actix-web = { workspace = true, optional = true }
|
||||||
|
@ -47,3 +48,4 @@ serial_test = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
|
test-context = "0.3.0"
|
||||||
|
|
|
@ -18,6 +18,8 @@ pub mod person_saved_combined_view;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod post_report_view;
|
pub mod post_report_view;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
|
pub mod post_tags_view;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
pub mod post_view;
|
pub mod post_view;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod private_message_report_view;
|
pub mod private_message_report_view;
|
||||||
|
|
|
@ -10,8 +10,11 @@ use crate::{
|
||||||
InternalToCombinedView,
|
InternalToCombinedView,
|
||||||
};
|
};
|
||||||
use diesel::{
|
use diesel::{
|
||||||
|
pg::Pg,
|
||||||
result::Error,
|
result::Error,
|
||||||
|
sql_types,
|
||||||
BoolExpressionMethods,
|
BoolExpressionMethods,
|
||||||
|
BoxableExpression,
|
||||||
ExpressionMethods,
|
ExpressionMethods,
|
||||||
JoinOnDsl,
|
JoinOnDsl,
|
||||||
NullableExpressionMethods,
|
NullableExpressionMethods,
|
||||||
|
@ -37,6 +40,8 @@ use lemmy_db_schema::{
|
||||||
post,
|
post,
|
||||||
post_actions,
|
post_actions,
|
||||||
post_aggregates,
|
post_aggregates,
|
||||||
|
post_tag,
|
||||||
|
tag,
|
||||||
},
|
},
|
||||||
source::{
|
source::{
|
||||||
combined::person_content::{person_content_combined_keys as key, PersonContentCombined},
|
combined::person_content::{person_content_combined_keys as key, PersonContentCombined},
|
||||||
|
@ -98,6 +103,19 @@ impl PersonContentCombinedQuery {
|
||||||
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
|
||||||
|
let post_tags: Box<
|
||||||
|
dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::Json>>,
|
||||||
|
> = Box::new(
|
||||||
|
post_tag::table
|
||||||
|
.inner_join(tag::table)
|
||||||
|
.select(diesel::dsl::sql::<diesel::sql_types::Json>(
|
||||||
|
"json_agg(tag.*)",
|
||||||
|
))
|
||||||
|
.filter(post_tag::post_id.eq(post::id))
|
||||||
|
.filter(tag::deleted.eq(false))
|
||||||
|
.single_value(),
|
||||||
|
);
|
||||||
|
|
||||||
// Notes: since the post_id and comment_id are optional columns,
|
// Notes: since the post_id and comment_id are optional columns,
|
||||||
// many joins must use an OR condition.
|
// many joins must use an OR condition.
|
||||||
// For example, the creator must be the person table joined to either:
|
// For example, the creator must be the person table joined to either:
|
||||||
|
@ -172,6 +190,7 @@ impl PersonContentCombinedQuery {
|
||||||
post_actions::hidden.nullable().is_not_null(),
|
post_actions::hidden.nullable().is_not_null(),
|
||||||
post_actions::like_score.nullable(),
|
post_actions::like_score.nullable(),
|
||||||
image_details::all_columns.nullable(),
|
image_details::all_columns.nullable(),
|
||||||
|
post_tags,
|
||||||
// Comment-specific
|
// Comment-specific
|
||||||
comment::all_columns.nullable(),
|
comment::all_columns.nullable(),
|
||||||
comment_aggregates::all_columns.nullable(),
|
comment_aggregates::all_columns.nullable(),
|
||||||
|
@ -262,6 +281,7 @@ impl InternalToCombinedView for PersonContentViewInternal {
|
||||||
my_vote: v.my_post_vote,
|
my_vote: v.my_post_vote,
|
||||||
image_details: v.image_details,
|
image_details: v.image_details,
|
||||||
banned_from_community: v.banned_from_community,
|
banned_from_community: v.banned_from_community,
|
||||||
|
tags: v.post_tags,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,11 @@ use crate::{
|
||||||
InternalToCombinedView,
|
InternalToCombinedView,
|
||||||
};
|
};
|
||||||
use diesel::{
|
use diesel::{
|
||||||
|
pg::Pg,
|
||||||
result::Error,
|
result::Error,
|
||||||
|
sql_types,
|
||||||
BoolExpressionMethods,
|
BoolExpressionMethods,
|
||||||
|
BoxableExpression,
|
||||||
ExpressionMethods,
|
ExpressionMethods,
|
||||||
JoinOnDsl,
|
JoinOnDsl,
|
||||||
NullableExpressionMethods,
|
NullableExpressionMethods,
|
||||||
|
@ -34,6 +37,8 @@ use lemmy_db_schema::{
|
||||||
post,
|
post,
|
||||||
post_actions,
|
post_actions,
|
||||||
post_aggregates,
|
post_aggregates,
|
||||||
|
post_tag,
|
||||||
|
tag,
|
||||||
},
|
},
|
||||||
source::{
|
source::{
|
||||||
combined::person_saved::{person_saved_combined_keys as key, PersonSavedCombined},
|
combined::person_saved::{person_saved_combined_keys as key, PersonSavedCombined},
|
||||||
|
@ -92,6 +97,19 @@ impl PersonSavedCombinedQuery {
|
||||||
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
|
||||||
|
let post_tags: Box<
|
||||||
|
dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::Json>>,
|
||||||
|
> = Box::new(
|
||||||
|
post_tag::table
|
||||||
|
.inner_join(tag::table)
|
||||||
|
.select(diesel::dsl::sql::<diesel::sql_types::Json>(
|
||||||
|
"json_agg(tag.*)",
|
||||||
|
))
|
||||||
|
.filter(post_tag::post_id.eq(post::id))
|
||||||
|
.filter(tag::deleted.eq(false))
|
||||||
|
.single_value(),
|
||||||
|
);
|
||||||
|
|
||||||
// Notes: since the post_id and comment_id are optional columns,
|
// Notes: since the post_id and comment_id are optional columns,
|
||||||
// many joins must use an OR condition.
|
// many joins must use an OR condition.
|
||||||
// For example, the creator must be the person table joined to either:
|
// For example, the creator must be the person table joined to either:
|
||||||
|
@ -174,6 +192,7 @@ impl PersonSavedCombinedQuery {
|
||||||
post_actions::hidden.nullable().is_not_null(),
|
post_actions::hidden.nullable().is_not_null(),
|
||||||
post_actions::like_score.nullable(),
|
post_actions::like_score.nullable(),
|
||||||
image_details::all_columns.nullable(),
|
image_details::all_columns.nullable(),
|
||||||
|
post_tags,
|
||||||
// Comment-specific
|
// Comment-specific
|
||||||
comment::all_columns.nullable(),
|
comment::all_columns.nullable(),
|
||||||
comment_aggregates::all_columns.nullable(),
|
comment_aggregates::all_columns.nullable(),
|
||||||
|
|
30
crates/db_views/src/post_tags_view.rs
Normal file
30
crates/db_views/src/post_tags_view.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
//! see post_view.rs for the reason for this json decoding
|
||||||
|
use crate::structs::PostTags;
|
||||||
|
use diesel::{
|
||||||
|
deserialize::FromSql,
|
||||||
|
pg::{Pg, PgValue},
|
||||||
|
serialize::ToSql,
|
||||||
|
sql_types::{self, Nullable},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl FromSql<Nullable<sql_types::Json>, Pg> for PostTags {
|
||||||
|
fn from_sql(bytes: PgValue) -> diesel::deserialize::Result<Self> {
|
||||||
|
let value = <serde_json::Value as FromSql<sql_types::Json, Pg>>::from_sql(bytes)?;
|
||||||
|
Ok(serde_json::from_value::<PostTags>(value)?)
|
||||||
|
}
|
||||||
|
fn from_nullable_sql(
|
||||||
|
bytes: Option<<Pg as diesel::backend::Backend>::RawValue<'_>>,
|
||||||
|
) -> diesel::deserialize::Result<Self> {
|
||||||
|
match bytes {
|
||||||
|
Some(bytes) => Self::from_sql(bytes),
|
||||||
|
None => Ok(Self { tags: vec![] }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSql<Nullable<sql_types::Json>, Pg> for PostTags {
|
||||||
|
fn to_sql(&self, out: &mut diesel::serialize::Output<Pg>) -> diesel::serialize::Result {
|
||||||
|
let value = serde_json::to_value(self)?;
|
||||||
|
<serde_json::Value as ToSql<sql_types::Json, Pg>>::to_sql(&value, &mut out.reborrow())
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,7 @@
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
use diesel::Queryable;
|
use diesel::Queryable;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
use diesel::{deserialize::FromSqlRow, expression::AsExpression, sql_types};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
aggregates::structs::{CommentAggregates, PersonAggregates, PostAggregates, SiteAggregates},
|
aggregates::structs::{CommentAggregates, PersonAggregates, PostAggregates, SiteAggregates},
|
||||||
source::{
|
source::{
|
||||||
|
@ -20,6 +22,7 @@ use lemmy_db_schema::{
|
||||||
private_message_report::PrivateMessageReport,
|
private_message_report::PrivateMessageReport,
|
||||||
registration_application::RegistrationApplication,
|
registration_application::RegistrationApplication,
|
||||||
site::Site,
|
site::Site,
|
||||||
|
tag::Tag,
|
||||||
},
|
},
|
||||||
SubscribedType,
|
SubscribedType,
|
||||||
};
|
};
|
||||||
|
@ -169,6 +172,7 @@ pub struct PostView {
|
||||||
#[cfg_attr(feature = "full", ts(optional))]
|
#[cfg_attr(feature = "full", ts(optional))]
|
||||||
pub my_vote: Option<i16>,
|
pub my_vote: Option<i16>,
|
||||||
pub unread_comments: i64,
|
pub unread_comments: i64,
|
||||||
|
pub tags: PostTags,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
|
||||||
|
@ -315,6 +319,7 @@ pub struct PersonContentViewInternal {
|
||||||
pub post_hidden: bool,
|
pub post_hidden: bool,
|
||||||
pub my_post_vote: Option<i16>,
|
pub my_post_vote: Option<i16>,
|
||||||
pub image_details: Option<ImageDetails>,
|
pub image_details: Option<ImageDetails>,
|
||||||
|
pub post_tags: PostTags,
|
||||||
// Comment-specific
|
// Comment-specific
|
||||||
pub comment: Option<Comment>,
|
pub comment: Option<Comment>,
|
||||||
pub comment_counts: Option<CommentAggregates>,
|
pub comment_counts: Option<CommentAggregates>,
|
||||||
|
@ -341,3 +346,12 @@ pub enum PersonContentCombinedView {
|
||||||
Post(PostView),
|
Post(PostView),
|
||||||
Comment(CommentView),
|
Comment(CommentView),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, Default)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS, FromSqlRow, AsExpression))]
|
||||||
|
#[serde(transparent)]
|
||||||
|
#[cfg_attr(feature = "full", diesel(sql_type = Nullable<sql_types::Json>))]
|
||||||
|
/// we wrap this in a struct so we can implement FromSqlRow<Json> for it
|
||||||
|
pub struct PostTags {
|
||||||
|
pub tags: Vec<Tag>,
|
||||||
|
}
|
||||||
|
|
|
@ -454,7 +454,6 @@ fn build_item(
|
||||||
protocol_and_hostname: &str,
|
protocol_and_hostname: &str,
|
||||||
) -> LemmyResult<Item> {
|
) -> LemmyResult<Item> {
|
||||||
// TODO add images
|
// TODO add images
|
||||||
let author_url = format!("{protocol_and_hostname}/u/{creator_name}");
|
|
||||||
let guid = Some(Guid {
|
let guid = Some(Guid {
|
||||||
permalink: true,
|
permalink: true,
|
||||||
value: url.to_owned(),
|
value: url.to_owned(),
|
||||||
|
@ -464,7 +463,8 @@ fn build_item(
|
||||||
Ok(Item {
|
Ok(Item {
|
||||||
title: Some(format!("Reply from {creator_name}")),
|
title: Some(format!("Reply from {creator_name}")),
|
||||||
author: Some(format!(
|
author: Some(format!(
|
||||||
"/u/{creator_name} <a href=\"{author_url}\">(link)</a>"
|
"/u/{creator_name} <a href=\"{}\">(link)</a>",
|
||||||
|
format_args!("{protocol_and_hostname}/u/{creator_name}")
|
||||||
)),
|
)),
|
||||||
pub_date: Some(published.to_rfc2822()),
|
pub_date: Some(published.to_rfc2822()),
|
||||||
comments: Some(url.to_owned()),
|
comments: Some(url.to_owned()),
|
||||||
|
|
|
@ -24,7 +24,8 @@ pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec<Url>) {
|
||||||
);
|
);
|
||||||
// restore custom emoji format
|
// restore custom emoji format
|
||||||
if let Some(extra) = extra {
|
if let Some(extra) = extra {
|
||||||
proxied = format!("{proxied} {extra}");
|
proxied.push(' ');
|
||||||
|
proxied.push_str(extra);
|
||||||
}
|
}
|
||||||
src.replace_range(start..end, &proxied);
|
src.replace_range(start..end, &proxied);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
DROP TABLE post_tag;
|
||||||
|
|
||||||
|
DROP TABLE tag;
|
||||||
|
|
23
migrations/2024-12-17-144959_community-post-tags/up.sql
Normal file
23
migrations/2024-12-17-144959_community-post-tags/up.sql
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
-- a tag is a federatable object that gives additional context to another object, which can be displayed and filtered on
|
||||||
|
-- currently, we only have community post tags, which is a tag that is created by post authors as well as mods of a community,
|
||||||
|
-- to categorize a post. in the future we may add more tag types, depending on the requirements,
|
||||||
|
-- this will lead to either expansion of this table (community_id optional, addition of tag_type enum)
|
||||||
|
-- or split of this table / creation of new tables.
|
||||||
|
CREATE TABLE tag (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
ap_id text NOT NULL UNIQUE,
|
||||||
|
name text NOT NULL,
|
||||||
|
community_id int NOT NULL REFERENCES community (id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
published timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated timestamptz,
|
||||||
|
deleted boolean NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- an association between a post and a tag. created/updated by the post author or mods of a community
|
||||||
|
CREATE TABLE post_tag (
|
||||||
|
post_id int NOT NULL REFERENCES post (id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
tag_id int NOT NULL REFERENCES tag (id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||||
|
published timestamptz NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (post_id, tag_id)
|
||||||
|
);
|
||||||
|
|
|
@ -190,10 +190,8 @@ async fn process_ranks_in_batches(
|
||||||
UPDATE {aggregates_table} a {set_clause}
|
UPDATE {aggregates_table} a {set_clause}
|
||||||
FROM batch WHERE a.{id_column} = batch.{id_column} RETURNING a.published;
|
FROM batch WHERE a.{id_column} = batch.{id_column} RETURNING a.published;
|
||||||
"#,
|
"#,
|
||||||
id_column = format!("{table_name}_id"),
|
id_column = format_args!("{table_name}_id"),
|
||||||
aggregates_table = format!("{table_name}_aggregates"),
|
aggregates_table = format_args!("{table_name}_aggregates"),
|
||||||
set_clause = set_clause,
|
|
||||||
where_clause = where_clause
|
|
||||||
))
|
))
|
||||||
.bind::<Timestamptz, _>(previous_batch_last_published)
|
.bind::<Timestamptz, _>(previous_batch_last_published)
|
||||||
.bind::<Integer, _>(update_batch_size)
|
.bind::<Integer, _>(update_batch_size)
|
||||||
|
|
Loading…
Reference in a new issue