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

40 KiB

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

โค้ดปัจจุบัน (มีปัญหา):

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

วิธีแก้ไขที่แนะนำ:

@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):

// สร้าง 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

โค้ดปัจจุบัน (มีปัญหา):

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();

วิธีแก้ไขที่แนะนำ:

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

โค้ดปัจจุบัน (มีปัญหา):

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

วิธีแก้ไขที่แนะนำ:

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 ทำให้หาต้นตอได้ยาก

โค้ดปัจจุบัน (มีปัญหา):

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

วิธีแก้ไขที่แนะนำ:

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

โค้ดปัจจุบัน (มีปัญหา):

rootId =
  orgRevisionActive?.posMasters?.filter((x) => x.next_holderId == profile.id)[0]
    ?.orgRootId || null;
// ❌ [0] อาจเป็น undefined หาก filter result ว่างเปล่า

วิธีแก้ไขที่แนะนำ:

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 หยุดทำงาน

โค้ดปัจจุบัน (มีปัญหา):

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

วิธีแก้ไขที่แนะนำ:

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 เต็ม

โค้ดปัจจุบัน (มีปัญหา):

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 ไม่รับประกันลำดับ

วิธีแก้ไขที่แนะนำ:

// ✅ วิธีที่ 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

โค้ดปัจจุบัน (มีปัญหา):

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,
      // ... ส่งคืนข้อมูล
    };
  }),
);

วิธีแก้ไขที่แนะนำ:

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 จะไม่ทำงานเลย

โค้ดปัจจุบัน (มีปัญหา):

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

วิธีแก้ไขที่แนะนำ:

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

โค้ดปัจจุบัน (มีปัญหา):

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

ประเภทปัญหา:

  1. Missing Error Handle - Delete Operation โดยไม่มี Transaction

สาเหตุที่ทำให้เสี่ยงต่อการ Crash:

การลบข้อมูล 2 ตารางต่อเนื่องกัน:

  1. Partial delete: หากลบสำเร็จตารางแรก แต่ fail ตารางที่สอง ข้อมูลจะไม่สมบูรณ์
  2. No rollback: ไม่มี transaction ครอบ
  3. Orphaned records: อาจมีข้อมูลที่เหลืออยู่โดยไม่มี parent

โค้ดปัจจุบัน (มีปัญหา):

@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();
}

วิธีแก้ไขที่แนะนำ:

@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

โค้ดปัจจุบัน (มีปัญหา):

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

วิธีแก้ไขที่แนะนำ:

// ✅ สร้าง 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:

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:

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:

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:

{
  "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