1061 lines
40 KiB
Markdown
1061 lines
40 KiB
Markdown
|
|
# รายงานการวิเคราะห์ความเสี่ยงการหยุดทำงานของระบบ (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
|