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