| 1 | # Project Development Standards |
| 2 | |
| 3 | ## General Principles |
| 4 | |
| 5 | - Type safety with input/output separation: Use DTOs only for request validation and transformation; use VOs only for response encapsulation. |
| 6 | - Clear layer responsibilities: Controller handles routing/parameter binding/response transformation only; Service handles business orchestration and permission filtering; Repository handles data access only, without business logic or permission checks. |
| 7 | - Unified exceptions: Business errors use AppException + ResponseCode; protocol-level errors (for example rate limit/auth transport semantics) may use standard HttpException; global filter ensures unified response format. |
| 8 | |
| 9 | ## Naming & Files |
| 10 | |
| 11 | - Classes/Interfaces/Enums use PascalCase; variables/functions use camelCase; constants use UPPER_SNAKE_CASE. |
| 12 | - File suffixes: `*.controller.ts` / `*.service.ts` / `*.module.ts` / `*.dto.ts` / `*.vo.ts` / `*.repository.ts`. |
| 13 | - File names must use kebab-case; camelCase is prohibited: `platform-rules.constants.ts`, not `platformRules.config.ts`. |
| 14 | - `*.config.ts` files are only for zod configuration; not allowed for constants, utils, or interfaces. |
| 15 | - Do not arbitrarily nest folders like `dto/`; create `*.dto.ts` files directly in the module root directory. |
| 16 | |
| 17 | ## Repository Method Naming |
| 18 | |
| 19 | - Methods must start with these prefixes: `get` / `list` / `create` / `update` / `delete` / `count` |
| 20 | - Aggregation methods may start with aggregation keywords: `aggregate` / `sum` / `avg` etc. |
| 21 | - Batch operations use `createMany` / `updateMany` / `deleteMany` format |
| 22 | - Methods returning a single value must start with `get`, format: `getByXxx` |
| 23 | - Methods returning arrays must start with `list`, format: `listByXxx` |
| 24 | - Methods returning counts must start with `count`, format: `countByXxx` |
| 25 | - Pagination methods must end with `WithPagination`, e.g., `listWithPagination` |
| 26 | - Non-standard prefixes are prohibited: `find` / `del` / `add` / `set` / `check` etc. |
| 27 | - Business verbs as prefixes are prohibited: `verify` / `mark` / `publish` etc. |
| 28 | |
| 29 | ### Examples |
| 30 | |
| 31 | - ✅ `getById` / `getByUserId` / `getByIdAndStatus` |
| 32 | - ✅ `listByUserId` / `listWithPagination` / `listByStatus` |
| 33 | - ✅ `create` / `createMany` / `createByUser` |
| 34 | - ✅ `updateById` / `updateByStatus` / `updateManyByIds` |
| 35 | - ✅ `deleteById` / `deleteByUserId` / `deleteManyByIds` |
| 36 | - ✅ `countByUserId` / `countByStatus` / `aggregateByDate` |
| 37 | - ❌ `findById` → `getById` |
| 38 | - ❌ `findList` → `listWithPagination` |
| 39 | - ❌ `delOne` → `deleteById` |
| 40 | - ❌ `addUseCount` → `updateUseCountById` |
| 41 | - ❌ `verify` → `updateVerifyById` |
| 42 | - ❌ `markAsRead` → `updateAsReadByIds` |
| 43 | |
| 44 | ## DTO (Input) |
| 45 | |
| 46 | - Write zod schema first, then use `createZodDto(schema, 'IdString')` to generate DTO; using entities as input is prohibited. |
| 47 | - Pagination input must use `PaginationDtoSchema` (page ≥1, pageSize ∈[1,1000], string numbers auto-convert). |
| 48 | |
| 49 | ```ts |
| 50 | import { createZodDto, PaginationDtoSchema } from '@yikart/common'; |
| 51 | import { z } from 'zod'; |
| 52 | |
| 53 | export const CreateOrderDtoSchema = z.object({ |
| 54 | productId: z.string(), |
| 55 | quantity: z.number().int().positive().default(1), |
| 56 | returnTo: z.url().optional(), |
| 57 | }); |
| 58 | export class CreateOrderDto extends createZodDto(CreateOrderDtoSchema, 'CreateOrderDto') {} |
| 59 | ``` |
| 60 | |
| 61 | ## VO (Output) |
| 62 | |
| 63 | - VOs expose only stable external fields; mapping is done in Service, Controller outputs using `VoClass.create(data)` (regular VO); returning database entities directly is prohibited. |
| 64 | - Pagination responses must use `createPaginationVo` (pagination VO instantiation uses `new`), with fields: page, pageSize, totalPages, total, list. |
| 65 | |
| 66 | ```ts |
| 67 | import { createPaginationVo, createZodDto } from '@yikart/common'; |
| 68 | import { z } from 'zod'; |
| 69 | |
| 70 | export const OrderDetailVoSchema = z.object({ id: z.string(), amount: z.number(), createdAt: z.coerce.date() }); |
| 71 | export class OrderDetailVo extends createZodDto(OrderDetailVoSchema, 'OrderDetailVo') {} |
| 72 | export class OrderListVo extends createPaginationVo(OrderDetailVoSchema, 'OrderListVo') {} |
| 73 | ``` |
| 74 | |
| 75 | ## API Documentation (Swagger) |
| 76 | |
| 77 | - Every Controller must have `@ApiTags('Category/Module')` decorator for grouping |
| 78 | - Every Controller method must have `@ApiDoc` decorator for documentation |
| 79 | - DTO/VO schema fields must use `.describe('description')` for field documentation |
| 80 | |
| 81 | ### Controller Documentation |
| 82 | |
| 83 | ```ts |
| 84 | import { ApiTags } from '@nestjs/swagger' |
| 85 | import { ApiDoc } from '@yikart/common' |
| 86 | |
| 87 | @ApiTags('AI/Material-Adaptation') |
| 88 | @Controller('/material-adaptation') |
| 89 | export class MaterialAdaptationController { |
| 90 | |
| 91 | @ApiDoc({ |
| 92 | summary: '适配素材到多个平台', |
| 93 | description: '使用 AI 将素材内容适配到指定的社交媒体平台', |
| 94 | body: AdaptMaterialDtoSchema, |
| 95 | response: [MaterialAdaptationVo], |
| 96 | }) |
| 97 | @Post('/') |
| 98 | async adaptMaterial(...): Promise<MaterialAdaptationVo[]> { ... } |
| 99 | } |
| 100 | ``` |
| 101 | |
| 102 | ### DTO/VO Field Documentation |
| 103 | |
| 104 | ```ts |
| 105 | export const CreateOrderDtoSchema = z.object({ |
| 106 | productId: z.string().describe('产品 ID'), |
| 107 | quantity: z.number().int().positive().default(1).describe('购买数量'), |
| 108 | returnTo: z.url().optional().describe('回调地址'), |
| 109 | }); |
| 110 | |
| 111 | export const OrderDetailVoSchema = z.object({ |
| 112 | id: z.string().describe('订单 ID'), |
| 113 | amount: z.number().describe('订单金额'), |
| 114 | createdAt: z.coerce.date().describe('创建时间'), |
| 115 | }); |
| 116 | ``` |
| 117 | |
| 118 | ### ApiDoc Options |
| 119 | |
| 120 | - `summary` (required): Brief description of the endpoint |
| 121 | - `description` (optional): Detailed description |
| 122 | - `body` (optional): Request body Zod schema (use `XxxDtoSchema`, not the class) |
| 123 | - `query` (optional): Query parameters Zod schema |
| 124 | - `response` (optional): Response VO class or `[VoClass]` for array response |
| 125 | |
| 126 | ## Exceptions & Error Codes |
| 127 | |
| 128 | - Business exceptions use `new AppException(code)` or `new AppException(code, data)`; messages are generated from code→message mapping, custom overrides are prohibited. |
| 129 | |
| 130 | ```ts |
| 131 | import { AppException, ResponseCode } from '@yikart/common'; |
| 132 | |
| 133 | throw new AppException(ResponseCode.MaterialGroupNotFound, { groupId: 'group_xxx' }); |
| 134 | ``` |
| 135 | |
| 136 | ## ResponseCode Standards |
| 137 | |
| 138 | - Success code is fixed at `Success = 0`; business error codes start from `10000` and are allocated by module range, cross-module reuse is prohibited. |
| 139 | - Naming uses PascalCase and must specify the concrete resource: e.g., `ContractNotFound`, `CommentNotFound`; generic permission names (`Unauthorized`/`AccessDenied` etc.) are prohibited. |
| 140 | - Define and export only in the common package; all services reference the same source to avoid scattered definitions. |
| 141 | - Must maintain code→default message mapping; use "Unknown error" for unmatched codes. |
| 142 | - Collaboration with AppException: pass only `code` or `code+data`, custom messages are not allowed (messages are generated from mapping). |
| 143 | - Addition workflow: Add constant to `ResponseCode` → Add default message to message mapping → Use in business code. |
| 144 | |
| 145 | ## Permissions & Data Access |
| 146 | |
| 147 | - Permissions are filtered through query conditions; prefer specific resource NotFound over generic permission exceptions; permission logic belongs in Service layer. |
| 148 | - Repository only accesses its own data model; cross-model operations, permission checks, and extra existence queries are prohibited (existence checks are done by Service first). |
| 149 | |
| 150 | ## Data Filtering & Aggregation |
| 151 | |
| 152 | - All filtering, counting, and aggregation MUST be done at database layer (Repository), not in application code. |
| 153 | - Prohibited: fetching a list from DB then using `.filter()` / `.some()` / `.find()` / `.reduce()` to narrow results in memory. |
| 154 | - Use Repository methods with proper query conditions instead. |
| 155 | |
| 156 | ```typescript |
| 157 | // WRONG — filtering in memory |
| 158 | const records = await this.repo.listByUserId(userId) |
| 159 | const pending = records.filter(r => r.status === 'pending') |
| 160 | const total = pending.reduce((sum, r) => sum + r.amount, 0) |
| 161 | |
| 162 | // CORRECT — filtering at database layer |
| 163 | const pending = await this.repo.listByUserIdAndStatus(userId, 'pending') |
| 164 | const total = await this.repo.sumAmountByUserIdAndStatus(userId, 'pending') |
| 165 | ``` |
| 166 | |
| 167 | - Exception: filtering on external API responses is acceptable since the data is not from our DB. |
| 168 | |
| 169 | ## Logging & Lint |
| 170 | |
| 171 | - Using console is prohibited; use dependency-injected Logger instance (e.g., `this.logger.log()`). |
| 172 | - Strictly follow ESLint (root `eslint.config.mjs`); must pass `pnpm lint -w` and type checking before commit. |
| 173 | |
| 174 | ## Mandatory Rules (Hard Requirements) |
| 175 | |
| 176 | - Controller handles routing/parameter binding/response transformation only; Service handles business orchestration and permission filtering; Repository handles data access only. |
| 177 | - Controller output must use VOs uniformly (regular VO uses `VoClass.create(data)`; pagination VO uses `new`); Service returns entity data, Controller converts to VO, not the reverse. |
| 178 | - Pagination Service methods must return `[list, total]` tuple directly from Repository (transparent pass-through); `new ListVo()` is only called in Controller; field mapping, default value filling, and field renaming are done inside `new ListVo(list.map(...), total, pagination)` in Controller; if no mapping is needed, pass `list` directly. |
| 179 | - VO schema optional fields must use `.optional()`; `.nullable()` is only for fields that can be explicitly set to `null` in business logic; DB `required: false` / `field?: Type` must map to `.optional()`, not `.nullable()`. |
| 180 | - DTO/VO must be defined with zod schema and generated using `createZodDto` / `createPaginationVo`. |
| 181 | - Pagination: input uses `PaginationDtoSchema`; output fields are fixed as page, pageSize, totalPages, total, list. |
| 182 | - AppException is constructed only with code or code+data, messages come from mapping; standard HttpException is reserved for protocol-layer errors. |
| 183 | - Permissions are filtered through query conditions; unmatched results are represented as specific resource NotFound; permission logic is in Service layer. |
| 184 | - Prefer soft delete (`deletedAt`); state transitions are handled in Service layer; avoid complex state enums. |
| 185 | - Statistics/counts are implemented at database layer, application-layer iteration aggregation is prohibited. |
| 186 | - HTTP decorator paths must start with `/`, empty parameters are prohibited; methods with pagination must end with `WithPagination`. |
| 187 | - Every Controller must have `@ApiTags` decorator; every Controller method must have `@ApiDoc` decorator. |
| 188 | - All DTO/VO schema fields must have `.describe()` for API documentation. |
| 189 | |
| 190 | ## Prohibited Practices (Hard Requirements) |
| 191 | |
| 192 | - Writing business logic in Controller or directly accessing database; returning database entities directly; skipping DTO/VO. |
| 193 | - Injecting Repository in Controller; data must be accessed through Service layer; Controller must not contain complex logic. |
| 194 | - Performing permission checks, cross-model operations, or extra existence queries in Repository; Repository methods containing unnecessary parameters. |
| 195 | - Using generic permission exception names (Unauthorized/PermissionDenied/AccessDenied) instead of specific resource NotFound. |
| 196 | - Overriding AppException default messages or creating custom business exception types; customizing HTTP status codes to represent business errors. |
| 197 | - Using `throw new Error()` instead of `AppException + ResponseCode` for business errors; unexpected infrastructure exceptions may be propagated to the global filter directly. |
| 198 | - Using Logger static methods or `console`; reading environment variables directly (must go through config module). |
| 199 | - Using `as any` or explicitly bypassing type checking; disabling/ignoring Lint during development. |
| 200 | - Using `z.nativeEnum`; must use `z.enum` and explicitly list enum values. |
| 201 | - Arbitrarily wrapping business logic with try-catch; exceptions should be handled uniformly by the framework. |
| 202 | - Delete operations returning meaningless objects like `{ success: true }`; should return `void`. |
| 203 | - Using wrong HTTP methods: delete uses `@Delete()` not `@Put()`; update uses `@Patch()` not `@Put()`. |
| 204 | - Designing complex state enums; doing statistics/aggregation iteration at application layer; pagination methods not following naming conventions. |
| 205 | - HTTP decorators not starting with `/` or being empty; Controller methods not following naming conventions. |
| 206 | - Writing redundant comments; code should be self-explanatory, avoid unnecessary comments explaining obvious logic. |
| 207 | - Creating Controller methods without `@ApiDoc` decorator; creating DTO/VO schema fields without `.describe()`. |
| 208 | - Writing data migration logic as NestJS Service/Controller; migrations must be pure MongoDB shell scripts placed in `migrations/` directory, executed via `mongosh`. |
| 209 | - Using `@InjectModel` or `@InjectConnection` in Service files; data access must go through Repository layer. |
| 210 | - Pagination Service methods doing `list.map(item => ({...}))` entity mapping for response fields; Service method return values containing `pageNo`/`pageSize` pagination parameters (Controller already has these). |
| 211 | - Using `.nullable()` for DB optional fields (`required: false` / `field?: Type`) instead of `.optional()`; this causes type mismatch and forces Controller to add `?? null` conversions. |
| 212 | |
| 213 | ## Build Verification |
| 214 | |
| 215 | - Must use `pnpm nx build <project>` to verify builds |
| 216 | - Using `tsc` directly for compilation is prohibited |
| 217 | - After renaming methods, must run `pnpm nx build` to ensure all references are updated |
| 218 | - Before merging, ensure all builds pass: `pnpm nx run-many --target=build --all` |
| 219 |