690328:1106 Fixing Refactor uuid by Kimi #01
CI / CD Pipeline / build (push) Successful in 5m11s
CI / CD Pipeline / deploy (push) Failing after 4m28s

This commit is contained in:
2026-03-28 11:06:25 +07:00
parent 76b18e7c37
commit da8579d21b
24 changed files with 606 additions and 182 deletions
+81 -2
View File
@@ -89,7 +89,7 @@ Reference these guidelines when:
- `db-hybrid-identifier` - **CRITICAL** ADR-019: INT PK + UUID public API
- `db-avoid-n-plus-one` - HIGH N+1 query prevention
- `db-use-transactions` - HIGH Transaction management
- `db-use-migrations` - N/A **ADR-009**: No TypeORM migrations - use SQL files
- `db-no-typeorm-migrations` - **CRITICAL** ADR-009: No TypeORM migrations - use SQL files
### 8. API Design (MEDIUM)
@@ -110,7 +110,86 @@ Reference these guidelines when:
- `devops-use-logging` - Structured logging
- `devops-graceful-shutdown` - Zero-downtime deployments
## How to Use
## NAP-DMS Project-Specific Rules (MUST FOLLOW)
These rules override general NestJS best practices for the NAP-DMS project:
### ADR-009: No TypeORM Migrations
- **ห้ามสร้างไฟล์ migration ของ TypeORM**
- แก้ไข schema โดยตรงที่: `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`
- ใช้ n8n workflow สำหรับ data migration ถ้าจำเป็น
### ADR-019: Hybrid Identifier Strategy (CRITICAL)
```typescript
@Entity()
export class Project {
@PrimaryGeneratedColumn()
@Exclude() // ห้ามส่งออกทาง API
id: number; // INT AUTO_INCREMENT - internal only
@Column({ type: 'uuid' })
@Expose({ name: 'id' }) // ส่งออกเป็น 'id' ทาง API
publicId: string; // UUIDv7 - public API identifier
}
```
### Two-Phase File Upload
```typescript
// Phase 1: Upload to temp
@Post('upload')
async uploadFile(@UploadedFile() file: Express.Multer.File) {
await this.virusScan(file);
const tempId = await this.fileStorage.saveToTemp(file);
return { temp_id: tempId, expires_at: addHours(new Date(), 24) };
}
// Phase 2: Commit in transaction
async createEntity(dto: CreateDto, tempIds: string[]) {
return this.dataSource.transaction(async (manager) => {
const entity = await manager.save(Entity, dto);
await this.fileStorage.commitFiles(tempIds, entity.id, manager);
return entity;
});
}
```
### Idempotency Requirement
- ทุก POST/PUT/PATCH ที่สำคัญต้องมี `Idempotency-Key` header
- ใช้ `IdempotencyInterceptor` ที่มีอยู่แล้ว
### Document Numbering (Double-Lock)
```typescript
async generateNextNumber(context: NumberingContext): Promise<string> {
const lockKey = `doc_num:${context.projectId}:${context.typeId}`;
const lock = await this.redisLock.acquire(lockKey, 3000);
try {
const counter = await this.counterRepo.findOne({
where: context,
lock: { mode: 'optimistic' },
});
counter.last_number++;
return this.formatNumber(await this.counterRepo.save(counter));
} finally {
await lock.release();
}
}
```
### Anti-Patterns (ห้ามทำ)
- ❌ ใช้ SQL Triggers สำหรับ business logic
- ❌ ใช้ `.env` ใน production (ใช้ Docker ENV)
- ❌ ใช้ `any` type (strict mode enforced)
- ❌ ใช้ `console.log` (ใช้ NestJS Logger)
- ❌ สร้างตาราง routing แยก (ใช้ Workflow Engine)
---
Read individual rule files for detailed explanations and code examples:
+156 -1
View File
@@ -165,7 +165,162 @@ See [self-hosting.md](./self-hosting.md) for:
- Cache handlers for multi-instance ISR
- What works vs needs extra setup
## Debug Tricks
## NAP-DMS Project-Specific Rules (MUST FOLLOW)
These rules are mandatory for the NAP-DMS LCBP3 frontend project:
### State Management (บังคับใช้)
**Server State - TanStack Query (React Query)**
```tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// ❌ ห้ามใช้ useEffect โดยตรง
// ✅ ใช้ TanStack Query
export function useCorrespondences(projectId: string) {
return useQuery({
queryKey: ['correspondences', projectId],
queryFn: () => correspondenceService.getAll(projectId),
staleTime: 5 * 60 * 1000,
});
}
```
**Form State - React Hook Form + Zod**
```tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
const schema = z.object({
title: z.string().min(1, 'กรุณาระบุหัวเรื่อง'),
projectUuid: z.string().uuid('กรุณาเลือกโปรเจกต์'),
});
const form = useForm({
resolver: zodResolver(schema),
});
```
### ADR-019 UUID Handling (CRITICAL)
```tsx
// Interface ต้องมีทั้ง id และ publicId
interface Contract {
id?: number; // Internal (อาจ undefined)
publicId?: string; // UUID - ใช้ตัวนี้
contractCode: string;
}
// Select options - ใช้ pattern นี้เสมอ
const options = contracts.map((c) => ({
label: `${c.contractName} (${c.contractCode})`,
value: String(c.publicId ?? c.id ?? ''), // fallback pattern
key: String(c.publicId ?? c.id ?? ''),
}));
// ❌ ห้ามใช้ parseInt บน UUID
// const id = parseInt(projectId); // WRONG!
// ✅ ส่ง UUID string ตรงๆ
apiClient.get(`/projects/${projectId}`); // projectId is UUID string
```
### Naming Conventions
**Code Identifiers - ภาษาอังกฤษ**
```tsx
// ✅ Correct
interface Correspondence {
documentNumber: string;
createdAt: string;
}
// ❌ Wrong
interface {
เลขที่: string;
}
```
**Comments - ภาษาไทย**
```tsx
// ✅ Correct - อธิบาย logic เป็นภาษาไทย
// ตรวจสอบว่ามีการระบุ projectUuid หรือไม่
if (!data.projectUuid) {
throw new Error('กรุณาเลือกโปรเจกต์');
}
// ❌ Wrong - ห้ามใช้ภาษาอังกฤษใน comments
// Check if projectUuid is provided
```
### UI Components
**บังคับใช้ shadcn/ui**
```tsx
// ✅ Correct
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
// ❌ Wrong - ไม่สร้าง component เองถ้ามีใน shadcn
const MyButton = () => <button className="...">Click</button>;
```
### File Upload Pattern
```tsx
import { useDropzone } from 'react-dropzone';
// Two-phase upload
const onDrop = useCallback(async (files: File[]) => {
// Phase 1: Upload to temp
const tempFiles = await Promise.all(files.map((file) => uploadService.uploadTemp(file)));
setTempIds(tempFiles.map((f) => f.tempId));
}, []);
// Phase 2: Commit on form submit
const onSubmit = async (data: FormData) => {
await correspondenceService.create({
...data,
tempFileIds,
});
};
```
### API Client Setup
```typescript
// lib/api/client.ts
const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 30000,
});
// Auto-add Idempotency-Key
apiClient.interceptors.request.use((config) => {
if (['post', 'put', 'patch'].includes(config.method?.toLowerCase() || '')) {
config.headers['Idempotency-Key'] = uuidv4();
}
return config;
});
```
### Anti-Patterns (ห้ามทำ)
- ❌ Fetch data ใน useEffect โดยตรง
- ❌ Props drilling ลึกเกิน 3 levels
- ❌ Inline styles (ใช้ Tailwind)
- ❌ console.log ใน production
- ❌ parseInt() บน UUID values
- ❌ ใช้ index เป็น key ใน list
- ❌ Snake_case ใน form field names (ใช้ camelCase)
---
See [debug-tricks.md](./debug-tricks.md) for: