mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-11 22:30:59 +04:30
Initial commit: Meshkee CMS API
NestJS backend with Prisma, Docker Compose for Postgres/Redis, and deploy docs for the production VM.
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
-- 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';
|
||||
Reference in New Issue
Block a user