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:
+178
@@ -0,0 +1,178 @@
|
||||
# Deploy Meshkee CMS API (Debian VM)
|
||||
|
||||
Stack: Docker (Postgres + Redis) → Node build on server → PM2 → Nginx + Let's Encrypt.
|
||||
|
||||
App path on server: `/opt/meshkee/app`
|
||||
|
||||
API domain: `api.meshkee.com` → `https://api.meshkee.com/api/v1`
|
||||
|
||||
> **Note:** Until the Git remote is accessible from the VM (deploy key / credentials), updates can be synced with `rsync` from your laptop. Pin `sharp@0.33.5` — this VM CPU lacks x64-v2 required by sharp 0.35+.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Debian VM with SSH access
|
||||
- Domain `A` record pointing at the VM (for HTTPS)
|
||||
- Git remote with this codebase (private repo → deploy key)
|
||||
- Production secrets (JWT, Postgres password, S3 keys)
|
||||
|
||||
## 1. Server packages
|
||||
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
sudo apt install -y ca-certificates curl gnupg git nginx ufw
|
||||
|
||||
# Docker
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
sudo apt update
|
||||
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
sudo usermod -aG docker "$USER"
|
||||
# log out/in (or newgrp docker) so docker works without sudo
|
||||
|
||||
# Node.js 20 LTS
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
sudo npm install -g pm2
|
||||
|
||||
# Certbot (after Nginx is installed)
|
||||
sudo apt install -y certbot python3-certbot-nginx
|
||||
```
|
||||
|
||||
Firewall:
|
||||
|
||||
```bash
|
||||
sudo ufw allow OpenSSH
|
||||
sudo ufw allow 'Nginx Full'
|
||||
sudo ufw --force enable
|
||||
```
|
||||
|
||||
## 2. Clone the app
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /opt/meshkee
|
||||
sudo chown "$USER:$USER" /opt/meshkee
|
||||
cd /opt/meshkee
|
||||
git clone <YOUR_GIT_REMOTE_URL> app
|
||||
cd app
|
||||
```
|
||||
|
||||
Private repo: create an SSH deploy key on the VM (`ssh-keygen -t ed25519 -C "meshkee-deploy"`), add the public key as a read-only deploy key on GitHub/GitLab, clone via SSH URL.
|
||||
|
||||
## 3. Production env
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env # set strong secrets — never commit this file
|
||||
```
|
||||
|
||||
Required production values:
|
||||
|
||||
- Strong `POSTGRES_PASSWORD` and matching `DATABASE_URL`
|
||||
- Long random `JWT_ACCESS_SECRET` / `JWT_REFRESH_SECRET`
|
||||
- Real `S3_ACCESS_KEY_ID` / `S3_SECRET_ACCESS_KEY`
|
||||
- `PORT=3000`
|
||||
- `SMS_ENABLED` as needed
|
||||
|
||||
## 4. Database + Redis
|
||||
|
||||
```bash
|
||||
cd /opt/meshkee/app
|
||||
docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
First Postgres volume init runs SQL under `database/migrations/` automatically.
|
||||
|
||||
Later schema updates:
|
||||
|
||||
```bash
|
||||
./database/migrate.sh
|
||||
```
|
||||
|
||||
## 5. Build and run (on the server)
|
||||
|
||||
```bash
|
||||
cd /opt/meshkee/app
|
||||
npm ci
|
||||
npm run prisma:generate
|
||||
npm run build
|
||||
```
|
||||
|
||||
Production seed (super admin only — skip sample data):
|
||||
|
||||
```bash
|
||||
./database/seed.sh database/seeds/002_super_admin_user.sql
|
||||
# optional reference data:
|
||||
# ./database/seed.sh database/seeds/004_iran_cities.sql
|
||||
# ./database/seed.sh database/seeds/005_business_categories.sql
|
||||
```
|
||||
|
||||
Start with PM2:
|
||||
|
||||
```bash
|
||||
pm2 start ecosystem.config.js
|
||||
pm2 save
|
||||
pm2 startup # run the command it prints (usually with sudo)
|
||||
```
|
||||
|
||||
Health check locally on the VM:
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:3000/api/v1/ | head
|
||||
# or hit a known public route such as tenant resolve
|
||||
```
|
||||
|
||||
## 6. Nginx + HTTPS
|
||||
|
||||
Create `/etc/nginx/sites-available/meshkee-api`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name api.example.com; # replace with your domain
|
||||
|
||||
client_max_body_size 15M;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enable and get a certificate:
|
||||
|
||||
```bash
|
||||
sudo ln -sf /etc/nginx/sites-available/meshkee-api /etc/nginx/sites-enabled/
|
||||
sudo rm -f /etc/nginx/sites-enabled/default
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
sudo certbot --nginx -d api.example.com
|
||||
```
|
||||
|
||||
API base URL: `https://api.example.com/api/v1`
|
||||
|
||||
## Ongoing updates
|
||||
|
||||
```bash
|
||||
cd /opt/meshkee/app
|
||||
git pull
|
||||
./database/migrate.sh # if there are new SQL migrations
|
||||
npm ci
|
||||
npm run prisma:generate
|
||||
npm run build
|
||||
pm2 restart meshkee-api
|
||||
```
|
||||
|
||||
## Useful commands
|
||||
|
||||
```bash
|
||||
pm2 status
|
||||
pm2 logs meshkee-api
|
||||
docker compose logs -f postgres
|
||||
```
|
||||
@@ -0,0 +1,601 @@
|
||||
# Meshkee CMS API — Project Context
|
||||
|
||||
> Living reference for developers and AI assistants working on this codebase.
|
||||
> Last updated: July 2026
|
||||
|
||||
## What This Project Is
|
||||
|
||||
**Meshkee CMS API** (`meshkee-cms-api`) is a multi-tenant backend for Meshkee business websites. Each business gets its own domain, content (products, categories, media), team members, and customer registrations. Platform super admins manage businesses and domains; business owners and staff manage per-business content through a permission-based dashboard.
|
||||
|
||||
**API base URL:** `/api/v1`
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| Runtime | Node.js, TypeScript (ES2021, strict) |
|
||||
| Framework | NestJS 11 |
|
||||
| ORM | Prisma 6 (`prisma db pull` — schema is introspected, not migrated via Prisma) |
|
||||
| Database | PostgreSQL 16 |
|
||||
| Cache | Redis 7 (OTP storage) |
|
||||
| Auth | JWT (access + refresh), Passport, bcrypt |
|
||||
| Validation | class-validator + class-transformer |
|
||||
| File storage | S3-compatible (Parmin), Sharp for image processing |
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Multi-tenancy model
|
||||
|
||||
```
|
||||
Platform (super admin)
|
||||
└── Business (tenant root)
|
||||
├── Domains (host → business resolution)
|
||||
├── Team members (BusinessUser + Role)
|
||||
├── Customers (BusinessCustomer)
|
||||
├── Categories (per entityType: product | blog | portfolio)
|
||||
├── Products
|
||||
│ └── Store items (product variants — price & stock per variation combo)
|
||||
├── Media library
|
||||
└── Settings (JSON)
|
||||
```
|
||||
|
||||
- **Tenant resolution:** `GET /tenants/:host` resolves a domain to a business (public, no auth).
|
||||
- **Business-scoped APIs:** Most CMS routes use `businesses/:businessId/...` with JWT + business permission checks.
|
||||
- **Platform APIs:** User/business/domain management requires `super_admin` (checked in services).
|
||||
|
||||
### Two "category" concepts
|
||||
|
||||
| Concept | Table | Purpose |
|
||||
|---------|-------|---------|
|
||||
| **Content Category** | `categories` | Per-business taxonomy for products, blogs, portfolios |
|
||||
| **Business Category** | `business_categories` | Platform-wide taxonomy classifying businesses (Retail, Creative, etc.) |
|
||||
|
||||
Do not confuse them when reading or writing code.
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.ts # Bootstrap, global prefix, pipes, interceptors
|
||||
├── app.module.ts # Root module wiring
|
||||
├── auth/ # JWT, OTP/SMS, permissions, guards, decorators
|
||||
├── users/ # Super-admin user management
|
||||
├── roles/ # Global + team role listing
|
||||
├── business-admin/ # Super-admin business CRUD + domains
|
||||
├── business-team/ # Per-business team invite/manage
|
||||
├── tenant/ # Domain → business resolution (public)
|
||||
├── domain-admin/ # Super-admin domain management
|
||||
├── categories/ # Content categories, variations, technical forms
|
||||
├── products/ # Products, variants, technical info
|
||||
├── cart/ # Customer shopping cart
|
||||
├── orders/ # Customer checkout + admin order management
|
||||
├── media/ # Upload/list/update/delete media
|
||||
├── storage/ # S3 driver abstraction
|
||||
├── prisma/ # PrismaModule + PrismaService
|
||||
├── redis/ # Redis client + OTP helpers
|
||||
└── common/ # Shared interceptors (BigInt serialization)
|
||||
|
||||
prisma/schema.prisma # Introspected from PostgreSQL (source of truth after migrations)
|
||||
database/
|
||||
├── migrations/ # Raw SQL migrations (numbered)
|
||||
├── seeds/ # Dev sample data + production super-admin
|
||||
├── migrate.sh, seed.sh, setup.sh
|
||||
postman/ # API collection for manual testing
|
||||
```
|
||||
|
||||
### NestJS module pattern
|
||||
|
||||
Each feature follows: `*.module.ts` → `*.controller.ts` → `*.service.ts` → `dto/`
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose up -d # Postgres (auto-runs migrations on first init) + Redis
|
||||
./database/seed.sh # Sample dev data
|
||||
npm install
|
||||
npm run prisma:generate
|
||||
npm run start:dev # http://localhost:3000/api/v1
|
||||
```
|
||||
|
||||
**Dev credentials** (after `001_sample_data.sql` seed): any seeded user, password `password`.
|
||||
|
||||
Example super admin: `+989121111111` / `password`
|
||||
|
||||
---
|
||||
|
||||
## Database & Migrations
|
||||
|
||||
### Workflow
|
||||
|
||||
1. Write a new SQL file in `database/migrations/` (e.g. `012_feature.sql`).
|
||||
2. Apply via `./database/migrate.sh` or `docker exec` into Postgres.
|
||||
3. Run `npm run prisma:pull` to sync `prisma/schema.prisma`.
|
||||
4. Run `npm run prisma:generate` to regenerate the client.
|
||||
|
||||
**Important:** This project does **not** use Prisma Migrate. SQL migrations are the authoritative schema history. Prisma schema is maintained separately via introspection.
|
||||
|
||||
### Migration files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `001_initial_schema.sql` | Users, businesses, domains |
|
||||
| `002_phone_permissions_content_media.sql` | RBAC, media, products, blogs, portfolios |
|
||||
| `003_categories.sql` | Content categories + assignments |
|
||||
| `004_user_types_and_business_members.sql` | Super admin, business owner, customer roles |
|
||||
| `005_business_team_roles.sql` | Team roles (admin, editor, viewer) |
|
||||
| `006_business_categories.sql` | Platform business category taxonomy |
|
||||
| `006_user_profile.sql` | User profile JSON |
|
||||
| `007_business_i18n_fields.sql` | Business Persian name |
|
||||
| `007_domain_expiry_active.sql` | Domain expiry/active flags |
|
||||
| `008_remove_name_en.sql` | Schema cleanup |
|
||||
| `009_categories_name_fa.sql` | Category `name_fa` |
|
||||
| `010_category_variations.sql` | Category variations, product variants (store items) |
|
||||
| `011_category_technical_forms.sql` | Technical forms + product values |
|
||||
| `012_comments.sql` | Product comments |
|
||||
| `013_expert_reviews.sql` | Expert reviews |
|
||||
| `014_addresses.sql` | User/business street addresses |
|
||||
| `015_business_profile.sql` | Business profile fields |
|
||||
| `015_cities.sql` | Location cities (country → province → city) |
|
||||
| `016_product_variation_values.sql` | Product-level variation option selections (input for store items) |
|
||||
| `017_product_variant_festival.sql` | Festival flag on store items |
|
||||
| `018_product_variant_reward_points.sql` | Reward points on store items |
|
||||
| `019_cart_and_orders.sql` | Shopping cart, orders, order items + order permissions |
|
||||
| `020_store_items_and_variants.sql` | `store_items` + `store_item_variants` (replaces `product_variants`) |
|
||||
| `030_website_homepage.sql` | Website category/brand groups, sliders, brand `sort_order` |
|
||||
|
||||
Docker mounts `./database/migrations` into Postgres init — migrations run automatically only on **first** volume creation. Use `migrate.sh` for subsequent migrations.
|
||||
|
||||
---
|
||||
|
||||
## Domain Model
|
||||
|
||||
### Enums
|
||||
|
||||
| Enum | Values |
|
||||
|------|--------|
|
||||
| `MediaEntityType` | `product`, `blog`, `portfolio` |
|
||||
| `ContentStatus` | `draft`, `published`, `archived` |
|
||||
| `VariationType` | `color`, `size`, `custom` |
|
||||
| `OrderStatus` | `pending`, `confirmed`, `processing`, `shipped`, `delivered`, `cancelled` |
|
||||
| `OrderSource` | `website`, `admin` |
|
||||
| `TechnicalFieldType` | `text`, `textarea`, `select`, `multi_select` |
|
||||
| `MediaType` | `image`, `video` |
|
||||
|
||||
### Core relationships
|
||||
|
||||
```
|
||||
Business 1──* Domain
|
||||
Business 1──* Category (entityType: product|blog|portfolio)
|
||||
Business 1──* Product
|
||||
Business 1──* Media
|
||||
|
||||
Category 1──* CategoryVariation 1──* CategoryVariationOption
|
||||
Category 1──0..1 CategoryTechnicalForm 1──* CategoryTechnicalFormField
|
||||
CategoryTechnicalFormField 1──* CategoryTechnicalFormFieldOption
|
||||
|
||||
Product *──0..1 Category (via CategoryAssignment)
|
||||
Product 1──* ProductVariationValue → CategoryVariationOption (which options this product offers)
|
||||
Product 1──* ProductVariationValue → CategoryVariationOption (which options this product offers)
|
||||
Product 1──0..1 StoreItem (one shop listing per product)
|
||||
StoreItem 1──* StoreItemVariant (purchasable SKUs: price, stock, variation combo)
|
||||
StoreItemVariant 1──* StoreItemVariantSelection → CategoryVariationOption
|
||||
Product 1──* ProductTechnicalFieldValue → CategoryTechnicalFormField
|
||||
|
||||
Business 1──* Cart (per customer) 1──* CartItem → ProductVariant
|
||||
Business 1──* Order 1──* OrderItem → ProductVariant (snapshot on order)
|
||||
User 1──* Cart, Order (as customer)
|
||||
|
||||
Media 1──* MediaAttachment (polymorphic: entityType + entityId)
|
||||
```
|
||||
|
||||
### Schema gap: blogs & portfolios
|
||||
|
||||
SQL migrations create `blogs` and `portfolios` tables and seed data populates them. Permissions exist (`blogs.*`, `portfolios.*`). However:
|
||||
|
||||
- No Prisma models for Blog/Portfolio
|
||||
- No NestJS modules or API endpoints
|
||||
|
||||
Categories and media attachments already support `blog` and `portfolio` entity types — infrastructure is ready, API is not.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
All routes are prefixed with `/api/v1`.
|
||||
|
||||
### Public
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| POST | `/auth/register` | Customer registration by domain |
|
||||
| POST | `/auth/login` | Cell + password |
|
||||
| POST | `/auth/refresh` | Refresh token |
|
||||
| POST | `/auth/send-otp` | Send OTP (Redis-backed) |
|
||||
| POST | `/auth/verify-otp` | Verify OTP |
|
||||
| GET | `/tenants/:host` | Resolve business from domain |
|
||||
| GET | `/tenants/:host/store-specials` | Active store specials |
|
||||
| GET | `/tenants/:host/website/category-groups` | Homepage category rows |
|
||||
| GET | `/tenants/:host/website/brand-groups` | Homepage brand rows |
|
||||
| GET | `/tenants/:host/website/sliders` | Homepage sliders with slides |
|
||||
|
||||
### Authenticated (JWT)
|
||||
|
||||
| Method | Path | Access |
|
||||
|--------|------|--------|
|
||||
| GET | `/auth/me` | Any user |
|
||||
| PATCH | `/auth/profile` | Any user |
|
||||
| POST | `/auth/change-password` | Any user |
|
||||
| GET | `/roles?scope=global\|team` | Super admin / team.read |
|
||||
| GET | `/business-categories` | Super admin or `business_categories.read` |
|
||||
|
||||
### Super admin only (service-level check)
|
||||
|
||||
| Module | Base path |
|
||||
|--------|-----------|
|
||||
| Users | `/users` |
|
||||
| Businesses | `/businesses` |
|
||||
| Domains | `/domains` |
|
||||
|
||||
### Business-scoped (JWT + `BusinessPermissionGuard`)
|
||||
|
||||
Pattern: `/businesses/:businessId/<resource>`
|
||||
|
||||
| Module | Base path | Key permissions |
|
||||
|--------|-----------|-----------------|
|
||||
| Team | `/team` | `business.team.*` |
|
||||
| Media | `/media` | `media.*` |
|
||||
| Categories | `/categories` | `categories.*` |
|
||||
| Products | `/products` | `products.*` |
|
||||
| Website | `/website/category-groups`, `/website/brand-groups`, `/website/sliders` | `website.*` |
|
||||
|
||||
#### Categories — notable sub-routes
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/categories/color-presets` | Predefined color palette |
|
||||
| GET/PUT | `/categories/:id/variations` | Category variation options |
|
||||
| GET/PUT | `/categories/:id/technical-form` | Technical form definition |
|
||||
|
||||
#### Products — notable sub-routes
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET/PUT | `/products/:id/variations` | Which category variation options apply to this product |
|
||||
| GET/POST/PATCH/DELETE | `/products/:id/variants` | Removed — use `/store-items` |
|
||||
| GET/PUT | `/products/:id/technical-info` | Product technical data |
|
||||
|
||||
#### Cart (customer — JWT, must be business customer)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/cart` | Get current user's cart |
|
||||
| POST | `/cart/items` | Add store item variant to cart (`storeItemVariantId`, `quantity`) |
|
||||
| PATCH | `/cart/items/:itemId` | Update cart item quantity |
|
||||
| DELETE | `/cart/items/:itemId` | Remove cart item |
|
||||
| DELETE | `/cart` | Clear cart |
|
||||
| POST | `/cart/checkout` | Place order from cart (requires `addressId` or `shippingAddress`) |
|
||||
|
||||
#### Orders
|
||||
|
||||
| Method | Path | Access | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| GET | `/orders` | Customer (own) or `orders.read` (all) | List orders |
|
||||
| GET | `/orders/:orderId` | Customer (own) or `orders.read` | Order detail |
|
||||
| POST | `/orders` | `orders.create` | Admin: create order for a customer |
|
||||
| PATCH | `/orders/:orderId` | `orders.update` | Admin: update status / admin notes |
|
||||
|
||||
---
|
||||
|
||||
## Auth & RBAC
|
||||
|
||||
### Global user roles
|
||||
|
||||
| Role slug | Dashboard | Notes |
|
||||
|-----------|-----------|-------|
|
||||
| `super_admin` | `super_admin` | Full platform access |
|
||||
| `business_owner` | `business` | Assigned to business owners |
|
||||
| `business_staff` | `business` | Legacy; team uses per-business roles |
|
||||
| `customer` | `customer` | Website registrants |
|
||||
|
||||
### Per-business team roles
|
||||
|
||||
Assigned via `business_users.role_id`:
|
||||
|
||||
| Role | Slug | Typical access |
|
||||
|------|------|----------------|
|
||||
| Owner | `isOwner=true` | All `business_owner` permissions |
|
||||
| Admin | `admin` | Full content + team read |
|
||||
| Editor | `editor` | Create/edit/publish content |
|
||||
| Viewer | `viewer` | Read-only |
|
||||
|
||||
### Permission groups (seeded)
|
||||
|
||||
`business.*`, `domains.*`, `products.*`, `blogs.*`, `portfolios.*`, `media.*`, `categories.*`, `users.*`, `roles.manage`, `business.team.*`, `business_categories.*`
|
||||
|
||||
Each resource typically has: `read`, `create`, `update`, `delete` (+ `publish` for content).
|
||||
|
||||
### Guards & decorators
|
||||
|
||||
```typescript
|
||||
@UseGuards(JwtAuthGuard, BusinessPermissionGuard)
|
||||
@RequireBusinessPermission('products.read')
|
||||
```
|
||||
|
||||
- `JwtAuthGuard` — validates Bearer JWT (`type: 'access'`)
|
||||
- `BusinessPermissionGuard` — checks permission for `businessId` route param
|
||||
- `PermissionsService` — `isSuperAdmin()`, `hasBusinessPermission()`
|
||||
- Super admins bypass business permission checks
|
||||
- `@CurrentUser()` injects `AuthUser` into handlers
|
||||
|
||||
### Auth flow notes
|
||||
|
||||
- Registration resolves tenant by `domain` → creates/links user → assigns `customer` role
|
||||
- OTP stored in Redis (`otp:{cellNumber}`), 5-min TTL; disabled when `SMS_ENABLED=false`
|
||||
- JWT payload: `sub`, `cellNumber`, `roles`, `dashboard`, `type`
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
### Products
|
||||
|
||||
- CRUD with slug, status, featured media, gallery attachments
|
||||
- One category per product (via `CategoryAssignment`)
|
||||
- Content JSON: `{ nameFa, html }`; metadata JSON: `{ tags }`
|
||||
- i18n: Persian name in `content.nameFa`, summary in `description`
|
||||
|
||||
### Category variations (product categories)
|
||||
|
||||
- Types: `color` (preset palette), `size`, `custom`
|
||||
- One color + one size per category (DB partial unique indexes)
|
||||
- `PUT /categories/:id/variations` replaces all variations (delete-all + recreate)
|
||||
- Color presets: `GET /categories/color-presets` (15 named colors with hex)
|
||||
|
||||
### Store (shop listings)
|
||||
|
||||
The **store** turns CMS products into purchasable items using a two-level model:
|
||||
|
||||
```
|
||||
Product variations → which options this product offers (product_variation_values)
|
||||
Store item → one shop listing per product (store_items)
|
||||
Store item variants → purchasable SKUs with price/stock per combination (store_item_variants)
|
||||
```
|
||||
|
||||
| Layer | Table | API | Purpose |
|
||||
|-------|-------|-----|---------|
|
||||
| Product variations | `product_variation_values` | `PUT /products/:id/variations` | Which category variation options apply to this product |
|
||||
| Store item | `store_items` | `GET /store-items/by-product/:productId` | One listing per product in the shop |
|
||||
| Store item variants | `store_item_variants` + `store_item_variant_selections` | `POST /store-items`, `PUT /store-items/sync` | Purchasable combinations with price & stock |
|
||||
|
||||
**Store item variant fields** (`store_item_variants`):
|
||||
|
||||
| Field | DB column | Notes |
|
||||
|-------|-----------|-------|
|
||||
| `sku` | `sku` | Optional merchant SKU |
|
||||
| `price` | `price` | Selling price (`NUMERIC(12,2)`) |
|
||||
| `compareAtPrice` | `compare_at_price` | Optional strike-through / was-price |
|
||||
| `stockQuantity` | `stock_quantity` | Integer ≥ 0; `null` = untracked |
|
||||
| `isActive` | `is_active` | Whether variant is available for purchase |
|
||||
| `selections` | `store_item_variant_selections` | Exactly one `CategoryVariationOption` per category variation |
|
||||
|
||||
**Rules:**
|
||||
|
||||
- One `store_items` row per product per business.
|
||||
- Create variants under the store item after setting product variation values.
|
||||
- Each variant picks one option per variation; the combination must be unique per store item.
|
||||
- Cart and orders reference `storeItemVariantId` (not the product directly).
|
||||
|
||||
**Example — batch create variants for a product:**
|
||||
|
||||
```json
|
||||
POST /businesses/:businessId/store-items
|
||||
{
|
||||
"productId": "1",
|
||||
"items": [
|
||||
{
|
||||
"selections": [
|
||||
{ "variationId": "1", "optionId": "1" },
|
||||
{ "variationId": "2", "optionId": "5" }
|
||||
],
|
||||
"price": 99.99,
|
||||
"stockQuantity": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Example — update a variant:**
|
||||
|
||||
```json
|
||||
PATCH /businesses/:businessId/store-items/variants/:variantId
|
||||
{
|
||||
"price": 89.99,
|
||||
"stockQuantity": 25
|
||||
}
|
||||
```
|
||||
|
||||
### Technical forms (product categories)
|
||||
|
||||
- One form per product category
|
||||
- Field types: `text`, `textarea`, `select`, `multi_select`
|
||||
- `PUT /categories/:id/technical-form` — define/replace form fields
|
||||
- `PUT /products/:id/technical-info` — fill product values (validated against category form)
|
||||
- Product must have a category assignment
|
||||
|
||||
**Example — define form:**
|
||||
|
||||
```json
|
||||
PUT /businesses/:businessId/categories/:categoryId/technical-form
|
||||
{
|
||||
"fields": [
|
||||
{ "label": "Weight", "type": "text", "isRequired": true },
|
||||
{ "label": "Material", "type": "select", "options": ["Cotton", "Polyester"] },
|
||||
{ "label": "Features", "type": "multi_select", "options": ["Waterproof", "Breathable"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Example — fill product data:**
|
||||
|
||||
```json
|
||||
PUT /businesses/:businessId/products/:productId/technical-info
|
||||
{
|
||||
"values": [
|
||||
{ "fieldKey": "weight", "value": "500g" },
|
||||
{ "fieldKey": "material", "value": "cotton" },
|
||||
{ "fieldKey": "features", "value": ["waterproof", "breathable"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Field keys are auto-slugified from labels (e.g. `"Weight"` → `"weight"`).
|
||||
|
||||
### Media
|
||||
|
||||
- Multipart upload to S3 via Sharp processing
|
||||
- Polymorphic attachments to products (and future blog/portfolio entities)
|
||||
- Featured image on products via `featuredMediaId`
|
||||
|
||||
---
|
||||
|
||||
## Coding Conventions
|
||||
|
||||
### DTOs
|
||||
|
||||
- `class-validator` decorators on all request bodies and query params
|
||||
- `@Type(() => Number)` for query param coercion
|
||||
- Slug format: `^[a-z0-9]+(?:-[a-z0-9]+)*$`
|
||||
- Cell numbers: E.164 `^\+[1-9]\d{6,14}$`
|
||||
|
||||
### Services
|
||||
|
||||
- Inject `PrismaService`, `PermissionsService`
|
||||
- Convert route param IDs with `BigInt(idRaw)`
|
||||
- Private `assertPermission()` / `assertSuperAdmin()` helpers
|
||||
- Use `$transaction` for multi-step writes
|
||||
- **Replace semantics** for nested resources (variations, technical forms) — delete-all then recreate
|
||||
|
||||
### Error handling
|
||||
|
||||
Use Nest exceptions: `NotFoundException`, `ForbiddenException`, `BadRequestException`, `ConflictException`, `UnauthorizedException`.
|
||||
|
||||
### Serialization
|
||||
|
||||
- Global `BigIntSerializerInterceptor` converts BigInt → Number in JSON responses
|
||||
- Services use private `serialize()` methods for consistent output shapes
|
||||
- IDs returned as strings in API responses
|
||||
|
||||
### Global validation pipe (`main.ts`)
|
||||
|
||||
```typescript
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
See `.env.example` for the full list. Key groups:
|
||||
|
||||
| Group | Variables |
|
||||
|-------|-----------|
|
||||
| Database | `DATABASE_URL`, `POSTGRES_*` |
|
||||
| Redis | `REDIS_URL`, `REDIS_HOST`, `REDIS_PORT` |
|
||||
| API | `PORT` |
|
||||
| JWT | `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`, `JWT_*_EXPIRES_IN` |
|
||||
| SMS | `SMS_ENABLED` |
|
||||
| S3 | `S3_ENDPOINT`, `S3_BUCKET`, `S3_PUBLIC_URL`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` |
|
||||
| Media | `MEDIA_MAX_FILE_SIZE_MB` |
|
||||
|
||||
---
|
||||
|
||||
## Implemented vs Planned
|
||||
|
||||
### Implemented
|
||||
|
||||
- Multi-tenant auth (register, login, OTP, profile)
|
||||
- Super admin: users, businesses, domains, system business categories
|
||||
- Business team management
|
||||
- Media upload (S3 + Sharp)
|
||||
- Categories (all entity types in DB; API supports `entityType` filter)
|
||||
- Products CRUD
|
||||
- Product variation values (which options a product offers)
|
||||
- Store items / product variants (price, stock, SKU)
|
||||
- Shopping cart + checkout + orders (customer + admin)
|
||||
- Product technical info
|
||||
- Category variations & technical forms
|
||||
- Tenant resolution by domain
|
||||
- RBAC with granular permissions
|
||||
|
||||
### Planned / partial
|
||||
|
||||
| Feature | DB | Permissions | API | Prisma |
|
||||
|---------|----|-------------|-----|--------|
|
||||
| Blogs | Yes | Yes | No | No model |
|
||||
| Portfolios | Yes | Yes | No | No model |
|
||||
| Customer dashboard | Partial | No | Register only | Yes |
|
||||
| SMS provider | — | — | Stub | — |
|
||||
| Store checkout (cart, orders) | Yes | Yes | Yes | Yes |
|
||||
| Customer favorites | — | `favorites.*` seeded | No | No |
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
**Postman collection:** `postman/Meshkee-CMS-Auth.postman_collection.json`
|
||||
|
||||
Variables: `baseUrl`, `accessToken`, `refreshToken`, `domain`, `businessId`, `categoryId`, `productId`, `variantId`, `variationId`, `optionId`, `cartItemId`, `orderId`
|
||||
|
||||
**Store workflow in Postman:** `Business Categories` → set variations → `Business Products` → create product → set product variations → create store item (variant) with price & stock → `Website - Cart` (login as customer) → add to cart → checkout → `Business Orders` (login as owner) to manage.
|
||||
|
||||
---
|
||||
|
||||
## Common Tasks for Contributors
|
||||
|
||||
### Add a new API feature
|
||||
|
||||
1. Write SQL migration in `database/migrations/`
|
||||
2. Apply migration, then `npm run prisma:pull && npm run prisma:generate`
|
||||
3. Create module: `src/<feature>/` with controller, service, DTOs
|
||||
4. Register in `app.module.ts`
|
||||
5. Add permissions to migration if business-scoped
|
||||
6. Update this document
|
||||
|
||||
### Add a business-scoped endpoint
|
||||
|
||||
1. Controller: `@Controller('businesses/:businessId/...')`
|
||||
2. Guards: `@UseGuards(JwtAuthGuard, BusinessPermissionGuard)`
|
||||
3. Permission: `@RequireBusinessPermission('resource.action')`
|
||||
4. Service: `assertPermission(businessId, actor.id, 'resource.action')` (defense in depth)
|
||||
|
||||
### Add a nested replace resource (like variations)
|
||||
|
||||
Follow the pattern in `CategoryVariationsService` / `CategoryTechnicalFormService`:
|
||||
|
||||
- `GET` returns current state
|
||||
- `PUT` validates input, deletes all existing, recreates in a transaction
|
||||
|
||||
---
|
||||
|
||||
## Related Files
|
||||
|
||||
| Purpose | Path |
|
||||
|---------|------|
|
||||
| Prisma schema | `prisma/schema.prisma` |
|
||||
| Env template | `.env.example` |
|
||||
| Docker services | `docker-compose.yml` |
|
||||
| Dev seed data | `database/seeds/001_sample_data.sql` |
|
||||
| Postman | `postman/Meshkee-CMS-Auth.postman_collection.json` |
|
||||
Reference in New Issue
Block a user