690328:1106 Fixing Refactor uuid by Kimi #01
This commit is contained in:
@@ -89,7 +89,7 @@ Reference these guidelines when:
|
|||||||
- `db-hybrid-identifier` - **CRITICAL** ADR-019: INT PK + UUID public API
|
- `db-hybrid-identifier` - **CRITICAL** ADR-019: INT PK + UUID public API
|
||||||
- `db-avoid-n-plus-one` - HIGH N+1 query prevention
|
- `db-avoid-n-plus-one` - HIGH N+1 query prevention
|
||||||
- `db-use-transactions` - HIGH Transaction management
|
- `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)
|
### 8. API Design (MEDIUM)
|
||||||
|
|
||||||
@@ -110,7 +110,86 @@ Reference these guidelines when:
|
|||||||
- `devops-use-logging` - Structured logging
|
- `devops-use-logging` - Structured logging
|
||||||
- `devops-graceful-shutdown` - Zero-downtime deployments
|
- `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:
|
Read individual rule files for detailed explanations and code examples:
|
||||||
|
|
||||||
|
|||||||
@@ -165,7 +165,162 @@ See [self-hosting.md](./self-hosting.md) for:
|
|||||||
- Cache handlers for multi-instance ISR
|
- Cache handlers for multi-instance ISR
|
||||||
- What works vs needs extra setup
|
- 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:
|
See [debug-tricks.md](./debug-tricks.md) for:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
auto_execution_mode: 0
|
||||||
|
description: Review code changes for bugs, security issues, and improvements
|
||||||
|
---
|
||||||
|
|
||||||
|
You are a senior software engineer performing a thorough code review to identify potential bugs.
|
||||||
|
|
||||||
|
Your task is to find all potential bugs and code improvements in the code changes. Focus on:
|
||||||
|
|
||||||
|
1. Logic errors and incorrect behavior
|
||||||
|
2. Edge cases that aren't handled
|
||||||
|
3. Null/undefined reference issues
|
||||||
|
4. Race conditions or concurrency issues
|
||||||
|
5. Security vulnerabilities
|
||||||
|
6. Improper resource management or resource leaks
|
||||||
|
7. API contract violations
|
||||||
|
8. Incorrect caching behavior, including cache staleness issues, cache key-related bugs, incorrect cache invalidation, and ineffective caching
|
||||||
|
9. Violations of existing code patterns or conventions
|
||||||
|
|
||||||
|
## 🔴 Tier 1 Critical Rules (CI Blockers)
|
||||||
|
|
||||||
|
The following are **CI-blocking issues** that must be caught in code review. These align with project specs in `specs/05-Engineering-Guidelines/` and `specs/06-Decision-Records/`:
|
||||||
|
|
||||||
|
### ADR-019: UUID Handling
|
||||||
|
|
||||||
|
- **❌ NEVER use `parseInt()`, `Number()`, or `+` operator on UUID values**
|
||||||
|
- Example of violation: `parseInt(projectId)` where `projectId` is UUID string
|
||||||
|
- ✅ Correct: Use UUID string directly without conversion
|
||||||
|
- **❌ NEVER expose internal INT PK in API responses**
|
||||||
|
- API must expose only `publicId` (transformed to `id` via `@Expose()`)
|
||||||
|
- Verify DTOs have `@Exclude()` on `id: number` field
|
||||||
|
|
||||||
|
### TypeScript Strict Rules
|
||||||
|
|
||||||
|
- **❌ ZERO `any` types allowed** — use proper types or `unknown` + narrowing
|
||||||
|
- **❌ ZERO `console.log`** — must use NestJS `Logger` (backend) or remove (frontend)
|
||||||
|
- **❌ NO `req: any` in controllers** — use `RequestWithUser` typed interface
|
||||||
|
|
||||||
|
### Database & Architecture
|
||||||
|
|
||||||
|
- **❌ NO SQL Triggers for business logic** — use NestJS Service methods instead
|
||||||
|
- **❌ NO `.env` files in production** — use Docker environment variables
|
||||||
|
- **❌ NO direct table/column name invention** — verify against `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`
|
||||||
|
|
||||||
|
### Security (ADR-016)
|
||||||
|
|
||||||
|
- Idempotency validation for critical `POST`/`PUT`/`PATCH` endpoints
|
||||||
|
- Two-phase file upload pattern (Upload → Temp → Commit → Permanent)
|
||||||
|
- Input validation with class-validator (backend) and Zod (frontend)
|
||||||
|
|
||||||
|
### Test Coverage Requirements
|
||||||
|
|
||||||
|
- **Backend Services:** 80% minimum
|
||||||
|
- **Backend Overall:** 70% minimum
|
||||||
|
- **Business Logic:** 80% minimum
|
||||||
|
|
||||||
|
Make sure to:
|
||||||
|
|
||||||
|
1. If exploring the codebase, call multiple tools in parallel for increased efficiency. Do not spend too much time exploring.
|
||||||
|
2. If you find any pre-existing bugs in the code, you should also report those since it's important for us to maintain general code quality for the user.
|
||||||
|
3. Do NOT report issues that are speculative or low-confidence. All your conclusions should be based on a complete understanding of the codebase.
|
||||||
|
4. Remember that if you were given a specific git commit, it may not be checked out and local code states may be different.
|
||||||
+28
-4
@@ -88,8 +88,29 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth
|
|||||||
| ---------------- | ------------------------- | ------------------------------------------- |
|
| ---------------- | ------------------------- | ------------------------------------------- |
|
||||||
| Internal / DB FK | `INT AUTO_INCREMENT` | Never exposed in API |
|
| Internal / DB FK | `INT AUTO_INCREMENT` | Never exposed in API |
|
||||||
| Public API / URL | `UUIDv7` (MariaDB native) | Stored as BINARY(16), no transformer needed |
|
| Public API / URL | `UUIDv7` (MariaDB native) | Stored as BINARY(16), no transformer needed |
|
||||||
| Entity Property | `publicId: string` | Exposed via `@Expose({ name: 'id' })` |
|
| Entity Property | `publicId: string` | Exposed directly in API (no transformation) |
|
||||||
| API Response | `id: string` (UUID) | INT `id` has `@Exclude()` — never appears |
|
| API Response | `publicId: string` (UUID) | INT `id` has `@Exclude()` — never appears |
|
||||||
|
|
||||||
|
### ✅ Updated Pattern (March 2026)
|
||||||
|
|
||||||
|
**Backend:** `UuidBaseEntity` exposes `publicId` directly — no `@Expose({ name: 'id' })` transformation
|
||||||
|
|
||||||
|
**Frontend:** Use `publicId` only — no `uuid` or `id` fallbacks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ CORRECT — Use publicId only
|
||||||
|
type ProjectOption = {
|
||||||
|
publicId?: string; // No uuid, no id fallback
|
||||||
|
projectName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ❌ WRONG — Multiple identifiers cause confusion
|
||||||
|
type ProjectOption = {
|
||||||
|
publicId?: string;
|
||||||
|
uuid?: string; // Don't do this
|
||||||
|
id?: number; // Don't do this
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
### ❌ Forbidden UUID Patterns
|
### ❌ Forbidden UUID Patterns
|
||||||
|
|
||||||
@@ -97,8 +118,11 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth
|
|||||||
// ❌ NEVER use parseInt on UUID
|
// ❌ NEVER use parseInt on UUID
|
||||||
parseInt(projectId); // "0195..." → 19 (WRONG!)
|
parseInt(projectId); // "0195..." → 19 (WRONG!)
|
||||||
|
|
||||||
// ✅ CORRECT — Use UUID string directly
|
// ❌ NEVER use id ?? '' fallback
|
||||||
const id = projectId; // "019505a1-7c3e-7000-8000-abc123def456"
|
const value = c.publicId ?? c.id ?? ''; // Wrong!
|
||||||
|
|
||||||
|
// ✅ CORRECT — Use publicId only
|
||||||
|
const value = c.publicId; // "019505a1-7c3e-7000-8000-abc123def456"
|
||||||
```
|
```
|
||||||
|
|
||||||
Read `specs/05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` before any UUID-related work.
|
Read `specs/05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` before any UUID-related work.
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"editor.fontSize": 20,
|
"editor.fontSize": 16,
|
||||||
"npm.packageManager": "pnpm"
|
"npm.packageManager": "pnpm"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,43 @@ Your task is to find all potential bugs and code improvements in the code change
|
|||||||
8. Incorrect caching behavior, including cache staleness issues, cache key-related bugs, incorrect cache invalidation, and ineffective caching
|
8. Incorrect caching behavior, including cache staleness issues, cache key-related bugs, incorrect cache invalidation, and ineffective caching
|
||||||
9. Violations of existing code patterns or conventions
|
9. Violations of existing code patterns or conventions
|
||||||
|
|
||||||
|
## 🔴 Tier 1 Critical Rules (CI Blockers)
|
||||||
|
|
||||||
|
The following are **CI-blocking issues** that must be caught in code review. These align with project specs in `specs/05-Engineering-Guidelines/` and `specs/06-Decision-Records/`:
|
||||||
|
|
||||||
|
### ADR-019: UUID Handling
|
||||||
|
|
||||||
|
- **❌ NEVER use `parseInt()`, `Number()`, or `+` operator on UUID values**
|
||||||
|
- Example of violation: `parseInt(projectId)` where `projectId` is UUID string
|
||||||
|
- ✅ Correct: Use UUID string directly without conversion
|
||||||
|
- **❌ NEVER expose internal INT PK in API responses**
|
||||||
|
- API must expose only `publicId` (transformed to `id` via `@Expose()`)
|
||||||
|
- Verify DTOs have `@Exclude()` on `id: number` field
|
||||||
|
|
||||||
|
### TypeScript Strict Rules
|
||||||
|
|
||||||
|
- **❌ ZERO `any` types allowed** — use proper types or `unknown` + narrowing
|
||||||
|
- **❌ ZERO `console.log`** — must use NestJS `Logger` (backend) or remove (frontend)
|
||||||
|
- **❌ NO `req: any` in controllers** — use `RequestWithUser` typed interface
|
||||||
|
|
||||||
|
### Database & Architecture
|
||||||
|
|
||||||
|
- **❌ NO SQL Triggers for business logic** — use NestJS Service methods instead
|
||||||
|
- **❌ NO `.env` files in production** — use Docker environment variables
|
||||||
|
- **❌ NO direct table/column name invention** — verify against `specs/03-Data-and-Storage/lcbp3-v1.8.0-schema-02-tables.sql`
|
||||||
|
|
||||||
|
### Security (ADR-016)
|
||||||
|
|
||||||
|
- Idempotency validation for critical `POST`/`PUT`/`PATCH` endpoints
|
||||||
|
- Two-phase file upload pattern (Upload → Temp → Commit → Permanent)
|
||||||
|
- Input validation with class-validator (backend) and Zod (frontend)
|
||||||
|
|
||||||
|
### Test Coverage Requirements
|
||||||
|
|
||||||
|
- **Backend Services:** 80% minimum
|
||||||
|
- **Backend Overall:** 70% minimum
|
||||||
|
- **Business Logic:** 80% minimum
|
||||||
|
|
||||||
Make sure to:
|
Make sure to:
|
||||||
|
|
||||||
1. If exploring the codebase, call multiple tools in parallel for increased efficiency. Do not spend too much time exploring.
|
1. If exploring the codebase, call multiple tools in parallel for increased efficiency. Do not spend too much time exploring.
|
||||||
|
|||||||
+30
-6
@@ -1,7 +1,7 @@
|
|||||||
# NAP-DMS Project Context & Rules
|
# NAP-DMS Project Context & Rules
|
||||||
- For: Windsurf Cascade (and compatible: Codex CLI, opencode, Amp, Amazon Q, AGENTS.md tools)
|
- For: Windsurf Cascade (and compatible: Codex CLI, opencode, Amp, Amazon Q, AGENTS.md tools)
|
||||||
- Version: 1.8.5 (Refactored) | Last synced from repo: 2026-03-27
|
- Version: 1.8.5 (Refactored) | Last synced from repo: 2026-03-27
|
||||||
- Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3)
|
- Repo: [https://git.np-dms.work/np-dms/lcbp3](https://git.np-dms.work/np-dms/lcbp3)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -84,8 +84,29 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth
|
|||||||
| ---------------- | ------------------------- | ------------------------------------------- |
|
| ---------------- | ------------------------- | ------------------------------------------- |
|
||||||
| Internal / DB FK | `INT AUTO_INCREMENT` | Never exposed in API |
|
| Internal / DB FK | `INT AUTO_INCREMENT` | Never exposed in API |
|
||||||
| Public API / URL | `UUIDv7` (MariaDB native) | Stored as BINARY(16), no transformer needed |
|
| Public API / URL | `UUIDv7` (MariaDB native) | Stored as BINARY(16), no transformer needed |
|
||||||
| Entity Property | `publicId: string` | Exposed via `@Expose({ name: 'id' })` |
|
| Entity Property | `publicId: string` | Exposed directly in API (no transformation) |
|
||||||
| API Response | `id: string` (UUID) | INT `id` has `@Exclude()` — never appears |
|
| API Response | `publicId: string` (UUID) | INT `id` has `@Exclude()` — never appears |
|
||||||
|
|
||||||
|
### ✅ Updated Pattern (March 2026)
|
||||||
|
|
||||||
|
**Backend:** `UuidBaseEntity` exposes `publicId` directly — no `@Expose({ name: 'id' })` transformation
|
||||||
|
|
||||||
|
**Frontend:** Use `publicId` only — no `uuid` or `id` fallbacks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ CORRECT — Use publicId only
|
||||||
|
type ProjectOption = {
|
||||||
|
publicId?: string; // No uuid, no id fallback
|
||||||
|
projectName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ❌ WRONG — Multiple identifiers cause confusion
|
||||||
|
type ProjectOption = {
|
||||||
|
publicId?: string;
|
||||||
|
uuid?: string; // Don't do this
|
||||||
|
id?: number; // Don't do this
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
### ❌ Forbidden UUID Patterns
|
### ❌ Forbidden UUID Patterns
|
||||||
|
|
||||||
@@ -93,8 +114,11 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth
|
|||||||
// ❌ NEVER use parseInt on UUID
|
// ❌ NEVER use parseInt on UUID
|
||||||
parseInt(projectId); // "0195..." → 19 (WRONG!)
|
parseInt(projectId); // "0195..." → 19 (WRONG!)
|
||||||
|
|
||||||
// ✅ CORRECT — Use UUID string directly
|
// ❌ NEVER use id ?? '' fallback
|
||||||
const id = projectId; // "019505a1-7c3e-7000-8000-abc123def456"
|
const value = c.publicId ?? c.id ?? ''; // Wrong!
|
||||||
|
|
||||||
|
// ✅ CORRECT — Use publicId only
|
||||||
|
const value = c.publicId; // "019505a1-7c3e-7000-8000-abc123def456"
|
||||||
```
|
```
|
||||||
|
|
||||||
Read `specs/05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` before any UUID-related work.
|
Read `specs/05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` before any UUID-related work.
|
||||||
|
|||||||
@@ -85,8 +85,29 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth
|
|||||||
| ---------------- | ------------------------- | ------------------------------------------- |
|
| ---------------- | ------------------------- | ------------------------------------------- |
|
||||||
| Internal / DB FK | `INT AUTO_INCREMENT` | Never exposed in API |
|
| Internal / DB FK | `INT AUTO_INCREMENT` | Never exposed in API |
|
||||||
| Public API / URL | `UUIDv7` (MariaDB native) | Stored as BINARY(16), no transformer needed |
|
| Public API / URL | `UUIDv7` (MariaDB native) | Stored as BINARY(16), no transformer needed |
|
||||||
| Entity Property | `publicId: string` | Exposed via `@Expose({ name: 'id' })` |
|
| Entity Property | `publicId: string` | Exposed directly in API (no transformation) |
|
||||||
| API Response | `id: string` (UUID) | INT `id` has `@Exclude()` — never appears |
|
| API Response | `publicId: string` (UUID) | INT `id` has `@Exclude()` — never appears |
|
||||||
|
|
||||||
|
### ✅ Updated Pattern (March 2026)
|
||||||
|
|
||||||
|
**Backend:** `UuidBaseEntity` exposes `publicId` directly — no `@Expose({ name: 'id' })` transformation
|
||||||
|
|
||||||
|
**Frontend:** Use `publicId` only — no `uuid` or `id` fallbacks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ CORRECT — Use publicId only
|
||||||
|
type ProjectOption = {
|
||||||
|
publicId?: string; // No uuid, no id fallback
|
||||||
|
projectName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ❌ WRONG — Multiple identifiers cause confusion
|
||||||
|
type ProjectOption = {
|
||||||
|
publicId?: string;
|
||||||
|
uuid?: string; // Don't do this
|
||||||
|
id?: number; // Don't do this
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
### ❌ Forbidden UUID Patterns
|
### ❌ Forbidden UUID Patterns
|
||||||
|
|
||||||
@@ -94,8 +115,11 @@ Spec priority: **`06-Decision-Records`** > **`05-Engineering-Guidelines`** > oth
|
|||||||
// ❌ NEVER use parseInt on UUID
|
// ❌ NEVER use parseInt on UUID
|
||||||
parseInt(projectId); // "0195..." → 19 (WRONG!)
|
parseInt(projectId); // "0195..." → 19 (WRONG!)
|
||||||
|
|
||||||
// ✅ CORRECT — Use UUID string directly
|
// ❌ NEVER use id ?? '' fallback
|
||||||
const id = projectId; // "019505a1-7c3e-7000-8000-abc123def456"
|
const value = c.publicId ?? c.id ?? ''; // Wrong!
|
||||||
|
|
||||||
|
// ✅ CORRECT — Use publicId only
|
||||||
|
const value = c.publicId; // "019505a1-7c3e-7000-8000-abc123def456"
|
||||||
```
|
```
|
||||||
|
|
||||||
Read `specs/05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` before any UUID-related work.
|
Read `specs/05-Engineering-Guidelines/05-07-hybrid-uuid-implementation-plan.md` before any UUID-related work.
|
||||||
|
|||||||
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"npm.packageManager": "pnpm"
|
"npm.packageManager": "pnpm",
|
||||||
|
"editor.fontSize": 16
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-14
@@ -76,7 +76,7 @@
|
|||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"typeorm": "^0.3.27",
|
"typeorm": "^0.3.27",
|
||||||
"uuid": "^11.0.0",
|
"uuid": "^9.0.0",
|
||||||
"winston": "^3.18.3",
|
"winston": "^3.18.3",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
@@ -115,24 +115,14 @@
|
|||||||
"typescript-eslint": "^8.57.1"
|
"typescript-eslint": "^8.57.1"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"preset": "ts-jest",
|
||||||
"js",
|
"testEnvironment": "node",
|
||||||
"json",
|
|
||||||
"ts"
|
|
||||||
],
|
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
},
|
|
||||||
"collectCoverageFrom": [
|
"collectCoverageFrom": [
|
||||||
"**/*.(t|j)s"
|
"**/*.(t|j)s"
|
||||||
],
|
],
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage"
|
||||||
"testEnvironment": "node",
|
|
||||||
"transformIgnorePatterns": [
|
|
||||||
"node_modules/(?!(uuid)/)"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { Organization } from '../../organization/entities/organization.entity';
|
|||||||
import { UserAssignment } from './user-assignment.entity';
|
import { UserAssignment } from './user-assignment.entity';
|
||||||
import { UserPreference } from './user-preference.entity';
|
import { UserPreference } from './user-preference.entity';
|
||||||
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
import { UuidBaseEntity } from '../../../common/entities/uuid-base.entity';
|
||||||
import { Exclude, Expose } from 'class-transformer';
|
import { Exclude } from 'class-transformer';
|
||||||
|
|
||||||
@Entity('users')
|
@Entity('users')
|
||||||
export class User extends UuidBaseEntity {
|
export class User extends UuidBaseEntity {
|
||||||
@@ -65,7 +65,6 @@ export class User extends UuidBaseEntity {
|
|||||||
organization?: Organization;
|
organization?: Organization;
|
||||||
|
|
||||||
// ADR-019: Expose UUID instead of INT ID
|
// ADR-019: Expose UUID instead of INT ID
|
||||||
@Expose({ name: 'primaryOrganizationId' })
|
|
||||||
get primaryOrganizationPublicId(): string | undefined {
|
get primaryOrganizationPublicId(): string | undefined {
|
||||||
return this.organization?.publicId;
|
return this.organization?.publicId;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"npm.packageManager": "pnpm"
|
"npm.packageManager": "pnpm",
|
||||||
|
"editor.fontSize": 16
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { useProjects } from '@/hooks/use-projects';
|
|||||||
import { CreateRfaDto } from '@/types/dto/rfa/rfa.dto';
|
import { CreateRfaDto } from '@/types/dto/rfa/rfa.dto';
|
||||||
import { useState, useEffect, type FormEvent } from 'react';
|
import { useState, useEffect, type FormEvent } from 'react';
|
||||||
import { correspondenceService } from '@/lib/services/correspondence.service';
|
import { correspondenceService } from '@/lib/services/correspondence.service';
|
||||||
import { Contract, getContractPublicId } from '@/types/contract';
|
import { Contract } from '@/types/contract';
|
||||||
|
|
||||||
const rfaSchema = z.object({
|
const rfaSchema = z.object({
|
||||||
projectId: z.string().min(1, 'Project is required'), // ADR-019: UUID
|
projectId: z.string().min(1, 'Project is required'), // ADR-019: UUID
|
||||||
@@ -41,16 +41,12 @@ type RFAFormData = z.infer<typeof rfaSchema>;
|
|||||||
|
|
||||||
type ProjectOption = {
|
type ProjectOption = {
|
||||||
publicId?: string;
|
publicId?: string;
|
||||||
uuid?: string; // Legacy alias for publicId
|
|
||||||
id?: number;
|
|
||||||
projectName?: string;
|
projectName?: string;
|
||||||
projectCode?: string;
|
projectCode?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContractOption = {
|
type ContractOption = {
|
||||||
publicId?: string;
|
publicId?: string;
|
||||||
uuid?: string;
|
|
||||||
id?: string;
|
|
||||||
contractName?: string;
|
contractName?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
contractCode?: string;
|
contractCode?: string;
|
||||||
@@ -78,20 +74,19 @@ type CorrespondenceTypeOption = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type OrganizationOption = {
|
type OrganizationOption = {
|
||||||
uuid?: string;
|
publicId?: string;
|
||||||
id?: number;
|
|
||||||
organizationCode?: string;
|
organizationCode?: string;
|
||||||
organizationName?: string;
|
organizationName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SelectableDrawingOption = {
|
type SelectableDrawingOption = {
|
||||||
uuid?: string;
|
publicId?: string;
|
||||||
drawingNumber?: string;
|
drawingNumber?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
legacyDrawingNumber?: string;
|
legacyDrawingNumber?: string;
|
||||||
currentRevisionUuid?: string;
|
currentRevisionPublicId?: string;
|
||||||
currentRevision?: {
|
currentRevision?: {
|
||||||
uuid?: string;
|
publicId?: string;
|
||||||
revisionLabel?: string;
|
revisionLabel?: string;
|
||||||
revisionNumber?: number | string;
|
revisionNumber?: number | string;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -145,11 +140,11 @@ export function RFAForm() {
|
|||||||
const createMutation = useCreateRFA();
|
const createMutation = useCreateRFA();
|
||||||
|
|
||||||
const { data: projectsData, isLoading: isLoadingProjects } = useProjects();
|
const { data: projectsData, isLoading: isLoadingProjects } = useProjects();
|
||||||
const projects = dedupeByKey(extractArrayData<ProjectOption>(projectsData), (project) => project.publicId ?? project.uuid ?? project.id);
|
const projects = dedupeByKey(extractArrayData<ProjectOption>(projectsData), (project) => project.publicId);
|
||||||
const { data: organizationsData, isLoading: isLoadingOrganizations } = useOrganizations({ isActive: true });
|
const { data: organizationsData, isLoading: isLoadingOrganizations } = useOrganizations({ isActive: true });
|
||||||
const organizations = dedupeByKey(
|
const organizations = dedupeByKey(
|
||||||
extractArrayData<OrganizationOption>(organizationsData),
|
extractArrayData<OrganizationOption>(organizationsData),
|
||||||
(organization) => organization.uuid ?? organization.id
|
(organization) => organization.publicId
|
||||||
);
|
);
|
||||||
const { data: correspondenceTypesData } = useCorrespondenceTypes();
|
const { data: correspondenceTypesData } = useCorrespondenceTypes();
|
||||||
const correspondenceTypes = extractArrayData<CorrespondenceTypeOption>(correspondenceTypesData);
|
const correspondenceTypes = extractArrayData<CorrespondenceTypeOption>(correspondenceTypesData);
|
||||||
@@ -185,7 +180,7 @@ export function RFAForm() {
|
|||||||
const { data: contractsData, isLoading: isLoadingContracts } = useContracts(selectedProjectId);
|
const { data: contractsData, isLoading: isLoadingContracts } = useContracts(selectedProjectId);
|
||||||
const contracts = dedupeByKey(
|
const contracts = dedupeByKey(
|
||||||
extractArrayData<ContractOption & Contract>(contractsData),
|
extractArrayData<ContractOption & Contract>(contractsData),
|
||||||
(contract) => contract.publicId ?? contract.uuid ?? contract.id
|
(contract) => contract.publicId
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedContractId = watch('contractId');
|
const selectedContractId = watch('contractId');
|
||||||
@@ -196,27 +191,27 @@ export function RFAForm() {
|
|||||||
const [shopDrawingSearch, setShopDrawingSearch] = useState('');
|
const [shopDrawingSearch, setShopDrawingSearch] = useState('');
|
||||||
const [shopDrawingPage, setShopDrawingPage] = useState(1);
|
const [shopDrawingPage, setShopDrawingPage] = useState(1);
|
||||||
const { data: shopDrawingsData, isLoading: isLoadingShopDrawings } = useDrawings('SHOP', {
|
const { data: shopDrawingsData, isLoading: isLoadingShopDrawings } = useDrawings('SHOP', {
|
||||||
projectUuid: selectedProjectId || '',
|
projectPublicId: selectedProjectId || '',
|
||||||
search: shopDrawingSearch,
|
search: shopDrawingSearch,
|
||||||
page: shopDrawingPage,
|
page: shopDrawingPage,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
});
|
});
|
||||||
const shopDrawings = dedupeByKey(
|
const shopDrawings = dedupeByKey(
|
||||||
extractArrayData<SelectableDrawingOption>(shopDrawingsData),
|
extractArrayData<SelectableDrawingOption>(shopDrawingsData),
|
||||||
(drawing) => drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid ?? drawing.uuid
|
(drawing) => drawing.currentRevisionPublicId ?? drawing.currentRevision?.publicId ?? drawing.publicId
|
||||||
);
|
);
|
||||||
|
|
||||||
const [asBuiltDrawingSearch, setAsBuiltDrawingSearch] = useState('');
|
const [asBuiltDrawingSearch, setAsBuiltDrawingSearch] = useState('');
|
||||||
const [asBuiltDrawingPage, setAsBuiltDrawingPage] = useState(1);
|
const [asBuiltDrawingPage, setAsBuiltDrawingPage] = useState(1);
|
||||||
const { data: asBuiltDrawingsData, isLoading: isLoadingAsBuiltDrawings } = useDrawings('AS_BUILT', {
|
const { data: asBuiltDrawingsData, isLoading: isLoadingAsBuiltDrawings } = useDrawings('AS_BUILT', {
|
||||||
projectUuid: selectedProjectId || '',
|
projectPublicId: selectedProjectId || '',
|
||||||
search: asBuiltDrawingSearch,
|
search: asBuiltDrawingSearch,
|
||||||
page: asBuiltDrawingPage,
|
page: asBuiltDrawingPage,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
});
|
});
|
||||||
const asBuiltDrawings = dedupeByKey(
|
const asBuiltDrawings = dedupeByKey(
|
||||||
extractArrayData<SelectableDrawingOption>(asBuiltDrawingsData),
|
extractArrayData<SelectableDrawingOption>(asBuiltDrawingsData),
|
||||||
(drawing) => drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid ?? drawing.uuid
|
(drawing) => drawing.currentRevisionPublicId ?? drawing.currentRevision?.publicId ?? drawing.publicId
|
||||||
);
|
);
|
||||||
const selectedDisciplineId = watch('disciplineId');
|
const selectedDisciplineId = watch('disciplineId');
|
||||||
|
|
||||||
@@ -381,7 +376,7 @@ export function RFAForm() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{projects.map((p) => {
|
{projects.map((p) => {
|
||||||
const projectValue = getOptionValue(p.publicId ?? p.uuid ?? p.id);
|
const projectValue = getOptionValue(p.publicId);
|
||||||
|
|
||||||
if (!projectValue) {
|
if (!projectValue) {
|
||||||
return null;
|
return null;
|
||||||
@@ -417,7 +412,7 @@ export function RFAForm() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{contracts.map((c) => {
|
{contracts.map((c) => {
|
||||||
const contractValue = getOptionValue(getContractPublicId(c) || c.uuid);
|
const contractValue = getOptionValue(c.publicId);
|
||||||
|
|
||||||
if (!contractValue) {
|
if (!contractValue) {
|
||||||
return null;
|
return null;
|
||||||
@@ -499,7 +494,7 @@ export function RFAForm() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{organizations.map((organization) => {
|
{organizations.map((organization) => {
|
||||||
const organizationValue = getOptionValue(organization.uuid ?? organization.id);
|
const organizationValue = getOptionValue(organization.publicId);
|
||||||
|
|
||||||
if (!organizationValue) {
|
if (!organizationValue) {
|
||||||
return null;
|
return null;
|
||||||
@@ -551,24 +546,24 @@ export function RFAForm() {
|
|||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
{shopDrawings.map((drawing) => {
|
{shopDrawings.map((drawing) => {
|
||||||
const revisionUuid = drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid;
|
const revisionPublicId = drawing.currentRevisionPublicId ?? drawing.currentRevision?.publicId;
|
||||||
|
|
||||||
if (!revisionUuid) {
|
if (!revisionPublicId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
key={revisionUuid}
|
key={revisionPublicId}
|
||||||
className="flex items-start gap-3 rounded-lg border p-3 cursor-pointer hover:bg-muted/50 transition-colors"
|
className="flex items-start gap-3 rounded-lg border p-3 cursor-pointer hover:bg-muted/50 transition-colors"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedShopDrawingRevisionIds.includes(revisionUuid)}
|
checked={selectedShopDrawingRevisionIds.includes(revisionPublicId)}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
const nextValues =
|
const nextValues =
|
||||||
checked === true
|
checked === true
|
||||||
? [...selectedShopDrawingRevisionIds, revisionUuid]
|
? [...selectedShopDrawingRevisionIds, revisionPublicId]
|
||||||
: selectedShopDrawingRevisionIds.filter((value) => value !== revisionUuid);
|
: selectedShopDrawingRevisionIds.filter((value) => value !== revisionPublicId);
|
||||||
setValue('shopDrawingRevisionIds', nextValues, { shouldDirty: true, shouldValidate: true });
|
setValue('shopDrawingRevisionIds', nextValues, { shouldDirty: true, shouldValidate: true });
|
||||||
clearErrors('shopDrawingRevisionIds');
|
clearErrors('shopDrawingRevisionIds');
|
||||||
}}
|
}}
|
||||||
@@ -647,24 +642,24 @@ export function RFAForm() {
|
|||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-1 gap-3">
|
||||||
{asBuiltDrawings.map((drawing) => {
|
{asBuiltDrawings.map((drawing) => {
|
||||||
const revisionUuid = drawing.currentRevisionUuid ?? drawing.currentRevision?.uuid;
|
const revisionPublicId = drawing.currentRevisionPublicId ?? drawing.currentRevision?.publicId;
|
||||||
|
|
||||||
if (!revisionUuid) {
|
if (!revisionPublicId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
key={revisionUuid}
|
key={revisionPublicId}
|
||||||
className="flex items-start gap-3 rounded-lg border p-3 cursor-pointer hover:bg-muted/50 transition-colors"
|
className="flex items-start gap-3 rounded-lg border p-3 cursor-pointer hover:bg-muted/50 transition-colors"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedAsBuiltDrawingRevisionIds.includes(revisionUuid)}
|
checked={selectedAsBuiltDrawingRevisionIds.includes(revisionPublicId)}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
const nextValues =
|
const nextValues =
|
||||||
checked === true
|
checked === true
|
||||||
? [...selectedAsBuiltDrawingRevisionIds, revisionUuid]
|
? [...selectedAsBuiltDrawingRevisionIds, revisionPublicId]
|
||||||
: selectedAsBuiltDrawingRevisionIds.filter((value) => value !== revisionUuid);
|
: selectedAsBuiltDrawingRevisionIds.filter((value) => value !== revisionPublicId);
|
||||||
setValue('asBuiltDrawingRevisionIds', nextValues, {
|
setValue('asBuiltDrawingRevisionIds', nextValues, {
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
shouldValidate: true,
|
shouldValidate: true,
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
export interface ContractProjectReference {
|
export interface ContractProjectReference {
|
||||||
publicId?: string;
|
publicId?: string;
|
||||||
id?: string;
|
|
||||||
projectCode: string;
|
projectCode: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Contract {
|
export interface Contract {
|
||||||
publicId?: string;
|
publicId?: string;
|
||||||
id?: string;
|
|
||||||
contractCode: string;
|
contractCode: string;
|
||||||
contractName: string;
|
contractName: string;
|
||||||
projectId?: number | string;
|
projectId?: number | string;
|
||||||
@@ -16,10 +14,3 @@ export interface Contract {
|
|||||||
endDate?: string;
|
endDate?: string;
|
||||||
project?: ContractProjectReference;
|
project?: ContractProjectReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getContractPublicId = (contract?: Pick<Contract, 'publicId' | 'id'>): string =>
|
|
||||||
String(contract?.publicId ?? contract?.id ?? '');
|
|
||||||
|
|
||||||
export const getProjectPublicId = (
|
|
||||||
project?: Pick<ContractProjectReference, 'publicId' | 'id'>
|
|
||||||
): string => String(project?.publicId ?? project?.id ?? '');
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
export interface Organization {
|
export interface Organization {
|
||||||
publicId: string; // ADR-019: exposed as 'id' in API responses
|
publicId: string; // ADR-019: public identifier
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
|
||||||
organizationName: string;
|
organizationName: string;
|
||||||
organizationCode: string;
|
organizationCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Attachment {
|
export interface Attachment {
|
||||||
publicId: string; // ADR-019: exposed as 'id' in API responses
|
publicId: string; // ADR-019: public identifier
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
@@ -17,8 +15,7 @@ export interface Attachment {
|
|||||||
|
|
||||||
// Used in List View mainly
|
// Used in List View mainly
|
||||||
export interface CorrespondenceRevision {
|
export interface CorrespondenceRevision {
|
||||||
publicId: string; // ADR-019: exposed as 'id' in API responses
|
publicId: string; // ADR-019: public identifier
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
|
||||||
revisionNumber: number;
|
revisionNumber: number;
|
||||||
revisionLabel?: string; // e.g. "A", "00"
|
revisionLabel?: string; // e.g. "A", "00"
|
||||||
subject: string;
|
subject: string;
|
||||||
@@ -42,22 +39,20 @@ export interface CorrespondenceRevision {
|
|||||||
|
|
||||||
// Nested Relation from Backend Refactor
|
// Nested Relation from Backend Refactor
|
||||||
correspondence: {
|
correspondence: {
|
||||||
publicId: string;
|
publicId: string; // ADR-019: public identifier
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
|
||||||
correspondenceNumber: string;
|
correspondenceNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
originatorId?: number;
|
originatorId?: number;
|
||||||
isInternal: boolean;
|
isInternal: boolean;
|
||||||
originator?: Organization;
|
originator?: Organization;
|
||||||
project?: { publicId: string; id?: number; projectName: string; projectCode: string };
|
project?: { publicId: string; projectName: string; projectCode: string };
|
||||||
type?: { id: number; typeName: string; typeCode: string };
|
type?: { id: number; typeName: string; typeCode: string };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep explicit Correspondence for Detail View if needed, or merge concepts
|
// Keep explicit Correspondence for Detail View if needed, or merge concepts
|
||||||
export interface Correspondence {
|
export interface Correspondence {
|
||||||
publicId: string; // ADR-019: exposed as 'id' in API responses
|
publicId: string; // ADR-019: public identifier
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
|
||||||
correspondenceNumber: string;
|
correspondenceNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
originatorId?: number;
|
originatorId?: number;
|
||||||
@@ -67,7 +62,7 @@ export interface Correspondence {
|
|||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
originator?: Organization;
|
originator?: Organization;
|
||||||
project?: { publicId: string; id?: number; projectName: string; projectCode: string };
|
project?: { publicId: string; projectName: string; projectCode: string };
|
||||||
type?: { id: number; typeName: string; typeCode: string };
|
type?: { id: number; typeName: string; typeCode: string };
|
||||||
revisions?: CorrespondenceRevision[]; // Nested revisions
|
revisions?: CorrespondenceRevision[]; // Nested revisions
|
||||||
recipients?: {
|
recipients?: {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Entity Interfaces
|
// Entity Interfaces
|
||||||
export interface DrawingRevision {
|
export interface DrawingRevision {
|
||||||
publicId: string; // ADR-019: exposed as 'id' in API responses
|
publicId: string; // ADR-019: public identifier
|
||||||
revisionId?: number; // Excluded from API responses (ADR-019)
|
|
||||||
revisionNumber: string;
|
revisionNumber: string;
|
||||||
title?: string; // Added
|
title?: string; // Added
|
||||||
legacyDrawingNumber?: string; // Added
|
legacyDrawingNumber?: string; // Added
|
||||||
@@ -15,8 +14,7 @@ export interface DrawingRevision {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ContractDrawing {
|
export interface ContractDrawing {
|
||||||
publicId: string; // ADR-019: exposed as 'id' in API responses
|
publicId: string; // ADR-019: public identifier
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
|
||||||
contractDrawingNo: string;
|
contractDrawingNo: string;
|
||||||
title: string;
|
title: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
@@ -28,8 +26,7 @@ export interface ContractDrawing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ShopDrawing {
|
export interface ShopDrawing {
|
||||||
publicId: string; // ADR-019: exposed as 'id' in API responses
|
publicId: string; // ADR-019: public identifier
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
|
||||||
drawingNumber: string;
|
drawingNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
mainCategoryId: number;
|
mainCategoryId: number;
|
||||||
@@ -41,8 +38,7 @@ export interface ShopDrawing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AsBuiltDrawing {
|
export interface AsBuiltDrawing {
|
||||||
publicId: string; // ADR-019: exposed as 'id' in API responses
|
publicId: string; // ADR-019: public identifier
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
|
||||||
drawingNumber: string;
|
drawingNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
mainCategoryId: number;
|
mainCategoryId: number;
|
||||||
@@ -54,8 +50,7 @@ export interface AsBuiltDrawing {
|
|||||||
|
|
||||||
// Unified Type for List
|
// Unified Type for List
|
||||||
export interface Drawing {
|
export interface Drawing {
|
||||||
publicId?: string; // ADR-019: exposed as 'id' in API responses
|
publicId?: string; // ADR-019: public identifier
|
||||||
drawingId?: number; // Excluded from API responses (ADR-019)
|
|
||||||
drawingNumber: string;
|
drawingNumber: string;
|
||||||
title: string; // Display title (from current revision for Shop/AsBuilt)
|
title: string; // Display title (from current revision for Shop/AsBuilt)
|
||||||
discipline?: string | { disciplineCode: string; disciplineName: string };
|
discipline?: string | { disciplineCode: string; disciplineName: string };
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export interface Discipline {
|
|||||||
codeNameTh?: string;
|
codeNameTh?: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
contract?: {
|
contract?: {
|
||||||
id?: number;
|
|
||||||
publicId?: string;
|
publicId?: string;
|
||||||
contractCode: string;
|
contractCode: string;
|
||||||
contractName: string;
|
contractName: string;
|
||||||
@@ -33,7 +32,6 @@ export interface RfaType {
|
|||||||
remark?: string;
|
remark?: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
contract?: {
|
contract?: {
|
||||||
id?: number;
|
|
||||||
publicId?: string;
|
publicId?: string;
|
||||||
contractCode: string;
|
contractCode: string;
|
||||||
contractName: string;
|
contractName: string;
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ export interface RFAItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RFA {
|
export interface RFA {
|
||||||
publicId: string; // ADR-019: from correspondence.publicId
|
publicId: string; // ADR-019: public identifier (from correspondence.publicId)
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
|
||||||
rfaTypeId: number;
|
rfaTypeId: number;
|
||||||
createdBy: number;
|
createdBy: number;
|
||||||
disciplineId?: number;
|
disciplineId?: number;
|
||||||
@@ -56,8 +55,7 @@ export interface RFA {
|
|||||||
};
|
};
|
||||||
// Shared Correspondence Relation
|
// Shared Correspondence Relation
|
||||||
correspondence?: {
|
correspondence?: {
|
||||||
publicId: string;
|
publicId: string; // ADR-019: public identifier
|
||||||
id?: number; // Excluded from API responses (ADR-019)
|
|
||||||
correspondenceNumber: string;
|
correspondenceNumber: string;
|
||||||
projectId: number;
|
projectId: number;
|
||||||
originatorId?: number;
|
originatorId?: number;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
// EDITOR SETTINGS
|
// EDITOR SETTINGS
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
"editor.fontSize": 20,
|
"editor.fontSize": 16,
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"editor.lineHeight": 1.6,
|
"editor.lineHeight": 1.6,
|
||||||
"editor.rulers": [80, 120],
|
"editor.rulers": [80, 120],
|
||||||
|
|||||||
Generated
+15
-9
@@ -211,8 +211,8 @@ importers:
|
|||||||
specifier: ^0.3.27
|
specifier: ^0.3.27
|
||||||
version: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@25.5.0)(typescript@5.9.3))
|
version: 0.3.27(ioredis@5.8.2)(mysql2@3.15.3)(redis@4.7.1)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@25.5.0)(typescript@5.9.3))
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^11.0.0
|
specifier: ^9.0.0
|
||||||
version: 11.1.0
|
version: 9.0.0
|
||||||
winston:
|
winston:
|
||||||
specifier: ^3.18.3
|
specifier: ^3.18.3
|
||||||
version: 3.18.3
|
version: 3.18.3
|
||||||
@@ -222,7 +222,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@compodoc/compodoc':
|
'@compodoc/compodoc':
|
||||||
specifier: ^1.1.32
|
specifier: ^1.1.32
|
||||||
version: 1.1.32(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(typescript@5.9.3)(vis-data@8.0.3(uuid@11.1.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))
|
version: 1.1.32(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(typescript@5.9.3)(vis-data@8.0.3(uuid@9.0.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))
|
||||||
'@eslint/eslintrc':
|
'@eslint/eslintrc':
|
||||||
specifier: ^3.2.0
|
specifier: ^3.2.0
|
||||||
version: 3.3.1
|
version: 3.3.1
|
||||||
@@ -8251,6 +8251,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
uuid@9.0.0:
|
||||||
|
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
v8-compile-cache-lib@3.0.1:
|
v8-compile-cache-lib@3.0.1:
|
||||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||||
|
|
||||||
@@ -9973,7 +9977,7 @@ snapshots:
|
|||||||
|
|
||||||
'@colors/colors@1.6.0': {}
|
'@colors/colors@1.6.0': {}
|
||||||
|
|
||||||
'@compodoc/compodoc@1.1.32(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(typescript@5.9.3)(vis-data@8.0.3(uuid@11.1.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))':
|
'@compodoc/compodoc@1.1.32(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(typescript@5.9.3)(vis-data@8.0.3(uuid@9.0.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular-devkit/schematics': 20.3.4(chokidar@4.0.3)
|
'@angular-devkit/schematics': 20.3.4(chokidar@4.0.3)
|
||||||
'@babel/core': 7.28.4
|
'@babel/core': 7.28.4
|
||||||
@@ -10017,7 +10021,7 @@ snapshots:
|
|||||||
tablesort: 5.6.0
|
tablesort: 5.6.0
|
||||||
ts-morph: 27.0.2
|
ts-morph: 27.0.2
|
||||||
uuid: 11.1.0
|
uuid: 11.1.0
|
||||||
vis-network: 10.0.2(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(uuid@11.1.0)(vis-data@8.0.3(uuid@11.1.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))
|
vis-network: 10.0.2(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(uuid@11.1.0)(vis-data@8.0.3(uuid@9.0.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@egjs/hammerjs'
|
- '@egjs/hammerjs'
|
||||||
- component-emitter
|
- component-emitter
|
||||||
@@ -17498,6 +17502,8 @@ snapshots:
|
|||||||
|
|
||||||
uuid@8.3.2: {}
|
uuid@8.3.2: {}
|
||||||
|
|
||||||
|
uuid@9.0.0: {}
|
||||||
|
|
||||||
v8-compile-cache-lib@3.0.1: {}
|
v8-compile-cache-lib@3.0.1: {}
|
||||||
|
|
||||||
v8-to-istanbul@9.3.0:
|
v8-to-istanbul@9.3.0:
|
||||||
@@ -17510,18 +17516,18 @@ snapshots:
|
|||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vis-data@8.0.3(uuid@11.1.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)):
|
vis-data@8.0.3(uuid@9.0.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
uuid: 11.1.0
|
uuid: 9.0.0
|
||||||
vis-util: 6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)
|
vis-util: 6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)
|
||||||
|
|
||||||
vis-network@10.0.2(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(uuid@11.1.0)(vis-data@8.0.3(uuid@11.1.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)):
|
vis-network@10.0.2(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)(keycharm@0.4.0)(uuid@11.1.0)(vis-data@8.0.3(uuid@9.0.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)))(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@egjs/hammerjs': 2.0.17
|
'@egjs/hammerjs': 2.0.17
|
||||||
component-emitter: 1.3.1
|
component-emitter: 1.3.1
|
||||||
keycharm: 0.4.0
|
keycharm: 0.4.0
|
||||||
uuid: 11.1.0
|
uuid: 11.1.0
|
||||||
vis-data: 8.0.3(uuid@11.1.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))
|
vis-data: 8.0.3(uuid@9.0.0)(vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1))
|
||||||
vis-util: 6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)
|
vis-util: 6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1)
|
||||||
|
|
||||||
vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1):
|
vis-util@6.0.0(@egjs/hammerjs@2.0.17)(component-emitter@1.3.1):
|
||||||
|
|||||||
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)
|
**Backend ใช้ Hybrid ID Strategy:** INT PK (internal) + UUID (public API)
|
||||||
- **Database:** `id` = INT AI (Primary Key), `uuid` = UUID (MariaDB native type)
|
- **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)
|
- **Backend Entity:** `id` = INT (@Exclude), `publicId` = UUID (exposed directly in API)
|
||||||
- **API Response:** ส่ง `id` (ซึ่งจริงๆ คือ `publicId` UUID string) ไม่มี INT id
|
- **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 (และวิธีแก้)
|
### ⚠️ Common Pitfalls (และวิธีแก้)
|
||||||
|
|
||||||
#### 1. ใช้ `.id` กับ Entity ที่ควรใช้ `.publicId`
|
#### 1. ใช้ `.id` กับ Entity ที่ควรใช้ `.publicId`
|
||||||
```tsx
|
```tsx
|
||||||
// ❌ WRONG - entity.id อาจเป็น undefined หรือ INT ที่ถูก @Exclude
|
// ❌ WRONG - entity.id ถูก @Exclude จาก API response
|
||||||
contracts.map((c) => <SelectItem key={c.id} value={String(c.id)}>)
|
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}>)
|
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
|
```tsx
|
||||||
// ❌ WRONG - parseInt บน UUID จะได้ค่า garbage
|
// ❌ WRONG - parseInt บน UUID จะได้ค่า garbage
|
||||||
const id = parseInt(projectId); // "0195..." → 19 (wrong!)
|
const id = parseInt(projectId); // "0195..." → 19 (wrong!)
|
||||||
|
|
||||||
// ✅ CORRECT - ส่ง UUID string ตรงๆ ไป backend
|
// ✅ CORRECT - ส่ง UUID string ตรงๆ ไป backend
|
||||||
const id = projectId; // "019505a1-7c3e-7000-8000-abc123def456"
|
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
|
```tsx
|
||||||
// ❌ WRONG - ใช้ชื่อ field ไม่ตรงกับ TypeScript interface
|
// ❌ WRONG - สมมติว่า API ส่ง { uuid: string } แต่จริงๆ ส่ง { publicId: string }
|
||||||
fields={[{ name: 'type_code', label: 'Code' }]}
|
type OrganizationOption = {
|
||||||
// interface มี typeCode (camelCase) แต่ form ส่ง type_code (snake_case)
|
uuid?: string; // ผิด!
|
||||||
|
organizationName?: string;
|
||||||
|
};
|
||||||
|
const value = org.uuid ?? org.id; // undefined → dropdown ว่าง
|
||||||
|
|
||||||
// ✅ CORRECT - ใช้ชื่อ field ตรงกับ interface
|
// ✅ CORRECT - Type ต้องตรงกับ API Response
|
||||||
fields={[{ name: 'typeCode', label: 'Code' }]}
|
type OrganizationOption = {
|
||||||
|
publicId?: string; // ตรงกับ API
|
||||||
|
organizationName?: string;
|
||||||
|
};
|
||||||
|
const value = org.publicId; // ได้ค่าถูกต้อง
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 4. Contract/Project Select ไม่มีข้อมูล
|
### 📝 Pattern: Select Options with publicId
|
||||||
```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
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// types/master-data.ts - Entity interfaces
|
// types/master-data.ts - Entity interfaces
|
||||||
export interface Contract {
|
export interface Contract {
|
||||||
id?: number; // Internal INT (อาจถูก @Exclude)
|
publicId?: string; // UUID ที่ API ส่ง — ใช้ตัวนี้
|
||||||
publicId?: string; // UUID ที่ API ส่ง (ต้องใช้ตัวนี้)
|
|
||||||
contractCode: string;
|
contractCode: string;
|
||||||
contractName: string;
|
contractName: string;
|
||||||
}
|
}
|
||||||
@@ -674,17 +691,17 @@ export interface Contract {
|
|||||||
// page.tsx - Select options
|
// page.tsx - Select options
|
||||||
const contractOptions = contracts.map((c) => ({
|
const contractOptions = contracts.map((c) => ({
|
||||||
label: `${c.contractName} (${c.contractCode})`,
|
label: `${c.contractName} (${c.contractCode})`,
|
||||||
value: String(c.publicId ?? c.id ?? ''), // ADR-019: publicId เป็น UUID
|
value: c.publicId, // publicId only — no fallback
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// GenericCrudTable fields
|
// GenericCrudTable fields
|
||||||
fields={[
|
fields={[
|
||||||
{
|
{
|
||||||
name: 'contractId',
|
name: 'contractPublicId', // DTO field name
|
||||||
label: 'Contract',
|
label: 'Contract',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
required: true,
|
required: true,
|
||||||
options: contractOptions, // ใช้ UUID string เป็น value
|
options: contractOptions,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
```
|
```
|
||||||
@@ -716,11 +733,11 @@ const columns: ColumnDef<Discipline>[] = [
|
|||||||
|
|
||||||
### ✅ Checklist ก่อน Commit
|
### ✅ Checklist ก่อน Commit
|
||||||
|
|
||||||
- [ ] ใช้ `publicId ?? id` pattern สำหรับ entity identifiers
|
- [ ] ใช้ `publicId` อย่างเดียว (ไม่มี `uuid` หรือ `id` fallback)
|
||||||
- [ ] ไม่ใช้ `parseInt()` บน UUID values
|
- [ ] ไม่ใช้ `parseInt()` บน UUID values
|
||||||
- [ ] Field names ตรงกับ TypeScript interfaces (camelCase)
|
- [ ] Type Definition ตรงกับ API Response field names
|
||||||
- [ ] Select options ใช้ UUID string เป็น value
|
- [ ] Select options ใช้ `publicId` เป็น value
|
||||||
- [ ] แสดง relation columns (Contract/Project) ในตาราง
|
- [ ] DTO field names ใช้ `publicId` suffix (e.g., `projectPublicId`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -243,21 +243,31 @@ async findByUuidOrId(identifier: string): Promise<Entity> {
|
|||||||
|
|
||||||
#### Pattern: Drawing Search (✅ FIXED — reference implementation)
|
#### Pattern: Drawing Search (✅ FIXED — reference implementation)
|
||||||
|
|
||||||
- Backend DTO accepts `projectUuid: string` instead of `projectId: number`
|
- Backend DTO accepts `projectPublicId: string` instead of `projectId: number`
|
||||||
- Controller resolves: `projectService.findOneByUuid(dto.projectUuid)` → `dto.projectId = project.id`
|
- Controller resolves: `projectService.findOneByUuid(dto.projectPublicId)` → `dto.projectId = project.id`
|
||||||
- Frontend sends UUID string directly (no `parseInt`)
|
- 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 |
|
| File | Field | Entity | Issue |
|
||||||
| ------------------------------------- | ----------------------------------------------------- | ------------ | ------------------------------------------------------------ |
|
| ------------------------------------- | ----------------------------------------------------- | ------------ | ------------------------------------------------------------ |
|
||||||
| `correspondences/form.tsx:212` | `projectId` | Project | `parseInt(p.id)` where `p.id` = UUID string (garbled number) |
|
| `correspondences/form.tsx` | `projectPublicId` | Project | Type uses `id` instead of `publicId` |
|
||||||
| `correspondences/form.tsx:326` | `fromOrganizationId` | Organization | `parseInt(String(org.id))` where `org.id` = undefined (NaN) |
|
| `correspondences/form.tsx` | `fromOrganizationPublicId` | Organization | Type uses `uuid/id` instead of `publicId` |
|
||||||
| `correspondences/form.tsx:349` | `toOrganizationId` | Organization | Same as above |
|
| `correspondences/form.tsx` | `toOrganizationPublicId` | Organization | Type uses `uuid/id` instead of `publicId` |
|
||||||
| `admin/users/page.tsx:47` | `primaryOrganizationId` (filter) | Organization | `parseInt(selectedOrgId)` where value = UUID string |
|
| `admin/users/page.tsx` | `primaryOrganizationPublicId` (filter) | Organization | Type uses `id` instead of `publicId` |
|
||||||
| `admin/user-dialog.tsx:226` | `primaryOrganizationId` | Organization | `parseInt(val)` where `org.id` = undefined → `"0"` fallback |
|
| `admin/user-dialog.tsx` | `primaryOrganizationPublicId` | Organization | Type uses `id` instead of `publicId` |
|
||||||
| `numbering/template-tester.tsx:71-74` | `originatorOrganizationId`, `recipientOrganizationId` | Organization | `parseInt` on org UUID |
|
| `numbering/template-tester.tsx` | `originatorOrganizationPublicId` / `recipientOrganizationPublicId` | Organization | Type uses `id` instead of `publicId` |
|
||||||
| `rfas/page.tsx:17` | `projectId` (URL param) | Project | `parseInt(searchParams.get('projectId'))` — UUID if from URL |
|
| `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)
|
#### Fix Strategy (same pattern as Drawing Search fix)
|
||||||
|
|
||||||
@@ -300,16 +310,6 @@ For each affected backend DTO:
|
|||||||
| Order | Task | Effort | Status |
|
| Order | Task | Effort | Status |
|
||||||
| ----- | ------------------------------------------------------------- | ------ | -------------------------- |
|
| ----- | ------------------------------------------------------------- | ------ | -------------------------- |
|
||||||
| 1 | UuidBaseEntity (no transformer needed — MariaDB native UUID) | S | ✅ Done |
|
| 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
|
**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
|
```typescript
|
||||||
export class CorrespondenceResponseDto {
|
export class CorrespondenceResponseDto {
|
||||||
// ✅ Expose publicId as 'id' in API response
|
// ✅ Expose publicId directly in API response
|
||||||
@Expose({ name: 'id' })
|
|
||||||
publicId!: string;
|
publicId!: string;
|
||||||
|
|
||||||
// ❌ Never expose internal INT id
|
// ❌ Never expose internal INT id
|
||||||
@@ -279,11 +278,12 @@ export class CorrespondenceResponseDto {
|
|||||||
|
|
||||||
// ... other fields
|
// ... other fields
|
||||||
// For FK references, also use publicId
|
// For FK references, also use publicId
|
||||||
@Expose({ name: 'project_id' })
|
|
||||||
projectPublicId!: string;
|
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
|
## Migration SQL Script
|
||||||
@@ -473,11 +473,29 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
|
|||||||
- API รับได้ทั้ง INT และ UUID ผ่าน `FindByIdOrUuid` pattern
|
- API รับได้ทั้ง INT และ UUID ผ่าน `FindByIdOrUuid` pattern
|
||||||
- API Response รวม UUID เป็น `id` field (via @Expose)
|
- 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
|
- Frontend ใช้ `publicId` เป็น Standard ทุก Type (ไม่ใช้ `uuid` หรือ `id` ที่เป็น number)
|
||||||
- URL parameters เปลี่ยนเป็น UUID
|
- URL parameters ใช้ `publicId` (UUID string)
|
||||||
- ไม่ต้อง Big-Bang migration — ค่อยๆ เปลี่ยนทีละ Module
|
- ทุก 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
|
### Phase 4: Cleanup
|
||||||
|
|
||||||
@@ -492,7 +510,7 @@ WHERE c.uuid = '019505a1-7c3e-7000-8000-abc123def456';
|
|||||||
| Area | Status |
|
| Area | Status |
|
||||||
| ---------------------- | ------------------------------------ |
|
| ---------------------- | ------------------------------------ |
|
||||||
| Security | ✅ Eliminates ID enumeration |
|
| Security | ✅ Eliminates ID enumeration |
|
||||||
| Performance | ✅ No impact on internal JOINs |
|
| Performance | ✅ No impact on internal JOINs |
|
||||||
| Migration Risk | ✅ Low — ADD COLUMN only |
|
| Migration Risk | ✅ Low — ADD COLUMN only |
|
||||||
| Storage Impact | ✅ Negligible (~3.8 MB) |
|
| Storage Impact | ✅ Negligible (~3.8 MB) |
|
||||||
| Backward Compatibility | ✅ Dual-mode transition |
|
| 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`_
|
_สำหรับรายละเอียดการ Implement ดูที่ Implementation Plan ใน `05-07-hybrid-uuid-implementation-plan.md`_
|
||||||
|
|||||||
Reference in New Issue
Block a user