first commit 2
This commit is contained in:
218
database/migrations/001_init.sql
Normal file
218
database/migrations/001_init.sql
Normal file
@@ -0,0 +1,218 @@
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE TYPE content_type AS ENUM ('news', 'article', 'video', 'audio', 'graphic', 'event_announcement');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE TYPE content_status AS ENUM ('draft', 'moderation', 'review', 'published', 'archived');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE TYPE content_visibility AS ENUM ('public', 'authenticated', 'role_restricted');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE TYPE subscription_type AS ENUM ('category', 'tag', 'speaker');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE TYPE comment_status AS ENUM ('visible', 'moderation', 'hidden');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (code IN ('administrator', 'editor', 'manager', 'user'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (role_id, permission_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
login TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
email TEXT UNIQUE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE RESTRICT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, role_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS speakers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
display_name TEXT NOT NULL,
|
||||
role_description TEXT,
|
||||
biography TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title TEXT NOT NULL UNIQUE,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title TEXT NOT NULL UNIQUE,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS content_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
title TEXT NOT NULL,
|
||||
lead TEXT,
|
||||
body TEXT,
|
||||
content_type content_type NOT NULL,
|
||||
status content_status NOT NULL DEFAULT 'draft',
|
||||
visibility content_visibility NOT NULL DEFAULT 'public',
|
||||
category_id UUID REFERENCES categories(id) ON DELETE SET NULL,
|
||||
author_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
author_label TEXT,
|
||||
speaker_id UUID REFERENCES speakers(id) ON DELETE SET NULL,
|
||||
published_at TIMESTAMPTZ,
|
||||
archived_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS content_tags (
|
||||
content_id UUID NOT NULL REFERENCES content_items(id) ON DELETE CASCADE,
|
||||
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (content_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS media_files (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
content_id UUID REFERENCES content_items(id) ON DELETE CASCADE,
|
||||
uploaded_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
size_bytes BIGINT NOT NULL CHECK (size_bytes >= 0),
|
||||
storage_key TEXT NOT NULL UNIQUE,
|
||||
public_url TEXT,
|
||||
checksum TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subscriptions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
subscription_type subscription_type NOT NULL,
|
||||
category_id UUID REFERENCES categories(id) ON DELETE CASCADE,
|
||||
tag_id UUID REFERENCES tags(id) ON DELETE CASCADE,
|
||||
speaker_id UUID REFERENCES speakers(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CHECK (
|
||||
(subscription_type = 'category' AND category_id IS NOT NULL AND tag_id IS NULL AND speaker_id IS NULL) OR
|
||||
(subscription_type = 'tag' AND tag_id IS NOT NULL AND category_id IS NULL AND speaker_id IS NULL) OR
|
||||
(subscription_type = 'speaker' AND speaker_id IS NOT NULL AND category_id IS NULL AND tag_id IS NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS subscriptions_unique_category
|
||||
ON subscriptions(user_id, category_id)
|
||||
WHERE subscription_type = 'category';
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS subscriptions_unique_tag
|
||||
ON subscriptions(user_id, tag_id)
|
||||
WHERE subscription_type = 'tag';
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS subscriptions_unique_speaker
|
||||
ON subscriptions(user_id, speaker_id)
|
||||
WHERE subscription_type = 'speaker';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
content_id UUID NOT NULL REFERENCES content_items(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
body TEXT NOT NULL,
|
||||
status comment_status NOT NULL DEFAULT 'visible',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
body TEXT,
|
||||
is_read BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
content_id UUID REFERENCES content_items(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
read_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS action_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
action TEXT NOT NULL,
|
||||
entity_type TEXT NOT NULL,
|
||||
entity_id UUID,
|
||||
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
request_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS content_views (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
content_id UUID NOT NULL REFERENCES content_items(id) ON DELETE CASCADE,
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
anonymous_key TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS users_login_idx ON users(login);
|
||||
CREATE INDEX IF NOT EXISTS content_items_type_status_idx ON content_items(content_type, status);
|
||||
CREATE INDEX IF NOT EXISTS content_items_category_idx ON content_items(category_id);
|
||||
CREATE INDEX IF NOT EXISTS content_items_published_at_idx ON content_items(published_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS content_items_search_idx ON content_items USING GIN (to_tsvector('russian', coalesce(title, '') || ' ' || coalesce(lead, '') || ' ' || coalesce(body, '')));
|
||||
CREATE INDEX IF NOT EXISTS comments_content_id_idx ON comments(content_id);
|
||||
CREATE INDEX IF NOT EXISTS notifications_user_read_idx ON notifications(user_id, is_read);
|
||||
CREATE INDEX IF NOT EXISTS action_logs_actor_created_idx ON action_logs(actor_user_id, created_at DESC);
|
||||
|
||||
INSERT INTO roles (code, name, description) VALUES
|
||||
('administrator', 'Администратор', 'Расширенное управление системой, пользователями, ролями и настройками'),
|
||||
('editor', 'Редактор', 'Создание и редактирование материалов, участие в модерации'),
|
||||
('manager', 'Менеджер', 'Публикация, управление контентом и подписками'),
|
||||
('user', 'Пользователь', 'Просмотр материалов, комментарии и подписки')
|
||||
ON CONFLICT (code) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
description = EXCLUDED.description;
|
||||
Reference in New Issue
Block a user