From dcd84551065f82109e50a316b4e81b3ef4eae70f Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 2 Jul 2026 15:59:46 +0330 Subject: [PATCH] add docs --- docs/api-conventions.md | 219 ++++++++++++++++++++++++++++++++++++++++ docs/architecture.md | 163 ++++++++++++++++++++++++++++++ docs/auth.md | 171 +++++++++++++++++++++++++++++++ docs/coding-style.md | 113 +++++++++++++++++++++ docs/database.md | 145 ++++++++++++++++++++++++++ 5 files changed, 811 insertions(+) create mode 100644 docs/api-conventions.md create mode 100644 docs/architecture.md create mode 100644 docs/auth.md create mode 100644 docs/coding-style.md create mode 100644 docs/database.md diff --git a/docs/api-conventions.md b/docs/api-conventions.md new file mode 100644 index 0000000..5834160 --- /dev/null +++ b/docs/api-conventions.md @@ -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 ` | 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`; 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. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..745f991 --- /dev/null +++ b/docs/architecture.md @@ -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//` with a consistent internal structure: + +``` +modules// +├── .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) +``` diff --git a/docs/auth.md b/docs/auth.md new file mode 100644 index 0000000..cc3d6cc --- /dev/null +++ b/docs/auth.md @@ -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 `. + +**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 ` 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. diff --git a/docs/coding-style.md b/docs/coding-style.md new file mode 100644 index 0000000..e7a5576 --- /dev/null +++ b/docs/coding-style.md @@ -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 | `.module.ts` | `order.module.ts` | +| Controller | `.controller.ts` in `controllers/` | `order.controller.ts` | +| Service | `.service.ts` in `providers/` | `order.service.ts` | +| Repository | `.repository.ts` in `repositories/` | `order.repository.ts` | +| Entity | `.entity.ts` in `entities/` | `order.entity.ts` | +| DTO | `-.dto.ts` in `dto/` | `create-order.dto.ts` | +| Event | `Event` in `events/` | `OrderCreatedEvent` | +| Listener | `.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` 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. diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 0000000..e02d126 --- /dev/null +++ b/docs/database.md @@ -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(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`: + +```typescript +@Injectable() +export class UserRepository extends EntityRepository { + constructor(readonly em: EntityManager) { + super(em, User); + } + + async findAllPaginated(dto: FindUsersDto): Promise> { + // 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 +├── .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.