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

30 KiB

รายงานการตรวจสอบ Unhandled Exception - Controllers ชุดที่ 3 (ไฟล์ที่ 21-30)

Project: BMA EHR Organization Backend Framework: TSOA + Express + TypeORM วันที่ตรวจสอบ: 2026-05-08 จำนวน Controllers: 10 ไฟล์ สถานะ: เสร็จสิ้น


สรุปผลการตรวจสอบ

ระดับความรุนแรง จำนวนจุดเสี่ยง
CRITICAL 0
HIGH 4
MEDIUM 5
LOW 2
BUG 2
รวมทั้งหมด 13

Controllers ที่ตรวจสอบ

  1. EmployeePositionController.ts
  2. EmployeeTempPositionController.ts
  3. ExRetirementController.ts
  4. GenderController.ts
  5. ImportDataController.ts
  6. InsigniaController.ts
  7. InsigniaTypeController.ts
  8. IssuesController.ts
  9. KeycloakSyncController.ts
  10. LoginController.ts

รายละเอียดจุดเสี่ยงแต่ละจุด

#1 - Promise.all Without Error Handling in Position Creation (HIGH)

File & Location: EmployeePositionController.ts:690-707 Method: createEmpMaster

Problem Type: 1. Unhandled Exception / 2. Missing Error Handle

Root Cause:

  • ใช้ Promise.all() สำหรับบันทึก positions หลายรายการพร้อมกัน
  • ถ้ามี position ไหน save ไม่สำเร็จ จะเกิด unhandled rejection
  • ไม่มี try-catch รองรับ ทำให้ไม่สามารถควบคุม error ได้
  • ส่งผลให้อาจเกิด data inconsistency ถ้า save บางส่วนสำเร็จ แต่บางส่วนล้มเหลว

Code ปัจจุบัน (เสี่ยง):

await Promise.all(
  requestBody.positions.map(async (x: any) => {
    const position = Object.assign(new EmployeePosition());
    position.positionName = x.posDictName;
    position.posTypeId = x.posTypeId == "" ? null : x.posTypeId;
    position.posLevelId = x.posLevelId == "" ? null : x.posLevelId;
    position.positionIsSelected = false;
    position.posMasterId = posMaster.id;
    position.createdUserId = request.user.sub;
    position.createdFullName = request.user.name;
    position.createdAt = new Date();
    position.lastUpdateUserId = request.user.sub;
    position.lastUpdateFullName = request.user.name;
    position.lastUpdatedAt = new Date();
    await this.employeePositionRepository.save(position, { data: request });
  }),
);
return new HttpSuccess(posMaster.id);

Recommended Fix:

try {
  await Promise.all(
    requestBody.positions.map(async (x: any) => {
      try {
        const position = Object.assign(new EmployeePosition());
        position.positionName = x.posDictName;
        position.posTypeId = x.posTypeId == "" ? null : x.posTypeId;
        position.posLevelId = x.posLevelId == "" ? null : x.posLevelId;
        position.positionIsSelected = false;
        position.posMasterId = posMaster.id;
        position.createdUserId = request.user.sub;
        position.createdFullName = request.user.name;
        position.createdAt = new Date();
        position.lastUpdateUserId = request.user.sub;
        position.lastUpdateFullName = request.user.name;
        position.lastUpdatedAt = new Date();
        await this.employeePositionRepository.save(position, { data: request });
      } catch (error) {
        console.error(`Failed to save position "${x.posDictName}":`, error);
        throw error; // Re-throw to be caught by Promise.all
      }
    }),
  );
  return new HttpSuccess(posMaster.id);
} catch (error) {
  console.error("Failed to save positions:", error);
  throw new HttpError(
    HttpStatus.INTERNAL_SERVER_ERROR,
    "Failed to save positions"
  );
}

#2 - Promise.all Without Error Handling in Position Update (HIGH)

File & Location: EmployeePositionController.ts:905-921 Method: updateEmpMaster

Problem Type: 1. Unhandled Exception / 2. Missing Error Handle

Root Cause:

  • Similar to #1 แต่เกิดใน update operation
  • Promise.all() โดยไม่มี error handling
  • เกิดการลบ positions เก่า ก่อน แล้วค่อยสร้างใหม่ ถ้าสร้างใหม่ fail จะเกิด data loss

Code ปัจจุบัน (เสี่ยง):

await this.employeePositionRepository.delete({ posMasterId: posMaster.id });

await Promise.all(
  requestBody.positions.map(async (x: any) => {
    const position = Object.assign(new EmployeePosition());
    position.positionName = x.posDictName;
    position.posTypeId = x.posTypeId == "" ? null : x.posTypeId;
    position.posLevelId = x.posLevelId == "" ? null : x.posLevelId;
    position.positionIsSelected = false;
    position.posMasterId = posMaster.id;
    position.createdUserId = request.user.sub;
    position.createdFullName = request.user.name;
    position.createdAt = new Date();
    position.lastUpdateUserId = request.user.sub;
    position.lastUpdateFullName = request.user.name;
    position.lastUpdatedAt = new Date();
    await this.employeePositionRepository.save(position, { data: request });
  }),
);

Recommended Fix:

// Get existing positions as backup before deletion
const existingPositions = await this.employeePositionRepository.find({
  where: { posMasterId: posMaster.id }
});

try {
  await this.employeePositionRepository.delete({ posMasterId: posMaster.id });

  await Promise.all(
    requestBody.positions.map(async (x: any) => {
      try {
        const position = Object.assign(new EmployeePosition());
        position.positionName = x.posDictName;
        position.posTypeId = x.posTypeId == "" ? null : x.posTypeId;
        position.posLevelId = x.posLevelId == "" ? null : x.posLevelId;
        position.positionIsSelected = false;
        position.posMasterId = posMaster.id;
        position.createdUserId = request.user.sub;
        position.createdFullName = request.user.name;
        position.createdAt = new Date();
        position.lastUpdateUserId = request.user.sub;
        position.lastUpdateFullName = request.user.name;
        position.lastUpdatedAt = new Date();
        await this.employeePositionRepository.save(position, { data: request });
      } catch (error) {
        console.error(`Failed to update position "${x.posDictName}":`, error);
        throw error;
      }
    }),
  );
} catch (error) {
  console.error("Failed to update positions, restoring backup:", error);
  // Restore backup positions
  await this.employeePositionRepository.save(existingPositions);
  throw new HttpError(
    HttpStatus.INTERNAL_SERVER_ERROR,
    "Failed to update positions"
  );
}

#3 - Async forEach Without Proper Error Handling (HIGH)

File & Location: EmployeePositionController.ts:2378-2395 Method: createEmpHolder

Problem Type: 1. Unhandled Exception

Root Cause:

  • ใช้ forEach กับ async function ซึ่งไม่รอให้ทุก operation สำเร็จ
  • การใช้ forEach กับ async จะไม่ catch error ที่เกิดใน loop
  • ถ้ามีการ save ที่ fail จะไม่ทราบ และ data อาจไม่ถูกต้อง

Code ปัจจุบัน (เสี่ยง):

dataMaster.positions.forEach(async (position) => {
  if (position.id === requestBody.position) {
    position.positionIsSelected = true;
    const profile = await this.profileRepository.findOne({
      where: { id: requestBody.profileId },
    });
    if (profile != null) {
      const _null: any = null;
      profile.posLevelId = position?.posLevelId ?? _null;
      profile.posTypeId = position?.posTypeId ?? _null;
      profile.position = position?.positionName ?? _null;
      await this.profileRepository.save(profile);
    }
  } else {
    position.positionIsSelected = false;
  }
  await this.employeePositionRepository.save(position);
});

Recommended Fix:

// Use Promise.all instead of forEach with async
await Promise.all(
  dataMaster.positions.map(async (position) => {
    try {
      if (position.id === requestBody.position) {
        position.positionIsSelected = true;
        const profile = await this.profileRepository.findOne({
          where: { id: requestBody.profileId },
        });
        if (profile != null) {
          const _null: any = null;
          profile.posLevelId = position?.posLevelId ?? _null;
          profile.posTypeId = position?.posTypeId ?? _null;
          profile.position = position?.positionName ?? _null;
          await this.profileRepository.save(profile);
        }
      } else {
        position.positionIsSelected = false;
      }
      await this.employeePositionRepository.save(position);
    } catch (error) {
      console.error(`Failed to update position ${position.id}:`, error);
      throw error;
    }
  })
);

#4 - Promise.all in EmployeeTempPositionController Without Error Handling (HIGH)

File & Location: EmployeeTempPositionController.ts:557-574 Method: createEmpMaster

Problem Type: 1. Unhandled Exception / 2. Missing Error Handle

Root Cause:

  • เหมือนกับ #1 แต่เกิดใน EmployeeTempPositionController
  • ใช้ Promise.all() โดยไม่มี error handling

Code ปัจจุบัน (เสี่ยง):

await Promise.all(
  requestBody.positions.map(async (x: any) => {
    const position = Object.assign(new EmployeePosition());
    position.positionName = x.posDictName;
    position.posTypeId = x.posTypeId == "" ? null : x.posTypeId;
    position.posLevelId = x.posLevelId == "" ? null : x.posLevelId;
    position.positionIsSelected = false;
    position.posMasterTempId = posMaster.id;
    position.createdUserId = request.user.sub;
    position.createdFullName = request.user.name;
    position.createdAt = new Date();
    position.lastUpdateUserId = request.user.sub;
    position.lastUpdateFullName = request.user.name;
    position.lastUpdatedAt = new Date();
    await this.employeePositionRepository.save(position, { data: request });
  }),
);

Recommended Fix: ใช้การแก้ไขเดียวกันกับ #1


#5 - Unsafe Token Fetch in ExRetirementController (MEDIUM)

File & Location: ExRetirementController.ts:148-173 Method: getToken

Problem Type: 2. Missing Error Handle

Root Cause:

  • ฟังก์ชัน getToken มีการ throw error แต่ใช้ Promise.reject
  • ไม่มีการระบุประเภทของ error ที่ชัดเจน
  • Error ที่เกิดขึ้นอาจไม่ถูก handle อย่างเหมาะสมในบางกรณี
  • Token cache อาจเก็บ token ที่หมดอายุ

Code ปัจจุบัน (เสี่ยง):

async function getToken(ClientID: string, ClientSecret: string): Promise<string> {
  const cacheKey = `${ClientID}:${ClientSecret}`;

  // ลองหา token ใน cache ก่อน
  const cachedToken = TokenCache.get(cacheKey);
  if (cachedToken) {
    return cachedToken;
  }

  // ถ้าไม่มีใน cache ให้ขอใหม่
  try {
    const formData = new FormData();
    formData.append("ClientID", ClientID);
    formData.append("ClientSecret", ClientSecret);
    const res = await axios.post(API_URL_BANGKOK + "/authorize", formData, {
      headers: {
        "Content-Type": "application/json",
      },
    });
    const token = res.data.token;
    TokenCache.set(cacheKey, token);
    return token;
  } catch (error) {
    return Promise.reject({ message: "Error occurred", error });
  }
}

Recommended Fix:

async function getToken(ClientID: string, ClientSecret: string): Promise<string> {
  const cacheKey = `${ClientID}:${ClientSecret}`;

  // ลองหา token ใน cache ก่อน
  const cachedToken = TokenCache.get(cacheKey);
  if (cachedToken) {
    return cachedToken;
  }

  // ถ้าไม่มีใน cache ให้ขอใหม่
  try {
    const formData = new FormData();
    formData.append("ClientID", ClientID);
    formData.append("ClientSecret", ClientSecret);
    const res = await axios.post(API_URL_BANGKOK + "/authorize", formData, {
      headers: {
        "Content-Type": "application/json",
      },
      timeout: 10000, // Add timeout
    });
    
    if (!res.data || !res.data.token) {
      throw new Error("Invalid token response from exprofile API");
    }
    
    const token = res.data.token;
    TokenCache.set(cacheKey, token);
    return token;
  } catch (error: any) {
    console.error("Failed to get exprofile token:", error);
    
    // More specific error handling
    if (error.response?.status === 401) {
      throw new Error("Invalid credentials for exprofile API");
    } else if (error.code === 'ECONNABORTED') {
      throw new Error("Request timeout while fetching exprofile token");
    } else {
      throw new Error(`Failed to fetch exprofile token: ${error.message}`);
    }
  }
}

#6 - Promise.all Without Error Handling in ImportDataController (MEDIUM)

File & Location: ImportDataController.ts:2425-2443 Method: Import Education Mis Data

Problem Type: 1. Unhandled Exception / 2. Missing Error Handle

Root Cause:

  • ใช้ Promise.all() สำหรับ batch insert ข้อมูลลง database
  • ไม่มี error handling ถ้า insert บางรายการ fail
  • ไม่สามารถรู้ได้ว่ามีกี่รายการที่สำเร็จ/ล้มเหลว

Code ปัจจุบัน (เสี่ยง):

await Promise.all(
  getExcel.map(async (item: any) => {
    const educationMis = new EducationMis();
    educationMis.EDUCATION_CODE = item.EDUCATION_CODE;
    educationMis.EDUCATION_NAME = item.EDUCATION_NAME;
    // ... set other properties
    await this.educationMisRepository.save(educationMis);
  }),
);

Recommended Fix:

const results = await Promise.allSettled(
  getExcel.map(async (item: any) => {
    try {
      const educationMis = new EducationMis();
      educationMis.EDUCATION_CODE = item.EDUCATION_CODE;
      educationMis.EDUCATION_NAME = item.EDUCATION_NAME;
      // ... set other properties
      await this.educationMisRepository.save(educationMis);
      return { status: 'success', code: item.EDUCATION_CODE };
    } catch (error) {
      console.error(`Failed to save education ${item.EDUCATION_CODE}:`, error);
      return { 
        status: 'failed', 
        code: item.EDUCATION_CODE, 
        error: error.message 
      };
    }
  }),
);

const failed = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && r.value.status === 'failed'));
if (failed.length > 0) {
  console.warn(`Failed to import ${failed.length} education records`);
  // Optionally notify user about partial failure
}

#7 - External API Call Without Proper Error Handling (MEDIUM)

File & Location: ExRetirementController.ts:50-103 Method: getExRetirement

Problem Type: 2. Missing Error Handle

Root Cause:

  • มีการเรียก external API แต่ error handling ยังไม่ครอบคลุม
  • ใช้ retry mechanism แต่ไม่มี exponential backoff
  • ไม่มี logging ที่ชัดเจนสำหรับการ debug

Code ปัจจุบัน (เสี่ยง):

let retryCount = 0;
const maxRetries = 2;

while (retryCount < maxRetries) {
  try {
    const token = await getToken(clientId, clientSecret);

    if (!token) {
      throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่สามารถขอ Token ได้");
    }

    const scope = "getOfficerRetireData";
    const startRecord = requestBody.page !== 1 ? (requestBody.page - 1) * 25 : 0;

    const formData = new FormData();
    formData.append("scope", scope);
    formData.append("startRecord", startRecord.toString());
    formData.append("retireYear", requestBody.retireYear);
    formData.append("citizenID", requestBody.citizenID);
    formData.append("firstNameTH", requestBody.firstNameTH);
    formData.append("lastNameTH", requestBody.lastNameTH);
    formData.append("officerTypeID", requestBody.type === "officer" ? "1" : "2");

    const res = await axios.post(API_URL_BANGKOK + "/getData", formData, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });

    return new HttpSuccess(res.data.data);
  } catch (error: any) {
    if (error.response?.status === 500 && retryCount < maxRetries - 1) {
      TokenCache.delete(`${clientId}:${clientSecret}`);
      retryCount++;
      continue;
    }
    throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้");
  }
}

Recommended Fix:

let retryCount = 0;
const maxRetries = 2;
const baseDelay = 1000; // 1 second

while (retryCount < maxRetries) {
  try {
    const token = await getToken(clientId, clientSecret);

    if (!token) {
      throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่สามารถขอ Token ได้");
    }

    const scope = "getOfficerRetireData";
    const startRecord = requestBody.page !== 1 ? (requestBody.page - 1) * 25 : 0;

    const formData = new FormData();
    formData.append("scope", scope);
    formData.append("startRecord", startRecord.toString());
    formData.append("retireYear", requestBody.retireYear);
    formData.append("citizenID", requestBody.citizenID);
    formData.append("firstNameTH", requestBody.firstNameTH);
    formData.append("lastNameTH", requestBody.lastNameTH);
    formData.append("officerTypeID", requestBody.type === "officer" ? "1" : "2");

    const res = await axios.post(API_URL_BANGKOK + "/getData", formData, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
      timeout: 30000, // 30 second timeout
    });

    return new HttpSuccess(res.data.data);
  } catch (error: any) {
    retryCount++;
    
    // Log error for debugging
    console.error(`Error fetching retirement data (attempt ${retryCount}/${maxRetries}):`, {
      message: error.message,
      status: error.response?.status,
      code: error.code
    });

    // Check if we should retry
    const shouldRetry = 
      (error.response?.status === 500 || 
       error.response?.status === 503 || 
       error.code === 'ECONNRESET' ||
       error.code === 'ETIMEDOUT') && 
      retryCount < maxRetries;

    if (shouldRetry) {
      TokenCache.delete(`${clientId}:${clientSecret}`);
      // Exponential backoff
      const delay = baseDelay * Math.pow(2, retryCount - 1);
      await new Promise(resolve => setTimeout(resolve, delay));
      continue;
    }
    
    // Don't retry on client errors (4xx) or after max retries
    throw new HttpError(
      HttpStatusCode.INTERNAL_SERVER_ERROR, 
      `ไม่สามารถติดต่อ API ได้: ${error.message}`
    );
  }
}

#8 - Missing Error Handling in IssuesController (MEDIUM)

File & Location: IssuesController.ts:54-71 Method: updateIssue

Problem Type: 2. Missing Error Handle

Root Cause:

  • ไม่มี try-catch รองรับ operation ที่อาจ fail
  • ไม่มีการตรวจสอบว่า request body ถูกต้องหรือไม่
  • การใช้ Object.assign โดยไม่ validate อาจทำให้เกิด invalid data

Code ปัจจุบัน (เสี่ยง):

@Put("{id}")
async updateIssue(
  @Path("id") id: string,
  @Body() requestBody: Partial<UpdateIssueRequest>,
  @Request() request: RequestWithUser,
) {
  let issue = await this.issuesRepository.findOneBy({ id });
  if (!issue) {
    this.setStatus(HttpStatusCode.NOT_FOUND);
    return { message: "ไม่พบข้อมูลที่ต้องการแก้ไข" };
  }
  Object.assign(issue, requestBody);
  issue.lastUpdateUserId = request.user.sub;
  issue.lastUpdateFullName = request.user.name;
  issue.lastUpdatedAt = new Date();
  await this.issuesRepository.save(issue);
  return new HttpSuccess(issue);
}

Recommended Fix:

@Put("{id}")
async updateIssue(
  @Path("id") id: string,
  @Body() requestBody: Partial<UpdateIssueRequest>,
  @Request() request: RequestWithUser,
) {
  try {
    let issue = await this.issuesRepository.findOneBy({ id });
    if (!issue) {
      this.setStatus(HttpStatusCode.NOT_FOUND);
      return new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลที่ต้องการแก้ไข");
    }
    
    // Validate request body if needed
    if (requestBody.status !== undefined) {
      // Validate status enum values
    }
    
    Object.assign(issue, requestBody);
    issue.lastUpdateUserId = request.user.sub;
    issue.lastUpdateFullName = request.user.name;
    issue.lastUpdatedAt = new Date();
    
    try {
      await this.issuesRepository.save(issue);
    } catch (saveError: any) {
      console.error("Failed to save issue:", saveError);
      throw new HttpError(
        HttpStatusCode.INTERNAL_SERVER_ERROR,
        "ไม่สามารถบันทึกข้อมูลได้"
      );
    }
    
    return new HttpSuccess(issue);
  } catch (error) {
    if (error instanceof HttpError) {
      throw error;
    }
    console.error("Error updating issue:", error);
    throw new HttpError(
      HttpStatusCode.INTERNAL_SERVER_ERROR,
      "เกิดข้อผิดพลาดในการอัปเดตข้อมูล"
    );
  }
}

#9 - Promise.all Without Error Handling in Province Import (MEDIUM)

File & Location: ImportDataController.ts:2856-2874 Method: Import Province Data

Problem Type: 1. Unhandled Exception / 2. Missing Error Handle

Root Cause:

  • Similar to #6 แต่เกิดใน province import
  • ใช้ Promise.all() โดยไม่มี error handling

Recommended Fix: ใช้การแก้ไขเดียวกันกับ #6 และ #11


#10 - Wrong Error Status Code (BUG)

File & Location: InsigniaController.ts:62-64, InsigniaTypeController.ts:58-60 Methods: CreateInsignia, CreateInsigniaType

Problem Type: 3. Logic Bug

Root Cause:

  • Throw HttpError(HttpStatusCode.NOT_FOUND, ...) สำหรับ duplicate data errors
  • ควรใช้ CONFLICT (409) หรือ BAD_REQUEST (400) แทน

Code ปัจจุบัน (ผิด):

if (rowRepeated) {
  throw new HttpError(HttpStatusCode.NOT_FOUND, "ข้อมูล Row นี้มีอยู่ในระบบแล้ว");
}

Recommended Fix:

if (rowRepeated) {
  throw new HttpError(HttpStatusCode.CONFLICT, "ข้อมูล Row นี้มีอยู่ในระบบแล้ว");
}

#11 - Promise.all Without Error Handling in Amphur Import (LOW)

File & Location: ImportDataController.ts:2889-2908 Method: Import Amphur Data

Problem Type: 1. Unhandled Exception / 2. Missing Error Handle

Root Cause:

  • Similar to #6 แต่เกิดใน amphur import
  • ใช้ Promise.all() โดยไม่มี error handling

Recommended Fix: ใช้การแก้ไขเดียวกันกับ #6


#12 - Redundant Promise.all in LoginController (LOW)

File & Location: LoginController.ts:38-47 Method: login

Problem Type: 3. Code Quality

Root Cause:

  • ใช้ Promise.all กับ array ที่มีแค่ 1 promise
  • การใช้งานไม่มีประโยชน์เพราะไม่ได้ parallelize อะไรเลย
  • Code อ่านยากและสับสน

Code ปัจจุบัน (ผิด):

let _data: any = null;
await Promise.all([
  await new CallAPI()
    .PostDataKeycloak(`/realms/${process.env.KC_REALMS}/protocol/openid-connect/token`, data)
    .then(async (x) => {
      _data = x;
    })
    .catch(async (x) => {
      throw new HttpError(HttpStatus.UNAUTHORIZED, "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง");
    }),
]);

Recommended Fix:

try {
  const _data = await new CallAPI().PostDataKeycloak(
    `/realms/${process.env.KC_REALMS}/protocol/openid-connect/token`, 
    data
  );
  
  if (!_data) {
    throw new HttpError(HttpStatus.UNAUTHORIZED, "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง");
  }
  
  return new HttpSuccess(_data);
} catch (error: any) {
  if (error instanceof HttpError) {
    throw error;
  }
  throw new HttpError(HttpStatus.UNAUTHORIZED, "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง");
}

#13 - Error Message from External API Not Handled (BUG)

File & Location: LoginController.ts:44-46, LoginController.ts:85-87 Methods: login, loginCheckin

Problem Type: 3. Logic Bug

Root Cause:

  • Catch error แล้ว throw HttpError ใหม่ แต่ไม่ได้ preserve error message ต้นทาง
  • ทำให้ user ไม่รู้สาเหตุที่แท้จริงของการ login ล้มเหลว

Code ปัจจุบัน (ผิด):

.catch(async (x) => {
  throw new HttpError(HttpStatus.UNAUTHORIZED, "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง");
}),

Recommended Fix:

.catch(async (error: any) => {
  const errorMessage = error?.response?.data?.error_description || 
                      error?.response?.data?.error ||
                      error?.message ||
                      "ชื่อผู้ใช้งานหรือรหัสผ่านไม่ถูกต้อง";
  
  console.error("Login failed:", error);
  throw new HttpError(HttpStatus.UNAUTHORIZED, errorMessage);
}),

สรุปคำแนะนำการแก้ไขแบบรวม

ระดับความสำคัญ

ต้องแก้โดยเร็ว (P1 - High):

  1. Promise.all operations without error handling - unhandled rejections อาจทำให้ service ไม่เสถียร
  2. Async forEach ที่ไม่รอ completion - อาจทำให้ data ไม่ถูกต้อง

ควรแก้ (P2 - Medium): 3. External API call error handling - ควรมี retry mechanism ที่ดีขึ้น 4. Missing error handling in IssuesController 5. Promise operations in import controllers

แก้เมื่อว่าง (P3 - Low): 6. Redundant Promise.all in LoginController 7. Error status code issues

แนวทางการแก้ไขแบบ Global

  1. สร้าง utility function สำหรับ Promise.all ที่มี error handling:
async function safePromiseAll<T>(
  items: T[],
  executor: (item: T, index: number) => Promise<any>,
  options: { 
    continueOnError?: boolean;
    throwOnError?: boolean;
  } = {}
) {
  const { continueOnError = false, throwOnError = true } = options;
  
  if (continueOnError) {
    const results = await Promise.allSettled(
      items.map((item, index) => executor(item, index))
    );
    
    const failures = results.filter(r => r.status === 'rejected');
    if (failures.length > 0 && throwOnError) {
      console.warn(`${failures.length} operations failed`);
    }
    
    return results;
  } else {
    return Promise.all(
      items.map((item, index) => executor(item, index))
    );
  }
}
  1. ใช้ try-catch รอบทุก database operation และ external API call

  2. Implement logging ที่สมบูรณ์สำหรับ debugging

  3. Use proper HTTP status codes ตามมาตรฐาน REST API


ไฟล์ที่ต้องแก้ไข

  1. src/controllers/EmployeePositionController.ts - Promise.all handling, forEach with async
  2. src/controllers/EmployeeTempPositionController.ts - Promise.all handling
  3. src/controllers/ExRetirementController.ts - External API error handling, token management
  4. src/controllers/ImportDataController.ts - Promise.all in import operations
  5. src/controllers/IssuesController.ts - Error handling
  6. src/controllers/LoginController.ts - Redundant Promise.all, error messages
  7. src/controllers/InsigniaController.ts - Error status codes
  8. src/controllers/InsigniaTypeController.ts - Error status codes

ข้อมูลเพิ่มเติม

  • Controllers ที่ยังไม่ได้ตรวจสอบ: 110 ไฟล์
  • จุดเสี่ยงที่พบซ้ำจากชุดที่ 1-2: Promise.all without error handling, wrong HTTP status codes

รายงานนี้ถูกสร้างโดย AI Code Review System สำหรับ BMA EHR Organization Project