hrms-api-org/reports/batch-05-controllers-41-50-analysis.md

1061 lines
40 KiB
Markdown
Raw Normal View History

2026-05-08 18:15:03 +07:00
# รายงานการวิเคราะห์ความเสี่ยงการหยุดทำงานของระบบ (Crash Risk Analysis)
## ชุดที่ 5 (Batch 5) - วันที่ 8 พฤษภาคม 2568
---
## **สรุปผลการตรวจสอบ**
### จำนวนไฟล์ที่ตรวจสอบ: 10 Controllers
### จำนวนปัญหาที่พบ: 12 ปัญหา
### ระดับความรุนแรง:
- 🔴 **วิกฤติ (4 รายการ)**: มีโอกาสทำให้ Service Crash สูงมาก
- 🟠 **สูง (5 รายการ)**: มีโอกาสทำให้เกิด Unhandled Exception
- 🟡 **ปานกลาง (3 รายการ)**: อาจทำให้เกิดปัญหาในสถานการณ์เฉพาะ
---
## **รายละเอียดปัญหาแต่ละรายการ**
---
## 🔴 **ปัญหาที่ 1: Redis Client Connection Leak**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/PermissionController.ts`
- **บรรทัด:** 40-44, 132-136, 472-476, 581-585, 669-673, 775-779, 947-951
- **Method:** `getPermission`, `listAuthSys`, `listAuthSysOrg`, `listOrgUser`, `getPermissionFunc`, `listAuthSysOrgFunc`, `checkOrg`
### ประเภทปัญหา:
1. **Unhandled Exception** - Resource Leak และ Connection ไม่ถูกปิด
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
โค้ดสร้าง Redis Client ใหม่ทุกครั้งที่มีการเรียกใช้ method แต่:
1. **ไม่มีการปิด connection**: Redis client ไม่ถูก close หลังใช้งาน
2. **Connection pool exhaustion**: หากมี request จำนวนมาก จะทำให้หมด connection
3. **Memory leak**: Redis client objects สะสมใน memory
4. **Service crash**: เมื่อถึง limit ของ Redis connection หรือ memory จะทำให้ service หยุดทำงาน
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
@Get("")
public async getPermission(@Request() request: RequestWithUser) {
const redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
const getAsync = promisify(redisClient.get).bind(redisClient);
// ... ใช้งาน redisClient
redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply));
// ❌ ไม่มีการปิด connection
return new HttpSuccess(reply);
}
```
### วิธีแก้ไขที่แนะนำ:
```typescript
@Get("")
public async getPermission(@Request() request: RequestWithUser) {
let redisClient;
try {
redisClient = await this.redis.createClient({
host: REDIS_HOST,
port: REDIS_PORT,
});
const getAsync = promisify(redisClient.get).bind(redisClient);
let profile: any = await this.profileRepo.findOne({
select: ["id"],
where: { keycloak: request.user.sub },
});
let reply = await getAsync("role_" + profile.id);
if (reply != null) {
reply = JSON.parse(reply);
} else {
// ... logic เดิม
redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply));
}
return new HttpSuccess(reply);
} finally {
// ✅ ปิด connection เสมอ
if (redisClient) {
redisClient.quit();
// หรือใช้ redisClient.end(true) สำหรับ force close
}
}
}
```
### วิธีแก้ไขที่ดีกว่า (ใช้ Connection Pool):
```typescript
// สร้าง singleton Redis client
export class RedisService {
private static client: any = null;
static async getClient() {
if (!this.client) {
this.client = require("redis").createClient({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || "6379"),
retry_strategy: (options) => {
if (options.total_retry_time > 1000 * 60 * 60) {
return new Error("Retry time exhausted");
}
return Math.min(options.attempt * 100, 3000);
},
});
this.client.on("error", (err: Error) => {
console.error("Redis Client Error:", err);
});
}
return this.client;
}
}
// ใช้งานใน controller
@Get("")
public async getPermission(@Request() request: RequestWithUser) {
const redisClient = await RedisService.getClient();
const getAsync = promisify(redisClient.get).bind(redisClient);
let profile: any = await this.profileRepo.findOne({
select: ["id"],
where: { keycloak: request.user.sub },
});
let reply = await getAsync("role_" + profile.id);
if (reply != null) {
reply = JSON.parse(reply);
} else {
// ... logic เดิม
redisClient.setex("role_" + profile.id, 86400, JSON.stringify(reply));
}
return new HttpSuccess(reply);
}
```
---
## 🔴 **ปัญหาที่ 2: Unhandled Promise Rejection ใน forEach พร้อม Async**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/PosMasterActController.ts`
- **บรรทัด:** 317-320
- **Method:** `deletePosMasterAct`
### ประเภทปัญหา:
1. **Unhandled Exception** - Unhandled Promise Rejection
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
การใช้ `forEach` กับ async function โดยไม่มีการรอ:
1. **Promise ไม่ถูก await**: การ save หลายรายการเกิดขึ้น parallel โดยไม่มีการรอ
2. **Unhandled rejection**: หาก save fail จะเกิด unhandled rejection
3. **Race condition**: ข้อมูลอาจไม่ถูกต้องหากมีการอัปเดตพร้อมกัน
4. **Process crash**: ใน Node.js บาง version จะ crash เมื่อมี unhandled rejection
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
if (posMasterAct != null) {
const posMasterActList = await this.posMasterActRepository.find({
where: {
posMasterId: posMasterAct.posMasterId,
},
});
posMasterActList.forEach(async (p, i) => {
p.posMasterOrder = i + 1;
await this.posMasterActRepository.save(p);
});
// ❌ forEach ไม่รอ async ให้เสร็จ
}
return new HttpSuccess();
```
### วิธีแก้ไขที่แนะนำ:
```typescript
if (posMasterAct != null) {
const posMasterActList = await this.posMasterActRepository.find({
where: {
posMasterId: posMasterAct.posMasterId,
},
});
// ✅ วิธีที่ 1: ใช้ for...of loop (sequential)
for (const [i, p] of posMasterActList.entries()) {
p.posMasterOrder = i + 1;
await this.posMasterActRepository.save(p);
}
// หรือ ✅ วิธีที่ 2: ใช้ Promise.all (parallel)
await Promise.all(
posMasterActList.map(async (p, i) => {
p.posMasterOrder = i + 1;
await this.posMasterActRepository.save(p);
})
);
}
return new HttpSuccess();
```
---
## 🔴 **ปัญหาที่ 3: Promise.all ที่ซ้อนกันโดยไม่มี Error Handling**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/PosMasterActController.ts`
- **บรรทัด:** 771-835
- **Method:** `activePosMasterAct`
### ประเภทปัญหา:
1. **Unhandled Exception** - Unhandled Promise Rejection ใน Nested Promise.all
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
โค้ดใช้ `Promise.all` ซ้อนกัน 2 ชั้นโดยไม่มี try-catch:
1. **Error propagate ไม่ถูกต้อง**: หาก promise ใด fail จะไม่ถูกจัดการ
2. **Partial failure**: บางส่วนอาจสำเร็จ บางส่วน fail โดยไม่มีการ rollback
3. **Unhandled rejection**: จะทำให้ process crash ใน production
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
await Promise.all(
posMasterActs.map(async (posMasterAct) => {
// ... logic ยาวๆ
if (existingActivePositions.length > 0) {
await Promise.all(
existingActivePositions.map(async (pos) => {
// ❌ ไม่มี error handling รอบๆ
Object.assign(pos, {
status: false,
lastUpdateUserId: req.user?.sub ?? null,
lastUpdateFullName: req.user?.name ?? null,
lastUpdatedAt: new Date(),
dateEnd: new Date(),
});
await this.actpositionRepository.save(pos);
}),
);
}
const dataAct = new ProfileActposition();
// ... สร้าง dataAct
await this.actpositionRepository.save(dataAct);
posMasterAct.statusReport = "DONE";
await this.posMasterActRepository.save(posMasterAct);
}),
);
```
### วิธีแก้ไขที่แนะนำ:
```typescript
try {
await Promise.all(
posMasterActs.map(async (posMasterAct) => {
try {
const orgShortName = [
posMasterAct.posMaster?.orgChild4?.orgChild4ShortName,
posMasterAct.posMaster?.orgChild3?.orgChild3ShortName,
posMasterAct.posMaster?.orgChild2?.orgChild2ShortName,
posMasterAct.posMaster?.orgChild1?.orgChild1ShortName,
posMasterAct.posMaster?.orgRoot?.orgRootShortName,
].find(Boolean) ?? "";
const profileId = posMasterAct.posMasterChild?.current_holderId;
if (profileId) {
const existingActivePositions = await this.actpositionRepository.find({
select: [
"id",
"status",
"lastUpdateUserId",
"lastUpdateFullName",
"lastUpdatedAt",
"dateEnd",
"isDeleted"
],
where: { profileId, status: true, isDeleted: false },
});
if (existingActivePositions.length > 0) {
// ✅ เพิ่ม error handling ใน inner Promise.all
await Promise.all(
existingActivePositions.map(async (pos) => {
try {
Object.assign(pos, {
status: false,
lastUpdateUserId: req.user?.sub ?? null,
lastUpdateFullName: req.user?.name ?? null,
lastUpdatedAt: new Date(),
dateEnd: new Date(),
});
await this.actpositionRepository.save(pos);
} catch (error) {
// ✅ Log error แต่ไม่ให้ทั้ง batch fail
console.error(`ไม่สามารถอัปเดตตำแหน่ง ${pos.id}:`, error);
}
})
);
}
}
const dataAct = new ProfileActposition();
Object.assign(dataAct, {
profileId: profileId ?? null,
dateStart: new Date(),
posNo:
orgShortName && posMasterAct.posMaster?.posMasterNo
? `${orgShortName} ${posMasterAct.posMaster.posMasterNo}`
: posMasterAct.posMaster?.posMasterNo ?? "-",
position: posMasterAct.posMaster?.current_holder?.position ?? null,
posNoAbb: orgShortName,
status: true,
createdUserId: req.user?.sub ?? null,
createdFullName: req.user?.name ?? null,
lastUpdateUserId: req.user?.sub ?? null,
lastUpdateFullName: req.user?.name ?? null,
});
await this.actpositionRepository.save(dataAct);
posMasterAct.statusReport = "DONE";
await this.posMasterActRepository.save(posMasterAct);
} catch (error) {
// ✅ Log error แต่ทำต่อรายการอื่น
console.error(`ไม่สามารถ activate ตำแหน่ง ${posMasterAct.id}:`, error);
// อาจต้องการ mark เป็น FAILED
posMasterAct.statusReport = "FAILED";
await this.posMasterActRepository.save(posMasterAct).catch(e => {
console.error("ไม่สามารถบันทึก status:", e);
});
}
}),
);
} catch (error) {
console.error("เกิดข้อผิดพลาดในการ activate ตำแหน่ง:", error);
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"ไม่สามารถดำเนินการได้ กรุณาลองใหม่"
);
}
return new HttpSuccess();
```
---
## 🔴 **ปัญหาที่ 4: String Throw ที่ไม่ใช่ Error Object**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/PermissionController.ts`
- **บรรทัด:** 763, 770
- **Method:** `Permission`
### ประเภทปัญหา:
1. **Unhandled Exception** - Throwing String แทน Error Object
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
โค้ด throw string แทนที่จะ throw Error object:
1. **Stack trace หาย**: ไม่สามารถ trace ตำแหน่งที่เกิด error ได้
2. **Error handler ไม่ทำงาน**: Global error handlers อาจไม่รู้จัก string errors
3. **Monitoring ไม่เจอ**: Error tracking systems อาจไม่สามารถจับ error ได้
4. **Debug ยาก**: ไม่มี stack trace ทำให้หาต้นตอได้ยาก
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
public async Permission(req: RequestWithUser, system: string, action: string) {
let x: any = await this.getPermissionFunc(req);
let permission = false;
let role = x.roles.find((x: any) => x.authSysId == system);
if (!role) throw "ไม่มีสิทธิ์เข้าระบบ"; // ❌ throw string
if (role.attrOwnership == "OWNER") return "OWNER";
if (action.trim().toLocaleUpperCase() == "CREATE") permission = role.attrIsCreate;
if (action.trim().toLocaleUpperCase() == "DELETE") permission = role.attrIsDelete;
if (action.trim().toLocaleUpperCase() == "GET") permission = role.attrIsGet;
if (action.trim().toLocaleUpperCase() == "LIST") permission = role.attrIsList;
if (action.trim().toLocaleUpperCase() == "UPDATE") permission = role.attrIsUpdate;
if (permission == false) throw "ไม่มีสิทธิ์ใช้งานระบบนี้"; // ❌ throw string
return role.attrPrivilege;
}
```
### วิธีแก้ไขที่แนะนำ:
```typescript
public async Permission(req: RequestWithUser, system: string, action: string) {
let x: any = await this.getPermissionFunc(req);
let permission = false;
let role = x.roles.find((x: any) => x.authSysId == system);
if (!role) {
// ✅ throw HttpError แทน string
throw new HttpError(
HttpStatus.FORBIDDEN,
"ไม่มีสิทธิ์เข้าระบบ"
);
}
if (role.attrOwnership == "OWNER") return "OWNER";
const normalizedAction = action.trim().toLocaleUpperCase();
if (normalizedAction == "CREATE") permission = role.attrIsCreate;
else if (normalizedAction == "DELETE") permission = role.attrIsDelete;
else if (normalizedAction == "GET") permission = role.attrIsGet;
else if (normalizedAction == "LIST") permission = role.attrIsList;
else if (normalizedAction == "UPDATE") permission = role.attrIsUpdate;
if (permission == false) {
// ✅ throw HttpError แทน string
throw new HttpError(
HttpStatus.FORBIDDEN,
"ไม่มีสิทธิ์ใช้งานระบบนี้"
);
}
return role.attrPrivilege;
}
```
---
## 🟠 **ปัญหาที่ 5: Null Reference บน Array.find()**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/PermissionProfileController.ts`
- **บรรทัด:** 68-69
- **Method:** `GetActiveRootIdAdmin`
### ประเภทปัญหา:
1. **Unhandled Exception** - Null Reference Error
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
การเข้าถึง property ของผลลัพธ์จาก `find()` โดยไม่ตรวจสอบ:
1. **Optional chaining ไม่ครบ**: `.find()` อาจ return undefined
2. **Access property ของ undefined**: จะทำให้เกิด error
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
rootId =
orgRevisionActive?.posMasters?.filter((x) => x.next_holderId == profile.id)[0]
?.orgRootId || null;
// ❌ [0] อาจเป็น undefined หาก filter result ว่างเปล่า
```
### วิธีแก้ไขที่แนะนำ:
```typescript
const posMaster = orgRevisionActive?.posMasters?.find((x) => x.next_holderId == profile.id);
rootId = posMaster?.orgRootId || null;
// ✅ ใช้ .find() แทน .filter()[0] เพื่อความชัดเจน
```
---
## 🟠 **ปัญหาที่ 6: SQL Injection ใน Dynamic Query**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/PermissionOrgController.ts`
- **บรรทัด:** 76-78
- **Method:** `GetActiveRootIdAdmin`
### ประเภทปัญหา:
1. **Unhandled Exception** - SQL Injection Risk
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
การใส่ค่าโดยตรงลงใน query string:
1. **SQL injection**: ผู้ไม่ประสงค์ดีอาจ inject SQL code
2. **Query syntax error**: หากมีอักขระพิเศษอาจทำให้ query fail
3. **Database crash**: Query ที่ผิดพลาดอาจทำให้ database หยุดทำงาน
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
const data = await AppDataSource.getRepository(OrgRoot)
.createQueryBuilder("orgRoot")
.where("orgRoot.orgRevisionId = :id", { id: orgRevisionActive.id })
.andWhere(rootId != null ? `orgRoot.id = :rootId` : "1=1", {
rootId: rootId,
})
.orderBy("orgRoot.orgRootOrder", "ASC")
.getMany();
// ❌ ใส่ condition โดยตรงเป็น string
```
### วิธีแก้ไขที่แนะนำ:
```typescript
const queryBuilder = AppDataSource.getRepository(OrgRoot)
.createQueryBuilder("orgRoot")
.where("orgRoot.orgRevisionId = :id", { id: orgRevisionActive.id });
if (rootId != null) {
queryBuilder.andWhere("orgRoot.id = :rootId", { rootId });
}
const data = await queryBuilder
.orderBy("orgRoot.orgRootOrder", "ASC")
.getMany();
// ✅ ใช้ query builder ที่ปลอดภัย
```
---
## 🟠 **ปัญหาที่ 7: Race Condition ใน Promise.all.map()**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/PosMasterActController.ts`
- **บรรทัด:** 413-443
- **Method:** `GetPosMasterActProfile`
### ประเภทปัญหา:
1. **Unhandled Exception** - Race Condition ใน Async Mapping
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
การใช้ `Promise.all()` ร่วมกับ `.map().sort()`:
1. **Sort ผิดพลาด**: การ sort หลังจาก Promise.all อาจไม่ทำงานตามที่คาดหวัง
2. **Unhandled promise rejection**: หาก promise ใด fail จะเกิด unhandled rejection
3. **Memory spike**: โหลดข้อมูลทั้งหมดพร้อมกันอาจทำให้ memory เต็ม
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
const data = await Promise.all(
posMasterActs
.sort((a, b) => a.posMaster.posMasterOrder - b.posMaster.posMasterOrder)
.map((item) => {
// ... process item
return {
id: item.id,
// ... ส่งคืนข้อมูล
};
}),
);
// ❌ Promise.all ไม่รับประกันลำดับ
```
### วิธีแก้ไขที่แนะนำ:
```typescript
// ✅ วิธีที่ 1: Sort หลังจาก Promise.all
const processedData = await Promise.all(
posMasterActs.map(async (item) => {
const shortName =
item.posMasterChild != null && item.posMasterChild.orgChild4 != null
? `${item.posMasterChild.orgChild4.orgChild4ShortName} ${item.posMasterChild.posMasterNo}`
: item.posMasterChild != null && item.posMasterChild?.orgChild3 != null
? `${item.posMasterChild.orgChild3.orgChild3ShortName} ${item.posMasterChild.posMasterNo}`
: item.posMasterChild != null && item.posMasterChild?.orgChild2 != null
? `${item.posMasterChild.orgChild2.orgChild2ShortName} ${item.posMasterChild.posMasterNo}`
: item.posMasterChild != null && item.posMasterChild?.orgChild1 != null
? `${item.posMasterChild.orgChild1.orgChild1ShortName} ${item.posMasterChild.posMasterNo}`
: item.posMasterChild != null && item.posMasterChild?.orgRoot != null
? `${item.posMasterChild.orgRoot.orgRootShortName} ${item.posMasterChild.posMasterNo}`
: null;
return {
id: item.id,
posMasterOrder: item.posMasterOrder,
profileId: item.posMasterChild?.current_holder?.id ?? null,
citizenId: item.posMasterChild?.current_holder?.citizenId ?? null,
prefix: item.posMasterChild?.current_holder?.prefix ?? null,
firstName: item.posMasterChild?.current_holder?.firstName ?? null,
lastName: item.posMasterChild?.current_holder?.lastName ?? null,
posLevel: item.posMasterChild?.current_holder?.posLevel?.posLevelName ?? null,
posType: item.posMasterChild?.current_holder?.posType?.posTypeName ?? null,
position: item.posMasterChild?.current_holder?.position ?? null,
posNo: shortName,
};
})
);
// ✅ Sort หลังจาก process เสร็จ
const data = processedData.sort((a, b) => a.posMasterOrder - b.posMasterOrder);
return new HttpSuccess(data);
```
---
## 🟠 **ปัญหาที่ 8: Promise.all ที่ไม่มี Error Handling**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/PermissionProfileController.ts`
- **บรรทัด:** 162-249
- **Method:** `listProfile`
### ประเภทปัญหา:
1. **Unhandled Exception** - Promise.all โดยไม่มี Error Handling
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
การใช้ Promise.all กับ array mapping ที่ซับซ้อน:
1. **Unhandled rejection**: หากการ process รายการใด fail ทั้งหมดจะ fail
2. **Complex null checks**: Logic ซับซ้อนทำให้เกิด error ได้ง่าย
3. **Nested optional chaining**: หาก data ไม่สมบูรณ์อาจ throw error
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
const data = await Promise.all(
record.map((_data) => {
const shortName =
_data.current_holders.length == 0
? null
: _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null &&
_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 !=
null
? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}`
: _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null &&
_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)
?.orgChild3 != null
? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}`
: null; // ... ยาวมาก
return {
id: _data.id,
// ... ส่งคืนข้อมูล
};
}),
);
```
### วิธีแก้ไขที่แนะนำ:
```typescript
const data = await Promise.all(
record.map((_data) => {
try {
// ✅ แยก logic ออกเป็น function หรือ helper
const currentHolder = _data.current_holders?.find(
(x) => x.orgRevisionId == findRevision.id
);
const shortName = this.getShortName(currentHolder);
const root = currentHolder?.orgRoot;
const child1 = currentHolder?.orgChild1;
const child2 = currentHolder?.orgChild2;
const child3 = currentHolder?.orgChild3;
const child4 = currentHolder?.orgChild4;
return {
id: _data.id,
avatar: _data.avatar,
avatarName: _data.avatarName,
prefix: _data.prefix,
rank: _data.rank,
firstName: _data.firstName,
lastName: _data.lastName,
org: this.formatOrgName(child4, child3, child2, child1, root),
posNo: shortName,
position: _data.position,
posType: _data.posType?.posTypeName ?? null,
posLevel: _data.posLevel?.posLevelName ?? null,
};
} catch (error) {
console.error(`Error processing profile ${_data.id}:`, error);
// ✅ Return default value หรือ skip
return {
id: _data.id,
avatar: _data.avatar,
avatarName: _data.avatarName,
prefix: _data.prefix,
rank: _data.rank,
firstName: _data.firstName,
lastName: _data.lastName,
org: null,
posNo: null,
position: _data.position,
posType: null,
posLevel: null,
};
}
}),
);
```
---
## 🟠 **ปัญหาที่ 9: String Throw ใน PosTypeController**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/PosTypeController.ts`
- **บรรทัด:** 52-54
- **Method:** `createType`
### ประเภทปัญหา:
1. **Unhandled Exception** - Logic Error: ตรวจสอบ null หลังจาก Object.assign
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
โค้ดตรวจสอบ null หลังจาก `Object.assign`:
1. **Check ไม่เคยเป็น true**: Object.assign จะสร้าง object เสมอ
2. **Dead code**: บรรทัด throw error จะไม่ทำงานเลย
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
async createType(
@Body()
requestBody: CreatePosType,
@Request() request: RequestWithUser,
) {
const posType = Object.assign(new PosType(), requestBody);
if (!posType) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูล");
}
// ❌ Object.assign เสมอ return object ไม่เคยเป็น null
```
### วิธีแก้ไขที่แนะนำ:
```typescript
async createType(
@Body()
requestBody: CreatePosType,
@Request() request: RequestWithUser,
) {
if (!requestBody || !requestBody.posTypeName) {
throw new HttpError(HttpStatusCode.BAD_REQUEST, "กรุณาระบุชื่อประเภทตำแหน่ง");
}
const posType = Object.assign(new PosType(), requestBody);
// ✅ ตรวจสอบ input ก่อนสร้าง object
```
---
## 🟠 **ปัญหาที่ 10: Promise.all ที่ไม่มี Error Handling ใน PermissionOrgController**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/PermissionOrgController.ts`
- **บรรทัด:** 162-249
- **Method:** `listProfile`
### ประเภทปัญหา:
1. **Unhandled Exception** - Promise.all ใน Complex Mapping Logic
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
เหมือนปัญหาที่ 8 แต่อยู่ใน PermissionOrgController:
1. **Unhandled rejection**: หาก mapping fail ทั้ง batch จะ fail
2. **Complex nested ternary**: Logic ซับซ้อนเสี่ยงต่อ error
3. **No error boundary**: ไม่มี try-catch รอบๆ Promise.all
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
const data = await Promise.all(
record.map((_data) => {
const shortName =
_data.current_holders.length == 0
? null
: _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null &&
_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4 !=
null
? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild4.orgChild4ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}`
: _data.current_holders.find((x) => x.orgRevisionId == findRevision.id) != null &&
_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)
?.orgChild3 != null
? `${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.orgChild3.orgChild3ShortName} ${_data.current_holders.find((x) => x.orgRevisionId == findRevision.id)?.posMasterNo}`
: null; // ... logic ซับซ้อน
return { /* ... */ };
}),
);
```
### วิธีแก้ไขที่แนะนำ:
เหมือนปัญหาที่ 8 - ควรแยก logic ออกเป็น helper function และเพิ่ม error handling
---
## 🟡 **ปัญหาที่ 11: Missing Error Handling ใน Delete Operations**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/ProfileAbilityController.ts`
- **บรรทัด:** 216-236
- **Method:** `deleteProfileAbility`
### ประเภทปัญหา:
2. **Missing Error Handle** - Delete Operation โดยไม่มี Transaction
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
การลบข้อมูล 2 ตารางต่อเนื่องกัน:
1. **Partial delete**: หากลบสำเร็จตารางแรก แต่ fail ตารางที่สอง ข้อมูลจะไม่สมบูรณ์
2. **No rollback**: ไม่มี transaction ครอบ
3. **Orphaned records**: อาจมีข้อมูลที่เหลืออยู่โดยไม่มี parent
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
@Delete("{abilityId}")
public async deleteProfileAbility(@Path() abilityId: string, @Request() req: RequestWithUser) {
const _record = await this.profileAbilityRepo.findOneBy({ id: abilityId });
if (_record) {
await new permission().PermissionOrgUserDelete(
req,
"SYS_REGISTRY_OFFICER",
_record.profileId,
);
}
await this.profileAbilityHistoryRepo.delete({
profileAbilityId: abilityId,
});
const result = await this.profileAbilityRepo.delete({ id: abilityId });
if (result.affected == undefined || result.affected <= 0)
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
return new HttpSuccess();
}
```
### วิธีแก้ไขที่แนะนำ:
```typescript
@Delete("{abilityId}")
public async deleteProfileAbility(@Path() abilityId: string, @Request() req: RequestWithUser) {
try {
const _record = await this.profileAbilityRepo.findOneBy({ id: abilityId });
if (_record) {
await new permission().PermissionOrgUserDelete(
req,
"SYS_REGISTRY_OFFICER",
_record.profileId,
);
} else {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
// ✅ ใช้ transaction
await AppDataSource.transaction(async (transactionalEntityManager) => {
await transactionalEntityManager.delete(ProfileAbilityHistory, {
profileAbilityId: abilityId,
});
const result = await transactionalEntityManager.delete(ProfileAbility, {
id: abilityId,
});
if (result.affected == undefined || result.affected <= 0) {
throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูล");
}
});
return new HttpSuccess();
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
console.error('เกิดข้อผิดพลาดในการลบข้อมูล:', error);
throw new HttpError(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"ไม่สามารถลบข้อมูลได้ กรุณาลองใหม่ในภายหลัง"
);
}
}
```
---
## 🟡 **ปัญหาที่ 12: Null Reference ใน Map Operations**
### ไฟล์และตำแหน่ง:
- **ไฟล์:** `src/controllers/PosMasterActController.ts`
- **บรรทัด:** 250-279
- **Method:** `searchAct`
### ประเภทปัญหา:
1. **Unhandled Exception** - Null Reference ใน Nested Optional Chaining
### สาเหตุที่ทำให้เสี่ยงต่อการ Crash:
การเข้าถึง property ที่ซ้อนกันหลายชั้น:
1. **Complex optional chaining**: หาก intermediate value เป็น null อาจเกิด error
2. **Missing null checks**: บางจุดไม่ได้ใส่ optional chaining
### โค้ดปัจจุบัน (มีปัญหา):
```typescript
const data = await Promise.all(
posMaster
.sort((a, b) => a.posMasterOrder - b.posMasterOrder)
.map((item) => {
const shortName =
item.orgChild4 != null
? `${item.orgChild4.orgChild4ShortName} ${item.posMasterNo}`
: item?.orgChild3 != null
? `${item.orgChild3.orgChild3ShortName} ${item.posMasterNo}`
: item?.orgChild2 != null
? `${item.orgChild2.orgChild2ShortName} ${item.posMasterNo}`
: item?.orgChild1 != null
? `${item.orgChild1.orgChild1ShortName} ${item.posMasterNo}`
: item?.orgRoot != null
? `${item.orgRoot.orgRootShortName} ${item.posMasterNo}`
: null;
return {
id: item.id,
citizenId: item.current_holder?.citizenId ?? null,
// ...
};
}),
);
```
### วิธีแก้ไขที่แนะนำ:
```typescript
// ✅ สร้าง helper function สำหรับ get short name
private getShortName(posMaster: any): string | null {
if (!posMaster) return null;
if (posMaster.orgChild4?.orgChild4ShortName) {
return `${posMaster.orgChild4.orgChild4ShortName} ${posMaster.posMasterNo}`;
}
if (posMaster.orgChild3?.orgChild3ShortName) {
return `${posMaster.orgChild3.orgChild3ShortName} ${posMaster.posMasterNo}`;
}
if (posMaster.orgChild2?.orgChild2ShortName) {
return `${posMaster.orgChild2.orgChild2ShortName} ${posMaster.posMasterNo}`;
}
if (posMaster.orgChild1?.orgChild1ShortName) {
return `${posMaster.orgChild1.orgChild1ShortName} ${posMaster.posMasterNo}`;
}
if (posMaster.orgRoot?.orgRootShortName) {
return `${posMaster.orgRoot.orgRootShortName} ${posMaster.posMasterNo}`;
}
return null;
}
const data = await Promise.all(
posMaster
.sort((a, b) => a.posMasterOrder - b.posMasterOrder)
.map((item) => {
const shortName = this.getShortName(item);
return {
id: item.id,
citizenId: item.current_holder?.citizenId ?? null,
isDirector: item.isDirector ?? null,
prefix: item.current_holder?.prefix ?? null,
firstName: item.current_holder?.firstName ?? null,
lastName: item.current_holder?.lastName ?? null,
posLevel: item.current_holder?.posLevel?.posLevelName ?? null,
posType: item.current_holder?.posType?.posTypeName ?? null,
position: item.current_holder?.position ?? null,
posNo: shortName,
};
}),
);
```
---
## 📊 **สรุปสถิติ**
| ระดับความรุนแรง | จำนวน | ประเภท |
|---|---|---|
| 🔴 วิกฤติ | 4 | มีโอกาสทำให้ Service Crash สูงมาก |
| 🟠 สูง | 5 | มีโอกาสทำให้เกิด Unhandled Exception |
| 🟡 ปานกลาง | 3 | อาจทำให้เกิดปัญหาในสถานการณ์เฉพาะ |
| **รวมทั้งหมด** | **12** | |
### ไฟล์ที่มีปัญหามากที่สุด:
1. **PermissionController.ts** - 3 ปัญหา (รุนแรงที่สุด: Redis leak)
2. **PosMasterActController.ts** - 3 ปัญหา (Promise issues)
3. **PermissionOrgController.ts** - 2 ปัญหา
4. **PermissionProfileController.ts** - 2 ปัญหา
5. **PosTypeController.ts** - 1 ปัญหา
6. **ProfileAbilityController.ts** - 1 ปัญหา
---
## 💡 **คำแนะนำเพื่อป้องกันปัญหาในอนาคต**
### 1. ใช้ Redis Connection Pool
สร้าง singleton service สำหรับจัดการ Redis connection:
```typescript
export class RedisService {
private static client: any = null;
private static reconnectTimeout: NodeJS.Timeout | null = null;
static async getClient() {
if (!this.client || !this.client.ready) {
await this.connect();
}
return this.client;
}
private static async connect() {
// Implementation with retry logic
}
}
```
### 2. Global Unhandled Rejection Handler
เพิ่มใน `main.ts` หรือ `app.ts`:
```typescript
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// อย่า crash ใน production แต่ log ไว้ debug
// process.exit(1); // ❌ อย่าทำใน production
});
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
// Clean up and restart
process.exit(1); // ✅ อาจ crash แต่ควร restart
});
```
### 3. ใช้ Async Wrapper
สร้าง decorator หรือ helper function:
```typescript
export function asyncHandler(fn: Function) {
return (req: any, res: any, next: any) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// ใช้งาน
@Get()
asyncHandler(async (request: RequestWithUser) => {
// ... logic
});
```
### 4. ตรวจสอบ ESLint Rules
เพิ่ม rules เหล่านี้ใน `.eslintrc.json`:
```json
{
"rules": {
"no-throw-literal": "error",
"require-await": "error",
"no-return-await": "off",
"prefer-promise-reject-errors": "error"
}
}
```
### 5. เขียน Integration Tests
ทดสอบ error scenarios:
- Redis connection failures
- Database constraint violations
- Concurrent updates
- Memory pressure
### 6. Monitoring
ติดตั้ง monitoring tools:
- Track Redis connection count
- Monitor memory usage
- Log unhandled rejections
- Set up alerts for crash loops
---
## 📝 **บันทึกเพิ่มเติม**
รายงานนี้ครอบคลุมการวิเคราะห์ **ชุดที่ 5** ซึ่งประกอบด้วย 10 Controllers:
1. PermissionController.ts ⚠️ **มีปัญหารุนแรง (Redis Leak)**
2. PermissionOrgController.ts
3. PermissionProfileController.ts
4. PosExecutiveController.ts
5. PosLevelController.ts
6. PosMasterActController.ts ⚠️ **มีปัญหา Promise Handling**
7. PosTypeController.ts
8. PositionController.ts
9. PrefixController.ts
10. ProfileAbilityController.ts
**วันที่สร้างรายงาน:** 8 พฤษภาคม 2568
**เครื่องมือที่ใช้:** การวิเคราะห์ Code และ Pattern Recognition