# Backend Development Guidelines **สำหรับ:** NAP-DMS LCBP3 Backend (NestJS + TypeScript) **เวอร์ชัน:** 1.5.0 **อัปเดต:** 2025-12-01 --- ## 🎯 หลักการพื้นฐาน ระบบ Backend ของเรามุ่งเน้น **"Data Integrity First"** - ความถูกต้องของข้อมูลต้องมาก่อน ตามด้วย Security และ UX ### หลักการหลัก 1. **Strict Typing:** ใช้ TypeScript เต็มรูปแบบ ห้ามใช้ `any` 2. **Data Integrity:** ป้องกัน Race Condition ด้วย Optimistic Locking + Redis Lock 3. **Security First:** ทุก Endpoint ต้องผ่าน Authentication, Authorization, และ Input Validation 4. **Idempotency:** Request สำคัญต้องทำซ้ำได้โดยไม่เกิดผลกระทบซ้ำซ้อน 5. **Resilience:** รองรับ Network Failure และ External Service Downtime --- ## 📁 โครงสร้างโปรเจกต์ ``` backend/ ├── src/ │ ├── common/ # Shared utilities │ │ ├── decorators/ # Custom decorators │ │ ├── dtos/ # Common DTOs │ │ ├── entities/ # Base entities │ │ ├── filters/ # Exception filters │ │ ├── guards/ # Auth guards, RBAC │ │ ├── interceptors/ # Logging, transform, idempotency │ │ ├── interfaces/ # Common interfaces │ │ └── utils/ # Helper functions │ ├── config/ # Configuration management │ ├── database/ │ │ ├── migrations/ │ │ └── seeds/ │ ├── modules/ # Business modules (domain-driven) │ │ ├── auth/ │ │ ├── circulation/ │ │ ├── correspondence/ │ │ ├── dashboard/ │ │ ├── document-numbering/ │ │ ├── drawing/ │ │ ├── json-schema/ │ │ ├── master/ │ │ ├── monitoring/ │ │ ├── notification/ │ │ ├── organizations/ │ │ ├── project/ │ │ ├── rfa/ │ │ ├── search/ │ │ ├── transmittal/ │ │ ├── user/ │ │ └── workflow-engine/ │ ├── app.module.ts │ └── main.ts ├── test/ # E2E tests └── scripts/ # Utility scripts ``` --- ## 🔐 Security Guidelines ### 1. Authentication & Authorization **JWT Authentication:** ```typescript // ใช้ @UseGuards(JwtAuthGuard) สำหรับ Protected Routes @Controller('projects') @UseGuards(JwtAuthGuard) export class ProjectController { // ... } ``` **RBAC (4 ระดับ):** ```typescript // ใช้ @RequirePermission() Decorator @Post(':id/contracts') @RequirePermission('contract.create', { scope: 'project' }) async createContract() { // Level 1: Global Permission // Level 2: Organization Permission // Level 3: Project Permission // Level 4: Contract Permission } ``` ### 2. Input Validation **ใช้ DTOs พร้อม class-validator:** ```typescript import { IsNotEmpty, IsUUID, MaxLength } from 'class-validator'; export class CreateCorrespondenceDto { @IsNotEmpty({ message: 'ต้องระบุโปรเจกต์' }) @IsUUID('4', { message: 'รูปแบบ Project ID ไม่ถูกต้อง' }) project_id: string; @IsNotEmpty() @MaxLength(500) title: string; } ``` ### 3. Rate Limiting ```typescript // กำหนด Rate Limit ตาม User Type @UseGuards(RateLimitGuard) @RateLimit({ points: 100, duration: 3600 }) // 100 requests/hour @Post('upload') async uploadFile() { } ``` ### 4. Secrets Management - **Production:** ใช้ Docker Environment Variables (ไม่ใส่ใน docker-compose.yml) - **Development:** ใช้ `docker-compose.override.yml` (gitignored) - **Validation:** Validate Environment Variables ตอน Start App ```typescript // src/common/config/env.validation.ts import * as Joi from 'joi'; export const envValidationSchema = Joi.object({ DATABASE_URL: Joi.string().required(), JWT_SECRET: Joi.string().min(32).required(), REDIS_URL: Joi.string().required(), }); ``` --- ## 🗄️ Database Best Practices ### 1. Optimistic Locking **ใช้ @VersionColumn() ป้องกัน Race Condition:** ```typescript @Entity() export class DocumentNumberCounter { @PrimaryGeneratedColumn('uuid') id: string; @Column() last_number: number; @VersionColumn() // Auto-increment on update version: number; } ``` ### 2. Virtual Columns สำหรับ JSON **สร้าง Index สำหรับ JSON field ที่ใช้ Search บ่อย:** ```sql -- Migration Script ALTER TABLE correspondence_revisions ADD COLUMN ref_project_id INT GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(details, '$.projectId'))) VIRTUAL; CREATE INDEX idx_ref_project_id ON correspondence_revisions(ref_project_id); ``` ### 3. Soft Delete ```typescript // Base Entity @Entity() export abstract class BaseEntity { @PrimaryGeneratedColumn('uuid') id: string; @CreateDateColumn() created_at: Date; @UpdateDateColumn() updated_at: Date; @DeleteDateColumn() deleted_at: Date; // NULL = Active, NOT NULL = Soft Deleted } ``` --- ## 📦 Core Modules ### 1. DocumentNumberingModule **Double-Lock Mechanism:** ```typescript @Injectable() export class DocumentNumberingService { async generateNextNumber(context: NumberingContext): Promise { const lockKey = `doc_num:${context.projectId}:${context.typeId}`; // Layer 1: Redis Lock (2-5 seconds TTL) const lock = await this.redisLock.acquire(lockKey, 3000); try { // Layer 2: Optimistic DB Lock const counter = await this.counterRepo.findOne({ where: context, lock: { mode: 'optimistic' }, }); counter.last_number++; await this.counterRepo.save(counter); // Throws if version changed return this.formatNumber(counter); } finally { await lock.release(); } } } ``` ### 2. FileStorageService (Two-Phase) **Phase 1: Upload to Temp** ```typescript @Post('upload') async uploadFile(@UploadedFile() file: Express.Multer.File) { // 1. Virus Scan await this.virusScan(file); // 2. Save to temp/ const tempId = await this.fileStorage.saveToTemp(file); // 3. Return temp_id return { temp_id: tempId, expires_at: addHours(new Date(), 24) }; } ``` **Phase 2: Commit to Permanent** ```typescript async createCorrespondence(dto: CreateDto, tempFileIds: string[]) { return this.dataSource.transaction(async (manager) => { // 1. Create Correspondence const correspondence = await manager.save(Correspondence, dto); // 2. Commit Files (ภายใน Transaction) await this.fileStorage.commitFiles(tempFileIds, correspondence.id, manager); return correspondence; }); } ``` **Cleanup Job:** ```typescript @Cron('0 */6 * * *') // ทุก 6 ชั่วโมง async cleanupOrphanFiles() { const expiredFiles = await this.attachmentRepo.find({ where: { is_temporary: true, expires_at: LessThan(new Date()), }, }); for (const file of expiredFiles) { await this.deleteFile(file.file_path); await this.attachmentRepo.remove(file); } } ``` ### 3. Idempotency Interceptor ```typescript @Injectable() export class IdempotencyInterceptor implements NestInterceptor { async intercept(context: ExecutionContext, next: CallHandler) { const request = context.switchToHttp().getRequest(); const idempotencyKey = request.headers['idempotency-key']; if (!idempotencyKey) { throw new BadRequestException('Idempotency-Key required'); } // ตรวจสอบ Cache const cached = await this.redis.get(`idempotency:${idempotencyKey}`); if (cached) { return of(JSON.parse(cached)); // Return ผลลัพธ์เดิม } // Execute & Cache Result return next.handle().pipe( tap(async (response) => { await this.redis.set( `idempotency:${idempotencyKey}`, JSON.stringify(response), 'EX', 86400 // 24 hours ); }) ); } } ``` --- ## 🔄 Workflow Engine Integration **ห้ามสร้างตาราง Routing แยก** - ใช้ Unified Workflow Engine ```typescript @Injectable() export class CorrespondenceWorkflowService { constructor(private workflowEngine: WorkflowEngineService) {} async submitCorrespondence(corrId: string, templateId: string) { // สร้าง Workflow Instance const instance = await this.workflowEngine.createInstance({ definition_name: 'CORRESPONDENCE_ROUTING', entity_type: 'correspondence', entity_id: corrId, template_id: templateId, }); // Execute Initial Transition await this.workflowEngine.executeTransition(instance.id, 'SUBMIT'); return instance; } } ``` --- ## ✅ Testing Standards ### 1. Unit Tests ```typescript describe('DocumentNumberingService', () => { let service: DocumentNumberingService; let mockRedisLock: jest.Mocked; beforeEach(async () => { const module = await Test.createTestingModule({ providers: [ DocumentNumberingService, { provide: RedisLock, useValue: mockRedisLock }, ], }).compile(); service = module.get(DocumentNumberingService); }); it('should generate unique numbers concurrently', async () => { // Test concurrent number generation const promises = Array(10) .fill(null) .map(() => service.generateNextNumber(context)); const results = await Promise.all(promises); const unique = new Set(results); expect(unique.size).toBe(10); // ไม่มีเลขซ้ำ }); }); ``` ### 2. E2E Tests ```typescript describe('Correspondence API (e2e)', () => { it('should create correspondence with idempotency', async () => { const idempotencyKey = uuidv4(); // Request 1 const response1 = await request(app.getHttpServer()) .post('/correspondences') .set('Idempotency-Key', idempotencyKey) .send(createDto); expect(response1.status).toBe(201); // Request 2 (Same Key) const response2 = await request(app.getHttpServer()) .post('/correspondences') .set('Idempotency-Key', idempotencyKey) .send(createDto); expect(response2.status).toBe(201); expect(response2.body.id).toBe(response1.body.id); // Same entity }); }); ``` --- ## 📊 Logging & Monitoring ### 1. Winston Logger ```typescript // src/modules/monitoring/logger/winston.config.ts export const winstonConfig = { level: process.env.NODE_ENV === 'production' ? 'warn' : 'debug', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), ], }; ``` ### 2. Audit Logging ```typescript @Post(':id/approve') @UseInterceptors(AuditLogInterceptor) async approve(@Param('id') id: string, @CurrentUser() user: User) { // AuditLogInterceptor จะบันทึก: // - user_id // - action: 'correspondence.approve' // - entity_type: 'correspondence' // - entity_id: id // - ip_address // - timestamp } ``` --- ## 🚫 Anti-Patterns (สิ่งที่ห้ามทำ) 1. ❌ **ห้ามใช้ SQL Triggers** สำหรับ Business Logic 2. ❌ **ห้ามใช้ .env** ใน Production (ใช้ Docker ENV) 3. ❌ **ห้ามใช้ `any` Type** 4. ❌ **ห้าม Hardcode Secrets** 5. ❌ **ห้ามสร้างตาราง Routing แยก** (ใช้ Workflow Engine) 6. ❌ **ห้ามใช้ console.log** (ใช้ Logger) --- ## 📚 เอกสารอ้างอิง - [FullStack Guidelines](./fullftack-js-V1.5.0.md) - [Backend Plan v1.4.5](../../docs/2_Backend_Plan_V1_4_5.md) - [Data Dictionary](../../docs/4_Data_Dictionary_V1_4_5.md) - [Workflow Engine Plan](../../docs/2_Backend_Plan_V1_4_4.Phase6A.md) --- ## 🔄 Update History | Version | Date | Changes | | ------- | ---------- | ---------------------------------- | | 1.5.0 | 2025-12-01 | Initial backend guidelines created |