-- 011: Announcement Channel — articles, comments, reactions, follows, cross-posts -- -- New tables: -- article, article_comment, article_reaction, -- channel_follow, article_cross_post -- ============================================================ -- 1. Articles -- ============================================================ -- models/channels/articles.rs → article CREATE TABLE IF NOT EXISTS article ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, author_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, title TEXT NOT NULL, slug TEXT NOT NULL, summary TEXT NULL, body TEXT NOT NULL, cover_image_url TEXT NULL, status TEXT NOT NULL, visibility TEXT NOT NULL, tags TEXT[] NOT NULL, published_at TIMESTAMPTZ NULL, published_by UUID NULL REFERENCES "user"(id) ON DELETE SET NULL, scheduled_at TIMESTAMPTZ NULL, unpublished_at TIMESTAMPTZ NULL, views_count BIGINT NOT NULL DEFAULT 0, comments_count BIGINT NOT NULL DEFAULT 0, reactions_count BIGINT NOT NULL DEFAULT 0, cross_posted BOOLEAN NOT NULL DEFAULT FALSE, cross_posted_from UUID NULL REFERENCES article(id) ON DELETE SET NULL, metadata JSONB NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, deleted_at TIMESTAMPTZ NULL, CONSTRAINT uq_article_channel_slug UNIQUE (channel_id, slug) ); CREATE INDEX IF NOT EXISTS idx_article_channel_id ON article (channel_id); CREATE INDEX IF NOT EXISTS idx_article_author_id ON article (author_id); CREATE INDEX IF NOT EXISTS idx_article_status ON article (status); CREATE INDEX IF NOT EXISTS idx_article_published_at ON article (published_at DESC) WHERE published_at IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_article_cross_posted_from ON article (cross_posted_from) WHERE cross_posted_from IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_article_deleted ON article (deleted_at) WHERE deleted_at IS NOT NULL; -- ============================================================ -- 2. Article Comments -- ============================================================ -- models/channels/article_comments.rs → article_comment CREATE TABLE IF NOT EXISTS article_comment ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES article(id) ON DELETE CASCADE, channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, author_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, parent_comment_id UUID NULL REFERENCES article_comment(id) ON DELETE CASCADE, body TEXT NOT NULL, edited_at TIMESTAMPTZ NULL, deleted_at TIMESTAMPTZ NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_article_comment_article_id ON article_comment (article_id); CREATE INDEX IF NOT EXISTS idx_article_comment_author_id ON article_comment (author_id); CREATE INDEX IF NOT EXISTS idx_article_comment_parent ON article_comment (parent_comment_id) WHERE parent_comment_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_article_comment_deleted ON article_comment (deleted_at) WHERE deleted_at IS NOT NULL; -- ============================================================ -- 3. Article Reactions -- ============================================================ -- models/channels/article_reactions.rs → article_reaction CREATE TABLE IF NOT EXISTS article_reaction ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES article(id) ON DELETE CASCADE, channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, content TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL, CONSTRAINT uq_article_reaction UNIQUE (article_id, user_id, content) ); CREATE INDEX IF NOT EXISTS idx_article_reaction_article_id ON article_reaction (article_id); CREATE INDEX IF NOT EXISTS idx_article_reaction_user_id ON article_reaction (user_id); -- ============================================================ -- 4. Channel Follows -- ============================================================ -- models/channels/channel_follows.rs → channel_follow CREATE TABLE IF NOT EXISTS channel_follow ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), source_channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE, target_workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, target_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL, webhook_url TEXT NULL, webhook_secret_ciphertext TEXT NULL, enabled BOOLEAN NOT NULL, followed_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE, unfollowed_at TIMESTAMPTZ NULL, last_delivery_at TIMESTAMPTZ NULL, last_delivery_status TEXT NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, CONSTRAINT uq_channel_follow_source_target UNIQUE (source_channel_id, target_workspace_id, target_channel_id) ); CREATE INDEX IF NOT EXISTS idx_channel_follow_source ON channel_follow (source_channel_id); CREATE INDEX IF NOT EXISTS idx_channel_follow_target_ws ON channel_follow (target_workspace_id); CREATE INDEX IF NOT EXISTS idx_channel_follow_target_channel ON channel_follow (target_channel_id) WHERE target_channel_id IS NOT NULL; -- ============================================================ -- 5. Article Cross-Posts -- ============================================================ -- models/channels/article_cross_posts.rs → article_cross_post CREATE TABLE IF NOT EXISTS article_cross_post ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES article(id) ON DELETE CASCADE, follow_id UUID NOT NULL REFERENCES channel_follow(id) ON DELETE CASCADE, target_workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE, target_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL, status TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, last_error TEXT NULL, sent_at TIMESTAMPTZ NULL, delivered_at TIMESTAMPTZ NULL, failed_at TIMESTAMPTZ NULL, created_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_article_cross_post_article_id ON article_cross_post (article_id); CREATE INDEX IF NOT EXISTS idx_article_cross_post_follow_id ON article_cross_post (follow_id); CREATE INDEX IF NOT EXISTS idx_article_cross_post_status ON article_cross_post (status); CREATE INDEX IF NOT EXISTS idx_article_cross_post_target_ws ON article_cross_post (target_workspace_id); -- ============================================================ -- 6. Triggers — auto-refresh updated_at -- ============================================================ DROP TRIGGER IF EXISTS trg_article_updated_at ON article; CREATE TRIGGER trg_article_updated_at BEFORE UPDATE ON article FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_article_comment_updated_at ON article_comment; CREATE TRIGGER trg_article_comment_updated_at BEFORE UPDATE ON article_comment FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_channel_follow_updated_at ON channel_follow; CREATE TRIGGER trg_channel_follow_updated_at BEFORE UPDATE ON channel_follow FOR EACH ROW EXECUTE FUNCTION set_updated_at();