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

37 KiB

รายงานการตรวจสอบ Unhandled Exception และ Crash Loop

Batch 10: Controllers 91-100

วันที่ตรวจสอบ: 2026-05-08
จำนวน Controllers ที่ตรวจสอบ: 10 Controllers


Controllers ที่ตรวจสอบในชุดนี้

  1. KeycloakSyncController.ts
  2. SocketController.ts
  3. ApiWebServiceController.ts
  4. ApiManageController.ts
  5. ImportDataController.ts
  6. ExRetirementController.ts
  7. IssuesController.ts
  8. DevelopmentRequestController.ts
  9. MyController.ts
  10. MainController.ts

รายการปัญหาที่พบ

1. 🔴 CRITICAL - KeycloakSyncController.ts - Unhandled Promise in Loop

File & Location: KeycloakSyncController.ts - syncByProfileIds() method

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

Root Cause:

for (const profileId of profileIds) {
  try {
    const success = await this.keycloakAttributeService.syncOnOrganizationChange(
      profileId,
      profileType,
    );
    // ...
  } catch (error: any) {
    result.failed++;
    result.details.push({ profileId, status: "failed", error: error.message });
  }
}

แม้ว่าจะมี try-catch ภายใน loop แต่การ catch error แล้วเพียงแค่บันทึกผลลัพธ์ อาจไม่เพียงพอสำหรับบางกรณี:

  • หาก syncOnOrganizationChange มี Promise rejection ที่ไม่ถูก handle อย่างถูกต้องภายใน service
  • หากเกิด error ระหว่างการทำงานของ loop ที่ไม่ใช่จาก syncOnOrganizationChange เช่น จาก result.details.push()
  • Error ที่เกิดขึ้นอาจเป็น unhandled rejection หาก service ไม่ return Promise อย่างถูกต้อง

Recommended Fix:

@Post("sync-profiles-batch")
async syncByProfileIds(
  @Body() request: { profileIds: string[]; profileType: "PROFILE" | "PROFILE_EMPLOYEE" },
) {
  const { profileIds, profileType } = request;

  if (!profileIds || profileIds.length === 0) {
    throw new HttpError(HttpStatus.BAD_REQUEST, "profileIds ต้องไม่ว่างเปล่า");
  }

  if (!["PROFILE", "PROFILE_EMPLOYEE"].includes(profileType)) {
    throw new HttpError(
      HttpStatus.BAD_REQUEST,
      "profileType ต้องเป็น PROFILE หรือ PROFILE_EMPLOYEE เท่านั้น",
    );
  }

  const result = {
    total: profileIds.length,
    success: 0,
    failed: 0,
    details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>,
  };

  // เพิ่ม timeout protection และ error handling ที่ดีขึ้น
  const SYNC_TIMEOUT = 30000; // 30 วินาทีต่อ profile

  for (const profileId of profileIds) {
    try {
      // เพิ่ม Promise.race เพื่อป้องกันการ hang
      const syncPromise = this.keycloakAttributeService.syncOnOrganizationChange(
        profileId,
        profileType,
      );
      
      const timeoutPromise = new Promise((_, reject) =>
        setTimeout(() => reject(new Error('Sync timeout')), SYNC_TIMEOUT)
      );

      const success = await Promise.race([syncPromise, timeoutPromise]) as boolean;

      if (success) {
        result.success++;
        result.details.push({ profileId, status: "success" });
      } else {
        result.failed++;
        result.details.push({
          profileId,
          status: "failed",
          error: "Sync returned false - ไม่พบข้อมูล profile หรือ Keycloak user ID",
        });
      }
    } catch (error: any) {
      result.failed++;
      // เพิ่ม validation ก่อน push เพื่อป้องกัน crash จาก invalid data
      const errorMessage = error?.message || String(error);
      result.details.push({ 
        profileId, 
        status: "failed", 
        error: errorMessage.substring(0, 500) // จำกัดความยาว
      });
      
      // Log error สำหรับ monitoring
      console.error(`[KeycloakSync] Failed to sync profile ${profileId}:`, error);
    }
  }

  return new HttpSuccess({
    message: "Batch sync เสร็จสิ้น",
    ...result,
  });
}

2. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Large Loop

File & Location: ImportDataController.ts - UploadFileSqlOfficer() method

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

Root Cause:

@Post("uploadProfile-Officer")
async UploadFileSqlOfficer(@Request() request: { user: Record<string, any> }) {
  const OFFICER = await this.OFFICERRepo.find();
  let rowCount = 0;
  // ... 
  for await (const item of OFFICER) {
    rowCount++;
    // ... การประมวลผลข้อมูล ...
    await this.profileRepo.save(profile);
  }
  return new HttpSuccess();
}

ปัญหาที่พบ:

  1. ไม่มี try-catch รอบ loop - หากเกิด error ระหว่างการประมวลผล เช่น:

    • Database connection lost
    • Invalid data format
    • Constraint violation
    • Memory overflow

    จะทำให้เกิด Unhandled Exception และ Process Crash

  2. ไม่มี Error Recovery - หากเกิด error ที่ record ใด record หนึ่ง ทั้งกระบวนการจะหยุดทันที และไม่มีการ rollback หรือ cleanup

  3. Loading all data at once - await this.OFFICERRepo.find() โหลดข้อมูลทั้งหมดเข้า memory อาจทำให้เกิด Out of Memory

  4. No transaction management - แต่ละรอบบันทึกแยกกัน หากเกิด error ข้อมูลบางส่วนอาจถูกบันทึกแล้วบางส่วนไม่ได้

Recommended Fix:

@Post("uploadProfile-Officer")
async UploadFileSqlOfficer(@Request() request: { user: Record<string, any> }) {
  const queryRunner = AppDataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();
  
  let rowCount = 0;
  let successCount = 0;
  let failedCount = 0;
  const errors: Array<{row: number, citizenId: string, error: string}> = [];
  
  try {
    // ใช้ pagination แทนการโหลดทั้งหมด
    const BATCH_SIZE = 500;
    let offset = 0;
    let hasMore = true;
    
    while (hasMore) {
      const OFFICER = await queryRunner.manager.find(OFFICER, {
        take: BATCH_SIZE,
        skip: offset,
        order: { id: 'ASC' }
      });
      
      if (OFFICER.length === 0) {
        hasMore = false;
        break;
      }
      
      for (const item of OFFICER) {
        rowCount++;
        
        try {
          let type_: any = null;
          let level_: any = null;
          const profile = new Profile();

          const existingProfile = await queryRunner.manager.findOne(Profile, {
            where: { citizenId: item.CIT.toString() },
          });

          if (existingProfile) {
            // ข้ามกรณีมีข้อมูลอยู่แล้ว
            continue;
          }

          // ... การประมวลผลข้อมูลเดิม ...
          
          // ใช้ queryRunner.manager.save แทน this.profileRepo.save
          await queryRunner.manager.save(profile);
          successCount++;
          
        } catch (itemError: any) {
          failedCount++;
          errors.push({
            row: rowCount,
            citizenId: item.CIT?.toString() || 'unknown',
            error: itemError?.message || String(itemError)
          });
          // Log แต่ไม่หยุดการทำงาน
          console.error(`[UploadOfficer] Error at row ${rowCount}:`, itemError);
        }
      }
      
      offset += BATCH_SIZE;
      
      // Commit ทุกๆ batch เพื่อป้องกัน transaction ใหญ่เกินไป
      await queryRunner.commitTransaction();
      await queryRunner.startTransaction();
    }
    
    // Commit transaction สุดท้าย
    await queryRunner.commitTransaction();
    
    return new HttpSuccess({
      message: "อัปโหลดข้อมูลเสร็จสิ้น",
      total: rowCount,
      success: successCount,
      failed: failedCount,
      errors: errors.slice(0, 100) // ส่งเฉพาะ 100 errors แรก
    });
    
  } catch (error: any) {
    await queryRunner.rollbackTransaction();
    throw new HttpError(
      HttpStatusCode.INTERNAL_SERVER_ERROR,
      `ไม่สามารถอัปโหลดข้อมูลได้: ${error?.message || 'Unknown error'}`
    );
  } finally {
    await queryRunner.release();
  }
}

3. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Employee Upload

File & Location: ImportDataController.ts - UploadFileSQL() method

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

Root Cause: เหมือนกับปัญหาข้างต้น แต่สำหรับการอัปโหลดข้อมูลลูกจ้างประจำ มีความเสี่ยงเช่นเดียวกัน:

  • ไม่มี try-catch ใน loop
  • ไม่มี transaction management
  • ไม่มี error recovery

Recommended Fix: ใช้ pattern เดียวกับข้อ 2 โดยใช้ QueryRunner สำหรับ transaction management


4. 🔴 CRITICAL - ImportDataController.ts - Unhandled Exception in Temp Employee Upload

File & Location: ImportDataController.ts - UploadFileSQLTemp() method

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

Root Cause:

if (item.CIT.toString() == "1101801164891") {
  continue;
}
const existingProfile = await this.profileEmpRepo.findOne({
  where: { employeeClass: "TEMP", citizenId: item.CIT.toString() },
});
if (existingProfile) {
  profile.id = existingProfile.id;
} else {
  continue;
}

ปัญหาเพิ่มเติม:

  1. Hardcoded citizenId check - มีการ hardcode เงื่อนไข item.CIT.toString() == "1101801164891" ซึ่งอาจเป็น bug หรือ test code ที่ลืมลบ
  2. การ skip ที่ไม่ชัดเจน - หากไม่พบ existingProfile จะ continue ทันที ทำให้ไม่สร้าง profile ใหม่
  3. ไม่มี error handling เหมือนปัญหาก่อนหน้า

Recommended Fix:

@Post("uploadProfile-EmployeeTemp")
async UploadFileSQLTemp(@Request() request: { user: Record<string, any> }) {
  const queryRunner = AppDataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();
  
  let rowCount = 0;
  let successCount = 0;
  let failedCount = 0;
  const errors: Array<{row: number, citizenId: string, error: string}> = [];
  
  try {
    const BATCH_SIZE = 500;
    let offset = 0;
    let hasMore = true;
    
    while (hasMore) {
      const EMPLOYEE = await queryRunner.manager.find(EMPLOYEETEMP, {
        take: BATCH_SIZE,
        skip: offset,
        order: { id: 'ASC' }
      });
      
      if (EMPLOYEE.length === 0) {
        hasMore = false;
        break;
      }
      
      for (const item of EMPLOYEE) {
        rowCount++;
        
        try {
          // เอา hardcode check ออก หรือเปลี่ยนเป็น configurable
          // if (item.CIT.toString() === "1101801164891") {
          //   continue;
          // }
          
          const existingProfile = await queryRunner.manager.findOne(ProfileEmployee, {
            where: { 
              employeeClass: "TEMP", 
              citizenId: item.CIT.toString() 
            },
          });

          let profile: ProfileEmployee;
          
          if (existingProfile) {
            profile = existingProfile;
          } else {
            // สร้าง profile ใหม่ถ้าไม่พบ
            profile = new ProfileEmployee();
            profile.employeeClass = "TEMP";
          }

          // ... การประมวลผลข้อมูลเดิม ...
          
          await queryRunner.manager.save(profile);
          successCount++;
          
        } catch (itemError: any) {
          failedCount++;
          errors.push({
            row: rowCount,
            citizenId: item.CIT?.toString() || 'unknown',
            error: itemError?.message || String(itemError)
          });
          console.error(`[UploadEmployeeTemp] Error at row ${rowCount}:`, itemError);
        }
      }
      
      offset += BATCH_SIZE;
      await queryRunner.commitTransaction();
      await queryRunner.startTransaction();
    }
    
    await queryRunner.commitTransaction();
    
    return new HttpSuccess({
      message: "อัปโหลดข้อมูลลูกจ้างชั่วคราวเสร็จสิ้น",
      total: rowCount,
      success: successCount,
      failed: failedCount,
      errors: errors.slice(0, 100)
    });
    
  } catch (error: any) {
    await queryRunner.rollbackTransaction();
    throw new HttpError(
      HttpStatusCode.INTERNAL_SERVER_ERROR,
      `ไม่สามารถอัปโหลดข้อมูลลูกจ้างชั่วคราวได้: ${error?.message || 'Unknown error'}`
    );
  } finally {
    await queryRunner.release();
  }
}

5. 🟡 HIGH - ExRetirementController.ts - Unhandled External API Error

File & Location: ExRetirementController.ts - getToken() function

Problem Type: 2. Missing Error Handle

Root Cause:

async function getToken(ClientID: string, ClientSecret: string): Promise<string> {
  // ...
  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 });
  }
}

ปัญหา:

  1. Generic error handling - Error ที่ return มาเป็น object ธรรมดา ไม่ใช่ Error instance ทำให้การ stack trace หายไป
  2. ไม่มี retry logic - หาก external API ล้ม ชั่วคราว จะไม่มีการ retry อัตโนมัติ
  3. No timeout - หาก external API ไม่ตอบสนอง จะทำให้ request ค้างไปตลอด

Recommended Fix:

async function getToken(ClientID: string, ClientSecret: string): Promise<string> {
  const cacheKey = `${ClientID}:${ClientSecret}`;
  
  const cachedToken = TokenCache.get(cacheKey);
  if (cachedToken) {
    return cachedToken;
  }

  const MAX_RETRIES = 3;
  const TIMEOUT = 10000; // 10 วินาที
  let lastError: any;

  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
    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: TIMEOUT,
      });
      
      const token = res.data.token;
      if (!token) {
        throw new Error('Token not found in response');
      }
      
      TokenCache.set(cacheKey, token);
      return token;
      
    } catch (error: any) {
      lastError = error;
      
      // ไม่ retry หากเป็น client error (4xx)
      if (error.response?.status >= 400 && error.response?.status < 500) {
        break;
      }
      
      // Retry หากเป็น server error หรือ network error
      if (attempt < MAX_RETRIES) {
        const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  }

  // Log error สำหรับ monitoring
  console.error(`[ExRetirement] Failed to get token after ${MAX_RETRIES} attempts:`, lastError);
  
  throw new Error(`ไม่สามารถขอ Token ได้: ${lastError?.message || 'Unknown error'}`);
}

6. 🟡 HIGH - ApiWebServiceController.ts - Potential Null Reference

File & Location: ApiWebServiceController.ts - listAttribute() method

Problem Type: 2. Missing Error Handle

Root Cause:

if (system == "organization") {
  tbMain = "OrgRoot";
  const revision = await this.orgRevisionRepository.findOne({
    select: ["id"],
    where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
  });
  condition = `OrgRoot.orgRevisionId = "${revision?.id}"`;
}

ปัญหา:

  1. revision อาจเป็น null - หากไม่พบ revision ที่ตรงตามเงื่อนไข revision?.id จะเป็น undefined
  2. SQL Injection vulnerability - การใส่ค่าโดยตรงเข้าไปใน condition string อาจทำให้เกิด SQL injection หรือ syntax error
  3. ไม่มี error handling - หาก query ล้มเพราะ invalid condition จะทำให้เกิด unhandled exception

Recommended Fix:

@Get("/:system/:code")
async listAttribute(
  @Request() request: RequestWithUserWebService,
  @Path("system")
  system: SystemCode,
  @Path("code") code: string,
  @Query("page") page: number = 1,
  @Query("pageSize") pageSize: number = 100,
): Promise<HttpSuccess | HttpError> {
  try {
    const apiName = await this.apiNameRepository.findOne({
      where: { code },
      select: ["id", "code", "methodApi", "system", "isActive"],
      relations: ["apiAttributes"],
      order: {
        apiAttributes: {
          ordering: "ASC",
        },
      },
    });

    if (!apiName || apiName.system != system || !apiName.isActive || apiName.methodApi != "GET") {
      throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบ API ที่ระบุ");
    }
    
    await isPermissionRequest(request, apiName.id);
    
    const offset = (page - 1) * pageSize;
    const propertyKey = apiName.apiAttributes.map((attr) => `${attr.tbName}.${attr.propertyKey}`);

    let tbMain: string = "";
    let condition: string = "1=1";
    let revisionId: string | null = null;
    
    if (system == "registry") {
      tbMain = "Profile";
    } else if (system == "registry_emp") {
      tbMain = "ProfileEmployee";
      condition = `ProfileEmployee.employeeClass = "PERM"`;
    } else if (system == "registry_temp") {
      tbMain = "ProfileEmployee";
      condition = `ProfileEmployee.employeeClass = "TEMP"`;
    } else if (system == "organization") {
      tbMain = "OrgRoot";
      const revision = await this.orgRevisionRepository.findOne({
        select: ["id"],
        where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
      });
      
      if (!revision) {
        throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่พบข้อมูล revision ปัจจุบัน");
      }
      
      revisionId = revision.id;
      condition = `OrgRoot.orgRevisionId = :revisionId`;
    } else if (system == "position") {
      tbMain = "PosMaster";
      const revision = await this.orgRevisionRepository.findOne({
        select: ["id"],
        where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
      });
      
      if (!revision) {
        throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่พบข้อมูล revision ปัจจุบัน");
      }
      
      revisionId = revision.id;
      condition = `PosMaster.orgRevisionId = :revisionId`;
    }

    const repo = AppDataSource.getRepository(tbMain);
    const metadata = repo.metadata;

    const relationMap: Record<string, string> = {};
    metadata.relations.forEach((rel) => {
      relationMap[rel.inverseEntityMetadata.name] = rel.propertyName;
    });

    let propertyOtherKey: any[] = [];
    propertyOtherKey = [
      ...new Set(propertyKey.map((x) => x.split(".")[0]).filter((tb) => tb !== tbMain)),
    ];

    const queryBuilder = repo.createQueryBuilder(tbMain);

    if (propertyOtherKey.length > 0) {
      propertyOtherKey.forEach((tb) => {
        const relationName = relationMap[tb];
        if (relationName) {
          queryBuilder.leftJoin(
            `${tbMain}.${relationName === "next_holder" ? "current_holder" : relationName}`,
            tb,
          );
        }
      });
    }

    let pk: string = "";
    const primaryColumns = metadata.primaryColumns;
    primaryColumns.forEach((col) => {
      pk = col.propertyName;
      if (!propertyKey.includes(`${tbMain}.${pk}`)) {
        propertyKey.push(`${tbMain}.${pk}`);
      }
    });

    // ใช้ parameterized query แทน string interpolation
    const queryParams: any = {};
    if (revisionId) {
      queryParams.revisionId = revisionId;
    }

    const [items, total] = await queryBuilder
      .select(propertyKey)
      .where(condition, queryParams)
      .orderBy(propertyKey[0], "ASC")
      .skip(offset)
      .take(pageSize)
      .getManyAndCount();

    const data = items.map((item) => {
      const { [pk]: removedPk, ...x } = item;
      return x;
    });

    // save api history after query success
    const history = {
      headerApi: JSON.stringify({
        host: request.headers.host,
        "x-api-key": request.headers["x-api-key"],
        connection: request.headers.connection,
        accept: request.headers.accept,
      }),
      tokenApi: Array.isArray(request.headers["x-api-key"])
        ? request.headers["x-api-key"][0] || ""
        : request.headers["x-api-key"] || "",
      requestApi: `${request.method} ${request.protocol}://${request.headers.host}${request.originalUrl || request.url}`,
      responseApi: "OK",
      ipApi: request.ip,
      codeApi: code,
      apiKeyId: request.user.id,
      apiNameId: apiName.id,
      createdFullName: request.user.name,
      lastUpdateFullName: request.user.name,
    };
    
    try {
      await this.apiHistoryRepository.save(history);
    } catch (historyError) {
      // Log แต่ไม่ให้กระทบต่อ response
      console.error('[ApiWebService] Failed to save history:', historyError);
    }

    return new HttpSuccess({ data: data, total });
    
  } catch (error: any) {
    if (error instanceof HttpError) {
      throw error;
    }
    throw new HttpError(
      HttpStatusCode.INTERNAL_SERVER_ERROR,
      `เกิดข้อผิดพลาด: ${error?.message || 'Unknown error'}`
    );
  }
}

7. 🟡 MEDIUM - ApiManageController.ts - Missing Transaction Error Handling

File & Location: ApiManageController.ts - createApi() method

Problem Type: 2. Missing Error Handle

Root Cause:

@Post("")
async createApi(
  @Request() req: RequestWithUser,
  @Body() apiData: CreateApi,
): Promise<HttpSuccess | HttpError> {
  const queryRunner = AppDataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();

  try {
    this.validateSuperAdminRole(req.user);
    // ...
    await queryRunner.commitTransaction();
    return new HttpSuccess(apiName.id);
  } catch (error) {
    await queryRunner.rollbackTransaction();
    throw new HttpError(...);
  } finally {
    await queryRunner.release();
  }
}

ปัญหา:

  1. validateSuperAdminRole อยู่นอก try-catch - หาก function นี้ throw error จะทำให้ queryRunner ไม่ถูก release และเกิด connection leak
  2. ไม่ validate req.user ก่อนเรียก validateSuperAdminRole - หาก req.user เป็น null หรือ undefined จะเกิด error

Recommended Fix:

@Post("")
async createApi(
  @Request() req: RequestWithUser,
  @Body() apiData: CreateApi,
): Promise<HttpSuccess | HttpError> {
  const queryRunner = AppDataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();

  try {
    // Validate request ก่อน
    if (!req.user) {
      throw new HttpError(HttpStatusCode.UNAUTHORIZED, "ไม่พบข้อมูลผู้ใช้");
    }
    
    this.validateSuperAdminRole(req.user);

    const code = this.generateApiCode();
    const postData = {
      name: apiData.name,
      code,
      pathApi: this.createApiPath(apiData.system as SystemCode, code),
      methodApi: apiData.methodApi || "GET",
      system: apiData.system || "registry",
      isActive: apiData.isActive || false,
      createdUserId: req.user?.sub,
      createdFullName: req.user?.name || "",
    };

    const apiName = await queryRunner.manager.getRepository(ApiName).save(postData);

    if (apiData.apiAttributes?.length) {
      let orderingCounter = 0;
      const attributesToSave = apiData.apiAttributes.flatMap((attr) =>
        attr.propertyKey.map((propertyKey) => ({
          apiNameId: apiName.id,
          tbName: attr.tbName,
          propertyKey,
          ordering: orderingCounter++,
          createdUserId: req.user?.sub,
          createdFullName: req.user?.name || "",
        })),
      );

      await queryRunner.manager.getRepository(ApiAttribute).save(attributesToSave);
    }

    await queryRunner.commitTransaction();
    return new HttpSuccess(apiName.id);
  } catch (error) {
    await queryRunner.rollbackTransaction();
    
    if (error instanceof HttpError) {
      throw error;
    }
    
    throw new HttpError(
      HttpStatusCode.INTERNAL_SERVER_ERROR,
      error instanceof Error ? error.message : "เกิดข้อผิดพลาด ไม่สามารถบันทึกข้อมูลได้ กรุณาลองใหม่ในภายหลัง",
    );
  } finally {
    // Ensure release is called even if rollback fails
    try {
      await queryRunner.release();
    } catch (releaseError) {
      console.error('[ApiManage] Failed to release queryRunner:', releaseError);
    }
  }
}

8. 🟡 MEDIUM - DevelopmentRequestController.ts - Unhandled Promise in Parallel Operations

File & Location: DevelopmentRequestController.ts - newDevelopmentRequest() method

Problem Type: 2. Missing Error Handle

Root Cause:

if (body.developmentProjects != null) {
  await Promise.all(
    body.developmentProjects.map(async (x) => {
      let developmentProject = new DevelopmentProject();
      developmentProject.name = x;
      developmentProject.createdUserId = req.user.sub;
      developmentProject.createdFullName = req.user.name;
      developmentProject.lastUpdateUserId = req.user.sub;
      developmentProject.lastUpdateFullName = req.user.name;
      developmentProject.createdAt = new Date();
      developmentProject.lastUpdateUpdatedAt = new Date();
      developmentProject.developmentRequestId = data.id;
      await this.developmentProjectRepository.save(developmentProject, { data: req });
      setLogDataDiff(req, { before, after: developmentProject });
    }),
  );
}

ปัญหา:

  1. Unhandled Promise rejection - หาก save หรือ setLogDataDiff ล้ม จะเกิด unhandled rejection
  2. ไม่มี error handling รายตัว - หาก project หนึ่งล้ม ทั้ง batch จะล้ม
  3. No cleanup on partial failure - หาก save บางส่วนสำเร็จแล้วล้ม จะมีข้อมูล partial อยู่ใน database

Recommended Fix:

if (body.developmentProjects != null) {
  const savedProjects: DevelopmentProject[] = [];
  
  try {
    for (const projectName of body.developmentProjects) {
      try {
        let developmentProject = new DevelopmentProject();
        developmentProject.name = projectName;
        developmentProject.createdUserId = req.user.sub;
        developmentProject.createdFullName = req.user.name;
        developmentProject.lastUpdateUserId = req.user.sub;
        developmentProject.lastUpdateFullName = req.user.name;
        developmentProject.createdAt = new Date();
        developmentProject.lastUpdateUpdatedAt = new Date();
        developmentProject.developmentRequestId = data.id;
        
        const saved = await this.developmentProjectRepository.save(developmentProject, { data: req });
        savedProjects.push(saved);
        
        setLogDataDiff(req, { before: null, after: saved });
      } catch (projectError: any) {
        console.error(`[DevelopmentRequest] Failed to save project ${projectName}:`, projectError);
        // Continue with next project instead of failing entire request
      }
    }
  } catch (batchError: any) {
    console.error('[DevelopmentRequest] Error in projects batch:', batchError);
    // Clean up any successfully saved projects if needed
    if (savedProjects.length > 0) {
      try {
        await this.developmentProjectRepository.delete({ 
          developmentRequestId: data.id 
        });
      } catch (cleanupError) {
        console.error('[DevelopmentRequest] Failed to cleanup projects:', cleanupError);
      }
    }
    throw batchError;
  }
}

9. 🟢 LOW - SocketController.ts - No Error Handling

File & Location: SocketController.ts - notify() method

Problem Type: 2. Missing Error Handle

Root Cause:

@Post("notify")
async notify(
  @Body()
  payload: {
    message: string;
    userId?: string | string[];
    roles?: string | string[];
    error?: boolean;
  },
) {
  sendWebSocket(
    "socket-notification",
    { success: !payload.error, message: payload.message },
    {
      roles: payload.roles || [],
      userId: payload.userId || [],
    },
  );
}

ปัญหา:

  1. ไม่มี try-catch - หาก sendWebSocket throw error จะเป็น unhandled exception
  2. ไม่ return ค่า - ไม่มีการ return HttpSuccess หรือ error response
  3. No validation - ไม่ validate payload ก่อนใช้งาน

Recommended Fix:

@Post("notify")
async notify(
  @Body()
  payload: {
    message: string;
    userId?: string | string[];
    roles?: string | string[];
    error?: boolean;
  },
) {
  try {
    // Validate payload
    if (!payload.message || typeof payload.message !== 'string') {
      throw new HttpError(HttpStatus.BAD_REQUEST, "message ต้องเป็น string ที่ไม่ว่างเปล่า");
    }
    
    // Validate userId and roles
    if (payload.userId && !Array.isArray(payload.userId) && typeof payload.userId !== 'string') {
      throw new HttpError(HttpStatus.BAD_REQUEST, "userId ต้องเป็น string หรือ array of strings");
    }
    
    if (payload.roles && !Array.isArray(payload.roles) && typeof payload.roles !== 'string') {
      throw new HttpError(HttpStatus.BAD_REQUEST, "roles ต้องเป็น string หรือ array of strings");
    }

    sendWebSocket(
      "socket-notification",
      { success: !payload.error, message: payload.message },
      {
        roles: payload.roles || [],
        userId: payload.userId || [],
      },
    );
    
    return new HttpSuccess({ 
      message: "ส่งการแจ้งเตือนสำเร็จ",
      notification: {
        type: "socket-notification",
        success: !payload.error,
        message: payload.message,
        roles: payload.roles || [],
        userId: payload.userId || [],
      }
    });
  } catch (error: any) {
    if (error instanceof HttpError) {
      throw error;
    }
    throw new HttpError(
      HttpStatus.INTERNAL_SERVER_ERROR,
      `ไม่สามารถส่งการแจ้งเตือนได้: ${error?.message || 'Unknown error'}`
    );
  }
}

10. 🟢 LOW - IssuesController.ts - Missing Try-Catch

File & Location: IssuesController.ts - getIssues() method

Problem Type: 2. Missing Error Handle

Root Cause:

@Get("lists")
async getIssues() {
  const issues = await this.issuesRepository.find({
    order: {
      createdAt: "DESC",
    },
  });
  return new HttpSuccess(issues);
}

ปัญหา:

  • ไม่มี try-catch หาก database connection ล้มหรือ query มีปัญหา จะเกิด unhandled exception

Recommended Fix:

@Get("lists")
async getIssues() {
  try {
    const issues = await this.issuesRepository.find({
      order: {
        createdAt: "DESC",
      },
    });
    return new HttpSuccess(issues);
  } catch (error: any) {
    throw new HttpError(
      HttpStatusCode.INTERNAL_SERVER_ERROR,
      `ไม่สามารถดึงรายการปัญหาได้: ${error?.message || 'Unknown error'}`
    );
  }
}

สรุปสถิติ

ระดับความรุนแรง จำนวน รายการ
🔴 CRITICAL 4 1, 2, 3, 4
🟡 HIGH 2 5, 6
🟡 MEDIUM 2 7, 8
🟢 LOW 2 9, 10

คำแนะนำการจัดลำดับการแก้ไข

แก้ไขทันที (P0 - Critical)

  1. ImportDataController.ts - ทั้ง 3 methods (UploadFileSqlOfficer, UploadFileSQL, UploadFileSQLTemp)
    • เพิ่ม transaction management
    • เพิ่ม try-catch ใน loops
    • เพิ่ม pagination แทนการโหลดทั้งหมด

แก้ไขเร็วๆ นี้ (P1 - High)

  1. KeycloakSyncController.ts - เพิ่ม timeout protection
  2. ExRetirementController.ts - ปรับปรุง error handling และ retry logic
  3. ApiWebServiceController.ts - แก้ null reference issue

แก้ไขในภายหลัง (P2 - Medium)

  1. ApiManageController.ts - ปรับปรุง transaction error handling
  2. DevelopmentRequestController.ts - เพิ่ม error handling สำหรับ parallel operations

แก้ไขเมื่อว่าง (P3 - Low)

  1. SocketController.ts - เพิ่ม validation และ error handling
  2. IssuesController.ts - เพิ่ม try-catch

ข้อเสนอแนะเพิ่มเติม

  1. ใช้ Global Error Handler - ให้พิจารณาใช้ TSOA's middleware หรือ NestJS interceptor สำหรับ centralized error handling
  2. เพิ่ม Health Check - สำหรับ endpoints ที่เชื่อมต่อกับ external services (Keycloak, ExProfile API)
  3. Circuit Breaker Pattern - สำหรับการเรียก external API เพื่อป้องกัน cascade failures
  4. Graceful Shutdown - ให้แน่ใจว่า long-running operations สามารถยกเลิกได้อย่างปลอดภัยเมื่อ server shutdown
  5. Logging Strategy - เพิ่ม structured logging สำหรับ monitoring และ debugging

ไฟล์รายงานที่เกี่ยวข้อง

  • Batch 1-10 Analysis - รายงานการตรวจสอบ Controllers ชุดก่อนหน้า
  • Security Audit Report - รายงานการตรวจสอบด้านความปลอดภัย