146 lines
7.2 KiB
SQL
146 lines
7.2 KiB
SQL
-- 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();
|