Files
lcbp3/specs/03-implementation/backend-guidelines.md
admin 863a727756
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled
251208:1625 Frontend: to be complete admin panel, Backend: tobe recheck all task
2025-12-08 16:25:56 +07:00

12 KiB

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:

// ใช้ @UseGuards(JwtAuthGuard) สำหรับ Protected Routes
@Controller('projects')
@UseGuards(JwtAuthGuard)
export class ProjectController {
  // ...
}

RBAC (4 ระดับ):

// ใช้ @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:

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

// กำหนด 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
// 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:

@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 บ่อย:

-- 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

// 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:

@Injectable()
export class DocumentNumberingService {
  async generateNextNumber(context: NumberingContext): Promise<string> {
    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

@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

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:

@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

@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

@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

describe('DocumentNumberingService', () => {
  let service: DocumentNumberingService;
  let mockRedisLock: jest.Mocked<RedisLock>;

  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

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

// 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

@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)

📚 เอกสารอ้างอิง


🔄 Update History

Version Date Changes
1.5.0 2025-12-01 Initial backend guidelines created