27 KiB
รายงานการตรวจสอบ Unhandled Exception - Controllers ชุดที่ 2 (ไฟล์ที่ 11-20)
Project: BMA EHR Organization Backend Framework: TSOA + Express + TypeORM วันที่ตรวจสอบ: 2026-05-08 จำนวน Controllers: 10 ไฟล์ สถานะ: เสร็จสิ้น
สรุปผลการตรวจสอบ
| ระดับความรุนแรง | จำนวนจุดเสี่ยง |
|---|---|
| CRITICAL | 1 |
| HIGH | 3 |
| MEDIUM | 4 |
| LOW | 2 |
| BUG | 2 |
| รวมทั้งหมด | 12 |
Controllers ที่ตรวจสอบ
- CommandOperatorController.ts
- CommandSalaryController.ts
- CommandSysController.ts
- CommandTypeController.ts
- DPISController.ts
- DevelopmentRequestController.ts
- DistrictController.ts
- EducationLevelController.ts
- EmployeePosLevelController.ts
- EmployeePosTypeController.ts
รายละเอียดจุดเสี่ยงแต่ละจุด
#1 - Transaction QueryRunner Not Released on Error (CRITICAL)
File & Location: CommandOperatorController.ts:169-222
Method: deleteCommandOperator
Problem Type: 1. Unhandled Exception / 2. Missing Error Handle
Root Cause:
- ใช้ QueryRunner และ Transaction แต่มี error handling ที่ไม่ปลอดภัย
- ถ้าเกิด error หลังจาก
throw errorใน catch block แล้ว จะไม่ถึงfinallyblock - QueryRunner จะไม่ถูก release ทำให้ connection leak
- ในกรณีที่ HttpError ถูก throw ภายใน catch จะไม่มีการ release queryRunner
Code ปัจจุบัน (เสี่ยง):
const queryRunner = AppDataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// ... operations
await queryRunner.commitTransaction();
return new HttpSuccess(true);
} catch (error) {
await queryRunner.rollbackTransaction();
throw error; // ❌ ถ้า throw HttpError จะไม่ถึง finally
} finally {
await queryRunner.release(); // ❌ จะไม่ถูกเรียกถ้า throw error ใน catch
}
Recommended Fix:
const queryRunner = AppDataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. หา operator
const operator = await queryRunner.manager.findOne(CommandOperator, {
where: {
id: operatorId,
commandId: commandId,
},
});
if (!operator) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบเจ้าหน้าที่ดำเนินการ");
}
const removedOrderNo = operator.orderNo;
// 3. ลบ
await queryRunner.manager.remove(operator);
// 4. re orderNumber ตัวที่เหลือ
await queryRunner.manager
.createQueryBuilder()
.update(CommandOperator)
.set({
orderNo: () => "orderNo - 1",
})
.where("commandId = :commandId", { commandId })
.andWhere("orderNo > :removedOrderNo", { removedOrderNo })
.execute();
await queryRunner.commitTransaction();
return new HttpSuccess(true);
} catch (error) {
await queryRunner.rollbackTransaction();
// Re-throw after rollback
throw error;
}
} finally {
// ✅ ใช้ finally block ระดับนอกสุดเพื่อให้แน่ใจว่าจะถูกเรียกเสมอ
if (queryRunner.isReleased) {
// Already released, skip
} else {
await queryRunner.release();
}
}
#2 - Promise.all Without Error Handling in Development Request (HIGH)
File & Location: DevelopmentRequestController.ts:349-364
Method: newDevelopmentRequest
Problem Type: 1. Unhandled Exception / 2. Missing Error Handle
Root Cause:
Promise.all()กับการบันทึก development projects หลายรายการ- ถ้ามี project ไหน save ไม่สำเร็จ จะทำให้ unhandled rejection
- ไม่มี try-catch รองรับ
- External API call ใช้
.catch()แต่ไม่ได้ throw error ต่อ
Code ปัจจุบัน (เสี่ยง):
if (body.developmentProjects != null) {
await Promise.all(
body.developmentProjects.map(async (x) => {
let developmentProject = new DevelopmentProject();
developmentProject.name = x;
developmentProject.createdUserId = req.user.sub;
developmentProject.createdFullName = req.user.name;
developmentProject.lastUpdateUserId = req.user.sub;
developmentProject.lastUpdateFullName = req.user.name;
developmentProject.createdAt = new Date();
developmentProject.lastUpdatedAt = new Date();
developmentProject.developmentRequestId = data.id;
await this.developmentProjectRepository.save(developmentProject, { data: req });
setLogDataDiff(req, { before, after: developmentProject });
}),
);
}
await new CallAPI()
.PostData(req, "/org/workflow/add-workflow", {
refId: data.id,
sysName: "REGISTRY_IDP",
posLevelName: profile.posLevel.posLevelName,
posTypeName: profile.posType.posTypeName,
fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`,
isDeputy: orgRoot?.isDeputy ?? false,
orgRootId: orgRoot?.id ?? null
})
.catch((error) => {
console.error("Error calling API:", error);
});
Recommended Fix:
if (body.developmentProjects != null) {
try {
await Promise.all(
body.developmentProjects.map(async (x) => {
try {
let developmentProject = new DevelopmentProject();
developmentProject.name = x;
developmentProject.createdUserId = req.user.sub;
developmentProject.createdFullName = req.user.name;
developmentProject.lastUpdateUserId = req.user.sub;
developmentProject.lastUpdateFullName = req.user.name;
developmentProject.createdAt = new Date();
developmentProject.lastUpdatedAt = new Date();
developmentProject.developmentRequestId = data.id;
await this.developmentProjectRepository.save(developmentProject, { data: req });
setLogDataDiff(req, { before, after: developmentProject });
} catch (error) {
console.error(`Failed to save development project "${x}":`, error);
throw error; // Re-throw to be caught by Promise.all
}
}),
);
} catch (error) {
console.error("Failed to save some development projects:", error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to save development projects"
);
}
}
// Call external API with proper error handling
try {
await new CallAPI().PostData(req, "/org/workflow/add-workflow", {
refId: data.id,
sysName: "REGISTRY_IDP",
posLevelName: profile.posLevel.posLevelName,
posTypeName: profile.posType.posTypeName,
fullName: `${profile.prefix}${profile.firstName} ${profile.lastName}`,
isDeputy: orgRoot?.isDeputy ?? false,
orgRootId: orgRoot?.id ?? null
});
} catch (error) {
console.error("Failed to call workflow API:", error);
// Optionally mark the request as having workflow issues
// But don't fail the entire request
}
#3 - QueryBuilder with Dynamic Conditions Without Error Handling (HIGH)
File & Location: DevelopmentRequestController.ts:122-265
Method: getDevelopmentRequestAdmin
Problem Type: 2. Missing Error Handle
Root Cause:
- Complex QueryBuilder พร้อม dynamic conditions หลายอย่าง
- ไม่มี try-catch รองรับ
- Permission check อาจ throw error
- Null reference risks หลายจุด (
orgRevisionPublish?.id,data.root, etc.)
Recommended Fix:
@Get("admin")
public async getDevelopmentRequestAdmin(
@Request() request: RequestWithUser,
@Query("status") status: string,
@Query("keyword") keyword: string = "",
@Query("page") page: number = 1,
@Query("pageSize") pageSize: number = 10,
@Query("sortBy") sortBy?: string,
@Query("descending") descending?: boolean,
) {
try {
// Validate inputs
if (page < 1) throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page number");
if (pageSize < 1 || pageSize > 1000) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page size");
}
let data = await new permission().PermissionOrgList(request, "SYS_REGISTRY_OFFICER");
const orgRevisionPublish = await this.orgRevisionRepository
.createQueryBuilder("orgRevision")
.where("orgRevision.orgRevisionIsDraft = false")
.andWhere("orgRevision.orgRevisionIsCurrent = true")
.getOne();
let query = await AppDataSource.getRepository(DevelopmentRequest)
.createQueryBuilder("developmentRequest")
.leftJoinAndSelect("developmentRequest.profile", "profile")
.leftJoinAndSelect("profile.current_holders", "current_holders")
.leftJoinAndSelect("current_holders.orgRevision", "orgRevision")
.andWhere(
status == undefined || status.trim().toUpperCase() == "ALL" || status == ""
? "1=1"
: "developmentRequest.status = :status",
{
status: status == undefined || status == null ? "" : status.trim().toUpperCase(),
},
)
.andWhere(
orgRevisionPublish ? `current_holders.orgRevisionId = :revisionId` : "1=1",
{
revisionId: orgRevisionPublish?.id,
},
)
// ... rest of the query
const [lists, total] = await query
.skip((page - 1) * pageSize)
.take(pageSize)
.getManyAndCount();
const _data = lists.map((item) => ({ ...item, profile: null }));
return new HttpSuccess({ data: _data, total });
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
console.error('Failed to get development requests:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to retrieve development requests"
);
}
}
#4 - Promise.all in Edit Development Request Without Error Handling (HIGH)
File & Location: DevelopmentRequestController.ts:402-417
Method: editUserDevelopmentRequest
Problem Type: 1. Unhandled Exception / 2. Missing Error Handle
Root Cause:
- ใช้
Promise.all()โดยไม่มี error handling - Similar to #2 but in edit operation
Code ปัจจุบัน (เสี่ยง):
await this.developmentProjectRepository.delete({ developmentRequestId: record.id });
if (body.developmentProjects != null) {
await Promise.all(
body.developmentProjects.map(async (x) => {
let developmentProject = new DevelopmentProject();
developmentProject.name = x;
developmentProject.createdUserId = req.user.sub;
developmentProject.createdFullName = req.user.name;
developmentProject.lastUpdateUserId = req.user.sub;
developmentProject.lastUpdateFullName = req.user.name;
developmentProject.createdAt = new Date();
developmentProject.lastUpdatedAt = new Date();
developmentProject.developmentRequestId = record.id;
await this.developmentProjectRepository.save(developmentProject, { data: req });
setLogDataDiff(req, { before: null, after: record });
}),
);
}
Recommended Fix:
await this.developmentProjectRepository.delete({ developmentRequestId: record.id });
if (body.developmentProjects != null) {
try {
await Promise.all(
body.developmentProjects.map(async (x) => {
try {
let developmentProject = new DevelopmentProject();
developmentProject.name = x;
developmentProject.createdUserId = req.user.sub;
developmentProject.createdFullName = req.user.name;
developmentProject.lastUpdateUserId = req.user.sub;
developmentProject.lastUpdateFullName = req.user.name;
developmentProject.createdAt = new Date();
developmentProject.lastUpdatedAt = new Date();
developmentProject.developmentRequestId = record.id;
await this.developmentProjectRepository.save(developmentProject, { data: req });
setLogDataDiff(req, { before: null, after: developmentProject });
} catch (error) {
console.error(`Failed to update development project "${x}":`, error);
throw error;
}
}),
);
} catch (error) {
console.error("Failed to update development projects:", error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to update development projects"
);
}
}
#5 - Null Reference Risk in Profile Query (MEDIUM)
File & Location: DPISController.ts:272-275
Method: GetProfileCitizenIdAsync
Problem Type: 2. Missing Error Handle
Root Cause:
findRevisionอาจเป็น null ถ้าไม่พบ current revision- การใช้
findRevision?.idในfind()operation จะเป็น undefined current_holders?.find()อาจ return undefined
Code ปัจจุบัน (เสี่ยง):
const findRevision = await this.orgRevisionRepo.findOne({
where: { orgRevisionIsCurrent: true },
});
var current_holder = profile.current_holders?.find((x) => x.orgRevisionId == findRevision?.id);
const mapProfile: DPISResult = {
// ...
organization: {
orgRootName: current_holder?.orgRoot?.orgRootName || "", // ❌ multiple levels of null checks
orgChild1Name: current_holder?.orgChild1?.orgChild1Name || "",
// ...
},
};
Recommended Fix:
const findRevision = await this.orgRevisionRepo.findOne({
where: { orgRevisionIsCurrent: true },
});
if (!findRevision) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"No current organization revision found"
);
}
var current_holder = profile.current_holders?.find((x) => x.orgRevisionId == findRevision.id);
if (!current_holder) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"No current organization assignment found for this profile"
);
}
const mapProfile: DPISResult = {
// ...
organization: {
orgRootName: current_holder.orgRoot?.orgRootName || "",
orgChild1Name: current_holder.orgChild1?.orgChild1Name || "",
orgChild2Name: current_holder.orgChild2?.orgChild2Name || "",
orgChild3Name: current_holder.orgChild3?.orgChild3Name || "",
orgChild4Name: current_holder.orgChild4?.orgChild4Name || "",
},
};
#6 - Unsafe Optional Chain in OrgRoot Query (MEDIUM)
File & Location: DevelopmentRequestController.ts:322-330
Method: newDevelopmentRequest
Problem Type: 2. Missing Error Handle
Root Cause:
- ใช้ optional chaining (
?.) และ nullish coalescing ใน query find()อาจ return undefined และการใช้!(non-null assertion) อาจทำให้ runtime error
Code ปัจจุบัน (เสี่ยง):
const orgRoot = await this.orgRootRepo.findOne({
select: {
id: true,
isDeputy: true
},
where: {
id: profile.current_holders.find(x => x.orgRootId)!.orgRootId ?? "" // ❌ unsafe
}
})
Recommended Fix:
const currentHolder = profile.current_holders.find(x => x.orgRootId);
if (!currentHolder || !currentHolder.orgRootId) {
throw new HttpError(
HttpStatus.BAD_REQUEST,
"Profile must have a current organization assignment"
);
}
const orgRoot = await this.orgRootRepo.findOne({
select: {
id: true,
isDeputy: true
},
where: {
id: currentHolder.orgRootId
}
});
if (!orgRoot) {
throw new HttpError(
HttpStatus.NOT_FOUND,
"Organization root not found"
);
}
#7 - Promise.all in Admin Edit Without Error Handling (MEDIUM)
File & Location: DevelopmentRequestController.ts:467-490
Method: editAdminDevelopmentRequest
Problem Type: 1. Unhandled Exception / 2. Missing Error Handle
Root Cause:
Promise.all()กับ nested save operations- ไม่มี error handling สำหรับ individual promises
Recommended Fix:
if (record.developmentProjects != null) {
try {
await Promise.all(
record.developmentProjects.map(async (x) => {
try {
let developmentProject = new DevelopmentProject();
let developmentProjectHistory = new DevelopmentProject();
Object.assign(developmentProject, {
...meta,
id: undefined,
name: record.name,
profileDevelopmentId: profileDevelopment.id,
});
Object.assign(developmentProject, {
...meta,
id: undefined,
name: record.name,
profileDevelopmentHistoryId: history.id,
});
await Promise.all([
this.developmentProjectRepository.save(developmentProject, { data: req }),
setLogDataDiff(req, { before: null, after: developmentProject }),
this.developmentProjectRepository.save(developmentProjectHistory, { data: req }),
]);
} catch (error) {
console.error(`Failed to save development project for "${record.name}":`, error);
throw error;
}
}),
);
} catch (error) {
console.error("Failed to save development projects:", error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to save development projects"
);
}
}
#8 - QueryBuilder Parameters Without Validation (MEDIUM)
File & Location: CommandSalaryController.ts:73-108
Method: GetAdmin
Problem Type: 2. Missing Error Handle
Root Cause:
- QueryBuilder พร้อม dynamic conditions
- ไม่มี input validation
- Page number validation เป็น optional (มี default value แต่ไม่ validate range)
Recommended Fix:
@Get("admin")
async GetAdmin(
@Query("page") page: number = 1,
@Query("pageSize") pageSize: number = 10,
@Query() commandSysId?: string | null,
@Query() isActive?: boolean | null,
@Query() searchKeyword?: string | null,
) {
try {
// Validate inputs
if (page < 1 || page > 10000) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page number");
}
if (pageSize < 1 || pageSize > 1000) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid page size");
}
const [commandSalarys, total] = await this.commandSalaryRepository
.createQueryBuilder("commandSalary")
.andWhere(
isActive != null && isActive != undefined ? "commandSalary.isActive = :isActive" : "1=1",
{
isActive:
isActive == null || isActive == undefined ? null : `${isActive == true ? 1 : 0}`,
},
)
// ... rest of query
.getManyAndCount();
return new HttpSuccess({ commandSalarys, total });
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
console.error('Failed to get command salaries:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to retrieve command salaries"
);
}
}
#9 - Hardcoded Response Data (LOW)
File & Location: CommandTypeController.ts:140-199
Method: GetById
Problem Type: 3. Code Quality
Root Cause:
- Hardcoded template data ใน code
- ไม่ flexible และยากต่อการ maintain
- ควรเก็บใน database หรือ configuration
Code ปัจจุบัน (เสี่ยง):
if (_commandType.code == "C-PM-10") {
let _commandType10: any;
_commandType10 = {
// ... hardcoded fields
name1: "๑. ..........................ประธาน",
name2: "๒. ..........................กรรมการ",
// ...
};
_commandType = _commandType10;
} else if (["C-PM-21", "C-PM-23"].includes(_commandType.code)) {
let _commandType21and23: any;
_commandType21and23 = {
// ... hardcoded fields
persons: [
{
no: "",
org: "",
// ...
},
],
};
_commandType = _commandType21and23;
}
Recommended Fix:
// Move these templates to database or configuration
const commandTemplates = await this.commandTemplateRepository.find({
where: { commandTypeCode: _commandType.code }
});
if (commandTemplates.length > 0) {
const template = commandTemplates[0];
return new HttpSuccess({
..._commandType,
...template.templateData
});
}
return new HttpSuccess(_commandType);
#10 - Missing Transaction for Related Operations (LOW)
File & Location: CommandOperatorController.ts:109-112
Method: swapCommandOperator
Problem Type: 2. Missing Error Handle
Root Cause:
- มีการ swap orderNo ระหว่าง 2 records
- ไม่ได้ใช้ transaction
- ถ้า save ตัวแรกสำเร็จ แต่ตัวที่สอง fail จะเกิด data inconsistency
Code ปัจจุบัน (เสี่ยง):
// swap
const temp = source.orderNo;
source.orderNo = dest.orderNo;
dest.orderNo = temp;
await Promise.all([
this.commandOperatorRepo.save(source),
this.commandOperatorRepo.save(dest),
]);
Recommended Fix:
const queryRunner = AppDataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
// swap
const temp = source.orderNo;
source.orderNo = dest.orderNo;
dest.orderNo = temp;
await Promise.all([
queryRunner.manager.save(CommandOperator, source),
queryRunner.manager.save(CommandOperator, dest),
]);
await queryRunner.commitTransaction();
return new HttpSuccess();
} catch (error) {
await queryRunner.rollbackTransaction();
console.error('Failed to swap command operators:', error);
throw new HttpError(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to swap operator order"
);
} finally {
await queryRunner.release();
}
#11 - Wrong Error Status Code (BUG)
File & Location: Multiple Files - CommandSysController.ts:127, CommandTypeController.ts:216, etc.
Methods: Post in various controllers
Problem Type: 3. Logic Bug
Root Cause:
- Throw
HttpError(HttpStatusCode.NOT_FOUND, ...)สำหรับ duplicate name errors - ควรใช้
BAD_REQUESTหรือCONFLICTแทน
Code ปัจจุบัน (ผิด):
if (checkName) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ชื่อนี้มีอยู่ในระบบแล้ว"); // ❌ Wrong status code
}
Recommended Fix:
if (checkName) {
throw new HttpError(HttpStatusCode.CONFLICT, "ชื่อนี้มีอยู่ในระบบแล้ว"); // ✅ Correct status code
}
#12 - Typos in Status Field (BUG)
File & Location: ChangePositionController.ts:79
Method: CreateChangePosition
Problem Type: 3. Logic Bug
Root Cause:
- Status เป็น "WAITTING" (ตัว T เกิน)
- ควรเป็น "WAITING"
Code ปัจจุบัน (ผิด):
changePosition.status = "WAITTING"; // ❌ typo
Recommended Fix:
changePosition.status = "WAITING"; // ✅ correct spelling
หมายเหตุ: ปัญหาเดียวกันนี้พบใน ChangePositionController.ts:241
สรุปคำแนะนำการแก้ไขแบบรวม
ระดับความสำคัญ
ต้องแก้ทันที (P0 - Critical):
- QueryRunner transaction not released on error - อาจทำให้ connection leak
ควรแก้โดยเร็ว (P1 - High): 2. Promise.all operations without error handling - unhandled rejections 3. QueryBuilder without validation and error handling
ควรแก้ (P2 - Medium): 4. Null reference checks 5. Unsafe optional chain usage 6. Promise operations in edit methods
แก้เมื่อว่าง (P3 - Low): 7. Hardcoded data 8. Missing transaction for related operations
แนวทางการแก้ไขแบบ Global
- Use try-finally pattern for all QueryRunner operations
- Add input validation for all query parameters
- Use Promise.allSettled หรือ wrap promises in try-catch
- Implement proper null checks ก่อน accessing nested properties
- Use transactions สำหรับ operations ที่ต้องการ consistency
ไฟล์ที่ต้องแก้ไข
- src/controllers/CommandOperatorController.ts - Transaction handling, Promise operations
- src/controllers/DevelopmentRequestController.ts - Promise.all, QueryBuilder validation
- src/controllers/DPISController.ts - Null reference checks
- src/controllers/CommandSalaryController.ts - Input validation
- src/controllers/CommandTypeController.ts - Error status codes, hardcoded data
ข้อมูลเพิ่มเติม
- Controllers ที่ยังไม่ได้ตรวจสอบ: 120 ไฟล์
- จุดเสี่ยงที่พบซ้ำจากชุดที่ 1: Promise.all without error handling, QueryBuilder without error handling
รายงานนี้ถูกสร้างโดย AI Code Review System สำหรับ BMA EHR Organization Project