diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 6442a1599..44e98424f 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, CommunityPostTagId, DbUrl, LanguageId, PostId, PostReportId}, + newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId, TagId}, ListingType, PostFeatureType, PostSortType, @@ -37,7 +37,7 @@ pub struct CreatePost { /// Instead of fetching a thumbnail, use a custom one. #[cfg_attr(feature = "full", ts(optional))] pub custom_thumbnail: Option, - pub community_post_tags: Option>, + pub tags: Option>, /// Time when this post should be scheduled. Null means publish immediately. #[cfg_attr(feature = "full", ts(optional))] pub scheduled_publish_time: Option, @@ -165,7 +165,7 @@ pub struct EditPost { /// Instead of fetching a thumbnail, use a custom one. #[cfg_attr(feature = "full", ts(optional))] pub custom_thumbnail: Option, - pub community_post_tags: Option>, + pub tags: Option>, /// Time when this post should be scheduled. Null means publish immediately. #[cfg_attr(feature = "full", ts(optional))] pub scheduled_publish_time: Option, diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index 7f3b231dd..2d7a16c2c 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -6,7 +6,6 @@ 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; @@ -36,4 +35,5 @@ pub mod private_message_report; pub mod registration_application; pub mod secret; pub mod site; +pub mod tag; pub mod tagline; diff --git a/crates/db_schema/src/impls/community_post_tag.rs b/crates/db_schema/src/impls/tag.rs similarity index 57% rename from crates/db_schema/src/impls/community_post_tag.rs rename to crates/db_schema/src/impls/tag.rs index 7d407e98e..f15db52b6 100644 --- a/crates/db_schema/src/impls/community_post_tag.rs +++ b/crates/db_schema/src/impls/tag.rs @@ -1,10 +1,12 @@ use crate::{ - newtypes::CommunityPostTagId, - schema::{community_post_tag, post_community_post_tag}, + newtypes::{CommunityId, TagId}, + schema::{community_post_tag, post_tag, tag}, source::community_post_tag::{ CommunityPostTag, CommunityPostTagInsertForm, - PostCommunityPostTagInsertForm, + PostTagInsertForm, + Tag, + TagInsertForm, }, traits::Crud, utils::{get_conn, DbPool}, @@ -14,13 +16,42 @@ 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 { + let conn = &mut get_conn(pool).await?; + insert_into(tag::table) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + pid: TagId, + form: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(tag::table.find(pid)) + .set(form) + .get_result::(conn) + .await + } +} + #[async_trait] impl Crud for CommunityPostTag { type InsertForm = CommunityPostTagInsertForm; type UpdateForm = CommunityPostTagInsertForm; - type IdType = CommunityPostTagId; + type IdType = (CommunityId, TagId); async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; @@ -32,7 +63,7 @@ impl Crud for CommunityPostTag { async fn update( pool: &mut DbPool<'_>, - pid: CommunityPostTagId, + pid: (CommunityId, TagId), form: &Self::UpdateForm, ) -> Result { let conn = &mut get_conn(pool).await?; @@ -43,13 +74,13 @@ impl Crud for CommunityPostTag { } } -impl PostCommunityPostTagInsertForm { +impl PostTagInsertForm { pub async fn insert_tag_associations( pool: &mut DbPool<'_>, - tags: &[PostCommunityPostTagInsertForm], + tags: &[PostTagInsertForm], ) -> LemmyResult<()> { let conn = &mut get_conn(pool).await?; - insert_into(post_community_post_tag::table) + insert_into(post_tag::table) .values(tags) .execute(conn) .await diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index fa9ff1597..963f847a5 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -287,5 +287,5 @@ 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); +/// The internal tag id. +pub struct TagId(pub i32); diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index ed9297ec3..ca1aa1a21 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -264,14 +264,10 @@ diesel::table! { } diesel::table! { - community_post_tag (id) { - id -> Int4, - ap_id -> Text, + community_post_tag (community_id, tag_id) { community_id -> Int4, - name -> Text, + tag_id -> Int4, published -> Timestamptz, - updated -> Nullable, - deleted -> Nullable, } } @@ -820,13 +816,6 @@ 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_report (id) { id -> Int4, @@ -844,6 +833,14 @@ diesel::table! { } } +diesel::table! { + post_tag (post_id, tag_id) { + post_id -> Int4, + tag_id -> Int4, + published -> Timestamptz, + } +} + diesel::table! { private_message (id) { id -> Int4, @@ -969,6 +966,17 @@ diesel::table! { } } +diesel::table! { + tag (id) { + id -> Int4, + ap_id -> Text, + name -> Text, + published -> Timestamptz, + updated -> Nullable, + deleted -> Nullable, + } +} + diesel::table! { tagline (id) { id -> Int4, @@ -1003,6 +1011,7 @@ diesel::joinable!(community_aggregates -> community (community_id)); diesel::joinable!(community_language -> community (community_id)); diesel::joinable!(community_language -> language (language_id)); diesel::joinable!(community_post_tag -> community (community_id)); +diesel::joinable!(community_post_tag -> tag (tag_id)); diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id)); diesel::joinable!(email_verification -> local_user (local_user_id)); diesel::joinable!(federation_allowlist -> instance (instance_id)); @@ -1050,9 +1059,9 @@ 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_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!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); @@ -1119,8 +1128,8 @@ diesel::allow_tables_to_appear_in_same_query!( post, post_actions, post_aggregates, - post_community_post_tag, post_report, + post_tag, private_message, private_message_report, received_activity, @@ -1131,5 +1140,6 @@ diesel::allow_tables_to_appear_in_same_query!( site, site_aggregates, site_language, + tag, tagline, ); diff --git a/crates/db_schema/src/source/community_post_tag.rs b/crates/db_schema/src/source/community_post_tag.rs index 6feeacd17..fbe90ff3c 100644 --- a/crates/db_schema/src/source/community_post_tag.rs +++ b/crates/db_schema/src/source/community_post_tag.rs @@ -1,6 +1,6 @@ -use crate::newtypes::{CommunityId, CommunityPostTagId, DbUrl, PostId}; +use crate::newtypes::{CommunityId, DbUrl, PostId, TagId}; #[cfg(feature = "full")] -use crate::schema::{community_post_tag, post_community_post_tag}; +use crate::schema::{community_post_tag, post_tag, tag}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -13,25 +13,36 @@ use ts_rs::TS; #[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 = community_post_tag))] +#[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 CommunityPostTag { - pub id: CommunityPostTagId, +pub struct Tag { + pub id: TagId, pub ap_id: DbUrl, - pub community_id: CommunityId, pub name: String, pub published: DateTime, pub updated: Option>, pub deleted: Option>, } +#[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 = community_post_tag))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", diesel(primary_key(community_id, tag_id)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct CommunityPostTag { + pub community_id: CommunityId, + pub tag_id: TagId, + pub published: DateTime, +} + #[derive(Debug, Clone)] #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = community_post_tag))] -pub struct CommunityPostTagInsertForm { +#[cfg_attr(feature = "full", diesel(table_name = tag))] +pub struct TagInsertForm { pub ap_id: DbUrl, - pub community_id: CommunityId, pub name: String, // default now pub published: Option>, @@ -41,8 +52,17 @@ pub struct CommunityPostTagInsertForm { #[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, +#[cfg_attr(feature = "full", diesel(table_name = community_post_tag))] +pub struct CommunityPostTagInsertForm { + pub community_id: CommunityId, + pub tag_id: TagId, + pub published: Option>, +} + +#[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, } diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 0244b9b05..ed62cabe4 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -5,7 +5,9 @@ use diesel::{ pg::Pg, query_builder::AsQuery, result::Error, + sql_types, BoolExpressionMethods, + BoxableExpression, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, @@ -23,7 +25,6 @@ use lemmy_db_schema::{ schema::{ community, community_actions, - community_post_tag, image_details, instance_actions, local_user, @@ -33,7 +34,8 @@ use lemmy_db_schema::{ post, post_actions, post_aggregates, - post_community_post_tag, + post_tag, + tag, }, source::{ community::{CommunityFollower, CommunityFollowerState}, @@ -82,82 +84,7 @@ fn queries<'a>() -> Queries< // TODO maybe this should go to localuser also let all_joins = move |query: post_aggregates::BoxedQuery<'a, Pg>, my_person_id: Option| { - let is_local_user_banned_from_community_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>, - > = if let Some(person_id) = my_person_id { - Box::new(is_local_user_banned_from_community(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_read_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_read(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_hidden_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_hidden(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let is_creator_blocked_selection: Box> = - if let Some(person_id) = my_person_id { - Box::new(is_creator_blocked(person_id)) - } else { - Box::new(false.into_sql::()) - }; - - let subscribed_type_selection: Box< - dyn BoxableExpression< - _, - Pg, - SqlType = sql_types::Nullable, - >, - > = if let Some(person_id) = my_person_id { - Box::new( - community_follower::table - .filter( - post_aggregates::community_id - .eq(community_follower::community_id) - .and(community_follower::person_id.eq(person_id)), - ) - .select(CommunityFollower::select_subscribed_type()) - .single_value(), - ) - } else { - Box::new(None::.into_sql::>()) - }; - - let score_selection: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new(score(person_id)) - } else { - Box::new(None::.into_sql::>()) - }; - - let read_comments: Box< - dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, - > = if let Some(person_id) = my_person_id { - Box::new( - person_post_aggregates::table - .filter( - post_aggregates::post_id - .eq(person_post_aggregates::post_id) - .and(person_post_aggregates::person_id.eq(person_id)), - ) - .select(person_post_aggregates::read_comments.nullable()) - .single_value(), - ) - } else { - Box::new(None::.into_sql::>()) - }; - - // We fetch post tags by letting postgresql aggregate them internally in a subquery into JSON. + // 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 @@ -170,15 +97,15 @@ fn queries<'a>() -> Queries< // 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< + let post_tags: Box< dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable>, > = Box::new( - post_community_post_tag::table - .inner_join(community_post_tag::table) + post_tag::table + .inner_join(tag::table) .select(diesel::dsl::sql::( - "json_agg(community_post_tag.*)", + "json_agg(tag.*)", )) - .filter(post_community_post_tag::post_id.eq(post_aggregates::post_id)) + .filter(post_tag::post_id.eq(post_aggregates::post_id)) .single_value(), ); query @@ -237,7 +164,7 @@ fn queries<'a>() -> Queries< post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), post_aggregates::comments, ), - community_post_tags, + post_tags, )) }; @@ -737,7 +664,9 @@ mod tests { community_post_tag::{ CommunityPostTag, CommunityPostTagInsertForm, - PostCommunityPostTagInsertForm, + PostTagInsertForm, + Tag, + TagInsertForm, }, instance::Instance, instance_block::{InstanceBlock, InstanceBlockForm}, @@ -766,7 +695,7 @@ mod tests { PostSortType, SubscribedType, }; - use lemmy_utils::error::LemmyResult; + use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use pretty_assertions::assert_eq; use serial_test::serial; use std::time::{Duration, Instant}; @@ -791,8 +720,8 @@ mod tests { inserted_post: Post, inserted_bot_post: Post, inserted_post_with_tags: Post, - tag_1: CommunityPostTag, - tag_2: CommunityPostTag, + tag_1: Tag, + tag_2: Tag, site: Site, } @@ -865,13 +794,12 @@ mod tests { PersonBlock::block(pool, &person_block).await?; // Two community post tags - let tag_1 = CommunityPostTag::create( + let tag_1 = Tag::create( pool, - &CommunityPostTagInsertForm { + &TagInsertForm { 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, @@ -879,13 +807,21 @@ mod tests { }, ) .await?; - let tag_2 = CommunityPostTag::create( + CommunityPostTag::create( pool, &CommunityPostTagInsertForm { + community_id: inserted_community.id, + tag_id: tag_1.id, + published: None, + }, + ) + .await?; + let tag_2 = Tag::create( + pool, + &TagInsertForm { 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, @@ -893,14 +829,21 @@ mod tests { }, ) .await?; + CommunityPostTag::create( + pool, + &CommunityPostTagInsertForm { + community_id: inserted_community.id, + tag_id: tag_2.id, + published: None, + }, + ) + .await?; // A sample post - let new_post = PostInsertForm::builder() - .name(POST.to_string()) - .creator_id(inserted_person.id) - .community_id(inserted_community.id) - .language_id(Some(LanguageId(47))) - .build(); + let new_post = PostInsertForm { + language_id: Some(LanguageId(47)), + ..PostInsertForm::new(POST.to_string(), inserted_person.id, inserted_community.id) + }; let inserted_post = Post::create(pool, &new_post).await?; @@ -912,26 +855,27 @@ mod tests { 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 new_post = PostInsertForm { + language_id: Some(LanguageId(47)), + ..PostInsertForm::new( + POST_WITH_TAGS.to_string(), + inserted_person.id, + inserted_community.id, + ) + }; let inserted_post_with_tags = Post::create(pool, &new_post).await?; let inserted_tags = vec![ - PostCommunityPostTagInsertForm { + PostTagInsertForm { post_id: inserted_post_with_tags.id, - community_post_tag_id: tag_1.id, + tag_id: tag_1.id, }, - PostCommunityPostTagInsertForm { + PostTagInsertForm { post_id: inserted_post_with_tags.id, - community_post_tag_id: tag_2.id, + tag_id: tag_2.id, }, ]; - PostCommunityPostTagInsertForm::insert_tag_associations(pool, &inserted_tags).await?; - + PostTagInsertForm::insert_tag_associations(pool, &inserted_tags).await?; let local_user_view = LocalUserView { local_user: inserted_local_user, @@ -1782,7 +1726,7 @@ mod tests { ); // Make sure that hidden field is true. - assert!(&post_listings_show_hidden.at(1).is_some_and(|p| p.hidden)); + assert!(&post_listings_show_hidden.get(1).is_some_and(|p| p.hidden)); cleanup(data, pool).await } @@ -1824,7 +1768,7 @@ mod tests { assert!( &post_listings_show_nsfw .first() - .ok_or(LemmyErrorType::CouldntFindPost)? + .ok_or(LemmyErrorType::NotFound)? .post .nsfw ); @@ -2262,7 +2206,7 @@ mod tests { #[tokio::test] #[serial] async fn post_tags_present() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests().await; + let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); let data = init_data(pool).await?; @@ -2272,8 +2216,7 @@ mod tests { Some(&data.local_user_view.local_user), false, ) - .await? - .ok_or(LemmyErrorType::CouldntFindPost)?; + .await?; assert_eq!(2, post_view.community_post_tags.tags.len()); assert_eq!(data.tag_1.name, post_view.community_post_tags.tags[0].name); diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index a0d18d0c1..deb819734 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -8,7 +8,7 @@ use lemmy_db_schema::{ comment::Comment, comment_report::CommentReport, community::Community, - community_post_tag::CommunityPostTag, + community_post_tag::Tag, custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword, images::{ImageDetails, LocalImage}, @@ -247,5 +247,5 @@ pub struct LocalImageView { #[serde(transparent)] #[cfg_attr(feature = "full", diesel(sql_type = Nullable))] pub struct PostCommunityPostTags { - pub tags: Vec, + pub tags: Vec, } diff --git a/migrations/2024-08-17-144959_community-post-tags/up.sql b/migrations/2024-08-17-144959_community-post-tags/up.sql deleted file mode 100644 index 09f83f4dd..000000000 --- a/migrations/2024-08-17-144959_community-post-tags/up.sql +++ /dev/null @@ -1,18 +0,0 @@ --- 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) 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) 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/migrations/2024-08-17-144959_community-post-tags/down.sql b/migrations/2024-12-17-144959_community-post-tags/down.sql similarity index 67% rename from migrations/2024-08-17-144959_community-post-tags/down.sql rename to migrations/2024-12-17-144959_community-post-tags/down.sql index 74c7fde73..d480ff4f9 100644 --- a/migrations/2024-08-17-144959_community-post-tags/down.sql +++ b/migrations/2024-12-17-144959_community-post-tags/down.sql @@ -1,5 +1,7 @@ -- This file should undo anything in `up.sql` -DROP TABLE post_community_post_tag; +DROP TABLE post_tag; DROP TABLE community_post_tag; +DROP TABLE tag; + diff --git a/migrations/2024-12-17-144959_community-post-tags/up.sql b/migrations/2024-12-17-144959_community-post-tags/up.sql new file mode 100644 index 000000000..3c91c746d --- /dev/null +++ b/migrations/2024-12-17-144959_community-post-tags/up.sql @@ -0,0 +1,26 @@ +-- a tag for a post, valid in a community. created by mods of a community +CREATE TABLE tag ( + id serial PRIMARY KEY, + ap_id text NOT NULL UNIQUE, + name text NOT NULL, + published timestamptz NOT NULL DEFAULT now(), + updated timestamptz, + deleted timestamptz +); + +-- indicates this tag was created by the mod of a community and can be applied to posts in this community +CREATE TABLE community_post_tag ( + community_id int NOT NULL REFERENCES community (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 (community_id, tag_id) +); + +-- 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) +); +