add docs
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
# API Conventions
|
||||
|
||||
HTTP API design standards for Negareh backend. Swagger documentation is available at `/docs` when the server is running.
|
||||
|
||||
## Base URL
|
||||
|
||||
No global path prefix. Routes encode the audience and resource directly:
|
||||
|
||||
```
|
||||
POST /public/auth/otp/request
|
||||
GET /admin/orders
|
||||
GET /public/orders/:id
|
||||
```
|
||||
|
||||
## Route naming
|
||||
|
||||
### Audience prefixes
|
||||
|
||||
| Prefix | Description |
|
||||
|--------|-------------|
|
||||
| `public/` | Customer-facing endpoints |
|
||||
| `admin/` | Back-office endpoints |
|
||||
|
||||
Auth endpoints embed the audience in the path: `public/auth/...`, `admin/auth/...`.
|
||||
|
||||
### HTTP methods
|
||||
|
||||
| Method | Usage |
|
||||
|--------|-------|
|
||||
| `GET` | Read single resource or list |
|
||||
| `POST` | Create resource or action (e.g. OTP request) |
|
||||
| `PATCH` | Partial update |
|
||||
| `PUT` | Full replace (rare) |
|
||||
| `DELETE` | Soft or hard delete |
|
||||
|
||||
### Path style
|
||||
|
||||
- Use **plural nouns** for collections: `/admin/orders`, `/public/tickets`.
|
||||
- Use **path parameters** for IDs: `/admin/orders/:id`.
|
||||
- Use **query parameters** for filtering, sorting, and pagination.
|
||||
- Nest related actions under the resource: `/admin/orders/:id/assign-designer`.
|
||||
|
||||
Controllers use `@Controller()` with the full path on each method (no shared controller prefix), except where a module uses a short prefix like `@Controller('admin')`.
|
||||
|
||||
## Request format
|
||||
|
||||
### Headers
|
||||
|
||||
| Header | Required | Description |
|
||||
|--------|----------|-------------|
|
||||
| `Content-Type: application/json` | Yes (JSON bodies) | Standard JSON requests |
|
||||
| `Authorization: Bearer <token>` | Protected routes | JWT access token |
|
||||
|
||||
### Validation
|
||||
|
||||
All inputs are validated through DTO classes with `class-validator`. The global `ValidationPipe` has `transform: true`, so query strings are coerced to the correct types.
|
||||
|
||||
Invalid input returns `400` with validation messages in the error envelope.
|
||||
|
||||
### File uploads
|
||||
|
||||
Multipart uploads use `@fastify/multipart`. Upload endpoints live in the `uploader` module and return file metadata/URLs from S3.
|
||||
|
||||
## Response format
|
||||
|
||||
### Success (single resource)
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 200,
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "01HXYZ...",
|
||||
"phone": "09362532122"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Success (paginated list)
|
||||
|
||||
Services return `PaginatedResult<T>`; the interceptor wraps it:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 200,
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "id": "01HXYZ...", "status": "pending" }
|
||||
],
|
||||
"meta": {
|
||||
"total": 42,
|
||||
"page": 1,
|
||||
"limit": 10,
|
||||
"totalPages": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pagination query parameters
|
||||
|
||||
Common fields on list DTOs:
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `page` | number | `1` | Page number (1-based) |
|
||||
| `limit` | number | `10` | Items per page |
|
||||
| `search` | string | — | Free-text search |
|
||||
| `orderBy` | string | `createdAt` | Sort field |
|
||||
| `order` | `asc` \| `desc` | `desc` | Sort direction |
|
||||
|
||||
Some endpoints may use cursor-based pagination and return `nextCursor` at the top level.
|
||||
|
||||
### Error
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 400,
|
||||
"success": false,
|
||||
"error": {
|
||||
"message": ["Phone number is invalid"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`message` is always an array of strings, even for a single error.
|
||||
|
||||
### HTTP status codes
|
||||
|
||||
| Code | When |
|
||||
|------|------|
|
||||
| `200` | Successful GET, PATCH, DELETE |
|
||||
| `201` | Successful POST (create) — when explicitly set |
|
||||
| `400` | Validation failure, bad input |
|
||||
| `401` | Missing or invalid token |
|
||||
| `403` | Authenticated but insufficient permissions |
|
||||
| `404` | Resource not found |
|
||||
| `429` | Rate limit exceeded |
|
||||
| `500` | Unexpected server error |
|
||||
|
||||
## Swagger
|
||||
|
||||
- Title: **Negareh API**
|
||||
- Path: `/docs`
|
||||
- Bearer auth scheme enabled — use the Authorize button to set JWT.
|
||||
- Controllers document endpoints with `@ApiTags`, `@ApiOperation`, `@ApiBody`, `@ApiBearerAuth`.
|
||||
- DTO fields use `@ApiProperty` for examples and descriptions.
|
||||
|
||||
## Controller pattern
|
||||
|
||||
```typescript
|
||||
@ApiTags('orders')
|
||||
@ApiBearerAuth()
|
||||
@Controller()
|
||||
export class OrderController {
|
||||
constructor(private readonly orderService: OrderService) {}
|
||||
|
||||
// User route
|
||||
@Get('public/orders')
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'Get all orders with pagination and filters' })
|
||||
findAll(@Query() dto: FindOrdersDto, @UserId() userId: string) {
|
||||
return this.orderService.findOrdersAsUser(userId, dto);
|
||||
}
|
||||
|
||||
// Admin route
|
||||
@Post('admin/orders')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(PermissionEnum.CREATE_ORDER)
|
||||
@ApiOperation({ summary: 'Create order' })
|
||||
create(@AdminId() adminId: string, @Body() body: CreateOrderAsAdminDto) {
|
||||
return this.orderService.createOrderAsAdmin(adminId, body);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Identifiers
|
||||
|
||||
- Resource IDs are **ULIDs** (26-character strings).
|
||||
- Pass IDs as path or body fields; do not use integer IDs.
|
||||
|
||||
## Dates and times
|
||||
|
||||
- Stored as `timestamptz` in UTC.
|
||||
- Serialized as ISO 8601 strings in JSON responses.
|
||||
- Token `expire` fields use Unix milliseconds.
|
||||
|
||||
## CORS
|
||||
|
||||
CORS is enabled for all origins in development with credentials support:
|
||||
|
||||
- Methods: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `OPTIONS`
|
||||
- `credentials: true`
|
||||
|
||||
Tighten `origin` in production deployments.
|
||||
|
||||
## Rate limiting
|
||||
|
||||
Global default: **5 requests per 60 seconds** per IP (`ThrottlerModule`).
|
||||
|
||||
Stricter limits on sensitive routes:
|
||||
|
||||
- OTP request: 3 per 180 seconds
|
||||
- Refresh token: custom decorator (`@RefreshTokenRateLimit()`)
|
||||
|
||||
Exceeded limits return `429 Too Many Requests`.
|
||||
|
||||
## Versioning
|
||||
|
||||
The API is currently unversioned (`v1` is implicit). Breaking changes should be coordinated with all clients (admin console, mobile app) before deployment.
|
||||
|
||||
## Adding a new endpoint checklist
|
||||
|
||||
1. Create or extend a DTO with validation and Swagger decorators.
|
||||
2. Add a thin controller method with correct `public/` or `admin/` path.
|
||||
3. Apply the appropriate guard and `@Permissions` if admin-only.
|
||||
4. Implement logic in the service; use repository for queries.
|
||||
5. Return typed data — the response interceptor handles wrapping.
|
||||
6. Document with `@ApiOperation` and related Swagger decorators.
|
||||
7. Add migration if new entities or columns are required.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Architecture
|
||||
|
||||
Negareh API is a NestJS backend built on **Fastify**, **PostgreSQL** (via MikroORM), and **Redis** (caching, OTP, queues). It powers both the customer-facing app and the admin console.
|
||||
|
||||
## Tech stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| Runtime | Node.js, TypeScript |
|
||||
| Framework | NestJS 11 |
|
||||
| HTTP server | Fastify (`@nestjs/platform-fastify`) |
|
||||
| ORM | MikroORM 6 (PostgreSQL driver) |
|
||||
| Auth | JWT + refresh tokens, OTP via SMS |
|
||||
| Cache / OTP | Redis (`@keyv/redis`, `CacheService`) |
|
||||
| Jobs | BullMQ (`@nestjs/bullmq`) |
|
||||
| Real-time | Socket.IO (notifications) |
|
||||
| File storage | AWS S3-compatible bucket |
|
||||
| Payments | Zarinpal gateway |
|
||||
| API docs | Swagger at `/docs` |
|
||||
|
||||
## High-level layout
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.ts # Bootstrap: Fastify, pipes, interceptors, CORS, Swagger
|
||||
├── app.module.ts # Root module — wires all feature modules
|
||||
├── config/ # MikroORM, cache, HTTP, Swagger
|
||||
├── core/ # Cross-cutting: exceptions, interceptors, middlewares
|
||||
├── common/ # Shared entities, decorators, enums, interfaces
|
||||
├── modules/ # Feature modules (domain boundaries)
|
||||
└── seeders/ # Database seed data
|
||||
|
||||
database/
|
||||
└── migrations/ # Versioned schema changes (MikroORM migrations)
|
||||
```
|
||||
|
||||
## Feature modules
|
||||
|
||||
Each domain lives under `src/modules/<name>/` with a consistent internal structure:
|
||||
|
||||
```
|
||||
modules/<feature>/
|
||||
├── <feature>.module.ts
|
||||
├── controllers/ # HTTP layer (thin)
|
||||
├── providers/ # Business logic (services)
|
||||
├── repositories/ # Data access (extends EntityRepository)
|
||||
├── entities/ # MikroORM entities
|
||||
├── dto/ # Request/response validation
|
||||
├── listeners/ # Event handlers (@OnEvent)
|
||||
├── events/ # Domain events (EventEmitter2)
|
||||
└── enum/ # Feature-specific enums
|
||||
```
|
||||
|
||||
Current modules:
|
||||
|
||||
- **auth** — OTP login, JWT, refresh tokens
|
||||
- **user** — End-user accounts and credit
|
||||
- **admin** — Admin accounts
|
||||
- **roles** — Roles and permissions (RBAC)
|
||||
- **product** — Products and categories
|
||||
- **form-builder** — Dynamic product fields and options
|
||||
- **request** — Customer requests / quotes
|
||||
- **invoice** — Invoicing
|
||||
- **order** — Order lifecycle
|
||||
- **payment** — Online payments (Zarinpal)
|
||||
- **print** — Print-related admin operations
|
||||
- **ticket** — Support tickets
|
||||
- **chat** — Order/chat messaging
|
||||
- **notification** — SMS, push, WebSocket notifications
|
||||
- **uploader** — File uploads to S3
|
||||
- **announcements**, **learnings**, **criticisms** — Content and feedback
|
||||
- **util** — Shared utilities (cache, phone normalization)
|
||||
|
||||
## Request lifecycle
|
||||
|
||||
1. **Fastify** receives the HTTP request.
|
||||
2. **ValidationPipe** validates and transforms DTOs (`class-validator` + `class-transformer`).
|
||||
3. **Guards** enforce authentication (`AuthGuard`, `AdminAuthGuard`) and permissions (`@Permissions`).
|
||||
4. **Controller** delegates to a **service**.
|
||||
5. **Service** uses **repositories** and/or `EntityManager` for persistence.
|
||||
6. **ResponseInterceptor** wraps successful responses in a standard envelope.
|
||||
7. **HttpExceptionFilter** formats error responses.
|
||||
|
||||
## Cross-cutting concerns
|
||||
|
||||
### Response envelope
|
||||
|
||||
Successful responses:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 200,
|
||||
"success": true,
|
||||
"data": { }
|
||||
}
|
||||
```
|
||||
|
||||
Paginated responses also include `meta`:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 200,
|
||||
"success": true,
|
||||
"data": [],
|
||||
"meta": { "total": 100, "page": 1, "limit": 10, "totalPages": 10 }
|
||||
}
|
||||
```
|
||||
|
||||
Errors:
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": 400,
|
||||
"success": false,
|
||||
"error": { "message": ["..."] }
|
||||
}
|
||||
```
|
||||
|
||||
### Domain events
|
||||
|
||||
Services emit events via `EventEmitter2`. Listeners in `listeners/` handle side effects (notifications, chat, payment follow-up) without bloating the main flow.
|
||||
|
||||
### Configuration
|
||||
|
||||
Use `ConfigService` for all environment variables. `ConfigModule` is registered globally in `AppModule`. Never read `process.env` directly in application code.
|
||||
|
||||
### Rate limiting
|
||||
|
||||
`@nestjs/throttler` is configured globally (5 requests per 60 seconds). Sensitive endpoints (OTP, refresh token) use stricter per-route limits via `@Throttle` or custom decorators.
|
||||
|
||||
## Design principles
|
||||
|
||||
- **Thin controllers, fat services** — HTTP concerns stay in controllers; business rules live in services.
|
||||
- **Reuse before rewrite** — Search for existing services, DTOs, and repositories before adding new ones.
|
||||
- **Module boundaries** — Features communicate through exported services or domain events, not by reaching into another module's internals.
|
||||
- **No duplicated logic** — Extend existing modules rather than copying patterns.
|
||||
- **Transactions for multi-entity writes** — Use `em.transactional()` when several rows must commit or roll back together.
|
||||
|
||||
## External integrations
|
||||
|
||||
| Integration | Purpose |
|
||||
|-------------|---------|
|
||||
| SMS.ir | OTP and transactional SMS |
|
||||
| Zarinpal | Payment gateway |
|
||||
| S3-compatible storage | File uploads |
|
||||
| Redis | OTP cache, general caching, BullMQ |
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run start:dev # Watch mode on APP_PORT (default 4000)
|
||||
```
|
||||
|
||||
Swagger UI: `http://localhost:4000/docs`
|
||||
|
||||
Database helpers (see [database.md](./database.md)):
|
||||
|
||||
```bash
|
||||
npm run migration:up # Apply pending migrations
|
||||
npm run db:seed # Run seeders
|
||||
npm run db:reset # Drop, create, migrate, seed (destructive)
|
||||
```
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
# Authentication & Authorization
|
||||
|
||||
Negareh API supports two actor types — **users** (customers) and **admins** (back-office). Both authenticate via **phone OTP** and receive **JWT access tokens** with **refresh tokens**.
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
┌─────────────┐ OTP request ┌─────────────┐
|
||||
│ Client │ ──────────────────► │ Auth API │
|
||||
│ │ ◄────────────────── │ (SMS/Redis)│
|
||||
└─────────────┘ OTP code └─────────────┘
|
||||
│ │
|
||||
│ OTP verify │
|
||||
└──────────────────────────────────► │
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Access JWT + Refresh │
|
||||
│ token (hashed in DB) │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
## OTP flow
|
||||
|
||||
### User (public)
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/public/auth/otp/request` | POST | Send OTP to phone (creates user on verify) |
|
||||
| `/public/auth/otp/verify` | POST | Verify OTP → returns tokens + user profile |
|
||||
| `/public/auth/refresh` | POST | Exchange refresh token for new token pair |
|
||||
|
||||
### Admin
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/admin/auth/otp/request` | POST | Send OTP (phone must exist in `admins` table) |
|
||||
| `/admin/auth/otp/verify` | POST | Verify OTP → returns tokens + admin profile |
|
||||
| `/admin/auth/refresh` | POST | Exchange refresh token for new token pair |
|
||||
|
||||
### OTP details
|
||||
|
||||
- Phone numbers are normalized to local format via `normalizePhoneToLocal()` (`09xxxxxxxxx`).
|
||||
- OTP codes are 5-digit numeric values stored in **Redis** with TTL (`OTP_EXPIRATION_TIME`, default 240 seconds).
|
||||
- OTP request endpoints are rate-limited (`@Throttle`: 3 requests per 180 seconds).
|
||||
- SMS delivery uses SMS.ir patterns (configured via `SMS_*` env vars).
|
||||
|
||||
### Request / verify DTOs
|
||||
|
||||
```typescript
|
||||
// Request OTP
|
||||
{ "phone": "09362532122" }
|
||||
|
||||
// Verify OTP
|
||||
{ "phone": "09362532122", "otp": "12345" }
|
||||
```
|
||||
|
||||
## JWT tokens
|
||||
|
||||
### Access token
|
||||
|
||||
- Signed with `JWT_SECRET`.
|
||||
- Expiration: `JWT_EXPIRATION_TIME` (seconds).
|
||||
- Sent as `Authorization: Bearer <token>`.
|
||||
|
||||
**User payload:**
|
||||
|
||||
```typescript
|
||||
{ userId: string }
|
||||
```
|
||||
|
||||
**Admin payload:**
|
||||
|
||||
```typescript
|
||||
{ adminId: string }
|
||||
```
|
||||
|
||||
### Refresh token
|
||||
|
||||
- Opaque 64-character hex string (not a JWT).
|
||||
- Stored **hashed** (SHA-256) in the `refresh_tokens` table.
|
||||
- Expiration: `REFRESH_TOKEN_EXPIRE` (days).
|
||||
- Rotated on each refresh — old token is deleted in the same transaction as the new pair.
|
||||
- Type field distinguishes `user` vs `admin` tokens.
|
||||
|
||||
### Refresh response shape
|
||||
|
||||
```json
|
||||
{
|
||||
"accessToken": { "token": "...", "expire": 1710000000000 },
|
||||
"refreshToken": { "token": "...", "expire": 1711000000000 }
|
||||
}
|
||||
```
|
||||
|
||||
`expire` values are Unix timestamps in milliseconds.
|
||||
|
||||
## Guards
|
||||
|
||||
### `AuthGuard` (users)
|
||||
|
||||
- Applied with `@UseGuards(AuthGuard)` on public routes that require a logged-in user.
|
||||
- Verifies JWT from `Authorization` header.
|
||||
- Sets `request.userId` from the token payload.
|
||||
- Extract identity in controllers with `@UserId()`.
|
||||
|
||||
### `AdminAuthGuard` (admins)
|
||||
|
||||
- Applied with `@UseGuards(AdminAuthGuard)` on admin routes.
|
||||
- Verifies JWT and sets `request.adminId`.
|
||||
- Extract identity with `@AdminId()`.
|
||||
- Reads `@Permissions(...)` metadata from the handler/class for RBAC checks.
|
||||
|
||||
## Role-based access control (RBAC)
|
||||
|
||||
Admins have a **role** with attached **permissions**. Permission names are defined in `PermissionEnum` (e.g. `view_orders`, `create_invoice`, `manage_admins`).
|
||||
|
||||
### Declaring required permissions
|
||||
|
||||
```typescript
|
||||
@Get('admin/orders')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(PermissionEnum.VIEW_ORDERS)
|
||||
findAllAsAdmin(@Query() dto: FindOrdersDto) {
|
||||
return this.orderService.findOrdersAsAdmin(dto);
|
||||
}
|
||||
```
|
||||
|
||||
The `@Permissions()` decorator stores required permission keys in metadata. `AdminAuthGuard` reads them via `Reflector`.
|
||||
|
||||
> **Note:** Permission enforcement against the database/cache is partially implemented. The guard structure and decorators are in place; ensure permission checks are active before relying on them in production.
|
||||
|
||||
## Route prefixes
|
||||
|
||||
Routes are namespaced by audience in the controller path (not a global prefix):
|
||||
|
||||
| Prefix | Audience | Guard |
|
||||
|--------|----------|-------|
|
||||
| `public/` | End users | `AuthGuard` (when protected) |
|
||||
| `admin/` | Back-office | `AdminAuthGuard` |
|
||||
|
||||
Unauthenticated routes use `public/auth/...` or `admin/auth/...` without guards.
|
||||
|
||||
## WebSocket auth
|
||||
|
||||
Admin WebSocket connections use `WsAdminAuthGuard` in the notifications module. Follow the same JWT verification pattern as HTTP admin auth.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `JWT_SECRET` | Signing key for access tokens |
|
||||
| `JWT_EXPIRATION_TIME` | Access token TTL (seconds) |
|
||||
| `REFRESH_TOKEN_EXPIRE` | Refresh token TTL (days) |
|
||||
| `OTP_EXPIRATION_TIME` | OTP cache TTL (seconds) |
|
||||
| `REDIS_URI` / `REDIS_HOST` | OTP and cache storage |
|
||||
| `SMS_BASE_URL`, `SMS_API_KEY`, `SMS_PATTERN_OTP` | SMS delivery |
|
||||
|
||||
## Security practices
|
||||
|
||||
- Refresh tokens are hashed at rest; raw tokens are only returned once to the client.
|
||||
- Token refresh runs inside a database transaction to prevent race conditions.
|
||||
- Rate limiting on OTP and refresh endpoints reduces brute-force risk.
|
||||
- Never log tokens, OTP codes, or secrets.
|
||||
- Use HTTPS in production; CORS is enabled with `credentials: true` for cookie support if needed.
|
||||
|
||||
## Client integration checklist
|
||||
|
||||
1. Request OTP with normalized Iranian mobile number.
|
||||
2. Verify OTP and store both `accessToken.token` and `refreshToken.token`.
|
||||
3. Send `Authorization: Bearer <accessToken>` on protected requests.
|
||||
4. On 401, call the refresh endpoint with the refresh token.
|
||||
5. Replace stored tokens with the new pair from the refresh response.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Coding Style
|
||||
|
||||
Conventions for writing and reviewing code in Negareh API. These align with the project's Cursor rules and existing patterns.
|
||||
|
||||
## TypeScript
|
||||
|
||||
- **Strict typing** — Never use `any`. Prefer `unknown` when the type is genuinely unknown, then narrow.
|
||||
- **Exported functions** — Add explicit return types.
|
||||
- **Interfaces for DTOs** — Use classes with decorators for request bodies; interfaces for internal shapes and transformers.
|
||||
- **Enums** — Use only when a fixed set of values is shared across layers (e.g. `PermissionEnum`, status enums). Avoid enums for one-off string unions.
|
||||
- **Readonly** — Prefer `readonly` on constructor-injected dependencies and immutable fields.
|
||||
- **Function size** — Keep functions under ~40 lines when practical; extract private methods for clarity.
|
||||
|
||||
## NestJS patterns
|
||||
|
||||
### Dependency injection
|
||||
|
||||
- Use **constructor injection** for all dependencies.
|
||||
- Register providers in the feature module; export only what other modules need.
|
||||
- Use `forwardRef()` when circular module dependencies are unavoidable (e.g. `AuthModule` ↔ `UserModule`).
|
||||
|
||||
### Controllers
|
||||
|
||||
- Keep controllers **thin** — validate input, call service, return result.
|
||||
- Use `@ApiTags`, `@ApiOperation`, and `@ApiBearerAuth` for Swagger.
|
||||
- Apply guards at method level: `@UseGuards(AuthGuard)` or `@UseGuards(AdminAuthGuard)`.
|
||||
- Extract identity from decorators: `@UserId()`, `@AdminId()`.
|
||||
|
||||
### Services
|
||||
|
||||
- All **business logic** belongs in services (`providers/`).
|
||||
- Throw NestJS `HttpException` subclasses (`BadRequestException`, `NotFoundException`, etc.).
|
||||
- Use `ConfigService` instead of `process.env`.
|
||||
|
||||
### DTOs
|
||||
|
||||
Every request body and query object must have a DTO class:
|
||||
|
||||
```typescript
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class ExampleDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'value' })
|
||||
@Transform(({ value }) => value?.trim())
|
||||
name: string;
|
||||
}
|
||||
```
|
||||
|
||||
- Use `class-validator` decorators for validation.
|
||||
- Use `class-transformer` (`@Transform`) for normalization (e.g. phone numbers).
|
||||
- Document fields with `@ApiProperty` for Swagger.
|
||||
|
||||
## File and naming conventions
|
||||
|
||||
| Artifact | Convention | Example |
|
||||
|----------|------------|---------|
|
||||
| Module | `<feature>.module.ts` | `order.module.ts` |
|
||||
| Controller | `<feature>.controller.ts` in `controllers/` | `order.controller.ts` |
|
||||
| Service | `<feature>.service.ts` in `providers/` | `order.service.ts` |
|
||||
| Repository | `<entity>.repository.ts` in `repositories/` | `order.repository.ts` |
|
||||
| Entity | `<entity>.entity.ts` in `entities/` | `order.entity.ts` |
|
||||
| DTO | `<action>-<entity>.dto.ts` in `dto/` | `create-order.dto.ts` |
|
||||
| Event | `<Entity><Action>Event` in `events/` | `OrderCreatedEvent` |
|
||||
| Listener | `<feature>.listeners.ts` in `listeners/` | `order.listeners.ts` |
|
||||
|
||||
- Use **kebab-case** for file names.
|
||||
- Use **PascalCase** for classes, **camelCase** for methods and variables.
|
||||
- Folder names: `controllers`, `providers`, `repositories`, `entities`, `dto`, `listeners`, `events`, `enum`.
|
||||
|
||||
## MikroORM usage
|
||||
|
||||
- Prefer **EntityRepository** subclasses over raw `EntityManager` queries in services.
|
||||
- Use **`populate`** instead of manual joins.
|
||||
- Use **`wrap(entity).assign(dto)`** for partial updates.
|
||||
- Use **`em.transactional()`** when updating multiple entities atomically.
|
||||
- Use **`Collection`** for `OneToMany` relations.
|
||||
- Avoid **N+1** — batch loads and selective `populate`.
|
||||
- Avoid **raw SQL** unless necessary (triggers, complex reports). Migrations are the exception.
|
||||
- Type repository injections explicitly.
|
||||
|
||||
## Performance
|
||||
|
||||
- Minimize database round-trips; batch where possible.
|
||||
- Paginate large lists — return `PaginatedResult<T>` with `data` and `meta`.
|
||||
- Select only required columns; avoid loading full entity graphs when a subset suffices.
|
||||
- Index foreign keys and frequently filtered columns.
|
||||
|
||||
## Error handling
|
||||
|
||||
- Use specific HTTP exceptions with clear messages.
|
||||
- Shared user-facing messages live in `src/common/enums/message.enum.ts`.
|
||||
- Do not swallow errors silently; log unexpected failures with NestJS `Logger`.
|
||||
|
||||
## Formatting and linting
|
||||
|
||||
```bash
|
||||
npm run format # Prettier
|
||||
npm run lint # ESLint (with fix)
|
||||
npm run build # TypeScript compile check
|
||||
```
|
||||
|
||||
Match the style of surrounding code. Do not reformat unrelated files in the same change.
|
||||
|
||||
## Before adding new code
|
||||
|
||||
1. Search for an existing implementation in `src/modules/` and `src/common/`.
|
||||
2. Reuse existing services, DTOs, and repositories.
|
||||
3. Follow the module's existing folder layout.
|
||||
4. Extend rather than duplicate.
|
||||
@@ -0,0 +1,145 @@
|
||||
# Database
|
||||
|
||||
Negareh API uses **PostgreSQL** with **MikroORM 6**. Schema changes are managed through migrations — never rely on `synchronize` in production.
|
||||
|
||||
## Connection
|
||||
|
||||
Configuration lives in `src/config/mikro-orm.config.ts` and reads from environment variables:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `DB_HOST` | PostgreSQL host |
|
||||
| `DB_PORT` | PostgreSQL port |
|
||||
| `DB_USER` | Database user |
|
||||
| `DB_PASS` | Database password |
|
||||
| `DB_NAME` | Database name |
|
||||
| `NODE_ENV` | `production` disables auto schema ensure |
|
||||
|
||||
In non-production, MikroORM can auto-create/update the database schema on startup (`ensureDatabase`). In production, only migrations should change the schema.
|
||||
|
||||
## Entity conventions
|
||||
|
||||
All persistent models extend `BaseEntity`:
|
||||
|
||||
```typescript
|
||||
// src/common/entities/base.entity.ts
|
||||
@Filter({ name: 'notDeleted', cond: { deletedAt: null }, default: true })
|
||||
export abstract class BaseEntity {
|
||||
@PrimaryKey({ type: 'string', columnType: 'char(26)' })
|
||||
id: string = ulid();
|
||||
|
||||
@Property({ defaultRaw: 'now()', columnType: 'timestamptz' })
|
||||
createdAt: Date = new Date();
|
||||
|
||||
@Property({ nullable: true, columnType: 'timestamptz' })
|
||||
deletedAt?: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
- **Primary keys** — ULID strings (`char(26)`), not auto-increment integers.
|
||||
- **Timestamps** — `timestamptz` with UTC (`forceUtcTimezone: true` in config).
|
||||
- **Soft deletes** — Set `deletedAt` instead of hard-deleting rows. The `notDeleted` filter excludes soft-deleted records by default.
|
||||
- **Indexes** — Add `@Index` on foreign keys and columns used in `WHERE` / `ORDER BY` clauses.
|
||||
- **Table names** — Explicit `tableName` in `@Entity({ tableName: 'users' })` when the name differs from the class.
|
||||
|
||||
### Relations
|
||||
|
||||
- Use MikroORM decorators: `@ManyToOne`, `@OneToMany`, `@ManyToMany`.
|
||||
- Initialize `OneToMany` with `new Collection<T>(this)`.
|
||||
- Use `populate` in queries to load relations and avoid N+1.
|
||||
|
||||
### Updates
|
||||
|
||||
```typescript
|
||||
wrap(entity).assign(partialDto);
|
||||
await em.flush();
|
||||
```
|
||||
|
||||
## Repositories
|
||||
|
||||
Custom data access extends `EntityRepository<Entity>`:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class UserRepository extends EntityRepository<User> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, User);
|
||||
}
|
||||
|
||||
async findAllPaginated(dto: FindUsersDto): Promise<PaginatedResult<User>> {
|
||||
// build FilterQuery, findAndCount with limit/offset
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register repositories as providers in the feature module alongside `MikroOrmModule.forFeature([Entity])`.
|
||||
|
||||
## Migrations
|
||||
|
||||
Migrations live in `database/migrations/` and are emitted as TypeScript.
|
||||
|
||||
### Commands
|
||||
|
||||
| Script | Action |
|
||||
|--------|--------|
|
||||
| `npm run migration:create` | Generate migration from entity diff |
|
||||
| `npm run migration:blank` | Create empty migration file |
|
||||
| `npm run migration:up` | Apply pending migrations |
|
||||
| `npm run migration:down` | Revert last migration |
|
||||
| `npm run migration:list` | List all migrations |
|
||||
| `npm run migration:pending` | Show unapplied migrations |
|
||||
| `npm run migration:fresh` | Drop and re-run all migrations |
|
||||
|
||||
### Guidelines
|
||||
|
||||
- **Always use migrations** for schema changes in shared environments.
|
||||
- Migrations are **transactional** (`allOrNothing: true`).
|
||||
- `safe: true` prevents destructive drops in generated migrations — review each file before applying.
|
||||
- Complex logic (triggers, functions) belongs in migrations — see `Migration20260626140000` for invoice total recalculation triggers.
|
||||
- **Never remove data** in migrations unless explicitly requested.
|
||||
|
||||
### Workflow
|
||||
|
||||
1. Modify entity files.
|
||||
2. Run `npm run migration:create`.
|
||||
3. Review the generated SQL in `database/migrations/`.
|
||||
4. Run `npm run migration:up`.
|
||||
5. Commit the migration file with the entity changes.
|
||||
|
||||
## Seeders
|
||||
|
||||
Seeders populate development/staging data. Entry point: `src/seeders/DatabaseSeeder.ts`.
|
||||
|
||||
```bash
|
||||
npm run db:seed # Run all seeders
|
||||
npm run db:reset # Drop + create + migrate + seed (destructive)
|
||||
```
|
||||
|
||||
Seeder structure:
|
||||
|
||||
```
|
||||
src/seeders/
|
||||
├── DatabaseSeeder.ts # Orchestrates seed order
|
||||
├── <entity>.seeder.ts # Per-entity seed logic
|
||||
└── data/ # Static seed data arrays
|
||||
```
|
||||
|
||||
Seed order matters — permissions and roles must exist before admins; categories before products.
|
||||
|
||||
## Query guidelines
|
||||
|
||||
- Avoid `SELECT *` in raw queries; MikroORM entity loads are fine but use `fields` option when you only need a subset.
|
||||
- Design filters to hit indexes (`$ilike` on indexed columns sparingly; prefer exact match on indexed fields).
|
||||
- Use `findAndCount` for paginated lists.
|
||||
- Wrap multi-step writes in `em.transactional(async (em) => { ... })`.
|
||||
|
||||
## Connection pool
|
||||
|
||||
Pool settings are tuned per environment in `mikro-orm.config.ts`:
|
||||
|
||||
- Production: min 5, max 20 connections
|
||||
- Development: min 2, max 10 connections
|
||||
|
||||
Statement and idle timeouts are set to 60 seconds to prevent hung transactions.
|
||||
Reference in New Issue
Block a user