From 003613b736f4d4142e4bacb8a025f98e3d2e3453 Mon Sep 17 00:00:00 2001 From: phiresky Date: Sun, 18 Aug 2024 14:49:52 +0200 Subject: [PATCH 1/3] partial post tags implementation --- Cargo.lock | 1 + crates/api_common/src/post.rs | 2 ++ crates/db_schema/src/newtypes.rs | 7 +++++ crates/db_schema/src/schema.rs | 24 +++++++++++++++++ .../src/source/community_post_tag.rs | 26 +++++++++++++++++++ crates/db_schema/src/source/mod.rs | 1 + crates/db_schema/src/utils.rs | 5 ++++ crates/db_views/Cargo.toml | 1 + crates/db_views/src/post_view.rs | 13 ++++++++++ crates/db_views/src/structs.rs | 19 ++------------ .../down.sql | 3 +++ .../up.sql | 17 ++++++++++++ src/api_routes_http.rs | 5 +++- 13 files changed, 106 insertions(+), 18 deletions(-) create mode 100644 crates/db_schema/src/source/community_post_tag.rs create mode 100644 migrations/2024-08-17-144959_community-post-tags/down.sql create mode 100644 migrations/2024-08-17-144959_community-post-tags/up.sql diff --git a/Cargo.lock b/Cargo.lock index 5dcdd045c2..271397841f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2996,6 +2996,7 @@ dependencies = [ "lemmy_utils", "pretty_assertions", "serde", + "serde_json", "serde_with", "serial_test", "tokio", diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 74369173be..6ddc677520 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -30,6 +30,7 @@ pub struct CreatePost { pub language_id: Option, /// Instead of fetching a thumbnail, use a custom one. pub custom_thumbnail: Option, + pub community_post_tags: Option>, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -124,6 +125,7 @@ pub struct EditPost { pub language_id: Option, /// Instead of fetching a thumbnail, use a custom one. pub custom_thumbnail: Option, + pub community_post_tags: Option>, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c715305bba..7f7328f658 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -283,3 +283,10 @@ impl InstanceId { 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 post id. +pub struct CommunityPostTagId(pub i32); \ No newline at end of file diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index fc418ec288..dab162a976 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -251,6 +251,18 @@ diesel::table! { } } +diesel::table! { + community_post_tag (id) { + id -> Int4, + ap_id -> Text, + community_id -> Int4, + name -> Text, + published -> Timestamptz, + updated -> Nullable, + deleted -> Nullable, + } +} + diesel::table! { custom_emoji (id) { id -> Int4, @@ -759,6 +771,13 @@ diesel::table! { } } +diesel::table! { + post_community_post_tag (post_id, community_post_tag_id) { + post_id -> Int4, + community_post_tag_id -> Int4, + } +} + diesel::table! { post_hide (person_id, post_id) { post_id -> Int4, @@ -974,6 +993,7 @@ diesel::joinable!(community_moderator -> community (community_id)); diesel::joinable!(community_moderator -> person (person_id)); diesel::joinable!(community_person_ban -> community (community_id)); diesel::joinable!(community_person_ban -> person (person_id)); +diesel::joinable!(community_post_tag -> community (community_id)); diesel::joinable!(custom_emoji -> local_site (local_site_id)); diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id)); diesel::joinable!(email_verification -> local_user (local_user_id)); @@ -1020,6 +1040,8 @@ diesel::joinable!(post_aggregates -> community (community_id)); diesel::joinable!(post_aggregates -> instance (instance_id)); diesel::joinable!(post_aggregates -> person (creator_id)); diesel::joinable!(post_aggregates -> post (post_id)); +diesel::joinable!(post_community_post_tag -> community_post_tag (community_post_tag_id)); +diesel::joinable!(post_community_post_tag -> post (post_id)); diesel::joinable!(post_hide -> person (person_id)); diesel::joinable!(post_hide -> post (post_id)); diesel::joinable!(post_like -> person (person_id)); @@ -1057,6 +1079,7 @@ diesel::allow_tables_to_appear_in_same_query!( community_language, community_moderator, community_person_ban, + community_post_tag, custom_emoji, custom_emoji_keyword, email_verification, @@ -1096,6 +1119,7 @@ diesel::allow_tables_to_appear_in_same_query!( person_post_aggregates, post, post_aggregates, + post_community_post_tag, post_hide, post_like, post_read, diff --git a/crates/db_schema/src/source/community_post_tag.rs b/crates/db_schema/src/source/community_post_tag.rs new file mode 100644 index 0000000000..80555711eb --- /dev/null +++ b/crates/db_schema/src/source/community_post_tag.rs @@ -0,0 +1,26 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +use ts_rs::TS; + +use crate::newtypes::{CommunityId, CommunityPostTagId}; + +/// 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. +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct CommunityPostTag { + pub id: CommunityPostTagId, + pub ap_id: String, + pub community_id: CommunityId, + pub name: String, + pub published: DateTime, + pub updated: Option>, + pub deleted: Option> +} + + \ No newline at end of file diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index bbc8aafa29..73a1d8020c 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -39,6 +39,7 @@ pub mod registration_application; pub mod secret; pub mod site; pub mod tagline; +pub mod community_post_tag; /// Default value for columns like [community::Community.inbox_url] which are marked as serde(skip). /// diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index b71c43495b..d089ba7a1c 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -508,6 +508,11 @@ pub mod functions { sql_function!(fn coalesce(x: diesel::sql_types::Nullable, y: T) -> T); sql_function!(fn set_config(setting_name: Text, new_value: Text, is_local: Bool) -> Text); + + sql_function! { + #[aggregate] + fn json_agg(obj: T) -> Json + } } pub const DELETED_REPLACEMENT_TEXT: &str = "*Permanently Deleted*"; diff --git a/crates/db_views/Cargo.toml b/crates/db_views/Cargo.toml index df8124c8a9..b2d2b2f8d7 100644 --- a/crates/db_views/Cargo.toml +++ b/crates/db_views/Cargo.toml @@ -35,6 +35,7 @@ diesel-async = { workspace = true, optional = true } diesel_ltree = { workspace = true, optional = true } serde = { workspace = true } serde_with = { workspace = true } +serde_json = { workspace = true } tracing = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true } actix-web = { workspace = true, optional = true } diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 0ec7e0a5d8..63bb7b1b7c 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -41,10 +41,13 @@ use lemmy_db_schema::{ post_like, post_read, post_saved, + post_community_post_tag, + community_post_tag }, source::{local_user::LocalUser, site::Site}, utils::{ functions::coalesce, + functions::json_agg, fuzzy_search, get_conn, limit_and_offset, @@ -214,6 +217,14 @@ fn queries<'a>() -> Queries< } else { Box::new(None::.into_sql::>()) }; + let community_post_tags: Box>> = + Box::new( + post_community_post_tag::table + .inner_join(community_post_tag::table) + .select(diesel::dsl::sql::("json_agg(community_post_tag.*)")) + .filter(post_community_post_tag::post_id.eq(post_aggregates::post_id)) + .single_value(), + ); query .inner_join(person::table) @@ -247,6 +258,7 @@ fn queries<'a>() -> Queries< post_aggregates::comments.nullable() - read_comments, post_aggregates::comments, ), + community_post_tags )) }; @@ -1764,6 +1776,7 @@ mod tests { hidden: false, saved: false, creator_blocked: false, + community_post_tags: None, }) } diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 3c219d63fb..55badcd069 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -3,23 +3,7 @@ use diesel::Queryable; use lemmy_db_schema::{ aggregates::structs::{CommentAggregates, PersonAggregates, PostAggregates, SiteAggregates}, source::{ - comment::Comment, - comment_report::CommentReport, - community::Community, - custom_emoji::CustomEmoji, - custom_emoji_keyword::CustomEmojiKeyword, - images::{ImageDetails, LocalImage}, - local_site::LocalSite, - local_site_rate_limit::LocalSiteRateLimit, - local_user::LocalUser, - local_user_vote_display_mode::LocalUserVoteDisplayMode, - person::Person, - post::Post, - post_report::PostReport, - private_message::PrivateMessage, - private_message_report::PrivateMessageReport, - registration_application::RegistrationApplication, - site::Site, + comment::Comment, comment_report::CommentReport, community::Community, community_post_tag::CommunityPostTag, custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword, images::{ImageDetails, LocalImage}, local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_user::LocalUser, local_user_vote_display_mode::LocalUserVoteDisplayMode, person::Person, post::Post, post_report::PostReport, private_message::PrivateMessage, private_message_report::PrivateMessageReport, registration_application::RegistrationApplication, site::Site }, SubscribedType, }; @@ -144,6 +128,7 @@ pub struct PostView { pub creator_blocked: bool, pub my_vote: Option, pub unread_comments: i64, + pub community_post_tags: Option } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] diff --git a/migrations/2024-08-17-144959_community-post-tags/down.sql b/migrations/2024-08-17-144959_community-post-tags/down.sql new file mode 100644 index 0000000000..cbb0e9b817 --- /dev/null +++ b/migrations/2024-08-17-144959_community-post-tags/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +drop table post_community_post_tag; +drop table community_post_tag; \ No newline at end of file diff --git a/migrations/2024-08-17-144959_community-post-tags/up.sql b/migrations/2024-08-17-144959_community-post-tags/up.sql new file mode 100644 index 0000000000..fd1633a6df --- /dev/null +++ b/migrations/2024-08-17-144959_community-post-tags/up.sql @@ -0,0 +1,17 @@ +-- a tag for a post, valid in a community. created by mods of a community +CREATE TABLE community_post_tag ( + id SERIAL PRIMARY KEY, + ap_id TEXT NOT NULL UNIQUE, + community_id INT NOT NULL REFERENCES community(id), + name TEXT NOT NULL, + published TIMESTAMPTZ NOT NULL, + updated TIMESTAMPTZ, + deleted TIMESTAMPTZ +); + +-- an association between a post and a community post tag. created/updated by the post author or mods of a community +CREATE TABLE post_community_post_tag ( + post_id INT NOT NULL REFERENCES post(id), + community_post_tag_id INT NOT NULL REFERENCES community_post_tag(id), + PRIMARY KEY (post_id, community_post_tag_id) +); diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 7b4b34158f..673cb055e2 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -183,11 +183,14 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("/follow", web::post().to(follow_community)) .route("/block", web::post().to(block_community)) .route("/delete", web::post().to(delete_community)) + .route("/post_tags", web::get().to(get_community_post_tags)) // Mod Actions .route("/remove", web::post().to(remove_community)) .route("/transfer", web::post().to(transfer_community)) .route("/ban_user", web::post().to(ban_from_community)) - .route("/mod", web::post().to(add_mod_to_community)), + .route("/mod", web::post().to(add_mod_to_community)) + .route("/post_tags", web::post().to(create_update_community_post_tag)) + .route("/post_tags/delete", web::post().to(delete_community_post_tag)), ) .service( web::scope("/federated_instances") From c130bee4201a8fa997439b5a72f20a11ef31faa0 Mon Sep 17 00:00:00 2001 From: phiresky Date: Mon, 2 Sep 2024 18:32:59 +0200 Subject: [PATCH 2/3] fixes --- crates/api_common/src/post.rs | 2 +- .../db_schema/src/impls/community_post_tag.rs | 59 +++++ crates/db_schema/src/impls/mod.rs | 1 + crates/db_schema/src/newtypes.rs | 3 +- .../src/source/community_post_tag.rs | 34 ++- crates/db_schema/src/source/mod.rs | 2 +- crates/db_views/src/post_view.rs | 231 ++++++++++++++---- crates/db_views/src/structs.rs | 67 ++++- .../down.sql | 6 +- .../up.sql | 21 +- src/api_routes_http.rs | 8 +- 11 files changed, 359 insertions(+), 75 deletions(-) create mode 100644 crates/db_schema/src/impls/community_post_tag.rs diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 6ddc677520..7a77fa56b1 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -1,5 +1,5 @@ use lemmy_db_schema::{ - newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId}, + newtypes::{CommentId, CommunityId, CommunityPostTagId, DbUrl, LanguageId, PostId, PostReportId}, ListingType, PostFeatureType, SortType, diff --git a/crates/db_schema/src/impls/community_post_tag.rs b/crates/db_schema/src/impls/community_post_tag.rs new file mode 100644 index 0000000000..7d407e98e6 --- /dev/null +++ b/crates/db_schema/src/impls/community_post_tag.rs @@ -0,0 +1,59 @@ +use crate::{ + newtypes::CommunityPostTagId, + schema::{community_post_tag, post_community_post_tag}, + source::community_post_tag::{ + CommunityPostTag, + CommunityPostTagInsertForm, + PostCommunityPostTagInsertForm, + }, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use anyhow::Context; +use diesel::{insert_into, result::Error, QueryDsl}; +use diesel_async::RunQueryDsl; +use lemmy_utils::error::LemmyResult; + +#[async_trait] +impl Crud for CommunityPostTag { + type InsertForm = CommunityPostTagInsertForm; + + type UpdateForm = CommunityPostTagInsertForm; + + type IdType = CommunityPostTagId; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(community_post_tag::table) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + pid: CommunityPostTagId, + form: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(community_post_tag::table.find(pid)) + .set(form) + .get_result::(conn) + .await + } +} + +impl PostCommunityPostTagInsertForm { + pub async fn insert_tag_associations( + pool: &mut DbPool<'_>, + tags: &[PostCommunityPostTagInsertForm], + ) -> LemmyResult<()> { + let conn = &mut get_conn(pool).await?; + insert_into(post_community_post_tag::table) + .values(tags) + .execute(conn) + .await + .context("Failed to insert post community tag associations")?; + Ok(()) + } +} diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index 3a4e71307c..4c0e29ad91 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -6,6 +6,7 @@ pub mod comment_reply; pub mod comment_report; pub mod community; pub mod community_block; +pub mod community_post_tag; pub mod custom_emoji; pub mod email_verification; pub mod federation_allowlist; diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 7f7328f658..eb85ed049b 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -284,9 +284,8 @@ impl InstanceId { } } - #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The post id. -pub struct CommunityPostTagId(pub i32); \ No newline at end of file +pub struct CommunityPostTagId(pub i32); diff --git a/crates/db_schema/src/source/community_post_tag.rs b/crates/db_schema/src/source/community_post_tag.rs index 80555711eb..8b094b2f63 100644 --- a/crates/db_schema/src/source/community_post_tag.rs +++ b/crates/db_schema/src/source/community_post_tag.rs @@ -1,26 +1,48 @@ +use crate::{ + newtypes::{CommunityId, CommunityPostTagId, DbUrl, PostId}, + schema::{community_post_tag, post_community_post_tag}, +}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use ts_rs::TS; -use crate::newtypes::{CommunityId, CommunityPostTagId}; - /// 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. #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", derive(TS, Queryable, Selectable, Identifiable))] +#[cfg_attr(feature = "full", diesel(table_name = community_post_tag))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] pub struct CommunityPostTag { pub id: CommunityPostTagId, - pub ap_id: String, + pub ap_id: DbUrl, pub community_id: CommunityId, pub name: String, pub published: DateTime, pub updated: Option>, - pub deleted: Option> + pub deleted: Option>, } - \ No newline at end of file +#[derive(Debug, Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = community_post_tag))] +pub struct CommunityPostTagInsertForm { + pub ap_id: DbUrl, + pub community_id: CommunityId, + pub name: String, + // default now + pub published: Option>, + pub updated: Option>, + pub deleted: Option>, +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = post_community_post_tag))] +pub struct PostCommunityPostTagInsertForm { + pub post_id: PostId, + pub community_post_tag_id: CommunityPostTagId, +} diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 73a1d8020c..efbab3da8c 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -10,6 +10,7 @@ pub mod comment_reply; pub mod comment_report; pub mod community; pub mod community_block; +pub mod community_post_tag; pub mod custom_emoji; pub mod custom_emoji_keyword; pub mod email_verification; @@ -39,7 +40,6 @@ pub mod registration_application; pub mod secret; pub mod site; pub mod tagline; -pub mod community_post_tag; /// Default value for columns like [community::Community.inbox_url] which are marked as serde(skip). /// diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 63bb7b1b7c..2b1ca95b56 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -28,6 +28,7 @@ use lemmy_db_schema::{ community_follower, community_moderator, community_person_ban, + community_post_tag, image_details, instance_block, local_user, @@ -37,17 +38,15 @@ use lemmy_db_schema::{ person_post_aggregates, post, post_aggregates, + post_community_post_tag, post_hide, post_like, post_read, post_saved, - post_community_post_tag, - community_post_tag }, source::{local_user::LocalUser, site::Site}, utils::{ functions::coalesce, - functions::json_agg, fuzzy_search, get_conn, limit_and_offset, @@ -217,14 +216,31 @@ fn queries<'a>() -> Queries< } else { Box::new(None::.into_sql::>()) }; - let community_post_tags: Box>> = - Box::new( - post_community_post_tag::table - .inner_join(community_post_tag::table) - .select(diesel::dsl::sql::("json_agg(community_post_tag.*)")) - .filter(post_community_post_tag::post_id.eq(post_aggregates::post_id)) - .single_value(), - ); + + // We fetch post tags by letting postgresql aggregate them internally in a subquery into JSON. + // This is a simple way to join m rows into n rows without duplicating the data and getting + // complex diesel types. In pure SQL you would usually do this either using a LEFT JOIN + then + // aggregating the results in the application code. But this results in a lot of duplicate + // data transferred (since each post will be returned once per tag that it has) and more + // complicated application code. The diesel docs suggest doing three separate sequential queries + // in this case (see https://diesel.rs/guides/relations.html#many-to-many-or-mn ): First fetch + // the posts, then fetch all relevant post-tag-association tuples from the db, and then fetch + // all the relevant tag objects. + // + // If we want to filter by post tag we will have to add + // separate logic below since this subquery can't affect filtering, but it is simple (`WHERE + // exists (select 1 from post_community_post_tags where community_post_tag_id in (1,2,3,4)`). + let community_post_tags: Box< + dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, + > = Box::new( + post_community_post_tag::table + .inner_join(community_post_tag::table) + .select(diesel::dsl::sql::( + "json_agg(community_post_tag.*)", + )) + .filter(post_community_post_tag::post_id.eq(post_aggregates::post_id)) + .single_value(), + ); query .inner_join(person::table) @@ -258,7 +274,7 @@ fn queries<'a>() -> Queries< post_aggregates::comments.nullable() - read_comments, post_aggregates::comments, ), - community_post_tags + community_post_tags, )) }; @@ -741,11 +757,12 @@ impl<'a> PostQuery<'a> { } } +#[allow(clippy::indexing_slicing)] #[cfg(test)] mod tests { use crate::{ post_view::{PaginationCursorData, PostQuery, PostView}, - structs::LocalUserView, + structs::{LocalUserView, PostCommunityPostTags}, }; use chrono::Utc; use lemmy_db_schema::{ @@ -765,6 +782,11 @@ mod tests { CommunityUpdateForm, }, community_block::{CommunityBlock, CommunityBlockForm}, + community_post_tag::{ + CommunityPostTag, + CommunityPostTagInsertForm, + PostCommunityPostTagInsertForm, + }, instance::Instance, instance_block::{InstanceBlock, InstanceBlockForm}, language::Language, @@ -790,6 +812,7 @@ mod tests { const POST_BY_BLOCKED_PERSON: &str = "post by blocked person"; const POST_BY_BOT: &str = "post by bot"; const POST: &str = "post"; + const POST_WITH_TAGS: &str = "post with tags"; fn names(post_views: &[PostView]) -> Vec<&str> { post_views.iter().map(|i| i.post.name.as_str()).collect() @@ -803,6 +826,9 @@ mod tests { inserted_community: Community, inserted_post: Post, inserted_bot_post: Post, + inserted_post_with_tags: Post, + tag_1: CommunityPostTag, + tag_2: CommunityPostTag, site: Site, } @@ -874,6 +900,36 @@ mod tests { PersonBlock::block(pool, &person_block).await?; + // Two community post tags + let tag_1 = CommunityPostTag::create( + pool, + &CommunityPostTagInsertForm { + ap_id: Url::parse(&format!("{}/tags/test_tag1", inserted_community.actor_id)) + .expect("valid") + .into(), + community_id: inserted_community.id, + name: "Test Tag 1".into(), + published: None, + updated: None, + deleted: None, + }, + ) + .await?; + let tag_2 = CommunityPostTag::create( + pool, + &CommunityPostTagInsertForm { + ap_id: Url::parse(&format!("{}/tags/test_tag2", inserted_community.actor_id)) + .expect("valid") + .into(), + community_id: inserted_community.id, + name: "Test Tag 2".into(), + published: None, + updated: None, + deleted: None, + }, + ) + .await?; + // A sample post let new_post = PostInsertForm::builder() .name(POST.to_string()) @@ -891,6 +947,28 @@ mod tests { .build(); let inserted_bot_post = Post::create(pool, &new_bot_post).await?; + + // A sample post with tags + let new_post = PostInsertForm::builder() + .name(POST_WITH_TAGS.to_string()) + .creator_id(inserted_person.id) + .community_id(inserted_community.id) + .language_id(Some(LanguageId(47))) + .build(); + + let inserted_post_with_tags = Post::create(pool, &new_post).await?; + let inserted_tags = vec![ + PostCommunityPostTagInsertForm { + post_id: inserted_post_with_tags.id, + community_post_tag_id: tag_1.id, + }, + PostCommunityPostTagInsertForm { + post_id: inserted_post_with_tags.id, + community_post_tag_id: tag_2.id, + }, + ]; + PostCommunityPostTagInsertForm::insert_tag_associations(pool, &inserted_tags).await?; + let local_user_view = LocalUserView { local_user: inserted_local_user, local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), @@ -930,6 +1008,9 @@ mod tests { inserted_community, inserted_post, inserted_bot_post, + inserted_post_with_tags, + tag_1, + tag_2, site, }) } @@ -948,12 +1029,14 @@ mod tests { LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; data.local_user_view.local_user.show_bot_accounts = false; - let read_post_listing = PostQuery { + let mut read_post_listing = PostQuery { community_id: Some(data.inserted_community.id), ..data.default_post_query() } .list(&data.site, pool) .await?; + // remove tags post + read_post_listing.remove(0); let post_listing_single_with_person = PostView::read( pool, @@ -990,7 +1073,10 @@ mod tests { .list(&data.site, pool) .await?; // should include bot post which has "undetermined" language - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_with_bots)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_with_bots) + ); cleanup(data, pool).await } @@ -1019,13 +1105,13 @@ mod tests { // Should be 2 posts, with the bot post, and the blocked assert_eq!( - vec![POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON], + vec![POST_WITH_TAGS, POST_BY_BOT, POST, POST_BY_BLOCKED_PERSON], names(&read_post_listing_multiple_no_person) ); assert_eq!( Some(&expected_post_listing_no_person), - read_post_listing_multiple_no_person.get(1) + read_post_listing_multiple_no_person.get(2) ); assert_eq!( expected_post_listing_no_person, @@ -1106,12 +1192,13 @@ mod tests { LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form).await?; data.local_user_view.local_user.show_bot_accounts = false; - let read_post_listing = PostQuery { + let mut read_post_listing = PostQuery { community_id: Some(data.inserted_community.id), ..data.default_post_query() } .list(&data.site, pool) .await?; + read_post_listing.remove(0); assert_eq!(vec![expected_post_with_upvote], read_post_listing); let like_removed = @@ -1196,6 +1283,7 @@ mod tests { .collect::>(); let expected_post_listing = vec![ + ("tegan".to_owned(), true, true), ("mybot".to_owned(), false, false), ("tegan".to_owned(), true, true), ]; @@ -1234,17 +1322,23 @@ mod tests { let post_listings_all = data.default_post_query().list(&data.site, pool).await?; // no language filters specified, all posts should be returned - assert_eq!(vec![EL_POSTO, POST_BY_BOT, POST], names(&post_listings_all)); + assert_eq!( + vec![EL_POSTO, POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_all) + ); LocalUserLanguage::update(pool, vec![french_id], data.local_user_view.local_user.id).await?; let post_listing_french = data.default_post_query().list(&data.site, pool).await?; // only one post in french and one undetermined should be returned - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listing_french)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listing_french) + ); assert_eq!( Some(french_id), - post_listing_french.get(1).map(|p| p.post.language_id) + post_listing_french.get(2).map(|p| p.post.language_id) ); LocalUserLanguage::update( @@ -1261,6 +1355,7 @@ mod tests { .map(|p| (p.post.name, p.post.language_id)) .collect::>(); let expected_post_listings_french_und = vec![ + (POST_WITH_TAGS.to_owned(), french_id), (POST_BY_BOT.to_owned(), UNDETERMINED_ID), (POST.to_owned(), french_id), ]; @@ -1291,7 +1386,7 @@ mod tests { // Make sure you don't see the removed post in the results let post_listings_no_admin = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST], names(&post_listings_no_admin)); + assert_eq!(vec![POST_WITH_TAGS, POST], names(&post_listings_no_admin)); // Removed bot post is shown to admins on its profile page data.local_user_view.local_user.admin = true; @@ -1376,7 +1471,12 @@ mod tests { // no instance block, should return all posts let post_listings_all = data.default_post_query().list(&data.site, pool).await?; assert_eq!( - vec![POST_FROM_BLOCKED_INSTANCE, POST_BY_BOT, POST], + vec![ + POST_FROM_BLOCKED_INSTANCE, + POST_WITH_TAGS, + POST_BY_BOT, + POST + ], names(&post_listings_all) ); @@ -1389,7 +1489,10 @@ mod tests { // now posts from communities on that instance should be hidden let post_listings_blocked = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_blocked)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_blocked) + ); assert!(post_listings_blocked .iter() .all(|p| p.post.id != post_from_blocked_instance.id)); @@ -1398,7 +1501,12 @@ mod tests { InstanceBlock::unblock(pool, &block_form).await?; let post_listings_blocked = data.default_post_query().list(&data.site, pool).await?; assert_eq!( - vec![POST_FROM_BLOCKED_INSTANCE, POST_BY_BOT, POST], + vec![ + POST_FROM_BLOCKED_INSTANCE, + POST_WITH_TAGS, + POST_BY_BOT, + POST + ], names(&post_listings_blocked) ); @@ -1539,7 +1647,7 @@ mod tests { // Make sure you don't see the read post in the results let post_listings_hide_read = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST], names(&post_listings_hide_read)); + assert_eq!(vec![POST_WITH_TAGS, POST], names(&post_listings_hide_read)); // Test with the show_read override as true let post_listings_show_read_true = PostQuery { @@ -1549,7 +1657,7 @@ mod tests { .list(&data.site, pool) .await?; assert_eq!( - vec![POST_BY_BOT, POST], + vec![POST_WITH_TAGS, POST_BY_BOT, POST], names(&post_listings_show_read_true) ); @@ -1560,7 +1668,10 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(vec![POST], names(&post_listings_show_read_false)); + assert_eq!( + vec![POST_WITH_TAGS, POST], + names(&post_listings_show_read_false) + ); cleanup(data, pool).await } @@ -1581,7 +1692,10 @@ mod tests { // Make sure you don't see the hidden post in the results let post_listings_hide_hidden = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST], names(&post_listings_hide_hidden)); + assert_eq!( + vec![POST_WITH_TAGS, POST], + names(&post_listings_hide_hidden) + ); // Make sure it does come back with the show_hidden option let post_listings_show_hidden = PostQuery { @@ -1592,15 +1706,13 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_hidden)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_show_hidden) + ); // Make sure that hidden field is true. - assert!( - &post_listings_show_hidden - .first() - .ok_or(LemmyErrorType::CouldntFindPost)? - .hidden - ); + assert!(&post_listings_show_hidden[1].hidden); cleanup(data, pool).await } @@ -1622,7 +1734,7 @@ mod tests { // Make sure you don't see the nsfw post in the regular results let post_listings_hide_nsfw = data.default_post_query().list(&data.site, pool).await?; - assert_eq!(vec![POST], names(&post_listings_hide_nsfw)); + assert_eq!(vec![POST_WITH_TAGS, POST], names(&post_listings_hide_nsfw)); // Make sure it does come back with the show_nsfw option let post_listings_show_nsfw = PostQuery { @@ -1633,16 +1745,13 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_nsfw)); + assert_eq!( + vec![POST_WITH_TAGS, POST_BY_BOT, POST], + names(&post_listings_show_nsfw) + ); // Make sure that nsfw field is true. - assert!( - &post_listings_show_nsfw - .first() - .ok_or(LemmyErrorType::CouldntFindPost)? - .post - .nsfw - ); + assert!(&post_listings_show_nsfw[1].post.nsfw); cleanup(data, pool).await } @@ -1776,7 +1885,7 @@ mod tests { hidden: false, saved: false, creator_blocked: false, - community_post_tags: None, + community_post_tags: PostCommunityPostTags::default(), }) } @@ -1810,7 +1919,7 @@ mod tests { } .list(&data.site, pool) .await?; - assert_eq!(2, authenticated_query.len()); + assert_eq!(3, authenticated_query.len()); let unauthenticated_post = PostView::read(pool, data.inserted_post.id, None, false).await?; assert!(unauthenticated_post.is_none()); @@ -1892,4 +2001,32 @@ mod tests { cleanup(data, pool).await } + + #[tokio::test] + #[serial] + async fn post_tags_present() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests().await; + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + let post_view = PostView::read( + pool, + data.inserted_post_with_tags.id, + Some(&data.local_user_view.local_user), + false, + ) + .await? + .ok_or(LemmyErrorType::CouldntFindPost)?; + + assert_eq!(2, post_view.community_post_tags.tags.len()); + assert_eq!(data.tag_1.name, post_view.community_post_tags.tags[0].name); + assert_eq!(data.tag_2.name, post_view.community_post_tags.tags[1].name); + + let all_posts = data.default_post_query().list(&data.site, pool).await?; + assert_eq!(2, all_posts[0].community_post_tags.tags.len()); // post with tags + assert_eq!(0, all_posts[1].community_post_tags.tags.len()); // bot post + assert_eq!(0, all_posts[2].community_post_tags.tags.len()); // normal post + + Ok(()) + } } diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 55badcd069..a5dfea88a8 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -1,9 +1,33 @@ #[cfg(feature = "full")] use diesel::Queryable; +use diesel::{ + deserialize::{FromSql, FromSqlRow}, + expression::AsExpression, + pg::{Pg, PgValue}, + serialize::ToSql, + sql_types::{self, Nullable}, +}; use lemmy_db_schema::{ aggregates::structs::{CommentAggregates, PersonAggregates, PostAggregates, SiteAggregates}, source::{ - comment::Comment, comment_report::CommentReport, community::Community, community_post_tag::CommunityPostTag, custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword, images::{ImageDetails, LocalImage}, local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_user::LocalUser, local_user_vote_display_mode::LocalUserVoteDisplayMode, person::Person, post::Post, post_report::PostReport, private_message::PrivateMessage, private_message_report::PrivateMessageReport, registration_application::RegistrationApplication, site::Site + comment::Comment, + comment_report::CommentReport, + community::Community, + community_post_tag::CommunityPostTag, + custom_emoji::CustomEmoji, + custom_emoji_keyword::CustomEmojiKeyword, + images::{ImageDetails, LocalImage}, + local_site::LocalSite, + local_site_rate_limit::LocalSiteRateLimit, + local_user::LocalUser, + local_user_vote_display_mode::LocalUserVoteDisplayMode, + person::Person, + post::Post, + post_report::PostReport, + private_message::PrivateMessage, + private_message_report::PrivateMessageReport, + registration_application::RegistrationApplication, + site::Site, }, SubscribedType, }; @@ -128,7 +152,7 @@ pub struct PostView { pub creator_blocked: bool, pub my_vote: Option, pub unread_comments: i64, - pub community_post_tags: Option + pub community_post_tags: PostCommunityPostTags, } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] @@ -213,3 +237,42 @@ pub struct LocalImageView { pub local_image: LocalImage, pub person: Person, } + +#[derive( + Clone, + serde::Serialize, + serde::Deserialize, + Debug, + PartialEq, + TS, + FromSqlRow, + AsExpression, + Default, +)] +#[serde(transparent)] +#[diesel(sql_type = Nullable)] +pub struct PostCommunityPostTags { + pub tags: Vec, +} + +impl FromSql, Pg> for PostCommunityPostTags { + fn from_sql(bytes: PgValue) -> diesel::deserialize::Result { + let value = >::from_sql(bytes)?; + Ok(serde_json::from_value::(value)?) + } + fn from_nullable_sql( + bytes: Option<::RawValue<'_>>, + ) -> diesel::deserialize::Result { + match bytes { + Some(bytes) => Self::from_sql(bytes), + None => Ok(Self { tags: vec![] }), + } + } +} + +impl ToSql, Pg> for PostCommunityPostTags { + fn to_sql(&self, out: &mut diesel::serialize::Output) -> diesel::serialize::Result { + let value = serde_json::to_value(self)?; + >::to_sql(&value, &mut out.reborrow()) + } +} diff --git a/migrations/2024-08-17-144959_community-post-tags/down.sql b/migrations/2024-08-17-144959_community-post-tags/down.sql index cbb0e9b817..74c7fde73c 100644 --- a/migrations/2024-08-17-144959_community-post-tags/down.sql +++ b/migrations/2024-08-17-144959_community-post-tags/down.sql @@ -1,3 +1,5 @@ -- This file should undo anything in `up.sql` -drop table post_community_post_tag; -drop table community_post_tag; \ No newline at end of file +DROP TABLE post_community_post_tag; + +DROP TABLE community_post_tag; + diff --git a/migrations/2024-08-17-144959_community-post-tags/up.sql b/migrations/2024-08-17-144959_community-post-tags/up.sql index fd1633a6df..09f83f4dd7 100644 --- a/migrations/2024-08-17-144959_community-post-tags/up.sql +++ b/migrations/2024-08-17-144959_community-post-tags/up.sql @@ -1,17 +1,18 @@ -- a tag for a post, valid in a community. created by mods of a community CREATE TABLE community_post_tag ( - id SERIAL PRIMARY KEY, - ap_id TEXT NOT NULL UNIQUE, - community_id INT NOT NULL REFERENCES community(id), - name TEXT NOT NULL, - published TIMESTAMPTZ NOT NULL, - updated TIMESTAMPTZ, - deleted TIMESTAMPTZ + id serial PRIMARY KEY, + ap_id text NOT NULL UNIQUE, + community_id int NOT NULL REFERENCES community (id) ON UPDATE CASCADE ON DELETE CASCADE, + name text NOT NULL, + published timestamptz NOT NULL DEFAULT now(), + updated timestamptz, + deleted timestamptz ); -- an association between a post and a community post tag. created/updated by the post author or mods of a community CREATE TABLE post_community_post_tag ( - post_id INT NOT NULL REFERENCES post(id), - community_post_tag_id INT NOT NULL REFERENCES community_post_tag(id), - PRIMARY KEY (post_id, community_post_tag_id) + post_id int NOT NULL REFERENCES post (id) ON UPDATE CASCADE ON DELETE CASCADE, + community_post_tag_id int NOT NULL REFERENCES community_post_tag (id) ON UPDATE CASCADE ON DELETE CASCADE, + PRIMARY KEY (post_id, community_post_tag_id) ); + diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 673cb055e2..552e188b34 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -183,14 +183,14 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("/follow", web::post().to(follow_community)) .route("/block", web::post().to(block_community)) .route("/delete", web::post().to(delete_community)) - .route("/post_tags", web::get().to(get_community_post_tags)) + // .route("/post_tags", web::get().to(get_community_post_tags)) // Mod Actions + // .route("/post_tags", web::post().to(create_update_community_post_tag)) + // .route("/post_tags/delete", web::post().to(delete_community_post_tag)), .route("/remove", web::post().to(remove_community)) .route("/transfer", web::post().to(transfer_community)) .route("/ban_user", web::post().to(ban_from_community)) - .route("/mod", web::post().to(add_mod_to_community)) - .route("/post_tags", web::post().to(create_update_community_post_tag)) - .route("/post_tags/delete", web::post().to(delete_community_post_tag)), + .route("/mod", web::post().to(add_mod_to_community)), ) .service( web::scope("/federated_instances") From 4d3b4ba283aa002e3714f913c4e744927b5cbb22 Mon Sep 17 00:00:00 2001 From: phiresky Date: Mon, 9 Sep 2024 19:59:17 +0200 Subject: [PATCH 3/3] fix lints --- .../src/source/community_post_tag.rs | 8 ++-- .../db_views/src/community_post_tags_view.rs | 29 ++++++++++++ crates/db_views/src/lib.rs | 2 + crates/db_views/src/structs.rs | 46 ++----------------- 4 files changed, 40 insertions(+), 45 deletions(-) create mode 100644 crates/db_views/src/community_post_tags_view.rs diff --git a/crates/db_schema/src/source/community_post_tag.rs b/crates/db_schema/src/source/community_post_tag.rs index 8b094b2f63..6feeacd17d 100644 --- a/crates/db_schema/src/source/community_post_tag.rs +++ b/crates/db_schema/src/source/community_post_tag.rs @@ -1,10 +1,10 @@ -use crate::{ - newtypes::{CommunityId, CommunityPostTagId, DbUrl, PostId}, - schema::{community_post_tag, post_community_post_tag}, -}; +use crate::newtypes::{CommunityId, CommunityPostTagId, DbUrl, PostId}; +#[cfg(feature = "full")] +use crate::schema::{community_post_tag, post_community_post_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. diff --git a/crates/db_views/src/community_post_tags_view.rs b/crates/db_views/src/community_post_tags_view.rs new file mode 100644 index 0000000000..43c0ee9136 --- /dev/null +++ b/crates/db_views/src/community_post_tags_view.rs @@ -0,0 +1,29 @@ +use crate::structs::PostCommunityPostTags; +use diesel::{ + deserialize::FromSql, + pg::{Pg, PgValue}, + serialize::ToSql, + sql_types::{self, Nullable}, +}; + +impl FromSql, Pg> for PostCommunityPostTags { + fn from_sql(bytes: PgValue) -> diesel::deserialize::Result { + let value = >::from_sql(bytes)?; + Ok(serde_json::from_value::(value)?) + } + fn from_nullable_sql( + bytes: Option<::RawValue<'_>>, + ) -> diesel::deserialize::Result { + match bytes { + Some(bytes) => Self::from_sql(bytes), + None => Ok(Self { tags: vec![] }), + } + } +} + +impl ToSql, Pg> for PostCommunityPostTags { + fn to_sql(&self, out: &mut diesel::serialize::Output) -> diesel::serialize::Result { + let value = serde_json::to_value(self)?; + >::to_sql(&value, &mut out.reborrow()) + } +} diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index e93c7409d1..7c0a071e75 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -6,6 +6,8 @@ pub mod comment_report_view; #[cfg(feature = "full")] pub mod comment_view; #[cfg(feature = "full")] +pub mod community_post_tags_view; +#[cfg(feature = "full")] pub mod custom_emoji_view; #[cfg(feature = "full")] pub mod local_image_view; diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index a5dfea88a8..5ad3981a89 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -1,12 +1,7 @@ #[cfg(feature = "full")] use diesel::Queryable; -use diesel::{ - deserialize::{FromSql, FromSqlRow}, - expression::AsExpression, - pg::{Pg, PgValue}, - serialize::ToSql, - sql_types::{self, Nullable}, -}; +#[cfg(feature = "full")] +use diesel::{deserialize::FromSqlRow, expression::AsExpression, sql_types}; use lemmy_db_schema::{ aggregates::structs::{CommentAggregates, PersonAggregates, PostAggregates, SiteAggregates}, source::{ @@ -238,41 +233,10 @@ pub struct LocalImageView { pub person: Person, } -#[derive( - Clone, - serde::Serialize, - serde::Deserialize, - Debug, - PartialEq, - TS, - FromSqlRow, - AsExpression, - Default, -)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq, Default)] +#[cfg_attr(feature = "full", derive(TS, FromSqlRow, AsExpression))] #[serde(transparent)] -#[diesel(sql_type = Nullable)] +#[cfg_attr(feature = "full", diesel(sql_type = Nullable))] pub struct PostCommunityPostTags { pub tags: Vec, } - -impl FromSql, Pg> for PostCommunityPostTags { - fn from_sql(bytes: PgValue) -> diesel::deserialize::Result { - let value = >::from_sql(bytes)?; - Ok(serde_json::from_value::(value)?) - } - fn from_nullable_sql( - bytes: Option<::RawValue<'_>>, - ) -> diesel::deserialize::Result { - match bytes { - Some(bytes) => Self::from_sql(bytes), - None => Ok(Self { tags: vec![] }), - } - } -} - -impl ToSql, Pg> for PostCommunityPostTags { - fn to_sql(&self, out: &mut diesel::serialize::Output) -> diesel::serialize::Result { - let value = serde_json::to_value(self)?; - >::to_sql(&value, &mut out.reborrow()) - } -}