mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-11 22:30:59 +04:30
NestJS backend with Prisma, Docker Compose for Postgres/Redis, and deploy docs for the production VM.
325 lines
14 KiB
SQL
325 lines
14 KiB
SQL
-- Meshkee CMS — RBAC, content tables, media
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- enums
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TYPE content_status AS ENUM ('draft', 'published', 'archived');
|
|
CREATE TYPE media_type AS ENUM ('image', 'video');
|
|
CREATE TYPE media_entity_type AS ENUM ('product', 'blog', 'portfolio');
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- permissions (RBAC)
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE permissions (
|
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
slug VARCHAR(100) NOT NULL,
|
|
group_name VARCHAR(50) NOT NULL,
|
|
description TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT permissions_slug_unique UNIQUE (slug)
|
|
);
|
|
|
|
CREATE TABLE roles (
|
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL,
|
|
slug VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
is_system BOOLEAN NOT NULL DEFAULT TRUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT roles_slug_unique UNIQUE (slug)
|
|
);
|
|
|
|
CREATE TABLE role_permissions (
|
|
role_id BIGINT NOT NULL,
|
|
permission_id BIGINT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
PRIMARY KEY (role_id, permission_id),
|
|
CONSTRAINT role_permissions_role_id_fkey
|
|
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE,
|
|
CONSTRAINT role_permissions_permission_id_fkey
|
|
FOREIGN KEY (permission_id) REFERENCES permissions (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE user_roles (
|
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
user_id BIGINT NOT NULL,
|
|
role_id BIGINT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT user_roles_user_role_unique UNIQUE (user_id, role_id),
|
|
CONSTRAINT user_roles_user_id_fkey
|
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
|
CONSTRAINT user_roles_role_id_fkey
|
|
FOREIGN KEY (role_id) REFERENCES roles (id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX idx_user_roles_user_id ON user_roles (user_id);
|
|
CREATE INDEX idx_role_permissions_permission_id ON role_permissions (permission_id);
|
|
|
|
CREATE TRIGGER roles_set_updated_at
|
|
BEFORE UPDATE ON roles
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- media (images & videos, scoped per business)
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE media (
|
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
business_id BIGINT NOT NULL,
|
|
uploaded_by BIGINT,
|
|
media_type media_type NOT NULL,
|
|
storage_disk VARCHAR(50) NOT NULL DEFAULT 'local',
|
|
storage_path TEXT NOT NULL,
|
|
public_url TEXT NOT NULL,
|
|
file_name VARCHAR(255) NOT NULL,
|
|
original_file_name VARCHAR(255) NOT NULL,
|
|
mime_type VARCHAR(100) NOT NULL,
|
|
file_size_bytes BIGINT NOT NULL,
|
|
width INTEGER,
|
|
height INTEGER,
|
|
duration_seconds NUMERIC(10, 2),
|
|
alt_text VARCHAR(255),
|
|
caption TEXT,
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT media_business_id_fkey
|
|
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
|
CONSTRAINT media_uploaded_by_fkey
|
|
FOREIGN KEY (uploaded_by) REFERENCES users (id) ON DELETE SET NULL,
|
|
CONSTRAINT media_file_size_positive CHECK (file_size_bytes > 0),
|
|
CONSTRAINT media_image_dimensions CHECK (
|
|
media_type <> 'image' OR (width IS NOT NULL AND height IS NOT NULL)
|
|
),
|
|
CONSTRAINT media_video_duration CHECK (
|
|
media_type <> 'video' OR duration_seconds IS NOT NULL
|
|
)
|
|
);
|
|
|
|
CREATE INDEX idx_media_business_id ON media (business_id);
|
|
CREATE INDEX idx_media_business_type ON media (business_id, media_type);
|
|
CREATE INDEX idx_media_created_at ON media (business_id, created_at DESC);
|
|
|
|
CREATE TRIGGER media_set_updated_at
|
|
BEFORE UPDATE ON media
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- products
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE products (
|
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
business_id BIGINT NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
slug VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
content JSONB NOT NULL DEFAULT '{}',
|
|
price NUMERIC(12, 2),
|
|
compare_at_price NUMERIC(12, 2),
|
|
sku VARCHAR(100),
|
|
stock_quantity INTEGER,
|
|
status content_status NOT NULL DEFAULT 'draft',
|
|
featured_media_id BIGINT,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
published_at TIMESTAMPTZ,
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT products_business_slug_unique UNIQUE (business_id, slug),
|
|
CONSTRAINT products_business_id_fkey
|
|
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
|
CONSTRAINT products_featured_media_id_fkey
|
|
FOREIGN KEY (featured_media_id) REFERENCES media (id) ON DELETE SET NULL,
|
|
CONSTRAINT products_price_non_negative CHECK (price IS NULL OR price >= 0),
|
|
CONSTRAINT products_compare_price_non_negative CHECK (compare_at_price IS NULL OR compare_at_price >= 0),
|
|
CONSTRAINT products_stock_non_negative CHECK (stock_quantity IS NULL OR stock_quantity >= 0)
|
|
);
|
|
|
|
CREATE INDEX idx_products_business_id ON products (business_id);
|
|
CREATE INDEX idx_products_business_status ON products (business_id, status);
|
|
CREATE INDEX idx_products_business_published ON products (business_id, published_at DESC);
|
|
|
|
CREATE TRIGGER products_set_updated_at
|
|
BEFORE UPDATE ON products
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- blogs
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE blogs (
|
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
business_id BIGINT NOT NULL,
|
|
author_id BIGINT,
|
|
title VARCHAR(255) NOT NULL,
|
|
slug VARCHAR(255) NOT NULL,
|
|
excerpt TEXT,
|
|
content JSONB NOT NULL DEFAULT '{}',
|
|
status content_status NOT NULL DEFAULT 'draft',
|
|
featured_media_id BIGINT,
|
|
published_at TIMESTAMPTZ,
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT blogs_business_slug_unique UNIQUE (business_id, slug),
|
|
CONSTRAINT blogs_business_id_fkey
|
|
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
|
CONSTRAINT blogs_author_id_fkey
|
|
FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE SET NULL,
|
|
CONSTRAINT blogs_featured_media_id_fkey
|
|
FOREIGN KEY (featured_media_id) REFERENCES media (id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE INDEX idx_blogs_business_id ON blogs (business_id);
|
|
CREATE INDEX idx_blogs_business_status ON blogs (business_id, status);
|
|
CREATE INDEX idx_blogs_business_published ON blogs (business_id, published_at DESC);
|
|
|
|
CREATE TRIGGER blogs_set_updated_at
|
|
BEFORE UPDATE ON blogs
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- portfolios
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE portfolios (
|
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
business_id BIGINT NOT NULL,
|
|
title VARCHAR(255) NOT NULL,
|
|
slug VARCHAR(255) NOT NULL,
|
|
description TEXT,
|
|
content JSONB NOT NULL DEFAULT '{}',
|
|
client_name VARCHAR(255),
|
|
project_url TEXT,
|
|
status content_status NOT NULL DEFAULT 'draft',
|
|
featured_media_id BIGINT,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
published_at TIMESTAMPTZ,
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT portfolios_business_slug_unique UNIQUE (business_id, slug),
|
|
CONSTRAINT portfolios_business_id_fkey
|
|
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
|
CONSTRAINT portfolios_featured_media_id_fkey
|
|
FOREIGN KEY (featured_media_id) REFERENCES media (id) ON DELETE SET NULL
|
|
);
|
|
|
|
CREATE INDEX idx_portfolios_business_id ON portfolios (business_id);
|
|
CREATE INDEX idx_portfolios_business_status ON portfolios (business_id, status);
|
|
CREATE INDEX idx_portfolios_business_published ON portfolios (business_id, published_at DESC);
|
|
|
|
CREATE TRIGGER portfolios_set_updated_at
|
|
BEFORE UPDATE ON portfolios
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- media attachments (galleries, inline images/videos on content)
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE media_attachments (
|
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
business_id BIGINT NOT NULL,
|
|
media_id BIGINT NOT NULL,
|
|
entity_type media_entity_type NOT NULL,
|
|
entity_id BIGINT NOT NULL,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
CONSTRAINT media_attachments_business_id_fkey
|
|
FOREIGN KEY (business_id) REFERENCES businesses (id) ON DELETE CASCADE,
|
|
CONSTRAINT media_attachments_media_id_fkey
|
|
FOREIGN KEY (media_id) REFERENCES media (id) ON DELETE CASCADE,
|
|
CONSTRAINT media_attachments_unique UNIQUE (media_id, entity_type, entity_id)
|
|
);
|
|
|
|
CREATE INDEX idx_media_attachments_entity
|
|
ON media_attachments (business_id, entity_type, entity_id, sort_order);
|
|
CREATE INDEX idx_media_attachments_media_id ON media_attachments (media_id);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- seed: default permissions & roles
|
|
-- ---------------------------------------------------------------------------
|
|
INSERT INTO permissions (name, slug, group_name, description) VALUES
|
|
('View business', 'business.read', 'business', 'View business profile and settings'),
|
|
('Update business', 'business.update', 'business', 'Edit business profile and settings'),
|
|
('View domains', 'domains.read', 'domains', 'View connected domains'),
|
|
('Manage domains', 'domains.manage', 'domains', 'Add, edit, and remove domains'),
|
|
('View products', 'products.read', 'products', 'View products'),
|
|
('Create products', 'products.create', 'products', 'Create products'),
|
|
('Update products', 'products.update', 'products', 'Edit products'),
|
|
('Delete products', 'products.delete', 'products', 'Delete products'),
|
|
('Publish products', 'products.publish', 'products', 'Publish and unpublish products'),
|
|
('View blogs', 'blogs.read', 'blogs', 'View blog posts'),
|
|
('Create blogs', 'blogs.create', 'blogs', 'Create blog posts'),
|
|
('Update blogs', 'blogs.update', 'blogs', 'Edit blog posts'),
|
|
('Delete blogs', 'blogs.delete', 'blogs', 'Delete blog posts'),
|
|
('Publish blogs', 'blogs.publish', 'blogs', 'Publish and unpublish blog posts'),
|
|
('View portfolios', 'portfolios.read', 'portfolios', 'View portfolio items'),
|
|
('Create portfolios', 'portfolios.create', 'portfolios', 'Create portfolio items'),
|
|
('Update portfolios', 'portfolios.update', 'portfolios', 'Edit portfolio items'),
|
|
('Delete portfolios', 'portfolios.delete', 'portfolios', 'Delete portfolio items'),
|
|
('Publish portfolios', 'portfolios.publish', 'portfolios', 'Publish and unpublish portfolio items'),
|
|
('View media', 'media.read', 'media', 'View uploaded media'),
|
|
('Upload media', 'media.create', 'media', 'Upload images and videos'),
|
|
('Update media', 'media.update', 'media', 'Edit media metadata'),
|
|
('Delete media', 'media.delete', 'media', 'Delete media files'),
|
|
('View users', 'users.read', 'users', 'View user accounts'),
|
|
('Manage users', 'users.manage', 'users', 'Create and manage user accounts'),
|
|
('Manage roles', 'roles.manage', 'users', 'Assign roles and permissions');
|
|
|
|
INSERT INTO roles (name, slug, description, is_system) VALUES
|
|
('Owner', 'owner', 'Full access to everything', TRUE),
|
|
('Administrator', 'admin', 'Manage content, media, and settings', TRUE),
|
|
('Editor', 'editor', 'Create and edit content', TRUE),
|
|
('Viewer', 'viewer', 'Read-only access', TRUE);
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
CROSS JOIN permissions p
|
|
WHERE r.slug = 'owner';
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
JOIN permissions p ON p.slug <> 'roles.manage'
|
|
WHERE r.slug = 'admin';
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
JOIN permissions p ON p.slug IN (
|
|
'business.read',
|
|
'products.read', 'products.create', 'products.update', 'products.publish',
|
|
'blogs.read', 'blogs.create', 'blogs.update', 'blogs.publish',
|
|
'portfolios.read', 'portfolios.create', 'portfolios.update', 'portfolios.publish',
|
|
'media.read', 'media.create', 'media.update'
|
|
)
|
|
WHERE r.slug = 'editor';
|
|
|
|
INSERT INTO role_permissions (role_id, permission_id)
|
|
SELECT r.id, p.id
|
|
FROM roles r
|
|
JOIN permissions p ON p.slug IN (
|
|
'business.read',
|
|
'domains.read',
|
|
'products.read',
|
|
'blogs.read',
|
|
'portfolios.read',
|
|
'media.read'
|
|
)
|
|
WHERE r.slug = 'viewer';
|