hrms-api-org/reports/batch-07-controllers-61-70-analysis.md
DESKTOP-1R2VSQH\Lenovo ThinkPad E490 85e9be08f6 report: Controllers
2026-05-08 18:15:03 +07:00

11 KiB

รายงานการวิเคราะห์จุดเสี่ยง Unhandled Exception - Controllers ชุดที่ 7 (61-70)

วันที่วิเคราะห์: 2026-05-08

สรุปผลการวิเคราะห์

จากการตรวจสอบ Controllers ทั้ง 10 ไฟล์ (61-70):

  1. ProfileAssistanceController
  2. ProfileAssistanceEmployeeController
  3. ProfileAssistanceEmployeeTempController
  4. ProfileCertificateController
  5. ProfileCertificateEmployeeController
  6. ProfileCertificateEmployeeTempController
  7. ProfileChildrenController
  8. ProfileChildrenEmployeeController
  9. ProfileChildrenEmployeeTempController
  10. ProfileDisciplineController

พบ 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: ProfileAssistanceController.ts, ProfileAssistanceEmployeeController.ts, ProfileAssistanceEmployeeTempController.ts

Method: detailProfileAssistanceUser, detailProfileAssistance, getProfileAssistanceHistory, getProfileAdminAssistanceHistory

Problem Type: 2. Missing Error Handle (Logic Issue)

Root Cause:

// Lines 42-48 (ProfileAssistanceController.ts)
const getProfileAssistanceId = await this.profileAssistanceRepo.find({
  where: { profileId: profile.id, isDeleted: false },
  order: { createdAt: "ASC" },
});
if (!getProfileAssistanceId) {
  throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}

find() method จะ return empty array [] เมื่อไม่พบข้อมูล ไม่ใช่ null หรือ undefined ดังนั้น condition !getProfileAssistanceId จะไม่เคยเป็น true

Recommended Fix:

const getProfileAssistanceId = await this.profileAssistanceRepo.find({
  where: { profileId: profile.id, isDeleted: false },
  order: { createdAt: "ASC" },
});
if (getProfileAssistanceId.length === 0) {
  throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
// หรือถ้าต้องการให้ return empty array ได้
return new HttpSuccess(getProfileAssistanceId);

2. File: ProfileCertificateController.ts, ProfileCertificateEmployeeController.ts

Method: deleteCertificate

Problem Type: 2. Missing Error Handle (Logic Error)

Root Cause:

// Lines 226-228 (ProfileCertificateController.ts)
if (certificateResult.affected && certificateResult.affected <= 0) {
  throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}

Logic ผิด เพราะ certificateResult.affected && certificateResult.affected <= 0 จะเป็น false เมื่อ affected = 0 (เนื่องจาก 0 ถือเป็น falsy value) ทำให้ไม่เคย throw error

Recommended Fix:

if (certificateResult.affected === undefined || certificateResult.affected <= 0) {
  throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}

3. All Controllers

Method: ทุก Method ที่ใช้ Promise.all

Problem Type: 2. Missing Error Handle (Partial Failure)

Root Cause:

// Pattern ที่ใช้ในหลาย ๆ Controller
await Promise.all([
  this.profileAssistanceRepo.save(record, { data: req }),
  setLogDataDiff(req, { before, after: record }),
  this.profileAssistanceHistoryRepo.save(history, { data: req }),
]);

ถ้า setLogDataDiff หรือ historyRepo.save ล้มเหลว แต่ profileAssistanceRepo.save สำเร็จ จะเกิด Data Inconsistency

Recommended Fix:

// ใช้ Transaction ของ TypeORM แทน
await AppDataSource.transaction(async (transactionalEntityManager) => {
  await transactionalEntityManager.save(ProfileAssistance, record);
  await transactionalEntityManager.save(ProfileAssistanceHistory, history);
});
setLogDataDiff(req, { before, after: record });

4. File: ProfileChildrenController.ts, ProfileChildrenEmployeeController.ts, ProfileChildrenEmployeeTempController.ts

Method: newChildren, editChildren

Problem Type: 2. Missing Error Handle (Unhandled Extension Function)

Root Cause:

// Lines 96, 125 (ProfileChildrenController.ts)
data.childrenCitizenId = Extension.CheckCitizen(String(data.childrenCitizenId));

ถ้า Extension.CheckCitizen() มีการ throw error จะทำให้เกิด Unhandled Exception

Recommended Fix:

try {
  data.childrenCitizenId = Extension.CheckCitizen(String(data.childrenCitizenId));
} catch (error) {
  throw new HttpError(HttpStatus.BAD_REQUEST, "รูปแบบเลขบัตรประชาชนไม่ถูกต้อง");
}

5. File: ProfileDisciplineController.ts

Method: editDiscipline

Problem Type: 2. Missing Error Handle (Inconsistent Code Pattern)

Root Cause:

// Lines 166-173 (ProfileDisciplineController.ts)
// await Promise.all(
this.disciplineRepository.save(record, { data: req });
setLogDataDiff(req, { before, after: record });
if (!(Object.keys(body).length === 1 && body.isUpload)) {
  this.disciplineHistoryRepository.save(history, { data: req });
  // setLogDataDiff(req, { before, after: history });
}
// );

มีการ comment out Promise.all แต่ยังคงเรียก save() โดยไม่มี await ในบางจุด ซึ่งอาจทำให้เกิด race condition

Recommended Fix:

await Promise.all([
  this.disciplineRepository.save(record, { data: req }),
  setLogDataDiff(req, { before, after: record }),
  ...(Object.keys(body).length === 1 && body.isUpload
    ? []
    : [this.disciplineHistoryRepository.save(history, { data: req })]),
]);

สรุปคำแนะนำการแก้ไข

ระดับความสำคัญ: สูง

  1. แก้ไข Logic การตรวจสอบผลลัพธ์จาก find() - ใช้ .length === 0 แทน !result
  2. แก้ไข Logic การตรวจสอบ affected - ใช้ === undefined || <= 0 แทน && <= 0
  3. ใช้ Transaction สำหรับ Operations ที่ต้องบันทึกข้อมูลหลายตาราง - เพื่อป้องกัน Data Inconsistency

ระดับความสำคัญ: ปานกลาง

  1. เพิ่ม Error Handling รอบ ๆ Extension Functions - เพื่อป้องกัน Unhandled Exception
  2. ทำให้ Pattern การใช้ Promise/await สอดคล้องกัน - หลีกเลี่ยงการเรียก save() โดยไม่มี await

ระดับความสำคัญ: ต่ำ

  1. Refactor code ให้ใช้ Transaction Manager - เพื่อให้ code สะอาดและปลอดภัยมากขึ้น
  2. เพิ่ม Error Boundary หรือ Global Error Handler - เพื่อจัดการ error ที่ไม่คาดคิด

การจัดการ Error ที่ดีที่สุดสำหรับ Microservices

// 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
});

// 4. ตรวจสอบผลลัพธ์จาก find() อย่างถูกต้อง
const results = await repo.find({ where: condition });
if (results.length === 0) {
  throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}

// 5. ตรวจสอบ affected อย่างถูกต้อง
const result = await repo.delete({ id });
if (result.affected === undefined || result.affected <= 0) {
  throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}

สรุป

Controllers ในชุดที่ 7 (61-70) มีความเสี่ยงต่ำต่อการเกิด Unhandled Exception ที่จะทำให้ Service Crash แต่มีจุดที่ควรปรับปรุงเพื่อ:

  1. ป้องกัน Logic Errors - โดยการตรวจสอบผลลัพธ์จาก find() และ affected อย่างถูกต้อง
  2. ป้องกัน Data Inconsistency - โดยการใช้ Transaction
  3. เพิ่มความแข็งแกร่งของระบบ - โดยการเพิ่ม Error Handling รอบ ๆ Extension Functions

ไม่มีจุดเสี่ยงระดับวิกฤตที่จะทำให้เกิด Crash Loop ในทันที แต่ควรปรับปรุงตามคำแนะนำเพื่อเพิ่มความเสถียรของระบบในระยะยาว