690328:1106 Fixing Refactor uuid by Kimi #01
This commit is contained in:
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.fontSize": 16
|
||||
}
|
||||
@@ -610,63 +610,80 @@ test.describe('Correspondence Workflow', () => {
|
||||
|
||||
**Backend ใช้ Hybrid ID Strategy:** INT PK (internal) + UUID (public API)
|
||||
- **Database:** `id` = INT AI (Primary Key), `uuid` = UUID (MariaDB native type)
|
||||
- **Backend Entity:** `id` = INT (@Exclude), `publicId` = UUID (exposed as `id` in API via @Expose)
|
||||
- **API Response:** ส่ง `id` (ซึ่งจริงๆ คือ `publicId` UUID string) ไม่มี INT id
|
||||
- **Backend Entity:** `id` = INT (@Exclude), `publicId` = UUID (exposed directly in API)
|
||||
- **API Response:** ส่ง `publicId` (UUID string) โดยตรง — ไม่มีการ Transform เป็น `id`
|
||||
|
||||
### ✅ Updated Pattern (March 2026)
|
||||
|
||||
ใช้ `publicId` เป็น Standard อย่างเดียว — ไม่มี fallback เป็น `uuid` หรือ `id`:
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT — Use publicId only
|
||||
type ProjectOption = {
|
||||
publicId?: string;
|
||||
projectName?: string;
|
||||
projectCode?: string;
|
||||
};
|
||||
|
||||
// ใช้ใน dropdown
|
||||
projects.map((p) => ({
|
||||
label: p.projectName,
|
||||
value: p.publicId, // ไม่ต้อง fallback
|
||||
}));
|
||||
```
|
||||
|
||||
### ⚠️ Common Pitfalls (และวิธีแก้)
|
||||
|
||||
#### 1. ใช้ `.id` กับ Entity ที่ควรใช้ `.publicId`
|
||||
```tsx
|
||||
// ❌ WRONG - entity.id อาจเป็น undefined หรือ INT ที่ถูก @Exclude
|
||||
// ❌ WRONG - entity.id ถูก @Exclude จาก API response
|
||||
contracts.map((c) => <SelectItem key={c.id} value={String(c.id)}>)
|
||||
|
||||
// ✅ CORRECT - ใช้ publicId (UUID) ที่ API ส่งมา
|
||||
// ✅ CORRECT - ใช้ publicId ที่ API ส่งมาโดยตรง
|
||||
contracts.map((c) => <SelectItem key={c.publicId} value={c.publicId}>)
|
||||
// หรือ fallback สำหรับ backward compatibility
|
||||
contracts.map((c) => <SelectItem key={c.publicId ?? c.id} value={String(c.publicId ?? c.id)}>)
|
||||
```
|
||||
|
||||
#### 2. parseInt() บน UUID string
|
||||
#### 2. Fallback หลายชั้น (uuid ?? id ?? '')
|
||||
```tsx
|
||||
// ❌ WRONG - สับสน ไม่ maintainable
|
||||
value: String(c.publicId ?? c.uuid ?? c.id ?? '')
|
||||
|
||||
// ✅ CORRECT - publicId อย่างเดียว
|
||||
value: c.publicId
|
||||
```
|
||||
|
||||
#### 3. parseInt() บน UUID string
|
||||
```tsx
|
||||
// ❌ WRONG - parseInt บน UUID จะได้ค่า garbage
|
||||
const id = parseInt(projectId); // "0195..." → 19 (wrong!)
|
||||
|
||||
// ✅ CORRECT - ส่ง UUID string ตรงๆ ไป backend
|
||||
const id = projectId; // "019505a1-7c3e-7000-8000-abc123def456"
|
||||
|
||||
// ✅ CORRECT - ถ้าต้องการ INT ให้ backend resolve เองผ่าน UuidResolver
|
||||
// Backend DTO รับ `projectUuid: string` แล้ว resolve เป็น `projectId: number` เอง
|
||||
```
|
||||
|
||||
#### 3. Field Name Mismatch (snake_case vs camelCase)
|
||||
#### 4. Select Option ไม่มีข้อมูล (Type Mismatch)
|
||||
```tsx
|
||||
// ❌ WRONG - ใช้ชื่อ field ไม่ตรงกับ TypeScript interface
|
||||
fields={[{ name: 'type_code', label: 'Code' }]}
|
||||
// interface มี typeCode (camelCase) แต่ form ส่ง type_code (snake_case)
|
||||
// ❌ WRONG - สมมติว่า API ส่ง { uuid: string } แต่จริงๆ ส่ง { publicId: string }
|
||||
type OrganizationOption = {
|
||||
uuid?: string; // ผิด!
|
||||
organizationName?: string;
|
||||
};
|
||||
const value = org.uuid ?? org.id; // undefined → dropdown ว่าง
|
||||
|
||||
// ✅ CORRECT - ใช้ชื่อ field ตรงกับ interface
|
||||
fields={[{ name: 'typeCode', label: 'Code' }]}
|
||||
// ✅ CORRECT - Type ต้องตรงกับ API Response
|
||||
type OrganizationOption = {
|
||||
publicId?: string; // ตรงกับ API
|
||||
organizationName?: string;
|
||||
};
|
||||
const value = org.publicId; // ได้ค่าถูกต้อง
|
||||
```
|
||||
|
||||
#### 4. Contract/Project Select ไม่มีข้อมูล
|
||||
```tsx
|
||||
// ❌ WRONG - สมมติว่า API ส่ง { id: number }
|
||||
const options = contracts.map((c) => ({ value: String(c.id), label: c.contractName }))
|
||||
|
||||
// ✅ CORRECT - API ส่ง { publicId: string } ตาม ADR-019
|
||||
const options = contracts.map((c) => ({
|
||||
value: String(c.publicId ?? c.id ?? ''), // fallback รองรับทั้ง 2 กรณี
|
||||
label: c.contractName
|
||||
}))
|
||||
```
|
||||
|
||||
### 📝 Pattern: Contract/Project Select Options
|
||||
### 📝 Pattern: Select Options with publicId
|
||||
|
||||
```typescript
|
||||
// types/master-data.ts - Entity interfaces
|
||||
export interface Contract {
|
||||
id?: number; // Internal INT (อาจถูก @Exclude)
|
||||
publicId?: string; // UUID ที่ API ส่ง (ต้องใช้ตัวนี้)
|
||||
publicId?: string; // UUID ที่ API ส่ง — ใช้ตัวนี้
|
||||
contractCode: string;
|
||||
contractName: string;
|
||||
}
|
||||
@@ -674,17 +691,17 @@ export interface Contract {
|
||||
// page.tsx - Select options
|
||||
const contractOptions = contracts.map((c) => ({
|
||||
label: `${c.contractName} (${c.contractCode})`,
|
||||
value: String(c.publicId ?? c.id ?? ''), // ADR-019: publicId เป็น UUID
|
||||
value: c.publicId, // publicId only — no fallback
|
||||
}));
|
||||
|
||||
// GenericCrudTable fields
|
||||
fields={[
|
||||
{
|
||||
name: 'contractId',
|
||||
name: 'contractPublicId', // DTO field name
|
||||
label: 'Contract',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: contractOptions, // ใช้ UUID string เป็น value
|
||||
options: contractOptions,
|
||||
},
|
||||
]}
|
||||
```
|
||||
@@ -716,11 +733,11 @@ const columns: ColumnDef<Discipline>[] = [
|
||||
|
||||
### ✅ Checklist ก่อน Commit
|
||||
|
||||
- [ ] ใช้ `publicId ?? id` pattern สำหรับ entity identifiers
|
||||
- [ ] ใช้ `publicId` อย่างเดียว (ไม่มี `uuid` หรือ `id` fallback)
|
||||
- [ ] ไม่ใช้ `parseInt()` บน UUID values
|
||||
- [ ] Field names ตรงกับ TypeScript interfaces (camelCase)
|
||||
- [ ] Select options ใช้ UUID string เป็น value
|
||||
- [ ] แสดง relation columns (Contract/Project) ในตาราง
|
||||
- [ ] Type Definition ตรงกับ API Response field names
|
||||
- [ ] Select options ใช้ `publicId` เป็น value
|
||||
- [ ] DTO field names ใช้ `publicId` suffix (e.g., `projectPublicId`)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -243,21 +243,31 @@ async findByUuidOrId(identifier: string): Promise<Entity> {
|
||||
|
||||
#### Pattern: Drawing Search (✅ FIXED — reference implementation)
|
||||
|
||||
- Backend DTO accepts `projectUuid: string` instead of `projectId: number`
|
||||
- Controller resolves: `projectService.findOneByUuid(dto.projectUuid)` → `dto.projectId = project.id`
|
||||
- Backend DTO accepts `projectPublicId: string` instead of `projectId: number`
|
||||
- Controller resolves: `projectService.findOneByUuid(dto.projectPublicId)` → `dto.projectId = project.id`
|
||||
- Frontend sends UUID string directly (no `parseInt`)
|
||||
- Frontend Type uses `publicId` only:
|
||||
```typescript
|
||||
type ProjectOption = {
|
||||
publicId?: string;
|
||||
projectName?: string;
|
||||
};
|
||||
```
|
||||
|
||||
#### Remaining Issues
|
||||
#### Remaining Issues (Updated Naming Convention)
|
||||
|
||||
| File | Field | Entity | Issue |
|
||||
| ------------------------------------- | ----------------------------------------------------- | ------------ | ------------------------------------------------------------ |
|
||||
| `correspondences/form.tsx:212` | `projectId` | Project | `parseInt(p.id)` where `p.id` = UUID string (garbled number) |
|
||||
| `correspondences/form.tsx:326` | `fromOrganizationId` | Organization | `parseInt(String(org.id))` where `org.id` = undefined (NaN) |
|
||||
| `correspondences/form.tsx:349` | `toOrganizationId` | Organization | Same as above |
|
||||
| `admin/users/page.tsx:47` | `primaryOrganizationId` (filter) | Organization | `parseInt(selectedOrgId)` where value = UUID string |
|
||||
| `admin/user-dialog.tsx:226` | `primaryOrganizationId` | Organization | `parseInt(val)` where `org.id` = undefined → `"0"` fallback |
|
||||
| `numbering/template-tester.tsx:71-74` | `originatorOrganizationId`, `recipientOrganizationId` | Organization | `parseInt` on org UUID |
|
||||
| `rfas/page.tsx:17` | `projectId` (URL param) | Project | `parseInt(searchParams.get('projectId'))` — UUID if from URL |
|
||||
| `correspondences/form.tsx` | `projectPublicId` | Project | Type uses `id` instead of `publicId` |
|
||||
| `correspondences/form.tsx` | `fromOrganizationPublicId` | Organization | Type uses `uuid/id` instead of `publicId` |
|
||||
| `correspondences/form.tsx` | `toOrganizationPublicId` | Organization | Type uses `uuid/id` instead of `publicId` |
|
||||
| `admin/users/page.tsx` | `primaryOrganizationPublicId` (filter) | Organization | Type uses `id` instead of `publicId` |
|
||||
| `admin/user-dialog.tsx` | `primaryOrganizationPublicId` | Organization | Type uses `id` instead of `publicId` |
|
||||
| `numbering/template-tester.tsx` | `originatorOrganizationPublicId` / `recipientOrganizationPublicId` | Organization | Type uses `id` instead of `publicId` |
|
||||
| `rfas/page.tsx` | `projectPublicId` (URL param) | Project | Type uses `id` instead of `publicId` |
|
||||
| `rfas/form.tsx` | `projectPublicId`, `contractPublicId`, `toOrganizationPublicId` | Multiple | ✅ FIXED — Now uses `publicId` exclusively |
|
||||
|
||||
> **Fix Applied:** `rfas/form.tsx` standardized to use `publicId` only (2026-03-28)
|
||||
|
||||
#### Fix Strategy (same pattern as Drawing Search fix)
|
||||
|
||||
@@ -300,16 +310,6 @@ For each affected backend DTO:
|
||||
| Order | Task | Effort | Status |
|
||||
| ----- | ------------------------------------------------------------- | ------ | -------------------------- |
|
||||
| 1 | UuidBaseEntity (no transformer needed — MariaDB native UUID) | S | ✅ Done |
|
||||
| 2 | Install `uuid` package | XS | ✅ Done |
|
||||
| 3 | Update 14 entity files with uuid column | M | ✅ Done |
|
||||
| 4 | Create ParseUuidPipe | S | ✅ Done |
|
||||
| 5 | Update controllers to use UUID params | L | ✅ Done |
|
||||
| 6 | Update services with findByUuid methods | L | ✅ Done |
|
||||
| 7 | Update DTOs to expose uuid, hide id | M | ✅ Done |
|
||||
| 8 | Update frontend API calls & routes | L | ✅ Done |
|
||||
| 9 | Drawing search: projectUuid migration | S | ✅ Done (2026-03-18) |
|
||||
| 10 | FK reference UUID migration (Correspondence, User, Numbering) | M | ❌ Pending (see Phase 5.4) |
|
||||
| 11 | Write unit + integration tests | M | ❌ Pending |
|
||||
|
||||
**Estimated Remaining Effort:** ~2-3 days for FK migration + ~2 days for tests
|
||||
|
||||
|
||||
@@ -266,12 +266,11 @@ async findByUuid(publicId: string): Promise<CorrespondenceDto> {
|
||||
}
|
||||
```
|
||||
|
||||
### DTO Pattern — Never Expose INT ID
|
||||
### DTO Pattern — Expose publicId Directly
|
||||
|
||||
```typescript
|
||||
export class CorrespondenceResponseDto {
|
||||
// ✅ Expose publicId as 'id' in API response
|
||||
@Expose({ name: 'id' })
|
||||
// ✅ Expose publicId directly in API response
|
||||
publicId!: string;
|
||||
|
||||
// ❌ Never expose internal INT id
|
||||
@@ -279,11 +278,12 @@ export class CorrespondenceResponseDto {
|
||||
|
||||
// ... other fields
|
||||
// For FK references, also use publicId
|
||||
@Expose({ name: 'project_id' })
|
||||
projectPublicId!: string;
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** We use `publicId` directly in API responses (not transformed to `id`) to maintain naming consistency between Backend Entity property and Frontend Type property. This prevents confusion when mapping data.
|
||||
|
||||
---
|
||||
|
||||
## Migration SQL Script
|
||||
@@ -473,11 +473,29 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
|
||||
- API รับได้ทั้ง INT และ UUID ผ่าน `FindByIdOrUuid` pattern
|
||||
- API Response รวม UUID เป็น `id` field (via @Expose)
|
||||
|
||||
### Phase 3: Frontend (Gradual Migration)
|
||||
### Phase 3: Frontend (Consistent publicId Usage)
|
||||
|
||||
- Frontend เปลี่ยนจากใช้ `id` (INT) เป็น `id` (UUID) ใน API response
|
||||
- URL parameters เปลี่ยนเป็น UUID
|
||||
- ไม่ต้อง Big-Bang migration — ค่อยๆ เปลี่ยนทีละ Module
|
||||
- Frontend ใช้ `publicId` เป็น Standard ทุก Type (ไม่ใช้ `uuid` หรือ `id` ที่เป็น number)
|
||||
- URL parameters ใช้ `publicId` (UUID string)
|
||||
- ทุก Type Definition ใช้ `publicId?: string` อย่างเดียว — ไม่มี fallback เป็น `uuid` หรือ `id`
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// ✅ Correct — Consistent publicId usage
|
||||
type ProjectOption = {
|
||||
publicId?: string;
|
||||
projectName?: string;
|
||||
projectCode?: string;
|
||||
};
|
||||
|
||||
// ❌ Wrong — Multiple identifier fields cause confusion
|
||||
type ProjectOption = {
|
||||
publicId?: string;
|
||||
uuid?: string; // Don't do this
|
||||
id?: number; // Don't do this
|
||||
projectName?: string;
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 4: Cleanup
|
||||
|
||||
@@ -492,7 +510,7 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
|
||||
| Area | Status |
|
||||
| ---------------------- | ------------------------------------ |
|
||||
| Security | ✅ Eliminates ID enumeration |
|
||||
| Performance | ✅ No impact on internal JOINs |
|
||||
| Performance | ✅ No impact on internal JOINs |
|
||||
| Migration Risk | ✅ Low — ADD COLUMN only |
|
||||
| Storage Impact | ✅ Negligible (~3.8 MB) |
|
||||
| Backward Compatibility | ✅ Dual-mode transition |
|
||||
@@ -500,4 +518,16 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
|
||||
|
||||
---
|
||||
|
||||
## Naming Convention Summary
|
||||
|
||||
| Context | Backend (TypeORM) | Frontend (TypeScript) | API Response |
|
||||
|---------|-------------------|----------------------|--------------|
|
||||
| **Entity Property** | `publicId: string` | `publicId?: string` | `publicId: string` |
|
||||
| **DB Column** | `uuid UUID` | — | — |
|
||||
| **Internal PK** | `id: number` (excluded) | — | — |
|
||||
|
||||
**Rule:** ใช้ `publicId` เป็น Identifier เดียวใน API — ไม่มีการ Transform เป็น `id` เพื่อป้องกัน confusion ระหว่าง Backend ↔ Frontend
|
||||
|
||||
---
|
||||
|
||||
_สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน `05-07-hybrid-uuid-implementation-plan.md`_
|
||||
|
||||
Reference in New Issue
Block a user