254 lines
10 KiB
Markdown
254 lines
10 KiB
Markdown
|
|
# รายงานการวิเคราะห์จุดเสี่ยง Unhandled Exception - Controllers ชุดที่ 6 (51-60)
|
||
|
|
|
||
|
|
## วันที่วิเคราะห์: 2026-05-08
|
||
|
|
|
||
|
|
## สรุปผลการวิเคราะห์
|
||
|
|
|
||
|
|
จากการตรวจสอบ Controllers ทั้ง 10 ไฟล์ (51-60):
|
||
|
|
1. ProfileAbilityEmployeeController
|
||
|
|
2. ProfileAbilityEmployeeTempController
|
||
|
|
3. ProfileAbsentLateController
|
||
|
|
4. ProfileActpositionController
|
||
|
|
5. ProfileActpositionEmployeeController
|
||
|
|
6. ProfileActpositionEmployeeTempController
|
||
|
|
7. ProfileAddressController
|
||
|
|
8. ProfileAddressEmployeeController
|
||
|
|
9. ProfileAddressEmployeeTempController
|
||
|
|
10. ProfileAssessmentsController
|
||
|
|
|
||
|
|
พบ **0 จุดเสี่ยงระดับวิกฤต** ที่อาจทำให้เกิด Unhandled Exception และ Crash Loop ในระบบ Microservices
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## รายละเอียดจุดเสี่ยงที่พบ
|
||
|
|
|
||
|
|
### ไม่พบจุดเสี่ยงระดับวิกฤต
|
||
|
|
|
||
|
|
Controllers ทั้งหมดในชุดนี้มีการจัดการ Error ที่ดี โดย:
|
||
|
|
|
||
|
|
1. **ทุก Method ใช้ async/await อย่างถูกต้อง** - ไม่มี Promise ที่ถูกเรียกโดยไม่มี await
|
||
|
|
2. **มีการ throw HttpError** - เมื่อเกิด Error จะ throw HttpError ที่มี Status Code ที่ชัดเจน
|
||
|
|
3. **Database Operations ล้วนอยู่ใน try-catch โดยนัย** - TypeORM repositories มีการ handle error ภายใน
|
||
|
|
4. **ใช้ Promise.all อย่างปลอดภัย** - ใน operations ที่ต้องบันทึกข้อมูลหลายจุดพร้อมกัน
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## จุดที่ควรปรับปรุง (แนะนำ)
|
||
|
|
|
||
|
|
แม้จะไม่พบจุดเสี่ยงระดับวิกฤต แต่มีจุดที่ควรปรับปรุงเพื่อเพิ่มความแข็งแกร่งของระบบ:
|
||
|
|
|
||
|
|
### 1. File: ProfileAbilityEmployeeController.ts, ProfileAbilityEmployeeTempController.ts, ProfileActpositionEmployeeController.ts, ProfileActpositionEmployeeTempController.ts
|
||
|
|
|
||
|
|
**Method:** `detailProfileAbilityUser`, `detailProfileActpositionUser`
|
||
|
|
|
||
|
|
**Problem Type:** 2. Missing Error Handle (Potential Null Reference)
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
// Lines 42-48
|
||
|
|
const getProfileAbilityId = await this.profileAbilityRepo.find({
|
||
|
|
where: { profileEmployeeId: profile.id, isDeleted: false },
|
||
|
|
order: { createdAt: "ASC" },
|
||
|
|
});
|
||
|
|
if (!getProfileAbilityId) {
|
||
|
|
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
`find()` method จะ return empty array `[]` เมื่อไม่พบข้อมูล ไม่ใช่ `null` หรือ `undefined` ดังนั้น condition `!getProfileAbilityId` จะไม่เคยเป็น true
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
const getProfileAbilityId = await this.profileAbilityRepo.find({
|
||
|
|
where: { profileEmployeeId: profile.id, isDeleted: false },
|
||
|
|
order: { createdAt: "ASC" },
|
||
|
|
});
|
||
|
|
if (getProfileAbilityId.length === 0) {
|
||
|
|
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
|
||
|
|
}
|
||
|
|
// หรือถ้าต้องการให้ return empty array ได้
|
||
|
|
return new HttpSuccess(getProfileAbilityId);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 2. File: ProfileAbsentLateController.ts
|
||
|
|
|
||
|
|
**Method:** `newAbsentLateBatch`
|
||
|
|
|
||
|
|
**Problem Type:** 2. Missing Error Handle (Transaction Safety)
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
// Lines 159-168
|
||
|
|
const result = await this.absentLateRepo.save(records, { data: req });
|
||
|
|
|
||
|
|
// บันทึก history สำหรับแต่ละ record
|
||
|
|
const historyRecords = result.map((data) => {
|
||
|
|
const history = new ProfileAbsentLateHistory();
|
||
|
|
Object.assign(history, { ...data, id: undefined });
|
||
|
|
history.profileAbsentLateId = data.id;
|
||
|
|
return history;
|
||
|
|
});
|
||
|
|
await this.historyRepo.save(historyRecords, { data: req });
|
||
|
|
```
|
||
|
|
|
||
|
|
ถ้าการบันทึก history ล้มเหลว ข้อมูลหลัก (records) จะถูกบันทึกไปแล้ว ทำให้เกิด Data Inconsistency
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
// ใช้ Transaction หรือ wrap ด้วย try-catch
|
||
|
|
try {
|
||
|
|
const result = await this.absentLateRepo.save(records, { data: req });
|
||
|
|
|
||
|
|
const historyRecords = result.map((data) => {
|
||
|
|
const history = new ProfileAbsentLateHistory();
|
||
|
|
Object.assign(history, { ...data, id: undefined });
|
||
|
|
history.profileAbsentLateId = data.id;
|
||
|
|
return history;
|
||
|
|
});
|
||
|
|
|
||
|
|
await this.historyRepo.save(historyRecords, { data: req });
|
||
|
|
|
||
|
|
return new HttpSuccess({ count: result.length, ids: result.map((r) => r.id) });
|
||
|
|
} catch (error) {
|
||
|
|
// ถ้าเกิด error ควร rollback หรือลบข้อมูลที่บันทึกไปแล้ว
|
||
|
|
// หรือใช้ Transaction ของ TypeORM
|
||
|
|
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดในการบันทึกข้อมูล");
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3. File: ProfileActpositionController.ts
|
||
|
|
|
||
|
|
**Method:** `getProfileActpositionHistory`
|
||
|
|
|
||
|
|
**Problem Type:** 2. Missing Error Handle (Potential Null Reference in Relations)
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
// Lines 95-104
|
||
|
|
const record = await this.profileActpositionHistoryRepo.find({
|
||
|
|
relations: ["histories"],
|
||
|
|
where: { profileActpositionId: actpositionId },
|
||
|
|
order: { createdAt: "DESC" },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!record) {
|
||
|
|
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
|
||
|
|
}
|
||
|
|
|
||
|
|
const mappedRecords = record.map(history => {
|
||
|
|
const firstHistory = history.histories ?? [];
|
||
|
|
```
|
||
|
|
|
||
|
|
มีการใช้ `relations: ["histories"]` แต่ไม่มีการตรวจสอบว่า relation นี้มีอยู่จริงใน Entity หรือไม่ ถ้า relation ไม่ถูกต้องอาจเกิด error
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
try {
|
||
|
|
const record = await this.profileActpositionHistoryRepo.find({
|
||
|
|
relations: ["histories"],
|
||
|
|
where: { profileActpositionId: actpositionId },
|
||
|
|
order: { createdAt: "DESC" },
|
||
|
|
});
|
||
|
|
|
||
|
|
if (record.length === 0) {
|
||
|
|
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
|
||
|
|
}
|
||
|
|
|
||
|
|
const mappedRecords = record.map(history => {
|
||
|
|
const firstHistory = Array.isArray(history.histories) ? history.histories[0] : null;
|
||
|
|
return {
|
||
|
|
// ... rest of mapping
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
return new HttpSuccess(mappedRecords);
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof HttpError) throw error;
|
||
|
|
throw new HttpError(HttpStatus.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดในการดึงข้อมูล");
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 4. All Controllers
|
||
|
|
|
||
|
|
**Method:** ทุก Method ที่ใช้ `Promise.all`
|
||
|
|
|
||
|
|
**Problem Type:** 2. Missing Error Handle (Partial Failure)
|
||
|
|
|
||
|
|
**Root Cause:**
|
||
|
|
```typescript
|
||
|
|
// Pattern ที่ใช้ในหลาย ๆ Controller
|
||
|
|
await Promise.all([
|
||
|
|
this.profileRepo.save(record, { data: req }),
|
||
|
|
setLogDataDiff(req, { before, after: record }),
|
||
|
|
this.historyRepo.save(history, { data: req }),
|
||
|
|
]);
|
||
|
|
```
|
||
|
|
|
||
|
|
ถ้า `setLogDataDiff` หรือ `historyRepo.save` ล้มเหลว แต่ `profileRepo.save` สำเร็จ จะเกิด Data Inconsistency
|
||
|
|
|
||
|
|
**Recommended Fix:**
|
||
|
|
```typescript
|
||
|
|
// ใช้ Transaction ของ TypeORM แทน
|
||
|
|
await AppDataSource.transaction(async (transactionalEntityManager) => {
|
||
|
|
await transactionalEntityManager.save(Profile, record);
|
||
|
|
await transactionalEntityManager.save(ProfileHistory, history);
|
||
|
|
// setLogDataDiff ควรอยู่นอก transaction หรือ handle error แยก
|
||
|
|
});
|
||
|
|
setLogDataDiff(req, { before, after: record });
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## สรุปคำแนะนำการแก้ไข
|
||
|
|
|
||
|
|
### ระดับความสำคัญ: สูง
|
||
|
|
1. **ใช้ Transaction สำหรับ Operations ที่ต้องบันทึกข้อมูลหลายตาราง** - เพื่อป้องกัน Data Inconsistency
|
||
|
|
2. **ตรวจสอบค่าที่ return จาก `find()` อย่างถูกต้อง** - ใช้ `.length === 0` แทน `!result`
|
||
|
|
|
||
|
|
### ระดับความสำคัญ: ปานกลาง
|
||
|
|
1. **เพิ่ม Error Boundary หรือ Global Error Handler** - เพื่อจัดการ error ที่ไม่คาดคิด
|
||
|
|
2. **Log error ที่เกิดขึ้น** - เพื่อช่วยในการ Debug และ Monitor
|
||
|
|
|
||
|
|
### ระดับความสำคัญ: ต่ำ
|
||
|
|
1. **Refactor code ให้ใช้ Transaction Manager** - เพื่อให้ code สะอาดและปลอดภัยมากขึ้น
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## การจัดการ Error ที่ดีที่สุดสำหรับ Microservices
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 1. ใช้ AsyncHandler Wrapper
|
||
|
|
export const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => {
|
||
|
|
Promise.resolve(fn(req, res, next)).catch(next);
|
||
|
|
};
|
||
|
|
|
||
|
|
// 2. ใช้ Global Error Handler
|
||
|
|
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
|
||
|
|
console.error('Unhandled error:', error);
|
||
|
|
res.status(500).json({ error: 'Internal server error' });
|
||
|
|
});
|
||
|
|
|
||
|
|
// 3. ใช้ Transaction สำหรับ Database Operations
|
||
|
|
await AppDataSource.transaction(async (manager) => {
|
||
|
|
// All database operations here
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## สรุป
|
||
|
|
|
||
|
|
Controllers ในชุดที่ 6 (51-60) มีความเสี่ยงต่ำต่อการเกิด **Unhandled Exception** ที่จะทำให้ Service Crash แต่มีจุดที่ควรปรับปรุงเพื่อ:
|
||
|
|
|
||
|
|
1. **ป้องกัน Data Inconsistency** - โดยการใช้ Transaction
|
||
|
|
2. **ปรับปรุง Logic การตรวจสอบข้อมูล** - โดยการเช็ค length ของ array ที่ return จาก find()
|
||
|
|
3. **เพิ่มความแข็งแกร่งของระบบ** - โดยการเพิ่ม Error Handling และ Logging
|
||
|
|
|
||
|
|
**ไม่มีจุดเสี่ยงระดับวิกฤตที่จะทำให้เกิด Crash Loop ในทันที** แต่ควรปรับปรุงตามคำแนะนำเพื่อเพิ่มความเสถียรของระบบในระยะยาว
|